mirror of
https://github.com/moodle/moodle.git
synced 2025-01-19 14:27:22 +01:00
Merge branch 'MDL-34399' of git://github.com/timhunt/moodle
This commit is contained in:
commit
3aa721e035
@ -38,6 +38,7 @@ $string['cachedef_config'] = 'Config settings';
|
||||
$string['cachedef_databasemeta'] = 'Database meta information';
|
||||
$string['cachedef_eventinvalidation'] = 'Event invalidation';
|
||||
$string['cachedef_locking'] = 'Locking';
|
||||
$string['cachedef_questiondata'] = 'Question definitions';
|
||||
$string['cachedef_string'] = 'Language string cache';
|
||||
$string['cachelock_file_default'] = 'Default file locking';
|
||||
$string['cachestores'] = 'Cache stores';
|
||||
|
@ -27,12 +27,14 @@
|
||||
*/
|
||||
|
||||
$definitions = array(
|
||||
|
||||
// Used to store processed lang files.
|
||||
'string' => array(
|
||||
'mode' => cache_store::MODE_APPLICATION,
|
||||
'persistent' => true,
|
||||
'persistentmaxsize' => 3
|
||||
),
|
||||
|
||||
// Used to store database meta information.
|
||||
'databasemeta' => array(
|
||||
'mode' => cache_store::MODE_APPLICATION,
|
||||
@ -42,15 +44,27 @@ $definitions = array(
|
||||
'persistent' => true,
|
||||
'persistentmaxsize' => 2
|
||||
),
|
||||
|
||||
// Used to store data from the config + config_plugins table in the database.
|
||||
'config' => array(
|
||||
'mode' => cache_store::MODE_APPLICATION,
|
||||
'persistent' => true
|
||||
),
|
||||
|
||||
// Event invalidation cache.
|
||||
'eventinvalidation' => array(
|
||||
'mode' => cache_store::MODE_APPLICATION,
|
||||
'persistent' => true,
|
||||
'requiredataguarantee' => true
|
||||
)
|
||||
),
|
||||
|
||||
// Cache for question definitions. This is used by the question_bank class.
|
||||
// Users probably do not need to know about this cache. They will just call
|
||||
// question_bank::load_question.
|
||||
'questiondata' => array(
|
||||
'mode' => cache_store::MODE_APPLICATION,
|
||||
'requiredataguarantee' => false,
|
||||
'datasource' => 'question_finder',
|
||||
'datasourcefile' => 'question/engine/bank.php',
|
||||
),
|
||||
);
|
||||
|
@ -343,6 +343,7 @@ function question_delete_question($questionid) {
|
||||
|
||||
// Finally delete the question record itself
|
||||
$DB->delete_records('question', array('id' => $questionid));
|
||||
question_bank::notify_question_edited($questionid);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -607,6 +608,11 @@ function question_move_questions_to_category($questionids, $newcategoryid) {
|
||||
|
||||
// TODO Deal with datasets.
|
||||
|
||||
// Purge these questions from the cache.
|
||||
foreach ($questions as $question) {
|
||||
question_bank::notify_question_edited($question->id);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -626,6 +632,8 @@ function question_move_category_to_context($categoryid, $oldcontextid, $newconte
|
||||
foreach ($questionids as $questionid => $qtype) {
|
||||
question_bank::get_qtype($qtype)->move_files(
|
||||
$questionid, $oldcontextid, $newcontextid);
|
||||
// Purge this question from the cache.
|
||||
question_bank::notify_question_edited($questionid);
|
||||
}
|
||||
|
||||
$subcatids = $DB->get_records_menu('question_categories',
|
||||
@ -860,8 +868,11 @@ function question_hash($question) {
|
||||
* Saves question options
|
||||
*
|
||||
* Simply calls the question type specific save_question_options() method.
|
||||
* @deprecated all code should now call the question type method directly.
|
||||
*/
|
||||
function save_question_options($question) {
|
||||
debugging('Please do not call save_question_options any more. Call the question type method directly.',
|
||||
DEBUG_DEVELOPER);
|
||||
question_bank::get_qtype($question->qtype)->save_question_options($question);
|
||||
}
|
||||
|
||||
@ -1393,21 +1404,11 @@ function question_require_capability_on($question, $cap) {
|
||||
* Get the real state - the correct question id and answer - for a random
|
||||
* question.
|
||||
* @param object $state with property answer.
|
||||
* @return mixed return integer real question id or false if there was an
|
||||
* error..
|
||||
* @deprecated this function has not been relevant since Moodle 2.1!
|
||||
*/
|
||||
function question_get_real_state($state) {
|
||||
global $OUTPUT;
|
||||
$realstate = clone($state);
|
||||
$matches = array();
|
||||
if (!preg_match('|^random([0-9]+)-(.*)|', $state->answer, $matches)) {
|
||||
echo $OUTPUT->notification(get_string('errorrandom', 'quiz_statistics'));
|
||||
return false;
|
||||
} else {
|
||||
$realstate->question = $matches[1];
|
||||
$realstate->answer = $matches[2];
|
||||
return $realstate;
|
||||
}
|
||||
throw new coding_exception('question_get_real_state has not been relevant since Moodle 2.1. ' .
|
||||
'I am not sure what you are trying to do, but stop it at once!');
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -441,9 +441,19 @@ class quiz_attempt {
|
||||
protected $quizobj;
|
||||
protected $attempt;
|
||||
|
||||
// More details of what happened for each question.
|
||||
/** @var question_usage_by_activity the question usage for this quiz attempt. */
|
||||
protected $quba;
|
||||
protected $pagelayout; // Array page no => array of numbers on the page in order.
|
||||
|
||||
/** @var array page no => array of slot numbers on the page in order. */
|
||||
protected $pagelayout;
|
||||
|
||||
/** @var array slot => displayed question number for this slot. (E.g. 1, 2, 3 or 'i'.) */
|
||||
protected $questionnumbers;
|
||||
|
||||
/** @var array slot => page number for this slot. */
|
||||
protected $questionpages;
|
||||
|
||||
/** @var mod_quiz_display_options cache for the appropriate review options. */
|
||||
protected $reviewoptions = null;
|
||||
|
||||
// Constructor =============================================================
|
||||
@ -545,12 +555,12 @@ class quiz_attempt {
|
||||
foreach ($slots as $slot) {
|
||||
$question = $this->quba->get_question($slot);
|
||||
if ($question->length > 0) {
|
||||
$question->_number = $number;
|
||||
$this->questionnumbers[$slot] = $number;
|
||||
$number += $question->length;
|
||||
} else {
|
||||
$question->_number = get_string('infoshort', 'quiz');
|
||||
$this->questionnumbers[$slot] = get_string('infoshort', 'quiz');
|
||||
}
|
||||
$question->_page = $page;
|
||||
$this->questionpages[$slot] = $page;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -906,16 +916,20 @@ class quiz_attempt {
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the grade obtained on a particular question, if the user is permitted
|
||||
* to see it. You must previously have called load_question_states to load the
|
||||
* state data about this question.
|
||||
*
|
||||
* @param int $slot the number used to identify this question within this attempt.
|
||||
* @return string the formatted grade, to the number of decimal places specified
|
||||
* by the quiz.
|
||||
* @return string the displayed question number for the question in this slot.
|
||||
* For example '1', '2', '3' or 'i'.
|
||||
*/
|
||||
public function get_question_number($slot) {
|
||||
return $this->quba->get_question($slot)->_number;
|
||||
return $this->questionnumbers[$slot];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $slot the number used to identify this question within this attempt.
|
||||
* @return int the page of the quiz this question appears on.
|
||||
*/
|
||||
public function get_question_page($slot) {
|
||||
return $this->questionpages[$slot];
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1047,7 +1061,7 @@ class quiz_attempt {
|
||||
*/
|
||||
public function start_attempt_url($slot = null, $page = -1) {
|
||||
if ($page == -1 && !is_null($slot)) {
|
||||
$page = $this->quba->get_question($slot)->_page;
|
||||
$page = $this->get_question_page($slot);
|
||||
} else {
|
||||
$page = 0;
|
||||
}
|
||||
@ -1162,7 +1176,7 @@ class quiz_attempt {
|
||||
public function render_question($slot, $reviewing, $thispageurl = null) {
|
||||
return $this->quba->render_question($slot,
|
||||
$this->get_display_options_with_edit_link($reviewing, $slot, $thispageurl),
|
||||
$this->quba->get_question($slot)->_number);
|
||||
$this->get_question_number($slot));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1178,7 +1192,7 @@ class quiz_attempt {
|
||||
public function render_question_at_step($slot, $seq, $reviewing, $thispageurl = '') {
|
||||
return $this->quba->render_question_at_step($slot, $seq,
|
||||
$this->get_display_options($reviewing),
|
||||
$this->quba->get_question($slot)->_number);
|
||||
$this->get_question_number($slot));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1191,7 +1205,7 @@ class quiz_attempt {
|
||||
$options->hide_all_feedback();
|
||||
$options->manualcomment = question_display_options::EDITABLE;
|
||||
return $this->quba->render_question($slot, $options,
|
||||
$this->quba->get_question($slot)->_number);
|
||||
$this->get_question_number($slot));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1483,7 +1497,7 @@ class quiz_attempt {
|
||||
// Fix up $page.
|
||||
if ($page == -1) {
|
||||
if (!is_null($slot) && !$showall) {
|
||||
$page = $this->quba->get_question($slot)->_page;
|
||||
$page = $this->get_question_page($slot);
|
||||
} else {
|
||||
$page = 0;
|
||||
}
|
||||
@ -1574,14 +1588,14 @@ abstract class quiz_nav_panel_base {
|
||||
|
||||
$button = new quiz_nav_question_button();
|
||||
$button->id = 'quiznavbutton' . $slot;
|
||||
$button->number = $qa->get_question()->_number;
|
||||
$button->number = $this->attemptobj->get_question_number($slot);
|
||||
$button->stateclass = $qa->get_state_class($showcorrectness);
|
||||
$button->navmethod = $this->attemptobj->get_navigation_method();
|
||||
if (!$showcorrectness && $button->stateclass == 'notanswered') {
|
||||
$button->stateclass = 'complete';
|
||||
}
|
||||
$button->statestring = $this->get_state_string($qa, $showcorrectness);
|
||||
$button->currentpage = $qa->get_question()->_page == $this->page;
|
||||
$button->currentpage = $this->attemptobj->get_question_page($slot) == $this->page;
|
||||
$button->flagged = $qa->is_flagged();
|
||||
$button->url = $this->get_question_url($slot);
|
||||
$buttons[] = $button;
|
||||
|
@ -1370,54 +1370,6 @@ function quiz_reset_userdata($data) {
|
||||
return $status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the current user is allowed to view a file uploaded in a quiz.
|
||||
* Teachers can view any from their courses, students can only view their own.
|
||||
*
|
||||
* @param int $attemptuniqueid int attempt id
|
||||
* @param int $questionid int question id
|
||||
* @return bool to indicate access granted or denied
|
||||
*/
|
||||
function quiz_check_file_access($attemptuniqueid, $questionid, $context = null) {
|
||||
global $USER, $DB, $CFG;
|
||||
require_once($CFG->dirroot . '/mod/quiz/locallib.php');
|
||||
|
||||
$attempt = $DB->get_record('quiz_attempts', array('uniqueid' => $attemptuniqueid));
|
||||
$attemptobj = quiz_attempt::create($attempt->id);
|
||||
|
||||
// Does the question exist?
|
||||
if (!$question = $DB->get_record('question', array('id' => $questionid))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($context === null) {
|
||||
$quiz = $DB->get_record('quiz', array('id' => $attempt->quiz));
|
||||
$cm = get_coursemodule_from_id('quiz', $quiz->id);
|
||||
$context = context_module::instance($cm->id);
|
||||
}
|
||||
|
||||
// Load those questions and the associated states.
|
||||
$attemptobj->load_questions(array($questionid));
|
||||
$attemptobj->load_question_states(array($questionid));
|
||||
|
||||
// Obtain the state.
|
||||
$state = $attemptobj->get_question_state($questionid);
|
||||
// Obtain the question.
|
||||
$question = $attemptobj->get_question($questionid);
|
||||
|
||||
// Access granted if the current user submitted this file.
|
||||
if ($attempt->userid != $USER->id) {
|
||||
return false;
|
||||
}
|
||||
// Access granted if the current user has permission to grade quizzes in this course.
|
||||
if (!(has_capability('mod/quiz:viewreports', $context) ||
|
||||
has_capability('mod/quiz:grade', $context))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return array($question, $state, array());
|
||||
}
|
||||
|
||||
/**
|
||||
* Prints quiz summaries on MyMoodle Page
|
||||
* @param arry $courses
|
||||
|
@ -458,7 +458,7 @@ abstract class question_behaviour {
|
||||
$fraction = null;
|
||||
} else if ($fraction > 1 || $fraction < $this->qa->get_min_fraction()) {
|
||||
throw new coding_exception('Score out of range when processing ' .
|
||||
'a manual grading action.', 'Question ' . $this->qa->get_question()->id .
|
||||
'a manual grading action.', 'Question ' . $this->question->id .
|
||||
', slot ' . $this->qa->get_slot() . ', fraction ' . $fraction);
|
||||
}
|
||||
$pendingstep->set_fraction($fraction);
|
||||
|
@ -96,7 +96,7 @@ class qbehaviour_immediatecbm extends qbehaviour_immediatefeedback {
|
||||
return question_attempt::DISCARD;
|
||||
}
|
||||
|
||||
if (!$this->qa->get_question()->is_gradable_response($pendingstep->get_qt_data()) ||
|
||||
if (!$this->question->is_gradable_response($pendingstep->get_qt_data()) ||
|
||||
!$pendingstep->has_behaviour_var('certainty')) {
|
||||
$pendingstep->set_state(question_state::$invalid);
|
||||
return question_attempt::KEEP;
|
||||
|
@ -1521,6 +1521,10 @@ class question_bank_view {
|
||||
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));
|
||||
|
||||
// Purge these questions from the cache.
|
||||
question_bank::notify_question_edited($unhide);
|
||||
|
||||
redirect($this->baseurl);
|
||||
}
|
||||
}
|
||||
|
@ -51,8 +51,6 @@ abstract class question_bank {
|
||||
/** @var array question type name => 1. Records which question definitions have been loaded. */
|
||||
private static $loadedqdefs = array();
|
||||
|
||||
protected static $questionfinder = null;
|
||||
|
||||
/** @var boolean nasty hack to allow unit tests to call {@link load_question()}. */
|
||||
private static $testmode = false;
|
||||
private static $testdata = array();
|
||||
@ -240,6 +238,23 @@ abstract class question_bank {
|
||||
self::$loadedqdefs[$qtypename] = 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method needs to be called whenever a question is edited.
|
||||
*/
|
||||
public static function notify_question_edited($questionid) {
|
||||
question_finder::get_instance()->uncache_question($questionid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a question definition data from the database. The data will be
|
||||
* returned as a plain stdClass object.
|
||||
* @param int $questionid the id of the question to load.
|
||||
* @return object question definition loaded from the database.
|
||||
*/
|
||||
public static function load_question_data($questionid) {
|
||||
return question_finder::get_instance()->load_question_data($questionid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a question definition from the database. The object returned
|
||||
* will actually be of an appropriate {@link question_definition} subclass.
|
||||
@ -256,12 +271,8 @@ abstract class question_bank {
|
||||
return self::return_test_question_data($questionid);
|
||||
}
|
||||
|
||||
$questiondata = $DB->get_record_sql('
|
||||
SELECT q.*, qc.contextid
|
||||
FROM {question} q
|
||||
JOIN {question_categories} qc ON q.category = qc.id
|
||||
WHERE q.id = :id', array('id' => $questionid), MUST_EXIST);
|
||||
get_question_options($questiondata);
|
||||
$questiondata = self::load_question_data($questionid);
|
||||
|
||||
if (!$allowshuffle) {
|
||||
$questiondata->options->shuffleanswers = false;
|
||||
}
|
||||
@ -282,6 +293,7 @@ abstract class question_bank {
|
||||
* @return question_finder a question finder.
|
||||
*/
|
||||
public static function get_finder() {
|
||||
return question_finder::get_instance();
|
||||
if (is_null(self::$questionfinder)) {
|
||||
self::$questionfinder = new question_finder();
|
||||
}
|
||||
@ -418,7 +430,55 @@ abstract class question_bank {
|
||||
* @copyright 2009 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class question_finder {
|
||||
class question_finder implements cache_data_source {
|
||||
/** @var question_finder the singleton instance of this class. */
|
||||
protected static $questionfinder = null;
|
||||
|
||||
/** @var cache the question definition cache. */
|
||||
protected $cache = null;
|
||||
|
||||
/**
|
||||
* @return question_finder a question finder.
|
||||
*/
|
||||
public static function get_instance() {
|
||||
if (is_null(self::$questionfinder)) {
|
||||
self::$questionfinder = new question_finder();
|
||||
}
|
||||
return self::$questionfinder;
|
||||
}
|
||||
|
||||
/* See cache_data_source::get_instance_for_cache. */
|
||||
public static function get_instance_for_cache(cache_definition $definition) {
|
||||
return self::get_instance();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return get the question definition cache we are using.
|
||||
*/
|
||||
protected function get_data_cache() {
|
||||
if ($this->cache == null) {
|
||||
$this->cache = cache::make('core', 'questiondata');
|
||||
}
|
||||
return $this->cache;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method needs to be called whenever a question is edited.
|
||||
*/
|
||||
public function uncache_question($questionid) {
|
||||
$this->get_data_cache()->delete($questionid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a question definition data from the database. The data will be
|
||||
* returned as a plain stdClass object.
|
||||
* @param int $questionid the id of the question to load.
|
||||
* @return object question definition loaded from the database.
|
||||
*/
|
||||
public function load_question_data($questionid) {
|
||||
return $this->get_data_cache()->get($questionid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the ids of all the questions in a list of categoryies.
|
||||
* @param array $categoryids either a categoryid, or a comma-separated list
|
||||
@ -444,4 +504,35 @@ class question_finder {
|
||||
AND hidden = 0
|
||||
$extraconditions", $qcparams + $extraparams, '', 'id,id AS id2');
|
||||
}
|
||||
|
||||
/* See cache_data_source::load_for_cache. */
|
||||
public function load_for_cache($questionid) {
|
||||
global $DB;
|
||||
$questiondata = $DB->get_record_sql('
|
||||
SELECT q.*, qc.contextid
|
||||
FROM {question} q
|
||||
JOIN {question_categories} qc ON q.category = qc.id
|
||||
WHERE q.id = :id', array('id' => $questionid), MUST_EXIST);
|
||||
get_question_options($questiondata);
|
||||
return $questiondata;
|
||||
}
|
||||
|
||||
/* See cache_data_source::load_many_for_cache. */
|
||||
public function load_many_for_cache(array $questionids) {
|
||||
global $DB;
|
||||
list($idcondition, $params) = $DB->get_in_or_equal($questionids);
|
||||
$questiondata = $DB->get_records_sql('
|
||||
SELECT q.*, qc.contextid
|
||||
FROM {question} q
|
||||
JOIN {question_categories} qc ON q.category = qc.id
|
||||
WHERE q.id ' . $idcondition, $params);
|
||||
|
||||
foreach ($questionids as $id) {
|
||||
if (!array_key_exists($id, $questionids)) {
|
||||
throw new dml_missing_record_exception('question', '', array('id' => $id));
|
||||
}
|
||||
get_question_options($questiondata[$id]);
|
||||
}
|
||||
return $questiondata;
|
||||
}
|
||||
}
|
||||
|
@ -281,6 +281,9 @@ if ($mform->is_cancelled()) {
|
||||
}
|
||||
}
|
||||
|
||||
// Purge this question from the cache.
|
||||
question_bank::notify_question_edited($question->id);
|
||||
|
||||
if (($qtypeobj->finished_edit_wizard($fromform)) || $movecontext) {
|
||||
if ($inpopup) {
|
||||
echo $OUTPUT->notification(get_string('changessaved'), '');
|
||||
|
@ -131,7 +131,7 @@ class qtype_shortanswer_question extends question_graded_by_strategy
|
||||
$args, $forcedownload) {
|
||||
if ($component == 'question' && $filearea == 'answerfeedback') {
|
||||
$currentanswer = $qa->get_last_qt_var('answer');
|
||||
$answer = $qa->get_question()->get_matching_answer(array('answer' => $currentanswer));
|
||||
$answer = $this->get_matching_answer(array('answer' => $currentanswer));
|
||||
$answerid = reset($args); // itemid is answer id.
|
||||
return $options->feedback && $answer && $answerid == $answer->id;
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user