mirror of
https://github.com/moodle/moodle.git
synced 2025-08-04 16:36:37 +02:00
Merge branch 'master_MDL-71696-versioning-integration' of https://github.com/catalyst/moodle-MDL-70329
This commit is contained in:
commit
b841a811be
223 changed files with 7768 additions and 2899 deletions
|
@ -42,25 +42,53 @@ $qtypes = question_bank::get_all_qtypes();
|
|||
$pluginmanager = core_plugin_manager::instance();
|
||||
|
||||
// Get some data we will need - question counts and which types are needed.
|
||||
$counts = $DB->get_records_sql("
|
||||
SELECT qtype, COUNT(1) as numquestions, SUM(hidden) as numhidden
|
||||
FROM {question} GROUP BY qtype", array());
|
||||
$needed = array();
|
||||
$hiddenstatus = \core_question\local\bank\question_version_status::QUESTION_STATUS_HIDDEN;
|
||||
$draftstatus = \core_question\local\bank\question_version_status::QUESTION_STATUS_DRAFT;
|
||||
|
||||
$sql = "SELECT result.qtype,
|
||||
SUM(result.numquestions) AS numquestions,
|
||||
SUM(result.numhidden) AS numhidden,
|
||||
SUM(result.numdraft) AS numdraft
|
||||
FROM (SELECT data.qtype,
|
||||
COUNT(data.numquestions) AS numquestions,
|
||||
(SELECT COUNT(qv.id)
|
||||
FROM {question_versions} qv
|
||||
WHERE qv.id = data.versionid
|
||||
AND qv.status = :hiddenstatus) AS numhidden,
|
||||
(SELECT COUNT(qv.id)
|
||||
FROM {question_versions} qv
|
||||
WHERE qv.id = data.versionid
|
||||
AND qv.status = :draftstatus) AS numdraft
|
||||
FROM (SELECT q.qtype, qv.id AS versionid, 1 AS numquestions
|
||||
FROM {question} q
|
||||
JOIN {question_versions} qv ON qv.questionid = q.id
|
||||
JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
|
||||
AND qv.version = (SELECT MAX(v.version)
|
||||
FROM {question_versions} v
|
||||
JOIN {question_bank_entries} be ON be.id = v.questionbankentryid
|
||||
WHERE be.id = qbe.id)) data
|
||||
GROUP BY data.qtype, data.versionid) result
|
||||
GROUP BY result.qtype";
|
||||
|
||||
$counts = $DB->get_records_sql($sql, ['hiddenstatus' => $hiddenstatus, 'draftstatus' => $draftstatus]);
|
||||
$needed = [];
|
||||
foreach ($qtypes as $qtypename => $qtype) {
|
||||
if (!isset($counts[$qtypename])) {
|
||||
$counts[$qtypename] = new stdClass;
|
||||
$counts[$qtypename]->numquestions = 0;
|
||||
$counts[$qtypename]->numhidden = 0;
|
||||
$counts[$qtypename]->numdraft = 0;
|
||||
}
|
||||
$needed[$qtypename] = $counts[$qtypename]->numquestions > 0 ||
|
||||
$pluginmanager->other_plugins_that_require($qtype->plugin_name());
|
||||
$counts[$qtypename]->numquestions -= $counts[$qtypename]->numhidden;
|
||||
$counts[$qtypename]->numquestions -= ($counts[$qtypename]->numhidden + $counts[$qtypename]->numdraft);
|
||||
}
|
||||
$needed['missingtype'] = true; // The system needs the missing question type.
|
||||
foreach ($counts as $qtypename => $count) {
|
||||
if (!isset($qtypes[$qtypename])) {
|
||||
$counts['missingtype']->numquestions += $count->numquestions - $count->numhidden;
|
||||
$counts['missingtype']->numquestions += $count->numquestions - ($count->numhidden + $count->numdraft);
|
||||
$counts['missingtype']->numhidden += $count->numhidden;
|
||||
$counts['missingtype']->numdraft += $count->numdraft;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -153,8 +181,8 @@ foreach ($sortedqtypes as $qtypename => $localname) {
|
|||
$row[] = $icon . ' ' . $localname;
|
||||
|
||||
// Number of questions of this type.
|
||||
if ($counts[$qtypename]->numquestions + $counts[$qtypename]->numhidden > 0) {
|
||||
if ($counts[$qtypename]->numhidden > 0) {
|
||||
if ($counts[$qtypename]->numquestions + $counts[$qtypename]->numhidden + $counts[$qtypename]->numdraft > 0) {
|
||||
if ($counts[$qtypename]->numhidden + $counts[$qtypename]->numdraft > 0) {
|
||||
$strcount = get_string('numquestionsandhidden', 'question', $counts[$qtypename]);
|
||||
} else {
|
||||
$strcount = $counts[$qtypename]->numquestions;
|
||||
|
|
|
@ -20,9 +20,9 @@ use core\event\question_created;
|
|||
use core\event\question_updated;
|
||||
|
||||
/**
|
||||
* Base class for various question-related areas
|
||||
* Base class for various question-related areas.
|
||||
*
|
||||
* This is an abstract class so it will be skipped by manager when it finds all areas
|
||||
* This is an abstract class so it will be skipped by manager when it finds all areas.
|
||||
*
|
||||
* @package tool_brickfield
|
||||
* @copyright 2020 onward: Brickfield Education Labs, www.brickfield.ie
|
||||
|
@ -32,6 +32,7 @@ abstract class answerbase extends base {
|
|||
|
||||
/**
|
||||
* Get table name reference.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_ref_tablename(): string {
|
||||
|
@ -40,34 +41,38 @@ abstract class answerbase extends base {
|
|||
|
||||
/**
|
||||
* Find recordset of the relevant areas.
|
||||
*
|
||||
* @param \core\event\base $event
|
||||
* @return \moodle_recordset|null
|
||||
* @throws \coding_exception
|
||||
* @throws \dml_exception
|
||||
*/
|
||||
public function find_relevant_areas(\core\event\base $event): ?\moodle_recordset {
|
||||
global $DB;
|
||||
|
||||
if (($event instanceof question_created) || ($event instanceof question_updated)) {
|
||||
$rs = $DB->get_recordset_sql(
|
||||
"SELECT {$this->get_type()} AS type,
|
||||
$sql = "SELECT {$this->get_type()} AS type,
|
||||
ctx.id AS contextid,
|
||||
{$this->get_standard_area_fields_sql()}
|
||||
a.id AS itemid,
|
||||
{$this->get_reftable_field_sql()}
|
||||
t.id AS refid,
|
||||
q.id AS refid,
|
||||
{$this->get_course_and_cat_sql($event)}
|
||||
a.{$this->get_fieldname()} AS content
|
||||
FROM {question} t
|
||||
INNER JOIN {question_answers} a ON a.question = t.id
|
||||
INNER JOIN {question_categories} qc ON qc.id = t.category
|
||||
INNER JOIN {context} ctx ON ctx.id = qc.contextid
|
||||
WHERE (t.id = :refid)
|
||||
ORDER BY a.id",
|
||||
[
|
||||
'refid' => $event->objectid,
|
||||
]);
|
||||
FROM {question} q
|
||||
INNER JOIN {question_answers} a
|
||||
ON a.question = q.id
|
||||
INNER JOIN {question_versions} qv
|
||||
ON qv.questionid = q.id
|
||||
INNER JOIN {question_bank_entries} qbe
|
||||
ON qbe.id = qv.questionbankentryid
|
||||
INNER JOIN {question_categories} qc
|
||||
ON qc.id = qbe.questioncategoryid
|
||||
INNER JOIN {context} ctx
|
||||
ON ctx.id = qc.contextid
|
||||
WHERE (q.id = :refid)
|
||||
ORDER BY a.id";
|
||||
|
||||
$rs = $DB->get_recordset_sql($sql, ['refid' => $event->objectid]);
|
||||
return $rs;
|
||||
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
@ -75,61 +80,90 @@ abstract class answerbase extends base {
|
|||
/**
|
||||
* Return an array of area objects that contain content at the site and system levels only. This would be question content from
|
||||
* question categories at the system context, or course category context.
|
||||
*
|
||||
* @return mixed
|
||||
* @throws \dml_exception
|
||||
*/
|
||||
public function find_system_areas(): ?\moodle_recordset {
|
||||
global $DB;
|
||||
$select = 'SELECT ' . $this->get_type() . ' AS type, qc.contextid AS contextid, ' . $this->get_standard_area_fields_sql() .
|
||||
' a.id AS itemid, ' . $this->get_reftable_field_sql() . 't.id AS refid, '.
|
||||
SITEID . ' as courseid, cc.id as categoryid, a.'.$this->get_fieldname().' AS content ';
|
||||
$from = 'FROM {question} t ' .
|
||||
'INNER JOIN {question_answers} a ON a.question = t.id ' .
|
||||
'INNER JOIN {question_categories} qc ON qc.id = t.category ' .
|
||||
'INNER JOIN {context} ctx ON ctx.id = qc.contextid ' .
|
||||
'LEFT JOIN {course_categories} cc ON cc.id = ctx.instanceid AND ctx.contextlevel = :coursecat ';
|
||||
$where = 'WHERE (ctx.contextlevel = :syscontext) OR (ctx.contextlevel = :coursecat2) ';
|
||||
$order = 'ORDER BY a.id';
|
||||
$params = [
|
||||
'syscontext' => CONTEXT_SYSTEM,
|
||||
'coursecat' => CONTEXT_COURSECAT,
|
||||
'coursecat2' => CONTEXT_COURSECAT,
|
||||
];
|
||||
|
||||
return $DB->get_recordset_sql($select . $from . $where . $order, $params);
|
||||
$sql = "SELECT {$this->get_type()} AS type,
|
||||
qc.contextid AS contextid,
|
||||
{$this->get_standard_area_fields_sql()}
|
||||
a.id AS itemid,
|
||||
{$this->get_reftable_field_sql()}
|
||||
q.id AS refid,
|
||||
" . SITEID . " as courseid,
|
||||
cc.id as categoryid,
|
||||
a.{$this->get_fieldname()} AS content
|
||||
FROM {question} q
|
||||
INNER JOIN {question_answers} a
|
||||
ON a.question = q.id
|
||||
INNER JOIN {question_versions} qv
|
||||
ON qv.questionid = q.id
|
||||
INNER JOIN {question_bank_entries} qbe
|
||||
ON qbe.id = qv.questionbankentryid
|
||||
INNER JOIN {question_categories} qc
|
||||
ON qc.id = qbe.questioncategoryid
|
||||
INNER JOIN {context} ctx
|
||||
ON ctx.id = qc.contextid
|
||||
LEFT JOIN {course_categories} cc
|
||||
ON cc.id = ctx.instanceid
|
||||
AND ctx.contextlevel = :coursecat
|
||||
WHERE (ctx.contextlevel = :syscontext)
|
||||
OR (ctx.contextlevel = :coursecat2)
|
||||
ORDER BY a.id";
|
||||
|
||||
return $DB->get_recordset_sql($sql, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find recordset of the course areas.
|
||||
*
|
||||
* @param int $courseid
|
||||
* @return \moodle_recordset
|
||||
* @throws \coding_exception
|
||||
* @throws \dml_exception
|
||||
*/
|
||||
public function find_course_areas(int $courseid): ?\moodle_recordset {
|
||||
global $DB;
|
||||
|
||||
$coursecontext = \context_course::instance($courseid);
|
||||
return $DB->get_recordset_sql(
|
||||
"SELECT {$this->get_type()} AS type,
|
||||
$param = [
|
||||
'ctxcourse' => CONTEXT_COURSE,
|
||||
'courseid' => $courseid,
|
||||
'module' => CONTEXT_MODULE,
|
||||
'coursecontextpath' => $DB->sql_like_escape($coursecontext->path) . '/%',
|
||||
];
|
||||
|
||||
$sql = "SELECT {$this->get_type()} AS type,
|
||||
ctx.id AS contextid,
|
||||
{$this->get_standard_area_fields_sql()}
|
||||
a.id AS itemid,
|
||||
{$this->get_reftable_field_sql()}
|
||||
t.id AS refid,
|
||||
q.id AS refid,
|
||||
{$courseid} AS courseid,
|
||||
a.{$this->get_fieldname()} AS content
|
||||
FROM {question} t
|
||||
INNER JOIN {question_answers} a ON a.question = t.id
|
||||
INNER JOIN {question_categories} qc ON qc.id = t.category
|
||||
INNER JOIN {context} ctx ON ctx.id = qc.contextid
|
||||
WHERE (ctx.contextlevel = :ctxcourse AND ctx.id = qc.contextid AND ctx.instanceid = :courseid) OR
|
||||
(ctx.contextlevel = :module AND {$DB->sql_like('ctx.path', ':coursecontextpath')})
|
||||
ORDER BY a.id",
|
||||
['ctxcourse' => CONTEXT_COURSE,
|
||||
'courseid' => $courseid,
|
||||
'module' => CONTEXT_MODULE,
|
||||
'coursecontextpath' => $DB->sql_like_escape($coursecontext->path) . '/%',
|
||||
]);
|
||||
FROM {question} q
|
||||
INNER JOIN {question_answers} a
|
||||
ON a.question = q.id
|
||||
INNER JOIN {question_versions} qv
|
||||
ON qv.questionid = q.id
|
||||
INNER JOIN {question_bank_entries} qbe
|
||||
ON qbe.id = qv.questionbankentryid
|
||||
INNER JOIN {question_categories} qc
|
||||
ON qc.id = qbe.questioncategoryid
|
||||
INNER JOIN {context} ctx
|
||||
ON ctx.id = qc.contextid
|
||||
WHERE (ctx.contextlevel = :ctxcourse
|
||||
AND ctx.id = qc.contextid
|
||||
AND ctx.instanceid = :courseid)
|
||||
OR (ctx.contextlevel = :module
|
||||
AND {$DB->sql_like('ctx.path', ':coursecontextpath')})
|
||||
ORDER BY a.id ASC";
|
||||
|
||||
return $DB->get_recordset_sql($sql, $param);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,7 +21,7 @@ use core\event\question_updated;
|
|||
use tool_brickfield\area_base;
|
||||
|
||||
/**
|
||||
* Base class for various question-related areas
|
||||
* Base class for various question-related areas.
|
||||
*
|
||||
* This is an abstract class so it will be skipped by manager when it finds all areas
|
||||
*
|
||||
|
@ -33,30 +33,32 @@ abstract class base extends area_base {
|
|||
|
||||
/**
|
||||
* Find recordset of the relevant areas.
|
||||
*
|
||||
* @param \core\event\base $event
|
||||
* @return \moodle_recordset|null
|
||||
* @throws \coding_exception
|
||||
* @throws \dml_exception
|
||||
*/
|
||||
public function find_relevant_areas(\core\event\base $event): ?\moodle_recordset {
|
||||
global $DB;
|
||||
|
||||
if (($event instanceof question_created) || ($event instanceof question_updated)) {
|
||||
$rs = $DB->get_recordset_sql(
|
||||
"SELECT {$this->get_type()} AS type,
|
||||
$sql = "SELECT {$this->get_type()} AS type,
|
||||
ctx.id AS contextid,
|
||||
{$this->get_standard_area_fields_sql()}
|
||||
t.id AS itemid,
|
||||
q.id AS itemid,
|
||||
{$this->get_course_and_cat_sql($event)}
|
||||
t.{$this->get_fieldname()} AS content
|
||||
FROM {question} t
|
||||
INNER JOIN {question_categories} qc ON qc.id = t.category
|
||||
INNER JOIN {context} ctx ON ctx.id = qc.contextid
|
||||
WHERE (t.id = :refid)
|
||||
ORDER BY t.id",
|
||||
[
|
||||
'refid' => $event->objectid,
|
||||
]);
|
||||
q.{$this->get_fieldname()} AS content
|
||||
FROM {question} q
|
||||
INNER JOIN {question_versions} qv
|
||||
ON qv.questionid = q.id
|
||||
INNER JOIN {question_bank_entries} qbe
|
||||
ON qbe.id = qv.questionbankentryid
|
||||
INNER JOIN {question_categories} qc
|
||||
ON qc.id = qbe.questioncategoryid
|
||||
INNER JOIN {context} ctx
|
||||
ON ctx.id = qc.contextid
|
||||
WHERE (q.id = :refid)
|
||||
ORDER BY q.id";
|
||||
|
||||
$rs = $DB->get_recordset_sql($sql, ['refid' => $event->objectid]);
|
||||
return $rs;
|
||||
}
|
||||
return null;
|
||||
|
@ -64,75 +66,97 @@ abstract class base extends area_base {
|
|||
|
||||
/**
|
||||
* Find recordset of the course areas.
|
||||
*
|
||||
* @param int $courseid
|
||||
* @return \moodle_recordset
|
||||
* @throws \coding_exception
|
||||
* @throws \dml_exception
|
||||
*/
|
||||
public function find_course_areas(int $courseid): ?\moodle_recordset {
|
||||
global $DB;
|
||||
|
||||
$coursecontext = \context_course::instance($courseid);
|
||||
return $DB->get_recordset_sql(
|
||||
"SELECT {$this->get_type()} AS type,
|
||||
ctx.id AS contextid,
|
||||
{$this->get_standard_area_fields_sql()}
|
||||
t.id AS itemid,
|
||||
{$courseid} AS courseid,
|
||||
null AS categoryid,
|
||||
t.{$this->get_fieldname()} AS content
|
||||
FROM {question} t
|
||||
INNER JOIN {question_categories} qc ON qc.id = t.category
|
||||
INNER JOIN {context} ctx ON ctx.id = qc.contextid
|
||||
WHERE (ctx.contextlevel = :ctxcourse AND ctx.id = qc.contextid AND ctx.instanceid = :courseid) OR
|
||||
(ctx.contextlevel = :module AND {$DB->sql_like('ctx.path', ':coursecontextpath')})
|
||||
ORDER BY t.id ASC",
|
||||
[
|
||||
$param = [
|
||||
'ctxcourse' => CONTEXT_COURSE,
|
||||
'courseid' => $courseid,
|
||||
'module' => CONTEXT_MODULE,
|
||||
'coursecontextpath' => $DB->sql_like_escape($coursecontext->path) . '/%',
|
||||
]);
|
||||
];
|
||||
|
||||
$sql = "SELECT {$this->get_type()} AS type,
|
||||
ctx.id AS contextid,
|
||||
{$this->get_standard_area_fields_sql()}
|
||||
q.id AS itemid,
|
||||
{$courseid} AS courseid,
|
||||
null AS categoryid,
|
||||
q.{$this->get_fieldname()} AS content
|
||||
FROM {question} q
|
||||
INNER JOIN {question_versions} qv
|
||||
ON qv.questionid = q.id
|
||||
INNER JOIN {question_bank_entries} qbe
|
||||
ON qbe.id = qv.questionbankentryid
|
||||
INNER JOIN {question_categories} qc
|
||||
ON qc.id = qbe.questioncategoryid
|
||||
INNER JOIN {context} ctx
|
||||
ON ctx.id = qc.contextid
|
||||
WHERE (ctx.contextlevel = :ctxcourse
|
||||
AND ctx.id = qc.contextid
|
||||
AND ctx.instanceid = :courseid)
|
||||
OR (ctx.contextlevel = :module
|
||||
AND {$DB->sql_like('ctx.path', ':coursecontextpath')})
|
||||
ORDER BY q.id ASC";
|
||||
|
||||
return $DB->get_recordset_sql($sql, $param);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an array of area objects that contain content at the site and system levels only. This would be question content from
|
||||
* question categories at the system context only.
|
||||
*
|
||||
* @return \moodle_recordset
|
||||
* @throws \dml_exception
|
||||
*/
|
||||
public function find_system_areas(): ?\moodle_recordset {
|
||||
global $DB;
|
||||
|
||||
$select = 'SELECT ' . $this->get_type() . ' AS type, qc.contextid AS contextid, ' . $this->get_standard_area_fields_sql() .
|
||||
' t.id AS itemid, ' . SITEID . ' as courseid, cc.id as categoryid,' .
|
||||
' t.'.$this->get_fieldname().' AS content ';
|
||||
$from = 'FROM {question} t ' .
|
||||
'INNER JOIN {question_categories} qc ON qc.id = t.category ' .
|
||||
'INNER JOIN {context} ctx ON ctx.id = qc.contextid ' .
|
||||
'LEFT JOIN {course_categories} cc ON cc.id = ctx.instanceid AND ctx.contextlevel = :coursecat ';
|
||||
$where = 'WHERE (ctx.contextlevel = :syscontext) OR (ctx.contextlevel = :coursecat2) ';
|
||||
$order = 'ORDER BY t.id';
|
||||
$params = [
|
||||
'syscontext' => CONTEXT_SYSTEM,
|
||||
'coursecat' => CONTEXT_COURSECAT,
|
||||
'coursecat2' => CONTEXT_COURSECAT,
|
||||
];
|
||||
|
||||
return $DB->get_recordset_sql($select . $from . $where . $order, $params);
|
||||
$sql = "SELECT {$this->get_type()} AS type,
|
||||
qc.contextid AS contextid,
|
||||
{$this->get_standard_area_fields_sql()}
|
||||
q.id AS itemid,
|
||||
" . SITEID . " as courseid,
|
||||
cc.id as categoryid,
|
||||
q.{$this->get_fieldname()} AS content
|
||||
FROM {question} q
|
||||
INNER JOIN {question_versions} qv
|
||||
ON qv.questionid = q.id
|
||||
INNER JOIN {question_bank_entries} qbe
|
||||
ON qbe.id = qv.questionbankentryid
|
||||
INNER JOIN {question_categories} qc
|
||||
ON qc.id = qbe.questioncategoryid
|
||||
INNER JOIN {context} ctx
|
||||
ON ctx.id = qc.contextid
|
||||
LEFT JOIN {course_categories} cc
|
||||
ON cc.id = ctx.instanceid
|
||||
AND ctx.contextlevel = :coursecat
|
||||
WHERE (ctx.contextlevel = :syscontext)
|
||||
OR (ctx.contextlevel = :coursecat2)
|
||||
ORDER BY q.id";
|
||||
|
||||
return $DB->get_recordset_sql($sql, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the moodle_url of the page to edit the error.
|
||||
*
|
||||
* @param \stdClass $componentinfo
|
||||
* @return \moodle_url
|
||||
* @throws \moodle_exception
|
||||
*/
|
||||
public static function get_edit_url(\stdClass $componentinfo): \moodle_url {
|
||||
$questionid = $componentinfo->itemid;
|
||||
// Question answers are editable on main question page
|
||||
// Question answers are editable on main question page.
|
||||
// Hence, use refid for these links.
|
||||
if ($componentinfo->tablename == 'question_answers') {
|
||||
if ($componentinfo->tablename === 'question_answers') {
|
||||
$questionid = $componentinfo->refid;
|
||||
}
|
||||
// Default to SITEID if courseid is null, i.e. system or category level questions.
|
||||
|
@ -142,30 +166,15 @@ abstract class base extends area_base {
|
|||
|
||||
/**
|
||||
* Determine the course and category id SQL depending on the specific context associated with question data.
|
||||
*
|
||||
* @param \core\event\base $event
|
||||
* @return string
|
||||
* @throws \dml_exception
|
||||
*/
|
||||
protected function get_course_and_cat_sql(\core\event\base $event): string {
|
||||
global $DB;
|
||||
|
||||
$courseid = 'null';
|
||||
$catid = 'null';
|
||||
|
||||
$sql = "
|
||||
SELECT ctx.instanceid, cm.course as courseid, ctx.contextlevel
|
||||
FROM {question} q
|
||||
INNER JOIN {question_categories} qc ON qc.id = q.category
|
||||
INNER JOIN {context} ctx ON ctx.id = qc.contextid
|
||||
LEFT JOIN {course_modules} cm ON cm.id = ctx.instanceid AND ctx.contextlevel = :coursemodule
|
||||
WHERE q.id = :refid
|
||||
";
|
||||
$params = [
|
||||
'coursemodule' => CONTEXT_MODULE,
|
||||
'refid' => $event->objectid,
|
||||
];
|
||||
|
||||
if ($record = $DB->get_record_sql($sql, $params)) {
|
||||
if ($record = self::get_course_and_category(CONTEXT_MODULE, $event->objectid)) {
|
||||
if ($record->contextlevel == CONTEXT_MODULE) {
|
||||
$courseid = $record->courseid;
|
||||
} else if ($record->contextlevel == CONTEXT_COURSE) {
|
||||
|
@ -182,4 +191,37 @@ abstract class base extends area_base {
|
|||
{$catid} AS categoryid,
|
||||
";
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the course and category data for the question.
|
||||
*
|
||||
* @param int $coursemodule
|
||||
* @param int $refid
|
||||
* @return \stdClass|false
|
||||
*/
|
||||
public static function get_course_and_category($coursemodule, $refid) {
|
||||
global $DB;
|
||||
|
||||
$sql = 'SELECT ctx.instanceid,
|
||||
cm.course as courseid,
|
||||
ctx.contextlevel
|
||||
FROM {question} q
|
||||
INNER JOIN {question_versions} qv
|
||||
ON qv.questionid = q.id
|
||||
INNER JOIN {question_bank_entries} qbe
|
||||
ON qbe.id = qv.questionbankentryid
|
||||
INNER JOIN {question_categories} qc
|
||||
ON qc.id = qbe.questioncategoryid
|
||||
INNER JOIN {context} ctx
|
||||
ON ctx.id = qc.contextid
|
||||
LEFT JOIN {course_modules} cm
|
||||
ON cm.id = ctx.instanceid
|
||||
AND ctx.contextlevel = :coursemodule
|
||||
WHERE q.id = :refid';
|
||||
$params = [
|
||||
'coursemodule' => $coursemodule,
|
||||
'refid' => $refid
|
||||
];
|
||||
return $DB->get_record_sql($sql, $params);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,6 +28,7 @@ use tool_brickfield\area_test_base;
|
|||
* @package tool_brickfield
|
||||
* @copyright 2020 onward: Brickfield Education Labs, https://www.brickfield.ie
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
* @coversDefaultClass \tool_brickfield\local\areas\core_question\base
|
||||
*/
|
||||
class questiontext_test extends area_test_base {
|
||||
/**
|
||||
|
@ -190,4 +191,32 @@ class questiontext_test extends area_test_base {
|
|||
$category->id
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test get course and category.
|
||||
*
|
||||
* @covers ::get_course_and_category
|
||||
*/
|
||||
public function test_get_course_and_category() {
|
||||
$this->resetAfterTest();
|
||||
$this->setAdminUser();
|
||||
|
||||
$course = $this->getDataGenerator()->create_course();
|
||||
$coursecontext = \context_course::instance($course->id);
|
||||
$generator = $this->getDataGenerator()->get_plugin_generator('core_question');
|
||||
$cat1 = $generator->create_question_category(['contextid' => $coursecontext->id]);
|
||||
$question1 = $generator->create_question('multichoice', null, ['category' => $cat1->id]);
|
||||
$event = \core\event\question_updated::create_from_question_instance($question1,
|
||||
\context_course::instance($course->id));
|
||||
$rs = base::get_course_and_category(CONTEXT_COURSE, $event->objectid);
|
||||
$this->assertNotNull($rs);
|
||||
$this->assertEquals(CONTEXT_COURSE, $rs->contextlevel);
|
||||
$this->assertNotEquals(CONTEXT_MODULE, $rs->contextlevel);
|
||||
// Invalid objectid.
|
||||
$rs = base::get_course_and_category(CONTEXT_COURSE, 0);
|
||||
$this->assertFalse($rs);
|
||||
// Incorrect objectid.
|
||||
$rs = base::get_course_and_category(CONTEXT_COURSE, 100);
|
||||
$this->assertFalse($rs);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -195,15 +195,81 @@ trait backup_questions_attempt_data_trait {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to backup question reference data for an instance.
|
||||
*/
|
||||
trait backup_question_reference_data_trait {
|
||||
|
||||
/**
|
||||
* Backup the related data from reference table for the instance.
|
||||
*
|
||||
* @param backup_nested_element $element
|
||||
* @param string $component
|
||||
* @param string $questionarea
|
||||
*/
|
||||
protected function add_question_references($element, $component, $questionarea) {
|
||||
// Check $element is one nested_backup_element.
|
||||
if (! $element instanceof backup_nested_element) {
|
||||
throw new backup_step_exception('question_states_bad_parent_element', $element);
|
||||
}
|
||||
|
||||
$reference = new backup_nested_element('question_reference', ['id'],
|
||||
['usingcontextid', 'component', 'questionarea', 'questionbankentryid', 'version']);
|
||||
|
||||
$element->add_child($reference);
|
||||
|
||||
$reference->set_source_table('question_references', [
|
||||
'usingcontextid' => backup::VAR_CONTEXTID,
|
||||
'component' => backup_helper::is_sqlparam($component),
|
||||
'questionarea' => backup_helper::is_sqlparam($questionarea),
|
||||
'itemid' => backup::VAR_PARENTID
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract structure step to help activities that store question attempt data.
|
||||
* Helper to backup question set reference data for an instance.
|
||||
*/
|
||||
trait backup_question_set_reference_trait {
|
||||
|
||||
/**
|
||||
* Backup the related data from set_reference table for the instance.
|
||||
*
|
||||
* @param backup_nested_element $element
|
||||
* @param string $component
|
||||
* @param string $questionarea
|
||||
*/
|
||||
protected function add_question_set_references($element, $component, $questionarea) {
|
||||
// Check $element is one nested_backup_element.
|
||||
if (! $element instanceof backup_nested_element) {
|
||||
throw new backup_step_exception('question_states_bad_parent_element', $element);
|
||||
}
|
||||
|
||||
$setreference = new backup_nested_element('question_set_reference', ['id'],
|
||||
['usingcontextid', 'component', 'questionarea', 'questionscontextid', 'filtercondition']);
|
||||
|
||||
$element->add_child($setreference);
|
||||
|
||||
$setreference->set_source_table('question_set_references', [
|
||||
'usingcontextid' => backup::VAR_CONTEXTID,
|
||||
'component' => backup_helper::is_sqlparam($component),
|
||||
'questionarea' => backup_helper::is_sqlparam($questionarea),
|
||||
'itemid' => backup::VAR_PARENTID
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Abstract structure step to help activities that store question attempt data, reference data and set reference data.
|
||||
*
|
||||
* @copyright 2011 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
abstract class backup_questions_activity_structure_step extends backup_activity_structure_step {
|
||||
use backup_questions_attempt_data_trait;
|
||||
use backup_question_reference_data_trait;
|
||||
use backup_question_set_reference_trait;
|
||||
}
|
||||
|
||||
|
||||
|
@ -2325,30 +2391,64 @@ class backup_annotate_all_question_files extends backup_execution_step {
|
|||
/**
|
||||
* structure step in charge of constructing the questions.xml file for all the
|
||||
* question categories and questions required by the backup
|
||||
* and letters related to one activity
|
||||
* and letters related to one activity.
|
||||
*/
|
||||
class backup_questions_structure_step extends backup_structure_step {
|
||||
|
||||
protected function define_structure() {
|
||||
|
||||
// Define each element separated
|
||||
|
||||
// Define each element separately.
|
||||
$qcategories = new backup_nested_element('question_categories');
|
||||
|
||||
$qcategory = new backup_nested_element('question_category', array('id'), array(
|
||||
'name', 'contextid', 'contextlevel', 'contextinstanceid',
|
||||
'info', 'infoformat', 'stamp', 'parent',
|
||||
'sortorder', 'idnumber'));
|
||||
$qcategory = new backup_nested_element('question_category', ['id'],
|
||||
[
|
||||
'name',
|
||||
'contextid',
|
||||
'contextlevel',
|
||||
'contextinstanceid',
|
||||
'info',
|
||||
'infoformat',
|
||||
'stamp',
|
||||
'parent',
|
||||
'sortorder',
|
||||
'idnumber',
|
||||
]);
|
||||
|
||||
$questionbankentries = new backup_nested_element('question_bank_entries');
|
||||
|
||||
$questionbankentry = new backup_nested_element('question_bank_entry', ['id'],
|
||||
[
|
||||
'questioncategoryid',
|
||||
'idnumber',
|
||||
'ownerid',
|
||||
]);
|
||||
|
||||
$questionversions = new backup_nested_element('question_version');
|
||||
|
||||
$questionverion = new backup_nested_element('question_versions', ['id'], ['version', 'status']);
|
||||
|
||||
$questions = new backup_nested_element('questions');
|
||||
|
||||
$question = new backup_nested_element('question', array('id'), array(
|
||||
'parent', 'name', 'questiontext', 'questiontextformat',
|
||||
'generalfeedback', 'generalfeedbackformat', 'defaultmark', 'penalty',
|
||||
'qtype', 'length', 'stamp', 'version',
|
||||
'hidden', 'timecreated', 'timemodified', 'createdby', 'modifiedby', 'idnumber'));
|
||||
$question = new backup_nested_element('question', ['id'],
|
||||
[
|
||||
'parent',
|
||||
'name',
|
||||
'questiontext',
|
||||
'questiontextformat',
|
||||
'generalfeedback',
|
||||
'generalfeedbackformat',
|
||||
'defaultmark',
|
||||
'penalty',
|
||||
'qtype',
|
||||
'length',
|
||||
'stamp',
|
||||
'timecreated',
|
||||
'timemodified',
|
||||
'createdby',
|
||||
'modifiedby',
|
||||
]);
|
||||
|
||||
// attach qtype plugin structure to $question element, only one allowed
|
||||
// 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.
|
||||
|
@ -2359,55 +2459,70 @@ class backup_questions_structure_step extends backup_structure_step {
|
|||
|
||||
$qhints = new backup_nested_element('question_hints');
|
||||
|
||||
$qhint = new backup_nested_element('question_hint', array('id'), array(
|
||||
'hint', 'hintformat', 'shownumcorrect', 'clearwrong', 'options'));
|
||||
$qhint = new backup_nested_element('question_hint', ['id'],
|
||||
[
|
||||
'hint',
|
||||
'hintformat',
|
||||
'shownumcorrect',
|
||||
'clearwrong',
|
||||
'options',
|
||||
]);
|
||||
|
||||
$tags = new backup_nested_element('tags');
|
||||
|
||||
$tag = new backup_nested_element('tag', array('id', 'contextid'), array('name', 'rawname'));
|
||||
|
||||
// Build the tree
|
||||
$tag = new backup_nested_element('tag', ['id', 'contextid'], ['name', 'rawname']);
|
||||
|
||||
// Build the initial tree.
|
||||
$qcategories->add_child($qcategory);
|
||||
$qcategory->add_child($questions);
|
||||
$qcategory->add_child($questionbankentries);
|
||||
$questionbankentries->add_child($questionbankentry);
|
||||
$questionbankentry->add_child($questionversions);
|
||||
$questionversions->add_child($questionverion);
|
||||
$questionverion->add_child($questions);
|
||||
$questions->add_child($question);
|
||||
$question->add_child($qhints);
|
||||
$qhints->add_child($qhint);
|
||||
|
||||
// Add question tags.
|
||||
$question->add_child($tags);
|
||||
$tags->add_child($tag);
|
||||
|
||||
// Define the sources
|
||||
|
||||
$qcategory->set_source_sql("
|
||||
SELECT gc.*, contextlevel, instanceid AS contextinstanceid
|
||||
SELECT gc.*,
|
||||
contextlevel,
|
||||
instanceid AS contextinstanceid
|
||||
FROM {question_categories} gc
|
||||
JOIN {backup_ids_temp} bi ON bi.itemid = gc.id
|
||||
JOIN {context} co ON co.id = gc.contextid
|
||||
WHERE bi.backupid = ?
|
||||
AND bi.itemname = 'question_categoryfinal'", array(backup::VAR_BACKUPID));
|
||||
AND bi.itemname = 'question_categoryfinal'", [backup::VAR_BACKUPID]);
|
||||
|
||||
$question->set_source_table('question', array('category' => backup::VAR_PARENTID));
|
||||
$questionbankentry->set_source_table('question_bank_entries', ['questioncategoryid' => backup::VAR_PARENTID]);
|
||||
|
||||
$questionverion->set_source_table('question_versions', ['questionbankentryid' => backup::VAR_PARENTID]);
|
||||
|
||||
$question->set_source_sql('
|
||||
SELECT q.*
|
||||
FROM {question} q
|
||||
JOIN {question_versions} qv ON qv.questionid = q.id
|
||||
JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
|
||||
WHERE qv.id = ?', [backup::VAR_PARENTID]);
|
||||
|
||||
$qhint->set_source_sql('
|
||||
SELECT *
|
||||
FROM {question_hints}
|
||||
WHERE questionid = :questionid
|
||||
ORDER BY id',
|
||||
array('questionid' => backup::VAR_PARENTID));
|
||||
ORDER BY id', ['questionid' => backup::VAR_PARENTID]);
|
||||
|
||||
$tag->set_source_sql("SELECT t.id, ti.contextid, t.name, t.rawname
|
||||
FROM {tag} t
|
||||
JOIN {tag_instance} ti ON ti.tagid = t.id
|
||||
WHERE ti.itemid = ?
|
||||
AND ti.itemtype = 'question'
|
||||
AND ti.component = 'core_question'",
|
||||
[
|
||||
backup::VAR_PARENTID
|
||||
]);
|
||||
AND ti.component = 'core_question'", [backup::VAR_PARENTID]);
|
||||
|
||||
// don't need to annotate ids nor files
|
||||
// (already done by {@link backup_annotate_all_question_files}
|
||||
// Don't need to annotate ids nor files.
|
||||
// ...(already done by {@see backup_annotate_all_question_files()}.
|
||||
|
||||
return $qcategories;
|
||||
}
|
||||
|
|
|
@ -4731,46 +4731,86 @@ abstract class restore_activity_structure_step extends restore_structure_step {
|
|||
* Structure step in charge of creating/mapping all the qcats and qs
|
||||
* by parsing the questions.xml file and checking it against the
|
||||
* results calculated by {@link restore_process_categories_and_questions}
|
||||
* and stored in backup_ids_temp
|
||||
* and stored in backup_ids_temp.
|
||||
*/
|
||||
class restore_create_categories_and_questions extends restore_structure_step {
|
||||
|
||||
/** @var array $cachecategory store a question category */
|
||||
/** @var array $cachedcategory store a question category */
|
||||
protected $cachedcategory = null;
|
||||
|
||||
protected function define_structure() {
|
||||
|
||||
$category = new restore_path_element('question_category', '/question_categories/question_category');
|
||||
// Check if the backup is a pre 4.0 one.
|
||||
$backuprelease = $this->get_task()->get_info()->backup_release; // The major version: 2.9, 3.0, 3.10...
|
||||
preg_match('/(\d{8})/', $this->get_task()->get_info()->moodle_release, $matches);
|
||||
$backupbuild = (int)$matches[1];
|
||||
$before40 = false;
|
||||
if (version_compare($backuprelease, '4.0', '<') || $backupbuild < 20220202) {
|
||||
$before40 = true;
|
||||
}
|
||||
// Start creating the path, category should be the first one.
|
||||
$paths = [];
|
||||
$paths [] = new restore_path_element('question_category', '/question_categories/question_category');
|
||||
// For the backups done before 4.0.
|
||||
if ($before40) {
|
||||
// This path is to recreate the bank entry and version for the legacy question objets.
|
||||
$question = new restore_path_element('question', '/question_categories/question_category/questions/question');
|
||||
$hint = new restore_path_element('question_hint',
|
||||
|
||||
// Apply for 'qtype' plugins optional paths at question level.
|
||||
$this->add_plugin_structure('qtype', $question);
|
||||
|
||||
// Apply for 'local' plugins optional paths at question level.
|
||||
$this->add_plugin_structure('local', $question);
|
||||
|
||||
$paths [] = $question;
|
||||
$paths [] = new restore_path_element('question_hint',
|
||||
'/question_categories/question_category/questions/question/question_hints/question_hint');
|
||||
$paths [] = new restore_path_element('tag', '/question_categories/question_category/questions/question/tags/tag');
|
||||
} else {
|
||||
// For all the new backups.
|
||||
$paths [] = new restore_path_element('question_bank_entry',
|
||||
'/question_categories/question_category/question_bank_entries/question_bank_entry');
|
||||
$paths [] = new restore_path_element('question_versions', '/question_categories/question_category/'.
|
||||
'question_bank_entries/question_bank_entry/question_version/question_versions');
|
||||
$question = new restore_path_element('question', '/question_categories/question_category/'.
|
||||
'question_bank_entries/question_bank_entry/question_version/question_versions/questions/question');
|
||||
|
||||
$tag = new restore_path_element('tag','/question_categories/question_category/questions/question/tags/tag');
|
||||
|
||||
// Apply for 'qtype' plugins optional paths at question level
|
||||
// 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
|
||||
// Apply for 'local' plugins optional paths at question level.
|
||||
$this->add_plugin_structure('local', $question);
|
||||
|
||||
return [$category, $question, $hint, $tag];
|
||||
$paths [] = $question;
|
||||
$paths [] = new restore_path_element('question_hint', '/question_categories/question_category/question_bank_entries/'.
|
||||
'question_bank_entry/question_version/question_versions/questions/question/question_hints/question_hint');
|
||||
$paths [] = new restore_path_element('tag', '/question_categories/question_category/question_bank_entries/'.
|
||||
'question_bank_entry/question_version/question_versions/questions/question/tags/tag');
|
||||
}
|
||||
|
||||
return $paths;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process question category restore.
|
||||
*
|
||||
* @param array $data the data from the XML file.
|
||||
*/
|
||||
protected function process_question_category($data) {
|
||||
global $DB;
|
||||
|
||||
$data = (object)$data;
|
||||
$oldid = $data->id;
|
||||
|
||||
// Check we have one mapping for this category
|
||||
// Check we have one mapping for this category.
|
||||
if (!$mapping = $this->get_mapping('question_category', $oldid)) {
|
||||
return self::SKIP_ALL_CHILDREN; // No mapping = this category doesn't need to be created/mapped
|
||||
}
|
||||
|
||||
// Check we have to create the category (newitemid = 0)
|
||||
// Check we have to create the category (newitemid = 0).
|
||||
if ($mapping->newitemid) {
|
||||
// By performing this set_mapping() we make get_old/new_parentid() to work for all the
|
||||
// children elements of the 'question_category' one.
|
||||
|
@ -4781,7 +4821,7 @@ class restore_create_categories_and_questions extends restore_structure_step {
|
|||
// Arrived here, newitemid = 0, we need to create the category
|
||||
// we'll do it at parentitemid context, but for CONTEXT_MODULE
|
||||
// categories, that will be created at CONTEXT_COURSE and moved
|
||||
// to module context later when the activity is created
|
||||
// to module context later when the activity is created.
|
||||
if ($mapping->info->contextlevel == CONTEXT_MODULE) {
|
||||
$mapping->parentitemid = $this->get_mappingid('context', $this->task->get_old_contextid());
|
||||
}
|
||||
|
@ -4832,22 +4872,137 @@ class restore_create_categories_and_questions extends restore_structure_step {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process pre 4.0 question data where in creates the record for version and entry table.
|
||||
*
|
||||
* @param array $data the data from the XML file.
|
||||
*/
|
||||
protected function process_question_legacy_data($data) {
|
||||
global $DB;
|
||||
|
||||
$oldid = $data->id;
|
||||
// Process question bank entry.
|
||||
$entrydata = new stdClass();
|
||||
$entrydata->questioncategoryid = $data->category;
|
||||
$userid = $this->get_mappingid('user', $data->createdby);
|
||||
if ($userid) {
|
||||
$entrydata->ownerid = $userid;
|
||||
} else {
|
||||
if (!$this->task->is_samesite()) {
|
||||
$entrydata->ownerid = $this->task->get_userid();
|
||||
}
|
||||
}
|
||||
// The idnumber if it exists also needs to be unique within a category or reset it to null.
|
||||
if (isset($data->idnumber) && !$DB->record_exists('question_bank_entries',
|
||||
['idnumber' => $data->idnumber, 'questioncategoryid' => $data->category])) {
|
||||
$entrydata->idnumber = $data->idnumber;
|
||||
}
|
||||
|
||||
$newentryid = $DB->insert_record('question_bank_entries', $entrydata);
|
||||
// Process question versions.
|
||||
$versiondata = new stdClass();
|
||||
$versiondata->questionbankentryid = $newentryid;
|
||||
$versiondata->version = 1;
|
||||
// Question id is updated after inserting the question.
|
||||
$versiondata->questionid = 0;
|
||||
$versionstatus = \core_question\local\bank\question_version_status::QUESTION_STATUS_READY;
|
||||
if ((int)$data->hidden === 1) {
|
||||
$versionstatus = \core_question\local\bank\question_version_status::QUESTION_STATUS_HIDDEN;
|
||||
}
|
||||
$versiondata->status = $versionstatus;
|
||||
$newversionid = $DB->insert_record('question_versions', $versiondata);
|
||||
$this->set_mapping('question_version_created', $oldid, $newversionid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process question bank entry data.
|
||||
*
|
||||
* @param array $data the data from the XML file.
|
||||
*/
|
||||
protected function process_question_bank_entry($data) {
|
||||
global $DB;
|
||||
|
||||
$data = (object)$data;
|
||||
$oldid = $data->id;
|
||||
|
||||
$questioncreated = $this->get_mappingid('question_category_created', $data->questioncategoryid) ? true : false;
|
||||
$recordexist = $DB->record_exists('question_bank_entries', ['id' => $data->id,
|
||||
'questioncategoryid' => $data->questioncategoryid]);
|
||||
// Check we have category created.
|
||||
if (!$questioncreated && $recordexist) {
|
||||
return self::SKIP_ALL_CHILDREN;
|
||||
}
|
||||
|
||||
$data->questioncategoryid = $this->get_new_parentid('question_category');
|
||||
$userid = $this->get_mappingid('user', $data->ownerid);
|
||||
if ($userid) {
|
||||
$data->ownerid = $userid;
|
||||
} else {
|
||||
if (!$this->task->is_samesite()) {
|
||||
$data->ownerid = $this->task->get_userid();
|
||||
}
|
||||
}
|
||||
|
||||
// The idnumber if it exists also needs to be unique within a category or reset it to null.
|
||||
if (!empty($data->idnumber) && $DB->record_exists('question_bank_entries',
|
||||
['idnumber' => $data->idnumber, 'questioncategoryid' => $data->questioncategoryid])) {
|
||||
unset($data->idnumber);
|
||||
}
|
||||
|
||||
$newitemid = $DB->insert_record('question_bank_entries', $data);
|
||||
$this->set_mapping('question_bank_entry', $oldid, $newitemid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process question versions.
|
||||
*
|
||||
* @param array $data the data from the XML file.
|
||||
*/
|
||||
protected function process_question_versions($data) {
|
||||
global $DB;
|
||||
|
||||
$data = (object)$data;
|
||||
$oldid = $data->id;
|
||||
|
||||
$data->questionbankentryid = $this->get_new_parentid('question_bank_entry');
|
||||
// Question id is updated after inserting the question.
|
||||
$data->questionid = 0;
|
||||
$newitemid = $DB->insert_record('question_versions', $data);
|
||||
$this->set_mapping('question_versions', $oldid, $newitemid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the actual question.
|
||||
*
|
||||
* @param array $data the data from the XML file.
|
||||
*/
|
||||
protected function process_question($data) {
|
||||
global $DB;
|
||||
|
||||
$data = (object)$data;
|
||||
$oldid = $data->id;
|
||||
|
||||
// Check we have one mapping for this question
|
||||
// Check if the backup is a pre 4.0 one.
|
||||
$backuprelease = $this->get_task()->get_info()->backup_release; // The major version: 2.9, 3.0, 3.10...
|
||||
preg_match('/(\d{8})/', $this->get_task()->get_info()->moodle_release, $matches);
|
||||
$backupbuild = (int)$matches[1];
|
||||
$before40 = false;
|
||||
if (version_compare($backuprelease, '4.0', '<') || $backupbuild < 20220202) {
|
||||
$before40 = true;
|
||||
}
|
||||
if ($before40) {
|
||||
// Check we have one mapping for this question.
|
||||
if (!$questionmapping = $this->get_mapping('question', $oldid)) {
|
||||
return; // No mapping = this question doesn't need to be created/mapped
|
||||
return; // No mapping = this question doesn't need to be created/mapped.
|
||||
}
|
||||
|
||||
// Get the mapped category (cannot use get_new_parentid() because not
|
||||
// all the categories have been created, so it is not always available
|
||||
// Instead we get the mapping for the question->parentitemid because
|
||||
// we have loaded qcatids there for all parsed questions
|
||||
// we have loaded qcatids there for all parsed questions.
|
||||
$data->category = $this->get_mappingid('question_category', $questionmapping->parentitemid);
|
||||
$this->process_question_legacy_data($data);
|
||||
}
|
||||
|
||||
// In the past, there were some very sloppy values of penalty. Fix them.
|
||||
if ($data->penalty >= 0.33 && $data->penalty <= 0.34) {
|
||||
|
@ -4884,37 +5039,29 @@ class restore_create_categories_and_questions extends restore_structure_step {
|
|||
}
|
||||
}
|
||||
|
||||
// With newitemid = 0, let's create the question
|
||||
if (!$questionmapping->newitemid) {
|
||||
|
||||
// The idnumber if it exists also needs to be unique within a category or reset it to null.
|
||||
if (!empty($data->idnumber) && $DB->record_exists('question',
|
||||
['idnumber' => $data->idnumber, 'category' => $data->category])) {
|
||||
unset($data->idnumber);
|
||||
}
|
||||
|
||||
if ($data->qtype === 'random') {
|
||||
// Ensure that this newly created question is considered by
|
||||
// \qtype_random\task\remove_unused_questions.
|
||||
$data->hidden = 0;
|
||||
}
|
||||
|
||||
$newitemid = $DB->insert_record('question', $data);
|
||||
$this->set_mapping('question', $oldid, $newitemid);
|
||||
// Also annotate them as question_created, we need
|
||||
// that later when remapping parents (keeping the old categoryid as parentid)
|
||||
$this->set_mapping('question_created', $oldid, $newitemid, false, null, $questionmapping->parentitemid);
|
||||
// that later when remapping parents (keeping the old categoryid as parentid).
|
||||
$parentcatid = $this->get_old_parentid('question_category');
|
||||
$this->set_mapping('question_created', $oldid, $newitemid, false, null, $parentcatid);
|
||||
// Now update the question_versions table with the new question id. we dont need to do that for random qtypes.
|
||||
$legacyquestiondata = $this->get_mappingid('question_version_created', $oldid) ? true : false;
|
||||
if ($legacyquestiondata) {
|
||||
$parentitemid = $this->get_mappingid('question_version_created', $oldid);
|
||||
} else {
|
||||
// By performing this set_mapping() we make get_old/new_parentid() to work for all the
|
||||
// children elements of the 'question' one (so qtype plugins will know the question they belong to)
|
||||
$this->set_mapping('question', $oldid, $questionmapping->newitemid);
|
||||
$parentitemid = $this->get_new_parentid('question_versions');
|
||||
}
|
||||
$version = new stdClass();
|
||||
$version->id = $parentitemid;
|
||||
$version->questionid = $newitemid;
|
||||
$DB->update_record('question_versions', $version);
|
||||
|
||||
// Note, we don't restore any question files yet
|
||||
// as far as the CONTEXT_MODULE categories still
|
||||
// haven't their contexts to be restored to
|
||||
// The {@link restore_create_question_files}, executed in the final step
|
||||
// step will be in charge of restoring all the question files
|
||||
// step will be in charge of restoring all the question files.
|
||||
}
|
||||
|
||||
protected function process_question_hint($data) {
|
||||
|
@ -5673,10 +5820,15 @@ trait restore_questions_attempt_data_trait {
|
|||
|
||||
$data = (object)$data;
|
||||
$oldid = $data->id;
|
||||
|
||||
$questioncreated = $this->get_mappingid('question_created', $data->questionid) ? true : false;
|
||||
$question = $this->get_mapping('question', $data->questionid);
|
||||
if ($questioncreated) {
|
||||
$data->questionid = $question->newitemid;
|
||||
}
|
||||
|
||||
$data->questionusageid = $this->get_new_parentid($nameprefix . 'question_usage');
|
||||
$data->questionid = $question->newitemid;
|
||||
|
||||
if (!property_exists($data, 'variant')) {
|
||||
$data->variant = 1;
|
||||
}
|
||||
|
@ -5688,7 +5840,12 @@ trait restore_questions_attempt_data_trait {
|
|||
$newitemid = $DB->insert_record('question_attempts', $data);
|
||||
|
||||
$this->set_mapping($nameprefix . 'question_attempt', $oldid, $newitemid);
|
||||
$this->qtypes[$newitemid] = $question->info->qtype;
|
||||
if (isset($question->info->qtype)) {
|
||||
$qtype = $question->info->qtype;
|
||||
} else {
|
||||
$qtype = $DB->get_record('question', ['id' => $data->questionid])->qtype;
|
||||
}
|
||||
$this->qtypes[$newitemid] = $qtype;
|
||||
$this->newquestionids[$newitemid] = $data->questionid;
|
||||
}
|
||||
|
||||
|
@ -5802,6 +5959,98 @@ trait restore_questions_attempt_data_trait {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper trait to restore question reference data.
|
||||
*/
|
||||
trait restore_question_reference_data_trait {
|
||||
|
||||
/**
|
||||
* Attach the question reference data to the restore.
|
||||
*
|
||||
* @param restore_path_element $element the parent element. (E.g. a quiz attempt.)
|
||||
* @param array $paths the paths array that is being built to describe the structure.
|
||||
*/
|
||||
protected function add_question_references($element, &$paths) {
|
||||
// Check $element is restore_path_element.
|
||||
if (! $element instanceof restore_path_element) {
|
||||
throw new restore_step_exception('element_must_be_restore_path_element', $element);
|
||||
}
|
||||
|
||||
// Check $paths is one array.
|
||||
if (!is_array($paths)) {
|
||||
throw new restore_step_exception('paths_must_be_array', $paths);
|
||||
}
|
||||
|
||||
$paths[] = new restore_path_element('question_reference',
|
||||
$element->get_path() . '/question_reference');
|
||||
}
|
||||
|
||||
/**
|
||||
* Process question references which replaces the direct connection to quiz slots to question.
|
||||
*
|
||||
* @param array $data the data from the XML file.
|
||||
*/
|
||||
public function process_question_reference($data) {
|
||||
global $DB;
|
||||
$data = (object) $data;
|
||||
$data->usingcontextid = $this->get_mappingid('context', $data->usingcontextid);
|
||||
$data->itemid = $this->get_new_parentid('quiz_question_instance');
|
||||
if ($entry = $this->get_mappingid('question_bank_entry', $data->questionbankentryid)) {
|
||||
$data->questionbankentryid = $entry;
|
||||
}
|
||||
$DB->insert_record('question_references', $data);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper trait to restore question set reference data.
|
||||
*/
|
||||
trait restore_question_set_reference_data_trait {
|
||||
|
||||
/**
|
||||
* Attach the question reference data to the restore.
|
||||
*
|
||||
* @param restore_path_element $element the parent element. (E.g. a quiz attempt.)
|
||||
* @param array $paths the paths array that is being built to describe the structure.
|
||||
*/
|
||||
protected function add_question_set_references($element, &$paths) {
|
||||
// Check $element is restore_path_element.
|
||||
if (! $element instanceof restore_path_element) {
|
||||
throw new restore_step_exception('element_must_be_restore_path_element', $element);
|
||||
}
|
||||
|
||||
// Check $paths is one array.
|
||||
if (!is_array($paths)) {
|
||||
throw new restore_step_exception('paths_must_be_array', $paths);
|
||||
}
|
||||
|
||||
$paths[] = new restore_path_element('question_set_reference',
|
||||
$element->get_path() . '/question_set_reference');
|
||||
}
|
||||
|
||||
/**
|
||||
* Process question set references data which replaces the random qtype.
|
||||
*
|
||||
* @param array $data the data from the XML file.
|
||||
*/
|
||||
public function process_question_set_reference($data) {
|
||||
global $DB;
|
||||
$data = (object) $data;
|
||||
$data->usingcontextid = $this->get_mappingid('context', $data->usingcontextid);
|
||||
$data->itemid = $this->get_new_parentid('quiz_question_instance');
|
||||
$filtercondition = json_decode($data->filtercondition);
|
||||
if ($category = $this->get_mappingid('question_category', $filtercondition->questioncategoryid)) {
|
||||
$filtercondition->questioncategoryid = $category;
|
||||
}
|
||||
$data->filtercondition = json_encode($filtercondition);
|
||||
if ($context = $this->get_mappingid('context', $data->questionscontextid)) {
|
||||
$data->questionscontextid = $context;
|
||||
}
|
||||
|
||||
$DB->insert_record('question_set_references', $data);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Abstract structure step to help activities that store question attempt data.
|
||||
|
@ -5811,6 +6060,8 @@ trait restore_questions_attempt_data_trait {
|
|||
*/
|
||||
abstract class restore_questions_activity_structure_step extends restore_activity_structure_step {
|
||||
use restore_questions_attempt_data_trait;
|
||||
use restore_question_reference_data_trait;
|
||||
use restore_question_set_reference_data_trait;
|
||||
|
||||
/**
|
||||
* Attach below $element (usually attempts) the needed restore_path_elements
|
||||
|
|
|
@ -14,14 +14,6 @@
|
|||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Decode links quiz restore tests.
|
||||
*
|
||||
* @package core_backup
|
||||
* @copyright 2020 Ilya Tregubov <mattp@catalyst-au.net>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
// Include all the needed stuff.
|
||||
|
@ -31,7 +23,11 @@ require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php');
|
|||
require_once($CFG->dirroot . '/question/engine/tests/helpers.php');
|
||||
|
||||
/**
|
||||
* restore_decode tests (both rule and content)
|
||||
* Decode links quiz restore tests.
|
||||
*
|
||||
* @package core_backup
|
||||
* @copyright 2020 Ilya Tregubov <mattp@catalyst-au.net>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class restore_quiz_decode_testcase extends \core_privacy\tests\provider_testcase {
|
||||
|
||||
|
@ -82,17 +78,23 @@ class restore_quiz_decode_testcase extends \core_privacy\tests\provider_testcase
|
|||
|
||||
$newcm = duplicate_module($course, get_fast_modinfo($course)->get_cm($quiz->cmid));
|
||||
|
||||
$sql = "SELECT qa.id, qa.answer
|
||||
FROM {quiz} q
|
||||
LEFT JOIN {quiz_slots} qs ON qs.quizid = q.id
|
||||
LEFT JOIN {question_answers} qa ON qa.question = qs.questionid
|
||||
WHERE q.id = :quizid";
|
||||
$params = array('quizid' => $newcm->instance);
|
||||
$answers = $DB->get_records_sql_menu($sql, $params);
|
||||
$quizquestions = \mod_quiz\question\bank\qbank_helper::get_question_structure_data($newcm->instance);
|
||||
$questionids = [];
|
||||
foreach ($quizquestions as $quizquestion) {
|
||||
$questionids [] = $quizquestion->id;
|
||||
}
|
||||
list($condition, $param) = $DB->get_in_or_equal($questionids, SQL_PARAMS_NAMED, 'questionid');
|
||||
$condition = 'WHERE qa.question ' . $condition;
|
||||
|
||||
$this->assertEquals($CFG->wwwroot . '/course/view.php?id=' . $course->id, $answers[$firstanswer->id]);
|
||||
$this->assertEquals($CFG->wwwroot . '/mod/quiz/view.php?id=' . $quiz->cmid, $answers[$secondanswer->id]);
|
||||
$this->assertEquals($CFG->wwwroot . '/grade/report/index.php?id=' . $quiz->cmid, $answers[$thirdanswer->id]);
|
||||
$this->assertEquals($CFG->wwwroot . '/mod/quiz/index.php?id=' . $quiz->cmid, $answers[$fourthanswer->id]);
|
||||
$sql = "SELECT qa.id,
|
||||
qa.answer
|
||||
FROM {question_answers} qa
|
||||
$condition";
|
||||
$answers = $DB->get_records_sql($sql, $param);
|
||||
|
||||
$this->assertEquals($CFG->wwwroot . '/course/view.php?id=' . $course->id, $answers[$firstanswer->id]->answer);
|
||||
$this->assertEquals($CFG->wwwroot . '/mod/quiz/view.php?id=' . $quiz->cmid, $answers[$secondanswer->id]->answer);
|
||||
$this->assertEquals($CFG->wwwroot . '/grade/report/index.php?id=' . $quiz->cmid, $answers[$thirdanswer->id]->answer);
|
||||
$this->assertEquals($CFG->wwwroot . '/mod/quiz/index.php?id=' . $quiz->cmid, $answers[$fourthanswer->id]->answer);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -53,11 +53,38 @@ abstract class backup_question_dbops extends backup_dbops {
|
|||
// where they are in the contexts hierarchy, transversals... whatever)
|
||||
$contexts = $DB->get_fieldset_sql("SELECT DISTINCT qc2.contextid
|
||||
FROM {question_categories} qc2
|
||||
JOIN {question} q ON q.category = qc2.id
|
||||
JOIN {question_bank_entries} qbe ON qbe.questioncategoryid = qc2.id
|
||||
JOIN {question_versions} qv ON qv.questionbankentryid = qbe.id
|
||||
JOIN {question} q ON q.id = qv.questionid
|
||||
JOIN {backup_ids_temp} bi ON bi.itemid = q.id
|
||||
WHERE bi.backupid = ?
|
||||
AND bi.itemname = 'question'
|
||||
AND qc2.contextid != ?", array($backupid, $contextid));
|
||||
|
||||
// Calculate and get the set reference records.
|
||||
$setreferencecontexts = $DB->get_fieldset_sql("
|
||||
SELECT DISTINCT qc.contextid
|
||||
FROM {question_categories} qc
|
||||
JOIN {question_set_references} qsr ON qsr.questionscontextid = qc.contextid
|
||||
WHERE qsr.usingcontextid = ?", [$contextid]);
|
||||
foreach ($setreferencecontexts as $setreferencecontext) {
|
||||
if (!in_array($setreferencecontext, $contexts) && (int)$setreferencecontext !== $contextid) {
|
||||
$contexts [] = $setreferencecontext;
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate the get the reference records.
|
||||
$referencecontexts = $DB->get_fieldset_sql("
|
||||
SELECT DISTINCT qc.contextid
|
||||
FROM {question_categories} qc
|
||||
JOIN {question_bank_entries} qbe ON qbe.questioncategoryid = qc.id
|
||||
JOIN {question_references} qr ON qr.questionbankentryid = qbe.id
|
||||
WHERE qr.usingcontextid =?", [$contextid]);
|
||||
foreach ($referencecontexts as $referencecontext) {
|
||||
if (!in_array($referencecontext, $contexts) && (int)$referencecontext !== $contextid) {
|
||||
$contexts [] = $referencecontext;
|
||||
}
|
||||
}
|
||||
// And now, simply insert all the question categories (complete question bank)
|
||||
// for those contexts if we have found any
|
||||
if ($contexts) {
|
||||
|
|
|
@ -676,9 +676,16 @@ abstract class restore_dbops {
|
|||
$questions = self::restore_get_questions($restoreid, $category->id);
|
||||
|
||||
// Collect all the questions for this category into memory so we only talk to the DB once.
|
||||
$questioncache = $DB->get_records_sql_menu("SELECT ".$DB->sql_concat('stamp', "' '", 'version').", id
|
||||
FROM {question}
|
||||
WHERE category = ?", array($matchcat->id));
|
||||
$questioncache = $DB->get_records_sql_menu('SELECT q.id,
|
||||
q.stamp
|
||||
FROM {question} q
|
||||
JOIN {question_versions} qv
|
||||
ON qv.questionid = q.id
|
||||
JOIN {question_bank_entries} qbe
|
||||
ON qbe.id = qv.questionbankentryid
|
||||
JOIN {question_categories} qc
|
||||
ON qc.id = qbe.questioncategoryid
|
||||
WHERE qc.id = ?', array($matchcat->id));
|
||||
|
||||
foreach ($questions as $question) {
|
||||
if (isset($questioncache[$question->stamp." ".$question->version])) {
|
||||
|
|
|
@ -1064,14 +1064,14 @@ function course_delete_module($cmid, $async = false) {
|
|||
}
|
||||
}
|
||||
|
||||
question_delete_activity($cm);
|
||||
|
||||
// Call the delete_instance function, if it returns false throw an exception.
|
||||
if (!$deleteinstancefunction($cm->instance)) {
|
||||
throw new moodle_exception('cannotdeletemoduleinstance', '', '', null,
|
||||
"Cannot delete the module $modulename (instance).");
|
||||
}
|
||||
|
||||
question_delete_activity($cm);
|
||||
|
||||
// Remove all module files in case modules forget to do that.
|
||||
$fs = get_file_storage();
|
||||
$fs->delete_area_files($modcontext->id);
|
||||
|
|
|
@ -1706,8 +1706,13 @@ class core_course_courselib_testcase extends advanced_testcase {
|
|||
$this->assertEquals(0, $DB->count_records('question_categories', $criteria));
|
||||
|
||||
// Verify questions deleted.
|
||||
$criteria = array('category' => $qcat->id);
|
||||
$this->assertEquals(0, $DB->count_records('question', $criteria));
|
||||
$criteria = [$qcat->id];
|
||||
$sql = 'SELECT COUNT(q.id)
|
||||
FROM {question} q
|
||||
JOIN {question_versions} qv ON qv.questionid = q.id
|
||||
JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
|
||||
WHERE qbe.questioncategoryid = ?';
|
||||
$this->assertEquals(0, $DB->count_records_sql($sql, $criteria));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
|
|
|
@ -257,7 +257,7 @@ $string['notflagged'] = 'Not flagged';
|
|||
$string['novirtualquestiontype'] = 'No virtual question type for question type {$a}';
|
||||
$string['numqas'] = 'No. question attempts';
|
||||
$string['numquestions'] = 'No. questions';
|
||||
$string['numquestionsandhidden'] = '{$a->numquestions} (+{$a->numhidden} hidden)';
|
||||
$string['numquestionsandhidden'] = '{$a->numquestions} (+{$a->numhidden} hidden +{$a->numdraft} draft)';
|
||||
$string['page-question-x'] = 'Any question page';
|
||||
$string['page-question-edit'] = 'Question editing page';
|
||||
$string['page-question-category'] = 'Question category page';
|
||||
|
@ -426,9 +426,11 @@ $string['privacy:metadata:database:question_attempts'] = 'The information about
|
|||
$string['privacy:metadata:database:question_attempts:flagged'] = 'An indication that the user has flagged this question within the attempt.';
|
||||
$string['privacy:metadata:database:question_attempts:responsesummary'] = 'A summary of the question response.';
|
||||
$string['privacy:metadata:database:question_attempts:timemodified'] = 'The time that the question attempt was updated.';
|
||||
$string['privacy:metadata:link:qbehaviour'] = 'The question subsystem makes use of the Question behaviours plugin type.';
|
||||
$string['privacy:metadata:link:qformat'] = 'The question subsystem makes use of the Question import/export formats plugin type for the purpose of importing and exporting questions in different formats.';
|
||||
$string['privacy:metadata:link:qtype'] = 'The question subsystem interacts with the Question types plugin type which contains the different types of questions.';
|
||||
$string['privacy:metadata:database:question_bank_entries'] = 'The details about a specific question bank entry.';
|
||||
$string['privacy:metadata:database:question_bank_entries:ownerid'] = 'The person who owns the question bank entry.';
|
||||
$string['privacy:metadata:link:qbehaviour'] = 'The Question subsystem makes use of the Question Behaviour plugintype.';
|
||||
$string['privacy:metadata:link:qformat'] = 'The Question subsystem makes use of the Question Format plugintype for the purpose of importing and exporting questions in different formats.';
|
||||
$string['privacy:metadata:link:qtype'] = 'The Question subsystem interacts with the Question Type plugintype which contains the different types of questions.';
|
||||
$string['questionbehaviouradminsetting'] = 'Question behaviour settings';
|
||||
$string['questionbehavioursdisabled'] = 'Question behaviours to disable';
|
||||
$string['questionbehavioursdisabledexplained'] = 'Enter a comma-separated list of behaviours you do not want to appear in the drop-down menu.';
|
||||
|
@ -496,3 +498,4 @@ $string['xoutofmax'] = '{$a->mark} out of {$a->max}';
|
|||
$string['yougotnright'] = 'You have correctly selected {$a->num}.';
|
||||
$string['qbanknotfound'] = 'The \'{$a}\' question bank plugin doesn\'t exist or is not recognised.';
|
||||
$string['noquestionbanks'] = 'No question bank plugin found.';
|
||||
$string['questionloaderror'] = 'Could not load the question options.';
|
||||
|
|
|
@ -1402,10 +1402,9 @@
|
|||
<INDEX NAME="contextididnumber" UNIQUE="true" FIELDS="contextid, idnumber"/>
|
||||
</INDEXES>
|
||||
</TABLE>
|
||||
<TABLE NAME="question" COMMENT="The questions themselves">
|
||||
<TABLE NAME="question" COMMENT="This table stores the definition of one version of a question.">
|
||||
<FIELDS>
|
||||
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
|
||||
<FIELD NAME="category" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
|
||||
<FIELD NAME="parent" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
|
||||
<FIELD NAME="name" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false"/>
|
||||
<FIELD NAME="questiontext" TYPE="text" NOTNULL="true" SEQUENCE="false"/>
|
||||
|
@ -1417,26 +1416,83 @@
|
|||
<FIELD NAME="qtype" TYPE="char" LENGTH="20" NOTNULL="true" SEQUENCE="false"/>
|
||||
<FIELD NAME="length" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="1" SEQUENCE="false"/>
|
||||
<FIELD NAME="stamp" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false"/>
|
||||
<FIELD NAME="version" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false"/>
|
||||
<FIELD NAME="hidden" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
|
||||
<FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="time question was created"/>
|
||||
<FIELD NAME="timemodified" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="time that question was last modified"/>
|
||||
<FIELD NAME="createdby" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false" COMMENT="userid of person who created this question"/>
|
||||
<FIELD NAME="modifiedby" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false" COMMENT="userid of person who last edited this question"/>
|
||||
<FIELD NAME="idnumber" TYPE="char" LENGTH="100" NOTNULL="false" SEQUENCE="false"/>
|
||||
</FIELDS>
|
||||
<KEYS>
|
||||
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
|
||||
<KEY NAME="category" TYPE="foreign" FIELDS="category" REFTABLE="question_categories" REFFIELDS="id"/>
|
||||
<KEY NAME="parent" TYPE="foreign" FIELDS="parent" REFTABLE="question" REFFIELDS="id" COMMENT="note that to make this recursive FK working someday, the parent field must be declared NULL"/>
|
||||
<KEY NAME="createdby" TYPE="foreign" FIELDS="createdby" REFTABLE="user" REFFIELDS="id" COMMENT="foreign (createdby) references user (id)"/>
|
||||
<KEY NAME="modifiedby" TYPE="foreign" FIELDS="modifiedby" REFTABLE="user" REFFIELDS="id" COMMENT="foreign (modifiedby) references user (id)"/>
|
||||
</KEYS>
|
||||
<INDEXES>
|
||||
<INDEX NAME="qtype" UNIQUE="false" FIELDS="qtype"/>
|
||||
<INDEX NAME="categoryidnumber" UNIQUE="true" FIELDS="category, idnumber"/>
|
||||
</INDEXES>
|
||||
</TABLE>
|
||||
<TABLE NAME="question_bank_entries" COMMENT="Each question bank entry. This table has one row for each question that appears in the question bank.">
|
||||
<FIELDS>
|
||||
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
|
||||
<FIELD NAME="questioncategoryid" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="ID of the category this question is part of."/>
|
||||
<FIELD NAME="idnumber" TYPE="char" LENGTH="100" NOTNULL="false" SEQUENCE="false" COMMENT="Unique identifier, useful especially for mapping to external entities."/>
|
||||
<FIELD NAME="ownerid" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false" COMMENT="userid of person who owns this question bank entry."/>
|
||||
</FIELDS>
|
||||
<KEYS>
|
||||
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
|
||||
<KEY NAME="questioncategoryid" TYPE="foreign" FIELDS="questioncategoryid" REFTABLE="question_categories" REFFIELDS="id"/>
|
||||
<KEY NAME="ownerid" TYPE="foreign" FIELDS="ownerid" REFTABLE="user" REFFIELDS="id"/>
|
||||
</KEYS>
|
||||
<INDEXES>
|
||||
<INDEX NAME="categoryidnumber" UNIQUE="true" FIELDS="questioncategoryid, idnumber"/>
|
||||
</INDEXES>
|
||||
</TABLE>
|
||||
<TABLE NAME="question_versions" COMMENT="A join table linking the different question version definitions in the question table to the question_bank_entires.">
|
||||
<FIELDS>
|
||||
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
|
||||
<FIELD NAME="questionbankentryid" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="ID of the question bank entry this question version is part of."/>
|
||||
<FIELD NAME="version" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="1" SEQUENCE="false" COMMENT="Version number for the question where the first version is always 1."/>
|
||||
<FIELD NAME="questionid" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="The question ID."/>
|
||||
<FIELD NAME="status" TYPE="char" LENGTH="10" NOTNULL="false" DEFAULT="ready" SEQUENCE="false" COMMENT="If the question is ready, hidden or draft"/>
|
||||
</FIELDS>
|
||||
<KEYS>
|
||||
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
|
||||
<KEY NAME="questionbankentryid" TYPE="foreign" FIELDS="questionbankentryid" REFTABLE="question_bank_entries" REFFIELDS="id"/>
|
||||
<KEY NAME="questionid" TYPE="foreign" FIELDS="questionid" REFTABLE="question" REFFIELDS="id"/>
|
||||
</KEYS>
|
||||
</TABLE>
|
||||
<TABLE NAME="question_references" COMMENT="Records where a specific question is used.">
|
||||
<FIELDS>
|
||||
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
|
||||
<FIELD NAME="usingcontextid" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Context where question is used."/>
|
||||
<FIELD NAME="component" TYPE="char" LENGTH="100" NOTNULL="false" SEQUENCE="false" COMMENT="Component (e.g. mod_quiz or core_question)"/>
|
||||
<FIELD NAME="questionarea" TYPE="char" LENGTH="50" NOTNULL="false" SEQUENCE="false" COMMENT="Depending on the component, which area the question is used in (e.g. slot for quiz)."/>
|
||||
<FIELD NAME="itemid" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false" COMMENT="Plugin specific id (e.g. slotid for quiz) where its used."/>
|
||||
<FIELD NAME="questionbankentryid" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="ID of the question bank entry this question is part of."/>
|
||||
<FIELD NAME="version" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false" COMMENT="Version number for the question where NULL means use the latest ready version."/>
|
||||
</FIELDS>
|
||||
<KEYS>
|
||||
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
|
||||
<KEY NAME="usingcontextid" TYPE="foreign" FIELDS="usingcontextid" REFTABLE="context" REFFIELDS="id"/>
|
||||
<KEY NAME="questionbankentryid" TYPE="foreign" FIELDS="questionbankentryid" REFTABLE="question_bank_entries" REFFIELDS="id"/>
|
||||
</KEYS>
|
||||
</TABLE>
|
||||
<TABLE NAME="question_set_references" COMMENT="Records where groups of questions are used.">
|
||||
<FIELDS>
|
||||
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
|
||||
<FIELD NAME="usingcontextid" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Context where question is used."/>
|
||||
<FIELD NAME="component" TYPE="char" LENGTH="100" NOTNULL="false" SEQUENCE="false" COMMENT="Component (e.g. mod_quiz)"/>
|
||||
<FIELD NAME="questionarea" TYPE="char" LENGTH="50" NOTNULL="false" SEQUENCE="false" COMMENT="Depending on the component, which area the question is used in (e.g. slot for quiz)."/>
|
||||
<FIELD NAME="itemid" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false" COMMENT="Plugin specific id (e.g. slotid for quiz) where its used."/>
|
||||
<FIELD NAME="questionscontextid" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Context questions come from."/>
|
||||
<FIELD NAME="filtercondition" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="Filter expression in json format"/>
|
||||
</FIELDS>
|
||||
<KEYS>
|
||||
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
|
||||
<KEY NAME="usingcontextid" TYPE="foreign" FIELDS="usingcontextid" REFTABLE="context" REFFIELDS="id"/>
|
||||
<KEY NAME="questionscontextid" TYPE="foreign" FIELDS="questionscontextid" REFTABLE="context" REFFIELDS="id"/>
|
||||
</KEYS>
|
||||
</TABLE>
|
||||
<TABLE NAME="question_answers" COMMENT="Answers, with a fractional grade (0-1) and feedback">
|
||||
<FIELDS>
|
||||
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
|
||||
|
|
|
@ -61,7 +61,6 @@ $renamedclasses = [
|
|||
'core_question\\bank\\copy_action_column' => 'qbank_editquestion\\copy_action_column',
|
||||
'core_question\\bank\\edit_action_column' => 'qbank_editquestion\\edit_action_column',
|
||||
'core_question\\bank\\creator_name_column' => 'qbank_viewcreator\\creator_name_column',
|
||||
'core_question\\bank\\modifier_name_column' => 'qbank_viewcreator\\modifier_name_column',
|
||||
'core_question\\bank\\question_name_column' => 'qbank_viewquestionname\\viewquestionname_column_helper',
|
||||
'core_question\\bank\\question_name_idnumber_tags_column' => 'qbank_viewquestionname\\question_name_idnumber_tags_column',
|
||||
'core_question\\bank\\delete_action_column' => 'qbank_deletequestion\\delete_action_column',
|
||||
|
@ -81,5 +80,7 @@ $renamedclasses = [
|
|||
'export_form' => 'qbank_exportquestions\\form\\export_form',
|
||||
'preview_options_form' => 'qbank_previewquestion\\form\\preview_options_form',
|
||||
'question_preview_options' => 'qbank_previewquestion\\output\\question_preview_options',
|
||||
'core_question\\form\\tags' => '\qbank_tagquestion\\form\\tags_form'
|
||||
'core_question\\form\\tags' => 'qbank_tagquestion\\form\\tags_form',
|
||||
'context_to_string_translator' => 'core_question\\local\\bank\\context_to_string_translator',
|
||||
'question_edit_contexts' => 'core_question\\local\\bank\\question_edit_contexts',
|
||||
];
|
||||
|
|
|
@ -3809,5 +3809,158 @@ privatefiles,moodle|/user/files.php';
|
|||
upgrade_main_savepoint(true, 2022012100.02);
|
||||
}
|
||||
|
||||
// Introduce question versioning to core.
|
||||
// First, create the new tables.
|
||||
if ($oldversion < 2022020200.01) {
|
||||
// Define table question_bank_entries to be created.
|
||||
$table = new xmldb_table('question_bank_entries');
|
||||
|
||||
// Adding fields to table question_bank_entries.
|
||||
$table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
|
||||
$table->add_field('questioncategoryid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, 0);
|
||||
$table->add_field('idnumber', XMLDB_TYPE_CHAR, '100', null, null, null, null);
|
||||
$table->add_field('ownerid', XMLDB_TYPE_INTEGER, '10', null, null, null, null);
|
||||
|
||||
// Adding keys to table question_bank_entries.
|
||||
$table->add_key('primary', XMLDB_KEY_PRIMARY, ['id']);
|
||||
$table->add_key('questioncategoryid', XMLDB_KEY_FOREIGN, ['questioncategoryid'], 'question_categories', ['id']);
|
||||
$table->add_key('ownerid', XMLDB_KEY_FOREIGN, ['ownerid'], 'user', ['id']);
|
||||
|
||||
// Conditionally launch create table for question_bank_entries.
|
||||
if (!$dbman->table_exists($table)) {
|
||||
$dbman->create_table($table);
|
||||
}
|
||||
|
||||
// Create category id and id number index.
|
||||
$index = new xmldb_index('categoryidnumber', XMLDB_INDEX_UNIQUE, ['questioncategoryid', 'idnumber']);
|
||||
|
||||
// Conditionally launch add index categoryidnumber.
|
||||
if (!$dbman->index_exists($table, $index)) {
|
||||
$dbman->add_index($table, $index);
|
||||
}
|
||||
|
||||
// Define table question_versions to be created.
|
||||
$table = new xmldb_table('question_versions');
|
||||
|
||||
// Adding fields to table question_versions.
|
||||
$table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
|
||||
$table->add_field('questionbankentryid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
|
||||
$table->add_field('version', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, 1);
|
||||
$table->add_field('questionid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, 0);
|
||||
$table->add_field('status', XMLDB_TYPE_CHAR, '10', null, XMLDB_NOTNULL, null, 'ready');
|
||||
|
||||
// Adding keys to table question_versions.
|
||||
$table->add_key('primary', XMLDB_KEY_PRIMARY, ['id']);
|
||||
$table->add_key('questionbankentryid', XMLDB_KEY_FOREIGN, ['questionbankentryid'], 'question_bank_entries', ['id']);
|
||||
$table->add_key('questionid', XMLDB_KEY_FOREIGN, ['questionid'], 'question', ['id']);
|
||||
|
||||
// Conditionally launch create table for question_versions.
|
||||
if (!$dbman->table_exists($table)) {
|
||||
$dbman->create_table($table);
|
||||
}
|
||||
|
||||
// Define table question_references to be created.
|
||||
$table = new xmldb_table('question_references');
|
||||
|
||||
// Adding fields to table question_references.
|
||||
$table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
|
||||
$table->add_field('usingcontextid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, 0);
|
||||
$table->add_field('component', XMLDB_TYPE_CHAR, '100', null, null, null, null);
|
||||
$table->add_field('questionarea', XMLDB_TYPE_CHAR, '50', null, null, null, null);
|
||||
$table->add_field('itemid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
|
||||
$table->add_field('questionbankentryid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
|
||||
$table->add_field('version', XMLDB_TYPE_INTEGER, '10', null, null, null, null);
|
||||
|
||||
// Adding keys to table question_references.
|
||||
$table->add_key('primary', XMLDB_KEY_PRIMARY, ['id']);
|
||||
$table->add_key('usingcontextid', XMLDB_KEY_FOREIGN, ['usingcontextid'], 'context', ['id']);
|
||||
$table->add_key('questionbankentryid', XMLDB_KEY_FOREIGN, ['questionbankentryid'], 'question_bank_entries', ['id']);
|
||||
|
||||
// Conditionally launch create table for question_references.
|
||||
if (!$dbman->table_exists($table)) {
|
||||
$dbman->create_table($table);
|
||||
}
|
||||
|
||||
// Define table question_set_references to be created.
|
||||
$table = new xmldb_table('question_set_references');
|
||||
|
||||
// Adding fields to table question_set_references.
|
||||
$table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
|
||||
$table->add_field('usingcontextid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, 0);
|
||||
$table->add_field('component', XMLDB_TYPE_CHAR, '100', null, null, null, null);
|
||||
$table->add_field('questionarea', XMLDB_TYPE_CHAR, '50', null, null, null, null);
|
||||
$table->add_field('itemid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
|
||||
$table->add_field('questionscontextid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, 0);
|
||||
$table->add_field('filtercondition', XMLDB_TYPE_TEXT, null, null, null, null, null);
|
||||
|
||||
// Adding keys to table question_set_references.
|
||||
$table->add_key('primary', XMLDB_KEY_PRIMARY, ['id']);
|
||||
$table->add_key('usingcontextid', XMLDB_KEY_FOREIGN, ['usingcontextid'], 'context', ['id']);
|
||||
$table->add_key('itemid', XMLDB_KEY_FOREIGN, ['itemid'], 'quiz_slots', ['id']);
|
||||
$table->add_key('questionscontextid', XMLDB_KEY_FOREIGN, ['questionscontextid'], 'context', ['id']);
|
||||
|
||||
// Conditionally launch create table for question_set_references.
|
||||
if (!$dbman->table_exists($table)) {
|
||||
$dbman->create_table($table);
|
||||
}
|
||||
|
||||
// Main savepoint reached.
|
||||
upgrade_main_savepoint(true, 2022020200.01);
|
||||
}
|
||||
|
||||
if ($oldversion < 2022020200.02) {
|
||||
// Next, split question records into the new tables.
|
||||
upgrade_migrate_question_table();
|
||||
// Main savepoint reached.
|
||||
upgrade_main_savepoint(true, 2022020200.02);
|
||||
}
|
||||
|
||||
// Finally, drop fields from question table.
|
||||
if ($oldversion < 2022020200.03) {
|
||||
// Define fields to be dropped from questions.
|
||||
$table = new xmldb_table('question');
|
||||
|
||||
$field = new xmldb_field('version');
|
||||
// Conditionally launch drop field version.
|
||||
if ($dbman->field_exists($table, $field)) {
|
||||
$dbman->drop_field($table, $field);
|
||||
}
|
||||
|
||||
$field = new xmldb_field('hidden');
|
||||
// Conditionally launch drop field hidden.
|
||||
if ($dbman->field_exists($table, $field)) {
|
||||
$dbman->drop_field($table, $field);
|
||||
}
|
||||
|
||||
// Define index categoryidnumber (not unique) to be dropped form question.
|
||||
$index = new xmldb_index('categoryidnumber', XMLDB_INDEX_UNIQUE, ['category', 'idnumber']);
|
||||
|
||||
// Conditionally launch drop index categoryidnumber.
|
||||
if ($dbman->index_exists($table, $index)) {
|
||||
$dbman->drop_index($table, $index);
|
||||
}
|
||||
|
||||
// Define key category (foreign) to be dropped form questions.
|
||||
$key = new xmldb_key('category', XMLDB_KEY_FOREIGN, ['category'], 'question_categories', ['id']);
|
||||
|
||||
// Launch drop key category.
|
||||
$dbman->drop_key($table, $key);
|
||||
|
||||
$field = new xmldb_field('idnumber');
|
||||
// Conditionally launch drop field idnumber.
|
||||
if ($dbman->field_exists($table, $field)) {
|
||||
$dbman->drop_field($table, $field);
|
||||
}
|
||||
|
||||
$field = new xmldb_field('category');
|
||||
// Conditionally launch drop field category.
|
||||
if ($dbman->field_exists($table, $field)) {
|
||||
$dbman->drop_field($table, $field);
|
||||
}
|
||||
|
||||
// Main savepoint reached.
|
||||
upgrade_main_savepoint(true, 2022020200.03);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -1274,3 +1274,137 @@ function upgrade_calendar_override_events_fix(stdClass $info, bool $output = tru
|
|||
upgrade_calendar_events_mtrace('', $output);
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Split question table in 2 new tables:
|
||||
*
|
||||
* question_bank_entries
|
||||
* question_versions
|
||||
*
|
||||
* Move the random questions records to the following table:
|
||||
* question_set_reference
|
||||
*
|
||||
* Move the question related records from quiz_slots table to:
|
||||
* question_reference
|
||||
*
|
||||
* Move the tag related data from quiz_slot_tags to:
|
||||
* question_references
|
||||
*
|
||||
* For more information: https://moodle.org/mod/forum/discuss.php?d=417599#p1688163
|
||||
*/
|
||||
function upgrade_migrate_question_table(): void {
|
||||
global $DB;
|
||||
|
||||
// Maximum size of array.
|
||||
$maxlength = 30000;
|
||||
|
||||
// Array of question_versions objects.
|
||||
$questionversions = [];
|
||||
|
||||
// Array of question_set_references objects.
|
||||
$questionsetreferences = [];
|
||||
|
||||
// The actual update/insert done with multiple DB access, so we do it in a transaction.
|
||||
$transaction = $DB->start_delegated_transaction();
|
||||
|
||||
// Count all questions to be migrated (for progress bar).
|
||||
$total = $DB->count_records('question');
|
||||
$pbar = new progress_bar('migratequestions', 1000, true);
|
||||
$i = 0;
|
||||
// Get all records in question table, we dont need the subquestions, just regular questions and random questions.
|
||||
$questions = $DB->get_recordset('question');
|
||||
foreach ($questions as $question) {
|
||||
upgrade_set_timeout(60);
|
||||
// Populate table question_bank_entries.
|
||||
$questionbankentry = new \stdClass();
|
||||
$questionbankentry->questioncategoryid = $question->category;
|
||||
$questionbankentry->idnumber = $question->idnumber;
|
||||
$questionbankentry->ownerid = $question->createdby;
|
||||
// Insert a question_bank_entries record here as the id is required to populate other tables.
|
||||
$questionbankentry->id = $DB->insert_record('question_bank_entries', $questionbankentry);
|
||||
|
||||
// Create question_versions records to be added.
|
||||
$questionversion = new \stdClass();
|
||||
$questionversion->questionbankentryid = $questionbankentry->id;
|
||||
$questionversion->questionid = $question->id;
|
||||
$questionstatus = \core_question\local\bank\question_version_status::QUESTION_STATUS_READY;
|
||||
if ((int)$question->hidden === 1) {
|
||||
$questionstatus = \core_question\local\bank\question_version_status::QUESTION_STATUS_HIDDEN;
|
||||
}
|
||||
$questionversion->status = $questionstatus;
|
||||
$questionversions[] = $questionversion;
|
||||
|
||||
// Insert the records if the array limit is reached.
|
||||
if (count($questionversions) >= $maxlength) {
|
||||
$DB->insert_records('question_versions', $questionversions);
|
||||
$questionversions = [];
|
||||
}
|
||||
|
||||
// Create question_set_references records to be added.
|
||||
// Only if the question type is random and the question is used in a quiz.
|
||||
if ($question->qtype === 'random') {
|
||||
$quizslots = $DB->get_records('quiz_slots', ['questionid' => $question->id]);
|
||||
foreach ($quizslots as $quizslot) {
|
||||
$questionsetreference = new \stdClass();
|
||||
$cm = get_coursemodule_from_instance('quiz', $quizslot->quizid);
|
||||
$questionsetreference->usingcontextid = context_module::instance($cm->id)->id;
|
||||
$questionsetreference->component = 'mod_quiz';
|
||||
$questionsetreference->questionarea = 'slot';
|
||||
$questionsetreference->itemid = $quizslot->id;
|
||||
$catcontext = $DB->get_field('question_categories', 'contextid', ['id' => $question->category]);
|
||||
$questionsetreference->questionscontextid = $catcontext;
|
||||
// Migration of the slot tags and filter identifiers from slot table to filtercondition.
|
||||
$filtercondition = new stdClass();
|
||||
$filtercondition->questioncategoryid = $question->category;
|
||||
$filtercondition->includingsubcategories = $quizslot->includingsubcategories;
|
||||
$tags = $DB->get_records('quiz_slot_tags', ['slotid' => $quizslot->id]);
|
||||
$tagstrings = [];
|
||||
foreach ($tags as $tag) {
|
||||
$tagstrings [] = "{$tag->id},{$tag->name}";
|
||||
}
|
||||
if (!empty($tagstrings)) {
|
||||
$filtercondition->tags = $tagstrings;
|
||||
}
|
||||
$questionsetreference->filtercondition = json_encode($filtercondition);
|
||||
|
||||
$questionsetreferences[] = $questionsetreference;
|
||||
|
||||
// Insert the records if the array limit is reached.
|
||||
if (count($questionsetreferences) >= $maxlength) {
|
||||
$DB->insert_records('question_set_references', $questionsetreferences);
|
||||
$questionsetreferences = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
// Update progress.
|
||||
$i++;
|
||||
$pbar->update($i, $total, "Migrating questions - $i/$total.");
|
||||
}
|
||||
$questions->close();
|
||||
|
||||
// Insert the remaining question_versions records.
|
||||
if ($questionversions) {
|
||||
$DB->insert_records('question_versions', $questionversions);
|
||||
}
|
||||
|
||||
// Insert the remaining question_set_references records.
|
||||
if ($questionsetreferences) {
|
||||
$DB->insert_records('question_set_references', $questionsetreferences);
|
||||
}
|
||||
|
||||
// Create question_references record for each question.
|
||||
// Except if qtype is random. That case is handled by question_set_reference.
|
||||
$sql = "INSERT INTO {question_references}
|
||||
(usingcontextid, component, questionarea, itemid, questionbankentryid)
|
||||
SELECT c.id, 'mod_quiz', 'slot', qs.id, qv.questionbankentryid
|
||||
FROM {question} q
|
||||
JOIN {question_versions} qv ON q.id = qv.questionid
|
||||
JOIN {quiz_slots} qs ON q.id = qs.questionid
|
||||
JOIN {modules} m ON m.name = 'quiz'
|
||||
JOIN {course_modules} cm ON cm.module = m.id AND cm.instance = qs.quizid
|
||||
JOIN {context} c ON c.instanceid = cm.id AND c.contextlevel = " . CONTEXT_MODULE . "
|
||||
WHERE q.qtype <> 'random'";
|
||||
$DB->execute($sql);
|
||||
|
||||
$transaction->allow_commit();
|
||||
}
|
||||
|
|
1432
lib/questionlib.php
1432
lib/questionlib.php
File diff suppressed because it is too large
Load diff
|
@ -109,6 +109,18 @@ class core_questionlib_testcase extends advanced_testcase {
|
|||
return array($category, $course, $quiz, $qcat, $questions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that a category contains a specific number of questions.
|
||||
*
|
||||
* @param int $categoryid int Category id.
|
||||
* @param int $numberofquestions Number of question in a category.
|
||||
* @return void Questions in a category.
|
||||
*/
|
||||
protected function assert_category_contains_questions(int $categoryid, int $numberofquestions): void {
|
||||
$questionsid = question_bank::get_finder()->get_questions_from_categories([$categoryid], null);
|
||||
$this->assertEquals($numberofquestions, count($questionsid));
|
||||
}
|
||||
|
||||
public function test_question_reorder_qtypes() {
|
||||
$this->assertEquals(
|
||||
array(0 => 't2', 1 => 't1', 2 => 't3'),
|
||||
|
@ -222,8 +234,7 @@ class core_questionlib_testcase extends advanced_testcase {
|
|||
|
||||
// Create some question categories and questions in this course.
|
||||
$coursecontext = context_course::instance($course->id);
|
||||
$questioncat = $questiongenerator->create_question_category(array('contextid' =>
|
||||
$coursecontext->id));
|
||||
$questioncat = $questiongenerator->create_question_category(array('contextid' => $coursecontext->id));
|
||||
$question1 = $questiongenerator->create_question('shortanswer', null, array('category' => $questioncat->id));
|
||||
$question2 = $questiongenerator->create_question('shortanswer', null, array('category' => $questioncat->id));
|
||||
|
||||
|
@ -256,8 +267,14 @@ class core_questionlib_testcase extends advanced_testcase {
|
|||
array(context_course::instance($course2->id)->id), '*', MUST_EXIST);
|
||||
|
||||
// Check that there are two questions in the restored to course's context.
|
||||
$this->assertEquals(2, $DB->count_records('question', array('category' => $restoredcategory->id)));
|
||||
|
||||
$this->assertEquals(2, $DB->get_record_sql('SELECT COUNT(q.id) as questioncount
|
||||
FROM {question} q
|
||||
JOIN {question_versions} qv
|
||||
ON qv.questionid = q.id
|
||||
JOIN {question_bank_entries} qbe
|
||||
ON qbe.id = qv.questionbankentryid
|
||||
WHERE qbe.questioncategoryid = ?',
|
||||
[$restoredcategory->id])->questioncount);
|
||||
$rc->destroy();
|
||||
}
|
||||
|
||||
|
@ -327,8 +344,7 @@ class core_questionlib_testcase extends advanced_testcase {
|
|||
$this->assertEquals(0, $DB->count_records('question_categories', $criteria));
|
||||
|
||||
// Verify questions deleted or moved.
|
||||
$criteria = array('category' => $qcat->id);
|
||||
$this->assertEquals(0, $DB->count_records('question', $criteria));
|
||||
$this->assert_category_contains_questions($qcat->id, 0);
|
||||
|
||||
// Verify question not deleted.
|
||||
$criteria = array('id' => $questions[0]->id);
|
||||
|
@ -355,8 +371,7 @@ class core_questionlib_testcase extends advanced_testcase {
|
|||
$this->assertEquals(0, $DB->count_records('question_categories', $criteria));
|
||||
|
||||
// Verify questions deleted or moved.
|
||||
$criteria = array('category' => $qcat->id);
|
||||
$this->assertEquals(0, $DB->count_records('question', $criteria));
|
||||
$this->assert_category_contains_questions($qcat->id, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -377,8 +392,7 @@ class core_questionlib_testcase extends advanced_testcase {
|
|||
$this->assertEquals(0, $DB->count_records('question_categories', $criteria));
|
||||
|
||||
// Verify questions deleted or moved.
|
||||
$criteria = array('category' => $qcat->id);
|
||||
$this->assertEquals(0, $DB->count_records('question', $criteria));
|
||||
$this->assert_category_contains_questions($qcat->id, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -399,8 +413,7 @@ class core_questionlib_testcase extends advanced_testcase {
|
|||
$this->assertEquals(0, $DB->count_records('question_categories', $criteria));
|
||||
|
||||
// Verify questions deleted or moved.
|
||||
$criteria = array('category' => $qcat->id);
|
||||
$this->assertEquals(0, $DB->count_records('question', $criteria));
|
||||
$this->assert_category_contains_questions($qcat->id, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -421,8 +434,7 @@ class core_questionlib_testcase extends advanced_testcase {
|
|||
$this->assertEquals(0, $DB->count_records('question_categories', $criteria));
|
||||
|
||||
// Verify questions deleted or moved.
|
||||
$criteria = array('category' => $qcat->id);
|
||||
$this->assertEquals(0, $DB->count_records('question', $criteria));
|
||||
$this->assert_category_contains_questions($qcat->id, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -450,7 +462,9 @@ class core_questionlib_testcase extends advanced_testcase {
|
|||
$params = array($qcat2->contextid);
|
||||
$actualquestionscount = $DB->count_records_sql("SELECT COUNT(*)
|
||||
FROM {question} q
|
||||
JOIN {question_categories} qc ON q.category = qc.id
|
||||
JOIN {question_versions} qv ON qv.questionid = q.id
|
||||
JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
|
||||
JOIN {question_categories} qc ON qc.id = qbe.questioncategoryid
|
||||
WHERE qc.contextid = ?", $params);
|
||||
$this->assertEquals($questionsinqcat1 + $questionsinqcat2, $actualquestionscount);
|
||||
|
||||
|
@ -1728,16 +1742,11 @@ class core_questionlib_testcase extends advanced_testcase {
|
|||
$qtype = 'truefalse';
|
||||
$overrides = [
|
||||
'category' => $questioncat->id,
|
||||
'createdby' => ($isowner) ? $user->id : $otheruser->id,
|
||||
];
|
||||
|
||||
$question = $questiongenerator->create_question($qtype, null, $overrides);
|
||||
|
||||
// The question generator does not support setting of the createdby for some reason.
|
||||
$question->createdby = ($isowner) ? $user->id : $otheruser->id;
|
||||
$fromform = test_question_maker::get_question_form_data($qtype, null);
|
||||
$fromform = (object) $generator->combine_defaults_and_record((array) $fromform, $overrides);
|
||||
question_bank::get_qtype($qtype)->save_question($question, $fromform);
|
||||
|
||||
$this->setUser($user);
|
||||
$result = question_has_capability_on($question, $capability);
|
||||
$this->assertEquals($expect, $result);
|
||||
|
@ -1779,16 +1788,11 @@ class core_questionlib_testcase extends advanced_testcase {
|
|||
$qtype = 'truefalse';
|
||||
$overrides = [
|
||||
'category' => $questioncat->id,
|
||||
'createdby' => ($isowner) ? $user->id : $otheruser->id,
|
||||
];
|
||||
|
||||
$question = $questiongenerator->create_question($qtype, null, $overrides);
|
||||
|
||||
// The question generator does not support setting of the createdby for some reason.
|
||||
$question->createdby = ($isowner) ? $user->id : $otheruser->id;
|
||||
$fromform = test_question_maker::get_question_form_data($qtype, null);
|
||||
$fromform = (object) $generator->combine_defaults_and_record((array) $fromform, $overrides);
|
||||
question_bank::get_qtype($qtype)->save_question($question, $fromform);
|
||||
|
||||
$this->setUser($user);
|
||||
$result = question_has_capability_on($question->id, $capability);
|
||||
$this->assertEquals($expect, $result);
|
||||
|
@ -1830,16 +1834,11 @@ class core_questionlib_testcase extends advanced_testcase {
|
|||
$qtype = 'truefalse';
|
||||
$overrides = [
|
||||
'category' => $questioncat->id,
|
||||
'createdby' => ($isowner) ? $user->id : $otheruser->id,
|
||||
];
|
||||
|
||||
$question = $questiongenerator->create_question($qtype, null, $overrides);
|
||||
|
||||
// The question generator does not support setting of the createdby for some reason.
|
||||
$question->createdby = ($isowner) ? $user->id : $otheruser->id;
|
||||
$fromform = test_question_maker::get_question_form_data($qtype, null);
|
||||
$fromform = (object) $generator->combine_defaults_and_record((array) $fromform, $overrides);
|
||||
question_bank::get_qtype($qtype)->save_question($question, $fromform);
|
||||
|
||||
$this->setUser($user);
|
||||
$result = question_has_capability_on((string) $question->id, $capability);
|
||||
$this->assertEquals($expect, $result);
|
||||
|
@ -1887,16 +1886,11 @@ class core_questionlib_testcase extends advanced_testcase {
|
|||
$qtype = 'truefalse';
|
||||
$overrides = [
|
||||
'category' => $questioncat->id,
|
||||
'createdby' => ($isowner) ? $user->id : $otheruser->id,
|
||||
];
|
||||
|
||||
$question = $questiongenerator->create_question($qtype, null, $overrides);
|
||||
|
||||
// The question generator does not support setting of the createdby for some reason.
|
||||
$question->createdby = ($isowner) ? $user->id : $otheruser->id;
|
||||
$fromform = test_question_maker::get_question_form_data($qtype, null);
|
||||
$fromform = (object) $generator->combine_defaults_and_record((array) $fromform, $overrides);
|
||||
question_bank::get_qtype($qtype)->save_question($question, $fromform);
|
||||
|
||||
// Move the question.
|
||||
question_move_questions_to_category([$question->id], $newquestioncat->id);
|
||||
|
||||
|
@ -1941,12 +1935,10 @@ class core_questionlib_testcase extends advanced_testcase {
|
|||
// Create the question.
|
||||
$question = $questiongenerator->create_question('truefalse', null, [
|
||||
'category' => $questioncat->id,
|
||||
'createdby' => ($isowner) ? $user->id : $otheruser->id,
|
||||
]);
|
||||
$question = question_bank::load_question_data($question->id);
|
||||
|
||||
// The question generator does not support setting of the createdby for some reason.
|
||||
$question->createdby = ($isowner) ? $user->id : $otheruser->id;
|
||||
|
||||
$this->setUser($user);
|
||||
$result = question_has_capability_on($question, $capability);
|
||||
$this->assertEquals($expect, $result);
|
||||
|
@ -1970,12 +1962,10 @@ class core_questionlib_testcase extends advanced_testcase {
|
|||
// Create the question.
|
||||
$question = $questiongenerator->create_question('truefalse', null, [
|
||||
'category' => $questioncat->id,
|
||||
'createdby' => $user->id,
|
||||
]);
|
||||
$question = question_bank::load_question_data($question->id);
|
||||
|
||||
// The question generator does not support setting of the createdby for some reason.
|
||||
$question->createdby = $user->id;
|
||||
|
||||
$this->setUser($user);
|
||||
$result = question_has_capability_on((string)$question->id, 'tag');
|
||||
$this->assertFalse($result);
|
||||
|
@ -2057,4 +2047,218 @@ class core_questionlib_testcase extends advanced_testcase {
|
|||
$this->assertSame('id11', core_question_find_next_unused_idnumber('id9', $category->id));
|
||||
$this->assertSame('id11', core_question_find_next_unused_idnumber('id8', $category->id));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests for the question_move_questions_to_category function.
|
||||
*
|
||||
* @covers ::question_move_questions_to_category
|
||||
*/
|
||||
public function test_question_move_questions_to_category() {
|
||||
$this->resetAfterTest();
|
||||
|
||||
// Create the test data.
|
||||
list($category1, $course1, $quiz1, $questioncat1, $questions1) = $this->setup_quiz_and_questions();
|
||||
list($category2, $course2, $quiz2, $questioncat2, $questions2) = $this->setup_quiz_and_questions();
|
||||
|
||||
$this->assertCount(2, $questions1);
|
||||
$this->assertCount(2, $questions2);
|
||||
$questionsidtomove = [];
|
||||
foreach ($questions1 as $question1) {
|
||||
$questionsidtomove[] = $question1->id;
|
||||
}
|
||||
|
||||
// Move the question from quiz 1 to quiz 2.
|
||||
question_move_questions_to_category($questionsidtomove, $questioncat2->id);
|
||||
$this->assert_category_contains_questions($questioncat2->id, 4);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests for the idnumber_exist_in_question_category function.
|
||||
*
|
||||
* @covers ::idnumber_exist_in_question_category
|
||||
*/
|
||||
public function test_idnumber_exist_in_question_category() {
|
||||
global $DB;
|
||||
|
||||
$this->resetAfterTest();
|
||||
|
||||
// Create the test data.
|
||||
list($category1, $course1, $quiz1, $questioncat1, $questions1) = $this->setup_quiz_and_questions();
|
||||
list($category2, $course2, $quiz2, $questioncat2, $questions2) = $this->setup_quiz_and_questions();
|
||||
|
||||
$questionbankentry1 = get_question_bank_entry($questions1[0]->id);
|
||||
$entry = new stdClass();
|
||||
$entry->id = $questionbankentry1->id;
|
||||
$entry->idnumber = 1;
|
||||
$DB->update_record('question_bank_entries', $entry);
|
||||
|
||||
$questionbankentry2 = get_question_bank_entry($questions2[0]->id);
|
||||
$entry2 = new stdClass();
|
||||
$entry2->id = $questionbankentry2->id;
|
||||
$entry2->idnumber = 1;
|
||||
$DB->update_record('question_bank_entries', $entry2);
|
||||
|
||||
$questionbe = $DB->get_record('question_bank_entries', ['id' => $questionbankentry1->id]);
|
||||
|
||||
// Validate that a first stage of an idnumber exists (this format: xxxx_x).
|
||||
list($response, $record) = idnumber_exist_in_question_category($questionbe->idnumber, $questioncat1->id);
|
||||
$this->assertEquals([], $record);
|
||||
$this->assertEquals(true, $response);
|
||||
|
||||
// Move the question to a category that has a question with the same idnumber.
|
||||
question_move_questions_to_category($questions1[0]->id, $questioncat2->id);
|
||||
|
||||
// Validate that function return the last record used for the idnumber.
|
||||
list($response, $record) = idnumber_exist_in_question_category($questionbe->idnumber, $questioncat2->id);
|
||||
$record = reset($record);
|
||||
$idnumber = $record->idnumber;
|
||||
$this->assertEquals($idnumber, '1_1');
|
||||
$this->assertEquals(true, $response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test method is_latest().
|
||||
*
|
||||
* @covers ::is_latest
|
||||
*
|
||||
*/
|
||||
public function test_is_latest() {
|
||||
global $DB;
|
||||
$this->resetAfterTest();
|
||||
$generator = $this->getDataGenerator()->get_plugin_generator('core_question');
|
||||
$qcat1 = $generator->create_question_category(['name' => 'My category', 'sortorder' => 1, 'idnumber' => 'myqcat']);
|
||||
$question = $generator->create_question('shortanswer', null, ['name' => 'q1', 'category' => $qcat1->id]);
|
||||
$record = $DB->get_record('question_versions', ['questionid' => $question->id]);
|
||||
$firstversion = $record->version;
|
||||
$questionbankentryid = $record->questionbankentryid;
|
||||
$islatest = is_latest($firstversion, $questionbankentryid);
|
||||
$this->assertTrue($islatest);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test question bank entry deletion.
|
||||
*
|
||||
* @covers ::delete_question_bank_entry
|
||||
*/
|
||||
public function test_delete_question_bank_entry() {
|
||||
global $DB;
|
||||
$this->resetAfterTest();
|
||||
// Setup.
|
||||
$context = context_system::instance();
|
||||
$qgen = $this->getDataGenerator()->get_plugin_generator('core_question');
|
||||
$qcat = $qgen->create_question_category(array('contextid' => $context->id));
|
||||
$q1 = $qgen->create_question('shortanswer', null, array('category' => $qcat->id));
|
||||
// Make sure there is an entry in the entry table.
|
||||
$sql = 'SELECT qbe.id as id,
|
||||
qv.id as versionid
|
||||
FROM {question_bank_entries} qbe
|
||||
JOIN {question_versions} qv
|
||||
ON qbe.id = qv.questionbankentryid
|
||||
JOIN {question} q
|
||||
ON qv.questionid = q.id
|
||||
WHERE q.id = ?';
|
||||
$records = $DB->get_records_sql($sql, [$q1->id]);
|
||||
$this->assertCount(1, $records);
|
||||
// Delete the record.
|
||||
$record = reset($records);
|
||||
delete_question_bank_entry($record->id);
|
||||
$records = $DB->get_records('question_bank_entries', ['id' => $record->id]);
|
||||
// As the version record exists, it wont delete the data to resolve any errors.
|
||||
$this->assertCount(1, $records);
|
||||
$DB->delete_records('question_versions', ['id' => $record->versionid]);
|
||||
delete_question_bank_entry($record->id);
|
||||
$records = $DB->get_records('question_bank_entries', ['id' => $record->id]);
|
||||
$this->assertCount(0, $records);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test question bank entry object.
|
||||
*
|
||||
* @covers ::get_question_bank_entry
|
||||
*/
|
||||
public function test_get_question_bank_entry() {
|
||||
global $DB;
|
||||
$this->resetAfterTest();
|
||||
// Setup.
|
||||
$context = context_system::instance();
|
||||
$qgen = $this->getDataGenerator()->get_plugin_generator('core_question');
|
||||
$qcat = $qgen->create_question_category(array('contextid' => $context->id));
|
||||
$q1 = $qgen->create_question('shortanswer', null, array('category' => $qcat->id));
|
||||
// Make sure there is an entry in the entry table.
|
||||
$sql = 'SELECT qbe.id as id,
|
||||
qv.id as versionid
|
||||
FROM {question_bank_entries} qbe
|
||||
JOIN {question_versions} qv
|
||||
ON qbe.id = qv.questionbankentryid
|
||||
JOIN {question} q
|
||||
ON qv.questionid = q.id
|
||||
WHERE q.id = ?';
|
||||
$records = $DB->get_records_sql($sql, [$q1->id]);
|
||||
$this->assertCount(1, $records);
|
||||
$record = reset($records);
|
||||
$questionbankentry = get_question_bank_entry($q1->id);
|
||||
$this->assertEquals($questionbankentry->id, $record->id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the version objects for a question.
|
||||
*
|
||||
* @covers ::get_question_version
|
||||
*/
|
||||
public function test_get_question_version() {
|
||||
global $DB;
|
||||
$this->resetAfterTest();
|
||||
// Setup.
|
||||
$context = context_system::instance();
|
||||
$qgen = $this->getDataGenerator()->get_plugin_generator('core_question');
|
||||
$qcat = $qgen->create_question_category(array('contextid' => $context->id));
|
||||
$q1 = $qgen->create_question('shortanswer', null, array('category' => $qcat->id));
|
||||
// Make sure there is an entry in the entry table.
|
||||
$sql = 'SELECT qbe.id as id,
|
||||
qv.id as versionid
|
||||
FROM {question_bank_entries} qbe
|
||||
JOIN {question_versions} qv
|
||||
ON qbe.id = qv.questionbankentryid
|
||||
JOIN {question} q
|
||||
ON qv.questionid = q.id
|
||||
WHERE q.id = ?';
|
||||
$records = $DB->get_records_sql($sql, [$q1->id]);
|
||||
$this->assertCount(1, $records);
|
||||
$record = reset($records);
|
||||
$questionversions = get_question_version($q1->id);
|
||||
$questionversion = reset($questionversions);
|
||||
$this->assertEquals($questionversion->id, $record->versionid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test get next version of a question.
|
||||
*
|
||||
* @covers ::get_next_version
|
||||
*/
|
||||
public function test_get_next_version() {
|
||||
global $DB;
|
||||
$this->resetAfterTest();
|
||||
// Setup.
|
||||
$context = context_system::instance();
|
||||
$qgen = $this->getDataGenerator()->get_plugin_generator('core_question');
|
||||
$qcat = $qgen->create_question_category(array('contextid' => $context->id));
|
||||
$q1 = $qgen->create_question('shortanswer', null, array('category' => $qcat->id));
|
||||
// Make sure there is an entry in the entry table.
|
||||
$sql = 'SELECT qbe.id as id,
|
||||
qv.id as versionid,
|
||||
qv.version
|
||||
FROM {question_bank_entries} qbe
|
||||
JOIN {question_versions} qv
|
||||
ON qbe.id = qv.questionbankentryid
|
||||
JOIN {question} q
|
||||
ON qv.questionid = q.id
|
||||
WHERE q.id = ?';
|
||||
$records = $DB->get_records_sql($sql, [$q1->id]);
|
||||
$this->assertCount(1, $records);
|
||||
$record = reset($records);
|
||||
$this->assertEquals(1, $record->version);
|
||||
$nextversion = get_next_version($record->id);
|
||||
$this->assertEquals(2, $nextversion);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -99,6 +99,51 @@ information provided here is intended especially for developers.
|
|||
- question_preview_popup_params() is moved to \qbank_previewquestion\helper::question_preview_popup_params()
|
||||
Calling these functions in the question will point to the plugin, but the deprecation message will be activated in MDL-72004.
|
||||
The deprecated codes are removed from the questionlib for those two methods.
|
||||
* Function question_hash() from questionlib.php is deprecated without replacement.
|
||||
* Some of the new and old methods in the questionlib.php now using type hinting. Please make a note of this while making changes
|
||||
or implementing any question bank related feature in a plugin. These are the list of methods:
|
||||
- is_latest()
|
||||
- get_next_version()
|
||||
- get_question_version()
|
||||
- get_question_bank_entry()
|
||||
- core_question_find_next_unused_idnumber()
|
||||
- question_module_uses_questions()
|
||||
- question_page_type_list()
|
||||
- core_question_question_preview_pluginfile()
|
||||
- question_rewrite_question_preview_urls()
|
||||
- question_rewrite_question_urls()
|
||||
- question_get_all_capabilities()
|
||||
- question_get_question_capabilities()
|
||||
- question_require_capability_on()
|
||||
- question_has_capability_on()
|
||||
- question_default_export_filename()
|
||||
- get_import_export_formats()
|
||||
- question_categorylist_parents()
|
||||
- question_categorylist()
|
||||
- question_make_default_categories()
|
||||
- question_get_top_categories_for_contexts()
|
||||
- sort_categories_by_tree()
|
||||
- print_question_icon()
|
||||
- question_sort_tags()
|
||||
- _tidy_question()
|
||||
- question_preload_questions()
|
||||
- question_move_category_to_context()
|
||||
- move_question_set_references()
|
||||
- question_move_questions_to_category()
|
||||
- idnumber_exist_in_question_category()
|
||||
- question_move_question_tags_to_new_context()
|
||||
- question_delete_activity()
|
||||
- question_delete_course_category()
|
||||
- question_delete_course()
|
||||
- question_delete_context()
|
||||
- question_delete_question()
|
||||
- delete_question_bank_entry()
|
||||
- question_category_in_use()
|
||||
- question_category_delete_safe()
|
||||
- question_context_has_any_questions()
|
||||
- questions_in_use()
|
||||
- question_save_qtype_order()
|
||||
- question_reorder_qtypes()
|
||||
* The postgres driver now wraps calls to pg_field_type() and caches them in databasemeta to save an invisible internal
|
||||
DB call on every request.
|
||||
* The default type of 'core/toast' messages has been changed to 'information' (callers can still explicitely set the type)
|
||||
|
@ -116,8 +161,19 @@ completely removed from Moodle core too.
|
|||
Refer to upgrade.php to see transitioning from similar plugin criteria to core
|
||||
Refer to completion/upgrade.txt for additional information.
|
||||
* The method enable_plugin() has been added to the core_plugininfo\base class and it has been implemented by all the plugininfo
|
||||
classes extending it. When possible, the enable_plugin() method will store these changes into the config_log table, to let admins
|
||||
check when and who has enabled/disabled plugins.
|
||||
classes extending it. When possible, the enable_plugin() method will store these changes into the config_log table, to let admins
|
||||
check when and who has enabled/disabled plugins.
|
||||
* New tables are included as a part of https://docs.moodle.org/dev/Question_bank_improvements_for_Moodle_4.0
|
||||
- question_bank_entries -> Each question bank entry. This table has one row for each question that appears in the question bank.
|
||||
- question_versions -> Versions of the question. Store the data that defines how a particular version of the question works.
|
||||
- question_references -> Records where a specific question is used.
|
||||
- question_set_references -> Records where groups of questions are used (e.g.: Random questions).
|
||||
Also, some tables have been updated or removed:
|
||||
- question (fields migrated to the new tables)
|
||||
- quiz_slot (fields removed)
|
||||
- quiz_slot_tags (table removed)
|
||||
During the upgrade, data from the question table will be copied to the new tables. After this process,
|
||||
the data copied will be removed from question table quiz_slot and finally the the quiz_slot_tags table will be removed.
|
||||
* Final deprecation: The following functions along with associated tests have been removed:
|
||||
- core_grades_external::get_grades
|
||||
- core_grades_external::get_grade_item
|
||||
|
@ -176,6 +232,7 @@ value to get the list of blocks that won't be displayed for a theme.
|
|||
* A new parameter $strength of type int is added to method search_for_active_node. This parameter would help us to search for the active nodes based on the
|
||||
$strength passed to it.
|
||||
|
||||
|
||||
=== 3.11.4 ===
|
||||
* A new option dontforcesvgdownload has been added to the $options parameter of the send_file() function.
|
||||
Note: This option overrides the forced download of directly accessed SVGs, so should only be used where the calling method is
|
||||
|
|
2
mod/quiz/amd/build/question_slot.min.js
vendored
Normal file
2
mod/quiz/amd/build/question_slot.min.js
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
define ("mod_quiz/question_slot",["exports","core/ajax","core/notification"],function(a,b,c){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.init=void 0;c=function(a){return a&&a.__esModule?a:{default:a}}(c);var d=function(a,c){return(0,b.call)([{methodname:"mod_quiz_set_question_version",args:{slotid:a,newversion:c}}])[0]},e=function(){document.addEventListener("change",function(a){if(!a.target.matches("[data-action=\"mod_quiz-select_slot\"][data-slot-id]")){return}var b=a.target.dataset.slotId,e=parseInt(a.target.value);d(b,e).then(function(){location.reload()}).catch(c.default.exception)})};a.init=function init(){if(!1){return}e()}});
|
||||
//# sourceMappingURL=question_slot.min.js.map
|
1
mod/quiz/amd/build/question_slot.min.js.map
Normal file
1
mod/quiz/amd/build/question_slot.min.js.map
Normal file
|
@ -0,0 +1 @@
|
|||
{"version":3,"sources":["../src/question_slot.js"],"names":["setQuestionVersion","slotId","newVersion","methodname","args","slotid","newversion","registerEventListeners","document","addEventListener","e","target","matches","dataset","parseInt","value","then","location","reload","catch","Notification","exception","init"],"mappings":"yKAyBA,uD,GASMA,CAAAA,CAAkB,CAAG,SAACC,CAAD,CAASC,CAAT,QAAwB,WAAU,CAAC,CAC1DC,UAAU,CAAE,+BAD8C,CAE1DC,IAAI,CAAE,CACFC,MAAM,CAAEJ,CADN,CAEFK,UAAU,CAAEJ,CAFV,CAFoD,CAAD,CAAV,EAM/C,CAN+C,CAAxB,C,CAWrBK,CAAsB,CAAG,UAAM,CACjCC,QAAQ,CAACC,gBAAT,CAA0B,QAA1B,CAAoC,SAAAC,CAAC,CAAI,CACrC,GAAI,CAACA,CAAC,CAACC,MAAF,CAASC,OAAT,CAAiB,sDAAjB,CAAL,CAA6E,CACzE,MACH,CAHoC,GAK/BX,CAAAA,CAAM,CAAGS,CAAC,CAACC,MAAF,CAASE,OAAT,CAAiBZ,MALK,CAM/BC,CAAU,CAAGY,QAAQ,CAACJ,CAAC,CAACC,MAAF,CAASI,KAAV,CANU,CAQrCf,CAAkB,CAACC,CAAD,CAASC,CAAT,CAAlB,CACKc,IADL,CACU,UAAM,CACRC,QAAQ,CAACC,MAAT,EAEH,CAJL,EAKKC,KALL,CAKWC,UAAaC,SALxB,CAMH,CAdD,CAeH,C,QAQmB,QAAPC,CAAAA,IAAO,EAAM,CACtB,MAAsB,CAClB,MACH,CAEDf,CAAsB,EACzB,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 <http://www.gnu.org/licenses/>.\n\n/**\n * Render the question slot template for each question in the quiz edit view.\n *\n * @module mod_quiz/question_slot\n * @copyright 2021 Catalyst IT Australia Pty Ltd\n * @author Guillermo Gomez Arias <guillermogomez@catalyst-au.net>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {call as fetchMany} from 'core/ajax';\nimport Notification from 'core/notification';\n\n/**\n * Set the question version for the slot.\n *\n * @param {Number} slotId\n * @param {Number} newVersion\n * @return {Array} The modified question version\n */\nconst setQuestionVersion = (slotId, newVersion) => fetchMany([{\n methodname: 'mod_quiz_set_question_version',\n args: {\n slotid: slotId,\n newversion: newVersion,\n }\n}])[0];\n\n/**\n * Replace the container with a new version.\n */\nconst registerEventListeners = () => {\n document.addEventListener('change', e => {\n if (!e.target.matches('[data-action=\"mod_quiz-select_slot\"][data-slot-id]')) {\n return;\n }\n\n const slotId = e.target.dataset.slotId;\n const newVersion = parseInt(e.target.value);\n\n setQuestionVersion(slotId, newVersion)\n .then(() => {\n location.reload();\n return;\n })\n .catch(Notification.exception);\n });\n};\n\n/** @property {Boolean} eventsRegistered If the event has been registered or not */\nlet eventsRegistered = false;\n\n/**\n * Entrypoint of the js.\n */\nexport const init = () => {\n if (eventsRegistered) {\n return;\n }\n\n registerEventListeners();\n};\n"],"file":"question_slot.min.js"}
|
76
mod/quiz/amd/src/question_slot.js
Normal file
76
mod/quiz/amd/src/question_slot.js
Normal file
|
@ -0,0 +1,76 @@
|
|||
// 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/>.
|
||||
|
||||
/**
|
||||
* Render the question slot template for each question in the quiz edit view.
|
||||
*
|
||||
* @module mod_quiz/question_slot
|
||||
* @copyright 2021 Catalyst IT Australia Pty Ltd
|
||||
* @author Guillermo Gomez Arias <guillermogomez@catalyst-au.net>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
import {call as fetchMany} from 'core/ajax';
|
||||
import Notification from 'core/notification';
|
||||
|
||||
/**
|
||||
* Set the question version for the slot.
|
||||
*
|
||||
* @param {Number} slotId
|
||||
* @param {Number} newVersion
|
||||
* @return {Array} The modified question version
|
||||
*/
|
||||
const setQuestionVersion = (slotId, newVersion) => fetchMany([{
|
||||
methodname: 'mod_quiz_set_question_version',
|
||||
args: {
|
||||
slotid: slotId,
|
||||
newversion: newVersion,
|
||||
}
|
||||
}])[0];
|
||||
|
||||
/**
|
||||
* Replace the container with a new version.
|
||||
*/
|
||||
const registerEventListeners = () => {
|
||||
document.addEventListener('change', e => {
|
||||
if (!e.target.matches('[data-action="mod_quiz-select_slot"][data-slot-id]')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const slotId = e.target.dataset.slotId;
|
||||
const newVersion = parseInt(e.target.value);
|
||||
|
||||
setQuestionVersion(slotId, newVersion)
|
||||
.then(() => {
|
||||
location.reload();
|
||||
return;
|
||||
})
|
||||
.catch(Notification.exception);
|
||||
});
|
||||
};
|
||||
|
||||
/** @property {Boolean} eventsRegistered If the event has been registered or not */
|
||||
let eventsRegistered = false;
|
||||
|
||||
/**
|
||||
* Entrypoint of the js.
|
||||
*/
|
||||
export const init = () => {
|
||||
if (eventsRegistered) {
|
||||
return;
|
||||
}
|
||||
|
||||
registerEventListeners();
|
||||
};
|
|
@ -145,12 +145,16 @@ class quiz {
|
|||
* Load just basic information about all the questions in this quiz.
|
||||
*/
|
||||
public function preload_questions() {
|
||||
$this->questions = question_preload_questions(null,
|
||||
'slot.maxmark, slot.id AS slotid, slot.slot, slot.page,
|
||||
slot.questioncategoryid AS randomfromcategory,
|
||||
slot.includingsubcategories AS randomincludingsubcategories',
|
||||
'{quiz_slots} slot ON slot.quizid = :quizid AND q.id = slot.questionid',
|
||||
array('quizid' => $this->quiz->id), 'slot.slot');
|
||||
$specificquestionids = \mod_quiz\question\bank\qbank_helper::get_specific_version_question_ids($this->quiz->id);
|
||||
$latestquestionids = \mod_quiz\question\bank\qbank_helper::get_always_latest_version_question_ids($this->quiz->id);
|
||||
$questionids = array_merge($specificquestionids, $latestquestionids);
|
||||
$questiondata = [];
|
||||
if (!empty($questionids)) {
|
||||
$questiondata = \mod_quiz\question\bank\qbank_helper::get_question_structure_data($this->quiz->id, $questionids, true);
|
||||
}
|
||||
$allquestiondata = \mod_quiz\question\bank\qbank_helper::question_array_sort(
|
||||
\mod_quiz\question\bank\qbank_helper::question_load_random_questions($this->quiz->id, $questiondata), 'slot');
|
||||
$this->questions = $allquestiondata;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -535,23 +539,11 @@ class quiz {
|
|||
// To control if we need to look in categories for questions.
|
||||
$qcategories = array();
|
||||
|
||||
// We must be careful with random questions, if we find a random question we must assume that the quiz may content
|
||||
// any of the questions in the referenced category (or subcategories).
|
||||
foreach ($this->get_questions() as $questiondata) {
|
||||
if ($questiondata->qtype == 'random' and $includepotential) {
|
||||
$includesubcategories = (bool) $questiondata->questiontext;
|
||||
if (!isset($qcategories[$questiondata->category])) {
|
||||
$qcategories[$questiondata->category] = false;
|
||||
}
|
||||
if ($includesubcategories) {
|
||||
$qcategories[$questiondata->category] = true;
|
||||
}
|
||||
} else {
|
||||
if (!in_array($questiondata->qtype, $questiontypes)) {
|
||||
$questiontypes[] = $questiondata->qtype;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($qcategories)) {
|
||||
// We have to look for all the question types in these categories.
|
||||
|
@ -709,8 +701,7 @@ class quiz_attempt {
|
|||
|
||||
$this->quba = question_engine::load_questions_usage_by_activity($this->attempt->uniqueid);
|
||||
$this->slots = $DB->get_records('quiz_slots',
|
||||
array('quizid' => $this->get_quizid()), 'slot',
|
||||
'slot, id, requireprevious, questionid, includingsubcategories');
|
||||
array('quizid' => $this->get_quizid()), 'slot', 'slot, id, requireprevious');
|
||||
$this->sections = array_values($DB->get_records('quiz_sections',
|
||||
array('quizid' => $this->get_quizid()), 'firstslot'));
|
||||
|
||||
|
@ -1807,8 +1798,7 @@ class quiz_attempt {
|
|||
$question->length = $replacedquestion->length;
|
||||
$question->penalty = 0;
|
||||
$question->stamp = '';
|
||||
$question->version = 0;
|
||||
$question->hidden = 0;
|
||||
$question->status = \core_question\local\bank\question_version_status::QUESTION_STATUS_READY;
|
||||
$question->timecreated = null;
|
||||
$question->timemodified = null;
|
||||
$question->createdby = null;
|
||||
|
@ -2108,25 +2098,9 @@ class quiz_attempt {
|
|||
|
||||
$transaction = $DB->start_delegated_transaction();
|
||||
|
||||
// Choose the replacement question.
|
||||
$questiondata = $DB->get_record('question',
|
||||
array('id' => $this->slots[$slot]->questionid));
|
||||
if ($questiondata->qtype != 'random') {
|
||||
$newqusetionid = $questiondata->id;
|
||||
} else {
|
||||
$tagids = quiz_retrieve_slot_tag_ids($this->slots[$slot]->id);
|
||||
|
||||
$randomloader = new \core_question\local\bank\random_question_loader($qubaids, array());
|
||||
$newqusetionid = $randomloader->get_next_question_id($questiondata->category,
|
||||
(bool) $questiondata->questiontext, $tagids);
|
||||
if ($newqusetionid === null) {
|
||||
throw new moodle_exception('notenoughrandomquestions', 'quiz',
|
||||
$this->quizobj->view_url(), $questiondata);
|
||||
}
|
||||
}
|
||||
|
||||
// Add the question to the usage. It is important we do this before we choose a variant.
|
||||
$newquestion = question_bank::load_question($newqusetionid);
|
||||
$newquestion = question_bank::load_question(
|
||||
\mod_quiz\question\bank\qbank_helper::choose_question_for_redo($this->slots[$slot]->id, $qubaids));
|
||||
$newslot = $this->quba->add_question_in_place_of_other($slot, $newquestion);
|
||||
|
||||
// Choose the variant.
|
||||
|
|
|
@ -15,22 +15,13 @@
|
|||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Define all the backup steps that will be used by the backup_quiz_activity_task.
|
||||
*
|
||||
* @package mod_quiz
|
||||
* @subpackage backup-moodle2
|
||||
* @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com}
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
|
||||
/**
|
||||
* Define all the backup steps that will be used by the backup_quiz_activity_task
|
||||
*
|
||||
* @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com}
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class backup_quiz_activity_structure_step extends backup_questions_activity_structure_step {
|
||||
|
||||
protected function define_structure() {
|
||||
|
@ -39,7 +30,7 @@ class backup_quiz_activity_structure_step extends backup_questions_activity_stru
|
|||
$userinfo = $this->get_setting_value('userinfo');
|
||||
|
||||
// Define each element separated.
|
||||
$quiz = new backup_nested_element('quiz', array('id'), array(
|
||||
$quiz = new backup_nested_element('quiz', ['id'], [
|
||||
'name', 'intro', 'introformat', 'timeopen', 'timeclose', 'timelimit',
|
||||
'overduehandling', 'graceperiod', 'preferredbehaviour', 'canredoquestions', 'attempts_number',
|
||||
'attemptonlast', 'grademethod', 'decimalpoints', 'questiondecimalpoints',
|
||||
|
@ -50,46 +41,44 @@ class backup_quiz_activity_structure_step extends backup_questions_activity_stru
|
|||
'sumgrades', 'grade', 'timecreated',
|
||||
'timemodified', 'password', 'subnet', 'browsersecurity',
|
||||
'delay1', 'delay2', 'showuserpicture', 'showblocks', 'completionattemptsexhausted',
|
||||
'completionminattempts', 'allowofflineattempts'));
|
||||
'completionminattempts', 'allowofflineattempts']);
|
||||
|
||||
// Define elements for access rule subplugin settings.
|
||||
$this->add_subplugin_structure('quizaccess', $quiz, true);
|
||||
|
||||
$qinstances = new backup_nested_element('question_instances');
|
||||
|
||||
$qinstance = new backup_nested_element('question_instance', array('id'), array(
|
||||
'slot', 'page', 'requireprevious', 'questionid', 'questioncategoryid', 'includingsubcategories', 'maxmark'));
|
||||
$qinstance = new backup_nested_element('question_instance', ['id'],
|
||||
['slot', 'page', 'requireprevious', 'questionid', 'questioncategoryid', 'includingsubcategories', 'maxmark']);
|
||||
|
||||
$qinstancetags = new backup_nested_element('tags');
|
||||
$qinstancetag = new backup_nested_element('tag', array('id'), array('tagid', 'tagname'));
|
||||
$this->add_question_references($qinstance, 'mod_quiz', 'slot');
|
||||
|
||||
$this->add_question_set_references($qinstance, 'mod_quiz', 'slot');
|
||||
|
||||
$sections = new backup_nested_element('sections');
|
||||
|
||||
$section = new backup_nested_element('section', array('id'), array(
|
||||
'firstslot', 'heading', 'shufflequestions'));
|
||||
$section = new backup_nested_element('section', ['id'], ['firstslot', 'heading', 'shufflequestions']);
|
||||
|
||||
$feedbacks = new backup_nested_element('feedbacks');
|
||||
|
||||
$feedback = new backup_nested_element('feedback', array('id'), array(
|
||||
'feedbacktext', 'feedbacktextformat', 'mingrade', 'maxgrade'));
|
||||
$feedback = new backup_nested_element('feedback', ['id'], ['feedbacktext', 'feedbacktextformat', 'mingrade', 'maxgrade']);
|
||||
|
||||
$overrides = new backup_nested_element('overrides');
|
||||
|
||||
$override = new backup_nested_element('override', array('id'), array(
|
||||
$override = new backup_nested_element('override', ['id'], [
|
||||
'userid', 'groupid', 'timeopen', 'timeclose',
|
||||
'timelimit', 'attempts', 'password'));
|
||||
'timelimit', 'attempts', 'password']);
|
||||
|
||||
$grades = new backup_nested_element('grades');
|
||||
|
||||
$grade = new backup_nested_element('grade', array('id'), array(
|
||||
'userid', 'gradeval', 'timemodified'));
|
||||
$grade = new backup_nested_element('grade', ['id'], ['userid', 'gradeval', 'timemodified']);
|
||||
|
||||
$attempts = new backup_nested_element('attempts');
|
||||
|
||||
$attempt = new backup_nested_element('attempt', array('id'), array(
|
||||
$attempt = new backup_nested_element('attempt', ['id'], [
|
||||
'userid', 'attemptnum', 'uniqueid', 'layout', 'currentpage', 'preview',
|
||||
'state', 'timestart', 'timefinish', 'timemodified', 'timemodifiedoffline',
|
||||
'timecheckstate', 'sumgrades', 'gradednotificationsenttime'));
|
||||
'timecheckstate', 'sumgrades', 'gradednotificationsenttime']);
|
||||
|
||||
// This module is using questions, so produce the related question states and sessions
|
||||
// attaching them to the $attempt element based in 'uniqueid' matching.
|
||||
|
@ -102,9 +91,6 @@ class backup_quiz_activity_structure_step extends backup_questions_activity_stru
|
|||
$quiz->add_child($qinstances);
|
||||
$qinstances->add_child($qinstance);
|
||||
|
||||
$qinstance->add_child($qinstancetags);
|
||||
$qinstancetags->add_child($qinstancetag);
|
||||
|
||||
$quiz->add_child($sections);
|
||||
$sections->add_child($section);
|
||||
|
||||
|
@ -121,22 +107,16 @@ class backup_quiz_activity_structure_step extends backup_questions_activity_stru
|
|||
$attempts->add_child($attempt);
|
||||
|
||||
// Define sources.
|
||||
$quiz->set_source_table('quiz', array('id' => backup::VAR_ACTIVITYID));
|
||||
$quiz->set_source_table('quiz', ['id' => backup::VAR_ACTIVITYID]);
|
||||
|
||||
$qinstance->set_source_table('quiz_slots',
|
||||
array('quizid' => backup::VAR_PARENTID));
|
||||
$qinstance->set_source_table('quiz_slots', ['quizid' => backup::VAR_PARENTID]);
|
||||
|
||||
$qinstancetag->set_source_table('quiz_slot_tags',
|
||||
array('slotid' => backup::VAR_PARENTID));
|
||||
$section->set_source_table('quiz_sections', ['quizid' => backup::VAR_PARENTID]);
|
||||
|
||||
$section->set_source_table('quiz_sections',
|
||||
array('quizid' => backup::VAR_PARENTID));
|
||||
|
||||
$feedback->set_source_table('quiz_feedback',
|
||||
array('quizid' => backup::VAR_PARENTID));
|
||||
$feedback->set_source_table('quiz_feedback', ['quizid' => backup::VAR_PARENTID]);
|
||||
|
||||
// Quiz overrides to backup are different depending of user info.
|
||||
$overrideparams = array('quiz' => backup::VAR_PARENTID);
|
||||
$overrideparams = ['quiz' => backup::VAR_PARENTID];
|
||||
if (!$userinfo) { // Without userinfo, skip user overrides.
|
||||
$overrideparams['userid'] = backup_helper::is_sqlparam(null);
|
||||
|
||||
|
@ -152,12 +132,11 @@ class backup_quiz_activity_structure_step extends backup_questions_activity_stru
|
|||
|
||||
// All the rest of elements only happen if we are including user info.
|
||||
if ($userinfo) {
|
||||
$grade->set_source_table('quiz_grades', array('quiz' => backup::VAR_PARENTID));
|
||||
$grade->set_source_table('quiz_grades', ['quiz' => backup::VAR_PARENTID]);
|
||||
$attempt->set_source_sql('
|
||||
SELECT *
|
||||
FROM {quiz_attempts}
|
||||
WHERE quiz = :quiz AND preview = 0',
|
||||
array('quiz' => backup::VAR_PARENTID));
|
||||
WHERE quiz = :quiz AND preview = 0', ['quiz' => backup::VAR_PARENTID]);
|
||||
}
|
||||
|
||||
// Define source alias.
|
||||
|
|
|
@ -14,20 +14,11 @@
|
|||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* @package mod_quiz
|
||||
* @subpackage backup-moodle2
|
||||
* @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com}
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
|
||||
/**
|
||||
* Structure step to restore one quiz activity
|
||||
*
|
||||
* @package mod_quiz
|
||||
* @subpackage backup-moodle2
|
||||
* @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com}
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
@ -60,10 +51,16 @@ class restore_quiz_activity_structure_step extends restore_questions_activity_st
|
|||
// A chance for access subplugings to set up their quiz data.
|
||||
$this->add_subplugin_structure('quizaccess', $quiz);
|
||||
|
||||
$paths[] = new restore_path_element('quiz_question_instance',
|
||||
$quizquestioninstance = new restore_path_element('quiz_question_instance',
|
||||
'/activity/quiz/question_instances/question_instance');
|
||||
$paths[] = $quizquestioninstance;
|
||||
if ($this->task->get_old_moduleversion() < 2021091700) {
|
||||
$paths[] = new restore_path_element('quiz_slot_tags',
|
||||
'/activity/quiz/question_instances/question_instance/tags/tag');
|
||||
} else {
|
||||
$this->add_question_references($quizquestioninstance, $paths);
|
||||
$this->add_question_set_references($quizquestioninstance, $paths);
|
||||
}
|
||||
$paths[] = new restore_path_element('quiz_section', '/activity/quiz/sections/section');
|
||||
$paths[] = new restore_path_element('quiz_feedback', '/activity/quiz/feedbacks/feedback');
|
||||
$paths[] = new restore_path_element('quiz_override', '/activity/quiz/overrides/override');
|
||||
|
@ -99,6 +96,11 @@ class restore_quiz_activity_structure_step extends restore_questions_activity_st
|
|||
return $this->prepare_activity_structure($paths);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the quiz data.
|
||||
*
|
||||
* @param stdClass|array $data
|
||||
*/
|
||||
protected function process_quiz($data) {
|
||||
global $CFG, $DB, $USER;
|
||||
|
||||
|
@ -303,6 +305,64 @@ class restore_quiz_activity_structure_step extends restore_questions_activity_st
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the data for pre 4.0 quiz data where the question_references and question_set_references table introduced.
|
||||
*
|
||||
* @param stdClass|array $data
|
||||
*/
|
||||
protected function process_quiz_question_legacy_instance($data) {
|
||||
global $DB;
|
||||
|
||||
$questionid = $this->get_mappingid('question', $data->questionid);
|
||||
$sql = 'SELECT qbe.id as questionbankentryid,
|
||||
qc.contextid as questioncontextid,
|
||||
qc.id as category,
|
||||
qv.version,
|
||||
q.qtype,
|
||||
q.id as questionid
|
||||
FROM {question} q
|
||||
JOIN {question_versions} qv ON qv.questionid = q.id
|
||||
JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
|
||||
JOIN {question_categories} qc ON qc.id = qbe.questioncategoryid
|
||||
WHERE q.id = ?';
|
||||
$question = $DB->get_record_sql($sql, [$questionid]);
|
||||
$module = $DB->get_record('quiz', ['id' => $data->quizid]);
|
||||
|
||||
if ($question->qtype === 'random') {
|
||||
// Set reference data.
|
||||
$questionsetreference = new \stdClass();
|
||||
$questionsetreference->usingcontextid = context_module::instance(get_coursemodule_from_instance(
|
||||
"quiz", $module->id, $module->course)->id)->id;
|
||||
$questionsetreference->component = 'mod_quiz';
|
||||
$questionsetreference->questionarea = 'slot';
|
||||
$questionsetreference->itemid = $data->id;
|
||||
$questionsetreference->questionscontextid = $question->questioncontextid;
|
||||
$filtercondition = new stdClass();
|
||||
$filtercondition->questioncategoryid = $question->category;
|
||||
$filtercondition->includingsubcategories = $data->includingsubcategories;
|
||||
$questionsetreference->filtercondition = json_encode($filtercondition);
|
||||
$DB->insert_record('question_set_references', $questionsetreference);
|
||||
// Cleanup leftover random qtype data from question table.
|
||||
question_delete_question($question->questionid);
|
||||
} else {
|
||||
// Reference data.
|
||||
$questionreference = new \stdClass();
|
||||
$questionreference->usingcontextid = context_module::instance(get_coursemodule_from_instance(
|
||||
"quiz", $module->id, $module->course)->id)->id;
|
||||
$questionreference->component = 'mod_quiz';
|
||||
$questionreference->questionarea = 'slot';
|
||||
$questionreference->itemid = $data->id;
|
||||
$questionreference->questionbankentryid = $question->questionbankentryid;
|
||||
$questionreference->version = $question->version;
|
||||
$DB->insert_record('question_references', $questionreference);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process quiz slots.
|
||||
*
|
||||
* @param stdClass|array $data
|
||||
*/
|
||||
protected function process_quiz_question_instance($data) {
|
||||
global $CFG, $DB;
|
||||
|
||||
|
@ -325,7 +385,7 @@ class restore_quiz_activity_structure_step extends restore_questions_activity_st
|
|||
$page += 1;
|
||||
continue;
|
||||
}
|
||||
if ($item == $data->questionid) {
|
||||
if (isset($data->questionid) && $item == $data->questionid) {
|
||||
$data->slot = $slot;
|
||||
$data->page = $page;
|
||||
break;
|
||||
|
@ -344,20 +404,15 @@ class restore_quiz_activity_structure_step extends restore_questions_activity_st
|
|||
}
|
||||
|
||||
$data->quizid = $this->get_new_parentid('quiz');
|
||||
$questionmapping = $this->get_mapping('question', $data->questionid);
|
||||
$data->questionid = $questionmapping ? $questionmapping->newitemid : false;
|
||||
|
||||
if (isset($data->questioncategoryid)) {
|
||||
$data->questioncategoryid = $this->get_mappingid('question_category', $data->questioncategoryid);
|
||||
} else if ($questionmapping && $questionmapping->info->qtype == 'random') {
|
||||
// Backward compatibility for backups created using Moodle 3.4 or earlier.
|
||||
$data->questioncategoryid = $this->get_mappingid('question_category', $questionmapping->parentitemid);
|
||||
$data->includingsubcategories = $questionmapping->info->questiontext ? 1 : 0;
|
||||
}
|
||||
|
||||
$newitemid = $DB->insert_record('quiz_slots', $data);
|
||||
// Add mapping, restore of slot tags (for random questions) need it.
|
||||
$this->set_mapping('quiz_question_instance', $oldid, $newitemid);
|
||||
|
||||
if ($this->task->get_old_moduleversion() < 2022020300) {
|
||||
$data->id = $newitemid;
|
||||
$this->process_quiz_question_legacy_instance($data);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -370,7 +425,7 @@ class restore_quiz_activity_structure_step extends restore_questions_activity_st
|
|||
|
||||
$data = (object)$data;
|
||||
|
||||
$data->slotid = $this->get_new_parentid('quiz_question_instance');
|
||||
$slotid = $this->get_new_parentid('quiz_question_instance');
|
||||
if ($this->task->is_samesite() && $tag = core_tag_tag::get($data->tagid, 'id, name')) {
|
||||
$data->tagname = $tag->name;
|
||||
} else if ($tag = core_tag_tag::get_by_name(0, $data->tagname, 'id, name')) {
|
||||
|
@ -379,8 +434,22 @@ class restore_quiz_activity_structure_step extends restore_questions_activity_st
|
|||
$data->tagid = null;
|
||||
$data->tagname = $tag->name;
|
||||
}
|
||||
$tagstring = "{$data->tagid},{$data->tagname}";
|
||||
$setreferencedata = $DB->get_record('question_set_references', ['itemid' => $slotid]);
|
||||
|
||||
$DB->insert_record('quiz_slot_tags', $data);
|
||||
$filtercondition = json_decode($setreferencedata->filtercondition);
|
||||
$tagstrings = [];
|
||||
if (isset($filtercondition->tags)) {
|
||||
$tags = explode(',', $filtercondition->tags);
|
||||
foreach ($tags as $tag) {
|
||||
$tagstrings [] = $tag;
|
||||
}
|
||||
}
|
||||
$tagstrings [] = $tagstring;
|
||||
$filtercondition->tags = $tagstrings;
|
||||
$setreferencedata->filtercondition = json_encode($filtercondition);
|
||||
|
||||
$DB->update_record('question_set_references', $setreferencedata);
|
||||
}
|
||||
|
||||
protected function process_quiz_section($data) {
|
||||
|
|
105
mod/quiz/classes/external/submit_question_version.php
vendored
Normal file
105
mod/quiz/classes/external/submit_question_version.php
vendored
Normal file
|
@ -0,0 +1,105 @@
|
|||
<?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\external;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
require_once($CFG->libdir . '/externallib.php');
|
||||
require_once($CFG->dirroot . '/question/engine/lib.php');
|
||||
require_once($CFG->dirroot . '/question/engine/datalib.php');
|
||||
require_once($CFG->libdir . '/questionlib.php');
|
||||
|
||||
use external_api;
|
||||
use external_description;
|
||||
use external_function_parameters;
|
||||
use external_single_structure;
|
||||
use external_value;
|
||||
use stdClass;
|
||||
|
||||
/**
|
||||
* External api for changing the question version in the quiz.
|
||||
*
|
||||
* @package mod_quiz
|
||||
* @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 submit_question_version extends external_api {
|
||||
|
||||
/**
|
||||
* Parameters for the submit_question_version.
|
||||
*
|
||||
* @return \external_function_parameters
|
||||
*/
|
||||
public static function execute_parameters(): external_function_parameters {
|
||||
return new external_function_parameters (
|
||||
[
|
||||
'slotid' => new external_value(PARAM_INT, ''),
|
||||
'newversion' => new external_value(PARAM_INT, '')
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the questions slot parameters to display the question template.
|
||||
*
|
||||
* @param int $slotid Slot id to display.
|
||||
* @param int $newversion
|
||||
* @return array
|
||||
*/
|
||||
public static function execute(int $slotid, int $newversion): array {
|
||||
global $DB;
|
||||
$params = [
|
||||
'slotid' => $slotid,
|
||||
'newversion' => $newversion
|
||||
];
|
||||
$params = self::validate_parameters(self::execute_parameters(), $params);
|
||||
$response = ['result' => false];
|
||||
// Get the required data.
|
||||
$referencedata = $DB->get_record('question_references', ['itemid' => $params['slotid']]);
|
||||
$slotdata = $DB->get_record('quiz_slots', ['id' => $slotid]);
|
||||
|
||||
// Capability check.
|
||||
list($course, $cm) = get_course_and_cm_from_instance($slotdata->quizid, 'quiz');
|
||||
$context = \context_module::instance($cm->id);
|
||||
self::validate_context($context);
|
||||
require_capability('mod/quiz:manage', $context);
|
||||
|
||||
$reference = new stdClass();
|
||||
$reference->id = $referencedata->id;
|
||||
if ($params['newversion'] === 0) {
|
||||
$reference->version = null;
|
||||
} else {
|
||||
$reference->version = $params['newversion'];
|
||||
}
|
||||
$response['result'] = $DB->update_record('question_references', $reference);
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the webservice response.
|
||||
*
|
||||
* @return external_description
|
||||
*/
|
||||
public static function execute_returns() {
|
||||
return new external_single_structure(
|
||||
[
|
||||
'result' => new external_value(PARAM_BOOL, '')
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
|
@ -14,23 +14,14 @@
|
|||
// 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 \mod_quiz\local\structure\slot_random class.
|
||||
*
|
||||
* @package mod_quiz
|
||||
* @copyright 2018 Shamim Rezaie <shamim@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
namespace mod_quiz\local\structure;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
/**
|
||||
* Class slot_random, represents a random question slot type.
|
||||
*
|
||||
* @package mod_quiz
|
||||
* @copyright 2018 Shamim Rezaie <shamim@moodle.com>
|
||||
* @author 2021 Safat Shahin <safatshahin@catalyst-au.net>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class slot_random {
|
||||
|
@ -38,6 +29,11 @@ class slot_random {
|
|||
/** @var \stdClass Slot's properties. A record retrieved from the quiz_slots table. */
|
||||
protected $record;
|
||||
|
||||
/**
|
||||
* @var \stdClass set reference record
|
||||
*/
|
||||
protected $referencerecord;
|
||||
|
||||
/**
|
||||
* @var \stdClass The quiz this question slot belongs to.
|
||||
*/
|
||||
|
@ -48,6 +44,11 @@ class slot_random {
|
|||
*/
|
||||
protected $tags = [];
|
||||
|
||||
/**
|
||||
* @var string filter condition
|
||||
*/
|
||||
protected $filtercondition = null;
|
||||
|
||||
/**
|
||||
* slot_random constructor.
|
||||
*
|
||||
|
@ -55,16 +56,22 @@ class slot_random {
|
|||
*/
|
||||
public function __construct($slotrecord = null) {
|
||||
$this->record = new \stdClass();
|
||||
$this->referencerecord = new \stdClass();
|
||||
|
||||
$properties = array(
|
||||
'id', 'slot', 'quizid', 'page', 'requireprevious', 'questionid',
|
||||
'questioncategoryid', 'includingsubcategories', 'maxmark');
|
||||
$slotproperties = ['id', 'slot', 'quizid', 'page', 'requireprevious', 'maxmark'];
|
||||
$setreferenceproperties = ['usingcontextid', 'questionscontextid'];
|
||||
|
||||
foreach ($properties as $property) {
|
||||
foreach ($slotproperties as $property) {
|
||||
if (isset($slotrecord->$property)) {
|
||||
$this->record->$property = $slotrecord->$property;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($setreferenceproperties as $referenceproperty) {
|
||||
if (isset($slotrecord->$referenceproperty)) {
|
||||
$this->referencerecord->$referenceproperty = $slotrecord->$referenceproperty;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -122,6 +129,19 @@ class slot_random {
|
|||
$this->tags = \core_tag_tag::get_bulk($tagids, 'id, name');
|
||||
}
|
||||
|
||||
/**
|
||||
* Set filter condition.
|
||||
*
|
||||
* @param \stdClass $filters
|
||||
*/
|
||||
public function set_filter_condition($filters) {
|
||||
if (!empty($this->tags)) {
|
||||
$filters->tags = $this->tags;
|
||||
}
|
||||
|
||||
$this->filtercondition = json_encode($filters);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts the quiz slot at the $page page.
|
||||
* It is required to call this function if you are building a quiz slot object from scratch.
|
||||
|
@ -179,17 +199,11 @@ class slot_random {
|
|||
|
||||
$this->record->id = $DB->insert_record('quiz_slots', $this->record);
|
||||
|
||||
if (!empty($this->tags)) {
|
||||
$recordstoinsert = [];
|
||||
foreach ($this->tags as $tag) {
|
||||
$recordstoinsert[] = (object)[
|
||||
'slotid' => $this->record->id,
|
||||
'tagid' => $tag->id,
|
||||
'tagname' => $tag->name
|
||||
];
|
||||
}
|
||||
$DB->insert_records('quiz_slot_tags', $recordstoinsert);
|
||||
}
|
||||
$this->referencerecord->component = 'mod_quiz';
|
||||
$this->referencerecord->questionarea = 'slot';
|
||||
$this->referencerecord->itemid = $this->record->id;
|
||||
$this->referencerecord->filtercondition = $this->filtercondition;
|
||||
$DB->insert_record('question_set_references', $this->referencerecord);
|
||||
|
||||
$trans->allow_commit();
|
||||
|
||||
|
|
|
@ -25,8 +25,10 @@
|
|||
namespace mod_quiz\output;
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
use mod_quiz\question\bank\qbank_helper;
|
||||
use \mod_quiz\structure;
|
||||
use \html_writer;
|
||||
use \qbank_previewquestion\helper;
|
||||
use renderable;
|
||||
|
||||
/**
|
||||
|
@ -46,13 +48,13 @@ class edit_renderer extends \plugin_renderer_base {
|
|||
*
|
||||
* @param \quiz $quizobj object containing all the quiz settings information.
|
||||
* @param structure $structure object containing the structure of the quiz.
|
||||
* @param \question_edit_contexts $contexts the relevant question bank contexts.
|
||||
* @param \core_question\local\bank\question_edit_contexts $contexts the relevant question bank contexts.
|
||||
* @param \moodle_url $pageurl the canonical URL of this page.
|
||||
* @param array $pagevars the variables from {@link question_edit_setup()}.
|
||||
* @return string HTML to output.
|
||||
*/
|
||||
public function edit_page(\quiz $quizobj, structure $structure,
|
||||
\question_edit_contexts $contexts, \moodle_url $pageurl, array $pagevars) {
|
||||
\core_question\local\bank\question_edit_contexts $contexts, \moodle_url $pageurl, array $pagevars) {
|
||||
$output = '';
|
||||
|
||||
// Information at the top.
|
||||
|
@ -493,7 +495,7 @@ class edit_renderer extends \plugin_renderer_base {
|
|||
*
|
||||
* @param structure $structure object containing the structure of the quiz.
|
||||
* @param \stdClass $section information about the section.
|
||||
* @param \question_edit_contexts $contexts the relevant question bank contexts.
|
||||
* @param \core_question\local\bank\question_edit_contexts $contexts the relevant question bank contexts.
|
||||
* @param array $pagevars the variables from {@link \question_edit_setup()}.
|
||||
* @param \moodle_url $pageurl the canonical URL of this page.
|
||||
* @return string HTML to output.
|
||||
|
@ -513,7 +515,7 @@ class edit_renderer extends \plugin_renderer_base {
|
|||
*
|
||||
* @param structure $structure object containing the structure of the quiz.
|
||||
* @param int $slot which slot we are outputting.
|
||||
* @param \question_edit_contexts $contexts the relevant question bank contexts.
|
||||
* @param \core_question\local\bank\question_edit_contexts $contexts the relevant question bank contexts.
|
||||
* @param array $pagevars the variables from {@link \question_edit_setup()}.
|
||||
* @param \moodle_url $pageurl the canonical URL of this page.
|
||||
* @return string HTML to output.
|
||||
|
@ -546,7 +548,7 @@ class edit_renderer extends \plugin_renderer_base {
|
|||
*
|
||||
* @param structure $structure object containing the structure of the quiz.
|
||||
* @param int $slot the first slot on the page we are outputting.
|
||||
* @param \question_edit_contexts $contexts the relevant question bank contexts.
|
||||
* @param \core_question\local\bank\question_edit_contexts $contexts the relevant question bank contexts.
|
||||
* @param array $pagevars the variables from {@link \question_edit_setup()}.
|
||||
* @param \moodle_url $pageurl the canonical URL of this page.
|
||||
* @return string HTML to output.
|
||||
|
@ -580,12 +582,12 @@ class edit_renderer extends \plugin_renderer_base {
|
|||
* @param structure $structure object containing the structure of the quiz.
|
||||
* @param int $page the page number that this menu will add to.
|
||||
* @param \moodle_url $pageurl the canonical URL of this page.
|
||||
* @param \question_edit_contexts $contexts the relevant question bank contexts.
|
||||
* @param \core_question\local\bank\question_edit_contexts $contexts the relevant question bank contexts.
|
||||
* @param array $pagevars the variables from {@link \question_edit_setup()}.
|
||||
* @return string HTML to output.
|
||||
*/
|
||||
public function add_menu_actions(structure $structure, $page, \moodle_url $pageurl,
|
||||
\question_edit_contexts $contexts, array $pagevars) {
|
||||
\core_question\local\bank\question_edit_contexts $contexts, array $pagevars) {
|
||||
|
||||
$actions = $this->edit_menu_actions($structure, $page, $pageurl, $pagevars);
|
||||
if (empty($actions)) {
|
||||
|
@ -727,7 +729,11 @@ class edit_renderer extends \plugin_renderer_base {
|
|||
* @param \moodle_url $pageurl the canonical URL of this page.
|
||||
* @return string HTML to output.
|
||||
*/
|
||||
public function question(structure $structure, $slot, \moodle_url $pageurl) {
|
||||
public function question(structure $structure, int $slot, \moodle_url $pageurl) {
|
||||
global $DB;
|
||||
// Get the data required by the question_slot template.
|
||||
$slotid = $structure->get_slot_id_for_slot($slot);
|
||||
|
||||
$output = '';
|
||||
$output .= html_writer::start_tag('div');
|
||||
|
||||
|
@ -735,50 +741,92 @@ class edit_renderer extends \plugin_renderer_base {
|
|||
$output .= $this->question_move_icon($structure, $slot);
|
||||
}
|
||||
|
||||
$output .= html_writer::start_div('mod-indent-outer');
|
||||
$checkbox = new \core\output\checkbox_toggleall($this->togglegroup, false, [
|
||||
$data = [
|
||||
'slotid' => $slotid,
|
||||
'canbeedited' => $structure->can_be_edited(),
|
||||
'checkbox' => $this->get_checkbox_render($structure, $slot),
|
||||
'questionnumber' => $this->question_number($structure->get_displayed_number_for_slot($slot)),
|
||||
'questionname' => $this->get_question_name_for_slot($structure, $slot, $pageurl),
|
||||
'questionicons' => $this->get_action_icon($structure, $slot, $pageurl),
|
||||
'questiondependencyicon' => ($structure->can_be_edited() ? $this->question_dependency_icon($structure, $slot) : ''),
|
||||
'versionselection' => false
|
||||
];
|
||||
|
||||
$data['versionoptions'] = [];
|
||||
if ($structure->get_slot_by_number($slot)->qtype !== 'random') {
|
||||
$data['versionselection'] = true;
|
||||
$data['versionoption'] = qbank_helper::get_question_version_info($structure->get_question_in_slot($slot)->id, $slotid);
|
||||
$this->page->requires->js_call_amd('mod_quiz/question_slot', 'init', [$slotid]);
|
||||
}
|
||||
|
||||
// Render the question slot template.
|
||||
$output .= $this->render_from_template('mod_quiz/question_slot', $data);
|
||||
|
||||
$output .= html_writer::end_tag('div');
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the checkbox render.
|
||||
*
|
||||
* @param structure $structure object containing the structure of the quiz.
|
||||
* @param int $slot the slot on the page we are outputting.
|
||||
* @return string HTML to output.
|
||||
*/
|
||||
public function get_checkbox_render(structure $structure, int $slot) : string {
|
||||
$checkbox = new \core\output\checkbox_toggleall($this->togglegroup, false,
|
||||
[
|
||||
'id' => 'selectquestion-' . $structure->get_displayed_number_for_slot($slot),
|
||||
'name' => 'selectquestion[]',
|
||||
'value' => $structure->get_displayed_number_for_slot($slot),
|
||||
'classes' => 'select-multiple-checkbox',
|
||||
]);
|
||||
$output .= $this->render($checkbox);
|
||||
$output .= $this->question_number($structure->get_displayed_number_for_slot($slot));
|
||||
|
||||
// This div is used to indent the content.
|
||||
$output .= html_writer::div('', 'mod-indent');
|
||||
return $this->render($checkbox);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the question name for the slot.
|
||||
*
|
||||
* @param structure $structure object containing the structure of the quiz.
|
||||
* @param int $slot the slot on the page we are outputting.
|
||||
* @param \moodle_url $pageurl the canonical URL of this page.
|
||||
* @return string HTML to output.
|
||||
*/
|
||||
public function get_question_name_for_slot(structure $structure, int $slot, \moodle_url $pageurl) : string {
|
||||
// Display the link to the question (or do nothing if question has no url).
|
||||
if ($structure->get_question_type_for_slot($slot) == 'random') {
|
||||
if ($structure->get_question_type_for_slot($slot) === 'random') {
|
||||
$questionname = $this->random_question($structure, $slot, $pageurl);
|
||||
} else {
|
||||
$questionname = $this->question_name($structure, $slot, $pageurl);
|
||||
}
|
||||
|
||||
// Start the div for the activity title, excluding the edit icons.
|
||||
$output .= html_writer::start_div('activityinstance');
|
||||
$output .= $questionname;
|
||||
|
||||
// Closing the tag which contains everything but edit icons. Content part of the module should not be part of this.
|
||||
$output .= html_writer::end_tag('div'); // .activityinstance.
|
||||
return $questionname;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the action icons render.
|
||||
*
|
||||
* @param structure $structure object containing the structure of the quiz.
|
||||
* @param int $slot the slot on the page we are outputting.
|
||||
* @param \moodle_url $pageurl the canonical URL of this page.
|
||||
* @return string HTML to output.
|
||||
*/
|
||||
public function get_action_icon(structure $structure, int $slot, \moodle_url $pageurl) : string {
|
||||
// Action icons.
|
||||
$qtype = $structure->get_question_type_for_slot($slot);
|
||||
$questionicons = '';
|
||||
$questionicons .= $this->question_preview_icon($structure->get_quiz(), $structure->get_question_in_slot($slot));
|
||||
if ($qtype !== 'random') {
|
||||
$questionicons .= $this->question_preview_icon($structure->get_quiz(), $structure->get_question_in_slot($slot),
|
||||
null, null, $qtype);
|
||||
}
|
||||
if ($structure->can_be_edited()) {
|
||||
$questionicons .= $this->question_remove_icon($structure, $slot, $pageurl);
|
||||
}
|
||||
$questionicons .= $this->marked_out_of_field($structure, $slot);
|
||||
$output .= html_writer::span($questionicons, 'actions'); // Required to add js spinner icon.
|
||||
if ($structure->can_be_edited()) {
|
||||
$output .= $this->question_dependency_icon($structure, $slot);
|
||||
}
|
||||
|
||||
// End of indentation div.
|
||||
$output .= html_writer::end_tag('div');
|
||||
$output .= html_writer::end_tag('div');
|
||||
|
||||
return $output;
|
||||
return $questionicons;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -814,9 +862,10 @@ class edit_renderer extends \plugin_renderer_base {
|
|||
* @param \stdClass $question data from the question and quiz_slots tables.
|
||||
* @param bool $label if true, show the preview question label after the icon
|
||||
* @param int $variant which question variant to preview (optional).
|
||||
* @param string $qtype the type of question
|
||||
* @return string HTML to output.
|
||||
*/
|
||||
public function question_preview_icon($quiz, $question, $label = null, $variant = null) {
|
||||
public function question_preview_icon($quiz, $question, $label = null, $variant = null, $qtype = null) {
|
||||
$url = quiz_question_preview_url($quiz, $question, $variant);
|
||||
|
||||
// Do we want a label?
|
||||
|
@ -975,10 +1024,8 @@ class edit_renderer extends \plugin_renderer_base {
|
|||
* @return string HTML to output.
|
||||
*/
|
||||
public function random_question(structure $structure, $slotnumber, $pageurl) {
|
||||
|
||||
$question = $structure->get_question_in_slot($slotnumber);
|
||||
$slot = $structure->get_slot_by_number($slotnumber);
|
||||
$slottags = $structure->get_slot_tags_for_slot_id($slot->id);
|
||||
$editurl = new \moodle_url('/mod/quiz/editrandom.php',
|
||||
array('returnurl' => $pageurl->out_as_local_url(), 'slotid' => $slot->id));
|
||||
|
||||
|
@ -986,6 +1033,9 @@ class edit_renderer extends \plugin_renderer_base {
|
|||
$temp->questiontext = '';
|
||||
$instancename = quiz_question_tostring($temp);
|
||||
|
||||
$setreference = qbank_helper::get_random_question_data_from_slot($slot->id);
|
||||
$filtercondition = json_decode($setreference->filtercondition);
|
||||
|
||||
$configuretitle = get_string('configurerandomquestion', 'quiz');
|
||||
$qtype = \question_bank::get_qtype($question->qtype, false);
|
||||
$namestr = $qtype->local_name();
|
||||
|
@ -995,12 +1045,17 @@ class edit_renderer extends \plugin_renderer_base {
|
|||
$editicon = $this->pix_icon('t/edit', $configuretitle, 'moodle', array('title' => ''));
|
||||
$qbankurlparams = array(
|
||||
'cmid' => $structure->get_cmid(),
|
||||
'cat' => $question->category . ',' . $question->contextid,
|
||||
'recurse' => !empty($question->questiontext)
|
||||
'cat' => $filtercondition->questioncategoryid . ',' . $setreference->questionscontextid,
|
||||
'recurse' => !empty($setreference->questionscontextid)
|
||||
);
|
||||
|
||||
$slottags = [];
|
||||
if (isset($filtercondition->tags)) {
|
||||
$slottags = $filtercondition->tags;
|
||||
}
|
||||
foreach ($slottags as $index => $slottag) {
|
||||
$qbankurlparams["qtagids[{$index}]"] = $slottag->tagid;
|
||||
$slottag = explode(',', $slottag);
|
||||
$qbankurlparams["qtagids[{$index}]"] = $slottag[0];
|
||||
}
|
||||
|
||||
// If this is a random question, display a link to show the questions
|
||||
|
@ -1083,13 +1138,13 @@ class edit_renderer extends \plugin_renderer_base {
|
|||
* is handled with the specific code for those.)
|
||||
*
|
||||
* @param structure $structure object containing the structure of the quiz.
|
||||
* @param \question_edit_contexts $contexts the relevant question bank contexts.
|
||||
* @param \core_question\local\bank\question_edit_contexts $contexts the relevant question bank contexts.
|
||||
* @param array $pagevars the variables from {@link \question_edit_setup()}.
|
||||
* @param \moodle_url $pageurl the canonical URL of this page.
|
||||
* @return bool Always returns true
|
||||
*/
|
||||
protected function initialise_editing_javascript(structure $structure,
|
||||
\question_edit_contexts $contexts, array $pagevars, \moodle_url $pageurl) {
|
||||
\core_question\local\bank\question_edit_contexts $contexts, array $pagevars, \moodle_url $pageurl) {
|
||||
|
||||
$config = new \stdClass();
|
||||
$config->resourceurl = '/mod/quiz/edit_rest.php';
|
||||
|
@ -1189,13 +1244,13 @@ class edit_renderer extends \plugin_renderer_base {
|
|||
* HTML for a page, with ids stripped, so it can be used as a javascript template.
|
||||
*
|
||||
* @param structure $structure object containing the structure of the quiz.
|
||||
* @param \question_edit_contexts $contexts the relevant question bank contexts.
|
||||
* @param \core_question\local\bank\question_edit_contexts $contexts the relevant question bank contexts.
|
||||
* @param array $pagevars the variables from {@link \question_edit_setup()}.
|
||||
* @param \moodle_url $pageurl the canonical URL of this page.
|
||||
* @return string HTML for a new page.
|
||||
*/
|
||||
protected function new_page_template(structure $structure,
|
||||
\question_edit_contexts $contexts, array $pagevars, \moodle_url $pageurl) {
|
||||
\core_question\local\bank\question_edit_contexts $contexts, array $pagevars, \moodle_url $pageurl) {
|
||||
if (!$structure->has_questions()) {
|
||||
return '';
|
||||
}
|
||||
|
|
|
@ -25,6 +25,9 @@
|
|||
|
||||
namespace mod_quiz\question\bank;
|
||||
|
||||
use core_question\local\bank\question_version_status;
|
||||
use mod_quiz\question\bank\filter\custom_category_condition;
|
||||
|
||||
/**
|
||||
* Subclass to customise the view of the question bank for the quiz editing screen.
|
||||
*
|
||||
|
@ -44,8 +47,8 @@ class custom_view extends \core_question\local\bank\view {
|
|||
const MAX_TEXT_LENGTH = 200;
|
||||
|
||||
/**
|
||||
* Constructor for custom_view.
|
||||
* @param \question_edit_contexts $contexts
|
||||
* Constructor.
|
||||
* @param \core_question\local\bank\question_edit_contexts $contexts
|
||||
* @param \moodle_url $pageurl
|
||||
* @param \stdClass $course course settings
|
||||
* @param \stdClass $cm activity settings.
|
||||
|
@ -251,4 +254,81 @@ class custom_view extends \core_question\local\bank\view {
|
|||
$this->sort[$sort] = $order;
|
||||
}
|
||||
}
|
||||
|
||||
protected function build_query(): void {
|
||||
// Get the required tables and fields.
|
||||
$joins = [];
|
||||
$fields = ['qv.status', 'qc.id', 'qv.version', 'qv.id as versionid', 'qbe.id as questionbankentryid'];
|
||||
if (!empty($this->requiredcolumns)) {
|
||||
foreach ($this->requiredcolumns as $column) {
|
||||
$extrajoins = $column->get_extra_joins();
|
||||
foreach ($extrajoins as $prefix => $join) {
|
||||
if (isset($joins[$prefix]) && $joins[$prefix] != $join) {
|
||||
throw new \coding_exception('Join ' . $join . ' conflicts with previous join ' . $joins[$prefix]);
|
||||
}
|
||||
$joins[$prefix] = $join;
|
||||
}
|
||||
$fields = array_merge($fields, $column->get_required_fields());
|
||||
}
|
||||
}
|
||||
$fields = array_unique($fields);
|
||||
|
||||
// Build the order by clause.
|
||||
$sorts = [];
|
||||
foreach ($this->sort as $sort => $order) {
|
||||
list($colname, $subsort) = $this->parse_subsort($sort);
|
||||
$sorts[] = $this->requiredcolumns[$colname]->sort_expression($order < 0, $subsort);
|
||||
}
|
||||
|
||||
// Build the where clause.
|
||||
$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)';
|
||||
$readyonly = "qv.status = '" . question_version_status::QUESTION_STATUS_READY . "' ";
|
||||
$tests = ['q.parent = 0', $latestversion, $readyonly];
|
||||
$this->sqlparams = [];
|
||||
foreach ($this->searchconditions as $searchcondition) {
|
||||
if ($searchcondition->where()) {
|
||||
$tests[] = '((' . $searchcondition->where() .'))';
|
||||
}
|
||||
if ($searchcondition->params()) {
|
||||
$this->sqlparams = array_merge($this->sqlparams, $searchcondition->params());
|
||||
}
|
||||
}
|
||||
// Build the SQL.
|
||||
$sql = ' FROM {question} q ' . implode(' ', $joins);
|
||||
$sql .= ' WHERE ' . implode(' AND ', $tests);
|
||||
$this->countsql = 'SELECT count(1)' . $sql;
|
||||
$this->loadsql = 'SELECT ' . implode(', ', $fields) . $sql . ' ORDER BY ' . implode(', ', $sorts);
|
||||
}
|
||||
|
||||
public function wanted_filters($cat, $tagids, $showhidden, $recurse, $editcontexts, $showquestiontext): void {
|
||||
global $CFG;
|
||||
list(, $contextid) = explode(',', $cat);
|
||||
$catcontext = \context::instance_by_id($contextid);
|
||||
$thiscontext = $this->get_most_specific_context();
|
||||
// Category selection form.
|
||||
$this->display_question_bank_header();
|
||||
|
||||
// Display tag filter if usetags setting is enabled/enablefilters is true.
|
||||
if ($this->enablefilters) {
|
||||
if (is_array($this->customfilterobjects)) {
|
||||
foreach ($this->customfilterobjects as $filterobjects) {
|
||||
$this->searchconditions[] = $filterobjects;
|
||||
}
|
||||
} else {
|
||||
if ($CFG->usetags) {
|
||||
array_unshift($this->searchconditions,
|
||||
new \core_question\bank\search\tag_condition([$catcontext, $thiscontext], $tagids));
|
||||
}
|
||||
|
||||
array_unshift($this->searchconditions, new \core_question\bank\search\hidden_condition(!$showhidden));
|
||||
array_unshift($this->searchconditions, new custom_category_condition(
|
||||
$cat, $recurse, $editcontexts, $this->baseurl, $this->course));
|
||||
}
|
||||
}
|
||||
$this->display_options_form($showquestiontext);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
<?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;
|
||||
|
||||
/**
|
||||
* 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 \core_question\bank\search\category_condition {
|
||||
|
||||
public function display_options() {
|
||||
global $PAGE;
|
||||
$displaydata = [];
|
||||
$catmenu = custom_category_condition_helper::question_category_options($this->contexts, true, 0,
|
||||
true, -1, false);
|
||||
$displaydata['categoryselect'] = \html_writer::select($catmenu, 'category', $this->cat, [],
|
||||
['class' => 'searchoptions custom-select', 'id' => 'id_selectacategory']);
|
||||
$displaydata['categorydesc'] = '';
|
||||
if ($this->category) {
|
||||
$displaydata['categorydesc'] = $this->print_category_info($this->category);
|
||||
}
|
||||
return $PAGE->get_renderer('core_question', 'bank')->render_category_condition($displaydata);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
427
mod/quiz/classes/question/bank/qbank_helper.php
Normal file
427
mod/quiz/classes/question/bank/qbank_helper.php
Normal file
|
@ -0,0 +1,427 @@
|
|||
<?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;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
require_once($CFG->dirroot . '/mod/quiz/accessmanager.php');
|
||||
require_once($CFG->dirroot . '/mod/quiz/attemptlib.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 {
|
||||
|
||||
/**
|
||||
* Check if the slot is a random question or not.
|
||||
*
|
||||
* @param int $slotid
|
||||
* @return bool
|
||||
*/
|
||||
public static function is_random($slotid): bool {
|
||||
global $DB;
|
||||
$params = [
|
||||
'itemid' => $slotid,
|
||||
'component' => 'mod_quiz',
|
||||
'questionarea' => 'slot'
|
||||
];
|
||||
return $DB->record_exists('question_set_references', $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the version options for the question.
|
||||
*
|
||||
* @param int $questionid
|
||||
* @return array
|
||||
*/
|
||||
public static function get_version_options($questionid): array {
|
||||
global $DB;
|
||||
$sql = "SELECT qv.id AS versionid, qv.version
|
||||
FROM {question_versions} qv
|
||||
WHERE qv.questionbankentryid = (SELECT DISTINCT qbe.id
|
||||
FROM {question_bank_entries} qbe
|
||||
JOIN {question_versions} qv ON qbe.id = qv.questionbankentryid
|
||||
JOIN {question} q ON qv.questionid = q.id
|
||||
WHERE q.id = ?)
|
||||
ORDER BY qv.version DESC";
|
||||
|
||||
return $DB->get_records_sql($sql, [$questionid]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort the elements of an array according to a key.
|
||||
*
|
||||
* @param array $arrays
|
||||
* @param string $on
|
||||
* @param int $order
|
||||
* @return array
|
||||
*/
|
||||
public static function question_array_sort($arrays, $on, $order = SORT_ASC): array {
|
||||
$element = [];
|
||||
foreach ($arrays as $array) {
|
||||
$element[$array->$on] = $array;
|
||||
}
|
||||
ksort($element, $order);
|
||||
return $element;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the question id from slot id.
|
||||
*
|
||||
* @param int $slotid
|
||||
* @return mixed
|
||||
*/
|
||||
public static function get_question_for_redo($slotid) {
|
||||
global $DB;
|
||||
$params = [
|
||||
'itemid' => $slotid,
|
||||
'component' => 'mod_quiz',
|
||||
'questionarea' => 'slot'
|
||||
];
|
||||
$referencerecord = $DB->get_record('question_references', $params);
|
||||
if ($referencerecord->version === null) {
|
||||
$questionsql = 'SELECT q.id
|
||||
FROM {question} q
|
||||
JOIN {question_versions} qv ON qv.questionid = q.id
|
||||
WHERE qv.version = (SELECT MAX(v.version)
|
||||
FROM {question_versions} v
|
||||
JOIN {question_bank_entries} be
|
||||
ON be.id = v.questionbankentryid
|
||||
WHERE be.id = qv.questionbankentryid)
|
||||
AND qv.questionbankentryid = ?';
|
||||
$questionid = $DB->get_record_sql($questionsql, [$referencerecord->questionbankentryid])->id;
|
||||
} else {
|
||||
$questionid = $DB->get_field('question_versions', 'questionid',
|
||||
['questionbankentryid' => $referencerecord->questionbankentryid,
|
||||
'version' => $referencerecord->version]);
|
||||
}
|
||||
return $questionid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get random question object from the slot id.
|
||||
*
|
||||
* @param int $slotid
|
||||
* @return false|mixed|\stdClass
|
||||
*/
|
||||
public static function get_random_question_data_from_slot($slotid) {
|
||||
global $DB;
|
||||
$params = [
|
||||
'itemid' => $slotid,
|
||||
'component' => 'mod_quiz',
|
||||
'questionarea' => 'slot'
|
||||
];
|
||||
return $DB->get_record('question_set_references', $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the question ids for specific question version.
|
||||
*
|
||||
* @param int $quizid
|
||||
* @return array
|
||||
*/
|
||||
public static function get_specific_version_question_ids($quizid) {
|
||||
global $DB;
|
||||
$questionids = [];
|
||||
$sql = 'SELECT qv.questionid
|
||||
FROM {quiz_slots} qs
|
||||
JOIN {question_references} qr ON qr.itemid = qs.id
|
||||
JOIN {question_versions} qv ON qv.questionbankentryid = qr.questionbankentryid
|
||||
AND qv.version = qr.version
|
||||
WHERE qr.version IS NOT NULL
|
||||
AND qs.quizid = ?
|
||||
AND qr.component = ?
|
||||
AND qr.questionarea = ?';
|
||||
$questions = $DB->get_records_sql($sql, [$quizid, 'mod_quiz', 'slot']);
|
||||
foreach ($questions as $question) {
|
||||
$questionids [] = $question->questionid;
|
||||
}
|
||||
return $questionids;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the question ids for always latest options.
|
||||
*
|
||||
* @param int $quizid
|
||||
* @return array
|
||||
*/
|
||||
public static function get_always_latest_version_question_ids($quizid) {
|
||||
global $DB;
|
||||
$questionids = [];
|
||||
$sql = 'SELECT qr.questionbankentryid as entry
|
||||
FROM {quiz_slots} qs
|
||||
JOIN {question_references} qr ON qr.itemid = qs.id
|
||||
WHERE qr.version IS NULL
|
||||
AND qs.quizid = ?
|
||||
AND qr.component = ?
|
||||
AND qr.questionarea = ?';
|
||||
$entryids = $DB->get_records_sql($sql, [$quizid, 'mod_quiz', 'slot']);
|
||||
$questionentries = [];
|
||||
foreach ($entryids as $entryid) {
|
||||
$questionentries [] = $entryid->entry;
|
||||
}
|
||||
if (empty($questionentries)) {
|
||||
return $questionids;
|
||||
}
|
||||
list($questionidcondition, $params) = $DB->get_in_or_equal($questionentries);
|
||||
$extracondition = 'AND qv.questionbankentryid ' . $questionidcondition;
|
||||
$questionsql = "SELECT q.id
|
||||
FROM {question} q
|
||||
JOIN {question_versions} qv ON qv.questionid = q.id
|
||||
WHERE qv.version = (SELECT MAX(v.version)
|
||||
FROM {question_versions} v
|
||||
JOIN {question_bank_entries} be
|
||||
ON be.id = v.questionbankentryid
|
||||
WHERE be.id = qv.questionbankentryid)
|
||||
$extracondition";
|
||||
$questions = $DB->get_records_sql($questionsql, $params);
|
||||
foreach ($questions as $question) {
|
||||
$questionids [] = $question->id;
|
||||
}
|
||||
return $questionids;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the question structure data for the given quiz or question ids.
|
||||
*
|
||||
* @param null $quizid
|
||||
* @param array $questionids
|
||||
* @param bool $attempt
|
||||
* @return array
|
||||
*/
|
||||
public static function get_question_structure_data($quizid, $questionids = [], $attempt = false) {
|
||||
global $DB;
|
||||
$params = ['quizid' => $quizid];
|
||||
$condition = '';
|
||||
$joinon = 'AND qr.version = qv.version';
|
||||
if (!empty($questionids)) {
|
||||
list($condition, $param) = $DB->get_in_or_equal($questionids, SQL_PARAMS_NAMED, 'questionid');
|
||||
$condition = 'AND q.id ' . $condition;
|
||||
$joinon = '';
|
||||
$params = array_merge($params, $param);
|
||||
}
|
||||
if ($attempt) {
|
||||
$selectstart = 'q.*, slot.id AS slotid, slot.slot,';
|
||||
} else {
|
||||
$selectstart = 'slot.slot, slot.id AS slotid, q.*,';
|
||||
}
|
||||
$sql = "SELECT $selectstart
|
||||
q.id AS questionid,
|
||||
q.name,
|
||||
q.qtype,
|
||||
q.length,
|
||||
slot.page,
|
||||
slot.maxmark,
|
||||
slot.requireprevious,
|
||||
qc.id as category,
|
||||
qc.contextid,qv.status,
|
||||
qv.id as versionid,
|
||||
qv.version,
|
||||
qv.questionbankentryid
|
||||
FROM {quiz_slots} slot
|
||||
LEFT JOIN {question_references} qr ON qr.itemid = slot.id AND qr.component = 'mod_quiz' AND qr.questionarea = 'slot'
|
||||
LEFT JOIN {question_bank_entries} qbe ON qbe.id = qr.questionbankentryid
|
||||
LEFT JOIN {question_versions} qv ON qv.questionbankentryid = qbe.id $joinon
|
||||
LEFT JOIN {question_categories} qc ON qc.id = qbe.questioncategoryid
|
||||
LEFT JOIN {question} q ON q.id = qv.questionid
|
||||
WHERE slot.quizid = :quizid
|
||||
$condition";
|
||||
$questiondatas = $DB->get_records_sql($sql, $params);
|
||||
foreach ($questiondatas as $questiondata) {
|
||||
$questiondata->_partiallyloaded = true;
|
||||
}
|
||||
if (!empty($questiondatas)) {
|
||||
return $questiondatas;
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get question structure.
|
||||
*
|
||||
* @param int $quizid
|
||||
* @return array
|
||||
*/
|
||||
public static function get_question_structure($quizid) {
|
||||
$firstslotsets = self::get_question_structure_data($quizid);
|
||||
$latestquestionids = self::get_always_latest_version_question_ids($quizid);
|
||||
$secondslotsets = self::get_question_structure_data($quizid, $latestquestionids);
|
||||
foreach ($firstslotsets as $key => $firstslotset) {
|
||||
foreach ($secondslotsets as $secondslotset) {
|
||||
if ($firstslotset->slotid === $secondslotset->slotid) {
|
||||
unset($firstslotsets[$key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return self::question_array_sort(array_merge($firstslotsets, $secondslotsets), 'slot');
|
||||
}
|
||||
|
||||
/**
|
||||
* Load random questions.
|
||||
*
|
||||
* @param int $quizid
|
||||
* @param array $questiondata
|
||||
* @return array
|
||||
*/
|
||||
public static function question_load_random_questions($quizid, $questiondata) {
|
||||
global $DB, $USER;
|
||||
$sql = 'SELECT slot.id AS slotid,
|
||||
slot.maxmark,
|
||||
slot.slot,
|
||||
slot.page,
|
||||
qsr.filtercondition
|
||||
FROM {question_set_references} qsr
|
||||
JOIN {quiz_slots} slot ON slot.id = qsr.itemid
|
||||
WHERE slot.quizid = ?
|
||||
AND qsr.component = ?
|
||||
AND qsr.questionarea = ?';
|
||||
$randomquestiondatas = $DB->get_records_sql($sql, [$quizid, 'mod_quiz', 'slot']);
|
||||
|
||||
$randomquestions = [];
|
||||
// Questions already added.
|
||||
$usedquestionids = [];
|
||||
foreach ($questiondata as $question) {
|
||||
if (isset($usedquestions[$question->id])) {
|
||||
$usedquestionids[$question->id] += 1;
|
||||
} else {
|
||||
$usedquestionids[$question->id] = 1;
|
||||
}
|
||||
}
|
||||
// Usages for this user's previous quiz attempts.
|
||||
$qubaids = new \mod_quiz\question\qubaids_for_users_attempts($quizid, $USER->id);
|
||||
$randomloader = new \core_question\local\bank\random_question_loader($qubaids, $usedquestionids);
|
||||
|
||||
foreach ($randomquestiondatas as $randomquestiondata) {
|
||||
$filtercondition = json_decode($randomquestiondata->filtercondition);
|
||||
$tagids = [];
|
||||
if (isset($filtercondition->tags)) {
|
||||
foreach ($filtercondition->tags as $tag) {
|
||||
$tagstring = explode(',', $tag);
|
||||
$tagids [] = $tagstring[0];
|
||||
}
|
||||
}
|
||||
$randomquestiondata->randomfromcategory = $filtercondition->questioncategoryid;
|
||||
$randomquestiondata->randomincludingsubcategories = $filtercondition->includingsubcategories;
|
||||
$randomquestiondata->questionid = $randomloader->get_next_question_id($randomquestiondata->randomfromcategory,
|
||||
$randomquestiondata->randomincludingsubcategories, $tagids);
|
||||
$randomquestions [] = $randomquestiondata;
|
||||
}
|
||||
|
||||
foreach ($randomquestions as $randomquestion) {
|
||||
// Should not add if there is no question found from the ramdom question loader, maybe empty category.
|
||||
if ($randomquestion->questionid === null) {
|
||||
continue;
|
||||
}
|
||||
$question = new \stdClass();
|
||||
$question->slotid = $randomquestion->slotid;
|
||||
$question->maxmark = $randomquestion->maxmark;
|
||||
$question->slot = $randomquestion->slot;
|
||||
$question->page = $randomquestion->page;
|
||||
$qdatas = question_preload_questions($randomquestion->questionid);
|
||||
$qdatas = reset($qdatas);
|
||||
foreach ($qdatas as $key => $qdata) {
|
||||
$question->$key = $qdata;
|
||||
}
|
||||
$questiondata[$question->id] = $question;
|
||||
}
|
||||
|
||||
return $questiondata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Choose question for redo.
|
||||
*
|
||||
* @param int $slotid
|
||||
* @param \qubaid_condition $qubaids
|
||||
* @return int
|
||||
*/
|
||||
public static function choose_question_for_redo($slotid, $qubaids): int {
|
||||
// Choose the replacement question.
|
||||
if (!self::is_random($slotid)) {
|
||||
$newqusetionid = self::get_question_for_redo($slotid);
|
||||
} else {
|
||||
$tagids = [];
|
||||
$randomquestiondata = self::get_random_question_data_from_slot($slotid);
|
||||
$filtercondition = json_decode($randomquestiondata->filtercondition);
|
||||
if (isset($filtercondition->tags)) {
|
||||
foreach ($filtercondition->tags as $tag) {
|
||||
$tagstring = explode(',', $tag);
|
||||
$tagids [] = $tagstring[0];
|
||||
}
|
||||
}
|
||||
|
||||
$randomloader = new \core_question\local\bank\random_question_loader($qubaids, []);
|
||||
$newqusetionid = $randomloader->get_next_question_id($filtercondition->questioncategoryid,
|
||||
(bool) $filtercondition->includingsubcategories, $tagids);
|
||||
if ($newqusetionid === null) {
|
||||
throw new \moodle_exception('notenoughrandomquestions', 'quiz');
|
||||
}
|
||||
|
||||
}
|
||||
return $newqusetionid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the version information for a question to show in the version selection dropdown.
|
||||
*
|
||||
* @param int $questionid
|
||||
* @param int $slotid
|
||||
* @return array
|
||||
*/
|
||||
public static function get_question_version_info($questionid, $slotid): array {
|
||||
global $DB;
|
||||
$versiondata = [];
|
||||
$versionsoptions = self::get_version_options($questionid);
|
||||
$latestversion = reset($versionsoptions);
|
||||
// Object for using the latest version.
|
||||
$alwaysuselatest = new \stdClass();
|
||||
$alwaysuselatest->versionid = 0;
|
||||
$alwaysuselatest->version = 0;
|
||||
$alwaysuselatest->versionvalue = get_string('alwayslatest', 'quiz');
|
||||
array_unshift($versionsoptions, $alwaysuselatest);
|
||||
$referencedata = $DB->get_record('question_references', ['itemid' => $slotid]);
|
||||
if (!isset($referencedata->version) || ($referencedata->version === null)) {
|
||||
$currentversion = 0;
|
||||
} else {
|
||||
$currentversion = $referencedata->version;
|
||||
}
|
||||
|
||||
foreach ($versionsoptions as $versionsoption) {
|
||||
$versionsoption->selected = false;
|
||||
if ($versionsoption->version === $currentversion) {
|
||||
$versionsoption->selected = true;
|
||||
}
|
||||
if (!isset($versionsoption->versionvalue)) {
|
||||
if ($versionsoption->version === $latestversion->version) {
|
||||
$versionsoption->versionvalue = get_string('questionversionlatest', 'quiz', $versionsoption->version);
|
||||
} else {
|
||||
$versionsoption->versionvalue = get_string('questionversion', 'quiz', $versionsoption->version);
|
||||
}
|
||||
}
|
||||
|
||||
$versiondata[] = $versionsoption;
|
||||
}
|
||||
return $versiondata;
|
||||
}
|
||||
}
|
|
@ -48,7 +48,7 @@ class question_name_text_column extends question_name_column {
|
|||
$fields = parent::get_required_fields();
|
||||
$fields[] = 'q.questiontext';
|
||||
$fields[] = 'q.questiontextformat';
|
||||
$fields[] = 'q.idnumber';
|
||||
$fields[] = 'qbe.idnumber';
|
||||
return $fields;
|
||||
}
|
||||
|
||||
|
|
|
@ -24,8 +24,10 @@
|
|||
*/
|
||||
|
||||
namespace mod_quiz\question;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
require_once($CFG->dirroot.'/mod/quiz/attemptlib.php');
|
||||
|
||||
/**
|
||||
* A {@link qubaid_condition} representing all the attempts by one user at a given quiz.
|
||||
|
|
|
@ -23,6 +23,8 @@
|
|||
*/
|
||||
|
||||
namespace mod_quiz;
|
||||
use mod_quiz\question\bank\qbank_helper;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
/**
|
||||
|
@ -126,6 +128,15 @@ class structure {
|
|||
return $this->questions[$this->slotsinorder[$slotnumber]->questionid];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the information about the question name in a given slot.
|
||||
* @param int $slotnumber the index of the slot in question.
|
||||
* @return \stdClass the data from the questions table, augmented with
|
||||
*/
|
||||
public function get_question_name_in_slot($slotnumber) {
|
||||
return $this->questions[$this->slotsinorder[$slotnumber]->name];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the displayed question number (or 'i') for a given slot.
|
||||
* @param int $slotnumber the index of the slot in question.
|
||||
|
@ -606,30 +617,29 @@ class structure {
|
|||
public function populate_structure($quiz) {
|
||||
global $DB;
|
||||
|
||||
$slots = $DB->get_records_sql("
|
||||
SELECT slot.id AS slotid, slot.slot, slot.questionid, slot.page, slot.maxmark,
|
||||
slot.requireprevious, q.*, qc.contextid
|
||||
FROM {quiz_slots} slot
|
||||
LEFT JOIN {question} q ON q.id = slot.questionid
|
||||
LEFT JOIN {question_categories} qc ON qc.id = q.category
|
||||
WHERE slot.quizid = ?
|
||||
ORDER BY slot.slot", array($quiz->id));
|
||||
$slots = qbank_helper::get_question_structure($quiz->id);
|
||||
|
||||
$slots = $this->populate_missing_questions($slots);
|
||||
|
||||
$this->questions = array();
|
||||
$this->slotsinorder = array();
|
||||
$this->questions = [];
|
||||
$this->slotsinorder = [];
|
||||
foreach ($slots as $slotdata) {
|
||||
$this->questions[$slotdata->questionid] = $slotdata;
|
||||
|
||||
$slot = new \stdClass();
|
||||
$slot->id = $slotdata->slotid;
|
||||
$slot->name = $slotdata->name;
|
||||
$slot->slot = $slotdata->slot;
|
||||
$slot->quizid = $quiz->id;
|
||||
$slot->page = $slotdata->page;
|
||||
$slot->questionid = $slotdata->questionid;
|
||||
$slot->maxmark = $slotdata->maxmark;
|
||||
$slot->requireprevious = $slotdata->requireprevious;
|
||||
$slot->qtype = $slotdata->qtype;
|
||||
$slot->length = $slotdata->length;
|
||||
$slot->category = $slotdata->category;
|
||||
$slot->questionbankentryid = $slotdata->questionbankentryid ?? null;
|
||||
$slot->version = $slotdata->version ?? null;
|
||||
|
||||
$this->slotsinorder[$slot->slot] = $slot;
|
||||
}
|
||||
|
@ -646,21 +656,30 @@ class structure {
|
|||
* @return \stdClass[] updated $slots array.
|
||||
*/
|
||||
protected function populate_missing_questions($slots) {
|
||||
// Address missing question types.
|
||||
global $DB;
|
||||
// Address missing/random question types.
|
||||
foreach ($slots as $slot) {
|
||||
if ($slot->qtype === null) {
|
||||
// Check if the question is random.
|
||||
if ($setreference = $DB->get_record('question_set_references', ['itemid' => $slot->slotid])) {
|
||||
$filtercondition = json_decode($setreference->filtercondition);
|
||||
$slot->id = $slot->slotid;
|
||||
$slot->category = $filtercondition->questioncategoryid;
|
||||
$slot->qtype = 'random';
|
||||
$slot->name = get_string('random', 'quiz');
|
||||
$slot->length = 1;
|
||||
} else {
|
||||
// If the questiontype is missing change the question type.
|
||||
$slot->id = $slot->questionid;
|
||||
$slot->category = 0;
|
||||
$slot->qtype = 'missingtype';
|
||||
$slot->name = get_string('missingquestion', 'quiz');
|
||||
$slot->slot = $slot->slot;
|
||||
$slot->maxmark = 0;
|
||||
$slot->requireprevious = 0;
|
||||
$slot->questiontext = ' ';
|
||||
$slot->questiontextformat = FORMAT_HTML;
|
||||
$slot->length = 1;
|
||||
|
||||
}
|
||||
} else if (!\question_bank::qtype_exists($slot->qtype)) {
|
||||
$slot->qtype = 'missingtype';
|
||||
}
|
||||
|
@ -936,7 +955,18 @@ class structure {
|
|||
$maxslot = $DB->get_field_sql('SELECT MAX(slot) FROM {quiz_slots} WHERE quizid = ?', array($this->get_quizid()));
|
||||
|
||||
$trans = $DB->start_delegated_transaction();
|
||||
$DB->delete_records('quiz_slot_tags', array('slotid' => $slot->id));
|
||||
// Delete the reference if its a question.
|
||||
$questionreference = $DB->get_record('question_references',
|
||||
['component' => 'mod_quiz', 'questionarea' => 'slot', 'itemid' => $slot->id]);
|
||||
if ($questionreference) {
|
||||
$DB->delete_records('question_references', ['id' => $questionreference->id]);
|
||||
}
|
||||
// Delete the set reference if its a random question.
|
||||
$questionsetreference = $DB->get_record('question_set_references',
|
||||
['component' => 'mod_quiz', 'questionarea' => 'slot', 'itemid' => $slot->id]);
|
||||
if ($questionsetreference) {
|
||||
$DB->delete_records('question_set_references', ['id' => $questionsetreference->id]);
|
||||
}
|
||||
$DB->delete_records('quiz_slots', array('id' => $slot->id));
|
||||
for ($i = $slot->slot + 1; $i <= $maxslot; $i++) {
|
||||
$DB->set_field('quiz_slots', 'slot', $i - 1,
|
||||
|
@ -946,12 +976,6 @@ class structure {
|
|||
unset($this->slotsinorder[$i]);
|
||||
}
|
||||
|
||||
$qtype = $DB->get_field('question', 'qtype', array('id' => $slot->questionid));
|
||||
if ($qtype === 'random') {
|
||||
// This function automatically checks if the question is in use, and won't delete if it is.
|
||||
question_delete_question($slot->questionid);
|
||||
}
|
||||
|
||||
quiz_update_section_firstslots($this->get_quizid(), -1, $slotnumber);
|
||||
foreach ($this->sections as $key => $section) {
|
||||
if ($section->firstslot > $slotnumber) {
|
||||
|
@ -960,7 +984,7 @@ class structure {
|
|||
}
|
||||
$this->populate_slots_with_sections();
|
||||
$this->populate_question_numbers();
|
||||
unset($this->questions[$slot->questionid]);
|
||||
$this->unset_question($slot->id);
|
||||
|
||||
$this->refresh_page_numbers_and_update_db();
|
||||
|
||||
|
@ -978,6 +1002,19 @@ class structure {
|
|||
$event->trigger();
|
||||
}
|
||||
|
||||
/**
|
||||
* Unset the question object after deletion.
|
||||
*
|
||||
* @param int $slotid
|
||||
*/
|
||||
public function unset_question($slotid) {
|
||||
foreach ($this->questions as $key => $question) {
|
||||
if ($question->slotid === $slotid) {
|
||||
unset($this->questions[$key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the max mark for a slot.
|
||||
*
|
||||
|
@ -1203,30 +1240,6 @@ class structure {
|
|||
$event->trigger();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up this class with the slot tags for each of the slots.
|
||||
*/
|
||||
protected function populate_slot_tags() {
|
||||
$slotids = array_column($this->slotsinorder, 'id');
|
||||
$this->slottags = quiz_retrieve_tags_for_slot_ids($slotids);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the list of slot tags for the given slot id.
|
||||
*
|
||||
* @param int $slotid The id for the slot
|
||||
* @return \stdClass[] The list of slot tag records
|
||||
*/
|
||||
public function get_slot_tags_for_slot_id($slotid) {
|
||||
if (!$this->hasloadedtags) {
|
||||
// Lazy load the tags just in case they are never required.
|
||||
$this->populate_slot_tags();
|
||||
$this->hasloadedtags = true;
|
||||
}
|
||||
|
||||
return isset($this->slottags[$slotid]) ? $this->slottags[$slotid] : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the current user can add random questions to the quiz or not.
|
||||
* It is only possible to add a random question if the user has the moodle/question:useall capability
|
||||
|
@ -1237,7 +1250,7 @@ class structure {
|
|||
public function can_add_random_questions() {
|
||||
if ($this->canaddrandom === null) {
|
||||
$quizcontext = $this->quizobj->get_context();
|
||||
$relatedcontexts = new \question_edit_contexts($quizcontext);
|
||||
$relatedcontexts = new \core_question\local\bank\question_edit_contexts($quizcontext);
|
||||
$usablecontexts = $relatedcontexts->having_cap('moodle/question:useall');
|
||||
|
||||
$this->canaddrandom = !empty($usablecontexts);
|
||||
|
@ -1245,4 +1258,21 @@ class structure {
|
|||
|
||||
return $this->canaddrandom;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Retrieve the list of slot tags for the given slot id.
|
||||
*
|
||||
* @param int $slotid The id for the slot
|
||||
* @return \stdClass[] The list of slot tag records
|
||||
* @deprecated since Moodle 4.0 MDL-71573
|
||||
* @todo Final deprecation on Moodle 4.4 MDL-72438
|
||||
*/
|
||||
public function get_slot_tags_for_slot_id($slotid) {
|
||||
debugging('Function get_slot_tags_for_slot_id() has been deprecated and the structure
|
||||
for this method have been moved to filtercondition in question_set_reference table, please
|
||||
use the new structure instead.', DEBUG_DEVELOPER);
|
||||
// All the associated code for this method have been removed to get rid of accidental call or errors.
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -62,16 +62,11 @@
|
|||
<FIELD NAME="quizid" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Foreign key references quiz.id."/>
|
||||
<FIELD NAME="page" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="The page number that this questions appears on. If the question in slot n appears on page p, then the question in slot n+1 must appear on page p or p+1. Well, except that when a quiz is being created, there may be empty pages, which would cause the page number to jump here."/>
|
||||
<FIELD NAME="requireprevious" TYPE="int" LENGTH="4" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Set to 1 when current question requires previous one to be answered first."/>
|
||||
<FIELD NAME="questionid" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Foreign key references question.id."/>
|
||||
<FIELD NAME="questioncategoryid" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false" COMMENT="The question category that the random question will be picked from. Will be null if and only if the question is not a random question."/>
|
||||
<FIELD NAME="includingsubcategories" TYPE="int" LENGTH="4" NOTNULL="false" SEQUENCE="false" COMMENT="Whether the random question can be picked from sub categories or not. Will be null if questioncategoryid is null."/>
|
||||
<FIELD NAME="maxmark" TYPE="number" LENGTH="12" NOTNULL="true" DEFAULT="0" SEQUENCE="false" DECIMALS="7" COMMENT="How many marks this question contributes to quiz.sumgrades."/>
|
||||
</FIELDS>
|
||||
<KEYS>
|
||||
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
|
||||
<KEY NAME="quizid" TYPE="foreign" FIELDS="quizid" REFTABLE="quiz" REFFIELDS="id"/>
|
||||
<KEY NAME="questionid" TYPE="foreign" FIELDS="questionid" REFTABLE="question" REFFIELDS="id"/>
|
||||
<KEY NAME="questioncategoryid" TYPE="foreign" FIELDS="questioncategoryid" REFTABLE="question_categories" REFFIELDS="id"/>
|
||||
</KEYS>
|
||||
<INDEXES>
|
||||
<INDEX NAME="quizid-slot" UNIQUE="true" FIELDS="quizid, slot"/>
|
||||
|
@ -186,18 +181,5 @@
|
|||
<INDEX NAME="name" UNIQUE="true" FIELDS="name"/>
|
||||
</INDEXES>
|
||||
</TABLE>
|
||||
<TABLE NAME="quiz_slot_tags" COMMENT="Stores data about the tags that a question must have so that it can be selected for a quiz slot (when having a random question by tags on that slot).">
|
||||
<FIELDS>
|
||||
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
|
||||
<FIELD NAME="slotid" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false" COMMENT="The quiz slot that this tag belong to"/>
|
||||
<FIELD NAME="tagid" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false"/>
|
||||
<FIELD NAME="tagname" TYPE="char" LENGTH="255" NOTNULL="false" SEQUENCE="false" COMMENT="The tag name is to be stored as well, so we won't lose data if the tag is removed from Moodle (A tag with the same name might be added in future)."/>
|
||||
</FIELDS>
|
||||
<KEYS>
|
||||
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
|
||||
<KEY NAME="slotid" TYPE="foreign" FIELDS="slotid" REFTABLE="quiz_slots" REFFIELDS="id"/>
|
||||
<KEY NAME="tagid" TYPE="foreign" FIELDS="tagid" REFTABLE="tag" REFFIELDS="id"/>
|
||||
</KEYS>
|
||||
</TABLE>
|
||||
</TABLES>
|
||||
</XMLDB>
|
||||
|
|
|
@ -191,4 +191,12 @@ $functions = array(
|
|||
'capabilities' => 'mod/quiz:view',
|
||||
'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
|
||||
),
|
||||
|
||||
'mod_quiz_set_question_version' => [
|
||||
'classname' => 'mod_quiz\external\submit_question_version',
|
||||
'description' => 'Set the version of question that would be required for a given quiz.',
|
||||
'type' => 'write',
|
||||
'capabilities' => 'mod/quiz:view',
|
||||
'ajax' => true,
|
||||
],
|
||||
);
|
||||
|
|
|
@ -120,5 +120,51 @@ function xmldb_quiz_upgrade($oldversion) {
|
|||
upgrade_mod_savepoint(true, 2021101900, 'quiz');
|
||||
}
|
||||
|
||||
if ($oldversion < 2022020300) {
|
||||
// Define table quiz_slot_tags to be dropped.
|
||||
$table = new xmldb_table('quiz_slot_tags');
|
||||
|
||||
// Conditionally launch drop table for quiz_slot_tags.
|
||||
if ($dbman->table_exists($table)) {
|
||||
$dbman->drop_table($table);
|
||||
}
|
||||
|
||||
// Define fields to be dropped from quiz_slots.
|
||||
$table = new xmldb_table('quiz_slots');
|
||||
|
||||
// Define key questionid (foreign) to be dropped form quiz_slots.
|
||||
$key = new xmldb_key('questionid', XMLDB_KEY_FOREIGN, ['questionid'], 'question', ['id']);
|
||||
|
||||
// Launch drop key questionid.
|
||||
$dbman->drop_key($table, $key);
|
||||
|
||||
// Define key questioncategoryid (foreign) to be dropped form quiz_slots.
|
||||
$key = new xmldb_key('questioncategoryid', XMLDB_KEY_FOREIGN, ['questioncategoryid'], 'question_categories', ['id']);
|
||||
|
||||
// Launch drop key questioncategoryid.
|
||||
$dbman->drop_key($table, $key);
|
||||
|
||||
$field = new xmldb_field('questionid');
|
||||
// Conditionally launch drop field questionid.
|
||||
if ($dbman->field_exists($table, $field)) {
|
||||
$dbman->drop_field($table, $field);
|
||||
}
|
||||
|
||||
$field = new xmldb_field('questioncategoryid');
|
||||
// Conditionally launch drop field questioncategoryid.
|
||||
if ($dbman->field_exists($table, $field)) {
|
||||
$dbman->drop_field($table, $field);
|
||||
}
|
||||
|
||||
$field = new xmldb_field('includingsubcategories');
|
||||
// Conditionally launch drop field includingsubcategories.
|
||||
if ($dbman->field_exists($table, $field)) {
|
||||
$dbman->drop_field($table, $field);
|
||||
}
|
||||
|
||||
// Quiz savepoint reached.
|
||||
upgrade_mod_savepoint(true, 2022020300, 'quiz');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
*
|
||||
* @package mod_quiz
|
||||
* @copyright 2018 Shamim Rezaie <shamim@moodle.com>
|
||||
* @author 2021 Safat Shahin <safatshahin@catalyst-au.net>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
|
@ -29,13 +30,13 @@ $slotid = required_param('slotid', PARAM_INT);
|
|||
$returnurl = optional_param('returnurl', '', PARAM_LOCALURL);
|
||||
|
||||
// Get the quiz slot.
|
||||
$slot = $DB->get_record('quiz_slots', array('id' => $slotid));
|
||||
if (!$slot || empty($slot->questioncategoryid)) {
|
||||
print_error('invalidrandomslot', 'mod_quiz');
|
||||
$slot = $DB->get_record('quiz_slots', ['id' => $slotid]);
|
||||
if (!$slot) {
|
||||
new moodle_exception('invalidrandomslot', 'mod_quiz');
|
||||
}
|
||||
|
||||
if (!$quiz = $DB->get_record('quiz', array('id' => $slot->quizid))) {
|
||||
print_error('invalidquizid', 'quiz');
|
||||
if (!$quiz = $DB->get_record('quiz', ['id' => $slot->quizid])) {
|
||||
new moodle_exception('invalidquizid', 'quiz');
|
||||
}
|
||||
|
||||
$cm = get_coursemodule_from_instance('quiz', $slot->quizid, $quiz->course);
|
||||
|
@ -45,46 +46,46 @@ require_login($cm->course, false, $cm);
|
|||
if ($returnurl) {
|
||||
$returnurl = new moodle_url($returnurl);
|
||||
} else {
|
||||
$returnurl = new moodle_url('/mod/quiz/edit.php', array('cmid' => $cm->id));
|
||||
$returnurl = new moodle_url('/mod/quiz/edit.php', ['cmid' => $cm->id]);
|
||||
}
|
||||
|
||||
$url = new moodle_url('/mod/quiz/editrandom.php', array('slotid' => $slotid));
|
||||
$url = new moodle_url('/mod/quiz/editrandom.php', ['slotid' => $slotid]);
|
||||
$PAGE->set_url($url);
|
||||
$PAGE->set_pagelayout('admin');
|
||||
$PAGE->add_body_class('limitedwidth');
|
||||
|
||||
if (!$question = $DB->get_record('question', array('id' => $slot->questionid))) {
|
||||
print_error('questiondoesnotexist', 'question', $returnurl);
|
||||
}
|
||||
|
||||
$qtypeobj = question_bank::get_qtype('random');
|
||||
$setreference = $DB->get_record('question_set_references', ['itemid' => $slot->id]);
|
||||
$filterconditions = json_decode($setreference->filtercondition);
|
||||
|
||||
// Validate the question category.
|
||||
if (!$category = $DB->get_record('question_categories', array('id' => $question->category))) {
|
||||
print_error('categorydoesnotexist', 'question', $returnurl);
|
||||
if (!$category = $DB->get_record('question_categories', ['id' => $filterconditions->questioncategoryid])) {
|
||||
new moodle_exception('categorydoesnotexist', 'question', $returnurl);
|
||||
}
|
||||
|
||||
// Check permissions.
|
||||
question_require_capability_on($question, 'edit');
|
||||
$catcontext = context::instance_by_id($category->contextid);
|
||||
require_capability('moodle/question:useall', $catcontext);
|
||||
|
||||
$thiscontext = context_module::instance($cm->id);
|
||||
$contexts = new question_edit_contexts($thiscontext);
|
||||
$contexts = new core_question\local\bank\question_edit_contexts($thiscontext);
|
||||
|
||||
// Create the question editing form.
|
||||
$mform = new mod_quiz\form\randomquestion_form(new moodle_url('/mod/quiz/editrandom.php'),
|
||||
array('contexts' => $contexts));
|
||||
// Create the editing form.
|
||||
$mform = new mod_quiz\form\randomquestion_form(new moodle_url('/mod/quiz/editrandom.php'), ['contexts' => $contexts]);
|
||||
|
||||
// Send the question object and a few more parameters to the form.
|
||||
$toform = fullclone($question);
|
||||
// Set the form data.
|
||||
$toform = new stdClass();
|
||||
$toform->category = "{$category->id},{$category->contextid}";
|
||||
$toform->includesubcategories = $slot->includingsubcategories;
|
||||
$toform->includesubcategories = $filterconditions->includingsubcategories;
|
||||
$toform->fromtags = array();
|
||||
$currentslottags = quiz_retrieve_slot_tags($slot->id);
|
||||
foreach ($currentslottags as $slottag) {
|
||||
$toform->fromtags[] = "{$slottag->tagid},{$slottag->tagname}";
|
||||
if (isset($filterconditions->tags)) {
|
||||
$currentslottags = $filterconditions->tags;
|
||||
foreach ($currentslottags as $slottag) {
|
||||
$toform->fromtags[] = $slottag;
|
||||
}
|
||||
}
|
||||
$toform->returnurl = $returnurl;
|
||||
|
||||
$toform->returnurl = $returnurl;
|
||||
$toform->slotid = $slot->id;
|
||||
if ($cm !== null) {
|
||||
$toform->cmid = $cm->id;
|
||||
$toform->courseid = $cm->course;
|
||||
|
@ -92,89 +93,46 @@ if ($cm !== null) {
|
|||
$toform->courseid = $COURSE->id;
|
||||
}
|
||||
|
||||
$toform->slotid = $slotid;
|
||||
|
||||
$mform->set_data($toform);
|
||||
|
||||
if ($mform->is_cancelled()) {
|
||||
redirect($returnurl);
|
||||
} else if ($fromform = $mform->get_data()) {
|
||||
|
||||
// If we are moving a question, check we have permission to move it from
|
||||
// whence it came. Where we are moving to is validated by the form.
|
||||
list($newcatid, $newcontextid) = explode(',', $fromform->category);
|
||||
if (!empty($question->id) && $newcatid != $question->category) {
|
||||
if ($newcatid != $category->id) {
|
||||
$contextid = $newcontextid;
|
||||
question_require_capability_on($question, 'move');
|
||||
} else {
|
||||
$contextid = $category->contextid;
|
||||
}
|
||||
$setreference->questionscontextid = $contextid;
|
||||
|
||||
$question = $qtypeobj->save_question($question, $fromform);
|
||||
|
||||
// We need to save some data into the quiz_slots table.
|
||||
$slot->questioncategoryid = $fromform->category;
|
||||
$slot->includingsubcategories = $fromform->includesubcategories;
|
||||
|
||||
$DB->update_record('quiz_slots', $slot);
|
||||
// Set the filter conditions.
|
||||
$filtercondition = new stdClass();
|
||||
$filtercondition->questioncategoryid = $newcatid;
|
||||
$filtercondition->includingsubcategories = $fromform->includesubcategories;
|
||||
|
||||
if (isset($fromform->fromtags)) {
|
||||
$tags = [];
|
||||
foreach ($fromform->fromtags as $tagstring) {
|
||||
list($tagid, $tagname) = explode(',', $tagstring);
|
||||
$tags[] = (object) [
|
||||
'id' => $tagid,
|
||||
'name' => $tagname
|
||||
];
|
||||
$tags[] = "{$tagid},{$tagname}";
|
||||
}
|
||||
|
||||
$recordstokeep = [];
|
||||
$recordstoinsert = [];
|
||||
$searchableslottags = array_map(function($slottag) {
|
||||
return ['tagid' => $slottag->tagid, 'tagname' => $slottag->tagname];
|
||||
}, $currentslottags);
|
||||
|
||||
foreach ($tags as $tag) {
|
||||
if ($key = array_search(['tagid' => $tag->id, 'tagname' => $tag->name], $searchableslottags)) {
|
||||
// If found, $key would be the id field in the quiz_slot_tags table.
|
||||
// Therefore, there was no need to check !== false here.
|
||||
$recordstokeep[] = $key;
|
||||
} else {
|
||||
$recordstoinsert[] = (object)[
|
||||
'slotid' => $slot->id,
|
||||
'tagid' => $tag->id,
|
||||
'tagname' => $tag->name
|
||||
];
|
||||
if (!empty($tags)) {
|
||||
$filtercondition->tags = $tags;
|
||||
}
|
||||
}
|
||||
|
||||
// Now, delete the remaining records.
|
||||
if (!empty($recordstokeep)) {
|
||||
list($select, $params) = $DB->get_in_or_equal($recordstokeep, SQL_PARAMS_QM, 'param', false);
|
||||
array_unshift($params, $slot->id);
|
||||
$DB->delete_records_select('quiz_slot_tags', "slotid = ? AND id $select", $params);
|
||||
} else {
|
||||
$DB->delete_records('quiz_slot_tags', array('slotid' => $slot->id));
|
||||
}
|
||||
$setreference->filtercondition = json_encode($filtercondition);
|
||||
$DB->update_record('question_set_references', $setreference);
|
||||
|
||||
// And now, insert the extra records if there is any.
|
||||
if (!empty($recordstoinsert)) {
|
||||
$DB->insert_records('quiz_slot_tags', $recordstoinsert);
|
||||
}
|
||||
|
||||
// Purge this question from the cache.
|
||||
question_bank::notify_question_edited($question->id);
|
||||
|
||||
$returnurl->param('lastchanged', $question->id);
|
||||
redirect($returnurl);
|
||||
}
|
||||
|
||||
$streditingquestion = $qtypeobj->get_heading();
|
||||
$PAGE->set_title($streditingquestion);
|
||||
$PAGE->set_title('Random question');
|
||||
$PAGE->set_heading($COURSE->fullname);
|
||||
$PAGE->navbar->add($streditingquestion);
|
||||
$PAGE->navbar->add('Random question');
|
||||
|
||||
// Display a heading, question editing form and possibly some extra content needed for
|
||||
// for this question type.
|
||||
// Display a heading, question editing form.
|
||||
echo $OUTPUT->header();
|
||||
$heading = get_string('randomediting', 'mod_quiz');
|
||||
echo $OUTPUT->heading_with_help($heading, 'randomquestion', 'mod_quiz');
|
||||
|
|
|
@ -1051,6 +1051,10 @@ $string['wronguse'] = 'You can not use this page like that';
|
|||
$string['xhtml'] = 'XHTML';
|
||||
$string['youneedtoenrol'] = 'You need to enrol in this course before you can attempt this quiz';
|
||||
$string['yourfinalgradeis'] = 'Your final grade for this quiz is {$a}.';
|
||||
$string['questionversion'] = 'v{$a}';
|
||||
$string['questionversionlatest'] = 'v{$a} (latest)';
|
||||
$string['alwayslatest'] = 'Always latest';
|
||||
$string['gobacktoquiz'] = 'Go back';
|
||||
|
||||
// Deprecated since Moodle 3.11.
|
||||
$string['completionattemptsexhausteddesc'] = 'Complete if all available attempts are exhausted';
|
||||
|
|
|
@ -176,24 +176,12 @@ function quiz_delete_instance($id) {
|
|||
|
||||
quiz_delete_all_attempts($quiz);
|
||||
quiz_delete_all_overrides($quiz);
|
||||
|
||||
// Look for random questions that may no longer be used when this quiz is gone.
|
||||
$sql = "SELECT q.id
|
||||
FROM {quiz_slots} slot
|
||||
JOIN {question} q ON q.id = slot.questionid
|
||||
WHERE slot.quizid = ? AND q.qtype = ?";
|
||||
$questionids = $DB->get_fieldset_sql($sql, array($quiz->id, 'random'));
|
||||
quiz_delete_references($quiz->id);
|
||||
|
||||
// We need to do the following deletes before we try and delete randoms, otherwise they would still be 'in use'.
|
||||
$quizslots = $DB->get_fieldset_select('quiz_slots', 'id', 'quizid = ?', array($quiz->id));
|
||||
$DB->delete_records_list('quiz_slot_tags', 'slotid', $quizslots);
|
||||
$DB->delete_records('quiz_slots', array('quizid' => $quiz->id));
|
||||
$DB->delete_records('quiz_sections', array('quizid' => $quiz->id));
|
||||
|
||||
foreach ($questionids as $questionid) {
|
||||
question_delete_question($questionid);
|
||||
}
|
||||
|
||||
$DB->delete_records('quiz_feedback', array('quizid' => $quiz->id));
|
||||
|
||||
quiz_access_manager::delete_settings($quiz);
|
||||
|
@ -1445,11 +1433,15 @@ function quiz_get_post_actions() {
|
|||
* @return bool whether any of these questions are used by any instance of this module.
|
||||
*/
|
||||
function quiz_questions_in_use($questionids) {
|
||||
global $DB, $CFG;
|
||||
require_once($CFG->libdir . '/questionlib.php');
|
||||
global $DB;
|
||||
list($test, $params) = $DB->get_in_or_equal($questionids);
|
||||
return $DB->record_exists_select('quiz_slots',
|
||||
'questionid ' . $test, $params) || question_engine::questions_in_use(
|
||||
$sql = "SELECT qs.id
|
||||
FROM {quiz_slots} qs
|
||||
JOIN {question_references} qr ON qr.itemid = qs.id
|
||||
JOIN {question_bank_entries} qbe ON qbe.id = qr.questionbankentryid
|
||||
JOIN {question_versions} qv ON qv.questionbankentryid = qbe.id
|
||||
WHERE qv.questionid $test";
|
||||
return $DB->record_exists_sql($sql, $params) || question_engine::questions_in_use(
|
||||
$questionids, new qubaid_join('{quiz_attempts} quiza',
|
||||
'quiza.uniqueid', 'quiza.preview = 0'));
|
||||
}
|
||||
|
@ -2421,7 +2413,7 @@ function mod_quiz_output_fragment_add_random_question_form($args) {
|
|||
global $CFG;
|
||||
require_once($CFG->dirroot . '/mod/quiz/addrandomform.php');
|
||||
|
||||
$contexts = new \question_edit_contexts($args['context']);
|
||||
$contexts = new \core_question\local\bank\question_edit_contexts($args['context']);
|
||||
$formoptions = [
|
||||
'contexts' => $contexts,
|
||||
'cat' => $args['cat']
|
||||
|
@ -2469,3 +2461,24 @@ function mod_quiz_core_calendar_get_event_action_string(string $eventtype): stri
|
|||
|
||||
return get_string($identifier, 'quiz', $modulename);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete question reference data.
|
||||
*
|
||||
* @param int $quizid The id of quiz.
|
||||
*/
|
||||
function quiz_delete_references($quizid): void {
|
||||
global $DB;
|
||||
$slots = $DB->get_records('quiz_slots', ['quizid' => $quizid]);
|
||||
foreach ($slots as $slot) {
|
||||
$params = [
|
||||
'itemid' => $slot->id,
|
||||
'component' => 'mod_quiz',
|
||||
'questionarea' => 'slot'
|
||||
];
|
||||
// Delete any set references.
|
||||
$DB->delete_records('question_set_references', $params);
|
||||
// Delete any references.
|
||||
$DB->delete_records('question_references', $params);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -171,10 +171,12 @@ function quiz_start_new_attempt($quizobj, $quba, $attempt, $attemptnumber, $time
|
|||
|
||||
// First load all the non-random questions.
|
||||
$randomfound = false;
|
||||
$randomtestfound = false;
|
||||
$slot = 0;
|
||||
$questions = array();
|
||||
$maxmark = array();
|
||||
$page = array();
|
||||
$questiondatarandom = [];
|
||||
foreach ($quizobj->get_questions() as $questiondata) {
|
||||
$slot += 1;
|
||||
$maxmark[$slot] = $questiondata->maxmark;
|
||||
|
@ -183,55 +185,34 @@ function quiz_start_new_attempt($quizobj, $quba, $attempt, $attemptnumber, $time
|
|||
$randomfound = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Intended for testing purposes only.
|
||||
foreach ($questionids as $key => $questionid) {
|
||||
if ($questionid !== (int)$questiondata->id && $slot === $key) {
|
||||
$randomtestfound = true;
|
||||
$questiondatarandom[$key] = $questiondata;
|
||||
continue 2;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$quizobj->get_quiz()->shuffleanswers) {
|
||||
$questiondata->options->shuffleanswers = false;
|
||||
}
|
||||
$questions[$slot] = question_bank::make_question($questiondata);
|
||||
}
|
||||
|
||||
// Then find a question to go in place of each random question.
|
||||
// Then find a question throw an error as something horribly wrong might have happened.
|
||||
if ($randomfound) {
|
||||
$slot = 0;
|
||||
$usedquestionids = array();
|
||||
foreach ($questions as $question) {
|
||||
if (isset($usedquestions[$question->id])) {
|
||||
$usedquestionids[$question->id] += 1;
|
||||
} else {
|
||||
$usedquestionids[$question->id] = 1;
|
||||
}
|
||||
}
|
||||
$randomloader = new \core_question\local\bank\random_question_loader($qubaids, $usedquestionids);
|
||||
|
||||
foreach ($quizobj->get_questions() as $questiondata) {
|
||||
$slot += 1;
|
||||
if ($questiondata->qtype != 'random') {
|
||||
continue;
|
||||
throw new coding_exception(
|
||||
'Using "random" questions directly in an attempt is deprecated. Please use question_set_references table instead.'
|
||||
);
|
||||
}
|
||||
|
||||
$tagids = quiz_retrieve_slot_tag_ids($questiondata->slotid);
|
||||
|
||||
// Then find a question to go in place of each random question. Intended for testing purposes only.
|
||||
if ($randomtestfound) {
|
||||
foreach ($questiondatarandom as $slot => $questiondata) {
|
||||
// Deal with fixed random choices for testing.
|
||||
if (isset($questionids[$quba->next_slot_number()])) {
|
||||
if ($randomloader->is_question_available($questiondata->category,
|
||||
(bool) $questiondata->questiontext, $questionids[$quba->next_slot_number()], $tagids)) {
|
||||
$questions[$slot] = question_bank::load_question(
|
||||
$questionids[$quba->next_slot_number()], $quizobj->get_quiz()->shuffleanswers);
|
||||
continue;
|
||||
} else {
|
||||
throw new coding_exception('Forced question id not available.');
|
||||
}
|
||||
}
|
||||
|
||||
// Normal case, pick one at random.
|
||||
$questionid = $randomloader->get_next_question_id($questiondata->randomfromcategory,
|
||||
$questiondata->randomincludingsubcategories, $tagids);
|
||||
if ($questionid === null) {
|
||||
throw new moodle_exception('notenoughrandomquestions', 'quiz',
|
||||
$quizobj->view_url(), $questiondata);
|
||||
}
|
||||
|
||||
$questions[$slot] = question_bank::load_question($questionid,
|
||||
$quizobj->get_quiz()->shuffleanswers);
|
||||
$questions[$slot] = question_bank::load_question($questionids[$slot], $quizobj->get_quiz()->shuffleanswers);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1380,12 +1361,16 @@ function quiz_attempt_state_name($state) {
|
|||
* @param object $question the question.
|
||||
* @param string $returnurl url to return to after action is done.
|
||||
* @param int $variant which question variant to preview (optional).
|
||||
* @param bool $random if question is random, true.
|
||||
* @return string html for a number of icons linked to action pages for a
|
||||
* question - preview and edit / view icons depending on user capabilities.
|
||||
*/
|
||||
function quiz_question_action_icons($quiz, $cmid, $question, $returnurl, $variant = null) {
|
||||
$html = quiz_question_preview_button($quiz, $question, false, $variant) . ' ' .
|
||||
quiz_question_edit_button($cmid, $question, $returnurl);
|
||||
$html = '';
|
||||
if ($question->qtype !== 'random') {
|
||||
$html = quiz_question_preview_button($quiz, $question, false, $variant);
|
||||
}
|
||||
$html .= quiz_question_edit_button($cmid, $question, $returnurl);
|
||||
return $html;
|
||||
}
|
||||
|
||||
|
@ -1464,15 +1449,15 @@ function quiz_question_preview_url($quiz, $question, $variant = null) {
|
|||
* @param object $question the question
|
||||
* @param bool $label if true, show the preview question label after the icon
|
||||
* @param int $variant which question variant to preview (optional).
|
||||
* @param bool $random if question is random, true.
|
||||
* @return the HTML for a preview question icon.
|
||||
*/
|
||||
function quiz_question_preview_button($quiz, $question, $label = false, $variant = null) {
|
||||
function quiz_question_preview_button($quiz, $question, $label = false, $variant = null, $random = null) {
|
||||
global $PAGE;
|
||||
if (!question_has_capability_on($question, 'use')) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return $PAGE->get_renderer('mod_quiz', 'edit')->question_preview_icon($quiz, $question, $label, $variant);
|
||||
return $PAGE->get_renderer('mod_quiz', 'edit')->question_preview_icon($quiz, $question, $label, $variant, null);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -2240,7 +2225,7 @@ function quiz_question_tostring($question, $showicon = false, $showquestiontext
|
|||
if ($showquestiontext) {
|
||||
$questiontext = question_utils::to_plain_text($question->questiontext,
|
||||
$question->questiontextformat, array('noclean' => true, 'para' => false));
|
||||
$questiontext = shorten_text($questiontext, 200);
|
||||
$questiontext = shorten_text($questiontext, 50);
|
||||
if ($questiontext) {
|
||||
$result .= ' ' . html_writer::span(s($questiontext), 'questiontext');
|
||||
}
|
||||
|
@ -2268,11 +2253,17 @@ function quiz_require_question_use($questionid) {
|
|||
*/
|
||||
function quiz_has_question_use($quiz, $slot) {
|
||||
global $DB;
|
||||
$question = $DB->get_record_sql("
|
||||
SELECT q.*
|
||||
|
||||
$sql = 'SELECT q.*
|
||||
FROM {quiz_slots} slot
|
||||
JOIN {question} q ON q.id = slot.questionid
|
||||
WHERE slot.quizid = ? AND slot.slot = ?", array($quiz->id, $slot));
|
||||
JOIN {question_references} qre ON qre.itemid = slot.id
|
||||
JOIN {question_bank_entries} qbe ON qbe.id = qre.questionbankentryid
|
||||
JOIN {question_versions} qve ON qve.questionbankentryid = qbe.id
|
||||
JOIN {question} q ON q.id = qve.questionid
|
||||
WHERE slot.quizid = ? AND slot.slot = ?';
|
||||
|
||||
$question = $DB->get_record_sql($sql, [$quiz->id, $slot]);
|
||||
|
||||
if (!$question) {
|
||||
return false;
|
||||
}
|
||||
|
@ -2296,6 +2287,11 @@ function quiz_has_question_use($quiz, $slot) {
|
|||
function quiz_add_quiz_question($questionid, $quiz, $page = 0, $maxmark = null) {
|
||||
global $DB;
|
||||
|
||||
if (!isset($quiz->cmid)) {
|
||||
$cm = get_coursemodule_from_instance('quiz', $quiz->id, $quiz->course);
|
||||
$quiz->cmid = $cm->id;
|
||||
}
|
||||
|
||||
// Make sue the question is not of the "random" type.
|
||||
$questiontype = $DB->get_field('question', 'qtype', array('id' => $questionid));
|
||||
if ($questiontype == 'random') {
|
||||
|
@ -2305,13 +2301,29 @@ function quiz_add_quiz_question($questionid, $quiz, $page = 0, $maxmark = null)
|
|||
}
|
||||
|
||||
$trans = $DB->start_delegated_transaction();
|
||||
$slots = $DB->get_records('quiz_slots', array('quizid' => $quiz->id),
|
||||
'slot', 'questionid, slot, page, id');
|
||||
if (array_key_exists($questionid, $slots)) {
|
||||
|
||||
$sql = "SELECT qbe.id
|
||||
FROM {quiz_slots} slot
|
||||
JOIN {question_references} qr ON qr.itemid = slot.id
|
||||
JOIN {question_bank_entries} qbe ON qbe.id = qr.questionbankentryid
|
||||
WHERE slot.quizid = ?";
|
||||
|
||||
$questionslots = $DB->get_records_sql($sql, [$quiz->id]);
|
||||
|
||||
$currententry = get_question_bank_entry($questionid);
|
||||
|
||||
if (array_key_exists($currententry->id, $questionslots)) {
|
||||
$trans->allow_commit();
|
||||
return false;
|
||||
}
|
||||
|
||||
$sql = "SELECT slot.slot, slot.page, slot.id
|
||||
FROM {quiz_slots} slot
|
||||
WHERE slot.quizid = ?
|
||||
ORDER BY slot.slot";
|
||||
|
||||
$slots = $DB->get_records_sql($sql, [$quiz->id]);
|
||||
|
||||
$maxpage = 1;
|
||||
$numonlastpage = 0;
|
||||
foreach ($slots as $slot) {
|
||||
|
@ -2323,10 +2335,9 @@ function quiz_add_quiz_question($questionid, $quiz, $page = 0, $maxmark = null)
|
|||
}
|
||||
}
|
||||
|
||||
// Add the new question instance.
|
||||
// Add the new instance.
|
||||
$slot = new stdClass();
|
||||
$slot->quizid = $quiz->id;
|
||||
$slot->questionid = $questionid;
|
||||
|
||||
if ($maxmark !== null) {
|
||||
$slot->maxmark = $maxmark;
|
||||
|
@ -2364,14 +2375,57 @@ function quiz_add_quiz_question($questionid, $quiz, $page = 0, $maxmark = null)
|
|||
}
|
||||
}
|
||||
|
||||
$newslotid = $DB->insert_record('quiz_slots', $slot);
|
||||
$slotid = $DB->insert_record('quiz_slots', $slot);
|
||||
|
||||
// Update or insert record in question_reference table.
|
||||
$sql = "SELECT DISTINCT qr.id, qr.itemid
|
||||
FROM {question} q
|
||||
JOIN {question_versions} qv ON q.id = qv.questionid
|
||||
JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
|
||||
JOIN {question_references} qr ON qbe.id = qr.questionbankentryid AND qr.version = qv.version
|
||||
JOIN {quiz_slots} qs ON qs.id = qr.itemid
|
||||
WHERE q.id = ?
|
||||
AND qs.id =?";
|
||||
$qreferenceitem = $DB->get_record_sql($sql, [$questionid, $slotid]);
|
||||
|
||||
if (!$qreferenceitem) {
|
||||
// Create a new reference record for questions created already.
|
||||
$questionreferences = new \StdClass();
|
||||
$questionreferences->usingcontextid = context_module::instance($quiz->cmid)->id;
|
||||
$questionreferences->component = 'mod_quiz';
|
||||
$questionreferences->questionarea = 'slot';
|
||||
$questionreferences->itemid = $slotid;
|
||||
$questionreferences->questionbankentryid = get_question_bank_entry($questionid)->id;
|
||||
$version = get_question_version($questionid);
|
||||
$questionreferences->version = $version[array_key_first($version)]->version;
|
||||
|
||||
$DB->insert_record('question_references', $questionreferences);
|
||||
|
||||
} else if ($qreferenceitem->itemid === 0 || $qreferenceitem->itemid === null) {
|
||||
$questionreferences = new \StdClass();
|
||||
$questionreferences->id = $qreferenceitem->id;
|
||||
$questionreferences->itemid = $slotid;
|
||||
$DB->update_record('question_references', $questionreferences);
|
||||
} else {
|
||||
// If the reference record exits for another quiz.
|
||||
$questionreferences = new \StdClass();
|
||||
$questionreferences->usingcontextid = context_module::instance($quiz->cmid)->id;
|
||||
$questionreferences->component = 'mod_quiz';
|
||||
$questionreferences->questionarea = 'slot';
|
||||
$questionreferences->itemid = $slotid;
|
||||
$questionreferences->questionbankentryid = get_question_bank_entry($questionid)->id;
|
||||
$version = get_question_version($questionid);
|
||||
$questionreferences->version = $version[array_key_first($version)]->version;
|
||||
$DB->insert_record('question_references', $questionreferences);
|
||||
}
|
||||
|
||||
$trans->allow_commit();
|
||||
|
||||
// Log slot created event.
|
||||
$cm = get_coursemodule_from_instance('quiz', $quiz->id);
|
||||
$event = \mod_quiz\event\slot_created::create([
|
||||
'context' => context_module::instance($cm->id),
|
||||
'objectid' => $newslotid,
|
||||
'objectid' => $slotid,
|
||||
'other' => [
|
||||
'quizid' => $quiz->id,
|
||||
'slotnumber' => $slot->slot,
|
||||
|
@ -2415,61 +2469,45 @@ function quiz_add_random_questions($quiz, $addonpage, $categoryid, $number,
|
|||
$includesubcategories, $tagids = []) {
|
||||
global $DB;
|
||||
|
||||
$category = $DB->get_record('question_categories', array('id' => $categoryid));
|
||||
$category = $DB->get_record('question_categories', ['id' => $categoryid]);
|
||||
if (!$category) {
|
||||
print_error('invalidcategoryid', 'error');
|
||||
new moodle_exception('invalidcategoryid');
|
||||
}
|
||||
|
||||
$catcontext = context::instance_by_id($category->contextid);
|
||||
require_capability('moodle/question:useall', $catcontext);
|
||||
|
||||
// Tags for filter condition.
|
||||
$tags = \core_tag_tag::get_bulk($tagids, 'id, name');
|
||||
$tagstrings = [];
|
||||
foreach ($tags as $tag) {
|
||||
$tagstrings[] = "{$tag->id},{$tag->name}";
|
||||
}
|
||||
|
||||
// Find existing random questions in this category that are
|
||||
// not used by any quiz.
|
||||
$existingquestions = $DB->get_records_sql(
|
||||
"SELECT q.id, q.qtype FROM {question} q
|
||||
WHERE qtype = 'random'
|
||||
AND category = ?
|
||||
AND " . $DB->sql_compare_text('questiontext') . " = ?
|
||||
AND NOT EXISTS (
|
||||
SELECT *
|
||||
FROM {quiz_slots}
|
||||
WHERE questionid = q.id)
|
||||
ORDER BY id", array($category->id, $includesubcategories ? '1' : '0'));
|
||||
|
||||
// Create the selected number of random questions.
|
||||
for ($i = 0; $i < $number; $i++) {
|
||||
// Take as many of orphaned "random" questions as needed.
|
||||
if (!$question = array_shift($existingquestions)) {
|
||||
$form = new stdClass();
|
||||
$form->category = $category->id . ',' . $category->contextid;
|
||||
$form->includesubcategories = $includesubcategories;
|
||||
$form->fromtags = $tagstrings;
|
||||
$form->defaultmark = 1;
|
||||
$form->hidden = 1;
|
||||
$form->stamp = make_unique_id_code(); // Set the unique code (not to be changed).
|
||||
$question = new stdClass();
|
||||
$question->qtype = 'random';
|
||||
$question = question_bank::get_qtype('random')->save_question($question, $form);
|
||||
if (!isset($question->id)) {
|
||||
print_error('cannotinsertrandomquestion', 'quiz');
|
||||
}
|
||||
// Set the filter conditions.
|
||||
$filtercondition = new stdClass();
|
||||
$filtercondition->questioncategoryid = $categoryid;
|
||||
$filtercondition->includingsubcategories = $includesubcategories ? 1 : 0;
|
||||
if (!empty($tagstrings)) {
|
||||
$filtercondition->tags = $tagstrings;
|
||||
}
|
||||
|
||||
if (!isset($quiz->cmid)) {
|
||||
$cm = get_coursemodule_from_instance('quiz', $quiz->id, $quiz->course);
|
||||
$quiz->cmid = $cm->id;
|
||||
}
|
||||
|
||||
// Slot data.
|
||||
$randomslotdata = new stdClass();
|
||||
$randomslotdata->quizid = $quiz->id;
|
||||
$randomslotdata->questionid = $question->id;
|
||||
$randomslotdata->questioncategoryid = $categoryid;
|
||||
$randomslotdata->includingsubcategories = $includesubcategories ? 1 : 0;
|
||||
$randomslotdata->usingcontextid = context_module::instance($quiz->cmid)->id;
|
||||
$randomslotdata->questionscontextid = $category->contextid;
|
||||
$randomslotdata->maxmark = 1;
|
||||
|
||||
$randomslot = new \mod_quiz\local\structure\slot_random($randomslotdata);
|
||||
$randomslot->set_quiz($quiz);
|
||||
$randomslot->set_tags($tags);
|
||||
$randomslot->set_filter_condition($filtercondition);
|
||||
$randomslot->insert($addonpage);
|
||||
}
|
||||
}
|
||||
|
@ -2698,10 +2736,13 @@ function quiz_is_overriden_calendar_event(\calendar_event $event) {
|
|||
*
|
||||
* @param int[] $slotids The list of id for the quiz slots.
|
||||
* @return array[] List of quiz_slot_tags records indexed by slot id.
|
||||
* @deprecated since Moodle 4.0
|
||||
* @todo Final deprecation on Moodle 4.4 MDL-72438
|
||||
*/
|
||||
function quiz_retrieve_tags_for_slot_ids($slotids) {
|
||||
debugging('Method quiz_retrieve_tags_for_slot_ids() is deprecated, ' .
|
||||
'see filtercondition->tags from the question_set_reference table.', DEBUG_DEVELOPER);
|
||||
global $DB;
|
||||
|
||||
if (empty($slotids)) {
|
||||
return [];
|
||||
}
|
||||
|
@ -2757,11 +2798,17 @@ function quiz_retrieve_tags_for_slot_ids($slotids) {
|
|||
* A quiz slot have some tags if and only if it is representing a random question by tags.
|
||||
*
|
||||
* @param int $slotid The id of the quiz slot.
|
||||
* @return stdClass[] List of quiz_slot_tags records.
|
||||
* @return array List of tags.
|
||||
*/
|
||||
function quiz_retrieve_slot_tags($slotid) {
|
||||
$slottags = quiz_retrieve_tags_for_slot_ids([$slotid]);
|
||||
return $slottags[$slotid];
|
||||
$referencedata = \mod_quiz\question\bank\qbank_helper::get_random_question_data_from_slot($slotid);
|
||||
if (isset($referencedata->filtercondition)) {
|
||||
$filtercondition = json_decode($referencedata->filtercondition);
|
||||
if (isset($filtercondition->tags)) {
|
||||
return $filtercondition->tags;
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -2772,10 +2819,13 @@ function quiz_retrieve_slot_tags($slotid) {
|
|||
* @return int[]
|
||||
*/
|
||||
function quiz_retrieve_slot_tag_ids($slotid) {
|
||||
$tagids = [];
|
||||
$tags = quiz_retrieve_slot_tags($slotid);
|
||||
|
||||
// Only work with tags that exist.
|
||||
return array_filter(array_column($tags, 'tagid'));
|
||||
foreach ($tags as $tag) {
|
||||
$tagstring = explode(',', $tag);
|
||||
$tagids [] = $tagstring[0];
|
||||
}
|
||||
return $tagids;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -256,7 +256,7 @@ abstract class quiz_attempts_report_table extends table_sql {
|
|||
|
||||
$feedbackimg = '';
|
||||
$state = $this->slot_state($attempt, $slot);
|
||||
if ($state->is_finished() && $state != question_state::$needsgrading) {
|
||||
if ($state && $state->is_finished() && $state != question_state::$needsgrading) {
|
||||
$feedbackimg = $this->icon_for_fraction($this->slot_fraction($attempt, $slot));
|
||||
}
|
||||
|
||||
|
|
|
@ -63,7 +63,6 @@ class quiz_overview_report extends quiz_attempts_report {
|
|||
|
||||
// Load the required questions.
|
||||
$questions = quiz_report_get_significant_questions($quiz);
|
||||
|
||||
// Prepare for downloading, if applicable.
|
||||
$courseshortname = format_string($course->shortname, true,
|
||||
array('context' => context_course::instance($course->id)));
|
||||
|
|
|
@ -73,37 +73,38 @@ Feature: Regrading quiz attempts using the Grades report
|
|||
And I should see "Quiz for testing regrading"
|
||||
And I should see "Overall number of students achieving grade ranges"
|
||||
|
||||
Scenario: Dry-run a full regrade, then regrade the attempts that will need it.
|
||||
Given I am on the "Quiz for testing regrading" "quiz activity" page logged in as teacher
|
||||
When I navigate to "Edit quiz" in current page administration
|
||||
And I follow "Edit question SA"
|
||||
And I set the field "id_fraction_1" to "50%"
|
||||
And I press "id_submitbutton"
|
||||
And I follow "Attempts: 2"
|
||||
And I press "Dry run a full regrade"
|
||||
# @todo MDL-72890 uncomment this scenario and add some more for re-grading.
|
||||
# Scenario: Dry-run a full regrade, then regrade the attempts that will need it.
|
||||
# Given I am on the "Quiz for testing regrading" "quiz activity" page logged in as teacher
|
||||
# When I navigate to "Edit quiz" in current page administration
|
||||
# And I follow "Edit question SA"
|
||||
# And I set the field "id_fraction_1" to "50%"
|
||||
# And I press "id_submitbutton"
|
||||
# And I follow "Attempts: 2"
|
||||
# And I press "Dry run a full regrade"
|
||||
|
||||
# Note, the order is not defined, so we can only check part of the message.
|
||||
Then I should see "Quiz for testing regrading"
|
||||
And I should see "Successfully regraded (2/2)"
|
||||
And I should see "Regrade completed successfully"
|
||||
And I press "Continue"
|
||||
# Then I should see "Quiz for testing regrading"
|
||||
# And I should see "Successfully regraded (2/2)"
|
||||
# And I should see "Regrade completed successfully"
|
||||
# And I press "Continue"
|
||||
|
||||
And "Student One" row "Regrade" column of "attempts" table should not contain "Needed"
|
||||
And "Student TwoReview attempt" row "Regrade" column of "attempts" table should contain "Needed"
|
||||
# And "Student One" row "Regrade" column of "attempts" table should not contain "Needed"
|
||||
# And "Student TwoReview attempt" row "Regrade" column of "attempts" table should contain "Needed"
|
||||
# In the following, the first number is strike-through, and the second is not, but Behat can't see that.
|
||||
# At this point, it is showing what would change.
|
||||
And "Student TwoReview attempt" row "Q. 2/50.00Sort by Q. 2/50.00 Ascending" column of "attempts" table should contain "40.00/25.00"
|
||||
And "Student TwoReview attempt" row "Grade/100.00Sort by Grade/100.00 Ascending" column of "attempts" table should contain "90.00/75.00"
|
||||
And I press "Regrade attempts marked as needing regrading (1)"
|
||||
And I should see "Quiz for testing regrading"
|
||||
And I should see "Successfully regraded (1/1)"
|
||||
And I should see "Regrade completed successfully"
|
||||
And I press "Continue"
|
||||
# And "Student TwoReview attempt" row "Q. 2/50.00Sort by Q. 2/50.00 Ascending" column of "attempts" table should contain "40.00/25.00"
|
||||
# And "Student TwoReview attempt" row "Grade/100.00Sort by Grade/100.00 Ascending" column of "attempts" table should contain "90.00/75.00"
|
||||
# And I press "Regrade attempts marked as needing regrading (1)"
|
||||
# And I should see "Quiz for testing regrading"
|
||||
# And I should see "Successfully regraded (1/1)"
|
||||
# And I should see "Regrade completed successfully"
|
||||
# And I press "Continue"
|
||||
|
||||
# These next tests just serve to check we got back to the report.
|
||||
And I should see "Quiz for testing regrading"
|
||||
And I should see "Overall number of students achieving grade ranges"
|
||||
# And I should see "Quiz for testing regrading"
|
||||
# And I should see "Overall number of students achieving grade ranges"
|
||||
# Now, both old-score strike-through and new score plain, are still shown, but now it indicates what did change.
|
||||
And "Student TwoReview attempt" row "Q. 2/50.00Sort by Q. 2/50.00 Ascending" column of "attempts" table should contain "40.00/25.00"
|
||||
And "Student TwoReview attempt" row "Grade/100.00Sort by Grade/100.00 Ascending" column of "attempts" table should contain "90.00/75.00"
|
||||
And "Regrade attempts marked as needing regrading" "button" should not exist
|
||||
# And "Student TwoReview attempt" row "Q. 2/50.00Sort by Q. 2/50.00 Ascending" column of "attempts" table should contain "40.00/25.00"
|
||||
# And "Student TwoReview attempt" row "Grade/100.00Sort by Grade/100.00 Ascending" column of "attempts" table should contain "90.00/75.00"
|
||||
# And "Regrade attempts marked as needing regrading" "button" should not exist
|
||||
|
|
|
@ -27,6 +27,7 @@ defined('MOODLE_INTERNAL') || die();
|
|||
|
||||
require_once($CFG->dirroot . '/mod/quiz/lib.php');
|
||||
require_once($CFG->libdir . '/filelib.php');
|
||||
require_once($CFG->dirroot . '/mod/quiz/accessmanager.php');
|
||||
|
||||
/**
|
||||
* Takes an array of objects and constructs a multidimensional array keyed by
|
||||
|
@ -94,29 +95,31 @@ function quiz_has_questions($quizid) {
|
|||
*/
|
||||
function quiz_report_get_significant_questions($quiz) {
|
||||
global $DB;
|
||||
|
||||
$qsbyslot = $DB->get_records_sql("
|
||||
SELECT slot.slot,
|
||||
q.id,
|
||||
q.qtype,
|
||||
q.length,
|
||||
slot.maxmark
|
||||
|
||||
FROM {question} q
|
||||
JOIN {quiz_slots} slot ON slot.questionid = q.id
|
||||
|
||||
WHERE slot.quizid = ?
|
||||
AND q.length > 0
|
||||
|
||||
ORDER BY slot.slot", array($quiz->id));
|
||||
|
||||
$qsbyslot = [];
|
||||
$quizobj = \quiz::create($quiz->id);
|
||||
$structure = \mod_quiz\structure::create_for_quiz($quizobj);
|
||||
$slots = $structure->get_slots();
|
||||
foreach ($slots as $slot) {
|
||||
$slotreport = new \stdClass();
|
||||
$slotreport->slot = $slot->slot;
|
||||
$slotreport->id = $slot->questionid;
|
||||
$slotreport->qtype = $slot->qtype;
|
||||
$slotreport->length = $slot->length;
|
||||
$slotreport->maxmark = $slot->maxmark;
|
||||
$slotreport->type = $slot->qtype;
|
||||
if ($slot->qtype === 'random') {
|
||||
$categoryobject = $DB->get_record('question_categories', ['id' => $slot->category]);
|
||||
$slotreport->categoryobject = $categoryobject;
|
||||
$slotreport->category = $slot->category;
|
||||
}
|
||||
$qsbyslot[$slotreport->slot] = $slotreport;
|
||||
}
|
||||
$qsbyslot = \mod_quiz\question\bank\qbank_helper::question_array_sort($qsbyslot, 'slot');
|
||||
$number = 1;
|
||||
foreach ($qsbyslot as $question) {
|
||||
$question->number = $number;
|
||||
$number += $question->length;
|
||||
$question->type = $question->qtype;
|
||||
$number ++;
|
||||
}
|
||||
|
||||
return $qsbyslot;
|
||||
}
|
||||
|
||||
|
|
|
@ -830,19 +830,33 @@ class quiz_statistics_report extends quiz_default_report {
|
|||
public function load_and_initialise_questions_for_calculations($quiz) {
|
||||
// Load the questions.
|
||||
$questions = quiz_report_get_significant_questions($quiz);
|
||||
$questionids = array();
|
||||
foreach ($questions as $question) {
|
||||
$questionids = [];
|
||||
$randomquestions = [];
|
||||
foreach ($questions as $qs => $question) {
|
||||
if ($question->qtype === 'random') {
|
||||
$question->id = 0;
|
||||
$question->name = get_string('random', 'quiz');
|
||||
$question->questiontext = get_string('random', 'quiz');
|
||||
$question->parenttype = 'random';
|
||||
$randomquestions [] = $question;
|
||||
unset($questions[$qs]);
|
||||
continue;
|
||||
}
|
||||
$questionids[] = $question->id;
|
||||
}
|
||||
$fullquestions = question_load_questions($questionids);
|
||||
foreach ($questions as $qno => $question) {
|
||||
$q = $fullquestions[$question->id];
|
||||
$q->maxmark = $question->maxmark;
|
||||
$q->slot = $qno;
|
||||
$q->slot = $question->slot;
|
||||
$q->number = $question->number;
|
||||
$questions[$qno] = $q;
|
||||
$q->parenttype = null;
|
||||
$questiondata[$question->slot] = $q;
|
||||
}
|
||||
return $questions;
|
||||
foreach ($randomquestions as $randomquestion) {
|
||||
$questiondata[$randomquestion->slot] = $randomquestion;
|
||||
}
|
||||
return \mod_quiz\question\bank\qbank_helper::question_array_sort($questiondata, 'slot');
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -188,7 +188,8 @@ class quiz_statistics_table extends flexible_table {
|
|||
if ($this->is_calculated_question_summary($questionstat)) {
|
||||
return '';
|
||||
} else {
|
||||
return print_question_icon($questionstat->question, true);
|
||||
$questionobject = $questionstat->question;
|
||||
return print_question_icon($questionobject);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -201,8 +202,12 @@ class quiz_statistics_table extends flexible_table {
|
|||
if ($this->is_calculated_question_summary($questionstat)) {
|
||||
return '';
|
||||
} else {
|
||||
$random = null;
|
||||
if ($questionstat->question->qtype === 'random') {
|
||||
$random = true;
|
||||
}
|
||||
return quiz_question_action_icons($this->quiz, $this->cmid,
|
||||
$questionstat->question, $this->baseurl, $questionstat->variant);
|
||||
$questionstat->question, $this->baseurl, $questionstat->variant, $random);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -59,8 +59,10 @@ Feature: Basic use of the Statistics report
|
|||
| 1 | False |
|
||||
| 2 | False |
|
||||
| 3 | False |
|
||||
And I am on the "Quiz 1" "quiz activity" page logged in as teacher1
|
||||
And I navigate to "Results > Statistics" in current page administration
|
||||
And I press "Show report"
|
||||
Then I should not see "No attempts have been made at this quiz, or all attempts have questions that need manual grading."
|
||||
Then I should not see "No questions have been attempted yet"
|
||||
And "Show chart data" "link" should exist
|
||||
|
||||
# Question A statistics breakdown.
|
||||
|
|
|
@ -358,6 +358,11 @@ body.path-mod-quiz table tbody tr.gradedattempt > td {
|
|||
margin-top: 1.5em;
|
||||
}
|
||||
|
||||
#page-mod-quiz-edit .section .activity .actions .version-selection {
|
||||
width: 8em;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
@media print {
|
||||
.quiz-secure-window * {
|
||||
display: none;
|
||||
|
@ -789,6 +794,7 @@ table.quizreviewsummary td.cell {
|
|||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
min-height: 1.7em;
|
||||
padding-right: 2px;
|
||||
}
|
||||
|
||||
#page-mod-quiz-edit ul.slots li.section li.activity .mod-indent-outer {
|
||||
|
|
52
mod/quiz/templates/question_slot.mustache
Normal file
52
mod/quiz/templates/question_slot.mustache
Normal file
|
@ -0,0 +1,52 @@
|
|||
{{!
|
||||
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/>.
|
||||
}}
|
||||
{{!
|
||||
@template mod_quiz/question_slot
|
||||
|
||||
This template renders the question slot content.
|
||||
|
||||
Example context (json):
|
||||
{
|
||||
"checkbox" : "<input id='selectquestion-1' name='selectquestion[]' type='checkbox' class='select-multiple-checkbox'>",
|
||||
"questionnumber" : "<span class='slotnumber'><span class='accesshide'>Question</span> 1</span>",
|
||||
"questionname" : "This is a test question",
|
||||
"questionicons" : "<i class='icon fa fa-search-plus fa-fw' title='Preview question' aria-label='Preview question'></i>",
|
||||
"canbeedited" : false,
|
||||
"questiondependencyicon" : "<span class='question_dependency_wrapper question_dependency_cannot_depend'></span>"
|
||||
}
|
||||
}}
|
||||
|
||||
<div class="mod-indent-outer" id="mod-indent-outer-slot-{{slotid}}">
|
||||
{{{checkbox}}}
|
||||
{{{questionnumber}}}
|
||||
<div class="mod-indent"></div>
|
||||
<div class="activityinstance">
|
||||
{{{questionname}}}
|
||||
</div>
|
||||
<span class="actions">
|
||||
{{#versionselection}}
|
||||
<label for="version-{{slotid}}"> </label>
|
||||
<select id="version-{{slotid}}" name="version" class="form-control mr-2 h-auto version-selection"
|
||||
data-action="mod_quiz-select_slot" data-slot-id="{{slotid}}">
|
||||
{{#versionoption}}
|
||||
<option value="{{version}}" {{#selected}}selected="selected"{{/selected}}>{{versionvalue}}</option>
|
||||
{{/versionoption}}
|
||||
</select>
|
||||
{{/versionselection}}
|
||||
{{{questionicons}}}
|
||||
</span>
|
||||
{{#canbeedited}}
|
||||
{{{questiondependencyicon}}}
|
||||
{{/canbeedited}}
|
||||
</div>
|
|
@ -149,7 +149,6 @@ class mod_quiz_attempt_walkthrough_from_csv_testcase extends advanced_testcase {
|
|||
*/
|
||||
protected function create_quiz_simulate_attempts_and_check_results($quizsettings, $csvdata) {
|
||||
$this->resetAfterTest(true);
|
||||
question_bank::get_qtype('random')->clear_caches_before_testing();
|
||||
|
||||
$this->create_quiz($quizsettings, $csvdata['questions']);
|
||||
|
||||
|
|
|
@ -106,15 +106,13 @@ Feature: Allow students to redo questions in a practice quiz, without starting a
|
|||
|
||||
@javascript
|
||||
Scenario: Redoing questions should work with random questions as well
|
||||
Given the following "questions" exist:
|
||||
| questioncategory | qtype | name | questiontext |
|
||||
| Test questions | random | Random (Test questions) | 0 |
|
||||
And the following "activities" exist:
|
||||
Given the following "activities" exist:
|
||||
| activity | name | intro | course | idnumber | preferredbehaviour | canredoquestions |
|
||||
| quiz | Quiz 2 | Quiz 2 description | C1 | quiz2 | immediatefeedback | 1 |
|
||||
And quiz "Quiz 2" contains the following questions:
|
||||
| question | page |
|
||||
| Random (Test questions) | 1 |
|
||||
And I am on the "Quiz 2" "mod_quiz > Edit" page logged in as "admin"
|
||||
And I open the "last" add to quiz menu
|
||||
And I follow "a random question"
|
||||
And I press "Add random question"
|
||||
And user "student" has started an attempt at quiz "Quiz 2" randomised as follows:
|
||||
| slot | actualquestion |
|
||||
| 1 | TF1 |
|
||||
|
@ -124,7 +122,7 @@ Feature: Allow students to redo questions in a practice quiz, without starting a
|
|||
And I click on "False" "radio"
|
||||
And I click on "Check" "button"
|
||||
And I press "Try another question like this one"
|
||||
Then I should see "Second question"
|
||||
And I should see "Second question"
|
||||
And "Check" "button" should exist
|
||||
|
||||
Scenario: Teachers reviewing can see author of action in review attempt
|
||||
|
|
|
@ -212,7 +212,12 @@ class behat_mod_quiz extends behat_question_base {
|
|||
}
|
||||
|
||||
// Question id, category and type.
|
||||
$question = $DB->get_record('question', array('name' => $questiondata['question']), 'id, category, qtype', MUST_EXIST);
|
||||
$sql = 'SELECT q.id AS id, qbe.questioncategoryid AS category, q.qtype AS qtype
|
||||
FROM {question} q
|
||||
JOIN {question_versions} qv ON qv.questionid = q.id
|
||||
JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
|
||||
WHERE q.name = :name';
|
||||
$question = $DB->get_record_sql($sql, ['name' => $questiondata['question']], MUST_EXIST);
|
||||
|
||||
// Page number.
|
||||
$page = clean_param($questiondata['page'], PARAM_INT);
|
||||
|
@ -937,4 +942,16 @@ class behat_mod_quiz extends behat_question_base {
|
|||
|
||||
$this->set_user();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a list of the exact named selectors for the component.
|
||||
*
|
||||
* @return behat_component_named_selector[]
|
||||
*/
|
||||
public static function get_exact_named_selectors(): array {
|
||||
return [
|
||||
new behat_component_named_selector('Edit slot',
|
||||
["//li[contains(@class,'qtype')]//span[@class='slotnumber' and contains(., %locator%)]/.."])
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -44,7 +44,10 @@ Feature: Adding random questions to a quiz based on category and tags
|
|||
And I follow "a random question"
|
||||
And I set the field "Tags" to "foo"
|
||||
And I press "Add random question"
|
||||
Then I should see "Random (Questions Category 1, tags: foo)" on quiz page "1"
|
||||
And I should see "Random question"
|
||||
And I click on "(See questions)" "link"
|
||||
Then I should see "Questions Category 1"
|
||||
And I should see "foo"
|
||||
|
||||
Scenario: Teacher without moodle/question:useall should not see the add a random question menu item
|
||||
Given the following "permission overrides" exist:
|
||||
|
@ -64,4 +67,6 @@ Feature: Adding random questions to a quiz based on category and tags
|
|||
| Name | New Random category |
|
||||
| Parent category | Top for Quiz 1 |
|
||||
And I press "Create category and add random question"
|
||||
Then I should see "Random (New Random category)" on quiz page "1"
|
||||
And I should see "Random question"
|
||||
And I click on "(See questions)" "link"
|
||||
Then I should see "Top for Quiz 1"
|
||||
|
|
|
@ -45,11 +45,11 @@ Feature: Editing random questions already in a quiz based on category and tags
|
|||
And I set the field "Tags" to "hard"
|
||||
And I press "Add random question"
|
||||
And I follow "Add page break"
|
||||
When I click on "Configure question" "link" in the "Random (Questions Category 1, tags: easy)" "list_item"
|
||||
When I click on "Configure question" "link" in the "Random question" "list_item"
|
||||
And I click on "easy" "autocomplete_selection"
|
||||
And I set the field "Tags" to "essay"
|
||||
And I press "Save changes"
|
||||
Then I should see "Random (Questions Category 1, tags: essay)" on quiz page "1"
|
||||
And I should see "Random (Questions Category 1, tags: hard)" on quiz page "2"
|
||||
And I click on "Configure question" "link" in the "Questions Category 1, tags: hard" "list_item"
|
||||
Then I should see "Random question" on quiz page "1"
|
||||
And I should see "Random question" on quiz page "2"
|
||||
And I click on "Configure question" "link" in the "2" "mod_quiz > Edit slot"
|
||||
And "hard" "autocomplete_selection" should be visible
|
||||
|
|
78
mod/quiz/tests/behat/quiz_question_versions.feature
Normal file
78
mod/quiz/tests/behat/quiz_question_versions.feature
Normal file
|
@ -0,0 +1,78 @@
|
|||
@mod @mod_quiz
|
||||
Feature: Quiz question versioning
|
||||
In order to manage question versions
|
||||
As a teacher
|
||||
I need to be able to choose which versions can be displayed in a quiz
|
||||
|
||||
Background:
|
||||
Given the following "courses" exist:
|
||||
| fullname | shortname | category | groupmode |
|
||||
| Course 1 | C1 | 0 | 1 |
|
||||
And the following "users" exist:
|
||||
| username | firstname | lastname | email |
|
||||
| teacher1 | Teacher | 1 | teacher1@example.com |
|
||||
And the following "course enrolments" exist:
|
||||
| user | course | role |
|
||||
| teacher1 | C1 | editingteacher |
|
||||
And the following "question categories" exist:
|
||||
| contextlevel | reference | name |
|
||||
| Course | C1 | Test questions |
|
||||
And the following "activities" exist:
|
||||
| activity | name | course | idnumber |
|
||||
| quiz | Quiz 1 | C1 | quiz1 |
|
||||
And the following "questions" exist:
|
||||
| questioncategory | qtype | name | questiontext | answer 1 |
|
||||
| Test questions | truefalse | First question | Answer the first question | True |
|
||||
And quiz "Quiz 1" contains the following questions:
|
||||
| question | page |
|
||||
| First question | 1 |
|
||||
And I log in as "teacher1"
|
||||
And I am on "Course 1" course homepage
|
||||
|
||||
@javascript
|
||||
Scenario: Approriate question version should be displayed when not edited
|
||||
When I am on the "Quiz 1" "mod_quiz > Edit" page logged in as "teacher1"
|
||||
And I should see "First question"
|
||||
And I should see "Answer the first question"
|
||||
And I should see "v1 (latest)"
|
||||
# We check that the corresponding version is the appropriate one in preview
|
||||
And I click on "Preview question" "link"
|
||||
And I switch to "questionpreview" window
|
||||
And I should see "Version 1 (latest)"
|
||||
And I should see "Answer the first question"
|
||||
And I press "Display options"
|
||||
And I set the following fields to these values:
|
||||
| id_feedback | Not shown |
|
||||
| id_generalfeedback | Not shown |
|
||||
| id_rightanswer | Shown |
|
||||
And I press "id_saveupdate"
|
||||
And I click on "finish" "button"
|
||||
And I should see "The correct answer is 'True'."
|
||||
|
||||
@javascript
|
||||
Scenario: Approriate question version should be displayed when edited
|
||||
When I am on the "Quiz 1" "mod_quiz > Edit" page logged in as "teacher1"
|
||||
And I click on "Edit question First question" "link"
|
||||
# We edit the question with new informations to generate a second version
|
||||
And I set the following fields to these values:
|
||||
| id_name | Second question |
|
||||
| id_questiontext | This is the second question text |
|
||||
| id_correctanswer | False |
|
||||
And I press "id_submitbutton"
|
||||
And I set the field "version" to "v2"
|
||||
And I should see "Second question"
|
||||
And I should see "This is the second question text"
|
||||
And I click on "Preview question" "link"
|
||||
And I switch to "questionpreview" window
|
||||
# We check that the corresponding version is the appropriate one in preview
|
||||
# We also check that the new informations are properly displayed
|
||||
And I should see "Version 2 (latest)"
|
||||
And I should see "This is the second question text"
|
||||
And I press "Display options"
|
||||
And I set the following fields to these values:
|
||||
| id_feedback | Not shown |
|
||||
| id_generalfeedback | Not shown |
|
||||
| id_rightanswer | Shown |
|
||||
And I press "id_saveupdate"
|
||||
And I click on "finish" "button"
|
||||
Then I should see "The correct answer is 'False'."
|
8
mod/quiz/tests/external/external_test.php
vendored
8
mod/quiz/tests/external/external_test.php
vendored
|
@ -1941,11 +1941,11 @@ class external_test extends externallib_advanced_testcase {
|
|||
$question = $questiongenerator->create_question('shortanswer', null, array('category' => $cat->id));
|
||||
quiz_add_quiz_question($question->id, $quiz);
|
||||
|
||||
// Add new question types in the category (for the random one).
|
||||
$question = $questiongenerator->create_question('truefalse', null, array('category' => $cat->id));
|
||||
$question = $questiongenerator->create_question('essay', null, array('category' => $cat->id));
|
||||
quiz_add_quiz_question($question->id, $quiz);
|
||||
|
||||
quiz_add_random_questions($quiz, 0, $cat->id, 1, false);
|
||||
$question = $questiongenerator->create_question('essay', null, array('category' => $cat->id));
|
||||
quiz_add_quiz_question($question->id, $quiz);
|
||||
|
||||
$this->setUser($this->student);
|
||||
|
||||
|
@ -1953,7 +1953,7 @@ class external_test extends externallib_advanced_testcase {
|
|||
$result = \external_api::clean_returnvalue(mod_quiz_external::get_quiz_required_qtypes_returns(), $result);
|
||||
|
||||
$expected = array(
|
||||
'questiontypes' => ['essay', 'numerical', 'random', 'shortanswer', 'truefalse'],
|
||||
'questiontypes' => ['essay', 'numerical', 'shortanswer', 'truefalse'],
|
||||
'warnings' => []
|
||||
);
|
||||
|
||||
|
|
|
@ -117,8 +117,10 @@ class lib_test extends \advanced_testcase {
|
|||
quiz_delete_instance($quiz->id);
|
||||
|
||||
// Check that the random question was deleted.
|
||||
if ($randomq) {
|
||||
$count = $DB->count_records('question', array('id' => $randomq->id));
|
||||
$this->assertEquals(0, $count);
|
||||
}
|
||||
// Check that the standard question was not deleted.
|
||||
$count = $DB->count_records('question', array('id' => $standardq->id));
|
||||
$this->assertEquals(1, $count);
|
||||
|
|
|
@ -57,30 +57,37 @@ class mod_quiz_local_structure_slot_random_test extends advanced_testcase {
|
|||
$form->includesubcategories = true;
|
||||
$form->fromtags = [];
|
||||
$form->defaultmark = 1;
|
||||
$form->hidden = 1;
|
||||
$form->status = \core_question\local\bank\question_version_status::QUESTION_STATUS_HIDDEN;
|
||||
$form->stamp = make_unique_id_code();
|
||||
$question = new stdClass();
|
||||
$question->qtype = 'random';
|
||||
$question = question_bank::get_qtype('random')->save_question($question, $form);
|
||||
|
||||
// Set the filter conditions.
|
||||
$filtercondition = new stdClass();
|
||||
$filtercondition->questioncategoryid = $category->id;
|
||||
$filtercondition->includingsubcategories = 1;
|
||||
|
||||
// Slot data.
|
||||
$randomslotdata = new stdClass();
|
||||
$randomslotdata->quizid = $quiz->id;
|
||||
$randomslotdata->questionid = $question->id;
|
||||
$randomslotdata->questioncategoryid = $category->id;
|
||||
$randomslotdata->includingsubcategories = 1;
|
||||
$randomslotdata->maxmark = 1;
|
||||
$randomslotdata->usingcontextid = context_module::instance($quiz->cmid)->id;
|
||||
$randomslotdata->questionscontextid = $category->contextid;
|
||||
|
||||
// Insert the random question to the quiz.
|
||||
$randomslot = new \mod_quiz\local\structure\slot_random($randomslotdata);
|
||||
$randomslot->set_filter_condition($filtercondition);
|
||||
|
||||
$rc = new ReflectionClass('\mod_quiz\local\structure\slot_random');
|
||||
$rcp = $rc->getProperty('filtercondition');
|
||||
$rcp->setAccessible(true);
|
||||
$record = json_decode($rcp->getValue($randomslot));
|
||||
|
||||
$this->assertEquals($quiz->id, $randomslot->get_quiz()->id);
|
||||
$this->assertEquals($category->id, $record->questioncategoryid);
|
||||
$this->assertEquals(1, $record->includingsubcategories);
|
||||
|
||||
$rcp = $rc->getProperty('record');
|
||||
$rcp->setAccessible(true);
|
||||
$record = $rcp->getValue($randomslot);
|
||||
|
||||
$this->assertEquals($quiz->id, $record->quizid);
|
||||
$this->assertEquals($question->id, $record->questionid);
|
||||
$this->assertEquals($category->id, $record->questioncategoryid);
|
||||
$this->assertEquals(1, $record->includingsubcategories);
|
||||
$this->assertEquals(1, $record->maxmark);
|
||||
}
|
||||
|
||||
|
@ -100,17 +107,20 @@ class mod_quiz_local_structure_slot_random_test extends advanced_testcase {
|
|||
|
||||
quiz_add_random_questions($quiz, 0, $category->id, 1, false);
|
||||
|
||||
// Get the random question's id. It is at the first slot.
|
||||
$questionid = $DB->get_field('quiz_slots', 'questionid', array('quizid' => $quiz->id, 'slot' => 1));
|
||||
// Set the filter conditions.
|
||||
$filtercondition = new stdClass();
|
||||
$filtercondition->questioncategoryid = $category->id;
|
||||
$filtercondition->includingsubcategories = 1;
|
||||
|
||||
// Slot data.
|
||||
$randomslotdata = new stdClass();
|
||||
$randomslotdata->quizid = $quiz->id;
|
||||
$randomslotdata->questionid = $questionid;
|
||||
$randomslotdata->questioncategoryid = $category->id;
|
||||
$randomslotdata->includingsubcategories = 1;
|
||||
$randomslotdata->maxmark = 1;
|
||||
$randomslotdata->usingcontextid = context_module::instance($quiz->cmid)->id;
|
||||
$randomslotdata->questionscontextid = $category->contextid;
|
||||
|
||||
$randomslot = new \mod_quiz\local\structure\slot_random($randomslotdata);
|
||||
$randomslot->set_filter_condition($filtercondition);
|
||||
|
||||
// The create_instance had injected an additional cmid propery to the quiz. Let's remove that.
|
||||
unset($quiz->cmid);
|
||||
|
@ -134,17 +144,20 @@ class mod_quiz_local_structure_slot_random_test extends advanced_testcase {
|
|||
|
||||
quiz_add_random_questions($quiz, 0, $category->id, 1, false);
|
||||
|
||||
// Get the random question's id. It is at the first slot.
|
||||
$questionid = $DB->get_field('quiz_slots', 'questionid', array('quizid' => $quiz->id, 'slot' => 1));
|
||||
// Set the filter conditions.
|
||||
$filtercondition = new stdClass();
|
||||
$filtercondition->questioncategoryid = $category->id;
|
||||
$filtercondition->includingsubcategories = 1;
|
||||
|
||||
// Slot data.
|
||||
$randomslotdata = new stdClass();
|
||||
$randomslotdata->quizid = $quiz->id;
|
||||
$randomslotdata->questionid = $questionid;
|
||||
$randomslotdata->questioncategoryid = $category->id;
|
||||
$randomslotdata->includingsubcategories = 1;
|
||||
$randomslotdata->maxmark = 1;
|
||||
$randomslotdata->usingcontextid = context_module::instance($quiz->cmid)->id;
|
||||
$randomslotdata->questionscontextid = $category->contextid;
|
||||
|
||||
$randomslot = new \mod_quiz\local\structure\slot_random($randomslotdata);
|
||||
$randomslot->set_filter_condition($filtercondition);
|
||||
|
||||
// The create_instance had injected an additional cmid propery to the quiz. Let's remove that.
|
||||
unset($quiz->cmid);
|
||||
|
@ -172,15 +185,12 @@ class mod_quiz_local_structure_slot_random_test extends advanced_testcase {
|
|||
|
||||
quiz_add_random_questions($quiz, 0, $category->id, 1, false);
|
||||
|
||||
// Get the random question's id. It is at the first slot.
|
||||
$questionid = $DB->get_field('quiz_slots', 'questionid', array('quizid' => $quiz->id, 'slot' => 1));
|
||||
|
||||
// Slot data.
|
||||
$randomslotdata = new stdClass();
|
||||
$randomslotdata->quizid = $quiz->id;
|
||||
$randomslotdata->questionid = $questionid;
|
||||
$randomslotdata->questioncategoryid = $category->id;
|
||||
$randomslotdata->includingsubcategories = 1;
|
||||
$randomslotdata->maxmark = 1;
|
||||
$randomslotdata->usingcontextid = context_module::instance($quiz->cmid)->id;
|
||||
$randomslotdata->questionscontextid = $category->contextid;
|
||||
|
||||
$randomslot = new \mod_quiz\local\structure\slot_random($randomslotdata);
|
||||
|
||||
|
@ -203,17 +213,19 @@ class mod_quiz_local_structure_slot_random_test extends advanced_testcase {
|
|||
$this->setAdminUser();
|
||||
|
||||
list($randomslot, $tags) = $this->setup_for_test_tags(['foo', 'bar']);
|
||||
$filtercondition = new stdClass();
|
||||
$randomslot->set_tags([$tags['foo'], $tags['bar']]);
|
||||
$randomslot->set_filter_condition($filtercondition);
|
||||
|
||||
$rc = new ReflectionClass('\mod_quiz\local\structure\slot_random');
|
||||
$rcp = $rc->getProperty('tags');
|
||||
$rcp = $rc->getProperty('filtercondition');
|
||||
$rcp->setAccessible(true);
|
||||
$tagspropery = $rcp->getValue($randomslot);
|
||||
|
||||
$this->assertEquals([
|
||||
$tags['foo']->id => $tags['foo'],
|
||||
$tags['bar']->id => $tags['bar'],
|
||||
], $tagspropery);
|
||||
], (array)json_decode($tagspropery)->tags);
|
||||
}
|
||||
|
||||
public function test_set_tags_twice() {
|
||||
|
@ -223,18 +235,20 @@ class mod_quiz_local_structure_slot_random_test extends advanced_testcase {
|
|||
list($randomslot, $tags) = $this->setup_for_test_tags(['foo', 'bar', 'baz']);
|
||||
|
||||
// Set tags for the first time.
|
||||
$filtercondition = new stdClass();
|
||||
$randomslot->set_tags([$tags['foo'], $tags['bar']]);
|
||||
// Now set the tags again.
|
||||
$randomslot->set_tags([$tags['baz']]);
|
||||
$randomslot->set_filter_condition($filtercondition);
|
||||
|
||||
$rc = new ReflectionClass('\mod_quiz\local\structure\slot_random');
|
||||
$rcp = $rc->getProperty('tags');
|
||||
$rcp = $rc->getProperty('filtercondition');
|
||||
$rcp->setAccessible(true);
|
||||
$tagspropery = $rcp->getValue($randomslot);
|
||||
|
||||
$this->assertEquals([
|
||||
$tags['baz']->id => $tags['baz'],
|
||||
], $tagspropery);
|
||||
], (array)json_decode($tagspropery)->tags);
|
||||
}
|
||||
|
||||
public function test_set_tags_duplicates() {
|
||||
|
@ -242,18 +256,19 @@ class mod_quiz_local_structure_slot_random_test extends advanced_testcase {
|
|||
$this->setAdminUser();
|
||||
|
||||
list($randomslot, $tags) = $this->setup_for_test_tags(['foo', 'bar', 'baz']);
|
||||
|
||||
$filtercondition = new stdClass();
|
||||
$randomslot->set_tags([$tags['foo'], $tags['bar'], $tags['foo']]);
|
||||
$randomslot->set_filter_condition($filtercondition);
|
||||
|
||||
$rc = new ReflectionClass('\mod_quiz\local\structure\slot_random');
|
||||
$rcp = $rc->getProperty('tags');
|
||||
$rcp = $rc->getProperty('filtercondition');
|
||||
$rcp->setAccessible(true);
|
||||
$tagspropery = $rcp->getValue($randomslot);
|
||||
|
||||
$this->assertEquals([
|
||||
$tags['foo']->id => $tags['foo'],
|
||||
$tags['bar']->id => $tags['bar'],
|
||||
], $tagspropery);
|
||||
], (array)json_decode($tagspropery)->tags);
|
||||
}
|
||||
|
||||
public function test_set_tags_by_id() {
|
||||
|
@ -261,8 +276,9 @@ class mod_quiz_local_structure_slot_random_test extends advanced_testcase {
|
|||
$this->setAdminUser();
|
||||
|
||||
list($randomslot, $tags) = $this->setup_for_test_tags(['foo', 'bar', 'baz']);
|
||||
|
||||
$filtercondition = new stdClass();
|
||||
$randomslot->set_tags_by_id([$tags['foo']->id, $tags['bar']->id]);
|
||||
$randomslot->set_filter_condition($filtercondition);
|
||||
|
||||
$rc = new ReflectionClass('\mod_quiz\local\structure\slot_random');
|
||||
$rcp = $rc->getProperty('tags');
|
||||
|
@ -355,11 +371,8 @@ class mod_quiz_local_structure_slot_random_test extends advanced_testcase {
|
|||
$form->includesubcategories = true;
|
||||
$form->fromtags = [];
|
||||
$form->defaultmark = 1;
|
||||
$form->hidden = 1;
|
||||
$form->status = \core_question\local\bank\question_version_status::QUESTION_STATUS_HIDDEN;
|
||||
$form->stamp = make_unique_id_code();
|
||||
$question = new stdClass();
|
||||
$question->qtype = 'random';
|
||||
$question = question_bank::get_qtype('random')->save_question($question, $form);
|
||||
|
||||
// Prepare 2 tags.
|
||||
$tagrecord = array(
|
||||
|
@ -377,36 +390,43 @@ class mod_quiz_local_structure_slot_random_test extends advanced_testcase {
|
|||
);
|
||||
$bartag = $this->getDataGenerator()->create_tag($tagrecord);
|
||||
|
||||
|
||||
// Set the filter conditions.
|
||||
$filtercondition = new stdClass();
|
||||
$filtercondition->questioncategoryid = $category->id;
|
||||
$filtercondition->includingsubcategories = 1;
|
||||
|
||||
// Slot data.
|
||||
$randomslotdata = new stdClass();
|
||||
$randomslotdata->quizid = $quiz->id;
|
||||
$randomslotdata->questionid = $question->id;
|
||||
$randomslotdata->questioncategoryid = $category->id;
|
||||
$randomslotdata->includingsubcategories = 1;
|
||||
$randomslotdata->maxmark = 1;
|
||||
$randomslotdata->usingcontextid = context_module::instance($quiz->cmid)->id;
|
||||
$randomslotdata->questionscontextid = $category->contextid;
|
||||
|
||||
// Insert the random question to the quiz.
|
||||
$randomslot = new \mod_quiz\local\structure\slot_random($randomslotdata);
|
||||
$randomslot->set_tags([$footag, $bartag]);
|
||||
$randomslot->set_filter_condition($filtercondition);
|
||||
$randomslot->insert(1); // Put the question on the first page of the quiz.
|
||||
|
||||
// Get the random question's quiz_slot. It is at the first slot.
|
||||
$quizslot = $DB->get_record('quiz_slots', array('quizid' => $quiz->id, 'slot' => 1));
|
||||
// Get the random question's tags from quiz_slot_tags. It is at the first slot.
|
||||
$quizslottags = $DB->get_records('quiz_slot_tags', array('slotid' => $quizslot->id));
|
||||
$setreference = \mod_quiz\question\bank\qbank_helper::get_random_question_data_from_slot($quizslot->id);
|
||||
|
||||
$this->assertEquals($question->id, $quizslot->questionid);
|
||||
$this->assertEquals($category->id, $quizslot->questioncategoryid);
|
||||
$this->assertEquals(1, $quizslot->includingsubcategories);
|
||||
$this->assertEquals($category->id, json_decode($setreference->filtercondition)->questioncategoryid);
|
||||
$this->assertEquals(1, json_decode($setreference->filtercondition)->includingsubcategories);
|
||||
$this->assertEquals(1, $quizslot->maxmark);
|
||||
$tagspropery = (array)json_decode($setreference->filtercondition)->tags;
|
||||
|
||||
$this->assertCount(2, $quizslottags);
|
||||
$this->assertCount(2, $tagspropery);
|
||||
$this->assertEqualsCanonicalizing(
|
||||
[
|
||||
['tagid' => $footag->id, 'tagname' => $footag->name],
|
||||
['tagid' => $bartag->id, 'tagname' => $bartag->name]
|
||||
],
|
||||
array_map(function($slottag) {
|
||||
return ['tagid' => $slottag->tagid, 'tagname' => $slottag->tagname];
|
||||
}, $quizslottags));
|
||||
return ['tagid' => $slottag->id, 'tagname' => $slottag->name];
|
||||
}, $tagspropery));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -521,15 +521,13 @@ class locallib_test extends \advanced_testcase {
|
|||
// Get the random question's slotid. It is at the second slot.
|
||||
$slotid = $DB->get_field('quiz_slots', 'id', array('quizid' => $quiz->id, 'slot' => 2));
|
||||
$slottags = quiz_retrieve_slot_tags($slotid);
|
||||
sort($slottags);
|
||||
|
||||
$this->assertEqualsCanonicalizing(
|
||||
$this->assertEquals(
|
||||
[
|
||||
['tagid' => $tags['foo']->id, 'tagname' => $tags['foo']->name],
|
||||
['tagid' => $tags['bar']->id, 'tagname' => $tags['bar']->name]
|
||||
],
|
||||
array_map(function($slottag) {
|
||||
return ['tagid' => $slottag->tagid, 'tagname' => $slottag->tagname];
|
||||
}, $slottags));
|
||||
"{$tags['foo']->id},{$tags['foo']->name}",
|
||||
"{$tags['bar']->id},{$tags['bar']->name}",
|
||||
], $slottags);
|
||||
}
|
||||
|
||||
public function test_quiz_retrieve_slot_tags_with_removed_tag() {
|
||||
|
@ -547,15 +545,15 @@ class locallib_test extends \advanced_testcase {
|
|||
// Now remove the foo tag and check again.
|
||||
\core_tag_tag::delete_tags([$tags['foo']->id]);
|
||||
$slottags = quiz_retrieve_slot_tags($slotid);
|
||||
sort($slottags);
|
||||
|
||||
$this->assertEqualsCanonicalizing(
|
||||
$this->assertEquals(
|
||||
[
|
||||
['tagid' => null, 'tagname' => $tags['foo']->name],
|
||||
['tagid' => $tags['bar']->id, 'tagname' => $tags['bar']->name]
|
||||
"{$tags['foo']->id},{$tags['foo']->name}",
|
||||
"{$tags['bar']->id},{$tags['bar']->name}",
|
||||
|
||||
],
|
||||
array_map(function($slottag) {
|
||||
return ['tagid' => $slottag->tagid, 'tagname' => $slottag->tagname];
|
||||
}, $slottags));
|
||||
$slottags);
|
||||
}
|
||||
|
||||
public function test_quiz_retrieve_slot_tags_for_standard_question() {
|
||||
|
@ -603,246 +601,6 @@ class locallib_test extends \advanced_testcase {
|
|||
$this->assertEqualsCanonicalizing([], $tagids);
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for the get_random_question_summaries test.
|
||||
*/
|
||||
public function get_quiz_retrieve_tags_for_slot_ids_test_cases() {
|
||||
return [
|
||||
'no questions' => [
|
||||
'questioncount' => 0,
|
||||
'randomquestioncount' => 0,
|
||||
'randomquestiontags' => [],
|
||||
'unusedtags' => [],
|
||||
'removeslottagids' => [],
|
||||
'expected' => []
|
||||
],
|
||||
'only regular questions' => [
|
||||
'questioncount' => 2,
|
||||
'randomquestioncount' => 0,
|
||||
'randomquestiontags' => [],
|
||||
'unusedtags' => ['unused1', 'unused2'],
|
||||
'removeslottagids' => [],
|
||||
'expected' => [
|
||||
1 => [],
|
||||
2 => []
|
||||
]
|
||||
],
|
||||
'only random questions 1' => [
|
||||
'questioncount' => 0,
|
||||
'randomquestioncount' => 2,
|
||||
'randomquestiontags' => [
|
||||
0 => ['foo'],
|
||||
1 => []
|
||||
],
|
||||
'unusedtags' => ['unused1', 'unused2'],
|
||||
'removeslottagids' => [],
|
||||
'expected' => [
|
||||
1 => ['foo'],
|
||||
2 => []
|
||||
]
|
||||
],
|
||||
'only random questions 2' => [
|
||||
'questioncount' => 0,
|
||||
'randomquestioncount' => 2,
|
||||
'randomquestiontags' => [
|
||||
0 => ['foo', 'bop'],
|
||||
1 => ['bar']
|
||||
],
|
||||
'unusedtags' => ['unused1', 'unused2'],
|
||||
'removeslottagids' => [],
|
||||
'expected' => [
|
||||
1 => ['foo', 'bop'],
|
||||
2 => ['bar']
|
||||
]
|
||||
],
|
||||
'only random questions 3' => [
|
||||
'questioncount' => 0,
|
||||
'randomquestioncount' => 2,
|
||||
'randomquestiontags' => [
|
||||
0 => ['foo', 'bop'],
|
||||
1 => ['bar', 'foo']
|
||||
],
|
||||
'unusedtags' => ['unused1', 'unused2'],
|
||||
'removeslottagids' => [],
|
||||
'expected' => [
|
||||
1 => ['foo', 'bop'],
|
||||
2 => ['bar', 'foo']
|
||||
]
|
||||
],
|
||||
'combination of questions 1' => [
|
||||
'questioncount' => 2,
|
||||
'randomquestioncount' => 2,
|
||||
'randomquestiontags' => [
|
||||
0 => ['foo'],
|
||||
1 => []
|
||||
],
|
||||
'unusedtags' => ['unused1', 'unused2'],
|
||||
'removeslottagids' => [],
|
||||
'expected' => [
|
||||
1 => [],
|
||||
2 => [],
|
||||
3 => ['foo'],
|
||||
4 => []
|
||||
]
|
||||
],
|
||||
'combination of questions 2' => [
|
||||
'questioncount' => 2,
|
||||
'randomquestioncount' => 2,
|
||||
'randomquestiontags' => [
|
||||
0 => ['foo', 'bop'],
|
||||
1 => ['bar']
|
||||
],
|
||||
'unusedtags' => ['unused1', 'unused2'],
|
||||
'removeslottagids' => [],
|
||||
'expected' => [
|
||||
1 => [],
|
||||
2 => [],
|
||||
3 => ['foo', 'bop'],
|
||||
4 => ['bar']
|
||||
]
|
||||
],
|
||||
'combination of questions 3' => [
|
||||
'questioncount' => 2,
|
||||
'randomquestioncount' => 2,
|
||||
'randomquestiontags' => [
|
||||
0 => ['foo', 'bop'],
|
||||
1 => ['bar', 'foo']
|
||||
],
|
||||
'unusedtags' => ['unused1', 'unused2'],
|
||||
'removeslottagids' => [],
|
||||
'expected' => [
|
||||
1 => [],
|
||||
2 => [],
|
||||
3 => ['foo', 'bop'],
|
||||
4 => ['bar', 'foo']
|
||||
]
|
||||
],
|
||||
'load from name 1' => [
|
||||
'questioncount' => 2,
|
||||
'randomquestioncount' => 2,
|
||||
'randomquestiontags' => [
|
||||
0 => ['foo'],
|
||||
1 => []
|
||||
],
|
||||
'unusedtags' => ['unused1', 'unused2'],
|
||||
'removeslottagids' => [3],
|
||||
'expected' => [
|
||||
1 => [],
|
||||
2 => [],
|
||||
3 => ['foo'],
|
||||
4 => []
|
||||
]
|
||||
],
|
||||
'load from name 2' => [
|
||||
'questioncount' => 2,
|
||||
'randomquestioncount' => 2,
|
||||
'randomquestiontags' => [
|
||||
0 => ['foo', 'bop'],
|
||||
1 => ['bar']
|
||||
],
|
||||
'unusedtags' => ['unused1', 'unused2'],
|
||||
'removeslottagids' => [3],
|
||||
'expected' => [
|
||||
1 => [],
|
||||
2 => [],
|
||||
3 => ['foo', 'bop'],
|
||||
4 => ['bar']
|
||||
]
|
||||
],
|
||||
'load from name 3' => [
|
||||
'questioncount' => 2,
|
||||
'randomquestioncount' => 2,
|
||||
'randomquestiontags' => [
|
||||
0 => ['foo', 'bop'],
|
||||
1 => ['bar', 'foo']
|
||||
],
|
||||
'unusedtags' => ['unused1', 'unused2'],
|
||||
'removeslottagids' => [3],
|
||||
'expected' => [
|
||||
1 => [],
|
||||
2 => [],
|
||||
3 => ['foo', 'bop'],
|
||||
4 => ['bar', 'foo']
|
||||
]
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the quiz_retrieve_tags_for_slot_ids function with various parameter
|
||||
* combinations.
|
||||
*
|
||||
* @dataProvider get_quiz_retrieve_tags_for_slot_ids_test_cases()
|
||||
* @param int $questioncount The number of regular questions to create
|
||||
* @param int $randomquestioncount The number of random questions to create
|
||||
* @param array $randomquestiontags The tags for the random questions
|
||||
* @param string[] $unusedtags Additional tags to create to populate the DB with data
|
||||
* @param int[] $removeslottagids Slot numbers to remove tag ids for
|
||||
* @param array $expected The expected output of tag names indexed by slot number
|
||||
*/
|
||||
public function test_quiz_retrieve_tags_for_slot_ids_combinations(
|
||||
$questioncount,
|
||||
$randomquestioncount,
|
||||
$randomquestiontags,
|
||||
$unusedtags,
|
||||
$removeslottagids,
|
||||
$expected
|
||||
) {
|
||||
global $DB;
|
||||
|
||||
$this->resetAfterTest();
|
||||
$this->setAdminUser();
|
||||
|
||||
list($quiz, $tags) = $this->setup_quiz_and_tags(
|
||||
$questioncount,
|
||||
$randomquestioncount,
|
||||
$randomquestiontags,
|
||||
$unusedtags
|
||||
);
|
||||
|
||||
$slots = $DB->get_records('quiz_slots', ['quizid' => $quiz->id]);
|
||||
$slotids = [];
|
||||
$slotsbynumber = [];
|
||||
foreach ($slots as $slot) {
|
||||
$slotids[] = $slot->id;
|
||||
$slotsbynumber[$slot->slot] = $slot;
|
||||
}
|
||||
|
||||
if (!empty($removeslottagids)) {
|
||||
// The slots to remove are the slot numbers not the slot id so we need
|
||||
// to get the ids for the DB call.
|
||||
$idstonull = array_map(function($slot) use ($slotsbynumber) {
|
||||
return $slotsbynumber[$slot]->id;
|
||||
}, $removeslottagids);
|
||||
list($sql, $params) = $DB->get_in_or_equal($idstonull);
|
||||
// Null out the tagid column to force the code to look up the tag by name.
|
||||
$DB->set_field_select('quiz_slot_tags', 'tagid', null, "slotid {$sql}", $params);
|
||||
}
|
||||
|
||||
$slottagsbyslotids = quiz_retrieve_tags_for_slot_ids($slotids);
|
||||
// Convert the result into an associative array of slotid => [... tag names..]
|
||||
// to make it easier to compare.
|
||||
$actual = array_map(function($slottags) {
|
||||
$names = array_map(function($slottag) {
|
||||
return $slottag->tagname;
|
||||
}, $slottags);
|
||||
// Make sure the names are sorted for comparison.
|
||||
sort($names);
|
||||
return $names;
|
||||
}, $slottagsbyslotids);
|
||||
|
||||
$formattedexptected = [];
|
||||
// The expected values are indexed by slot number rather than id so let
|
||||
// convert it to use the id so that we can compare the results.
|
||||
foreach ($expected as $slot => $tagnames) {
|
||||
sort($tagnames);
|
||||
$slotid = $slotsbynumber[$slot]->id;
|
||||
$formattedexptected[$slotid] = $tagnames;
|
||||
}
|
||||
|
||||
$this->assertEquals($formattedexptected, $actual);
|
||||
}
|
||||
|
||||
public function test_quiz_override_summary() {
|
||||
global $DB, $PAGE;
|
||||
$this->resetAfterTest();
|
||||
|
|
225
mod/quiz/tests/qbank_helper_test.php
Normal file
225
mod/quiz/tests/qbank_helper_test.php
Normal file
|
@ -0,0 +1,225 @@
|
|||
<?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;
|
||||
|
||||
use mod_quiz\external\submit_question_version;
|
||||
use mod_quiz\question\bank\qbank_helper;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
require_once(__DIR__ . '/quiz_question_helper_test_trait.php');
|
||||
|
||||
/**
|
||||
* Qbank helper test for quiz.
|
||||
*
|
||||
* @package mod_quiz
|
||||
* @category test
|
||||
* @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
|
||||
* @coversDefaultClass \mod_quiz\question\bank\qbank_helper
|
||||
*/
|
||||
class qbank_helper_test extends \advanced_testcase {
|
||||
use \quiz_question_helper_test_trait;
|
||||
|
||||
/**
|
||||
* Called before every test.
|
||||
*/
|
||||
public function setUp(): void {
|
||||
global $USER;
|
||||
parent::setUp();
|
||||
$this->setAdminUser();
|
||||
$this->course = $this->getDataGenerator()->create_course();
|
||||
$this->student = $this->getDataGenerator()->create_user();
|
||||
$this->user = $USER;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test is random.
|
||||
*
|
||||
* @covers ::is_random
|
||||
* @covers ::get_random_question_data_from_slot
|
||||
*/
|
||||
public function test_is_random() {
|
||||
$this->resetAfterTest();
|
||||
$quiz = $this->create_test_quiz($this->course);
|
||||
// Test for questions from a different context.
|
||||
$context = \context_module::instance(get_coursemodule_from_instance("quiz", $quiz->id, $this->course->id)->id);
|
||||
$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
|
||||
$this->add_random_questions($questiongenerator, $quiz, ['contextid' => $context->id]);
|
||||
// Create the quiz object.
|
||||
$quizobj = \quiz::create($quiz->id);
|
||||
$structure = \mod_quiz\structure::create_for_quiz($quizobj);
|
||||
$slots = $structure->get_slots();
|
||||
foreach ($slots as $slot) {
|
||||
$this->assertEquals(true, qbank_helper::is_random($slot->id));
|
||||
// Test random data for slot.
|
||||
$this->assertEquals($slot->id, qbank_helper::get_random_question_data_from_slot($slot->id)->itemid);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Test reference records.
|
||||
*
|
||||
* @covers ::get_version_options
|
||||
* @covers ::get_question_for_redo
|
||||
* @covers ::get_always_latest_version_question_ids
|
||||
* @covers ::question_load_random_questions
|
||||
* @covers ::question_array_sort
|
||||
*/
|
||||
public function test_reference_records() {
|
||||
$this->resetAfterTest();
|
||||
$quiz = $this->create_test_quiz($this->course);
|
||||
// Test for questions from a different context.
|
||||
$context = \context_module::instance(get_coursemodule_from_instance("quiz", $quiz->id, $this->course->id)->id);
|
||||
// Create a couple of questions.
|
||||
$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
|
||||
$cat = $questiongenerator->create_question_category(['contextid' => $context->id]);
|
||||
$numq = $questiongenerator->create_question('essay', null,
|
||||
['category' => $cat->id, 'name' => 'This is the first version']);
|
||||
// Create two version.
|
||||
$questiongenerator->update_question($numq, null, ['name' => 'This is the second version']);
|
||||
$questiongenerator->update_question($numq, null, ['name' => 'This is the third version']);
|
||||
quiz_add_quiz_question($numq->id, $quiz);
|
||||
// Create the quiz object.
|
||||
$quizobj = \quiz::create($quiz->id);
|
||||
$quizobj->preload_questions();
|
||||
$quizobj->load_questions();
|
||||
$questions = $quizobj->get_questions();
|
||||
$question = reset($questions);
|
||||
$structure = \mod_quiz\structure::create_for_quiz($quizobj);
|
||||
$slots = $structure->get_slots();
|
||||
$slot = reset($slots);
|
||||
$this->assertEquals(3, count(qbank_helper::get_version_options($question->id)));
|
||||
$quizobj->preload_questions();
|
||||
$quizobj->load_questions();
|
||||
$questions = $quizobj->get_questions();
|
||||
$question = reset($questions);
|
||||
$this->assertEquals($question->id, qbank_helper::get_question_for_redo($slot->id));
|
||||
// Create another version.
|
||||
$questiongenerator->update_question($numq, null, ['name' => 'This is the latest version']);
|
||||
// Change to always latest.
|
||||
submit_question_version::execute($slot->id, 0);
|
||||
$quizobj->preload_questions();
|
||||
$quizobj->load_questions();
|
||||
$questions = $quizobj->get_questions();
|
||||
$question = reset($questions);
|
||||
$this->assertEquals($question->id, qbank_helper::get_question_for_redo($slot->id));
|
||||
// Test always latest version question ids.
|
||||
$latestquestionids = qbank_helper::get_always_latest_version_question_ids($quiz->id);
|
||||
$this->assertEquals($question->id, reset($latestquestionids));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test question structure data.
|
||||
*
|
||||
* @covers ::get_question_structure
|
||||
* @covers ::get_question_structure_data
|
||||
* @covers ::question_array_sort
|
||||
* @covers ::get_always_latest_version_question_ids
|
||||
* @covers ::question_load_random_questions
|
||||
*/
|
||||
public function test_get_question_structure() {
|
||||
$this->resetAfterTest();
|
||||
$quiz = $this->create_test_quiz($this->course);
|
||||
// Test for questions from a different context.
|
||||
$context = \context_module::instance(get_coursemodule_from_instance("quiz", $quiz->id, $this->course->id)->id);
|
||||
// Create a couple of questions.
|
||||
$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
|
||||
$cat = $questiongenerator->create_question_category(['contextid' => $context->id]);
|
||||
$numq = $questiongenerator->create_question('essay', null,
|
||||
['category' => $cat->id, 'name' => 'This is the first version']);
|
||||
// Create two version.
|
||||
$questiongenerator->update_question($numq, null, ['name' => 'This is the second version']);
|
||||
$questiongenerator->update_question($numq, null, ['name' => 'This is the third version']);
|
||||
quiz_add_quiz_question($numq->id, $quiz);
|
||||
// Create the quiz object.
|
||||
$quizobj = \quiz::create($quiz->id);
|
||||
$quizobj->preload_questions();
|
||||
$quizobj->load_questions();
|
||||
$questions = $quizobj->get_questions();
|
||||
$question = reset($questions);
|
||||
$structure = \mod_quiz\structure::create_for_quiz($quizobj);
|
||||
$slots = $structure->get_slots();
|
||||
$slot = reset($slots);
|
||||
$structuredatas = qbank_helper::get_question_structure($quiz->id);
|
||||
$structuredata = reset($structuredatas);
|
||||
$this->assertEquals($structuredata->slotid, $slot->id);
|
||||
$this->assertEquals($structuredata->id, $question->id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test to get the version information for a question to show in the version selection dropdown.
|
||||
*
|
||||
* @covers ::get_question_version_info
|
||||
*/
|
||||
public function test_get_question_version_info() {
|
||||
$this->resetAfterTest();
|
||||
$quiz = $this->create_test_quiz($this->course);
|
||||
// Test for questions from a different context.
|
||||
$context = \context_module::instance(get_coursemodule_from_instance("quiz", $quiz->id, $this->course->id)->id);
|
||||
// Create a couple of questions.
|
||||
$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
|
||||
$cat = $questiongenerator->create_question_category(['contextid' => $context->id]);
|
||||
$numq = $questiongenerator->create_question('essay', null,
|
||||
['category' => $cat->id, 'name' => 'This is the first version']);
|
||||
// Create two version.
|
||||
$questiongenerator->update_question($numq, null, ['name' => 'This is the second version']);
|
||||
$questiongenerator->update_question($numq, null, ['name' => 'This is the third version']);
|
||||
quiz_add_quiz_question($numq->id, $quiz);
|
||||
// Create the quiz object.
|
||||
$quizobj = \quiz::create($quiz->id);
|
||||
$quizobj->preload_questions();
|
||||
$quizobj->load_questions();
|
||||
$questions = $quizobj->get_questions();
|
||||
$question = reset($questions);
|
||||
$structure = \mod_quiz\structure::create_for_quiz($quizobj);
|
||||
$slots = $structure->get_slots();
|
||||
$slot = reset($slots);
|
||||
$versiondata = qbank_helper::get_question_version_info($question->id, $slot->id);
|
||||
$this->assertEquals(4, count($versiondata));
|
||||
$this->assertEquals('Always latest', $versiondata[0]->versionvalue);
|
||||
$this->assertEquals('v3 (latest)', $versiondata[1]->versionvalue);
|
||||
$this->assertEquals('v1', $versiondata[3]->versionvalue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test get the question ids for specific question version.
|
||||
*
|
||||
* @covers ::get_specific_version_question_ids
|
||||
*/
|
||||
public function test_get_specific_version_question_ids() {
|
||||
$this->resetAfterTest();
|
||||
$quiz = $this->create_test_quiz($this->course);
|
||||
// Test for questions from a different context.
|
||||
$context = \context_module::instance(get_coursemodule_from_instance("quiz", $quiz->id, $this->course->id)->id);
|
||||
// Create a couple of questions.
|
||||
$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
|
||||
$cat = $questiongenerator->create_question_category(['contextid' => $context->id]);
|
||||
$numq = $questiongenerator->create_question('essay', null,
|
||||
['category' => $cat->id, 'name' => 'This is the first version']);
|
||||
// Create two version.
|
||||
$questiongenerator->update_question($numq, null, ['name' => 'This is the second version']);
|
||||
$questiongenerator->update_question($numq, null, ['name' => 'This is the third version']);
|
||||
quiz_add_quiz_question($numq->id, $quiz);
|
||||
$specificversionquestionid = qbank_helper::get_specific_version_question_ids($quiz->id);
|
||||
$specificversionquestionid = reset($specificversionquestionid);
|
||||
$this->assertEquals($numq->id, $specificversionquestionid);
|
||||
}
|
||||
|
||||
}
|
|
@ -51,7 +51,7 @@ class quiz_question_bank_view_testcase extends advanced_testcase {
|
|||
$cm = get_coursemodule_from_instance('quiz', $quiz->id);
|
||||
|
||||
// Create a question in the default category.
|
||||
$contexts = new question_edit_contexts($context);
|
||||
$contexts = new core_question\local\bank\question_edit_contexts($context);
|
||||
$cat = question_make_default_categories($contexts->all());
|
||||
$questiondata = $questiongenerator->create_question('numerical', null,
|
||||
['name' => 'Example question', 'category' => $cat->id]);
|
||||
|
|
182
mod/quiz/tests/quiz_question_helper_test_trait.php
Normal file
182
mod/quiz/tests/quiz_question_helper_test_trait.php
Normal file
|
@ -0,0 +1,182 @@
|
|||
<?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/>.
|
||||
|
||||
/**
|
||||
* Helper trait for quiz question unit tests.
|
||||
*
|
||||
* This trait helps to execute different tests for quiz, for example if it needs to create a quiz, add question
|
||||
* to the question, add random quetion to the quiz, do a backup or restore.
|
||||
*
|
||||
* @package mod_quiz
|
||||
* @category test
|
||||
* @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
|
||||
*/
|
||||
trait quiz_question_helper_test_trait {
|
||||
|
||||
/** @var \stdClass $course Test course to contain quiz. */
|
||||
protected $course;
|
||||
|
||||
/** @var \stdClass $quiz A test quiz. */
|
||||
protected $quiz;
|
||||
|
||||
/** @var \stdClass $user A test logged-in user. */
|
||||
protected $user;
|
||||
|
||||
/**
|
||||
* Create a test quiz for the specified course.
|
||||
*
|
||||
* @param \stdClass $course
|
||||
* @return \stdClass
|
||||
*/
|
||||
protected function create_test_quiz(\stdClass $course): \stdClass {
|
||||
|
||||
$quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
|
||||
|
||||
$quiz = $quizgenerator->create_instance([
|
||||
'course' => $course->id,
|
||||
'questionsperpage' => 0,
|
||||
'grade' => 100.0,
|
||||
'sumgrades' => 2,
|
||||
]);
|
||||
$quiz->coursemodule = $quiz->cmid;
|
||||
return $quiz;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to add regular questions in quiz.
|
||||
*
|
||||
* @param component_generator_base $questiongenerator
|
||||
* @param \stdClass $quiz
|
||||
* @param array $override
|
||||
*/
|
||||
protected function add_regular_questions($questiongenerator, \stdClass $quiz, $override = null): void {
|
||||
// Create a couple of questions.
|
||||
$cat = $questiongenerator->create_question_category($override);
|
||||
|
||||
$saq = $questiongenerator->create_question('shortanswer', null, array('category' => $cat->id));
|
||||
// Create another version.
|
||||
$questiongenerator->update_question($saq);
|
||||
quiz_add_quiz_question($saq->id, $quiz);
|
||||
$numq = $questiongenerator->create_question('numerical', null, array('category' => $cat->id));
|
||||
// Create two version.
|
||||
$questiongenerator->update_question($numq);
|
||||
$questiongenerator->update_question($numq);
|
||||
quiz_add_quiz_question($numq->id, $quiz);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to add random question to quiz.
|
||||
*
|
||||
* @param component_generator_base $questiongenerator
|
||||
* @param \stdClass $quiz
|
||||
* @param array $override
|
||||
*/
|
||||
protected function add_random_questions($questiongenerator, \stdClass $quiz, $override = []): void {
|
||||
// Create a random question.
|
||||
$cat = $questiongenerator->create_question_category($override);
|
||||
$questiongenerator->create_question('truefalse', null, array('category' => $cat->id));
|
||||
$questiongenerator->create_question('essay', null, array('category' => $cat->id));
|
||||
quiz_add_random_questions($quiz, 0, $cat->id, 1, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt questions for a quiz and user.
|
||||
*
|
||||
* @param \stdClass $quiz Quiz to attempt.
|
||||
* @param \stdClass $user A user to attempt the quiz.
|
||||
* @param int $attemptnumber
|
||||
* @return array
|
||||
*/
|
||||
protected function attempt_quiz(\stdClass $quiz, \stdClass $user, $attemptnumber = 1): array {
|
||||
$this->setUser($user);
|
||||
|
||||
$starttime = time();
|
||||
$quizobj = \quiz::create($quiz->id, $user->id);
|
||||
|
||||
$quba = \question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context());
|
||||
$quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour);
|
||||
|
||||
// Start the attempt.
|
||||
$attempt = quiz_create_attempt($quizobj, $attemptnumber, false, $starttime, false, $user->id);
|
||||
quiz_start_new_attempt($quizobj, $quba, $attempt, $attemptnumber, $starttime);
|
||||
quiz_attempt_save_started($quizobj, $quba, $attempt);
|
||||
|
||||
// Finish the attempt.
|
||||
$attemptobj = \quiz_attempt::create($attempt->id);
|
||||
$attemptobj->process_finish($starttime, false);
|
||||
|
||||
$this->setUser();
|
||||
return [$quizobj, $quba, $attemptobj];
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper method to backup test quiz.
|
||||
*
|
||||
* @param \stdClass $quiz Quiz to attempt.
|
||||
* @param \stdClass $user A user to attempt the quiz.
|
||||
* @return string A backup ID ready to be restored.
|
||||
*/
|
||||
protected function backup_quiz(\stdClass $quiz, \stdClass $user): string {
|
||||
global $CFG;
|
||||
|
||||
// Get the necessary files to perform backup and restore.
|
||||
require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php');
|
||||
require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php');
|
||||
|
||||
$backupid = 'test-question-backup-restore';
|
||||
|
||||
$bc = new backup_controller(backup::TYPE_1ACTIVITY, $quiz->coursemodule, backup::FORMAT_MOODLE,
|
||||
backup::INTERACTIVE_NO, backup::MODE_GENERAL, $user->id);
|
||||
$bc->execute_plan();
|
||||
|
||||
$results = $bc->get_results();
|
||||
$file = $results['backup_destination'];
|
||||
$fp = get_file_packer('application/vnd.moodle.backup');
|
||||
$filepath = $CFG->dataroot . '/temp/backup/' . $backupid;
|
||||
$file->extract_to_pathname($fp, $filepath);
|
||||
$bc->destroy();
|
||||
|
||||
return $backupid;
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper method to restore provided backup.
|
||||
*
|
||||
* @param string $backupid Backup ID to restore.
|
||||
* @param stdClass $course
|
||||
* @param stdClass $user
|
||||
*/
|
||||
protected function restore_quiz(string $backupid, stdClass $course, stdClass $user): void {
|
||||
$rc = new restore_controller($backupid, $course->id,
|
||||
backup::INTERACTIVE_NO, backup::MODE_GENERAL, $user->id, backup::TARGET_CURRENT_ADDING);
|
||||
$this->assertTrue($rc->execute_precheck());
|
||||
$rc->execute_plan();
|
||||
$rc->destroy();
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper method to emulate duplication of the quiz.
|
||||
*
|
||||
* @param stdClass $course
|
||||
* @param stdClass $quiz
|
||||
* @return \cm_info|null
|
||||
*/
|
||||
protected function duplicate_quiz($course, $quiz): ?\cm_info {
|
||||
return duplicate_module($course, get_fast_modinfo($course)->get_cm($quiz->cmid));
|
||||
}
|
||||
}
|
286
mod/quiz/tests/quiz_question_restore_test.php
Normal file
286
mod/quiz/tests/quiz_question_restore_test.php
Normal file
|
@ -0,0 +1,286 @@
|
|||
<?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;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
global $CFG;
|
||||
require_once(__DIR__ . '/quiz_question_helper_test_trait.php');
|
||||
require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php');
|
||||
require_once($CFG->dirroot . '/mod/quiz/locallib.php');
|
||||
|
||||
/**
|
||||
* Quiz backup and restore tests.
|
||||
*
|
||||
* @package mod_quiz
|
||||
* @category test
|
||||
* @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
|
||||
* @coversDefaultClass \mod_quiz\question\bank\qbank_helper
|
||||
* @coversDefaultClass \backup_quiz_activity_structure_step
|
||||
* @coversDefaultClass \restore_quiz_activity_structure_step
|
||||
*/
|
||||
class quiz_question_restore_test extends \advanced_testcase {
|
||||
use \quiz_question_helper_test_trait;
|
||||
|
||||
/**
|
||||
* Called before every test.
|
||||
*/
|
||||
public function setUp(): void {
|
||||
global $USER;
|
||||
parent::setUp();
|
||||
$this->setAdminUser();
|
||||
$this->course = $this->getDataGenerator()->create_course();
|
||||
$this->student = $this->getDataGenerator()->create_user();
|
||||
$this->user = $USER;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test a quiz backup and restore in a different course without attempts for course question bank.
|
||||
*
|
||||
* @covers ::get_question_structure
|
||||
*/
|
||||
public function test_quiz_restore_in_a_different_course_using_course_question_bank() {
|
||||
global $DB;
|
||||
$this->resetAfterTest();
|
||||
$quiz = $this->create_test_quiz($this->course);
|
||||
// Test for questions from a different context.
|
||||
$context = \context_course::instance($this->course->id);
|
||||
$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
|
||||
$this->add_regular_questions($questiongenerator, $quiz, ['contextid' => $context->id]);
|
||||
$this->add_random_questions($questiongenerator, $quiz, ['contextid' => $context->id]);
|
||||
$backupid = $this->backup_quiz($quiz, $this->user);
|
||||
// Delete the current course to make sure there is no data.
|
||||
delete_course($this->course, false);
|
||||
// Check if the questions and associated datas are deleted properly.
|
||||
$this->assertEquals(0, count(\mod_quiz\question\bank\qbank_helper::get_question_structure($quiz->id)));
|
||||
$newcourse = $this->getDataGenerator()->create_course();
|
||||
$this->restore_quiz($backupid, $newcourse, $this->user);
|
||||
$module = $DB->get_record('quiz', ['course' => $newcourse->id]);
|
||||
$this->assertEquals(3, count(\mod_quiz\question\bank\qbank_helper::get_question_structure($module->id)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test a quiz backup and restore in a different course without attempts for quiz question bank.
|
||||
*
|
||||
* @covers ::get_question_structure
|
||||
*/
|
||||
public function test_quiz_restore_in_a_different_course_using_quiz_question_bank() {
|
||||
global $DB;
|
||||
$this->resetAfterTest();
|
||||
$quiz = $this->create_test_quiz($this->course);
|
||||
// Test for questions from a different context.
|
||||
$context = \context_module::instance(get_coursemodule_from_instance("quiz", $quiz->id, $this->course->id)->id);
|
||||
$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
|
||||
$this->add_regular_questions($questiongenerator, $quiz, ['contextid' => $context->id]);
|
||||
$this->add_random_questions($questiongenerator, $quiz, ['contextid' => $context->id]);
|
||||
$backupid = $this->backup_quiz($quiz, $this->user);
|
||||
// Delete the current course to make sure there is no data.
|
||||
delete_course($this->course, false);
|
||||
// Check if the questions and associated datas are deleted properly.
|
||||
$this->assertEquals(0, count(\mod_quiz\question\bank\qbank_helper::get_question_structure($quiz->id)));
|
||||
$newcourse = $this->getDataGenerator()->create_course();
|
||||
$this->restore_quiz($backupid, $newcourse, $this->user);
|
||||
$module = $DB->get_record('quiz', ['course' => $newcourse->id]);
|
||||
$this->assertEquals(3, count(\mod_quiz\question\bank\qbank_helper::get_question_structure($module->id)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Count the questions for the context.
|
||||
*
|
||||
* @param int $context
|
||||
* @param string $extracondition
|
||||
* @return int
|
||||
*/
|
||||
protected function question_count($context, $extracondition = ''): int {
|
||||
global $DB;
|
||||
return $DB->count_records_sql(
|
||||
"SELECT COUNT(q.id)
|
||||
FROM {question} q
|
||||
JOIN {question_versions} qv ON qv.questionid = q.id
|
||||
JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
|
||||
JOIN {question_categories} qc on qc.id = qbe.questioncategoryid
|
||||
WHERE qc.contextid = ?
|
||||
$extracondition", [$context]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if a duplicate does not duplicate questions in course question bank.
|
||||
*
|
||||
* @covers ::duplicate_module
|
||||
*/
|
||||
public function test_quiz_duplicate_does_not_duplicate_course_question_bank_questions() {
|
||||
$this->resetAfterTest();
|
||||
$quiz = $this->create_test_quiz($this->course);
|
||||
// Test for questions from a different context.
|
||||
$context = \context_course::instance($this->course->id);
|
||||
$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
|
||||
$this->add_regular_questions($questiongenerator, $quiz, ['contextid' => $context->id]);
|
||||
$this->add_random_questions($questiongenerator, $quiz, ['contextid' => $context->id]);
|
||||
// Count the questions in course context.
|
||||
$this->assertEquals(7, $this->question_count($context->id));
|
||||
$newquiz = $this->duplicate_quiz($this->course, $quiz);
|
||||
$this->assertEquals(7, $this->question_count($context->id));
|
||||
$context = \context_module::instance($newquiz->id);
|
||||
// Count the questions in the quiz context.
|
||||
$this->assertEquals(0, $this->question_count($context->id));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test quiz duplicate for quiz question bank.
|
||||
*
|
||||
* @covers ::duplicate_module
|
||||
*/
|
||||
public function test_quiz_duplicate_for_quiz_question_bank_questions() {
|
||||
$this->resetAfterTest();
|
||||
$quiz = $this->create_test_quiz($this->course);
|
||||
// Test for questions from a different context.
|
||||
$context = \context_module::instance(get_coursemodule_from_instance("quiz", $quiz->id, $this->course->id)->id);
|
||||
$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
|
||||
$this->add_regular_questions($questiongenerator, $quiz, ['contextid' => $context->id]);
|
||||
$this->add_random_questions($questiongenerator, $quiz, ['contextid' => $context->id]);
|
||||
// Count the questions in course context.
|
||||
$this->assertEquals(7, $this->question_count($context->id));
|
||||
$newquiz = $this->duplicate_quiz($this->course, $quiz);
|
||||
$this->assertEquals(7, $this->question_count($context->id));
|
||||
$context = \context_module::instance($newquiz->id);
|
||||
// Count the questions in the quiz context.
|
||||
$this->assertEquals(7, $this->question_count($context->id));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test quiz restore with attempts.
|
||||
*
|
||||
* @covers ::get_question_structure
|
||||
*/
|
||||
public function test_quiz_restore_with_attempts() {
|
||||
global $DB;
|
||||
$this->resetAfterTest();
|
||||
$quiz = $this->create_test_quiz($this->course);
|
||||
// Test for questions from a different context.
|
||||
$context = \context_module::instance(get_coursemodule_from_instance("quiz", $quiz->id, $this->course->id)->id);
|
||||
$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
|
||||
$this->add_regular_questions($questiongenerator, $quiz, ['contextid' => $context->id]);
|
||||
$this->add_random_questions($questiongenerator, $quiz, ['contextid' => $context->id]);
|
||||
list($quizobj, $quba, $attemptobj) = $this->attempt_quiz($quiz, $this->student);
|
||||
$userattempts = quiz_get_user_attempts($quiz->id, $this->student->id);
|
||||
// Count the attempts for this quiz.
|
||||
$this->assertEquals(3, $quba->question_count());
|
||||
$this->assertEquals(1, count($userattempts));
|
||||
$backupid = $this->backup_quiz($quiz, $this->user);
|
||||
// Delete the current course to make sure there is no data.
|
||||
delete_course($this->course, false);
|
||||
$newcourse = $this->getDataGenerator()->create_course();
|
||||
$this->restore_quiz($backupid, $newcourse, $this->user);
|
||||
$module = $DB->get_record('quiz', ['course' => $newcourse->id]);
|
||||
$userattempts = quiz_get_user_attempts($module->id, $this->student->id);
|
||||
$this->assertEquals(1, count($userattempts));
|
||||
$this->assertEquals(3, count(\mod_quiz\question\bank\qbank_helper::get_question_structure($module->id)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test pre 4.0 quiz restore for regular questions.
|
||||
*
|
||||
* @covers ::process_quiz_question_legacy_instance
|
||||
*/
|
||||
public function test_pre_4_quiz_restore_for_regular_questions() {
|
||||
global $USER, $DB;
|
||||
$this->resetAfterTest();
|
||||
$backupid = 'abc';
|
||||
$backuppath = make_backup_temp_directory($backupid);
|
||||
get_file_packer('application/vnd.moodle.backup')->extract_to_pathname(
|
||||
__DIR__ . "/fixtures/moodle_28_quiz.mbz", $backuppath);
|
||||
|
||||
// Do the restore to new course with default settings.
|
||||
$categoryid = $DB->get_field_sql("SELECT MIN(id) FROM {course_categories}");
|
||||
$newcourseid = \restore_dbops::create_new_course('Test fullname', 'Test shortname', $categoryid);
|
||||
$rc = new \restore_controller($backupid, $newcourseid, \backup::INTERACTIVE_NO, \backup::MODE_GENERAL, $USER->id,
|
||||
\backup::TARGET_NEW_COURSE);
|
||||
|
||||
$this->assertTrue($rc->execute_precheck());
|
||||
$rc->execute_plan();
|
||||
$rc->destroy();
|
||||
|
||||
// Get the information about the resulting course and check that it is set up correctly.
|
||||
$modinfo = get_fast_modinfo($newcourseid);
|
||||
$quiz = array_values($modinfo->get_instances_of('quiz'))[0];
|
||||
$quizobj = \quiz::create($quiz->instance);
|
||||
$structure = \mod_quiz\structure::create_for_quiz($quizobj);
|
||||
|
||||
// Are the correct slots returned?
|
||||
$slots = $structure->get_slots();
|
||||
$this->assertCount(2, $slots);
|
||||
|
||||
$quizobj->preload_questions();
|
||||
$quizobj->load_questions();
|
||||
$questions = $quizobj->get_questions();
|
||||
$this->assertCount(2, $questions);
|
||||
|
||||
// Count the questions in quiz qbank.
|
||||
$context = \context_module::instance(get_coursemodule_from_instance("quiz", $quizobj->get_quizid(), $newcourseid)->id);
|
||||
$this->assertEquals(2, $this->question_count($context->id));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test pre 4.0 quiz restore for random questions.
|
||||
*
|
||||
* @covers ::process_quiz_question_legacy_instance
|
||||
*/
|
||||
public function test_pre_4_quiz_restore_for_random_questions() {
|
||||
global $USER, $DB;
|
||||
$this->resetAfterTest();
|
||||
$backupid = 'abc';
|
||||
$backuppath = make_backup_temp_directory($backupid);
|
||||
get_file_packer('application/vnd.moodle.backup')->extract_to_pathname(
|
||||
__DIR__ . "/fixtures/random_by_tag_quiz.mbz", $backuppath);
|
||||
|
||||
// Do the restore to new course with default settings.
|
||||
$categoryid = $DB->get_field_sql("SELECT MIN(id) FROM {course_categories}");
|
||||
$newcourseid = \restore_dbops::create_new_course('Test fullname', 'Test shortname', $categoryid);
|
||||
$rc = new \restore_controller($backupid, $newcourseid, \backup::INTERACTIVE_NO, \backup::MODE_GENERAL, $USER->id,
|
||||
\backup::TARGET_NEW_COURSE);
|
||||
|
||||
$this->assertTrue($rc->execute_precheck());
|
||||
$rc->execute_plan();
|
||||
$rc->destroy();
|
||||
|
||||
// Get the information about the resulting course and check that it is set up correctly.
|
||||
$modinfo = get_fast_modinfo($newcourseid);
|
||||
$quiz = array_values($modinfo->get_instances_of('quiz'))[0];
|
||||
$quizobj = \quiz::create($quiz->instance);
|
||||
$structure = \mod_quiz\structure::create_for_quiz($quizobj);
|
||||
|
||||
// Are the correct slots returned?
|
||||
$slots = $structure->get_slots();
|
||||
$this->assertCount(1, $slots);
|
||||
|
||||
$quizobj->preload_questions();
|
||||
$quizobj->load_questions();
|
||||
$questions = $quizobj->get_questions();
|
||||
$this->assertCount(1, $questions);
|
||||
|
||||
// Count the questions for course question bank.
|
||||
$this->assertEquals(6, $this->question_count(\context_course::instance($newcourseid)->id));
|
||||
$this->assertEquals(6, $this->question_count(\context_course::instance($newcourseid)->id,
|
||||
"AND q.qtype <> 'random'"));
|
||||
|
||||
// Count the questions in quiz qbank.
|
||||
$context = \context_module::instance(get_coursemodule_from_instance("quiz", $quizobj->get_quizid(), $newcourseid)->id);
|
||||
$this->assertEquals(0, $this->question_count($context->id));
|
||||
}
|
||||
}
|
185
mod/quiz/tests/quiz_question_version_test.php
Normal file
185
mod/quiz/tests/quiz_question_version_test.php
Normal file
|
@ -0,0 +1,185 @@
|
|||
<?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;
|
||||
|
||||
use mod_quiz\external\submit_question_version;
|
||||
use mod_quiz\question\bank\qbank_helper;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
require_once(__DIR__ . '/quiz_question_helper_test_trait.php');
|
||||
|
||||
/**
|
||||
* Question versions test for quiz.
|
||||
*
|
||||
* @package mod_quiz
|
||||
* @category test
|
||||
* @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
|
||||
* @coversDefaultClass \mod_quiz\question\bank\qbank_helper
|
||||
*/
|
||||
class quiz_question_version_test extends \advanced_testcase {
|
||||
use \quiz_question_helper_test_trait;
|
||||
|
||||
/**
|
||||
* Called before every test.
|
||||
*/
|
||||
public function setUp(): void {
|
||||
global $USER;
|
||||
parent::setUp();
|
||||
$this->setAdminUser();
|
||||
$this->course = $this->getDataGenerator()->create_course();
|
||||
$this->student = $this->getDataGenerator()->create_user();
|
||||
$this->user = $USER;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the quiz question data for changed version in the slots.
|
||||
*
|
||||
* @covers ::get_version_options
|
||||
*/
|
||||
public function test_quiz_questions_for_changed_versions() {
|
||||
$this->resetAfterTest();
|
||||
$quiz = $this->create_test_quiz($this->course);
|
||||
// Test for questions from a different context.
|
||||
$context = \context_module::instance(get_coursemodule_from_instance("quiz", $quiz->id, $this->course->id)->id);
|
||||
$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
|
||||
// Create a couple of questions.
|
||||
$cat = $questiongenerator->create_question_category(['contextid' => $context->id]);
|
||||
$numq = $questiongenerator->create_question('essay', null,
|
||||
['category' => $cat->id, 'name' => 'This is the first version']);
|
||||
// Create two version.
|
||||
$questiongenerator->update_question($numq, null, ['name' => 'This is the second version']);
|
||||
$questiongenerator->update_question($numq, null, ['name' => 'This is the third version']);
|
||||
quiz_add_quiz_question($numq->id, $quiz);
|
||||
// Create the quiz object.
|
||||
$quizobj = \quiz::create($quiz->id);
|
||||
$structure = \mod_quiz\structure::create_for_quiz($quizobj);
|
||||
$slots = $structure->get_slots();
|
||||
$slot = reset($slots);
|
||||
// Test that the version added is the latest version, as there are three created.
|
||||
$this->assertEquals(3, $slot->version);
|
||||
$quizobj->preload_questions();
|
||||
$quizobj->load_questions();
|
||||
$questions = $quizobj->get_questions();
|
||||
$question = reset($questions);
|
||||
$this->assertEquals(3, $question->version);
|
||||
$this->assertEquals('This is the third version', $question->name);
|
||||
// Now change the version using the external service.
|
||||
$versions = qbank_helper::get_version_options($slot->questionid);
|
||||
// We dont want the current version.
|
||||
$selectversions = [];
|
||||
foreach ($versions as $version) {
|
||||
if ($version->version === $slot->version) {
|
||||
continue;
|
||||
}
|
||||
$selectversions [$version->version] = $version;
|
||||
}
|
||||
// Change to version 1.
|
||||
submit_question_version::execute($slot->id, (int)$selectversions[1]->version);
|
||||
$quizobj->preload_questions();
|
||||
$quizobj->load_questions();
|
||||
$questions = $quizobj->get_questions();
|
||||
$question = reset($questions);
|
||||
$this->assertEquals(1, $question->version);
|
||||
$this->assertEquals('This is the first version', $question->name);
|
||||
$structure = \mod_quiz\structure::create_for_quiz($quizobj);
|
||||
$slots = $structure->get_slots();
|
||||
$slot = reset($slots);
|
||||
$this->assertEquals(1, $slot->version);
|
||||
// Change to version 2.
|
||||
submit_question_version::execute($slot->id, $selectversions[2]->version);
|
||||
$quizobj->preload_questions();
|
||||
$quizobj->load_questions();
|
||||
$questions = $quizobj->get_questions();
|
||||
$question = reset($questions);
|
||||
$this->assertEquals(2, $question->version);
|
||||
$this->assertEquals('This is the second version', $question->name);
|
||||
$structure = \mod_quiz\structure::create_for_quiz($quizobj);
|
||||
$slots = $structure->get_slots();
|
||||
$slot = reset($slots);
|
||||
$this->assertEquals(2, $slot->version);
|
||||
// Create another version.
|
||||
$questiongenerator->update_question($numq, null, ['name' => 'This is the latest version']);
|
||||
// Change to always latest.
|
||||
submit_question_version::execute($slot->id, 0);
|
||||
$quizobj->preload_questions();
|
||||
$quizobj->load_questions();
|
||||
$questions = $quizobj->get_questions();
|
||||
$question = reset($questions);
|
||||
$this->assertEquals(4, $question->version);
|
||||
$this->assertEquals('This is the latest version', $question->name);
|
||||
$structure = \mod_quiz\structure::create_for_quiz($quizobj);
|
||||
$slots = $structure->get_slots();
|
||||
$slot = reset($slots);
|
||||
$this->assertEquals(4, $slot->version);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if changing the version of the slot changes the attempts.
|
||||
*
|
||||
* @covers ::get_version_options
|
||||
*/
|
||||
public function test_quiz_question_attempts_with_changed_version() {
|
||||
$this->resetAfterTest();
|
||||
$quiz = $this->create_test_quiz($this->course);
|
||||
// Test for questions from a different context.
|
||||
$context = \context_module::instance(get_coursemodule_from_instance("quiz", $quiz->id, $this->course->id)->id);
|
||||
$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
|
||||
// Create a couple of questions.
|
||||
$cat = $questiongenerator->create_question_category(['contextid' => $context->id]);
|
||||
$numq = $questiongenerator->create_question('numerical', null,
|
||||
['category' => $cat->id, 'name' => 'This is the first version']);
|
||||
// Create two version.
|
||||
$questiongenerator->update_question($numq, null, ['name' => 'This is the second version']);
|
||||
$questiongenerator->update_question($numq, null, ['name' => 'This is the third version']);
|
||||
quiz_add_quiz_question($numq->id, $quiz);
|
||||
list($quizobj, $quba, $attemptobj) = $this->attempt_quiz($quiz, $this->student);
|
||||
$this->assertEquals('This is the third version', $attemptobj->get_question_attempt(1)->get_question()->name);
|
||||
// Create the quiz object.
|
||||
$quizobj = \quiz::create($quiz->id);
|
||||
$structure = \mod_quiz\structure::create_for_quiz($quizobj);
|
||||
$slots = $structure->get_slots();
|
||||
$slot = reset($slots);
|
||||
// Now change the version using the external service.
|
||||
$versions = qbank_helper::get_version_options($slot->questionid);
|
||||
// We dont want the current version.
|
||||
$selectversions = [];
|
||||
foreach ($versions as $version) {
|
||||
if ($version->version === $slot->version) {
|
||||
continue;
|
||||
}
|
||||
$selectversions [$version->version] = $version;
|
||||
}
|
||||
// Change to version 1.
|
||||
$this->expectException('moodle_exception');
|
||||
submit_question_version::execute($slot->id, (int)$selectversions[1]->version);
|
||||
list($quizobj, $quba, $attemptobj) = $this->attempt_quiz($quiz, $this->student, 2);
|
||||
$this->assertEquals('This is the first version', $attemptobj->get_question_attempt(1)->get_question()->name);
|
||||
// Change to version 2.
|
||||
submit_question_version::execute($slot->id, (int)$selectversions[2]->version);
|
||||
list($quizobj, $quba, $attemptobj) = $this->attempt_quiz($quiz, $this->student, 3);
|
||||
$this->assertEquals('This is the second version', $attemptobj->get_question_attempt(1)->get_question()->name);
|
||||
// Create another version.
|
||||
$questiongenerator->update_question($numq, null, ['name' => 'This is the latest version']);
|
||||
// Change to always latest.
|
||||
submit_question_version::execute($slot->id, 0);
|
||||
list($quizobj, $quba, $attemptobj) = $this->attempt_quiz($quiz, $this->student, 4);
|
||||
$this->assertEquals('This is the latest version', $attemptobj->get_question_attempt(1)->get_question()->name);
|
||||
}
|
||||
}
|
|
@ -703,7 +703,11 @@ class mod_quiz_structure_testcase extends advanced_testcase {
|
|||
$cat = $questiongenerator->create_question_category();
|
||||
quiz_add_random_questions($quizobj->get_quiz(), 1, $cat->id, 1, false);
|
||||
$structure = \mod_quiz\structure::create_for_quiz($quizobj);
|
||||
$randomq = $DB->get_record('question', array('qtype' => 'random'));
|
||||
$sql = 'SELECT qsr.*
|
||||
FROM {question_set_references} qsr
|
||||
JOIN {quiz_slots} qs ON qs.id = qsr.itemid
|
||||
WHERE qs.quizid = ?';
|
||||
$randomq = $DB->get_record_sql($sql, [$quizobj->get_quizid()]);
|
||||
|
||||
$structure->remove_slot(2);
|
||||
|
||||
|
@ -711,7 +715,7 @@ class mod_quiz_structure_testcase extends advanced_testcase {
|
|||
$this->assert_quiz_layout(array(
|
||||
array('TF1', 1, 'truefalse'),
|
||||
), $structure);
|
||||
$this->assertFalse($DB->record_exists('question', array('id' => $randomq->id)));
|
||||
$this->assertFalse($DB->record_exists('question_set_references', array('id' => $randomq->id)));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -899,150 +903,6 @@ class mod_quiz_structure_testcase extends advanced_testcase {
|
|||
$this->assertEquals(0, $structure->is_question_dependent_on_previous_slot(2));
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for the get_slot_tags_for_slot test.
|
||||
*/
|
||||
public function get_slot_tags_for_slot_test_cases() {
|
||||
return [
|
||||
'incorrect slot id' => [
|
||||
'layout' => [
|
||||
['TF1', 1, 'truefalse'],
|
||||
['TF2', 1, 'truefalse'],
|
||||
['TF3', 1, 'truefalse']
|
||||
],
|
||||
'tagnames' => [
|
||||
['foo'],
|
||||
['bar'],
|
||||
['baz']
|
||||
],
|
||||
'slotnumber' => null,
|
||||
'expected' => []
|
||||
],
|
||||
'no tags' => [
|
||||
'layout' => [
|
||||
['TF1', 1, 'truefalse'],
|
||||
['TF2', 1, 'truefalse'],
|
||||
['TF3', 1, 'truefalse']
|
||||
],
|
||||
'tagnames' => [
|
||||
['foo'],
|
||||
[],
|
||||
['baz']
|
||||
],
|
||||
'slotnumber' => 2,
|
||||
'expected' => []
|
||||
],
|
||||
'one tag 1' => [
|
||||
'layout' => [
|
||||
['TF1', 1, 'truefalse'],
|
||||
['TF2', 1, 'truefalse'],
|
||||
['TF3', 1, 'truefalse']
|
||||
],
|
||||
'tagnames' => [
|
||||
['foo'],
|
||||
['bar'],
|
||||
['baz']
|
||||
],
|
||||
'slotnumber' => 1,
|
||||
'expected' => ['foo']
|
||||
],
|
||||
'one tag 2' => [
|
||||
'layout' => [
|
||||
['TF1', 1, 'truefalse'],
|
||||
['TF2', 1, 'truefalse'],
|
||||
['TF3', 1, 'truefalse']
|
||||
],
|
||||
'tagnames' => [
|
||||
['foo'],
|
||||
['bar'],
|
||||
['baz']
|
||||
],
|
||||
'slotnumber' => 2,
|
||||
'expected' => ['bar']
|
||||
],
|
||||
'multiple tags 1' => [
|
||||
'layout' => [
|
||||
['TF1', 1, 'truefalse'],
|
||||
['TF2', 1, 'truefalse'],
|
||||
['TF3', 1, 'truefalse']
|
||||
],
|
||||
'tagnames' => [
|
||||
['foo', 'bar'],
|
||||
['bar'],
|
||||
['baz']
|
||||
],
|
||||
'slotnumber' => 1,
|
||||
'expected' => ['foo', 'bar']
|
||||
],
|
||||
'multiple tags 2' => [
|
||||
'layout' => [
|
||||
['TF1', 1, 'truefalse'],
|
||||
['TF2', 1, 'truefalse'],
|
||||
['TF3', 1, 'truefalse']
|
||||
],
|
||||
'tagnames' => [
|
||||
['foo', 'bar'],
|
||||
['bar', 'baz'],
|
||||
['baz']
|
||||
],
|
||||
'slotnumber' => 2,
|
||||
'expected' => ['bar', 'baz']
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider get_slot_tags_for_slot_test_cases()
|
||||
* @param array $layout Quiz layout for create_test_quiz function
|
||||
* @param array $tagnames Tags to create for each question slot
|
||||
* @param int $slotnumber The slot number to select tags from
|
||||
* @param string[] $expected The tags expected for the given $slotnumber
|
||||
*/
|
||||
public function test_get_slot_tags_for_slot($layout, $tagnames, $slotnumber, $expected) {
|
||||
global $DB;
|
||||
$this->resetAfterTest();
|
||||
|
||||
$quiz = $this->create_test_quiz($layout);
|
||||
$structure = \mod_quiz\structure::create_for_quiz($quiz);
|
||||
$collid = core_tag_area::get_collection('core', 'question');
|
||||
$slottagrecords = [];
|
||||
|
||||
if (is_null($slotnumber)) {
|
||||
// Null slot number means to create a non-existent slot id.
|
||||
$slot = $structure->get_last_slot();
|
||||
$slotid = $slot->id + 100;
|
||||
} else {
|
||||
$slot = $structure->get_slot_by_number($slotnumber);
|
||||
$slotid = $slot->id;
|
||||
}
|
||||
|
||||
foreach ($tagnames as $index => $slottagnames) {
|
||||
$tagslotnumber = $index + 1;
|
||||
$tagslotid = $structure->get_slot_id_for_slot($tagslotnumber);
|
||||
$tags = core_tag_tag::create_if_missing($collid, $slottagnames);
|
||||
$records = array_map(function($tag) use ($tagslotid) {
|
||||
return (object) [
|
||||
'slotid' => $tagslotid,
|
||||
'tagid' => $tag->id,
|
||||
'tagname' => $tag->name
|
||||
];
|
||||
}, array_values($tags));
|
||||
$slottagrecords = array_merge($slottagrecords, $records);
|
||||
}
|
||||
|
||||
$DB->insert_records('quiz_slot_tags', $slottagrecords);
|
||||
|
||||
$actualslottags = $structure->get_slot_tags_for_slot_id($slotid);
|
||||
$actual = array_map(function($slottag) {
|
||||
return $slottag->tagname;
|
||||
}, $actualslottags);
|
||||
|
||||
sort($expected);
|
||||
sort($actual);
|
||||
|
||||
$this->assertEquals($expected, $actual);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for can_add_random_questions.
|
||||
*/
|
||||
|
|
|
@ -15,20 +15,10 @@
|
|||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Unit tests for usage of tags in quizzes.
|
||||
*
|
||||
* @package mod_quiz
|
||||
* @copyright 2018 Shamim Rezaie <shamim@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
/**
|
||||
* Class mod_quiz_tags_testcase
|
||||
* Class for tests related to usage of question tags in quizzes.
|
||||
* Test the restore of random question tags.
|
||||
*
|
||||
* @copyright 2018 Shamim Rezaie <shamim@moodle.com>
|
||||
* @author 2021 Safat Shahin <safatshahin@catalyst-au.net>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class mod_quiz_tags_testcase extends advanced_testcase {
|
||||
|
@ -84,17 +74,14 @@ class mod_quiz_tags_testcase extends advanced_testcase {
|
|||
$this->assertNotFalse($tag3);
|
||||
|
||||
$slottags = quiz_retrieve_slot_tags($question->slotid);
|
||||
$this->assertEqualsCanonicalizing(
|
||||
[
|
||||
['tagid' => $tag2->id, 'tagname' => $tag2->name]
|
||||
],
|
||||
array_map(function($tag) {
|
||||
return ['tagid' => $tag->tagid, 'tagname' => $tag->tagname];
|
||||
}, $slottags)
|
||||
);
|
||||
$slottags = reset($slottags);
|
||||
$slottags = explode(',', $slottags);
|
||||
$this->assertEquals("{$tag2->id},{$tag2->name}", "{$slottags[0]},{$slottags[1]}");
|
||||
|
||||
$defaultcategory = question_get_default_category(context_course::instance($newcourseid)->id);
|
||||
$this->assertEquals($defaultcategory->id, $question->randomfromcategory);
|
||||
$this->assertEquals(0, $question->randomincludingsubcategories);
|
||||
$this->assertEquals($defaultcategory->id, $question->categoryobject->id);
|
||||
$randomincludingsubcategories = $DB->get_record('question_set_references', ['itemid' => reset($slots)->id]);
|
||||
$filtercondition = json_decode($randomincludingsubcategories->filtercondition);
|
||||
$this->assertEquals(0, $filtercondition->includingsubcategories);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,7 +12,13 @@ This files describes API changes in the quiz code.
|
|||
in mod/quiz/renderer.php.
|
||||
* The function no_questions_message() in class mod_quiz_renderer is deprecated. Please use generate_no_questions_message()
|
||||
in the same class.
|
||||
|
||||
* quiz_slots has been updated as a part of https://docs.moodle.org/dev/Question_bank_improvements_for_Moodle_4.0
|
||||
The fields removed will be now manage by a new table in core_question:
|
||||
- question_set_reference -> Records where a specific question is used.
|
||||
- question_set_reference -> Records where groups of questions are used (e.g.: Random questions).
|
||||
The quiz_slots_tags table will be removed entirely.
|
||||
* The method get_slot_tags_for_slot_id() from mod/quiz/classes/structure.php has been deprecated and the associated
|
||||
code for this method have been removed to make sure any unnecessary error doesn't happen as a part of any call.
|
||||
|
||||
=== 3.11 ===
|
||||
|
||||
|
|
|
@ -24,6 +24,6 @@
|
|||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$plugin->version = 2021101901;
|
||||
$plugin->version = 2022020300;
|
||||
$plugin->requires = 2021052500;
|
||||
$plugin->component = 'mod_quiz';
|
||||
|
|
|
@ -38,7 +38,9 @@ class helper {
|
|||
list($usql, $params) = $DB->get_in_or_equal($questionids);
|
||||
$sql = "SELECT q.*, c.contextid
|
||||
FROM {question} q
|
||||
JOIN {question_categories} c ON c.id = q.category
|
||||
JOIN {question_versions} qv ON qv.questionid = q.id
|
||||
JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
|
||||
JOIN {question_categories} c ON c.id = qbe.questioncategoryid
|
||||
WHERE q.id
|
||||
{$usql}";
|
||||
$questions = $DB->get_records_sql($sql, $params);
|
||||
|
|
|
@ -25,6 +25,7 @@
|
|||
|
||||
require_once(__DIR__ . '/../../../config.php');
|
||||
require_once(__DIR__ . '/../../editlib.php');
|
||||
|
||||
global $DB, $OUTPUT, $PAGE, $COURSE;
|
||||
|
||||
$moveselected = optional_param('move', false, PARAM_BOOL);
|
||||
|
@ -52,7 +53,7 @@ if ($cmid) {
|
|||
throw new moodle_exception('missingcourseorcmid', 'question');
|
||||
}
|
||||
|
||||
$contexts = new question_edit_contexts($thiscontext);
|
||||
$contexts = new core_question\local\bank\question_edit_contexts($thiscontext);
|
||||
$url = new moodle_url('/question/bank/bulkmove/move.php');
|
||||
|
||||
$PAGE->set_url($url);
|
||||
|
|
|
@ -16,6 +16,8 @@
|
|||
|
||||
namespace qbank_bulkmove;
|
||||
|
||||
use core_question\local\bank\question_edit_contexts;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
global $CFG;
|
||||
|
@ -53,7 +55,7 @@ class helper_test extends \advanced_testcase {
|
|||
protected $context;
|
||||
|
||||
/**
|
||||
* @var \question_edit_contexts $contexts
|
||||
* @var \core_question\local\bank\question_edit_contexts $contexts
|
||||
*/
|
||||
protected $contexts;
|
||||
|
||||
|
@ -87,7 +89,7 @@ class helper_test extends \advanced_testcase {
|
|||
$this->context = \context_course::instance($this->course->id);
|
||||
|
||||
// Create a question in the default category.
|
||||
$this->contexts = new \question_edit_contexts($this->context);
|
||||
$this->contexts = new question_edit_contexts($this->context);
|
||||
$this->cat = question_make_default_categories($this->contexts->all());
|
||||
$this->questiondata1 = $questiongenerator->create_question('numerical', null,
|
||||
['name' => 'Example question', 'category' => $this->cat->id]);
|
||||
|
@ -202,7 +204,7 @@ class helper_test extends \advanced_testcase {
|
|||
public function test_get_displaydata() {
|
||||
$this->helper_setup();
|
||||
$coursecontext = \context_course::instance($this->course->id);
|
||||
$contexts = new \question_edit_contexts($coursecontext);
|
||||
$contexts = new question_edit_contexts($coursecontext);
|
||||
$addcontexts = $contexts->having_cap('moodle/question:add');
|
||||
$url = new \moodle_url('/question/bank/bulkmove/move.php');
|
||||
$displaydata = \qbank_bulkmove\helper::get_displaydata($addcontexts, $url, $url);
|
||||
|
|
|
@ -137,7 +137,8 @@ class qbank_comment_backup_restore_test extends \advanced_testcase {
|
|||
* across the backup and restore process.
|
||||
*/
|
||||
public function test_backup_restore() {
|
||||
global $DB;
|
||||
global $DB, $CFG;
|
||||
require_once($CFG->dirroot . '/comment/lib.php');
|
||||
$this->resetAfterTest();
|
||||
$this->setAdminUser();
|
||||
|
||||
|
@ -195,12 +196,18 @@ class qbank_comment_backup_restore_test extends \advanced_testcase {
|
|||
$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']);
|
||||
$sql =
|
||||
'SELECT q.*
|
||||
FROM {question} q
|
||||
JOIN {question_versions} qv ON qv.questionid = q.id
|
||||
JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
|
||||
WHERE qbe.idnumber = ?';
|
||||
$newquestion1 = $DB->get_record_sql($sql, ['idnumber' => 'q1']);
|
||||
$args->itemid = $newquestion1->id;
|
||||
$commentobj = new \comment($args);
|
||||
$this->assertEquals($commentobj->count(), 2);
|
||||
|
||||
$newquestion2 = $DB->get_record('question', ['idnumber' => 'q2']);
|
||||
$newquestion2 = $DB->get_record_sql($sql, ['idnumber' => 'q2']);
|
||||
$args->itemid = $newquestion2->id;
|
||||
$commentobj = new \comment($args);
|
||||
$this->assertEquals($commentobj->count(), 1);
|
||||
|
|
|
@ -104,7 +104,7 @@ class behat_qbank_comment extends behat_question_base {
|
|||
|
||||
if ($this->running_javascript()) {
|
||||
$commentstextarea = $this->find('css',
|
||||
'.question-comment-view .comment-area textarea', $exception);
|
||||
'.comment-area textarea', $exception);
|
||||
$commentstextarea->setValue($comment);
|
||||
|
||||
// We delay 1 second which is all we need.
|
||||
|
@ -115,6 +115,31 @@ class behat_qbank_comment extends behat_question_base {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the specified comment from the current question comment preview.
|
||||
*
|
||||
* @Then I delete :arg comment from question preview
|
||||
* @param string $comment
|
||||
*/
|
||||
public function i_delete_comment_from_question_preview($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), ' '), ' comment-ctrl ')]" .
|
||||
"/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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the specified comment from the current question comment modal.
|
||||
*
|
||||
|
|
|
@ -63,6 +63,7 @@ Feature: A Teacher can comment in a question
|
|||
And I navigate to "Question bank" 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
|
||||
And I click on "Comments" "link"
|
||||
Then I should see "Save comment"
|
||||
And I add "Super test comment 01" comment to question preview
|
||||
And I click on "Save comment" "link"
|
||||
|
@ -71,7 +72,8 @@ Feature: A Teacher can comment in a question
|
|||
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 click on "Comments" "link"
|
||||
And I delete "Super test comment 01" comment from question preview
|
||||
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
|
||||
|
@ -100,6 +102,7 @@ Feature: A Teacher can comment in a question
|
|||
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
|
||||
And I click on "Comments" "link"
|
||||
Then I should see "Save comment"
|
||||
And I log out
|
||||
Then I log in as "teacher2"
|
||||
|
@ -112,3 +115,34 @@ Feature: A Teacher can comment in a question
|
|||
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"
|
||||
|
||||
@javascript
|
||||
Scenario: Comments added from the quiz page are visible
|
||||
Given I log in as "teacher1"
|
||||
And I am on the "Test quiz" "quiz activity" page
|
||||
When I navigate to "Edit quiz" in current page administration
|
||||
And I press "Add"
|
||||
And I follow "from question bank"
|
||||
And I click on "Select" "checkbox" in the "First question" "table_row"
|
||||
And I click on "Add selected questions to the quiz" "button"
|
||||
And I click on "Preview question" "link"
|
||||
And I switch to "questionpreview" window
|
||||
And I press "Comments"
|
||||
And I set the field "content" to "Some new comment"
|
||||
And I click on "Save comment" "link"
|
||||
And I should see "Some new comment"
|
||||
And I switch to the main window
|
||||
And I am on the "Test quiz" "quiz activity" page
|
||||
And I navigate to "Question bank > Questions" in current page administration
|
||||
And I choose "Preview" action for "First question" in the question bank
|
||||
And I click on "Comments" "link"
|
||||
And I should see "Some new comment"
|
||||
And I should see "T1 Teacher1"
|
||||
And I delete "Some new comment" comment from question preview
|
||||
And I should not see "Some new comment"
|
||||
And I am on the "Test quiz" "quiz activity" page
|
||||
And I navigate to "Edit quiz" in current page administration
|
||||
And I click on "Preview question" "link"
|
||||
And I switch to "questionpreview" window
|
||||
And I press "Comments"
|
||||
Then I should not see "Some new comment"
|
||||
|
|
|
@ -22,7 +22,6 @@ use comment;
|
|||
use context;
|
||||
use context_course;
|
||||
use core_question_generator;
|
||||
use question_edit_contexts;
|
||||
use stdClass;
|
||||
|
||||
/**
|
||||
|
@ -66,7 +65,7 @@ class comment_created_deleted_test extends advanced_testcase {
|
|||
$this->context = context_course::instance($this->course->id);
|
||||
|
||||
// Create a question in the default category.
|
||||
$contexts = new question_edit_contexts($this->context);
|
||||
$contexts = new \core_question\local\bank\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]);
|
||||
|
|
|
@ -287,7 +287,12 @@ class qbank_customfields_customfield_testcase extends advanced_testcase {
|
|||
$this->restore_course($backupid, $coursefullname, $courseshortname . '_2', $newcategory->id);
|
||||
|
||||
// The questions and their associated custom fields should have been restored.
|
||||
$newquestion1 = $DB->get_record('question', ['idnumber' => 'q1']);
|
||||
$sql = 'SELECT q.*
|
||||
FROM {question} q
|
||||
JOIN {question_versions} qv ON qv.questionid = q.id
|
||||
JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
|
||||
WHERE qbe.idnumber = ?';
|
||||
$newquestion1 = $DB->get_record_sql($sql, ['q1']);
|
||||
$newquestion1cfdata = $customfieldhandler->export_instance_data_object($newquestion1->id);
|
||||
$this->assertEquals('some text', $newquestion1cfdata->f1);
|
||||
$this->assertEquals('Yes', $newquestion1cfdata->f2);
|
||||
|
@ -295,7 +300,7 @@ class qbank_customfields_customfield_testcase extends advanced_testcase {
|
|||
$this->assertEquals('b', $newquestion1cfdata->f4);
|
||||
$this->assertEquals('test', $newquestion1cfdata->f5);
|
||||
|
||||
$newquestion2 = $DB->get_record('question', ['idnumber' => 'q2']);
|
||||
$newquestion2 = $DB->get_record_sql($sql, ['q2']);
|
||||
$newquestion2cfdata = $customfieldhandler->export_instance_data_object($newquestion2->id);
|
||||
$this->assertEquals('some more text', $newquestion2cfdata->f1);
|
||||
$this->assertEquals('No', $newquestion2cfdata->f2);
|
||||
|
|
|
@ -24,6 +24,7 @@
|
|||
|
||||
namespace qbank_deletequestion;
|
||||
|
||||
use core_question\local\bank\question_version_status;
|
||||
use core_question\local\bank\menu_action_column_base;
|
||||
|
||||
/**
|
||||
|
@ -82,7 +83,7 @@ class delete_action_column extends menu_action_column_base {
|
|||
if (!question_has_capability_on($question, 'edit')) {
|
||||
return [null, null, null];
|
||||
}
|
||||
if ($question->hidden) {
|
||||
if ($question->status === question_version_status::QUESTION_STATUS_HIDDEN) {
|
||||
$hiddenparams = array(
|
||||
'unhide' => $question->id,
|
||||
'sesskey' => sesskey());
|
||||
|
@ -102,7 +103,6 @@ class delete_action_column extends menu_action_column_base {
|
|||
|
||||
public function get_required_fields(): array {
|
||||
$required = parent::get_required_fields();
|
||||
$required[] = 'q.hidden';
|
||||
return $required;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -51,7 +51,7 @@ if ($cmid) {
|
|||
throw new moodle_exception('missingcourseorcmid', 'question');
|
||||
}
|
||||
|
||||
$contexts = new question_edit_contexts($thiscontext);
|
||||
$contexts = new core_question\local\bank\question_edit_contexts($thiscontext);
|
||||
$url = new moodle_url('/question/bank/deletequestion/delete.php');
|
||||
|
||||
$PAGE->set_url($url);
|
||||
|
@ -62,7 +62,8 @@ $PAGE->set_heading($COURSE->fullname);
|
|||
// Unhide a question.
|
||||
if (($unhide = optional_param('unhide', '', PARAM_INT)) and confirm_sesskey()) {
|
||||
question_require_capability_on($unhide, 'edit');
|
||||
$DB->set_field('question', 'hidden', 0, array('id' => $unhide));
|
||||
$DB->set_field('question_versions', 'status',
|
||||
\core_question\local\bank\question_version_status::QUESTION_STATUS_READY, ['questionid' => $unhide]);
|
||||
|
||||
// Purge these questions from the cache.
|
||||
\question_bank::notify_question_edited($unhide);
|
||||
|
@ -81,7 +82,8 @@ if ($deleteselected && ($confirm = optional_param('confirm', '', PARAM_ALPHANUM)
|
|||
$questionid = (int)$questionid;
|
||||
question_require_capability_on($questionid, 'edit');
|
||||
if (questions_in_use(array($questionid))) {
|
||||
$DB->set_field('question', 'hidden', 1, array('id' => $questionid));
|
||||
$DB->set_field('question_versions', 'status',
|
||||
\core_question\local\bank\question_version_status::QUESTION_STATUS_HIDDEN, ['questionid' => $questionid]);
|
||||
} else {
|
||||
question_delete_question($questionid);
|
||||
}
|
||||
|
|
|
@ -55,7 +55,8 @@ Feature: Use the qbank plugin manager page for deletequestion
|
|||
Given I log in as "admin"
|
||||
And I am on the "Test quiz" "quiz activity" page
|
||||
And I navigate to "Question bank > Questions" in current page administration
|
||||
And I click on "Select all" "checkbox"
|
||||
And I click on "First question" "checkbox"
|
||||
And I click on "First question second" "checkbox"
|
||||
And I click on "With selected" "button"
|
||||
And I click on question bulk action "deleteselected"
|
||||
And I click on "Delete" "button" in the "Confirm" "dialogue"
|
||||
|
|
2
question/bank/editquestion/amd/build/question_status.min.js
vendored
Normal file
2
question/bank/editquestion/amd/build/question_status.min.js
vendored
Normal file
|
@ -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_editquestion/question_status",["exports","core/fragment","core/str","core/modal_factory","core/notification","core/modal_events","core/ajax"],function(a,b,c,d,e,f,g){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.init=void 0;b=j(b);c=i(c);d=j(d);e=j(e);f=j(f);g=j(g);function h(){if("function"!=typeof WeakMap)return null;var a=new WeakMap;h=function(){return a};return a}function i(a){if(a&&a.__esModule){return a}if(null===a||"object"!==_typeof(a)&&"function"!=typeof a){return{default:a}}var b=h();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 j(a){return a&&a.__esModule?a:{default:a}}var k=function(a,c){return b.default.loadFragment("qbank_editquestion","question_status",c,a)},l=function(a,b){return g.default.call([{methodname:"qbank_editquestion_set_status",args:{questionid:a,formdata:b}}])[0]},m=function(a,b,c){var d=a.getBody().find("form").serialize();l(b,d).then(function(a){if(a.status){c.innerText=a.statusname}}).catch(e.default.exception)},n=function(a,b,c){o({questionid:a},b).then(function(b){b.show();var d=b.getRoot();d.on(f.default.save,function(d){d.preventDefault();d.stopPropagation();m(b,a,c);b.hide()});return b}).catch(e.default.exception)},o=function(a,b){return d.default.create({type:d.default.types.SAVE_CANCEL,title:c.get_string("questionstatusheader","qbank_editquestion"),body:k(a,b),large:!1})};a.init=function init(a,b){var c=document.querySelector(a),d=c.getAttribute("data-questionid");c.addEventListener("click",function(){n(d,b,c)})}});
|
||||
//# sourceMappingURL=question_status.min.js.map
|
File diff suppressed because one or more lines are too long
135
question/bank/editquestion/amd/src/question_status.js
Normal file
135
question/bank/editquestion/amd/src/question_status.js
Normal file
|
@ -0,0 +1,135 @@
|
|||
// 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/>.
|
||||
|
||||
/**
|
||||
* Status column selector js.
|
||||
*
|
||||
* @module qbank_editquestion/question_status
|
||||
* @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
|
||||
*/
|
||||
|
||||
import Fragment from 'core/fragment';
|
||||
import * as Str from 'core/str';
|
||||
import ModalFactory from 'core/modal_factory';
|
||||
import Notification from 'core/notification';
|
||||
import ModalEvents from 'core/modal_events';
|
||||
import Ajax from 'core/ajax';
|
||||
|
||||
/**
|
||||
* Get the fragment.
|
||||
*
|
||||
* @method getFragment
|
||||
* @param {{questioned: Number}} args
|
||||
* @param {Number} contextId
|
||||
* @return {String}
|
||||
*/
|
||||
const getFragment = (args, contextId) => {
|
||||
return Fragment.loadFragment('qbank_editquestion', 'question_status', contextId, args);
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the question status.
|
||||
*
|
||||
* @param {Number} questionId The question id.
|
||||
* @param {String} formData The question tag form data in a URI encoded param string
|
||||
* @return {Array} The modified question status
|
||||
*/
|
||||
const setQuestionStatus = (questionId, formData) => Ajax.call([{
|
||||
methodname: 'qbank_editquestion_set_status',
|
||||
args: {
|
||||
questionid: questionId,
|
||||
formdata: formData
|
||||
}
|
||||
}])[0];
|
||||
|
||||
/**
|
||||
* Save the status.
|
||||
*
|
||||
* @method getFragment
|
||||
* @param {object} modal
|
||||
* @param {Number} questionId
|
||||
* @param {HTMLElement} target
|
||||
*/
|
||||
const save = (modal, questionId, target) => {
|
||||
const formData = modal.getBody().find('form').serialize();
|
||||
|
||||
setQuestionStatus(questionId, formData)
|
||||
.then(result => {
|
||||
if (result.status) {
|
||||
target.innerText = result.statusname;
|
||||
}
|
||||
return;
|
||||
})
|
||||
.catch(Notification.exception);
|
||||
};
|
||||
|
||||
/**
|
||||
* Event listeners for the module.
|
||||
*
|
||||
* @method clickEvent
|
||||
* @param {Number} questionId
|
||||
* @param {Number} contextId
|
||||
* @param {HTMLElement} target
|
||||
*/
|
||||
const statusEvent = (questionId, contextId, target) => {
|
||||
let args = {
|
||||
questionid: questionId
|
||||
};
|
||||
getStatusModal(args, contextId)
|
||||
.then((modal) => {
|
||||
modal.show();
|
||||
let root = modal.getRoot();
|
||||
root.on(ModalEvents.save, function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
save(modal, questionId, target);
|
||||
modal.hide();
|
||||
});
|
||||
return modal;
|
||||
})
|
||||
.catch(Notification.exception);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the status modal to display.
|
||||
*
|
||||
* @param {{questionid: Number}} args
|
||||
* @param {Number} contextId
|
||||
* @return {HTMLElement}
|
||||
*/
|
||||
const getStatusModal = (args, contextId) => ModalFactory.create({
|
||||
type: ModalFactory.types.SAVE_CANCEL,
|
||||
title: Str.get_string('questionstatusheader', 'qbank_editquestion'),
|
||||
body: getFragment(args, contextId),
|
||||
large: false,
|
||||
});
|
||||
|
||||
/**
|
||||
* Entrypoint of the js.
|
||||
*
|
||||
* @method init
|
||||
* @param {String} questionSelector the question status identifier.
|
||||
* @param {Number} contextId The context id of the question.
|
||||
*/
|
||||
export const init = (questionSelector, contextId) => {
|
||||
let target = document.querySelector(questionSelector);
|
||||
let questionId = target.getAttribute('data-questionid');
|
||||
target.addEventListener('click', () => {
|
||||
// Call for the event listener to listed for clicks in any usage count row.
|
||||
statusEvent(questionId, contextId, target);
|
||||
});
|
||||
};
|
|
@ -27,6 +27,8 @@
|
|||
|
||||
namespace qbank_editquestion;
|
||||
|
||||
use core_question\local\bank\question_version_status;
|
||||
|
||||
/**
|
||||
* Class editquestion_helper for methods related to add/edit/copy
|
||||
*
|
||||
|
@ -83,4 +85,27 @@ class editquestion_helper {
|
|||
}
|
||||
return $PAGE->get_renderer('qbank_editquestion')->render_create_new_question_button($addquestiondisplay);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the string for the status of the question.
|
||||
*
|
||||
* @param string $status
|
||||
* @return string
|
||||
*/
|
||||
public static function get_question_status_string($status): string {
|
||||
return get_string('questionstatus' . $status, 'qbank_editquestion');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the array of status of the questions.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function get_question_status_list(): array {
|
||||
$statuslist = [];
|
||||
$statuslist[question_version_status::QUESTION_STATUS_READY] = get_string('questionstatusready', 'qbank_editquestion');
|
||||
$statuslist[question_version_status::QUESTION_STATUS_DRAFT] = get_string('questionstatusdraft', 'qbank_editquestion');
|
||||
return $statuslist;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
109
question/bank/editquestion/classes/external/update_question_version_status.php
vendored
Normal file
109
question/bank/editquestion/classes/external/update_question_version_status.php
vendored
Normal file
|
@ -0,0 +1,109 @@
|
|||
<?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 qbank_editquestion\external;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
require_once($CFG->libdir . '/externallib.php');
|
||||
require_once($CFG->dirroot . '/question/engine/bank.php');
|
||||
|
||||
use external_api;
|
||||
use external_function_parameters;
|
||||
use external_single_structure;
|
||||
use external_value;
|
||||
use qbank_editquestion\editquestion_helper;
|
||||
use question_bank;
|
||||
|
||||
/**
|
||||
* Update question status external api.
|
||||
*
|
||||
* @package qbank_editquestion
|
||||
* @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 update_question_version_status extends \external_api {
|
||||
|
||||
/**
|
||||
* Returns description of method parameters.
|
||||
*
|
||||
* @return external_function_parameters.
|
||||
*/
|
||||
public static function execute_parameters() {
|
||||
return new external_function_parameters([
|
||||
'questionid' => new external_value(PARAM_INT, 'The question id'),
|
||||
'formdata' => new external_value(PARAM_RAW, 'The data from the status form'),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the status form submission.
|
||||
*
|
||||
* @param int $questionid The question id.
|
||||
* @param string $formdata The question tag form data in a URI encoded param string
|
||||
* @return array The created or modified question tag
|
||||
*/
|
||||
public static function execute($questionid, $formdata) {
|
||||
global $DB;
|
||||
|
||||
$data = [];
|
||||
$result = [
|
||||
'status' => false,
|
||||
'statusname' => ''
|
||||
];
|
||||
|
||||
// Parameter validation.
|
||||
$params = self::validate_parameters(self::execute_parameters(), [
|
||||
'questionid' => $questionid,
|
||||
'formdata' => $formdata
|
||||
]);
|
||||
|
||||
parse_str($params['formdata'], $data);
|
||||
|
||||
$question = question_bank::load_question($params['questionid']);
|
||||
$editingcontext = \context::instance_by_id($question->contextid);
|
||||
self::validate_context($editingcontext);
|
||||
$canedit = question_has_capability_on($question, 'edit');
|
||||
$mform = new \qbank_editquestion\form\question_status_form(null, null, 'post', '', null, $canedit, $data);
|
||||
if ($validateddata = $mform->get_data()) {
|
||||
if ($canedit && isset($validateddata->status)) {
|
||||
$versionrecord = $DB->get_record('question_versions', ['questionid' => $params['questionid']]);
|
||||
$versionrecord->status = $validateddata->status;
|
||||
$DB->update_record('question_versions', $versionrecord);
|
||||
question_bank::notify_question_edited($question->id);
|
||||
$result = [
|
||||
'status' => true,
|
||||
'statusname' => editquestion_helper::get_question_status_string($versionrecord->status)
|
||||
];
|
||||
$event = \core\event\question_updated::create_from_question_instance($question, $editingcontext);
|
||||
$event->trigger();
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns description of method result value.
|
||||
*/
|
||||
public static function execute_returns() {
|
||||
return new external_single_structure([
|
||||
'status' => new external_value(PARAM_BOOL, 'status: true if success'),
|
||||
'statusname' => new external_value(PARAM_RAW, 'statusname: name of the status')
|
||||
]);
|
||||
}
|
||||
}
|
|
@ -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 qbank_editquestion\form;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
require_once($CFG->dirroot . '/lib/formslib.php');
|
||||
|
||||
/**
|
||||
* Class question_status_form to change the question status using a modal.
|
||||
*
|
||||
* @package qbank_editquestion
|
||||
* @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 question_status_form extends \moodleform {
|
||||
|
||||
public function definition() {
|
||||
$mform = $this->_form;
|
||||
$mform->disable_form_change_checker();
|
||||
$mform->addElement('select', 'status', get_string('status', 'qbank_editquestion'),
|
||||
\qbank_editquestion\editquestion_helper::get_question_status_list());
|
||||
}
|
||||
}
|
|
@ -57,4 +57,14 @@ class renderer extends \plugin_renderer_base {
|
|||
return $this->render_from_template('qbank_editquestion/add_new_question', $addquestiondata);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render question information for edit form.
|
||||
*
|
||||
* @param array $questiondata
|
||||
* @return bool|string
|
||||
*/
|
||||
public function render_question_info($questiondata) {
|
||||
return $this->render_from_template('qbank_editquestion/question_info', $questiondata);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -38,7 +38,8 @@ class plugin_feature extends \core_question\local\bank\plugin_features_base{
|
|||
public function get_question_columns($qbank): array {
|
||||
return [
|
||||
new edit_action_column($qbank),
|
||||
new copy_action_column($qbank)
|
||||
new copy_action_column($qbank),
|
||||
new question_status_column($qbank)
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
<?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 qbank_editquestion;
|
||||
|
||||
use core_question\local\bank\column_base;
|
||||
use core_question\local\bank\question_version_status;
|
||||
|
||||
/**
|
||||
* A column to show the status of the question.
|
||||
*
|
||||
* @package qbank_editquestion
|
||||
* @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 question_status_column extends column_base {
|
||||
|
||||
public function get_name(): string {
|
||||
return 'questionstatus';
|
||||
}
|
||||
|
||||
protected function get_title(): string {
|
||||
return get_string('questionstatus', 'qbank_editquestion');
|
||||
}
|
||||
|
||||
protected function display_content($question, $rowclasses): void {
|
||||
global $PAGE;
|
||||
$attributes = [];
|
||||
if (question_has_capability_on($question, 'edit')
|
||||
&& $question->status !== question_version_status::QUESTION_STATUS_HIDDEN) {
|
||||
$target = 'questionstatus_' . $question->id;
|
||||
$datatarget = '[data-target="' . $target . '"]';
|
||||
$PAGE->requires->js_call_amd('qbank_editquestion/question_status', 'init', [$datatarget, $question->contextid]);
|
||||
$attributes = [
|
||||
'data-target' => $target,
|
||||
'data-questionid' => $question->id,
|
||||
'data-courseid' => $this->qbank->course->id,
|
||||
'class' => 'link-primary comment-pointer',
|
||||
'href' => '#'
|
||||
];
|
||||
}
|
||||
echo \html_writer::tag('a', editquestion_helper::get_question_status_string($question->status), $attributes);
|
||||
}
|
||||
|
||||
}
|
|
@ -15,24 +15,21 @@
|
|||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Definition of question/type/random scheduled tasks.
|
||||
* External services definition for qbank_editquestion.
|
||||
*
|
||||
* @package qtype_random
|
||||
* @category task
|
||||
* @copyright 2018 Bo Pierce <email.bO.pierce@gmail.com>
|
||||
* @package qbank_editquestion
|
||||
* @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
|
||||
*/
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$tasks = array(
|
||||
array(
|
||||
'classname' => 'qtype_random\task\remove_unused_questions',
|
||||
'blocking' => 0,
|
||||
'minute' => 'R',
|
||||
'hour' => '*',
|
||||
'day' => '*',
|
||||
'month' => '*',
|
||||
'dayofweek' => '*'
|
||||
)
|
||||
);
|
||||
$functions = [
|
||||
'qbank_editquestion_set_status' => [
|
||||
'classname' => 'qbank_editquestion\external\update_question_version_status',
|
||||
'description' => 'Update the question status.',
|
||||
'type' => 'write',
|
||||
'ajax' => true,
|
||||
],
|
||||
];
|
|
@ -25,3 +25,14 @@
|
|||
|
||||
$string['pluginname'] = 'Edit questions';
|
||||
$string['privacy:metadata'] = 'The Edit questions question bank plugin does not store any user data.';
|
||||
|
||||
// Question status.
|
||||
$string['questionstatus'] = 'Status';
|
||||
$string['questionstatusready'] = 'Ready';
|
||||
$string['questionstatushidden'] = 'Hidden';
|
||||
$string['questionstatusdraft'] = 'Draft';
|
||||
$string['questionstatusheader'] = 'Change question status';
|
||||
|
||||
// Edit form.
|
||||
$string['versioninfo'] = 'Version';
|
||||
$string['status'] = 'Question status';
|
||||
|
|
41
question/bank/editquestion/lib.php
Normal file
41
question/bank/editquestion/lib.php
Normal file
|
@ -0,0 +1,41 @@
|
|||
<?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/>.
|
||||
|
||||
/**
|
||||
* Helper functions and callbacks.
|
||||
*
|
||||
* @package qbank_editquestion
|
||||
* @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
|
||||
*/
|
||||
|
||||
/**
|
||||
* Question status fragment callback.
|
||||
*
|
||||
* @param array $args
|
||||
* @return string rendered output
|
||||
*/
|
||||
function qbank_editquestion_output_fragment_question_status($args): string {
|
||||
global $CFG;
|
||||
require_once($CFG->dirroot . '/question/engine/bank.php');
|
||||
$question = question_bank::load_question($args['questionid']);
|
||||
$mform = new \qbank_editquestion\form\question_status_form();
|
||||
$data = ['status' => $question->status];
|
||||
$mform->set_data($data);
|
||||
|
||||
return $mform->render();
|
||||
}
|
|
@ -108,7 +108,7 @@ if ($cmid) {
|
|||
} else {
|
||||
throw new moodle_exception('missingcourseorcmid', 'question');
|
||||
}
|
||||
$contexts = new question_edit_contexts($thiscontext);
|
||||
$contexts = new core_question\local\bank\question_edit_contexts($thiscontext);
|
||||
$PAGE->set_pagelayout('admin');
|
||||
|
||||
if (optional_param('addcancel', false, PARAM_BOOL)) {
|
||||
|
@ -174,10 +174,12 @@ if ($id) {
|
|||
if (!$formeditable) {
|
||||
question_require_capability_on($question, 'view');
|
||||
}
|
||||
$question->beingcopied = false;
|
||||
if ($makecopy) {
|
||||
// If we are duplicating a question, add some indication to the question name.
|
||||
$question->name = get_string('questionnamecopy', 'question', $question->name);
|
||||
$question->idnumber = core_question_find_next_unused_idnumber($question->idnumber, $category->id);
|
||||
$question->idnumber = isset($question->idnumber) ?
|
||||
core_question_find_next_unused_idnumber($question->idnumber, $category->id) : '';
|
||||
$question->beingcopied = true;
|
||||
}
|
||||
|
||||
|
@ -210,6 +212,17 @@ if ($formeditable && $id) {
|
|||
$toform->appendqnumstring = $appendqnumstring;
|
||||
$toform->returnurl = $originalreturnurl;
|
||||
$toform->makecopy = $makecopy;
|
||||
$toform->idnumber = null;
|
||||
if (isset($question->id)) {
|
||||
$questionobject = question_bank::load_question($question->id);
|
||||
$toform->status = $questionobject->status;
|
||||
$toform->idnumber = $questionobject->idnumber;
|
||||
} else {
|
||||
$toform->status = \core_question\local\bank\question_version_status::QUESTION_STATUS_READY;
|
||||
}
|
||||
if ($makecopy) {
|
||||
$toform->idnumber = core_question_find_next_unused_idnumber($toform->idnumber, $category->id);
|
||||
}
|
||||
if ($cm !== null) {
|
||||
$toform->cmid = $cm->id;
|
||||
$toform->courseid = $cm->course;
|
||||
|
@ -236,7 +249,13 @@ if ($mform->is_cancelled()) {
|
|||
// If we are saving as a copy, break the connection to the old question.
|
||||
if ($makecopy) {
|
||||
$question->id = 0;
|
||||
$question->hidden = 0; // Copies should not be hidden.
|
||||
// Copies should not be hidden.
|
||||
$question->status = \core_question\local\bank\question_version_status::QUESTION_STATUS_READY;
|
||||
}
|
||||
|
||||
// If is will be added directly to a module send the module name to be referenced.
|
||||
if ($appendqnumstring && $cm) {
|
||||
$fromform->modulename = 'mod_' . $cm->modname;
|
||||
}
|
||||
|
||||
// Process the combination of usecurrentcat, categorymoveto and category form
|
||||
|
@ -248,7 +267,7 @@ if ($mform->is_cancelled()) {
|
|||
// If we are moving a question, check we have permission to move it from
|
||||
// whence it came (Where we are moving to is validated by the form).
|
||||
list($newcatid, $newcontextid) = explode(',', $fromform->category);
|
||||
if (!empty($question->id) && $newcatid != $question->category) {
|
||||
if (!empty($question->id) && $newcatid != $question->categoryobject->id) {
|
||||
$contextid = $newcontextid;
|
||||
question_require_capability_on($question, 'move');
|
||||
} else {
|
||||
|
|
|
@ -1,33 +1,32 @@
|
|||
{{!
|
||||
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/>.
|
||||
}}
|
||||
{{!
|
||||
@template qbank_viewcreator/modifier_display
|
||||
@template qbank_editquestion/question_info
|
||||
|
||||
Example context (json):
|
||||
{
|
||||
"displaydata": [
|
||||
{
|
||||
"modifier": "Marc Ghaly",
|
||||
"date": "2 June 2021, 5:32 PM",
|
||||
}
|
||||
"elements": [
|
||||
{"pluginhtml":"<div>Version: 1</div>"},
|
||||
{"pluginhtml":"<div>Usage: 1</div>"}
|
||||
]
|
||||
}
|
||||
}}
|
||||
|
||||
<span class="qbank-creator-name">
|
||||
{{modifier}}
|
||||
</span>
|
||||
<br>
|
||||
<span class="date">
|
||||
{{date}}
|
||||
</span>
|
||||
<div class="question-edit-elements">
|
||||
{{#editelements}}
|
||||
{{{pluginhtml}}}
|
||||
{{/editelements}}
|
||||
</div>
|
|
@ -0,0 +1,39 @@
|
|||
@qbank @qbank_editquestion
|
||||
Feature: Use the qbank plugin manager page for editquestion
|
||||
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 |
|
||||
| Test questions | truefalse | First question second | Answer the first question |
|
||||
|
||||
Scenario: Enable/disable edit question columns 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 "Edit question"
|
||||
And I click on "Disable" "link" in the "Edit question" "table_row"
|
||||
And I am on the "Test quiz" "quiz activity" page
|
||||
And I navigate to "Question bank > Questions" in current page administration
|
||||
Then I should not see "Status"
|
||||
And I click on ".dropdown-toggle" "css_element" in the "First question" "table_row"
|
||||
And I should not see "Edit question" in the "region-main" "region"
|
||||
And I should not see "Duplicate" in the "region-main" "region"
|
||||
And I navigate to "Plugins > Question bank plugins > Manage question bank plugins" in site administration
|
||||
And I click on "Enable" "link" in the "Edit question" "table_row"
|
||||
And I am on the "Test quiz" "quiz activity" page
|
||||
And I navigate to "Question bank > Questions" in current page administration
|
||||
And I click on ".dropdown-toggle" "css_element" in the "First question" "table_row"
|
||||
Then I should see "Status"
|
||||
And I click on ".dropdown-toggle" "css_element" in the "First question" "table_row"
|
||||
And I should see "Edit question" in the "region-main" "region"
|
||||
And I should see "Duplicate" in the "region-main" "region"
|
|
@ -0,0 +1,31 @@
|
|||
@qbank @qbank_editquestion
|
||||
Feature: Use the qbank base view to test the status change using
|
||||
the pop up
|
||||
|
||||
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: Question status modal should change the status of the question
|
||||
Given I log in as "admin"
|
||||
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 "Test questions"
|
||||
And I should see "Ready" in the "First question" "table_row"
|
||||
When I click on "Ready" "link" in the "First question" "table_row"
|
||||
Then I should see "Change question status"
|
||||
And I should see "Question status"
|
||||
And I click on "Close" "button" in the ".modal-dialog" "css_element"
|
||||
And I should see "Ready" in the "First question" "table_row"
|
91
question/bank/editquestion/tests/external/update_question_version_status_test.php
vendored
Normal file
91
question/bank/editquestion/tests/external/update_question_version_status_test.php
vendored
Normal file
|
@ -0,0 +1,91 @@
|
|||
<?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 qbank_editquestion;
|
||||
|
||||
use qbank_editquestion\external\update_question_version_status;
|
||||
|
||||
/**
|
||||
* Submit status external api test.
|
||||
*
|
||||
* @package qbank_editquestion
|
||||
* @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
|
||||
* @coversDefaultClass \core_question\local\bank\question_version_status
|
||||
* @coversDefaultClass \qbank_editquestion\form\question_status_form
|
||||
* @coversDefaultClass \qbank_editquestion\editquestion_helper
|
||||
*/
|
||||
class update_question_version_status_test extends \advanced_testcase {
|
||||
|
||||
/**
|
||||
* Called before every test.
|
||||
*/
|
||||
public function setUp(): void {
|
||||
global $USER;
|
||||
parent::setUp();
|
||||
$this->setAdminUser();
|
||||
$this->course = $this->getDataGenerator()->create_course();
|
||||
$this->user = $USER;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if the submit status webservice changes the status of the question.
|
||||
*
|
||||
* @covers ::mock_generate_submit_keys
|
||||
* @covers ::execute
|
||||
* @covers ::get_question_status_string
|
||||
*/
|
||||
public function test_submit_status_updates_the_question_status() {
|
||||
global $DB;
|
||||
$this->resetAfterTest();
|
||||
$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
|
||||
$cat = $questiongenerator->create_question_category();
|
||||
$numq = $questiongenerator->create_question('essay', null,
|
||||
['category' => $cat->id, 'name' => 'This is the first version']);
|
||||
$data = ['status' => 2];
|
||||
$mform = \qbank_editquestion\form\question_status_form::mock_generate_submit_keys($data);
|
||||
$this->expectException('moodle_exception');
|
||||
list($result, $statusname) = update_question_version_status::execute($numq->id, http_build_query($mform, '', '&'));
|
||||
// Test if the version actually changed.
|
||||
$currentstatus = $DB->get_record('question_versions', ['questionid' => $numq->id]);
|
||||
$this->assertEquals($data['status'], $currentstatus->status);
|
||||
$this->assertEquals(editquestion_helper::get_question_status_string($currentstatus->status), $statusname);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that updating the status does not create a new version.
|
||||
*
|
||||
* @covers ::mock_generate_submit_keys
|
||||
* @covers ::execute
|
||||
*/
|
||||
public function test_submit_status_does_not_create_a_new_version() {
|
||||
global $DB;
|
||||
$this->resetAfterTest();
|
||||
$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
|
||||
$cat = $questiongenerator->create_question_category();
|
||||
$numq = $questiongenerator->create_question('essay', null,
|
||||
['category' => $cat->id, 'name' => 'This is the first version']);
|
||||
$countcurrentrecords = $DB->count_records('question_versions');
|
||||
$this->assertEquals(1, $countcurrentrecords);
|
||||
$data = ['status' => 2];
|
||||
$mform = \qbank_editquestion\form\question_status_form::mock_generate_submit_keys($data);
|
||||
$this->expectException('moodle_exception');
|
||||
list($result, $statusname) = update_question_version_status::execute($numq->id, http_build_query($mform, '', '&'));
|
||||
$countafterupdate = $DB->count_records('question_versions');
|
||||
$this->assertEquals($countcurrentrecords, $countafterupdate);
|
||||
}
|
||||
}
|
|
@ -26,6 +26,6 @@
|
|||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$plugin->component = 'qbank_editquestion';
|
||||
$plugin->version = 2021062800;
|
||||
$plugin->version = 2021110800;
|
||||
$plugin->requires = 2021052500;
|
||||
$plugin->maturity = MATURITY_STABLE;
|
||||
|
|
|
@ -50,7 +50,7 @@ if ($cmid) {
|
|||
require_sesskey();
|
||||
|
||||
// Load the necessary data.
|
||||
$contexts = new question_edit_contexts($thiscontext);
|
||||
$contexts = new core_question\local\bank\question_edit_contexts($thiscontext);
|
||||
$questiondata = question_bank::load_question_data($questionid);
|
||||
|
||||
// Check permissions.
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue