Merge branch 'master_MDL-71696-versioning-integration' of https://github.com/catalyst/moodle-MDL-70329

This commit is contained in:
Sara Arjona 2022-02-03 13:25:27 +01:00
commit b841a811be
223 changed files with 7768 additions and 2899 deletions

View file

@ -92,7 +92,7 @@ class category_condition extends condition {
$categoryids = [$this->category->id];
}
list($catidtest, $this->params) = $DB->get_in_or_equal($categoryids, SQL_PARAMS_NAMED, 'cat');
$this->where = 'q.category ' . $catidtest;
$this->where = 'qbe.questioncategoryid ' . $catidtest;
}
/**

View file

@ -25,6 +25,8 @@
namespace core_question\bank\search;
use core_question\local\bank\question_version_status;
/**
* This class controls whether hidden / deleted questions are hidden in the list.
*
@ -46,7 +48,8 @@ class hidden_condition extends condition {
public function __construct($hide = true) {
$this->hide = $hide;
if ($hide) {
$this->where = 'q.hidden = 0';
$this->where = "qv.status = '" . question_version_status::QUESTION_STATUS_READY . "' " .
" OR qv.status = '" . question_version_status::QUESTION_STATUS_DRAFT . "' ";
}
}

View file

@ -173,7 +173,7 @@ class core_question_external extends external_api {
$cantag = question_has_capability_on($question, 'tag');
$questioncontext = \context::instance_by_id($question->contextid);
$contexts = new \question_edit_contexts($editingcontext);
$contexts = new \core_question\local\bank\question_edit_contexts($editingcontext);
$formoptions = [
'editingcontext' => $editingcontext,
@ -300,7 +300,7 @@ class core_question_external extends external_api {
$categorycontextid = $DB->get_field('question_categories', 'contextid', ['id' => $categoryid], MUST_EXIST);
$categorycontext = \context::instance_by_id($categorycontextid);
$editcontexts = new \question_edit_contexts($categorycontext);
$editcontexts = new \core_question\local\bank\question_edit_contexts($categorycontext);
// The user must be able to view all questions in the category that they are requesting.
$editcontexts->require_cap('moodle/question:viewall');

View file

@ -47,7 +47,9 @@ abstract class action_column_base extends column_base {
}
public function get_extra_joins(): array {
return ['qc' => 'JOIN {question_categories} qc ON qc.id = q.category'];
return ['qv' => 'JOIN {question_versions} qv ON qv.questionid = q.id',
'qbe' => 'JOIN {question_bank_entries} qbe on qbe.id = qv.questionbankentryid',
'qc' => 'JOIN {question_categories} qc ON qc.id = qbe.questioncategoryid'];
}
public function get_required_fields(): array {

View file

@ -0,0 +1,92 @@
<?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 core_question\local\bank;
/**
* Converts contextlevels to strings and back to help with reading/writing contexts to/from import/export files.
*
* @package core_question
* @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
* @author 2021 Safat Shahin <safatshahin@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class context_to_string_translator {
/**
* @var array used to translate between contextids and strings for this context.
*/
protected $contexttostringarray = [];
/**
* context_to_string_translator constructor.
*
* @param \context[] $contexts
*/
public function __construct($contexts) {
$this->generate_context_to_string_array($contexts);
}
/**
* Context to string.
*
* @param int $contextid
* @return mixed
*/
public function context_to_string($contextid) {
return $this->contexttostringarray[$contextid];
}
/**
* String to context.
*
* @param string $contextname
* @return false|int|string
*/
public function string_to_context($contextname) {
return array_search($contextname, $this->contexttostringarray);
}
/**
* Generate context to array.
*
* @param \context[] $contexts
*/
protected function generate_context_to_string_array($contexts) {
if (!$this->contexttostringarray) {
$catno = 1;
foreach ($contexts as $context) {
switch ($context->contextlevel) {
case CONTEXT_MODULE :
$contextstring = 'module';
break;
case CONTEXT_COURSE :
$contextstring = 'course';
break;
case CONTEXT_COURSECAT :
$contextstring = "cat$catno";
$catno++;
break;
case CONTEXT_SYSTEM :
$contextstring = 'system';
break;
}
$this->contexttostringarray[$context->id] = $contextstring;
}
}
}
}

