first commit

This commit is contained in:
CHIEFSOFT\ameye
2024-09-30 18:11:26 -04:00
commit e592ca6823
27270 changed files with 5002257 additions and 0 deletions
@@ -0,0 +1,63 @@
<?php
// 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 <http://www.gnu.org/licenses/>.
namespace mod_quiz\question\bank;
/**
* A column type for the add this question to the quiz action.
*
* @package mod_quiz
* @category question
* @copyright 2009 Tim Hunt
* @author 2021 Safat Shahin <safatshahin@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class add_action_column extends \core_question\local\bank\column_base {
/** @var string caches a lang string used repeatedly. */
protected $stradd;
public function init(): void {
parent::init();
$this->stradd = get_string('addtoquiz', 'quiz');
}
public function get_extra_classes(): array {
return ['iconcol'];
}
public function get_title(): string {
return '&#160;';
}
public function get_name() {
return 'addtoquizaction';
}
protected function display_content($question, $rowclasses) {
global $OUTPUT;
if (!question_has_capability_on($question, 'use')) {
return;
}
$link = new \action_link(
$this->qbank->add_to_quiz_url($question->id),
'',
null,
['title' => $this->stradd],
new \pix_icon('t/add', $this->stradd));
echo $OUTPUT->render($link);
}
}
@@ -0,0 +1,303 @@
<?php
// 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 <http://www.gnu.org/licenses/>.
/**
* Defines the custom question bank view used on the Edit quiz page.
*
* @package mod_quiz
* @category question
* @copyright 1999 onwards Martin Dougiamas and others {@link http://moodle.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_quiz\question\bank;
defined('MOODLE_INTERNAL') || die();
use core\output\datafilter;
use core_question\local\bank\column_base;
use core_question\local\bank\condition;
use core_question\local\bank\column_manager_base;
use core_question\local\bank\question_version_status;
use mod_quiz\question\bank\filter\custom_category_condition;
use qbank_managecategories\category_condition;
require_once($CFG->dirroot . '/mod/quiz/locallib.php');
/**
* Subclass to customise the view of the question bank for the quiz editing screen.
*
* @copyright 2009 Tim Hunt
* @author 2021 Safat Shahin <safatshahin@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class custom_view extends \core_question\local\bank\view {
/** @var int number of questions per page to show in the add from question bank modal. */
const DEFAULT_PAGE_SIZE = 20;
/** @var bool $quizhasattempts whether the quiz this is used by has been attemptd. */
protected $quizhasattempts = false;
/** @var \stdClass $quiz the quiz settings. */
protected $quiz = false;
/**
* @var string $component the component the api is used from.
*/
public $component = 'mod_quiz';
/**
* Constructor.
* @param \core_question\local\bank\question_edit_contexts $contexts
* @param \moodle_url $pageurl
* @param \stdClass $course course settings
* @param \stdClass $cm activity settings.
* @param \stdClass $quiz quiz settings.
*/
public function __construct($contexts, $pageurl, $course, $cm, $params, $extraparams) {
// Default filter condition.
if (!isset($params['filter'])) {
$params['filter'] = [];
[$categoryid, $contextid] = custom_category_condition::validate_category_param($params['cat']);
if (!is_null($categoryid)) {
$category = custom_category_condition::get_category_record($categoryid, $contextid);
$params['filter']['category'] = [
'jointype' => custom_category_condition::JOINTYPE_DEFAULT,
'values' => [$category->id],
'filteroptions' => ['includesubcategories' => false],
];
}
}
$this->init_columns($this->wanted_columns(), $this->heading_column());
parent::__construct($contexts, $pageurl, $course, $cm, $params, $extraparams);
[$this->quiz, ] = get_module_from_cmid($cm->id);
$this->set_quiz_has_attempts(quiz_has_attempts($this->quiz->id));
$this->pagesize = self::DEFAULT_PAGE_SIZE;
}
/**
* Just use the base column manager in this view.
*
* @return void
*/
protected function init_column_manager(): void {
$this->columnmanager = new column_manager_base();
}
/**
* Don't display plugin controls.
*
* @param \core\context $context
* @param int $categoryid
* @return string
*/
protected function get_plugin_controls(\core\context $context, int $categoryid): string {
return '';
}
protected function get_question_bank_plugins(): array {
$questionbankclasscolumns = [];
$customviewcolumns = [
'mod_quiz\question\bank\add_action_column' . column_base::ID_SEPARATOR . 'add_action_column',
'core_question\local\bank\checkbox_column' . column_base::ID_SEPARATOR . 'checkbox_column',
'qbank_viewquestiontype\question_type_column' . column_base::ID_SEPARATOR . 'question_type_column',
'mod_quiz\question\bank\question_name_text_column' . column_base::ID_SEPARATOR . 'question_name_text_column',
'mod_quiz\question\bank\preview_action_column' . column_base::ID_SEPARATOR . 'preview_action_column',
];
foreach ($customviewcolumns as $columnid) {
[$columnclass, $columnname] = explode(column_base::ID_SEPARATOR, $columnid, 2);
if (class_exists($columnclass)) {
$questionbankclasscolumns[$columnid] = $columnclass::from_column_name($this, $columnname);
}
}
return $questionbankclasscolumns;
}
protected function heading_column(): string {
return 'mod_quiz\\question\\bank\\question_name_text_column';
}
protected function default_sort(): array {
// Using the extended class for quiz specific sort.
return [
'qbank_viewquestiontype__question_type_column' => SORT_ASC,
'mod_quiz__question__bank__question_name_text_column' => SORT_ASC,
];
}
/**
* Let the question bank display know whether the quiz has been attempted,
* hence whether some bits of UI, like the add this question to the quiz icon,
* should be displayed.
*
* @param bool $quizhasattempts whether the quiz has attempts.
*/
private function set_quiz_has_attempts($quizhasattempts): void {
$this->quizhasattempts = $quizhasattempts;
if ($quizhasattempts && isset($this->visiblecolumns['addtoquizaction'])) {
unset($this->visiblecolumns['addtoquizaction']);
}
}
/**
* URL of add to quiz.
*
* @param $questionid
* @return \moodle_url
*/
public function add_to_quiz_url($questionid) {
$params = $this->baseurl->params();
$params['addquestion'] = $questionid;
$params['sesskey'] = sesskey();
$params['cmid'] = $this->cm->id;
return new \moodle_url('/mod/quiz/edit.php', $params);
}
/**
* Renders the html question bank (same as display, but returns the result).
*
* Note that you can only output this rendered result once per page, as
* it contains IDs which must be unique.
*
* @param array $pagevars
* @param string $tabname
* @return string HTML code for the form
*/
public function render($pagevars, $tabname): string {
ob_start();
$this->display();
$out = ob_get_contents();
ob_end_clean();
return $out;
}
protected function display_bottom_controls(\context $catcontext): void {
$cmoptions = new \stdClass();
$cmoptions->hasattempts = !empty($this->quizhasattempts);
$canuseall = has_capability('moodle/question:useall', $catcontext);
echo \html_writer::start_tag('div', ['class' => 'pt-2']);
if ($canuseall) {
// Add selected questions to the quiz.
$params = [
'type' => 'submit',
'name' => 'add',
'class' => 'btn btn-primary',
'value' => get_string('addselectedquestionstoquiz', 'quiz'),
'data-action' => 'toggle',
'data-togglegroup' => 'qbank',
'data-toggle' => 'action',
'disabled' => true,
];
echo \html_writer::empty_tag('input', $params);
}
echo \html_writer::end_tag('div');
}
/**
* Override the base implementation in \core_question\local\bank\view
* because we don't want to print new question form in the fragment
* for the modal.
*
* @param false|mixed|\stdClass $category
* @param bool $canadd
*/
protected function create_new_question_form($category, $canadd): void {
}
/**
* Override the base implementation in \core_question\local\bank\view
* because we don't want to print the headers in the fragment
* for the modal.
*/
protected function display_question_bank_header(): void {
}
protected function build_query(): void {
// Get the required tables and fields.
[$fields, $joins] = $this->get_component_requirements(array_merge($this->requiredcolumns, $this->questionactions));
// Build the order by clause.
$sorts = [];
foreach ($this->sort as $sortname => $sortorder) {
[$colname, $subsort] = $this->parse_subsort($sortname);
$sorts[] = $this->requiredcolumns[$colname]->sort_expression($sortorder == SORT_DESC, $subsort);
}
// Build the where clause.
$latestversion = '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)';
$onlyready = '((' . "qv.status = '" . question_version_status::QUESTION_STATUS_READY . "'" .'))';
$this->sqlparams = [];
$conditions = [];
foreach ($this->searchconditions as $searchcondition) {
if ($searchcondition->where()) {
$conditions[] = '((' . $searchcondition->where() .'))';
}
if ($searchcondition->params()) {
$this->sqlparams = array_merge($this->sqlparams, $searchcondition->params());
}
}
$majorconditions = ['q.parent = 0', $latestversion, $onlyready];
// Get higher level filter condition.
$jointype = isset($this->pagevars['jointype']) ? (int)$this->pagevars['jointype'] : condition::JOINTYPE_DEFAULT;
$nonecondition = ($jointype === datafilter::JOINTYPE_NONE) ? ' NOT ' : '';
$separator = ($jointype === datafilter::JOINTYPE_ALL) ? ' AND ' : ' OR ';
// Build the SQL.
$sql = ' FROM {question} q ' . implode(' ', $joins);
$sql .= ' WHERE ' . implode(' AND ', $majorconditions);
if (!empty($conditions)) {
$sql .= ' AND ' . $nonecondition . ' ( ';
$sql .= implode($separator, $conditions);
$sql .= ' ) ';
}
$this->countsql = 'SELECT count(1)' . $sql;
$this->loadsql = 'SELECT ' . implode(', ', $fields) . $sql . ' ORDER BY ' . implode(', ', $sorts);
}
public function add_standard_search_conditions(): void {
foreach ($this->plugins as $componentname => $plugin) {
if (\core\plugininfo\qbank::is_plugin_enabled($componentname)) {
$pluginentrypointobject = new $plugin();
if ($componentname === 'qbank_managecategories') {
$pluginentrypointobject = new quiz_managecategories_feature();
}
if ($componentname === 'qbank_viewquestiontext' || $componentname === 'qbank_deletequestion') {
continue;
}
$pluginobjects = $pluginentrypointobject->get_question_filters($this);
foreach ($pluginobjects as $pluginobject) {
$this->add_searchcondition($pluginobject, $pluginobject->get_condition_key());
}
}
}
}
/**
* Return the quiz settings for the quiz this question bank is displayed in.
*
* @return bool|\stdClass
*/
public function get_quiz() {
return $this->quiz;
}
}
@@ -0,0 +1,51 @@
<?php
// 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 <http://www.gnu.org/licenses/>.
namespace mod_quiz\question\bank\filter;
use qbank_managecategories\helper;
/**
* A custom filter condition for quiz to select question categories.
*
* This is required as quiz will only use ready questions and the count should show according to that.
*
* @package mod_quiz
* @category question
* @copyright 2021 Catalyst IT Australia Pty Ltd
* @author Safat Shahin <safatshahin@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class custom_category_condition extends \qbank_managecategories\category_condition {
public function get_initial_values() {
$catmenu = custom_category_condition_helper::question_category_options($this->contexts, true, 0, true, -1, false);
$values = [];
foreach ($catmenu as $menu) {
foreach ($menu as $catlist) {
foreach ($catlist as $key => $value) {
$values[] = (object) [
// Remove contextid from value.
'value' => strpos($key, ',') === false ? $key : substr($key, 0, strpos($key, ',')),
'title' => $value,
'selected' => ($key === $this->cat),
];
}
}
}
return $values;
}
}
@@ -0,0 +1,129 @@
<?php
// 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 <http://www.gnu.org/licenses/>.
namespace mod_quiz\question\bank\filter;
use core_question\local\bank\question_version_status;
/**
* A custom filter condition helper for quiz to select question categories.
*
* This is required as quiz will only use ready questions and the count should show according to that.
*
* @package mod_quiz
* @category question
* @copyright 2021 Catalyst IT Australia Pty Ltd
* @author Safat Shahin <safatshahin@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class custom_category_condition_helper extends \qbank_managecategories\helper {
public static function question_category_options(array $contexts, bool $top = false, int $currentcat = 0,
bool $popupform = false, int $nochildrenof = -1,
bool $escapecontextnames = true): array {
global $CFG;
$pcontexts = [];
foreach ($contexts as $context) {
$pcontexts[] = $context->id;
}
$contextslist = join(', ', $pcontexts);
$categories = self::get_categories_for_contexts($contextslist, 'parent, sortorder, name ASC', $top);
if ($top) {
$categories = self::question_fix_top_names($categories);
}
$categories = self::question_add_context_in_key($categories);
$categories = self::add_indented_names($categories, $nochildrenof);
// Sort cats out into different contexts.
$categoriesarray = [];
foreach ($pcontexts as $contextid) {
$context = \context::instance_by_id($contextid);
$contextstring = $context->get_context_name(true, true, $escapecontextnames);
foreach ($categories as $category) {
if ($category->contextid == $contextid) {
$cid = $category->id;
if ($currentcat != $cid || $currentcat == 0) {
$a = new \stdClass;
$a->name = format_string($category->indentedname, true,
['context' => $context]);
if ($category->idnumber !== null && $category->idnumber !== '') {
$a->idnumber = s($category->idnumber);
}
if (!empty($category->questioncount)) {
$a->questioncount = $category->questioncount;
}
if (isset($a->idnumber) && isset($a->questioncount)) {
$formattedname = get_string('categorynamewithidnumberandcount', 'question', $a);
} else if (isset($a->idnumber)) {
$formattedname = get_string('categorynamewithidnumber', 'question', $a);
} else if (isset($a->questioncount)) {
$formattedname = get_string('categorynamewithcount', 'question', $a);
} else {
$formattedname = $a->name;
}
$categoriesarray[$contextstring][$cid] = $formattedname;
}
}
}
}
if ($popupform) {
$popupcats = [];
foreach ($categoriesarray as $contextstring => $optgroup) {
$group = [];
foreach ($optgroup as $key => $value) {
$key = str_replace($CFG->wwwroot, '', $key);
$group[$key] = $value;
}
$popupcats[] = [$contextstring => $group];
}
return $popupcats;
} else {
return $categoriesarray;
}
}
public static function get_categories_for_contexts($contexts, string $sortorder = 'parent, sortorder, name ASC',
bool $top = false, int $showallversions = 0): array {
global $DB;
$topwhere = $top ? '' : 'AND c.parent <> 0';
$statuscondition = "AND qv.status = '". question_version_status::QUESTION_STATUS_READY . "' ";
$sql = "SELECT c.*,
(SELECT COUNT(1)
FROM {question} q
JOIN {question_versions} qv ON qv.questionid = q.id
JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
WHERE q.parent = '0'
$statuscondition
AND c.id = qbe.questioncategoryid
AND ($showallversions = 1
OR (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)
)
)
) AS questioncount
FROM {question_categories} c
WHERE c.contextid IN ($contexts) $topwhere
ORDER BY $sortorder";
return $DB->get_records_sql($sql);
}
}
@@ -0,0 +1,58 @@
<?php
// 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 <http://www.gnu.org/licenses/>.
namespace mod_quiz\question\bank;
/**
* A column type for the preview question action.
*
* @package mod_quiz
* @category question
* @copyright 2023 Catalyst IT Europe Ltd.
* @author Mark Johnson <mark.johnson@catalyst-eu.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class preview_action_column extends \core_question\local\bank\column_base {
public function get_extra_classes(): array {
return ['iconcol'];
}
#[\Override]
public function get_title(): string {
return '&#160;';
}
#[\Override]
public function get_name() {
return 'previewquestionaction';
}
#[\Override]
public function get_default_width(): int {
return 45;
}
#[\Override]
protected function display_content($question, $rowclasses) {
global $PAGE;
if (!question_has_capability_on($question, 'use')) {
return;
}
$editrenderer = $PAGE->get_renderer('quiz', 'edit');
echo $editrenderer->question_preview_icon($this->qbank->get_quiz(), $question);
}
}
@@ -0,0 +1,375 @@
<?php
// 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 <http://www.gnu.org/licenses/>.
namespace mod_quiz\question\bank;
use context_module;
use core_question\local\bank\question_version_status;
use core_question\local\bank\random_question_loader;
use core_question\question_reference_manager;
use qbank_tagquestion\tag_condition;
use qubaid_condition;
use stdClass;
defined('MOODLE_INTERNAL') || die();
require_once($CFG->dirroot . '/question/engine/bank.php');
/**
* Helper class for question bank and its associated data.
*
* @package mod_quiz
* @category question
* @copyright 2021 Catalyst IT Australia Pty Ltd
* @author Safat Shahin <safatshahin@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class qbank_helper {
/**
* Get the available versions of a question where one of the version has the given question id.
*
* @param int $questionid id of a question.
* @return stdClass[] other versions of this question. Each object has fields versionid,
* version and questionid. Array is returned most recent version first.
*/
public static function get_version_options(int $questionid): array {
global $DB;
return $DB->get_records_sql("
SELECT allversions.id AS versionid,
allversions.version,
allversions.questionid
FROM {question_versions} allversions
WHERE allversions.questionbankentryid = (
SELECT givenversion.questionbankentryid
FROM {question_versions} givenversion
WHERE givenversion.questionid = ?
)
AND allversions.status <> ?
ORDER BY allversions.version DESC
", [$questionid, question_version_status::QUESTION_STATUS_DRAFT]);
}
/**
* Get the information about which questions should be used to create a quiz attempt.
*
* Each element in the returned array is indexed by slot.slot (slot number) an each object hass:
* - All the field of the slot table.
* - contextid for where the question(s) come from.
* - category id for where the questions come from.
* - For non-random questions, All the fields of the question table (but id is in questionid).
* Also question version and question bankentryid.
* - For random questions, filtercondition, which is also unpacked into category, randomrecurse,
* randomtags, and note that these also have a ->name set and ->qtype set to 'random'.
*
* @param int $quizid the id of the quiz to load the data for.
* @param context_module $quizcontext the context of this quiz.
* @param int|null $slotid optional, if passed only load the data for this one slot (if it is in this quiz).
* @return array indexed by slot, with information about the content of each slot.
*/
public static function get_question_structure(int $quizid, context_module $quizcontext,
int $slotid = null): array {
global $DB;
$params = [
'draft' => question_version_status::QUESTION_STATUS_DRAFT,
'quizcontextid' => $quizcontext->id,
'quizcontextid2' => $quizcontext->id,
'quizcontextid3' => $quizcontext->id,
'quizid' => $quizid,
'quizid2' => $quizid,
];
$slotidtest = '';
$slotidtest2 = '';
if ($slotid !== null) {
$params['slotid'] = $slotid;
$params['slotid2'] = $slotid;
$slotidtest = ' AND slot.id = :slotid';
$slotidtest2 = ' AND lslot.id = :slotid2';
}
// Load all the data about each slot.
$slotdata = $DB->get_records_sql("
SELECT slot.slot,
slot.id AS slotid,
slot.page,
slot.displaynumber,
slot.requireprevious,
slot.maxmark,
slot.quizgradeitemid,
qsr.filtercondition,
qsr.usingcontextid,
qv.status,
qv.id AS versionid,
qv.version,
qr.version AS requestedversion,
qv.questionbankentryid,
q.id AS questionid,
q.*,
qc.id AS category,
COALESCE(qc.contextid, qsr.questionscontextid) AS contextid
FROM {quiz_slots} slot
-- case where a particular question has been added to the quiz.
LEFT JOIN {question_references} qr ON qr.usingcontextid = :quizcontextid AND qr.component = 'mod_quiz'
AND qr.questionarea = 'slot' AND qr.itemid = slot.id
LEFT JOIN {question_bank_entries} qbe ON qbe.id = qr.questionbankentryid
-- This way of getting the latest version for each slot is a bit more complicated
-- than we would like, but the simpler SQL did not work in Oracle 11.2.
-- (It did work fine in Oracle 19.x, so once we have updated our min supported
-- version we could consider digging the old code out of git history from
-- just before the commit that added this comment.
-- For relevant question_bank_entries, this gets the latest non-draft slot number.
LEFT JOIN (
SELECT lv.questionbankentryid,
MAX(CASE WHEN lv.status <> :draft THEN lv.version END) AS usableversion,
MAX(lv.version) AS anyversion
FROM {quiz_slots} lslot
JOIN {question_references} lqr ON lqr.usingcontextid = :quizcontextid2 AND lqr.component = 'mod_quiz'
AND lqr.questionarea = 'slot' AND lqr.itemid = lslot.id
JOIN {question_versions} lv ON lv.questionbankentryid = lqr.questionbankentryid
WHERE lslot.quizid = :quizid2
$slotidtest2
AND lqr.version IS NULL
GROUP BY lv.questionbankentryid
) latestversions ON latestversions.questionbankentryid = qr.questionbankentryid
LEFT JOIN {question_versions} qv ON qv.questionbankentryid = qbe.id
-- Either specified version, or latest usable version, or a draft version.
AND qv.version = COALESCE(qr.version,
latestversions.usableversion,
latestversions.anyversion)
LEFT JOIN {question_categories} qc ON qc.id = qbe.questioncategoryid
LEFT JOIN {question} q ON q.id = qv.questionid
-- Case where a random question has been added.
LEFT JOIN {question_set_references} qsr ON qsr.usingcontextid = :quizcontextid3 AND qsr.component = 'mod_quiz'
AND qsr.questionarea = 'slot' AND qsr.itemid = slot.id
WHERE slot.quizid = :quizid
$slotidtest
ORDER BY slot.slot
", $params);
// Unpack the random info from question_set_reference.
foreach ($slotdata as $slot) {
// Ensure the right id is the id.
$slot->id = $slot->slotid;
if ($slot->filtercondition) {
// Unpack the information about a random question.
$slot->questionid = 's' . $slot->id; // Sometimes this is used as an array key, so needs to be unique.
$filter = json_decode($slot->filtercondition, true);
$slot->filtercondition = question_reference_manager::convert_legacy_set_reference_filter_condition($filter);
$slot->category = $slot->filtercondition['filter']['category']['values'][0] ?? 0;
$slot->qtype = 'random';
$slot->name = get_string('random', 'quiz');
$slot->length = 1;
} else if ($slot->qtype === null) {
// This question must have gone missing. Put in a placeholder.
$slot->questionid = 's' . $slot->id; // Sometimes this is used as an array key, so needs to be unique.
$slot->category = 0;
$slot->qtype = 'missingtype';
$slot->name = get_string('missingquestion', 'quiz');
$slot->questiontext = ' ';
$slot->questiontextformat = FORMAT_HTML;
$slot->length = 1;
} else if (!\question_bank::qtype_exists($slot->qtype)) {
// Question of unknown type found in the database. Set to placeholder question types instead.
$slot->qtype = 'missingtype';
} else {
$slot->_partiallyloaded = 1;
}
}
return $slotdata;
}
/**
* Get this list of random selection tag ids from one of the slots returned by get_question_structure.
*
* @param stdClass $slotdata one of the array elements returned by get_question_structure.
* @return array list of tag ids.
*/
public static function get_tag_ids_for_slot(stdClass $slotdata): array {
$tagids = [];
if (!isset($slotdata->filtercondition['filter'])) {
return $tagids;
}
$filter = $slotdata->filtercondition['filter'];
if (isset($filter['qtagids'])) {
$tagids = $filter['qtagids']['values'];
}
return $tagids;
}
/**
* Given a slot from the array returned by get_question_structure, describe the random question it represents.
*
* @param stdClass $slotdata one of the array elements returned by get_question_structure.
* @return string that can be used to display the random slot.
*/
public static function describe_random_question(stdClass $slotdata): string {
$qtagids = self::get_tag_ids_for_slot($slotdata);
if ($qtagids) {
$tagnames = [];
$tags = \core_tag_tag::get_bulk($qtagids, 'id, name');
foreach ($tags as $tag) {
$tagnames[] = $tag->name;
}
$description = get_string('randomqnametags', 'mod_quiz', implode(",", $tagnames));
} else {
$description = get_string('randomqname', 'mod_quiz');
}
return shorten_text($description, 255);
}
/**
* Choose question for redo in a particular slot.
*
* @param int $quizid the id of the quiz to load the data for.
* @param context_module $quizcontext the context of this quiz.
* @param int $slotid optional, if passed only load the data for this one slot (if it is in this quiz).
* @param qubaid_condition $qubaids attempts to consider when avoiding picking repeats of random questions.
* @return int the id of the question to use.
*/
public static function choose_question_for_redo(int $quizid, context_module $quizcontext,
int $slotid, qubaid_condition $qubaids): int {
$slotdata = self::get_question_structure($quizid, $quizcontext, $slotid);
$slotdata = reset($slotdata);
// Non-random question.
if ($slotdata->qtype != 'random') {
return $slotdata->questionid;
}
// Random question.
$randomloader = new random_question_loader($qubaids, []);
$fitlercondition = $slotdata->filtercondition;
$filter = $fitlercondition['filter'] ?? [];
$newqusetionid = $randomloader->get_next_filtered_question_id($filter);
if ($newqusetionid === null) {
throw new \moodle_exception('notenoughrandomquestions', 'quiz');
}
return $newqusetionid;
}
/**
* Check all the questions in an attempt and return information about their versions.
*
* Once a quiz attempt has been started, it continues to use the version of each question
* it was started with. This checks the version used for each question, against the
* quiz settings for that slot, and returns which version would be used if the quiz
* attempt was being started now.
*
* There are several cases for each slot:
* - If this slot is currently set to use version 'Always latest' (which includes
* random slots) and if there is now a newer version than the one in the attempt,
* use that.
* - If the slot is currently set to use a fixed version of the question, and that
* is different from the version currently in the attempt, use that.
* - Otherwise, use the same version.
*
* This is used in places like the re-grade code.
*
* The returned data probably contains a bit more information than is strictly needed,
* (see the SQL for details) but returning a few extra ints is fast, and this could
* prove invaluable when debugging. The key information is probably:
* - questionattemptslot <-- array key
* - questionattemptid
* - currentversion
* - currentquestionid
* - newversion
* - newquestionid
*
* @param stdClass $attempt a quiz_attempt database row.
* @param context_module $quizcontext the quiz context for the quiz the attempt belongs to.
* @return array for each question_attempt in the quiz attempt, information about whether it is using
* the latest version of the question. Array indexed by questionattemptslot.
*/
public static function get_version_information_for_questions_in_attempt(
stdClass $attempt,
context_module $quizcontext,
): array {
global $DB;
return $DB->get_records_sql("
SELECT qa.slot AS questionattemptslot,
qa.id AS questionattemptid,
slot.slot AS quizslot,
slot.id AS quizslotid,
qr.id AS questionreferenceid,
currentqv.version AS currentversion,
currentqv.questionid AS currentquestionid,
newqv.version AS newversion,
newqv.questionid AS newquestionid
-- Start with the question currently used in the attempt.
FROM {question_attempts} qa
JOIN {question_versions} currentqv ON currentqv.questionid = qa.questionid
-- Join in the question metadata which says if this is a qa from a 'Try another question like this one'.
JOIN {question_attempt_steps} firststep ON firststep.questionattemptid = qa.id
AND firststep.sequencenumber = 0
LEFT JOIN {question_attempt_step_data} otherslotinfo ON otherslotinfo.attemptstepid = firststep.id
AND otherslotinfo.name = :otherslotmetadataname
-- Join in the quiz slot information, and hence for non-random slots, the questino_reference.
JOIN {quiz_slots} slot ON slot.quizid = :quizid
AND slot.slot = COALESCE({$DB->sql_cast_char2int('otherslotinfo.value', true)}, qa.slot)
LEFT JOIN {question_references} qr ON qr.usingcontextid = :quizcontextid
AND qr.component = 'mod_quiz'
AND qr.questionarea = 'slot'
AND qr.itemid = slot.id
-- Finally, get the new version for this slot.
JOIN {question_versions} newqv ON newqv.questionbankentryid = currentqv.questionbankentryid
AND newqv.version = COALESCE(
-- If the quiz setting say use a particular version, use that.
qr.version,
-- Otherwise, we need the latest non-draft version of the current questions.
(SELECT MAX(version)
FROM {question_versions}
WHERE questionbankentryid = currentqv.questionbankentryid AND status <> :draft),
-- Otherwise, there is not a suitable other version, so stick with the current one.
currentqv.version
)
-- We want this for questions in the current attempt.
WHERE qa.questionusageid = :questionusageid
-- Order not essential, but fast and good for debugging.
ORDER BY qa.slot
", [
'otherslotmetadataname' => ':_originalslot',
'quizid' => $attempt->quiz,
'quizcontextid' => $quizcontext->id,
'draft' => question_version_status::QUESTION_STATUS_DRAFT,
'questionusageid' => $attempt->uniqueid,
]);
}
}
@@ -0,0 +1,72 @@
<?php
// 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 <http://www.gnu.org/licenses/>.
namespace mod_quiz\question\bank;
/**
* A column type for the name of the question name.
*
* @package mod_quiz
* @category question
* @copyright 2009 Tim Hunt
* @author 2021 Safat Shahin <safatshahin@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class question_name_column extends \core_question\local\bank\column_base {
/**
* @var null $checkboxespresent
*/
protected $checkboxespresent = null;
public function get_name(): string {
return 'questionname';
}
public function get_title(): string {
return get_string('question');
}
protected function label_for($question): string {
if (is_null($this->checkboxespresent)) {
$this->checkboxespresent = $this->qbank->has_column('core_question\local\bank\checkbox_column');
}
if ($this->checkboxespresent) {
return 'checkq' . $question->id;
} else {
return '';
}
}
protected function display_content($question, $rowclasses): void {
$labelfor = $this->label_for($question);
if ($labelfor) {
echo \html_writer::start_tag('label', ['for' => $labelfor]);
}
echo format_string($question->name);
if ($labelfor) {
echo \html_writer::end_tag('label');
}
}
public function get_required_fields(): array {
return ['q.id', 'q.name'];
}
public function is_sortable() {
return 'q.name';
}
}
@@ -0,0 +1,69 @@
<?php
// 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 <http://www.gnu.org/licenses/>.
namespace mod_quiz\question\bank;
/**
* A column type for the name followed by the start of the question text.
*
* @package mod_quiz
* @category question
* @copyright 2009 Tim Hunt
* @author 2021 Safat Shahin <safatshahin@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class question_name_text_column extends question_name_column {
#[\Override]
public function get_name(): string {
return 'questionnametext';
}
#[\Override]
public function get_default_width(): int {
// In the places this is used, this seems to make it use all the available space, without overflowing.
return 800;
}
#[\Override]
protected function display_content($question, $rowclasses): void {
echo \html_writer::start_tag('div');
$labelfor = $this->label_for($question);
if ($labelfor) {
echo \html_writer::start_tag('label', ['for' => $labelfor]);
}
echo quiz_question_tostring($question, false, true, true, $question->tags);
if ($labelfor) {
echo \html_writer::end_tag('label');
}
echo \html_writer::end_tag('div');
}
#[\Override]
public function get_required_fields(): array {
$fields = parent::get_required_fields();
$fields[] = 'q.questiontext';
$fields[] = 'q.questiontextformat';
$fields[] = 'qbe.idnumber';
return $fields;
}
#[\Override]
public function load_additional_data(array $questions) {
parent::load_additional_data($questions);
parent::load_question_tags($questions);
}
}
@@ -0,0 +1,39 @@
<?php
// 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 <http://www.gnu.org/licenses/>.
namespace mod_quiz\question\bank;
use core_question\local\bank\view;
use mod_quiz\question\bank\filter\custom_category_condition;
/**
* Class quiz_managecategories_feature
*
* Overrides the default categories feature with a custom category condition.
*
* @package mod_quiz
* @copyright 2022 Catalyst IT EU Ltd.
* @author Mark Johnson <mark.johnson@catalyst-eu.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class quiz_managecategories_feature extends \qbank_managecategories\plugin_feature {
public function get_question_filters(view $qbank = null): array {
return [
new custom_category_condition($qbank),
];
}
}
@@ -0,0 +1,42 @@
<?php
// 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 <http://www.gnu.org/licenses/>.
namespace mod_quiz\question\bank;
use qbank_viewquestiontype\question_type_column;
/**
* Subclass to customise the view of the question bank for the quiz editing screen.
*
* @package mod_quiz
* @copyright 2022 Catalyst IT Australia Pty Ltd
* @author 2022 Nathan Nguyen <nathannguyen@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class random_question_view extends custom_view {
#[\Override]
protected function get_question_bank_plugins(): array {
return [
new question_type_column($this),
new question_name_text_column($this),
new preview_action_column($this),
];
}
#[\Override]
protected function display_bottom_controls(\context $catcontext): void {
}
}
@@ -0,0 +1,112 @@
<?php
// 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 <http://www.gnu.org/licenses/>.
namespace mod_quiz\question;
defined('MOODLE_INTERNAL') || die();
require_once($CFG->dirroot . '/question/engine/lib.php');
/**
* An extension of question_display_options that includes the extra options used by the quiz.
*
* @package mod_quiz
* @category question
* @copyright 2022 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class display_options extends \question_display_options {
/**
* The bitmask patterns use in the review option settings.
*
* In the quiz settings, the review... (e.g. reviewmarks) values are
* bit patterns that allow what is visible to be change at different times.
* These constants define which bit is for which time.
*
* @var int bit used to indicate 'during the attempt'.
*/
const DURING = 0x10000;
/** @var int as above, bit used to indicate 'immediately after the attempt'. */
const IMMEDIATELY_AFTER = 0x01000;
/** @var int as above, bit used to indicate 'later while the quiz is still open'. */
const LATER_WHILE_OPEN = 0x00100;
/** @var int as above, bit used to indicate 'after the quiz is closed'. */
const AFTER_CLOSE = 0x00010;
/**
* @var bool if this is false, then the student is not allowed to review
* anything about the attempt.
*/
public $attempt = true;
/**
* @var int whether the attempt overall feedback is visible.
*/
public $overallfeedback = self::VISIBLE;
/**
* Set up the various options from the quiz settings, and a time constant.
*
* @param \stdClass $quiz the quiz settings from the database.
* @param int $when of the constants {@see DURING}, {@see IMMEDIATELY_AFTER},
* {@see LATER_WHILE_OPEN} or {@see AFTER_CLOSE}.
* @return display_options instance of this class set up appropriately.
*/
public static function make_from_quiz(\stdClass $quiz, int $when): self {
$options = new self();
$options->attempt = self::extract($quiz->reviewattempt, $when, true, false);
$options->correctness = self::extract($quiz->reviewcorrectness, $when);
$options->marks = self::extract($quiz->reviewmaxmarks, $when,
self::extract($quiz->reviewmarks, $when, self::MARK_AND_MAX, self::MAX_ONLY), self::HIDDEN);
$options->feedback = self::extract($quiz->reviewspecificfeedback, $when);
$options->generalfeedback = self::extract($quiz->reviewgeneralfeedback, $when);
$options->rightanswer = self::extract($quiz->reviewrightanswer, $when);
$options->overallfeedback = self::extract($quiz->reviewoverallfeedback, $when);
$options->numpartscorrect = $options->feedback;
$options->manualcomment = $options->feedback;
if ($quiz->questiondecimalpoints != -1) {
$options->markdp = $quiz->questiondecimalpoints;
} else {
$options->markdp = $quiz->decimalpoints;
}
return $options;
}
/**
* Helper function to return one value or another depending on whether one bit is set.
*
* @param int $setting the setting to unpack (e.g. $quiz->reviewmarks).
* @param int $when of the constants {@see DURING}, {@see IMMEDIATELY_AFTER},
* {@see LATER_WHILE_OPEN} or {@see AFTER_CLOSE}.
* @param bool|int $whenset value to return when the bit is set.
* @param bool|int $whennotset value to return when the bit is set.
* @return bool|int $whenset or $whennotset, depending.
*/
protected static function extract(int $setting, int $when,
$whenset = self::VISIBLE, $whennotset = self::HIDDEN) {
if ($setting & $when) {
return $whenset;
} else {
return $whennotset;
}
}
}
@@ -0,0 +1,57 @@
<?php
// 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 <http://www.gnu.org/licenses/>.
namespace mod_quiz\question;
use mod_quiz\quiz_attempt;
defined('MOODLE_INTERNAL') || die();
require_once($CFG->dirroot . '/question/engine/datalib.php');
/**
* A {@see qubaid_condition} for finding all the question usages belonging to a particular quiz.
*
* @package mod_quiz
* @category question
* @copyright 2010 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class qubaids_for_quiz extends \qubaid_join {
/**
* Constructor.
*
* @param int $quizid The quiz to search.
* @param bool $includepreviews Whether to include preview attempts
* @param bool $onlyfinished Whether to only include finished attempts or not
*/
public function __construct(int $quizid, bool $includepreviews = true, bool $onlyfinished = false) {
$where = 'quiza.quiz = :quizaquiz';
$params = ['quizaquiz' => $quizid];
if (!$includepreviews) {
$where .= ' AND preview = 0';
}
if ($onlyfinished) {
$where .= ' AND state = :statefinished';
$params['statefinished'] = quiz_attempt::FINISHED;
}
parent::__construct('{quiz_attempts} quiza', 'quiza.uniqueid', $where, $params);
}
}
@@ -0,0 +1,43 @@
<?php
// 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 <http://www.gnu.org/licenses/>.
namespace mod_quiz\question;
/**
* A {@see qubaid_condition} for finding all the question usages belonging to a particular user and quiz combination.
*
* @package mod_quiz
* @category question
* @copyright 2018 Andrew Nicols <andrwe@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @deprecated This class was never needed because qubaids_for_users_attempts already existed and is more flexible.
*/
class qubaids_for_quiz_user extends qubaids_for_users_attempts {
/**
* Constructor.
*
* @param int $quizid The quiz to search.
* @param int $userid The user to filter on
* @param bool $includepreviews Whether to include preview attempts
* @param bool $onlyfinished Whether to only include finished attempts or not
*/
public function __construct(int $quizid, int $userid,
bool $includepreviews = true, bool $onlyfinished = false) {
debugging('qubaids_for_quiz_user is deprecated. Please use qubaids_for_users_attempts instead.');
parent::__construct($quizid, $userid,
$onlyfinished ? 'finished' : 'all', $includepreviews);
}
}
@@ -0,0 +1,71 @@
<?php
// 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 <http://www.gnu.org/licenses/>.
namespace mod_quiz\question;
use mod_quiz\quiz_attempt;
defined('MOODLE_INTERNAL') || die();
require_once($CFG->dirroot . '/question/engine/datalib.php');
/**
* A {@see qubaid_condition} representing all the attempts by one user at a given quiz.
*
* @package mod_quiz
* @category question
* @copyright 2015 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class qubaids_for_users_attempts extends \qubaid_join {
/**
* Constructor.
*
* This takes the same arguments as {@link quiz_get_user_attempts()}.
*
* @param int $quizid the quiz id.
* @param int $userid the userid.
* @param string $status 'all', 'finished' or 'unfinished' to control
* @param bool $includepreviews defaults to false.
*/
public function __construct($quizid, $userid, $status = 'finished', $includepreviews = false) {
$where = 'quiza.quiz = :quizaquiz AND quiza.userid = :userid';
$params = ['quizaquiz' => $quizid, 'userid' => $userid];
if (!$includepreviews) {
$where .= ' AND preview = 0';
}
switch ($status) {
case 'all':
break;
case 'finished':
$where .= ' AND state IN (:state1, :state2)';
$params['state1'] = quiz_attempt::FINISHED;
$params['state2'] = quiz_attempt::ABANDONED;
break;
case 'unfinished':
$where .= ' AND state IN (:state1, :state2)';
$params['state1'] = quiz_attempt::IN_PROGRESS;
$params['state2'] = quiz_attempt::OVERDUE;
break;
}
parent::__construct('{quiz_attempts} quiza', 'quiza.uniqueid', $where, $params);
}
}