Merge branch 'MDL-34399' of git://github.com/timhunt/moodle

This commit is contained in:
Dan Poltawski 2012-10-29 12:18:01 +08:00
commit 3aa721e035
11 changed files with 173 additions and 93 deletions

View File

@ -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';

View File

@ -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',
),
);

View File

@ -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!');
}
/**

View File

@ -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;

View File

@ -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

View File

@ -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);

View File

@ -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;

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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'), '');

View File

@ -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;