View file

@ -97,4 +97,12 @@ class edit_menu_column extends column_base {
return ['q.qtype'];
}
/**
* Get menuable actions.
*
* @return menuable_action Menuable actions.
*/
public function get_actions(): array {
return $this->actions;
}
}

View file

@ -0,0 +1,222 @@
<?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 core_question\local\bank;
/**
* Tracks all the contexts related to the one we are currently editing questions and provides helper methods to check permissions.
*
* @package core_question
* @copyright 2007 Jamie Pratt me@jamiep.org
* @author 2021 Safat Shahin <safatshahin@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class question_edit_contexts {
/**
* @var \string[][] array of the capabilities.
*/
public static $caps = [
'editq' => [
'moodle/question:add',
'moodle/question:editmine',
'moodle/question:editall',
'moodle/question:viewmine',
'moodle/question:viewall',
'moodle/question:usemine',
'moodle/question:useall',
'moodle/question:movemine',
'moodle/question:moveall'],
'questions' => [
'moodle/question:add',
'moodle/question:editmine',
'moodle/question:editall',
'moodle/question:viewmine',
'moodle/question:viewall',
'moodle/question:movemine',
'moodle/question:moveall'],
'categories' => [
'moodle/question:managecategory'],
'import' => [
'moodle/question:add'],
'export' => [
'moodle/question:viewall',
'moodle/question:viewmine']];
/**
* @var array of contexts.
*/
protected $allcontexts;
/**
* Constructor
* @param \context $thiscontext the current context.
*/
public function __construct(\context $thiscontext) {
$this->allcontexts = array_values($thiscontext->get_parent_contexts(true));
}
/**
* Get all the contexts.
*
* @return \context[] all parent contexts
*/
public function all() {
return $this->allcontexts;
}
/**
* Get the lowest context.
*
* @return \context lowest context which must be either the module or course context
*/
public function lowest() {
return $this->allcontexts[0];
}
/**
* Get the contexts having cap.
*
* @param string $cap capability
* @return \context[] parent contexts having capability, zero based index
*/
public function having_cap($cap) {
$contextswithcap = [];
foreach ($this->allcontexts as $context) {
if (has_capability($cap, $context)) {
$contextswithcap[] = $context;
}
}
return $contextswithcap;
}
/**
* Get the contexts having at least one cap.
*
* @param array $caps capabilities
* @return \context[] parent contexts having at least one of $caps, zero based index
*/
public function having_one_cap($caps) {
$contextswithacap = [];
foreach ($this->allcontexts as $context) {
foreach ($caps as $cap) {
if (has_capability($cap, $context)) {
$contextswithacap[] = $context;
break; // Done with caps loop.
}
}
}
return $contextswithacap;
}
/**
* Context having at least one cap.
*
* @param string $tabname edit tab name
* @return \context[] parent contexts having at least one of $caps, zero based index
*/
public function having_one_edit_tab_cap($tabname) {
return $this->having_one_cap(self::$caps[$tabname]);
}
/**
* Contexts for adding question and also using it.
*
* @return \context[] those contexts where a user can add a question and then use it.
*/
public function having_add_and_use() {
$contextswithcap = [];
foreach ($this->allcontexts as $context) {
if (!has_capability('moodle/question:add', $context)) {
continue;
}
if (!has_any_capability(['moodle/question:useall', 'moodle/question:usemine'], $context)) {
continue;
}
$contextswithcap[] = $context;
}
return $contextswithcap;
}
/**
* Has at least one parent context got the cap $cap?
*
* @param string $cap capability
* @return boolean
*/
public function have_cap($cap) {
return (count($this->having_cap($cap)));
}
/**
* Has at least one parent context got one of the caps $caps?
*
* @param array $caps capability
* @return boolean
*/
public function have_one_cap($caps) {
foreach ($caps as $cap) {
if ($this->have_cap($cap)) {
return true;
}
}
return false;
}
/**
* Has at least one parent context got one of the caps for actions on $tabname
*
* @param string $tabname edit tab name
* @return boolean
*/
public function have_one_edit_tab_cap($tabname) {
return $this->have_one_cap(self::$caps[$tabname]);
}
/**
* Throw error if at least one parent context hasn't got the cap $cap
*
* @param string $cap capability
*/
public function require_cap($cap) {
if (!$this->have_cap($cap)) {
throw new \moodle_exception('nopermissions', '', '', $cap);
}
}
/**
* Throw error if at least one parent context hasn't got one of the caps $caps
*
* @param array $caps capabilities
*/
public function require_one_cap($caps) {
if (!$this->have_one_cap($caps)) {
$capsstring = join(', ', $caps);
throw new \moodle_exception('nopermissions', '', '', $capsstring);
}
}
/**
* Throw error if at least one parent context hasn't got one of the caps $caps
*
* @param string $tabname edit tab name
*/
public function require_one_edit_tab_cap($tabname) {
if (!$this->have_one_edit_tab_cap($tabname)) {
throw new \moodle_exception('nopermissions', '', '', 'access question edit tab '.$tabname);
}
}
}

