diff --git a/backup/moodle2/backup_plan_builder.class.php b/backup/moodle2/backup_plan_builder.class.php index c3c200a4411..eae28a80c56 100644 --- a/backup/moodle2/backup_plan_builder.class.php +++ b/backup/moodle2/backup_plan_builder.class.php @@ -36,6 +36,7 @@ require_once($CFG->dirroot . '/backup/moodle2/backup_block_task.class.php'); require_once($CFG->dirroot . '/backup/moodle2/backup_default_block_task.class.php'); require_once($CFG->dirroot . '/backup/moodle2/backup_xml_transformer.class.php'); require_once($CFG->dirroot . '/backup/moodle2/backup_plugin.class.php'); +require_once($CFG->dirroot . '/backup/moodle2/backup_qbank_plugin.class.php'); require_once($CFG->dirroot . '/backup/moodle2/backup_qtype_plugin.class.php'); require_once($CFG->dirroot . '/backup/moodle2/backup_qtype_extrafields_plugin.class.php'); require_once($CFG->dirroot . '/backup/moodle2/backup_gradingform_plugin.class.php'); diff --git a/backup/moodle2/backup_qbank_plugin.class.php b/backup/moodle2/backup_qbank_plugin.class.php new file mode 100644 index 00000000000..52db8ab2e16 --- /dev/null +++ b/backup/moodle2/backup_qbank_plugin.class.php @@ -0,0 +1,38 @@ +. + +/** + * Defines backup_qbank_plugin class. + * + * @package core_backup + * @subpackage moodle2 + * @category backup + * @copyright 2021 Catalyst IT Australia Pty Ltd + * @author Safat Shahin + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +/** + * Base class for qbank backup plugins. + * + * @package core_backup + * @copyright 2021 Catalyst IT Australia Pty Ltd + * @author Safat Shahin + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +abstract class backup_qbank_plugin extends backup_plugin { + // Use default parent behaviour. +} diff --git a/backup/moodle2/backup_stepslib.php b/backup/moodle2/backup_stepslib.php index d17b0da0ddb..18b10897d71 100644 --- a/backup/moodle2/backup_stepslib.php +++ b/backup/moodle2/backup_stepslib.php @@ -787,34 +787,28 @@ class backup_filters_structure_step extends backup_structure_step { } /** - * structure step in charge of constructing the comments.xml file for all the comments found - * in a given context + * Structure step in charge of constructing the comments.xml file for all the comments found in a given context. */ class backup_comments_structure_step extends backup_structure_step { protected function define_structure() { - - // Define each element separated - + // Define each element separated. $comments = new backup_nested_element('comments'); $comment = new backup_nested_element('comment', array('id'), array( - 'commentarea', 'itemid', 'content', 'format', + 'component', 'commentarea', 'itemid', 'content', 'format', 'userid', 'timecreated')); - // Build the tree - + // Build the tree. $comments->add_child($comment); - // Define sources - + // Define sources. $comment->set_source_table('comments', array('contextid' => backup::VAR_CONTEXTID)); - // Define id annotations - + // Define id annotations. $comment->annotate_ids('user', 'userid'); - // Return the root element (comments) + // Return the root element (comments). return $comments; } } @@ -2356,6 +2350,9 @@ class backup_questions_structure_step extends backup_structure_step { // attach qtype plugin structure to $question element, only one allowed $this->add_plugin_structure('qtype', $question, false); + // Attach qbank plugin stucture to $question element, multiple allowed. + $this->add_plugin_structure('qbank', $question, true); + // attach local plugin stucture to $question element, multiple allowed $this->add_plugin_structure('local', $question, true); diff --git a/backup/moodle2/restore_plan_builder.class.php b/backup/moodle2/restore_plan_builder.class.php index 02dc0629501..a1177aa89c9 100644 --- a/backup/moodle2/restore_plan_builder.class.php +++ b/backup/moodle2/restore_plan_builder.class.php @@ -35,6 +35,7 @@ require_once($CFG->dirroot . '/backup/moodle2/restore_final_task.class.php'); require_once($CFG->dirroot . '/backup/moodle2/restore_block_task.class.php'); require_once($CFG->dirroot . '/backup/moodle2/restore_default_block_task.class.php'); require_once($CFG->dirroot . '/backup/moodle2/restore_plugin.class.php'); +require_once($CFG->dirroot . '/backup/moodle2/restore_qbank_plugin.class.php'); require_once($CFG->dirroot . '/backup/moodle2/restore_qtype_plugin.class.php'); require_once($CFG->dirroot . '/backup/moodle2/restore_qtype_extrafields_plugin.class.php'); require_once($CFG->dirroot . '/backup/moodle2/restore_format_plugin.class.php'); @@ -46,6 +47,7 @@ require_once($CFG->dirroot . '/backup/moodle2/restore_plagiarism_plugin.class.ph require_once($CFG->dirroot . '/backup/moodle2/restore_gradingform_plugin.class.php'); require_once($CFG->dirroot . '/backup/moodle2/restore_enrol_plugin.class.php'); require_once($CFG->dirroot . '/backup/moodle2/backup_plugin.class.php'); +require_once($CFG->dirroot . '/backup/moodle2/backup_qbank_plugin.class.php'); require_once($CFG->dirroot . '/backup/moodle2/backup_qtype_plugin.class.php'); require_once($CFG->dirroot . '/backup/moodle2/backup_qtype_extrafields_plugin.class.php'); require_once($CFG->dirroot . '/backup/moodle2/backup_format_plugin.class.php'); diff --git a/backup/moodle2/restore_qbank_plugin.class.php b/backup/moodle2/restore_qbank_plugin.class.php new file mode 100644 index 00000000000..3e52cbc4b08 --- /dev/null +++ b/backup/moodle2/restore_qbank_plugin.class.php @@ -0,0 +1,39 @@ +. + +/** + * Defines restore_qbank_plugin class. + * + * @package core_backup + * @subpackage moodle2 + * @category backup + * @copyright 2021 Catalyst IT Australia Pty Ltd + * @author Safat Shahin + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + + +/** + * Base class for qbank backup plugins. + * + * @package core_backup + * @copyright 2021 Catalyst IT Australia Pty Ltd + * @author Safat Shahin + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +abstract class restore_qbank_plugin extends restore_plugin { + // Use default parent behaviour. +} diff --git a/backup/moodle2/restore_stepslib.php b/backup/moodle2/restore_stepslib.php index 5b6d3cde29c..ef15049a11c 100644 --- a/backup/moodle2/restore_stepslib.php +++ b/backup/moodle2/restore_stepslib.php @@ -4750,10 +4750,13 @@ class restore_create_categories_and_questions extends restore_structure_step { // Apply for 'qtype' plugins optional paths at question level $this->add_plugin_structure('qtype', $question); + // Apply for 'qbank' plugins optional paths at question level. + $this->add_plugin_structure('qbank', $question); + // Apply for 'local' plugins optional paths at question level $this->add_plugin_structure('local', $question); - return array($category, $question, $hint, $tag); + return [$category, $question, $hint, $tag]; } protected function process_question_category($data) { diff --git a/lang/en/role.php b/lang/en/role.php index d155930f186..3cd4c0b7a99 100644 --- a/lang/en/role.php +++ b/lang/en/role.php @@ -365,6 +365,8 @@ $string['question:viewall'] = 'View all questions'; $string['question:viewmine'] = 'View your own questions'; $string['question:tagall'] = 'Tag all questions'; $string['question:tagmine'] = 'Tag your own questions'; +$string['question:commentall'] = 'Comment all questions'; +$string['question:commentmine'] = 'Comment your own questions'; $string['rating:rate'] = 'Add ratings to items'; $string['rating:view'] = 'View the total rating you received'; $string['rating:viewany'] = 'View total ratings that anyone received'; diff --git a/lib/classes/plugin_manager.php b/lib/classes/plugin_manager.php index 8de966b2790..7435ed2294d 100644 --- a/lib/classes/plugin_manager.php +++ b/lib/classes/plugin_manager.php @@ -1944,6 +1944,7 @@ class core_plugin_manager { ), 'qbank' => [ + 'comment', 'deletequestion', 'editquestion', 'exporttoxml', diff --git a/lib/questionlib.php b/lib/questionlib.php index 1fdf92f5add..2ed3d79dcaa 100644 --- a/lib/questionlib.php +++ b/lib/questionlib.php @@ -387,6 +387,9 @@ function question_delete_question($questionid) { } } + // Delete question comments. + $DB->delete_records('comments', ['itemid' => $questionid, 'component' => 'qbank_comment', + 'commentarea' => 'question']); // Finally delete the question record itself $DB->delete_records('question', array('id' => $questionid)); question_bank::notify_question_edited($questionid); @@ -1569,7 +1572,7 @@ function question_has_capability_on($questionorid, $cap, $notused = -1) { // These are existing questions capabilities that are set per category. // Each of these has a 'mine' and 'all' version that is appended to the capability name. - $capabilitieswithallandmine = ['edit' => 1, 'view' => 1, 'use' => 1, 'move' => 1, 'tag' => 1]; + $capabilitieswithallandmine = ['edit' => 1, 'view' => 1, 'use' => 1, 'move' => 1, 'tag' => 1, 'comment' => 1]; if (!isset($capabilitieswithallandmine[$cap])) { return has_capability('moodle/question:' . $cap, $context); @@ -1742,6 +1745,8 @@ function question_get_question_capabilities() { 'moodle/question:moveall', 'moodle/question:tagmine', 'moodle/question:tagall', + 'moodle/question:commentmine', + 'moodle/question:commentall', ); } diff --git a/question/bank/comment/amd/build/comment.min.js b/question/bank/comment/amd/build/comment.min.js new file mode 100644 index 00000000000..2d78b1d9412 --- /dev/null +++ b/question/bank/comment/amd/build/comment.min.js @@ -0,0 +1,2 @@ +function _typeof(a){"@babel/helpers - typeof";if("function"==typeof Symbol&&"symbol"==typeof Symbol.iterator){_typeof=function(a){return typeof a}}else{_typeof=function(a){return a&&"function"==typeof Symbol&&a.constructor===Symbol&&a!==Symbol.prototype?"symbol":typeof a}}return _typeof(a)}define ("qbank_comment/comment",["exports","core/fragment","core/str","core/modal_events","core/modal_factory","core/notification"],function(a,b,c,d,e,f){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.init=void 0;b=i(b);c=h(c);d=i(d);e=i(e);f=i(f);function g(){if("function"!=typeof WeakMap)return null;var a=new WeakMap;g=function(){return a};return a}function h(a){if(a&&a.__esModule){return a}if(null===a||"object"!==_typeof(a)&&"function"!=typeof a){return{default:a}}var b=g();if(b&&b.has(a)){return b.get(a)}var c={},d=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var e in a){if(Object.prototype.hasOwnProperty.call(a,e)){var f=d?Object.getOwnPropertyDescriptor(a,e):null;if(f&&(f.get||f.set)){Object.defineProperty(c,e,f)}else{c[e]=a[e]}}}c.default=a;if(b){b.set(a,c)}return c}function i(a){return a&&a.__esModule?a:{default:a}}var j=function(a,g,h){var i=b.default.loadFragment("qbank_comment","question_comment",h,{questionid:a,courseid:g});e.default.create({type:e.default.types.SAVE_CANCEL,title:c.get_string("commentheader","qbank_comment"),body:i,large:!0}).then(function(a){var b=a.getRoot();b.on(d.default.bodyRendered,function(){var a=document.querySelectorAll("div.comment-area a")[0];a.style.display="none"});c.get_strings([{key:"addcomment",component:"qbank_comment"},{key:"close",component:"qbank_comment"}]).then(function(b){a.setButtonText("save",b[0]);a.setButtonText("cancel",b[1])}).fail(f.default.exception);b.on(d.default.cancel,function(){location.reload();a.hide()});b.on(d.default.save,function(a){a.preventDefault();var b=document.querySelectorAll("div.comment-area a")[0],c=document.querySelectorAll("div.comment-area textarea")[0];if(c.value!=c.getAttribute("aria-label")&&""!=c.value){b.click()}});b.on("click","button[data-action=\"hide\"]",function(){location.reload();a.hide()});a.show();return a}).fail(f.default.exception)};a.init=function init(a){var b=document.querySelector(a),c=b.getAttribute("data-questionid"),d=b.getAttribute("data-courseid");b.addEventListener("click",function(){j(c,d,1)})}}); +//# sourceMappingURL=comment.min.js.map diff --git a/question/bank/comment/amd/build/comment.min.js.map b/question/bank/comment/amd/build/comment.min.js.map new file mode 100644 index 00000000000..bb87c239006 --- /dev/null +++ b/question/bank/comment/amd/build/comment.min.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["../src/comment.js"],"names":["commentEvent","questionId","courseID","contextId","commentFragment","Fragment","loadFragment","questionid","courseid","ModalFactory","create","type","types","SAVE_CANCEL","title","Str","get_string","body","large","then","modal","root","getRoot","on","ModalEvents","bodyRendered","submitlink","document","querySelectorAll","style","display","get_strings","key","component","strings","setButtonText","fail","Notification","exception","cancel","location","reload","hide","save","e","preventDefault","textarea","value","getAttribute","click","show","init","questionSelector","target","querySelector","addEventListener"],"mappings":"ygBAwBA,OACA,OACA,OACA,OACA,O,4lBAUMA,CAAAA,CAAY,CAAG,SAACC,CAAD,CAAaC,CAAb,CAAuBC,CAAvB,CAAqC,IAKlDC,CAAAA,CAAe,CAAGC,UAASC,YAAT,CAAsB,eAAtB,CAAuC,kBAAvC,CAA2DH,CAA3D,CAJX,CACPI,UAAU,CAAEN,CADL,CAEPO,QAAQ,CAAEN,CAFH,CAIW,CALgC,CAMtDO,UAAaC,MAAb,CAAoB,CAChBC,IAAI,CAAEF,UAAaG,KAAb,CAAmBC,WADT,CAEhBC,KAAK,CAAEC,CAAG,CAACC,UAAJ,CAAe,eAAf,CAAgC,eAAhC,CAFS,CAGhBC,IAAI,CAAEb,CAHU,CAIhBc,KAAK,GAJW,CAApB,EAKGC,IALH,CAKQ,SAACC,CAAD,CAAW,CACf,GAAIC,CAAAA,CAAI,CAAGD,CAAK,CAACE,OAAN,EAAX,CAGAD,CAAI,CAACE,EAAL,CAAQC,UAAYC,YAApB,CAAkC,UAAW,CACzC,GAAMC,CAAAA,CAAU,CAAGC,QAAQ,CAACC,gBAAT,CAA0B,oBAA1B,EAAgD,CAAhD,CAAnB,CACAF,CAAU,CAACG,KAAX,CAAiBC,OAAjB,CAA2B,MAC9B,CAHD,EAMAf,CAAG,CAACgB,WAAJ,CAAgB,CACZ,CAACC,GAAG,CAAE,YAAN,CAAoBC,SAAS,CAAE,eAA/B,CADY,CAEZ,CAACD,GAAG,CAAE,OAAN,CAAeC,SAAS,CAAE,eAA1B,CAFY,CAAhB,EAGGd,IAHH,CAGQ,SAACe,CAAD,CAAa,CACjBd,CAAK,CAACe,aAAN,CAAoB,MAApB,CAA4BD,CAAO,CAAC,CAAD,CAAnC,EACAd,CAAK,CAACe,aAAN,CAAoB,QAApB,CAA8BD,CAAO,CAAC,CAAD,CAArC,CAEH,CAPD,EAOGE,IAPH,CAOQC,UAAaC,SAPrB,EASAjB,CAAI,CAACE,EAAL,CAAQC,UAAYe,MAApB,CAA4B,UAAW,CACnCC,QAAQ,CAACC,MAAT,GACArB,CAAK,CAACsB,IAAN,EACH,CAHD,EAMArB,CAAI,CAACE,EAAL,CAAQC,UAAYmB,IAApB,CAA0B,SAASC,CAAT,CAAY,CAClCA,CAAC,CAACC,cAAF,GADkC,GAE5BnB,CAAAA,CAAU,CAAGC,QAAQ,CAACC,gBAAT,CAA0B,oBAA1B,EAAgD,CAAhD,CAFe,CAG5BkB,CAAQ,CAAGnB,QAAQ,CAACC,gBAAT,CAA0B,2BAA1B,EAAuD,CAAvD,CAHiB,CAMlC,GAAIkB,CAAQ,CAACC,KAAT,EAAkBD,CAAQ,CAACE,YAAT,CAAsB,YAAtB,CAAlB,EAA2E,EAAlB,EAAAF,CAAQ,CAACC,KAAtE,CAAmF,CAC/ErB,CAAU,CAACuB,KAAX,EACH,CAEJ,CAVD,EAWA5B,CAAI,CAACE,EAAL,CAAQ,OAAR,CAAiB,8BAAjB,CAA+C,UAAM,CACjDiB,QAAQ,CAACC,MAAT,GACArB,CAAK,CAACsB,IAAN,EACH,CAHD,EAIAtB,CAAK,CAAC8B,IAAN,GACA,MAAO9B,CAAAA,CACV,CA/CD,EA+CGgB,IA/CH,CA+CQC,UAAaC,SA/CrB,CAgDH,C,QAQmB,QAAPa,CAAAA,IAAO,CAACC,CAAD,CAAsB,IAClCC,CAAAA,CAAM,CAAG1B,QAAQ,CAAC2B,aAAT,CAAuBF,CAAvB,CADyB,CAGlCnD,CAAU,CAAGoD,CAAM,CAACL,YAAP,CAAoB,iBAApB,CAHqB,CAIlC9C,CAAQ,CAAGmD,CAAM,CAACL,YAAP,CAAoB,eAApB,CAJuB,CAKtCK,CAAM,CAACE,gBAAP,CAAwB,OAAxB,CAAiC,UAAM,CAEnCvD,CAAY,CAACC,CAAD,CAAaC,CAAb,CALA,CAKA,CACf,CAHD,CAIH,C","sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Column selector js.\n *\n * @package qbank_comment\n * @copyright 2021 Catalyst IT Australia Pty Ltd\n * @author Safat Shahin \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Fragment from 'core/fragment';\nimport * as Str from 'core/str';\nimport ModalEvents from 'core/modal_events';\nimport ModalFactory from 'core/modal_factory';\nimport Notification from 'core/notification';\n\n/**\n * Event listeners for the module.\n *\n * @method clickEvent\n * @param {int} questionId\n * @param {int} courseID\n * @param {int} contextId\n */\nconst commentEvent = (questionId, courseID, contextId) => {\n let args = {\n questionid: questionId,\n courseid: courseID\n };\n let commentFragment = Fragment.loadFragment('qbank_comment', 'question_comment', contextId, args);\n ModalFactory.create({\n type: ModalFactory.types.SAVE_CANCEL,\n title: Str.get_string('commentheader', 'qbank_comment'),\n body: commentFragment,\n large: true,\n }).then((modal) => {\n let root = modal.getRoot();\n\n // Don't display the default add comment link in the modal.\n root.on(ModalEvents.bodyRendered, function() {\n const submitlink = document.querySelectorAll(\"div.comment-area a\")[0];\n submitlink.style.display = 'none';\n });\n\n // Get the required strings and updated the modal button text labels.\n Str.get_strings([\n {key: 'addcomment', component: 'qbank_comment'},\n {key: 'close', component: 'qbank_comment'},\n ]).then((strings) => {\n modal.setButtonText('save', strings[0]);\n modal.setButtonText('cancel', strings[1]);\n return;\n }).fail(Notification.exception);\n\n root.on(ModalEvents.cancel, function() {\n location.reload();\n modal.hide();\n });\n\n // Handle adding the comment when the button in the modal is clicked.\n root.on(ModalEvents.save, function(e) {\n e.preventDefault();\n const submitlink = document.querySelectorAll(\"div.comment-area a\")[0];\n const textarea = document.querySelectorAll(\"div.comment-area textarea\")[0];\n\n // Check there is a valid comment to add, and trigger adding if there is.\n if (textarea.value != textarea.getAttribute('aria-label') && textarea.value != '') {\n submitlink.click();\n }\n\n });\n root.on('click', 'button[data-action=\"hide\"]', () => {\n location.reload();\n modal.hide();\n });\n modal.show();\n return modal;\n }).fail(Notification.exception);\n};\n\n/**\n * Entrypoint of the js.\n *\n * @method init\n * @param {string} questionSelector the question comment identifier.\n */\nexport const init = (questionSelector) => {\n let target = document.querySelector(questionSelector);\n let contextId = 1;\n let questionId = target.getAttribute('data-questionid'),\n courseID = target.getAttribute('data-courseid');\n target.addEventListener('click', () => {\n // Call for the event listener to listed for clicks in any comment count row.\n commentEvent(questionId, courseID, contextId);\n });\n};\n"],"file":"comment.min.js"} \ No newline at end of file diff --git a/question/bank/comment/amd/src/comment.js b/question/bank/comment/amd/src/comment.js new file mode 100644 index 00000000000..ed1c455e0a0 --- /dev/null +++ b/question/bank/comment/amd/src/comment.js @@ -0,0 +1,110 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * Column selector js. + * + * @package qbank_comment + * @copyright 2021 Catalyst IT Australia Pty Ltd + * @author Safat Shahin + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +import Fragment from 'core/fragment'; +import * as Str from 'core/str'; +import ModalEvents from 'core/modal_events'; +import ModalFactory from 'core/modal_factory'; +import Notification from 'core/notification'; + +/** + * Event listeners for the module. + * + * @method clickEvent + * @param {int} questionId + * @param {int} courseID + * @param {int} contextId + */ +const commentEvent = (questionId, courseID, contextId) => { + let args = { + questionid: questionId, + courseid: courseID + }; + let commentFragment = Fragment.loadFragment('qbank_comment', 'question_comment', contextId, args); + ModalFactory.create({ + type: ModalFactory.types.SAVE_CANCEL, + title: Str.get_string('commentheader', 'qbank_comment'), + body: commentFragment, + large: true, + }).then((modal) => { + let root = modal.getRoot(); + + // Don't display the default add comment link in the modal. + root.on(ModalEvents.bodyRendered, function() { + const submitlink = document.querySelectorAll("div.comment-area a")[0]; + submitlink.style.display = 'none'; + }); + + // Get the required strings and updated the modal button text labels. + Str.get_strings([ + {key: 'addcomment', component: 'qbank_comment'}, + {key: 'close', component: 'qbank_comment'}, + ]).then((strings) => { + modal.setButtonText('save', strings[0]); + modal.setButtonText('cancel', strings[1]); + return; + }).fail(Notification.exception); + + root.on(ModalEvents.cancel, function() { + location.reload(); + modal.hide(); + }); + + // Handle adding the comment when the button in the modal is clicked. + root.on(ModalEvents.save, function(e) { + e.preventDefault(); + const submitlink = document.querySelectorAll("div.comment-area a")[0]; + const textarea = document.querySelectorAll("div.comment-area textarea")[0]; + + // Check there is a valid comment to add, and trigger adding if there is. + if (textarea.value != textarea.getAttribute('aria-label') && textarea.value != '') { + submitlink.click(); + } + + }); + root.on('click', 'button[data-action="hide"]', () => { + location.reload(); + modal.hide(); + }); + modal.show(); + return modal; + }).fail(Notification.exception); +}; + +/** + * Entrypoint of the js. + * + * @method init + * @param {string} questionSelector the question comment identifier. + */ +export const init = (questionSelector) => { + let target = document.querySelector(questionSelector); + let contextId = 1; + let questionId = target.getAttribute('data-questionid'), + courseID = target.getAttribute('data-courseid'); + target.addEventListener('click', () => { + // Call for the event listener to listed for clicks in any comment count row. + commentEvent(questionId, courseID, contextId); + }); +}; diff --git a/question/bank/comment/backup/moodle2/backup_qbank_comment_plugin.class.php b/question/bank/comment/backup/moodle2/backup_qbank_comment_plugin.class.php new file mode 100644 index 00000000000..021f7c54740 --- /dev/null +++ b/question/bank/comment/backup/moodle2/backup_qbank_comment_plugin.class.php @@ -0,0 +1,59 @@ +. + +/** + * Provides the information to backup question comments. + * + * @package qbank_comment + * @copyright 2021 Catalyst IT Australia Pty Ltd + * @author Matt Porritt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class backup_qbank_comment_plugin extends \backup_qbank_plugin { + + /** + * Returns the comment information to attach to question element. + */ + protected function define_question_plugin_structure() { + + // Define the virtual plugin element with the condition to fulfill. + $plugin = $this->get_plugin_element(); + + // Create one standard named plugin element (the visible container). + $pluginwrapper = new backup_nested_element($this->get_recommended_name()); + + // Connect the visible container ASAP. + $plugin->add_child($pluginwrapper); + + $comments = new backup_nested_element('comments'); + + $comment = new backup_nested_element('comment', ['id'], ['component', 'commentarea', 'itemid', 'contextid', + 'content', 'format', 'userid', 'timecreated']); + + $pluginwrapper->add_child($comments); + $comments->add_child($comment); + + $comment->set_source_sql("SELECT c.* + FROM {comments} c + WHERE c.commentarea = 'question' + AND c.component = 'qbank_comment' + AND c.itemid = ?", [backup::VAR_PARENTID]); + + $comment->annotate_ids('user', 'userid'); + + return $plugin; + } +} diff --git a/question/bank/comment/backup/moodle2/restore_qbank_comment_plugin.class.php b/question/bank/comment/backup/moodle2/restore_qbank_comment_plugin.class.php new file mode 100644 index 00000000000..cb2ed7886f3 --- /dev/null +++ b/question/bank/comment/backup/moodle2/restore_qbank_comment_plugin.class.php @@ -0,0 +1,58 @@ +. + +/** + * Restore plugin class that provides the necessary information needed to restore comments for questions. + * + * @package qbank_comment + * @copyright 2021 Catalyst IT Australia Pty Ltd + * @author Matt Porritt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class restore_qbank_comment_plugin extends restore_qbank_plugin { + + /** + * Returns the paths to be handled by the plugin at question level. + */ + protected function define_question_plugin_structure() { + return [ + new restore_path_element('comment', $this->get_pathfor('/comments/comment')) + ]; + } + + /** + * Process the question comments element. + * + * @param array $data The comment data to restore. + */ + public function process_comment($data) { + global $DB, $CFG; + + $data = (object)$data; + + $newquestionid = $this->get_new_parentid('question'); + $questioncreated = (bool) $this->get_mappingid('question_created', $this->get_old_parentid('question')); + if (!$questioncreated) { + // This question already exists in the question bank. Nothing for us to do. + return; + } + + if ($CFG->usecomments) { + $data->itemid = $newquestionid; + $DB->insert_record('comments', $data); + } + } +} diff --git a/question/bank/comment/classes/comment_count_column.php b/question/bank/comment/classes/comment_count_column.php new file mode 100644 index 00000000000..966c8a9f8d9 --- /dev/null +++ b/question/bank/comment/classes/comment_count_column.php @@ -0,0 +1,80 @@ +. + +namespace qbank_comment; + +use core_question\local\bank\column_base; +use question_bank; + +/** + * A column to show the number of comments. + * + * @package qbank_comment + * @copyright 2021 Catalyst IT Australia Pty Ltd + * @author Safat Shahin + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class comment_count_column extends column_base { + + /** + * Get the name of the column, used internally. + * + * @return string + */ + public function get_name(): string { + return 'commentcount'; + } + + /** + * Get the title of the column that will be displayed. + * + * @return string + */ + protected function get_title(): string { + return get_string('commentplural', 'qbank_comment'); + } + + /** + * Generate the content to be displayed. + * + * @param object $question The question object. + * @param string $rowclasses Classes that can be added. + */ + protected function display_content($question, $rowclasses): void { + global $DB, $PAGE; + $args = [ + 'component' => 'qbank_comment', + 'commentarea' => 'question', + 'itemid' => $question->id, + 'contextid' => 1 + ]; + $commentcount = $DB->count_records('comments', $args); + $attributes = []; + if (question_has_capability_on($question, 'comment')) { + $target = 'questioncommentpreview_' . $question->id; + $datatarget = '[data-target="' . $target . '"]'; + $PAGE->requires->js_call_amd('qbank_comment/comment', 'init', [$datatarget]); + $attributes = [ + 'data-target' => $target, + 'data-questionid' => $question->id, + 'data-courseid' => $this->qbank->course->id, + 'class' => 'link-primary comment-pointer' + ]; + } + echo \html_writer::tag('a', $commentcount, $attributes); + } + +} diff --git a/question/bank/comment/classes/event/comment_created.php b/question/bank/comment/classes/event/comment_created.php new file mode 100644 index 00000000000..f0142105390 --- /dev/null +++ b/question/bank/comment/classes/event/comment_created.php @@ -0,0 +1,51 @@ +. + +namespace qbank_comment\event; + +/** + * qbank_comment comment created event. + * + * @package qbank_comment + * @copyright 2021 Catalyst IT Australia Pty Ltd + * @author Safat Shahin + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class comment_created extends \core\event\comment_created { + + /** + * Get URL related to the action, null in this case. + * + * @return null + */ + public function get_url() { + return null; + } + + /** + * Returns description of what happened. + * + * @return string + */ + public function get_description() { + $a = new \stdClass(); + $a->userid = $this->userid; + $a->objectid = $this->objectid; + $a->component = $this->component; + $a->itemid = $this->other['itemid']; + return get_string('comment_added', 'qbank_comment', $a); + } +} diff --git a/question/bank/comment/classes/event/comment_deleted.php b/question/bank/comment/classes/event/comment_deleted.php new file mode 100644 index 00000000000..c1738fba528 --- /dev/null +++ b/question/bank/comment/classes/event/comment_deleted.php @@ -0,0 +1,51 @@ +. + +namespace qbank_comment\event; + +/** + * qbank_comment comment deleted event. + * + * @package qbank_comment + * @copyright 2021 Catalyst IT Australia Pty Ltd + * @author Safat Shahin + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class comment_deleted extends \core\event\comment_deleted { + + /** + * Get URL related to the action, null in this case. + * + * @return null + */ + public function get_url() { + return null; + } + + /** + * Returns description of what happened. + * + * @return string + */ + public function get_description() { + $a = new \stdClass(); + $a->userid = $this->userid; + $a->objectid = $this->objectid; + $a->component = $this->component; + $a->itemid = $this->other['itemid']; + return get_string('comment_removed', 'qbank_comment', $a); + } +} diff --git a/question/bank/comment/classes/output/renderer.php b/question/bank/comment/classes/output/renderer.php new file mode 100644 index 00000000000..5aecc49c146 --- /dev/null +++ b/question/bank/comment/classes/output/renderer.php @@ -0,0 +1,38 @@ +. + +namespace qbank_comment\output; + +/** + * Class renderer for comment. + * + * @package qbank_comment + * @copyright 2021 Catalyst IT Australia Pty Ltd + * @author Safat Shahin + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class renderer extends \plugin_renderer_base { + + /** + * Render the html fragment for comment modal. + * + * @param array $displaydata + * @return string + */ + public function render_comment_fragment($displaydata): string { + return $this->render_from_template('qbank_comment/comment_modal', $displaydata); + } +} diff --git a/question/bank/comment/classes/plugin_feature.php b/question/bank/comment/classes/plugin_feature.php new file mode 100644 index 00000000000..ee6d7355b58 --- /dev/null +++ b/question/bank/comment/classes/plugin_feature.php @@ -0,0 +1,40 @@ +. + +namespace qbank_comment; + +/** + * Class plugin_features is the entrypoint for the columns. + * + * @package qbank_comment + * @copyright 2021 Catalyst IT Australia Pty Ltd + * @author Safat Shahin + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class plugin_feature extends \core_question\local\bank\plugin_features_base { + + /** + * Get the columns provided by this plugin. + * + * @param \core_question\local\bank\view $qbank + * @return comment_count_column[] + */ + public function get_question_columns(\core_question\local\bank\view $qbank): array { + return [ + new comment_count_column($qbank) + ]; + } +} diff --git a/question/bank/comment/classes/privacy/provider.php b/question/bank/comment/classes/privacy/provider.php new file mode 100644 index 00000000000..3a306834e8f --- /dev/null +++ b/question/bank/comment/classes/privacy/provider.php @@ -0,0 +1,140 @@ +. + +namespace qbank_comment\privacy; + +use core_privacy\local\metadata\collection; +use core_privacy\local\request\approved_contextlist; +use core_privacy\local\request\contextlist; +use core_privacy\local\request\userlist; +use core_privacy\local\request\approved_userlist; + +/** + * Privacy Subsystem for qbank_comment. + * + * @package qbank_comment + * @copyright 2021 Catalyst IT Australia Pty Ltd + * @author Safat Shahin + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements + // The qbank_comment stores user provided data. + \core_privacy\local\metadata\provider, + \core_privacy\local\request\core_userlist_provider, + // The qbank_comment provides data directly to core. + \core_privacy\local\request\plugin\provider { + + /** + * Returns meta data about this system. + * + * @param collection $collection + * @return collection + */ + public static function get_metadata(collection $collection): collection { + return $collection->add_subsystem_link('core_comment', [], 'privacy:metadata:core_comment'); + } + + /** + * Get the list of contexts that contain user information for the specified user. + * + * @param int $userid + * @return contextlist + */ + public static function get_contexts_for_userid(int $userid): contextlist { + $contextlist = new contextlist(); + + $sql = "SELECT contextid + FROM {comments} + WHERE component = :component + AND userid = :userid"; + $params = [ + 'area' => 'question', + 'component' => 'qbank_comment', + 'userid' => $userid + ]; + + $contextlist->add_from_sql($sql, $params); + return $contextlist; + } + + /** + * Get the list of users within a specific context. + * + * @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination. + */ + public static function get_users_in_context(userlist $userlist) { + $context = $userlist->get_context(); + + $params = [ + 'contextid' => $context->id, + 'area' => 'question', + 'component' => 'qbank_comment' + ]; + + $sql = "SELECT userid as userid + FROM {comments} + WHERE component = :component + AND contextid = :contextid"; + + $userlist->add_from_sql('userid', $sql, $params); + } + + /** + * Export all user data for the specified user, in the specified contexts. + * + * @param approved_contextlist $contextlist + */ + public static function export_user_data(approved_contextlist $contextlist) { + $contexts = $contextlist->get_contexts(); + foreach ($contexts as $context) { + \core_comment\privacy\provider::export_comments( + $context, + 'qbank_comment', + 'question', + 0, + [] + ); + } + } + + /** + * Delete all data for all users in the specified context. + * + * @param \context $context + */ + public static function delete_data_for_all_users_in_context(\context $context) { + \core_comment\privacy\provider::delete_comments_for_all_users($context, 'qbank_comment'); + } + + /** + * Delete multiple users within a single context. + * + * @param approved_userlist $userlist The approved context and user information to delete information for. + */ + public static function delete_data_for_users(approved_userlist $userlist) { + \core_comment\privacy\provider::delete_comments_for_users($userlist, 'qbank_comment'); + } + + /** + * Delete all user data for the specified user, in the specified contexts. + * + * @param approved_contextlist $contextlist + */ + public static function delete_data_for_user(approved_contextlist $contextlist) { + \core_comment\privacy\provider::delete_comments_for_user($contextlist, 'qbank_comment'); + } + +} diff --git a/question/bank/comment/db/access.php b/question/bank/comment/db/access.php new file mode 100644 index 00000000000..965ad1fcc84 --- /dev/null +++ b/question/bank/comment/db/access.php @@ -0,0 +1,50 @@ +. + +/** + * Capability definitions for this module. + * + * @package qbank_comment + * @copyright 2021 Catalyst IT Australia Pty Ltd + * @author Matt Porritt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$capabilities = [ + // Controls whether users can comment their own questions. + 'moodle/question:commentmine' => [ + 'captype' => 'write', + 'contextlevel' => CONTEXT_COURSE, + 'archetypes' => [ + 'editingteacher' => CAP_ALLOW, + 'manager' => CAP_ALLOW + ], + 'clonepermissionsfrom' => 'moodle/question:editmine' + ], + + // Controls whether users can comment all questions. + 'moodle/question:commentall' => [ + 'captype' => 'write', + 'contextlevel' => CONTEXT_COURSE, + 'archetypes' => [ + 'editingteacher' => CAP_ALLOW, + 'manager' => CAP_ALLOW + ], + 'clonepermissionsfrom' => 'moodle/question:editall' + ], +]; diff --git a/question/bank/comment/lang/en/qbank_comment.php b/question/bank/comment/lang/en/qbank_comment.php new file mode 100644 index 00000000000..449c1d53d1f --- /dev/null +++ b/question/bank/comment/lang/en/qbank_comment.php @@ -0,0 +1,41 @@ +. + +/** + * Strings for component qbank_comment, language 'en'. + * + * @package qbank_comment + * @copyright 2021 Catalyst IT Australia Pty Ltd + * @author Safat Shahin + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +$string['pluginname'] = 'Question comment'; +$string['privacy:metadata:core_comment'] = 'Question comment plugin helps users with permission to comment in a question.'; +// Column. +$string['comment'] = 'Comment'; +$string['commentplural'] = 'Comments'; +// Modal. +$string['addcomment'] = 'Add comment'; +$string['close'] = 'Close'; +$string['commentheader'] = 'Question comments'; +$string['commentdisabled'] = 'Comment feature is disabled "sitewide", + please ask your "Site administrator" to enable "usecomments" from "Advanced settings" in order to comment in this question.'; +// Events. +$string['comment_added'] = 'The user with id \'{$a->userid}\' added the comment with id \'{$a->objectid}\' + to the \'{$a->component}\' for the question with id \'{$a->itemid}\'.'; +$string['comment_removed'] = 'The user with id \'{$a->userid}\' deleted the comment with id \'{$a->objectid}\' + to the \'{$a->component}\' for the question with id \'{$a->itemid}\'.'; diff --git a/question/bank/comment/lib.php b/question/bank/comment/lib.php new file mode 100644 index 00000000000..51664541c4a --- /dev/null +++ b/question/bank/comment/lib.php @@ -0,0 +1,138 @@ +. + +/** + * Helper functions and callbacks. + * + * @package qbank_comment + * @copyright 2021 Catalyst IT Australia Pty Ltd + * @author Safat Shahin + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * Validate comment parameter before perform other comments actions. + * + * @param stdClass $commentparam + * { + * context => context the context object + * courseid => int course id + * cm => stdClass course module object + * commentarea => string comment area + * itemid => int itemid + * } + * @return boolean + */ +function qbank_comment_comment_validate($commentparam): bool { + if ($commentparam->commentarea != 'question' && $commentparam->component != 'qbank_comment') { + throw new comment_exception('invalidcommentarea'); + } + return true; +} + +/** + * Running additional permission check on plugins. + * + * @param stdClass $args + * @return array + */ +function qbank_comment_comment_permissions($args): array { + return ['post' => true, 'view' => true]; +} + +/** + * Validate comment data before displaying comments. + * + * @param array $comments + * @param stdClass $args + * @return array $comments + */ +function qbank_comment_comment_display($comments, $args): array { + if ($args->commentarea != 'question' && $args->component != 'qbank_comment') { + throw new comment_exception('core_question'); + } + return $comments; +} + +/** + * Comment content for callbacks. + * + * @param question_definition $question + * @param int $courseid + * @return string + */ +function qbank_comment_preview_display($question, $courseid): string { + global $CFG, $PAGE; + if (question_has_capability_on($question, 'comment') && $CFG->usecomments + && core\plugininfo\qbank::is_plugin_enabled('qbank_comment')) { + \comment::init($PAGE); + $args = new \stdClass; + $args->contextid = 1; // Static data to bypass comment sql as context is not needed. + $args->courseid = $courseid; + $args->area = 'question'; + $args->itemid = $question->id; + $args->component = 'qbank_comment'; + $args->notoggle = true; + $args->autostart = true; + $args->displaycancel = false; + $args->linktext = get_string('commentheader', 'qbank_comment'); + $comment = new \comment($args); + $comment->set_view_permission(true); + $comment->set_fullwidth(); + return $comment->output(); + } else { + return ''; + } +} + +/** + * Question comment fragment callback. + * + * @param array $args + * @return string rendered output + * @todo cleanup after class renaming to remove check for previewlib.php MDL-71679 + */ +function qbank_comment_output_fragment_question_comment($args): string { + global $USER, $PAGE, $CFG; + $displaydata = []; + require_once($CFG->dirroot . '/question/engine/bank.php'); + $question = question_bank::load_question($args['questionid']); + $quba = question_engine::make_questions_usage_by_activity( + 'core_question_preview', context_user::instance($USER->id)); + + if (class_exists('\\qbank_previewquestion\\question_preview_options')) { + $options = new \qbank_previewquestion\question_preview_options($question); + } else { + require_once($CFG->dirroot . '/question/previewlib.php'); + $options = new question_preview_options($question); + } + + $options->load_user_defaults(); + $options->set_from_request(); + $quba->set_preferred_behaviour($options->behaviour); + $slot = $quba->add_question($question, $options->maxmark); + $quba->start_question($slot, $options->variant); + $displaydata['question'] = $quba->render_question($slot, $options, '1'); + $displaydata['comment'] = qbank_comment_preview_display($question, $args['courseid']); + $displaydata['commenstdisabled'] = false; + if (empty($displaydata['comment']) && !$CFG->usecomments) { + $displaydata['commenstdisabled'] = true; + } + + return $PAGE->get_renderer('qbank_comment')->render_comment_fragment($displaydata); +} diff --git a/question/bank/comment/styles.css b/question/bank/comment/styles.css new file mode 100644 index 00000000000..6fa9bcb860e --- /dev/null +++ b/question/bank/comment/styles.css @@ -0,0 +1,9 @@ +/* Comment text area size to maximum */ +.question-comment-view .comment-ctrl .comment-area { + max-width: none; + width: 100%; +} + +.comment-pointer { + cursor: pointer; +} diff --git a/question/bank/comment/templates/comment_modal.mustache b/question/bank/comment/templates/comment_modal.mustache new file mode 100644 index 00000000000..571078529e6 --- /dev/null +++ b/question/bank/comment/templates/comment_modal.mustache @@ -0,0 +1,47 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template qbank_comment/comment_modal + + The template that renders the modal window for adding comments to questions. + * question - The html of the question. + * comment - The html of the comment. + * commenstdisabled - If true display the comment disabled notification. + + Example context (json): + { + "commentdata": [ + { + "question": "question html", + "comment": "comment html", + "commenstdisabled": "bool" + } + ] + } +}} + +
+ {{{question}}} +
+
+ {{{comment}}} + {{#commenstdisabled}} + + {{#str}} commentdisabled, qbank_comment {{/str}} + + {{/commenstdisabled}} +
diff --git a/question/bank/comment/tests/backup_test.php b/question/bank/comment/tests/backup_test.php new file mode 100644 index 00000000000..f4aa106999f --- /dev/null +++ b/question/bank/comment/tests/backup_test.php @@ -0,0 +1,209 @@ +. + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php'); +require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php'); + + +/** + * Question comment backup and restore unit tests. + * + * @package qbank_comment + * @copyright 2021 Catalyst IT Australia Pty Ltd + * @author Matt Porritt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class qbank_comment_backup_restore_test extends \advanced_testcase { + + /** + * @var array Data object for generating a question. + */ + protected $question1data; + + /** + * @var array Data object for generating a question. + */ + protected $question2data; + + /** + * @var component_generator_base Question Generator. + */ + protected $qgen; + + /** + * @var core_course_category Course category. + */ + protected $category; + + /** + * @var stdClass Course object. + */ + protected $course; + + /** + * Set up + */ + protected function setUp(): void { + parent::setUp(); + $this->resetAfterTest(); + $this->setAdminUser(); + + // Set up custom fields. + $data = new \stdClass(); + $data->component = 'qbank_comment'; + $data->area = 'question'; + + // Question initial set up. + $this->category = $this->getDataGenerator()->create_category(); + $this->course = $this->getDataGenerator()->create_course(['category' => $this->category->id]); + $context = context_coursecat::instance($this->category->id); + $this->qgen = $this->getDataGenerator()->get_plugin_generator('core_question'); + $qcat = $this->qgen->create_question_category(['contextid' => $context->id]); + + $this->question1data = ['category' => $qcat->id, 'idnumber' => 'q1']; + $this->question2data = ['category' => $qcat->id, 'idnumber' => 'q2']; + } + + /** + * Makes a backup of the course. + * + * @param stdClass $course The course object. + * @return string Unique identifier for this backup. + */ + protected function backup_course(\stdClass $course): string { + global $CFG, $USER; + + // Turn off file logging, otherwise it can't delete the file (Windows). + $CFG->backup_file_logger_level = backup::LOG_NONE; + + // Do backup with default settings. MODE_IMPORT means it will just + // create the directory and not zip it. + $bc = new backup_controller(backup::TYPE_1COURSE, $course->id, + backup::FORMAT_MOODLE, backup::INTERACTIVE_NO, backup::MODE_IMPORT, + $USER->id); + $backupid = $bc->get_backupid(); + $bc->execute_plan(); + $bc->destroy(); + + return $backupid; + } + + /** + * Restores a backup that has been made earlier. + * + * @param string $backupid The unique identifier of the backup. + * @param string $fullname Full name of the new course that is going to be created. + * @param string $shortname Short name of the new course that is going to be created. + * @param int $categoryid The course category the backup is going to be restored in. + * @return int The new course id. + */ + protected function restore_course(string $backupid, string $fullname, string $shortname, int $categoryid): int { + global $CFG, $USER; + + // Turn off file logging, otherwise it can't delete the file (Windows). + $CFG->backup_file_logger_level = backup::LOG_NONE; + + // Do restore to new course with default settings. + $newcourseid = restore_dbops::create_new_course($fullname, $shortname, $categoryid); + $rc = new restore_controller($backupid, $newcourseid, + backup::INTERACTIVE_NO, backup::MODE_GENERAL, $USER->id, + backup::TARGET_NEW_COURSE); + + $rc->execute_precheck(); + $rc->execute_plan(); + $rc->destroy(); + + return $newcourseid; + } + + /** + * Test comments attached to questions persist + * across the backup and restore process. + */ + public function test_backup_restore() { + global $DB; + + $this->resetAfterTest(); + $this->setAdminUser(); + + $courseshortname = $this->course->shortname; + $coursefullname = $this->course->fullname; + + // Create 2 questions. + $question1 = $this->qgen->create_question('shortanswer', null, $this->question1data); + $question2 = $this->qgen->create_question('shortanswer', null, $this->question2data); + + // Add comments to the questions. + $coursecontext = \context_course::instance($this->course->id); + $args = new stdClass; + $args->context = $coursecontext; + $args->course = $this->course; + $args->area = 'question'; + $args->itemid = $question1->id; + $args->component = 'qbank_comment'; + $args->linktext = get_string('commentheader', 'qbank_comment'); + $args->notoggle = true; + $args->autostart = true; + $args->displaycancel = false; + + // Two comments for question 1. + $commentobj1 = new \comment($args); + $commentobj1->add('new comment for question 1 _ 1'); + $comment1 = $commentobj1->add('new comment for question 1 _ 2'); + + // One comment for question 2. + $args->itemid = $question2->id; + $commentobj2 = new \comment($args); + $comment2 = $commentobj2->add('new comment for question 2'); + + // Create a quiz and the questions to that. + $quiz = $this->getDataGenerator()->create_module( + 'quiz', ['course' => $this->course->id, 'name' => 'restored_quiz']); + quiz_add_quiz_question($question1->id, $quiz); + quiz_add_quiz_question($question2->id, $quiz); + + // Backup the course. + $backupid = $this->backup_course($this->course); + + // Now delete everything. + delete_course($this->course, false); + question_delete_question($question1->id); + question_delete_question($question2->id); + + // Check the comment data for the questions has also gone. + $DB->record_exists('comments', ['id' => $comment1->id]); + $this->assertFalse($DB->record_exists('comments', ['id' => $comment1->id])); + $this->assertFalse($DB->record_exists('comments', ['id' => $comment2->id])); + + // Restore the backup we had made earlier into a new course. + $newcategory = $this->getDataGenerator()->create_category(); + $this->restore_course($backupid, $coursefullname, $courseshortname . '_2', $newcategory->id); + + // The questions and their associated comments should have been restored. + $newquestion1 = $DB->get_record('question', ['idnumber' => 'q1']); + $args->itemid = $newquestion1->id; + $commentobj = new \comment($args); + $this->assertEquals($commentobj->count(), 2); + + $newquestion2 = $DB->get_record('question', ['idnumber' => 'q2']); + $args->itemid = $newquestion2->id; + $commentobj = new \comment($args); + $this->assertEquals($commentobj->count(), 1); + } +} diff --git a/question/bank/comment/tests/behat/behat_qbank_comment.php b/question/bank/comment/tests/behat/behat_qbank_comment.php new file mode 100644 index 00000000000..344ae8a9bd7 --- /dev/null +++ b/question/bank/comment/tests/behat/behat_qbank_comment.php @@ -0,0 +1,143 @@ +. + +/** + * Commenting system steps definitions for question. + * + * @package qbank_comment + * @copyright 2021 Catalyst IT Australia Pty Ltd + * @author Safat Shahin + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +// NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php. +require_once(__DIR__ . '/../../../../../lib/behat/behat_base.php'); +require_once(__DIR__ . '/../../../../tests/behat/behat_question_base.php'); + +use Behat\Mink\Exception\ExpectationException as ExpectationException, + Behat\Mink\Exception\ElementNotFoundException as ElementNotFoundException; + +/** + * Steps definitions to deal with the commenting system in question. + * + * @package qbank_comment + * @copyright 2021 Catalyst IT Australia Pty Ltd + * @author Safat Shahin + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class behat_qbank_comment extends behat_question_base { + + /** + * Looks for a table, then looks for a row that contains the given text. + * Once it finds the right row, it clicks a link in that row. + * + * @When I click :arg1 on the row on the comments column + * @param string $linkname + * @param string $rowtext + */ + public function i_click_on_the_row_containing($linkname) { + $exception = new ElementNotFoundException($this->getSession(), + 'Cannot find any row on the page containing the text ' . $linkname); + $row = $this->find('css', sprintf('table tbody tr td.commentcount a:contains("%s")', $linkname), $exception); + $row->click(); + } + + /** + * Looks for the appropriate comment count in the column. + * + * @Then I should see :arg1 on the comments column + * @param string $linkdata + */ + public function i_should_see_on_the_column($linkdata) { + $exception = new ElementNotFoundException($this->getSession(), + 'Cannot find any row with the comment count of ' . $linkdata . ' on the column named Comments'); + $this->find('css', sprintf('table tbody tr td.commentcount a:contains("%s")', $linkdata), $exception); + } + + /** + * Adds the specified option to the question comments of the current modal. + * + * @Then I add :arg1 comment to question + * @param string $comment + */ + public function i_add_comment_to_question($comment) { + + // Getting the textarea and setting the provided value. + $exception = new ElementNotFoundException($this->getSession(), 'Question '); + + if ($this->running_javascript()) { + $commentstextarea = $this->find('css', + '.modal-dialog .question-comment-view .comment-area textarea', $exception); + $commentstextarea->setValue($comment); + + // We delay 1 second which is all we need. + $this->getSession()->wait(1000); + + } else { + throw new ExpectationException('JavaScript not running', $this->getSession()); + } + } + + /** + * Adds the specified option to the question comments of the question preview. + * + * @Then I add :arg1 comment to question preview + * @param string $comment + */ + public function i_add_comment_to_question_preview($comment) { + + // Getting the textarea and setting the provided value. + $exception = new ElementNotFoundException($this->getSession(), 'Question '); + + if ($this->running_javascript()) { + $commentstextarea = $this->find('css', + '.question-comment-view .comment-area textarea', $exception); + $commentstextarea->setValue($comment); + + // We delay 1 second which is all we need. + $this->getSession()->wait(1000); + + } else { + throw new ExpectationException('JavaScript not running', $this->getSession()); + } + } + + /** + * Deletes the specified comment from the current question comment modal. + * + * @Then I delete :arg comment from question + * @param string $comment + */ + public function i_delete_comment_from_question($comment) { + + $exception = new ElementNotFoundException($this->getSession(), '"' . $comment . '" comment '); + + // Using xpath liternal to avoid possible problems with comments containing quotes. + $commentliteral = behat_context_helper::escape($comment); + + $commentxpath = "//*[contains(concat(' ', normalize-space(@class), ' '), ' question-comment-view ')]" . + "/descendant::div[@class='comment-message'][contains(., $commentliteral)]"; + + // Click on delete icon. + $this->execute('behat_general::i_click_on_in_the', + ["Delete comment posted by", "icon", $this->escape($commentxpath), "xpath_element"] + ); + + // Wait for the animation to finish, in theory is just 1 sec, adding 4 just in case. + $this->getSession()->wait(4 * 1000); + } + +} diff --git a/question/bank/comment/tests/behat/question_comment.feature b/question/bank/comment/tests/behat/question_comment.feature new file mode 100644 index 00000000000..7b1dd784fc4 --- /dev/null +++ b/question/bank/comment/tests/behat/question_comment.feature @@ -0,0 +1,114 @@ +@qbank @qbank_comment @javascript +Feature: A Teacher can comment in a question + + Background: + Given the following "users" exist: + | username | firstname | lastname | email | + | teacher1 | T1 | Teacher1 | teacher1@example.com | + | teacher2 | T2 | Teacher2 | teacher2@example.com | + And the following "courses" exist: + | fullname | shortname | category | + | Course 1 | C1 | 0 | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + | teacher2 | C1 | editingteacher | + And the following "activities" exist: + | activity | name | course | idnumber | + | quiz | Test quiz | C1 | quiz1 | + And the following "question categories" exist: + | contextlevel | reference | name | + | Course | C1 | Test questions | + And the following "questions" exist: + | questioncategory | qtype | name | questiontext | + | Test questions | truefalse | First question | Answer the first question | + + @javascript + Scenario: Add a comment in question + Given I log in as "teacher1" + And I am on the "Test quiz" "quiz activity" page + And I navigate to "Question bank > Questions" in current page administration + And I set the field "Select a category" to "Test questions" + And I should see "0" on the comments column + When I click "0" on the row on the comments column + And I add "Super test comment 01" comment to question + And I click on "Add comment" "button" in the ".modal-dialog" "css_element" + And I should see "Super test comment 01" + And I click on "Close" "button" in the ".modal-dialog" "css_element" + Then I should see "1" on the comments column + + @javascript + Scenario: Delete a comment from question + Given I log in as "teacher1" + And I am on the "Test quiz" "quiz activity" page + And I navigate to "Question bank > Questions" in current page administration + And I set the field "Select a category" to "Test questions" + And I should see "0" on the comments column + When I click "0" on the row on the comments column + And I add "Super test comment 01 to be deleted" comment to question + And I click on "Add comment" "button" in the ".modal-dialog" "css_element" + And I should see "Super test comment 01 to be deleted" + And I click on "Close" "button" in the ".modal-dialog" "css_element" + Then I should see "1" on the comments column + And I click "1" on the row on the comments column + And I delete "Super test comment 01 to be deleted" comment from question + And I should not see "Super test comment 01 to be deleted" + And I click on "Close" "button" in the ".modal-dialog" "css_element" + But I should see "0" on the comments column + + @javascript + Scenario: Preview question with comments + Given I log in as "teacher1" + And I am on the "Test quiz" "quiz activity" page + And I navigate to "Question bank > Questions" in current page administration + And I set the field "Select a category" to "Test questions" + And I choose "Preview" action for "First question" in the question bank + Then I should see "Save comment" + And I add "Super test comment 01" comment to question preview + And I click on "Save comment" "link" + And I wait "1" seconds + Then I should see "Super test comment 01" + And I click on "Close preview" "button" + Then I should see "1" on the comments column + And I choose "Preview" action for "First question" in the question bank + And I delete "Super test comment 01" comment from question + And I should not see "Super test comment 01" + And I click on "Close preview" "button" + Then I should see "0" on the comments column + + @javascript + Scenario: Teacher with comment permissions for their own questions but not others questions + Given I log in as "admin" + And I set the following system permissions of "Teacher" role: + | capability | permission | + | moodle/question:commentmine | Allow | + | moodle/question:commentall | Prevent | + And I log out + Then I log in as "teacher1" + And I am on the "Test quiz" "quiz activity" page + And I navigate to "Question bank > Questions" in current page administration + And I set the field "Select a category" to "Test questions" + And I choose "Preview" action for "First question" in the question bank + Then I should not see "Save comment" + And I click on "Close preview" "button" + Then I click on "Create a new question ..." "button" + And I set the field "item_qtype_essay" to "1" + And I press "submitbutton" + Then I should see "Adding an Essay question" + And I set the field "Question name" to "Essay 01 new" + And I set the field "Question text" to "Please write 200 words about Essay 01" + And I press "id_submitbutton" + Then I should see "Essay 01 new" + And I choose "Preview" action for "Essay 01 new" in the question bank + Then I should see "Save comment" + And I log out + Then I log in as "teacher2" + And I am on the "Test quiz" "quiz activity" page + And I navigate to "Question bank > Questions" in current page administration + And I set the field "Select a category" to "Test questions" + And I choose "Preview" action for "First question" in the question bank + Then I should not see "Save comment" + And I click on "Close preview" "button" + And I choose "Preview" action for "Essay 01 new" in the question bank + Then I should not see "Save comment" + And I click on "Close preview" "button" diff --git a/question/bank/comment/tests/behat/question_comment_column.feature b/question/bank/comment/tests/behat/question_comment_column.feature new file mode 100644 index 00000000000..aab197817a3 --- /dev/null +++ b/question/bank/comment/tests/behat/question_comment_column.feature @@ -0,0 +1,32 @@ +@qbank @qbank_comment +Feature: Use the qbank plugin manager page for comment + In order to check the plugin behaviour with enable and disable + + Background: + Given the following "courses" exist: + | fullname | shortname | category | + | Course 1 | C1 | 0 | + And the following "activities" exist: + | activity | name | course | idnumber | + | quiz | Test quiz | C1 | quiz1 | + And the following "question categories" exist: + | contextlevel | reference | name | + | Course | C1 | Test questions | + And the following "questions" exist: + | questioncategory | qtype | name | questiontext | + | Test questions | truefalse | First question | Answer the first question | + + @javascript + Scenario: Enable/disable comment column from the base view + Given I log in as "admin" + When I navigate to "Plugins > Question bank plugins > Manage question bank plugins" in site administration + And I should see "Question comment" + And I click on "Disable" "link" in the "Question comment" "table_row" + And I am on the "Test quiz" "quiz activity" page + And I navigate to "Question bank > Questions" in current page administration + Then "#categoryquestions .header.commentcount" "css_element" should not be visible + And I navigate to "Plugins > Question bank plugins > Manage question bank plugins" in site administration + And I click on "Enable" "link" in the "Question comment" "table_row" + And I am on the "Test quiz" "quiz activity" page + And I navigate to "Question bank > Questions" in current page administration + And "#categoryquestions .header.commentcount" "css_element" should be visible diff --git a/question/bank/comment/tests/event/comment_created_deleted_test.php b/question/bank/comment/tests/event/comment_created_deleted_test.php new file mode 100644 index 00000000000..fb1ebb752ae --- /dev/null +++ b/question/bank/comment/tests/event/comment_created_deleted_test.php @@ -0,0 +1,128 @@ +. + +namespace qbank_comment\event; + +use advanced_testcase; +use cache; +use comment; +use context; +use context_course; +use core_question_generator; +use question_edit_contexts; +use stdClass; + +/** + * Event tests for question comments. + * + * @package qbank_comment + * @copyright 2021 Catalyst IT Australia Pty Ltd + * @author Safat Shahin + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class qbank_comment_comment_created_deleted_test extends advanced_testcase { + + /** @var stdClass Keeps course object */ + private $course; + + /** @var context Keeps context */ + private $context; + + /** @var stdClass Keeps question object */ + private $questiondata; + + /** @var stdClass Keeps comment object */ + private $comment; + + /** + * Setup test data. + */ + public function setUp(): void { + global $CFG; + require_once($CFG->dirroot . '/comment/lib.php'); + + $this->resetAfterTest(); + $this->setAdminUser(); + $generator = $this->getDataGenerator(); + + /** @var core_question_generator $questiongenerator */ + $questiongenerator = $generator->get_plugin_generator('core_question'); + + // Create a course. + $this->course = $generator->create_course(); + $this->context = context_course::instance($this->course->id); + + // Create a question in the default category. + $contexts = new question_edit_contexts($this->context); + $cat = question_make_default_categories($contexts->all()); + $this->questiondata = $questiongenerator->create_question('numerical', null, + ['name' => 'Example question', 'category' => $cat->id]); + + // Ensure the question is not in the cache. + $cache = cache::make('core', 'questiondata'); + $cache->delete($this->questiondata->id); + + // Comment on question. + $args = new stdClass; + $args->context = $this->context; + $args->course = $this->course; + $args->area = 'question'; + $args->itemid = $this->questiondata->id; + $args->component = 'qbank_comment'; + $args->linktext = get_string('commentheader', 'qbank_comment'); + $args->notoggle = true; + $args->autostart = true; + $args->displaycancel = false; + $this->comment = new comment($args); + } + + /** + * Test comment_created event. + */ + public function test_comment_created() { + // Triggering and capturing the event. + $sink = $this->redirectEvents(); + $this->comment->add('New comment'); + $events = $sink->get_events(); + $this->assertCount(1, $events); + $event = reset($events); + + // Checking that the event contains the expected values. + $this->assertInstanceOf('\qbank_comment\event\comment_created', $event); + $this->assertEquals($this->context, $event->get_context()); + $this->assertStringContainsString('\'qbank_comment\' for the question with id \''.$this->questiondata->id.'\'', + $event->get_description()); + } + + /** + * Test comment_created event. + */ + public function test_comment_deleted() { + // Triggering and capturing the event. + $newcomment = $this->comment->add('New comment to delete'); + $sink = $this->redirectEvents(); + $this->comment->delete($newcomment->id); + $events = $sink->get_events(); + $this->assertCount(1, $events); + $event = reset($events); + + // Checking that the event contains the expected values. + $this->assertInstanceOf('\qbank_comment\event\comment_deleted', $event); + $this->assertEquals($this->context, $event->get_context()); + $this->assertStringContainsString('\'qbank_comment\' for the question with id \''.$this->questiondata->id.'\'', + $event->get_description()); + } +} diff --git a/question/bank/comment/tests/lib_test.php b/question/bank/comment/tests/lib_test.php new file mode 100644 index 00000000000..c56ffb7a517 --- /dev/null +++ b/question/bank/comment/tests/lib_test.php @@ -0,0 +1,126 @@ +. + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->dirroot . '/question/bank/comment/lib.php'); + + +/** + * Comment lib unit tests. + * + * @package qbank_comment + * @copyright 2021 Catalyst IT Australia Pty Ltd + * @author Matt Porritt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class qbank_comment_lib_test extends \advanced_testcase { + + /** + * Test the comment validation callback. + */ + public function test_qbank_comment_comment_validate() { + $commentparams = new \stdClass(); + $commentparams->commentarea = 'question'; + $commentparams->component = 'qbank_comment'; + + $isvalid = qbank_comment_comment_validate($commentparams); + $this->assertTrue($isvalid); + + $this->expectException('comment_exception'); + $commentparams->commentarea = 'core_comment'; + $commentparams->component = 'blog_comment'; + qbank_comment_comment_validate($commentparams); + + } + + /** + * Test the comment display callback. + */ + public function test_qbank_comment_comment_display() { + $comment = new \stdClass(); + $comment->text = 'test'; + $comments = [$comment]; + + $commentparams = new \stdClass(); + $commentparams->commentarea = 'question'; + $commentparams->component = 'qbank_comment'; + + $responses = qbank_comment_comment_display($comments, $commentparams); + $this->assertEquals($comment->text, $responses[0]->text); + + $this->expectException('comment_exception'); + $commentparams->commentarea = 'core_comment'; + $commentparams->component = 'blog_comment'; + qbank_comment_comment_display($comments, $commentparams); + + } + + /** + * Test the comment preview callback. + */ + public function test_qbank_comment_preview_display() { + $this->resetAfterTest(); + global $PAGE; + $PAGE->set_url('/'); + + // Make a test question. + $category = $this->getDataGenerator()->create_category(); + $course = $this->getDataGenerator()->create_course(['category' => $category->id]); + $qgen = $this->getDataGenerator()->get_plugin_generator('core_question'); + $context = context_coursecat::instance($category->id); + $qcat = $qgen->create_question_category(['contextid' => $context->id]); + $question = $qgen->create_question('shortanswer', null, ['category' => $qcat->id, 'idnumber' => 'q1']); + + $result = qbank_comment_preview_display($question, $course->id); + + // User doesn't have perms so expecting no output. + $this->assertEmpty($result); + + // Expect output. + $this->setAdminUser(); + $result = qbank_comment_preview_display($question, $course->id); + $this->assertStringContainsString('comment-action-post', $result); + } + + /** + * Test the comment preview callback. + */ + public function test_qbank_comment_output_fragment_question_comment() { + $this->resetAfterTest(); + $this->setAdminUser(); + global $PAGE; + $PAGE->set_url('/'); + + // Make a test question. + $category = $this->getDataGenerator()->create_category(); + $course = $this->getDataGenerator()->create_course(['category' => $category->id]); + $qgen = $this->getDataGenerator()->get_plugin_generator('core_question'); + $context = context_coursecat::instance($category->id); + $qcat = $qgen->create_question_category(['contextid' => $context->id]); + $question = $qgen->create_question('shortanswer', null, ['category' => $qcat->id, 'idnumber' => 'q1']); + $args = [ + 'questionid' => $question->id, + 'courseid' => $course->id, + ]; + + $result = qbank_comment_output_fragment_question_comment($args); + + // Expect output. + $this->assertStringContainsString('comment-action-post', $result); + } +} diff --git a/question/bank/comment/tests/privacy/provider_test.php b/question/bank/comment/tests/privacy/provider_test.php new file mode 100644 index 00000000000..65cc74cb1ab --- /dev/null +++ b/question/bank/comment/tests/privacy/provider_test.php @@ -0,0 +1,358 @@ +. + +namespace qbank_comment\privacy; + +use comment; +use context; +use context_course; +use core_privacy\local\metadata\collection; +use qbank_comment\privacy\provider; +use core_privacy\local\request\approved_userlist; +use stdClass; + +/** + * Privacy api tests. + * + * @package qbank_comment + * @copyright 2021 Catalyst IT Australia Pty Ltd + * @author Safat Shahin + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class qbank_comment_provider_test extends \core_privacy\tests\provider_testcase { + + /** @var stdClass A teacher who is only enrolled in course1. */ + protected $teacher1; + + /** @var stdClass A teacher who is only enrolled in course2. */ + protected $teacher2; + + /** @var stdClass A teacher who is enrolled in both course1 and course2. */ + protected $teacher3; + + /** @var stdClass A test course. */ + protected $course1; + + /** @var stdClass A test course. */ + protected $course2; + + /** + * Set up function for tests in this class. + */ + protected function setUp(): void { + global $DB; + + $this->resetAfterTest(); + $this->setAdminUser(); + + // Create courses. + $generator = $this->getDataGenerator(); + $this->course1 = $generator->create_course(); + $this->course2 = $generator->create_course(); + + // Create and enrol teachers. + $this->teacher1 = $generator->create_user(); + $this->teacher2 = $generator->create_user(); + $this->teacher3 = $generator->create_user(); + + $studentrole = $DB->get_record('role', ['shortname' => 'editingteacher']); + $generator->enrol_user($this->teacher1->id, $this->course1->id, $studentrole->id); + $generator->enrol_user($this->teacher2->id, $this->course2->id, $studentrole->id); + $generator->enrol_user($this->teacher3->id, $this->course1->id, $studentrole->id); + $generator->enrol_user($this->teacher3->id, $this->course2->id, $studentrole->id); + } + + /** + * Posts a comment on a given context. + * + * @param string $text The comment's text. + * @param context $context The context on which we want to put the comment. + */ + protected function add_comment($text, context $context) { + $args = new stdClass; + $args->context = $context; + $args->area = 'question'; + $args->itemid = 0; + $args->component = 'qbank_comment'; + $args->linktext = get_string('commentheader', 'qbank_comment'); + $args->notoggle = true; + $args->autostart = true; + $args->displaycancel = false; + $comment = new comment($args); + + $comment->add($text); + } + + /** + * Test for provider::get_metadata(). + */ + public function test_get_metadata() { + $collection = new collection('qbank_comment'); + $newcollection = provider::get_metadata($collection); + $itemcollection = $newcollection->get_collection(); + $this->assertCount(1, $itemcollection); + + $link = reset($itemcollection); + + $this->assertEquals('core_comment', $link->get_name()); + $this->assertEmpty($link->get_privacy_fields()); + $this->assertEquals('privacy:metadata:core_comment', $link->get_summary()); + } + + /** + * Test for provider::get_contexts_for_userid() when user had not posted any comments.. + */ + public function test_get_contexts_for_userid_no_comment() { + $this->setUser($this->teacher1); + $coursecontext1 = context_course::instance($this->course1->id); + $this->add_comment('New comment', $coursecontext1); + + $this->setUser($this->teacher2); + $contextlist = provider::get_contexts_for_userid($this->teacher2->id); + $this->assertCount(0, $contextlist); + } + + /** + * Test for provider::get_contexts_for_userid(). + */ + public function test_get_contexts_for_userid() { + $coursecontext1 = context_course::instance($this->course1->id); + $coursecontext2 = context_course::instance($this->course2->id); + + $this->setUser($this->teacher3); + $this->add_comment('New comment', $coursecontext1); + $this->add_comment('New comment', $coursecontext1); + $this->add_comment('New comment', $coursecontext2); + + $contextlist = provider::get_contexts_for_userid($this->teacher3->id); + $this->assertCount(2, $contextlist); + + $contextids = $contextlist->get_contextids(); + $this->assertEqualsCanonicalizing([$coursecontext1->id, $coursecontext2->id], $contextids); + } + + /** + * Test for provider::export_user_data() when the user has not posted any comments. + */ + public function test_export_for_context_no_comment() { + $coursecontext1 = context_course::instance($this->course1->id); + $coursecontext2 = context_course::instance($this->course2->id); + + $this->setUser($this->teacher1); + $this->add_comment('New comment', $coursecontext1); + + $this->setUser($this->teacher2); + + $this->setUser($this->teacher2); + $this->export_context_data_for_user($this->teacher1->id, $coursecontext2, 'qbank_comment'); + $writer = \core_privacy\local\request\writer::with_context($coursecontext2); + $this->assertFalse($writer->has_any_data()); + } + + /** + * Test for provider::export_user_data(). + */ + public function test_export_for_context() { + $coursecontext1 = context_course::instance($this->course1->id); + $coursecontext2 = context_course::instance($this->course2->id); + + $this->setUser($this->teacher3); + $this->add_comment('New comment', $coursecontext1); + $this->add_comment('New comment', $coursecontext1); + $this->add_comment('New comment', $coursecontext2); + + // Export all of the data for the context. + $this->export_context_data_for_user($this->teacher3->id, $coursecontext1, 'qbank_comment'); + $writer = \core_privacy\local\request\writer::with_context($coursecontext1); + $this->assertTrue($writer->has_any_data()); + } + + /** + * Test for provider::delete_data_for_all_users_in_context(). + */ + public function test_delete_data_for_all_users_in_context() { + global $DB; + + $coursecontext1 = context_course::instance($this->course1->id); + $coursecontext2 = context_course::instance($this->course2->id); + + $this->setUser($this->teacher1); + $this->add_comment('New comment', $coursecontext1); + + $this->setUser($this->teacher2); + $this->add_comment('New comment', $coursecontext2); + + $this->setUser($this->teacher3); + $this->add_comment('New comment', $coursecontext1); + $this->add_comment('New comment', $coursecontext1); + $this->add_comment('New comment', $coursecontext2); + + // Before deletion, we should have 3 comments in $coursecontext1 and 2 comments in $coursecontext2. + $this->assertEquals( + 3, + $DB->count_records('comments', ['component' => 'qbank_comment', 'contextid' => $coursecontext1->id]) + ); + $this->assertEquals( + 2, + $DB->count_records('comments', ['component' => 'qbank_comment', 'contextid' => $coursecontext2->id]) + ); + + // Delete data based on context. + provider::delete_data_for_all_users_in_context($coursecontext1); + + // After deletion, the comments for $coursecontext1 should have been deleted. + $this->assertEquals( + 0, + $DB->count_records('comments', ['component' => 'qbank_comment', 'contextid' => $coursecontext1->id]) + ); + $this->assertEquals( + 2, + $DB->count_records('comments', ['component' => 'qbank_comment', 'contextid' => $coursecontext2->id]) + ); + } + + /** + * Test for provider::delete_data_for_user(). + */ + public function test_delete_data_for_user() { + global $DB; + + $coursecontext1 = context_course::instance($this->course1->id); + $coursecontext2 = context_course::instance($this->course2->id); + + $this->setUser($this->teacher1); + $this->add_comment('New comment', $coursecontext1); + + $this->setUser($this->teacher2); + $this->add_comment('New comment', $coursecontext2); + + $this->setUser($this->teacher3); + $this->add_comment('New comment', $coursecontext1); + $this->add_comment('New comment', $coursecontext1); + $this->add_comment('New comment', $coursecontext2); + + // Before deletion, we should have 3 comments in $coursecontext1 and 2 comments in $coursecontext2, + // and 3 comments by student12 in $coursecontext1 and $coursecontext2 combined. + $this->assertEquals( + 3, + $DB->count_records('comments', ['component' => 'qbank_comment', 'contextid' => $coursecontext1->id]) + ); + $this->assertEquals( + 2, + $DB->count_records('comments', ['component' => 'qbank_comment', 'contextid' => $coursecontext2->id]) + ); + $this->assertEquals( + 3, + $DB->count_records('comments', ['component' => 'qbank_comment', 'userid' => $this->teacher3->id]) + ); + + $contextlist = new \core_privacy\local\request\approved_contextlist($this->teacher3, 'qbank_comment', + [$coursecontext1->id, $coursecontext2->id]); + provider::delete_data_for_user($contextlist); + + // After deletion, the comments for the student12 should have been deleted. + $this->assertEquals( + 1, + $DB->count_records('comments', ['component' => 'qbank_comment', 'contextid' => $coursecontext1->id]) + ); + $this->assertEquals( + 1, + $DB->count_records('comments', ['component' => 'qbank_comment', 'contextid' => $coursecontext2->id]) + ); + $this->assertEquals( + 0, + $DB->count_records('comments', ['component' => 'qbank_comment', 'userid' => $this->teacher3->id]) + ); + } + + /** + * Test that only users within a course context are fetched. + */ + public function test_get_users_in_context() { + $component = 'qbank_comment'; + + $coursecontext1 = context_course::instance($this->course1->id); + $coursecontext2 = context_course::instance($this->course2->id); + + $userlist1 = new \core_privacy\local\request\userlist($coursecontext1, $component); + provider::get_users_in_context($userlist1); + $this->assertCount(0, $userlist1); + + $userlist2 = new \core_privacy\local\request\userlist($coursecontext2, $component); + provider::get_users_in_context($userlist2); + $this->assertCount(0, $userlist2); + + $this->setUser($this->teacher3); + $this->add_comment('New comment', $coursecontext1); + $this->add_comment('New comment', $coursecontext2); + $this->setUser($this->teacher1); + $this->add_comment('New comment', $coursecontext1); + + // The list of users should contain teacher3 and user1. + provider::get_users_in_context($userlist1); + $this->assertCount(2, $userlist1); + $this->assertTrue(in_array($this->teacher1->id, $userlist1->get_userids())); + $this->assertTrue(in_array($this->teacher3->id, $userlist1->get_userids())); + + // The list of users should contain teacher3. + provider::get_users_in_context($userlist2); + $this->assertCount(1, $userlist2); + $expected = [$this->teacher3->id]; + $actual = $userlist2->get_userids(); + $this->assertEquals($expected, $actual); + } + + /** + * Test that data for users in approved userlist is deleted. + */ + public function test_delete_data_for_users() { + $component = 'qbank_comment'; + + $coursecontext1 = context_course::instance($this->course1->id); + $coursecontext2 = context_course::instance($this->course2->id); + + $this->setUser($this->teacher3); + $this->add_comment('New comment', $coursecontext1); + $this->add_comment('New comment', $coursecontext2); + $this->setUser($this->teacher1); + $this->add_comment('New comment', $coursecontext1); + + $userlist1 = new \core_privacy\local\request\userlist($coursecontext1, $component); + provider::get_users_in_context($userlist1); + $this->assertCount(2, $userlist1); + + $userlist2 = new \core_privacy\local\request\userlist($coursecontext2, $component); + provider::get_users_in_context($userlist2); + $this->assertCount(1, $userlist2); + + // Convert $userlist1 into an approved_contextlist. + $approvedlist1 = new approved_userlist($coursecontext1, $component, $userlist1->get_userids()); + // Delete using delete_data_for_user. + provider::delete_data_for_users($approvedlist1); + + // Re-fetch users in coursecontext1. + $userlist1 = new \core_privacy\local\request\userlist($coursecontext1, $component); + provider::get_users_in_context($userlist1); + // The user data in coursecontext1 should be deleted. + $this->assertCount(0, $userlist1); + + // Re-fetch users in coursecontext2. + $userlist2 = new \core_privacy\local\request\userlist($coursecontext2, $component); + provider::get_users_in_context($userlist2); + // The user data in coursecontext2 should be still present. + $this->assertCount(1, $userlist2); + } +} diff --git a/question/bank/comment/version.php b/question/bank/comment/version.php new file mode 100644 index 00000000000..c3450db91a2 --- /dev/null +++ b/question/bank/comment/version.php @@ -0,0 +1,31 @@ +. + +/** + * Version information for qbank_comment. + * + * @package qbank_comment + * @copyright 2021 Catalyst IT Australia Pty Ltd + * @author Safat Shahin + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->component = 'qbank_comment'; +$plugin->version = 2021090100; +$plugin->requires = 2021052500; +$plugin->maturity = MATURITY_STABLE; diff --git a/question/bank/previewquestion/tests/helper_test.php b/question/bank/previewquestion/tests/helper_test.php index 33214dcc110..22182ca2dbd 100644 --- a/question/bank/previewquestion/tests/helper_test.php +++ b/question/bank/previewquestion/tests/helper_test.php @@ -27,7 +27,7 @@ use core\plugininfo\qbank; * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @coversDefaultClass \qbank_previewquestion\helper */ -class helper_test extends \advanced_testcase { +class qbank_preview_helper_test extends \advanced_testcase { /** * @var bool|\context|\context_course $context @@ -148,10 +148,13 @@ class helper_test extends \advanced_testcase { * @covers ::get_preview_extra_elements */ public function test_get_preview_extra_elements() { + global $PAGE; + $PAGE->set_url('/'); + $question = \question_bank::load_question($this->questiondata->id); list($comment, $extraelements) = helper::get_preview_extra_elements($question, $this->context->instanceid); if (qbank::is_plugin_enabled('qbank_comment')) { - $this->assertEquals("
", $comment); + $this->assertStringContainsString("comment-area", $comment); } else { $this->assertEquals('', $comment); } diff --git a/version.php b/version.php index 0979d38f79b..8315141dcf0 100644 --- a/version.php +++ b/version.php @@ -29,7 +29,7 @@ defined('MOODLE_INTERNAL') || die(); -$version = 2021100600.00; // YYYYMMDD = weekly release date of this DEV branch. +$version = 2021100600.01; // YYYYMMDD = weekly release date of this DEV branch. // RR = release increments - 00 in DEV branches. // .XX = incremental changes. $release = '4.0dev+ (Build: 20211006)'; // Human-friendly version name