diff --git a/.github/workflows/moodle-plugin-ci.yml b/.github/workflows/moodle-plugin-ci.yml index 35748095..ad89f1dc 100644 --- a/.github/workflows/moodle-plugin-ci.yml +++ b/.github/workflows/moodle-plugin-ci.yml @@ -38,9 +38,9 @@ jobs: include: - {php: '7.3', moodle-branch: MOODLE_400_STABLE, database: pgsql} - {php: '7.4', moodle-branch: MOODLE_401_STABLE, database: mariadb} - - {php: '8.1', moodle-branch: MOODLE_402_STABLE, database: pgsql} + - {php: '8.0', moodle-branch: MOODLE_402_STABLE, database: pgsql} - {php: '8.2', moodle-branch: MOODLE_403_STABLE, database: mariadb} - - {php: '8.0', moodle-branch: master, database: pgsql} + - {php: '8.1', moodle-branch: main, database: pgsql} steps: - name: Check out repository code @@ -73,10 +73,12 @@ jobs: - name: PHP Lint if: ${{ always() }} + continue-on-error: true # This step will show errors but will not fail run: moodle-plugin-ci phplint - name: PHP Copy/Paste Detector if: ${{ always() }} + continue-on-error: true # This step will show errors but will not fail run: moodle-plugin-ci phpcpd - name: PHP Mess Detector diff --git a/amd/build/question_nav_tabs.min.js b/amd/build/question_nav_tabs.min.js index 74f98e31..77d8e1d5 100644 --- a/amd/build/question_nav_tabs.min.js +++ b/amd/build/question_nav_tabs.min.js @@ -1,11 +1,10 @@ -define("mod_studentquiz/question_nav_tabs",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0; +define("mod_studentquiz/question_nav_tabs",["exports","core_user/repository"],(function(_exports,UserRepository){function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,UserRepository=function(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}newObj.default=obj,cache&&cache.set(obj,newObj);return newObj} /** * Javascript for question-nav-tabs. * * @module mod_studentquiz/question_nav_tabs * @copyright 2021 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -const updateActiveTab=e=>M.util.set_user_preference("mod_studentquiz_question_active_tab",e.target.dataset.tabId);_exports.init=()=>{document.querySelectorAll(".question-nav-tabs > .nav-tabs > .nav-item.nav-link").forEach((tab=>{tab.addEventListener("click",updateActiveTab)}))}})); + */(UserRepository);const updateActiveTab=e=>{void 0!==UserRepository.setUserPreference?UserRepository.setUserPreference("mod_studentquiz_question_active_tab",e.target.dataset.tabId):M.util.set_user_preference("mod_studentquiz_question_active_tab",e.target.dataset.tabId)};_exports.init=()=>{document.querySelectorAll(".question-nav-tabs > .nav-tabs > .nav-item.nav-link").forEach((tab=>{tab.addEventListener("click",updateActiveTab)}))}})); //# sourceMappingURL=question_nav_tabs.min.js.map \ No newline at end of file diff --git a/amd/build/question_nav_tabs.min.js.map b/amd/build/question_nav_tabs.min.js.map index c24f4f34..4882b442 100644 --- a/amd/build/question_nav_tabs.min.js.map +++ b/amd/build/question_nav_tabs.min.js.map @@ -1 +1 @@ -{"version":3,"file":"question_nav_tabs.min.js","sources":["../src/question_nav_tabs.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 * Javascript for question-nav-tabs.\n *\n * @module mod_studentquiz/question_nav_tabs\n * @copyright 2021 The Open University\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n/**\n * Update the current active tab to user preferences.\n *\n * @private\n * @param {Object} e Event\n * @return {Promise} The promise object.\n */\nconst updateActiveTab = (e) => {\n return M.util.set_user_preference('mod_studentquiz_question_active_tab', e.target.dataset.tabId);\n};\n\n/**\n * Init the question-nav-tabs.\n *\n */\nexport const init = () => {\n let tabs = document.querySelectorAll('.question-nav-tabs > .nav-tabs > .nav-item.nav-link');\n\n tabs.forEach((tab) => {\n tab.addEventListener('click', updateActiveTab);\n });\n};\n"],"names":["updateActiveTab","e","M","util","set_user_preference","target","dataset","tabId","document","querySelectorAll","forEach","tab","addEventListener"],"mappings":";;;;;;;;MA8BMA,gBAAmBC,GACdC,EAAEC,KAAKC,oBAAoB,sCAAuCH,EAAEI,OAAOC,QAAQC,qBAO1E,KACLC,SAASC,iBAAiB,uDAEhCC,SAASC,MACVA,IAAIC,iBAAiB,QAASZ"} \ No newline at end of file +{"version":3,"file":"question_nav_tabs.min.js","sources":["../src/question_nav_tabs.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 * Javascript for question-nav-tabs.\n *\n * @module mod_studentquiz/question_nav_tabs\n * @copyright 2021 The Open University\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport * as UserRepository from 'core_user/repository';\n\n/**\n * Update the current active tab to user preferences.\n *\n * @private\n * @param {Object} e Event\n */\nconst updateActiveTab = (e) => {\n if (typeof UserRepository.setUserPreference !== 'undefined') {\n UserRepository.setUserPreference('mod_studentquiz_question_active_tab', e.target.dataset.tabId);\n } else {\n M.util.set_user_preference('mod_studentquiz_question_active_tab', e.target.dataset.tabId);\n }\n};\n\n/**\n * Init the question-nav-tabs.\n *\n */\nexport const init = () => {\n let tabs = document.querySelectorAll('.question-nav-tabs > .nav-tabs > .nav-item.nav-link');\n\n tabs.forEach((tab) => {\n tab.addEventListener('click', updateActiveTab);\n });\n};\n"],"names":["updateActiveTab","e","UserRepository","setUserPreference","target","dataset","tabId","M","util","set_user_preference","document","querySelectorAll","forEach","tab","addEventListener"],"mappings":";;;;;;;4BA8BMA,gBAAmBC,SAC2B,IAArCC,eAAeC,kBACtBD,eAAeC,kBAAkB,sCAAuCF,EAAEG,OAAOC,QAAQC,OAEzFC,EAAEC,KAAKC,oBAAoB,sCAAuCR,EAAEG,OAAOC,QAAQC,sBAQvE,KACLI,SAASC,iBAAiB,uDAEhCC,SAASC,MACVA,IAAIC,iBAAiB,QAASd"} \ No newline at end of file diff --git a/amd/src/question_nav_tabs.js b/amd/src/question_nav_tabs.js index 50ab3b68..5fce8ef6 100644 --- a/amd/src/question_nav_tabs.js +++ b/amd/src/question_nav_tabs.js @@ -20,16 +20,20 @@ * @copyright 2021 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +import * as UserRepository from 'core_user/repository'; /** * Update the current active tab to user preferences. * * @private * @param {Object} e Event - * @return {Promise} The promise object. */ const updateActiveTab = (e) => { - return M.util.set_user_preference('mod_studentquiz_question_active_tab', e.target.dataset.tabId); + if (typeof UserRepository.setUserPreference !== 'undefined') { + UserRepository.setUserPreference('mod_studentquiz_question_active_tab', e.target.dataset.tabId); + } else { + M.util.set_user_preference('mod_studentquiz_question_active_tab', e.target.dataset.tabId); + } }; /** diff --git a/classes/condition/studentquiz_condition.php b/classes/condition/studentquiz_condition.php index 6266e59d..9435e6b5 100755 --- a/classes/condition/studentquiz_condition.php +++ b/classes/condition/studentquiz_condition.php @@ -34,7 +34,7 @@ * @copyright 2017 HSR (http://www.hsr.ch) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class studentquiz_condition extends \core_question\bank\search\condition { +class studentquiz_condition extends \core_question\local\bank\condition { /** * Due to fix_sql_params not accepting repeated use of named params, @@ -67,6 +67,24 @@ public function __construct($cm, $filterform, $report, $studentquiz) { $this->init(); } + /** + * Return title of the condition + * + * @return string title of the condition + */ + public function get_title() { + return get_string('showhidden', 'core_question'); + } + + /** + * Return filter class associated with this condition + * + * @return string filter class + */ + public function get_filter_class() { + return 'qbank_deletequestion/datafilter/filtertypes/hidden'; + } + /** @var stdClass */ protected $cm; @@ -83,7 +101,7 @@ public function __construct($cm, $filterform, $report, $studentquiz) { protected $tests; /** @var array */ - protected $params; + protected array $params = []; /** @var bool */ protected $isfilteractive = false; diff --git a/classes/condition/studentquiz_condition_pre_43.php b/classes/condition/studentquiz_condition_pre_43.php new file mode 100644 index 00000000..f3aae8f8 --- /dev/null +++ b/classes/condition/studentquiz_condition_pre_43.php @@ -0,0 +1,251 @@ +. + +/** + * Modify stuff conditionally + * + * @package mod_studentquiz + * @copyright 2017 HSR (http://www.hsr.ch) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_studentquiz\condition; +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->dirroot . '/mod/studentquiz/classes/local/db.php'); +use mod_studentquiz\local\db; + +/** + * Conditionally modify question bank queries. + * + * @copyright 2017 HSR (http://www.hsr.ch) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class studentquiz_condition_pre_43 extends \core_question\bank\search\condition { + + /** + * Due to fix_sql_params not accepting repeated use of named params, + * we need to get unique names for params that will be used more than + * once... + * + * init() from parent class duplicated here as we can't call it directly + * (private) :-P + * + * where() overridden with call to init() followed by call to parent + * where()... + * + * params() always returns $this->params, which doesn't change between + * calls to get_in_or_equal, so don't need to fix anything there. + * Which is fortunate, as there'd be no way to keep where() and params() + * in sync. + * + * @param stdClass $cm + * @param stdClass $filterform + * @param \mod_studentquiz_report $report + * @param stdClass $studentquiz + */ + public function __construct($cm, $filterform, $report, $studentquiz) { + $this->cm = $cm; + $this->filterform = $filterform; + $this->tests = array(); + $this->params = array(); + $this->report = $report; + $this->studentquiz = $studentquiz; + $this->init(); + } + + /** @var stdClass */ + protected $cm; + + /** @var stdClass $filterform Search condition depends on filterform */ + protected $filterform; + + /** @var stdClass */ + protected $studentquiz; + + /** @var \mod_studentquiz_report */ + protected $report; + + /** @var array */ + protected $tests; + + /** @var array */ + protected $params; + + /** @var bool */ + protected $isfilteractive = false; + + /** + * Whether the filter is active. + * @return bool + */ + public function is_filter_active() { + return $this->isfilteractive; + } + + /** + * Initialize. + */ + protected function init() { + if ($adddata = $this->filterform->get_data()) { + + $this->tests = array(); + $this->params = array(); + + foreach ($this->filterform->get_fields() as $field) { + + // Validate input. + $data = $field->check_data($adddata); + + // If input is valid, at least one filter was activated. + if ($data === false) { + continue; + } else { + $this->isfilteractive = true; + } + + $sqldata = $field->get_sql_filter($data); + + // Disable filtering by firstname if anonymized. + if ($field->_name == 'firstname' && !(mod_studentquiz_check_created_permission($this->cm->id) || + !$this->report->is_anonymized())) { + continue; + } + + // Disable filtering by firstname if anonymized. + if ($field->_name == 'lastname' && !(mod_studentquiz_check_created_permission($this->cm->id) || + !$this->report->is_anonymized())) { + continue; + } + + // Respect leading and ending ',' for the tagarray as provided by tag_column.php. + if ($field->_name == 'tagarray') { + foreach ($sqldata[1] as $key => $value) { + if (!empty($value)) { + $sqldata[1][$key] = "%,$value,%"; + } else { + $sqldata[0] = "$field->_name IS NULL"; + } + } + } + + // TODO: cleanup that buggy filter function to remove this! + // The user_filter_checkbox class has a buggy get_sql_filter function. + if ($field->_name == 'createdby') { + $sqldata = array($field->_name . ' = ' . intval($data['value']), array()); + } + + if (is_array($sqldata)) { + $sqldata[0] = str_replace($field->_name, $this->get_sql_field($field->_name) + , $sqldata[0]); + $sqldata[0] = $this->get_special_sql($sqldata[0], $field->_name); + $this->tests[] = '((' . $sqldata[0] . '))'; + $this->params = array_merge($this->params, $sqldata[1]); + } + } + } + } + + /** + * Replaces special fields with additional sql instructions in the query + * + * @param string $sqldata the sql query + * @param string $name affected field name + * @return string modified sql query + */ + private function get_special_sql($sqldata, $name) { + if (substr($sqldata, 0, 12) === 'mydifficulty') { + return str_replace('mydifficulty', '(CASE WHEN sp.attempts > 0 THEN + ROUND(1 - (CAST(sp.correctattempts AS DECIMAL) / CAST(sp.attempts AS DECIMAL)), 2) + ELSE 0 + END)', $sqldata); + } + if ($name == "onlynew") { + return str_replace('myattempts', 'sp.attempts', $sqldata); + } + return $sqldata; + } + + /** + * Replaces fields with additional sql instructions in place of the field + * + * @param string $name affected field name + * @return string modified sql query + */ + private function get_sql_field($name) { + if (substr($name, 0, 12) === 'mydifficulty') { + return str_replace('mydifficulty', '(CASE WHEN sp.attempts > 0 THEN + ROUND(1 - (CAST(sp.correctattempts AS DECIMAL) / CAST(sp.attempts AS DECIMAL)), 2) + ELSE 0 + END)', $name); + } + if (substr($name, 0, 10) === 'myattempts') { + return 'sp.attempts'; + } + return $this->get_sql_table_prefix($name) . $name; + } + + + /** + * Get the sql table prefix + * + * @param string $name + * @return string return sql prefix + */ + private function get_sql_table_prefix($name) { + switch($name){ + case 'difficultylevel': + return 'dl.'; + case 'rate': + return 'vo.'; + case 'publiccomment': + return 'copub.'; + case 'state': + return 'sqq.'; + case 'firstname': + case 'lastname': + return 'uc.'; + case 'lastanswercorrect': + return 'sp.'; + case 'mydifficulty': + return 'mydiffs.'; + case 'myattempts': + return 'myatts.'; + case 'myrate': + return 'myrate.'; + case 'tagarray': + return 'tags.'; + default; + return 'q.'; + } + } + + /** + * Provide SQL fragment to be ANDed into the WHERE clause to filter which questions are shown. + * @return string SQL fragment. Must use named parameters. + */ + public function where() { + return implode(' AND ', $this->tests); + } + + /** + * Return parameters to be bound to the above WHERE clause fragment. + * @return array parameter name => value. + */ + public function params() { + return $this->params; + } +} diff --git a/classes/local/studentquiz_question.php b/classes/local/studentquiz_question.php index 0efbc300..1a6962cc 100644 --- a/classes/local/studentquiz_question.php +++ b/classes/local/studentquiz_question.php @@ -40,10 +40,14 @@ class studentquiz_question { private $cm; /** @var \context_module $context - Context. */ - private $context; + protected $context; /** @var stdClass - StudentQuiz data. */ - private $studentquiz; + protected $studentquiz; + + /** @var int - StudentQuiz question id. */ + protected $id; + /** * studentquiz_question constructor. diff --git a/classes/question/bank/attempts_column.php b/classes/question/bank/attempts_column.php index ad0de71c..58c573c2 100644 --- a/classes/question/bank/attempts_column.php +++ b/classes/question/bank/attempts_column.php @@ -34,6 +34,15 @@ class attempts_column extends studentquiz_column_base { /** @var \stdClass */ protected $studentquiz; + /** @var int category id*/ + protected $categoryid; + + /** @var int current user id*/ + protected $currentuserid; + + /** @var int student quiz id*/ + protected $studentquizid; + /** * Initialise Parameters for join */ diff --git a/classes/question/bank/difficulty_level_column.php b/classes/question/bank/difficulty_level_column.php index 0c163a2a..25bfe4e6 100644 --- a/classes/question/bank/difficulty_level_column.php +++ b/classes/question/bank/difficulty_level_column.php @@ -34,6 +34,12 @@ class difficulty_level_column extends studentquiz_column_base { /** @var \stdClass */ protected $studentquiz; + /** @var int current user id*/ + protected $currentuserid; + + /** @var int student quiz id*/ + protected $studentquizid; + /** * Initialise Parameters for join */ diff --git a/classes/question/bank/preview_column.php b/classes/question/bank/legacy/preview_column.php similarity index 100% rename from classes/question/bank/preview_column.php rename to classes/question/bank/legacy/preview_column.php diff --git a/classes/question/bank/sq_delete_action_column.php b/classes/question/bank/legacy/sq_delete_action_column.php similarity index 100% rename from classes/question/bank/sq_delete_action_column.php rename to classes/question/bank/legacy/sq_delete_action_column.php diff --git a/classes/question/bank/sq_edit_action_column.php b/classes/question/bank/legacy/sq_edit_action_column.php similarity index 99% rename from classes/question/bank/sq_edit_action_column.php rename to classes/question/bank/legacy/sq_edit_action_column.php index 63911379..3feaf7d5 100644 --- a/classes/question/bank/sq_edit_action_column.php +++ b/classes/question/bank/legacy/sq_edit_action_column.php @@ -15,7 +15,6 @@ // along with Moodle. If not, see . namespace mod_studentquiz\bank; - use qbank_editquestion\edit_action_column; use mod_studentquiz\local\studentquiz_helper; diff --git a/classes/question/bank/legacy/sq_edit_menu_column_pre_43.php b/classes/question/bank/legacy/sq_edit_menu_column_pre_43.php new file mode 100644 index 00000000..b1b6cf75 --- /dev/null +++ b/classes/question/bank/legacy/sq_edit_menu_column_pre_43.php @@ -0,0 +1,38 @@ +. + +namespace mod_studentquiz\bank; + +use core_question\local\bank\edit_menu_column; + +/** + * Represent edit column in studentquiz_bank_view which gathers together all the actions into a menu. + * + * @package mod_studentquiz + * @author Thong Bui + * @copyright 2021 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class sq_edit_menu_column_pre_43 extends edit_menu_column { + /** + * Title for this column. + * + * @return string Title of column + */ + public function get_title() { + return get_string('actions'); + } +} diff --git a/classes/question/bank/sq_hidden_action_column.php b/classes/question/bank/legacy/sq_hidden_action_column.php similarity index 100% rename from classes/question/bank/sq_hidden_action_column.php rename to classes/question/bank/legacy/sq_hidden_action_column.php diff --git a/classes/question/bank/sq_pin_action_column.php b/classes/question/bank/legacy/sq_pin_action_column.php similarity index 100% rename from classes/question/bank/sq_pin_action_column.php rename to classes/question/bank/legacy/sq_pin_action_column.php diff --git a/classes/question/bank/legacy/studentquiz_bank_view_pre_43.php b/classes/question/bank/legacy/studentquiz_bank_view_pre_43.php new file mode 100644 index 00000000..cf22661e --- /dev/null +++ b/classes/question/bank/legacy/studentquiz_bank_view_pre_43.php @@ -0,0 +1,681 @@ +. + +/** + * The question bank view + * + * + * @package mod_studentquiz + * @copyright 2017 HSR (http://www.hsr.ch) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_studentquiz\question\bank; + +use mod_studentquiz\local\studentquiz_helper; +use mod_studentquiz\utils; +use stdClass; +use core_question\local\bank\question_version_status; + +defined('MOODLE_INTERNAL') || die(); + +require_once(__DIR__ .'/../../../../locallib.php'); +require_once(__DIR__ . '/../studentquiz_column_base.php'); +require_once(__DIR__ . '/../question_bank_filter.php'); +require_once(__DIR__ . '/../question_text_row.php'); +require_once(__DIR__ . '/../rate_column.php'); +require_once(__DIR__ . '/../difficulty_level_column.php'); +require_once(__DIR__ . '/../tag_column.php'); +require_once(__DIR__ . '/../attempts_column.php'); +require_once(__DIR__ . '/../comments_column.php'); +require_once(__DIR__ . '/../state_column.php'); +require_once(__DIR__ . '/../anonym_creator_name_column.php'); +require_once(__DIR__ . '/../state_pin_column.php'); +require_once(__DIR__ . '/../question_name_column.php'); +require_once(__DIR__ . '/preview_column.php'); +require_once(__DIR__ . '/sq_hidden_action_column.php'); +require_once(__DIR__ . '/sq_edit_action_column.php'); +require_once(__DIR__ . '/sq_pin_action_column.php'); +require_once(__DIR__ . '/sq_delete_action_column.php'); +require_once(__DIR__ . '/sq_edit_menu_column_pre_43.php'); + +/** + * Module instance settings form + * + * @package mod_studentquiz + * @copyright 2017 HSR (http://www.hsr.ch) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class studentquiz_bank_view_pre_43 extends \core_question\local\bank\view { + /** + * @var stdClass filtered questions from database + */ + private $questions; + + /** + * @var array of ids of the questions that are displayed on current page + * (IF the filter result is paginated, ids on other pages are not collected!) + */ + private $displayedquestionsids = array(); + + /** + * @var int totalnumber from filtered questions + */ + private $totalnumber; + + /** + * @var string sql tag name field + */ + private $tagnamefield; + + /** + * @var bool is filter active + */ + private $isfilteractive; + + /** + * @var question_bank_filter_form class + */ + private $filterform; + + /** + * @var array of user_filter_* + */ + private $fields; + + /** + * @var object $studentquiz current studentquiz record + */ + private $studentquiz; + + /** + * @var \core\dml\sql_join Current group join sql. + */ + private $currentgroupjoinsql; + + /** + * @var int Currently viewing user id. + */ + protected $userid; + + + /** + * @var mixed + */ + private $pagevars; + + /** + * @var stdClass StudentQuiz renderer. + */ + protected $renderer; + + /** @var mod_studentquiz_report */ + protected $report; + + /** + * Constructor assuming we already have the necessary data loaded. + * + * @param \core_question\local\bank\question_edit_contexts $contexts + * @param \moodle_url $pageurl + * @param object $course + * @param object|null $cm + * @param object $studentquiz + * @param mixed $pagevars + * @param mod_studentquiz_report $report + */ + public function __construct($contexts, $pageurl, $course, $cm, $studentquiz, $pagevars, $report) { + $this->set_filter_post_data(); + global $USER, $PAGE; + $this->pagevars = $pagevars; + $this->studentquiz = $studentquiz; + $this->userid = $USER->id; + $this->report = $report; + parent::__construct($contexts, $pageurl, $course, $cm); + $this->set_filter_form_fields($this->is_anonymized()); + $this->initialize_filter_form($pageurl); + $currentgroup = groups_get_activity_group($cm, true); + $this->currentgroupjoinsql = utils::groups_get_questions_joins($currentgroup, 'sqq.groupid'); + // Init search conditions with filterform state. + $categorycondition = new \core_question\bank\search\category_condition( + $pagevars['cat'], $pagevars['recurse'], $contexts, $pageurl, $course); + $studentquizcondition = new \mod_studentquiz\condition\studentquiz_condition_pre_43($cm, $this->filterform, + $this->report, $studentquiz); + $this->isfilteractive = $studentquizcondition->is_filter_active(); + $this->searchconditions = array ($categorycondition, $studentquizcondition); + $this->renderer = $PAGE->get_renderer('mod_studentquiz', 'overview'); + } + + /** + * Shows the question bank interface. + * + * The function also processes a number of actions: + * + * Actions affecting the question pool: + * move Moves a question to a different category + * deleteselected Deletes the selected questions from the category + * Other actions: + * category Chooses the category + * params: $tabname question bank edit tab name, for permission checking + * $pagevars current list of page variables + * + * @param array $pagevars + * @param string $tabname + */ + public function display($pagevars, $tabname): void { + $page = $pagevars['qpage']; + $perpage = $pagevars['qperpage']; + $cat = $pagevars['cat']; + $recurse = $pagevars['recurse']; + $showhidden = $pagevars['showhidden']; + $showquestiontext = $pagevars['qbshowtext']; + $tagids = []; + if (!empty($pagevars['qtagids'])) { + $tagids = $pagevars['qtagids']; + } + $output = ''; + + $this->build_query(); + + // Get result set. + $questions = $this->load_questions($page, $perpage); + $this->questions = $questions; + $this->countsql = count($this->questions); + if ($this->countsql || $this->isfilteractive) { + // We're unable to force the filter form to submit with get method. We have 2 forms on the page + // which need to interact with each other, so forcing method as get here. + $output .= str_replace('method="post"', 'method="get"', $this->renderer->render_filter_form($this->filterform)); + } + echo $output; + if ($this->countsql > 0) { + $this->display_question_list($this->baseurl, $cat, null, $page, $perpage, + $this->contexts->having_cap('moodle/question:add') + ); + } else { + list($message, $questionsubmissionallow) = mod_studentquiz_check_availability($this->studentquiz->opensubmissionfrom, + $this->studentquiz->closesubmissionfrom, 'submission'); + if ($questionsubmissionallow) { + echo $this->renderer->render_no_questions_notification($this->isfilteractive); + } + } + } + + /** + * Get all questions + * @return stdClass array of questions + */ + public function get_questions() { + return $this->questions; + } + + /** + * Override base default sort + */ + protected function default_sort(): array { + return [ + 'mod_studentquiz\bank\anonym_creator_name_column-timecreated' => -1, + 'mod_studentquiz\bank\question_name_column' => 1, + ]; + } + + /** + * Create the SQL query to retrieve the indicated questions, based on + * \core_question\local\bank\search\condition filters. + */ + protected function build_query(): void { + global $CFG; + + // Hard coded setup. + $params = array(); + $joins = [ + 'qv' => 'JOIN {question_versions} qv ON qv.questionid = q.id', + 'qbe' => 'JOIN {question_bank_entries} qbe on qbe.id = qv.questionbankentryid', + 'qc' => 'JOIN {question_categories} qc ON qc.id = qbe.questioncategoryid', + 'qr' => "JOIN {question_references} qr ON qr.questionbankentryid = qbe.id AND qv.version = (SELECT MAX(v.version) + FROM {question_versions} v + JOIN {question_bank_entries} be + ON be.id = v.questionbankentryid + WHERE be.id = qbe.id) + AND qr.component = 'mod_studentquiz' + AND qr.questionarea = 'studentquiz_question' + AND qc.contextid = qr.usingcontextid", + 'sqq' => 'JOIN {studentquiz_question} sqq ON sqq.id = qr.itemid' + ]; + $fields = [ + 'sqq.id studentquizquestionid', + 'qc.id categoryid', + 'qv.version', + 'qv.id versionid', + 'qbe.id questionbankentryid', + 'qv.status', + 'q.timecreated', + 'q.createdby', + ]; + // Only show ready and draft question. + $tests = [ + 'q.parent = 0', + "qv.status <> :status", + ]; + $params['status'] = question_version_status::QUESTION_STATUS_HIDDEN; + foreach ($this->requiredcolumns as $column) { + $extrajoins = $column->get_extra_joins(); + foreach ($extrajoins as $prefix => $join) { + if (isset($joins[$prefix]) && $joins[$prefix] != $join) { + throw new \coding_exception('Join ' . $join . ' conflicts with previous join ' . $joins[$prefix]); + } + $joins[$prefix] = $join; + } + $fields = array_merge($fields, $column->get_required_fields()); + } + $fields = array_unique($fields); + if ($this->currentgroupjoinsql->wheres) { + $params += $this->currentgroupjoinsql->params; + $tests[] = $this->currentgroupjoinsql->wheres; + } + + // Build the order by clause. + $sorts = array(); + foreach ($this->sort as $sort => $order) { + list($colname, $subsort) = $this->parse_subsort($sort); + $sorts[] = $this->requiredcolumns[$colname]->sort_expression($order < 0, $subsort); + } + + // Build the where clause and load params from search conditions. + foreach ($this->searchconditions as $searchcondition) { + if (!empty($searchcondition->where())) { + $tests[] = $searchcondition->where(); + } + if (!empty($searchcondition->params())) { + $params = array_merge($params, $searchcondition->params()); + } + } + array_unshift($sorts, 'sqq.pinned DESC'); + + // Build the complete SQL query. + $sql = ' FROM {question} q ' . implode(' ', $joins); + $sql .= ' WHERE ' . implode(' AND ', $tests); + $this->sqlparams = $params; + $this->loadsql = 'SELECT ' . implode(', ', $fields) . $sql . ' ORDER BY ' . implode(', ', $sorts); + } + + /** + * Has questions in category + * @return bool + */ + protected function has_questions_in_category() { + return $this->totalnumber > 0; + } + + /** + * Create new default question form + * @param int $categoryid question category + * @param bool $canadd capability state + */ + public function create_new_question_form($categoryid, $canadd): void { + global $OUTPUT; + + $output = ''; + + $caption = get_string('createnewquestion', 'studentquiz'); + + if ($canadd) { + $returnurl = $this->baseurl; + $params = array( + // TODO: MAGIC CONSTANT! + 'returnurl' => $returnurl->out_as_local_url(false), + 'category' => $categoryid, + 'cmid' => $this->studentquiz->coursemodule, + ); + + $url = new \moodle_url('/question/bank/editquestion/addquestion.php', $params); + + $allowedtypes = (empty($this->studentquiz->allowedqtypes)) ? 'ALL' : $this->studentquiz->allowedqtypes; + $allowedtypes = ($allowedtypes == 'ALL') ? mod_studentquiz_get_question_types_keys() : explode(',', $allowedtypes); + $qtypecontainer = \html_writer::div( + \qbank_editquestion\editquestion_helper::print_choose_qtype_to_add_form(array(), $allowedtypes, true + ), '', array('id' => 'qtypechoicecontainer')); + $questionsubmissionbutton = new \single_button($url, $caption, 'get', 'primary'); + + list($message, $questionsubmissionallow) = mod_studentquiz_check_availability($this->studentquiz->opensubmissionfrom, + $this->studentquiz->closesubmissionfrom, 'submission'); + + $questionsubmissionbutton->disabled = !$questionsubmissionallow; + $output .= \html_writer::div($OUTPUT->render($questionsubmissionbutton) . $qtypecontainer, 'createnewquestion py-3'); + + if (!empty($message)) { + $output .= $this->renderer->render_availability_message($message, 'mod_studentquiz_submission_info'); + } + } else { + $output .= $this->renderer->render_warning_message(get_string('nopermissionadd', 'question')); + } + echo $output; + } + + /** + * Prints the table of questions in a category with interactions + * + * @param \moodle_url $pageurl The URL to reload this page. + * @param string $categoryandcontext 'categoryID,contextID'. + * @param int $recurse Whether to include subcategories. + * @param int $page The number of the page to be displayed + * @param int $perpage Number of questions to show per page + * @param array $addcontexts contexts where the user is allowed to add new questions. + */ + protected function display_question_list($pageurl, $categoryandcontext, $recurse = 1, $page = 0, + $perpage = 100, $addcontexts = []): void { + $output = ''; + $category = $this->get_current_category($categoryandcontext); + list($categoryid, $contextid) = explode(',', $categoryandcontext); + $catcontext = \context::instance_by_id($contextid); + + $output .= \html_writer::start_tag('fieldset', array('class' => 'invisiblefieldset', 'style' => 'display:block;')); + + $output .= $this->renderer->render_hidden_field($this->cm->id, $this->baseurl, $perpage); + + $output .= $this->renderer->render_control_buttons($catcontext, $this->has_questions_in_category(), + $addcontexts, $category); + + $output .= $this->renderer->render_pagination_bar($this->pagevars, $this->baseurl, $this->totalnumber, $page, + $perpage, true); + + $output .= $this->display_question_list_rows(); + + $output .= $this->renderer->render_pagination_bar($this->pagevars, $this->baseurl, $this->totalnumber, $page, + $perpage, false); + + $output .= $this->renderer->render_control_buttons($catcontext, $this->has_questions_in_category(), + $addcontexts, $category); + + $output .= \html_writer::end_tag('fieldset'); + $output = $this->renderer->render_question_form($output); + $output .= $this->renderer->display_javascript_snippet(); + + echo $output; + } + + /** + * Prints the effective question table + * + * @return string + */ + protected function display_question_list_rows() { + $output = ''; + $output .= \html_writer::start_div('categoryquestionscontainer'); + ob_start(); + $this->print_table($this->questions); + $output .= ob_get_contents(); + ob_end_clean(); + $output .= \html_writer::end_div(); + return $output; + } + + /** + * Return the row classes for question table + * + * @param object $question the row from the $question table, augmented with extra information. + * @param int $rowcount Row index + * @return array Classes of row + */ + protected function get_row_classes($question, $rowcount): array { + $classes = parent::get_row_classes($question, $rowcount); + if (($key = array_search('dimmed_text', $classes)) !== false) { + unset($classes[$key]); + } + return $classes; + } + + /** + * Set filter form fields + * @param bool $anonymize if false, questions can get filtered by author last name and first name instead by own userid only. + */ + private function set_filter_form_fields($anonymize = true) { + $this->fields = array(); + + // Fast filters. + $this->fields[] = new \toggle_filter_checkbox('onlynew', + get_string('filter_label_onlynew', 'studentquiz'), + false, 'myattempts', array('myattempts', 'myattempts_op'), 0, 0, + get_string('filter_label_onlynew_help', 'studentquiz')); + + $this->fields[] = new \toggle_filter_checkbox('only_new_state', + get_string('state_newplural', 'studentquiz'), false, 'sqq.state', + ['approved'], 2, studentquiz_helper::STATE_NEW); + $this->fields[] = new \toggle_filter_checkbox('only_approved_state', + get_string('state_approvedplural', 'studentquiz'), false, 'sqq.state', + ['approved'], 2, studentquiz_helper::STATE_APPROVED); + $this->fields[] = new \toggle_filter_checkbox('only_disapproved_state', + get_string('state_disapprovedplural', 'studentquiz'), false, 'sqq.state', + ['approved'], 2, studentquiz_helper::STATE_DISAPPROVED); + $this->fields[] = new \toggle_filter_checkbox('only_changed_state', + get_string('state_changedplural', 'studentquiz'), false, 'sqq.state', + ['approved'], 2, studentquiz_helper::STATE_CHANGED); + $this->fields[] = new \toggle_filter_checkbox('only_reviewable_state', + get_string('state_reviewableplural', 'studentquiz'), false, 'sqq.state', + ['approved'], 2, studentquiz_helper::STATE_REVIEWABLE); + + $this->fields[] = new \toggle_filter_checkbox('onlygood', + get_string('filter_label_onlygood', 'studentquiz'), + false, 'vo.rate', array('rate', 'rate_op'), 1, 4, + get_string('filter_label_onlygood_help', 'studentquiz', '4')); + + $this->fields[] = new \toggle_filter_checkbox('onlymine', + get_string('filter_label_onlymine', 'studentquiz'), + false, 'q.createdby', array('createdby'), 2, $this->userid, + get_string('filter_label_onlymine_help', 'studentquiz')); + + $this->fields[] = new \toggle_filter_checkbox('onlydifficultforme', + get_string('filter_label_onlydifficultforme', 'studentquiz'), + false, 'mydifficulty', array('mydifficulty', 'mydifficulty_op'), 1, 0.60, + get_string('filter_label_onlydifficultforme_help', 'studentquiz', '60')); + + $this->fields[] = new \toggle_filter_checkbox('onlydifficult', + get_string('filter_label_onlydifficult', 'studentquiz'), + false, 'dl.difficultylevel', array('difficultylevel', 'difficultylevel_op'), 1, 0.60, + get_string('filter_label_onlydifficult_help', 'studentquiz', '60')); + + // Advanced filters. + $this->fields[] = new \studentquiz_user_filter_text('tagarray', get_string('filter_label_tags', 'studentquiz'), + true, 'tagarray'); + + $states = array(); + foreach (studentquiz_helper::$statename as $num => $name) { + if ($num == studentquiz_helper::STATE_DELETE || $num == studentquiz_helper::STATE_HIDE) { + continue; + } + $states[$num] = get_string('state_'.$name, 'studentquiz'); + } + $this->fields[] = new \user_filter_simpleselect('state', get_string('state_column_name', 'studentquiz'), + true, 'state', $states); + + $this->fields[] = new \user_filter_number('rate', get_string('filter_label_rates', 'studentquiz'), + true, 'rate'); + $this->fields[] = new \user_filter_percent('difficultylevel', get_string('filter_label_difficulty_level', 'studentquiz'), + true, 'difficultylevel'); + + $this->fields[] = new \user_filter_number('publiccomment', get_string('filter_label_comment', 'studentquiz'), + true, 'publiccomment'); + $this->fields[] = new \studentquiz_user_filter_text('name', get_string('filter_label_question', 'studentquiz'), + true, 'name'); + $this->fields[] = new \studentquiz_user_filter_text('questiontext', get_string('filter_label_questiontext', 'studentquiz'), + true, 'questiontext'); + + if ($anonymize) { + $this->fields[] = new \user_filter_checkbox('createdby', get_string('filter_label_show_mine', 'studentquiz'), + true, 'createdby'); + } else { + $this->fields[] = new \studentquiz_user_filter_text('firstname', get_string('firstname'), true, 'firstname'); + $this->fields[] = new \studentquiz_user_filter_text('lastname', get_string('lastname'), true, 'lastname'); + } + + $this->fields[] = new \studentquiz_user_filter_date('timecreated', get_string('filter_label_createdate', 'studentquiz'), + true, 'timecreated'); + + $this->fields[] = new \user_filter_simpleselect('lastanswercorrect', + get_string('filter_label_mylastattempt', 'studentquiz'), + true, 'lastanswercorrect', array( + '1' => get_string('lastattempt_right', 'studentquiz'), + '0' => get_string('lastattempt_wrong', 'studentquiz') + )); + + $this->fields[] = new \user_filter_number('myattempts', get_string('filter_label_myattempts', 'studentquiz'), + true, 'myattempts'); + + $this->fields[] = new \user_filter_number('mydifficulty', get_string('filter_label_mydifficulty', 'studentquiz'), + true, 'mydifficulty'); + + $this->fields[] = new \user_filter_number('myrate', get_string('filter_label_myrate', 'studentquiz'), + true, 'myrate'); + } + + /** + * Set data for filter recognition + * We have two forms in the view.php page which need to interact with each other. All params are sent through GET, + * but the moodle filter form can only process POST, so we need to copy them there. + */ + private function set_filter_post_data() { + $_POST = $_GET; + } + + /** + * Initialize filter form + * @param moodle_url $pageurl + */ + private function initialize_filter_form($pageurl) { + $this->isfilteractive = false; + + // If reset button was pressed, redirect the user again to the page. + // This means all submitted data is intentionally lost and thus the form clean again. + if (optional_param('resetbutton', false, PARAM_ALPHA)) { + // Reset to clean state. + $pageurl->remove_all_params(); + $pageurl->params(['id' => $this->cm->id]); + redirect($pageurl->out()); + } + $this->filterform = new \mod_studentquiz_question_bank_filter_form( + $this->fields, + $pageurl->out(false), + array_merge(['cmid' => $this->cm->id], $this->pagevars) + ); + } + + /** + * Load question from database + * @param int $page + * @param int $perpage + * @return paginated array of questions + */ + private function load_questions($page, $perpage) { + global $DB; + $rs = $DB->get_recordset_sql($this->loadsql, $this->sqlparams); + + $counterquestions = 0; + $numberofdisplayedquestions = 0; + $showall = $this->pagevars['showall']; + $rs->rewind(); + + // Skip Questions on previous pages. + while ($rs->valid() && !$showall && $counterquestions < $page * $perpage) { + $rs->next(); + $counterquestions++; + } + + // Reset and start from 0 if page was empty. + if (!$showall && $counterquestions < $page * $perpage) { + $rs->rewind(); + } + + // Unfortunately we cant just render the questions directly. + // We need to annotate tags first. + $questions = array(); + // Load questions. + while ($rs->valid() && ($showall || $numberofdisplayedquestions < $perpage)) { + $question = $rs->current(); + $numberofdisplayedquestions++; + $counterquestions++; + $this->displayedquestionsids[] = $question->id; + $rs->next(); + $questions[] = $question; + } + + // Iterate to end. + while ($rs->valid()) { + $rs->next(); + $counterquestions++; + } + $this->totalnumber = $counterquestions; + $rs->close(); + return $questions; + } + + /** + * TODO: rename function and apply (there is duplicate method) + * @return bool studentquiz is set to anoymize ranking. + */ + public function is_anonymized() { + if (!$this->studentquiz->anonymrank) { + return false; + } + $context = \context_module::instance($this->studentquiz->coursemodule); + if (has_capability('mod/studentquiz:unhideanonymous', $context)) { + return false; + } + // Instance is anonymized and isn't allowed to unhide that. + return true; + } + + /** + * Get Studentquiz object of question bank. + * @return \stdClass studentquiz object. + */ + public function get_studentquiz() { + return $this->studentquiz; + } + + /** + * Deal with a sort name of the form columnname, or colname_subsort by + * breaking it up, validating the bits that are present, and returning them. + * If there is no subsort, then $subsort is returned as ''. + * + * @param string $sort the sort parameter to process. + * @return array array($colname, $subsort). + */ + protected function parse_subsort($sort): array { + // When we sort by public/private comments and turn off the setting studentquiz | privatecomment, + // the parse_subsort function will throw exception. We should redirect to the base_url after cleaning all sort params. + $showprivatecomment = $this->studentquiz->privatecommenting; + if ($showprivatecomment && $sort == 'mod_studentquiz\bank\comment_column' || + !$showprivatecomment && ($sort == 'mod_studentquiz\bank\comment_column-privatecomment' || + $sort == 'mod_studentquiz\bank\comment_column-publiccomment')) { + for ($i = 1; $i <= self::MAX_SORTS; $i++) { + $this->baseurl->remove_params('qbs' . $i); + } + redirect($this->base_url()); + } + + return parent::parse_subsort($sort); + } + + /** + * Return the all the required column for the view. + * + * @return \question_bank_column_base[] + */ + protected function wanted_columns(): array { + global $PAGE; + $renderer = $PAGE->get_renderer('mod_studentquiz'); + $this->requiredcolumns = $renderer->get_columns_for_question_bank_view_pre_43($this); + return $this->requiredcolumns; + } +} diff --git a/classes/question/bank/rate_column.php b/classes/question/bank/rate_column.php index cad474e4..a1df9e0d 100644 --- a/classes/question/bank/rate_column.php +++ b/classes/question/bank/rate_column.php @@ -31,6 +31,12 @@ class rate_column extends studentquiz_column_base { */ protected $renderer; + /** @var int current user id*/ + protected $currentuserid; + + /** @var int student quiz id*/ + protected $studentquizid; + /** * Initialise Parameters for join */ diff --git a/classes/question/bank/sq_delete_action.php b/classes/question/bank/sq_delete_action.php new file mode 100644 index 00000000..5c811284 --- /dev/null +++ b/classes/question/bank/sq_delete_action.php @@ -0,0 +1,51 @@ +. + +namespace mod_studentquiz\bank; + +use qbank_deletequestion\delete_action; +use mod_studentquiz\local\studentquiz_helper; + +/** + * Represent delete action in studentquiz_bank_view. + * + * @package mod_studentquiz + * @copyright 2021 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class sq_delete_action extends delete_action { + + /** + * Override method to get url and label for delete action of the studentquiz. + * + * @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. + * $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->state == studentquiz_helper::STATE_APPROVED && + !has_capability('mod/studentquiz:previewothers', $this->qbank->get_most_specific_context())) { + // Do not render delete icon if the question is in approved state for the student. + return [null, null, null]; + } + + return parent::get_url_icon_and_label($question); + } + +} diff --git a/classes/question/bank/sq_edit_action.php b/classes/question/bank/sq_edit_action.php new file mode 100644 index 00000000..7cf12241 --- /dev/null +++ b/classes/question/bank/sq_edit_action.php @@ -0,0 +1,52 @@ +. + +namespace mod_studentquiz\bank; + +use qbank_editquestion\edit_action; +use mod_studentquiz\local\studentquiz_helper; + +/** + * Represent edit action in studentquiz_bank_view + * + * @package mod_studentquiz + * @author Huong Nguyen + * @copyright 2019 HSR (http://www.hsr.ch) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class sq_edit_action extends edit_action { + + /** + * Override method to get url and label for edit action of the studentquiz. + * + * @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. + * $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->state == studentquiz_helper::STATE_APPROVED || $question->state == studentquiz_helper::STATE_DISAPPROVED) && + !has_capability('mod/studentquiz:previewothers', $this->qbank->get_most_specific_context())) { + // Do not render Edit icon if Question is in approved/disapproved state for Student. + return [null, null, null]; + } + + return parent::get_url_icon_and_label($question); + } + +} diff --git a/classes/question/bank/sq_edit_menu_column.php b/classes/question/bank/sq_edit_menu_column.php index 19553967..5e9b8237 100644 --- a/classes/question/bank/sq_edit_menu_column.php +++ b/classes/question/bank/sq_edit_menu_column.php @@ -27,12 +27,75 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class sq_edit_menu_column extends edit_menu_column { + + /** @var int */ + protected $currentuserid; + + /** + * 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 { + global $OUTPUT; + $actions = $this->qbank->get_question_actions(); + + $menu = new \action_menu(); + $menu->set_menu_trigger(get_string('edit')); + foreach ($actions as $action) { + $action = $action->get_action_menu_link($question); + if ($action) { + $menu->add($action); + } + } + + echo $OUTPUT->render($menu); + } + + /** + * Initialise Parameters for join + */ + protected function init(): void { + global $USER; + $this->currentuserid = $USER->id; + parent::init(); + } + + /** + * Return an array 'table_alias' => 'JOIN clause' to bring in any data that + * this column required. + * + * The return values for all the columns will be checked. It is OK if two + * columns join in the same table with the same alias and identical JOIN clauses. + * If to columns try to use the same alias with different joins, you get an error. + * The only table included by default is the question table, which is aliased to 'q'. + * + * It is important that your join simply adds additional data (or NULLs) to the + * existing rows of the query. It must not cause additional rows. + * + * @return array 'table_alias' => 'JOIN clause' + */ + public function get_extra_joins(): array { + $hidden = "sqq.hidden = 0"; + $mine = "q.createdby = $this->currentuserid"; + + // Without permission, a user can only see non-hidden question or its their own. + $sqlextra = "AND ($hidden OR $mine)"; + if (has_capability('mod/studentquiz:previewothers', $this->qbank->get_most_specific_context())) { + $sqlextra = ""; + } + + return ['sqh' => "JOIN {studentquiz_question} sqh ON sqh.id = qr.itemid $sqlextra"]; + } + /** - * Title for this column. + * Required columns * - * @return string Title of column + * @return array fields required. 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_title() { - return get_string('actions'); + public function get_required_fields(): array { + return ['sqq.hidden AS sq_hidden']; } } diff --git a/classes/question/bank/sq_hidden_action.php b/classes/question/bank/sq_hidden_action.php new file mode 100644 index 00000000..1409446a --- /dev/null +++ b/classes/question/bank/sq_hidden_action.php @@ -0,0 +1,91 @@ +. + +namespace mod_studentquiz\bank; + +use core_question\local\bank\question_action_base; + +/** + * Represent sq_hiden action in studentquiz_bank_view + * + * @package mod_studentquiz + * @author Huong Nguyen + * @copyright 2019 HSR (http://www.hsr.ch) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class sq_hidden_action extends question_action_base { + + /** @var int */ + protected $currentuserid; + + /** + * Initialise Parameters for join + */ + protected function init(): void { + global $USER; + $this->currentuserid = $USER->id; + parent::init(); + } + + /** + * Column name + * + * @return string 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() { + return 'sq_hidden'; + } + + /** + * Override method to get url and label for show/hidden action of the studentquiz. + * + * @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. + * $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 { + $courseid = $this->qbank->get_courseid(); + $cmid = $this->qbank->cm->id; + if (has_capability('mod/studentquiz:previewothers', $this->qbank->get_most_specific_context())) { + if (!empty($question->sq_hidden)) { + $url = new \moodle_url('/mod/studentquiz/hideaction.php', + ['studentquizquestionid' => $question->studentquizquestionid, 'sesskey' => sesskey(), + 'courseid' => $courseid, 'hide' => 0, 'cmid' => $cmid, 'returnurl' => $this->qbank->base_url()]); + return [$url, 't/show', get_string('show')]; + } else { + $url = new \moodle_url('/mod/studentquiz/hideaction.php', + ['studentquizquestionid' => $question->studentquizquestionid, 'sesskey' => sesskey(), + 'courseid' => $courseid, 'hide' => 1, 'cmid' => $cmid, 'returnurl' => $this->qbank->base_url()]); + return [$url, 't/hide', get_string('hide')]; + } + } + + return [null, null, null]; + } + + /** + * Required columns + * + * @return array fields required. 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 { + return ['sqq.hidden AS sq_hidden']; + } +} diff --git a/classes/question/bank/sq_pin_action.php b/classes/question/bank/sq_pin_action.php new file mode 100644 index 00000000..6c4d8fc7 --- /dev/null +++ b/classes/question/bank/sq_pin_action.php @@ -0,0 +1,96 @@ +. + + +namespace mod_studentquiz\bank; + +use core_question\local\bank\question_action_base; +use moodle_url; + +/** + * Represent pin action in studentquiz_bank_view + * + * @package mod_studentquiz + * @copyright 2021 The Open University. + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class sq_pin_action extends question_action_base { + /** @var mod_studentquiz Renderer of student quiz. */ + protected $renderer; + + protected $currentuserid; + + /** + * Init method. + */ + protected function init(): void { + global $USER, $PAGE; + $this->currentuserid = $USER->id; + $this->renderer = $PAGE->get_renderer('mod_studentquiz'); + } + + /** + * Get the internal name for this column. + * + * @return string Column name. + */ + public function get_name() { + return 'pin_toggle'; + } + + /** + * Get required fields. + * + * @return array Fields required. + */ + public function get_required_fields(): array { + return array('sqq.pinned AS pinned'); + } + + + /** + * Override method to get url and label for pin action of the studentquiz. + * + * @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. + * $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 { + $output = ''; + $courseid = $this->qbank->get_courseid(); + $cmid = $this->qbank->cm->id; + if (has_capability('mod/studentquiz:pinquestion', $this->qbank->get_most_specific_context())) { + if ($question->pinned) { + $url = new moodle_url('/mod/studentquiz/pinaction.php', + ['studentquizquestionid' => $question->studentquizquestionid, + 'pin' => 0, 'sesskey' => sesskey(), 'cmid' => $cmid, + 'returnurl' => $this->qbank->base_url(), 'courseid' => $courseid]); + return [$url, 'i/star', get_string('unpin', 'studentquiz'), 'courseid' => $courseid]; + } else { + $url = new moodle_url('/mod/studentquiz/pinaction.php', + ['studentquizquestionid' => $question->studentquizquestionid, + 'pin' => 1, 'sesskey' => sesskey(), 'cmid' => $cmid, + 'returnurl' => $this->qbank->base_url(), 'courseid' => $courseid]); + return [$url, 't/emptystar', get_string('pin', 'studentquiz')]; + } + } + + return [null, null, null]; + } + +} diff --git a/classes/question/bank/sq_preview_action.php b/classes/question/bank/sq_preview_action.php new file mode 100644 index 00000000..99f09619 --- /dev/null +++ b/classes/question/bank/sq_preview_action.php @@ -0,0 +1,77 @@ +. + +namespace mod_studentquiz\bank; + +/** + * A action type for preview link to mod_studentquiz_preview + * + * @package mod_studentquiz + * @copyright 2017 HSR (http://www.hsr.ch) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class sq_preview_action extends \qbank_previewquestion\preview_action { + + /** + * Renderer + * @var stdClass + */ + protected $renderer; + + /** @var stdClass */ + protected $context; + + /** @var string */ + protected $previewtext; + + /** + * Loads config of current userid and can see + */ + public function init(): void { + global $PAGE; + $this->renderer = $PAGE->get_renderer('mod_studentquiz'); + $this->context = $this->qbank->get_most_specific_context(); + $this->previewtext = get_string('preview'); + } + + /** + * Look up if current user is allowed to preview this question + * @param object $question The current question object + * @return boolean + */ + private function can_preview($question) { + global $USER; + return ($question->createdby == $USER->id) || has_capability('mod/studentquiz:previewothers', $this->context); + } + + /** + * Override this function and return the appropriate action menu link, or null if it does not apply to this question. + * + * @param \stdClass $question Data about the question being displayed in this row. + * @return \action_menu_link|null The action, if applicable to this question. + */ + public function get_action_menu_link(\stdClass $question): ?\action_menu_link { + if ($this->can_preview($question)) { + $params = ['cmid' => $this->context->instanceid, 'studentquizquestionid' => $question->studentquizquestionid]; + $link = new \moodle_url('/mod/studentquiz/preview.php', $params); + + return new \action_menu_link_secondary($link, new \pix_icon('t/preview', ''), + $this->previewtext, ['target' => 'questionpreview']); + } + + return null; + } +} diff --git a/classes/question/bank/state_pin_column.php b/classes/question/bank/state_pin_column.php index ca29f045..de3e382d 100644 --- a/classes/question/bank/state_pin_column.php +++ b/classes/question/bank/state_pin_column.php @@ -26,10 +26,13 @@ * @copyright 2021 The Open University. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class state_pin_column extends action_column_base { +class state_pin_column extends \core_question\local\bank\column_base { /** @var mod_studentquiz Renderer of student quiz. */ protected $renderer; + /** @var int current user id*/ + protected $currentuserid; + /** * Init method. */ @@ -57,6 +60,15 @@ public function get_title(): string { return ''; } + /** + * Get required fields. + * + * @return array Fields required. + */ + public function get_required_fields(): array { + return array('sqq.pinned AS pinned'); + } + /** * Output the contents of this column. * diff --git a/classes/question/bank/studentquiz_bank_view.php b/classes/question/bank/studentquiz_bank_view.php index 605b26fc..6af0f8fd 100755 --- a/classes/question/bank/studentquiz_bank_view.php +++ b/classes/question/bank/studentquiz_bank_view.php @@ -29,6 +29,8 @@ use mod_studentquiz\utils; use stdClass; use core_question\local\bank\question_version_status; +use qbank_managecategories\category_condition; +use core_question\local\bank\column_manager_base; defined('MOODLE_INTERNAL') || die(); @@ -42,15 +44,15 @@ require_once(__DIR__ . '/attempts_column.php'); require_once(__DIR__ . '/comments_column.php'); require_once(__DIR__ . '/state_column.php'); +require_once(__DIR__ . '/state_pin_column.php'); require_once(__DIR__ . '/anonym_creator_name_column.php'); -require_once(__DIR__ . '/preview_column.php'); require_once(__DIR__ . '/question_name_column.php'); -require_once(__DIR__ . '/sq_hidden_action_column.php'); -require_once(__DIR__ . '/sq_edit_action_column.php'); -require_once(__DIR__ . '/sq_pin_action_column.php'); -require_once(__DIR__ . '/state_pin_column.php'); +require_once(__DIR__ . '/sq_edit_action.php'); +require_once(__DIR__ . '/sq_preview_action.php'); +require_once(__DIR__ . '/sq_delete_action.php'); +require_once(__DIR__ . '/sq_hidden_action.php'); +require_once(__DIR__ . '/sq_pin_action.php'); require_once(__DIR__ . '/sq_edit_menu_column.php'); -require_once(__DIR__ . '/sq_delete_action_column.php'); /** * Module instance settings form @@ -115,7 +117,7 @@ class studentquiz_bank_view extends \core_question\local\bank\view { /** * @var mixed */ - private $pagevars; + protected $pagevars; /** * @var stdClass StudentQuiz renderer. @@ -139,18 +141,17 @@ class studentquiz_bank_view extends \core_question\local\bank\view { public function __construct($contexts, $pageurl, $course, $cm, $studentquiz, $pagevars, $report) { $this->set_filter_post_data(); global $USER, $PAGE; - $this->pagevars = $pagevars; $this->studentquiz = $studentquiz; $this->userid = $USER->id; $this->report = $report; - parent::__construct($contexts, $pageurl, $course, $cm); + parent::__construct($contexts, $pageurl, $course, $cm, $pagevars); + $this->set_filter_form_fields($this->is_anonymized()); $this->initialize_filter_form($pageurl); $currentgroup = groups_get_activity_group($cm, true); $this->currentgroupjoinsql = utils::groups_get_questions_joins($currentgroup, 'sqq.groupid'); // Init search conditions with filterform state. - $categorycondition = new \core_question\bank\search\category_condition( - $pagevars['cat'], $pagevars['recurse'], $contexts, $pageurl, $course); + $categorycondition = new category_condition($this); $studentquizcondition = new \mod_studentquiz\condition\studentquiz_condition($cm, $this->filterform, $this->report, $studentquiz); $this->isfilteractive = $studentquizcondition->is_filter_active(); @@ -160,49 +161,23 @@ public function __construct($contexts, $pageurl, $course, $cm, $studentquiz, $pa /** * Shows the question bank interface. - * - * The function also processes a number of actions: - * - * Actions affecting the question pool: - * move Moves a question to a different category - * deleteselected Deletes the selected questions from the category - * Other actions: - * category Chooses the category - * params: $tabname question bank edit tab name, for permission checking - * $pagevars current list of page variables - * - * @param array $pagevars - * @param string $tabname */ - public function display($pagevars, $tabname): void { - $page = $pagevars['qpage']; - $perpage = $pagevars['qperpage']; - $cat = $pagevars['cat']; - $recurse = $pagevars['recurse']; - $showhidden = $pagevars['showhidden']; - $showquestiontext = $pagevars['qbshowtext']; - $tagids = []; - if (!empty($pagevars['qtagids'])) { - $tagids = $pagevars['qtagids']; - } + public function display(): void { $output = ''; $this->build_query(); // Get result set. - $questions = $this->load_questions($page, $perpage); + $questions = $this->load_questions(); $this->questions = $questions; - $this->countsql = count($this->questions); - if ($this->countsql || $this->isfilteractive) { + if ($this->totalnumber || $this->isfilteractive) { // We're unable to force the filter form to submit with get method. We have 2 forms on the page // which need to interact with each other, so forcing method as get here. $output .= str_replace('method="post"', 'method="get"', $this->renderer->render_filter_form($this->filterform)); } echo $output; - if ($this->countsql > 0) { - $this->display_question_list($this->baseurl, $cat, null, $page, $perpage, - $this->contexts->having_cap('moodle/question:add') - ); + if ($this->totalnumber > 0) { + $this->display_question_list(); } else { list($message, $questionsubmissionallow) = mod_studentquiz_check_availability($this->studentquiz->opensubmissionfrom, $this->studentquiz->closesubmissionfrom, 'submission'); @@ -225,11 +200,30 @@ public function get_questions() { */ protected function default_sort(): array { return [ - 'mod_studentquiz\bank\anonym_creator_name_column-timecreated' => -1, - 'mod_studentquiz\bank\question_name_column' => 1, + 'mod_studentquiz__bank__anonym_creator_name_column-timecreated' => SORT_DESC, + 'mod_studentquiz__bank__question_name_column' => SORT_ASC, ]; } + public function new_sort_url($sortname, $newsortreverse): string { + // Due to the way sorting param name change in Moodle 4.3. + // We need to override this so we can remove all sort params in the url. + // So that when we run the new_sort_url function, our sort name always be the first param in the url. + // Example: 4.2, we have a default sorting ['qb1' => 'columnA', 'qb2' => 'columnB']. + // After we run the new_sort_url function, it will return ['qb1' => 'columnC', 'qb2' => 'columnA', 'qb3' => 'columnB']. + // But in 4.3, each column is unique key, so we can't override the param like that. + // Example: ['columnA' => 3, 'columnB' => 4]. + // We want our columnC to be move the become the first element of the sorting array. + // The simple way is just remove all existing sorting param in the baseurl, so when we running the new_sort_url function. + // It will return like this ['columnC' => 4, 'columnA' => 3, 'columnB' => 4]. + foreach ($this->baseurl->params() as $paramname => $value) { + if (strpos($paramname, 'sortdata') !== false) { + $this->baseurl->remove_params($paramname); + } + } + return parent::new_sort_url($sortname, $newsortreverse); + } + /** * Create the SQL query to retrieve the indicated questions, based on * \core_question\local\bank\search\condition filters. @@ -262,6 +256,7 @@ protected function build_query(): void { 'qv.status', 'q.timecreated', 'q.createdby', + 'qc.contextid', ]; // Only show ready and draft question. $tests = [ @@ -289,7 +284,7 @@ protected function build_query(): void { $sorts = array(); foreach ($this->sort as $sort => $order) { list($colname, $subsort) = $this->parse_subsort($sort); - $sorts[] = $this->requiredcolumns[$colname]->sort_expression($order < 0, $subsort); + $sorts[] = $this->requiredcolumns[$colname]->sort_expression($order === SORT_DESC, $subsort); } // Build the where clause and load params from search conditions. @@ -307,6 +302,7 @@ protected function build_query(): void { $sql = ' FROM {question} q ' . implode(' ', $joins); $sql .= ' WHERE ' . implode(' AND ', $tests); $this->sqlparams = $params; + $this->countsql = 'SELECT count(1)' . $sql; $this->loadsql = 'SELECT ' . implode(', ', $fields) . $sql . ' ORDER BY ' . implode(', ', $sorts); } @@ -329,7 +325,9 @@ public function create_new_question_form($categoryid, $canadd): void { $output = ''; $caption = get_string('createnewquestion', 'studentquiz'); - + if (is_object($categoryid)) { + $categoryid = $categoryid->id; + } if ($canadd) { $returnurl = $this->baseurl; $params = array( @@ -365,27 +363,22 @@ public function create_new_question_form($categoryid, $canadd): void { /** * Prints the table of questions in a category with interactions - * - * @param \moodle_url $pageurl The URL to reload this page. - * @param string $categoryandcontext 'categoryID,contextID'. - * @param int $recurse Whether to include subcategories. - * @param int $page The number of the page to be displayed - * @param int $perpage Number of questions to show per page - * @param array $addcontexts contexts where the user is allowed to add new questions. */ - protected function display_question_list($pageurl, $categoryandcontext, $recurse = 1, $page = 0, - $perpage = 100, $addcontexts = []): void { + public function display_question_list(): void { $output = ''; - $category = $this->get_current_category($categoryandcontext); - list($categoryid, $contextid) = explode(',', $categoryandcontext); + [$categoryid, $contextid] = category_condition::validate_category_param($this->pagevars['cat']); + $category = category_condition::get_category_record($categoryid, $contextid); $catcontext = \context::instance_by_id($contextid); + $page = $this->get_pagevars('qpage'); + $perpage = $this->get_pagevars('qperpage'); + $addcontexts = $this->contexts->having_cap('moodle/question:add'); $output .= \html_writer::start_tag('fieldset', array('class' => 'invisiblefieldset', 'style' => 'display:block;')); $output .= $this->renderer->render_hidden_field($this->cm->id, $this->baseurl, $perpage); $output .= $this->renderer->render_control_buttons($catcontext, $this->has_questions_in_category(), - $addcontexts, $category); + $addcontexts, $category, $this->get_pagevars('filter')); $output .= $this->renderer->render_pagination_bar($this->pagevars, $this->baseurl, $this->totalnumber, $page, $perpage, true); @@ -396,7 +389,7 @@ protected function display_question_list($pageurl, $categoryandcontext, $recurse $perpage, false); $output .= $this->renderer->render_control_buttons($catcontext, $this->has_questions_in_category(), - $addcontexts, $category); + $addcontexts, $category, $this->get_pagevars('filter')); $output .= \html_writer::end_tag('fieldset'); $output = $this->renderer->render_question_form($output); @@ -572,12 +565,13 @@ private function initialize_filter_form($pageurl) { /** * Load question from database - * @param int $page - * @param int $perpage - * @return paginated array of questions + * + * @return array array of questions */ - private function load_questions($page, $perpage) { + public function load_questions() { global $DB; + $page = $this->get_pagevars('qpage'); + $perpage = $this->get_pagevars('qperpage'); $rs = $DB->get_recordset_sql($this->loadsql, $this->sqlparams); $counterquestions = 0; @@ -678,4 +672,31 @@ protected function wanted_columns(): array { $this->requiredcolumns = $renderer->get_columns_for_question_bank_view($this); return $this->requiredcolumns; } + + /** + * Allow qbank plugins to override the column manager. + * + * If multiple qbank plugins define a column manager, this will pick the first one sorted alphabetically. + * + * @return void + */ + protected function init_column_manager(): void { + $this->columnmanager = new column_manager_base(); + } + + /** + * Initialise list of menu actions specific for SQ. + * + * @return void + */ + protected function init_question_actions(): void { + $this->questionactions = [ + new \mod_studentquiz\bank\sq_edit_action($this), + new \mod_studentquiz\bank\sq_preview_action($this), + new \mod_studentquiz\bank\sq_delete_action($this), + new \mod_studentquiz\bank\sq_hidden_action($this), + new \mod_studentquiz\bank\sq_pin_action($this), + ]; + + } } diff --git a/classes/question/bank/studentquiz_column_base.php b/classes/question/bank/studentquiz_column_base.php index c81119db..a7028e95 100644 --- a/classes/question/bank/studentquiz_column_base.php +++ b/classes/question/bank/studentquiz_column_base.php @@ -51,4 +51,14 @@ public function display($question, $rowclasses): void { public function get_extra_classes():array { return $this->extraclasses; } + + /** + * Required columns + * + * @return array fields required. 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 { + return ['sqq.hidden AS sq_hidden']; + } } diff --git a/classes/question/bank/tag_column.php b/classes/question/bank/tag_column.php index ac330eac..636257f6 100644 --- a/classes/question/bank/tag_column.php +++ b/classes/question/bank/tag_column.php @@ -47,6 +47,9 @@ class tag_column extends studentquiz_column_base { */ protected $renderer; + /** @var int category id */ + protected $categoryid; + /** * Initialise Parameters for join */ diff --git a/classes/utils.php b/classes/utils.php index 9c39a8b3..34f99aef 100644 --- a/classes/utils.php +++ b/classes/utils.php @@ -497,9 +497,10 @@ public static function mark_question_comment_current_active_tab(&$tabs, $private if (!$found) { $tabs[0]['active'] = true; } - - // Allow user to update user preference via ajax. - user_preference_allow_ajax_update(self::USER_PREFERENCE_QUESTION_ACTIVE_TAB, PARAM_TEXT); + if (self::moodle_version_is("<=", "42")) { + // Allow user to update user preference via ajax. + user_preference_allow_ajax_update(self::USER_PREFERENCE_QUESTION_ACTIVE_TAB, PARAM_TEXT); + } } /** @@ -556,7 +557,7 @@ public static function can_view_state_history($cmid, $question) { public static function get_question_state($studentquizquestion) { global $DB; - return $DB->get_field('studentquiz_question', 'state', ['id' => $studentquizquestion->id]); + return $DB->get_field('studentquiz_question', 'state', ['id' => $studentquizquestion->get_id()]); } /** diff --git a/lib.php b/lib.php index d5d778cc..af1b66c6 100755 --- a/lib.php +++ b/lib.php @@ -658,3 +658,18 @@ function studentquiz_questions_in_use(array $questionids): bool { return question_engine::questions_in_use($questionids, new qubaid_join('{studentquiz_attempt} sa', 'sa.questionusageid')); } + +/** + * Implements callback user_preferences, lists preferences that users are allowed to update directly + * + * Used in {@see core_user::fill_preferences_cache()}, see also {@see useredit_update_user_preference()} + * + * @return array + */ +function mod_studentquiz_user_preferences() { + return [utils::USER_PREFERENCE_QUESTION_ACTIVE_TAB => [ + 'type' => PARAM_TEXT, + 'null' => NULL_NOT_ALLOWED, + 'default' => utils::COMMENT_TYPE_PRIVATE] + ]; +} diff --git a/renderer.php b/renderer.php index 55cefe20..94fa02fe 100755 --- a/renderer.php +++ b/renderer.php @@ -524,7 +524,9 @@ public function render_difficulty_level_column($question, $rowclasses) { $mydifficultytitle = get_string('mydifficulty_column_name', 'studentquiz'); $title = ""; if (!empty($question->difficultylevel) || !empty($question->mydifficulty)) { - $title = $difficultytitle . ': ' . (100 * round($question->difficultylevel, 2)) . '% '; + if (!empty($question->difficultylevel)) { + $title = $difficultytitle . ': ' . (100 * round($question->difficultylevel, 2)) . '% '; + } if (!empty($question->mydifficulty)) { $title .= ', ' . $mydifficultytitle . ': ' . (100 * round($question->mydifficulty, 2)) . '%'; } else { @@ -919,6 +921,30 @@ public function render_report_more_link($url) { * @return array */ public function get_columns_for_question_bank_view(mod_studentquiz\question\bank\studentquiz_bank_view $view) { + return [ + new core_question\local\bank\checkbox_column($view), + new qbank_viewquestiontype\question_type_column($view), + new \mod_studentquiz\bank\state_column($view), + new \mod_studentquiz\bank\state_pin_column($view), + new \mod_studentquiz\bank\question_name_column($view), + new \mod_studentquiz\bank\sq_edit_menu_column($view), + new qbank_history\version_number_column($view), + new \mod_studentquiz\bank\anonym_creator_name_column($view), + new \mod_studentquiz\bank\tag_column($view), + new \mod_studentquiz\bank\attempts_column($view), + new \mod_studentquiz\bank\difficulty_level_column($view), + new \mod_studentquiz\bank\rate_column($view), + new \mod_studentquiz\bank\comment_column($view), + ]; + } + + /** + * Get all the required columns for StudentQuiz view. + * + * @param mod_studentquiz\question\bank\studentquiz_bank_view_pre_43 $view + * @return array + */ + public function get_columns_for_question_bank_view_pre_43(mod_studentquiz\question\bank\studentquiz_bank_view_pre_43 $view) { return [ new core_question\local\bank\checkbox_column($view), new qbank_viewquestiontype\question_type_column($view), @@ -930,7 +956,7 @@ public function get_columns_for_question_bank_view(mod_studentquiz\question\bank new \mod_studentquiz\bank\sq_delete_action_column($view), new \mod_studentquiz\bank\sq_hidden_action_column($view), new \mod_studentquiz\bank\sq_pin_action_column($view), - new \mod_studentquiz\bank\sq_edit_menu_column($view), + new \mod_studentquiz\bank\sq_edit_menu_column_pre_43($view), new qbank_history\version_number_column($view), new \mod_studentquiz\bank\anonym_creator_name_column($view), new \mod_studentquiz\bank\tag_column($view), @@ -1198,9 +1224,10 @@ function createBoltBar(mine, average) { * @param bool $hasquestionincategory * @param mixed $addcontexts * @param stdClass $category + * @param array $filter * @return string */ - public function render_control_buttons($catcontext, $hasquestionincategory, $addcontexts, $category) { + public function render_control_buttons($catcontext, $hasquestionincategory, $addcontexts, $category, array $filter = []) { global $COURSE; $output = ''; @@ -1216,8 +1243,16 @@ public function render_control_buttons($catcontext, $hasquestionincategory, $add $studentquiz->openansweringfrom, $studentquiz->closeansweringfrom, 'answering'); $deleteurl = new \moodle_url('/question/bank/deletequestion/delete.php', ['courseid' => $COURSE->id, 'returnurl' => $this->page->url]); + + // Due to Moodle 4.3 changes. + // We need a filter param in moveurl. + $returnmoveurl = $this->page->url; + if ($filter) { + $returnmoveurl->param('filter', json_encode($filter)); + } $movetourl = new \moodle_url('/question/bank/bulkmove/move.php', ['courseid' => $COURSE->id, - 'returnurl' => $this->page->url]); + 'returnurl' => $returnmoveurl]); + $changestateurl = new \moodle_url('/mod/studentquiz/changestate.php', ['courseid' => $COURSE->id, 'returnurl' => $this->page->url]); if ($hasquestionincategory) { diff --git a/reportlib.php b/reportlib.php index c777d8bc..b5d135b8 100755 --- a/reportlib.php +++ b/reportlib.php @@ -98,6 +98,8 @@ public function get_enrolled_users() { /** @var stdClass */ protected $studentquizstats; + protected $questionstats; + /** * Overall Stats of the studentquiz * @return stdClass diff --git a/styles.css b/styles.css index d690ebe3..6e4e4b56 100644 --- a/styles.css +++ b/styles.css @@ -194,6 +194,25 @@ font-weight: normal; } +.path-mod-studentquiz #categoryquestions th { + font-weight: 400; +} + +.path-mod-studentquiz #categoryquestions td { + vertical-align: inherit; +} + +.path-mod-studentquiz #categoryquestions td, +.path-mod-studentquiz #categoryquestions th { + padding: 0.2em; +} + +.path-mod-studentquiz .question-bank-table td.modifiername span.date, +.path-mod-studentquiz .question-bank-table td.creatorname span.date { + font-weight: normal; + font-size: 0.8em; +} + .path-mod-studentquiz #categoryquestions td.questionname, .path-mod-studentquiz #categoryquestions td.creatorname { white-space: normal; diff --git a/tests/bank_performance_test.php b/tests/bank_performance_test.php index a0631a45..23977224 100644 --- a/tests/bank_performance_test.php +++ b/tests/bank_performance_test.php @@ -17,11 +17,17 @@ namespace mod_studentquiz; use mod_studentquiz\question\bank\studentquiz_bank_view; +use mod_studentquiz\question\bank\studentquiz_bank_view_pre_43; +use mod_studentquiz\utils; defined('MOODLE_INTERNAL') || die(); global $CFG; -require_once($CFG->dirroot . '/mod/studentquiz/classes/question/bank/studentquiz_bank_view.php'); +if (utils::moodle_version_is("<=", "42")) { + require_once($CFG->dirroot . '/mod/studentquiz/classes/question/bank/legacy/studentquiz_bank_view_pre_43.php'); +} else { + require_once($CFG->dirroot . '/mod/studentquiz/classes/question/bank/studentquiz_bank_view.php'); +} require_once($CFG->dirroot . '/mod/studentquiz/reportlib.php'); require_once($CFG->dirroot . '/lib/questionlib.php'); require_once($CFG->dirroot . '/question/editlib.php'); @@ -85,10 +91,17 @@ public function run_questionbank($result) { ); $report = new mod_studentquiz_report($result['cm']->id); - $questionbank = new studentquiz_bank_view( - new \core_question\local\bank\question_edit_contexts(\context_module::instance($result['cm']->id)), - new moodle_url('/mod/studentquiz/view.php', array('cmid' => $result['cm']->id)), - $result['course'], $result['cm'], $result['studentquiz'], $pagevars, $report); + if (utils::moodle_version_is("<=", "42")) { + $questionbank = new studentquiz_bank_view_pre_43( + new \core_question\local\bank\question_edit_contexts(\context_module::instance($result['cm']->id)), + new moodle_url('/mod/studentquiz/view.php', array('cmid' => $result['cm']->id)), + $result['course'], $result['cm'], $result['studentquiz'], $pagevars, $report); + } else { + $questionbank = new studentquiz_bank_view( + new \core_question\local\bank\question_edit_contexts(\context_module::instance($result['cm']->id)), + new moodle_url('/mod/studentquiz/view.php', array('cmid' => $result['cm']->id)), + $result['course'], $result['cm'], $result['studentquiz'], $pagevars, $report); + } return $questionbank; } diff --git a/tests/bank_view_test.php b/tests/bank_view_test.php index ead48a9e..856d03bc 100644 --- a/tests/bank_view_test.php +++ b/tests/bank_view_test.php @@ -18,11 +18,16 @@ use mod_studentquiz\local\studentquiz_question; use mod_studentquiz\question\bank\studentquiz_bank_view; +use mod_studentquiz\question\bank\studentquiz_bank_view_pre_43; defined('MOODLE_INTERNAL') || die(); global $CFG; -require_once($CFG->dirroot . '/mod/studentquiz/classes/question/bank/studentquiz_bank_view.php'); +if (utils::moodle_version_is("<=", "42")) { + require_once($CFG->dirroot . '/mod/studentquiz/classes/question/bank/legacy/studentquiz_bank_view_pre_43.php'); +} else { + require_once($CFG->dirroot . '/mod/studentquiz/classes/question/bank/studentquiz_bank_view.php'); +} require_once($CFG->dirroot . '/mod/studentquiz/reportlib.php'); require_once($CFG->dirroot . '/lib/questionlib.php'); require_once($CFG->dirroot . '/question/editlib.php'); @@ -107,16 +112,29 @@ public function run_questionbank() { 'cat' => $this->cat->id . ',' . $this->ctx->id, 'showall' => 0, 'showallprinted' => 0, + 'tabname' => 'questions', + 'qperpage' => 100, + 'qpage' => 0, ); $report = new \mod_studentquiz_report($this->cm->id); - $questionbank = new studentquiz_bank_view( + if (utils::moodle_version_is("<=", "42")) { + $questionbank = new studentquiz_bank_view_pre_43( new \core_question\local\bank\question_edit_contexts(\context_module::instance($this->cm->id)) , new \moodle_url('/mod/studentquiz/view.php', array('cmid' => $this->cm->id)) , $this->course , $this->cm , $this->studentquiz , $pagevars, $report); + } else { + $questionbank = new studentquiz_bank_view( + new \core_question\local\bank\question_edit_contexts(\context_module::instance($this->cm->id)) + , new \moodle_url('/mod/studentquiz/view.php', array('cmid' => $this->cm->id)) + , $this->course + , $this->cm + , $this->studentquiz + , $pagevars, $report); + } return $questionbank; } @@ -154,29 +172,49 @@ public function test_wanted_columns() { $this->resetAfterTest(true); $questionbank = $this->run_questionbank(); - $reflector = new \ReflectionClass('mod_studentquiz\question\bank\studentquiz_bank_view'); + $below42 = utils::moodle_version_is("<=", "42"); + if ($below42) { + $reflector = new \ReflectionClass('mod_studentquiz\question\bank\studentquiz_bank_view_pre_43'); + } else { + $reflector = new \ReflectionClass('mod_studentquiz\question\bank\studentquiz_bank_view'); + } $method = $reflector->getMethod('wanted_columns'); $method->setAccessible(true); $requiredcolumns = $method->invokeArgs($questionbank, [$questionbank]); - - $this->assertInstanceOf('core_question\local\bank\checkbox_column', $requiredcolumns[0]); - $this->assertInstanceOf('qbank_viewquestiontype\question_type_column', $requiredcolumns[1]); - $this->assertInstanceOf('mod_studentquiz\bank\state_column', $requiredcolumns[2]); - $this->assertInstanceOf('mod_studentquiz\bank\state_pin_column', $requiredcolumns[3]); - $this->assertInstanceOf('mod_studentquiz\bank\question_name_column', $requiredcolumns[4]); - $this->assertInstanceOf('mod_studentquiz\bank\sq_edit_action_column', $requiredcolumns[5]); - $this->assertInstanceOf('mod_studentquiz\bank\preview_column', $requiredcolumns[6]); - $this->assertInstanceOf('mod_studentquiz\bank\sq_delete_action_column', $requiredcolumns[7]); - $this->assertInstanceOf('mod_studentquiz\bank\sq_hidden_action_column', $requiredcolumns[8]); - $this->assertInstanceOf('mod_studentquiz\bank\sq_pin_action_column', $requiredcolumns[9]); - $this->assertInstanceOf('mod_studentquiz\bank\sq_edit_menu_column', $requiredcolumns[10]); - $this->assertInstanceOf('qbank_history\version_number_column', $requiredcolumns[11]); - $this->assertInstanceOf('mod_studentquiz\bank\anonym_creator_name_column', $requiredcolumns[12]); - $this->assertInstanceOf('mod_studentquiz\bank\tag_column', $requiredcolumns[13]); - $this->assertInstanceOf('mod_studentquiz\bank\attempts_column', $requiredcolumns[14]); - $this->assertInstanceOf('mod_studentquiz\bank\difficulty_level_column', $requiredcolumns[15]); - $this->assertInstanceOf('mod_studentquiz\bank\rate_column', $requiredcolumns[16]); - $this->assertInstanceOf('mod_studentquiz\bank\comment_column', $requiredcolumns[17]); + if ($below42) { + $this->assertInstanceOf('core_question\local\bank\checkbox_column', $requiredcolumns[0]); + $this->assertInstanceOf('qbank_viewquestiontype\question_type_column', $requiredcolumns[1]); + $this->assertInstanceOf('mod_studentquiz\bank\state_column', $requiredcolumns[2]); + $this->assertInstanceOf('mod_studentquiz\bank\state_pin_column', $requiredcolumns[3]); + $this->assertInstanceOf('mod_studentquiz\bank\question_name_column', $requiredcolumns[4]); + $this->assertInstanceOf('mod_studentquiz\bank\sq_edit_action_column', $requiredcolumns[5]); + $this->assertInstanceOf('mod_studentquiz\bank\preview_column', $requiredcolumns[6]); + $this->assertInstanceOf('mod_studentquiz\bank\sq_delete_action_column', $requiredcolumns[7]); + $this->assertInstanceOf('mod_studentquiz\bank\sq_hidden_action_column', $requiredcolumns[8]); + $this->assertInstanceOf('mod_studentquiz\bank\sq_pin_action_column', $requiredcolumns[9]); + $this->assertInstanceOf('mod_studentquiz\bank\sq_edit_menu_column_pre_43', $requiredcolumns[10]); + $this->assertInstanceOf('qbank_history\version_number_column', $requiredcolumns[11]); + $this->assertInstanceOf('mod_studentquiz\bank\anonym_creator_name_column', $requiredcolumns[12]); + $this->assertInstanceOf('mod_studentquiz\bank\tag_column', $requiredcolumns[13]); + $this->assertInstanceOf('mod_studentquiz\bank\attempts_column', $requiredcolumns[14]); + $this->assertInstanceOf('mod_studentquiz\bank\difficulty_level_column', $requiredcolumns[15]); + $this->assertInstanceOf('mod_studentquiz\bank\rate_column', $requiredcolumns[16]); + $this->assertInstanceOf('mod_studentquiz\bank\comment_column', $requiredcolumns[17]); + } else { + $this->assertInstanceOf('core_question\local\bank\checkbox_column', $requiredcolumns[0]); + $this->assertInstanceOf('qbank_viewquestiontype\question_type_column', $requiredcolumns[1]); + $this->assertInstanceOf('mod_studentquiz\bank\state_column', $requiredcolumns[2]); + $this->assertInstanceOf('mod_studentquiz\bank\state_pin_column', $requiredcolumns[3]); + $this->assertInstanceOf('mod_studentquiz\bank\question_name_column', $requiredcolumns[4]); + $this->assertInstanceOf('\mod_studentquiz\bank\sq_edit_menu_column', $requiredcolumns[5]); + $this->assertInstanceOf('qbank_history\version_number_column', $requiredcolumns[6]); + $this->assertInstanceOf('mod_studentquiz\bank\anonym_creator_name_column', $requiredcolumns[7]); + $this->assertInstanceOf('mod_studentquiz\bank\tag_column', $requiredcolumns[8]); + $this->assertInstanceOf('mod_studentquiz\bank\attempts_column', $requiredcolumns[9]); + $this->assertInstanceOf('mod_studentquiz\bank\difficulty_level_column', $requiredcolumns[10]); + $this->assertInstanceOf('mod_studentquiz\bank\rate_column', $requiredcolumns[11]); + $this->assertInstanceOf('mod_studentquiz\bank\comment_column', $requiredcolumns[12]); + } } /** @@ -206,7 +244,7 @@ protected function create_random_questions($count, $userid) { protected function create_rate($sqq, $userid) { $raterecord = new \stdClass(); $raterecord->rate = 5; - $raterecord->studentquizquestionid = $sqq->id; + $raterecord->studentquizquestionid = $sqq->get_id(); $raterecord->userid = $userid; } @@ -217,7 +255,7 @@ protected function create_rate($sqq, $userid) { */ protected function create_comment($sqq, $userid) { $commentrecord = new \stdClass(); - $commentrecord->studentquizquestionid = $sqq->id; + $commentrecord->studentquizquestionid = $sqq->get_id(); $commentrecord->userid = $userid; $this->studentquizgenerator->create_comment($commentrecord); diff --git a/tests/behat/backup.feature b/tests/behat/backup.feature index d906e4b1..7539b247 100644 --- a/tests/behat/backup.feature +++ b/tests/behat/backup.feature @@ -10,7 +10,8 @@ Feature: Restore specific studentquiz old backup to test UI feature @javascript @_file_upload Scenario: Restore moodle backups containing history comments. Given I am on the "Course 1" "restore" page logged in as "admin" - And I press "Manage backup files" + # Main branch has change the text to "Manage course backups" so we should use xpath. + And I click on "(//*[@class='singlebutton']//button)[1]" "xpath_element" And I upload "mod/studentquiz/tests/fixtures/backup-moodle311-c1-historycomment.mbz" file to "Files" filemanager And I press "Save changes" And I restore "backup-moodle311-c1-historycomment.mbz" backup into a new course using this options: @@ -31,7 +32,8 @@ Feature: Restore specific studentquiz old backup to test UI feature @javascript @_file_upload @_switch_window Scenario: Restore moodle backups containing old StudentQuiz activity without state history table. Given I am on the "Course 1" "restore" page logged in as "admin" - And I press "Manage backup files" + # Main branch has change the text to "Manage course backups" so we should use xpath. + And I click on "(//*[@class='singlebutton']//button)[1]" "xpath_element" And I upload "mod/studentquiz/tests/fixtures/backup-moodle2-course-3-sqo-20211011-missing_state_history.mbz" file to "Files" filemanager And I press "Save changes" And I restore "backup-moodle2-course-3-sqo-20211011-missing_state_history.mbz" backup into a new course using this options: diff --git a/tests/behat/backup_restore_availability_setting.feature b/tests/behat/backup_restore_availability_setting.feature index c1df4e2d..75b4a6bf 100644 --- a/tests/behat/backup_restore_availability_setting.feature +++ b/tests/behat/backup_restore_availability_setting.feature @@ -14,7 +14,8 @@ Feature: Backup and restore activity studentquiz @javascript @_file_upload Scenario: Restore moodle backups containing old StudentQuiz activity has availability and question publishing setting. When I am on the "Course 1" "restore" page logged in as "admin" - And I press "Manage backup files" + # Main branch has change the text to "Manage course backups" so we should use xpath. + And I click on "(//*[@class='singlebutton']//button)[1]" "xpath_element" And I upload "mod/studentquiz/tests/fixtures/backup-moodle2-availability-setting.mbz" file to "Files" filemanager And I press "Save changes" And I restore "backup-moodle2-availability-setting.mbz" backup into a new course using this options: diff --git a/tests/behat/custom_completion.feature b/tests/behat/custom_completion.feature index bebbc194..0f44670f 100644 --- a/tests/behat/custom_completion.feature +++ b/tests/behat/custom_completion.feature @@ -21,15 +21,9 @@ Feature: Set a studentquiz to be marked complete when the student meets the cond @javascript Scenario: Check studentquiz mark done when the student meets the conditions of the completion point - Given I add a "StudentQuiz" to section "1" - When I set the following fields to these values: - | StudentQuiz Name | StudentQuiz 1 | - | Description | Test studentquiz description | - | completion | 2 | - | completionpointenabled | 1 | - | completionpoint | 10 | - And I press "Save and display" - And I log out + Given the following "activities" exist: + | course | activity | name | intro | completion | completionpointenabled | completionpoint | publishnewquestion | questionquantifier | + | C1 | studentquiz | StudentQuiz 1 | Test studentquiz description | 2 | 1 | 10 | 1 | 10 | # Create owned question by student role. And I am on the "StudentQuiz 1" "mod_studentquiz > View" page logged in as "student1" And I click on "Create new question" "button" @@ -43,15 +37,9 @@ Feature: Set a studentquiz to be marked complete when the student meets the cond @javascript Scenario: Check studentquiz mark done when the student meets the conditions of the completion created - Given I add a "StudentQuiz" to section "1" - When I set the following fields to these values: - | StudentQuiz Name | StudentQuiz 1 | - | Description | Test studentquiz description | - | completion | 2 | - | completionquestionpublishedenabled | 1 | - | completionquestionpublished | 2 | - And I press "Save and display" - And I log out + Given the following "activities" exist: + | course | activity | name | intro | completion | completionquestionpublishedenabled | completionquestionpublished | publishnewquestion | + | C1 | studentquiz | StudentQuiz 1 | Test studentquiz description | 2 | 1 | 2 | 1 | # Create owned question by student role. And I am on the "StudentQuiz 1" "mod_studentquiz > View" page logged in as "student1" And I click on "Create new question" "button" @@ -73,16 +61,9 @@ Feature: Set a studentquiz to be marked complete when the student meets the cond @javascript Scenario: Check studentquiz mark done when the student meets the conditions of the completion created approved - Given I add a "StudentQuiz" to section "1" - When I set the following fields to these values: - | StudentQuiz Name | StudentQuiz 1 | - | Description | Test studentquiz description | - | completion | 2 | - | publishnewquestion | 1 | - | completionquestionapprovedenabled | 1 | - | completionquestionapproved | 1 | - And I press "Save and display" - And I log out + Given the following "activities" exist: + | course | activity | name | intro | completion | completionquestionapprovedenabled | completionquestionapproved | publishnewquestion | approvedquantifier | + | C1 | studentquiz | StudentQuiz 1 | Test studentquiz description | 2 | 1 | 1 | 1 | 5 | # Create owned question by student role. And I am on the "StudentQuiz 1" "mod_studentquiz > View" page logged in as "student1" And I click on "Create new question" "button" diff --git a/tests/behat/navigation.feature b/tests/behat/navigation.feature index 630f418e..63e2ced9 100644 --- a/tests/behat/navigation.feature +++ b/tests/behat/navigation.feature @@ -55,7 +55,6 @@ Feature: Navigation to the pages And I should see "Categories" And I should see "Import" And I should see "Export" - And I should see "Select a category:" Scenario: Check that the More link exist in My Progress and Ranking block When I navigate to "StudentQuiz" in current page administration diff --git a/tests/behat/state_visibility_old_backup_import.feature b/tests/behat/state_visibility_old_backup_import.feature index b79699cf..53ddb482 100644 --- a/tests/behat/state_visibility_old_backup_import.feature +++ b/tests/behat/state_visibility_old_backup_import.feature @@ -13,7 +13,8 @@ Feature: Restore of studentquizzes in moodle exports contain old approved column @javascript @_file_upload Scenario: Restore moodle backups containing old StudentQuiz activity with old approved column When I am on the "Course 1" "restore" page logged in as "admin" - And I press "Manage backup files" + # Main branch has change the text to "Manage course backups" so we should use xpath. + And I click on "(//*[@class='singlebutton']//button)[1]" "xpath_element" And I upload "mod/studentquiz/tests/fixtures/backup-moodle2-aggregated-before.mbz" file to "Files" filemanager And I press "Save changes" And I restore "backup-moodle2-aggregated-before.mbz" backup into a new course using this options: diff --git a/tests/comment_test.php b/tests/comment_test.php index 5e549271..db1e0ed9 100644 --- a/tests/comment_test.php +++ b/tests/comment_test.php @@ -213,9 +213,9 @@ public function test_create_root_comment() { // Create root comment. $sqq1 = $this->studentquizquestions[0]; $text = 'Root comment'; - $comment = $this->create_comment($this->rootid, $sqq1->id, $text); + $comment = $this->create_comment($this->rootid, $sqq1->get_id(), $text); $this->assertEquals($text, $comment->content); - $this->assertEquals($sqq1->id, $comment->studentquizquestionid); + $this->assertEquals($sqq1->get_id(), $comment->studentquizquestionid); $this->assertEquals($this->rootid, $comment->parentid); } @@ -227,12 +227,12 @@ public function test_create_reply_comment() { $sqq1 = $this->studentquizquestions[0]; $text = 'Root comment'; $textreply = 'Reply root comment'; - $comment = $this->create_comment($this->rootid, $sqq1->id, $text); - $reply = $this->create_comment($comment->id, $sqq1->id, $textreply); + $comment = $this->create_comment($this->rootid, $sqq1->get_id(), $text); + $reply = $this->create_comment($comment->id, $sqq1->get_id(), $textreply); // Check text reply. $this->assertEquals($textreply, $reply->content); // Check question id. - $this->assertEquals($sqq1->id, $reply->studentquizquestionid); + $this->assertEquals($sqq1->get_id(), $reply->studentquizquestionid); // Check if reply belongs to comment. $this->assertEquals($comment->id, $reply->parentid); } @@ -248,7 +248,7 @@ public function test_create_reply_comment() { */ public function test_shorten_comment(string $content, string $expected, int $expectedlength): void { $sq1 = $this->studentquizquestions[0]; - $comment = $this->create_comment($this->rootid, $sq1->id, $content); + $comment = $this->create_comment($this->rootid, $sq1->get_id(), $content); $this->assertEquals($expectedlength, strlen($comment->shortcontent)); $this->assertEquals($expected, $comment->shortcontent); } @@ -291,7 +291,7 @@ public function test_delete_comment() { $sqq1 = $this->studentquizquestions[0]; $text = 'Root comment'; // Dont need to convert to use delete. - $comment = $this->create_comment($this->rootid, $sqq1->id, $text, false); + $comment = $this->create_comment($this->rootid, $sqq1->get_id(), $text, false); // Try to delete. $comment->delete(); // Get new data. @@ -312,9 +312,9 @@ public function test_fetch_all_comments() { $text = 'Root comment'; $textreply = 'Reply root comment'; $numreplies = 3; - $comment = $this->create_comment($this->rootid, $sqq1->id, $text); + $comment = $this->create_comment($this->rootid, $sqq1->get_id(), $text); for ($i = 0; $i < $numreplies; $i++) { - $this->create_comment($comment->id, $sqq1->id, $textreply); + $this->create_comment($comment->id, $sqq1->get_id(), $textreply); } $comments = $this->commentarea->fetch_all(0); $data = []; @@ -342,7 +342,7 @@ public function test_report_feature() { global $DB; $sqq1 = $this->studentquizquestions[0]; // Need to use comment class functions. Don't use convert to response data. - $comment = $this->create_comment($this->rootid, $sqq1->id, 'Test comment', false); + $comment = $this->create_comment($this->rootid, $sqq1->get_id(), 'Test comment', false); // Assume that we didn't input any emails for report. It will return false. $this->assertFalse($comment->can_report()); // Turn on report. @@ -377,7 +377,7 @@ private function generate_comment_list_for_sort() { 'parentid' => $this->rootid, 'userid' => $user->id, 'created' => $k + 1, - 'studentquizquestionid' => $sqq2->id + 'studentquizquestionid' => $sqq2->get_id() ]; } $DB->insert_records('studentquiz_comment', $records); @@ -548,7 +548,7 @@ private function seed_studentquiz_period_setting($period) { 'parentid' => $this->rootid, 'userid' => $commentarea->get_user()->id, 'created' => time(), - 'studentquizquestionid' => $sqq1->id + 'studentquizquestionid' => $sqq1->get_id() ]); return $commentarea; } @@ -562,7 +562,7 @@ public function test_edit_comment() { $sqq1 = $this->studentquizquestions[0]; $text = 'Root comment'; // Dont need to convert to use delete. - $comment = $this->create_comment($this->rootid, $sqq1->id, $text, false); + $comment = $this->create_comment($this->rootid, $sqq1->get_id(), $text, false); $formdata = new \stdClass(); $formdata->message['text'] = 'Edited comment'; $formdata->type = utils::COMMENT_TYPE_PUBLIC; @@ -619,7 +619,7 @@ public function test_create_comment_history() { // Create root comment. $sqq1 = $this->studentquizquestions[0]; $text = 'Root comment for history'; - $comment = $this->create_comment($this->rootid, $sqq1->id, $text, false); + $comment = $this->create_comment($this->rootid, $sqq1->get_id(), $text, false); $comparestr = 'comment' . $comment->get_id(); $historyid = $comment->create_history($comment->get_id(), $comment->get_user_id(), 0, $comparestr); $history = $DB->get_record('studentquiz_comment_history', ['id' => $historyid]); @@ -634,7 +634,7 @@ public function test_create_comment_history() { */ public function test_get_histories() { $sqq1 = $this->studentquizquestions[0]; - $comment = $this->create_comment($this->rootid, $sqq1->id, 'demo content', false); + $comment = $this->create_comment($this->rootid, $sqq1->get_id(), 'demo content', false); $comment->create_history($comment->get_id(), $comment->get_user_id(), 1, 'comment1' . $comment->get_id()); $comment->create_history($comment->get_id(), $comment->get_user_id(), 1, 'comment2' . $comment->get_id()); $histories = $this->commentarea->get_history($comment->get_id()); diff --git a/tests/cron_test.php b/tests/cron_test.php index 2bacaabe..ffa89f61 100644 --- a/tests/cron_test.php +++ b/tests/cron_test.php @@ -107,14 +107,14 @@ protected function setUp(): void { $this->studentquizquestions[1] = studentquiz_question::get_studentquiz_question_from_question($this->questions[1]); // Prepare comment. $commentrecord = new \stdClass(); - $commentrecord->studentquizquestionid = $this->studentquizquestions[0]->id; + $commentrecord->studentquizquestionid = $this->studentquizquestions[0]->get_id(); $commentrecord->userid = $this->student1->id; $this->getDataGenerator()->get_plugin_generator('mod_studentquiz')->create_comment($commentrecord); // Prepare rate. $raterecord = new \stdClass(); $raterecord->rate = 5; - $raterecord->studentquizquestionid = $this->studentquizquestions[0]->id; + $raterecord->studentquizquestionid = $this->studentquizquestions[0]->get_id(); $raterecord->userid = $this->student1->id; \mod_studentquiz\utils::save_rate($raterecord); } @@ -337,19 +337,19 @@ public function test_delete_orphaned_questions(): void { $this->assertEquals(0, $DB->count_records('question', ['id' => $q2v1->id])); $this->assertEquals(0, $DB->count_records('studentquiz_rate', - ['studentquizquestionid' => $sqq->id])); + ['studentquizquestionid' => $sqq->get_id()])); $this->assertEquals(0, $DB->count_records('studentquiz_comment', - ['studentquizquestionid' => $sqq->id])); + ['studentquizquestionid' => $sqq->get_id()])); $this->assertEquals(0, $DB->count_records('studentquiz_question', - ['id' => $sqq->id])); + ['id' => $sqq->get_id()])); $this->assertEquals(0, $DB->count_records('question', ['id' => $this->questions[0]->id])); $this->assertEquals(0, $DB->count_records('studentquiz_rate', - ['studentquizquestionid' => $this->studentquizquestions[0]->id])); + ['studentquizquestionid' => $this->studentquizquestions[0]->get_id()])); $this->assertEquals(0, $DB->count_records('studentquiz_comment', - ['studentquizquestionid' => $this->studentquizquestions[0]->id])); + ['studentquizquestionid' => $this->studentquizquestions[0]->get_id()])); $this->assertEquals(0, $DB->count_records('studentquiz_question', - ['id' => $this->studentquizquestions[0]->id])); + ['id' => $this->studentquizquestions[0]->get_id()])); $this->assertEquals(1, $DB->count_records('question', ['id' => $q2v2->id])); $this->assertEquals(1, $DB->count_records('question', ['id' => $q2v3->id])); diff --git a/tests/delete_instance_test.php b/tests/delete_instance_test.php index d818fa72..ed27aac0 100644 --- a/tests/delete_instance_test.php +++ b/tests/delete_instance_test.php @@ -60,7 +60,7 @@ public function test_delete_instance(): void { 'text' => 'Root message', 'format' => 1 ], - 'studentquizquestionid' => $sqq->id, + 'studentquizquestionid' => $sqq->get_id(), 'cmid' => $activity->cmid, 'replyto' => 0, 'type' => utils::COMMENT_TYPE_PUBLIC @@ -89,13 +89,13 @@ public function test_delete_instance(): void { $rate = (object) [ 'id' => 0, 'rate' => rand(1, 5), - 'studentquizquestionid' => $sqq->id, + 'studentquizquestionid' => $sqq->get_id(), 'userid' => $user->id ]; $DB->insert_record('studentquiz_rate', $rate); $progress = (object) [ - 'studentquizquestionid' => $sqq->id, + 'studentquizquestionid' => $sqq->get_id(), 'userid' => $user->id, 'studentquizid' => $studentquiz->id, 'lastanswercorrect' => rand(0, 1), @@ -114,10 +114,10 @@ public function test_delete_instance(): void { ]; $DB->insert_record('studentquiz_notification', $notification); // Before deletion. - self::check_sq_instance_data($sqq->id, $studentquiz->id, $commentid, 1); + self::check_sq_instance_data($sqq->get_id(), $studentquiz->id, $commentid, 1); studentquiz_delete_instance($studentquiz->id); // After deletion. - self::check_sq_instance_data($sqq->id, $studentquiz->id, $commentid, 0); + self::check_sq_instance_data($sqq->get_id(), $studentquiz->id, $commentid, 0); } diff --git a/tests/generator_test.php b/tests/generator_test.php index b4e49bba..a610ed8a 100644 --- a/tests/generator_test.php +++ b/tests/generator_test.php @@ -75,7 +75,7 @@ public function test_create_comment() { $user = $this->getDataGenerator()->create_user(); $commentrecord = new \stdClass(); - $commentrecord->studentquizquestionid = $this->studentquizquestion->id; + $commentrecord->studentquizquestionid = $this->studentquizquestion->get_id(); $commentrecord->userid = $user->id; $this->studentquizgenerator->create_comment($commentrecord); @@ -97,7 +97,7 @@ public function test_create_rate() { $raterecord = new \stdClass(); $raterecord->rate = 5; - $raterecord->studentquizquestionid = $this->studentquizquestion->id; + $raterecord->studentquizquestionid = $this->studentquizquestion->get_id(); $raterecord->userid = $user->id; \mod_studentquiz\utils::save_rate($raterecord); $this->assertEquals($count + 1, $DB->count_records('studentquiz_rate')); diff --git a/tests/privacy_test.php b/tests/privacy_test.php index d7294838..7e95a57b 100644 --- a/tests/privacy_test.php +++ b/tests/privacy_test.php @@ -595,7 +595,8 @@ public function test_delete_data_for_all_users_in_context() { list($questionsql, $questionparams) = $DB->get_in_or_equal([$this->questions[0]->id, $this->questions[1]->id], SQL_PARAMS_NAMED); list($sqqsql, $sqqparams) = - $DB->get_in_or_equal([$this->studentquizquestions[0]->id, $this->studentquizquestions[1]->id], SQL_PARAMS_NAMED); + $DB->get_in_or_equal([$this->studentquizquestions[0]->get_id(), $this->studentquizquestions[1]->get_id()], + SQL_PARAMS_NAMED); // Check all personal data belong to first context is deleted. $this->assertFalse($DB->record_exists_sql("SELECT 1 FROM {studentquiz_question} WHERE id {$sqqsql}" @@ -620,7 +621,8 @@ public function test_delete_data_for_all_users_in_context() { list($questionsql, $questionparams) = $DB->get_in_or_equal([$this->questions[2]->id, $this->questions[3]->id], SQL_PARAMS_NAMED); list($sqqsql, $sqqparams) = - $DB->get_in_or_equal([$this->studentquizquestions[2]->id, $this->studentquizquestions[3]->id], SQL_PARAMS_NAMED); + $DB->get_in_or_equal([$this->studentquizquestions[2]->get_id(), $this->studentquizquestions[3]->get_id()], + SQL_PARAMS_NAMED); $this->assertTrue($DB->record_exists_sql("SELECT 1 FROM {studentquiz_question} WHERE id {$sqqsql}" , $sqqparams)); $this->assertTrue($DB->record_exists_sql("SELECT 1 FROM {studentquiz_rate} WHERE studentquizquestionid {$sqqsql}" @@ -752,7 +754,7 @@ public function test_get_users_in_context_rating() { $question = self::create_question('Question', 'truefalse', $this->studentquiz[2]->categoryid, $anotheruser); $sqq = studentquiz_question::get_studentquiz_question_from_question($question); - $this->create_rate($sqq->id, $this->users[0]->id); + $this->create_rate($sqq->get_id(), $this->users[0]->id); $userlist = new userlist($this->contexts[2], $this->component); provider::get_users_in_context($userlist); @@ -761,7 +763,7 @@ public function test_get_users_in_context_rating() { $this->assertEquals([$anotheruser->id, $this->users[0]->id], $userlist->get_userids()); // Second student rate on another user question. - $this->create_rate($sqq->id, $this->users[1]->id); + $this->create_rate($sqq->get_id(), $this->users[1]->id); provider::get_users_in_context($userlist); $this->assertCount(3, $userlist); $this->assertEquals([$anotheruser->id, $this->users[0]->id, $this->users[1]->id ], $userlist->get_userids()); @@ -778,7 +780,7 @@ public function test_get_users_in_context_comment() { $question = self::create_question('Question', 'truefalse', $this->studentquiz[2]->categoryid, $anotheruser); $sqq = studentquiz_question::get_studentquiz_question_from_question($question); - $this->create_comment($sqq->id, $this->users[0]->id); + $this->create_comment($sqq->get_id(), $this->users[0]->id); $userlist = new userlist($this->contexts[2], $this->component); provider::get_users_in_context($userlist); @@ -787,7 +789,7 @@ public function test_get_users_in_context_comment() { $this->assertEquals([$anotheruser->id, $this->users[0]->id], $userlist->get_userids()); // Second student comment on another user question. - $this->create_comment($sqq->id, $this->users[1]->id); + $this->create_comment($sqq->get_id(), $this->users[1]->id); provider::get_users_in_context($userlist); $this->assertCount(3, $userlist); $this->assertEquals([$anotheruser->id, $this->users[0]->id, $this->users[1]->id ], $userlist->get_userids()); @@ -805,7 +807,7 @@ public function test_get_users_in_context_comment_history() { $question = self::create_question('Question', 'truefalse', $this->studentquiz[2]->categoryid, $anotheruser); $sqq = studentquiz_question::get_studentquiz_question_from_question($question); - $comment = $this->create_comment($sqq->id, $this->users[0]->id); + $comment = $this->create_comment($sqq->get_id(), $this->users[0]->id); $this->create_comment_history($comment->id, $this->users[0]->id); $userlist = new userlist($this->contexts[2], $this->component); @@ -870,7 +872,7 @@ public function test_get_users_in_context_change_state() { $question = self::create_question('Question', 'truefalse', $this->studentquiz[2]->categoryid, $anotheruser); $sqq = studentquiz_question::get_studentquiz_question_from_question($question); - $this->create_state_history($sqq->id, $this->users[0]->id); + $this->create_state_history($sqq->get_id(), $this->users[0]->id); $userlist = new userlist($this->contexts[2], $this->component); provider::get_users_in_context($userlist); diff --git a/view.php b/view.php index c6047694..792e7210 100755 --- a/view.php +++ b/view.php @@ -86,6 +86,11 @@ get_string('pagesize_invalid_input', 'studentquiz'), null, \core\output\notification::NOTIFY_ERROR); } + // In Moodle 4.3, we have a filter param after we move the questions, but in SQ, we don't use that, so remove the filter param. + if ($filter = optional_param('filter', 0, PARAM_RAW)) { + $baseurl->remove_params('filter'); + redirect($baseurl); + } } $renderer = $PAGE->get_renderer('mod_studentquiz', 'overview'); diff --git a/viewlib.php b/viewlib.php index 7209a0c5..54da39d7 100755 --- a/viewlib.php +++ b/viewlib.php @@ -27,6 +27,7 @@ require_once(__DIR__ . '/locallib.php'); use mod_studentquiz\local\studentquiz_question; +use mod_studentquiz\utils; /** * This class loads and represents the state for the main view. * @@ -123,6 +124,7 @@ public function __construct($course, $context, $cm, $studentquiz, $userid, $repo * Loads the question custom bank view. */ private function load_questionbank() { + global $CFG; $_POST['cat'] = $this->get_category_id() . ',' . $this->get_context_id(); $params = $_GET; // Get edit question link setup. @@ -133,6 +135,9 @@ private function load_questionbank() { $pagevars['cat'] = $this->get_category_id() . ',' . $this->get_context_id(); $this->pageurl = new moodle_url($thispageurl); foreach ($params as $key => $value) { + if ($key === 'sortdata') { + continue; + } if ($key == 'timecreated_sdt' || $key == 'timecreated_edt') { $value = http_build_query($value); } @@ -163,8 +168,15 @@ private function load_questionbank() { $thispageurl->remove_params('changepagesize'); } $this->qbpagevar = array_merge($pagevars, $params); - $this->questionbank = new \mod_studentquiz\question\bank\studentquiz_bank_view( - $contexts, $thispageurl, $this->course, $this->cm, $this->studentquiz, $pagevars, $this->report); + if (utils::moodle_version_is("<=", "42")) { + require_once($CFG->dirroot . '/mod/studentquiz/classes/question/bank/legacy/studentquiz_bank_view_pre_43.php'); + $this->questionbank = new \mod_studentquiz\question\bank\studentquiz_bank_view_pre_43( + $contexts, $thispageurl, $this->course, $this->cm, $this->studentquiz, $pagevars, $this->report); + } else { + $this->questionbank = new \mod_studentquiz\question\bank\studentquiz_bank_view( + $contexts, $thispageurl, $this->course, $this->cm, $this->studentquiz, $pagevars, $this->report); + } + } /**