diff --git a/.github/workflows/moodle-ci.yml b/.github/workflows/moodle-ci.yml index 44d0182..bc29064 100644 --- a/.github/workflows/moodle-ci.yml +++ b/.github/workflows/moodle-ci.yml @@ -31,14 +31,14 @@ jobs: fail-fast: false matrix: include: - - php: '8.0' - moodle-branch: 'MOODLE_403_STABLE' - database: 'pgsql' - php: '8.1' moodle-branch: 'MOODLE_404_STABLE' database: 'pgsql' - php: '8.1' - moodle-branch: 'MOODLE_404_STABLE' + moodle-branch: 'MOODLE_405_STABLE' + database: 'pgsql' + - php: '8.1' + moodle-branch: 'MOODLE_405_STABLE' database: 'mariadb' steps: diff --git a/TODO b/TODO deleted file mode 100644 index d6e01a6..0000000 --- a/TODO +++ /dev/null @@ -1,26 +0,0 @@ - -These notes concern Bug #167. -The file should be removed before merging into master. - -I. Slow grading. - -Hopefully this is improved by the new index timereviewed - -II. Slow matchmaking - -Dubious code. -A. file classes/matchmaking/n_closest/n_closest_selector.php, line 88-90. -The SQL query orders by the absolute value of a difference comparing the rating with a certain target value. A suitable index may or may not help here. -Three columns are involved, and you probably want to order before the the inequality constraints. I am not sure if postgres would do that efficiently. Two possible alternatives -1 remove the blacklisted question at the client. -2 create new tables where the blacklist can be filtered out in one constraint. This may or may not be feasible. - -B. inactive_attempts() in classes/capquiz_question_attempt.php -This is the function to make the blacklist, to avoid frequent repeat questions. -This appears to retrieve every attempt from the DB, with no obvious reason. The SQL query should be critically reviewed. - -The blacklist handling should probably be simplified, one way or another. Suggestions: -a. It may be better to store the blacklist in a field for each capquiz user, rather than recreating it from the attempts. -b. It may be better to filter out the forbidden questions in the client, rather than having a long list of inequality constraints in an SQL call. - - diff --git a/action.php b/action.php deleted file mode 100755 index 191e8eb..0000000 --- a/action.php +++ /dev/null @@ -1,46 +0,0 @@ -. - -/** - * Performs actions on the capquiz - * - * @package mod_capquiz - * @author Aleksander Skrede - * @copyright 2018 NTNU - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - -namespace mod_capquiz; - -require_once("../../config.php"); -require_once($CFG->libdir . '/formslib.php'); -require_once($CFG->dirroot . '/mod/capquiz/lib.php'); -require_once($CFG->dirroot . '/mod/capquiz/classes/capquiz_action_performer.php'); - -$cmid = capquiz_urls::require_course_module_id_param(); -$cm = get_coursemodule_from_id('capquiz', $cmid, 0, false, MUST_EXIST); -require_login($cm->course, false, $cm); - -$context = \context_module::instance($cmid); -require_capability('mod/capquiz:instructor', $context); - -$action = required_param('action', PARAM_TEXT); - -$capquiz = new capquiz($cmid); -capquiz_urls::set_page_url($capquiz, capquiz_urls::$urlasync); -capquiz_action_performer::perform($action, $capquiz); - -capquiz_urls::redirect_to_dashboard(); diff --git a/adminlib.php b/adminlib.php index 5ce721b..2d5d5d6 100644 --- a/adminlib.php +++ b/adminlib.php @@ -80,7 +80,6 @@ public function search($query): array { } } - /** * Class that handles the display and configuration of the list of capquiz plugins. * diff --git a/adminmanageplugins.php b/adminmanageplugins.php index ca758fd..e3a6802 100644 --- a/adminmanageplugins.php +++ b/adminmanageplugins.php @@ -25,6 +25,8 @@ require_once("../../config.php"); +global $CFG, $PAGE; + require_login(); require_once($CFG->dirroot . '/mod/capquiz/adminlib.php'); diff --git a/amd/build/attempt.min.js b/amd/build/attempt.min.js deleted file mode 100644 index ad1802c..0000000 --- a/amd/build/attempt.min.js +++ /dev/null @@ -1,9 +0,0 @@ -/** - * @module mod_capquiz - * @author Sebastian S. Gundersen - * @copyright 2019 NTNU - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -define("mod_capquiz/attempt",["jquery","core/str"],(function($,mString){function showTooltip($element,text){var $tooltip=$(".capquiz-star-tooltip");$tooltip.html(text),$tooltip.css("display","block");var x=$element.offset().left-$tooltip.width()/2,y=$element.offset().top+32;$tooltip.css("left",x+"px"),$tooltip.css("top",y+"px")}function enableTooltips(){$(document).on("mouseover",".capquiz-quiz-stars span",(function(){var $self=$(this);$self.hasClass("capquiz-star")?$.when(mString.get_string("tooltip_achieved_star","capquiz")).done((function(text){showTooltip($self,text)})):$self.hasClass("capquiz-lost-star")?$.when(mString.get_string("tooltip_lost_star","capquiz")).done((function(text){showTooltip($self,text)})):$self.hasClass("capquiz-no-star")?$.when(mString.get_string("tooltip_no_star","capquiz")).done((function(text){showTooltip($self,text)})):$self.hasClass("capquiz-help-stars")&&$.when(mString.get_string("tooltip_help_star","capquiz")).done((function(text){showTooltip($self,text)}))})),$(document).on("mouseleave",".capquiz-quiz-stars span",(function(){$(".capquiz-star-tooltip").css("display","none")}))}return{initialize:function(){enableTooltips()}}})); - -//# sourceMappingURL=attempt.min.js.map \ No newline at end of file diff --git a/amd/build/attempt.min.js.map b/amd/build/attempt.min.js.map deleted file mode 100644 index d7aac5b..0000000 --- a/amd/build/attempt.min.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"attempt.min.js","sources":["../src/attempt.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 mod_capquiz\n * @author Sebastian S. Gundersen \n * @copyright 2019 NTNU\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\ndefine(['jquery', 'core/str'], function($, mString) {\n\n /**\n * Show star tooltip.\n * @param {Object} $element\n * @param {string} text\n */\n function showTooltip($element, text) {\n var $tooltip = $('.capquiz-star-tooltip');\n $tooltip.html(text);\n $tooltip.css('display', 'block');\n var x = $element.offset().left - $tooltip.width() / 2;\n var y = $element.offset().top + 32;\n $tooltip.css('left', x + 'px');\n $tooltip.css('top', y + 'px');\n }\n\n /**\n * Hide star tooltip.\n */\n function hideTooltip() {\n $('.capquiz-star-tooltip').css('display', 'none');\n }\n\n /**\n * Register event listeners for showing tooltips on the stars.\n */\n function enableTooltips() {\n $(document).on('mouseover', '.capquiz-quiz-stars span', function() {\n var $self = $(this);\n if ($self.hasClass('capquiz-star')) {\n $.when(mString.get_string('tooltip_achieved_star', 'capquiz')).done(function(text) {\n showTooltip($self, text);\n });\n } else if ($self.hasClass('capquiz-lost-star')) {\n $.when(mString.get_string('tooltip_lost_star', 'capquiz')).done(function(text) {\n showTooltip($self, text);\n });\n } else if ($self.hasClass('capquiz-no-star')) {\n $.when(mString.get_string('tooltip_no_star', 'capquiz')).done(function(text) {\n showTooltip($self, text);\n });\n } else if ($self.hasClass('capquiz-help-stars')) {\n $.when(mString.get_string('tooltip_help_star', 'capquiz')).done(function(text) {\n showTooltip($self, text);\n });\n }\n });\n $(document).on('mouseleave', '.capquiz-quiz-stars span', function() {\n hideTooltip();\n });\n }\n\n return {\n initialize: function() {\n enableTooltips();\n }\n };\n\n});\n"],"names":["define","$","mString","showTooltip","$element","text","$tooltip","html","css","x","offset","left","width","y","top","enableTooltips","document","on","$self","this","hasClass","when","get_string","done","initialize"],"mappings":";;;;;;AAsBAA,6BAAO,CAAC,SAAU,aAAa,SAASC,EAAGC,kBAO9BC,YAAYC,SAAUC,UACvBC,SAAWL,EAAE,yBACjBK,SAASC,KAAKF,MACdC,SAASE,IAAI,UAAW,aACpBC,EAAIL,SAASM,SAASC,KAAOL,SAASM,QAAU,EAChDC,EAAIT,SAASM,SAASI,IAAM,GAChCR,SAASE,IAAI,OAAQC,EAAI,MACzBH,SAASE,IAAI,MAAOK,EAAI,eAanBE,iBACLd,EAAEe,UAAUC,GAAG,YAAa,4BAA4B,eAChDC,MAAQjB,EAAEkB,MACVD,MAAME,SAAS,gBACfnB,EAAEoB,KAAKnB,QAAQoB,WAAW,wBAAyB,YAAYC,MAAK,SAASlB,MACzEF,YAAYe,MAAOb,SAEhBa,MAAME,SAAS,qBACtBnB,EAAEoB,KAAKnB,QAAQoB,WAAW,oBAAqB,YAAYC,MAAK,SAASlB,MACrEF,YAAYe,MAAOb,SAEhBa,MAAME,SAAS,mBACtBnB,EAAEoB,KAAKnB,QAAQoB,WAAW,kBAAmB,YAAYC,MAAK,SAASlB,MACnEF,YAAYe,MAAOb,SAEhBa,MAAME,SAAS,uBACtBnB,EAAEoB,KAAKnB,QAAQoB,WAAW,oBAAqB,YAAYC,MAAK,SAASlB,MACrEF,YAAYe,MAAOb,YAI/BJ,EAAEe,UAAUC,GAAG,aAAc,4BAA4B,WA3BzDhB,EAAE,yBAAyBO,IAAI,UAAW,iBAgCvC,CACHgB,WAAY,WACRT"} \ No newline at end of file diff --git a/amd/build/edit_questions.min.js b/amd/build/edit_questions.min.js deleted file mode 100755 index e709700..0000000 --- a/amd/build/edit_questions.min.js +++ /dev/null @@ -1,9 +0,0 @@ -/** - * @module mod_capquiz - * @author Sebastian S. Gundersen - * @copyright 2019 NTNU - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -define("mod_capquiz/edit_questions",["jquery"],(function($){var parameters={courseModuleId:0};function sendAction(data,onSuccess,onError){$.ajax({type:"post",url:"action.php",data:data,success:onSuccess,error:onError})}function sendDefaultQuestionRating(data,rating,onSuccess,onError){sendAction({action:"set-default-question-rating",id:parameters.courseModuleId,rating:rating},onSuccess,onError)}function sendQuestionRating(data,rating,onSuccess,onError){sendAction({action:"set-question-rating",id:parameters.courseModuleId,"question-id":data.questionId,rating:rating},onSuccess,onError)}function submitInput($input,sendInput,data){$input.data("saving",!0),$input.data("dirty",!1);var $indicator=$input.next();$indicator.css("color","blue"),sendInput(data,$input.val(),(function(){!0===$input.data("dirty")?submitInput($input,sendInput,data):($indicator.css("color","green"),$input.data("dirty",!1),$input.data("saving",!1))}),(function(){$indicator.css("color","red")}))}function submitQuestionRating($input){submitInput($input,sendQuestionRating,{questionId:$input.data("question-id")})}function submitDefaultQuestionRating($input){submitInput($input,sendDefaultQuestionRating,null)}function registerListener(query,submit){$(document).on("input",query,(function(event){var $input=$(event.target);!0!==$input.data("saving")?submit($input):$input.data("dirty",!0)}))}function sortTable($header){const column=$header.index(),$table=$header.parent().parent();let $rows=$table.find("tr:gt(0)").toArray().sort((function(rowA,rowB){const $colA=$(rowA).children("td").eq(0),$colB=$(rowB).children("td").eq(0);return parseInt($colA.text())-parseInt($colB.text())}));$table.append($rows),$rows=$table.find("tr:gt(0)").toArray().sort((function(rowA,rowB){const $colA=$(rowA).children("td").eq(column),$colB=$(rowB).children("td").eq(column),$itemA=$colA.find(".capquiz-sortable-item"),$itemB=$colB.find(".capquiz-sortable-item");let valA,valB;return valA=0===$itemA.length?$colA.html():0===$itemA.val().length?$itemA.html():$itemA.val(),valB=0===$itemB.length?$colB.html():0===$itemB.val().length?$itemB.html():$itemB.val(),$.isNumeric(valA)&&$.isNumeric(valB)?valA-valB:valA.toString().localeCompare(valB)}));const ascending="true"===$table.data("asc");$table.data("asc",ascending?"false":"true");const iconName=ascending?"fa-arrow-up":"fa-arrow-down";$.each($table.find(".capquiz-sortable"),(function(){$(this).find(".fa").remove()})),$header.prepend(''),ascending||($rows=$rows.reverse()),$table.append($rows);let i=1;$table.find("tr:gt(0)").each((function(){$(this).find("td:first-child").html(i),i++}))}return{initialize:function(courseModuleId){parameters.courseModuleId=courseModuleId,registerListener(".capquiz-question-rating input",submitQuestionRating),registerListener(".capquiz-default-question-rating input",submitDefaultQuestionRating),$(".capquiz-question-rating-submit-wrapper button").each((function(index,object){$(object).attr("tabindex",-1)})),$(document).on("click",".capquiz-sortable",(function(){sortTable($(this))})),$(".capquiz-sortable-default-asc").each((function(){sortTable($(this)),sortTable($(this))})),$(".capquiz-sortable-default-desc").each((function(){sortTable($(this))})),$(".capquiz-add-selected-questions").on("click",(function(){var questionIds="";$("#categoryquestions td input[type=checkbox]:checked").each((function(){questionIds+=$(this).attr("name").slice(1)+","})),$.post("action.php",{action:"add-question",id:parameters.courseModuleId,"question-id":questionIds},(function(){location.reload()}))}))}}})); - -//# sourceMappingURL=edit_questions.min.js.map \ No newline at end of file diff --git a/amd/build/edit_questions.min.js.map b/amd/build/edit_questions.min.js.map deleted file mode 100644 index a7b839c..0000000 --- a/amd/build/edit_questions.min.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"edit_questions.min.js","sources":["../src/edit_questions.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 mod_capquiz\n * @author Sebastian S. Gundersen \n * @copyright 2019 NTNU\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\ndefine(['jquery'], function($) {\n\n var parameters = {\n courseModuleId: 0,\n };\n\n /**\n * Send an action to the server.\n * @param {Object} data\n * @param {Object} onSuccess\n * @param {Object} onError\n */\n function sendAction(data, onSuccess, onError) {\n $.ajax({\n type: 'post',\n url: 'action.php',\n data: data,\n success: onSuccess,\n error: onError\n });\n }\n\n /**\n * Send the new default rating for the question list to the server.\n * @param {Object} data\n * @param {number} rating\n * @param {Object} onSuccess\n * @param {Object} onError\n */\n function sendDefaultQuestionRating(data, rating, onSuccess, onError) {\n sendAction({\n 'action': 'set-default-question-rating',\n 'id': parameters.courseModuleId,\n 'rating': rating,\n }, onSuccess, onError);\n }\n\n /**\n * Send the new rating for the question to the server.\n * @param {Object} data\n * @param {number} rating\n * @param {Object} onSuccess\n * @param {Object} onError\n */\n function sendQuestionRating(data, rating, onSuccess, onError) {\n sendAction({\n 'action': 'set-question-rating',\n 'id': parameters.courseModuleId,\n 'question-id': data.questionId,\n 'rating': rating,\n }, onSuccess, onError);\n }\n\n /**\n * Send the new value, and avoid race condition.\n * @param {Object} $input\n * @param {Object} sendInput\n * @param {Object} data\n */\n function submitInput($input, sendInput, data) {\n $input.data('saving', true);\n $input.data('dirty', false);\n var $indicator = $input.next();\n $indicator.css('color', 'blue');\n sendInput(data, $input.val(), function() {\n if ($input.data('dirty') === true) {\n submitInput($input, sendInput, data);\n } else {\n $indicator.css('color', 'green');\n $input.data('dirty', false);\n $input.data('saving', false);\n }\n }, function() {\n $indicator.css('color', 'red');\n });\n }\n\n /**\n * Send the new rating for the question, and avoid race condition.\n * @param {Object} $input\n */\n function submitQuestionRating($input) {\n submitInput($input, sendQuestionRating, {questionId: $input.data('question-id')});\n }\n\n /**\n * Send the new default rating for the question list, and avoid race condition.\n * @param {Object} $input\n */\n function submitDefaultQuestionRating($input) {\n submitInput($input, sendDefaultQuestionRating, null);\n }\n\n /**\n * Register an input event listener for submission.\n * @param {string} query\n * @param {Object} submit\n */\n function registerListener(query, submit) {\n $(document).on('input', query, function(event) {\n var $input = $(event.target);\n var isBeingSaved = $input.data('saving');\n if (isBeingSaved === true) {\n $input.data('dirty', true);\n return;\n }\n submit($input);\n });\n }\n\n /**\n * Sorts a table by the respective column based on $header.\n * It searches for an element of class \"capquiz-sortable-item\" inside the , and if found,\n * the value attribute is used if it exists. Otherwise, the inner html is used to sort by.\n *\n * The tag may not have the item class, as it has no effect on the sorting.\n * Their children elements are not required to have the class either. The inner html of will be used then.\n *\n * The first column in the table must be an index of the row.\n *\n * @param {Object} $header The header column for which to sort the table by.\n */\n function sortTable($header) {\n const column = $header.index();\n const $table = $header.parent().parent();\n let $rows = $table.find('tr:gt(0)').toArray().sort(function(rowA, rowB) {\n const $colA = $(rowA).children('td').eq(0);\n const $colB = $(rowB).children('td').eq(0);\n return parseInt($colA.text()) - parseInt($colB.text());\n });\n $table.append($rows);\n $rows = $table.find('tr:gt(0)').toArray().sort(function(rowA, rowB) {\n const $colA = $(rowA).children('td').eq(column);\n const $colB = $(rowB).children('td').eq(column);\n const $itemA = $colA.find('.capquiz-sortable-item');\n const $itemB = $colB.find('.capquiz-sortable-item');\n let valA;\n if ($itemA.length === 0) {\n valA = $colA.html();\n } else {\n valA = $itemA.val().length === 0 ? $itemA.html() : $itemA.val();\n }\n let valB;\n if ($itemB.length === 0) {\n valB = $colB.html();\n } else {\n valB = $itemB.val().length === 0 ? $itemB.html() : $itemB.val();\n }\n if ($.isNumeric(valA) && $.isNumeric(valB)) {\n return valA - valB;\n } else {\n return valA.toString().localeCompare(valB);\n }\n });\n const ascending = ($table.data('asc') === 'true');\n $table.data('asc', ascending ? 'false' : 'true');\n const iconName = (ascending ? 'fa-arrow-up' : 'fa-arrow-down');\n $.each($table.find('.capquiz-sortable'), function() {\n $(this).find('.fa').remove();\n });\n $header.prepend('');\n if (!ascending) {\n $rows = $rows.reverse();\n }\n $table.append($rows);\n let i = 1;\n $table.find('tr:gt(0)').each(function() {\n $(this).find('td:first-child').html(i);\n i++;\n });\n }\n\n /**\n * Register click event listeners for the sortable table columns.\n */\n function registerSortListener() {\n $(document).on('click', '.capquiz-sortable', function() {\n sortTable($(this));\n });\n $('.capquiz-sortable-default-asc').each(function() {\n sortTable($(this));\n sortTable($(this));\n });\n $('.capquiz-sortable-default-desc').each(function() {\n sortTable($(this));\n });\n }\n\n /**\n * Set the tab indices for the question rating elements to be more user friendly.\n */\n function fixTabIndicesForQuestionRatingInputs() {\n $('.capquiz-question-rating-submit-wrapper button').each(function(index, object) {\n $(object).attr('tabindex', -1);\n });\n }\n\n /**\n * Register click event listener for \"Add to quiz\" button.\n */\n function listenAddToQuiz() {\n $('.capquiz-add-selected-questions').on('click', function() {\n var questionIds = '';\n $('#categoryquestions td input[type=checkbox]:checked').each(function() {\n questionIds += $(this).attr('name').slice(1) + ',';\n });\n $.post('action.php', {\n 'action': 'add-question',\n 'id': parameters.courseModuleId,\n 'question-id': questionIds,\n }, function() {\n location.reload();\n });\n });\n }\n\n return {\n initialize: function(courseModuleId) {\n parameters.courseModuleId = courseModuleId;\n registerListener('.capquiz-question-rating input', submitQuestionRating);\n registerListener('.capquiz-default-question-rating input', submitDefaultQuestionRating);\n fixTabIndicesForQuestionRatingInputs();\n registerSortListener();\n listenAddToQuiz();\n }\n };\n\n});\n"],"names":["define","$","parameters","courseModuleId","sendAction","data","onSuccess","onError","ajax","type","url","success","error","sendDefaultQuestionRating","rating","sendQuestionRating","questionId","submitInput","$input","sendInput","$indicator","next","css","val","submitQuestionRating","submitDefaultQuestionRating","registerListener","query","submit","document","on","event","target","sortTable","$header","column","index","$table","parent","$rows","find","toArray","sort","rowA","rowB","$colA","children","eq","$colB","parseInt","text","append","$itemA","$itemB","valA","valB","length","html","isNumeric","toString","localeCompare","ascending","iconName","each","this","remove","prepend","reverse","i","initialize","object","attr","questionIds","slice","post","location","reload"],"mappings":";;;;;;AAsBAA,oCAAO,CAAC,WAAW,SAASC,OAEpBC,WAAa,CACbC,eAAgB,YASXC,WAAWC,KAAMC,UAAWC,SACjCN,EAAEO,KAAK,CACHC,KAAM,OACNC,IAAK,aACLL,KAAMA,KACNM,QAASL,UACTM,MAAOL,mBAWNM,0BAA0BR,KAAMS,OAAQR,UAAWC,SACxDH,WAAW,QACG,iCACJF,WAAWC,sBACPW,QACXR,UAAWC,kBAUTQ,mBAAmBV,KAAMS,OAAQR,UAAWC,SACjDH,WAAW,QACG,yBACJF,WAAWC,6BACFE,KAAKW,kBACVF,QACXR,UAAWC,kBASTU,YAAYC,OAAQC,UAAWd,MACpCa,OAAOb,KAAK,UAAU,GACtBa,OAAOb,KAAK,SAAS,OACjBe,WAAaF,OAAOG,OACxBD,WAAWE,IAAI,QAAS,QACxBH,UAAUd,KAAMa,OAAOK,OAAO,YACG,IAAzBL,OAAOb,KAAK,SACZY,YAAYC,OAAQC,UAAWd,OAE/Be,WAAWE,IAAI,QAAS,SACxBJ,OAAOb,KAAK,SAAS,GACrBa,OAAOb,KAAK,UAAU,OAE3B,WACCe,WAAWE,IAAI,QAAS,mBAQvBE,qBAAqBN,QAC1BD,YAAYC,OAAQH,mBAAoB,CAACC,WAAYE,OAAOb,KAAK,0BAO5DoB,4BAA4BP,QACjCD,YAAYC,OAAQL,0BAA2B,eAQ1Ca,iBAAiBC,MAAOC,QAC7B3B,EAAE4B,UAAUC,GAAG,QAASH,OAAO,SAASI,WAChCb,OAASjB,EAAE8B,MAAMC,SAEA,IADFd,OAAOb,KAAK,UAK/BuB,OAAOV,QAHHA,OAAOb,KAAK,SAAS,eAmBxB4B,UAAUC,eACTC,OAASD,QAAQE,QACjBC,OAASH,QAAQI,SAASA,aAC5BC,MAAQF,OAAOG,KAAK,YAAYC,UAAUC,MAAK,SAASC,KAAMC,YACxDC,MAAQ5C,EAAE0C,MAAMG,SAAS,MAAMC,GAAG,GAClCC,MAAQ/C,EAAE2C,MAAME,SAAS,MAAMC,GAAG,UACjCE,SAASJ,MAAMK,QAAUD,SAASD,MAAME,WAEnDb,OAAOc,OAAOZ,OACdA,MAAQF,OAAOG,KAAK,YAAYC,UAAUC,MAAK,SAASC,KAAMC,YACpDC,MAAQ5C,EAAE0C,MAAMG,SAAS,MAAMC,GAAGZ,QAClCa,MAAQ/C,EAAE2C,MAAME,SAAS,MAAMC,GAAGZ,QAClCiB,OAASP,MAAML,KAAK,0BACpBa,OAASL,MAAMR,KAAK,8BACtBc,KAMAC,YAJAD,KADkB,IAAlBF,OAAOI,OACAX,MAAMY,OAEkB,IAAxBL,OAAO7B,MAAMiC,OAAeJ,OAAOK,OAASL,OAAO7B,MAI1DgC,KADkB,IAAlBF,OAAOG,OACAR,MAAMS,OAEkB,IAAxBJ,OAAO9B,MAAMiC,OAAeH,OAAOI,OAASJ,OAAO9B,MAE1DtB,EAAEyD,UAAUJ,OAASrD,EAAEyD,UAAUH,MAC1BD,KAAOC,KAEPD,KAAKK,WAAWC,cAAcL,eAGvCM,UAAoC,SAAvBxB,OAAOhC,KAAK,OAC/BgC,OAAOhC,KAAK,MAAOwD,UAAY,QAAU,cACnCC,SAAYD,UAAY,cAAgB,gBAC9C5D,EAAE8D,KAAK1B,OAAOG,KAAK,sBAAsB,WACrCvC,EAAE+D,MAAMxB,KAAK,OAAOyB,YAExB/B,QAAQgC,QAAQ,gBAAkBJ,SAAW,UACxCD,YACDtB,MAAQA,MAAM4B,WAElB9B,OAAOc,OAAOZ,WACV6B,EAAI,EACR/B,OAAOG,KAAK,YAAYuB,MAAK,WACzB9D,EAAE+D,MAAMxB,KAAK,kBAAkBiB,KAAKW,GACpCA,aAgDD,CACHC,WAAY,SAASlE,gBACjBD,WAAWC,eAAiBA,eAC5BuB,iBAAiB,iCAAkCF,sBACnDE,iBAAiB,yCAA0CD,6BA5B/DxB,EAAE,kDAAkD8D,MAAK,SAAS3B,MAAOkC,QACrErE,EAAEqE,QAAQC,KAAK,YAAa,MAjBhCtE,EAAE4B,UAAUC,GAAG,QAAS,qBAAqB,WACzCG,UAAUhC,EAAE+D,UAEhB/D,EAAE,iCAAiC8D,MAAK,WACpC9B,UAAUhC,EAAE+D,OACZ/B,UAAUhC,EAAE+D,UAEhB/D,EAAE,kCAAkC8D,MAAK,WACrC9B,UAAUhC,EAAE+D,UAiBhB/D,EAAE,mCAAmC6B,GAAG,SAAS,eACzC0C,YAAc,GAClBvE,EAAE,sDAAsD8D,MAAK,WACzDS,aAAevE,EAAE+D,MAAMO,KAAK,QAAQE,MAAM,GAAK,OAEnDxE,EAAEyE,KAAK,aAAc,QACP,kBACJxE,WAAWC,6BACFqE,cAChB,WACCG,SAASC"} \ No newline at end of file diff --git a/amd/build/edit_slots.min.js b/amd/build/edit_slots.min.js new file mode 100644 index 0000000..9a71d52 --- /dev/null +++ b/amd/build/edit_slots.min.js @@ -0,0 +1,3 @@ +define("mod_capquiz/edit_slots",["exports","core/ajax"],(function(_exports,_ajax){var obj;Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.setQuestionRating=_exports.init=_exports.SELECTORS=void 0,_ajax=(obj=_ajax)&&obj.__esModule?obj:{default:obj};const SELECTORS={questionRating:".capquiz-question-rating",questionRatingInput:".capquiz-question-rating input",questionRatingSaveButton:".capquiz-question-rating-save",questionRatingResetButton:".capquiz-question-rating-reset"};_exports.SELECTORS=SELECTORS;const setQuestionRating=(courseModuleId,slotId,rating)=>_ajax.default.call([{methodname:"mod_capquiz_set_question_rating",args:{cmid:courseModuleId,slotid:slotId,rating:rating}}])[0];_exports.setQuestionRating=setQuestionRating;_exports.init=courseModuleId=>{window.addEventListener("beforeunload",(()=>{for(const input of document.querySelectorAll(SELECTORS.questionRatingInput))"yes"===input.dataset.dirty&&event.preventDefault()})),document.addEventListener("input",(event=>{const questionRating=event.target.closest(SELECTORS.questionRating);if(questionRating){const input=questionRating.querySelector("input"),dirty=input.dataset.initialValue!==input.value,saveButton=questionRating.querySelector(SELECTORS.questionRatingSaveButton),resetButton=questionRating.querySelector(SELECTORS.questionRatingResetButton);saveButton.classList.toggle("d-none",!dirty),resetButton.classList.toggle("d-none",!dirty),dirty?input.dataset.dirty="yes":input.removeAttribute("data-dirty")}})),document.addEventListener("click",(async event=>{const saveButton=event.target.closest(SELECTORS.questionRatingSaveButton);if(saveButton){const questionRating=event.target.closest(SELECTORS.questionRating),input=questionRating.querySelector(SELECTORS.questionRatingInput),resetButton=questionRating.querySelector(SELECTORS.questionRatingResetButton);saveButton.disabled=!0,resetButton.disabled=!0;const newValue=parseFloat(input.value);await setQuestionRating(courseModuleId,parseInt(input.dataset.slotId),newValue),input.dataset.initialValue=input.value,saveButton.classList.add("d-none"),resetButton.classList.add("d-none"),saveButton.disabled=!1,resetButton.disabled=!1,input.removeAttribute("data-dirty")}const resetButton=event.target.closest(SELECTORS.questionRatingResetButton);if(resetButton){const questionRating=event.target.closest(SELECTORS.questionRating),input=questionRating.querySelector(SELECTORS.questionRatingInput);input.value=input.dataset.initialValue,input.removeAttribute("data-dirty"),questionRating.querySelector(SELECTORS.questionRatingSaveButton).classList.add("d-none"),resetButton.classList.add("d-none")}}))}})); + +//# sourceMappingURL=edit_slots.min.js.map \ No newline at end of file diff --git a/amd/build/edit_slots.min.js.map b/amd/build/edit_slots.min.js.map new file mode 100644 index 0000000..ae53eab --- /dev/null +++ b/amd/build/edit_slots.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"edit_slots.min.js","sources":["../src/edit_slots.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 mod_capquiz\n * @author Sebastian Gundersen \n * @copyright 2024 Norwegian University of Science and Technology (NTNU)\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n\"use strict\";\n\nimport Ajax from 'core/ajax';\n\nexport const SELECTORS = {\n questionRating: '.capquiz-question-rating',\n questionRatingInput: '.capquiz-question-rating input',\n questionRatingSaveButton: '.capquiz-question-rating-save',\n questionRatingResetButton: '.capquiz-question-rating-reset',\n};\n\n/**\n * Set question rating.\n *\n * @param {number} courseModuleId\n * @param {number} slotId\n * @param {number} rating\n * @returns {Promise}\n */\nexport const setQuestionRating = (courseModuleId, slotId, rating) => {\n return Ajax.call([{\n methodname: 'mod_capquiz_set_question_rating',\n args: {cmid: courseModuleId, slotid: slotId, rating: rating},\n }])[0];\n};\n\n/**\n * Initialize.\n *\n * @param {number} courseModuleId\n */\nexport const init = courseModuleId => {\n window.addEventListener('beforeunload', () => {\n for (const input of document.querySelectorAll(SELECTORS.questionRatingInput)) {\n if (input.dataset.dirty === 'yes') {\n event.preventDefault();\n }\n }\n });\n document.addEventListener('input', event => {\n const questionRating = event.target.closest(SELECTORS.questionRating);\n if (questionRating) {\n const input = questionRating.querySelector('input');\n const dirty = input.dataset.initialValue !== input.value;\n const saveButton = questionRating.querySelector(SELECTORS.questionRatingSaveButton);\n const resetButton = questionRating.querySelector(SELECTORS.questionRatingResetButton);\n saveButton.classList.toggle('d-none', !dirty);\n resetButton.classList.toggle('d-none', !dirty);\n if (dirty) {\n input.dataset.dirty = 'yes';\n } else {\n input.removeAttribute('data-dirty');\n }\n }\n });\n document.addEventListener('click', async event => {\n const saveButton = event.target.closest(SELECTORS.questionRatingSaveButton);\n if (saveButton) {\n const questionRating = event.target.closest(SELECTORS.questionRating);\n const input = questionRating.querySelector(SELECTORS.questionRatingInput);\n const resetButton = questionRating.querySelector(SELECTORS.questionRatingResetButton);\n saveButton.disabled = true;\n resetButton.disabled = true;\n const newValue = parseFloat(input.value);\n await setQuestionRating(courseModuleId, parseInt(input.dataset.slotId), newValue);\n input.dataset.initialValue = input.value;\n saveButton.classList.add('d-none');\n resetButton.classList.add('d-none');\n saveButton.disabled = false;\n resetButton.disabled = false;\n input.removeAttribute('data-dirty');\n }\n const resetButton = event.target.closest(SELECTORS.questionRatingResetButton);\n if (resetButton) {\n const questionRating = event.target.closest(SELECTORS.questionRating);\n const input = questionRating.querySelector(SELECTORS.questionRatingInput);\n input.value = input.dataset.initialValue;\n input.removeAttribute('data-dirty');\n questionRating.querySelector(SELECTORS.questionRatingSaveButton).classList.add('d-none');\n resetButton.classList.add('d-none');\n }\n });\n};\n"],"names":["SELECTORS","questionRating","questionRatingInput","questionRatingSaveButton","questionRatingResetButton","setQuestionRating","courseModuleId","slotId","rating","Ajax","call","methodname","args","cmid","slotid","window","addEventListener","input","document","querySelectorAll","dataset","dirty","event","preventDefault","target","closest","querySelector","initialValue","value","saveButton","resetButton","classList","toggle","removeAttribute","async","disabled","newValue","parseFloat","parseInt","add"],"mappings":"+QA0BaA,UAAY,CACrBC,eAAgB,2BAChBC,oBAAqB,iCACrBC,yBAA0B,gCAC1BC,0BAA2B,qEAWlBC,kBAAoB,CAACC,eAAgBC,OAAQC,SAC/CC,cAAKC,KAAK,CAAC,CACdC,WAAY,kCACZC,KAAM,CAACC,KAAMP,eAAgBQ,OAAQP,OAAQC,OAAQA,WACrD,8DAQYF,iBAChBS,OAAOC,iBAAiB,gBAAgB,SAC/B,MAAMC,SAASC,SAASC,iBAAiBnB,UAAUE,qBACxB,QAAxBe,MAAMG,QAAQC,OACdC,MAAMC,oBAIlBL,SAASF,iBAAiB,SAASM,cACzBrB,eAAiBqB,MAAME,OAAOC,QAAQzB,UAAUC,mBAClDA,eAAgB,OACVgB,MAAQhB,eAAeyB,cAAc,SACrCL,MAAQJ,MAAMG,QAAQO,eAAiBV,MAAMW,MAC7CC,WAAa5B,eAAeyB,cAAc1B,UAAUG,0BACpD2B,YAAc7B,eAAeyB,cAAc1B,UAAUI,2BAC3DyB,WAAWE,UAAUC,OAAO,UAAWX,OACvCS,YAAYC,UAAUC,OAAO,UAAWX,OACpCA,MACAJ,MAAMG,QAAQC,MAAQ,MAEtBJ,MAAMgB,gBAAgB,kBAIlCf,SAASF,iBAAiB,SAASkB,MAAAA,cACzBL,WAAaP,MAAME,OAAOC,QAAQzB,UAAUG,6BAC9C0B,WAAY,OACN5B,eAAiBqB,MAAME,OAAOC,QAAQzB,UAAUC,gBAChDgB,MAAQhB,eAAeyB,cAAc1B,UAAUE,qBAC/C4B,YAAc7B,eAAeyB,cAAc1B,UAAUI,2BAC3DyB,WAAWM,UAAW,EACtBL,YAAYK,UAAW,QACjBC,SAAWC,WAAWpB,MAAMW,aAC5BvB,kBAAkBC,eAAgBgC,SAASrB,MAAMG,QAAQb,QAAS6B,UACxEnB,MAAMG,QAAQO,aAAeV,MAAMW,MACnCC,WAAWE,UAAUQ,IAAI,UACzBT,YAAYC,UAAUQ,IAAI,UAC1BV,WAAWM,UAAW,EACtBL,YAAYK,UAAW,EACvBlB,MAAMgB,gBAAgB,oBAEpBH,YAAcR,MAAME,OAAOC,QAAQzB,UAAUI,8BAC/C0B,YAAa,OACP7B,eAAiBqB,MAAME,OAAOC,QAAQzB,UAAUC,gBAChDgB,MAAQhB,eAAeyB,cAAc1B,UAAUE,qBACrDe,MAAMW,MAAQX,MAAMG,QAAQO,aAC5BV,MAAMgB,gBAAgB,cACtBhC,eAAeyB,cAAc1B,UAAUG,0BAA0B4B,UAAUQ,IAAI,UAC/ET,YAAYC,UAAUQ,IAAI"} \ No newline at end of file diff --git a/amd/build/qbank_modal.min.js b/amd/build/qbank_modal.min.js new file mode 100644 index 0000000..fdf1ebd --- /dev/null +++ b/amd/build/qbank_modal.min.js @@ -0,0 +1,3 @@ +define("mod_capquiz/qbank_modal",["exports","core/modal","core/modal_events","core_form/changechecker","core/fragment"],(function(_exports,_modal,ModalEvents,FormChangeChecker,Fragment){var obj;function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireWildcard(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}return newObj.default=obj,cache&&cache.set(obj,newObj),newObj}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=_exports.QuestionBankModal=void 0,_modal=(obj=_modal)&&obj.__esModule?obj:{default:obj},ModalEvents=_interopRequireWildcard(ModalEvents),FormChangeChecker=_interopRequireWildcard(FormChangeChecker),Fragment=_interopRequireWildcard(Fragment);class QuestionBankModal extends _modal.default{configure(modalConfig){modalConfig.large=!0,modalConfig.show=!0,this.contextId=modalConfig.contextId,super.configure(modalConfig)}show(){return this.setBody(Fragment.loadFragment("mod_capquiz","capquiz_qbank",this.contextId,{querystring:window.location.search})),super.show()}registerEventListeners(){super.registerEventListeners(this),this.getModal().on("click","a",(event=>{const target=event.currentTarget;target.closest("td.capquiz_add_question")||target.closest("td.capquiz_preview_question")||target.closest(".sorters")||(event.preventDefault(),this.reloadBodyContent(target.getAttribute("search")))})),this.getRoot().on(ModalEvents.bodyRendered,(()=>{FormChangeChecker.disableAllChecks()}))}}_exports.QuestionBankModal=QuestionBankModal;_exports.init=contextId=>{QuestionBankModal.registerModalType(),document.addEventListener("click",(async event=>{const target=event.target.closest('.menu [data-action="questionbank"]');target&&(event.preventDefault(),await QuestionBankModal.create({contextId:contextId,title:target.dataset.header,addOnPage:target.dataset.addonpage,templateContext:{hidden:!0},large:!0}))}))}})); + +//# sourceMappingURL=qbank_modal.min.js.map \ No newline at end of file diff --git a/amd/build/qbank_modal.min.js.map b/amd/build/qbank_modal.min.js.map new file mode 100644 index 0000000..220a461 --- /dev/null +++ b/amd/build/qbank_modal.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"qbank_modal.min.js","sources":["../src/qbank_modal.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 mod_capquiz\n * @author Sebastian Gundersen \n * @copyright 2024 Norwegian University of Science and Technology (NTNU)\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n\"use strict\";\n\nimport Modal from 'core/modal';\nimport * as ModalEvents from 'core/modal_events';\nimport * as FormChangeChecker from 'core_form/changechecker';\nimport * as Fragment from 'core/fragment';\n\nexport class QuestionBankModal extends Modal {\n\n /**\n * @param {Object} modalConfig\n */\n configure(modalConfig) {\n modalConfig.large = true;\n modalConfig.show = true;\n this.contextId = modalConfig.contextId;\n super.configure(modalConfig);\n }\n\n /**\n * Display this modal.\n *\n * @returns {Promise}\n */\n show() {\n this.setBody(Fragment.loadFragment('mod_capquiz', 'capquiz_qbank', this.contextId, {\n querystring: window.location.search,\n }));\n return super.show();\n }\n\n /**\n * Register event listeners.\n */\n registerEventListeners() {\n super.registerEventListeners(this);\n this.getModal().on('click', 'a', event => {\n const target = event.currentTarget;\n if (target.closest('td.capquiz_add_question')) {\n return;\n }\n if (target.closest('td.capquiz_preview_question')) {\n return;\n }\n if (target.closest('.sorters')) {\n return;\n }\n event.preventDefault();\n this.reloadBodyContent(target.getAttribute('search'));\n });\n this.getRoot().on(ModalEvents.bodyRendered, () => {\n FormChangeChecker.disableAllChecks();\n });\n }\n}\n\n/**\n * Initialize.\n *\n * @param {number} contextId\n */\nexport const init = contextId => {\n QuestionBankModal.registerModalType();\n document.addEventListener('click', async event => {\n const target = event.target.closest('.menu [data-action=\"questionbank\"]');\n if (target) {\n event.preventDefault();\n await QuestionBankModal.create({\n contextId: contextId,\n title: target.dataset.header,\n addOnPage: target.dataset.addonpage,\n templateContext: {\n hidden: true,\n },\n large: true,\n });\n }\n });\n};\n"],"names":["QuestionBankModal","Modal","configure","modalConfig","large","show","contextId","setBody","Fragment","loadFragment","this","querystring","window","location","search","super","registerEventListeners","getModal","on","event","target","currentTarget","closest","preventDefault","reloadBodyContent","getAttribute","getRoot","ModalEvents","bodyRendered","FormChangeChecker","disableAllChecks","registerModalType","document","addEventListener","async","create","title","dataset","header","addOnPage","addonpage","templateContext","hidden"],"mappings":"s7CA6BaA,0BAA0BC,eAKnCC,UAAUC,aACNA,YAAYC,OAAQ,EACpBD,YAAYE,MAAO,OACdC,UAAYH,YAAYG,gBACvBJ,UAAUC,aAQpBE,mBACSE,QAAQC,SAASC,aAAa,cAAe,gBAAiBC,KAAKJ,UAAW,CAC/EK,YAAaC,OAAOC,SAASC,UAE1BC,MAAMV,OAMjBW,+BACUA,uBAAuBN,WACxBO,WAAWC,GAAG,QAAS,KAAKC,cACvBC,OAASD,MAAME,cACjBD,OAAOE,QAAQ,4BAGfF,OAAOE,QAAQ,gCAGfF,OAAOE,QAAQ,cAGnBH,MAAMI,sBACDC,kBAAkBJ,OAAOK,aAAa,oBAE1CC,UAAUR,GAAGS,YAAYC,cAAc,KACxCC,kBAAkBC,kFAUVxB,YAChBN,kBAAkB+B,oBAClBC,SAASC,iBAAiB,SAASC,MAAAA,cACzBd,OAASD,MAAMC,OAAOE,QAAQ,sCAChCF,SACAD,MAAMI,uBACAvB,kBAAkBmC,OAAO,CAC3B7B,UAAWA,UACX8B,MAAOhB,OAAOiB,QAAQC,OACtBC,UAAWnB,OAAOiB,QAAQG,UAC1BC,gBAAiB,CACbC,QAAQ,GAEZtC,OAAO"} \ No newline at end of file diff --git a/amd/src/attempt.js b/amd/src/attempt.js deleted file mode 100644 index 0588066..0000000 --- a/amd/src/attempt.js +++ /dev/null @@ -1,82 +0,0 @@ -// This file is part of Moodle - http://moodle.org/ -// -// Moodle is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Moodle is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Moodle. If not, see . - -/** - * @module mod_capquiz - * @author Sebastian S. Gundersen - * @copyright 2019 NTNU - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - -define(['jquery', 'core/str'], function($, mString) { - - /** - * Show star tooltip. - * @param {Object} $element - * @param {string} text - */ - function showTooltip($element, text) { - var $tooltip = $('.capquiz-star-tooltip'); - $tooltip.html(text); - $tooltip.css('display', 'block'); - var x = $element.offset().left - $tooltip.width() / 2; - var y = $element.offset().top + 32; - $tooltip.css('left', x + 'px'); - $tooltip.css('top', y + 'px'); - } - - /** - * Hide star tooltip. - */ - function hideTooltip() { - $('.capquiz-star-tooltip').css('display', 'none'); - } - - /** - * Register event listeners for showing tooltips on the stars. - */ - function enableTooltips() { - $(document).on('mouseover', '.capquiz-quiz-stars span', function() { - var $self = $(this); - if ($self.hasClass('capquiz-star')) { - $.when(mString.get_string('tooltip_achieved_star', 'capquiz')).done(function(text) { - showTooltip($self, text); - }); - } else if ($self.hasClass('capquiz-lost-star')) { - $.when(mString.get_string('tooltip_lost_star', 'capquiz')).done(function(text) { - showTooltip($self, text); - }); - } else if ($self.hasClass('capquiz-no-star')) { - $.when(mString.get_string('tooltip_no_star', 'capquiz')).done(function(text) { - showTooltip($self, text); - }); - } else if ($self.hasClass('capquiz-help-stars')) { - $.when(mString.get_string('tooltip_help_star', 'capquiz')).done(function(text) { - showTooltip($self, text); - }); - } - }); - $(document).on('mouseleave', '.capquiz-quiz-stars span', function() { - hideTooltip(); - }); - } - - return { - initialize: function() { - enableTooltips(); - } - }; - -}); diff --git a/amd/src/edit_questions.js b/amd/src/edit_questions.js deleted file mode 100755 index a2984f4..0000000 --- a/amd/src/edit_questions.js +++ /dev/null @@ -1,250 +0,0 @@ -// This file is part of Moodle - http://moodle.org/ -// -// Moodle is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Moodle is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Moodle. If not, see . - -/** - * @module mod_capquiz - * @author Sebastian S. Gundersen - * @copyright 2019 NTNU - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - -define(['jquery'], function($) { - - var parameters = { - courseModuleId: 0, - }; - - /** - * Send an action to the server. - * @param {Object} data - * @param {Object} onSuccess - * @param {Object} onError - */ - function sendAction(data, onSuccess, onError) { - $.ajax({ - type: 'post', - url: 'action.php', - data: data, - success: onSuccess, - error: onError - }); - } - - /** - * Send the new default rating for the question list to the server. - * @param {Object} data - * @param {number} rating - * @param {Object} onSuccess - * @param {Object} onError - */ - function sendDefaultQuestionRating(data, rating, onSuccess, onError) { - sendAction({ - 'action': 'set-default-question-rating', - 'id': parameters.courseModuleId, - 'rating': rating, - }, onSuccess, onError); - } - - /** - * Send the new rating for the question to the server. - * @param {Object} data - * @param {number} rating - * @param {Object} onSuccess - * @param {Object} onError - */ - function sendQuestionRating(data, rating, onSuccess, onError) { - sendAction({ - 'action': 'set-question-rating', - 'id': parameters.courseModuleId, - 'question-id': data.questionId, - 'rating': rating, - }, onSuccess, onError); - } - - /** - * Send the new value, and avoid race condition. - * @param {Object} $input - * @param {Object} sendInput - * @param {Object} data - */ - function submitInput($input, sendInput, data) { - $input.data('saving', true); - $input.data('dirty', false); - var $indicator = $input.next(); - $indicator.css('color', 'blue'); - sendInput(data, $input.val(), function() { - if ($input.data('dirty') === true) { - submitInput($input, sendInput, data); - } else { - $indicator.css('color', 'green'); - $input.data('dirty', false); - $input.data('saving', false); - } - }, function() { - $indicator.css('color', 'red'); - }); - } - - /** - * Send the new rating for the question, and avoid race condition. - * @param {Object} $input - */ - function submitQuestionRating($input) { - submitInput($input, sendQuestionRating, {questionId: $input.data('question-id')}); - } - - /** - * Send the new default rating for the question list, and avoid race condition. - * @param {Object} $input - */ - function submitDefaultQuestionRating($input) { - submitInput($input, sendDefaultQuestionRating, null); - } - - /** - * Register an input event listener for submission. - * @param {string} query - * @param {Object} submit - */ - function registerListener(query, submit) { - $(document).on('input', query, function(event) { - var $input = $(event.target); - var isBeingSaved = $input.data('saving'); - if (isBeingSaved === true) { - $input.data('dirty', true); - return; - } - submit($input); - }); - } - - /** - * Sorts a table by the respective column based on $header. - * It searches for an element of class "capquiz-sortable-item" inside the , and if found, - * the value attribute is used if it exists. Otherwise, the inner html is used to sort by. - * - * The tag may not have the item class, as it has no effect on the sorting. - * Their children elements are not required to have the class either. The inner html of will be used then. - * - * The first column in the table must be an index of the row. - * - * @param {Object} $header The header column for which to sort the table by. - */ - function sortTable($header) { - const column = $header.index(); - const $table = $header.parent().parent(); - let $rows = $table.find('tr:gt(0)').toArray().sort(function(rowA, rowB) { - const $colA = $(rowA).children('td').eq(0); - const $colB = $(rowB).children('td').eq(0); - return parseInt($colA.text()) - parseInt($colB.text()); - }); - $table.append($rows); - $rows = $table.find('tr:gt(0)').toArray().sort(function(rowA, rowB) { - const $colA = $(rowA).children('td').eq(column); - const $colB = $(rowB).children('td').eq(column); - const $itemA = $colA.find('.capquiz-sortable-item'); - const $itemB = $colB.find('.capquiz-sortable-item'); - let valA; - if ($itemA.length === 0) { - valA = $colA.html(); - } else { - valA = $itemA.val().length === 0 ? $itemA.html() : $itemA.val(); - } - let valB; - if ($itemB.length === 0) { - valB = $colB.html(); - } else { - valB = $itemB.val().length === 0 ? $itemB.html() : $itemB.val(); - } - if ($.isNumeric(valA) && $.isNumeric(valB)) { - return valA - valB; - } else { - return valA.toString().localeCompare(valB); - } - }); - const ascending = ($table.data('asc') === 'true'); - $table.data('asc', ascending ? 'false' : 'true'); - const iconName = (ascending ? 'fa-arrow-up' : 'fa-arrow-down'); - $.each($table.find('.capquiz-sortable'), function() { - $(this).find('.fa').remove(); - }); - $header.prepend(''); - if (!ascending) { - $rows = $rows.reverse(); - } - $table.append($rows); - let i = 1; - $table.find('tr:gt(0)').each(function() { - $(this).find('td:first-child').html(i); - i++; - }); - } - - /** - * Register click event listeners for the sortable table columns. - */ - function registerSortListener() { - $(document).on('click', '.capquiz-sortable', function() { - sortTable($(this)); - }); - $('.capquiz-sortable-default-asc').each(function() { - sortTable($(this)); - sortTable($(this)); - }); - $('.capquiz-sortable-default-desc').each(function() { - sortTable($(this)); - }); - } - - /** - * Set the tab indices for the question rating elements to be more user friendly. - */ - function fixTabIndicesForQuestionRatingInputs() { - $('.capquiz-question-rating-submit-wrapper button').each(function(index, object) { - $(object).attr('tabindex', -1); - }); - } - - /** - * Register click event listener for "Add to quiz" button. - */ - function listenAddToQuiz() { - $('.capquiz-add-selected-questions').on('click', function() { - var questionIds = ''; - $('#categoryquestions td input[type=checkbox]:checked').each(function() { - questionIds += $(this).attr('name').slice(1) + ','; - }); - $.post('action.php', { - 'action': 'add-question', - 'id': parameters.courseModuleId, - 'question-id': questionIds, - }, function() { - location.reload(); - }); - }); - } - - return { - initialize: function(courseModuleId) { - parameters.courseModuleId = courseModuleId; - registerListener('.capquiz-question-rating input', submitQuestionRating); - registerListener('.capquiz-default-question-rating input', submitDefaultQuestionRating); - fixTabIndicesForQuestionRatingInputs(); - registerSortListener(); - listenAddToQuiz(); - } - }; - -}); diff --git a/amd/src/edit_slots.js b/amd/src/edit_slots.js new file mode 100644 index 0000000..eed22ef --- /dev/null +++ b/amd/src/edit_slots.js @@ -0,0 +1,105 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * @module mod_capquiz + * @author Sebastian Gundersen + * @copyright 2024 Norwegian University of Science and Technology (NTNU) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +"use strict"; + +import Ajax from 'core/ajax'; + +export const SELECTORS = { + questionRating: '.capquiz-question-rating', + questionRatingInput: '.capquiz-question-rating input', + questionRatingSaveButton: '.capquiz-question-rating-save', + questionRatingResetButton: '.capquiz-question-rating-reset', +}; + +/** + * Set question rating. + * + * @param {number} courseModuleId + * @param {number} slotId + * @param {number} rating + * @returns {Promise} + */ +export const setQuestionRating = (courseModuleId, slotId, rating) => { + return Ajax.call([{ + methodname: 'mod_capquiz_set_question_rating', + args: {cmid: courseModuleId, slotid: slotId, rating: rating}, + }])[0]; +}; + +/** + * Initialize. + * + * @param {number} courseModuleId + */ +export const init = courseModuleId => { + window.addEventListener('beforeunload', () => { + for (const input of document.querySelectorAll(SELECTORS.questionRatingInput)) { + if (input.dataset.dirty === 'yes') { + event.preventDefault(); + } + } + }); + document.addEventListener('input', event => { + const questionRating = event.target.closest(SELECTORS.questionRating); + if (questionRating) { + const input = questionRating.querySelector('input'); + const dirty = input.dataset.initialValue !== input.value; + const saveButton = questionRating.querySelector(SELECTORS.questionRatingSaveButton); + const resetButton = questionRating.querySelector(SELECTORS.questionRatingResetButton); + saveButton.classList.toggle('d-none', !dirty); + resetButton.classList.toggle('d-none', !dirty); + if (dirty) { + input.dataset.dirty = 'yes'; + } else { + input.removeAttribute('data-dirty'); + } + } + }); + document.addEventListener('click', async event => { + const saveButton = event.target.closest(SELECTORS.questionRatingSaveButton); + if (saveButton) { + const questionRating = event.target.closest(SELECTORS.questionRating); + const input = questionRating.querySelector(SELECTORS.questionRatingInput); + const resetButton = questionRating.querySelector(SELECTORS.questionRatingResetButton); + saveButton.disabled = true; + resetButton.disabled = true; + const newValue = parseFloat(input.value); + await setQuestionRating(courseModuleId, parseInt(input.dataset.slotId), newValue); + input.dataset.initialValue = input.value; + saveButton.classList.add('d-none'); + resetButton.classList.add('d-none'); + saveButton.disabled = false; + resetButton.disabled = false; + input.removeAttribute('data-dirty'); + } + const resetButton = event.target.closest(SELECTORS.questionRatingResetButton); + if (resetButton) { + const questionRating = event.target.closest(SELECTORS.questionRating); + const input = questionRating.querySelector(SELECTORS.questionRatingInput); + input.value = input.dataset.initialValue; + input.removeAttribute('data-dirty'); + questionRating.querySelector(SELECTORS.questionRatingSaveButton).classList.add('d-none'); + resetButton.classList.add('d-none'); + } + }); +}; diff --git a/amd/src/qbank_modal.js b/amd/src/qbank_modal.js new file mode 100644 index 0000000..a7fecef --- /dev/null +++ b/amd/src/qbank_modal.js @@ -0,0 +1,101 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * @module mod_capquiz + * @author Sebastian Gundersen + * @copyright 2024 Norwegian University of Science and Technology (NTNU) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +"use strict"; + +import Modal from 'core/modal'; +import * as ModalEvents from 'core/modal_events'; +import * as FormChangeChecker from 'core_form/changechecker'; +import * as Fragment from 'core/fragment'; + +export class QuestionBankModal extends Modal { + + /** + * @param {Object} modalConfig + */ + configure(modalConfig) { + modalConfig.large = true; + modalConfig.show = true; + this.contextId = modalConfig.contextId; + super.configure(modalConfig); + } + + /** + * Display this modal. + * + * @returns {Promise} + */ + show() { + this.setBody(Fragment.loadFragment('mod_capquiz', 'capquiz_qbank', this.contextId, { + querystring: window.location.search, + })); + return super.show(); + } + + /** + * Register event listeners. + */ + registerEventListeners() { + super.registerEventListeners(this); + this.getModal().on('click', 'a', event => { + const target = event.currentTarget; + if (target.closest('td.capquiz_add_question')) { + return; + } + if (target.closest('td.capquiz_preview_question')) { + return; + } + if (target.closest('.sorters')) { + return; + } + event.preventDefault(); + this.reloadBodyContent(target.getAttribute('search')); + }); + this.getRoot().on(ModalEvents.bodyRendered, () => { + FormChangeChecker.disableAllChecks(); + }); + } +} + +/** + * Initialize. + * + * @param {number} contextId + */ +export const init = contextId => { + QuestionBankModal.registerModalType(); + document.addEventListener('click', async event => { + const target = event.target.closest('.menu [data-action="questionbank"]'); + if (target) { + event.preventDefault(); + await QuestionBankModal.create({ + contextId: contextId, + title: target.dataset.header, + addOnPage: target.dataset.addonpage, + templateContext: { + hidden: true, + }, + large: true, + }); + } + }); +}; diff --git a/async.php b/async.php deleted file mode 100755 index 7a9ed25..0000000 --- a/async.php +++ /dev/null @@ -1,54 +0,0 @@ -. - -/** - * Async handler for various quiz activities. - * - * @package mod_capquiz - * @author Aleksander Skrede - * @copyright 2018 NTNU - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - -namespace mod_capquiz; - -require_once('../../config.php'); - -$cmid = capquiz_urls::require_course_module_id_param(); -$cm = get_coursemodule_from_id('capquiz', $cmid, 0, false, MUST_EXIST); -require_login($cm->course, false, $cm); -$context = \context_module::instance($cmid); -require_capability('mod/capquiz:student', $context); - -$action = required_param('action', PARAM_TEXT); -$attemptid = optional_param('attempt', null, PARAM_INT); -$cmid = capquiz_urls::require_course_module_id_param(); -$capquiz = new capquiz($cmid); - -capquiz_urls::set_page_url($capquiz, capquiz_urls::$urlasync); - -if ($attemptid !== null) { - $user = $capquiz->user(); - $attempt = capquiz_question_attempt::load_attempt($user, $attemptid); - if ($action === 'answered') { - $capquiz->question_engine($user)->attempt_answered($user, $attempt); - } else if ($action === 'reviewed') { - $capquiz->question_engine($user)->attempt_reviewed($attempt); - } - capquiz_urls::redirect_to_dashboard(); -} - -capquiz_urls::redirect_to_front_page(); diff --git a/attempt.php b/attempt.php new file mode 100644 index 0000000..19cfc51 --- /dev/null +++ b/attempt.php @@ -0,0 +1,97 @@ +. + +/** + * Show current question attempt for user. + * + * @package mod_capquiz + * @author Sebastian Gundersen + * @copyright 2024 Norwegian University of Science and Technology (NTNU) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +use mod_capquiz\api; +use mod_capquiz\capquiz; +use mod_capquiz\capquiz_attempt; +use mod_capquiz\capquiz_slot; +use mod_capquiz\capquiz_user; +use mod_capquiz\output\attempt; +use mod_capquiz\output\renderer; + +require_once(__DIR__ . '/../../config.php'); + +global $CFG, $OUTPUT, $PAGE, $USER; + +require_once($CFG->libdir . '/formslib.php'); +require_once($CFG->dirroot . '/question/editlib.php'); +require_once($CFG->dirroot . '/mod/capquiz/lib.php'); + +$cmid = required_param('id', PARAM_INT); +$cm = get_coursemodule_from_id('capquiz', $cmid, 0, false, MUST_EXIST); +require_login($cm->course, false, $cm); +$context = context_module::instance($cmid); +if (!has_any_capability(['mod/capquiz:student', 'mod/capquiz:instructor'], $context)) { + redirect(new moodle_url('/')); +} + +$PAGE->set_context($context); +$PAGE->set_cm($cm); +$PAGE->set_pagelayout('incourse'); +$PAGE->set_url(new moodle_url('/mod/capquiz/attempt.php', ['id' => $cmid])); + +/** @var renderer $renderer */ +$renderer = $PAGE->get_renderer('mod_capquiz'); + +$capquiz = new capquiz($cm->instance); +$user = capquiz_user::get_record_by_core_user($capquiz, $USER->id); +if (!$user) { + $user = api::create_user($capquiz, $USER->id); +} + +$attempt = capquiz_attempt::get_record_unreviewed_by_user($user->get('id')); +if (!$attempt) { + $slot = capquiz_slot::get_record_for_next_question($user); + if ($slot) { + $attempt = api::create_attempt($user, $slot); + } +} + +$action = optional_param('action', '', PARAM_ALPHA); +switch ($action) { + case 'submit': + // The attempt will now be submitted. The page would render as normal such that the user can + // review question feedback, but we want to redirect to a 'clean' attempt page. + api::submit_attempt($capquiz, $user, $attempt); + redirect($PAGE->url); + + case 'review': + // The attempt will now be marked as reviewed. A new question will be presented when redirected. + api::mark_attempt_as_reviewed($attempt); + redirect($PAGE->url); + + default: + break; +} + +echo $OUTPUT->header(); + +if ($attempt) { + echo $renderer->render(new attempt($attempt, $user, $capquiz)); +} else { + echo get_string('you_finished_capquiz', 'capquiz'); +} + +echo $OUTPUT->footer(); diff --git a/backup/moodle2/backup_capquiz_activity_task.class.php b/backup/moodle2/backup_capquiz_activity_task.class.php index 7319225..556fec3 100644 --- a/backup/moodle2/backup_capquiz_activity_task.class.php +++ b/backup/moodle2/backup_capquiz_activity_task.class.php @@ -46,7 +46,6 @@ protected function define_my_settings() { */ protected function define_my_steps() { $this->add_step(new backup_capquiz_activity_structure_step('capquiz_structure', 'capquiz.xml')); - // TODO: This might not be necessary in future Moodle versions, if discussed subclass is added. $this->add_step(new backup_calculate_question_categories('activity_question_categories')); $this->add_step(new backup_delete_temp_questions('clean_temp_questions')); } diff --git a/backup/moodle2/backup_capquiz_stepslib.php b/backup/moodle2/backup_capquiz_stepslib.php index 0bd25a8..938a8de 100644 --- a/backup/moodle2/backup_capquiz_stepslib.php +++ b/backup/moodle2/backup_capquiz_stepslib.php @@ -40,93 +40,88 @@ protected function define_structure() { 'introformat', 'timecreated', 'timemodified', - 'published', - 'default_user_rating', + 'usermodified', + 'defaultuserrating', + 'starstopass', + 'timedue', + 'numquestioncandidates', + 'minquestionsuntilreappearance', + 'userwinprobability', + 'userkfactor', + 'questionkfactor', ]); - $questionlist = new backup_nested_element('questionlist', null, [ - 'id', - 'capquiz_id', - 'title', - 'author', - 'description', - 'star_ratings', - 'is_template', - 'time_created', - 'time_modified', - 'default_question_rating', - ]); - $questions = new backup_nested_element('questions'); - $question = new backup_nested_element('question', ['id'], [ - 'question_id', - 'question_list_id', + + $slots = new backup_nested_element('questions'); + $slot = new backup_nested_element('question', ['id'], [ + 'capquizid', 'rating', + 'timecreated', + 'timemodified', + 'usermodified', ]); - $questionratings = new backup_nested_element('questionratings'); - $questionrating = new backup_nested_element('question_rating', ['id'], [ - 'capquiz_question_id', + $this->add_question_references($slot, 'mod_capquiz', 'slot'); + $this->add_question_set_references($slot, 'mod_capquiz', 'slot'); + + $questionratings = new backup_nested_element('qratings'); + $questionrating = new backup_nested_element('qrating', ['id'], [ + 'slotid', 'rating', 'manual', 'timecreated', + 'timemodified', + 'usermodified', ]); - $questionselections = new backup_nested_element('questionselections'); - $questionselection = new backup_nested_element('questionselection', ['id'], [ - 'capquiz_id', - 'strategy', - 'configuration', - ]); - $ratingsystems = new backup_nested_element('ratingsystems'); - $ratingsystem = new backup_nested_element('ratingsystem', ['id'], [ - 'capquiz_id', - 'rating_system', - 'configuration', - ]); + $users = new backup_nested_element('users'); $user = new backup_nested_element('user', ['id'], [ - 'user_id', - 'capquiz_id', - 'question_usage_id', + 'userid', + 'capquizid', + 'questionusageid', 'rating', - 'highest_level', + 'higheststars', + 'starsgraded', + 'timecreated', + 'timemodified', + 'usermodified', ]); - $this->add_question_usages($user, 'question_usage_id'); + $this->add_question_usages($user, 'questionusageid'); $userratings = new backup_nested_element('userratings'); - $userrating = new backup_nested_element('user_rating', ['id'], [ - 'capquiz_user_id', + $userrating = new backup_nested_element('userrating', ['id'], [ + 'capquizuserid', 'rating', 'manual', 'timecreated', + 'timemodified', + 'usermodified', ]); + $attempts = new backup_nested_element('attempts'); $attempt = new backup_nested_element('attempt', ['id'], [ 'slot', - 'user_id', - 'question_id', + 'capquizuserid', + 'slotid', 'reviewed', 'answered', - 'time_answered', - 'time_reviewed', - 'question_rating_id', - 'question_prev_rating_id', - 'prev_question_rating_id', - 'prev_question_prev_rating_id', - 'user_rating_id', - 'user_prev_rating_id', + 'timeanswered', + 'timereviewed', + 'questionratingid', + 'questionprevratingid', + 'prevquestionratingid', + 'prevquestionprevratingid', + 'userratingid', + 'userprevratingid', + 'timecreated', + 'timemodified', + 'usermodified', ]); // Build the tree. - $capquiz->add_child($questionlist); - $questionlist->add_child($questions); - $questions->add_child($question); - $question->add_child($questionratings); + $capquiz->add_child($slots); + $slots->add_child($slot); + $slot->add_child($questionratings); $questionratings->add_child($questionrating); - $capquiz->add_child($questionselections); - $questionselections->add_child($questionselection); - - $capquiz->add_child($ratingsystems); - $ratingsystems->add_child($ratingsystem); - $capquiz->add_child($users); $users->add_child($user); $user->add_child($userratings); @@ -136,20 +131,16 @@ protected function define_structure() { // Define sources. $capquiz->set_source_table('capquiz', ['id' => backup::VAR_ACTIVITYID]); - $questionlist->set_source_table('capquiz_question_list', ['capquiz_id' => backup::VAR_PARENTID]); - $question->set_source_table('capquiz_question', ['question_list_id' => backup::VAR_PARENTID]); - $questionrating->set_source_table('capquiz_question_rating', ['capquiz_question_id' => backup::VAR_PARENTID]); - $questionselection->set_source_table('capquiz_question_selection', ['capquiz_id' => backup::VAR_PARENTID]); - $ratingsystem->set_source_table('capquiz_rating_system', ['capquiz_id' => backup::VAR_PARENTID]); + $slot->set_source_table('capquiz_slot', ['capquizid' => backup::VAR_PARENTID]); + $questionrating->set_source_table('capquiz_question_rating', ['slotid' => backup::VAR_PARENTID]); if ($this->get_setting_value('userinfo')) { - $user->set_source_table('capquiz_user', ['capquiz_id' => backup::VAR_PARENTID]); - $userrating->set_source_table('capquiz_user_rating', ['capquiz_user_id' => backup::VAR_PARENTID]); - $attempt->set_source_table('capquiz_attempt', ['user_id' => backup::VAR_PARENTID]); + $user->set_source_table('capquiz_user', ['capquizid' => backup::VAR_PARENTID]); + $userrating->set_source_table('capquiz_user_rating', ['capquizuserid' => backup::VAR_PARENTID]); + $attempt->set_source_table('capquiz_attempt', ['capquizuserid' => backup::VAR_PARENTID]); } // Define id annotations. - $user->annotate_ids('user', 'user_id'); - $question->annotate_ids('question', 'question_id'); + $user->annotate_ids('user', 'userid'); // Define file annotations. $capquiz->annotate_files('mod_capquiz', 'intro', null); diff --git a/backup/moodle2/restore_capquiz_stepslib.php b/backup/moodle2/restore_capquiz_stepslib.php index ff80470..843f3f9 100644 --- a/backup/moodle2/restore_capquiz_stepslib.php +++ b/backup/moodle2/restore_capquiz_stepslib.php @@ -33,7 +33,7 @@ class restore_capquiz_activity_structure_step extends restore_questions_activity /** * @var \stdClass for inform_new_usage_id */ - private $currentquestionlist; + private $currentcapuser; /** * Define the structure to be processed by this backup step. @@ -41,18 +41,14 @@ class restore_capquiz_activity_structure_step extends restore_questions_activity protected function define_structure() { $paths = []; $paths[] = new restore_path_element('capquiz', '/activity/capquiz'); - $questionlist = new restore_path_element('capquiz_question_list', '/activity/capquiz/questionlist'); - $paths[] = $questionlist; - $paths[] = new restore_path_element('capquiz_question', '/activity/capquiz/questionlist/questions/question'); - $paths[] = new restore_path_element( - 'capquiz_question_rating', '/activity/capquiz/questionlist/questions/question/questionratings/question_rating'); - $paths[] = new restore_path_element('capquiz_question_selection', '/activity/capquiz/questionselections/questionselection'); - $paths[] = new restore_path_element('capquiz_rating_system', '/activity/capquiz/ratingsystems/ratingsystem'); + $paths[] = new restore_path_element('capquiz_slot', '/activity/capquiz/slots/slot'); + $paths[] = new restore_path_element('capquiz_question_rating', '/activity/capquiz/slots/slot/qratings/qrating'); + if ($this->get_setting_value('userinfo')) { $capuser = new restore_path_element('capquiz_user', '/activity/capquiz/users/user'); $this->add_question_usages($capuser, $paths); $paths[] = $capuser; - $paths[] = new restore_path_element('capquiz_user_rating', '/activity/capquiz/users/user/userratings/user_rating'); + $paths[] = new restore_path_element('capquiz_user_rating', '/activity/capquiz/users/user/userratings/userrating'); $paths[] = new restore_path_element('capquiz_attempt', '/activity/capquiz/users/user/attempts/attempt'); } return $this->prepare_activity_structure($paths); @@ -63,51 +59,30 @@ protected function define_structure() { * * @param object $data */ - protected function process_capquiz($data) { + protected function process_capquiz($data): void { global $DB; $data = (object)$data; $oldid = $data->id; $data->course = $this->get_courseid(); - $data->timecreated = $this->apply_date_offset($data->timecreated); - $data->timemodified = $this->apply_date_offset($data->timemodified); + $data->timeopen = $this->apply_date_offset($data->timeopen); + $data->timedue = $this->apply_date_offset($data->timedue); $newitemid = $DB->insert_record('capquiz', $data); $this->apply_activity_instance($newitemid); $this->set_mapping('capquiz', $oldid, $newitemid); } /** - * Processes and backs up capquiz question list + * Processes and backs up capquiz slot * * @param object $data */ - protected function process_capquiz_question_list($data) { + protected function process_capquiz_slot($data): void { global $DB; $data = (object)$data; $oldid = $data->id; - $data->capquiz_id = $this->get_new_parentid('capquiz'); - $data->time_created = $this->apply_date_offset($data->time_created); - $data->time_modified = $this->apply_date_offset($data->time_modified); - $data->context_id = \context_course::instance($this->get_courseid())->id; - $newitemid = $DB->insert_record('capquiz_question_list', $data); - $this->set_mapping('capquiz_question_list', $oldid, $newitemid); - } - - /** - * Processes and backs up capquiz questions - * - * @param object $data - */ - protected function process_capquiz_question($data) { - global $DB; - $data = (object)$data; - $oldid = $data->id; - $data->question_id = $this->get_mappingid('question', $data->question_id); - if (!$data->question_id) { - return; - } - $data->question_list_id = $this->get_new_parentid('capquiz_question_list'); - $newitemid = $DB->insert_record('capquiz_question', $data); - $this->set_mapping('capquiz_question', $oldid, $newitemid); + $data->capquizid = $this->get_new_parentid('capquiz'); + $newitemid = $DB->insert_record('capquiz_slot', $data); + $this->set_mapping('capquiz_slot', $oldid, $newitemid); } /** @@ -115,43 +90,15 @@ protected function process_capquiz_question($data) { * * @param object $data */ - protected function process_capquiz_question_rating($data) { + protected function process_capquiz_question_rating($data): void { global $DB; $data = (object)$data; $oldid = $data->id; - $data->capquiz_question_id = $this->get_new_parentid('capquiz_question'); + $data->slotid = $this->get_new_parentid('capquiz_slot'); $newitemid = $DB->insert_record('capquiz_question_rating', $data); $this->set_mapping('capquiz_question_rating', $oldid, $newitemid); } - /** - * Processes and backs up capquiz question selection - * - * @param object $data - */ - protected function process_capquiz_question_selection($data) { - global $DB; - $data = (object)$data; - $data->capquiz_id = $this->get_new_parentid('capquiz'); - $oldid = $data->id; - $newitemid = $DB->insert_record('capquiz_question_selection', $data); - $this->set_mapping('capquiz_question_selection', $oldid, $newitemid); - } - - /** - * Processes and backs up capquiz question rating system - * - * @param object $data - */ - protected function process_capquiz_rating_system($data) { - global $DB; - $data = (object)$data; - $data->capquiz_id = $this->get_new_parentid('capquiz'); - $oldid = $data->id; - $newitemid = $DB->insert_record('capquiz_rating_system', $data); - $this->set_mapping('capquiz_rating_system', $oldid, $newitemid); - } - /** * Processes and backs up capquiz user * @@ -159,8 +106,8 @@ protected function process_capquiz_rating_system($data) { */ protected function process_capquiz_user($data) { $data = (object)$data; - $data->user_id = $this->get_mappingid('user', $data->user_id); - $data->capquiz_id = $this->get_new_parentid('capquiz'); + $data->userid = $this->get_mappingid('user', $data->userid); + $data->capquizid = $this->get_new_parentid('capquiz'); $this->currentcapuser = clone($data); } @@ -173,7 +120,7 @@ protected function process_capquiz_user_rating($data) { global $DB; $data = (object)$data; $oldid = $data->id; - $data->capquiz_user_id = $this->get_new_parentid('capquiz_user'); + $data->capquizuserid = $this->get_new_parentid('capquiz_user'); $newitemid = $DB->insert_record('capquiz_user_rating', $data); $this->set_mapping('capquiz_user_rating', $oldid, $newitemid); } @@ -187,14 +134,14 @@ protected function process_capquiz_attempt($data) { global $DB; $data = (object)$data; $oldid = $data->id; - $data->user_id = $this->get_new_parentid('capquiz_user'); - $data->question_id = $this->get_mappingid('capquiz_question', $data->question_id); - $data->question_rating_id = $this->get_mappingid('capquiz_question_rating', $data->question_rating_id); - $data->question_prev_rating_id = $this->get_mappingid('capquiz_question_rating', $data->question_prev_rating_id); - $data->prev_question_rating_id = $this->get_mappingid('capquiz_question_rating', $data->prev_question_rating_id); - $data->prev_question_prev_rating_id = $this->get_mappingid('capquiz_question_rating', $data->prev_question_prev_rating_id); - $data->user_rating_id = $this->get_mappingid('capquiz_user_rating', $data->user_rating_id); - $data->user_prev_rating_id = $this->get_mappingid('capquiz_user_rating', $data->user_prev_rating_id); + $data->capquizuserid = $this->get_new_parentid('capquiz_user'); + $data->slotid = $this->get_mappingid('capquiz_slot', $data->slotid); + $data->questionratingid = $this->get_mappingid('capquiz_question_rating', $data->questionratingid); + $data->questionprevratingid = $this->get_mappingid('capquiz_question_rating', $data->questionprevratingid); + $data->prevquestionratingid = $this->get_mappingid('capquiz_question_rating', $data->prevquestionratingid); + $data->prevquestionprevratingid = $this->get_mappingid('capquiz_question_rating', $data->prevquestionprevratingid); + $data->userratingid = $this->get_mappingid('capquiz_user_rating', $data->userratingid); + $data->userprevratingid = $this->get_mappingid('capquiz_user_rating', $data->userprevratingid); $newitemid = $DB->insert_record('capquiz_attempt', $data); $this->set_mapping('capquiz_attempt', $oldid, $newitemid); } @@ -204,11 +151,11 @@ protected function process_capquiz_attempt($data) { * * @param int $newusageid */ - protected function inform_new_usage_id($newusageid) { + protected function inform_new_usage_id($newusageid): void { global $DB; $data = $this->currentcapuser; $oldid = $data->id; - $data->question_usage_id = $newusageid; + $data->questionusageid = $newusageid; $newitemid = $DB->insert_record('capquiz_user', $data); $this->set_mapping('capquiz_user', $oldid, $newitemid); } diff --git a/classes/api.php b/classes/api.php new file mode 100644 index 0000000..264e2bf --- /dev/null +++ b/classes/api.php @@ -0,0 +1,302 @@ +. + +declare(strict_types=1); + +namespace mod_capquiz; + +use mod_capquiz\local\helpers\elo; +use mod_capquiz\local\helpers\stars; +use question_bank; +use question_engine; +use question_usage_by_activity; + +/** + * CAPQuiz API. + * + * @package mod_capquiz + * @author Sebastian Gundersen + * @copyright 2024 Norwegian University of Science and Technology (NTNU) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class api { + /** + * Create a new CAPQuiz user. + * + * @param capquiz $capquiz + * @param int $moodleuserid + */ + public static function create_user(capquiz $capquiz, int $moodleuserid): capquiz_user { + $user = new capquiz_user(); + $user->set_many([ + 'userid' => $moodleuserid, + 'capquizid' => $capquiz->get('id'), + 'rating' => $capquiz->get('defaultuserrating'), + ]); + $user->create(); + self::create_user_rating($user, $user->get('rating'), false); + return $user; + } + + /** + * Create a new question usage. + * + * @param capquiz $capquiz + * @return question_usage_by_activity + */ + public static function create_question_usage(capquiz $capquiz): question_usage_by_activity { + global $PAGE; + $quba = question_engine::make_questions_usage_by_activity('mod_capquiz', $PAGE->cm->context); + $quba->set_preferred_behaviour($capquiz->get('questionbehaviour')); + question_engine::save_questions_usage_by_activity($quba); + return $quba; + } + + /** + * CAPQuiz doesn't support every question behaviour due to the nature of how the quiz + * behaves, so we need to filter which can be used. + * + * 'Adaptive mode' is discouraged in favor of 'Interactive with multiple tries'. + * + * 'Adaptive mode (no penalty)' is disabled because it would defeat the purpose of user and question ratings. + * 'Deferred feedback' and 'Deferred feedback with CBM' are disabled because CAPQuiz relies on immediate feedback. + * + * @return string[] + */ + public static function get_unsupported_question_behaviours(): array { + return ['adaptivenopenalty', 'deferredfeedback', 'deferredcbm']; + } + + /** + * Creates a new question attempt for a user. + * + * @param capquiz_user $user + * @param capquiz_slot $slot + */ + public static function create_attempt(capquiz_user $user, capquiz_slot $slot): ?capquiz_attempt { + global $DB; + $question = $slot->find_question(); + if (!$question) { + return null; + } + $questions = question_load_questions([$question->id]); + $question = reset($questions); + if (!$question) { + return null; + } + $transaction = $DB->start_delegated_transaction(); + $quba = $user->get_question_usage(); + $qubaslot = $quba->add_question(question_bank::make_question($question)); + $quba->start_question($qubaslot); + question_engine::save_questions_usage_by_activity($quba); + $attempt = new capquiz_attempt(); + $attempt->set_many([ + 'slot' => $qubaslot, + 'capquizid' => $user->get('capquizid'), + 'capquizuserid' => $user->get('id'), + 'slotid' => $slot->get('id'), + ]); + $attempt->create(); + $transaction->allow_commit(); + return $attempt; + } + + /** + * Create a question. + * + * @param capquiz $capquiz + * @param int $questionid + * @param float $rating + * @return capquiz_slot + */ + public static function create_slot(capquiz $capquiz, int $questionid, float $rating): capquiz_slot { + global $DB; + $transaction = $DB->start_delegated_transaction(); + $context = \context_module::instance($capquiz->get_cmid()); + $questionbankentry = get_question_bank_entry($questionid); + $slot = new capquiz_slot(); + $slot->set_many([ + 'capquizid' => $capquiz->get('id'), + 'rating' => $rating, + ]); + $slot->create(); + self::create_question_rating($slot, $slot->get('rating'), false); + $DB->insert_record('question_references', (object)[ + 'usingcontextid' => $context->id, + 'component' => 'mod_capquiz', + 'questionarea' => 'slot', + 'itemid' => $slot->get('id'), + 'questionbankentryid' => $questionbankentry->id, + 'version' => null, // Always latest. + ]); + $transaction->allow_commit(); + return $slot; + } + + /** + * Create new question rating. + * + * @param capquiz_slot $slot + * @param float $rating + * @param bool $manual + */ + public static function create_question_rating(capquiz_slot $slot, float $rating, bool $manual): capquiz_question_rating { + $slot->set('rating', $rating); + $slot->save(); + $questionrating = new capquiz_question_rating(); + $questionrating->set_many([ + 'slotid' => $slot->get('id'), + 'rating' => $rating, + 'manual' => $manual, + ]); + return $questionrating->create(); + } + + /** + * Inserts a new user rating record to the database. + * + * @param capquiz_user $user + * @param float $rating + * @param bool $manual + */ + public static function create_user_rating(capquiz_user $user, float $rating, bool $manual): capquiz_user_rating { + $user->set('rating', $rating); + $user->save(); + $userrating = new capquiz_user_rating(); + $userrating->set_many([ + 'capquizuserid' => $user->get('id'), + 'rating' => $rating, + 'manual' => $manual, + ]); + return $userrating->create(); + } + + /** + * Mark the question attempt as reviewed by the user. + * + * @param capquiz_attempt $attempt + * @return bool false if attempt has already been reviewed, true otherwise + */ + public static function mark_attempt_as_reviewed(capquiz_attempt $attempt): bool { + if (!$attempt->get('answered') || $attempt->get('reviewed')) { + return false; + } + $attempt->set_many([ + 'reviewed' => 1, + 'timereviewed' => \core\di::get(\core\clock::class)->time(), + ]); + $attempt->save(); + return true; + } + + /** + * Submit question attempt. + * + * @param capquiz $capquiz + * @param capquiz_user $user + * @param capquiz_attempt $attempt + * @return bool false if attempt has already been answered, true otherwise + */ + public static function submit_attempt(capquiz $capquiz, capquiz_user $user, capquiz_attempt $attempt): bool { + global $DB; + if ($attempt->get('answered') && $attempt->get('reviewed')) { + return false; + } + $transaction = $DB->start_delegated_transaction(); + $quba = $user->get_question_usage(); + $quba->process_action($attempt->get('slot'), $quba->extract_responses($attempt->get('slot'))); + $quba->update_question_flags(); + + // Some question behaviours let users try again, so we need to return early before we finish the question. + // We don't want to touch any user or question ratings until the question attempt is actually finished. + // The reason we check for question_state::$complete is due to the adaptive mode question behaviour. + // It seems to expect some final action before finishing the attempt, but we'll just treat it as finished. + $score = $quba->get_question_attempt($attempt->get('slot'))->get_fraction(); + $attemptstate = $attempt->get_state($quba); + if (!$attemptstate->is_finished() && $attemptstate !== \question_state::$complete) { + question_engine::save_questions_usage_by_activity($quba); + $transaction->allow_commit(); + return true; + } + + // At this point, we're certain that the question attempt is finished. We start by adjusting the user rating. + $quba->finish_question($attempt->get('slot')); + question_engine::save_questions_usage_by_activity($quba); + $userratingbeforeattempt = capquiz_user_rating::get_latest_by_user($user->get('id')); + $slot = new capquiz_slot($attempt->get('slotid')); + $newuserrating = elo::new_rating($capquiz->get('userkfactor'), (float)$score, $user->get('rating'), $slot->get('rating')); + $userratingafterattempt = self::create_user_rating($user, $newuserrating, false); + $attempt->set_many([ + 'answered' => 1, + 'timeanswered' => \core\di::get(\core\clock::class)->time(), + 'userprevratingid' => $userratingbeforeattempt->get('id'), + 'userratingid' => $userratingafterattempt->get('id'), + ]); + $attempt->save(); + for ($star = $capquiz->get_max_stars(); $star > 0; $star--) { + $requiredrating = stars::get_required_rating_for_star($capquiz->get('starratings'), $star); + if ($user->get('rating') >= $requiredrating && $user->get('higheststars') < $star) { + $user->set('higheststars', $star); + // Users may continue the quiz after the due time, but grades shouldn't be affected. + if (!$capquiz->is_past_due_time()) { + $user->set('starsgraded', $star); + capquiz_update_grades($capquiz->to_record(), $user->get('userid')); + } + $user->save(); + break; + } + } + $transaction->allow_commit(); + + // Don't update question ratings when an instructor attempts the quiz. + if (has_capability('mod/capquiz:instructor', \context_module::instance($capquiz->get_cmid()))) { + return true; + } + + // The question ratings are updated based on the user's previous attempt. + // If this is the user's first attempt, the question ratings remain the same. + $previousattempt = capquiz_attempt::get_record_previously_reviewed_by_user($user->get('id')); + if (empty($previousattempt)) { + return true; + } + + // Update question ratings. + $transaction = $DB->start_delegated_transaction(); + $previouscorrect = $previousattempt->get_state($quba)->is_correct(); + $currentcorrect = $attemptstate->is_correct(); + $previousslot = new capquiz_slot($previousattempt->get('slotid')); + $attempt->set_many([ + 'prevquestionprevratingid' => capquiz_question_rating::get_latest_by_slot($previousslot)->get('id'), + 'questionprevratingid' => capquiz_question_rating::get_latest_by_slot($slot)->get('id'), + ]); + if ($previouscorrect !== $currentcorrect) { + $hardslot = $previouscorrect ? $slot : $previousslot; + $easyslot = $previouscorrect ? $previousslot : $slot; + $easyrating = $easyslot->get('rating'); + $hardrating = $hardslot->get('rating'); + $questionkfactor = $capquiz->get('questionkfactor'); + self::create_question_rating($easyslot, elo::new_rating($questionkfactor, 0.0, $easyrating, $hardrating), false); + self::create_question_rating($hardslot, elo::new_rating($questionkfactor, 1.0, $hardrating, $easyrating), false); + } + $attempt->set_many([ + 'prevquestionratingid' => capquiz_question_rating::get_latest_by_slot($previousslot)->get('id'), + 'questionratingid' => capquiz_question_rating::get_latest_by_slot($slot)->get('id'), + ]); + $attempt->save(); + $transaction->allow_commit(); + return true; + } +} diff --git a/classes/bank/add_to_quiz_action.php b/classes/bank/add_to_quiz_action.php deleted file mode 100644 index 6cb9496..0000000 --- a/classes/bank/add_to_quiz_action.php +++ /dev/null @@ -1,46 +0,0 @@ -. - -namespace mod_capquiz\bank; - -use mod_capquiz\capquiz_urls; -use stdClass; - -/** - * Question bank action to add question to quiz. - * - * @package mod_capquiz - * @copyright 2024 NTNU - * @author 2024 Sebastian Gundersen - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -class add_to_quiz_action extends \core_question\local\bank\question_action_base { - /** - * Get the information required to display this action either as a menu item or a separate action column. - * - * @param stdClass $question the row from the $question table, augmented with extra information. - * @return array with three elements. - * $url - the URL to perform the action. - * $icon - the icon for this action. E.g. 't/delete'. - * $label - text label to display in the UI (either in the menu, or as a tool-tip on the icon) - */ - protected function get_url_icon_and_label(stdClass $question): array { - if (!question_has_capability_on($question, 'use')) { - return [null, null, null]; - } - return [capquiz_urls::add_question_to_list_url($question->id), 't/add', get_string('addtoquiz', 'quiz')]; - } -} diff --git a/classes/bank/question_bank_view.php b/classes/bank/question_bank_view.php deleted file mode 100755 index 8fc8212..0000000 --- a/classes/bank/question_bank_view.php +++ /dev/null @@ -1,67 +0,0 @@ -. - -/** - * This file defines a class represeting a question bank view. - * - * It is based on similar implementations from the Core Quiz, - * but intended to run in a pane rather than a modal overlay, - * some differences are needed. It includes legacy code from - * different versions of moodle, and should have been refactored. - * - * @package mod_capquiz - * @author Hans Georg Schaathun - * @copyright 2018/2022 NTNU - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - -namespace mod_capquiz\bank; - -use context; - -/** - * Class question_bank_view - * - * @package mod_capquiz - * @author Aleksander Skrede - * @copyright 2018 NTNU - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -class question_bank_view extends \core_question\local\bank\view { - - /** - * Specify the column heading - */ - protected function heading_column(): string { - return question_name_text_column::class; - } - - /** - * Display button to add selected questions to the quiz. - * - * @param context $catcontext - */ - protected function display_bottom_controls(context $catcontext): void { - echo '
'; - if (has_capability('moodle/question:useall', $catcontext)) { - echo ''; - } - echo '
'; - } - -} diff --git a/classes/bank/question_name_column.php b/classes/bank/question_name_column.php deleted file mode 100644 index ec92b48..0000000 --- a/classes/bank/question_name_column.php +++ /dev/null @@ -1,119 +0,0 @@ -. - -namespace mod_capquiz\bank; - -use stdClass; - -/** - * A column type for the name of the question name. - * - * This class is copied to CAPQuiz from the Core Quiz, without - * modification (as of Fri 9 Sep 08:29:48 UTC 2022 ). - * - * @package mod_capquiz - * @category question - * @copyright 2009 Tim Hunt - * @author 2021 Safat Shahin - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -class question_name_column extends \core_question\local\bank\column_base { - - /** - * @var null $checkboxespresent - */ - protected $checkboxespresent = null; - - /** - * Get the internal name for this column. Used as a CSS class name, - * and to store information about the current sort. Must match PARAM_ALPHA. - * - * @return string column name. - */ - public function get_name(): string { - return 'questionname'; - } - - /** - * Title for this column. Not used if is_sortable returns an array. - * - * @return string column title. - */ - public function get_title(): string { - return get_string('question'); - } - - /** - * Lable for this column. - * - * @param stdClass $question the row from the $question table, augmented with extra information. - * @return string column label. - */ - protected function label_for(stdClass $question): string { - if (is_null($this->checkboxespresent)) { - $this->checkboxespresent = $this->qbank->has_column('core_question\local\bank\checkbox_column'); - } - if ($this->checkboxespresent) { - return 'checkq' . $question->id; - } else { - return ''; - } - } - - /** - * Output the contents of this column. - * - * @param object $question the row from the $question table, augmented with extra information. - * @param string $rowclasses CSS class names that should be applied to this row of output. - */ - protected function display_content($question, $rowclasses): void { - $labelfor = $this->label_for($question); - if ($labelfor) { - echo \html_writer::start_tag('label', ['for' => $labelfor]); - } - echo format_string($question->name); - if ($labelfor) { - echo \html_writer::end_tag('label'); - } - } - - /** - * Use table alias 'q' for the question table, or one of the - * ones from get_extra_joins. Every field requested must specify a table prefix. - * - * @return array fields required. - */ - public function get_required_fields(): array { - return ['q.id', 'q.name']; - } - - /** - * Can this column be sorted on? You can return either: - * + false for no (the default), - * + a field name, if sorting this column corresponds to sorting on that datbase field. - * + an array of subnames to sort on as follows - * return [ - * 'firstname' => ['field' => 'uc.firstname', 'title' => get_string('firstname')], - * 'lastname' => ['field' => 'uc.lastname', 'title' => get_string('lastname')], - * ]; - * As well as field, and field, you can also add 'revers' => 1 if you want the default sort - * order to be DESC. - * @return mixed as above. - */ - public function is_sortable() { - return 'q.name'; - } -} diff --git a/classes/bank/question_name_text_column.php b/classes/bank/question_name_text_column.php deleted file mode 100644 index e2b1f01..0000000 --- a/classes/bank/question_name_text_column.php +++ /dev/null @@ -1,146 +0,0 @@ -. - -namespace mod_capquiz\bank; - -use core_tag_tag; -use html_writer; -use question_utils; -use stdClass; - -/** - * A column type for the name followed by the start of the question text. - * - * This class is copied to CAPQuiz from the Core Quiz, with the addition of - * the `quiz_question_tostring` method copied from Core Quiz' locallib. - * - * @package mod_capquiz - * @category question - * @copyright 2009 Tim Hunt - * @author 2022 Hans Georg Schaathun - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -class question_name_text_column extends question_name_column { - - /** - * Get the internal name for this column. Used as a CSS class name, - * and to store information about the current sort. Must match PARAM_ALPHA. - */ - public function get_name(): string { - return 'questionnametext'; - } - - /** - * Creates a textual representation of a question for display. - * - * @param stdClass $question A question object from the database questions table - * @param bool $showicon If true, show the question's icon with the question. False by default. - * @param bool $showquestiontext If true (default), show question text after question name. - * If false, show only question name. - * @param bool $showidnumber If true, show the question's idnumber, if any. False by default. - * @param bool|core_tag_tag[] $showtags if array passed, show those tags. Else, if true, get and show tags, - * else, don't show tags (which is the default). - */ - protected function quiz_question_tostring(stdClass $question, bool $showicon = false, bool $showquestiontext = true, - bool $showidnumber = false, array|bool $showtags = false): string { - global $OUTPUT; - $result = ''; - - // Question name. - $name = shorten_text(format_string($question->name), 200); - if ($showicon) { - $name .= print_question_icon($question) . ' ' . $name; - } - $result .= html_writer::span($name, 'questionname'); - - // Question idnumber. - if ($showidnumber && $question->idnumber !== null && $question->idnumber !== '') { - $result .= ' '; - $result .= html_writer::span( - html_writer::span(get_string('idnumber', 'question'), 'accesshide') - . ' ' . s($question->idnumber), 'badge badge-primary' - ); - } - - // Question tags. - if (is_array($showtags)) { - $tags = $showtags; - } else if ($showtags) { - $tags = core_tag_tag::get_item_tags('core_question', 'question', $question->id); - } else { - $tags = []; - } - if ($tags) { - $result .= $OUTPUT->tag_list($tags, null, 'd-inline', 0, null, true); - } - - // Question text. - if ($showquestiontext) { - $questiontext = question_utils::to_plain_text($question->questiontext, - $question->questiontextformat, ['noclean' => true, 'para' => false]); - $questiontext = shorten_text($questiontext, 50); - if ($questiontext) { - $result .= ' ' . html_writer::span(s($questiontext), 'questiontext'); - } - } - - return $result; - } - - /** - * Output the contents of this column. - * @param object $question the row from the $question table, augmented with extra information. - * @param string $rowclasses CSS class names that should be applied to this row of output. - */ - protected function display_content($question, $rowclasses): void { - echo html_writer::start_tag('div'); - $labelfor = $this->label_for($question); - if ($labelfor) { - echo html_writer::start_tag('label', ['for' => $labelfor]); - } - echo $this->quiz_question_tostring($question, false, true, true, $question->tags); - if ($labelfor) { - echo html_writer::end_tag('label'); - } - echo html_writer::end_tag('div'); - } - - /** - * Use table alias 'q' for the question table, or one of the - * ones from get_extra_joins. Every field requested must specify a table prefix. - */ - public function get_required_fields(): array { - $fields = parent::get_required_fields(); - $fields[] = 'q.questiontext'; - $fields[] = 'q.questiontextformat'; - $fields[] = 'qbe.idnumber'; - return $fields; - } - - /** - * If this column needs extra data (e.g. tags) then load that here. - * - * The extra data should be added to the question object in the array. - * Probably a good idea to check that another column has not already - * loaded the data you want. - * - * @param stdClass[] $questions the questions that will be displayed. - */ - public function load_additional_data(array $questions): void { - parent::load_additional_data($questions); - parent::load_question_tags($questions); - } -} diff --git a/classes/capquiz.php b/classes/capquiz.php index 23a849e..43a4ea0 100755 --- a/classes/capquiz.php +++ b/classes/capquiz.php @@ -14,278 +14,201 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . -/** - * This file defines the class capquiz representing a capquiz - * - * @package mod_capquiz - * @author Sebastian S. Gundersen - * @author Aleksander Skrede - * @copyright 2019 NTNU - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ +declare(strict_types=1); namespace mod_capquiz; -use context_module; -use moodle_page; -use renderer_base; -use stdClass; +use core\persistent; +use lang_string; +use mod_capquiz\local\helpers\stars; +use question_display_options; defined('MOODLE_INTERNAL') || die(); require_once($CFG->libdir . '/questionlib.php'); -require_once($CFG->dirroot . '/mod/capquiz/classes/capquiz_rating_system_loader.php'); -require_once($CFG->dirroot . '/mod/capquiz/classes/capquiz_matchmaking_strategy_loader.php'); /** - * Class capquiz + * CAPQuiz instance. * * @package mod_capquiz - * @author Sebastian S. Gundersen - * @author Aleksander Skrede - * @copyright 2019 NTNU + * @author Sebastian Gundersen + * @copyright 2024 Norwegian University of Science and Technology (NTNU) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class capquiz { - - /** @var context_module $context */ - private $context; - - /** @var stdClass $cm */ - private $cm; - - /** @var stdClass $courserecord */ - private $courserecord; - - /** @var stdClass $record */ - private $record; - - /** @var renderer_base $renderer */ - private renderer_base $renderer; - - /** @var ?capquiz_question_list $qlist */ - private ?capquiz_question_list $qlist; +class capquiz extends persistent { - /** @var moodle_page $page */ - private moodle_page $page; + /** @var string The table name. */ + const TABLE = 'capquiz'; /** - * Constructor. - * - * @param int $cmid - */ - public function __construct(int $cmid) { - global $DB, $PAGE; - $this->cm = get_coursemodule_from_id('capquiz', $cmid, 0, false, MUST_EXIST); - $this->context = context_module::instance($cmid); - $PAGE->set_context($this->context); - $this->renderer = $PAGE->get_renderer('mod_capquiz'); - $this->courserecord = $DB->get_record('course', ['id' => $this->cm->course], '*', MUST_EXIST); - $this->record = $DB->get_record('capquiz', ['id' => $this->cm->instance], '*', MUST_EXIST); - $this->qlist = capquiz_question_list::load_question_list($this); - $this->page = $PAGE; - } - - /** - * Updates the grades if the grading is completed or if forced - * - * @param bool $force + * Check if the CAPQuiz is open. */ - public function update_grades(bool $force = false): void { - if (!$this->is_grading_completed() || $force) { - capquiz_update_grades($this->record); - } + public function is_open(): bool { + $now = \core\di::get(\core\clock::class)->time(); + return $this->get('timeopen') > $now && $this->get('timedue') < $now; } /** - * Returns the page of the CapQuiz + * Returns true if the capquiz is completely graded. */ - public function get_page(): moodle_page { - return $this->page; + public function is_past_due_time(): bool { + $now = \core\di::get(\core\clock::class)->time(); + return $this->get('timedue') < $now && $this->get('timedue') > 0; } /** - * Returns the capquiz' id + * Returns the number of stars that can be achieved. */ - public function id(): int { - return $this->record->id; + public function get_max_stars(): int { + return stars::get_max_stars($this->get('starratings')); } /** - * Returns the capquiz' name - */ - public function name(): string { - return $this->record->name; - } - - /** - * Returns true if the capquiz is published - */ - public function is_published(): bool { - return $this->record->published; - } - - /** - * Returns true if the capquiz is completely graded - */ - public function is_grading_completed(): bool { - return $this->record->timedue < time() && $this->record->timedue > 0; - } - - /** - * Returns the amount of stars needed to pass - */ - public function stars_to_pass(): int { - return $this->record->stars_to_pass; - } - - /** - * Returns the time when the quiz is due - */ - public function time_due(): int { - return $this->record->timedue; - } - - /** - * Sets a new value for stars to pass + * The star ratings are internally stored as a CSV string. Use this helper to easily get them as a float array. * - * @param int $stars + * @return float[] */ - public function set_stars_to_pass(int $stars): void { - global $DB; - $this->record->stars_to_pass = $stars; - $DB->update_record('capquiz', $this->record); + public function get_star_ratings_array(): array { + return array_map('floatval', explode(',', $this->get('starratings'))); } /** - * Sets a new due time + * Get course module. * - * @param int $time + * @return \stdClass */ - public function set_time_due(int $time): void { - global $DB; - $this->record->timedue = $time; - $DB->update_record('capquiz', $this->record); + public function get_cm(): \stdClass { + return get_coursemodule_from_instance('capquiz', $this->get('id')); } /** - * Sets a new default rating + * Get course module id. * - * @param float $rating - */ - public function set_default_user_rating(float $rating): void { - global $DB; - $this->record->default_user_rating = $rating; - $DB->update_record('capquiz', $this->record); - } - - /** - * Publishes the capquiz if it can publish it + * @return int */ - public function publish(): bool { - global $DB; - if (!$this->can_publish()) { - return false; - } - $this->record->published = true; - $DB->update_record('capquiz', $this->record); - return $this->is_published(); + public function get_cmid(): int { + return (int)$this->get_cm()->id; } /** - * Returns true if the capquiz is publishable - */ - public function can_publish(): bool { - if (!$this->has_question_list() || $this->is_published()) { - return false; - } - return $this->question_list()->has_questions(); - } - - /** - * Returns a new question engine based on the user + * Custom validation for the starratings property. + * Check if each rating in the CSV string is a valid number, and that they're greater than the previous. * - * @param capquiz_user $user - */ - public function question_engine(capquiz_user $user): ?capquiz_question_engine { - $quba = $user->question_usage(); - if (!$quba) { - return null; + * @param string $ratings + * @return true|lang_string + */ + protected function validate_starratings(string $ratings): bool|lang_string { + $previous = 0.0; + foreach (explode(',', $ratings) as $rating) { + $previous = filter_var($rating, FILTER_VALIDATE_FLOAT, ['options' => ['min_range' => $previous + 1.0]]); + if (!$previous) { + return new lang_string('errorvalidatestarratings', 'capquiz'); + } } - $ratingsystemloader = new capquiz_rating_system_loader($this); - $strategyloader = new capquiz_matchmaking_strategy_loader($this); - return new capquiz_question_engine($this, $quba, $strategyloader, $ratingsystemloader); + return true; } /** - * Returns the capquiz user - */ - public function user(): ?capquiz_user { - global $USER; - return capquiz_user::load_user($this, $USER->id, $this->context()); - } - - /** - * Returns the default rating - */ - public function default_user_rating(): float { - return $this->record->default_user_rating; - } - - /** - * Returns true if the capquiz has a question list - */ - public function has_question_list(): bool { - return $this->qlist !== null; - } - - /** - * Returns the quiz' question list - */ - public function question_list(): ?capquiz_question_list { - return $this->qlist; - } - - /** - * Returns the current context - */ - public function context(): context_module { - return $this->context; - } - - /** - * Returns teh current course module - */ - public function course_module(): stdClass { - return $this->cm; - } - - /** - * Returns the current course + * Custom validation for the questiondisplayoptions property. + * Check if each property has a valid name and value for {@see question_display_options}. + * + * @param string $options + * @return bool|lang_string */ - public function course(): stdClass { - return $this->courserecord; - } + protected function validate_questiondisplayoptions(string $options): bool|lang_string { - /** - * Returns the current renderer - */ - public function renderer(): renderer_base { - return $this->renderer; + return true; } /** - * Validates the matchmaking and rating systems - */ - public function validate_matchmaking_and_rating_systems(): void { - $ratingsystemloader = new capquiz_rating_system_loader($this); - if (!$ratingsystemloader->has_rating_system()) { - $ratingsystemloader->set_default_rating_system(); - } - $strategyloader = new capquiz_matchmaking_strategy_loader($this); - if (!$strategyloader->has_strategy()) { - $strategyloader->set_default_strategy(); - } + * Return the definition of the properties of this model. + * + * @return array + */ + protected static function define_properties(): array { + return [ + 'course' => [ + 'type' => PARAM_INT, + 'default' => 0, + 'null' => NULL_NOT_ALLOWED, + ], + 'name' => [ + 'type' => PARAM_TEXT, + 'null' => NULL_NOT_ALLOWED, + ], + 'intro' => [ + 'type' => PARAM_TEXT, + 'null' => NULL_ALLOWED, + ], + 'introformat' => [ + 'type' => PARAM_INT, + 'default' => 0, + 'null' => NULL_NOT_ALLOWED, + ], + 'defaultuserrating' => [ + 'type' => PARAM_FLOAT, + 'default' => 1200.0, + 'null' => NULL_NOT_ALLOWED, + ], + 'defaultquestionrating' => [ + 'type' => PARAM_FLOAT, + 'default' => 600.0, + 'null' => NULL_NOT_ALLOWED, + ], + 'starratings' => [ + 'type' => PARAM_TEXT, + 'default' => '1300,1450,1600,1800,2000', + 'null' => NULL_NOT_ALLOWED, + ], + 'starstopass' => [ + 'type' => PARAM_INT, + 'default' => 3, + 'null' => NULL_NOT_ALLOWED, + ], + 'timeopen' => [ + 'type' => PARAM_INT, + 'default' => 0, + 'null' => NULL_NOT_ALLOWED, + ], + 'timedue' => [ + 'type' => PARAM_INT, + 'default' => 0, + 'null' => NULL_NOT_ALLOWED, + ], + 'numquestioncandidates' => [ + 'type' => PARAM_INT, + 'default' => 10, + 'null' => NULL_NOT_ALLOWED, + ], + 'minquestionsuntilreappearance' => [ + 'type' => PARAM_INT, + 'default' => 0, + 'null' => NULL_NOT_ALLOWED, + ], + 'userwinprobability' => [ + 'type' => PARAM_FLOAT, + 'default' => 0.75, + 'null' => NULL_NOT_ALLOWED, + ], + 'userkfactor' => [ + 'type' => PARAM_FLOAT, + 'default' => 32.0, + 'null' => NULL_NOT_ALLOWED, + ], + 'questionkfactor' => [ + 'type' => PARAM_FLOAT, + 'default' => 8.0, + 'null' => NULL_NOT_ALLOWED, + ], + 'questionbehaviour' => [ + 'type' => PARAM_TEXT, + 'default' => 'immediatefeedback', + 'null' => NULL_NOT_ALLOWED, + ], + 'questiondisplayoptions' => [ + 'type' => PARAM_TEXT, + 'null' => NULL_NOT_ALLOWED, + ], + ]; } - } diff --git a/classes/capquiz_action_performer.php b/classes/capquiz_action_performer.php deleted file mode 100755 index 8e5e304..0000000 --- a/classes/capquiz_action_performer.php +++ /dev/null @@ -1,264 +0,0 @@ -. - -/** - * This file defines a class used to perform different actions on the capquiz - * - * @package mod_capquiz - * @author Aleksander Skrede - * @author Sebastian S. Gundersen - * @copyright 2019 NTNU - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - -namespace mod_capquiz; - -use stdClass; - -defined('MOODLE_INTERNAL') || die(); - -require_once($CFG->libdir . '/questionlib.php'); -require_once($CFG->dirroot . '/mod/capquiz/classes/capquiz_rating_system_loader.php'); -require_once($CFG->dirroot . '/mod/capquiz/classes/capquiz_matchmaking_strategy_loader.php'); - -/** - * Class capquiz_action_performer - * - * @package mod_capquiz - * @author Aleksander Skrede - * @author Sebastian S. Gundersen - * @copyright 2019 NTNU - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -class capquiz_action_performer { - - /** - * Performs an action on a capquiz - * - * @param string $action The action to perform - * @param capquiz $capquiz The capquiz on which the action will be performed - */ - public static function perform(string $action, capquiz $capquiz): void { - switch ($action) { - case 'redirect': - self::redirect(); - break; - case 'set-question-list': - self::assign_question_list($capquiz); - break; - case 'add-question': - self::add_question_to_list($capquiz); - break; - case 'remove-question': - self::remove_question_from_list($capquiz); - break; - case 'publish-question-list': - self::publish_capquiz($capquiz); - break; - case 'set-question-rating': - self::set_question_rating($capquiz); - break; - case 'set-default-question-rating': - self::set_default_question_rating($capquiz); - break; - case 'create-question-list-template': - self::create_question_list_template($capquiz); - break; - case 'merge_qlist': - self::merge_question_list($capquiz); - break; - case 'delete_qlist': - self::delete_question_list(); - break; - case 'regrade-all': - $capquiz->update_grades(true); - capquiz_urls::redirect_to_url(capquiz_urls::view_classlist_url()); - break; - default: - break; - } - } - - /** - * Redirects the user to another page - */ - public static function redirect(): void { - $url = optional_param('target-url', null, PARAM_TEXT); - if ($url) { - capquiz_urls::redirect_to_url(new \moodle_url($url)); - } - } - - /** - * Assigns a question list to the capquiz - * - * @param capquiz $capquiz - */ - public static function assign_question_list(capquiz $capquiz): void { - $qlistid = optional_param('question-list-id', 0, PARAM_INT); - $qlist = capquiz_question_list::load_any($qlistid); - if ($qlist) { - $capquiz->validate_matchmaking_and_rating_systems(); - $qlist->create_instance_copy($capquiz); - } - } - - /** - * Adds questions from the capquiz' question to the capquiz - * - * @param capquiz $capquiz - */ - public static function add_question_to_list(capquiz $capquiz): void { - $qlist = $capquiz->question_list(); - $questionids = required_param('question-id', PARAM_TEXT); - $questionids = explode(',', $questionids); - foreach ($questionids as $questionid) { - self::create_capquiz_question((int)$questionid, $qlist, $qlist->default_question_rating()); - } - capquiz_urls::redirect_to_previous(); - } - - /** - * Removes a question - * - * @param capquiz $capquiz - */ - public static function remove_question_from_list(capquiz $capquiz): void { - $questionid = optional_param('question-id', 0, PARAM_INT); - if ($questionid && $capquiz->has_question_list()) { - self::remove_capquiz_question($questionid, $capquiz->question_list()->id()); - } - capquiz_urls::redirect_to_previous(); - } - - /** - * Publishes the capquiz - * - * @param capquiz $capquiz - */ - public static function publish_capquiz(capquiz $capquiz): void { - $capquiz->publish(); - } - - /** - * Sets a new question rating on a question - * - * @param capquiz $capquiz - */ - public static function set_question_rating(capquiz $capquiz): void { - $questionid = required_param('question-id', PARAM_INT); - $question = $capquiz->question_list()->question($questionid); - if (!$question) { - throw new \Exception('The specified question does not exist'); - } - $rating = optional_param('rating', null, PARAM_FLOAT); - if ($rating !== null) { - $question->set_rating($rating, true); - } - capquiz_urls::redirect_to_url(capquiz_urls::view_question_list_url()); - } - - /** - * Sets a new default question rating - * - * @param capquiz $capquiz - */ - public static function set_default_question_rating(capquiz $capquiz): void { - $rating = optional_param('rating', null, PARAM_FLOAT); - if ($rating !== null) { - $capquiz->question_list()->set_default_question_rating($rating); - } - capquiz_urls::redirect_to_url(capquiz_urls::view_question_list_url()); - } - - /** - * Creates a template from the capquiz' current question list - * - * @param capquiz $capquiz - * @return capquiz_question_list - */ - public static function create_question_list_template(capquiz $capquiz): capquiz_question_list { - $qlist = $capquiz->question_list(); - if (!$qlist) { - throw new \Exception('Failed to find question list for this CAPQuiz.'); - } else if (!$qlist->has_questions()) { - throw new \Exception('Attempted to create template without questions.'); - } - $qlistcopy = $qlist->create_template_copy($capquiz); - if ($qlistcopy === null) { - throw new \Exception('Failed to create a template from this question list.'); - } - return $qlistcopy; - } - - /** - * Creates a new capquiz question and adds it to the database - * - * @param int $questionid - * @param capquiz_question_list $list - * @param float $rating - */ - private static function create_capquiz_question(int $questionid, capquiz_question_list $list, float $rating): void { - global $DB; - if ($questionid === 0) { - return; - } - $ratedquestion = new stdClass(); - $ratedquestion->question_list_id = $list->id(); - $ratedquestion->question_id = $questionid; - $ratedquestion->rating = $rating; - $capquizquestionid = $DB->insert_record('capquiz_question', $ratedquestion, true); - capquiz_question_rating::insert_question_rating_entry($capquizquestionid, $rating); - } - - /** - * Removes matching questions from the database - * - * @param int $questionid - * @param int $qlistid - */ - private static function remove_capquiz_question(int $questionid, int $qlistid): void { - global $DB; - $DB->delete_records('capquiz_question', ['id' => $questionid, 'question_list_id' => $qlistid]); - } - - /** - * Merges the capquiz' question list to its current context - * - * @param capquiz $capquiz - */ - private static function merge_question_list(capquiz $capquiz): void { - global $DB; - $srcqlistid = required_param('qlistid', PARAM_INT); - $srcqlistrecord = $DB->get_record('capquiz_question_list', ['id' => $srcqlistid]); - if ($srcqlistrecord) { - $capquiz->question_list()->merge(new capquiz_question_list($srcqlistrecord)); - } - capquiz_urls::redirect_to_url(capquiz_urls::view_question_list_url()); - } - - /** - * Deletes the question list and its questions from the database - */ - private static function delete_question_list(): void { - global $DB; - $srcqlistid = required_param('qlistid', PARAM_INT); - $DB->delete_records('capquiz_question', ['question_list_id' => $srcqlistid]); - $DB->delete_records('capquiz_question_list', ['id' => $srcqlistid]); - capquiz_urls::redirect_to_url(capquiz_urls::view_import_url()); - } - -} diff --git a/classes/capquiz_attempt.php b/classes/capquiz_attempt.php new file mode 100755 index 0000000..54db70e --- /dev/null +++ b/classes/capquiz_attempt.php @@ -0,0 +1,171 @@ +. + +declare(strict_types=1); + +namespace mod_capquiz; + +use core\persistent; + +/** + * Question attempt. + * + * @package mod_capquiz + * @author Sebastian Gundersen + * @copyright 2024 Norwegian University of Science and Technology (NTNU) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class capquiz_attempt extends persistent { + + /** @var string The table name. */ + const TABLE = 'capquiz_attempt'; + + /** + * Get the question state for this attempt. + * + * @param \question_usage_by_activity $quba + */ + public function get_state(\question_usage_by_activity $quba): \question_state { + return $quba->get_question_attempt($this->get('slot'))->get_state(); + } + + /** + * Get fraction for this attempt if finished, or null otherwise. + * + * @param \question_usage_by_activity $quba + */ + public function get_fraction(\question_usage_by_activity $quba): ?float { + $fraction = $quba->get_question_attempt($this->get('slot'))->get_fraction(); + return $fraction === null ? null : (float)$fraction; + } + + /** + * Get unreviewed attempt for a given user. + * + * @param int $capquizuserid + * @return ?capquiz_attempt + */ + public static function get_record_unreviewed_by_user(int $capquizuserid): ?self { + return self::get_record([ + 'capquizuserid' => $capquizuserid, + 'reviewed' => 0, + ]) ?: null; + } + + /** + * Get the previously reviewed attempt by a given user. + * + * @param int $capquizuserid + * @return capquiz_attempt|null + */ + public static function get_record_previously_reviewed_by_user(int $capquizuserid): ?self { + $records = self::get_records([ + 'capquizuserid' => $capquizuserid, + ], 'timereviewed', 'DESC', 0, 1); + return empty($records) ? null : reset($records); + } + + /** + * Get attempts reviewed for a given user. + * + * @param int $capquizuserid + * @param int $limit + * @return capquiz_attempt[] + */ + public static function get_records_reviewed_by_user(int $capquizuserid, int $limit): array { + return self::get_records([ + 'capquizuserid' => $capquizuserid, + 'answered' => 1, + 'reviewed' => 1, + ], 'timecreated', 'DESC', 0, $limit); + } + + /** + * Return the definition of the properties of this model. + * + * @return array + */ + protected static function define_properties(): array { + return [ + 'slot' => [ + 'type' => PARAM_INT, + 'null' => NULL_NOT_ALLOWED, + ], + 'capquizid' => [ + 'type' => PARAM_INT, + 'null' => NULL_NOT_ALLOWED, + ], + 'capquizuserid' => [ + 'type' => PARAM_INT, + 'null' => NULL_NOT_ALLOWED, + ], + 'slotid' => [ + 'type' => PARAM_INT, + 'null' => NULL_NOT_ALLOWED, + ], + 'reviewed' => [ + 'type' => PARAM_INT, + 'default' => 0, + 'null' => NULL_NOT_ALLOWED, + ], + 'answered' => [ + 'type' => PARAM_INT, + 'default' => 0, + 'null' => NULL_NOT_ALLOWED, + ], + 'timeanswered' => [ + 'type' => PARAM_INT, + 'default' => 0, + 'null' => NULL_NOT_ALLOWED, + ], + 'timereviewed' => [ + 'type' => PARAM_INT, + 'default' => 0, + 'null' => NULL_NOT_ALLOWED, + ], + 'questionratingid' => [ + 'type' => PARAM_INT, + 'default' => null, + 'null' => NULL_ALLOWED, + ], + 'questionprevratingid' => [ + 'type' => PARAM_INT, + 'default' => null, + 'null' => NULL_ALLOWED, + ], + 'prevquestionratingid' => [ + 'type' => PARAM_INT, + 'default' => null, + 'null' => NULL_ALLOWED, + ], + 'prevquestionprevratingid' => [ + 'type' => PARAM_INT, + 'default' => null, + 'null' => NULL_ALLOWED, + ], + 'userratingid' => [ + 'type' => PARAM_INT, + 'default' => null, + 'null' => NULL_ALLOWED, + ], + 'userprevratingid' => [ + 'type' => PARAM_INT, + 'default' => null, + 'null' => NULL_ALLOWED, + ], + ]; + } +} diff --git a/classes/capquiz_matchmaking_strategy.php b/classes/capquiz_matchmaking_strategy.php deleted file mode 100755 index 8736d33..0000000 --- a/classes/capquiz_matchmaking_strategy.php +++ /dev/null @@ -1,67 +0,0 @@ -. - -/** - * This file defines a class that represents a capquiz matchmaking strategy - * - * @package mod_capquiz - * @author Aleksander Skrede - * @copyright 2018 NTNU - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - -namespace mod_capquiz; - -use stdClass; - -/** - * Class capquiz_matchmaking_strategy - * - * @package mod_capquiz - * @author Aleksander Skrede - * @copyright 2018 NTNU - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -abstract class capquiz_matchmaking_strategy { - - /** - * Sets a new matchmaking strategy configuration - * - * @param stdClass $configuration - */ - abstract public function configure(stdClass $configuration): void; - - /** - * Returns the current configuration - */ - abstract public function configuration(): stdClass; - - /** - * Returns the default configuration - */ - abstract public function default_configuration(): stdClass; - - /** - * Returns a new question for the user based on the matchmaking strategy configuration - * - * @param capquiz_user $user - * @param capquiz_question_list $qlist - * @param array $inactiveattempts - */ - abstract public function next_question_for_user(capquiz_user $user, capquiz_question_list $qlist, - array $inactiveattempts): ?capquiz_question; - -} diff --git a/classes/capquiz_matchmaking_strategy_loader.php b/classes/capquiz_matchmaking_strategy_loader.php deleted file mode 100755 index ee1d8cf..0000000 --- a/classes/capquiz_matchmaking_strategy_loader.php +++ /dev/null @@ -1,227 +0,0 @@ -. - -/** - * This file defines a class used to load capquiz matchmaking strategies - * - * @package mod_capquiz - * @author Aleksander Skrede - * @copyright 2018 NTNU - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - -namespace mod_capquiz; - -use moodle_url; -use stdClass; - -defined('MOODLE_INTERNAL') || die(); - -require_once($CFG->dirroot . '/mod/capquiz/classes/matchmaking/capquiz_matchmaking_strategy_registry.php'); -require_once($CFG->dirroot . '/mod/capquiz/classes/matchmaking/chronologic/chronologic_selector.php'); -require_once($CFG->dirroot . '/mod/capquiz/classes/matchmaking/n_closest/n_closest_selector.php'); -require_once($CFG->dirroot . '/mod/capquiz/classes/matchmaking/n_closest/n_closest_configuration_form.php'); - -/** - * Class capquiz_matchmaking_strategy_loader - * - * @package mod_capquiz - * @author Aleksander Skrede - * @copyright 2018 NTNU - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -class capquiz_matchmaking_strategy_loader { - - /** @var capquiz $capquiz */ - private capquiz $capquiz; - - /** @var ?stdClass $record */ - private ?stdClass $record = null; - - /** @var capquiz_matchmaking_strategy_registry $registry */ - private capquiz_matchmaking_strategy_registry $registry; - - /** @var ?stdClass $configuration */ - private ?stdClass $configuration; - - /** - * Constructor. - * - * @param capquiz $capquiz - */ - public function __construct(capquiz $capquiz) { - $this->capquiz = $capquiz; - $this->registry = new capquiz_matchmaking_strategy_registry($capquiz); - $this->load_configuration(); - } - - /** - * Returns localized strategy name - * - * @param string $name - */ - public static function localized_strategy_name(string $name): string { - // TODO: This is a hack. The database records currently store the names, which makes localization hard. - return match ($name) { - 'N-closest' => get_string('n_closest', 'capquiz'), - 'Chronological' => get_string('chronological', 'capquiz'), - default => get_string('no_strategy_specified', 'capquiz'), - }; - } - - /** - * Returns the selected strategy - * - * @return ?capquiz_matchmaking_strategy - */ - public function selector(): ?capquiz_matchmaking_strategy { - if (!$this->record) { - return null; - } - $strategy = $this->registry->selector($this->record->strategy); - if ($this->configuration) { - $strategy->configure($this->configuration); - } - return $strategy; - } - - /** - * Returns configuration form for the current matchmaking strategy - * - * @param moodle_url $url - */ - public function configuration_form(moodle_url $url): mixed { - if ($this->record && $this->configuration) { - return $this->registry->configuration_form($this->record->strategy, $this->configuration, $url); - } - return null; - } - - /** - * Returns true if this instance has a strategy set - */ - public function has_strategy(): bool { - return $this->selector() !== null; - } - - /** - * Returns the name of the current strategy - */ - public function current_strategy_name(): string { - if ($this->record) { - return $this->record->strategy; - } - return get_string('no_strategy_specified', 'capquiz'); - } - - /** - * Configures teh current strategy - * - * @param stdClass $candidateconfig - */ - public function configure_current_strategy(stdClass $candidateconfig): void { - if (!$this->record) { - return; - } - $selector = $this->selector(); - $selector->configure($candidateconfig); - $config = $selector->configuration(); - $this->record->configuration = empty((array)$config) ? '' : $this->serialize($config); - $this->update_configuration($this->record); - } - - /** - * Sets the default strategy - */ - public function set_default_strategy(): void { - $this->set_strategy($this->registry->default_selection_strategy()); - } - - /** - * Sets strategy based on the strategy name - * - * @param string $strategy - */ - public function set_strategy(string $strategy): void { - $selector = $this->registry->selector($strategy); - $record = new stdClass; - $record->strategy = $strategy; - $record->capquiz_id = $this->capquiz->id(); - $defaultconfig = $selector->default_configuration(); - $record->configuration = empty((array)$defaultconfig) ? '' : $this->serialize($defaultconfig); - global $DB; - if ($this->record) { - $record->id = $this->record->id; - $this->update_configuration($record); - } else { - $DB->insert_record('capquiz_question_selection', $record); - $this->set_configuration($record); - } - } - - /** - * Loads the strategy configuration from the database - */ - private function load_configuration(): void { - global $DB; - $conditions = ['capquiz_id' => $this->capquiz->id()]; - $config = $DB->get_record('capquiz_question_selection', $conditions); - if ($config) { - $this->set_configuration($config); - } - } - - /** - * Updates the strategy configuration and updates the database record - * - * @param stdClass $config - */ - private function update_configuration(stdClass $config): void { - global $DB; - if ($DB->update_record('capquiz_question_selection', $config)) { - $this->set_configuration($config); - } - } - - /** - * Sets this configuration as a new configuration - * - * @param stdClass $record - */ - private function set_configuration(stdClass $record): void { - $this->record = $record; - $this->configuration = $this->deserialize($record->configuration) ?: null; - } - - /** - * Returns the current configuration as a JSON string - * - * @param stdClass $configuration - */ - private function serialize(stdClass $configuration): string { - return json_encode($configuration); - } - - /** - * Takes in JSON encoded configuration string and returns a decoded configuration - * - * @param string $configuration - */ - private function deserialize(string $configuration): mixed { - return json_decode($configuration, false); - } - -} diff --git a/classes/capquiz_question.php b/classes/capquiz_question.php deleted file mode 100755 index 39bb865..0000000 --- a/classes/capquiz_question.php +++ /dev/null @@ -1,184 +0,0 @@ -. - -/** - * This file defines a class representing a capquiz question - * - * @package mod_capquiz - * @author Aleksander Skrede - * @author Sebastian S. Gundersen - * @copyright 2018 NTNU - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - -namespace mod_capquiz; - -use stdClass; - -/** - * Class capquiz_question - * - * @package mod_capquiz - * @author Aleksander Skrede - * @author Sebastian S. Gundersen - * @copyright 2018 NTNU - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -class capquiz_question { - - /** @var stdClass $record */ - private stdClass $record; - - /** @var capquiz_question_rating $rating */ - private capquiz_question_rating $rating; - - /** - * Constructor. - * - * @param stdClass $record - */ - public function __construct(stdClass $record) { - global $DB; - $this->record = $record; - // TODO: This query should probably be done in question list. - $question = $DB->get_record('question', ['id' => $record->question_id]); - if ($question !== false) { - $this->record->name = $question->name; - $this->record->text = $question->questiontext; - } else { - $this->record->name = get_string('missing_question', 'capquiz'); - $this->record->text = $this->record->name; - } - $rating = capquiz_question_rating::latest_question_rating_by_question($record->id); - if ($rating === null) { - $this->rating = capquiz_question_rating::insert_question_rating_entry($this->id(), $this->rating()); - } else { - $this->rating = $rating; - } - } - - /** - * Loads a specific question from the database - * - * @param int $questionid - */ - public static function load(int $questionid): ?capquiz_question { - global $DB; - $record = $DB->get_record('capquiz_question', ['id' => $questionid]); - return empty($record) ? null : new capquiz_question($record); - } - - /** - * Returns this questions database entry - */ - public function entry(): stdClass { - return $this->record; - } - - /** - * Returns this questions database entry id - */ - public function id(): int { - return $this->record->id; - } - - /** - * Returns this question's question bank question id - */ - public function question_id(): int { - return $this->record->question_id; - } - - /** - * Returns the id of the question list this question is in - */ - public function question_list_id(): int { - return $this->record->question_list_id; - } - - /** - * Returns this questions rating - */ - public function rating(): float { - return $this->record->rating; - } - - /** - * Returns this questions capquiz question rating - * - * @return capquiz_question_rating - */ - public function get_capquiz_question_rating(): capquiz_question_rating { - return $this->rating; - } - - /** - * Sets this questions rating and capquiz question rating - * - * @param float $rating - * @param bool $manual - */ - public function set_rating(float $rating, bool $manual = false) { - global $DB; - $this->record->rating = $rating; - $DB->update_record('capquiz_question', $this->record); - $questionrating = capquiz_question_rating::create_question_rating($this, $rating, $manual); - $this->rating = $questionrating; - } - - /** - * Returns this questions name - */ - public function name(): string { - return $this->record->name; - } - - /** - * Returns this questions text - * - * @return string - */ - public function text(): string { - return $this->record->text; - } - - /** - * Returns the id of the course this question is in - * - * @return int - * @throws \dml_exception - */ - public function course_id(): int { - global $DB; - $sql = 'SELECT c.id AS id - FROM {capquiz_question} cq - JOIN {question} q ON q.id = cq.question_id - JOIN {question_versions} qv - ON q.id = qv.questionid - JOIN {question_bank_entries} qbe - ON qbe.id = qv.questionbankentryid - JOIN {question_categories} qc - ON qc.id = qbe.questioncategoryid - JOIN {context} ctx ON ctx.id = qc.contextid - LEFT JOIN {course_modules} cm ON cm.id = ctx.instanceid AND ctx.contextlevel = 70 - JOIN {course} c ON (ctx.contextlevel = 50 AND c.id = ctx.instanceid) - OR (ctx.contextlevel = 70 AND c.id = cm.course) - WHERE cq.id = :questionid'; - $course = $DB->get_record_sql($sql, ['questionid' => $this->id()]); - return $course ? $course->id : 0; - } - -} diff --git a/classes/capquiz_question_attempt.php b/classes/capquiz_question_attempt.php deleted file mode 100755 index 6db66f2..0000000 --- a/classes/capquiz_question_attempt.php +++ /dev/null @@ -1,317 +0,0 @@ -. - -/** - * This file defines a class represeting a capquiz question attempt - * - * @package mod_capquiz - * @author Aleksander Skrede - * @copyright 2018 NTNU - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - -namespace mod_capquiz; - -use question_bank; -use question_engine; -use question_state; -use question_usage_by_activity; -use stdClass; - -/** - * Class capquiz_question_attempt - * - * @package mod_capquiz - * @author Aleksander Skrede - * @copyright 2018 NTNU - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -class capquiz_question_attempt { - - /** @var stdClass $record */ - private stdClass $record; - - /** @var question_usage_by_activity $quba */ - private question_usage_by_activity $quba; - - /** - * Constructor. - * - * @param question_usage_by_activity $quba - * @param stdClass $record - */ - public function __construct(question_usage_by_activity $quba, stdClass $record) { - $this->record = $record; - $this->quba = $quba; - } - - /** - * Creates a new question attempt for a user - * - * @param capquiz_user $user - * @param capquiz_question $question - */ - public static function create_attempt(capquiz_user $user, capquiz_question $question): ?capquiz_question_attempt { - $quba = $user->question_usage(); - $questions = question_load_questions([$question->question_id()]); - $targetquestion = reset($questions); - if (!$targetquestion) { - return null; - } - $questiondefinition = question_bank::make_question($targetquestion); - $slot = $quba->add_question($questiondefinition); - $quba->start_question($slot); - question_engine::save_questions_usage_by_activity($quba); - return self::insert_attempt_entry($user, $question, $slot); - } - - /** - * Returns the users currently active attempt - * - * @param capquiz_user $user - */ - public static function active_attempt(capquiz_user $user): ?capquiz_question_attempt { - global $DB; - $entry = $DB->get_record('capquiz_attempt', ['user_id' => $user->id(), 'reviewed' => false]); - if (empty($entry)) { - return null; - } - return new capquiz_question_attempt($user->question_usage(), $entry); - } - - /** - * Loads a users attempt based on the user and attempt id - * - * @param capquiz_user $user - * @param int $attemptid - */ - public static function load_attempt(capquiz_user $user, int $attemptid): ?capquiz_question_attempt { - global $DB; - $entry = $DB->get_record('capquiz_attempt', ['id' => $attemptid, 'user_id' => $user->id()]); - if (empty($entry)) { - return null; - } - return new capquiz_question_attempt($user->question_usage(), $entry); - } - - /** - * Returns the users previous attempt - * - * @param capquiz_user $user - */ - public static function previous_attempt(capquiz_user $user): ?capquiz_question_attempt { - global $DB; - $sql = 'SELECT * - FROM {capquiz_attempt} - WHERE user_id = :userid - ORDER BY time_reviewed DESC - LIMIT 1'; - $attempt = $DB->get_record_sql($sql, ['userid' => $user->id()], MUST_EXIST); - return new capquiz_question_attempt($user->question_usage(), $attempt); - } - - /** - * Returns the users inactive attempts (Answered and reviewed) - * - * @param capquiz_user $user - * @return capquiz_question_attempt[] - */ - public static function inactive_attempts(capquiz_user $user): array { - global $DB; - $records = $DB->get_records('capquiz_attempt', [ - 'user_id' => $user->id(), - 'answered' => true, - 'reviewed' => true, - ]); - return array_map(function (stdClass $record) use ($user) { - return new capquiz_question_attempt($user->question_usage(), $record); - }, array_values($records)); - } - - /** - * Returns the attempts id - */ - public function id(): int { - return $this->record->id; - } - - /** - * Returns the id of the question - */ - public function question_id(): int { - return $this->record->question_id; - } - - /** - * Returns the slot of the question - */ - public function question_slot(): int { - return $this->record->slot; - } - - /** - * Returns true if the attempt has an answer - */ - public function is_answered(): bool { - return $this->record->answered; - } - - /** - * Returns true if the answer is correct - */ - public function is_correctly_answered(): bool { - if (!$this->is_answered()) { - return false; - } - $moodleattempt = $this->quba->get_question_attempt($this->question_slot()); - return $moodleattempt->get_state()->is_correct(); - } - - /** - * Returns the state of the question - */ - public function get_state(): question_state { - $moodleattempt = $this->quba->get_question_attempt($this->question_slot()); - return $moodleattempt->get_state(); - } - - /** - * Returns true if the attempt is reviewed - */ - public function is_reviewed(): bool { - return $this->record->reviewed; - } - - /** - * Returns true if the attempt is not reviewed - */ - public function is_pending(): bool { - return !$this->is_reviewed(); - } - - /** - * Checks if the question is valid - */ - public function is_question_valid(): bool { - global $DB; - $sql = 'SELECT cq.id - FROM {capquiz_attempt} ca - JOIN {capquiz_question} cq - ON ca.question_id = cq.id - WHERE ca.id = :attemptid'; - $result = $DB->get_record_sql($sql, ['attemptid' => $this->id()]); - return $result !== false; - } - - /** - * Deletes attempt from database - */ - public function delete(): void { - global $DB; - $DB->delete_records('capquiz_attempt', ['id' => $this->id()]); - } - - /** - * Marks attempt as answered - */ - public function mark_as_answered(): void { - global $DB; - $submitteddata = $this->quba->extract_responses($this->question_slot()); - $this->quba->process_action($this->question_slot(), $submitteddata); - $this->record->answered = true; - $this->record->time_answered = time(); - $this->quba->finish_question($this->question_slot(), time()); - question_engine::save_questions_usage_by_activity($this->quba); - $DB->update_record('capquiz_attempt', $this->record); - } - - /** - * Marks attempt as viewed - */ - public function mark_as_reviewed(): void { - global $DB; - $this->record->reviewed = true; - $this->record->time_reviewed = time(); - $DB->update_record('capquiz_attempt', $this->record); - } - - /** - * Sets current question rating - * - * @param capquiz_question_rating $rating - * @param bool $previous - */ - public function set_question_rating(capquiz_question_rating $rating, bool $previous = false): void { - global $DB; - if ($previous) { - $this->record->question_prev_rating_id = $rating->id(); - } else { - $this->record->question_rating_id = $rating->id(); - } - $DB->update_record('capquiz_attempt', $this->record); - } - - /** - * Sets rating for the previous rating - * - * @param capquiz_question_rating $rating - * @param bool $previous - */ - public function set_previous_question_rating(capquiz_question_rating $rating, bool $previous = false): void { - global $DB; - if ($previous) { - $this->record->prev_question_prev_rating_id = $rating->id(); - } else { - $this->record->prev_question_rating_id = $rating->id(); - } - $DB->update_record('capquiz_attempt', $this->record); - } - - /** - * Sets the user rating of the attempt - * - * @param capquiz_user_rating $rating - * @param bool $previous - */ - public function set_user_rating(capquiz_user_rating $rating, bool $previous = false): void { - global $DB; - if ($previous) { - $this->record->user_prev_rating_id = $rating->id(); - } else { - $this->record->user_rating_id = $rating->id(); - } - $DB->update_record('capquiz_attempt', $this->record); - } - - /** - * Inserts an attempt into the database - * - * @param capquiz_user $user - * @param capquiz_question $question - * @param int $slot - */ - private static function insert_attempt_entry(capquiz_user $user, capquiz_question $question, - int $slot): ?capquiz_question_attempt { - global $DB; - $record = new stdClass(); - $record->slot = $slot; - $record->user_id = $user->id(); - $record->question_id = $question->id(); - $DB->insert_record('capquiz_attempt', $record); - return self::active_attempt($user); - } - -} diff --git a/classes/capquiz_question_engine.php b/classes/capquiz_question_engine.php deleted file mode 100755 index bbbbe91..0000000 --- a/classes/capquiz_question_engine.php +++ /dev/null @@ -1,228 +0,0 @@ -. - -/** - * This file defines a class represeting a capquiz question engine - * - * @package mod_capquiz - * @author Aleksander Skrede - * @copyright 2018 NTNU - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - -namespace mod_capquiz; - -use question_usage_by_activity; - -/** - * Class capquiz_question_engine - * - * @package mod_capquiz - * @author Aleksander Skrede - * @copyright 2018 NTNU - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -class capquiz_question_engine { - - /** @var capquiz $capquiz */ - private capquiz $capquiz; - - /** @var question_usage_by_activity $quba */ - private question_usage_by_activity $quba; - - /** @var capquiz_matchmaking_strategy_loader $matchmakingloader */ - private capquiz_matchmaking_strategy_loader $matchmakingloader; - - /** @var capquiz_rating_system_loader $ratingsystemloader */ - private capquiz_rating_system_loader $ratingsystemloader; - - /** - * Constructor. - * - * @param capquiz $capquiz - * @param question_usage_by_activity $quba - * @param capquiz_matchmaking_strategy_loader $strategyloader - * @param capquiz_rating_system_loader $ratingsystemloader - */ - public function __construct(capquiz $capquiz, - question_usage_by_activity $quba, - capquiz_matchmaking_strategy_loader $strategyloader, - capquiz_rating_system_loader $ratingsystemloader) { - $this->capquiz = $capquiz; - $this->quba = $quba; - $this->matchmakingloader = $strategyloader; - $this->ratingsystemloader = $ratingsystemloader; - } - - /** - * Checks if the user has finished their attempt - * - * @param capquiz_user $user - */ - public function user_is_completed(capquiz_user $user): bool { - if (capquiz_question_attempt::active_attempt($user)) { - return false; - } - if ($this->find_question_for_user($user)) { - return false; - } - return true; - } - - /** - * Gets an attempt for the user, returns a new one if there are no active attempts - * - * @param capquiz_user $user - */ - public function attempt_for_user(capquiz_user $user): ?capquiz_question_attempt { - $attempt = capquiz_question_attempt::active_attempt($user); - return $attempt !== null ? $attempt : $this->new_attempt_for_user($user); - } - - /** - * Calls attempt_for_user with the user parameter as the current user - */ - public function attempt_for_current_user(): ?capquiz_question_attempt { - return $this->attempt_for_user($this->capquiz->user()); - } - - /** - * Deletes attempt if it is invalid - * - * @param capquiz_user $user - */ - public function delete_invalid_attempt(capquiz_user $user): void { - $attempt = $this->attempt_for_user($user); - if ($attempt !== null && !$attempt->is_question_valid()) { - $attempt->delete(); - } - } - - /** - * Handles answer - * - * @param capquiz_user $user - * @param capquiz_question_attempt $attempt - */ - public function attempt_answered(capquiz_user $user, capquiz_question_attempt $attempt): void { - if (!$attempt->is_question_valid()) { - return; - } - $ratingsystem = $this->ratingsystemloader->rating_system(); - $attempt->mark_as_answered(); - $attempt->set_user_rating($user->get_capquiz_user_rating(), true); - $question = $this->capquiz->question_list()->question($attempt->question_id()); - if ($attempt->is_correctly_answered()) { - $ratingsystem->update_user_rating($user, $question, 1); - $this->set_new_highest_star_if_attained($user); - } else { - $ratingsystem->update_user_rating($user, $question, 0); - } - $attempt->set_user_rating($user->get_capquiz_user_rating()); - $previousattempt = capquiz_question_attempt::previous_attempt($user); - if ($previousattempt) { - $this->update_question_rating($previousattempt, $attempt); - } - } - - /** - * Sets a new "highest star" score if the new score is the highest score yet - * - * @param capquiz_user $user - */ - private function set_new_highest_star_if_attained(capquiz_user $user): void { - $qlist = $this->capquiz->question_list(); - for ($star = $qlist->max_stars(); $star > 0; $star--) { - $required = $qlist->star_rating($star); - if ($user->rating() >= $required && $user->highest_stars_achieved() < $star) { - $user->set_highest_star($star); - break; - } - } - } - - /** - * Marks attempt as reviewed - * - * @param capquiz_question_attempt $attempt - */ - public function attempt_reviewed(capquiz_question_attempt $attempt): void { - $attempt->mark_as_reviewed(); - } - - /** - * Creates a new attempt for the user - * - * @param capquiz_user $user - */ - private function new_attempt_for_user(capquiz_user $user): ?capquiz_question_attempt { - $question = $this->find_question_for_user($user); - if ($question === null) { - return null; - } - return capquiz_question_attempt::create_attempt($user, $question); - } - - /** - * Finds a new question for the user - * - * @param capquiz_user $user - */ - private function find_question_for_user(capquiz_user $user): ?capquiz_question { - $selector = $this->matchmakingloader->selector(); - if ($selector === null) { - return null; - } - $questionlist = $this->capquiz->question_list(); - $inactiveattempts = capquiz_question_attempt::inactive_attempts($user); - return $selector->next_question_for_user($user, $questionlist, $inactiveattempts); - } - - /** - * Updates the question ratings - * - * @param capquiz_question_attempt $previous - * @param capquiz_question_attempt $current - */ - private function update_question_rating(capquiz_question_attempt $previous, capquiz_question_attempt $current): void { - $ratingsystem = $this->ratingsystemloader->rating_system(); - $currentcorrect = $current->is_correctly_answered(); - $previouscorrect = $previous->is_correctly_answered(); - $qlist = $this->capquiz->question_list(); - $currentquestion = $qlist->question($current->question_id()); - if (!$currentquestion) { - return; - } - $previousquestion = $qlist->question($previous->question_id()); - if (!$previousquestion) { - return; - } - $current->set_previous_question_rating($previousquestion->get_capquiz_question_rating(), true); - $current->set_question_rating($currentquestion->get_capquiz_question_rating(), true); - if ($previouscorrect && !$currentcorrect) { - $ratingsystem->question_victory_ratings($currentquestion, $previousquestion); - } else if (!$previouscorrect && $currentcorrect) { - $ratingsystem->question_victory_ratings($previousquestion, $currentquestion); - } else { - $previousquestion->set_rating($previousquestion->rating()); - $currentquestion->set_rating($currentquestion->rating()); - } - - $current->set_previous_question_rating($previousquestion->get_capquiz_question_rating()); - $current->set_question_rating($currentquestion->get_capquiz_question_rating()); - } - -} diff --git a/classes/capquiz_question_list.php b/classes/capquiz_question_list.php deleted file mode 100755 index 20e2d43..0000000 --- a/classes/capquiz_question_list.php +++ /dev/null @@ -1,410 +0,0 @@ -. - -/** - * This file defines the class capquiz_question_list - * - * @package mod_capquiz - * @author Aleksander Skrede - * @author Sebastian S. Gundersen - * @copyright 2019 NTNU - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - -namespace mod_capquiz; - -use context_course; -use stdClass; - -/** - * Class capquiz_question_list - * - * @package mod_capquiz - * @author Aleksander Skrede - * @author Sebastian S. Gundersen - * @copyright 2019 NTNU - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -class capquiz_question_list { - - /** @var stdClass $record */ - private stdClass $record; - - /** @var capquiz_question[] $questions */ - private array $questions; - - /** - * Constructor. - * - * @param stdClass $record - */ - public function __construct(stdClass $record) { - global $DB; - $this->record = $record; - $records = $DB->get_records('capquiz_question', ['question_list_id' => $this->record->id]); - $this->questions = array_map(fn(stdClass $question) => new capquiz_question($question), array_values($records)); - } - - /** - * Returns array of ratings - * - * @return int[] - */ - public function star_ratings_array(): array { - $ratings = explode(',', $this->record->star_ratings); - return array_map(fn(string $rating) => (float)$rating, $ratings); - } - - /** - * Returns the count of all ratings (The value needed for a full star score) - */ - public function max_stars(): int { - return count($this->star_ratings_array()); - } - - /** - * Returns the value of the star rating $star - * - * @param int $star - */ - public function star_rating(int $star): float { - $stars = $this->star_ratings_array(); - return $stars[$star - 1]; - } - - /** - * Sets the star ratings to new values and updates database - * - * @param int[] $ratings - */ - public function set_star_ratings(array $ratings): void { - global $DB; - $starratings = implode(',', $ratings); - if (strlen($starratings) < 250) { - $this->record->star_ratings = $starratings; - $DB->update_record('capquiz_question_list', $this->record); - } - } - - /** - * Returns the completion level to the next rating as a percent value - * - * @param capquiz $capquiz - * @param float $rating - */ - public function next_level_percent(capquiz $capquiz, float $rating): int { - $goal = 0; - for ($star = 1; $star <= $this->max_stars(); $star++) { - $goal = $this->star_rating($star); - if ($goal > $rating) { - $previous = $star > 1 ? $this->star_rating($star - 1) : $capquiz->default_user_rating(); - $rating -= $previous; - $goal -= $previous; - break; - } - } - return $goal >= 1 ? (int)($rating / $goal * 100) : 0; - } - - /** - * Returns the id - */ - public function id(): int { - return $this->record->id; - } - - /** - * Returns the author user - */ - public function author(): ?stdClass { - global $DB; - return $DB->get_record('user', ['id' => $this->record->author]) ?: null; - } - - /** - * Returns true if the list contains questions - */ - public function has_questions(): bool { - return count($this->questions) > 0; - } - - /** - * Returns true if the question list is a template - */ - public function is_template(): bool { - return $this->record->is_template; - } - - /** - * Returns the default question rating - */ - public function default_question_rating(): float { - return $this->record->default_question_rating; - } - - /** - * Sets teh default question rating - * - * @param float $rating - */ - public function set_default_question_rating(float $rating): void { - global $DB; - $this->record->default_question_rating = $rating; - $DB->update_record('capquiz_question_list', $this->record); - } - - /** - * Returns the title of the question list - */ - public function title(): string { - return $this->record->title; - } - - /** - * Returns the description of the question list - */ - public function description(): string { - return $this->record->description; - } - - /** - * Returns the time of when the list was created - */ - public function time_created(): string { - return $this->record->time_created; - } - - /** - * Returns the last time when the list was modified - */ - public function time_modified(): string { - return $this->record->time_modified; - } - - /** - * Returns the amount of questions in the list - */ - public function question_count(): int { - return count($this->questions); - } - - /** - * Returns all the questions in the list in an array - * - * @return capquiz_question[] - */ - public function questions(): array { - return $this->questions; - } - - /** - * Returns the question with the id of $questionid - * - * @param int $questionid - */ - public function question(int $questionid): ?capquiz_question { - foreach ($this->questions as $question) { - if ($question->id() === $questionid) { - return $question; - } - } - return null; - } - - /** - * Checks if the list has the question with questionid $questionid - * - * @param int $questionid - * @return mixed|capquiz_question|null - */ - public function has_question(int $questionid): mixed { - foreach ($this->questions as $question) { - if ($question->question_id() === $questionid) { - return $question; - } - } - return null; - } - - /** - * The questions from $that will be imported to this question list. - * - * @param capquiz_question_list $that The question list to import questions from. - */ - public function merge(capquiz_question_list $that): void { - global $DB; - foreach ($that->questions as $question) { - if ($this->has_question($question->question_id()) === null) { - $newquestion = new stdClass(); - $newquestion->question_list_id = $this->id(); - $newquestion->question_id = $question->question_id(); - $newquestion->rating = $question->rating(); - $capquizquestionid = $DB->insert_record('capquiz_question', $newquestion, true); - capquiz_question_rating::insert_question_rating_entry($capquizquestionid, $newquestion->rating); - } - } - } - - /** - * Creates a copy of this instance - * - * @param capquiz $capquiz - */ - public function create_instance_copy(capquiz $capquiz): ?capquiz_question_list { - return $this->create_copy($capquiz, false); - } - - /** - * Updates database record - * - * @param int $capquizid - */ - public function convert_to_instance(int $capquizid): bool { - global $DB; - if ($this->id() || !$this->is_template()) { - return false; - } - $this->record->capquiz_id = $capquizid; - $this->record->is_template = 0; - $DB->update_record('capquiz_question_list', $this->record); - return true; - } - - /** - * Creates a copy of this instance as template - * - * @param capquiz $capquiz - */ - public function create_template_copy(capquiz $capquiz): ?capquiz_question_list { - return $this->create_copy($capquiz, true); - } - - /** - * Copies the questions in this list to database - * - * @param int $qlistid - */ - private function copy_questions_to_list(int $qlistid): void { - global $DB; - foreach ($this->questions() as $question) { - $record = $question->entry(); - $record->id = null; - $record->question_list_id = $qlistid; - $capquizquestionid = $DB->insert_record('capquiz_question', $record); - capquiz_question_rating::insert_question_rating_entry($capquizquestionid, $record->rating); - } - } - - /** - * Creates a copy of this instance and inserts the new copy into the database - * - * @param capquiz $capquiz - * @param bool $template - * @return ?capquiz_question_list The new but identical (apart from identicators) question list instance - */ - private function create_copy(capquiz $capquiz, bool $template): ?capquiz_question_list { - global $DB; - $record = $this->record; - $record->id = null; - $record->capquiz_id = $template ? null : $capquiz->id(); - $record->context_id = context_course::instance($capquiz->course()->id)->id; - $record->is_template = $template; - $record->time_created = time(); - $record->time_modified = time(); - $transaction = $DB->start_delegated_transaction(); - try { - $newid = $DB->insert_record('capquiz_question_list', $record); - $this->copy_questions_to_list($newid); - $DB->commit_delegated_transaction($transaction); - $record->id = $newid; - return new capquiz_question_list($record); - } catch (\dml_exception $exception) { - $DB->rollback_delegated_transaction($transaction, $exception); - } - } - - /** - * Create new question list instance and insert it in database - * - * @param capquiz $capquiz - * @param string $title - * @param string $description - * @param array $ratings - */ - public static function create_new_instance(capquiz $capquiz, string $title, string $description, - array $ratings): ?capquiz_question_list { - global $DB, $USER; - if (count($ratings) < 5) { - return null; - } - $record = new stdClass(); - $record->capquiz_id = $capquiz->id(); - $record->title = $title; - $record->description = $description; - $record->star_ratings = implode(',', $ratings); - $record->author = $USER->id; - $record->is_template = 0; - $record->time_created = time(); - $record->time_modified = time(); - $record->context_id = context_course::instance($capquiz->course()->id)->id; - $qlistid = $DB->insert_record('capquiz_question_list', $record); - $qlist = self::load_any($qlistid); - if (!$qlist) { - return null; - } - $capquiz->validate_matchmaking_and_rating_systems(); - return $qlist; - } - - /** - * Loads question list from database based on the capquiz - * - * @param capquiz $capquiz - */ - public static function load_question_list(capquiz $capquiz): ?capquiz_question_list { - global $DB; - $record = $DB->get_record('capquiz_question_list', ['capquiz_id' => $capquiz->id()]); - return $record ? new capquiz_question_list($record) : null; - } - - /** - * Loads question list from database based on the question list id - * - * @param int $qlistid - */ - public static function load_any(int $qlistid): ?capquiz_question_list { - global $DB; - $record = $DB->get_record('capquiz_question_list', ['id' => $qlistid]); - return $record ? new capquiz_question_list($record) : null; - } - - /** - * Loads question list templates - * - * @return capquiz_question_list[] - * @throws \dml_exception - */ - public static function load_question_list_templates(): array { - global $DB; - $records = $DB->get_records('capquiz_question_list', ['is_template' => 1]); - $qlists = []; - foreach ($records as $record) { - $qlists[] = new capquiz_question_list($record); - } - return $qlists; - } - -} diff --git a/classes/capquiz_question_rating.php b/classes/capquiz_question_rating.php index 80d2bca..bcede5e 100755 --- a/classes/capquiz_question_rating.php +++ b/classes/capquiz_question_rating.php @@ -14,135 +14,57 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . -/** - * This file defines a class represeting a capquiz question rating - * - * @package mod_capquiz - * @author André Storhaug - * @copyright 2019 NTNU - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ +declare(strict_types=1); namespace mod_capquiz; -use dml_exception; -use stdClass; +use core\persistent; /** * Class capquiz_question_rating * * @package mod_capquiz - * @author André Storhaug - * @copyright 2019 NTNU + * @author Sebastian Gundersen + * @copyright 2024 Norwegian University of Science and Technology (NTNU) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class capquiz_question_rating { - - /** @var stdClass $record */ - private stdClass $record; - - /** - * Constructor. - * - * @param stdClass $record - */ - public function __construct(stdClass $record) { - $this->record = $record; - } - - /** - * Loads a question rating from the database with a matching id - * - * @param int $questionratingid - */ - public static function load_question_rating(int $questionratingid): ?capquiz_question_rating { - global $DB; - $record = $DB->get_record('capquiz_question_rating', ['id' => $questionratingid]); - return empty($record) ? null : new capquiz_question_rating($record); - } - - /** - * Creates a new question rating and inserts it to the database - * - * @param capquiz_question $question - * @param float $rating - * @param bool $manual - */ - public static function create_question_rating(capquiz_question $question, float $rating, - bool $manual = false): capquiz_question_rating { - return self::insert_question_rating_entry($question->id(), $rating, $manual); - } +class capquiz_question_rating extends persistent { - /** - * Insert new question rating to database - * - * @param int $questionid - * @param float $rating - * @param bool $manual - */ - public static function insert_question_rating_entry(int $questionid, float $rating, - bool $manual = false): capquiz_question_rating { - global $DB; - $record = new stdClass(); - $record->capquiz_question_id = $questionid; - $record->rating = $rating; - $record->manual = $manual; - $record->timecreated = time(); - $ratingid = $DB->insert_record('capquiz_question_rating', $record); - $record->id = $ratingid; - return new capquiz_question_rating($record); - } + /** @var string The table name. */ + const TABLE = 'capquiz_question_rating'; /** - * Load information about the latest question rating for an attempt from the database. + * Get the latest question rating for a given slot. * - * @param int $questionid + * @param capquiz_slot $slot */ - public static function latest_question_rating_by_question(int $questionid): ?capquiz_question_rating { + public static function get_latest_by_slot(capquiz_slot $slot): ?capquiz_question_rating { global $DB; - $sql = "SELECT cqr.* - FROM {capquiz_question_rating} cqr - JOIN {capquiz_question} cq ON cq.id = cqr.capquiz_question_id - WHERE cqr.id = ( - SELECT MAX(cqr2.id) - FROM {capquiz_question_rating} cqr2 - JOIN {capquiz_question} cq2 ON cq2.id = cqr2.capquiz_question_id - WHERE cq2.id = cq.id - ) - AND cq.id = :question_id"; - $record = $DB->get_record_sql($sql, ['question_id' => $questionid]); - return empty($record) ? null : new capquiz_question_rating($record); - } - - /** - * Returns this question ratings id - */ - public function id(): int { - return $this->record->id; - } - - /** - * Returns the time of when the question rating was created - */ - public function timecreated(): string { - return $this->record->timecreated; + $records = $DB->get_records(self::TABLE, ['slotid' => $slot->get('id')], 'timecreated DESC', '*', 0, 1); + return empty($records) ? null : new self(0, reset($records)); } /** - * Returns the question rating - */ - public function rating(): float { - return $this->record->rating; - } - - /** - * Sets the question rating + * Return the definition of the properties of this model. * - * @param float $rating + * @return array */ - public function set_rating(float $rating): void { - global $DB; - $this->record->rating = $rating; - $DB->update_record('capquiz_question_rating', $this->record); + protected static function define_properties(): array { + return [ + 'slotid' => [ + 'type' => PARAM_INT, + 'null' => NULL_NOT_ALLOWED, + ], + 'rating' => [ + 'type' => PARAM_FLOAT, + 'default' => 0.0, + 'null' => NULL_NOT_ALLOWED, + ], + 'manual' => [ + 'type' => PARAM_BOOL, + 'default' => false, + 'null' => NULL_NOT_ALLOWED, + ], + ]; } } diff --git a/classes/capquiz_rating_system.php b/classes/capquiz_rating_system.php deleted file mode 100755 index baad384..0000000 --- a/classes/capquiz_rating_system.php +++ /dev/null @@ -1,75 +0,0 @@ -. - -/** - * This file defines an abstract capquiz rating system - * - * @package mod_capquiz - * @author Sebastian S. Gundersen - * @author Aleksander Skrede - * @copyright 2019 NTNU - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - -namespace mod_capquiz; - -use stdClass; - -/** - * Class capquiz_rating_system - * - * @package mod_capquiz - * @author Aleksander Skrede - * @copyright 2018 NTNU - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -abstract class capquiz_rating_system { - - /** - * Function to configure a rating system - * - * @param stdClass $config - */ - abstract public function configure(stdClass $config): void; - - /** - * Function to get the rating system configuration - */ - abstract public function configuration(): stdClass; - - /** - * Function to get the default rating system configuration - */ - abstract public function default_configuration(): stdClass; - - /** - * Updates the users rating based on the rating system and its configuration - * - * @param capquiz_user $user - * @param capquiz_question $question - * @param float $score - */ - abstract public function update_user_rating(capquiz_user $user, capquiz_question $question, float $score): void; - - /** - * Updates the winning and losing questions ratings - * - * @param capquiz_question $winner - * @param capquiz_question $loser - */ - abstract public function question_victory_ratings(capquiz_question $winner, capquiz_question $loser): void; - -} diff --git a/classes/capquiz_rating_system_loader.php b/classes/capquiz_rating_system_loader.php deleted file mode 100755 index 881fe94..0000000 --- a/classes/capquiz_rating_system_loader.php +++ /dev/null @@ -1,213 +0,0 @@ -. - -/** - * This file defines a class used to load capquiz rating systems - * - * @package mod_capquiz - * @author Aleksander Skrede - * @copyright 2019 NTNU - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - -namespace mod_capquiz; - -use moodle_url; -use stdClass; - -defined('MOODLE_INTERNAL') || die(); - -require_once($CFG->dirroot . '/mod/capquiz/classes/rating_system/capquiz_rating_system_registry.php'); -require_once($CFG->dirroot . '/mod/capquiz/classes/rating_system/elo_rating/elo_rating_system.php'); - -/** - * capquiz_rating_system_loader class - * - * @package mod_capquiz - * @author Aleksander Skrede - * @copyright 2018 NTNU - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -class capquiz_rating_system_loader { - - /** @var capquiz $capquiz */ - private capquiz $capquiz; - - /** @var ?stdClass $record */ - private ?stdClass $record = null; - - /** @var capquiz_rating_system_registry $registry */ - private capquiz_rating_system_registry $registry; - - /** @var ?stdClass $configuration */ - private ?stdClass $configuration; - - /** - * Constructor. - * - * @param capquiz $capquiz - */ - public function __construct(capquiz $capquiz) { - $this->capquiz = $capquiz; - $this->registry = new capquiz_rating_system_registry(); - $this->load_configuration(); - } - - /** - * Returns rating system - */ - public function rating_system(): ?capquiz_rating_system { - if (!$this->record) { - return null; - } - $system = $this->registry->rating_system($this->record->rating_system); - if ($this->configuration) { - $system->configure($this->configuration); - } - return $system; - } - - /** - * Checks if this instance has a rating system - * - * @return bool - */ - public function has_rating_system(): bool { - return $this->rating_system() !== null; - } - - /** - * Returns configuration form - * - * @param moodle_url $url - */ - public function configuration_form(moodle_url $url): mixed { - if ($this->record && $this->configuration) { - return $this->registry->configuration_form($this->record->rating_system, $this->configuration, $url); - } - return null; - } - - /** - * Returns the current rating systems name - * - * @return string rating system name - */ - public function current_rating_system_name(): string { - if ($this->record) { - return $this->record->rating_system; - } - return 'No rating system specified'; - } - - /** - * Configure the current rating system. - * - * @param stdClass $candidateconfig - */ - public function configure_current_rating_system(stdClass $candidateconfig): void { - if (!$this->record) { - return; - } - $system = $this->rating_system(); - $system->configure($candidateconfig); - $config = $system->configuration(); - $this->record->configuration = empty((array)$config) ? '' : $this->serialize($config); - $this->update_configuration($this->record); - } - - /** - * Set the default rating system. - */ - public function set_default_rating_system(): void { - $this->set_rating_system($this->registry->default_rating_system()); - } - - /** - * Set the rating system - * - * @param string $ratingsystem - */ - public function set_rating_system(string $ratingsystem): void { - global $DB; - $system = $this->registry->rating_system($ratingsystem); - $record = new stdClass; - $record->rating_system = $ratingsystem; - $record->capquiz_id = $this->capquiz->id(); - $defaultconfig = $system->default_configuration(); - $record->configuration = empty((array)$defaultconfig) ? '' : $this->serialize($defaultconfig); - if ($this->record) { - $record->id = $this->record->id; - $this->update_configuration($record); - } else { - $DB->insert_record('capquiz_rating_system', $record); - $this->set_configuration($record); - } - } - - /** - * Loads this instances configuration - */ - private function load_configuration(): void { - global $DB; - $configuration = $DB->get_record('capquiz_rating_system', ['capquiz_id' => $this->capquiz->id()]); - if ($configuration) { - $this->set_configuration($configuration); - } - } - - /** - * Updates this instances configuration as well as updates the database - * - * @param stdClass $configuration - */ - private function update_configuration(stdClass $configuration): void { - global $DB; - if ($DB->update_record('capquiz_rating_system', $configuration)) { - $this->set_configuration($configuration); - } - } - - /** - * Sets this instances configuration - * - * @param stdClass $record - */ - private function set_configuration(stdClass $record): void { - $this->record = $record; - $this->configuration = $this->deserialize($record->configuration); - } - - /** - * Serializes the input configuration object - * - * @param stdClass $configuration the configuration to be serialized - * @return string json string representing the input configuration - */ - private function serialize(stdClass $configuration): string { - return json_encode($configuration); - } - - /** - * Deserializes JSON formatted configuration string - * - * @param string $configuration The JSON string to be deserialized back into a configuration object - */ - private function deserialize(string $configuration): mixed { - return json_decode($configuration, false); - } - -} diff --git a/classes/capquiz_slot.php b/classes/capquiz_slot.php new file mode 100755 index 0000000..a629ed0 --- /dev/null +++ b/classes/capquiz_slot.php @@ -0,0 +1,220 @@ +. + +declare(strict_types=1); + +namespace mod_capquiz; + +use core\persistent; +use mod_capquiz\local\helpers\elo; +use stdClass; + +/** + * CAPQuiz slot. + * + * @package mod_capquiz + * @author Sebastian Gundersen + * @copyright 2024 Norwegian University of Science and Technology (NTNU) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class capquiz_slot extends persistent { + + /** @var string The table name. */ + const TABLE = 'capquiz_slot'; + + /** + * Returns the question_references record for this slot. + * + * @return ?stdClass question_references record + */ + public function find_question_reference(): ?stdClass { + global $DB; + return $DB->get_record('question_references', [ + 'component' => 'mod_capquiz', + 'questionarea' => 'slot', + 'itemid' => $this->get('id'), + ]) ?: null; + } + + /** + * Returns the question_bank_entries record for this slot. + * + * @return ?stdClass question_bank_entries record + */ + public function find_question_bank_entry(): ?stdClass { + global $DB; + return $DB->get_record_sql(" + SELECT qbe.* + FROM {question_references} qr + JOIN {question_bank_entries} qbe + ON qbe.id = qr.questionbankentryid + WHERE qr.component = 'mod_capquiz' + AND qr.questionarea = 'slot' + AND qr.itemid = :slotid", [ + 'slotid' => $this->get('id'), + ]) ?: null; + } + + /** + * Returns the currently used question_versions record for this slot. + * + * @return ?stdClass question_versions record + */ + public function find_question_version(): ?stdClass { + global $DB; + return $DB->get_record_sql(" + SELECT qv.*, + qr.version AS referencedversion + FROM {question_references} qr + JOIN {question_bank_entries} qbe + ON qbe.id = qr.questionbankentryid + JOIN {question_versions} qv + ON qv.questionbankentryid = qbe.id + AND qv.version = COALESCE( + qr.version, + (SELECT MAX(qv2.version) + FROM {question_versions} qv2 + WHERE qv2.questionbankentryid = qr.questionbankentryid) + ) + WHERE qr.component = 'mod_capquiz' + AND qr.questionarea = 'slot' + AND qr.itemid = :slotid", [ + 'slotid' => $this->get('id'), + ]) ?: null; + } + + /** + * Returns the currently used question record for this slot. + * + * @return ?stdClass question record + */ + public function find_question(): ?stdClass { + global $DB; + return $DB->get_record_sql(" + SELECT q.* + FROM {question_references} qr + JOIN {question_versions} qv + ON qv.questionbankentryid = qr.questionbankentryid + AND qv.version = COALESCE( + qr.version, + (SELECT MAX(qv2.version) + FROM {question_versions} qv2 + WHERE qv2.questionbankentryid = qr.questionbankentryid) + ) + JOIN {question} q + ON q.id = qv.questionid + WHERE qr.component = 'mod_capquiz' + AND qr.questionarea = 'slot' + AND qr.itemid = :slotid", [ + 'slotid' => $this->get('id'), + ]) ?: null; + } + + /** + * Returns the course record related to the question in this slot. + * + * @return ?stdClass course record + */ + public function find_related_course(): ?stdClass { + global $DB; + return $DB->get_record_sql(" + SELECT c.id AS id + FROM {question_references} qr + JOIN {question_bank_entries} qbe + ON qbe.id = qr.questionbankentryid + JOIN {question_categories} qc + ON qc.id = qbe.questioncategoryid + JOIN {context} ctx + ON ctx.id = qc.contextid + LEFT JOIN {course_modules} cm + ON cm.id = ctx.instanceid + AND ctx.contextlevel = 70 + JOIN {course} c + ON (ctx.contextlevel = 50 AND c.id = ctx.instanceid) + OR (ctx.contextlevel = 70 AND c.id = cm.course) + WHERE qr.component = 'mod_capquiz' + AND qr.questionarea = 'slot' + AND qr.itemid = :slotid", [ + 'slotid' => $this->get('id'), + ]) ?: null; + } + + /** + * Finds the next range of slots suitable for a given user based on their rating. + * + * @param capquiz_user $user + * @return self[] + */ + private static function get_records_for_question_selection(capquiz_user $user): array { + global $DB; + $capquiz = new capquiz($user->get('capquizid')); + $minquestionsuntilreappearance = $capquiz->get('minquestionsuntilreappearance'); + $inactiveattempts = capquiz_attempt::get_records_reviewed_by_user($user->get('id'), $minquestionsuntilreappearance); + $inactiveattemptsiterator = new \ArrayIterator(array_reverse($inactiveattempts, true)); + $excludedslotids = []; + for ($i = 0; $i < $minquestionsuntilreappearance; $i++) { + if (!$inactiveattemptsiterator->valid()) { + break; + } + /** @var capquiz_attempt $attempt */ + $attempt = $inactiveattemptsiterator->current(); + $excludedslotids[] = $attempt->get('slotid'); + $inactiveattemptsiterator->next(); + } + $excludedslotids = array_unique($excludedslotids); + $excludedslotidscsv = implode(',', $excludedslotids); + $slotrecords = $DB->get_records_sql(" + SELECT * + FROM {capquiz_slot} + WHERE capquizid = :capquizid + AND id NOT IN ($excludedslotidscsv) + ORDER BY ABS(:idealquestionrating - rating)", [ + 'capquizid' => $capquiz->get('id'), + 'idealquestionrating' => elo::ideal_question_rating($capquiz->get('userwinprobability'), $user->get('rating')), + ], 0, $capquiz->get('numquestioncandidates')); + return array_values(array_map(fn(stdClass $record) => new self(0, $record), $slotrecords)); + } + + /** + * Get the next suitable question for a given user based on their rating. + * + * @param capquiz_user $user + * @return ?self + */ + public static function get_record_for_next_question(capquiz_user $user): ?self { + $slots = self::get_records_for_question_selection($user); + return empty($slots) ? null : $slots[mt_rand(0, count($slots) - 1)]; + } + + /** + * Return the definition of the properties of this model. + * + * @return array + */ + protected static function define_properties(): array { + return [ + 'capquizid' => [ + 'type' => PARAM_INT, + 'null' => NULL_NOT_ALLOWED, + ], + 'rating' => [ + 'type' => PARAM_FLOAT, + 'default' => 0.0, + 'null' => NULL_NOT_ALLOWED, + ], + ]; + } +} diff --git a/classes/capquiz_urls.php b/classes/capquiz_urls.php deleted file mode 100755 index 85e100d..0000000 --- a/classes/capquiz_urls.php +++ /dev/null @@ -1,372 +0,0 @@ -. - -/** - * This file defines a class that represents a capquiz url - * - * @package mod_capquiz - * @author Aleksander Skrede - * @author Sebastian S. Gundersen - * @author André Storhaug - * @copyright 2019 NTNU - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - -namespace mod_capquiz; - -use moodle_url; - -defined('MOODLE_INTERNAL') || die(); - -require_once($CFG->dirroot . '/mod/capquiz/report/reportlib.php'); - -/** - * Class capquiz_urls - * - * @package mod_capquiz - * @author Aleksander Skrede - * @author Sebastian S. Gundersen - * @author André Storhaug - * @copyright 2019 NTNU - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -class capquiz_urls { - - /** @var string The URL to the entrypoint view for the capquiz */ - public static string $urlview = '/mod/capquiz/view.php'; - - /** @var string The URL to update the user attempts and return to the dashboard */ - public static string $urlasync = '/mod/capquiz/async.php'; - - /** @var string The URL to the error page */ - public static string $urlerror = '/mod/capquiz/error.php'; - - /** @var string The URL to the action page */ - public static string $urlaction = '/mod/capquiz/action.php'; - - /** @var string The URL to the classlist view */ - public static string $urlviewclasslist = '/mod/capquiz/view_classlist.php'; - - /** @var string The URL to the grading view */ - public static string $urlviewgrading = '/mod/capquiz/view_grading.php'; - - /** @var string The URL to the import view */ - public static string $urlviewimport = '/mod/capquiz/view_import.php'; - - /** @var string The URL to the report view */ - public static string $urlviewreport = '/mod/capquiz/view_report.php'; - - /** @var string The URL for the capquiz editor */ - public static string $urledit = '/mod/capquiz/edit.php'; - - /** @var string The URL to the create question list view */ - public static string $urlviewcreateqlist = '/mod/capquiz/view_create_question_list.php'; - - /** @var string The URL to the rating system view */ - public static string $urlviewratingsystemconfig = '/mod/capquiz/view_rating_system.php'; - - /** - * Returns a redirect url - * - * @param moodle_url $target - */ - public static function redirect(moodle_url $target): moodle_url { - $url = self::create_view_url(self::$urlaction); - $url->param('action', 'redirect'); - $url->param('target-url', $target->out_as_local_url()); - return $url; - } - - /** - * Generates a url based on a relative url - * - * @param string $relativeurl - */ - public static function create_view_url(string $relativeurl): moodle_url { - global $CFG; - $url = new moodle_url($CFG->wwwroot . $relativeurl); - $url->param('id', self::require_course_module_id_param()); - return $url; - } - - /** - * Returns the course module id - */ - public static function require_course_module_id_param(): int { - $id = optional_param('id', 0, PARAM_INT); - if ($id !== 0) { - return $id; - } - return required_param('cmid', PARAM_INT); - } - - /** - * Redirects to the front page - */ - public static function redirect_to_front_page(): void { - global $CFG; - redirect(new moodle_url($CFG->wwwroot)); - } - - /** - * Redirects to the dashboard - */ - public static function redirect_to_dashboard(): void { - self::redirect_to_url(self::create_view_url(self::$urlview)); - } - - /** - * Redirects to specified url - * - * @param moodle_url $url - */ - public static function redirect_to_url(moodle_url $url): void { - redirect($url); - } - - /** - * Redirects to the previous page - */ - public static function redirect_to_previous(): void { - header('Location: ' . $_SERVER['HTTP_REFERER']); - exit; - } - - /** - * Sets teh current page url - * - * @param capquiz $capquiz - * @param string $url - */ - public static function set_page_url(capquiz $capquiz, string $url): void { - global $PAGE; - $PAGE->set_context($capquiz->context()); - $PAGE->set_cm($capquiz->course_module()); - $PAGE->set_pagelayout('incourse'); - $PAGE->set_url(self::create_view_url($url)); - } - - /** - * Returns url to the front page of the capquiz dashboard - */ - public static function view_url(): moodle_url { - return self::create_view_url(self::$urlview); - } - - /** - * Returns the url to the question list view - * - * @param int $questionpage - */ - public static function view_question_list_url(int $questionpage = 0): moodle_url { - $url = self::create_view_url(self::$urledit); - $url->param('qpage', $questionpage); - return $url; - } - - /** - * Returns the url to the rating system view - */ - public static function view_rating_system_url(): moodle_url { - return self::create_view_url(self::$urlviewratingsystemconfig); - } - - /** - * Returns the url to the grading view - */ - public static function view_grading_url(): moodle_url { - return self::create_view_url(self::$urlviewgrading); - } - - /** - * Returns the url to the classlist/leaderboard view - */ - public static function view_classlist_url(): moodle_url { - return self::create_view_url(self::$urlviewclasslist); - } - - /** - * Returns url to the "create question list" view - */ - public static function view_create_question_list_url(): moodle_url { - return self::create_view_url(self::$urlviewcreateqlist); - } - - /** - * Returns url to the import view - */ - public static function view_import_url(): moodle_url { - return self::create_view_url(self::$urlviewimport); - } - - /** - * Returns url to the report view - * - * @param string $mode - */ - public static function view_report_url(string $mode = ''): moodle_url { - return self::report_url(self::$urlviewreport, $mode); - } - - /** - * Generates and returns url to the report view - * - * @param string $relativeurl - * @param string $mode - */ - public static function report_url(string $relativeurl, string $mode): moodle_url { - $url = self::create_view_url($relativeurl); - if ($mode !== '') { - $url->param('mode', $mode); - } - return $url; - } - - /** - * Generates and returns url to add a qyestion to the list with - * the parameters to add question to the list - * - * @param int $questionid - */ - public static function add_question_to_list_url(int $questionid): moodle_url { - $url = self::create_view_url(self::$urlaction); - $url->param('action', 'add-question'); - $url->param('question-id', $questionid); - return $url; - } - - /** - * Generates and returns url to remove a question from a list with - * the parameters to remove question from the list - * - * @param int $questionid - */ - public static function remove_question_from_list_url(int $questionid): moodle_url { - $url = self::create_view_url(self::$urlaction); - $url->param('action', 'remove-question'); - $url->param('question-id', $questionid); - return $url; - } - - /** - * Generates and returns url to publish a question list with - * the parameters to publish the question list - * - * @param capquiz_question_list $qlist - */ - public static function question_list_publish_url(capquiz_question_list $qlist): moodle_url { - $url = self::create_view_url(self::$urlaction); - $url->param('action', 'publish-question-list'); - $url->param('question-list-id', $qlist->id()); - return $url; - } - - /** - * Generates and returns url to create a question list template with - * the parameters to create the template - * - * @param capquiz_question_list $qlist - */ - public static function question_list_create_template_url(capquiz_question_list $qlist): moodle_url { - $url = self::create_view_url(self::$urlaction); - $url->param('action', 'create-question-list-template'); - $url->param('question-list-id', $qlist->id()); - return $url; - } - - /** - * Generates and returns url to select a question list with - * the parameters to set the question list - * - * @param capquiz_question_list $qlist - */ - public static function question_list_select_url(capquiz_question_list $qlist): moodle_url { - $url = self::create_view_url(self::$urlaction); - $url->param('action', 'set-question-list'); - $url->param('question-list-id', $qlist->id()); - return $url; - } - - /** - * Generates and returns url to set a question rating with - * the parameters to set the question rating - * - * @param int $questionid - */ - public static function set_question_rating_url(int $questionid): moodle_url { - $url = self::create_view_url(self::$urlaction); - $url->param('action', 'set-question-rating'); - $url->param('question-id', $questionid); - return $url; - } - - /** - * Generates and returns url to regrade all - */ - public static function regrade_all_url(): moodle_url { - $url = self::create_view_url(self::$urlaction); - $url->param('action', 'regrade-all'); - return $url; - } - - /** - * Generates and returns url to merge qlist with the parameters to merge the qlist - * - * @param int $qlistid - */ - public static function merge_qlist(int $qlistid): moodle_url { - $url = self::create_view_url(self::$urlaction); - $url->param('action', 'merge_qlist'); - $url->param('qlistid', $qlistid); - return $url; - } - - /** - * Generates and returns url to delete a question list with the parameters to delete the list - * - * @param int $qlistid - */ - public static function delete_qlist(int $qlistid): moodle_url { - $url = self::create_view_url(self::$urlaction); - $url->param('action', 'delete_qlist'); - $url->param('qlistid', $qlistid); - return $url; - } - - /** - * Generates and returns url to submit an attempt - * - * @param capquiz_question_attempt $attempt - */ - public static function response_submit_url(capquiz_question_attempt $attempt): moodle_url { - $url = self::create_view_url(self::$urlasync); - $url->param('action', 'answered'); - $url->param('attempt', $attempt->id()); - return $url; - } - - /** - * Generates and returns url to mark an attempt as reviewed - * - * @param capquiz_question_attempt $attempt - */ - public static function response_reviewed_url(capquiz_question_attempt $attempt): moodle_url { - $url = self::create_view_url(self::$urlasync); - $url->param('action', 'reviewed'); - $url->param('attempt', $attempt->id()); - return $url; - } -} diff --git a/classes/capquiz_user.php b/classes/capquiz_user.php index 043aa32..8b6cc31 100755 --- a/classes/capquiz_user.php +++ b/classes/capquiz_user.php @@ -14,240 +14,99 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . -/** - * This file defines a class representing a capquiz user - * - * @package mod_capquiz - * @author Aleksander Skrede - * @author Sebastian S. Gundersen - * @copyright 2019 NTNU - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ +declare(strict_types=1); namespace mod_capquiz; -use context_module; -use question_engine; -use question_usage_by_activity; -use stdClass; +use core\persistent; /** * capquiz_user class * * @package mod_capquiz - * @author Aleksander Skrede - * @author Sebastian S. Gundersen - * @copyright 2019 NTNU + * @author Sebastian Gundersen + * @copyright 2024 Norwegian University of Science and Technology (NTNU) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class capquiz_user { +class capquiz_user extends persistent { - /** @var stdClass $record */ - private stdClass $record; - - /** @var stdClass $user */ - private stdClass $user; - - /** @var capquiz_user_rating $rating */ - private capquiz_user_rating $rating; - - /** @var question_usage_by_activity $quba */ - private question_usage_by_activity $quba; + /** @var string The table name. */ + const TABLE = 'capquiz_user'; /** - * Constructor. + * Get the question usage for this user's quiz attempts. + * TODO: This function should not create the question usage. * - * @param stdClass $record - * @param context_module $context + * @return \question_usage_by_activity */ - public function __construct(stdClass $record, context_module $context) { - global $DB; - $this->record = $record; - $this->user = $DB->get_record('user', ['id' => $this->record->user_id]); - - $rating = capquiz_user_rating::latest_user_rating_by_user($record->id); - if ($rating === null) { - $this->rating = capquiz_user_rating::insert_user_rating_entry($this->id(), $this->rating()); - } else { - $this->rating = $rating; + public function get_question_usage(): \question_usage_by_activity { + if (!$this->get('questionusageid')) { + $quba = api::create_question_usage(new capquiz($this->get('capquizid'))); + $this->set('questionusageid', $quba->get_id()); + $this->save(); } - $this->create_question_usage($context); - $this->quba = question_engine::load_questions_usage_by_activity($this->record->question_usage_id); - } - - /** - * Verify if user has permission to use question - */ - private function has_question_usage(): bool { - return $this->record->question_usage_id !== null; + return \question_engine::load_questions_usage_by_activity($this->get('questionusageid')); } /** - * Create question usage - * - * @param context_module $context - */ - public function create_question_usage(context_module $context): void { - global $DB; - if ($this->has_question_usage()) { - return; - } - $quba = question_engine::make_questions_usage_by_activity('mod_capquiz', $context); - $quba->set_preferred_behaviour('immediatefeedback'); - // TODO: Don't suppress the error if it becomes possible to save QUBAs without slots. - @question_engine::save_questions_usage_by_activity($quba); - $this->record->question_usage_id = $quba->get_id(); - $DB->update_record('capquiz_user', $this->record); - } - - - /** - * Return this user's quba. - */ - public function question_usage(): ?question_usage_by_activity { - return $this->quba; - } - - /** - * Loads capquiz user + * Find the related CAPQuiz user for a user. * * @param capquiz $capquiz * @param int $moodleuserid - * @param context_module $context + * @return ?capquiz_user */ - public static function load_user(capquiz $capquiz, int $moodleuserid, context_module $context): ?capquiz_user { - global $DB; - if ($user = self::load_db_entry($capquiz, $moodleuserid, $context)) { - return $user; - } - $record = new stdClass(); - $record->user_id = $moodleuserid; - $record->capquiz_id = $capquiz->id(); - $record->rating = $capquiz->default_user_rating(); - $capquizuserid = $DB->insert_record('capquiz_user', $record); - capquiz_user_rating::insert_user_rating_entry($capquizuserid, $record->rating); - return self::load_db_entry($capquiz, $moodleuserid, $context); + public static function get_record_by_core_user(capquiz $capquiz, int $moodleuserid): ?capquiz_user { + return self::get_record([ + 'userid' => $moodleuserid, + 'capquizid' => $capquiz->get('id'), + ]) ?: null; } /** - * Returns count of users in this capquiz + * Get all records belonging to a CAPQuiz. * * @param int $capquizid - * @return int count of users in this capquiz + * @return self[] */ - public static function user_count(int $capquizid): int { - global $DB; - return $DB->count_records('capquiz_user', ['capquiz_id' => $capquizid]); + public static function get_records_by_capquiz(int $capquizid): array { + return self::get_records(['capquizid' => $capquizid]); } /** - * Returns list of all users in this capquiz + * Return the definition of the properties of this model. * - * @param int $capquizid - * @param context_module $context - * @return capquiz_user[] - */ - public static function list_users(int $capquizid, context_module $context): array { - global $DB; - $records = $DB->get_records('capquiz_user', ['capquiz_id' => $capquizid]); - return array_map(fn(stdClass $record) => new capquiz_user($record, $context), array_values($records)); - } - - /** - * Return this user's id - */ - public function id(): int { - return $this->record->id; - } - - /** - * Returns this user's username - */ - public function username(): string { - return $this->user->username; - } - - /** - * Returns this user's first name - */ - public function first_name(): string { - return $this->user->firstname; + * @return array + */ + protected static function define_properties(): array { + return [ + 'userid' => [ + 'type' => PARAM_INT, + 'null' => NULL_NOT_ALLOWED, + ], + 'capquizid' => [ + 'type' => PARAM_INT, + 'null' => NULL_NOT_ALLOWED, + ], + 'questionusageid' => [ + 'type' => PARAM_INT, + 'default' => null, + 'null' => NULL_ALLOWED, + ], + 'rating' => [ + 'type' => PARAM_FLOAT, + 'default' => 0.0, + 'null' => NULL_NOT_ALLOWED, + ], + 'higheststars' => [ + 'type' => PARAM_INT, + 'default' => 0, + 'null' => NULL_NOT_ALLOWED, + ], + 'starsgraded' => [ + 'type' => PARAM_INT, + 'default' => 0, + 'null' => NULL_NOT_ALLOWED, + ], + ]; } - - /** - * Returns this user's last name - */ - public function last_name(): string { - return $this->user->lastname; - } - - /** - * Return users rating - */ - public function rating(): float { - return $this->record->rating; - } - - /** - * Get this user's capquiz rating - */ - public function get_capquiz_user_rating(): capquiz_user_rating { - return $this->rating; - } - - /** - * Return the highest star rating this user has achieved - */ - public function highest_stars_achieved(): int { - return $this->record->highest_level; - } - - /** - * Return the highest star grade - */ - public function highest_stars_graded(): int { - return $this->record->stars_graded; - } - - /** - * Set this user's highest star rating - * - * @param int $higheststar - */ - public function set_highest_star(int $higheststar): void { - global $DB; - $this->record->highest_level = $higheststar; - $DB->update_record('capquiz_user', $this->record); - } - - /** - * Set this user's rating - * - * @param float $rating - * @param bool $manual - */ - public function set_rating(float $rating, bool $manual = false): void { - global $DB; - $this->record->rating = $rating; - $DB->update_record('capquiz_user', $this->record); - $userrating = capquiz_user_rating::create_user_rating($this, $rating, $manual); - $this->rating = $userrating; - } - - /** - * Load user entry from database - * - * @param capquiz $capquiz - * @param int $moodleuserid - * @param context_module $context - */ - private static function load_db_entry(capquiz $capquiz, int $moodleuserid, context_module $context): ?capquiz_user { - global $DB; - $entry = $DB->get_record('capquiz_user', [ - 'user_id' => $moodleuserid, - 'capquiz_id' => $capquiz->id(), - ]); - return empty($entry) ? null : new capquiz_user($entry, $context); - } - } diff --git a/classes/capquiz_user_rating.php b/classes/capquiz_user_rating.php index 5bfcd1f..f26462d 100755 --- a/classes/capquiz_user_rating.php +++ b/classes/capquiz_user_rating.php @@ -14,129 +14,57 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . -/** - * This file defines a class represeting a capquiz user rating - * - * @package mod_capquiz - * @author André Storhaug - * @copyright 2019 NTNU - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ +declare(strict_types=1); namespace mod_capquiz; -use stdClass; +use core\persistent; /** - * Class capquiz_user_rating + * User rating. * * @package mod_capquiz - * @author André Storhaug - * @copyright 2019 NTNU + * @author Sebastian Gundersen + * @copyright 2024 Norwegian University of Science and Technology (NTNU) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class capquiz_user_rating { +class capquiz_user_rating extends persistent { - /** @var stdClass $record */ - private stdClass $record; - - /** - * Constructor. - * - * @param stdClass $record - */ - public function __construct(stdClass $record) { - $this->record = $record; - } + /** @var string The table name. */ + const TABLE = 'capquiz_user_rating'; /** - * Loads and returns user rating from database - * - * @param int $questionratingid - */ - public static function load_user_rating(int $questionratingid): ?capquiz_user_rating { - global $DB; - $record = $DB->get_record('capquiz_question_rating', ['id' => $questionratingid]); - if ($record === false) { - return null; - } - return new capquiz_user_rating($record); - } - - /** - * Creates and inserts a new user rating to the database - * - * @param capquiz_user $user - * @param float $rating - * @param bool $manual - */ - public static function create_user_rating(capquiz_user $user, float $rating, bool $manual = false): ?capquiz_user_rating { - return self::insert_user_rating_entry($user->id(), $rating, $manual); - } - - /** - * Load information about the latest user rating for an capquiz user from the database. + * Get the latest user rating for a given CAPQuiz user. * * @param int $capquizuserid */ - public static function latest_user_rating_by_user(int $capquizuserid): ?capquiz_user_rating { + public static function get_latest_by_user(int $capquizuserid): ?capquiz_user_rating { global $DB; - $sql = "SELECT cur.* - FROM {capquiz_user_rating} cur - JOIN {capquiz_user} cu ON cu.id = cur.capquiz_user_id - WHERE cur.id = ( - SELECT MAX(cur2.id) - FROM {capquiz_user_rating} cur2 - JOIN {capquiz_user} cu2 ON cu2.id = cur2.capquiz_user_id - WHERE cu2.id = cu.id - ) - AND cu.id = :capquiz_user_id"; - $record = $DB->get_record_sql($sql, ['capquiz_user_id' => $capquizuserid]); - - return $record ? new capquiz_user_rating($record) : null; - } - - /** - * Inserts a new user rating record to the database - * - * @param int $capquizuserid - * @param float $rating - * @param bool $manual - */ - public static function insert_user_rating_entry(int $capquizuserid, float $rating, bool $manual = false): capquiz_user_rating { - global $DB, $USER; - $record = new stdClass(); - $record->capquiz_user_id = $capquizuserid; - $record->rating = $rating; - $record->manual = $manual; - $record->timecreated = time(); - $record->user_id = $USER->id; - $record->id = $DB->insert_record('capquiz_user_rating', $record); - return new capquiz_user_rating($record); + $records = $DB->get_records(self::TABLE, ['capquizuserid' => $capquizuserid], 'timecreated DESC', '*', 0, 1); + return empty($records) ? null : new self(0, reset($records)); } /** - * Returns this user ratings id - */ - public function id(): int { - return $this->record->id; - } - - /** - * Returns this user ratings rating - */ - public function rating(): float { - return $this->record->rating; - } - - /** - * Sets this user ratings rating and updates the database record + * Return the definition of the properties of this model. * - * @param float $rating + * @return array */ - public function set_rating(float $rating): void { - global $DB; - $this->record->rating = $rating; - $DB->update_record('capquiz_user_rating', $this->record); + protected static function define_properties(): array { + return [ + 'capquizuserid' => [ + 'type' => PARAM_INT, + 'null' => NULL_NOT_ALLOWED, + ], + 'rating' => [ + 'type' => PARAM_FLOAT, + 'default' => 0.0, + 'null' => NULL_NOT_ALLOWED, + ], + 'manual' => [ + 'type' => PARAM_BOOL, + 'default' => false, + 'null' => NULL_NOT_ALLOWED, + ], + ]; } } diff --git a/classes/external/set_question_rating.php b/classes/external/set_question_rating.php new file mode 100644 index 0000000..4604892 --- /dev/null +++ b/classes/external/set_question_rating.php @@ -0,0 +1,82 @@ +. + +namespace mod_capquiz\external; + +use core_external\external_api; +use core_external\external_function_parameters; +use core_external\external_single_structure; +use core_external\external_value; +use mod_capquiz\api; +use mod_capquiz\capquiz_slot; + +/** + * Add questions to a CAPQuiz. + * + * @package mod_capquiz + * @author Sebastian Gundersen + * @copyright 2025 Norwegian University of Science and Technology (NTNU) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class set_question_rating extends external_api { + /** + * Describe parameters. + * + * @return external_function_parameters + */ + public static function execute_parameters(): external_function_parameters { + return new external_function_parameters([ + 'cmid' => new external_value(PARAM_INT), + 'slotid' => new external_value(PARAM_INT), + 'rating' => new external_value(PARAM_FLOAT), + ]); + } + + /** + * Execute. + * + * @param int $cmid + * @param int $slotid + * @param float $rating + * @return array + */ + public static function execute(int $cmid, int $slotid, float $rating): array { + [ + 'cmid' => $cmid, + 'slotid' => $slotid, + 'rating' => $rating, + ] = self::validate_parameters(self::execute_parameters(), [ + 'cmid' => $cmid, + 'slotid' => $slotid, + 'rating' => $rating, + ]); + $context = \context_module::instance($cmid); + self::validate_context($context); + require_capability('mod/capquiz:instructor', $context); + $slot = new capquiz_slot($slotid); + $questionrating = api::create_question_rating($slot, $rating, true); + return ['questionratingid' => $questionrating->get('id')]; + } + + /** + * Describe return value. + * + * @return external_single_structure + */ + public static function execute_returns(): external_single_structure { + return new external_single_structure(['questionratingid' => new external_value(PARAM_INT)]); + } +} diff --git a/classes/form/view/grading_configuration_form.php b/classes/form/view/grading_configuration_form.php deleted file mode 100644 index 65852fe..0000000 --- a/classes/form/view/grading_configuration_form.php +++ /dev/null @@ -1,121 +0,0 @@ -. - -/** - * CAPQuiz grading configuration form definition. - * - * @package mod_capquiz - * @author Aleksander Skrede - * @copyright 2018 NTNU - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - -namespace mod_capquiz\form\view; - -use mod_capquiz\capquiz; -use moodle_url; - -defined('MOODLE_INTERNAL') || die(); - -require_once($CFG->libdir . '/formslib.php'); - -/** - * grading_configuration_form class - * - * @package mod_capquiz - * @author Aleksander Skrede - * @copyright 2018 NTNU - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -class grading_configuration_form extends \moodleform { - - /** @var capquiz $capquiz */ - private capquiz $capquiz; - - /** - * Constructor. - * - * @param capquiz $capquiz - * @param moodle_url $url - */ - public function __construct(capquiz $capquiz, moodle_url $url) { - $this->capquiz = $capquiz; - parent::__construct($url); - } - - /** - * Defines form - */ - public function definition(): void { - $qlist = $this->capquiz->question_list(); - $form = $this->_form; - $form->addElement('text', 'default_user_rating', get_string('default_user_rating', 'capquiz')); - $form->setType('default_user_rating', PARAM_INT); - $form->setDefault('default_user_rating', $this->capquiz->default_user_rating()); - $form->addRule('default_user_rating', get_string('default_user_rating_required', 'capquiz'), 'required', null, 'client'); - for ($star = 1; $star <= $qlist->max_stars(); $star++) { - $groupname = "star_group_$star"; - $input = "star_rating_$star"; - $text = get_string('level_rating', 'capquiz', $star); - $elements = []; - $elements[] = $form->createElement('text', $input, $text); - if ($star > 1) { - $elements[] = $form->createElement('submit', "delstarbutton$star", get_string('delete_star', 'capquiz')); - } - $form->addGroup($elements, $groupname, $text, [''], false); - $form->setType($input, PARAM_INT); - $form->setDefault($input, $qlist->star_rating($star)); - } - - $form->addElement('submit', 'addstarbutton', get_string('add_star', 'capquiz')); - - $strstarstopass = get_string('stars_to_pass', 'capquiz'); - $strstarstopassrequired = get_string('stars_to_pass_required', 'capquiz'); - $form->addElement('text', 'starstopass', $strstarstopass); - $form->setType('starstopass', PARAM_INT); - $form->setDefault('starstopass', $this->capquiz->stars_to_pass()); - $form->addRule('starstopass', $strstarstopassrequired, 'required', null, 'client'); - - $strduedate = get_string('due_time_grading', 'capquiz'); - $form->addElement('date_time_selector', 'timedue', $strduedate); - $form->setType('timedue', PARAM_INT); - $timedue = $this->capquiz->time_due(); - $oneweek = 60 * 60 * 24 * 7; - $form->setDefault('timedue', $timedue ? $timedue : time() + $oneweek); - - $form->addElement('submit', 'submitbutton', get_string('savechanges')); - } - - /** - * Validate the data from the form. - * - * @param array $data array of ("fieldname"=>value) of submitted data - * @param array $files array of uploaded files "element_name"=>tmp_file_path - * @return array of "element_name"=>"error_description" if there are errors, - * or an empty array if everything is OK (true allowed for backwards compatibility too). - */ - public function validations($data, $files): array { - $errors = []; - if (empty($data['default_user_rating'])) { - $errors['default_user_rating'] = get_string('default_user_rating_required', 'capquiz'); - } - if (empty($data['starstopass']) || $data['starstopass'] < 0 || $data['starstopass'] > 5) { - $errors['starstopass'] = get_string('stars_to_pass_required', 'capquiz'); - } - return $errors; - } - -} diff --git a/classes/form/view/matchmaking_strategy_selection_form.php b/classes/form/view/matchmaking_strategy_selection_form.php deleted file mode 100755 index 5b25326..0000000 --- a/classes/form/view/matchmaking_strategy_selection_form.php +++ /dev/null @@ -1,98 +0,0 @@ -. - -/** - * CAPQuiz matchmaking strategy selection form definition. - * - * @package mod_capquiz - * @author Aleksander Skrede - * @copyright 2018 NTNU - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - -namespace mod_capquiz\form\view; - -use mod_capquiz\capquiz; -use mod_capquiz\capquiz_matchmaking_strategy_loader; -use mod_capquiz\capquiz_matchmaking_strategy_registry; -use moodle_url; - -defined('MOODLE_INTERNAL') || die(); - -require_once($CFG->libdir . '/formslib.php'); - -/** - * matchmaking_strategy_selection form class - * @package mod_capquiz - * @author Aleksander Skrede - * @copyright 2018 NTNU - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -class matchmaking_strategy_selection_form extends \moodleform { - - /** @var capquiz $capquiz */ - private capquiz $capquiz; - - /** - * Constructor. - * - * @param capquiz $capquiz - * @param moodle_url $url - */ - public function __construct(capquiz $capquiz, moodle_url $url) { - $this->capquiz = $capquiz; - parent::__construct($url); - } - - /** - * Defines form - */ - public function definition(): void { - $form = $this->_form; - $loader = new capquiz_matchmaking_strategy_loader($this->capquiz); - $registry = new capquiz_matchmaking_strategy_registry($this->capquiz); - $strategies = $registry->selection_strategies(); - $index = 0; - $selectedindex = -1; - $radioarray = []; - foreach ($strategies as $strategy) { - if ($loader->current_strategy_name() === $strategy) { - $selectedindex = $index; - } - $localized = capquiz_matchmaking_strategy_loader::localized_strategy_name($strategy); - $radioarray[] = $form->createElement('radio', 'strategy', '', $localized, $index, [$strategy]); - $index++; - } - $form->addGroup($radioarray, 'radioar', '', '
', false); - $this->add_action_buttons(false); - if ($selectedindex > -1) { - $form->setDefault('strategy', $selectedindex); - } - } - - /** - * Validate the data from the form. - * - * @param array $data array of ("fieldname"=>value) of submitted data - * @param array $files array of uploaded files "element_name"=>tmp_file_path - * @return array of "element_name"=>"error_description" if there are errors, - * or an empty array if everything is OK (true allowed for backwards compatibility too). - */ - public function validations($data, $files): array { - return []; - } - -} diff --git a/classes/form/view/question_list_create_form.php b/classes/form/view/question_list_create_form.php deleted file mode 100755 index 63c5d78..0000000 --- a/classes/form/view/question_list_create_form.php +++ /dev/null @@ -1,95 +0,0 @@ -. - -/** - * CAPQuiz question list form definition. - * - * @package mod_capquiz - * @author Aleksander Skrede - * @copyright 2018 NTNU - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - -namespace mod_capquiz\form\view; - -defined('MOODLE_INTERNAL') || die(); - -require_once($CFG->libdir . '/formslib.php'); - -/** - * question_list_create_form class - * - * @package mod_capquiz - * @author Aleksander Skrede - * @copyright 2018 NTNU - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -class question_list_create_form extends \moodleform { - - /** - * Defines form - */ - public function definition(): void { - $form = $this->_form; - $form->addElement('text', 'title', get_string('title', 'capquiz')); - $form->setType('title', PARAM_TEXT); - $form->addRule('title', get_string('title_required', 'capquiz'), 'required', null, 'client'); - - $form->addElement('textarea', 'description', get_string('description', 'capquiz')); - $form->setType('description', PARAM_TEXT); - $form->addRule('description', get_string('description_required', 'capquiz'), 'required', null, 'client'); - - $ratings = [1300, 1450, 1600, 1800, 2000]; - for ($level = 1; $level < 6; $level++) { - $element = "level_{$level}_rating"; - $text = get_string('level_rating', 'capquiz', $level); - $requiredtext = get_string('level_rating_required', 'capquiz', $level); - $form->addElement('text', $element, $text); - $form->setType($element, PARAM_INT); - $form->addRule($element, $requiredtext, 'required', null, 'client'); - $form->setDefault($element, $ratings[$level - 1]); - } - - $form->addElement('submit', 'submitbutton', get_string('create_question_list', 'capquiz')); - } - - /** - * Validate the data from the form. - * - * @param array $data array of ("fieldname"=>value) of submitted data - * @param array $files array of uploaded files "element_name"=>tmp_file_path - * @return array of "element_name"=>"error_description" if there are errors, - * or an empty array if everything is OK (true allowed for backwards compatibility too). - */ - public function validations($data, $files): array { - $errors = []; - if (empty($data['title'])) { - $errors['title'] = get_string('title_required', 'capquiz'); - } - if (empty($data['description'])) { - $errors['description'] = get_string('description_required', 'capquiz'); - } - for ($level = 1; $level < 6; $level++) { - $element = "level_{$level}_rating"; - if (empty($data[$element])) { - $requiredtext = get_string('level_rating_required', 'capquiz', $level); - $errors[$element] = $requiredtext; - } - } - return $errors; - } - -} diff --git a/classes/form/view/rating_system_selection_form.php b/classes/form/view/rating_system_selection_form.php deleted file mode 100755 index c95401f..0000000 --- a/classes/form/view/rating_system_selection_form.php +++ /dev/null @@ -1,97 +0,0 @@ -. - -/** - * CAPQuiz rating system selection form definition. - * - * @package mod_capquiz - * @author Aleksander Skrede - * @copyright 2018 NTNU - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - -namespace mod_capquiz\form\view; - -use mod_capquiz\capquiz; -use mod_capquiz\capquiz_rating_system_loader; -use mod_capquiz\capquiz_rating_system_registry; -use moodle_url; - -defined('MOODLE_INTERNAL') || die(); - -require_once($CFG->libdir . '/formslib.php'); - -/** - * rating_system_selection_form class - * - * @package mod_capquiz - * @author Aleksander Skrede - * @copyright 2018 NTNU - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -class rating_system_selection_form extends \moodleform { - - /** @var capquiz $capquiz */ - private capquiz $capquiz; - - /** - * Constructor. - * - * @param capquiz $capquiz - * @param moodle_url $url - */ - public function __construct(capquiz $capquiz, moodle_url $url) { - $this->capquiz = $capquiz; - parent::__construct($url); - } - - /** - * Defines form - */ - public function definition(): void { - $form = $this->_form; - $loader = new capquiz_rating_system_loader($this->capquiz); - $registry = new capquiz_rating_system_registry(); - $index = 0; - $selectedindex = -1; - $radioarray = []; - foreach ($registry->rating_systems() as $ratingsystem) { - if ($loader->current_rating_system_name() === $ratingsystem) { - $selectedindex = $index; - } - $radioarray[] = $form->createElement('radio', 'rating_system', '', $ratingsystem, $index, [$ratingsystem]); - $index++; - } - $form->addGroup($radioarray, 'radioar', '', '
', false); - $this->add_action_buttons(false); - if ($selectedindex > -1) { - $form->setDefault('rating_system', $selectedindex); - } - } - - /** - * Validate the data from the form. - * - * @param array $data array of ("fieldname"=>value) of submitted data - * @param array $files array of uploaded files "element_name"=>tmp_file_path - * @return array of "element_name"=>"error_description" if there are errors, - * or an empty array if everything is OK (true allowed for backwards compatibility too). - */ - public function validations($data, $files): array { - return []; - } - -} diff --git a/classes/local/helpers/elo.php b/classes/local/helpers/elo.php new file mode 100644 index 0000000..ba3df1a --- /dev/null +++ b/classes/local/helpers/elo.php @@ -0,0 +1,61 @@ +. + +declare(strict_types=1); + +namespace mod_capquiz\local\helpers; + +/** + * ELO rating system helper for CAPQuiz. + * + * @package mod_capquiz + * @author Sebastian Gundersen + * @copyright 2024 Norwegian University of Science and Technology (NTNU) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class elo { + /** + * Calculate a new rating based on the old rating and score towards the opponent's rating. + * + * @param float $kfactor + * @param float $score + * @param float $rating + * @param float $opponentrating + */ + public static function new_rating(float $kfactor, float $score, float $rating, float $opponentrating): float { + return $rating + $kfactor * ($score - self::expected_score($rating, $opponentrating)); + } + + /** + * Calculate the expected score. + * + * @param float $rating + * @param float $opponentrating + */ + public static function expected_score(float $rating, float $opponentrating): float { + return 1.0 / (1.0 + pow(10.0, ($opponentrating - $rating) / 400.0)); + } + + /** + * Calculate ideal question rating. + * + * @param float $winprobability + * @param float $rating + */ + public static function ideal_question_rating(float $winprobability, float $rating): float { + return 400.0 * log(1.0 / $winprobability - 1.0, 10.0) + $rating; + } +} diff --git a/classes/local/helpers/questions.php b/classes/local/helpers/questions.php new file mode 100644 index 0000000..1ef33b4 --- /dev/null +++ b/classes/local/helpers/questions.php @@ -0,0 +1,75 @@ +. + +declare(strict_types=1); + +namespace mod_capquiz\local\helpers; + +use mod_capquiz\capquiz; +use question_display_options; + +/** + * Helper functions for questions. + * + * @package mod_capquiz + * @author Sebastian Gundersen + * @copyright 2025 Norwegian University of Science and Technology (NTNU) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class questions { + /** + * Get all question records that are found by question references via the given context and question area. + * + * @param int $contextid + * @param string $questionarea + * @return \stdClass[] + */ + public static function get_all_questions_by_references(int $contextid, string $questionarea): array { + global $DB; + return $DB->get_records_sql(" + SELECT q.* + FROM {question_references} qr + JOIN {question_bank_entries} qbe + ON qbe.id = qr.questionbankentryid + JOIN {question_versions} qv + ON qv.questionbankentryid = qbe.id + JOIN {question} q + ON q.id = qv.questionid + WHERE qr.component = 'mod_capquiz' + AND qr.usingcontextid = :contextid + AND qr.questionarea = :questionarea", [ + 'contextid' => $contextid, + 'questionarea' => $questionarea, + ]); + } + + /** + * Get question display options for reviewing question attempts. + * + * @param capquiz $capquiz + * @return question_display_options + */ + public static function get_question_display_options(capquiz $capquiz): question_display_options { + $options = new question_display_options(); + $options->context = \context_module::instance($capquiz->get_cmid()); + foreach (json_decode($capquiz->get('questiondisplayoptions'), true) ?: [] as $key => $value) { + if (property_exists($options, $key)) { + $options->$key = $value; + } + } + return $options; + } +} diff --git a/classes/local/helpers/stars.php b/classes/local/helpers/stars.php new file mode 100644 index 0000000..7c07d6e --- /dev/null +++ b/classes/local/helpers/stars.php @@ -0,0 +1,89 @@ +. + +declare(strict_types=1); + +namespace mod_capquiz\local\helpers; + +use mod_capquiz\capquiz; +use mod_capquiz\capquiz_user; + +/** + * Helper functions for the star level system. + * + * @package mod_capquiz + * @author Sebastian Gundersen + * @copyright 2024 Norwegian University of Science and Technology (NTNU) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class stars { + /** + * Returns the completion level to the next rating as a percent value + * + * @param capquiz $capquiz + * @param float $userrating + */ + public static function get_percent_to_next_star(capquiz $capquiz, float $userrating): int { + $defaultrating = $capquiz->get('defaultuserrating'); + $ratings = $capquiz->get('starratings'); + $nextrating = 0; + for ($star = 1; $star <= $capquiz->get_max_stars(); $star++) { + $nextrating = self::get_required_rating_for_star($ratings, $star); + if ($nextrating > $userrating) { + $previous = $star === 1 ? $defaultrating : self::get_required_rating_for_star($ratings, $star - 1); + $userrating -= $previous; + $nextrating -= $previous; + break; + } + } + return $nextrating >= 1 ? (int)($userrating / $nextrating * 100.0) : 0; + } + + /** + * Get the required rating for a given star. + * + * @param string $ratingscsv CSV of star ratings (e.g. 1200,1400,1600,1800,2000) + * @param int $star 1..n + */ + public static function get_required_rating_for_star(string $ratingscsv, int $star): float { + $index = $star - 1; + $ratings = explode(',', $ratingscsv); + if ($index >= count($ratings)) { + return (float)$ratings[count($ratings) - 1]; + } + return (float)$ratings[$index]; + } + + /** + * Returns the number of stars that can be achieved. + * + * @param string $ratingscsv CSV of star ratings (e.g. 1200,1400,1600,1800,2000) + */ + public static function get_max_stars(string $ratingscsv): int { + return substr_count($ratingscsv, ',') + 1; + } + + /** + * Check if user has achieved a passing grade. + * + * @param capquiz_user $user + * @param capquiz $capquiz + * @return bool + */ + public static function is_user_passing(capquiz_user $user, capquiz $capquiz): bool { + return $user->get('starsgraded') >= $capquiz->get('starstopass'); + } +} diff --git a/report/attemptsreport_options.php b/classes/local/reports/options.php similarity index 68% rename from report/attemptsreport_options.php rename to classes/local/reports/options.php index 5451f1f..b95d524 100644 --- a/report/attemptsreport_options.php +++ b/classes/local/reports/options.php @@ -14,17 +14,11 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . -/** - * Base class for the options that control what is visible in an {@see quiz_attempts_report}. - * - * @package mod_capquiz - * @author André Storhaug - * @copyright 2019 Norwegian University of Science and Technology (NTNU) - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ +declare(strict_types=1); -namespace mod_capquiz\report; +namespace mod_capquiz\local\reports; +use cm_info; use mod_capquiz\capquiz; use moodle_url; use stdClass; @@ -34,55 +28,65 @@ require_once($CFG->libdir . '/formslib.php'); /** - * Base class for the options that control what is visible in an {@see quiz_attempts_report}. + * Base class for the options that control what is visible in a report. * + * @package mod_capquiz * @author André Storhaug * @copyright 2019 Norwegian University of Science and Technology (NTNU) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class capquiz_attempts_report_options { +class options { - /** @var string the report mode. */ - public $mode; + /** @var int default page size for reports. */ + const DEFAULT_PAGE_SIZE = 30; - /** @var object the settings for the capquiz being reported on. */ - public $capquiz; + /** @var string constant used for the options, means all users with attempts. */ + const ALL_WITH = 'all_with'; - /** @var object the course module objects for the capquiz being reported on. */ - public $cm; + /** @var string constant used for the options, means only enrolled users with attempts. */ + const ENROLLED_WITH = 'enrolled_with'; - /** @var object the course settings for the course the capquiz is in. */ - public $course; + /** @var string constant used for the options, means only enrolled users without attempts. */ + const ENROLLED_WITHOUT = 'enrolled_without'; - /** - * @var string capquiz_attempts_report::ALL_WITH or capquiz_attempts_report::ENROLLED_WITH - * capquiz_attempts_report::ENROLLED_WITHOUT or capquiz_attempts_report::ENROLLED_ALL - */ - public $attempts = capquiz_attempts_report::ENROLLED_WITH; + /** @var string constant used for the options, means all enrolled users. */ + const ENROLLED_ALL = 'enrolled_any'; - /** - * @var bool whether to show all attempts, or just the ones that are answered. - */ - public $onlyanswered = true; + /** @var string report type */ + public string $reporttype; + + /** @var capquiz the settings for the capquiz being reported on. */ + public capquiz $capquiz; + + /** @var cm_info the course module objects for the capquiz being reported on. */ + public cm_info $cm; + + /** @var stdClass the course settings for the course the capquiz is in. */ + public stdClass $course; + + /** @var string ALL_WITH, ENROLLED_WITH, ENROLLED_WITHOUT, or ENROLLED_ALL */ + public string $attempts = self::ENROLLED_WITH; + + /** @var bool whether to show all attempts, or just the ones that are answered. */ + public bool $onlyanswered = true; /** @var int Number of attempts to show per page. */ - public $pagesize = capquiz_attempts_report::DEFAULT_PAGE_SIZE; + public int $pagesize = self::DEFAULT_PAGE_SIZE; /** @var string whether the data should be downloaded in some format, or '' to display it. */ - public $download = ''; + public string $download = ''; /** @var bool whether the report table should have a column of checkboxes. */ - public $checkboxcolumn = false; + public bool $checkboxcolumn = false; /** * Constructor. - * @param string $mode which report these options are for. - * @param capquiz $capquiz the settings for the capquiz being reported on. - * @param object $cm the course module objects for the capquiz being reported on. - * @param object $course the course settings for the coures this capquiz is in. + * + * @param capquiz $capquiz + * @param cm_info $cm + * @param stdClass $course */ - public function __construct(string $mode, capquiz $capquiz, object $cm, object $course) { - $this->mode = $mode; + public function __construct(capquiz $capquiz, cm_info $cm, stdClass $course) { $this->capquiz = $capquiz; $this->cm = $cm; $this->course = $course; @@ -96,7 +100,7 @@ public function __construct(string $mode, capquiz $capquiz, object $cm, object $ protected function get_url_params(): array { return [ 'id' => $this->cm->id, - 'mode' => $this->mode, + 'reporttype' => $this->reporttype, 'attempts' => $this->attempts, 'onlyanswered' => $this->onlyanswered, ]; @@ -106,7 +110,7 @@ protected function get_url_params(): array { * Get the URL to show the report with these options. */ public function get_url(): moodle_url { - return new moodle_url('/mod/capquiz/view_report.php', $this->get_url_params()); + return new moodle_url('/mod/capquiz/report.php', $this->get_url_params()); } /** @@ -144,6 +148,7 @@ public function get_initial_form_data(): stdClass { /** * Set the fields of this object from the form data. + * * @param stdClass $fromform The data from $mform->get_data() from the settings form. */ public function setup_from_form_data(stdClass $fromform): void { @@ -157,7 +162,7 @@ public function setup_from_form_data(stdClass $fromform): void { */ public function setup_from_params(): void { $this->attempts = optional_param('attempts', $this->attempts, PARAM_ALPHAEXT); - $this->onlyanswered = optional_param('onlyanswered', $this->onlyanswered, PARAM_BOOL); + $this->onlyanswered = (bool)optional_param('onlyanswered', $this->onlyanswered, PARAM_BOOL); $this->pagesize = optional_param('pagesize', $this->pagesize, PARAM_INT); $this->download = optional_param('download', $this->download, PARAM_ALPHA); } @@ -167,7 +172,7 @@ public function setup_from_params(): void { * (For those settings that are backed by user-preferences). */ public function setup_from_user_preferences(): void { - $this->pagesize = get_user_preferences('capquiz_report_pagesize', $this->pagesize); + $this->pagesize = (int)get_user_preferences('capquiz_report_pagesize', $this->pagesize); } /** @@ -183,8 +188,7 @@ public function update_user_preferences(): void { */ public function resolve_dependencies(): void { if ($this->pagesize < 1) { - $this->pagesize = capquiz_attempts_report::DEFAULT_PAGE_SIZE; + $this->pagesize = self::DEFAULT_PAGE_SIZE; } } - } diff --git a/view_classlist.php b/classes/local/reports/report.php old mode 100755 new mode 100644 similarity index 53% rename from view_classlist.php rename to classes/local/reports/report.php index e28c451..8309340 --- a/view_classlist.php +++ b/classes/local/reports/report.php @@ -14,27 +14,29 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . +declare(strict_types=1); + +namespace mod_capquiz\local\reports; + +use cm_info; +use mod_capquiz\capquiz; + /** - * Display leaderboard + * Interface for CAPQuiz reports. * * @package mod_capquiz - * @author Aleksander Skrede - * @copyright 2018 NTNU + * @author Sebastian Gundersen + * @copyright 2024 Norwegian University of Science and Technology (NTNU) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ - -namespace mod_capquiz; - -require_once('../../config.php'); -require_once($CFG->dirroot . '/question/editlib.php'); -require_once($CFG->dirroot . '/mod/capquiz/lib.php'); - -$cmid = capquiz_urls::require_course_module_id_param(); -$cm = get_coursemodule_from_id('capquiz', $cmid, 0, false, MUST_EXIST); -require_login($cm->course, false, $cm); -$context = \context_module::instance($cmid); -require_capability('mod/capquiz:instructor', $context); - -$capquiz = new capquiz($cmid); -capquiz_urls::set_page_url($capquiz, capquiz_urls::$urledit); -$capquiz->renderer()->display_leaderboard($capquiz); +interface report { + /** + * Display report. + * + * @param capquiz $capquiz + * @param cm_info $cm + * @param \stdClass $course + * @param string $download + */ + public function display(capquiz $capquiz, cm_info $cm, \stdClass $course, string $download): void; +} diff --git a/report/attemptsreport_table.php b/classes/local/reports/table.php similarity index 63% rename from report/attemptsreport_table.php rename to classes/local/reports/table.php index f474ea4..0ee01ba 100644 --- a/report/attemptsreport_table.php +++ b/classes/local/reports/table.php @@ -14,23 +14,17 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . -/** - * Base class for the table used by a {@see quiz_attempts_report}. - * - * @package mod_capquiz - * @author André Storhaug - * @copyright 2019 Norwegian University of Science and Technology (NTNU) - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ +declare(strict_types=1); -namespace mod_capquiz\report; +namespace mod_capquiz\local\reports; use coding_exception; use core\context; -use core\context\module; use core\dml\sql_join; use html_writer; +use mod_capquiz\capquiz; use moodle_url; +use popup_action; use qubaid_condition; use qubaid_list; use question_engine_data_mapper; @@ -43,66 +37,64 @@ require_once($CFG->libdir . '/tablelib.php'); /** - * Base class for the table used by a {@see capquiz_attempts_report}. + * Base class for the table used by a {@see report}. * * @package mod_capquiz * @author André Storhaug * @copyright 2019 Norwegian University of Science and Technology (NTNU) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -abstract class capquiz_attempts_report_table extends table_sql { - - /** @var string the name of the userid field */ - public $useridfield = 'userid'; +abstract class table extends table_sql { /** @var moodle_url the URL of this report. */ - protected $reporturl; + protected moodle_url $reporturl; /** @var array the display options. */ - protected $displayoptions; + protected array $displayoptions; /** - * @var array information about the latest step of each question. + * @var ?array information about the latest step of each question. * Loaded by {@see load_question_latest_steps()}, if applicable. */ - protected $lateststeps = null; + protected ?array $lateststeps = null; - /** @var object the capquiz settings for the capquiz we are reporting on. */ - protected $capquiz; + /** @var capquiz the capquiz settings for the capquiz we are reporting on. */ + protected capquiz $capquiz; /** @var context the capquiz context. */ - protected $context; + protected context $context; - /** @var object mod_quiz_attempts_report_options the options affecting this report. */ - protected $options; + /** @var options the options affecting this report. */ + protected options $options; /** @var sql_join Contains joins, wheres, params to find the students in the course. */ - protected $studentsjoins; + protected sql_join $studentsjoins; /** @var array the questions that comprise this capquiz.. */ - protected $questions; + protected array $questions; /** @var bool whether to include the column with checkboxes to select each attempt. */ - protected $includecheckboxes; + protected bool $includecheckboxes; /** @var string date format. */ - protected $strtimeformat; + protected string $strtimeformat; /** * Constructor. * * @param string $uniqueid - * @param object $quiz + * @param capquiz $capquiz * @param context $context - * @param capquiz_attempts_report_options $options + * @param options $options * @param sql_join $studentsjoins Contains joins, wheres, params * @param array $questions * @param moodle_url $reporturl */ - public function __construct($uniqueid, $quiz, context $context, capquiz_attempts_report_options $options, - sql_join $studentsjoins, $questions, $reporturl) { - parent::__construct($uniqueid); - $this->capquiz = $quiz; + public function __construct($uniqueid, capquiz $capquiz, context $context, options $options, + sql_join $studentsjoins, array $questions, moodle_url $reporturl) { + parent::__construct("capquiz_report_$uniqueid"); + $this->useridfield = 'userid'; + $this->capquiz = $capquiz; $this->context = $context; $this->studentsjoins = $studentsjoins; $this->questions = $questions; @@ -113,6 +105,7 @@ public function __construct($uniqueid, $quiz, context $context, capquiz_attempts /** * Generate the display of the checkbox column. + * * @param stdClass $attempt the table row being output. * @return string HTML content to go inside the td. */ @@ -124,24 +117,6 @@ public function col_checkbox(stdClass $attempt): string { } } - /** - * Generate the display of the user's full name column. - * - * @param object $attempt the table row being output. - */ - public function col_fullname($attempt): string { - $html = parent::col_fullname($attempt); - if ($this->is_downloading() || empty($attempt->attempt)) { - return $html; - } - return $html; - // phpcs:disable - /*. html_writer::empty_tag('br') . html_writer::link( - new moodle_url('/mod/capquiz/review.php', array('attempt' => $attempt->attempt)), - get_string('reviewattempt', 'quiz'), array('class' => 'reviewlink'))*/ - // phpcs:enable - } - /** * Generate the display of the time answered column. * @@ -156,27 +131,26 @@ public function col_timeanswered(stdClass $attempt): string { * * @param stdClass $attempt the table row being output. */ - public function col_timereviewed(stdClass $attempt) { + public function col_timereviewed(stdClass $attempt): string { return property_exists($attempt, 'attempt') ? userdate($attempt->timereviewed, $this->strtimeformat) : '-'; } - /** - * Generate the display of the question id column. + * Generate the display of the slot id column. * * @param stdClass $attempt the table row being output. */ - public function col_questionid(stdClass $attempt): string { - return $attempt->questionid ?: '-'; + public function col_slotid(stdClass $attempt): string { + return $attempt->slotid ?: '-'; } /** - * Generate the display of the moodle question rating column. + * Generate the display of the moodle question id column. * * @param stdClass $attempt the table row being output. */ - public function col_moodlequestionid(stdClass $attempt): string { - return $attempt->moodlequestionid ?: '-'; + public function col_questionid(stdClass $attempt): string { + return $attempt->questionid ?: '-'; } /** @@ -192,48 +166,33 @@ public function col_userid(stdClass $attempt): string { * Make a link to review an individual question in a popup window. * * @param string $data HTML fragment. The text to make into the link. - * @param object $attempt data for the row of the table being output. + * @param stdClass $attempt data for the row of the table being output. * @param int $slot the number used to identify this question within this usage. */ - public function make_review_link($data, $attempt, $slot): string { + public function make_review_link(string $data, stdClass $attempt, int $slot): string { $feedbackimg = ''; $state = $this->slot_state($attempt, $slot); if ($state->is_finished() && $state != question_state::$needsgrading) { $feedbackimg = $this->icon_for_fraction($this->slot_fraction($attempt, $slot)); } - - $output = html_writer::tag('span', $feedbackimg . html_writer::tag('span', - $data, ['class' => $state->get_state_class(true)]), ['class' => 'que']); - - $reviewparams = ['attempt' => $attempt->attempt, 'slot' => $slot]; - if (isset($attempt->try)) { - $reviewparams['step'] = $this->step_no_for_try($attempt->usageid, $slot, $attempt->try); - } - - // TODO enable this when capquiz implements a "review question attempt" page. - // phpcs:disable - /*$url = new moodle_url('/mod/capquiz/reviewquestion.php', $reviewparams); - $output = $OUTPUT->action_link($url, $output, - new popup_action('click', $url, 'reviewquestion', array('height' => 450, 'width' => 650)), - array('title' => get_string('reviewresponse', 'quiz')));*/ - // phpcs:enable - return $output; + return html_writer::tag('span', + $feedbackimg . html_writer::tag('span', $data, ['class' => $state->get_state_class(true)]), ['class' => 'que']); } /** * Make a link to review an individual question in a popup window. * * @param string $data HTML fragment. The text to make into the link. - * @param object $attempt data for the row of the table being output. + * @param stdClass $attempt data for the row of the table being output. * @param int $slot the number used to identify this question within this usage. */ - public function make_preview_link($data, $attempt, $slot) { + public function make_preview_link(string $data, stdClass $attempt, int $slot) { global $OUTPUT; $questionid = $this->slot_questionid($attempt, $slot); $output = html_writer::tag('span', html_writer::tag('span', $data), ['class' => 'que']); - $url = \qbank_previewquestion\helper::question_preview_url($questionid)->out(false); - return $OUTPUT->action_link($url, $output, new \popup_action('click', $url, 'previewquestion', [ - 'height' => 450, 'width' => 650]), ['title' => get_string('previewquestion', 'quiz')]); + $url = \qbank_previewquestion\helper::question_preview_url($questionid); + $action = new popup_action('click', $url, 'previewquestion', ['height' => 450, 'width' => 650]); + return $OUTPUT->action_link($url, $output, $action, ['title' => get_string('previewquestion', 'quiz')]); } /** @@ -256,7 +215,7 @@ protected function slot_state(stdClass $attempt, int $slot): question_state { */ protected function slot_questionid(stdClass $attempt, int $slot): int { $stepdata = $this->lateststeps[$attempt->usageid][$slot]; - return $stepdata->questionid; + return (int)$stepdata->questionid; } /** @@ -279,7 +238,7 @@ protected function icon_for_fraction(float $fraction): string { */ protected function slot_fraction(stdClass $attempt, int $slot): float { $stepdata = $this->lateststeps[$attempt->usageid][$slot]; - return $stepdata->fraction; + return (float)$stepdata->fraction; } /** @@ -304,22 +263,20 @@ public function setup_sql_queries(sql_join $allowedjoins): void { * @return array with 4 elements ($fields, $from, $where, $params) that can be used to * build the actual database query. */ - public function base_sql(sql_join $allowedstudentsjoins) { + public function base_sql(sql_join $allowedstudentsjoins): array { global $DB; - $fields = 'DISTINCT ' . $DB->sql_concat('u.id', "'#'", 'COALESCE(ca.id, 0)') . ' AS uniqueid,'; - $extrafields = \core_user\fields::for_identity($this->context) - ->including( - 'id', 'idnumber', 'firstname', 'lastname', 'picture', 'imagealt', 'institution', 'department', 'email' - ) + ->including('id', 'idnumber', 'firstname', 'lastname', 'picture', 'imagealt', 'institution', 'department', 'email') ->get_sql('u')->selects; - // phpcs:disable - // $allnames = get_all_user_name_fields(true, 'u'); - // phpcs:enable - $allnames = \core_user\fields::for_name()->with_identity($this->context)->get_sql('u')->selects; + + $allnames = \core_user\fields::for_name() + ->with_identity($this->context) + ->get_sql('u')->selects; + + $fields = 'DISTINCT ' . $DB->sql_concat('u.id', "'#'", 'COALESCE(ca.id, 0)') . ' AS uniqueid,'; $fields .= ' - cu.question_usage_id AS usageid, + cu.questionusageid AS usageid, ca.id AS attempt, u.id AS userid, u.idnumber' . $allnames . ', @@ -329,70 +286,69 @@ public function base_sql(sql_join $allowedstudentsjoins) { u.department, u.email' . $extrafields . ', ca.slot, - ca.time_answered AS timeanswered, - ca.time_reviewed AS timereviewed'; - - // This part is the same for all cases. Join the users and capquiz_attempts tables. - $from = " {user} u"; - $from .= "\nJOIN {capquiz_user} cu ON u.id = cu.user_id"; - $from .= "\nLEFT JOIN {capquiz_question_list} cql - ON cql.capquiz_id = :capquizid - AND cql.is_template = 0"; - - $from .= "\nJOIN {question_usages} qu ON qu.id = cu.question_usage_id"; - $from .= "\nJOIN {question_attempts} qa ON qa.questionusageid = qu.id"; - - $from .= "\nJOIN {capquiz_attempt} ca ON ca.user_id = cu.id AND ca.slot = qa.slot"; - $from .= "\nJOIN {capquiz_question} cq ON cq.question_list_id = cql.id AND cq.id = ca.question_id"; - - $params = ['capquizid' => $this->capquiz->id()]; + ca.timeanswered, + ca.timereviewed'; + + // This part is the same for all cases. Join the user and capquiz_attempt tables. + $from = ' {user} u + JOIN {capquiz_user} cu + ON cu.userid = u.id + AND cu.capquizid = :capquizid + JOIN {question_usages} qu + ON qu.id = cu.questionusageid + JOIN {question_attempts} qa + ON qa.questionusageid = qu.id + JOIN {capquiz_attempt} ca + ON ca.capquizuserid = cu.id + AND ca.slot = qa.slot + JOIN {capquiz_slot} cs2 + ON cs2.id = ca.slotid'; + + $params = ['capquizid' => $this->capquiz->get('id')]; switch ($this->options->attempts) { - case capquiz_attempts_report::ALL_WITH: + case options::ALL_WITH: // Show all attempts, including students who are no longer in the course. $where = 'ca.id IS NOT NULL'; break; - case capquiz_attempts_report::ENROLLED_WITH: + case options::ENROLLED_WITH: // Show only students with attempts. $from .= "\n" . $allowedstudentsjoins->joins; $where = "ca.id IS NOT NULL AND " . $allowedstudentsjoins->wheres; $params = array_merge($params, $allowedstudentsjoins->params); break; - // phpcs:disable - /* - case capquiz_attempts_report::ENROLLED_WITHOUT: + case options::ENROLLED_WITHOUT: // Show only students without attempts. $from .= "\n" . $allowedstudentsjoins->joins; $where = "ca.id IS NULL AND " . $allowedstudentsjoins->wheres; $params = array_merge($params, $allowedstudentsjoins->params); break; - case capquiz_attempts_report::ENROLLED_ALL: + case options::ENROLLED_ALL: // Show all students with or without attempts. $from .= "\n" . $allowedstudentsjoins->joins; $where = $allowedstudentsjoins->wheres; $params = array_merge($params, $allowedstudentsjoins->params); break; - */ - // phpcs:enable + default: + return [$fields, $from, '', $params]; } if ($this->options->onlyanswered) { $where .= " AND ca.answered = 1"; } - return [$fields, $from, $where, $params]; } /** - * A chance for subclasses to modify the SQL after the count query has been generated, - * and before the full query is constructed. + * A chance for subclasses to modify the SQL after the count query is generated, and before the full query is constructed. + * * @param string $fields SELECT list. * @param string $from JOINs part of the SQL. * @param string $where WHERE clauses. * @param array $params Query params. * @return array with 4 elements ($fields, $from, $where, $params) as from base_sql. */ - protected function update_sql_after_count($fields, $from, $where, $params) { + protected function update_sql_after_count(string $fields, string $from, string $where, array $params): array { return [$fields, $from, $where, $params]; } @@ -400,32 +356,21 @@ protected function update_sql_after_count($fields, $from, $where, $params) { * Query the db. Store results in the table object for use by build_table. * * @param int $pagesize size of page for paginated displayed table. - * @param bool $useinitialsbar do you want to use the initials bar. Bar - * will only be used if there is a fullname column defined for the table. + * @param bool $useinitialsbar do you want to use the initials bar (only used if there is a fullname column) */ public function query_db($pagesize, $useinitialsbar = true): void { parent::query_db($pagesize, $useinitialsbar); - if ($this->requires_extra_data()) { + if ($this->requires_latest_steps_loaded()) { $this->load_extra_data(); } } - /** - * Does this report require loading any more data after the main query. After the main query then - * you can use $this->get - * - * @return bool should {@see query_db()} call {@see load_extra_data}? - */ - protected function requires_extra_data() { - return $this->requires_latest_steps_loaded(); - } - /** * Does this report require the detailed information for each question from the question_attempts_steps table? * * @return bool should {@see load_extra_data} call {@see load_question_latest_steps}? */ - protected function requires_latest_steps_loaded() { + protected function requires_latest_steps_loaded(): bool { return false; } @@ -449,10 +394,9 @@ protected function load_question_latest_steps(?qubaid_condition $qubaids = null) if ($qubaids === null) { $qubaids = $this->get_qubaids_condition(); } - $dm = new question_engine_data_mapper(); - $latesstepdata = $dm->load_questions_usages_latest_steps($qubaids, array_map(fn($o) => $o->slot, $this->questions)); $lateststeps = []; - foreach ($latesstepdata as $step) { + $dm = new question_engine_data_mapper(); + foreach ($dm->load_questions_usages_latest_steps($qubaids, array_map(fn($o) => $o->slot, $this->questions)) as $step) { $lateststeps[$step->questionusageid][$step->slot] = $step; } return $lateststeps; @@ -482,8 +426,8 @@ protected function get_qubaids_condition(): qubaid_list { * @return array column name => SORT_... constant. */ public function get_sort_columns(): array { - // Add attemptid as a final tie-break to the sort. This ensures that - // Attempts by the same student appear in order when just sorting by name. + // Add attemptid as a final tie-break to the sort. + // This ensures that attempts by the same student appear in order when just sorting by name. $sortcolumns = parent::get_sort_columns(); $sortcolumns['attempt'] = SORT_ASC; return $sortcolumns; @@ -492,7 +436,7 @@ public function get_sort_columns(): array { /** * Wrap start of table */ - public function wrap_html_start() { + public function wrap_html_start(): void { if ($this->is_downloading() || !$this->includecheckboxes) { return; } @@ -505,16 +449,13 @@ public function wrap_html_start() { } /** - * End of table wrap - * - * @throws coding_exception + * End of table wrap. */ - public function wrap_html_finish() { + public function wrap_html_finish(): void { global $PAGE; if ($this->is_downloading() || !$this->includecheckboxes) { return; } - echo '
'; echo '' . get_string('selectall', 'quiz') . ' / '; echo '' . get_string('selectnone', 'quiz') . ' '; @@ -530,39 +471,8 @@ public function wrap_html_finish() { }); });"); echo '  '; - - // TODO enable when support for attempt deletion is added {@see delete_selected_attempts}. - // phpcs:disable - // $this->submit_buttons(); - // phpcs:enable echo '
'; - - // Close the form. echo ''; echo ''; } - - /** - * Is this a column that depends on joining to the latest state information? - * If so, return the corresponding slot. If not, return false. - * @param string $column a column name - * @return int false if no, else a slot. - */ - protected function is_latest_step_column($column) { - return false; - } - - /** - * Output any submit buttons required by the $this->includecheckboxes form. - */ - protected function submit_buttons() { - global $PAGE; - if (has_capability('mod/capquiz:deleteattempts', $this->context)) { - echo ''; - $PAGE->requires->event_handler('#deleteattemptsbutton', 'click', 'M.util.show_confirm_dialog', - ['message' => get_string('deleteattemptcheck', 'quiz')]); - } - } - } diff --git a/classes/matchmaking/capquiz_matchmaking_strategy_registry.php b/classes/matchmaking/capquiz_matchmaking_strategy_registry.php deleted file mode 100755 index 1ec206e..0000000 --- a/classes/matchmaking/capquiz_matchmaking_strategy_registry.php +++ /dev/null @@ -1,155 +0,0 @@ -. - -/** - * This file defines a class acting as a registry for matchmaking strategies - * - * @package mod_capquiz - * @author Aleksander Skrede - * @copyright 2018 NTNU - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - -namespace mod_capquiz; - -use coding_exception; -use moodle_url; -use stdClass; - -defined('MOODLE_INTERNAL') || die(); - -require_once($CFG->dirroot . '/mod/capquiz/classes/capquiz_matchmaking_strategy.php'); -require_once($CFG->dirroot . '/mod/capquiz/classes/matchmaking/chronologic/chronologic_selector.php'); -require_once($CFG->dirroot . '/mod/capquiz/classes/matchmaking/n_closest/n_closest_selector.php'); -require_once($CFG->dirroot . '/mod/capquiz/classes/matchmaking/n_closest/n_closest_configuration_form.php'); - -/** - * Class capquiz_matchmaking_strategy_registry - * - * @package mod_capquiz - * @author Aleksander Skrede - * @copyright 2018 NTNU - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -class capquiz_matchmaking_strategy_registry { - - /** @var capquiz $capquiz */ - private capquiz $capquiz; - - /** @var array $strategies */ - private array $strategies; - - /** - * Constructor. - * - * @param capquiz $capquiz - */ - public function __construct(capquiz $capquiz) { - $this->capquiz = $capquiz; - $this->register_selection_strategies(); - } - - /** - * Returns the specified matchmaking strategy or throws an error if it does not exist - * - * @param string $strategy - */ - public function selector(string $strategy): capquiz_matchmaking_strategy { - $value = $this->strategies[$strategy]; - if ($value) { - return array_values($value)[0](); - } - $this->throw_strategy_exception($strategy); - } - - /** - * Returns a configuration form for the matchmaking strategy - * - * @param string $strategy - * @param stdClass $config - * @param moodle_url $url - */ - public function configuration_form(string $strategy, stdClass $config, moodle_url $url) { - $value = $this->strategies[$strategy]; - if ($value) { - $configfunc = array_values($value)[1]; - return $configfunc($url, $config); - } - $this->throw_strategy_exception($strategy); - } - - /** - * Returns true if the registry has the specified strategy - * - * @param string $strategy - */ - public function has_strategy(string $strategy): bool { - return isset($this->strategies[$strategy]); - } - - /** - * Returns the default selection strategy - */ - public function default_selection_strategy(): string { - // The default selection strategy is added first. - // Modify capquiz_matchmaking_strategy_registry::register_selection_strategies() to change this. - $selectionstrategies = $this->selection_strategies(); - return reset($selectionstrategies); - } - - /** - * Returns all selection strategies' names - * - * @return string[] - */ - public function selection_strategies(): array { - $names = []; - foreach (array_keys($this->strategies) as $value) { - $names[] = $value; - } - return $names; - } - - /** - * Registers the selection strategies, the first registered will be the default strategy - */ - private function register_selection_strategies(): void { - // The first listed will be selected by default when creating a new activity. - $capquiz = $this->capquiz; - $this->strategies = [ - 'N-closest' => [ - fn() => new n_closest_selector($capquiz), - fn(moodle_url $url, stdClass $config) => new n_closest_configuration_form($config, $url), - ], - 'Chronological' => [ - fn() => new chronologic_selector(), - fn(moodle_url $url, stdClass $config) => null, - ], - ]; - } - - /** - * Creates and throws a strategy exception - * - * @param string $strategy - */ - private function throw_strategy_exception(string $strategy) { - $msg = "The specified strategy '$strategy' does not exist."; - $msg .= " Options are {'" . implode("', '", $this->selection_strategies()); - $msg .= "'}. This issue must be fixed by a programmer"; - throw new coding_exception($msg); - } -} diff --git a/classes/matchmaking/chronologic/chronologic_selector.php b/classes/matchmaking/chronologic/chronologic_selector.php deleted file mode 100755 index f09e45a..0000000 --- a/classes/matchmaking/chronologic/chronologic_selector.php +++ /dev/null @@ -1,86 +0,0 @@ -. - -/** - * This file defines a class which acts as a selector for the chronologic matchmaking strategy - * - * @package mod_capquiz - * @author Aleksander Skrede - * @copyright 2018 NTNU - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - -namespace mod_capquiz; - -use stdClass; - -/** - * Class chronologic_selector - * - * @package mod_capquiz - * @author Aleksander Skrede - * @copyright 2018 NTNU - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -class chronologic_selector extends capquiz_matchmaking_strategy { - - /** - * Nothing to configure - * - * @param stdClass $config - */ - public function configure(stdClass $config): void { - } - - /** - * No configuration needed - */ - public function configuration(): stdClass { - return new stdClass(); - } - - /** - * No configuration needed - */ - public function default_configuration(): stdClass { - return new stdClass(); - } - - /** - * Returns the next question for the user in a chronological order - * - * @param capquiz_user $user - * @param capquiz_question_list $qlist - * @param capquiz_question_attempt[] $inactiveattempts - */ - public function next_question_for_user(capquiz_user $user, capquiz_question_list $qlist, - array $inactiveattempts): ?capquiz_question { - $answered = function (capquiz_question $q) use ($inactiveattempts) { - foreach ($inactiveattempts as $inactiveattempt) { - if ($inactiveattempt->question_id() === $q->id()) { - return true; - } - } - return false; - }; - foreach ($qlist->questions() as $question) { - if (!$answered($question)) { - return $question; - } - } - return null; - } -} diff --git a/classes/matchmaking/n_closest/n_closest_configuration_form.php b/classes/matchmaking/n_closest/n_closest_configuration_form.php deleted file mode 100755 index 6d91a37..0000000 --- a/classes/matchmaking/n_closest/n_closest_configuration_form.php +++ /dev/null @@ -1,111 +0,0 @@ -. - -/** - * This file defines the configuration form for the n_closest matchmaking strategy - * - * @package mod_capquiz - * @author Aleksander Skrede - * @copyright 2018 NTNU - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - -namespace mod_capquiz; - -use moodle_url; -use stdClass; - -defined('MOODLE_INTERNAL') || die(); - -require_once($CFG->libdir . '/formslib.php'); - -/** - * Class n_closest_configuration_form - * - * @package mod_capquiz - * @author Aleksander Skrede - * @copyright 2018 NTNU - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -class n_closest_configuration_form extends \moodleform { - - /** @var stdClass $configuration */ - private stdClass $configuration; - - /** - * Constructor. - * - * @param stdClass $configuration - * @param moodle_url $url - */ - public function __construct(stdClass $configuration, moodle_url $url) { - $this->configuration = $configuration; - parent::__construct($url); - } - - /** - * Defines form - */ - public function definition(): void { - $form = $this->_form; - - $form->addElement('text', 'number_of_questions_to_select', get_string('number_of_questions_to_select', 'capquiz')); - $form->setType('number_of_questions_to_select', PARAM_INT); - $form->setDefault('number_of_questions_to_select', $this->configuration->number_of_questions_to_select); - $form->addRule('number_of_questions_to_select', get_string('number_of_questions_to_select_required', 'capquiz'), - 'required', null, 'client'); - $form->addHelpButton('number_of_questions_to_select', 'number_of_questions_to_select', 'capquiz'); - - $form->addElement('text', 'user_win_probability', get_string('user_win_probability', 'capquiz')); - $form->setType('user_win_probability', PARAM_FLOAT); - $form->setDefault('user_win_probability', $this->configuration->user_win_probability); - $form->addRule('user_win_probability', get_string('user_win_probability_required', 'capquiz'), - 'required', null, 'client'); - $form->addHelpButton('user_win_probability', 'user_win_probability', 'capquiz'); - - $form->addElement('text', 'prevent_same_question_for_turns', get_string('prevent_question_n_times', 'capquiz')); - $form->setType('prevent_same_question_for_turns', PARAM_INT); - $form->setDefault('prevent_same_question_for_turns', $this->configuration->prevent_same_question_for_turns); - $form->addRule('prevent_same_question_for_turns', get_string('field_required', 'capquiz'), - 'required', null, 'client'); - $form->addHelpButton('prevent_same_question_for_turns', 'prevent_question_n_times', 'capquiz'); - - $this->add_action_buttons(false); - } - - /** - * Validate the data from the form. - * - * @param array $data array of ("fieldname"=>value) of submitted data - * @param array $files array of uploaded files "element_name"=>tmp_file_path - * @return array of "element_name"=>"error_description" if there are errors, - * or an empty array if everything is OK (true allowed for backwards compatibility too). - */ - public function validations($data, $files): array { - $errors = []; - if (empty($data['user_win_probability'])) { - $errors['user_win_probability'] = get_string('user_win_probability_required', 'capquiz'); - } - if (empty($data['number_of_questions'])) { - $errors['number_of_questions'] = get_string('number_of_questions_to_select_required', 'capquiz'); - } - if (empty($data['prevent_same_question_for_turns'])) { - $errors['prevent_same_question_for_turns'] = get_string('field_required', 'capquiz'); - } - return $errors; - } - -} diff --git a/classes/matchmaking/n_closest/n_closest_selector.php b/classes/matchmaking/n_closest/n_closest_selector.php deleted file mode 100755 index 4a2221a..0000000 --- a/classes/matchmaking/n_closest/n_closest_selector.php +++ /dev/null @@ -1,174 +0,0 @@ -. - -/** - * This file defines a class which acts as a selector for the n_closest matchmaking strategy - * - * @package mod_capquiz - * @author Aleksander Skrede - * @copyright 2018 NTNU - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - -namespace mod_capquiz; - -use stdClass; - -/** - * Class n_closest_selector - * - * @package mod_capquiz - * @author Aleksander Skrede - * @copyright 2018 NTNU - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -class n_closest_selector extends capquiz_matchmaking_strategy { - - /** @var capquiz The capquiz */ - private capquiz $capquiz; - - /** @var float The propability of the user winning */ - private float $userwinprobability; - - /** @var int The number of questions to select */ - private int $numquestionstoselect; - - /** @var int The number of turns between each time a question can be selected */ - private int $preventsamequestionforturns; - - /** - * Constructor. - * - * @param capquiz $capquiz - */ - public function __construct(capquiz $capquiz) { - $this->capquiz = $capquiz; - $this->configure($this->default_configuration()); - } - - /** - * Configure the strategy - * - * @param stdClass $configuration - */ - public function configure(stdClass $configuration): void { - if ($configuration->user_win_probability > 0) { - $this->userwinprobability = $configuration->user_win_probability; - } - if ($configuration->number_of_questions_to_select > 0) { - $this->numquestionstoselect = $configuration->number_of_questions_to_select; - } - if ($configuration->prevent_same_question_for_turns >= 0) { - $this->preventsamequestionforturns = $configuration->prevent_same_question_for_turns; - } - } - - /** - * Returns the current strategy configuration - */ - public function configuration(): stdClass { - $config = new stdClass; - $config->prevent_same_question_for_turns = $this->preventsamequestionforturns; - $config->user_win_probability = $this->userwinprobability; - $config->number_of_questions_to_select = $this->numquestionstoselect; - return $config; - } - - /** - * Returns the default strategy configuration - */ - public function default_configuration(): stdClass { - $config = new stdClass; - $config->user_win_probability = 0.75; - $config->prevent_same_question_for_turns = 0; - $config->number_of_questions_to_select = 10; - return $config; - } - - /** - * Selects the next question for the user based on the configuration - * - * @param capquiz_user $user - * @param capquiz_question_list $qlist - * @param array $inactiveattempts - */ - public function next_question_for_user(capquiz_user $user, capquiz_question_list $qlist, - array $inactiveattempts): ?capquiz_question { - $excluded = $this->determine_excluded_questions($inactiveattempts); - $candidates = $this->find_questions_closest_to_rating($user, $excluded); - if (count($candidates) === 0) { - return null; - } - $index = mt_rand(0, count($candidates) - 1); - if ($question = $candidates[$index]) { - return $question; - } - return null; - } - - /** - * Finds the questions closest to the users rating - * - * @param capquiz_user $user - * @param array $excludedquestions - */ - private function find_questions_closest_to_rating(capquiz_user $user, array $excludedquestions): array { - global $DB; - $sql = 'SELECT * FROM {capquiz_question} WHERE question_list_id = ?'; - $sql .= str_repeat(' AND id <> ?', count($excludedquestions)); - $sql .= ' ORDER BY ABS(? - rating)'; - $params = []; - $params[] = $this->capquiz->question_list()->id(); - if (count($excludedquestions) > 0) { - array_push($params, ...$excludedquestions); - } - $params[] = $this->ideal_question_rating($user); - $questionentries = $DB->get_records_sql($sql, $params, 0, $this->numquestionstoselect); - $questions = []; - foreach ($questionentries as $questionentry) { - $questions[] = new capquiz_question($questionentry); - } - return $questions; - } - - /** - * Returns the ideal question rating - * - * @param capquiz_user $user - */ - private function ideal_question_rating(capquiz_user $user): float { - return 400.0 * log((1.0 / $this->userwinprobability) - 1.0, 10.0) + $user->rating(); - } - - /** - * Identifies questions to exclude and returns them in an array - * - * @param capquiz_question_attempt[] $inactiveattempts - */ - private function determine_excluded_questions(array $inactiveattempts): array { - $it = new \ArrayIterator(array_reverse($inactiveattempts, true)); - $excluded = []; - for ($i = 0; $i < $this->preventsamequestionforturns; $i++) { - if (!$it->valid()) { - break; - } - $excluded[] = $it->current()->question_id(); - $it->next(); - } - return array_unique($excluded); - } - -} diff --git a/classes/output/attempt.php b/classes/output/attempt.php new file mode 100755 index 0000000..b93e3ed --- /dev/null +++ b/classes/output/attempt.php @@ -0,0 +1,105 @@ +. + +namespace mod_capquiz\output; + +use core\output\renderable; +use core\output\renderer_base; +use core\output\templatable; +use mod_capquiz\capquiz; +use mod_capquiz\capquiz_user; +use mod_capquiz\capquiz_attempt; +use mod_capquiz\local\helpers\questions; +use mod_capquiz\local\helpers\stars; +use moodle_url; +use question_engine; + +/** + * Render question attempt. + * + * @package mod_capquiz + * @author Sebastian Gundersen + * @copyright 2024 Norwegian University of Science and Technology (NTNU) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class attempt implements renderable, templatable { + + /** @var capquiz_attempt $attempt */ + private capquiz_attempt $attempt; + + /** @var capquiz_user $user */ + private capquiz_user $user; + + /** @var capquiz $capquiz */ + private capquiz $capquiz; + + /** + * Constructor. + * + * @param capquiz_attempt $attempt + * @param capquiz_user $user + * @param capquiz $capquiz + */ + public function __construct(capquiz_attempt $attempt, capquiz_user $user, capquiz $capquiz) { + $this->attempt = $attempt; + $this->user = $user; + $this->capquiz = $capquiz; + } + + /** + * Render the question attempt. + * + * @param renderer_base $output + * @return bool|string + */ + public function render(renderer_base $output): bool|string { + return $output->render_from_template('capquiz/attempt', $this->export_for_template($output)); + } + + /** + * Export parameters for template. + * + * @param renderer_base $output + * @return array + */ + public function export_for_template(renderer_base $output): array { + $output->get_page()->requires->js_module('core_question_engine'); + $qubaslot = $this->attempt->get('slot'); + $quba = $this->user->get_question_usage(); + // This adds the necessary JS and CSS for the question type. + $quba->render_question_head_html($qubaslot); + question_engine::initialise_js(); + $cm = $this->capquiz->get_cm(); + return [ + 'header' => $output->render(new attempt_header($this->user)), + 'attempt' => [ + 'url' => (new moodle_url('/mod/capquiz/attempt.php', ['id' => $cm->id, 'action' => 'submit']))->out(false), + 'body' => $quba->render_question($qubaslot, questions::get_question_display_options($this->capquiz)), + 'slots' => $this->attempt->get('slot'), + ], + 'gradingdone' => $this->capquiz->is_past_due_time(), + 'finalgrade' => $this->user->get('starsgraded'), + 'gradingpass' => stars::is_user_passing($this->user, $this->capquiz), + 'duedate' => userdate($this->capquiz->get('timedue'), get_string('strftimedatetime', 'langconfig')), + 'reviewbutton' => $this->attempt->get('answered') ? [ + 'type' => 'primary', + 'method' => 'post', + 'url' => (new moodle_url('/mod/capquiz/attempt.php', ['id' => $cm->id, 'action' => 'review']))->out(false), + 'label' => get_string('next'), + ] : [], + ]; + } +} diff --git a/classes/output/attempt_header.php b/classes/output/attempt_header.php new file mode 100644 index 0000000..40f0d4d --- /dev/null +++ b/classes/output/attempt_header.php @@ -0,0 +1,86 @@ +. + +namespace mod_capquiz\output; + +use core\output\renderable; +use core\output\renderer_base; +use mod_capquiz\capquiz; +use mod_capquiz\capquiz_user; +use mod_capquiz\local\helpers\stars; +use templatable; + +/** + * Question attempt header displaying an overview of stars achieved, lost, and not yet reached. + * + * @package mod_capquiz + * @author Sebastian Gundersen + * @copyright 2024 Norwegian University of Science and Technology (NTNU) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class attempt_header implements renderable, templatable { + + /** @var capquiz_user $user */ + private capquiz_user $user; + + /** + * Constructor. + * + * @param capquiz_user $user + */ + public function __construct(capquiz_user $user) { + $this->user = $user; + } + + /** + * Render the classlist. + * + * @param renderer_base $output + * @return bool|string + */ + public function render(renderer_base $output): bool|string { + return $output->render_from_template('capquiz/attempt_header', $this->export_for_template($output)); + } + + /** + * Export parameters for template. + * + * @param renderer_base $output + * @return array + */ + public function export_for_template(renderer_base $output): array { + $capquiz = new capquiz($this->user->get('capquizid')); + $percent = stars::get_percent_to_next_star($capquiz, $this->user->get('rating')); + $stars = []; + $starratings = $capquiz->get('starratings'); + for ($star = 1; $star <= $capquiz->get_max_stars(); $star++) { + if ($this->user->get('higheststars') >= $star) { + if ($this->user->get('rating') >= stars::get_required_rating_for_star($starratings, $star)) { + $stars[] = ['icon' => 'star', 'tooltip' => get_string('tooltip_achieved_star', 'capquiz')]; + } else { + $stars[] = ['icon' => 'blank-star', 'tooltip' => get_string('tooltip_lost_star', 'capquiz')]; + } + } else { + $stars[] = ['icon' => 'no-star', 'tooltip' => get_string('tooltip_no_star', 'capquiz')]; + } + } + return [ + 'percentup' => max($percent, 0), + 'percentdown' => min($percent, 0), + 'stars' => $stars, + ]; + } +} diff --git a/classes/output/basic_renderer.php b/classes/output/basic_renderer.php deleted file mode 100755 index 2309030..0000000 --- a/classes/output/basic_renderer.php +++ /dev/null @@ -1,79 +0,0 @@ -. - -/** - * This file defines a class used to render buttons - * - * @package mod_capquiz - * @author Aleksander Skrede - * @copyright 2018 NTNU - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - -namespace mod_capquiz\output; - -use mod_capquiz\capquiz_urls; -use moodle_url; -use renderer_base; - -/** - * Class basic_renderer - * - * @package mod_capquiz - * @author Aleksander Skrede - * @copyright 2018 NTNU - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -class basic_renderer { - /** - * Renders the home button - * - * @param renderer_base $renderer - */ - public static function render_home_button(renderer_base $renderer): string { - $url = capquiz_urls::redirect(capquiz_urls::view_url()); - return self::render_action_button($renderer, $url, get_string('home', 'capquiz')); - } - - /** - * Renders a button - * - * @param renderer_base $renderer - * @param moodle_url $url - * @param string $label - * @param string $httpmethod The HTTP method to use for the form - * @param string[] $params The keys are used as names - * @param string $id - */ - public static function render_action_button(renderer_base $renderer, moodle_url $url, string $label, - string $httpmethod = 'post', array $params = [], string $id = ''): string { - $paramobjects = []; - foreach ($params as $name => $value) { - $paramobjects = [ - 'name' => $name, - 'value' => $value, - ]; - } - return $renderer->render_from_template('core/single_button', [ - 'type' => 'primary', - 'method' => $httpmethod, - 'url' => $url->out(false), - 'label' => $label, - 'params' => $paramobjects, - 'id' => $id, - ]); - } -} diff --git a/classes/output/classlist.php b/classes/output/classlist.php new file mode 100644 index 0000000..1c53488 --- /dev/null +++ b/classes/output/classlist.php @@ -0,0 +1,100 @@ +. + +namespace mod_capquiz\output; + +use core\output\renderer_base; +use mod_capquiz\capquiz; +use mod_capquiz\capquiz_user; +use moodle_url; +use renderable; +use templatable; + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->dirroot . '/question/editlib.php'); + +/** + * Render student leaderboard/classlist. + * + * @package mod_capquiz + * @author Aleksander Skrede + * @author Sebastian Gundersen + * @copyright 2024 Norwegian University of Science and Technology (NTNU) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class classlist implements renderable, templatable { + + /** @var capquiz $capquiz */ + private capquiz $capquiz; + + /** + * Constructor. + * + * @param capquiz $capquiz + */ + public function __construct(capquiz $capquiz) { + $this->capquiz = $capquiz; + } + + /** + * Render the classlist. + * + * @param renderer_base $output + * @return bool|string + */ + public function render(renderer_base $output): bool|string { + return $output->render_from_template('capquiz/classlist', $this->export_for_template($output)); + } + + /** + * Export parameters for template. + * + * @param renderer_base $output + * @return array + */ + public function export_for_template(renderer_base $output): array { + global $DB; + $rows = []; + $users = capquiz_user::get_records(['capquizid' => $this->capquiz->get('id')], 'rating', 'DESC'); + $userids = array_map(fn(capquiz_user $user) => $user->get('userid'), $users); + $coreusers = $DB->get_records_list('user', 'id', $userids, '', 'id,username,firstname,lastname'); + foreach ($users as $user) { + $coreuser = $coreusers[$user->get('userid')] ?? null; + $rows[] = [ + 'username' => $coreuser?->username, + 'firstname' => $coreuser?->firstname, + 'lastname' => $coreuser?->lastname, + 'rating' => round($user->get('rating'), 2), + 'stars' => $user->get('higheststars'), + 'gradedstars' => $user->get('starsgraded'), + 'passinggrade' => $user->get('starsgraded') >= $this->capquiz->get('starstopass'), + ]; + } + $cmid = $this->capquiz->get_cmid(); + return [ + 'usercount' => count($rows), + 'users' => $rows, + 'regrade' => [ + 'type' => 'primary', + 'method' => 'post', + 'url' => (new moodle_url('/mod/capquiz/edit.php', ['id' => $cmid, 'action' => 'regradeall']))->out(false), + 'label' => get_string('regrade_all', 'capquiz'), + 'disabled' => !$this->capquiz->is_past_due_time(), + ], + ]; + } +} diff --git a/classes/output/classlist_renderer.php b/classes/output/classlist_renderer.php deleted file mode 100755 index 17d600c..0000000 --- a/classes/output/classlist_renderer.php +++ /dev/null @@ -1,105 +0,0 @@ -. - -/** - * This file defines a class used to render a capquiz' classlist - * - * @package mod_capquiz - * @author Aleksander Skrede - * @author Sebastian S. Gundersen - * @copyright 2019 NTNU - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - -namespace mod_capquiz\output; - -use mod_capquiz\capquiz; -use mod_capquiz\capquiz_urls; -use mod_capquiz\capquiz_user; -use moodle_page; -use renderer_base; - -defined('MOODLE_INTERNAL') || die(); - -require_once($CFG->dirroot . '/question/editlib.php'); - -/** - * Class classlist_renderer used for rendering a capquiz' class in the form of a list/leaderboard - * - * @package mod_capquiz - * @author Aleksander Skrede - * @author Sebastian S. Gundersen - * @copyright 2019 NTNU - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -class classlist_renderer { - - /** @var capquiz $capquiz */ - private capquiz $capquiz; - - /** @var renderer_base $renderer */ - private renderer_base $renderer; - - /** @var moodle_page $page */ - private moodle_page $page; - - /** - * Constructor. - * - * @param capquiz $capquiz The capquiz whose classlist should be rendered - * @param renderer_base $renderer The renderer used to render the classlist - */ - public function __construct(capquiz $capquiz, renderer_base $renderer) { - $this->capquiz = $capquiz; - $this->renderer = $renderer; - $this->page = $capquiz->get_page(); - } - - /** - * Renders the entire classlist of the $capquiz in the constructor - */ - public function render(): bool|string { - $cmid = $this->capquiz->course_module()->id; - $this->page->requires->js_call_amd('mod_capquiz/edit_questions', 'initialize', [$cmid]); - $users = capquiz_user::list_users($this->capquiz->id(), $this->capquiz->context()); - $rows = []; - for ($i = 0; $i < count($users); $i++) { - $user = $users[$i]; - $rows[] = [ - 'index' => $i + 1, - 'username' => $user->username(), - 'firstname' => $user->first_name(), - 'lastname' => $user->last_name(), - 'rating' => round($user->rating(), 2), - 'stars' => $user->highest_stars_achieved(), - 'graded_stars' => $user->highest_stars_graded(), - 'passing_grade' => $user->highest_stars_graded() >= $this->capquiz->stars_to_pass(), - ]; - } - return $this->renderer->render_from_template('capquiz/classlist', [ - 'users' => $rows, - 'regrade' => [ - 'type' => 'primary', - 'method' => 'post', - 'classes' => 'capquiz-regrade-all', - 'url' => capquiz_urls::regrade_all_url()->out(false), - 'label' => get_string('regrade_all', 'capquiz'), - 'disabled' => !$this->capquiz->is_grading_completed(), - ], - ]); - } - -} diff --git a/classes/output/edit_questions.php b/classes/output/edit_questions.php new file mode 100644 index 0000000..536d595 --- /dev/null +++ b/classes/output/edit_questions.php @@ -0,0 +1,142 @@ +. + +namespace mod_capquiz\output; + +use mod_capquiz\capquiz; +use mod_capquiz\capquiz_slot; +use moodle_url; +use renderer_base; + +/** + * Edit question list. + * + * @package mod_capquiz + * @author Sebastian Gundersen + * @copyright 2024 Norwegian University of Science and Technology (NTNU) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class edit_questions implements \renderable, \templatable { + + /** @var capquiz $capquiz */ + private capquiz $capquiz; + + /** + * Constructor. + * + * @param capquiz $capquiz + */ + public function __construct(capquiz $capquiz) { + $this->capquiz = $capquiz; + } + + /** + * Render question list. + * + * @param renderer_base $output + * @return bool|string + */ + public function render(renderer_base $output): bool|string { + return $output->render_from_template('capquiz/edit_questions', $this->export_for_template($output)); + } + + /** + * Export parameters for template. + * + * @param renderer_base $output + * @return array + */ + public function export_for_template(renderer_base $output): array { + $cmid = $this->capquiz->get_cmid(); + $context = \context_module::instance($cmid); + $output->get_page()->requires->js_call_amd('mod_capquiz/edit_slots', 'init', [$cmid]); + $output->get_page()->requires->js_call_amd('mod_capquiz/qbank_modal', 'init', [$context->id]); + $rows = []; + $editstr = get_string('edit'); + $previewstr = get_string('preview'); + $deletestr = get_string('remove'); + foreach (capquiz_slot::get_records(['capquizid' => $this->capquiz->get('id')], 'rating') as $slot) { + $editaction = false; + $previewaction = false; + $deleteaction = false; + $relatedcourse = $slot->find_related_course(); + $question = $slot->find_question(); + if ($question && $relatedcourse) { + $editaction = [ + 'url' => (new moodle_url('/question/bank/editquestion/question.php', [ + 'cmid' => $cmid, + 'id' => $question->id, + ]))->out(false), + 'label' => $editstr, + 'icon' => [ + 'key' => 'i/edit', + 'title' => $editstr, + ], + 'attributes' => [['name' => 'target', 'value' => '_blank']], + ]; + $previewaction = [ + 'url' => \qbank_previewquestion\helper::question_preview_url($question->id)->out(false), + 'label' => $previewstr, + 'icon' => [ + 'key' => 'e/find_replace', + 'title' => $previewstr, + ], + 'attributes' => [['name' => 'target', 'value' => '_blank']], + ]; + $deleteaction = [ + 'url' => (new moodle_url('/mod/capquiz/edit.php', [ + 'id' => $cmid, + 'action' => 'deleteslot', + 'slotid' => $slot->get('id'), + ]))->out(false), + 'label' => $deletestr, + 'icon' => [ + 'key' => 'e/delete', + 'title' => $deletestr, + ], + ]; + } + $questionversion = $slot->find_question_version(); + $rows[] = [ + 'name' => $question?->name ?? get_string('cannotloadquestion', 'question'), + 'rating' => round($slot->get('rating'), 2), + 'slotid' => $slot->get('id'), + 'version' => $questionversion->version, + 'latestversion' => $questionversion->referencedversion === null, + 'deleteaction' => $deleteaction, + 'editaction' => $editaction, + 'previewaction' => $previewaction, + ]; + } + $menu = new \action_menu(); + $trigger = \html_writer::tag('span', get_string('add')); + $menu->set_menu_trigger($trigger); + + $fromqbankstring = get_string('fromquestionbank', 'capquiz'); + $icon = new \pix_icon('t/add', $fromqbankstring, 'moodle', ['class' => 'iconsmall', 'title' => '']); + $qbankaction = new \action_menu_link_secondary($output->get_page()->url, $icon, $fromqbankstring, [ + 'class' => 'questionbank', + 'data-header' => $fromqbankstring, + 'data-action' => 'questionbank', + ]); + + $menu->add($qbankaction); + return [ + 'slots' => $rows, + 'actionmenu' => $output->render($menu), + ]; + } +} diff --git a/classes/output/grading_configuration_renderer.php b/classes/output/grading_configuration_renderer.php deleted file mode 100644 index 426669a..0000000 --- a/classes/output/grading_configuration_renderer.php +++ /dev/null @@ -1,121 +0,0 @@ -. - -/** - * This file defines a class used to render the grading configuration view - * - * @package mod_capquiz - * @author Sebastian S. Gundersen - * @copyright 2019 NTNU - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - -namespace mod_capquiz\output; - -use mod_capquiz\capquiz; -use mod_capquiz\capquiz_urls; -use mod_capquiz\form\view\grading_configuration_form; -use moodle_page; -use stdClass; - -defined('MOODLE_INTERNAL') || die(); - -require_once($CFG->dirroot . '/question/editlib.php'); - -/** - * Class grading_configuration_renderer - * - * @package mod_capquiz - * @author Sebastian S. Gundersen - * @copyright 2019 NTNU - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -class grading_configuration_renderer { - - /** @var capquiz $capquiz */ - private capquiz $capquiz; - - /** @var renderer $renderer */ - private renderer $renderer; - - /** @var moodle_page $page */ - private moodle_page $page; - - /** - * Constructor. - * - * @param capquiz $capquiz - * @param renderer $renderer - */ - public function __construct(capquiz $capquiz, renderer $renderer) { - $this->capquiz = $capquiz; - $this->renderer = $renderer; - $this->page = $capquiz->get_page(); - } - - /** - * Render grading configuration view - */ - public function render(): bool|string { - return $this->renderer->render_from_template('capquiz/configure_grading', [ - 'rating_form' => $this->get_rating_configuration(), - ]); - } - - /** - * Returns rating configuration form - */ - private function get_rating_configuration(): string { - $url = $this->page->url; - $form = new grading_configuration_form($this->capquiz, $url); - $formdata = $form->get_data(); - if ($formdata) { - $this->process_rating_configuration($formdata); - } - return $form->render(); - } - - /** - * Processes the rating configuration formdata - * - * @param stdClass $formdata - */ - private function process_rating_configuration(stdClass $formdata): void { - $star = 1; - $ratings = []; - while (isset($formdata->{"star_rating_$star"})) { - if (!isset($formdata->{"delstarbutton$star"})) { - $ratings[] = (int)$formdata->{"star_rating_$star"}; - } - $star++; - } - if (isset($formdata->addstarbutton)) { - $ratings[] = end($ratings) + 100; - } - if ($formdata->default_user_rating) { - $this->capquiz->set_default_user_rating($formdata->default_user_rating); - } - $this->capquiz->question_list()->set_star_ratings($ratings); - if ($formdata->starstopass) { - $this->capquiz->set_stars_to_pass($formdata->starstopass); - } - if ($formdata->timedue) { - $this->capquiz->set_time_due($formdata->timedue); - } - redirect(capquiz_urls::view_grading_url()); - } - -} diff --git a/classes/output/import_renderer.php b/classes/output/import_renderer.php deleted file mode 100644 index 8a457b2..0000000 --- a/classes/output/import_renderer.php +++ /dev/null @@ -1,129 +0,0 @@ -. - -/** - * This file defines a class used to render the question list imports - * - * @package mod_capquiz - * @author Sebastian S. Gundersen - * @copyright 2019 NTNU - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - -namespace mod_capquiz\output; - -use context_course; -use mod_capquiz\capquiz; -use mod_capquiz\capquiz_urls; -use renderer_base; - -/** - * Class import_renderer - * - * @package mod_capquiz - * @author Sebastian S. Gundersen - * @copyright 2019 NTNU - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -class import_renderer { - - /** @var capquiz $capquiz */ - private capquiz $capquiz; - - /** @var renderer_base $renderer */ - private renderer_base $renderer; - - /** - * Constructor. - * - * @param capquiz $capquiz The current capquiz - * @param renderer_base $renderer The renderer to be used by this instance - */ - public function __construct(capquiz $capquiz, renderer_base $renderer) { - $this->capquiz = $capquiz; - $this->renderer = $renderer; - } - - /** - * Fetches questions in the list from the database - * - * @param int $qlistid the id of the list with the question - * @return array Array of all questions in list $qlistid - */ - private function get_questions_in_list(int $qlistid): array { - global $DB; - $sql = 'SELECT cq.id AS id, - cq.rating AS rating, - q.name AS name - FROM {capquiz_question} cq - JOIN {question} q - ON q.id = cq.question_id - WHERE cq.question_list_id = :qlistid - ORDER BY cq.rating'; - return $DB->get_records_sql($sql, ['qlistid' => $qlistid]); - } - - /** - * Get list of all questions - */ - private function get_question_lists(): array { - global $DB; - $path = context_course::instance($this->capquiz->course()->id)->path; - $sql = 'SELECT DISTINCT cql.* - FROM {capquiz_question_list} cql - JOIN {context} ctx - ON (ctx.id = cql.context_id AND ctx.path LIKE :pathpart) - OR cql.context_id IS NULL - WHERE cql.is_template = 1 - ORDER BY cql.time_created DESC'; - return $DB->get_records_sql($sql, ['pathpart' => $path . '%']); - } - - /** - * Render - */ - public function render(): bool|string { - $srcqlists = $this->get_question_lists(); - $qlists = []; - foreach ($srcqlists as $srcqlist) { - $questions = []; - foreach ($this->get_questions_in_list($srcqlist->id) as $question) { - $questions[] = $question; - } - $qlists[] = [ - 'title' => $srcqlist->title, - 'time_created' => $srcqlist->time_created, - 'description' => $srcqlist->description, - 'questions' => $questions, - 'merge' => [ - 'type' => 'primary', - 'method' => 'post', - 'url' => capquiz_urls::merge_qlist($srcqlist->id)->out(false), - 'label' => get_string('merge', 'capquiz'), - ], - 'delete' => [ - 'type' => 'danger', - 'method' => 'post', - 'url' => capquiz_urls::delete_qlist($srcqlist->id)->out(false), - 'label' => get_string('delete'), - ], - ]; - } - return $this->renderer->render_from_template('capquiz/merge_with_question_list', ['lists' => $qlists]); - } - -} - diff --git a/classes/output/index_table.php b/classes/output/index_table.php new file mode 100644 index 0000000..130e4f1 --- /dev/null +++ b/classes/output/index_table.php @@ -0,0 +1,75 @@ +. + +namespace mod_capquiz\output; + +use core\output\renderer_base; +use mod_capquiz\capquiz; +use renderable; +use templatable; + +/** + * Display overview of a list of CAPQuizzes. + * + * @package mod_capquiz + * @author Sebastian Gundersen + * @copyright 2024 Norwegian University of Science and Technology (NTNU) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class index_table implements renderable, templatable { + + /** @var capquiz[] $capquizzes */ + private array $capquizzes; + + /** + * Constructor. + * + * @param capquiz[] $capquizzes + */ + public function __construct(array $capquizzes) { + $this->capquizzes = $capquizzes; + } + + /** + * Render the index table. + * + * @param renderer_base $output + * @return bool|string + */ + public function render(renderer_base $output): bool|string { + return $output->render_from_template('capquiz/index_table', $this->export_for_template($output)); + } + + /** + * Export parameters for template. + * + * @param renderer_base $output + * @return array + */ + public function export_for_template(renderer_base $output): array { + return [ + 'capquizzes' => array_map(fn(capquiz $capquiz) => [ + 'viewurl' => (new \moodle_url('/mod/capquiz/view.php', ['id' => $capquiz->get_cmid()]))->out(false), + 'name' => $capquiz->get('name'), + 'timeopen' => $capquiz->get('timeopen'), + 'timedue' => $capquiz->get('timedue'), + 'isopen' => $capquiz->is_open(), + 'defaultuserrating' => $capquiz->get('defaultuserrating'), + 'defaultquestionrating' => $capquiz->get('defaultquestionrating'), + ], $this->capquizzes), + ]; + } +} diff --git a/classes/output/instructor_dashboard_renderer.php b/classes/output/instructor_dashboard_renderer.php deleted file mode 100755 index c459b7f..0000000 --- a/classes/output/instructor_dashboard_renderer.php +++ /dev/null @@ -1,157 +0,0 @@ -. - -/** - * This file defines a class used to render the instructor dashboard - * - * @package mod_capquiz - * @author Aleksander Skrede - * @copyright 2018 NTNU - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - -namespace mod_capquiz\output; - -use mod_capquiz\capquiz; -use mod_capquiz\capquiz_urls; -use mod_capquiz\capquiz_user; -use renderer_base; - -defined('MOODLE_INTERNAL') || die(); - -require_once($CFG->dirroot . '/question/editlib.php'); - -/** - * Class instructor_dashboard_renderer - * - * @package mod_capquiz - * @author Aleksander Skrede - * @copyright 2018 NTNU - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -class instructor_dashboard_renderer { - - /** @var capquiz $capquiz */ - private capquiz $capquiz; - - /** @var renderer_base $renderer */ - private renderer_base $renderer; - - /** - * Constructor. - * - * @param capquiz $capquiz - * @param renderer_base $renderer - */ - public function __construct(capquiz $capquiz, renderer_base $renderer) { - $this->capquiz = $capquiz; - $this->renderer = $renderer; - } - - /** - * Render instructor dashboard - */ - public function render(): string { - $html = $this->render_summary(); - $html .= $this->render_publish(); - $html .= $this->render_template(); - return $html; - } - - /** - * Render the instructor dashboard summary - */ - private function render_summary(): bool|string { - $qlist = $this->capquiz->question_list(); - if (!$qlist) { - return 'question list error'; - } - return $this->renderer->render_from_template('capquiz/instructor_dashboard_summary', [ - 'published_status' => get_string($this->capquiz->is_published() ? 'published' : 'not_published', 'capquiz'), - 'question_list_title' => $qlist->title(), - 'question_count' => $qlist->question_count(), - 'enrolled_student_count' => capquiz_user::user_count($this->capquiz->id()), - ]); - } - - /** - * Renders publish button - */ - private function render_publish(): bool|string { - $published = $this->capquiz->is_published(); - $canpublish = $this->capquiz->can_publish(); - $qlist = $this->capquiz->question_list(); - if (!$qlist) { - return 'question list error'; - } - $message = null; - if (!$canpublish) { - if ($qlist->question_count() === 0) { - $message = get_string('publish_no_questions_in_list', 'capquiz'); - } else if ($published) { - $message = get_string('publish_already_published', 'capquiz'); - } - } - return $this->renderer->render_from_template('capquiz/instructor_dashboard_publish', [ - 'publish' => $this->publish_button(), - 'message' => $message ?: false, - ]); - } - - /** - * Renders template - */ - private function render_template(): bool|string { - $qlist = $this->capquiz->question_list(); - if (!$qlist) { - return 'question list error'; - } - $message = null; - if (!$qlist->has_questions()) { - $message = get_string('template_no_questions_in_list', 'capquiz'); - } - return $this->renderer->render_from_template('capquiz/instructor_dashboard_template', [ - 'create_template' => $this->create_template_button(), - 'message' => $message ?: false, - ]); - } - - /** - * Creates publish button - */ - private function publish_button(): array { - return [ - 'type' => 'primary', - 'method' => 'post', - 'url' => capquiz_urls::question_list_publish_url($this->capquiz->question_list())->out(false), - 'label' => get_string('publish', 'capquiz'), - 'disabled' => !$this->capquiz->can_publish(), - ]; - } - - /** - * Creates create template button - */ - private function create_template_button(): array { - return [ - 'type' => 'primary', - 'method' => 'post', - 'url' => capquiz_urls::question_list_create_template_url($this->capquiz->question_list())->out(false), - 'label' => get_string('create_template', 'capquiz'), - 'disabled' => !$this->capquiz->question_list()->has_questions(), - ]; - } -} diff --git a/classes/output/matchmaking_configuration_renderer.php b/classes/output/matchmaking_configuration_renderer.php deleted file mode 100644 index c720301..0000000 --- a/classes/output/matchmaking_configuration_renderer.php +++ /dev/null @@ -1,109 +0,0 @@ -. - -/** - * This file defines a class used to render the matchmaking configuration view - * - * @package mod_capquiz - * @author Aleksander Skrede - * @copyright 2018 NTNU - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - -namespace mod_capquiz\output; - -use mod_capquiz\capquiz; -use mod_capquiz\capquiz_matchmaking_strategy_loader; -use mod_capquiz\capquiz_urls; -use moodle_page; - -defined('MOODLE_INTERNAL') || die(); - -require_once($CFG->dirroot . '/question/editlib.php'); - -/** - * Class matchmaking_configuration_renderer - * - * @package mod_capquiz - * @author Aleksander Skrede - * @copyright 2018 NTNU - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -class matchmaking_configuration_renderer { - - /** @var capquiz $capquiz */ - private capquiz $capquiz; - - /** @var renderer $renderer */ - private renderer $renderer; - - /** @var capquiz_matchmaking_strategy_loader $registry */ - private capquiz_matchmaking_strategy_loader $registry; - - /** @var moodle_page $page */ - private moodle_page $page; - - /** - * matchmaking_configuration_renderer constructor. - * @param capquiz $capquiz - * @param renderer $renderer - */ - public function __construct(capquiz $capquiz, renderer $renderer) { - $this->capquiz = $capquiz; - $this->renderer = $renderer; - $this->registry = new capquiz_matchmaking_strategy_loader($this->capquiz); - $this->page = $capquiz->get_page(); - } - - /** - * Calls submethod that renders the matchmaking_configuration view - */ - public function render(): bool|string { - if ($this->registry->has_strategy()) { - return $this->render_configuration(); - } else { - return '

' . get_string('no_matchmaking_strategy_selected', 'capquiz') . '

'; - } - } - - /** - * Renders the matchmaking configuration view - */ - private function render_configuration(): bool|string { - $strategy = $this->registry->current_strategy_name(); - return $this->renderer->render_from_template('capquiz/matchmaking_configuration', [ - 'strategy' => capquiz_matchmaking_strategy_loader::localized_strategy_name($strategy), - 'form' => $this->render_form(), - ]); - } - - /** - * Returns the rendered matchmaking configuration form - */ - private function render_form(): string { - $url = $this->page->url; - if ($form = $this->registry->configuration_form($url)) { - $formdata = $form->get_data(); - if ($formdata) { - $this->registry->configure_current_strategy($formdata); - $url = capquiz_urls::view_rating_system_url(); - redirect($url); - } - return $form->render(); - } - return get_string('nothing_to_configure_for_strategy', 'capquiz'); - } -} diff --git a/classes/output/matchmaking_strategy_selection_renderer.php b/classes/output/matchmaking_strategy_selection_renderer.php deleted file mode 100644 index 76c24ab..0000000 --- a/classes/output/matchmaking_strategy_selection_renderer.php +++ /dev/null @@ -1,99 +0,0 @@ -. - -/** - * This file defines a class used to render the matchmaking strategy selection form - * - * @package mod_capquiz - * @author Aleksander Skrede - * @copyright 2018 NTNU - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - -namespace mod_capquiz\output; - -use mod_capquiz\capquiz; -use mod_capquiz\capquiz_matchmaking_strategy_loader; -use mod_capquiz\capquiz_matchmaking_strategy_registry; -use mod_capquiz\capquiz_urls; -use mod_capquiz\form\view\matchmaking_strategy_selection_form; -use moodle_page; -use moodle_url; - -/** - * class matchmaking_strategy_selection_renderer - * - * @package mod_capquiz - * @author Aleksander Skrede - * @copyright 2018 NTNU - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -class matchmaking_strategy_selection_renderer { - - /** @var moodle_url $url */ - private moodle_url $url; - - /** @var capquiz $capquiz */ - private capquiz $capquiz; - - /** @var renderer $renderer */ - private renderer $renderer; - - /** @var moodle_page $page */ - private moodle_page $page; - - /** - * matchmaking_strategy_selection_renderer constructor. - * - * @param capquiz $capquiz - * @param renderer $renderer - */ - public function __construct(capquiz $capquiz, renderer $renderer) { - $this->capquiz = $capquiz; - $this->renderer = $renderer; - $this->url = capquiz_urls::view_rating_system_url(); - $this->page = $capquiz->get_page(); - } - - /** - * Sets redirect url - * - * @param moodle_url $url - */ - public function set_redirect_url(moodle_url $url): void { - $this->url = $url; - } - - /** - * Renders the matchmaking strategy selection form - */ - public function render(): bool|string { - $url = $this->page->url; - $form = new matchmaking_strategy_selection_form($this->capquiz, $url); - $formdata = $form->get_data(); - if ($formdata) { - $loader = new capquiz_matchmaking_strategy_loader($this->capquiz); - $registry = new capquiz_matchmaking_strategy_registry($this->capquiz); - $strategy = $registry->selection_strategies()[$formdata->strategy]; - $loader->set_strategy($strategy); - redirect($this->url); - } - return $this->renderer->render_from_template('capquiz/matchmaking_selection_strategy', [ - 'form' => $form->render(), - ]); - } - -} diff --git a/classes/output/question_attempt_renderer.php b/classes/output/question_attempt_renderer.php deleted file mode 100755 index 68ea873..0000000 --- a/classes/output/question_attempt_renderer.php +++ /dev/null @@ -1,270 +0,0 @@ -. - -/** - * This file defines a class used to render a question attempt - * - * @package mod_capquiz - * @author Aleksander Skrede - * @copyright 2018 NTNU - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - -namespace mod_capquiz\output; - -use core\context\module; -use mod_capquiz\capquiz; -use mod_capquiz\capquiz_question_list; -use mod_capquiz\capquiz_user; -use mod_capquiz\capquiz_urls; -use mod_capquiz\capquiz_question; -use mod_capquiz\capquiz_question_attempt; -use moodle_page; -use question_display_options; -use renderer_base; - -/** - * Class question_attempt_renderer - * - * @package mod_capquiz - * @author Aleksander Skrede - * @copyright 2018 NTNU - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -class question_attempt_renderer { - - /** @var capquiz $capquiz */ - private capquiz $capquiz; - - /** @var renderer_base $renderer */ - private renderer_base $renderer; - - /** @var moodle_page $page */ - private moodle_page $page; - - /** - * Constructor. - * - * @param capquiz $capquiz - * @param renderer_base $renderer - */ - public function __construct(capquiz $capquiz, renderer_base $renderer) { - $this->capquiz = $capquiz; - $this->renderer = $renderer; - $this->render_question_head_html(); - $this->page = $capquiz->get_page(); - } - - /** - * Renders the question head - */ - private function render_question_head_html(): void { - $user = $this->capquiz->user(); - $qengine = $this->capquiz->question_engine($user); - if ($qengine === null) { - return; - } - $attempt = $qengine->attempt_for_user($user); - if ($attempt !== null) { - $user->question_usage()->render_question_head_html($attempt->question_slot()); - } - } - - /** - * Renders the question attempt view - */ - public function render(): string { - if (!$this->capquiz->is_published()) { - return get_string('nothing_here_yet', 'capquiz'); - } - $this->page->requires->js_call_amd('mod_capquiz/attempt', 'initialize', []); - $user = $this->capquiz->user(); - $qengine = $this->capquiz->question_engine($user); - $attempt = $qengine->attempt_for_user($user); - if ($attempt) { - if ($attempt->is_answered()) { - return $this->render_review($attempt); - } - if ($attempt->is_pending()) { - return $this->render_attempt($attempt, self::attempt_display_options($this->capquiz->context())); - } - } - return get_string('you_finished_capquiz', 'capquiz'); - } - - /** - * Render the attempt - * - * @param capquiz_question_attempt $attempt - * @param question_display_options $options - */ - private function render_attempt(capquiz_question_attempt $attempt, question_display_options $options): string { - $user = $this->capquiz->user(); - $html = $this->render_progress($user); - $html .= $this->render_question_attempt($attempt, $options); - return $html; - } - - /** - * Render the attempt review - * - * @param capquiz_question_attempt $attempt - */ - private function render_review(capquiz_question_attempt $attempt): string { - $html = $this->render_attempt($attempt, self::review_display_options($this->capquiz->context())); - $html .= $this->render_review_next_button($attempt); - return $html; - } - - /** - * Render the review next button - * - * @param capquiz_question_attempt $attempt - */ - public function render_review_next_button(capquiz_question_attempt $attempt): string { - $url = capquiz_urls::response_reviewed_url($attempt); - $label = get_string('next', 'capquiz'); - return basic_renderer::render_action_button($this->renderer, $url, $label, id: 'capquiz_review_next'); - } - - /** - * Render a users progress - * - * @param capquiz_user $user - */ - private function render_progress(capquiz_user $user): string { - $qlist = $this->capquiz->question_list(); - $percent = $qlist->next_level_percent($this->capquiz, $user->rating()); - list($stars, $blankstars, $nostars) = $this->user_star_progress($user, $qlist); - $student = [ - 'up' => $percent >= 0 ? ['percent' => $percent] : false, - 'down' => $percent < 0 ? ['percent' => -$percent] : false, - 'stars' => $stars, - 'blankstars' => $blankstars, - 'nostars' => $nostars, - ]; - return $this->renderer->render_from_template('capquiz/student_progress', [ - 'progress' => ['student' => $student], - ]); - } - - /** - * Render the question attempt - * - * @param capquiz_question_attempt $attempt - * @param question_display_options $options - */ - public function render_question_attempt(capquiz_question_attempt $attempt, question_display_options $options): string { - $user = $this->capquiz->user(); - $quba = $user->question_usage(); - $this->page->requires->js_module('core_question_engine'); - return $this->renderer->render_from_template('capquiz/student_question_attempt', [ - 'attempt' => [ - 'url' => capquiz_urls::response_submit_url($attempt)->out(false), - 'body' => $quba->render_question($attempt->question_slot(), $options, $attempt->question_id()), - 'slots' => '', - ], - 'gradingdone' => $this->capquiz->is_grading_completed(), - 'finalgrade' => $user->highest_stars_graded(), - 'gradingpass' => $user->highest_stars_graded() >= $this->capquiz->stars_to_pass(), - 'duedate' => userdate($this->capquiz->time_due(), get_string('strftimedatetime', 'langconfig')), - ]); - } - - /** - * Render question attempts metainfo - * - * @param capquiz_user $user - * @param capquiz_question_attempt $attempt - */ - public function render_metainfo(capquiz_user $user, capquiz_question_attempt $attempt): string { - $question = capquiz_question::load($attempt->question_id()); - if ($question == null) { - return 'Question was not found.'; - } - return $this->renderer->render_from_template('capquiz/student_question_metainfo', [ - 'metainfo' => [ - 'rating' => [ - 'student' => $user->rating(), - 'question' => $question->rating(), - ], - 'question' => [ - 'capquiz_id' => $question->id(), - 'moodle_id' => $question->question_id(), - ], - ], - ]); - } - - /** - * Checks a users star progress - * - * @param capquiz_user $user - * @param capquiz_question_list $qlist - * @return array[] - */ - private function user_star_progress(capquiz_user $user, capquiz_question_list $qlist): array { - $stars = []; - $blankstars = []; - $nostars = []; - for ($star = 1; $star <= $qlist->max_stars(); $star++) { - if ($user->highest_stars_achieved() >= $star) { - if ($user->rating() >= $qlist->star_rating($star)) { - $stars[] = true; - } else { - $blankstars[] = true; - } - } else { - $nostars[] = true; - } - } - return [$stars, $blankstars, $nostars]; - } - - /** - * Returns the display options for the attempt review - * - * @param module $context - */ - public static function review_display_options(module $context): question_display_options { - $options = new question_display_options(); - $options->context = $context; - $options->readonly = true; - $options->flags = question_display_options::VISIBLE; - $options->marks = question_display_options::VISIBLE; - $options->rightanswer = question_display_options::VISIBLE; - $options->numpartscorrect = question_display_options::VISIBLE; - $options->manualcomment = question_display_options::HIDDEN; - return $options; - } - - /** - * Returns the display options for the attempt - * - * @param module $context - */ - public static function attempt_display_options(module $context): question_display_options { - $options = new question_display_options(); - $options->context = $context; - $options->flags = question_display_options::HIDDEN; - $options->marks = question_display_options::HIDDEN; - $options->rightanswer = question_display_options::HIDDEN; - $options->numpartscorrect = question_display_options::HIDDEN; - $options->manualcomment = question_display_options::HIDDEN; - return $options; - } - -} diff --git a/classes/output/question_bank_renderer.php b/classes/output/question_bank_renderer.php deleted file mode 100644 index 661b538..0000000 --- a/classes/output/question_bank_renderer.php +++ /dev/null @@ -1,114 +0,0 @@ -. - -/** - * This file defines a class used to render a question bank - * - * @package mod_capquiz - * @author Aleksander Skrede - * @copyright 2018 NTNU - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - -namespace mod_capquiz\output; - -use mod_capquiz\capquiz; -use mod_capquiz\capquiz_urls; -use mod_capquiz\bank\question_bank_view; -use moodle_page; - -/** - * Class question_bank_renderer - * - * @package mod_capquiz - * @author Aleksander Skrede - * @copyright 2018 NTNU - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -class question_bank_renderer { - - /** @var capquiz $capquiz */ - private capquiz $capquiz; - - /** @var moodle_page $page */ - private moodle_page $page; - - /** - * question_bank_renderer constructor. - * @param capquiz $capquiz - */ - public function __construct(capquiz $capquiz) { - $this->capquiz = $capquiz; - $this->page = $capquiz->get_page(); - } - - /** - * Creates question bank view - */ - public function create_view(): question_bank_view { - list($url, $contexts, $cmid, $cm, $capquizrecord, $pagevars) = $this->setup_question_edit(); - return new question_bank_view($contexts, $url, $this->capquiz->course(), $this->capquiz->course_module(), $pagevars); - } - - /** - * Renders question bank - */ - public function render(): string { - // phpcs:disable - // $questionsperpage = optional_param('qperpage', 10, PARAM_INT); - // $questionpage = optional_param('qpage', 0, PARAM_INT); - // phpcs:enable - $questionview = $this->create_view(); - // phpcs:disable - // $html = "

" . get_string('available_questions', 'capquiz') . "

"; - // phpcs:enable - ob_start(); - $questionview->display(); - $qbank = ob_get_clean(); - return \html_writer::div(\html_writer::div($qbank, 'bd'), 'questionbankformforpopup'); - } - - /** - * This is mostly a copy from editlib.php's question_edit_setup() function. - * The original function expects the course module id parameter to be "cmid", but this module gets passed "id" - * Moodle coding standard does not allow us to override $_GET or $_POST before calling question_edit_setup() - */ - private function setup_question_edit(): array { - $params = []; - $params['cmid'] = capquiz_urls::require_course_module_id_param(); - $params['qpage'] = optional_param('qpage', null, PARAM_INT); - $params['cat'] = optional_param('cat', null, PARAM_SEQUENCE); - $params['category'] = optional_param('category', null, PARAM_SEQUENCE); - $params['qperpage'] = optional_param('qperpage', null, PARAM_INT); - for ($i = 1; $i <= \core_question\local\bank\view::MAX_SORTS; $i++) { - $param = 'qbs' . $i; - if ($sort = optional_param($param, '', PARAM_TEXT)) { - $params[$param] = $sort; - } else { - break; - } - } - $params['recurse'] = optional_param('recurse', null, PARAM_BOOL); - $params['showhidden'] = optional_param('showhidden', null, PARAM_BOOL); - $params['qbshowtext'] = optional_param('qbshowtext', null, PARAM_BOOL); - $params['cpage'] = optional_param('cpage', null, PARAM_INT); - $params['qtagids'] = optional_param_array('qtagids', null, PARAM_INT); - $this->page->set_pagelayout('admin'); - $edittab = 'editq'; - return question_build_edit_resources($edittab, capquiz_urls::$urledit, $params); - } - -} diff --git a/classes/output/question_list_creator_renderer.php b/classes/output/question_list_creator_renderer.php deleted file mode 100644 index be869e3..0000000 --- a/classes/output/question_list_creator_renderer.php +++ /dev/null @@ -1,97 +0,0 @@ -. - -/** - * This file defines a class used to render a capquiz' question list creator - * - * @package mod_capquiz - * @author Aleksander Skrede - * @copyright 2018 NTNU - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - -namespace mod_capquiz\output; - -use mod_capquiz\capquiz; -use mod_capquiz\capquiz_urls; -use mod_capquiz\capquiz_question_list; -use mod_capquiz\form\view\question_list_create_form; -use moodle_page; - -defined('MOODLE_INTERNAL') || die(); - -require_once($CFG->dirroot . '/question/editlib.php'); - -/** - * Class question_list_creator_renderer - * - * @package mod_capquiz - * @author Aleksander Skrede - * @copyright 2018 NTNU - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -class question_list_creator_renderer { - - /** @var capquiz $capquiz */ - private capquiz $capquiz; - - /** @var renderer $renderer */ - private renderer $renderer; - - /** @var moodle_page $PAGE */ - private moodle_page $page; - - /** - * Constructor. - * - * @param capquiz $capquiz The capquiz whose question list creator should be rendered - * @param renderer $renderer The renderer used to render the question list creator - */ - public function __construct(capquiz $capquiz, renderer $renderer) { - $this->capquiz = $capquiz; - $this->renderer = $renderer; - $this->page = $capquiz->get_page(); - } - - /** - * Renders the question list creator - */ - public function render(): bool|string { - $url = $this->page->url; - $form = new question_list_create_form($url); - $formdata = $form->get_data(); - if ($formdata) { - $ratings = [ - $formdata->level_1_rating, - $formdata->level_2_rating, - $formdata->level_3_rating, - $formdata->level_4_rating, - $formdata->level_5_rating, - ]; - $title = $formdata->title; - $description = $formdata->description; - $qlist = capquiz_question_list::create_new_instance($this->capquiz, $title, $description, $ratings); - if ($qlist) { - redirect(capquiz_urls::create_view_url(capquiz_urls::$urlview)); - } - capquiz_urls::redirect_to_front_page(); - } - return $this->renderer->render_from_template('capquiz/create_question_list', [ - 'form' => $form->render(), - ]); - } - -} diff --git a/classes/output/question_list_renderer.php b/classes/output/question_list_renderer.php deleted file mode 100644 index 0f360de..0000000 --- a/classes/output/question_list_renderer.php +++ /dev/null @@ -1,140 +0,0 @@ -. - -/** - * This file defines a class used to render question list - * - * @package mod_capquiz - * @author Aleksander Skrede - * @author Sebastian S. Gundersen - * @copyright 2019 NTNU - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - -namespace mod_capquiz\output; - -use mod_capquiz\capquiz; -use mod_capquiz\capquiz_urls; -use mod_capquiz\capquiz_question_list; -use moodle_page; -use moodle_url; - -/** - * Class question_list_renderer - * - * @package mod_capquiz - * @author Aleksander Skrede - * @author Sebastian S. Gundersen - * @copyright 2019 NTNU - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -class question_list_renderer { - - /** @var capquiz $capquiz */ - private capquiz $capquiz; - - /** @var renderer $renderer */ - private renderer $renderer; - - /** @var moodle_page $page */ - private moodle_page $page; - - /** - * Constructor. - * - * @param capquiz $capquiz The capquiz whose question list should be rendered - * @param renderer $renderer The renderer used to render the question list - */ - public function __construct(capquiz $capquiz, renderer $renderer) { - $this->capquiz = $capquiz; - $this->renderer = $renderer; - $this->page = $capquiz->get_page(); - } - - /** - * Renders question list - */ - public function render(): bool|string { - $cmid = $this->capquiz->course_module()->id; - $this->page->requires->js_call_amd('mod_capquiz/edit_questions', 'initialize', [$cmid]); - $qlist = $this->capquiz->question_list(); - if ($qlist?->has_questions()) { - return $this->render_questions($qlist); - } - $title = get_string('question_list', 'capquiz'); - $noquestions = get_string('question_list_no_questions', 'capquiz'); - return "

$title

$noquestions

"; - } - - /** - * Renders all the individual questions - * - * @param capquiz_question_list $qlist - */ - private function render_questions(capquiz_question_list $qlist): bool|string { - $rows = []; - $questions = $qlist->questions(); - for ($i = 0; $i < $qlist->question_count(); $i++) { - $question = $questions[$i]; - $courseid = $question->course_id(); - $editurl = new moodle_url('/question/bank/editquestion/question.php', [ - 'cmid' => $this->page->cm->id, - 'id' => $question->question_id(), - ]); - $previewurl = new moodle_url('/question/bank/previewquestion/preview.php', [ - 'cmid' => $this->page->cm->id, - 'id' => $question->question_id(), - ]); - $targetblank = ['name' => 'target', 'value' => '_blank']; - $edit = $courseid === 0 ? false : [ - 'url' => $editurl->out(false), - 'label' => get_string('edit'), - 'classes' => 'fa fa-edit', - 'attributes' => [$targetblank], - ]; - $preview = $courseid === 0 ? false : [ - 'url' => $previewurl->out(false), - 'label' => get_string('preview'), - 'classes' => 'fa fa-search-plus', - 'attributes' => [$targetblank], - ]; - $rows[] = [ - 'index' => $i + 1, - 'name' => $question->name(), - 'rating' => round($question->rating(), 3), - 'question_id' => $question->id(), - 'rating_url' => capquiz_urls::set_question_rating_url($question->id())->out(false), - 'delete' => [ - 'url' => capquiz_urls::remove_question_from_list_url($question->id())->out(false), - 'label' => get_string('remove', 'capquiz'), - 'classes' => 'fa fa-trash', - ], - 'edit' => $edit, - 'preview' => $preview, - ]; - } - $message = null; - if ($qlist->has_questions()) { - $message = get_string('update_rating_explanation', 'capquiz'); - } - return $this->renderer->render_from_template('capquiz/question_list', [ - 'default_rating' => $qlist->default_question_rating(), - 'questions' => $rows, - 'message' => $message ?: false, - ]); - } - -} diff --git a/classes/output/question_list_selection_renderer.php b/classes/output/question_list_selection_renderer.php deleted file mode 100755 index f882c82..0000000 --- a/classes/output/question_list_selection_renderer.php +++ /dev/null @@ -1,82 +0,0 @@ -. - -/** - * This file defines a class used to render a capquiz' question list selection - * - * @package mod_capquiz - * @author Aleksander Skrede - * @copyright 2018 NTNU - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - -namespace mod_capquiz\output; - -use context_module; -use mod_capquiz\capquiz_question_list; -use mod_capquiz\capquiz_urls; -use renderer_base; - -/** - * Class question_list_selection_renderer - * - * @package mod_capquiz - * @author Sebastian S. Gundersen - * @copyright 2018 NTNU - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -class question_list_selection_renderer { - - /** @var renderer_base $renderer */ - private renderer_base $renderer; - - /** - * Constructor. - * - * @param renderer_base $renderer The renderer used to render the question list selection - */ - public function __construct(renderer_base $renderer) { - $this->renderer = $renderer; - } - - /** - * Renders the question list selection - */ - public function render(): bool|string { - $templates = capquiz_question_list::load_question_list_templates(); - $lists = []; - foreach ($templates as $template) { - $lists[] = [ - 'title' => $template->title(), - 'description' => $template->description(), - 'author' => $template->author()->username, - 'created' => date('Y-m-d H:i:s', substr($template->time_created(), 0, 10)), - 'url' => capquiz_urls::question_list_select_url($template), - ]; - } - - $createurl = capquiz_urls::view_create_question_list_url(); - $params = $createurl->params(); - $createurl->remove_all_params(); - $createlabel = get_string('create_question_list', 'capquiz'); - - return $this->renderer->render_from_template('capquiz/question_list_selection', [ - 'lists' => $lists, - 'create' => basic_renderer::render_action_button($this->renderer, $createurl, $createlabel, 'get', $params), - ]); - } - -} diff --git a/classes/output/rating_system_configuration_renderer.php b/classes/output/rating_system_configuration_renderer.php deleted file mode 100644 index 5a4df1f..0000000 --- a/classes/output/rating_system_configuration_renderer.php +++ /dev/null @@ -1,107 +0,0 @@ -. - -/** - * This file defines a class used to render the rating system configuration view - * - * @package mod_capquiz - * @author Aleksander Skrede - * @copyright 2018 NTNU - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - -namespace mod_capquiz\output; - -use mod_capquiz\capquiz; -use mod_capquiz\capquiz_rating_system_loader; -use mod_capquiz\capquiz_urls; -use moodle_page; - -defined('MOODLE_INTERNAL') || die(); - -require_once($CFG->dirroot . '/question/editlib.php'); - -/** - * Class rating_system_configuration_renderer - * - * @package mod_capquiz - * @author Aleksander Skrede - * @copyright 2018 NTNU - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -class rating_system_configuration_renderer { - - /** @var capquiz $capquiz */ - private capquiz $capquiz; - - /** @var renderer $renderer */ - private renderer $renderer; - - /** @var capquiz_rating_system_loader $registry */ - private capquiz_rating_system_loader $registry; - - /** @var moodle_page $page */ - private moodle_page $page; - - /** - * rating_system_configuration_renderer constructor. - * @param capquiz $capquiz - * @param renderer $renderer - */ - public function __construct(capquiz $capquiz, renderer $renderer) { - $this->capquiz = $capquiz; - $this->renderer = $renderer; - $this->registry = new capquiz_rating_system_loader($capquiz); - $this->page = $capquiz->get_page(); - } - - /** - * Calls submethod that renders the rating_system_configuration view - */ - public function render(): bool|string { - if ($this->registry->has_rating_system()) { - return $this->render_configuration(); - } else { - return '

No rating system has been specified

'; - } - } - - /** - * Renders the rating configuration view - */ - private function render_configuration(): bool|string { - return $this->renderer->render_from_template('capquiz/rating_system_configuration', [ - 'strategy' => $this->registry->current_rating_system_name(), - 'form' => $this->render_form(), - ]); - } - - /** - * Renders the rating configuration form - */ - private function render_form(): string { - $url = $this->page->url; - if ($form = $this->registry->configuration_form($url)) { - $formdata = $form->get_data(); - if ($formdata) { - $this->registry->configure_current_rating_system($formdata); - redirect(capquiz_urls::view_rating_system_url()); - } - return $form->render(); - } - return 'There is nothing to configure for this rating system'; - } -} diff --git a/classes/output/rating_system_selection_renderer.php b/classes/output/rating_system_selection_renderer.php deleted file mode 100644 index d53dd9f..0000000 --- a/classes/output/rating_system_selection_renderer.php +++ /dev/null @@ -1,98 +0,0 @@ -. - -/** - * This file defines a class used to render the rating system selection form - * - * @package mod_capquiz - * @author Aleksander Skrede - * @copyright 2018 NTNU - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - -namespace mod_capquiz\output; - -use mod_capquiz\capquiz; -use mod_capquiz\capquiz_rating_system_loader; -use mod_capquiz\capquiz_rating_system_registry; -use mod_capquiz\capquiz_urls; -use mod_capquiz\form\view\rating_system_selection_form; -use moodle_page; -use moodle_url; - -/** - * Class rating_system_selection_renderer - * - * @package mod_capquiz - * @author Aleksander Skrede - * @copyright 2018 NTNU - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -class rating_system_selection_renderer { - - /** @var moodle_url $url */ - private moodle_url $url; - - /** @var capquiz $capquiz */ - private capquiz $capquiz; - - /** @var renderer $renderer */ - private renderer $renderer; - - /** @var moodle_page $page */ - private moodle_page $page; - - /** - * rating_system_selection_renderer constructor. - * - * @param capquiz $capquiz - * @param renderer $renderer - */ - public function __construct(capquiz $capquiz, renderer $renderer) { - $this->capquiz = $capquiz; - $this->renderer = $renderer; - $this->url = capquiz_urls::view_rating_system_url(); - $this->page = $capquiz->get_page(); - } - - /** - * Sets redirect url - * - * @param moodle_url $url - */ - public function set_redirect_url(moodle_url $url): void { - $this->url = $url; - } - - /** - * Renders the rating system selection form - */ - public function render(): bool|string { - $url = $this->page->url; - $form = new rating_system_selection_form($this->capquiz, $url); - $formdata = $form->get_data(); - if ($formdata) { - $registry = new capquiz_rating_system_registry(); - $loader = new capquiz_rating_system_loader($this->capquiz); - $loader->set_rating_system($registry->rating_systems()[$formdata->rating_system]); - redirect($this->url); - } - return $this->renderer->render_from_template('capquiz/rating_system_selection', [ - 'form' => $form->render(), - ]); - } - -} diff --git a/classes/output/renderer.php b/classes/output/renderer.php index 16c09db..56112f2 100755 --- a/classes/output/renderer.php +++ b/classes/output/renderer.php @@ -14,234 +14,15 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . -/** - * This file defines a class used as a superclass to different renderers - * - * @package mod_capquiz - * @author Aleksander Skrede - * @copyright 2018 NTNU - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - namespace mod_capquiz\output; -use core_renderer; -use mod_capquiz\capquiz; -use mod_capquiz\capquiz_urls; -use moodle_url; -use renderer_base; -use tabobject; - -defined('MOODLE_INTERNAL') || die(); - -require_once($CFG->dirroot . '/mod/capquiz/classes/output/basic_renderer.php'); -require_once($CFG->dirroot . '/mod/capquiz/classes/output/classlist_renderer.php'); -require_once($CFG->dirroot . '/mod/capquiz/classes/output/question_list_renderer.php'); -require_once($CFG->dirroot . '/mod/capquiz/classes/output/question_bank_renderer.php'); -require_once($CFG->dirroot . '/mod/capquiz/classes/output/question_attempt_renderer.php'); -require_once($CFG->dirroot . '/mod/capquiz/classes/output/unauthorized_view_renderer.php'); -require_once($CFG->dirroot . '/mod/capquiz/classes/output/question_list_creator_renderer.php'); -require_once($CFG->dirroot . '/mod/capquiz/classes/output/instructor_dashboard_renderer.php'); -require_once($CFG->dirroot . '/mod/capquiz/classes/output/matchmaking_configuration_renderer.php'); -require_once($CFG->dirroot . '/mod/capquiz/classes/output/grading_configuration_renderer.php'); -require_once($CFG->dirroot . '/mod/capquiz/classes/output/matchmaking_strategy_selection_renderer.php'); - /** - * Main plugin renderer for capquiz + * CAPQuiz renderer. * * @package mod_capquiz - * @author Aleksander Skrede - * @copyright 2018 NTNU + * @author Sebastian Gundersen + * @copyright 2024 Norwegian University of Science and Technology (NTNU) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class renderer extends \plugin_renderer_base { - - /** - * Returns a reference to the current renderer - */ - public function output_renderer(): core_renderer|renderer_base { - return $this->output; - } - - /** - * Creates a tab - * - * @param string $name Name of the tab - * @param string $title Title of the tab - * @param moodle_url $link Link - */ - private function tab(string $name, string $title, moodle_url $link): tabobject { - return new tabobject($name, $link, get_string($title, 'capquiz')); - } - - /** - * Creates all tabs - * - * @param string $activetab The currently active cab - */ - private function tabs(string $activetab): bool|string { - $tabs = [ - $this->tab('view_dashboard', 'dashboard', capquiz_urls::view_url()), - $this->tab('view_rating_system', 'rating_system', capquiz_urls::view_rating_system_url()), - $this->tab('view_questions', 'questions', capquiz_urls::view_question_list_url()), - $this->tab('view_grading', 'grading', capquiz_urls::view_grading_url()), - $this->tab('view_classlist', 'classlist', capquiz_urls::view_classlist_url()), - $this->tab('view_import', 'other_question_lists', capquiz_urls::view_import_url()), - $this->tab('view_report', 'reports', capquiz_urls::view_report_url()), - ]; - return print_tabs([$tabs], $activetab, null, null, true); - } - - /** - * Display a tabbed view - * - * @param string $view - * @param string $activetab - */ - public function display_tabbed_view(string $view, string $activetab): void { - echo $this->output->header(); - echo $this->tabs($activetab); - echo $view; - echo $this->output->footer(); - } - - /** - * Display multiple tabbed views - * - * @param string[] $views The renderers to render the tabs - * @param string $activetab The currently active tab - */ - public function display_tabbed_views(array $views, string $activetab): void { - echo $this->output->header(); - echo $this->tabs($activetab); - foreach ($views as $view) { - echo $view; - } - echo $this->output->footer(); - } - - /** - * Display view. - * - * @param string $view - */ - public function display_view(string $view): void { - echo $this->output->header(); - echo $view; - echo $this->output->footer(); - } - - /** - * Display the question attempt view - * - * @param capquiz $capquiz - */ - public function display_question_attempt_view(capquiz $capquiz): void { - $renderer = new question_attempt_renderer($capquiz, $this); - $this->display_view($renderer->render()); - } - - /** - * Display the instructor dashboard - * - * @param capquiz $capquiz - */ - public function display_instructor_dashboard(capquiz $capquiz): void { - $renderer = new instructor_dashboard_renderer($capquiz, $this); - $this->display_tabbed_view($renderer->render(), 'view_dashboard'); - } - - /** - * Display the question list create view - * - * @param capquiz $capquiz - */ - public function display_question_list_create_view(capquiz $capquiz): void { - $renderer = new question_list_creator_renderer($capquiz, $this); - $this->display_view($renderer->render()); - } - - /** - * Display the choose question list view - */ - public function display_choose_question_list_view(): void { - $renderer = new question_list_selection_renderer($this); - $this->display_view($renderer->render()); - } - - /** - * Display the unauthorized view - */ - public function display_unauthorized_view(): void { - $renderer = new unauthorized_view_renderer($this); - $this->display_view($renderer->render()); - } - - /** - * Display the question list view - * - * @param capquiz $capquiz - */ - public function display_question_list_view(capquiz $capquiz): void { - $r1 = new question_list_renderer($capquiz, $this); - $r2 = new question_bank_renderer($capquiz); - $html = '
' . $r1->render() . '
'; - $html .= '
' . $r2->render() . '
'; - $this->display_tabbed_view($html, 'view_questions'); - } - - /** - * Display the rating system configuration - * - * @param capquiz $capquiz - */ - public function display_rating_system_configuration(capquiz $capquiz): void { - $this->display_tabbed_views([ - (new matchmaking_strategy_selection_renderer($capquiz, $this))->render(), - (new matchmaking_configuration_renderer($capquiz, $this))->render(), - (new rating_system_selection_renderer($capquiz, $this))->render(), - (new rating_system_configuration_renderer($capquiz, $this))->render(), - ], 'view_rating_system'); - } - - - /** - * Display the leaderboard view - * - * @param capquiz $capquiz - */ - public function display_leaderboard(capquiz $capquiz): void { - $renderer = new classlist_renderer($capquiz, $this); - $this->display_tabbed_view($renderer->render(), 'view_classlist'); - } - - /** - * Display the import view - * - * @param capquiz $capquiz - */ - public function display_import(capquiz $capquiz): void { - $renderer = new import_renderer($capquiz, $this); - $this->display_tabbed_view($renderer->render(), 'view_import'); - } - - /** - * Display the grading configuration view - * - * @param capquiz $capquiz - */ - public function display_grading_configuration(capquiz $capquiz): void { - $renderer = new grading_configuration_renderer($capquiz, $this); - $this->display_tabbed_view($renderer->render(), 'view_grading'); - } - - /** - * Display the report view - * - * @param capquiz $capquiz - */ - public function display_report(capquiz $capquiz): void { - $renderer = new report_renderer($capquiz); - $this->display_tabbed_view($renderer->render(), 'view_report'); - } +class renderer extends \core\output\plugin_renderer_base { } diff --git a/classes/output/report_renderer.php b/classes/output/report_renderer.php deleted file mode 100644 index f6936ed..0000000 --- a/classes/output/report_renderer.php +++ /dev/null @@ -1,111 +0,0 @@ -. - -/** - * This file defines a class used to render a report - * - * @package mod_capquiz - * @author Aleksander Skrede - * @copyright 2018 NTNU - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - -namespace mod_capquiz\output; - -use capquiz_exception; -use mod_capquiz\capquiz; -use mod_capquiz\capquiz_urls; -use mod_capquiz\report\capquiz_report_factory; -use moodle_page; -use tabobject; - -defined('MOODLE_INTERNAL') || die(); - -require_once(__DIR__ . '/../../report/reportfactory.php'); - -/** - * Class report_renderer - * - * @package mod_capquiz - * @author André Storhaug - * @copyright 2019 Norwegian University of Science and Technology (NTNU) - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -class report_renderer { - - /** @var capquiz $capquiz */ - private capquiz $capquiz; - - /** @var moodle_page $page */ - private moodle_page $page; - - /** - * Constructor. - * - * @param capquiz $capquiz - */ - public function __construct(capquiz $capquiz) { - $this->capquiz = $capquiz; - $this->page = $capquiz->get_page(); - } - - /** - * Renders report - */ - public function render(): string { - global $CFG; - $html = ''; - $download = optional_param('download', '', PARAM_RAW); - $mode = optional_param('mode', '', PARAM_ALPHA); - - $reportlist = capquiz_report_list($this->capquiz->context()); - if (empty($reportlist)) { - return get_string('noreports', 'capquiz'); - } - if ($mode === '') { - // Default to first accessible report and redirect. - capquiz_urls::redirect_to_url(capquiz_urls::view_report_url(reset($reportlist))); - } - if (!in_array($mode, $reportlist)) { - throw new capquiz_exception('erroraccessingreport', 'capquiz', - $CFG->wwwroot . '/mod/capquiz/view.php?id=' . $this->capquiz->course()->id); - } - $report = capquiz_report_factory::make($mode); - $this->setup_report(); - - $row = []; - foreach ($reportlist as $rep) { - $url = capquiz_urls::view_report_url($rep); - $row[] = new tabobject('capquiz_' . $rep, $url, get_string('pluginname', 'capquizreport_' . $rep)); - } - $tabs[] = $row; - - $html .= print_tabs($tabs, 'capquiz_' . $mode, null, null, true); - - ob_start(); - $report->display($this->capquiz, $this->capquiz->course_module(), $this->capquiz->course(), $download); - $html .= ob_get_clean(); - return $html; - } - - /** - * Sets pagelayout to "report" - */ - private function setup_report(): void { - $this->page->set_pagelayout('report'); - } -} - diff --git a/classes/output/unauthorized_view_renderer.php b/classes/output/unauthorized_view_renderer.php deleted file mode 100755 index 2ea302e..0000000 --- a/classes/output/unauthorized_view_renderer.php +++ /dev/null @@ -1,57 +0,0 @@ -. - -/** - * This file defines a class used to render the unauthorized view - * - * @package mod_capquiz - * @author Sebastian S. Gundersen - * @copyright 2018 NTNU - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - -namespace mod_capquiz\output; - -/** - * Class unauthorized_view_renderer - * - * @package mod_capquiz - * @author Sebastian S. Gundersen - * @copyright 2018 NTNU - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -class unauthorized_view_renderer { - - /** @var renderer */ - private renderer $renderer; - - /** - * Constructor. - * - * @param renderer $renderer - */ - public function __construct(renderer $renderer) { - $this->renderer = $renderer; - } - - /** - * Renders the "unauthorized" view - */ - public function render(): bool|string { - return $this->renderer->render_from_template('capquiz/unauthorized', []); - } - -} diff --git a/classes/plugininfo/capquizreport.php b/classes/plugininfo/capquizreport.php index ea6ab49..309bc39 100644 --- a/classes/plugininfo/capquizreport.php +++ b/classes/plugininfo/capquizreport.php @@ -14,15 +14,6 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . -/** - * Subplugin info class. - * - * @package mod_capquiz - * @author André Storhaug - * @copyright 2019 Norwegian University of Science and Technology (NTNU) - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - namespace mod_capquiz\plugininfo; use admin_settingpage; diff --git a/classes/privacy/provider.php b/classes/privacy/provider.php index 1e34a07..6688c00 100644 --- a/classes/privacy/provider.php +++ b/classes/privacy/provider.php @@ -14,16 +14,6 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . -/** - * Privacy Subsystem implementation for mod_capquiz. - * - * @package mod_capquiz - * @author André Storhaug - * @author Sebastian Søviknes Gundersen - * @copyright 2019 NTNU - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - namespace mod_capquiz\privacy; use context; @@ -40,9 +30,10 @@ /** * Privacy Subsystem implementation for mod_capquiz. * + * @package mod_capquiz * @author André Storhaug - * @author Sebastian Søviknes Gundersen - * @copyright 2019 NTNU + * @author Sebastian Gundersen + * @copyright 2019 Norwegian University of Science and Technology (NTNU) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements @@ -55,57 +46,49 @@ class provider implements /** * Returns metadata about this system. * - * @param collection $items The initialised collection to add metadata to. + * @param collection $collection The initialised collection to add metadata to. * @return collection A listing of user data stored through this system. */ - public static function get_metadata(collection $items): collection { + public static function get_metadata(collection $collection): collection { // The table 'capquiz' stores a record for each capquiz. // It does not contain user personal data, but data is returned from it for contextual requirements. // The table 'capquiz_attempt' stores a record of each capquiz attempt. // It contains a userid which links to the user making the attempt and contains information about that attempt. - $items->add_database_table('capquiz_attempt', [ + $collection->add_database_table('capquiz_attempt', [ 'userid' => 'privacy:metadata:capquiz_attempt:userid', - 'time_answered' => 'privacy:metadata:capquiz_attempt:time_answered', - 'time_reviewed' => 'privacy:metadata:capquiz_attempt:time_reviewed', + 'timeanswered' => 'privacy:metadata:capquiz_attempt:timeanswered', + 'timereviewed' => 'privacy:metadata:capquiz_attempt:timereviewed', ], 'privacy:metadata:capquiz_attempt'); - // The 'capquiz_question' table is used to map the usage of a question used in a CAPQuiz activity. + // The table 'capquiz_slot' represents a question used by a capquiz. // It does not contain user data. // The 'capquiz_question_rating' contains each change of rating for a question. // It does not contain user data. - // The 'capquiz_question_list' table is used to store the set of question lists used by a CapQuiz activity. - // It does not contain user data. - - // The 'capquiz_question_selection' contains selections / settings for each CAPQuiz activity. - // It does not contain user data. - - // The 'capquiz_rating_system' does not contain any user identifying data and does not need a mapping. - // The table 'capquiz_user' stores a record of each user in each capquiz attempt. // This is to kep track of rating and achievement level. // It contains a userid which links to the user and contains information about that user. - $items->add_database_table('capquiz_user', [ + $collection->add_database_table('capquiz_user', [ 'userid' => 'privacy:metadata:capquiz_user:userid', 'rating' => 'privacy:metadata:capquiz_user:rating', - 'highest_level' => 'privacy:metadata:capquiz_user:highest_level', + 'higheststars' => 'privacy:metadata:capquiz_user:higheststars', ], 'privacy:metadata:capquiz_user'); // The table 'capquiz_user_rating' stores a record of each user rating in each capquiz attempt. // This is to kep track of rating and achievement level, in addition to provide a historical log. - // It contains a capquiz_user_id which links to the capquiz_user and contains information about that user. - $items->add_database_table('capquiz_user_rating', [ - 'capquiz_user_id' => 'privacy:metadata:capquiz_user_rating:capquiz_user_id', + // It contains a capquiz user id which links to the capquiz_user and contains information about that user. + $collection->add_database_table('capquiz_user_rating', [ + 'capquizuserid' => 'privacy:metadata:capquiz_user_rating:capquizuserid', 'rating' => 'privacy:metadata:capquiz_user_rating:rating', 'manual' => 'privacy:metadata:capquiz_user_rating:manual', 'timecreated' => 'privacy:metadata:capquiz_user_rating:timecreated', ], 'privacy:metadata:capquiz_user_rating'); // CAPQuiz links to the 'core_question' subsystem for all question functionality. - $items->add_subsystem_link('core_question', [], 'privacy:metadata:core_question'); - return $items; + $collection->add_subsystem_link('core_question', [], 'privacy:metadata:core_question'); + return $collection; } /** @@ -125,11 +108,9 @@ public static function get_contexts_for_userid(int $userid): contextlist { AND m.name = :modname JOIN {capquiz} cq ON cq.id = cm.instance - JOIN {capquiz_question_list} cql - ON cql.capquiz_id = cq.id JOIN {capquiz_user} cu - ON cu.capquiz_id = cq.id - AND cu.user_id = :userid'; + ON cu.capquizid = cq.id + AND cu.userid = :userid'; $contextlist = new contextlist(); $contextlist->add_from_sql($sql, [ 'contextlevel' => CONTEXT_MODULE, @@ -151,15 +132,15 @@ public static function export_user_data(approved_contextlist $contextlist): void } $user = $contextlist->get_user(); list($contextsql, $contextparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED); - $sql = "SELECT cx.id AS contextid, - cm.id AS cmid, - ca.id AS capattemptid, - ca.question_id AS questionid, - ca.answered AS answered, - ca.time_reviewed AS timereviewed, - ca.time_answered AS timeanswered, - cu.id AS capuserid, - cu.question_usage_id AS qubaid + $sql = "SELECT cx.id AS contextid, + cm.id AS cmid, + ca.id AS capquizattemptid, + ca.slotid AS capquizslotid, + ca.answered AS answered, + ca.timereviewed AS timereviewed, + ca.timeanswered AS timeanswered, + cu.id AS capquizuserid, + cu.questionusageid AS questionusageid FROM {context} cx JOIN {course_modules} cm ON cm.id = cx.instanceid @@ -169,13 +150,11 @@ public static function export_user_data(approved_contextlist $contextlist): void AND m.name = :modname JOIN {capquiz} cq ON cq.id = cm.instance - JOIN {capquiz_question_list} cql - ON cql.capquiz_id = cq.id JOIN {capquiz_user} cu - ON cu.capquiz_id = cq.id - AND cu.user_id = :userid + ON cu.capquizid = cq.id + AND cu.userid = :userid JOIN {capquiz_attempt} ca - ON ca.user_id = cu.id + ON ca.capquizuserid = cu.id WHERE cx.id {$contextsql}"; $params = [ 'contextlevel' => CONTEXT_MODULE, @@ -193,7 +172,7 @@ public static function export_user_data(approved_contextlist $contextlist): void // Start new data module. $context = context_module::instance($attempt->cmid); } - $qubaidforcontext[$context->id] = $attempt->qubaid; + $qubaidforcontext[$context->id] = $attempt->questionusageid; // Store the quiz attempt data. $data = new stdClass(); $data->timereviewed = transform::datetime($attempt->timereviewed); @@ -203,12 +182,12 @@ public static function export_user_data(approved_contextlist $contextlist): void // where X is the attempt number. $subcontext = [ get_string('attempts', 'capquiz'), - get_string('attempt', 'capquiz') . " $attempt->capattemptid", + get_string('attempt', 'capquiz') . ' ' . $attempt->capquizattemptid, ]; writer::with_context($context)->export_data($subcontext, $data); - static::export_user_rating($context, $attempt->capuserid); + static::export_user_rating($context, $attempt->capquizuserid); } $attempts->close(); @@ -225,8 +204,7 @@ public static function export_user_data(approved_contextlist $contextlist): void writer::with_context($context)->export_data([], $data); // This attempt was made by the user. They 'own' all data on it. Store the question usage data. \core_question\privacy\provider::export_question_usage( - $user->id, $context, [], $qubaidforcontext[$context->id], $options, true - ); + $user->id, $context, [], $qubaidforcontext[$context->id], $options, true); } } @@ -234,9 +212,9 @@ public static function export_user_data(approved_contextlist $contextlist): void * Export the rating data from a specified user * * @param context $context The context to export the users rating for - * @param int $userid the specified users id + * @param int $capquizuserid */ - public static function export_user_rating(context $context, int $userid) { + public static function export_user_rating(context $context, int $capquizuserid): void { global $DB; $sql = "SELECT cur.id AS ratingid, cur.rating AS rating, @@ -244,9 +222,9 @@ public static function export_user_rating(context $context, int $userid) { cur.timecreated AS timecreated FROM {capquiz_user} cu JOIN {capquiz_user_rating} cur - ON cur.capquiz_user_id = cu.id - WHERE cu.id = :userid"; - $ratings = $DB->get_recordset_sql($sql, ['userid' => $userid]); + ON cur.capquizuserid = cu.id + WHERE cu.id = :capquizuserid"; + $ratings = $DB->get_recordset_sql($sql, ['capquizuserid' => $capquizuserid]); foreach ($ratings as $rating) { $data = new stdClass(); @@ -275,12 +253,13 @@ public static function delete_data_for_all_users_in_context(context $context): v if (!$cm) { return; } - $users = $DB->get_records('capquiz_user', ['capquiz_id' => $cm->instance]); - foreach ($users as $user) { - $DB->delete_records('capquiz_attempt', ['user_id' => $user->id]); - $DB->delete_records('capquiz_user_rating', ['capquiz_user_id' => $user->id]); + $users = $DB->get_records('capquiz_user', ['capquizid' => $cm->instance], fields: 'id'); + if (!empty($users)) { + $userids = array_keys($users); + $DB->delete_records_list('capquiz_attempt', 'capquizuserid', $userids); + $DB->delete_records_list('capquiz_user_rating', 'capquizuserid', $userids); + $DB->delete_records('capquiz_user', ['capquizid' => $cm->instance]); } - $DB->delete_records('capquiz_user', ['capquiz_id' => $cm->instance]); \core_question\privacy\provider::delete_data_for_all_users_in_context($context); } @@ -291,17 +270,17 @@ public static function delete_data_for_all_users_in_context(context $context): v */ public static function delete_data_for_user(approved_contextlist $contextlist): void { global $DB; - if (empty($contextlist->count())) { + if ($contextlist->count() === 0) { return; } $userid = $contextlist->get_user()->id; foreach ($contextlist->get_contexts() as $context) { $cm = get_coursemodule_from_id('capquiz', $context->instanceid); - $user = $DB->get_record('capquiz_user', ['capquiz_id' => $cm->instance, 'user_id' => $userid]); + $user = $DB->get_record('capquiz_user', ['capquizid' => $cm->instance, 'userid' => $userid]); if ($user) { - $DB->delete_records('capquiz_attempt', ['user_id' => $user->id]); - $DB->delete_records('capquiz_user_rating', ['capquiz_user_id' => $user->id]); - $DB->delete_records('capquiz_user', ['capquiz_id' => $cm->instance, 'user_id' => $userid]); + $DB->delete_records('capquiz_attempt', ['capquizuserid' => $user->id]); + $DB->delete_records('capquiz_user_rating', ['capquizuserid' => $user->id]); + $DB->delete_records('capquiz_user', ['capquizid' => $cm->instance, 'userid' => $userid]); } } \core_question\privacy\provider::delete_data_for_user($contextlist); diff --git a/classes/question/bank/add_question_column.php b/classes/question/bank/add_question_column.php new file mode 100644 index 0000000..5cc3d45 --- /dev/null +++ b/classes/question/bank/add_question_column.php @@ -0,0 +1,106 @@ +. + +declare(strict_types=1); + +namespace mod_capquiz\question\bank; + +use mod_capquiz\local\helpers\questions; + +/** + * Question bank column for adding questions to CAPQuiz. + * + * @package mod_capquiz + * @author Sebastian Gundersen + * @copyright 2024 Norwegian University of Science and Technology (NTNU) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class add_question_column extends \core_question\local\bank\column_base { + + /** @var int[] Question IDs that have already been added the CAPQuiz */ + private array $addedquestionids = []; + + /** + * Load IDs of questions already added to the quiz. This lets us only show an add action for unadded questions. + */ + protected function init(): void { + $context = \context_module::instance($this->qbank->cm->id); + $questions = questions::get_all_questions_by_references($context->id, 'slot'); + $this->addedquestionids = array_column($questions, 'id'); + } + + /** + * Display content. + * + * @param \stdClass $question + * @param string $rowclasses + */ + protected function display_content($question, $rowclasses): void { + global $OUTPUT; + if (!question_has_capability_on($question, 'use')) { + return; + } + if (in_array($question->id, $this->addedquestionids)) { + echo $OUTPUT->render(new \pix_icon('t/check', '')); + return; + } + $url = new \moodle_url('/mod/capquiz/edit.php', [ + 'id' => $this->qbank->cm->id, + 'action' => 'addquestion', + 'questionid' => $question->id, + ]); + $title = get_string('add_to_quiz', 'capquiz'); + $link = new \action_link($url, '', null, ['title' => $title], new \pix_icon('t/add', $title)); + echo $OUTPUT->render($link); + } + + /** + * Return the default column width in pixels. + * + * @return int + */ + public function get_default_width(): int { + return 24; + } + + /** + * Get the internal name for this column. Used as a CSS class name, and to store information about the current sort. + * Must match PARAM_ALPHA. + * + * @return string + */ + public function get_name(): string { + return 'capquiz_add_question'; + } + + /** + * Extra class names to apply to every cell in this column. + * + * @return string[] + */ + public function get_extra_classes(): array { + return ['iconcol']; + } + + /** + * Get title for this column. Not used if is_sortable returns an array. + * + * @return string + */ + public function get_title(): string { + return ''; + } +} diff --git a/classes/question/bank/checkbox_column.php b/classes/question/bank/checkbox_column.php new file mode 100644 index 0000000..a7988e7 --- /dev/null +++ b/classes/question/bank/checkbox_column.php @@ -0,0 +1,56 @@ +. + +declare(strict_types=1); + +namespace mod_capquiz\question\bank; + +use mod_capquiz\local\helpers\questions; + +/** + * Checkbox column which only displays for questions that haven't been added. + * + * @package mod_capquiz + * @author Sebastian Gundersen + * @copyright 2024 Norwegian University of Science and Technology (NTNU) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class checkbox_column extends \core_question\local\bank\checkbox_column { + + /** @var int[] Question IDs that have already been added the CAPQuiz */ + private array $addedquestionids = []; + + /** + * Load IDs of questions already added to the quiz. + */ + protected function init(): void { + $context = \context_module::instance($this->qbank->cm->id); + $questions = questions::get_all_questions_by_references($context->id, 'slot'); + $this->addedquestionids = array_column($questions, 'id'); + } + + /** + * Display content. + * + * @param \stdClass $question + * @param string $rowclasses + */ + protected function display_content($question, $rowclasses): void { + if (!in_array($question->id, $this->addedquestionids)) { + parent::display_content($question, $rowclasses); + } + } +} diff --git a/classes/question/bank/preview_question_column.php b/classes/question/bank/preview_question_column.php new file mode 100644 index 0000000..cc7dc2b --- /dev/null +++ b/classes/question/bank/preview_question_column.php @@ -0,0 +1,92 @@ +. + +declare(strict_types=1); + +namespace mod_capquiz\question\bank; + +use qbank_previewquestion\question_preview_options; + +/** + * Question bank column for previewing a question. + * + * @package mod_capquiz + * @author Sebastian Gundersen + * @copyright 2024 Norwegian University of Science and Technology (NTNU) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class preview_question_column extends \core_question\local\bank\column_base { + /** + * Display content. + * + * @param \stdClass $question + * @param string $rowclasses + * @return void + */ + protected function display_content($question, $rowclasses): void { + global $OUTPUT; + if (!\question_bank::is_qtype_installed($question->qtype)) { + return; + } + if (!question_has_capability_on($question, 'use')) { + return; + } + $context = $this->qbank->get_most_specific_context(); + $version = $this->qbank->is_listing_specific_versions() ? $question->version : question_preview_options::ALWAYS_LATEST; + $url = \qbank_previewquestion\helper::question_preview_url( + $question->id, null, null, null, null, $context, $this->qbank->returnurl, $version); + $title = get_string('preview'); + $action = new \popup_action('click', $url, \qbank_previewquestion\helper::question_preview_popup_params()); + echo $OUTPUT->render(new \action_link($url, '', $action, ['title' => $title], new \pix_icon('t/preview', $title))); + } + + /** + * Return the default column width in pixels. + * + * @return int + */ + public function get_default_width(): int { + return 24; + } + + /** + * Get the internal name for this column. Used as a CSS class name, and to store information about the current sort. + * Must match PARAM_ALPHA. + * + * @return string + */ + public function get_name(): string { + return 'capquiz_preview_question'; + } + + /** + * Extra class names to apply to every cell in this column. + * + * @return string[] + */ + public function get_extra_classes(): array { + return ['iconcol']; + } + + /** + * Get title for this column. Not used if is_sortable returns an array. + * + * @return string + */ + public function get_title(): string { + return ''; + } +} diff --git a/classes/question/bank/question_bank_view.php b/classes/question/bank/question_bank_view.php new file mode 100644 index 0000000..9d35f76 --- /dev/null +++ b/classes/question/bank/question_bank_view.php @@ -0,0 +1,141 @@ +. + +declare(strict_types=1); + +namespace mod_capquiz\question\bank; + +use context; +use core_question\local\bank\column_base; +use core_question\local\bank\column_manager_base; +use core_question\local\bank\question_edit_contexts; +use mod_quiz\question\bank\question_name_text_column; +use moodle_url; +use stdClass; + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->dirroot . '/mod/quiz/locallib.php'); + +/** + * Question bank view for CAPQuiz. + * + * @package mod_capquiz + * @author Sebastian Gundersen + * @copyright 2024 Norwegian University of Science and Technology (NTNU) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class question_bank_view extends \core_question\local\bank\view { + /** + * Constructor. + * + * @param question_edit_contexts $contexts + * @param moodle_url $pageurl + * @param stdClass $course course settings + * @param stdClass $cm activity settings. + * @param array $params + * @param array $extraparams + */ + public function __construct($contexts, $pageurl, $course, $cm, $params, $extraparams) { + for ($i = 1; $i <= \core_question\local\bank\view::MAX_SORTS; $i++) { + $sort = optional_param("qbs$i", '', PARAM_TEXT); + if ($sort) { + $params["qbs$i"] = $sort; + } else { + break; + } + } + parent::__construct($contexts, $pageurl, $course, $cm, $params, $extraparams); + } + + /** + * Get question bank plugins. + * + * @return array + */ + protected function get_question_bank_plugins(): array { + $questionbankclasscolumns = []; + $customviewcolumns = [ + 'mod_capquiz\question\bank\add_question_column' . column_base::ID_SEPARATOR . 'add_question_column', + 'mod_capquiz\question\bank\checkbox_column' . column_base::ID_SEPARATOR . 'checkbox_column', + 'qbank_viewquestiontype\question_type_column' . column_base::ID_SEPARATOR . 'question_type_column', + 'mod_quiz\question\bank\question_name_text_column' . column_base::ID_SEPARATOR . 'question_name_text_column', + 'mod_capquiz\question\bank\preview_question_column' . column_base::ID_SEPARATOR . 'preview_question_column', + ]; + foreach ($customviewcolumns as $columnid) { + [$columnclass, $columnname] = explode(column_base::ID_SEPARATOR, $columnid, 2); + if (class_exists($columnclass)) { + $questionbankclasscolumns[$columnid] = $columnclass::from_column_name($this, $columnname); + } + } + return $questionbankclasscolumns; + } + + /** + * Don't print the header. + */ + protected function display_question_bank_header(): void { + } + + /** + * Just use the base column manager in this view. + * + * @return void + */ + protected function init_column_manager(): void { + $this->columnmanager = new column_manager_base(); + } + + /** + * Don't display plugin controls. + * + * @param \core\context $context + * @param int $categoryid + * @return string + */ + protected function get_plugin_controls(\core\context $context, int $categoryid): string { + return ''; + } + + /** + * Specify the column heading. + */ + protected function heading_column(): string { + return question_name_text_column::class; + } + + /** + * Display button to add selected questions to the quiz. + * + * @param context $catcontext + */ + protected function display_bottom_controls(context $catcontext): void { + echo '
'; + if (has_capability('moodle/question:useall', $catcontext)) { + echo \html_writer::empty_tag('input', [ + 'type' => 'submit', + 'name' => 'addselectedquestions', + 'class' => 'btn btn-primary', + 'value' => get_string('add_to_quiz', 'capquiz'), + 'data-action' => 'toggle', + 'data-togglegroup' => 'qbank', + 'data-toggle' => 'action', + 'disabled' => true, + ]); + } + echo '
'; + } +} diff --git a/classes/rating_system/capquiz_rating_system_registry.php b/classes/rating_system/capquiz_rating_system_registry.php deleted file mode 100755 index efdd352..0000000 --- a/classes/rating_system/capquiz_rating_system_registry.php +++ /dev/null @@ -1,138 +0,0 @@ -. - -/** - * This file defines a class used as a registry for the rating system - * - * @package mod_capquiz - * @author Aleksander Skrede - * @copyright 2018 NTNU - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - -namespace mod_capquiz; - -use coding_exception; -use moodle_url; -use stdClass; - -defined('MOODLE_INTERNAL') || die(); - -require_once($CFG->dirroot . '/mod/capquiz/classes/rating_system/elo_rating/elo_rating_system.php'); -require_once($CFG->dirroot . '/mod/capquiz/classes/rating_system/elo_rating/elo_rating_system_form.php'); - -/** - * Class capquiz_rating_system_registry - * - * @package mod_capquiz - * @author Aleksander Skrede - * @copyright 2018 NTNU - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -class capquiz_rating_system_registry { - - /** @var callable[][] $systems */ - private array $systems; - - /** - * Constructor. - */ - public function __construct() { - $this->register_rating_systems(); - } - - /** - * Returns rating system - * - * @param string $system - */ - public function rating_system(string $system): capquiz_rating_system { - $value = $this->systems[$system]; - if (!$value) { - $this->throw_rating_system_exception($system); - } - return array_values($value)[0](); - } - - /** - * Returns configuration form - * - * @param string $system - * @param stdClass $configuration - * @param moodle_url $url - */ - public function configuration_form(string $system, stdClass $configuration, moodle_url $url) { - $value = $this->systems[$system]; - if ($value) { - $configfunc = array_values($value)[1]; - return $configfunc($url, $configuration); - } - $this->throw_rating_system_exception($system); - } - - /** - * Checks if this instance has a rating system - * - * @param string $system - */ - public function has_rating_system(string $system): bool { - return isset($this->systems[$system]); - } - - /** - * Returns the default rating system - */ - public function default_rating_system(): string { - // Default rating system is added first. - // Modify caquiz_rating_system_registry::register_rating_systems() to change this. - $ratingsystems = $this->rating_systems(); - return reset($ratingsystems); - } - - /** - * Returns the names of all rating systems. - * - * @return string[] - */ - public function rating_systems(): array { - return array_keys($this->systems); - } - - /** - * Registers rating systems - */ - private function register_rating_systems(): void { - // The first listed will be selected by default when creating a new activity. - $this->systems = [ - 'Elo' => [ - fn() => new elo_rating_system(), - fn(moodle_url $url, stdClass $config) => new elo_rating_system_form($config, $url), - ], - ]; - } - - /** - * Creates and throws exception - * - * @param string $system - */ - private function throw_rating_system_exception(string $system) { - $msg = "The specified rating system '$system' does not exist."; - $msg .= " Options are {'" . implode("', '", $this->rating_systems()); - $msg .= "'}. This issue must be fixed by a programmer"; - throw new coding_exception($msg); - } -} diff --git a/classes/rating_system/elo_rating/elo_rating_system.php b/classes/rating_system/elo_rating/elo_rating_system.php deleted file mode 100755 index b888d38..0000000 --- a/classes/rating_system/elo_rating/elo_rating_system.php +++ /dev/null @@ -1,121 +0,0 @@ -. - -/** - * This file defines a class used as a registry for the rating system - * - * @package mod_capquiz - * @author Aleksander Skrede - * @copyright 2018 NTNU - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - -namespace mod_capquiz; - -use stdClass; - -/** - * Class elo_rating_system - * - * @package mod_capquiz - * @author Aleksander Skrede - * @copyright 2018 NTNU - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -class elo_rating_system extends capquiz_rating_system { - - /** @var float $studentkfactor */ - private float $studentkfactor; - - /** @var float $questionkfactor */ - private float $questionkfactor; - - /** - * Configures the rating system - * - * @param stdClass $config - */ - public function configure(stdClass $config): void { - if ($config->student_k_factor) { - $this->studentkfactor = $config->student_k_factor; - } - if ($config->question_k_factor) { - $this->questionkfactor = $config->question_k_factor; - } - } - - /** - * Returns the current configuration - */ - public function configuration(): stdClass { - $config = new stdClass; - $config->student_k_factor = $this->studentkfactor; - $config->question_k_factor = $this->questionkfactor; - return $config; - } - - /** - * Returns the default configuration - */ - public function default_configuration(): stdClass { - $config = new stdClass; - $config->student_k_factor = 32; - $config->question_k_factor = 8; - return $config; - } - - /** - * Updates the users rating - * - * @param capquiz_user $user - * @param capquiz_question $question - * @param float $score - */ - public function update_user_rating(capquiz_user $user, capquiz_question $question, float $score): void { - $current = $user->rating(); - $factor = $this->studentkfactor; - $newrating = $current + $factor * ($score - $this->expected_result($current, $question->rating())); - $user->set_rating($newrating); - } - - /** - * Updates the winning and losing questions ratings - * - * @param capquiz_question $winner - * @param capquiz_question $loser - */ - public function question_victory_ratings(capquiz_question $winner, capquiz_question $loser): void { - $loserating = $loser->rating(); - $winrating = $winner->rating(); - $factor = $this->questionkfactor; - $newloserating = $loserating + $factor * (0 - $this->expected_result($loserating, $winrating)); - $newwinrating = $winrating + $factor * (1 - $this->expected_result($winrating, $loserating)); - $loser->set_rating($newloserating); - $winner->set_rating($newwinrating); - } - - /** - * Calculates the expected score in favour of the player with rating $a, - * against a player with rating $b - * - * @param float $a - * @param float $b - */ - private function expected_result(float $a, float $b): float { - $exponent = ($b - $a) / 400.0; - return 1.0 / (1.0 + pow(10.0, $exponent)); - } -} diff --git a/classes/rating_system/elo_rating/elo_rating_system_form.php b/classes/rating_system/elo_rating/elo_rating_system_form.php deleted file mode 100755 index 05f35d2..0000000 --- a/classes/rating_system/elo_rating/elo_rating_system_form.php +++ /dev/null @@ -1,81 +0,0 @@ -. - -/** - * This file defines a class used to represent an elo rating system form - * - * @package mod_capquiz - * @author Aleksander Skrede - * @copyright 2018 NTNU - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - -namespace mod_capquiz; - -use moodle_url; -use stdClass; - -defined('MOODLE_INTERNAL') || die(); - -require_once($CFG->libdir . '/formslib.php'); - -/** - * Class elo_rating_system_form - * - * @package mod_capquiz - * @author Aleksander Skrede - * @copyright 2018 NTNU - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -class elo_rating_system_form extends \moodleform { - - /** @var stdClass $config */ - private stdClass $config; - - /** - * Constructor. - * - * @param stdClass $config - * @param moodle_url $url - */ - public function __construct(stdClass $config, moodle_url $url) { - $this->config = $config; - parent::__construct($url); - } - - /** - * Defines rating system form - */ - public function definition(): void { - $form = $this->_form; - - $form->addElement('text', 'student_k_factor', get_string('student_k_factor', 'capquiz')); - $form->setType('student_k_factor', PARAM_INT); - $form->addRule('student_k_factor', get_string('student_k_factor_specified_rule', 'capquiz'), 'required', null, 'client'); - $form->addRule('student_k_factor', get_string('k_factor_numeric_rule', 'capquiz'), 'numeric', null, 'client'); - $form->setDefault('student_k_factor', $this->config->student_k_factor); - $form->addHelpButton('student_k_factor', 'student_k_factor', 'capquiz'); - - $form->addElement('text', 'question_k_factor', get_string('question_k_factor', 'capquiz')); - $form->setType('question_k_factor', PARAM_INT); - $form->addRule('question_k_factor', get_string('question_k_factor_specified_rule', 'capquiz'), 'required', null, 'client'); - $form->addRule('question_k_factor', get_string('k_factor_numeric_rule', 'capquiz'), 'numeric', null, 'client'); - $form->setDefault('question_k_factor', $this->config->question_k_factor); - $form->addHelpButton('question_k_factor', 'question_k_factor', 'capquiz'); - - $this->add_action_buttons(false); - } -} diff --git a/db/access.php b/db/access.php index 0c22e13..cff5c07 100755 --- a/db/access.php +++ b/db/access.php @@ -18,8 +18,8 @@ * File to define access policies * * @package mod_capquiz - * @author Sebastian S. Gundersen - * @copyright 2018 NTNU + * @author Sebastian Gundersen + * @copyright 2018 Norwegian University of Science and Technology (NTNU) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ diff --git a/db/install.xml b/db/install.xml index b5ca101..126eebe 100755 --- a/db/install.xml +++ b/db/install.xml @@ -1,166 +1,141 @@ - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - - - - -
- - - - - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - - - - - - - -
- - - - - - - - - - - - - - - -
- - - - - - - - - - - -
- - - - - - - - - - - -
-
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + +
+
diff --git a/view_import.php b/db/services.php similarity index 53% rename from view_import.php rename to db/services.php index 922cf25..02b12ca 100644 --- a/view_import.php +++ b/db/services.php @@ -15,26 +15,22 @@ // along with Moodle. If not, see . /** - * Displays the import view + * CAPQuiz webservices. * * @package mod_capquiz - * @author Sebastian S. Gundersen - * @copyright 2019 NTNU + * @author Sebastian Gundersen + * @copyright 2025 Norwegian University of Science and Technology (NTNU) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -namespace mod_capquiz; +defined('MOODLE_INTERNAL') || die; -require_once('../../config.php'); -require_once($CFG->dirroot . '/question/editlib.php'); -require_once($CFG->dirroot . '/mod/capquiz/lib.php'); - -$cmid = capquiz_urls::require_course_module_id_param(); -$cm = get_coursemodule_from_id('capquiz', $cmid, 0, false, MUST_EXIST); -require_login($cm->course, false, $cm); -$context = \context_module::instance($cmid); -require_capability('mod/capquiz:instructor', $context); - -$capquiz = new capquiz($cmid); -capquiz_urls::set_page_url($capquiz, capquiz_urls::$urledit); -$capquiz->renderer()->display_import($capquiz); +$functions = [ + 'mod_capquiz_set_question_rating' => [ + 'classname' => \mod_capquiz\external\set_question_rating::class, + 'description' => 'Set question rating manually', + 'type' => 'write', + 'capabilities' => 'mod/capquiz:instructor', + 'ajax' => true, + ], +]; diff --git a/db/upgrade.php b/db/upgrade.php index bb69056..772806f 100755 --- a/db/upgrade.php +++ b/db/upgrade.php @@ -18,12 +18,11 @@ * File to keep track of upgrades to the capquiz plugin * * @package mod_capquiz - * @author Sebastian S. Gundersen - * @copyright 2018 NTNU + * @author Sebastian Gundersen + * @copyright 2024 Norwegian University of Science and Technology (NTNU) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -/** /** * Function to upgrade mod_capquiz * @@ -182,16 +181,11 @@ function xmldb_capquiz_upgrade($oldversion) { $dbman->add_key($atable, $aprevqprevrkey); } - $aurfield = new xmldb_field( - 'user_rating_id', XMLDB_TYPE_INTEGER, 11, null, null, null, null); - $aurkey = new xmldb_key( - 'user_rating_id', XMLDB_KEY_FOREIGN, ['user_rating_id'], 'capquiz_user_rating', ['id']); - $aprevurfield = new xmldb_field( - 'user_prev_rating_id', - XMLDB_TYPE_INTEGER, 11, null, null, null, null); - $aprevurkey = new xmldb_key( - 'user_prev_rating_id', - XMLDB_KEY_FOREIGN, ['user_prev_rating_id'], 'capquiz_user_rating', ['id']); + $aurfield = new xmldb_field('user_rating_id', XMLDB_TYPE_INTEGER, 11, null, null, null, null); + $aurkey = new xmldb_key('user_rating_id', XMLDB_KEY_FOREIGN, ['user_rating_id'], 'capquiz_user_rating', ['id']); + $aprevurfield = new xmldb_field('user_prev_rating_id', XMLDB_TYPE_INTEGER, 11, null, null, null, null); + $aprevurkey = new xmldb_key('user_prev_rating_id', XMLDB_KEY_FOREIGN, + ['user_prev_rating_id'], 'capquiz_user_rating', ['id']); if (!$dbman->field_exists($atable, $aurfield)) { $dbman->add_field($atable, $aurfield); @@ -254,7 +248,7 @@ function xmldb_capquiz_upgrade($oldversion) { $qlists = $DB->get_records('capquiz_question_list'); $totalqlists = count($qlists); $qlistindex = 0; - foreach ($qlists as &$qlist) { + foreach ($qlists as $qlist) { $qlistindex++; $oldqubaid = $qlist->question_usage_id; if (!$oldqubaid) { @@ -273,7 +267,7 @@ function xmldb_capquiz_upgrade($oldversion) { echo ''; echo ''; $userindex = 0; - foreach ($users as &$user) { + foreach ($users as $user) { // Create new question usage for user. $newquba = new stdClass(); @@ -297,7 +291,7 @@ function xmldb_capquiz_upgrade($oldversion) { , [$user->user_id, $oldqubaid]); $slot = 1; - foreach ($attempts as &$attempt) { + foreach ($attempts as $attempt) { $attempt->slot = $slot; $attempt->questionusageid = $newqubaid; $DB->update_record_raw('question_attempts', $attempt, true); @@ -346,7 +340,6 @@ function xmldb_capquiz_upgrade($oldversion) { upgrade_mod_savepoint(true, 2021020600, 'capquiz'); } if ($oldversion < 2021021100) { - $table = new xmldb_table('capquiz_attempt'); $key = new xmldb_key('user_id', XMLDB_KEY_FOREIGN, ['user_id'], 'users', ['id']); @@ -357,5 +350,605 @@ function xmldb_capquiz_upgrade($oldversion) { // Capquiz savepoint reached. upgrade_mod_savepoint(true, 2021021100, 'capquiz'); } + if ($oldversion < 2024101300) { + $table = new xmldb_table('capquiz'); + $field = new xmldb_field('numquestioncandidates', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '10'); + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + $field = new xmldb_field('minquestionsuntilreappearance', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0'); + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + $field = new xmldb_field('userwinprobability', XMLDB_TYPE_NUMBER, '10, 2', null, XMLDB_NOTNULL, null, '0.75'); + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + $field = new xmldb_field('userkfactor', XMLDB_TYPE_NUMBER, '10, 2', null, XMLDB_NOTNULL, null, '32'); + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + $field = new xmldb_field('questionkfactor', XMLDB_TYPE_NUMBER, '10, 2', null, XMLDB_NOTNULL, null, '8'); + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + foreach ($DB->get_records('capquiz') as $capquiz) { + $selection = $DB->get_record('capquiz_question_selection', ['capquiz_id' => $capquiz->id]); + if ($selection) { + $config = json_decode($selection->configuration); + if ($selection->strategy === 'N-closest') { + $capquiz->numquestioncandidates = (int)$config->number_of_questions_to_select; + $capquiz->minquestionsuntilreappearance = (int)$config->prevent_same_question_for_turns; + $capquiz->userwinprobability = (float)$config->user_win_probability; + } + $DB->update_record('capquiz', $capquiz); + $DB->delete_records('capquiz_question_selection', ['id' => $selection->id]); + } + $ratingsystem = $DB->get_record('capquiz_rating_system', ['capquiz_id' => $capquiz->id]); + if ($ratingsystem) { + $config = json_decode($ratingsystem->configuration); + $capquiz->userkfactor = (float)$config->student_k_factor; + $capquiz->questionkfactor = (float)$config->question_k_factor; + $DB->update_record('capquiz', $capquiz); + $DB->delete_records('capquiz_rating_system', ['id' => $ratingsystem->id]); + } + } + $table = new xmldb_table('capquiz_question_selection'); + if ($dbman->table_exists($table)) { + $dbman->drop_table($table); + } + $table = new xmldb_table('capquiz_rating_system'); + if ($dbman->table_exists($table)) { + $dbman->drop_table($table); + } + + upgrade_mod_savepoint(true, 2024101300, 'capquiz'); + } + + if ($oldversion < 2024102000) { + // Rename fields to standard names for capquiz_question_list. + $table = new xmldb_table('capquiz_question_list'); + $field = new xmldb_field('time_modified', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0'); + $dbman->rename_field($table, $field, 'timemodified'); + $field = new xmldb_field('time_created', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0'); + $dbman->rename_field($table, $field, 'timecreated'); + $field = new xmldb_field('default_question_rating', XMLDB_TYPE_FLOAT, '11', null, XMLDB_NOTNULL, null, '600'); + $dbman->rename_field($table, $field, 'defaultquestionrating'); + + // Fix default question rating type for capquiz_question_list. + $field = new xmldb_field('defaultquestionrating'); + $field->setType(XMLDB_TYPE_NUMBER); + $field->setLength(10); + $field->setDecimals('2'); + $dbman->change_field_type($table, $field); + + // Add timemodified and timecreated fields to capquiz_question. + $table = new xmldb_table('capquiz_question'); + $field = new xmldb_field('timemodified', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0'); + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + $field = new xmldb_field('timecreated', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0'); + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // Add timemodified and timecreated fields to capquiz_user. + $table = new xmldb_table('capquiz_user'); + $field = new xmldb_field('timemodified', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0'); + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + $field = new xmldb_field('timecreated', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0'); + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // Add timemodified and timecreated fields to capquiz_attempt. + $table = new xmldb_table('capquiz_attempt'); + $field = new xmldb_field('timemodified', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0'); + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + $field = new xmldb_field('timecreated', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0'); + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + upgrade_mod_savepoint(true, 2024102000, 'capquiz'); + } + + if ($oldversion < 2024102100) { + $table = new xmldb_table('capquiz'); + $field = new xmldb_field('defaultquestionrating', XMLDB_TYPE_NUMBER, '10, 2', null, XMLDB_NOTNULL, null, '600'); + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + foreach ($DB->get_records('capquiz') as $capquiz) { + $qlist = $DB->get_record('capquiz_question_list', ['capquiz_id' => $capquiz->id]); + if ($qlist) { + $capquiz->defaultquestionrating = $qlist->defaultquestionrating; + $DB->update_record('capquiz', $capquiz); + } + } + $table = new xmldb_table('capquiz_question_list'); + $field = new xmldb_field('defaultquestionrating'); + if ($dbman->field_exists($table, $field)) { + $dbman->drop_field($table, $field); + } + + upgrade_mod_savepoint(true, 2024102100, 'capquiz'); + } + if ($oldversion < 2024102102) { + $table = new xmldb_table('capquiz'); + $field = new xmldb_field('stars_to_pass', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '3'); + $dbman->rename_field($table, $field, 'starstopass'); + $field = new xmldb_field('starratings', XMLDB_TYPE_CHAR, '255', null, XMLDB_NOTNULL, null, '1300,1450,1600,1800,2000'); + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + foreach ($DB->get_records('capquiz') as $capquiz) { + $qlist = $DB->get_record('capquiz_question_list', ['capquiz_id' => $capquiz->id]); + if ($qlist) { + $capquiz->starratings = $qlist->star_ratings; + $DB->update_record('capquiz', $capquiz); + } + } + $table = new xmldb_table('capquiz_question_list'); + $field = new xmldb_field('star_ratings'); + if ($dbman->field_exists($table, $field)) { + $dbman->drop_field($table, $field); + } + + upgrade_mod_savepoint(true, 2024102102, 'capquiz'); + } + if ($oldversion < 2024122900) { + // Remove question foreign key. + $table = new xmldb_table('capquiz_question'); + $key = new xmldb_key('question_id', XMLDB_KEY_FOREIGN, ['question_id'], 'question', ['id']); + $dbman->drop_key($table, $key); + + // Rename question id field to follow guidelines. + $table = new xmldb_table('capquiz_question'); + $field = new xmldb_field('question_id', XMLDB_TYPE_INTEGER, '11', null, null, null, null); + if ($dbman->field_exists($table, $field)) { + $dbman->rename_field($table, $field, 'questionid'); + } + + // Add new question foreign key. + $table = new xmldb_table('capquiz_question'); + $key = new xmldb_key('questionid', XMLDB_KEY_FOREIGN, ['questionid'], 'question', ['id']); + $dbman->add_key($table, $key); + + // Add capquiz id field to question table. + $table = new xmldb_table('capquiz_question'); + $field = new xmldb_field('capquizid', XMLDB_TYPE_INTEGER, '11', null, XMLDB_NOTNULL, null, null, null); + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // Add foreign key for capquiz id to the question table. + $table = new xmldb_table('capquiz_question'); + $key = new xmldb_key('capquizid', XMLDB_KEY_FOREIGN, ['capquizid'], 'capquiz', ['id']); + $dbman->add_key($table, $key); + + // Set the capquiz ids for all questions. + foreach ($DB->get_records('capquiz_question_list') as $qlist) { + foreach ($DB->get_records('capquiz_question', ['question_list_id' => $qlist->id]) as $question) { + $question->capquizid = $qlist->capquiz_id; + $DB->update_record('capquiz_question', $question); + } + } + + // Remove foreign key to question list from the question table. + $table = new xmldb_table('capquiz_question'); + $key = new xmldb_key('question_list_id', XMLDB_KEY_FOREIGN, ['question_list_id'], 'capquiz_question_list', ['id']); + $dbman->drop_key($table, $key); + + // Remove the question list id field from question table. + $table = new xmldb_table('capquiz_question'); + $field = new xmldb_field('question_list_id'); + if ($dbman->field_exists($table, $field)) { + $dbman->drop_field($table, $field); + } + + // Drop the question list table. + $table = new xmldb_table('capquiz_question_list'); + if ($dbman->table_exists($table)) { + $dbman->drop_table($table); + } + + // Remove timereviewed index from capquiz attempt table. + $table = new xmldb_table('capquiz_attempt'); + $index = new xmldb_index('timereviewed', XMLDB_INDEX_NOTUNIQUE, ['user_id', 'time_reviewed']); + if ($dbman->index_exists($table, $index)) { + $dbman->drop_index($table, $index); + } + + // Remove capquiz user foreign key from capquiz attempt. + $table = new xmldb_table('capquiz_attempt'); + $dbman->drop_key($table, new xmldb_key('user_id', XMLDB_KEY_FOREIGN, ['user_id'], 'capquiz_user', ['id'])); + + // Rename capquiz user id field to follow guidelines and make it clear it's a capquiz user. + $table = new xmldb_table('capquiz_attempt'); + $field = new xmldb_field('user_id', XMLDB_TYPE_INTEGER, '11', null, XMLDB_NOTNULL, null, null); + if ($dbman->field_exists($table, $field)) { + $dbman->rename_field($table, $field, 'capquizuserid'); + } + + // Add new capquiz user foreign key to capquiz attempt table. + $table = new xmldb_table('capquiz_attempt'); + $dbman->add_key($table, new xmldb_key('capquizuserid', XMLDB_KEY_FOREIGN, ['capquizuserid'], 'capquiz_user', ['id'])); + + // Remove question foreign key from capquiz attempt. + $table = new xmldb_table('capquiz_attempt'); + $dbman->drop_key($table, new xmldb_key('question_id', XMLDB_KEY_FOREIGN, ['question_id'], 'capquiz_question', ['id'])); + + // Rename capquiz question field to follow guidelines and make it clear it's a capquiz question. + $table = new xmldb_table('capquiz_attempt'); + $field = new xmldb_field('question_id', XMLDB_TYPE_INTEGER, '11', null, XMLDB_NOTNULL, null, null); + if ($dbman->field_exists($table, $field)) { + $dbman->rename_field($table, $field, 'capquizquestionid'); + } + + // Add new capquiz question foreign key to capquiz attempt table. + $table = new xmldb_table('capquiz_attempt'); + $dbman->add_key($table, + new xmldb_key('capquizquestionid', XMLDB_KEY_FOREIGN, ['capquizquestionid'], 'capquiz_question', ['id'])); + + // Rename capquiz id field in capquiz user. + $table = new xmldb_table('capquiz_user'); + $dbman->drop_key($table, new xmldb_key('capquiz_id', XMLDB_KEY_FOREIGN, ['capquiz_id'], 'capquiz', ['id'])); + $table = new xmldb_table('capquiz_user'); + $field = new xmldb_field('capquiz_id', XMLDB_TYPE_INTEGER, '11', null, XMLDB_NOTNULL, null, null); + if ($dbman->field_exists($table, $field)) { + $dbman->rename_field($table, $field, 'capquizid'); + } + $table = new xmldb_table('capquiz_user'); + $dbman->add_key($table, new xmldb_key('capquizid', XMLDB_KEY_FOREIGN, ['capquizid'], 'capquiz', ['id'])); + + // Rename user id field in capquiz user. + $table = new xmldb_table('capquiz_user'); + $dbman->drop_key($table, new xmldb_key('user_id', XMLDB_KEY_FOREIGN, ['user_id'], 'user', ['id'])); + $table = new xmldb_table('capquiz_user'); + $field = new xmldb_field('user_id', XMLDB_TYPE_INTEGER, '11', null, XMLDB_NOTNULL, null, null); + if ($dbman->field_exists($table, $field)) { + $dbman->rename_field($table, $field, 'userid'); + } + $table = new xmldb_table('capquiz_user'); + $dbman->add_key($table, new xmldb_key('userid', XMLDB_KEY_FOREIGN, ['userid'], 'user', ['id'])); + + // Rename question usage id field in capquiz user. + $table = new xmldb_table('capquiz_user'); + $dbman->drop_key($table, + new xmldb_key('question_usage_id', XMLDB_KEY_FOREIGN_UNIQUE, ['question_usage_id'], 'question_usages', ['id'])); + $table = new xmldb_table('capquiz_user'); + $field = new xmldb_field('question_usage_id', XMLDB_TYPE_INTEGER, '10', null, null, null, null); + if ($dbman->field_exists($table, $field)) { + $dbman->rename_field($table, $field, 'questionusageid'); + } + $table = new xmldb_table('capquiz_user'); + $dbman->add_key($table, + new xmldb_key('questionusageid', XMLDB_KEY_FOREIGN_UNIQUE, ['questionusageid'], 'question_usages', ['id'])); + + // Rename highest level field in capquiz user. + $table = new xmldb_table('capquiz_user'); + $field = new xmldb_field('highest_level', XMLDB_TYPE_INTEGER, '11', null, XMLDB_NOTNULL, null, 0); + if ($dbman->field_exists($table, $field)) { + $dbman->rename_field($table, $field, 'higheststars'); + } + + // Rename stars graded field in capquiz user. + $table = new xmldb_table('capquiz_user'); + $field = new xmldb_field('stars_graded', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, 0); + if ($dbman->field_exists($table, $field)) { + $dbman->rename_field($table, $field, 'starsgraded'); + } + + // Add timemodified and usermodified fields to capquiz_user_rating. + $table = new xmldb_table('capquiz_user_rating'); + $field = new xmldb_field('timemodified', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0'); + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + $table = new xmldb_table('capquiz_user_rating'); + $field = new xmldb_field('usermodified', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0'); + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // Add timemodified and usermodified fields to capquiz_question_rating. + $table = new xmldb_table('capquiz_question_rating'); + $field = new xmldb_field('timemodified', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0'); + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + $table = new xmldb_table('capquiz_question_rating'); + $field = new xmldb_field('usermodified', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0'); + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // Add usermodified field to capquiz_attempt. + $table = new xmldb_table('capquiz_attempt'); + $field = new xmldb_field('usermodified', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0'); + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // Add usermodified field to capquiz_user. + $table = new xmldb_table('capquiz_user'); + $field = new xmldb_field('usermodified', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0'); + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // Add usermodified field to capquiz_question. + $table = new xmldb_table('capquiz_question'); + $field = new xmldb_field('usermodified', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0'); + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // Add usermodified field to capquiz. + $table = new xmldb_table('capquiz'); + $field = new xmldb_field('usermodified', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0'); + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // Rename time answered field in capquiz_attempt. + $table = new xmldb_table('capquiz_attempt'); + $field = new xmldb_field('time_answered', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0'); + if ($dbman->field_exists($table, $field)) { + $dbman->rename_field($table, $field, 'timeanswered'); + } + + // Rename time reviewed field in capquiz_attempt. + $table = new xmldb_table('capquiz_attempt'); + $field = new xmldb_field('time_reviewed', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0'); + if ($dbman->field_exists($table, $field)) { + $dbman->rename_field($table, $field, 'timereviewed'); + } + + // Rename capquiz question id field in capquiz question rating. + $table = new xmldb_table('capquiz_question_rating'); + $dbman->drop_key($table, + new xmldb_key('capquiz_question_id', XMLDB_KEY_FOREIGN, ['capquiz_question_id'], 'capquiz_question', ['id'])); + $table = new xmldb_table('capquiz_question_rating'); + $field = new xmldb_field('capquiz_question_id', XMLDB_TYPE_INTEGER, '11', null, XMLDB_NOTNULL, null, null); + if ($dbman->field_exists($table, $field)) { + $dbman->rename_field($table, $field, 'capquizquestionid'); + } + $table = new xmldb_table('capquiz_question_rating'); + $dbman->add_key($table, + new xmldb_key('capquizquestionid', XMLDB_KEY_FOREIGN, ['capquizquestionid'], 'capquiz_question', ['id'])); + + // Rename capquiz user id field in capquiz user rating. + $table = new xmldb_table('capquiz_user_rating'); + $dbman->drop_key($table, + new xmldb_key('capquiz_user_id', XMLDB_KEY_FOREIGN, ['capquiz_user_id'], 'capquiz_user', ['id'])); + $table = new xmldb_table('capquiz_user_rating'); + $field = new xmldb_field('capquiz_user_id', XMLDB_TYPE_INTEGER, '11', null, XMLDB_NOTNULL, null, null); + if ($dbman->field_exists($table, $field)) { + $dbman->rename_field($table, $field, 'capquizuserid'); + } + $table = new xmldb_table('capquiz_user_rating'); + $dbman->add_key($table, + new xmldb_key('capquizuserid', XMLDB_KEY_FOREIGN, ['capquizuserid'], 'capquiz_user', ['id'])); + + // Add timeopen field to capquiz. + $table = new xmldb_table('capquiz'); + $field = new xmldb_field('timeopen', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0'); + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // Set timeopen and timedue fields based on published state. + foreach ($DB->get_records('capquiz') as $capquiz) { + $capquiz->timeopen = $capquiz->published ? time() : 0; + $DB->update_record('capquiz', $capquiz); + } + + // Remove published field from capquiz. + $table = new xmldb_table('capquiz'); + $field = new xmldb_field('published', XMLDB_TYPE_INTEGER, '2', null, XMLDB_NOTNULL, null, null); + if ($dbman->field_exists($table, $field)) { + $dbman->drop_field($table, $field); + } + + upgrade_mod_savepoint(true, 2024122900, 'capquiz'); + } + + if ($oldversion < 2024122901) { + // Add question behaviour field to capquiz. + $table = new xmldb_table('capquiz'); + $field = new xmldb_field('questionbehaviour', XMLDB_TYPE_CHAR, '32', null, XMLDB_NOTNULL, null, 'immediatefeedback'); + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + upgrade_mod_savepoint(true, 2024122901, 'capquiz'); + } + + if ($oldversion < 2024122902) { + + // Rename capquiz question rating id field in capquiz attempt. + $table = new xmldb_table('capquiz_attempt'); + $key = new xmldb_key('question_rating_id', XMLDB_KEY_FOREIGN, ['question_rating_id'], 'capquiz_question_rating', ['id']); + $dbman->drop_key($table, $key); + $field = new xmldb_field('question_rating_id', XMLDB_TYPE_INTEGER, '11', null, XMLDB_NOTNULL, null, null); + if ($dbman->field_exists($table, $field)) { + $dbman->rename_field($table, $field, 'questionratingid'); + } + $key = new xmldb_key('questionratingid', XMLDB_KEY_FOREIGN, ['questionratingid'], 'capquiz_question_rating', ['id']); + $dbman->add_key($table, $key); + + // Rename question_prev_rating_id. + $table = new xmldb_table('capquiz_attempt'); + $key = new xmldb_key('question_prev_rating_id', XMLDB_KEY_FOREIGN, ['question_prev_rating_id'], + 'capquiz_question_rating', ['id']); + $dbman->drop_key($table, $key); + $field = new xmldb_field('question_prev_rating_id', XMLDB_TYPE_INTEGER, '11', null, XMLDB_NOTNULL, null, null); + if ($dbman->field_exists($table, $field)) { + $dbman->rename_field($table, $field, 'questionprevratingid'); + } + $key = new xmldb_key('questionprevratingid', XMLDB_KEY_FOREIGN, ['questionprevratingid'], + 'capquiz_question_rating', ['id']); + $dbman->add_key($table, $key); + + // Rename prev_question_rating_id. + $table = new xmldb_table('capquiz_attempt'); + $key = new xmldb_key('prev_question_rating_id', XMLDB_KEY_FOREIGN, ['prev_question_rating_id'], + 'capquiz_question_rating', ['id']); + $dbman->drop_key($table, $key); + $field = new xmldb_field('prev_question_rating_id', XMLDB_TYPE_INTEGER, '11', null, XMLDB_NOTNULL, null, null); + if ($dbman->field_exists($table, $field)) { + $dbman->rename_field($table, $field, 'prevquestionratingid'); + } + $key = new xmldb_key('prevquestionratingid', XMLDB_KEY_FOREIGN, ['prevquestionratingid'], + 'capquiz_question_rating', ['id']); + $dbman->add_key($table, $key); + + // Rename prev_question_prev_rating_id. + $table = new xmldb_table('capquiz_attempt'); + $key = new xmldb_key('prev_question_prev_rating_id', XMLDB_KEY_FOREIGN, ['prev_question_prev_rating_id'], + 'capquiz_question_rating', ['id']); + $dbman->drop_key($table, $key); + $field = new xmldb_field('prev_question_prev_rating_id', XMLDB_TYPE_INTEGER, '11', null, XMLDB_NOTNULL, null, null); + if ($dbman->field_exists($table, $field)) { + $dbman->rename_field($table, $field, 'prevquestionprevratingid'); + } + $key = new xmldb_key('prevquestionprevratingid', XMLDB_KEY_FOREIGN, ['prevquestionprevratingid'], + 'capquiz_question_rating', ['id']); + $dbman->add_key($table, $key); + + // Rename user_rating_id. + $table = new xmldb_table('capquiz_attempt'); + $key = new xmldb_key('user_rating_id', XMLDB_KEY_FOREIGN, ['user_rating_id'], 'capquiz_user_rating', ['id']); + $dbman->drop_key($table, $key); + $field = new xmldb_field('user_rating_id', XMLDB_TYPE_INTEGER, '11', null, XMLDB_NOTNULL, null, null); + if ($dbman->field_exists($table, $field)) { + $dbman->rename_field($table, $field, 'userratingid'); + } + $table = new xmldb_table('capquiz_attempt'); + $key = new xmldb_key('userratingid', XMLDB_KEY_FOREIGN, ['userratingid'], 'capquiz_user_rating', ['id']); + $dbman->add_key($table, $key); + + // Rename user_prev_rating_id. + // The foreign key was wrong in the install.xml, causing new installs to not add the key due to it being a duplicate. + // This will still try to delete the correct one, but we'll ignore the exception in case it fails. + try { + $table = new xmldb_table('capquiz_attempt'); + $key = new xmldb_key('user_prev_rating_id', XMLDB_KEY_FOREIGN, ['user_prev_rating_id'], + 'capquiz_user_rating', ['id']); + $dbman->drop_key($table, $key); + } catch (\Exception) { + // Try to avoid code checker complaining. + $table = new xmldb_table('capquiz_attempt'); + } + $field = new xmldb_field('user_prev_rating_id', XMLDB_TYPE_INTEGER, '11', null, XMLDB_NOTNULL, null, null); + if ($dbman->field_exists($table, $field)) { + $dbman->rename_field($table, $field, 'userprevratingid'); + } + $key = new xmldb_key('userprevratingid', XMLDB_KEY_FOREIGN, ['userprevratingid'], 'capquiz_user_rating', ['id']); + $dbman->add_key($table, $key); + + upgrade_mod_savepoint(true, 2024122902, 'capquiz'); + } + + if ($oldversion < 2024122903) { + // Add capquiz id to capquiz attempt table to avoid having to go through capquiz user unnecessarily. + $table = new xmldb_table('capquiz_attempt'); + $field = new xmldb_field('capquizid', XMLDB_TYPE_INTEGER, '11', null, XMLDB_NOTNULL, null, 0); + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + $total = $DB->count_records('capquiz_attempt'); + if ($total > 0) { + $progressbar = new progress_bar('upgradecapquizattempts', 500, true); + $progress = 0; + $attempts = $DB->get_recordset('capquiz_attempt'); + // While it's possible to do this with one query, it's probably best to prioritize compatibility here. + foreach ($attempts as $attempt) { + $user = $DB->get_record('capquiz_user', ['id' => $attempt->capquizuserid], '*', MUST_EXIST); + $attempt->capquizid = $user->capquizid; + $DB->update_record('capquiz_attempt', $attempt); + $progress++; + $progressbar->update($progress, $total, "Upgrading CAPQuiz question attempts - $progress/$total."); + } + $attempts->close(); + } + $field->setDefault(null); + $dbman->change_field_default($table, $field); + $key = new xmldb_key('capquizid', XMLDB_KEY_FOREIGN, ['capquizid'], 'capquiz', ['id']); + $dbman->add_key($table, $key); + } + upgrade_mod_savepoint(true, 2024122903, 'capquiz'); + } + + if ($oldversion < 2024122904) { + // Drop old keys and rename columns. + $table = new xmldb_table('capquiz_attempt'); + $dbman->drop_key($table, + new xmldb_key('capquizquestionid', XMLDB_KEY_FOREIGN, ['capquizquestionid'], 'capquiz_question', ['id'])); + $dbman->rename_field($table, + new xmldb_field('capquizquestionid', XMLDB_TYPE_INTEGER, '11', null, XMLDB_NOTNULL, null, null), 'slotid'); + + $table = new xmldb_table('capquiz_question_rating'); + $dbman->drop_key($table, + new xmldb_key('capquizquestionid', XMLDB_KEY_FOREIGN, ['capquizquestionid'], 'capquiz_question', ['id'])); + $dbman->rename_field($table, + new xmldb_field('capquizquestionid', XMLDB_TYPE_INTEGER, '11', null, XMLDB_NOTNULL, null, null), 'slotid'); + + // Rename question table to slot. + $table = new xmldb_table('capquiz_question'); + $dbman->rename_table($table, 'capquiz_slot'); + + // Add new keys. + $table = new xmldb_table('capquiz_attempt'); + $dbman->add_key($table, new xmldb_key('slotid', XMLDB_KEY_FOREIGN, ['slotid'], 'capquiz_slot', ['id'])); + + $table = new xmldb_table('capquiz_question_rating'); + $dbman->add_key($table, new xmldb_key('slotid', XMLDB_KEY_FOREIGN, ['slotid'], 'capquiz_slot', ['id'])); + + // Migrate from questionids to question references, and change all to latest version. + foreach ($DB->get_records('capquiz') as $capquiz) { + $cmid = get_coursemodule_from_instance('capquiz', $capquiz->id)->id; + $context = \context_module::instance($cmid); + foreach ($DB->get_records('capquiz_slot', ['capquizid' => $capquiz->id]) as $slot) { + $questionbankentry = get_question_bank_entry($slot->questionid); + $DB->insert_record('question_references', (object)[ + 'usingcontextid' => $context->id, + 'component' => 'mod_capquiz', + 'questionarea' => 'slot', + 'itemid' => $slot->id, + 'questionbankentryid' => $questionbankentry->id, + 'version' => null, + ]); + } + } + + // Remove questionid field from slot. + $table = new xmldb_table('capquiz_slot'); + $dbman->drop_key($table, new xmldb_key('questionid', XMLDB_KEY_FOREIGN, ['questionid'], 'question', ['id'])); + $dbman->drop_field($table, new xmldb_field('questionid', XMLDB_TYPE_INTEGER, 11, null, XMLDB_NOTNULL, null, null)); + + upgrade_mod_savepoint(true, 2024122904, 'capquiz'); + } + + if ($oldversion < 2025010500) { + $table = new xmldb_table('capquiz'); + $field = new xmldb_field('questiondisplayoptions', XMLDB_TYPE_TEXT, null, null, null, null, null); + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + foreach ($DB->get_records('capquiz') as $capquiz) { + $capquiz->questiondisplayoptions = json_encode([]); + $DB->update_record('capquiz', $capquiz); + } + $field->setNotNull(); + $dbman->change_field_notnull($table, $field); + } + upgrade_mod_savepoint(true, 2025010500, 'capquiz'); + } + return true; } diff --git a/edit.php b/edit.php old mode 100755 new mode 100644 index 7d34ce5..0cad3ff --- a/edit.php +++ b/edit.php @@ -15,28 +15,80 @@ // along with Moodle. If not, see . /** - * Edit capquiz + * Edit question list. * * @package mod_capquiz - * @author Aleksander Skrede - * @copyright 2018 NTNU + * @author Sebastian Gundersen + * @copyright 2024 Norwegian University of Science and Technology (NTNU) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -namespace mod_capquiz; +use mod_capquiz\api; +use mod_capquiz\capquiz; +use mod_capquiz\capquiz_slot; + +require_once(__DIR__ . '/../../config.php'); + +global $CFG, $OUTPUT, $PAGE, $USER, $DB; -require_once("../../config.php"); require_once($CFG->libdir . '/formslib.php'); +require_once($CFG->dirroot . '/question/editlib.php'); require_once($CFG->dirroot . '/mod/capquiz/lib.php'); -$cmid = capquiz_urls::require_course_module_id_param(); +$cmid = optional_param('id', 0, PARAM_INT); +if (!$cmid) { + $cmid = required_param('cmid', PARAM_INT); +} $cm = get_coursemodule_from_id('capquiz', $cmid, 0, false, MUST_EXIST); require_login($cm->course, false, $cm); -$context = \context_module::instance($cmid); +$context = context_module::instance($cmid); require_capability('mod/capquiz:instructor', $context); -$capquiz = new capquiz($cmid); -capquiz_urls::set_page_url($capquiz, capquiz_urls::$urledit); -$bankrenderer = new output\question_bank_renderer($capquiz, $capquiz->renderer()); -$bankview = $bankrenderer->create_view(); -$capquiz->renderer()->display_question_list_view($capquiz); +$PAGE->set_context($context); +$PAGE->set_cm($cm); +$PAGE->set_pagelayout('admin'); +$PAGE->set_url(new moodle_url('/mod/capquiz/edit.php', ['id' => $cmid])); + +$capquiz = new capquiz($cm->instance); + +if (optional_param('addselectedquestions', false, PARAM_BOOL)) { + // Question IDs are submitted in input names starting with q, and ending with the question ID. + foreach ((array)data_submitted() as $key => $value) { + $matches = []; + if (preg_match('!^q([0-9]+)$!', $key, $matches)) { + $questionid = (int)$matches[1]; + api::create_slot($capquiz, $questionid, $capquiz->get('defaultquestionrating')); + } + } + redirect($PAGE->url); +} + +$action = optional_param('action', null, PARAM_ALPHA); +switch ($action) { + case 'addquestion': + $questionid = required_param('questionid', PARAM_INT); + api::create_slot($capquiz, $questionid, $capquiz->get('defaultquestionrating')); + redirect($PAGE->url); + + case 'deleteslot': + $slotid = required_param('slotid', PARAM_INT); + $slot = new capquiz_slot($slotid); + // We have already confirmed capability on this quiz, so it's enough to check the slot belongs to it. + if ($slot->get('capquizid') === $capquiz->get('id')) { + $slot->delete(); + } + redirect($PAGE->url); + + case 'regradeall': + capquiz_update_grades($capquiz->to_record()); + redirect($PAGE->url); + + default: + break; +} + +echo $OUTPUT->header(); +/** @var \mod_capquiz\output\renderer $renderer */ +$renderer = $PAGE->get_renderer('mod_capquiz'); +echo $renderer->render(new \mod_capquiz\output\edit_questions($capquiz)); +echo $OUTPUT->footer(); diff --git a/error.php b/error.php deleted file mode 100755 index 9ae7e08..0000000 --- a/error.php +++ /dev/null @@ -1,46 +0,0 @@ -. - -/** - * Error page - * - * @package mod_capquiz - * @author Aleksander Skrede - * @copyright 2018 NTNU - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - -namespace mod_capquiz; - -require_once("../../config.php"); -require_once($CFG->libdir . '/formslib.php'); -require_once($CFG->dirroot . '/mod/capquiz/lib.php'); - -$cmid = capquiz_urls::require_course_module_id_param(); -$cm = get_coursemodule_from_id('capquiz', $cmid, 0, false, MUST_EXIST); -require_login($cm->course, false, $cm); - -$cmid = capquiz_urls::require_course_module_id_param(); -$capquiz = new capquiz($cmid); -if (!$capquiz) { - capquiz_urls::redirect_to_front_page(); -} - -capquiz_urls::set_page_url($capquiz, capquiz_urls::$urlerror); - -echo $capquiz->renderer()->output_renderer()->header(); -echo 'Something went wrong.'; -echo $capquiz->renderer()->output_renderer()->footer(); diff --git a/grade.php b/grade.php index a4e2e80..2f0ab25 100644 --- a/grade.php +++ b/grade.php @@ -15,34 +15,31 @@ // along with Moodle. If not, see . /** - * Displays the entry page into the capquiz + * Redirect to report page, or view page if the user can't see reports. * * @package mod_capquiz - * @author Sebastian S. Gundersen - * @copyright 2019 NTNU + * @author Sebastian Gundersen + * @copyright 2024 Norwegian University of Science and Technology (NTNU) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -namespace mod_capquiz; +require_once(__DIR__ . '/../../config.php'); -require_once('../../config.php'); +global $CFG, $DB, $PAGE; -require_login(); +require_once($CFG->dirroot . '/mod/capquiz/locallib.php'); $cmid = required_param('id', PARAM_INT); $cm = get_coursemodule_from_id('capquiz', $cmid, 0, false, MUST_EXIST); require_login($cm->course, false, $cm); +$context = context_module::instance($cm->id); -$cmid = capquiz_urls::require_course_module_id_param(); -$capquiz = new capquiz($cmid); -if (!$capquiz) { - capquiz_urls::redirect_to_front_page(); -} - -capquiz_urls::set_page_url($capquiz, capquiz_urls::$urlview); +$PAGE->set_context($context); +$PAGE->set_cm($cm); +$PAGE->set_url('/mod/capquiz/grade.php', ['id' => $cm->id]); -if (has_capability('mod/capquiz:instructor', $capquiz->context())) { - redirect(capquiz_urls::view_classlist_url()); +if (!has_capability('mod/capquiz:instructor', $context)) { + redirect(new moodle_url('/mod/capquiz/view.php', ['id' => $cm->id])); } -redirect(capquiz_urls::view_url()); +redirect(new moodle_url('/mod/capquiz/report.php', ['id' => $cm->id])); diff --git a/index.php b/index.php index 69a1cf8..0b13d24 100755 --- a/index.php +++ b/index.php @@ -15,22 +15,41 @@ // along with Moodle. If not, see . /** - * Index page + * List all instances of CAPQuiz in a course. * * @package mod_capquiz - * @author Aleksander Skrede - * @copyright 2018 NTNU + * @author Sebastian Gundersen + * @copyright 2024 Norwegian University of Science and Technology (NTNU) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -namespace mod_capquiz; +use mod_capquiz\capquiz; +use mod_capquiz\output\index_table; +use mod_capquiz\output\renderer; require_once('../../config.php'); -require_login(); +global $DB, $PAGE, $OUTPUT; $courseid = required_param('id', PARAM_INT); -$course = $DB->get_record('course', ['id' => $courseid]); -if ($course) { - $PAGE->set_url(capquiz_urls::create_view_url(capquiz_urls::$urlview)); +$course = get_course($courseid); +$context = context_course::instance($courseid); +require_login($course); + +$capquizplural = get_string('modulenameplural', 'capquiz'); + +$PAGE->set_context($context); +$PAGE->set_pagelayout('incourse'); +$PAGE->set_url(new moodle_url('/mod/capquiz/index.php', ['id' => $courseid])); +$PAGE->set_title($capquizplural); +$PAGE->set_heading($course->fullname); + +echo $OUTPUT->header(); + +if (has_capability('mod/capquiz:instructor', $context)) { + /** @var renderer $renderer */ + $renderer = $PAGE->get_renderer('mod_capquiz'); + echo $renderer->render(new index_table(capquiz::get_records(['course' => $courseid]))); } + +echo $OUTPUT->footer(); diff --git a/lang/en/capquiz.php b/lang/en/capquiz.php index 587e20c..fdbef22 100755 --- a/lang/en/capquiz.php +++ b/lang/en/capquiz.php @@ -19,21 +19,18 @@ * * @package mod_capquiz * @author Aleksander Skrede - * @copyright 2018 NTNU + * @author Sebastian Gundersen + * @copyright 2024 Norwegian University of Science and Technology (NTNU) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); -$string['action'] = 'Action'; -$string['add_a_quiz_question'] = 'Add a question to the list'; $string['add_star'] = 'Add star'; -$string['add_the_quiz_question'] = 'Add the question to the list'; $string['add_to_quiz'] = 'Add to capquiz'; $string['attempt'] = 'Attempt'; $string['attempts'] = 'Attempts'; $string['author'] = 'Author'; -$string['available_questions'] = 'Available questions'; $string['capquiz:addinstance'] = 'Add an instance of CAPQuiz'; $string['capquiz:deleteattempts'] = 'Delete capquiz attempts'; @@ -42,18 +39,11 @@ $string['capquiz:student'] = 'Attempt CAPQuiz instances'; $string['capquiz:viewreports'] = 'View capquiz reports'; -$string['capquizquestionid'] = 'CAPQuiz question id'; $string['capquizreporttype'] = 'Report type'; -$string['choose_rating_system'] = 'Choose rating system'; -$string['choose_selection_strategy'] = 'Choose selection strategy'; +$string['capquizslotid'] = 'Slot id'; $string['chronological'] = 'Chronological'; $string['classlist'] = 'Class list'; $string['configure'] = 'Configure'; -$string['configure_capquiz'] = 'Configure CAPQuiz'; -$string['configure_grading'] = 'Configure grading'; -$string['create_own_template'] = 'You can also create your own'; -$string['create_question_list'] = 'Create question list'; -$string['create_template'] = 'Make template'; $string['created'] = 'Created'; $string['dashboard'] = 'Dashboard'; @@ -65,8 +55,6 @@ $string['delete_star'] = 'Delete star'; $string['deleted_attempts'] = 'Deleted attempts'; $string['deleted_grades'] = 'Deleted grades'; -$string['description'] = 'Description'; -$string['description_required'] = 'Description is required.'; $string['due_date_grading'] = 'Due date for final grading was {$a}.'; $string['due_time_grading'] = 'Time for final grading'; @@ -74,10 +62,11 @@ $string['earned_level_star'] = 'You earned {$a} stars in this activity!'; $string['enrolled_students'] = 'Enrolled students'; $string['erroraccessingreport'] = 'You cannot access this report'; +$string['errorvalidatestarratings'] = 'The star ratings list is invalid'; $string['false'] = 'False'; $string['field_required'] = 'This field is required'; -$string['firstname'] = 'First name'; +$string['fromquestionbank'] = 'from question bank'; $string['grade_fail'] = 'Failed'; $string['grade_has_been_set_fail'] = 'You achieved {$a} stars, which is a failing grade.'; @@ -88,21 +77,14 @@ $string['grading_is_completed'] = 'Grading is completed.'; $string['hideshow'] = 'Hide/Show'; -$string['home'] = 'Home'; - -$string['import'] = 'Import'; $string['k_factor_numeric_rule'] = 'K-factor must be a numeric value'; -$string['lastname'] = 'Last name'; $string['level_rating'] = 'Rating required for {$a} stars'; -$string['level_rating_required'] = 'Rating required for {$a} stars is a required field'; $string['level_stars'] = '{$a} Stars'; $string['managecapquizreportplugins'] = 'Manage report plugins'; $string['matchmaking'] = 'Matchmaking'; -$string['merge'] = 'Merge'; -$string['missing_question'] = 'This question is missing.'; $string['modulename'] = 'CAPQuiz'; $string['modulename_help'] = '

The CAPQuiz activity enables a teacher to create quizzes comprising of questions of various types. Questions can be rated to different difficulties while students are given an initial rating. Using these ratings CAPQuiz will match questions to individual students based on their skill level.

'; $string['modulenameplural'] = 'CAPQuizzes'; @@ -112,81 +94,58 @@ $string['name'] = 'Name'; $string['name_required'] = 'Name is required'; $string['need_to_log_in'] = 'You need to log in'; -$string['next'] = 'Next'; $string['no_enrolled_students'] = 'No students are enrolled'; -$string['no_matchmaking_strategy_selected'] = 'No selection strategy has been specified'; $string['no_questions_added_to_list'] = 'No questions added to the list'; -$string['no_strategy_specified'] = 'No strategy specified'; -$string['no_templates_created'] = 'No templates have been created.'; $string['noreports'] = 'No reports accessible'; -$string['not_published'] = 'Not published'; -$string['nothing_here_yet'] = 'Nothing here yet'; -$string['nothing_to_configure_for_strategy'] = 'There is nothing to configure for this strategy'; +$string['notopenforstudents'] = 'Not open for students'; $string['number_of_questions_to_select'] = 'Number of questions to draw'; $string['number_of_questions_to_select_help'] = 'This indicates how many questions are to be drawn from the question bank before matchmaking. A match is made by selecting a question randomly from these.'; $string['number_of_questions_to_select_required'] = 'Number of questions to draw is required'; -$string['one_star'] = '1 Star'; -$string['other_question_lists'] = 'Other question lists'; - $string['pass_or_fail'] = 'Pass / Fail'; $string['pluginadministration'] = 'CAPQuiz administration'; $string['pluginname'] = 'CAPQuiz'; $string['prevent_question_n_times'] = 'Prevent the same question to be used for N attempts'; $string['prevent_question_n_times_help'] = 'This will prevent a student from being matched with the same question for the specified number of attempts.'; $string['privacy:metadata:capquiz_attempt'] = 'Details about each attempt on a CAPQuiz.'; -$string['privacy:metadata:capquiz_attempt:time_answered'] = 'The time that the attempt was answered.'; -$string['privacy:metadata:capquiz_attempt:time_reviewed'] = 'The time that the attempt was reviewed.'; -$string['privacy:metadata:capquiz_attempt:userid'] = 'The user who made the attempt.'; +$string['privacy:metadata:capquiz_attempt:capquizuserid'] = 'The CAPQuiz user who made the attempt.'; +$string['privacy:metadata:capquiz_attempt:timeanswered'] = 'The time that the attempt was answered.'; +$string['privacy:metadata:capquiz_attempt:timereviewed'] = 'The time that the attempt was reviewed.'; $string['privacy:metadata:capquiz_user'] = 'Additional details stored about the user'; -$string['privacy:metadata:capquiz_user:highest_level'] = 'The user\'s highest number of stars achieved.'; +$string['privacy:metadata:capquiz_user:higheststars'] = 'The user\'s highest number of stars achieved.'; $string['privacy:metadata:capquiz_user:rating'] = 'The rating of the user.'; -$string['privacy:metadata:capquiz_user:userid'] = 'The CAPQuiz user.'; +$string['privacy:metadata:capquiz_user:userid'] = 'The user.'; $string['privacy:metadata:capquiz_user_rating'] = 'Details about each user rating created in a CAPQuiz.'; -$string['privacy:metadata:capquiz_user_rating:capquiz_user_id'] = 'The user who\'s rating it is.'; +$string['privacy:metadata:capquiz_user_rating:capquizuserid'] = 'The user who\'s rating it is.'; $string['privacy:metadata:capquiz_user_rating:manual'] = 'Whether or not the user rating was created manually'; $string['privacy:metadata:capquiz_user_rating:rating'] = 'The user\'s rating.'; $string['privacy:metadata:capquiz_user_rating:timecreated'] = 'The time that the user rating was created'; $string['privacy:metadata:core_question'] = 'The CAPQuiz activity stores question usage information in the core_question subsystem.'; -$string['publish'] = 'Publish'; -$string['publish_already_published'] = '

This CAPQuiz is already published

'; -$string['publish_explanation'] = '

Students are unable to answer questions as long as the capquiz is not published. This is useful if you\'re still building your question list and modifying question ratings. Similarly, modifying the default student rating before a capquiz has been published ensures that all students are given the same initial rating.

Students can answer questions once the capquiz has been published. After this point you can still modify your question list and assign different rating to questions. However, modifying the default student rating will not influence rating of students that has already entered the capquiz, but will influence the initial rating of students that has yet to enter the capquiz.

Once CAPQuiz has been published, it can not be reverted and will be visible to students.

'; -$string['publish_no_questions_in_list'] = '

There doesn\'t seem to be any questions in the question list for this CAPQuiz instance. You must have at least one question before you can publish

'; -$string['published'] = 'Published'; $string['question_count'] = 'Question count'; $string['question_k_factor'] = 'Question k-factor'; $string['question_k_factor_help'] = 'A higher question k-factor will make question ratings change faster. Question ratings will only change if one question is answered correctly and the other question is answered incorrectly, in no particular order. The question that is answered incorrectly will gain rating, since it \'won\' over the question that was answered correctly.'; $string['question_k_factor_required'] = 'Question k-factor is required.'; $string['question_k_factor_specified_rule'] = 'Question k-factor must be specified'; -$string['question_list'] = 'Question list'; -$string['question_list_no_questions'] = 'This capquiz has no questions. Add some questions from the question bank below.'; -$string['question_list_not_published'] = 'The question list is not yet published'; -$string['question_list_settings'] = 'Question list settings'; -$string['question_lists'] = 'Question lists'; -$string['question_rating'] = 'Question rating'; +$string['questionbehaviourwarningadaptive'] = '\'Adaptive mode\' is not recommended for CAPQuiz. Please consider \'Interactive with multiple tries\' instead.'; $string['questionid'] = 'Question id'; $string['questionrating'] = 'Question rating'; $string['questions'] = 'Questions'; -$string['questions_in_list'] = 'Questions in the list'; +$string['questionselection'] = 'Question selection'; $string['rating'] = 'Rating'; $string['rating_system'] = 'Rating system'; $string['regrade_all'] = 'Regrade all'; -$string['remove'] = 'Remove'; -$string['report'] = 'report'; $string['reportplugin'] = 'Report plugins'; -$string['reports'] = 'Reports'; $string['reportshowonlyanswered'] = 'Show only answered attempts'; $string['reportshowonlyanswered_help'] = 'Show only attempts by students that have been answered and submitted.'; +$string['reviewattemptdisplayoptions'] = 'Feedback shown during review'; +$string['reviewattemptdisplayoptions_help'] = 'Select the types of feedback that are visible to students after submitting an attempt.'; -$string['select'] = 'Select'; -$string['select_template'] = 'Select one of these templates for your capquiz'; $string['settings'] = 'CAPQuiz settings'; $string['stars'] = 'Stars'; $string['stars_to_pass'] = 'Number of stars required for passing grade'; -$string['stars_to_pass_required'] = 'Stars for passing grade is required (0-5)'; -$string['status'] = 'Status'; +$string['stars_to_pass_required'] = 'Stars for passing grade is required'; $string['strftimedatetimeseconds'] = '%d %B %Y, %I:%M:%S %p'; $string['student_k_factor'] = 'Student k-factor'; $string['student_k_factor_help'] = 'A higher student k-factor will make ratings change faster. If a student answers correctly, the student will receive a higher rating gain and similarly will lose more rating if the question was answered correctly'; @@ -195,10 +154,10 @@ $string['subplugintype_capquizreport'] = 'Report'; $string['subplugintype_capquizreport_plural'] = 'Reports'; -$string['template'] = 'Template'; -$string['template_explanation'] = '

A template is a read-only copy of a question list. Templates allow instructors to reuse question lists between courses or semesters, and can be shared with other instructors. Since a template is a copy of it\'s original question list, instructors can be sure that ratings won\'t be influenced when sharing between CAPQuiz instances. However, if multiple question lists are created from the same template, any changes made to the original question in the question bank will be visible in all templates and question lists. This includes renaming the question title, changing correct answers, descriptions and marks.

'; -$string['template_no_questions_in_list'] = '

There doesn\'t seem to be any questions in the question list for this CAPQuiz instance. Creating a template requires questions in the question list. Add some questions and come back to create your template.

'; $string['timeanswered'] = 'Time answered'; +$string['timeduenotset'] = 'No due time has been set'; +$string['timeopen'] = 'Open for students'; +$string['timeopen_help'] = 'Opens the quiz for students only after the open time. The quiz will not open if this field is disabled.'; $string['timereviewed'] = 'Time reviewed'; $string['title'] = 'Title'; $string['title_required'] = 'Title is required'; @@ -213,7 +172,6 @@ $string['user_win_probability_help'] = 'This specifies the probability of a student answering the question correctly. A probability of 0.5 will make the matchmaking engine try to find a question with a similar rating as the student.'; $string['user_win_probability_required'] = 'Desired user win probability is required'; $string['userid'] = 'User id'; -$string['username'] = 'Username'; $string['userrating'] = 'User rating'; $string['userratings'] = 'User ratings'; diff --git a/lib.php b/lib.php index a89e2f6..f46b815 100755 --- a/lib.php +++ b/lib.php @@ -18,30 +18,37 @@ * This file contains functions used by the capquiz interface * * @package mod_capquiz + * @author Sebastian Gundersen * @author Aleksander Skrede * @author André Storhaug - * @copyright 2018 NTNU + * @copyright 2025 Norwegian University of Science and Technology (NTNU) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ use mod_capquiz\capquiz; -use mod_capquiz\output\question_attempt_renderer; +use mod_capquiz\capquiz_attempt; +use mod_capquiz\capquiz_question_rating; +use mod_capquiz\capquiz_slot; +use mod_capquiz\capquiz_user; +use mod_capquiz\capquiz_user_rating; +use mod_capquiz\local\helpers\questions; +use mod_capquiz\local\helpers\stars; +use mod_capquiz\question\bank\question_bank_view; defined('MOODLE_INTERNAL') || die(); -require_once($CFG->dirroot . '/lib/gradelib.php'); +require_once($CFG->dirroot . '/mod/capquiz/adminlib.php'); /** * Add this capquiz instance to the database * * @param stdClass $modformdata The data submitted from the form */ -function capquiz_add_instance(stdClass $modformdata): bool|int { +function capquiz_add_instance(stdClass $modformdata): int { global $DB; - $modformdata->time_modified = time(); - $modformdata->time_created = time(); - $modformdata->published = false; - return $DB->insert_record('capquiz', $modformdata); + $modformdata->timecreated = \core\di::get(\core\clock::class)->time(); + $modformdata->timemodified = $modformdata->timecreated; + return $DB->insert_record(capquiz::TABLE, $modformdata); } /** @@ -52,7 +59,7 @@ function capquiz_add_instance(stdClass $modformdata): bool|int { function capquiz_update_instance(stdClass $capquiz): bool { global $DB; $capquiz->id = $capquiz->instance; - $DB->update_record('capquiz', $capquiz); + $DB->update_record(capquiz::TABLE, $capquiz); $capquiz->cmidnumber = get_coursemodule_from_instance('capquiz', $capquiz->id)->id; return true; } @@ -60,15 +67,24 @@ function capquiz_update_instance(stdClass $capquiz): bool { /** * Delete this instance from the database * - * @param int $cmid Course module id for the instance to be deleted + * @param int $capquizid CAPQuiz ID */ -function capquiz_delete_instance(int $cmid): void { - $capquiz = new capquiz($cmid); - $user = $capquiz->user(); - if ($user !== null) { - $quba = $user->question_usage(); - question_engine::delete_questions_usage_by_activity($quba->get_id()); +function capquiz_delete_instance(int $capquizid): void { + global $DB; + $capquizuserids = array_column($DB->get_records(capquiz_user::TABLE, ['capquizid' => $capquizid], fields: 'id'), 'id'); + $qubaidjoin = new qubaid_join('{ ' . capquiz_user::TABLE . '} cu', 'cu.questionusageid', 'cu.capquizid = :capquizid', [ + 'capquizid' => $capquizid, + ]); + question_engine::delete_questions_usage_by_activities($qubaidjoin); + $DB->delete_records_list(capquiz_attempt::TABLE, 'capquizuserid', $capquizuserids); + $DB->delete_records_list(capquiz_user_rating::TABLE, 'capquizuserid', $capquizuserids); + $DB->delete_records(capquiz_user::TABLE, ['capquizid' => $capquizid]); + $DB->delete_records(capquiz_question_rating::TABLE, ['capquizid' => $capquizid]); + $DB->delete_records(capquiz_slot::TABLE, ['capquizid' => $capquizid]); + foreach ($DB->get_records('event', ['modulename' => 'capquiz', 'instance' => $capquizid]) as $event) { + calendar_event::load($event)->delete(); } + $DB->delete_records(capquiz::TABLE, ['id' => $capquizid]); } /** @@ -80,36 +96,46 @@ function capquiz_delete_instance(int $cmid): void { function capquiz_reset_userdata(stdClass $data): array { global $DB; $status = []; - $strmodname = get_string('modulenameplural', 'capquiz'); - $strdeletegrades = get_string('deleted_grades', 'capquiz'); - $strdeleteattempts = get_string('deleted_attempts', 'capquiz'); - capquiz_reset_gradebook($data->courseid); - $status[] = ['component' => $strmodname, 'item' => $strdeletegrades, 'error' => false]; - - $instances = $DB->get_records('capquiz', ['course' => $data->courseid]); - foreach ($instances as $instance) { - $users = $DB->get_records('capquiz_user', ['capquiz_id' => $instance->id]); - if (!$users) { - continue; - } - foreach ($users as $user) { - question_engine::delete_questions_usage_by_activity($user->question_usage_id); - $DB->delete_records('capquiz_attempt', ['user_id' => $user->id]); + $status[] = [ + 'component' => get_string('modulenameplural', 'capquiz'), + 'item' => get_string('deleted_grades', 'capquiz'), + 'error' => false, + ]; + foreach (capquiz::get_records(['course' => $data->courseid]) as $capquiz) { + foreach (capquiz_user::get_records(['capquizid' => $capquiz->get('id')]) as $user) { + question_engine::delete_questions_usage_by_activity($user->get('questionusageid')); } - $DB->delete_records('capquiz_user', ['capquiz_id' => $instance->id]); + $DB->delete_records(capquiz_attempt::TABLE, ['capquizid' => $capquiz->get('id')]); + $DB->delete_records(capquiz_user::TABLE, ['capquizid' => $capquiz->get('id')]); } - $status[] = ['component' => $strmodname, 'item' => $strdeleteattempts, 'error' => false]; + $status[] = [ + 'component' => get_string('modulenameplural', 'capquiz'), + 'item' => get_string('deleted_attempts', 'capquiz'), + 'error' => false, + ]; return $status; } /** - * Finds all assignment notifications that have yet to be mailed out, and mails them. + * Generates and returns list of available CAPQuiz report sub-plugins * - * Cron function to be run periodically according to the moodle cron. + * @return array list of valid reports present */ -function capquiz_cron(): bool { - return true; +function capquiz_report_list(): array { + static $reportlist; + if (!empty($reportlist)) { + return $reportlist; + } + $reportlist = []; + $pluginmanager = new capquiz_plugin_manager('capquizreport'); + $enabledplugins = core_plugin_manager::instance()->get_enabled_plugins('capquizreport'); + foreach ($pluginmanager->get_sorted_plugins_list() as $reportname) { + if (isset($enabledplugins[$reportname])) { + $reportlist[] = $reportname; + } + } + return $reportlist; } /** @@ -120,89 +146,80 @@ function capquiz_cron(): bool { */ function capquiz_extend_settings_navigation(settings_navigation $settings, navigation_node $capquiznode): void { global $PAGE, $CFG; + $cm = $settings->get_page()->cm; + if (!has_capability('mod/capquiz:instructor', $cm->context)) { + return; + } + $capquiznode->add_node(navigation_node::create( + get_string('questions', 'capquiz'), + new moodle_url('/mod/capquiz/edit.php', ['id' => $cm->id]), + navigation_node::TYPE_SETTING, + null, + 'capquiz_edit', + )); + $reportsnode = $capquiznode->add_node(navigation_node::create( + get_string('results', 'quiz'), + new moodle_url('/mod/capquiz/report.php', ['id' => $cm->id]), + navigation_node::TYPE_SETTING, + null, + 'capquiz_viewreports', + )); + // We could use showchildreninsubmenu = true to show report types in a submenu, + // but this seems to mess with the styling when a tab in the show more submenu is active. + // Maybe this changes in a future version of Moodle? + foreach (capquiz_report_list() as $reporttype) { + $reportsnode->add_node(navigation_node::create( + get_string('pluginname', "capquizreport_$reporttype"), + new moodle_url('/mod/capquiz/report.php', ['id' => $cm->id, 'reporttype' => $reporttype]), + navigation_node::TYPE_SETTING, + null, + "capquiz_viewreport_$reporttype", + new pix_icon('i/report', ''), + )); + } - // Require {@link https://github.com/moodle/moodle/blob/master/lib/questionlib.php} - // Included here as we only ever want to include this file if we really need to. require_once($CFG->libdir . '/questionlib.php'); - question_extend_settings_navigation($capquiznode, $PAGE->cm->context)->trim_if_empty(); } /** - * Get an upto date list of user grades and feedback for the gradebook. + * Return grade for a given user, or all users. + * TODO: This function seems to have been implemented incorrectly for a long time. + * Need to fix dategraded/datesubmitted. Raw grade has been fixed from 'higheststars' to 'starsgraded'. * * @param stdClass $capquiz database record * @param int $userid int or 0 for all users + * @see quiz_get_user_grades */ function capquiz_get_user_grades(stdClass $capquiz, int $userid = 0): array { - global $DB; - $params = ['capquiz_id' => $capquiz->id]; + $params = ['capquizid' => $capquiz->id]; if ($userid > 0) { - $params['user_id'] = $userid; - } - $users = $DB->get_records('capquiz_user', $params); - if (!$users) { - return []; + $params['userid'] = $userid; } $grades = []; - foreach ($users as $user) { - $grade = new stdClass(); - $grade->userid = $user->user_id; - $grade->rawgrade = $user->highest_level; - $grade->dategraded = time(); - $grade->datesubmitted = time(); - $grades[$user->user_id] = $grade; + foreach (capquiz_user::get_records($params) as $user) { + $grades[$user->get('userid')] = (object) [ + 'userid' => $user->get('userid'), + 'rawgrade' => $user->get('starsgraded'), + 'dategraded' => \core\di::get(\core\clock::class)->time(), + 'datesubmitted' => $user->get('timemodified'), + ]; } return $grades; } /** - * Create grade item for given assignment. - * - * @param stdClass $capquiz record with extra cmidnumber - * @param array|string|null $grades optional array/object of grade(s); 'reset' means reset grades in gradebook - * @return int 0 if ok, error code otherwise - */ -function capquiz_grade_item_update(stdClass $capquiz, $grades = null): int { - global $DB; - $capquiz->cmidnumber = get_coursemodule_from_instance('capquiz', $capquiz->id)->id; - $params = [ - 'itemname' => $capquiz->name, - 'idnumber' => $capquiz->cmidnumber, - ]; - $params['gradetype'] = GRADE_TYPE_VALUE; - $params['grademax'] = 5; - $params['grademin'] = 0; - if ($grades === 'reset') { - $params['reset'] = true; - $grades = null; - } - $status = grade_update('mod/capquiz', $capquiz->course, 'mod', 'capquiz', $capquiz->id, 0, $grades, $params); - $item = grade_item::fetch([ - 'courseid' => $capquiz->course, - 'itemtype' => 'mod', - 'itemmodule' => 'capquiz', - 'iteminstance' => $capquiz->id, - 'outcomeid' => null, - ]); - $item->gradepass = $capquiz->stars_to_pass; - $item->update(); - $users = $DB->get_records('capquiz_user', ['capquiz_id' => $capquiz->id]); - foreach ($users as $user) { - $user->stars_graded = $user->highest_level; - $DB->update_record('capquiz_user', $user); - } - return $status; -} - -/** - * Update activity grades + * Update grades in gradebook. * * @param stdClass $capquiz database record * @param int $userid specific user only, 0 means all * @param bool $nullifnone + * @see quiz_update_grades */ function capquiz_update_grades(stdClass $capquiz, int $userid = 0, $nullifnone = true): void { + global $CFG; + require_once($CFG->libdir . '/gradelib.php'); + $grades = capquiz_get_user_grades($capquiz, $userid); if ($grades) { capquiz_grade_item_update($capquiz, $grades); @@ -210,26 +227,85 @@ function capquiz_update_grades(stdClass $capquiz, int $userid = 0, $nullifnone = $grade = new stdClass(); $grade->userid = $userid; $grade->rawgrade = null; - capquiz_grade_item_update($capquiz, [$userid => $grade]); + capquiz_grade_item_update($capquiz, $grade); } else { capquiz_grade_item_update($capquiz); } } /** - * Reset activity gradebook + * Create or update the grade item for a given CAPQuiz. + * + * @param stdClass $capquiz record with extra cmidnumber + * @param array|string|null $grades optional array/object of grade(s); 'reset' means reset grades in gradebook + * @return int 0 if ok, error code otherwise + * @see quiz_grade_item_update + */ +function capquiz_grade_item_update(stdClass $capquiz, $grades = null): int { + global $CFG; + require_once($CFG->libdir . '/gradelib.php'); + + if (!isset($capquiz->cmidnumber)) { + $capquiz->cmidnumber = get_coursemodule_from_instance('capquiz', $capquiz->id, $capquiz->course)->id; + } + $itemdetails = [ + 'itemname' => $capquiz->name, + 'idnumber' => $capquiz->cmidnumber, + 'gradetype' => GRADE_TYPE_VALUE, + 'grademax' => stars::get_max_stars($capquiz->starratings), + 'grademin' => 0, + 'gradepass' => $capquiz->starstopass, + ]; + if ($grades === 'reset') { + $itemdetails['reset'] = true; + $grades = null; + } + return grade_update('mod/capquiz', $capquiz->course, 'mod', 'capquiz', $capquiz->id, 0, $grades, $itemdetails); +} + +/** + * Remove all grades from gradebook. * - * @param int $courseid id of the course to be reset + * @param int $courseid id of the course to be reset * @param string $type Optional type of assignment to limit the reset to a particular assignment type + * @see quiz_reset_gradebook */ function capquiz_reset_gradebook($courseid, $type = ''): void { - global $DB; - $instances = $DB->get_records('capquiz', ['course' => $courseid]); - foreach ($instances as $instance) { - capquiz_grade_item_update($instance, 'reset'); + foreach (capquiz::get_records(['course' => $courseid]) as $capquiz) { + capquiz_grade_item_update($capquiz->to_record(), 'reset'); } } +/** + * Generates the question bank in a fragment output. This allows + * the question bank to be displayed in a modal. + * + * The only expected argument provided in the $args array is + * 'querystring'. The value should be the list of parameters + * URL encoded and used to build the question bank page. + * + * The individual list of parameters expected can be found in + * question_build_edit_resources. + * + * @param array $args The fragment arguments. + * @return string The rendered mform fragment. + */ +function capquiz_output_fragment_capquiz_qbank(array $args): string { + global $PAGE; + require_capability('mod/capquiz:instructor', $PAGE->context); + $querystring = parse_url($args['querystring'], PHP_URL_QUERY); + $params = []; + parse_str($querystring, $params); + $params['cmid'] = $PAGE->cm->id; + [$url, $contexts, $cmid, $cm, $capquiz, $pagevars] = question_build_edit_resources('editq', '/mod/capquiz/edit.php', $params); + $extraparams = ['cmid' => $cmid]; + ob_start(); + $qbank = new question_bank_view($contexts, $url, get_course($cm->course), $cm, $pagevars, $extraparams); + $qbank->display(); + $qbankhtml = ob_get_clean(); + return html_writer::div(html_writer::div($qbankhtml, 'bd'), 'questionbankformforpopup'); +} + /** * Serve question files. * @@ -237,7 +313,7 @@ function capquiz_reset_gradebook($courseid, $type = ''): void { * @param stdClass $context * @param string $component * @param string $filearea - * @param int $qubaid + * @param int $questionusageid * @param int $slot * @param array $args * @param bool $forcedownload @@ -245,13 +321,12 @@ function capquiz_reset_gradebook($courseid, $type = ''): void { * @see quiz_question_pluginfile */ function capquiz_question_pluginfile(stdClass $course, stdClass $context, string $component, string $filearea, - int $qubaid, int $slot, array $args, bool $forcedownload, array $options = []): void { - global $DB; - $user = $DB->get_record('capquiz_user', ['question_usage_id' => $qubaid]); - $cm = get_coursemodule_from_instance('capquiz', $user->capquiz_id, $course->id, false, MUST_EXIST); + int $questionusageid, int $slot, array $args, bool $forcedownload, array $options = []): void { + $user = capquiz_user::get_record(['questionusageid' => $questionusageid], MUST_EXIST); + $cm = get_coursemodule_from_instance('capquiz', $user->get('capquizid'), $course->id, false, MUST_EXIST); require_login($course, false, $cm); - $quba = question_engine::load_questions_usage_by_activity($qubaid); - $displayoptions = question_attempt_renderer::attempt_display_options(context_module::instance($cm->id)); + $quba = question_engine::load_questions_usage_by_activity($questionusageid); + $displayoptions = questions::get_question_display_options(new capquiz($user->get('capquizid'))); if (!$quba->check_file_access($slot, $displayoptions, $component, $filearea, $args, $forcedownload)) { send_file_not_found(); } @@ -265,31 +340,20 @@ function capquiz_question_pluginfile(stdClass $course, stdClass $context, string send_stored_file($file, 0, 0, $forcedownload, $options); } -// Ugly hack to make 3.11 and 4.0 work seamlessly. -if (!defined('FEATURE_MOD_PURPOSE')) { - define('FEATURE_MOD_PURPOSE', 'mod_purpose'); -} -if (!defined('MOD_PURPOSE_ASSESSMENT')) { - define('MOD_PURPOSE_ASSESSMENT', 'assessment'); -} - /** - * Checks if $feature is supported + * Checks if $feature is supported. * * @param string $feature */ -function capquiz_supports(string $feature) { - switch ($feature) { - case FEATURE_MOD_INTRO: - case FEATURE_BACKUP_MOODLE2: - case FEATURE_SHOW_DESCRIPTION: - case FEATURE_USES_QUESTIONS: - case FEATURE_GRADE_HAS_GRADE: - return true; - case FEATURE_MOD_PURPOSE: - return MOD_PURPOSE_ASSESSMENT; - default: - return false; - } +function capquiz_supports(string $feature): bool|string { + return match ($feature) { + FEATURE_MOD_INTRO, + FEATURE_BACKUP_MOODLE2, + FEATURE_SHOW_DESCRIPTION, + FEATURE_USES_QUESTIONS, + FEATURE_GRADE_HAS_GRADE => true, + FEATURE_MOD_PURPOSE => MOD_PURPOSE_ASSESSMENT, + default => false, + }; } diff --git a/locallib.php b/locallib.php deleted file mode 100644 index 2d46b01..0000000 --- a/locallib.php +++ /dev/null @@ -1,34 +0,0 @@ -. - -/** - * Library of internal classes and functions for module CAPQuiz - * - * @package mod_capquiz - * @author André Storhaug - * @copyright 2019 Norwegian University of Science and Technology (NTNU) - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - -/** - * Base class for all the types of exception we throw. - * - * @author André Storhaug - * @copyright 2019 Norwegian University of Science and Technology (NTNU) - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -class capquiz_exception extends moodle_exception { -} diff --git a/mod_form.php b/mod_form.php index bbf9629..71ba1eb 100755 --- a/mod_form.php +++ b/mod_form.php @@ -14,47 +14,237 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . -/** - * This files defines a form used to add/modify the capquiz module - * - * @package mod_capquiz - * @author Aleksander Skrede - * @copyright 2018 NTNU - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ +declare(strict_types=1); + +use mod_capquiz\api; +use mod_capquiz\capquiz; +use mod_capquiz\capquiz_user; +use mod_capquiz\local\helpers\questions; +use mod_capquiz\local\helpers\stars; defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot . '/course/moodleform_mod.php'); /** - * Class mod_capquiz_mod_form + * CAPQuiz settings form. * + * @package mod_capquiz + * @author Sebastian Gundersen * @author Aleksander Skrede - * @copyright 2018 NTNU + * @copyright 2024 Norwegian University of Science and Technology (NTNU) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class mod_capquiz_mod_form extends moodleform_mod { - /** - * Defines the corm + * Defines the form. */ public function definition(): void { + global $OUTPUT; $form = $this->_form; + + $form->addElement('header', 'general', get_string('general', 'form')); $form->addElement('text', 'name', get_string('name'), ['size' => '64']); $form->setType('name', PARAM_TEXT); $form->addRule('name', null, 'required', null, 'client'); $form->addRule('name', get_string('maximumchars', '', 255), 'maxlength', 255, 'client'); - $form->addElement('text', 'default_user_rating', get_string('default_user_rating', 'capquiz')); - $form->setType('default_user_rating', PARAM_INT); - $form->addRule('default_user_rating', get_string('default_rating_specified_rule', 'capquiz'), 'required', null, 'client'); - $form->addRule('default_user_rating', get_string('default_rating_numeric_rule', 'capquiz'), 'numeric', null, 'client'); - $form->setDefault('default_user_rating', 1200); + $this->standard_intro_elements(); + + $capquiz = new capquiz(empty($this->_cm) ? 0 : (int)$this->_cm->instance); + + // Open and due dates. + $form->addElement('date_time_selector', 'timeopen', get_string('timeopen', 'capquiz'), ['optional' => true]); + $form->addHelpButton('timeopen', 'timeopen', 'capquiz'); + + $form->addElement('date_time_selector', 'timedue', get_string('due_time_grading', 'capquiz'), ['optional' => true]); + + $unsupportedbehaviours = implode(',', api::get_unsupported_question_behaviours()); + $currentbehaviour = $capquiz->get('questionbehaviour'); + $behaviours = question_engine::get_behaviour_options($currentbehaviour); + $behaviours = question_engine::sort_behaviours($behaviours, '', $unsupportedbehaviours, $currentbehaviour); + + $form->addElement('select', 'questionbehaviour', get_string('howquestionsbehave', 'question'), $behaviours); + $form->addHelpButton('questionbehaviour', 'howquestionsbehave', 'question'); + $form->addElement('static', 'questionbehaviourwarning', '', get_string('questionbehaviourwarningadaptive', 'capquiz')); + $form->hideIf('questionbehaviourwarning', 'questionbehaviour', 'ne', 'adaptive'); + + $group = [ + $form->createElement('checkbox', 'reviewfeedback', '', get_string('specificfeedback', 'question')), + $form->createElement('checkbox', 'reviewgeneralfeedback', '', get_string('generalfeedback', 'question')), + $form->createElement('checkbox', 'reviewrightanswer', '', get_string('rightanswer', 'question')), + $form->createElement('checkbox', 'reviewcorrectness', '', get_string('whethercorrect', 'question')), + ]; + $displayoptions = questions::get_question_display_options($capquiz); + $form->setDefault('reviewfeedback', $displayoptions->feedback); + $form->setDefault('reviewgeneralfeedback', $displayoptions->generalfeedback); + $form->setDefault('reviewrightanswer', $displayoptions->rightanswer); + $form->setDefault('reviewcorrectness', $displayoptions->correctness); + $form->addGroup($group, 'reviewoptions', get_string('reviewattemptdisplayoptions', 'capquiz'), null, false); + $form->addHelpButton('reviewoptions', 'reviewattemptdisplayoptions', 'capquiz'); + + // Grading. + $form->addElement('header', 'gradingheader', get_string('grading', 'capquiz')); + + $defaultratingnumericrule = get_string('default_rating_numeric_rule', 'capquiz'); - $this->standard_intro_elements(get_string('description')); + $form->addElement('text', 'defaultuserrating', get_string('default_user_rating', 'capquiz')); + $form->setType('defaultuserrating', PARAM_INT); + $form->setDefault('defaultuserrating', $capquiz->get('defaultuserrating')); + $form->addRule('defaultuserrating', get_string('default_user_rating_required', 'capquiz'), 'required', null, 'client'); + $form->addRule('defaultuserrating', $defaultratingnumericrule, 'numeric', null, 'client'); + + $form->addElement('text', 'defaultquestionrating', get_string('default_question_rating', 'capquiz')); + $form->setType('defaultquestionrating', PARAM_INT); + $form->setDefault('defaultquestionrating', $capquiz->get('defaultquestionrating')); + $form->addRule('defaultquestionrating', get_string('field_required', 'capquiz'), 'required', null, 'client'); + $form->addRule('defaultquestionrating', $defaultratingnumericrule, 'numeric', null, 'client'); + + for ($star = 1; $star <= $capquiz->get_max_stars(); $star++) { + $input = "star_rating_$star"; + $text = get_string('level_rating', 'capquiz', $star); + $elements = []; + $elements[] = $form->createElement('text', $input, $text); + if ($star > 1) { + $elements[] = $form->createElement('submit', "delstarbutton$star", get_string('delete_star', 'capquiz')); + } + $form->addGroup($elements, "star_group_$star", $text, [''], false); + $form->setType($input, PARAM_INT); + $form->setDefault($input, stars::get_required_rating_for_star($capquiz->get('starratings'), $star)); + } + $form->addElement('submit', 'addstarbutton', get_string('add_star', 'capquiz')); + + $form->addElement('text', 'starstopass', get_string('stars_to_pass', 'capquiz')); + $form->setType('starstopass', PARAM_INT); + $form->setDefault('starstopass', $capquiz->get('starstopass')); + $form->addRule('starstopass', get_string('stars_to_pass_required', 'capquiz'), 'required', null, 'client'); + + // Question selection. + $form->addElement('header', 'questionselectionheader', get_string('questionselection', 'capquiz')); + + $form->addElement('text', 'numquestioncandidates', get_string('number_of_questions_to_select', 'capquiz')); + $form->setType('numquestioncandidates', PARAM_INT); + $form->setDefault('numquestioncandidates', $capquiz->get('numquestioncandidates')); + $numquestioncandidatesstr = get_string('number_of_questions_to_select_required', 'capquiz'); + $form->addRule('numquestioncandidates', $numquestioncandidatesstr, 'required', null, 'client'); + $form->addHelpButton('numquestioncandidates', 'number_of_questions_to_select', 'capquiz'); + + $form->addElement('text', 'minquestionsuntilreappearance', get_string('prevent_question_n_times', 'capquiz')); + $form->setType('minquestionsuntilreappearance', PARAM_INT); + $form->setDefault('minquestionsuntilreappearance', $capquiz->get('minquestionsuntilreappearance')); + $form->addRule('minquestionsuntilreappearance', get_string('field_required', 'capquiz'), 'required', null, 'client'); + $form->addHelpButton('minquestionsuntilreappearance', 'prevent_question_n_times', 'capquiz'); + + $form->addElement('text', 'userwinprobability', get_string('user_win_probability', 'capquiz')); + $form->setType('userwinprobability', PARAM_FLOAT); + $form->setDefault('userwinprobability', $capquiz->get('userwinprobability')); + $form->addRule('userwinprobability', get_string('user_win_probability_required', 'capquiz'), 'required', null, 'client'); + $form->addHelpButton('userwinprobability', 'user_win_probability', 'capquiz'); + + // Rating system. + $form->addElement('header', 'ratingsystemheader', get_string('rating_system', 'capquiz')); + + $form->addElement('text', 'userkfactor', get_string('student_k_factor', 'capquiz')); + $form->setType('userkfactor', PARAM_FLOAT); + $form->addRule('userkfactor', get_string('student_k_factor_specified_rule', 'capquiz'), 'required', null, 'client'); + $form->addRule('userkfactor', get_string('k_factor_numeric_rule', 'capquiz'), 'numeric', null, 'client'); + $form->setDefault('userkfactor', $capquiz->get('userkfactor')); + $form->addHelpButton('userkfactor', 'student_k_factor', 'capquiz'); + + $form->addElement('text', 'questionkfactor', get_string('question_k_factor', 'capquiz')); + $form->setType('questionkfactor', PARAM_FLOAT); + $form->addRule('questionkfactor', get_string('question_k_factor_specified_rule', 'capquiz'), 'required', null, 'client'); + $form->addRule('questionkfactor', get_string('k_factor_numeric_rule', 'capquiz'), 'numeric', null, 'client'); + $form->setDefault('questionkfactor', $capquiz->get('questionkfactor')); + $form->addHelpButton('questionkfactor', 'question_k_factor', 'capquiz'); + + // Standard elements. $this->standard_coursemodule_elements(); $this->add_action_buttons(); } + /** + * Validate the data from the form. + * + * @param array $data + * @param array $files + * @return array + */ + public function validation($data, $files): array { + $errors = parent::validation($data, $files); + if ($data['timeopen'] !== 0 && $data['timedue'] !== 0 && $data['timeopen'] > $data['timedue']) { + $errors['timedue'] = get_string('closebeforeopen', 'quiz'); + } + if (empty($data['defaultuserrating'])) { + $errors['defaultuserrating'] = get_string('default_user_rating_required', 'capquiz'); + } + if (empty($data['defaultquestionrating'])) { + $errors['defaultquestionrating'] = get_string('field_required', 'capquiz'); + } + if (empty($data['starstopass']) || $data['starstopass'] < 0 || $data['starstopass'] > 5) { + $errors['starstopass'] = get_string('stars_to_pass_required', 'capquiz'); + } + if (empty($data['userwinprobability'])) { + $errors['userwinprobability'] = get_string('user_win_probability_required', 'capquiz'); + } + if (empty($data['numquestioncandidates'])) { + $errors['numquestioncandidates'] = get_string('number_of_questions_to_select_required', 'capquiz'); + } + if (empty($data['minquestionsuntilreappearance'])) { + $errors['minquestionsuntilreappearance'] = get_string('field_required', 'capquiz'); + } + if (!array_key_exists($data['questionbehaviour'], question_engine::get_archetypal_behaviours())) { + $errors['questionbehaviour'] = get_string('error'); + } else if (in_array($data['questionbehaviour'], api::get_unsupported_question_behaviours())) { + $errors['questionbehaviour'] = get_string('error'); + } + return $errors; + } + + /** + * Process data after default module form processing. + * + * @param stdClass $data + * @return void + */ + public function data_postprocessing($data): void { + parent::data_postprocessing($data); + if (empty($this->_cm->id)) { + return; + } + $capquiz = new capquiz((int)$this->_cm->instance); + if ($data->questionbehaviour !== $capquiz->get('questionbehaviour')) { + foreach (capquiz_user::get_records_by_capquiz($capquiz->get('id')) as $user) { + $quba = $user->get_question_usage(); + $quba->set_preferred_behaviour($data->questionbehaviour); + question_engine::save_questions_usage_by_activity($quba); + } + } + $visible = question_display_options::VISIBLE; + $hidden = question_display_options::HIDDEN; + $capquiz->set('questiondisplayoptions', json_encode([ + 'feedback' => isset($data->reviewfeedback) ? $visible : $hidden, + 'generalfeedback' => isset($data->reviewgeneralfeedback) ? $visible : $hidden, + 'rightanswer' => isset($data->reviewrightanswer) ? $visible : $hidden, + 'correctness' => isset($data->reviewcorrectness) ? $visible : $hidden, + ])); + $star = 1; + $ratings = []; + while (isset($data->{"star_rating_$star"})) { + if (!isset($data->{"delstarbutton$star"})) { + $ratings[] = (int)$data->{"star_rating_$star"}; + } + $star++; + } + if (isset($data->addstarbutton)) { + $ratings[] = end($ratings) + 100; + } + $starratings = implode(',', $ratings); + while (strlen($starratings) > 250) { + array_pop($ratings); + $starratings = implode(',', $ratings); + } + $capquiz->set('starratings', $starratings); + $capquiz->save(); + } } diff --git a/report.php b/report.php new file mode 100644 index 0000000..cbf47ff --- /dev/null +++ b/report.php @@ -0,0 +1,77 @@ +. + +/** + * Show a report. + * + * @package mod_capquiz + * @author Sebastian Gundersen + * @copyright 2024 Norwegian University of Science and Technology (NTNU) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +use mod_capquiz\capquiz; +use mod_capquiz\local\reports\report; + +require_once(__DIR__ . '/../../config.php'); + +global $CFG, $OUTPUT, $PAGE; + +require_once($CFG->dirroot . '/mod/capquiz/report/reportlib.php'); + +$cmid = required_param('id', PARAM_INT); +$download = optional_param('download', '', PARAM_RAW); +$reporttype = optional_param('reporttype', '', PARAM_ALPHA); + +$cm = get_coursemodule_from_id('capquiz', $cmid, 0, false, MUST_EXIST); +require_login($cm->course, false, $cm); + +$PAGE->set_context(context_module::instance($cmid)); +$PAGE->set_cm($cm); +$PAGE->set_pagelayout('report'); +$PAGE->set_url(new moodle_url('/mod/capquiz/report.php', ['id' => $cmid])); + +if (!has_capability('mod/capquiz:instructor', $PAGE->context)) { + throw new moodle_exception('erroraccessingreport', 'capquiz'); +} + +$availablereporttypes = capquiz_report_list(); + +if (empty($reporttype)) { + $reporttype = reset($availablereporttypes); + $PAGE->url->param('reporttype', $reporttype); +} + +if (!in_array($reporttype, $availablereporttypes)) { + throw new moodle_exception('erroraccessingreport', 'capquiz'); +} + +$filepath = "$CFG->dirroot/mod/capquiz/report/$reporttype/classes/report.php"; +if (!is_readable($filepath)) { + throw new moodle_exception("report type '$reporttype' doesn't provide expected file '$filepath'"); +} +require_once($filepath); +$classname = "capquizreport_$reporttype\\report"; +if (!class_exists($classname)) { + throw new moodle_exception("report type '$reporttype' doesn't define expected class '$classname' in '$filepath'"); +} + +/** @var report $report */ +$report = new $classname(); + +echo $OUTPUT->header(); +$report->display(new capquiz($cm->instance), $PAGE->cm, $PAGE->course, $download); +echo $OUTPUT->footer(); diff --git a/report/attempts/attempts_form.php b/report/attempts/classes/form.php similarity index 66% rename from report/attempts/attempts_form.php rename to report/attempts/classes/form.php index 6b86efc..5fbc0d0 100644 --- a/report/attempts/attempts_form.php +++ b/report/attempts/classes/form.php @@ -14,32 +14,54 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . -/** - * CAPQuiz attempts settings form definition. - * - * @package capquizreport_attempts - * @author André Storhaug - * @copyright 2019 Norwegian University of Science and Technology (NTNU) - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ +declare(strict_types=1); namespace capquizreport_attempts; -use mod_capquiz\report\capquiz_attempts_report_form; -use MoodleQuickForm; - defined('MOODLE_INTERNAL') || die(); -require_once($CFG->dirroot . '/mod/capquiz/report/attemptsreport_form.php'); +require_once($CFG->libdir . '/formslib.php'); /** * This is the settings form for the capquiz attempts report. * + * @package capquizreport_attempts * @author André Storhaug * @copyright 2019 Norwegian University of Science and Technology (NTNU) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class capquizreport_attempts_settings_form extends capquiz_attempts_report_form { +class form extends \moodleform { + /** + * Defines the form + */ + protected function definition(): void { + $mform = $this->_form; + + $mform->addElement('select', 'attempts', get_string('reportattemptsfrom', 'quiz'), [ + \mod_capquiz\local\reports\options::ENROLLED_WITH => get_string('reportuserswith', 'quiz'), + \mod_capquiz\local\reports\options::ENROLLED_WITHOUT => get_string('reportuserswithout', 'quiz'), + \mod_capquiz\local\reports\options::ENROLLED_ALL => get_string('reportuserswithorwithout', 'quiz'), + \mod_capquiz\local\reports\options::ALL_WITH => get_string('reportusersall', 'quiz'), + ]); + + $mform->addElement('advcheckbox', 'onlyanswered', '', get_string('reportshowonlyanswered', 'capquiz')); + $mform->addHelpButton('onlyanswered', 'reportshowonlyanswered', 'capquiz'); + + $mform->addElement('text', 'pagesize', get_string('pagesize', 'quiz')); + $mform->setType('pagesize', PARAM_INT); + + $mform->addGroup([ + $mform->createElement('advcheckbox', 'ansstate', '', get_string('ansstate', 'capquizreport_attempts')), + $mform->createElement('advcheckbox', 'urating', '', get_string('urating', 'capquizreport_attempts')), + $mform->createElement('advcheckbox', 'uprevrating', '', get_string('uprevrating', 'capquizreport_attempts')), + $mform->createElement('advcheckbox', 'qprevrating', '', get_string('qprevrating', 'capquizreport_attempts')), + $mform->createElement('advcheckbox', 'qtext', '', get_string('questiontext', 'quiz_responses')), + $mform->createElement('advcheckbox', 'resp', '', get_string('response', 'quiz_responses')), + $mform->createElement('advcheckbox', 'right', '', get_string('rightanswer', 'quiz_responses')), + ], 'coloptions', get_string('showthe', 'quiz_responses'), [' '], false); + + $mform->addElement('submit', 'submitbutton', get_string('showreport', 'quiz')); + } /** * Validate the data from the form. @@ -51,32 +73,10 @@ class capquizreport_attempts_settings_form extends capquiz_attempts_report_form */ public function validation($data, $files): array { $errors = parent::validation($data, $files); - if (!($data['urating'] - || $data['uprevrating'] - || $data['qprevrating'] - || $data['ansstate'] - || $data['qtext'] - || $data['resp'] - || $data['right'])) { + if (!($data['urating'] || $data['uprevrating'] || $data['qprevrating'] + || $data['ansstate'] || $data['qtext'] || $data['resp'] || $data['right'])) { $errors['coloptions'] = get_string('reportmustselectstate', 'quiz'); } return $errors; } - - /** - * Adds any additional preference fields to form - * - * @param MoodleQuickForm $mform the form to add preference fields to - */ - protected function other_preference_fields(MoodleQuickForm $mform): void { - $mform->addGroup([ - $mform->createElement('advcheckbox', 'ansstate', '', get_string('ansstate', 'capquizreport_attempts')), - $mform->createElement('advcheckbox', 'urating', '', get_string('urating', 'capquizreport_attempts')), - $mform->createElement('advcheckbox', 'uprevrating', '', get_string('uprevrating', 'capquizreport_attempts')), - $mform->createElement('advcheckbox', 'qprevrating', '', get_string('qprevrating', 'capquizreport_attempts')), - $mform->createElement('advcheckbox', 'qtext', '', get_string('questiontext', 'quiz_responses')), - $mform->createElement('advcheckbox', 'resp', '', get_string('response', 'quiz_responses')), - $mform->createElement('advcheckbox', 'right', '', get_string('rightanswer', 'quiz_responses')), - ], 'coloptions', get_string('showthe', 'quiz_responses'), [' '], false); - } } diff --git a/report/attempts/attempts_options.php b/report/attempts/classes/options.php similarity index 67% rename from report/attempts/attempts_options.php rename to report/attempts/classes/options.php index 175f967..99fbe8f 100644 --- a/report/attempts/attempts_options.php +++ b/report/attempts/classes/options.php @@ -14,55 +14,61 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . -/** - * Class to store the options for a {@see capquiz_attempts_report}. - * - * @package capquizreport_attempts - * @author André Storhaug - * @copyright 2019 Norwegian University of Science and Technology (NTNU) - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ +declare(strict_types=1); namespace capquizreport_attempts; +use cm_info; use context_module; -use mod_capquiz\report\capquiz_attempts_report; -use mod_capquiz\report\capquiz_attempts_report_options; +use mod_capquiz\capquiz; use stdClass; defined('MOODLE_INTERNAL') || die(); -require_once($CFG->dirroot . '/mod/capquiz/report/attemptsreport_options.php'); +require_once($CFG->dirroot . '/mod/capquiz/classes/local/reports/options.php'); /** - * Class to store the options for a {@see capquiz_attempts_report}. + * Options for the questions report table. * + * @package capquizreport_attempts * @author André Storhaug * @copyright 2019 Norwegian University of Science and Technology (NTNU) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class capquizreport_attempts_options extends capquiz_attempts_report_options { +class options extends \mod_capquiz\local\reports\options { /** @var bool whether to show the question answer state (correct or wrong) columns. */ - public $showansstate = true; + public bool $showansstate = true; /** @var bool whether to show the previous question rating columns. */ - public $showqprevrating = true; + public bool $showqprevrating = true; /** @var bool whether to show the user rating columns. */ - public $showurating = true; + public bool $showurating = true; /** @var bool whether to show the previous user rating columns. */ - public $showuprevrating = true; + public bool $showuprevrating = true; /** @var bool whether to show the question text columns. */ - public $showqtext = false; + public bool $showqtext = false; /** @var bool whether to show the students' response columns. */ - public $showresponses = false; + public bool $showresponses = false; /** @var bool whether to show the correct response columns. */ - public $showright = false; + public bool $showright = false; + + /** + * Constructor. + * + * @param capquiz $capquiz + * @param cm_info $cm + * @param stdClass $course + */ + public function __construct(capquiz $capquiz, cm_info $cm, stdClass $course) { + parent::__construct($capquiz, $cm, $course); + $this->reporttype = 'attempts'; + } /** * Get the current value of the settings to pass to the settings form. @@ -76,7 +82,6 @@ public function get_initial_form_data(): stdClass { $toform->qtext = $this->showqtext; $toform->resp = $this->showresponses; $toform->right = $this->showright; - return $toform; } @@ -86,14 +91,13 @@ public function get_initial_form_data(): stdClass { */ public function setup_from_form_data($fromform): void { parent::setup_from_form_data($fromform); - - $this->showansstate = $fromform->ansstate; - $this->showurating = $fromform->urating; - $this->showuprevrating = $fromform->uprevrating; - $this->showqprevrating = $fromform->qprevrating; - $this->showqtext = $fromform->qtext; - $this->showresponses = $fromform->resp; - $this->showright = $fromform->right; + $this->showansstate = (bool)$fromform->ansstate; + $this->showurating = (bool)$fromform->urating; + $this->showuprevrating = (bool)$fromform->uprevrating; + $this->showqprevrating = (bool)$fromform->qprevrating; + $this->showqtext = (bool)$fromform->qtext; + $this->showresponses = (bool)$fromform->resp; + $this->showright = (bool)$fromform->right; } /** @@ -101,7 +105,6 @@ public function setup_from_form_data($fromform): void { */ public function setup_from_params(): void { parent::setup_from_params(); - $this->showansstate = optional_param('ansstate', $this->showansstate, PARAM_BOOL); $this->showurating = optional_param('urating', $this->showurating, PARAM_BOOL); $this->showuprevrating = optional_param('uprevrating', $this->showuprevrating, PARAM_BOOL); @@ -117,14 +120,13 @@ public function setup_from_params(): void { */ public function setup_from_user_preferences(): void { parent::setup_from_user_preferences(); - - $this->showansstate = get_user_preferences('capquizreport_attempts_ansstate', $this->showansstate); - $this->showurating = get_user_preferences('capquizreport_attempts_urating', $this->showurating); - $this->showuprevrating = get_user_preferences('capquizreport_attempts_uprevrating', $this->showuprevrating); - $this->showqprevrating = get_user_preferences('capquizreport_attempts_qprevrating', $this->showqprevrating); - $this->showqtext = get_user_preferences('capquizreport_attempts_qtext', $this->showqtext); - $this->showresponses = get_user_preferences('capquizreport_attempts_resp', $this->showresponses); - $this->showright = get_user_preferences('capquizreport_attempts_right', $this->showright); + $this->showansstate = (bool)get_user_preferences('capquizreport_attempts_ansstate', $this->showansstate); + $this->showurating = (bool)get_user_preferences('capquizreport_attempts_urating', $this->showurating); + $this->showuprevrating = (bool)get_user_preferences('capquizreport_attempts_uprevrating', $this->showuprevrating); + $this->showqprevrating = (bool)get_user_preferences('capquizreport_attempts_qprevrating', $this->showqprevrating); + $this->showqtext = (bool)get_user_preferences('capquizreport_attempts_qtext', $this->showqtext); + $this->showresponses = (bool)get_user_preferences('capquizreport_attempts_resp', $this->showresponses); + $this->showright = (bool)get_user_preferences('capquizreport_attempts_right', $this->showright); } /** @@ -133,7 +135,6 @@ public function setup_from_user_preferences(): void { */ public function update_user_preferences(): void { parent::update_user_preferences(); - set_user_preference('capquizreport_attempts_ansstate', $this->showansstate); set_user_preference('capquizreport_attempts_urating', $this->showurating); set_user_preference('capquizreport_attempts_uprevrating', $this->showuprevrating); @@ -148,24 +149,17 @@ public function update_user_preferences(): void { */ public function resolve_dependencies(): void { parent::resolve_dependencies(); - - if (!$this->showansstate - && !$this->showurating - && !$this->showuprevrating - && !$this->showqprevrating - && !$this->showqtext - && !$this->showresponses - && !$this->showright) { + if (!$this->showansstate && !$this->showurating && !$this->showqprevrating && !$this->showuprevrating + && !$this->showqtext && !$this->showresponses && !$this->showright) { // We have to show at least something. $this->showansstate = true; $this->showurating = true; $this->showqprevrating = true; } - - // We only want to show the checkbox to delete attempts - // if the user has permissions and if the report mode is showing attempts. - $this->checkboxcolumn = has_capability('mod/capquiz:deleteattempts', context_module::instance($this->cm->id)) - && ($this->attempts != capquiz_attempts_report::ENROLLED_WITHOUT); + // We only want to show the checkbox to delete attempts if the user has permissions, + // and if the report type is showing attempts. + $candeleteattempts = has_capability('mod/capquiz:deleteattempts', context_module::instance($this->cm->id)); + $this->checkboxcolumn = $candeleteattempts && $this->attempts !== self::ENROLLED_WITHOUT; } /** diff --git a/report/attempts/classes/privacy/provider.php b/report/attempts/classes/privacy/provider.php index 74c3e70..149a781 100644 --- a/report/attempts/classes/privacy/provider.php +++ b/report/attempts/classes/privacy/provider.php @@ -14,14 +14,7 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . -/** - * Privacy Subsystem implementation for capquizreport_attempts. - * - * @package capquizreport_attempts - * @author André Storhaug - * @copyright 2019 Norwegian University of Science and Technology (NTNU) - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ +declare(strict_types=1); namespace capquizreport_attempts\privacy; @@ -36,10 +29,8 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements null_provider { - /** - * Get the language string identifier with the component's language - * file to explain why this plugin stores no data. + * Get the language string identifier with the component's language file to explain why this plugin stores no data. * * @return string */ diff --git a/report/attempts/classes/report.php b/report/attempts/classes/report.php new file mode 100644 index 0000000..bd3d459 --- /dev/null +++ b/report/attempts/classes/report.php @@ -0,0 +1,205 @@ +. + +declare(strict_types=1); + +namespace capquizreport_attempts; + +use cm_info; +use context_course; +use context_module; +use mod_capquiz\capquiz; +use mod_capquiz\capquiz_slot; +use moodle_url; +use stdClass; + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->dirroot . '/mod/capquiz/classes/local/reports/report.php'); +require_once($CFG->dirroot . '/mod/capquiz/report/attempts/classes/form.php'); +require_once($CFG->dirroot . '/mod/capquiz/report/attempts/classes/options.php'); +require_once($CFG->dirroot . '/mod/capquiz/report/attempts/classes/table.php'); + +/** + * The attempts report provides summary information about each attempt in a capquiz. + * + * @package capquizreport_attempts + * @author André Storhaug + * @copyright 2019 Norwegian University of Science and Technology (NTNU) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class report implements \mod_capquiz\local\reports\report { + /** + * Display attempts report. + * + * @param capquiz $capquiz + * @param cm_info $cm + * @param stdClass $course + * @param string $download + */ + public function display(capquiz $capquiz, cm_info $cm, stdClass $course, string $download): void { + global $DB, $CFG, $OUTPUT, $PAGE; + $context = context_module::instance($cm->id); + $studentsjoins = get_enrolled_with_capabilities_join($context); + $baseurl = new moodle_url('/mod/capquiz/report.php', ['id' => $context->instanceid, 'reporttype' => 'attempts']); + $form = new form($baseurl, ['capquiz' => $capquiz, 'context' => $context]); + $options = new options($capquiz, $cm, $course); + if ($fromform = $form->get_data()) { + $options->process_settings_from_form($fromform); + } else { + $options->process_settings_from_params(); + } + $form->set_data($options->get_initial_form_data()); + $questions = capquiz_report_get_questions($capquiz); + $courseshortname = format_string($course->shortname, true, ['context' => context_course::instance($course->id)]); + $table = new table($capquiz, $context, $options, $studentsjoins, $questions, $options->get_url()); + $filenamesuffix = get_string('attemptsfilename', 'capquizreport_attempts'); + $capquizname = format_string($capquiz->get('name')); + $filename = "$courseshortname - $capquizname - $filenamesuffix"; + $table->is_downloading($options->download, $filename, "$courseshortname $capquizname"); + if ($table->is_downloading()) { + raise_memory_limit(MEMORY_EXTRA); + } + $hasstudents = false; + if (!empty($studentsjoins->joins)) { + $sql = "SELECT DISTINCT u.id + FROM {user} u + $studentsjoins->joins + WHERE $studentsjoins->wheres"; + $hasstudents = $DB->record_exists_sql($sql, $studentsjoins->params); + } + $hasquestions = capquiz_slot::count_records(['capquizid' => $capquiz->get('id')]) > 0; + if (!$table->is_downloading()) { + $PAGE->set_title($capquiz->get('name')); + $PAGE->set_heading($course->fullname); + $title = get_string('pluginname', 'capquizreport_attempts') . ' ' . get_string('report'); + echo $OUTPUT->heading(format_string($title, true, ['context' => context_module::instance($cm->id)])); + echo \html_writer::div(get_string('attemptsnum', 'quiz', capquiz_report_num_attempt($capquiz->get('id')))); + if (!$hasquestions) { + echo capquiz_no_questions_message($cm, $context); + } else if (!$hasstudents) { + echo $OUTPUT->notification(get_string('nostudentsyet')); + } + $form->display(); + } + if (!$hasquestions || empty($questions)) { + return; + } + if (!$hasstudents && $options->attempts !== \mod_capquiz\local\reports\options::ALL_WITH) { + return; + } + $table->setup_sql_queries($studentsjoins); + $columns = []; + $headers = []; + if (!$table->is_downloading() && $options->checkboxcolumn) { + $columns[] = 'checkbox'; + $headers[] = null; + } + if (!$table->is_downloading() && $CFG->grade_report_showuserimage) { + $columns[] = 'picture'; + $headers[] = ''; + } + if (!$table->is_downloading()) { + $columns[] = 'fullname'; + $headers[] = get_string('name'); + } else { + $columns[] = 'lastname'; + $headers[] = get_string('lastname'); + $columns[] = 'firstname'; + $headers[] = get_string('firstname'); + } + // Some extra fields are always displayed when downloading because there's no space constraint, + // therefore do not include in extra-field list. + $fields = \core_user\fields::for_identity($context); + if ($table->is_downloading()) { + $fields = $fields->including('institution', 'department', 'email')->get_required_fields(); + } + foreach ($fields->get_required_fields() as $field) { + $columns[] = $field; + $headers[] = \core_user\fields::get_display_name($field); + } + if ($table->is_downloading()) { + $columns[] = 'institution'; + $headers[] = get_string('institution'); + $columns[] = 'department'; + $headers[] = get_string('department'); + $columns[] = 'email'; + $headers[] = get_string('email'); + $columns[] = 'userid'; + $headers[] = get_string('userid', 'capquiz'); + $columns[] = 'questionid'; + $headers[] = get_string('moodlequestionid', 'capquiz'); + } + if ($options->showansstate) { + $columns[] = 'answerstate'; + $headers[] = get_string('answerstate', 'capquizreport_attempts'); + } + if ($options->showurating) { + $columns[] = 'userrating'; + $headers[] = get_string('userrating', 'capquiz'); + } + if ($options->showuprevrating) { + $columns[] = 'userprevrating'; + $headers[] = get_string('userprevrating', 'capquizreport_attempts'); + } + if ($options->showqprevrating) { + $columns[] = 'questionprevrating'; + $headers[] = get_string('questionprevrating', 'capquizreport_attempts'); + } + if ($table->is_downloading()) { + $columns[] = 'questionprevratingmanual'; + $headers[] = get_string('questionprevratingmanual', 'capquizreport_attempts'); + } + if ($table->is_downloading()) { + $columns[] = 'timeanswered'; + $headers[] = get_string('timeanswered', 'capquiz'); + $columns[] = 'timereviewed'; + $headers[] = get_string('timereviewed', 'capquiz'); + } + if ($options->showqtext) { + $columns[] = 'question'; + $headers[] = get_string('question'); + } + if ($options->showresponses) { + $columns[] = 'response'; + $headers[] = get_string('response', 'capquizreport_attempts'); + } + if ($options->showright) { + $columns[] = 'right'; + $headers[] = get_string('rightanswer', 'question'); + } + $table->define_columns($columns); + $table->define_headers($headers); + $table->sortable(true, 'uniqueid'); + $table->define_baseurl($options->get_url()); + $table->column_suppress('picture'); + $table->column_suppress('fullname'); + foreach (\core_user\fields::for_identity($context)->get_required_fields() as $field) { + $table->column_suppress($field); + } + $table->column_class('picture', 'picture'); + $table->column_class('lastname', 'bold'); + $table->column_class('firstname', 'bold'); + $table->column_class('fullname', 'bold'); + $table->no_sorting('answerstate'); + $table->no_sorting('question'); + $table->no_sorting('response'); + $table->no_sorting('right'); + $table->set_attribute('id', 'responses'); + $table->collapsible(true); + $table->out($options->pagesize, true); + } +} diff --git a/report/attempts/attempts_table.php b/report/attempts/classes/table.php similarity index 65% rename from report/attempts/attempts_table.php rename to report/attempts/classes/table.php index 258e3bc..a30f3c1 100644 --- a/report/attempts/attempts_table.php +++ b/report/attempts/classes/table.php @@ -14,67 +14,56 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . -/** - * This file defines the capquiz attempts table for showing question attempts. - * - * @package capquizreport_attempts - * @author André Storhaug - * @copyright 2019 Norwegian University of Science and Technology (NTNU) - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ +declare(strict_types=1); namespace capquizreport_attempts; use core\context; use core\dml\sql_join; -use mod_capquiz\report\capquiz_attempts_report_options; -use mod_capquiz\report\capquiz_attempts_report_table; +use mod_capquiz\capquiz; use moodle_url; use stdClass; defined('MOODLE_INTERNAL') || die(); -require_once($CFG->dirroot . '/mod/capquiz/report/attemptsreport_table.php'); require_once($CFG->dirroot . '/mod/quiz/locallib.php'); +require_once($CFG->dirroot . '/mod/capquiz/classes/local/reports/table.php'); /** * This is a table subclass for displaying the capquiz attempts report. * + * @package capquizreport_attempts * @author André Storhaug * @copyright 2019 Norwegian University of Science and Technology (NTNU) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class capquizreport_attempts_table extends capquiz_attempts_report_table { - +class table extends \mod_capquiz\local\reports\table { /** - * Constructor - * @param object $capquiz + * Constructor. + * + * @param capquiz $capquiz * @param context $context - * @param capquiz_attempts_report_options $options + * @param options $options * @param sql_join $studentsjoins * @param array $questions * @param moodle_url $reporturl */ - public function __construct($capquiz, context $context, capquiz_attempts_report_options $options, - sql_join $studentsjoins, $questions, $reporturl) { - parent::__construct('mod-capquiz-report-attempts-report', $capquiz, $context, - $options, $studentsjoins, $questions, $reporturl); + public function __construct(capquiz $capquiz, context $context, options $options, sql_join $studentsjoins, + array $questions, moodle_url $reporturl) { + parent::__construct('attempts', $capquiz, $context, $options, $studentsjoins, $questions, $reporturl); } /** - * Take the data returned from the db_query and go through all the rows - * processing each col using either col_{columnname} method or other_cols - * method or if other_cols returns NULL then put the data straight into the - * table. + * Take the data returned from the db_query and go through all the rows processing each col using either col_{columnname} + * method or other_cols method or if other_cols returns NULL then put the data straight into the table. * * After calling this function, don't forget to call close_recordset. */ - public function build_table() { - if (!$this->rawdata) { - return; + public function build_table(): void { + if ($this->rawdata) { + $this->strtimeformat = str_replace(',', ' ', get_string('strftimedatetimeseconds', 'capquiz')); + parent::build_table(); } - $this->strtimeformat = str_replace(',', ' ', get_string('strftimedatetimeseconds', 'capquiz')); - parent::build_table(); } /** @@ -86,9 +75,9 @@ public function build_table() { */ public function other_cols($colname, $attempt): ?string { return match ($colname) { - 'question' => $this->data_col($attempt->slot, 'questionsummary', $attempt), - 'response' => $this->data_col($attempt->slot, 'responsesummary', $attempt), - 'right' => $this->data_col($attempt->slot, 'rightanswer', $attempt), + 'question' => $this->data_col((int)$attempt->slot, 'questionsummary', $attempt), + 'response' => $this->data_col((int)$attempt->slot, 'responsesummary', $attempt), + 'right' => $this->data_col((int)$attempt->slot, 'rightanswer', $attempt), default => null, }; } @@ -114,7 +103,7 @@ public function data_col(int $slot, string $field, stdClass $attempt): string { return '-'; } $value = $this->field_from_extra_data($attempt, $slot, $field); - $summary = $value !== null ? trim($value) : '-'; + $summary = empty($value) ? '-' : trim($value); if ($this->is_downloading() && $this->is_downloading() != 'html') { return $summary; } @@ -138,7 +127,7 @@ public function data_col(int $slot, string $field, stdClass $attempt): string { * @param int $slot * @param string $field */ - protected function field_from_extra_data(stdClass $attempt, int $slot, string $field): string { + protected function field_from_extra_data(stdClass $attempt, int $slot, string $field): ?string { if (!isset($this->lateststeps[$attempt->usageid][$slot])) { return '-'; } @@ -161,11 +150,11 @@ public function col_answerstate(stdClass $attempt): string { if ($attempt->attempt === null || $attempt->usageid === 0) { return '-'; } - $state = $this->slot_state($attempt, $attempt->slot); + $state = $this->slot_state($attempt, (int)$attempt->slot); if ($this->is_downloading()) { - return $state->__toString(); + return (string)$state; } else { - return $this->make_review_link($state, $attempt, $attempt->slot); + return $this->make_review_link((string)$state, $attempt, (int)$attempt->slot); } } @@ -232,33 +221,12 @@ public function col_questionprevratingmanual(stdClass $attempt): string { * Does this report require the detailed information for each question from the question_attempts_steps table? */ protected function requires_latest_steps_loaded(): bool { - if ($this->options->showansstate - || $this->options->showqtext - || $this->options->showresponses - || $this->options->showright) { - return true; - } else { - return false; - } - } - - /** - * Is this a column that depends on joining to the latest state information? - * If so, return the corresponding slot. If not, return false. - * - * @param string $column - * @return false|int|mixed - */ - protected function is_latest_step_column($column) { - if (preg_match('/^(?:question|response|right)/', $column, $matches)) { - return $matches[1]; - } - return false; + return $this->options->showansstate || $this->options->showqtext || $this->options->showresponses + || $this->options->showright; } /** - * A chance for subclasses to modify the SQL after the count query has been generated, - * and before the full query is constructed. + * A chance for subclasses to modify the SQL after the count query is generated, and before the full query is constructed. * * @param string $fields SELECT list. * @param string $from JOINs part of the SQL. @@ -266,20 +234,24 @@ protected function is_latest_step_column($column) { * @param array $params Query params. * @return array with 4 elements ($fields, $from, $where, $params) as from base_sql. */ - protected function update_sql_after_count($fields, $from, $where, $params): array { + protected function update_sql_after_count(string $fields, string $from, string $where, array $params): array { $fields .= ', - cq.question_id AS moodlequestionid, - cqr.rating AS questionrating, - pcqr.rating AS prevquestionrating, - pcqr.manual AS manualprevqrating, - cur.rating AS userrating, - pcur.rating AS prevuserrating, - pcur.rating AS manualprevurating'; + qa.questionid AS questionid, + cqr.rating AS questionrating, + pcqr.rating AS prevquestionrating, + pcqr.manual AS manualprevqrating, + cur.rating AS userrating, + pcur.rating AS prevuserrating, + pcur.rating AS manualprevurating'; - $from .= "\nLEFT JOIN {capquiz_question_rating} cqr ON cqr.id = ca.question_rating_id"; - $from .= "\nLEFT JOIN {capquiz_question_rating} pcqr ON pcqr.id = ca.question_prev_rating_id"; - $from .= "\nLEFT JOIN {capquiz_user_rating} cur ON cur.id = ca.user_rating_id"; - $from .= "\nLEFT JOIN {capquiz_user_rating} pcur ON pcur.id = ca.user_prev_rating_id"; + $from .= " LEFT JOIN {capquiz_question_rating} cqr + ON cqr.id = ca.questionratingid + LEFT JOIN {capquiz_question_rating} pcqr + ON pcqr.id = ca.questionprevratingid + LEFT JOIN {capquiz_user_rating} cur + ON cur.id = ca.userratingid + LEFT JOIN {capquiz_user_rating} pcur + ON pcur.id = ca.userprevratingid"; return [$fields, $from, $where, $params]; } diff --git a/report/attempts/lang/en/capquizreport_attempts.php b/report/attempts/lang/en/capquizreport_attempts.php index 7618787..7319bf7 100644 --- a/report/attempts/lang/en/capquizreport_attempts.php +++ b/report/attempts/lang/en/capquizreport_attempts.php @@ -27,27 +27,17 @@ $string['answerstate'] = 'Answer state'; $string['attemptsfilename'] = 'attempts'; -$string['correct'] = 'Correct'; - -$string['false'] = 'False'; - $string['pluginname'] = 'Attempts'; -$string['privacy:metadata'] = 'The capquizreport attempts plugin does not store any personal data.'; +$string['privacy:metadata'] = 'The CAPQuiz attempts report plugin does not store any personal data.'; $string['qprevrating'] = 'question\'s previous rating'; $string['qrating'] = 'question rating'; -$string['question'] = 'Question'; $string['questionprevrating'] = 'Question\'s previous rating'; $string['questionprevratingmanual'] = 'Previous rating manually updated'; $string['rating_manually_updated'] = 'Previous rating was manually updated'; $string['response'] = 'Response'; -$string['rightanswer'] = 'Right answer'; - -$string['true'] = 'True'; $string['uprevrating'] = 'user\'s previous rating'; $string['urating'] = 'user rating'; $string['userprevrating'] = 'User\'s previous rating'; - -$string['wrong'] = 'Wrong'; diff --git a/report/attempts/report.php b/report/attempts/report.php deleted file mode 100644 index 285f79e..0000000 --- a/report/attempts/report.php +++ /dev/null @@ -1,247 +0,0 @@ -. - -/** - * CAPQuiz attempts report class. - * - * @package capquizreport_attempts - * @author André Storhaug - * @copyright 2019 Norwegian University of Science and Technology (NTNU) - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - -namespace capquizreport_attempts; - -use context_course; -use mod_capquiz\capquiz; -use mod_capquiz\report\capquiz_attempts_report; -use stdClass; - -defined('MOODLE_INTERNAL') || die(); - -require_once($CFG->dirroot . '/mod/capquiz/report/attemptsreport.php'); -require_once(__DIR__ . '/attempts_form.php'); -require_once(__DIR__ . '/attempts_table.php'); -require_once(__DIR__ . '/attempts_options.php'); - -/** - * The capquiz attempts report provides summary information about each attempt in a capquiz. - * - * @author André Storhaug - * @copyright 2019 Norwegian University of Science and Technology (NTNU) - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -class capquizreport_attempts_report extends capquiz_attempts_report { - - /** - * Displays the full report - * @param capquiz $capquiz capquiz object - * @param stdClass $cm - course_module object - * @param stdClass $course - course object - * @param string $download - type of download being requested - */ - public function display($capquiz, $cm, $course, $download): bool { - global $DB; - - list($studentsjoins) = $this->init('attempts', 'capquizreport_attempts\capquizreport_attempts_settings_form', - $capquiz, $cm, $course); - - $this->options = new capquizreport_attempts_options('attempts', $capquiz, $cm, $course); - - if ($fromform = $this->form->get_data()) { - $this->options->process_settings_from_form($fromform); - } else { - $this->options->process_settings_from_params(); - } - - $this->form->set_data($this->options->get_initial_form_data()); - - // Load the required questions. - $questions = capquiz_report_get_questions($capquiz); - - // Prepare for downloading, if applicable. - $courseshortname = format_string($course->shortname, true, ['context' => context_course::instance($course->id)]); - - $table = new capquizreport_attempts_table($capquiz, $this->context, - $this->options, $studentsjoins, $questions, $this->options->get_url()); - - $filename = capquiz_report_download_filename(get_string('attemptsfilename', 'capquizreport_attempts'), - $courseshortname, $capquiz->name()); - - $table->is_downloading($this->options->download, $filename, $courseshortname . ' ' . format_string($capquiz->name())); - - if ($table->is_downloading()) { - raise_memory_limit(MEMORY_EXTRA); - } - - $hasstudents = false; - if (!empty($studentsjoins->joins)) { - $sql = "SELECT DISTINCT u.id - FROM {user} u - $studentsjoins->joins - WHERE $studentsjoins->wheres"; - $hasstudents = $DB->record_exists_sql($sql, $studentsjoins->params); - } - - // phpcs:disable - // TODO enable when support for attempt deletion is implemented {@see delete_selected_attempts}. - // $this->process_actions($capquiz, $cm, $studentsjoins, $this->options->get_url()); - // phpcs:enable - - $hasquestions = capquiz_has_questions($capquiz->id()); - // Start output. - if (!$table->is_downloading()) { - // Only print headers if not asked to download data. - $this->print_standard_header_and_messages($cm, $course, $capquiz, $this->options, $hasquestions, $hasstudents); - - // Print the display options. - $this->form->display(); - } - - if ($hasquestions && !empty($questions) && ($hasstudents || $this->options->attempts == self::ALL_WITH)) { - $table->setup_sql_queries($studentsjoins); - - // Define table columns. - $columns = []; - $headers = []; - - if (!$table->is_downloading() && $this->options->checkboxcolumn) { - $columns[] = 'checkbox'; - $headers[] = null; - } - - $this->add_user_columns($table, $columns, $headers); - - if ($table->is_downloading()) { - $this->add_userid_column($columns, $headers); - $this->add_moodlequestionid_column($columns, $headers); - } - - if ($this->options->showansstate) { - $columns[] = 'answerstate'; - $headers[] = get_string('answerstate', 'capquizreport_attempts'); - } - - $this->add_rating_columns($columns, $headers); - - if ($table->is_downloading()) { - $columns[] = 'questionprevratingmanual'; - $headers[] = get_string('questionprevratingmanual', 'capquizreport_attempts'); - } - - if ($table->is_downloading()) { - $this->add_time_columns($columns, $headers); - } - - if ($this->options->showqtext) { - $columns[] = 'question'; - $headers[] = get_string('question', 'capquizreport_attempts'); - } - if ($this->options->showresponses) { - $columns[] = 'response'; - $headers[] = get_string('response', 'capquizreport_attempts'); - } - if ($this->options->showright) { - $columns[] = 'right'; - $headers[] = get_string('rightanswer', 'capquizreport_attempts'); - } - - $table->define_columns($columns); - $table->define_headers($headers); - $table->sortable(true, 'uniqueid'); - - // Set up the table. - $table->define_baseurl($this->options->get_url()); - - $this->configure_user_columns($table); - - $table->no_sorting('answerstate'); - $table->no_sorting('question'); - $table->no_sorting('response'); - $table->no_sorting('right'); - - $table->set_attribute('id', 'responses'); - - $table->collapsible(true); - - $table->out($this->options->pagesize, true); - - } - return true; - } - - /** - * Adds rating columns to this report - * - * @param array $columns columns to be added - * @param array $headers column headers - */ - protected function add_rating_columns(array &$columns, array &$headers) { - if ($this->options->showurating) { - $this->add_user_rating_column($columns, $headers); - } - if ($this->options->showuprevrating) { - $this->add_user_previous_rating_column($columns, $headers); - } - if ($this->options->showqprevrating) { - $this->add_question_previous_rating_column($columns, $headers); - } - } - - /** - * Adds a user rating column to this report - * - * @param array $columns columns to be added - * @param array $headers column headers - */ - protected function add_user_rating_column(array &$columns, array &$headers) { - $columns[] = 'userrating'; - $headers[] = get_string('userrating', 'capquiz'); - } - - /** - * Adds a column with a users previous rating - * - * @param array $columns columns to be added - * @param array $headers column headers - */ - protected function add_user_previous_rating_column(array &$columns, array &$headers) { - $columns[] = 'userprevrating'; - $headers[] = get_string('userprevrating', 'capquizreport_attempts'); - } - - /** - * Adds column with question rating - * - * @param array $columns columns to be added - * @param array $headers column headers - */ - protected function add_question_rating_column(array &$columns, array &$headers) { - $columns[] = 'questionrating'; - $headers[] = get_string('questionrating', 'capquiz'); - } - - /** - * Adds column with previous question ratings - * - * @param array $columns columns to be added - * @param array $headers column headers - */ - protected function add_question_previous_rating_column(array &$columns, array &$headers) { - $columns[] = 'questionprevrating'; - $headers[] = get_string('questionprevrating', 'capquizreport_attempts'); - } -} diff --git a/report/attempts/version.php b/report/attempts/version.php index b8ecd7d..23de367 100644 --- a/report/attempts/version.php +++ b/report/attempts/version.php @@ -25,8 +25,8 @@ defined('MOODLE_INTERNAL') || die(); -$plugin->version = 2024100700; +$plugin->version = 2024102100; $plugin->requires = 2022041901; $plugin->component = 'capquizreport_attempts'; $plugin->maturity = MATURITY_STABLE; -$plugin->release = '1.1.0'; +$plugin->release = '1.2.0'; diff --git a/report/attemptsreport.php b/report/attemptsreport.php deleted file mode 100644 index 9fe5bef..0000000 --- a/report/attemptsreport.php +++ /dev/null @@ -1,286 +0,0 @@ -. - -/** - * The file defines a base class to be used to build a report like the overview or responses report, with one row per attempt. - * - * @package mod_capquiz - * @author André Storhaug - * @copyright 2019 Norwegian University of Science and Technology (NTNU) - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - -namespace mod_capquiz\report; - -use capquizreport_attempts\capquizreport_attempts_options; -use context_module; -use core\context\module; -use core\dml\sql_join; -use mod_capquiz\capquiz; -use mod_quiz\local\reports\attempts_report_options_form; -use moodle_url; -use stdClass; -use table_sql; - -defined('MOODLE_INTERNAL') || die(); - -require_once($CFG->libdir . '/tablelib.php'); -require_once($CFG->dirroot . '/mod/capquiz/report/report.php'); - -/** - * Base class for capquiz reports that are basically a table with one row for each attempt. - * - * @author André Storhaug - * @copyright 2019 Norwegian University of Science and Technology (NTNU) - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -abstract class capquiz_attempts_report extends report { - /** @var int default page size for reports. */ - const DEFAULT_PAGE_SIZE = 30; - - /** @var string constant used for the options, means all users with attempts. */ - const ALL_WITH = 'all_with'; - /** @var string constant used for the options, means only enrolled users with attempts. */ - const ENROLLED_WITH = 'enrolled_with'; - /** @var string constant used for the options, means only enrolled users without attempts. */ - const ENROLLED_WITHOUT = 'enrolled_without'; - /** @var string constant used for the options, means all enrolled users. */ - const ENROLLED_ALL = 'enrolled_any'; - - /** @var string the mode this report is. */ - protected $mode; - - /** @var module the capquiz context. */ - protected module $context; - - /** @var attempts_report_options_form The settings form to use. */ - protected $form; - - /** @var ?capquizreport_attempts_options the options affecting this report. */ - protected $options = null; - - /** - * Initialise various aspects of this report. - * - * @param string $mode - * @param string $formclass - * @param object $capquiz - * @param object $cm - * @param object $course - * @return array with four elements: - * 0 => integer the current group id (0 for none). - * 1 => \core\dml\sql_join Contains joins, wheres, params for all the students in this course. - * 2 => \core\dml\sql_join Contains joins, wheres, params for all the students in the current group. - * 3 => \core\dml\sql_join Contains joins, wheres, params for all the students to show in the report. - * Will be the same as either element 1 or 2. - */ - protected function init(string $mode, string $formclass, $capquiz, $cm, $course) { - $this->mode = $mode; - $this->context = context_module::instance($cm->id); - $studentsjoins = get_enrolled_with_capabilities_join($this->context); - $this->form = new $formclass($this->get_base_url(), ['capquiz' => $capquiz, 'context' => $this->context]); - return [$studentsjoins]; - } - - - /** - * Get the base URL for this report. - */ - protected function get_base_url(): moodle_url { - return new moodle_url('/mod/capquiz/view_report.php', ['id' => $this->context->instanceid, 'mode' => $this->mode]); - } - - /** - * Outputs the things you commonly want at the top of a capquiz report. - * - * Calls through to {@see print_header_and_tabs()} and then - * outputs the standard group selector, number of attempts summary, - * and messages to cover common cases when the report can't be shown. - * - * @param stdClass $cm the course_module information. - * @param stdClass $course the course settings. - * @param capquiz $capquiz the capquiz settings. - * @param capquiz_attempts_report_options $options the current report settings. - * @param bool $hasquestions whether there are any questions in the capquiz. - * @param bool $hasstudents whether there are any relevant students. - */ - protected function print_standard_header_and_messages(stdClass $cm, stdClass $course, capquiz $capquiz, - capquiz_attempts_report_options $options, bool $hasquestions, - bool $hasstudents): void { - global $OUTPUT; - - $this->print_header_and_tabs($cm, $course, $capquiz, $this->mode); - - // Print information on the number of existing attempts. - $strattemptnum = capquiz_num_attempt_summary($capquiz, true); - if (!empty($strattemptnum)) { - echo '
' . $strattemptnum . '
'; - } - - if (!$hasquestions) { - echo capquiz_no_questions_message($capquiz, $cm, $this->context); - } else if (!$capquiz->is_published()) { - echo capquiz_not_published_message($capquiz, $cm, $this->context); - } else if (!$hasstudents) { - echo $OUTPUT->notification(get_string('nostudentsyet')); - } - } - - /** - * Add all the user-related columns to the $columns and $headers arrays. - * - * @param table_sql $table the table being constructed. - * @param array $columns the list of columns. Added to. - * @param array $headers the columns headings. Added to. - */ - protected function add_user_columns(table_sql $table, array &$columns, array &$headers): void { - global $CFG; - if (!$table->is_downloading() && $CFG->grade_report_showuserimage) { - $columns[] = 'picture'; - $headers[] = ''; - } - if (!$table->is_downloading()) { - $columns[] = 'fullname'; - $headers[] = get_string('name'); - } else { - $columns[] = 'lastname'; - $headers[] = get_string('lastname'); - $columns[] = 'firstname'; - $headers[] = get_string('firstname'); - } - - // When downloading, some extra fields are always displayed (because - // there's no space constraint) so do not include in extra-field list. - $fields = \core_user\fields::for_identity($this->context); - if ($table->is_downloading()) { - $fields = $fields->including('institution', 'department', 'email')->get_required_fields(); - } - $extrafields = $fields->get_required_fields(); - - foreach ($extrafields as $field) { - $columns[] = $field; - $headers[] = \core_user\fields::get_display_name($field); - } - - if ($table->is_downloading()) { - $columns[] = 'institution'; - $headers[] = get_string('institution'); - - $columns[] = 'department'; - $headers[] = get_string('department'); - - $columns[] = 'email'; - $headers[] = get_string('email'); - } - } - - /** - * Add the state column to the $columns and $headers arrays. - * - * @param array $columns the list of columns. Added to. - * @param array $headers the columns headings. Added to. - */ - protected function add_questionid_column(array &$columns, array &$headers): void { - $columns[] = 'questionid'; - $headers[] = get_string('questionid', 'capquiz'); - } - - /** - * Add the state column to the $columns and $headers arrays. - * - * @param array $columns the list of columns. Added to. - * @param array $headers the columns headings. Added to. - */ - protected function add_moodlequestionid_column(array &$columns, array &$headers): void { - $columns[] = 'moodlequestionid'; - $headers[] = get_string('moodlequestionid', 'capquiz'); - } - - /** - * Add the state column to the $columns and $headers arrays. - * - * @param array $columns the list of columns. Added to. - * @param array $headers the columns headings. Added to. - */ - protected function add_userid_column(array &$columns, array &$headers): void { - $columns[] = 'userid'; - $headers[] = get_string('userid', 'capquiz'); - } - - /** - * Add all the time-related columns to the $columns and $headers arrays. - * - * @param array $columns the list of columns. Added to. - * @param array $headers the columns headings. Added to. - */ - protected function add_time_columns(array &$columns, array &$headers): void { - $columns[] = 'timeanswered'; - $headers[] = get_string('timeanswered', 'capquiz'); - - $columns[] = 'timereviewed'; - $headers[] = get_string('timereviewed', 'capquiz'); - } - - /** - * Set the display options for the user-related columns in the table. - * - * @param table_sql $table the table being constructed. - */ - protected function configure_user_columns(table_sql $table): void { - $table->column_suppress('picture'); - $table->column_suppress('fullname'); - $extrafields = \core_user\fields::for_identity($this->context)->get_required_fields(); - foreach ($extrafields as $field) { - $table->column_suppress($field); - } - $table->column_class('picture', 'picture'); - $table->column_class('lastname', 'bold'); - $table->column_class('firstname', 'bold'); - $table->column_class('fullname', 'bold'); - } - - /** - * Process any submitted actions. - * - * @param stdClass $capquiz - * @param stdClass $cm the cm object for the capquiz. - * @param sql_join $allowedjoins (joins, wheres, params) the users whose attempt this user is allowed to modify. - * @param moodle_url $redirecturl where to redircet to after a successful action. - */ - protected function process_actions(stdClass $capquiz, stdClass $cm, sql_join $allowedjoins, moodle_url $redirecturl): void { - if (optional_param('delete', 0, PARAM_BOOL) && confirm_sesskey()) { - if ($attemptids = optional_param_array('attemptid', [], PARAM_INT)) { - require_capability('mod/capquiz:deleteattempts', $this->context); - $this->delete_selected_attempts($capquiz, $cm, $attemptids, $allowedjoins); - redirect($redirecturl); - } - } - } - - /** - * Delete the capquiz attempts. - * - * @param stdClass $capquiz the capquiz settings. Attempts that don't belong to this capquiz are not deleted. - * @param stdClass $cm the course_module object. - * @param array $attemptids the list of attempt ids to delete. - * @param sql_join $allowedjoins (joins, wheres, params) This list of userids that are visible in the report. - * Users can only delete attempts that they are allowed to see in the report. - * Empty means all users. - */ - protected function delete_selected_attempts(stdClass $capquiz, stdClass $cm, array $attemptids, sql_join $allowedjoins) { - // TODO implement to add support for attempt deletion. - } -} diff --git a/report/attemptsreport_form.php b/report/attemptsreport_form.php deleted file mode 100644 index 559dcf2..0000000 --- a/report/attemptsreport_form.php +++ /dev/null @@ -1,118 +0,0 @@ -. - -/** - * Base class for the settings form for {@see capquiz_attempts_report}s. - * - * @package mod_capquiz - * @author André Storhaug - * @copyright 2019 Norwegian University of Science and Technology (NTNU) - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - -namespace mod_capquiz\report; - -use moodleform; -use MoodleQuickForm; - -defined('MOODLE_INTERNAL') || die(); - -require_once($CFG->libdir . '/formslib.php'); - -/** - * Base class for the settings form for {@see capquiz_attempts_report}s. - * - * @author André Storhaug - * @copyright 2019 Norwegian University of Science and Technology (NTNU) - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -abstract class capquiz_attempts_report_form extends moodleform { - - /** - * Validate the data from the form. - * - * @param array $data array of ("fieldname"=>value) of submitted data - * @param array $files array of uploaded files "element_name"=>tmp_file_path - * @return array of "element_name"=>"error_description" if there are errors, - * or an empty array if everything is OK (true allowed for backwards compatibility too). - */ - public function validation($data, $files) { - $errors = parent::validation($data, $files); - return $errors; - } - - /** - * Defines the form - */ - protected function definition(): void { - $mform = $this->_form; - - $mform->addElement('header', 'preferencespage', get_string('reportwhattoinclude', 'quiz')); - $this->standard_attempt_fields($mform); - $this->other_attempt_fields($mform); - - $mform->addElement('header', 'preferencesuser', get_string('reportdisplayoptions', 'quiz')); - $this->standard_preference_fields($mform); - $this->other_preference_fields($mform); - - $mform->addElement('submit', 'submitbutton', get_string('showreport', 'quiz')); - } - - /** - * Adds the standard attempt fields to form - * - * @param MoodleQuickForm $mform the form to add attempt fields to - */ - protected function standard_attempt_fields(MoodleQuickForm $mform): void { - $mform->addElement('select', 'attempts', get_string('reportattemptsfrom', 'quiz'), [ - capquiz_attempts_report::ENROLLED_WITH => get_string('reportuserswith', 'quiz'), - // phpcs:disable - // capquiz_attempts_report::ENROLLED_WITHOUT => get_string('reportuserswithout', 'quiz'), - // capquiz_attempts_report::ENROLLED_ALL => get_string('reportuserswithorwithout', 'quiz'), - // phpcs:enable - capquiz_attempts_report::ALL_WITH => get_string('reportusersall', 'quiz'), - ]); - - $mform->addElement('advcheckbox', 'onlyanswered', '', get_string('reportshowonlyanswered', 'capquiz')); - $mform->addHelpButton('onlyanswered', 'reportshowonlyanswered', 'capquiz'); - } - - /** - * Adds any additional attempt fields to form - * - * @param MoodleQuickForm $mform the form to add attempt fields to - */ - protected function other_attempt_fields(MoodleQuickForm $mform) { - } - - /** - * Adds the standard preference fields to form - * - * @param MoodleQuickForm $mform the form to add preference fields to - */ - protected function standard_preference_fields(MoodleQuickForm $mform) { - $mform->addElement('text', 'pagesize', get_string('pagesize', 'quiz')); - $mform->setType('pagesize', PARAM_INT); - } - - /** - * Adds any additional preference fields to form - * - * @param MoodleQuickForm $mform the form to add preference fields to - */ - protected function other_preference_fields(MoodleQuickForm $mform) { - } -} diff --git a/report/questions/questions_form.php b/report/questions/classes/form.php similarity index 54% rename from report/questions/questions_form.php rename to report/questions/classes/form.php index e11134d..8dd0e77 100644 --- a/report/questions/questions_form.php +++ b/report/questions/classes/form.php @@ -14,70 +14,42 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . -/** - * CAPQuiz questions settings form definition. - * - * @package capquizreport_questions - * @author André Storhaug - * @copyright 2019 Norwegian University of Science and Technology (NTNU) - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ +declare(strict_types=1); namespace capquizreport_questions; -use mod_capquiz\report\capquiz_attempts_report; -use mod_capquiz\report\capquiz_attempts_report_form; -use MoodleQuickForm; - defined('MOODLE_INTERNAL') || die(); -require_once($CFG->dirroot . '/mod/capquiz/report/attemptsreport_form.php'); +require_once($CFG->libdir . '/formslib.php'); /** * This is the settings form for the capquiz questions report. * + * @package capquizreport_questions * @author André Storhaug * @copyright 2019 Norwegian University of Science and Technology (NTNU) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class capquizreport_questions_settings_form extends capquiz_attempts_report_form { - +class form extends \moodleform { /** * Defines the form */ protected function definition(): void { $mform = $this->_form; - $this->standard_attempt_fields($mform); - $this->other_attempt_fields($mform); - $mform->addElement('header', 'preferencesuser', get_string('reportdisplayoptions', 'quiz')); - - $this->standard_preference_fields($mform); - $this->other_preference_fields($mform); - $mform->addElement('submit', 'submitbutton', get_string('showreport', 'quiz')); - } - - /** - * Adds the standard attempt fields to form - * - * @param MoodleQuickForm $mform the form to add attempt fields to - */ - protected function standard_attempt_fields(MoodleQuickForm $mform): void { - $mform->addElement('hidden', 'attempts', capquiz_attempts_report::ALL_WITH); + $mform->addElement('hidden', 'attempts', \mod_capquiz\local\reports\options::ALL_WITH); $mform->setType('attempts', PARAM_ALPHAEXT); $mform->addElement('hidden', 'onlyanswered', 1); $mform->setType('onlyanswered', PARAM_INT); - } - /** - * Adds any additional preference fields to form - * - * @param MoodleQuickForm $mform the form to add preference fields to - */ - protected function other_preference_fields(MoodleQuickForm $mform): void { + $mform->addElement('text', 'pagesize', get_string('pagesize', 'quiz')); + $mform->setType('pagesize', PARAM_INT); + $mform->addGroup([ $mform->createElement('advcheckbox', 'qtext', '', get_string('questiontext', 'quiz_responses')), ], 'coloptions', get_string('showthe', 'quiz_responses'), [' '], false); + + $mform->addElement('submit', 'submitbutton', get_string('showreport', 'quiz')); } } diff --git a/report/questions/questions_options.php b/report/questions/classes/options.php similarity index 71% rename from report/questions/questions_options.php rename to report/questions/classes/options.php index 7a77f78..f01ea16 100644 --- a/report/questions/questions_options.php +++ b/report/questions/classes/options.php @@ -14,45 +14,48 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . -/** - * Class to store the options for a {@see capquiz_questions_report}. - * - * @package capquizreport_questions - * @author André Storhaug - * @copyright 2019 Norwegian University of Science and Technology (NTNU) - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ +declare(strict_types=1); namespace capquizreport_questions; -use mod_capquiz\report\capquiz_attempts_report; -use mod_capquiz\report\capquiz_attempts_report_options; +use cm_info; +use mod_capquiz\capquiz; use stdClass; defined('MOODLE_INTERNAL') || die(); -require_once($CFG->dirroot . '/mod/capquiz/report/attemptsreport_options.php'); +require_once($CFG->dirroot . '/mod/capquiz/classes/local/reports/options.php'); /** - * Class to store the options for a {@see capquiz_questions_report}. + * Options for the attempts report table. * + * @package capquizreport_questions * @author André Storhaug * @copyright 2019 Norwegian University of Science and Technology (NTNU) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class capquizreport_questions_options extends capquiz_attempts_report_options { +class options extends \mod_capquiz\local\reports\options { /** @var bool whether to show the question text columns. */ - public $showqtext = false; + public bool $showqtext = false; /** - * @var string quiz_attempts_report::ALL_WITH or quiz_attempts_report::ENROLLED_WITH - * quiz_attempts_report::ENROLLED_WITHOUT or quiz_attempts_report::ENROLLED_ALL + * Constructor. + * + * @param capquiz $capquiz + * @param cm_info $cm + * @param stdClass $course */ - public $attempts = capquiz_attempts_report::ALL_WITH; + public function __construct(capquiz $capquiz, cm_info $cm, stdClass $course) { + parent::__construct($capquiz, $cm, $course); + $this->reporttype = 'questions'; + $this->attempts = self::ALL_WITH; + } /** * Get the current value of the settings to pass to the settings form. + * + * @return stdClass */ public function get_initial_form_data(): stdClass { $toform = parent::get_initial_form_data(); @@ -62,11 +65,12 @@ public function get_initial_form_data(): stdClass { /** * Set the fields of this object from the form data. + * * @param stdClass $fromform The data from $mform->get_data() from the settings form. */ public function setup_from_form_data(stdClass $fromform): void { parent::setup_from_form_data($fromform); - $this->showqtext = $fromform->qtext; + $this->showqtext = (bool)$fromform->qtext; } /** @@ -74,7 +78,7 @@ public function setup_from_form_data(stdClass $fromform): void { */ public function setup_from_params(): void { parent::setup_from_params(); - $this->showqtext = optional_param('qtext', $this->showqtext, PARAM_BOOL); + $this->showqtext = (bool)optional_param('qtext', $this->showqtext, PARAM_BOOL); } /** @@ -83,7 +87,7 @@ public function setup_from_params(): void { */ public function setup_from_user_preferences(): void { parent::setup_from_user_preferences(); - $this->showqtext = get_user_preferences('capquizreport_questions_qtext', $this->showqtext); + $this->showqtext = (bool)get_user_preferences('capquizreport_questions_qtext', $this->showqtext); } /** diff --git a/report/questions/classes/privacy/provider.php b/report/questions/classes/privacy/provider.php index 1b347ad..11a6c2f 100644 --- a/report/questions/classes/privacy/provider.php +++ b/report/questions/classes/privacy/provider.php @@ -14,14 +14,7 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . -/** - * Privacy Subsystem implementation for capquizreport_questions. - * - * @package capquizreport_questions - * @author André Storhaug - * @copyright 2019 Norwegian University of Science and Technology (NTNU) - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ +declare(strict_types=1); namespace capquizreport_questions\privacy; @@ -36,10 +29,8 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements null_provider { - /** - * Get the language string identifier with the component's language - * file to explain why this plugin stores no data. + * Get the language string identifier with the component's language file to explain why this plugin stores no data. * * @return string */ diff --git a/report/questions/classes/report.php b/report/questions/classes/report.php new file mode 100644 index 0000000..a5407f5 --- /dev/null +++ b/report/questions/classes/report.php @@ -0,0 +1,148 @@ +. + +declare(strict_types=1); + +namespace capquizreport_questions; + +use cm_info; +use context_course; +use context_module; +use mod_capquiz\capquiz; +use mod_capquiz\capquiz_slot; +use moodle_url; +use stdClass; + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->dirroot . '/mod/capquiz/classes/local/reports/report.php'); +require_once($CFG->dirroot . '/mod/capquiz/report/questions/classes/form.php'); +require_once($CFG->dirroot . '/mod/capquiz/report/questions/classes/options.php'); +require_once($CFG->dirroot . '/mod/capquiz/report/questions/classes/table.php'); + +/** + * The questions report provides summary information about each question in a capquiz (mainly ratings). + * + * @package capquizreport_questions + * @author André Storhaug + * @copyright 2019 Norwegian University of Science and Technology (NTNU) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class report implements \mod_capquiz\local\reports\report { + /** + * Display questions report. + * + * @param capquiz $capquiz + * @param cm_info $cm + * @param stdClass $course + * @param string $download + */ + public function display(capquiz $capquiz, cm_info $cm, stdClass $course, string $download): void { + global $DB, $OUTPUT, $PAGE; + $context = context_module::instance($cm->id); + $studentsjoins = get_enrolled_with_capabilities_join($context); + $baseurl = new moodle_url('/mod/capquiz/report.php', ['id' => $context->instanceid, 'reporttype' => 'questions']); + $form = new form($baseurl, ['capquiz' => $capquiz, 'context' => $context]); + $options = new options($capquiz, $cm, $course); + if ($fromform = $form->get_data()) { + $options->process_settings_from_form($fromform); + } else { + $options->process_settings_from_params(); + } + $form->set_data($options->get_initial_form_data()); + $questions = capquiz_report_get_questions($capquiz); + $courseshortname = format_string($course->shortname, true, ['context' => context_course::instance($course->id)]); + $table = new table($capquiz, $context, $options, $studentsjoins, $questions, $options->get_url()); + $filenamesuffix = get_string('questionsfilename', 'capquizreport_questions'); + $capquizname = format_string($capquiz->get('name')); + $filename = "$courseshortname - $capquizname - $filenamesuffix"; + $table->is_downloading($options->download, $filename, "$courseshortname $capquizname"); + if ($table->is_downloading()) { + raise_memory_limit(MEMORY_EXTRA); + } + + $hasstudents = false; + if (!empty($studentsjoins->joins)) { + $sql = "SELECT DISTINCT u.id + FROM {user} u + $studentsjoins->joins + WHERE $studentsjoins->wheres"; + $hasstudents = $DB->record_exists_sql($sql, $studentsjoins->params); + } + + $hasquestions = capquiz_slot::count_records(['capquizid' => $capquiz->get('id')]) > 0; + if (!$table->is_downloading()) { + $PAGE->set_title($capquiz->get('name')); + $PAGE->set_heading($course->fullname); + $title = get_string('pluginname', 'capquizreport_questions') . ' ' . get_string('report'); + echo $OUTPUT->heading(format_string($title, true, ['context' => context_module::instance($cm->id)])); + if (!$hasquestions) { + echo capquiz_no_questions_message($cm, $context); + } else if (!$hasstudents) { + echo $OUTPUT->notification(get_string('nostudentsyet')); + } + $form->display(); + } + + if ($hasquestions && !empty($questions) + && ($hasstudents || $options->attempts === \mod_capquiz\local\reports\options::ALL_WITH)) { + $table->setup_sql_queries($studentsjoins); + $columns = []; + $headers = []; + if ($table->is_downloading()) { + $columns[] = 'attemptid'; + $headers[] = get_string('attemptid', 'capquizreport_questions'); + } + $columns[] = 'slotid'; + $headers[] = get_string('questionid', 'capquiz'); + $columns[] = 'questionrating'; + $headers[] = get_string('questionrating', 'capquiz'); + $columns[] = 'questionprevrating'; + $headers[] = get_string('questionprevrating', 'capquizreport_questions'); + if ($table->is_downloading()) { + $columns[] = 'questionprevratingmanual'; + $headers[] = get_string('questionprevratingmanual', 'capquizreport_questions'); + } + $columns[] = 'questionid'; + $headers[] = get_string('moodlequestionid', 'capquiz'); + if ($options->showqtext) { + $columns[] = 'question'; + $headers[] = get_string('question'); + } + $columns[] = 'timecreated'; + $headers[] = get_string('timecreated'); + + $table->define_columns($columns); + $table->define_headers($headers); + $table->sortable(true, 'uniqueidquestion'); + $table->define_baseurl($options->get_url()); + $table->column_suppress('picture'); + $table->column_suppress('fullname'); + foreach (\core_user\fields::for_identity($context)->get_required_fields() as $field) { + $table->column_suppress($field); + } + $table->column_class('picture', 'picture'); + $table->column_class('lastname', 'bold'); + $table->column_class('firstname', 'bold'); + $table->column_class('fullname', 'bold'); + $table->no_sorting('answerstate'); + $table->no_sorting('question'); + $table->set_attribute('id', 'responses'); + $table->collapsible(true); + $table->out($options->pagesize, true); + } + } +} diff --git a/report/questions/questions_table.php b/report/questions/classes/table.php similarity index 67% rename from report/questions/questions_table.php rename to report/questions/classes/table.php index 6f39675..68fcb7f 100644 --- a/report/questions/questions_table.php +++ b/report/questions/classes/table.php @@ -14,73 +14,59 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . -/** - * This file defines the capquiz questions table for showing question data. - * - * @package capquizreport_questions - * @author André Storhaug - * @copyright 2019 Norwegian University of Science and Technology (NTNU) - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ +declare(strict_types=1); namespace capquizreport_questions; use core\context; use core\dml\sql_join; -use mod_capquiz\report\capquiz_attempts_report; -use mod_capquiz\report\capquiz_attempts_report_options; -use mod_capquiz\report\capquiz_attempts_report_table; +use mod_capquiz\capquiz; use moodle_url; -use quiz_responses_options; use stdClass; defined('MOODLE_INTERNAL') || die(); -require_once($CFG->dirroot . '/mod/capquiz/report/attemptsreport_table.php'); require_once($CFG->dirroot . '/mod/quiz/locallib.php'); - +require_once($CFG->dirroot . '/mod/capquiz/classes/local/reports/table.php'); /** * This is a table subclass for displaying the capquiz question report. * + * @package capquizreport_questions * @author André Storhaug * @copyright 2019 Norwegian University of Science and Technology (NTNU) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class capquizreport_questions_table extends capquiz_attempts_report_table { - +class table extends \mod_capquiz\local\reports\table { /** * Constructor. * - * @param object $capquiz + * @param capquiz $capquiz * @param context $context - * @param capquiz_attempts_report_options $options + * @param options $options * @param sql_join $studentsjoins * @param array $questions * @param moodle_url $reporturl */ - public function __construct($capquiz, $context, capquiz_attempts_report_options $options, - sql_join $studentsjoins, $questions, $reporturl) { - parent::__construct('mod-capquiz-report-questions-report', $capquiz, $context, - $options, $studentsjoins, $questions, $reporturl); + public function __construct(capquiz $capquiz, context $context, options $options, sql_join $studentsjoins, + array $questions, moodle_url $reporturl) { + parent::__construct('questions', $capquiz, $context, $options, $studentsjoins, $questions, $reporturl); } /** * Take the data returned from the db_query and go through all the rows * processing each col using either col_{columnname} method or other_cols - * method or if other_cols returns NULL then put the data straight into the - * table. + * method or if other_cols returns NULL then put the data straight into the table. * * This overwrites the parent method because full SQL query may fail on Mysql * because of the limit in the number of tables in the join. Therefore we only * join 59 tables in the main query and add the rest here. */ public function build_table(): void { - if (!$this->rawdata) { - return; + if ($this->rawdata) { + $this->strtimeformat = str_replace(',', ' ', get_string('strftimedatetimeseconds', 'capquiz')); + parent::build_table(); } - $this->strtimeformat = str_replace(',', ' ', get_string('strftimedatetimeseconds', 'capquiz')); - parent::build_table(); } /** @@ -91,7 +77,7 @@ public function build_table(): void { */ public function other_cols($colname, $attempt): ?string { return match ($colname) { - 'question' => $this->data_col($attempt->slot, 'questionsummary', $attempt), + 'question' => $this->data_col((int)$attempt->slot, 'questionsummary', $attempt), default => null, }; } @@ -108,7 +94,7 @@ public function data_col(int $slot, string $field, stdClass $attempt): string { return '-'; } $value = $this->field_from_extra_data($attempt, $slot, $field); - $summary = $value !== null ? trim($value) : '-'; + $summary = empty($value) ? '-' : trim($value); if ($this->is_downloading() && $this->is_downloading() != 'html') { return $summary; } @@ -129,7 +115,7 @@ public function data_col(int $slot, string $field, stdClass $attempt): string { * @param int $slot * @param string $field */ - protected function field_from_extra_data(stdClass $attempt, int $slot, string $field): string { + protected function field_from_extra_data(stdClass $attempt, int $slot, string $field): ?string { if (!isset($this->lateststeps[$attempt->usageid][$slot])) { return '-'; } @@ -151,11 +137,11 @@ public function col_answerstate(stdClass $attempt): string { if ($attempt->attempt === null || $attempt->usageid === 0) { return '-'; } - $state = $this->slot_state($attempt, $attempt->slot); + $state = $this->slot_state($attempt, (int)$attempt->slot); if ($this->is_downloading()) { - return $state->__toString(); + return (string)$state; } else { - return $this->make_review_link($state, $attempt, $attempt->slot); + return $this->make_review_link((string)$state, $attempt, (int)$attempt->slot); } } @@ -267,44 +253,68 @@ public function col_questionprevratingmanual(stdClass $attempt): string { public function base_sql(sql_join $allowedstudentsjoins): array { global $DB; list($fields, $from, $where, $params) = parent::base_sql($allowedstudentsjoins); - $fields1 = ', - 1 AS identifier, - ca.time_answered AS timecreated, - cqr.rating AS questionrating, - pcqr.rating AS questionprevrating, - pcqr.manual AS manualprevqrating, - cq2.id AS questionid, - cq2.question_id AS moodlequestionid'; - - $from1 = "\nJOIN {capquiz_question_rating} cqr ON cqr.id = ca.question_rating_id"; - $from1 .= "\nJOIN {capquiz_question_rating} pcqr ON pcqr.id = ca.question_prev_rating_id"; - $from1 .= "\nJOIN {capquiz_question} cq2 ON cq2.id = cqr.capquiz_question_id"; - - $sql1 = "SELECT {$fields}{$fields1} - \nFROM ({$from}{$from1}) - \nWHERE {$where}"; - - $fields2 = ', - 2 AS identifier, - ca.time_answered AS timecreated, - cqr.rating AS questionrating, - pcqr.rating AS questionprevrating, - pcqr.manual AS manualprevqrating, - cq2.id AS questionid, - cq2.question_id AS moodlequestionid'; - $from2 = "\nJOIN {capquiz_question_rating} cqr ON cqr.id = ca.prev_question_rating_id"; - $from2 .= "\nJOIN {capquiz_question_rating} pcqr ON pcqr.id = ca.prev_question_prev_rating_id"; - $from2 .= "\nJOIN {capquiz_question} cq2 ON cq2.id = cqr.capquiz_question_id"; + $sql1 = "SELECT $fields, + 1 AS identifier, + ca.timeanswered AS timecreated, + cqr.rating AS questionrating, + pcqr.rating AS questionprevrating, + pcqr.manual AS manualprevqrating, + cs.id AS slotid, + qv.questionid AS questionid + FROM $from + JOIN {capquiz_question_rating} cqr + ON cqr.id = ca.questionratingid + JOIN {capquiz_question_rating} pcqr + ON pcqr.id = ca.questionprevratingid + JOIN {capquiz_slot} cs + ON cs.id = cqr.slotid + JOIN {question_references} qr + ON qr.itemid = cs.id + AND qr.component = 'mod_capquiz' + AND qr.questionarea = 'slot' + JOIN {question_versions} qv + ON qv.questionbankentryid = qr.questionbankentryid + AND qv.version = COALESCE( + qr.version, + (SELECT MAX(qv2.version) + FROM {question_versions} qv2 + WHERE qv2.questionbankentryid = qr.questionbankentryid) + ) + WHERE $where"; - $sql2 = "SELECT {$fields}{$fields2} - \nFROM {$from}{$from2} - \nWHERE {$where}"; + $sql2 = "SELECT $fields, + 2 AS identifier, + ca.timeanswered AS timecreated, + cqr.rating AS questionrating, + pcqr.rating AS questionprevrating, + pcqr.manual AS manualprevqrating, + cs2.id AS slotid, + qv.questionid AS questionid + FROM $from + JOIN {capquiz_question_rating} cqr + ON cqr.id = ca.prevquestionratingid + JOIN {capquiz_question_rating} pcqr + ON pcqr.id = ca.prevquestionprevratingid + JOIN {capquiz_slot} cs + ON cs.id = cqr.slotid + JOIN {question_references} qr + ON qr.itemid = cs.id + AND qr.component = 'mod_capquiz' + AND qr.questionarea = 'slot' + JOIN {question_versions} qv + ON qv.questionbankentryid = qr.questionbankentryid + AND qv.version = COALESCE( + qr.version, + (SELECT MAX(qv2.version) + FROM {question_versions} qv2 + WHERE qv2.questionbankentryid = qr.questionbankentryid) + ) + WHERE $where"; $fields = 'DISTINCT ' . $DB->sql_concat('userid', "'#'", 'COALESCE(attempt, 0)', "'#'", 'identifier') - . ' AS uniqueidquestion,'; - $fields .= "ratings.*"; - $from = "(\n{$sql1} \nUNION ALL\n {$sql2}) AS ratings"; + . ' AS uniqueidquestion, ratings.*'; + $from = "($sql1 UNION ALL $sql2) AS ratings"; list($from, $params) = uniquify_sql_params($from, $params); return [$fields, $from, '1=1', $params]; diff --git a/report/questions/lang/en/capquizreport_questions.php b/report/questions/lang/en/capquizreport_questions.php index fc08a56..38c443f 100644 --- a/report/questions/lang/en/capquizreport_questions.php +++ b/report/questions/lang/en/capquizreport_questions.php @@ -27,29 +27,18 @@ $string['answerstate'] = 'Answer state'; $string['attemptid'] = 'Attempt id'; -$string['correct'] = 'Correct'; - -$string['false'] = 'False'; - $string['pluginname'] = 'Questions'; -$string['privacy:metadata'] = 'The capquizreport questions plugin does not store any personal data.'; +$string['privacy:metadata'] = 'The CAPQuiz questions report plugin does not store any personal data.'; $string['qprevrating'] = 'question\'s previous rating'; $string['qrating'] = 'question rating'; -$string['question'] = 'Question'; $string['questionprevrating'] = 'Question\'s previous rating'; $string['questionprevratingmanual'] = 'Previous rating manually updated'; $string['questionsfilename'] = 'questions'; $string['rating_manually_updated'] = 'Previous rating was manually updated'; $string['response'] = 'Response'; -$string['rightanswer'] = 'Right answer'; - -$string['timecreated'] = 'Time created'; -$string['true'] = 'True'; $string['uprevrating'] = 'user\'s previous rating'; $string['urating'] = 'user rating'; $string['userprevrating'] = 'User\'s previous rating'; - -$string['wrong'] = 'Wrong'; diff --git a/report/questions/report.php b/report/questions/report.php deleted file mode 100644 index 6305e22..0000000 --- a/report/questions/report.php +++ /dev/null @@ -1,223 +0,0 @@ -. - -/** - * CAPQuiz questions report class. - * - * @package capquizreport_questions - * @author André Storhaug - * @copyright 2019 Norwegian University of Science and Technology (NTNU) - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - -namespace capquizreport_questions; - -use context_course; -use mod_capquiz\capquiz; -use mod_capquiz\report\capquiz_attempts_report; -use mod_capquiz\report\capquiz_attempts_report_options; -use stdClass; - -defined('MOODLE_INTERNAL') || die(); - -require_once($CFG->dirroot . '/mod/capquiz/report/attemptsreport.php'); -require_once(__DIR__ . '/questions_form.php'); -require_once(__DIR__ . '/questions_table.php'); -require_once(__DIR__ . '/questions_options.php'); - -/** - * The capquiz questions report provides summary information about the questions in a capquiz (mainly ratings). - * - * @author André Storhaug - * @copyright 2019 Norwegian University of Science and Technology (NTNU) - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -class capquizreport_questions_report extends capquiz_attempts_report { - - /** - * Displays the full report. - * - * @param capquiz $capquiz - * @param stdClass $cm - * @param stdClass $course - * @param string $download type of download being requested - */ - public function display($capquiz, $cm, $course, $download): bool { - global $DB; - - list($studentsjoins) = $this->init( - 'questions', 'capquizreport_questions\capquizreport_questions_settings_form', $capquiz, $cm, $course); - - $this->options = new capquizreport_questions_options('questions', $capquiz, $cm, $course); - - if ($fromform = $this->form->get_data()) { - $this->options->process_settings_from_form($fromform); - - } else { - $this->options->process_settings_from_params(); - } - - $this->form->set_data($this->options->get_initial_form_data()); - - // Load the required questions. - $questions = capquiz_report_get_questions($capquiz); - - // Prepare for downloading, if applicable. - $courseshortname = format_string($course->shortname, true, ['context' => context_course::instance($course->id)]); - - $table = new capquizreport_questions_table($capquiz, $this->context, - $this->options, $studentsjoins, $questions, $this->options->get_url()); - - $filename = capquiz_report_download_filename(get_string('questionsfilename', 'capquizreport_questions'), - $courseshortname, $capquiz->name()); - - $table->is_downloading($this->options->download, $filename, $courseshortname . ' ' . format_string($capquiz->name())); - if ($table->is_downloading()) { - raise_memory_limit(MEMORY_EXTRA); - } - - $hasstudents = false; - if (!empty($studentsjoins->joins)) { - $sql = "SELECT DISTINCT u.id - FROM {user} u - $studentsjoins->joins - WHERE $studentsjoins->wheres"; - $hasstudents = $DB->record_exists_sql($sql, $studentsjoins->params); - } - - $hasquestions = capquiz_has_questions($capquiz->id()); - // Start output. - if (!$table->is_downloading()) { - // Only print headers if not asked to download data. - $this->print_standard_header_and_messages($cm, $course, $capquiz, $this->options, $hasquestions, $hasstudents); - - // Print the display options. - $this->form->display(); - } - - if ($hasquestions && !empty($questions) && ($hasstudents || $this->options->attempts == self::ALL_WITH)) { - - $table->setup_sql_queries($studentsjoins); - - // Define table columns. - $columns = []; - $headers = []; - - if ($table->is_downloading()) { - $columns[] = 'attemptid'; - $headers[] = get_string('attemptid', 'capquizreport_questions'); - } - - $this->add_questionid_column($columns, $headers); - $this->add_question_rating_columns($columns, $headers); - - if ($table->is_downloading()) { - $columns[] = 'questionprevratingmanual'; - $headers[] = get_string('questionprevratingmanual', 'capquizreport_questions'); - } - - $this->add_moodlequestionid_column($columns, $headers); - - if ($this->options->showqtext) { - $columns[] = 'question'; - $headers[] = get_string('question', 'capquizreport_questions'); - } - - $columns[] = 'timecreated'; - $headers[] = get_string('timecreated', 'capquizreport_questions'); - - $table->define_columns($columns); - $table->define_headers($headers); - $table->sortable(true, 'uniqueidquestion'); - - // Set up the table. - $table->define_baseurl($this->options->get_url()); - - $this->configure_user_columns($table); - - $table->no_sorting('answerstate'); - $table->no_sorting('question'); - - $table->set_attribute('id', 'responses'); - - $table->collapsible(true); - - $table->out($this->options->pagesize, true); - } - return true; - } - - /** - * Outputs the things you commonly want at the top of a capquiz report. - * - * Calls through to {@see print_header_and_tabs()} and then - * outputs the standard group selector, number of attempts summary, - * and messages to cover common cases when the report can't be shown. - * - * @param stdClass $cm the course_module information. - * @param stdClass $course the course settings. - * @param capquiz $capquiz the capquiz settings. - * @param capquiz_attempts_report_options $options the current report settings. - * @param bool $hasquestions whether there are any questions in the capquiz. - * @param bool $hasstudents whether there are any relevant students. - */ - protected function print_standard_header_and_messages($cm, $course, capquiz $capquiz, - capquiz_attempts_report_options $options, - bool $hasquestions, bool $hasstudents): void { - global $OUTPUT; - $this->print_header_and_tabs($cm, $course, $capquiz, $this->mode); - if (!$hasquestions) { - echo capquiz_no_questions_message($capquiz, $cm, $this->context); - } else if (!$capquiz->is_published()) { - echo capquiz_not_published_message($capquiz, $cm, $this->context); - } else if (!$hasstudents) { - echo $OUTPUT->notification(get_string('nostudentsyet')); - } - } - - /** - * Adds column with question rating - * - * @param array $columns columns to be added - * @param array $headers column headers - */ - protected function add_question_rating_columns(array &$columns, array &$headers): void { - $this->add_question_rating_column($columns, $headers); - $this->add_question_previous_rating_column($columns, $headers); - } - - /** - * Adds column with question rating - * - * @param array $columns columns to be added - * @param array $headers column headers - */ - protected function add_question_rating_column(array &$columns, array &$headers): void { - $columns[] = 'questionrating'; - $headers[] = get_string('questionrating', 'capquiz'); - } - - /** - * Adds column with previous question ratings - * - * @param array $columns columns to be added - * @param array $headers column headers - */ - protected function add_question_previous_rating_column(array &$columns, array &$headers): void { - $columns[] = 'questionprevrating'; - $headers[] = get_string('questionprevrating', 'capquizreport_questions'); - } -} diff --git a/report/questions/version.php b/report/questions/version.php index b0a7313..da5f3a7 100644 --- a/report/questions/version.php +++ b/report/questions/version.php @@ -25,8 +25,8 @@ defined('MOODLE_INTERNAL') || die(); -$plugin->version = 2024100700; +$plugin->version = 2024102101; $plugin->requires = 2022041901; $plugin->component = 'capquizreport_questions'; $plugin->maturity = MATURITY_STABLE; -$plugin->release = '1.1.0'; +$plugin->release = '1.2.0'; diff --git a/report/report.php b/report/report.php deleted file mode 100644 index 61ac4fd..0000000 --- a/report/report.php +++ /dev/null @@ -1,85 +0,0 @@ -. - -/** - * Base class for capquiz report plugins. - * - * @package mod_capquiz - * @author André Storhaug - * @copyright 2019 Norwegian University of Science and Technology (NTNU) - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - -namespace mod_capquiz\report; - -use context_module; -use mod_capquiz\capquiz; -use stdClass; - -/** - * Base class for capquiz report plugins. - * - * Doesn't do anything on it's own -- it needs to be extended. - * This class displays capquiz reports. - * - * This file can refer to itself as report.php to pass variables - * to itself - all these will also be globally available. - * - * @author André Storhaug - * @copyright 2019 Norwegian University of Science and Technology (NTNU) - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -class report { - /** - * Displays the full report. - * - * @param capquiz $capquiz - * @param stdClass $cm - * @param stdClass $course - * @param string $download type of download being requested - */ - public function display(capquiz $capquiz, stdClass $cm, stdClass $course, string $download): bool { - // This function renders the html for the report. - return true; - } - - /** - * Allows the plugin to control who can see this plugin. - * - * @param context_module $contextmodule - */ - public function canview(context_module $contextmodule): bool { - return true; - } - - /** - * Initialise some parts of $PAGE and start output. - * - * @param stdClass $cm the course_module information. - * @param stdClass $course the course settings. - * @param capquiz $capquiz the capquiz settings. - * @param string $reportmode the report name. - */ - public function print_header_and_tabs(stdClass $cm, stdClass $course, capquiz $capquiz, string $reportmode = 'attempts'): void { - global $PAGE, $OUTPUT; - $PAGE->set_title($capquiz->name()); - $PAGE->set_heading($course->fullname); - $context = context_module::instance($cm->id); - echo $OUTPUT->heading(format_string( - get_string('pluginname', 'capquizreport_' . $reportmode) . ' ' . get_string('report', 'capquiz'), - true, ['context' => $context])); - } -} diff --git a/report/reportfactory.php b/report/reportfactory.php deleted file mode 100644 index 8a49ab5..0000000 --- a/report/reportfactory.php +++ /dev/null @@ -1,75 +0,0 @@ -. - -/** - * Capquiz report factory. Provides a convenient way to create an capquiz report of any type. - * - * @package mod_capquiz - * @author André Storhaug - * @copyright 2019 Norwegian University of Science and Technology (NTNU) - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - -namespace mod_capquiz\report; - -use capquiz_exception; - -defined('MOODLE_INTERNAL') || die(); - -require_once($CFG->dirroot . '/mod/capquiz/locallib.php'); - -/** - * Capquiz report factory. Provides a convenient way to create an capquiz report of any type. - * - * @package mod_capquiz - * @author André Storhaug - * @copyright 2019 Norwegian University of Science and Technology (NTNU) - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -class capquiz_report_factory { - - /** - * Create a capquiz report of a given type and return it. - * - * @param string $type the required type. - */ - public static function make(string $type): capquiz_attempts_report { - $class = self::class_for_type($type); - return new $class(); - } - - /** - * The class name corresponding to a report type. - * - * @param string $type report type name. - */ - protected static function class_for_type(string $type): string { - global $CFG; - $typelc = strtolower($type); - $file = $CFG->dirroot . '/mod/capquiz/report/' . $type . '/report.php'; - $class = "capquizreport_{$typelc}\\capquizreport_{$typelc}_report"; - if (!is_readable($file)) { - throw new capquiz_exception('capquiz_report_factory: unknown report type ' . $type); - } - include_once($file); - - if (!class_exists($class)) { - throw new capquiz_exception('capquiz_report_factory: report type ' . $type . - ' does not define the expected class ' . $class); - } - return $class; - } -} diff --git a/report/reportlib.php b/report/reportlib.php index f7480e6..4db719e 100644 --- a/report/reportlib.php +++ b/report/reportlib.php @@ -25,8 +25,6 @@ use core\context\module; use mod_capquiz\capquiz; -use mod_capquiz\capquiz_urls; -use mod_capquiz\report\capquiz_report_factory; defined('MOODLE_INTERNAL') || die(); @@ -34,56 +32,6 @@ require_once($CFG->dirroot . '/mod/capquiz/lib.php'); require_once($CFG->libdir . '/filelib.php'); -/** - * Generates and returns list of available CAPQuiz report sub-plugins - * - * @param context_module $context the context level to check caps against - * @return array list of valid reports present - */ -function capquiz_report_list(context_module $context): array { - static $reportlist; - if (!empty($reportlist)) { - return $reportlist; - } - $pluginmanager = new capquiz_plugin_manager('capquizreport'); - $enabledplugins = core_plugin_manager::instance()->get_enabled_plugins('capquizreport'); - foreach ($pluginmanager->get_sorted_plugins_list() as $reportname) { - $report = capquiz_report_factory::make($reportname); - if (isset($enabledplugins[$reportname]) && $report->canview($context)) { - $reportlist[] = $reportname; - } - } - return $reportlist; -} - -/** - * Create a filename for use when downloading data from a capquiz report. It is - * expected that this will be passed to flexible_table::is_downloading, which - * cleans the filename of bad characters and adds the file extension. - * - * @param string $report the type of report. - * @param string $courseshortname the course shortname. - * @param string $capquizname the capquiz name. - * @return string the filename. - */ -function capquiz_report_download_filename(string $report, string $courseshortname, string $capquizname): string { - return $courseshortname . '-' . format_string($capquizname) . '-' . $report; -} - -/** - * Are there any questions in this capquiz? - * - * @param int $capquizid - */ -function capquiz_has_questions(int $capquizid): bool { - global $DB; - $sql = 'SELECT cq.id - FROM {capquiz_question} cq - JOIN {capquiz_question_list} cql ON cql.capquiz_id = :capquizid AND cql.is_template = 0 - WHERE cq.question_list_id = cql.id'; - return $DB->record_exists_sql($sql, ['capquizid' => $capquizid]); -} - /** * Get the questions in this capquiz, in order. * @@ -93,99 +41,85 @@ function capquiz_has_questions(int $capquizid): bool { */ function capquiz_report_get_questions(capquiz $capquiz): array { global $DB; - $sql = 'SELECT DISTINCT ' . $DB->sql_concat('qa.id', "'#'", 'cu.id', 'ca.slot') . ' AS uniqueid, - ca.slot, - q.id, - q.qtype, - q.length - FROM {question} q - JOIN {capquiz_question} cq ON cq.question_id = q.id - JOIN {capquiz_question_list} cql ON cql.id = cq.question_list_id AND cql.is_template = 0 - JOIN {capquiz_user} cu ON cu.capquiz_id = cql.capquiz_id - JOIN {question_usages} qu ON qu.id = cu.question_usage_id - JOIN {question_attempts} qa ON qa.questionusageid = qu.id - JOIN {capquiz_attempt} ca ON ca.question_id = cq.id AND ca.slot = qa.slot AND ca.user_id = cu.id - - WHERE cu.capquiz_id = ? - AND q.length > 0 - - ORDER BY ca.slot'; - $qsbyslot = $DB->get_records_sql($sql, [$capquiz->id()]); + $sql = 'SELECT DISTINCT ' . $DB->sql_concat('qa.id', "'#'", 'cu.id', 'ca.slot') . " AS uniqueid, + ca.slot, + q.id, + q.qtype, + q.length + FROM {capquiz_slot} cs + JOIN {question_references} qr + ON qr.itemid = cs.id + AND qr.component = 'mod_capquiz' + AND qr.questionarea = 'slot' + JOIN {question_versions} qv + ON qv.questionbankentryid = qr.questionbankentryid + AND qv.version = COALESCE( + qr.version, + (SELECT MAX(qv2.version) + FROM {question_versions} qv2 + WHERE qv2.questionbankentryid = qr.questionbankentryid) + ) + JOIN {question} q + ON q.id = qv.questionid + AND q.length > 0 + JOIN {capquiz_user} cu + ON cu.capquizid = cs.capquizid + JOIN {question_usages} qu + ON qu.id = cu.questionusageid + JOIN {question_attempts} qa + ON qa.questionusageid = qu.id + JOIN {capquiz_attempt} ca + ON ca.slotid = cs.id + AND ca.slot = qa.slot + AND ca.capquizuserid = cu.id + WHERE cs.capquizid = :capquizid + ORDER BY ca.slot"; + $questionsbyslot = $DB->get_records_sql($sql, ['capquizid' => $capquiz->get('id')]); $number = 1; - foreach ($qsbyslot as $question) { + foreach ($questionsbyslot as $question) { $question->number = $number; $number += $question->length; $question->type = $question->qtype; } - return $qsbyslot; + return $questionsbyslot; } /** - * Return a textual summary of the number of attempts that have been made at a particular quiz, - * returns '' if no attempts have been made yet, unless $returnzero is passed as true. + * Returns the number of question attempts in a CAPQuiz. * - * @param capquiz $capquiz - * @param bool $returnzero if false (default), when no attempts have been - * made '' is returned instead of 'Attempts: 0'. - * @return string a string like "Attempts: 123". - */ -function capquiz_num_attempt_summary(capquiz $capquiz, bool $returnzero = false): string { - $numattempts = capquiz_report_num_attempt($capquiz); - if ($numattempts || $returnzero) { - return get_string('attemptsnum', 'quiz', $numattempts); - } - return ''; -} - -/** - * Returns the number of CAPQuiz attempts. - * - * @param capquiz $capquiz - * @return int number of answered CAPQuiz attempts + * @param int $capquizid */ -function capquiz_report_num_attempt(capquiz $capquiz): int { +function capquiz_report_num_attempt(int $capquizid): int { global $DB; $sql = 'SELECT COUNT(ca.id) FROM {capquiz_attempt} ca - JOIN {capquiz_user} cu ON cu.capquiz_id = :capquizid AND cu.id = ca.user_id - JOIN {question_usages} qu ON qu.id = cu.question_usage_id - JOIN {question_attempts} qa ON qa.questionusageid = qu.id AND qa.slot = ca.slot - JOIN {capquiz_question} cq ON cq.id = ca.question_id'; - return $DB->count_records_sql($sql, ['capquizid' => $capquiz->id()]); + JOIN {capquiz_user} cu + ON cu.capquizid = :capquizid + AND cu.id = ca.capquizuserid + JOIN {question_usages} qu + ON qu.id = cu.questionusageid + JOIN {question_attempts} qa + ON qa.questionusageid = qu.id + AND qa.slot = ca.slot + JOIN {capquiz_slot} cs + ON cs.id = ca.slotid'; + return $DB->count_records_sql($sql, ['capquizid' => $capquizid]); } /** * Generate a message saying that this capquiz has no questions, with a button to * go to the edit page, if the user has the right capability. * - * @param object $quiz the quiz settings. - * @param object $cm the course_module object. + * @param cm_info $cm the course_module object. * @param module $context the quiz context. * @return string HTML to output. */ -function capquiz_no_questions_message($quiz, $cm, module $context): string { +function capquiz_no_questions_message(cm_info $cm, module $context): string { global $OUTPUT; $output = $OUTPUT->notification(get_string('noquestions', 'quiz')); if (has_capability('mod/capquiz:manage', $context)) { - $output .= $OUTPUT->single_button(capquiz_urls::view_question_list_url(), get_string('editquiz', 'quiz'), 'get'); - } - return $output; -} - -/** - * Generate a message saying that this capquiz has no questions, with a button to - * go to the dashboard page (question list settings), if the user has the right capability. - * - * @param object $quiz the quiz settings. - * @param object $cm the course_module object. - * @param module $context the quiz context. - * @return string HTML to output. - */ -function capquiz_not_published_message($quiz, $cm, module $context): string { - global $OUTPUT; - $output = $OUTPUT->notification(get_string('question_list_not_published', 'capquiz')); - if (has_capability('mod/capquiz:manage', $context)) { - $output .= $OUTPUT->single_button(capquiz_urls::view_url(), get_string('question_list_settings', 'capquiz'), 'get'); + $url = new moodle_url('/mod/capquiz/edit.php', ['id' => $cm->id]); + $output .= $OUTPUT->single_button($url, get_string('editquiz', 'quiz'), 'get'); } return $output; } diff --git a/settings.php b/settings.php index 6a91d24..0df316a 100644 --- a/settings.php +++ b/settings.php @@ -26,14 +26,12 @@ require_once($CFG->dirroot . '/mod/capquiz/adminlib.php'); -$modcapquizfolder = new admin_category('modcapquizfolder', - new lang_string('pluginname', 'capquiz'), $module->is_enabled() === false); +global $ADMIN; +$modcapquizfolder = new admin_category('modcapquizfolder', new lang_string('pluginname', 'capquiz'), !$module->is_enabled()); $ADMIN->add('modsettings', $modcapquizfolder); -$settings = new admin_settingpage($section, - get_string('settings', 'capquiz'), 'moodle/site:config', !$module->is_enabled()); - +$settings = new admin_settingpage($section, get_string('settings', 'capquiz'), 'moodle/site:config', !$module->is_enabled()); $ADMIN->add('modcapquizfolder', $settings); // Tell core we already added the settings structure. diff --git a/styles.css b/styles.css index f78332d..ff04a3a 100755 --- a/styles.css +++ b/styles.css @@ -1,85 +1,16 @@ -.capquiz-list { - border: 1px solid #eaeaea; - padding: 0; -} - -.capquiz-list li { - padding: 4px; - background: #fafafa; - border-bottom: 1px solid #eaeaea; -} - -.capquiz-question-list li { - display: flex; -} - -.capquiz-flex { - display: flex; -} - -.capquiz-flex-item { - width: 50%; -} - -.capquiz-tree tr td, -.capquiz-tree tr th { - padding: 6px; -} - -.capquiz-tree tr:first-child { - background: #fafafa; -} - -.capquiz-tree tr:first-child td { - font-weight: bold; -} - -.capquiz-attempt-metainfo { - width: 100%; - background: #f7f7f7; - padding: 8px; - margin: 8px 0; - border: 1px solid #e0e0e0; - overflow: hidden; -} - -.capquiz-attempt-metainfo tr:first-child { - background: #fafafa; -} - -.capquiz-qlist-questions tr td { - padding: 2px 4px; -} - -.capquiz-quiz-top { - background: #f7f7f7; - padding: 8px; - margin-bottom: 8px; - border: 1px solid #e0e0e0; +.capquiz-attempt-header { min-height: 44px; overflow: hidden; text-align: center; } -.capquiz-quiz-stars .icon { - width: 24px; - height: 24px; -} - -.capquiz-quiz-progress { - position: relative; - display: inline-block; - background: #e0e0e0; +.capquiz-star-progress { min-width: unset; - width: 100%; - margin-top: 8px; - border: 1px solid #bbb; height: 32px; } @media (min-width: 576px) { - .capquiz-quiz-progress { - background: #e0e0e0; + .capquiz-star-progress { min-width: 256px; width: 30%; margin-top: 0; @@ -89,11 +20,7 @@ } } -.capquiz-reduced-width { - width: 80%; -} - -.capquiz-quiz-progress-fill { +.capquiz-star-progress-fill { background: #59f; text-align: left; height: 30px; @@ -101,7 +28,7 @@ max-width: 100%; } -.capquiz-quiz-progress-unfill { +.capquiz-star-progress-unfill { position: absolute; top: -1px; right: 0; @@ -113,7 +40,7 @@ max-width: 100%; } -.capquiz-quiz-progress-fill .capquiz-quiz-progress-percent { +.capquiz-star-progress-fill .capquiz-star-progress-percent { min-width: 254px; padding-left: 8px; position: absolute; @@ -121,7 +48,7 @@ margin-top: 4px; } -.capquiz-quiz-progress-unfill .capquiz-quiz-progress-percent { +.capquiz-star-progress-unfill .capquiz-star-progress-percent { min-width: 254px; padding-right: 8px; position: absolute; @@ -141,85 +68,134 @@ border: 1px solid #bbb; } -.capquiz-default-question-rating { - display: inline-block; +.capquiz-stars { + position: relative; } -.capquiz-sortable { - cursor: pointer; - user-select: none; +.capquiz-stars .icon { + width: 24px; + height: 24px; } -.capquiz-details { - background: #f7f7f7; - border: 1px solid #e0e0e0; - transition: 0.2s ease all; +.capquiz-stars > span { + cursor: pointer; } -.capquiz-details:hover { - background: #f0f0f0; - border: 1px solid #ddd; +.capquiz-question-rating { + min-width: 24rem; + gap: 0.5rem; } -.capquiz-details summary { - outline: none; - user-select: none; - padding: 8px; +.capquiz-question-rating input { + margin: 0.5rem 0; } -.capquiz-details-content { - background: #fdfdfd; - padding: 8px; +#page-mod-capquiz-edit .question-bank-table .questionname { + position: relative; + zoom: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-weight: bold; + color: #555; } -.capquiz-quiz-stars { +#page-mod-capquiz-edit .question-bank-table .questiontext { position: relative; + zoom: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + padding: 0.3em; } -.capquiz-star, -.capquiz-lost-star, -.capquiz-no-star, -.capquiz-help-stars { - cursor: pointer; +#page-mod-capquiz-edit .question-bank-table .questiontext p { + margin: 0; } -.capquiz-star-tooltip { - position: fixed; - padding: 8px; - border: 1px solid #555; - background: #333; - color: #dbdbdb; - display: none; - z-index: 1000; - border-radius: 6px; - max-width: 600px; +#page-mod-capquiz-edit .question-bank-table .questionnametext { + padding: 0 0.2em; } -.capquiz-question-rating { - width: 112px; +#page-mod-capquiz-edit table.question-bank-table td, +#page-mod-capquiz-edit table.question-bank-table th { + overflow: hidden; + white-space: nowrap; } -.capquiz-question-rating input { - width: 80px; +#page-mod-capquiz-edit table.question-bank-table td, +#page-mod-capquiz-edit table.question-bank-table th { + overflow: hidden; + white-space: nowrap; +} + +#page-mod-capquiz-edit .question-bank-table { + width: auto; } -.capquiz-classlist tr td, -.capquiz-classlist tr th { - padding: 4px 16px; +#page-mod-capquiz-edit .question-bank-table .header { + text-align: center; + padding: 0 2px; + border: 0 none; + vertical-align: top; + width: auto; +} + +#page-mod-capquiz-edit .question-bank-table .header.checkbox { + vertical-align: bottom; + text-align: center; } -.capquiz-classlist tr:nth-child(odd) td { - background: #fafafa; +#page-mod-capquiz-edit .question-bank-table .header.qtype .sorters { + white-space: nowrap; } -.capquiz-add-selected-questions { - margin: 4px; +#page-mod-capquiz-edit .question-bank-table th .sorters { + font-weight: normal; + font-size: 0.8em; } -.capquiz-regrade-all { - margin-top: 16px; +#page-mod-capquiz-edit .question-bank-table td.modifiername, +#page-mod-capquiz-edit .question-bank-table td.creatorname, +#page-mod-capquiz-edit .question-bank-table td.questionlastused { + line-height: 1em; +} + +#page-mod-capquiz-edit .question-bank-table td.modifiername span.date, +#page-mod-capquiz-edit .question-bank-table td.creatorname span.date { + font-weight: normal; + font-size: 0.8em; } #page-mod-capquiz-edit table.question-bank-table { - width: 100%; + table-layout: fixed; + overflow-x: visible; + border-collapse: separate; + border-spacing: 0; +} + +#page-mod-capquiz-edit .question-bank-table .iconcol { + width: 16px; + text-align: center; + padding: 0; +} + +#page-mod-capquiz-edit .question-bank-table .iconcol .icon { + width: 16px; +} + +#page-mod-capquiz-edit .question-bank-table .checkbox { + width: 32px; + text-align: center; + padding: 0; +} + +#page-mod-capquiz-edit .question-bank-table .editmenu { + width: 5em; +} + +#page-mod-capquiz-edit .question-bank-table .qtype { + text-align: center; + width: 28px; + padding: 0; } diff --git a/templates/student_question_attempt.mustache b/templates/attempt.mustache similarity index 64% rename from templates/student_question_attempt.mustache rename to templates/attempt.mustache index 740862b..7bc4db3 100755 --- a/templates/student_question_attempt.mustache +++ b/templates/attempt.mustache @@ -15,7 +15,7 @@ along with Moodle. If not, see . }} {{! - @template mod_capquiz/student_question_attempt + @template mod_capquiz/attempt Example context (json): { @@ -29,23 +29,32 @@ "gradingpass": false } }} +{{#header}} + {{{header}}} +{{/header}} + {{#gradingdone}} -

{{#str}} grading_is_completed, capquiz {{/str}}

+

{{#str}}grading_is_completed, capquiz{{/str}}

{{#gradingpass}} -

{{#str}} grade_has_been_set_pass, capquiz, {{finalgrade}} {{/str}}

+

{{#str}}grade_has_been_set_pass, capquiz, {{finalgrade}}{{/str}}

{{/gradingpass}} {{^gradingpass}} -

{{#str}} due_date_grading, capquiz, {{duedate}} {{/str}}

-

{{#str}} grade_has_been_set_fail, capquiz, {{finalgrade}} {{/str}}

+

{{#str}}due_date_grading, capquiz, {{duedate}}{{/str}}

+

{{#str}}grade_has_been_set_fail, capquiz, {{finalgrade}}{{/str}}

{{/gradingpass}} {{/gradingdone}} {{#attempt}} -
+
- + - {{{ body }}} + {{{body}}}
{{/attempt}} + +{{#reviewbutton}} + {{>core/single_button}} +{{/reviewbutton}} diff --git a/templates/attempt_header.mustache b/templates/attempt_header.mustache new file mode 100755 index 0000000..c634837 --- /dev/null +++ b/templates/attempt_header.mustache @@ -0,0 +1,63 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template mod_capquiz/attempt_header + + Example context (json): + { + "percentup": 40, + "percentdown": 0, + "stars": [ + { + "icon": "star", + "tooltip": "You achieved this star" + }, + { + "icon": "star", + "tooltip": "You achieved this star" + }, + { + "icon": "no-star", + "tooltip": "You have not achieved this star" + } + ] + } +}} +
+
+ {{#percentup}} +
+
{{percentup}}%
+
+ {{/percentup}} + {{#percentdown}} +
+
{{percentdown}}%
+
+ {{/percentdown}} +
+
+ {{#stars}} + + {{#pix}}{{icon}}, capquiz{{/pix}} + + {{/stars}} + + {{#pix}}help{{/pix}} + +
+
diff --git a/templates/button.mustache b/templates/button.mustache deleted file mode 100755 index f685762..0000000 --- a/templates/button.mustache +++ /dev/null @@ -1,31 +0,0 @@ -{{! - This file is part of Moodle - http://moodle.org/ - - Moodle is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - Moodle is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with Moodle. If not, see . -}} -{{! - @template mod_capquiz/button - - Example context (json): - { - "method": "post", - "url": "", - "primary": true, - "tooltip": "A button.", - "label": "Button" - } -}} -{{#button}} - {{>core/single_button}} -{{/button}} diff --git a/templates/classlist.mustache b/templates/classlist.mustache index 5b5a5d8..5e00826 100755 --- a/templates/classlist.mustache +++ b/templates/classlist.mustache @@ -21,62 +21,59 @@ { "users": [ { - "index": 1, - "username": "sebastsg", - "firstname": "Sebastian", - "lastname": "Gundersen", + "username": "user1", + "firstname": "First", + "lastname": "Last", "rating": 1653.6, "stars": 4, - "graded_stars": 3, - "passing_grade": true + "gradedstars": 3, + "passinggrade": true } ], "regrade": { - "id": "id", "method": "post", - "classes": "capquiz-regrade-all", - "url": "#", - "primary": true, + "id": "capquiz_regrade_button", + "type": "primary", + "url": "/mod/capquiz/edit.php?id=1&action=regradeall", "label": "Regrade all", - "diabled": false + "disabled": false } } }} -

{{#str}} classlist, capquiz {{/str}}

- +

{{#str}}classlist, capquiz{{/str}}

+

{{#str}}enrolled_students, capquiz{{/str}}: {{usercount}}

+
+ - - - - - - - - + + + + + + + + + {{#users}} - {{#.}} - - - - - - - - - {{/.}} + + + + + + + {{/users}} +
#{{#str}} username, capquiz {{/str}}{{#str}} firstname, capquiz {{/str}}{{#str}} lastname, capquiz {{/str}}{{#str}} rating, capquiz {{/str}}{{#str}} stars, capquiz {{/str}}{{#str}} graded_stars, capquiz {{/str}}{{#str}} pass_or_fail, capquiz {{/str}}{{#str}}username{{/str}}{{#str}}firstname{{/str}}{{#str}}lastname{{/str}}{{#str}}rating, capquiz{{/str}}{{#str}}stars, capquiz{{/str}}{{#str}}graded_stars, capquiz{{/str}}{{#str}}pass_or_fail, capquiz{{/str}}
{{ index }}{{ username }}{{ firstname }}{{ lastname }}{{ rating }}{{ stars }}{{ graded_stars }} - {{#passing_grade}}{{#str}} grade_pass, capquiz {{/str}}{{/passing_grade}} - {{^passing_grade}}{{#str}} grade_fail, capquiz {{/str}}{{/passing_grade}} - {{username}}{{firstname}}{{lastname}}{{rating}}{{stars}}{{gradedstars}} + {{#passinggrade}}{{#str}}grade_pass, capquiz{{/str}}{{/passinggrade}} + {{^passinggrade}}{{#str}}grade_fail, capquiz{{/str}}{{/passinggrade}} +
- {{^users}} -

{{#str}} no_enrolled_students, capquiz {{/str}}

+

{{#str}}no_enrolled_students, capquiz{{/str}}

{{/users}} - {{#regrade}} - {{> core/single_button}} + {{>core/single_button}} {{/regrade}} diff --git a/templates/configure_grading.mustache b/templates/configure_grading.mustache deleted file mode 100644 index 4f41bc7..0000000 --- a/templates/configure_grading.mustache +++ /dev/null @@ -1,26 +0,0 @@ -{{! - This file is part of Moodle - http://moodle.org/ - - Moodle is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - Moodle is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with Moodle. If not, see . -}} -{{! - @template mod_capquiz/configure_badge_rating - - Example context (json): - { - "form": "raw html for the form" - } -}} -

{{#str}} configure_grading, capquiz {{/str}}

-{{{ rating_form }}} diff --git a/templates/create_question_list.mustache b/templates/create_question_list.mustache deleted file mode 100755 index bb924e4..0000000 --- a/templates/create_question_list.mustache +++ /dev/null @@ -1,26 +0,0 @@ -{{! - This file is part of Moodle - http://moodle.org/ - - Moodle is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - Moodle is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with Moodle. If not, see . -}} -{{! - @template mod_capquiz/create_question_list - - Example context (json): - { - "form": "raw html for the form" - } -}} -

{{#str}} create_question_list, capquiz {{/str}}

-{{{ form }}} diff --git a/templates/edit_questions.mustache b/templates/edit_questions.mustache new file mode 100755 index 0000000..70d74e9 --- /dev/null +++ b/templates/edit_questions.mustache @@ -0,0 +1,92 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template mod_capquiz/edit_questions + + Example context (json): + { + "slots": [ + { + "name": "First question", + "rating": 500.0, + "slotid": 2, + "deleteaction": { + "id": "capquiz_delete_slot_button", + "method": "post", + "url": "/mod/capquiz/edit.php?id=1&action=deleteslot&slotid=2", + "label": "Delete" + }, + "editaction": { + "id": "capquiz_edit_question_button", + "method": "post", + "url": "/question/bank/editquestion/question.php?cmid=1&id=56", + "label": "Edit" + }, + "previewaction": { + "id": "capquiz_preview_question_button", + "method": "post", + "url": "/question/bank/previewquestion/preview.php?id=56", + "label": "Preview" + } + } + ], + "actionmenu": "" + } +}} +

{{#str}}questions, capquiz{{/str}}

+ +{{^slots}} +

{{#str}}no_questions_added_to_list, capquiz{{/str}}

+{{/slots}} + + + + + + + + + + + + {{#slots}} + + + + + + + {{/slots}} + +
{{#str}}title, capquiz{{/str}}{{#str}}rating, capquiz{{/str}}{{#str}}version{{/str}}{{#str}}action{{/str}}
{{{name}}} + + + + + {{^latestversion}}{{#str}}questionversion, quiz, {{version}}{{/str}}{{/latestversion}} + {{#latestversion}}{{#str}}questionversionlatest, quiz, {{version}}{{/str}}{{/latestversion}} + + {{#editaction}}{{>core/action_link}}{{/editaction}} + {{#previewaction}}{{>core/action_link}}{{/previewaction}} + {{#deleteaction}}{{>core/action_link}}{{/deleteaction}} +
+ +
+
{{{actionmenu}}}
+
diff --git a/templates/index_table.mustache b/templates/index_table.mustache new file mode 100644 index 0000000..6981350 --- /dev/null +++ b/templates/index_table.mustache @@ -0,0 +1,70 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template mod_capquiz/index_table + + Example context (json): + { + "capquizzes": [ + { + "name": "CAPQuiz", + "timeopen": 0, + "timedue": 1742506980, + "isopen": false, + "defaultuserrating": 1200.0, + "defaultquestionrating": 600.0 + } + ] + } +}} +

{{#str}}modulenameplural, capquiz{{/str}}

+ + + + + + + + + + + + {{#capquizzes}} + + + {{#timeopen}} + + {{/timeopen}} + {{^timeopen}} + + {{/timeopen}} + {{#timedue}} + + {{/timedue}} + {{^timedue}} + + {{/timedue}} + + + + {{/capquizzes}} + +
{{#str}}name{{/str}}{{#str}}timeopen, capquiz{{/str}}{{#str}}due_time_grading, capquiz{{/str}}{{#str}}default_user_rating, capquiz{{/str}}{{#str}}default_question_rating, capquiz{{/str}}
{{name}} + {{#isopen}}{{#pix}}i/checked{{/pix}}{{/isopen}} + {{^isopen}}{{#pix}}e/cancel{{/pix}}{{/isopen}} + {{#userdate}}{{timeopen}}, {{#str}}strftimedate, core_langconfig{{/str}}{{/userdate}} + {{#pix}}e/cancel{{/pix}}{{#str}}notopenforstudents, capquiz{{/str}}{{#userdate}}{{timedue}}, {{#str}}strftimedate, core_langconfig{{/str}}{{/userdate}}{{#str}}timeduenotset, capquiz{{/str}}{{defaultuserrating}}{{defaultquestionrating}}
diff --git a/templates/instructor_dashboard_publish.mustache b/templates/instructor_dashboard_publish.mustache deleted file mode 100755 index 6df8fa1..0000000 --- a/templates/instructor_dashboard_publish.mustache +++ /dev/null @@ -1,44 +0,0 @@ -{{! - This file is part of Moodle - http://moodle.org/ - - Moodle is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - Moodle is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with Moodle. If not, see . -}} -{{! - @template mod_capquiz/instructor_dashboard_publish - - Example context (json): - { - "publish": { - "id": "id", - "method": "post", - "url": "#", - "primary": true, - "label": "Publish" - }, - "message": "" - } -}} -

{{#str}} publish, capquiz {{/str}}

- -{{#str}} publish_explanation, capquiz {{/str}} - -{{#publish}} - {{>core/single_button}} -{{/publish}} - -
- -{{#message}} - {{{message}}} -{{/message}} diff --git a/templates/instructor_dashboard_summary.mustache b/templates/instructor_dashboard_summary.mustache deleted file mode 100755 index e333910..0000000 --- a/templates/instructor_dashboard_summary.mustache +++ /dev/null @@ -1,65 +0,0 @@ -{{! - This file is part of Moodle - http://moodle.org/ - - Moodle is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - Moodle is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with Moodle. If not, see . -}} -{{! - @template mod_capquiz/instructor_dashboard_summary - - Example context (json): - { - "published_status": "Published", - "question_list_title": "My question list", - "question_count": 15, - "enrolled_student_count": 40 - } -}} -

{{#str}} dashboard, capquiz {{/str}}

-
- - - - - - - - - - - - - - - - - -
- {{#str}} status, capquiz {{/str}}: - - {{{published_status}}} -
- {{#str}} question_list, capquiz {{/str}}: - - {{{question_list_title}}} -
- {{#str}} question_count, capquiz {{/str}}: - - {{{question_count}}} -
- {{#str}} enrolled_students, capquiz {{/str}}: - - {{{enrolled_student_count}}} -
-
-
diff --git a/templates/instructor_dashboard_template.mustache b/templates/instructor_dashboard_template.mustache deleted file mode 100755 index 664517d..0000000 --- a/templates/instructor_dashboard_template.mustache +++ /dev/null @@ -1,45 +0,0 @@ -{{! - This file is part of Moodle - http://moodle.org/ - - Moodle is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - Moodle is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with Moodle. If not, see . -}} -{{! - @template mod_capquiz/instructor_dashboard_template - - Example context (json): - { - "create_template": { - "id": "id", - "primary": true, - "method": "post", - "url": "#", - "label": "Create template", - "disabled": false - }, - "message": "" - } -}} -

{{#str}} template, capquiz {{/str}}

- -{{#str}}template_explanation, capquiz {{/str}} - -{{#create_template}} - {{>core/single_button}} -{{/create_template}} - -
- -{{#message}} - {{{message}}} -{{/message}} diff --git a/templates/matchmaking_configuration.mustache b/templates/matchmaking_configuration.mustache deleted file mode 100755 index 3e52043..0000000 --- a/templates/matchmaking_configuration.mustache +++ /dev/null @@ -1,27 +0,0 @@ -{{! - This file is part of Moodle - http://moodle.org/ - - Moodle is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - Moodle is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with Moodle. If not, see . -}} -{{! - @template mod_capquiz/matchmaking_configuration - - Example context (json): - { - "strategy": "N-closest", - "form": "raw html for the form" - } -}} -

{{#str}} configure, capquiz {{/str}} {{strategy}}

-{{{ form }}} diff --git a/templates/matchmaking_selection_strategy.mustache b/templates/matchmaking_selection_strategy.mustache deleted file mode 100755 index e364c1b..0000000 --- a/templates/matchmaking_selection_strategy.mustache +++ /dev/null @@ -1,26 +0,0 @@ -{{! - This file is part of Moodle - http://moodle.org/ - - Moodle is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - Moodle is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with Moodle. If not, see . -}} -{{! - @template mod_capquiz/matchmaking_selection_strategy - - Example context (json): - { - "form": "raw html for the configuration form" - } -}} -

{{#str}} choose_selection_strategy, capquiz {{/str}}

-{{{ form }}} diff --git a/templates/merge_with_question_list.mustache b/templates/merge_with_question_list.mustache deleted file mode 100644 index 057449e..0000000 --- a/templates/merge_with_question_list.mustache +++ /dev/null @@ -1,71 +0,0 @@ -{{! - This file is part of Moodle - http://moodle.org/ - - Moodle is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - Moodle is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with Moodle. If not, see . -}} -{{! - @template mod_capquiz/merge_with_question_list - - Example context (json): - { - "lists": [ - { - "id": 1, - "title": "Question list", - "description": "A description of the list.", - "questions": [ - { - "name": "Question name", - "rating": 1000.0 - } - ] - } - ] - } -}} -

{{#str}}question_list, capquiz{{/str}}

- -{{#lists}} -
- - {{ title }} - - {{#userdate}}{{time_created}}, %A, %d %B %Y, %I:%M %p{{/userdate}} - - -
-

{{ description }}

- {{#merge}} - {{>core/single_button}} - {{/merge}} - {{#delete}} - - {{>core/single_button}} - - {{/delete}} - - - - - - {{#questions}} - - - - - {{/questions}} -
{{#str}} rating, capquiz {{/str}}{{#str}} name, capquiz {{/str}}
{{ rating }}{{ name }}
-
-
-{{/lists}} diff --git a/templates/question_list.mustache b/templates/question_list.mustache deleted file mode 100755 index cdf49a3..0000000 --- a/templates/question_list.mustache +++ /dev/null @@ -1,95 +0,0 @@ -{{! - This file is part of Moodle - http://moodle.org/ - - Moodle is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - Moodle is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with Moodle. If not, see . -}} -{{! - @template mod_capquiz/question_list - - Example context (json): - { - "questions": [ - { - "index": 1, - "name": "First question", - "rating": 500.0, - "question_id": 56, - "rating_url": "#", - "button": { - "id": "id", - "primary": true, - "method": "post", - "url": "#", - "label": "Remove" - } - } - ], - "message": "Message for the user" - } -}} -

{{#str}}question_list, capquiz{{/str}}

- -{{#str}} default_question_rating, capquiz {{/str}}: -
- - {{#pix}} t/check, core {{/pix}} -
- -
- -{{#message}} - {{{message}}} -{{/message}} - - - - - - - - - {{#questions}} - - {{#.}} - - - - - - - {{/.}} - - {{/questions}} -
#{{#str}}title, capquiz{{/str}}{{#str}}rating, capquiz{{/str}}{{#str}}action, capquiz{{/str}}
{{ index }}{{{ name }}} -
- - {{#pix}} t/check, core {{/pix}} -
-
- {{#delete}} - {{>core/action_link}} - {{/delete}} - - {{#edit}} - {{>core/action_link}} - {{/edit}} - - {{#preview}} - {{>core/action_link}} - {{/preview}} -
- -{{^questions}} - {{#str}}no_questions_added_to_list, capquiz{{/str}} -{{/questions}} diff --git a/templates/question_list_selection.mustache b/templates/question_list_selection.mustache deleted file mode 100755 index 033c2fd..0000000 --- a/templates/question_list_selection.mustache +++ /dev/null @@ -1,59 +0,0 @@ -{{! - This file is part of Moodle - http://moodle.org/ - - Moodle is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - Moodle is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with Moodle. If not, see . -}} -{{! - @template mod_capquiz/question_list_selection - - Example context (json): - { - "lists": [ - { - "title": "My question list", - "description": "These are my questions", - "author": "username", - "created": "2019-01-16 16:04:20", - "url": "#" - } - ], - "create": "raw html for a button to create the question list" - } -}} -

- {{#str}} question_lists, capquiz {{/str}} -

-

{{#str}}select_template, capquiz{{/str}}

-{{^lists}} - {{#str}}no_templates_created, capquiz{{/str}} -{{/lists}} -
    - {{#lists}} -
  1. - -
    -

    {{ title }}

    -

    {{ description }}

    - {{#str}}author, capquiz{{/str}}: {{author}} -
    - {{#str}}created, capquiz{{/str}}: {{created}} -
    -
  2. - {{/lists}} -
-
-

{{#str}}create_own_template, capquiz{{/str}}

-{{{create}}} diff --git a/templates/rating_system_configuration.mustache b/templates/rating_system_configuration.mustache deleted file mode 100755 index 0e93152..0000000 --- a/templates/rating_system_configuration.mustache +++ /dev/null @@ -1,27 +0,0 @@ -{{! - This file is part of Moodle - http://moodle.org/ - - Moodle is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - Moodle is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with Moodle. If not, see . -}} -{{! - @template mod_capquiz/rating_system_configuration - - Example context (json): - { - "strategy": "Elo", - "form": "raw html for the form" - } -}} -

{{#str}} configure, capquiz {{/str}} {{strategy}}

-{{{ form }}} diff --git a/templates/rating_system_selection.mustache b/templates/rating_system_selection.mustache deleted file mode 100755 index 777b8af..0000000 --- a/templates/rating_system_selection.mustache +++ /dev/null @@ -1,26 +0,0 @@ -{{! - This file is part of Moodle - http://moodle.org/ - - Moodle is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - Moodle is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with Moodle. If not, see . -}} -{{! - @template mod_capquiz/rating_system_selection - - Example context (json): - { - "form": "raw html for the form" - } -}} -

{{#str}} choose_rating_system, capquiz {{/str}}

-{{{ form }}} diff --git a/templates/student_progress.mustache b/templates/student_progress.mustache deleted file mode 100755 index ee586c1..0000000 --- a/templates/student_progress.mustache +++ /dev/null @@ -1,74 +0,0 @@ -{{! - This file is part of Moodle - http://moodle.org/ - - Moodle is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - Moodle is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with Moodle. If not, see . -}} -{{! - @template mod_capquiz/student_progress - - Example context (json): - { - "progress": { - "student": { - "up": { - "percent": 40 - }, - "stars": 3 - } - } - } -}} -{{#progress}} -
- {{#student}} -
- {{#up}} -
-
- {{ percent }}% -
-
- {{/up}} - {{#down}} -
-
- -{{ percent }}% -
-
- {{/down}} -
-
- {{#stars}} - {{#.}} - {{#pix}} star, capquiz {{/pix}} - {{/.}} - {{/stars}} - {{#blankstars}} - {{#.}} - {{#pix}} blank-star, capquiz {{/pix}} - {{/.}} - {{/blankstars}} - {{#nostars}} - {{#.}} - {{#pix}} no-star, capquiz {{/pix}} - {{/.}} - {{/nostars}} - - {{#pix}} help{{/pix}} - -
-
- {{/student}} -
-{{/progress}} diff --git a/templates/student_question.mustache b/templates/student_question.mustache deleted file mode 100755 index 3874a10..0000000 --- a/templates/student_question.mustache +++ /dev/null @@ -1,38 +0,0 @@ -{{! - This file is part of Moodle - http://moodle.org/ - - Moodle is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - Moodle is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with Moodle. If not, see . -}} -{{! - @template mod_capquiz/student_question - - Example context (json): - { - "attempt": { - "url": "#", - "body": "question body html", - "slots": "" - } - } -}} -{{#attempt}} -
-
- - - {{{ body }}} -
-
-{{/attempt}} \ No newline at end of file diff --git a/templates/student_question_metainfo.mustache b/templates/student_question_metainfo.mustache deleted file mode 100755 index a9fca63..0000000 --- a/templates/student_question_metainfo.mustache +++ /dev/null @@ -1,83 +0,0 @@ -{{! - This file is part of Moodle - http://moodle.org/ - - Moodle is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - Moodle is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with Moodle. If not, see . -}} -{{! - @template mod_capquiz/student_question_metainfo - - Example context (json): - { - "metainfo": { - "rating": { - "student": 1500.0, - "question": 1450.0 - }, - "question": { - "capquiz_id": 46, - "moodle_id": 71 - } - } - } -}} -{{#metainfo}} - - - - - -
- {{#rating}} - - - - - - - - - -
- {{#str}}your_rating, capquiz{{/str}}: - - {{ student }} -
- {{#str}}question_rating, capquiz{{/str}}: - - {{ question }} -
- {{/rating}} -
- {{#question}} - - - - - - - - - -
- CAPQuiz ID: - - {{ capquiz_id }} -
- Moodle ID: - - {{ moodle_id }} -
- {{/question}} -
-{{/metainfo}} \ No newline at end of file diff --git a/templates/unauthorized.mustache b/templates/unauthorized.mustache deleted file mode 100755 index 49a74aa..0000000 --- a/templates/unauthorized.mustache +++ /dev/null @@ -1,24 +0,0 @@ -{{! - This file is part of Moodle - http://moodle.org/ - - Moodle is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - Moodle is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with Moodle. If not, see . -}} -{{! - @template mod_capquiz/unauthorized - - Example context (json): - { - } -}} -

{{#str}}need_to_log_in, capquiz{{/str}}

\ No newline at end of file diff --git a/version.php b/version.php index 1568516..8927921 100755 --- a/version.php +++ b/version.php @@ -19,17 +19,17 @@ * * @package mod_capquiz * @author Aleksander Skrede - * @author Sebastian S. Gundersen + * @author Sebastian Gundersen * @author André Storhaug - * @copyright 2019 NTNU + * @copyright 2024 Norwegian University of Science and Technology (NTNU) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); -$plugin->version = 2024100700; +$plugin->version = 2025010500; $plugin->requires = 2022041901; // 4.0 $plugin->cron = 0; $plugin->component = 'mod_capquiz'; $plugin->maturity = MATURITY_STABLE; -$plugin->release = '0.8.0'; +$plugin->release = '0.9.0'; diff --git a/view.php b/view.php index 6755019..754abdc 100755 --- a/view.php +++ b/view.php @@ -15,40 +15,51 @@ // along with Moodle. If not, see . /** - * Displays the correct view, depending on your role, acts as an entrypoint to the capquiz + * View page. * * @package mod_capquiz - * @author Aleksander Skrede - * @copyright 2018 NTNU + * @author Sebastian Gundersen + * @copyright 2024 Norwegian University of Science and Technology (NTNU) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -namespace mod_capquiz; +use mod_capquiz\capquiz; +use mod_capquiz\output\classlist; +use mod_capquiz\output\renderer; -require_once('../../config.php'); +require_once(__DIR__ . '/../../config.php'); + +global $CFG, $OUTPUT, $PAGE, $USER; + +require_once($CFG->libdir . '/formslib.php'); require_once($CFG->dirroot . '/question/editlib.php'); require_once($CFG->dirroot . '/mod/capquiz/lib.php'); -$cmid = capquiz_urls::require_course_module_id_param(); +$cmid = required_param('id', PARAM_INT); $cm = get_coursemodule_from_id('capquiz', $cmid, 0, false, MUST_EXIST); require_login($cm->course, false, $cm); +$context = context_module::instance($cmid); + +$PAGE->set_context($context); +$PAGE->set_cm($cm); +$PAGE->set_pagelayout('incourse'); +$PAGE->set_url(new moodle_url('/mod/capquiz/view.php', ['id' => $cmid])); + +/** @var renderer $renderer */ +$renderer = $PAGE->get_renderer('mod_capquiz'); -$capquiz = new capquiz($cmid); -$renderer = $capquiz->renderer(); - -capquiz_urls::set_page_url($capquiz, capquiz_urls::$urlview); - -if (has_capability('mod/capquiz:instructor', $capquiz->context())) { - if ($capquiz->has_question_list()) { - $renderer->display_instructor_dashboard($capquiz); - } else { - $renderer->display_choose_question_list_view(); - } -} else { - require_capability('mod/capquiz:student', $capquiz->context()); - // Question engine is null if the quiz is not published. - $qengine = $capquiz->question_engine($capquiz->user()); - $qengine?->delete_invalid_attempt($capquiz->user()); - $capquiz->update_grades(); - $renderer->display_question_attempt_view($capquiz); +echo $OUTPUT->header(); + +if (has_capability('mod/capquiz:instructor', $context)) { + echo $renderer->render(new classlist(new capquiz($cm->instance))); +} + +if (has_any_capability(['mod/capquiz:student', 'mod/capquiz:instructor'], $context)) { + $attempturl = new moodle_url('/mod/capquiz/attempt.php', ['id' => $cmid]); + echo '

Attempt quiz

'; + echo '
'; + echo $renderer->render(new action_link($attempturl, get_string('attempt', 'capquiz'))); + echo '
'; } + +echo $OUTPUT->footer(); diff --git a/view_create_question_list.php b/view_create_question_list.php deleted file mode 100755 index e7d9cd8..0000000 --- a/view_create_question_list.php +++ /dev/null @@ -1,47 +0,0 @@ -. - -/** - * Displays the create_question_list view - * - * @package mod_capquiz - * @author Sebastian S. Gundersen - * @copyright 2018 NTNU - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - -namespace mod_capquiz; - -require_once('../../config.php'); -require_once($CFG->dirroot . '/question/editlib.php'); -require_once($CFG->dirroot . '/mod/capquiz/lib.php'); - -$cmid = capquiz_urls::require_course_module_id_param(); -$cm = get_coursemodule_from_id('capquiz', $cmid, 0, false, MUST_EXIST); -require_login($cm->course, false, $cm); -$context = \context_module::instance($cmid); -require_capability('mod/capquiz:instructor', $context); - -$cmid = capquiz_urls::require_course_module_id_param(); -$capquiz = new capquiz($cmid); -capquiz_urls::set_page_url($capquiz, capquiz_urls::$urlviewcreateqlist); - -$renderer = $capquiz->renderer(); -if ($capquiz->has_question_list()) { - $renderer->display_instructor_dashboard($capquiz); -} else { - $renderer->display_question_list_create_view($capquiz); -} diff --git a/view_grading.php b/view_grading.php deleted file mode 100644 index 8863fc6..0000000 --- a/view_grading.php +++ /dev/null @@ -1,40 +0,0 @@ -. - -/** - * Displays the grading view - * - * @package mod_capquiz - * @author Aleksander Skrede - * @copyright 2018 NTNU - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - -namespace mod_capquiz; - -require_once('../../config.php'); -require_once($CFG->dirroot . '/question/editlib.php'); -require_once($CFG->dirroot . '/mod/capquiz/lib.php'); - -$cmid = capquiz_urls::require_course_module_id_param(); -$cm = get_coursemodule_from_id('capquiz', $cmid, 0, false, MUST_EXIST); -require_login($cm->course, false, $cm); -$context = \context_module::instance($cmid); -require_capability('mod/capquiz:instructor', $context); - -$capquiz = new capquiz($cmid); -capquiz_urls::set_page_url($capquiz, capquiz_urls::$urlviewgrading); -$capquiz->renderer()->display_grading_configuration($capquiz); diff --git a/view_rating_system.php b/view_rating_system.php deleted file mode 100644 index 7d005ad..0000000 --- a/view_rating_system.php +++ /dev/null @@ -1,40 +0,0 @@ -. - -/** - * Displays the rating_system view - * - * @package mod_capquiz - * @author Aleksander Skrede - * @copyright 2018 NTNU - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - -namespace mod_capquiz; - -require_once('../../config.php'); -require_once($CFG->dirroot . '/question/editlib.php'); -require_once($CFG->dirroot . '/mod/capquiz/lib.php'); - -$cmid = capquiz_urls::require_course_module_id_param(); -$cm = get_coursemodule_from_id('capquiz', $cmid, 0, false, MUST_EXIST); -require_login($cm->course, false, $cm); -$context = \context_module::instance($cmid); -require_capability('mod/capquiz:instructor', $context); - -$capquiz = new capquiz($cmid); -capquiz_urls::set_page_url($capquiz, capquiz_urls::$urlviewratingsystemconfig); -$capquiz->renderer()->display_rating_system_configuration($capquiz); diff --git a/view_report.php b/view_report.php deleted file mode 100644 index e20aca5..0000000 --- a/view_report.php +++ /dev/null @@ -1,41 +0,0 @@ -. - -/** - * Displays capquiz report - * - * @package mod_capquiz - * @author André Storhaug - * @copyright 2019 Norwegian University of Science and Technology (NTNU) - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - -namespace mod_capquiz; - -require_once('../../config.php'); -require_once($CFG->dirroot . '/question/editlib.php'); -require_once($CFG->dirroot . '/mod/capquiz/lib.php'); - -$cmid = capquiz_urls::require_course_module_id_param(); -$cm = get_coursemodule_from_id('capquiz', $cmid, 0, false, MUST_EXIST); -require_login($cm->course, false, $cm); -$context = \context_module::instance($cmid); -require_capability('mod/capquiz:instructor', $context); - -$cmid = capquiz_urls::require_course_module_id_param(); -$capquiz = new capquiz($cmid); -capquiz_urls::set_page_url($capquiz, capquiz_urls::$urledit); -$capquiz->renderer()->display_report($capquiz);