View file

@ -0,0 +1,43 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core_question\local\bank;
/**
* Class question_version_status contains the statuses for a question.
*
* @package core_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 question_version_status {
/**
* Const if the question is ready to use.
*/
const QUESTION_STATUS_READY = 'ready';
/**
* Const if the question is hidden.
*/
const QUESTION_STATUS_HIDDEN = 'hidden';
/**
* const if the question is in draft.
*/
const QUESTION_STATUS_DRAFT = 'draft';
}

View file

@ -290,7 +290,25 @@ class random_question_loader {
$fieldsstring = implode(',', $fields);
}
return $DB->get_records_list('question', 'id', $questionids, 'id', $fieldsstring, $offset, $limit);
// Create the query to get the questions (validate that at least we have a question id. If not, do not execute the sql).
$hasquestions = false;
if (!empty($questionids)) {
$hasquestions = true;
}
if ($hasquestions) {
list($condition, $param) = $DB->get_in_or_equal($questionids, SQL_PARAMS_NAMED, 'questionid');
$condition = 'WHERE q.id ' . $condition;
$sql = "SELECT {$fieldsstring}
FROM (SELECT q.*, qbe.questioncategoryid as category
FROM {question} q
JOIN {question_versions} qv ON qv.questionid = q.id
JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
{$condition}) q";
return $DB->get_records_sql($sql, $param, $offset, $limit);
} else {
return [];
}
}
/**

View file

@ -70,7 +70,7 @@ class view {
protected $editquestionurl;
/**
* @var \question_edit_contexts
* @var \core_question\local\bank\question_edit_contexts
*/
protected $contexts;
@ -157,7 +157,7 @@ class view {
/**
* Constructor for view.
*
* @param \question_edit_contexts $contexts
* @param \core_question\local\bank\question_edit_contexts $contexts
* @param \moodle_url $pageurl
* @param object $course course settings
* @param object $cm (optional) activity settings.
@ -244,8 +244,9 @@ class view {
'preview_action_column',
'delete_action_column',
'export_xml_action_column',
'question_status_column',
'creator_name_column',
'modifier_name_column'
'comment_count_column'
];
if (question_get_display_preference('qbshowtext', 0, PARAM_BOOL, new \moodle_url(''))) {
$corequestionbankcolumns[] = 'question_text_row';
@ -560,7 +561,7 @@ class view {
protected function build_query(): void {
// Get the required tables and fields.
$joins = [];
$fields = ['q.hidden', 'q.category'];
$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();
@ -583,7 +584,12 @@ class view {
}
// Build the where clause.
$tests = ['q.parent = 0'];
$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)';
$tests = ['q.parent = 0', $latestversion];
$this->sqlparams = [];
foreach ($this->searchconditions as $searchcondition) {
if ($searchcondition->where()) {
@ -1153,7 +1159,7 @@ class view {
*/
protected function get_row_classes($question, $rowcount): array {
$classes = [];
if ($question->hidden) {
if ($question->status === question_version_status::QUESTION_STATUS_HIDDEN) {
$classes[] = 'dimmed_text';
}
if ($question->id == $this->lastchangedid) {
@ -1219,4 +1225,20 @@ class view {
$this->searchconditions[] = $searchcondition;
}
/**
* Gets visible columns.
* @return array $this->visiblecolumns Visible columns.
*/
public function get_visiblecolumns(): array {
return $this->visiblecolumns;
}
/**
* Get required columns.
*
* @return array Required columns.
*/
public function get_requiredcolumns(): array {
return $this->requiredcolumns;
}
}

View file

@ -126,6 +126,10 @@ class provider implements
// The 'question_statistics' table contains aggregated statistics about responses.
// It does not contain any identifiable user data.
$items->add_database_table('question_bank_entries', [
'ownerid' => 'privacy:metadata:database:question_bank_entries:ownerid',
], 'privacy:metadata:database:question_bank_entries');
// The question subsystem makes use of the qtype, qformat, and qbehaviour plugin types.
$items->add_plugintype_link('qtype', [], 'privacy:metadata:link:qtype');
$items->add_plugintype_link('qformat', [], 'privacy:metadata:link:qformat');
@ -336,12 +340,13 @@ class provider implements
// A user may have created or updated a question.
// Questions are linked against a question category, which has a contextid field.
$sql = "SELECT cat.contextid
$sql = "SELECT qc.contextid
FROM {question} q
INNER JOIN {question_categories} cat ON cat.id = q.category
WHERE
q.createdby = :useridcreated OR
q.modifiedby = :useridmodified";
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.createdby = :useridcreated
OR q.modifiedby = :useridmodified";
$params = [
'useridcreated' => $userid,
'useridmodified' => $userid,
@ -363,9 +368,10 @@ class provider implements
// Questions are linked against a question category, which has a contextid field.
$sql = "SELECT q.createdby, q.modifiedby
FROM {question} q
JOIN {question_categories} cat
ON cat.id = q.category
WHERE cat.contextid = :contextid";
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 = :contextid";
$params = [
'contextid' => $context->id
@ -487,7 +493,8 @@ class provider implements
/**
* Delete all data for all users in the specified context.
*
* @param context $context The specific context to delete data for.
* @param \context $context The specific context to delete data for.
* @throws \dml_exception
*/
public static function delete_data_for_all_users_in_context(\context $context) {
global $DB;
@ -496,17 +503,19 @@ class provider implements
// user. They are still exported in the list of a users data, but they are not removed.
// The userid is instead anonymised.
$DB->set_field_select('question', 'createdby', 0,
'category IN (SELECT id FROM {question_categories} WHERE contextid = :contextid)',
[
'contextid' => $context->id,
]);
$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
JOIN {question_categories} qc ON qc.id = qbe.questioncategoryid
WHERE qc.contextid = ?';
$DB->set_field_select('question', 'modifiedby', 0,
'category IN (SELECT id FROM {question_categories} WHERE contextid = :contextid)',
[
'contextid' => $context->id,
]);
$questions = $DB->get_records_sql($sql, [$context->id]);
foreach ($questions as $question) {
$question->createdby = 0;
$question->modifiedby = 0;
$DB->update_record('question', $question);
}
}
/**
@ -523,15 +532,36 @@ class provider implements
list($contextsql, $contextparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED);
$contextparams['createdby'] = $contextlist->get_user()->id;
$DB->set_field_select('question', 'createdby', 0, "
category IN (SELECT id FROM {question_categories} WHERE contextid {$contextsql})
AND createdby = :createdby", $contextparams);
$questiondata = $DB->get_records_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
JOIN {question_categories} qc ON qc.id = qbe.questioncategoryid
WHERE qc.contextid {$contextsql}
AND q.createdby = :createdby", $contextparams);
foreach ($questiondata as $question) {
$question->createdby = 0;
$DB->update_record('question', $question);
}
list($contextsql, $contextparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED);
$contextparams['modifiedby'] = $contextlist->get_user()->id;
$DB->set_field_select('question', 'modifiedby', 0, "
category IN (SELECT id FROM {question_categories} WHERE contextid {$contextsql})
AND modifiedby = :modifiedby", $contextparams);
$questiondata = $DB->get_records_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
JOIN {question_categories} qc ON qc.id = qbe.questioncategoryid
WHERE qc.contextid {$contextsql}
AND q.modifiedby = :modifiedby", $contextparams);
foreach ($questiondata as $question) {
$question->modifiedby = 0;
$DB->update_record('question', $question);
}
}
/**
@ -554,12 +584,32 @@ class provider implements
$params = ['contextid' => $context->id];
$DB->set_field_select('question', 'createdby', 0, "
category IN (SELECT id FROM {question_categories} WHERE contextid = :contextid)
AND createdby {$createdbysql}", $params + $createdbyparams);
$questiondata = $DB->get_records_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
JOIN {question_categories} qc ON qc.id = qbe.questioncategoryid
WHERE qc.contextid = :contextid
AND q.createdby {$createdbysql}", $params + $createdbyparams);
$DB->set_field_select('question', 'modifiedby', 0, "
category IN (SELECT id FROM {question_categories} WHERE contextid = :contextid)
AND modifiedby {$modifiedbysql}", $params + $modifiedbyparams);
foreach ($questiondata as $question) {
$question->createdby = 0;
$DB->update_record('question', $question);
}
$questiondata = $DB->get_records_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
JOIN {question_categories} qc ON qc.id = qbe.questioncategoryid
WHERE qc.contextid = :contextid
AND q.modifiedby {$modifiedbysql}", $params + $modifiedbyparams);
foreach ($questiondata as $question) {
$question->modifiedby = 0;
$DB->update_record('question', $question);
}
}
}

View file

@ -42,7 +42,7 @@ class all_calculated_for_qubaid_condition {
/**
* @var object[]
*/
public $subquestions;
public $subquestions = [];
/**
* Holds slot (position) stats and stats for variants of questions in slots.

View file

@ -346,7 +346,15 @@ class calculator {
* @param calculated $stats question stats to update.
*/
protected function initial_question_walker($stats) {
$stats->markaverage = $stats->totalmarks / $stats->s;
if ($stats->s != 0) {
$stats->markaverage = $stats->totalmarks / $stats->s;
$stats->othermarkaverage = $stats->totalothermarks / $stats->s;
$stats->summarksaverage = $stats->totalsummarks / $stats->s;
} else {
$stats->markaverage = 0;
$stats->othermarkaverage = 0;
$stats->summarksaverage = 0;
}
if ($stats->maxmark != 0) {
$stats->facility = $stats->markaverage / $stats->maxmark;
@ -354,10 +362,6 @@ class calculator {
$stats->facility = null;
}
$stats->othermarkaverage = $stats->totalothermarks / $stats->s;
$stats->summarksaverage = $stats->totalsummarks / $stats->s;
sort($stats->markarray, SORT_NUMERIC);
sort($stats->othermarksarray, SORT_NUMERIC);