MDL-16263 A way for students to flag/bookmark, particular questions during a quiz attempt for later review.

This is an initial implementation that is now at a working state, but with a few things left to do. It seemed like a good idea to commit it before leaving work on Friday night.
This commit is contained in:
tjhunt 2008-08-29 10:08:27 +00:00
parent 57f43d239a
commit 62e76c6766
18 changed files with 340 additions and 29 deletions

View file

@ -44,6 +44,8 @@ $string['categorycurrent'] = 'Current Category';
$string['categorycurrentuse'] = 'Use This Category'; $string['categorycurrentuse'] = 'Use This Category';
$string['categorymoveto'] = 'Save in Category'; $string['categorymoveto'] = 'Save in Category';
$string['changepublishstatuscat'] = '<a href=\"$a->caturl\">Category \"$a->name\"</a> in course \"$a->coursename\" will have it\'s sharing status changed from <strong>$a->changefrom to $a->changeto</strong>.'; $string['changepublishstatuscat'] = '<a href=\"$a->caturl\">Category \"$a->name\"</a> in course \"$a->coursename\" will have it\'s sharing status changed from <strong>$a->changefrom to $a->changeto</strong>.';
$string['clicktoflag'] = 'Click to flag this question';
$string['clicktounflag'] = 'Click to un-flag this question';
$string['cwrqpfs'] = 'Random questions selecting questions from sub categories.'; $string['cwrqpfs'] = 'Random questions selecting questions from sub categories.';
$string['cwrqpfsinfo'] = '<p>During the upgrade to Moodle 1.9 we will separate question categories into $string['cwrqpfsinfo'] = '<p>During the upgrade to Moodle 1.9 we will separate question categories into
different contexts. Some question categories and questions on your site will have to have their sharing different contexts. Some question categories and questions on your site will have to have their sharing
@ -86,6 +88,8 @@ $string['exporterror'] = 'Errors occur during exporting!';
$string['filesareasite']= 'the site files area'; $string['filesareasite']= 'the site files area';
$string['filesareacourse']= 'the course files area'; $string['filesareacourse']= 'the course files area';
$string['filestomove']= 'Move / copy files to $a?'; $string['filestomove']= 'Move / copy files to $a?';
$string['flagged'] = 'Flagged';
$string['flagthisquestion'] = 'Flag this question';
$string['formquestionnotinids'] = 'Form contained question that is not in questionids'; $string['formquestionnotinids'] = 'Form contained question that is not in questionids';
$string['fractionsnomax'] = 'One of the answers should have a score of 100%% so it is possible to get full marks for this question.'; $string['fractionsnomax'] = 'One of the answers should have a score of 100%% so it is possible to get full marks for this question.';
$string['getcategoryfromfile'] = 'Get category from file'; $string['getcategoryfromfile'] = 'Get category from file';
@ -123,6 +127,7 @@ $string['nopermissionadd'] = 'You don\'t have permission to add questions here.'
$string['noprobs'] = 'No problems found in your question database.'; $string['noprobs'] = 'No problems found in your question database.';
$string['notenoughdatatoeditaquestion'] = 'Neither a question id, nor a category id and question type, was specified.'; $string['notenoughdatatoeditaquestion'] = 'Neither a question id, nor a category id and question type, was specified.';
$string['notenoughdatatomovequestions'] = 'You need to provide the question ids of questions you want to move.'; $string['notenoughdatatomovequestions'] = 'You need to provide the question ids of questions you want to move.';
$string['notflagged'] = 'Not flagged';
$string['novirtualquestiontype'] = 'No virtual question type for question type $a'; $string['novirtualquestiontype'] = 'No virtual question type for question type $a';
$string['parenthesisinproperstart'] = 'Parenthesis before ** is not properly started in $a**'; $string['parenthesisinproperstart'] = 'Parenthesis before ** is not properly started in $a**';
$string['parenthesisinproperclose'] = 'Parenthesis before ** is not properly closed in $a**'; $string['parenthesisinproperclose'] = 'Parenthesis before ** is not properly closed in $a**';

View file

@ -110,6 +110,7 @@ $string['question:add'] = 'Add new questions';
$string['question:config'] = 'Configure question types'; $string['question:config'] = 'Configure question types';
$string['question:editall'] = 'Edit all questions'; $string['question:editall'] = 'Edit all questions';
$string['question:editmine'] = 'Edit your own questions'; $string['question:editmine'] = 'Edit your own questions';
$string['question:flag'] = 'Flag questions while attempting them';
$string['question:managecategory'] = 'Edit question categories'; $string['question:managecategory'] = 'Edit question categories';
$string['question:moveall'] = 'Move all questions'; $string['question:moveall'] = 'Move all questions';
$string['question:movemine'] = 'Move your own questions'; $string['question:movemine'] = 'Move your own questions';

View file

@ -1002,8 +1002,20 @@ $moodle_capabilities = array(
) )
), ),
'moodle/site:doclinks' => array( // While attempting questions, the ability to flag particular questions for later reference.
'moodle/question:flag' => array(
'captype' => 'write',
'contextlevel' => CONTEXT_COURSE,
'legacy' => array(
'student' => CAP_ALLOW,
'teacher' => CAP_ALLOW,
'editingteacher' => CAP_ALLOW,
'coursecreator' => CAP_ALLOW,
'admin' => CAP_ALLOW
)
),
'moodle/site:doclinks' => array(
'captype' => 'read', 'captype' => 'read',
'contextlevel' => CONTEXT_SYSTEM, 'contextlevel' => CONTEXT_SYSTEM,
'legacy' => array( 'legacy' => array(

View file

@ -1043,7 +1043,8 @@
<FIELD NAME="newest" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="true" DEFAULT="0" SEQUENCE="false" ENUM="false" PREVIOUS="questionid" NEXT="newgraded"/> <FIELD NAME="newest" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="true" DEFAULT="0" SEQUENCE="false" ENUM="false" PREVIOUS="questionid" NEXT="newgraded"/>
<FIELD NAME="newgraded" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="true" DEFAULT="0" SEQUENCE="false" ENUM="false" PREVIOUS="newest" NEXT="sumpenalty"/> <FIELD NAME="newgraded" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="true" DEFAULT="0" SEQUENCE="false" ENUM="false" PREVIOUS="newest" NEXT="sumpenalty"/>
<FIELD NAME="sumpenalty" TYPE="number" LENGTH="12" NOTNULL="true" UNSIGNED="false" DEFAULT="0" SEQUENCE="false" ENUM="false" DECIMALS="7" PREVIOUS="newgraded" NEXT="manualcomment"/> <FIELD NAME="sumpenalty" TYPE="number" LENGTH="12" NOTNULL="true" UNSIGNED="false" DEFAULT="0" SEQUENCE="false" ENUM="false" DECIMALS="7" PREVIOUS="newgraded" NEXT="manualcomment"/>
<FIELD NAME="manualcomment" TYPE="text" LENGTH="small" NOTNULL="true" SEQUENCE="false" ENUM="false" PREVIOUS="sumpenalty"/> <FIELD NAME="manualcomment" TYPE="text" LENGTH="small" NOTNULL="true" SEQUENCE="false" ENUM="false" PREVIOUS="sumpenalty" NEXT="flagged"/>
<FIELD NAME="flagged" TYPE="int" LENGTH="2" NOTNULL="true" UNSIGNED="false" DEFAULT="0" SEQUENCE="false" ENUM="false" COMMENT="The person attempting the question may mark certain questions within their question_attempt if the module that owns the attempt allow it. This field stores the status of that flag." PREVIOUS="manualcomment"/>
</FIELDS> </FIELDS>
<KEYS> <KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id" NEXT="attemptid"/> <KEY NAME="primary" TYPE="primary" FIELDS="id" NEXT="attemptid"/>

View file

@ -721,6 +721,22 @@ function xmldb_main_upgrade($oldversion) {
upgrade_main_savepoint($result, 2008082602); upgrade_main_savepoint($result, 2008082602);
} }
if ($result && $oldversion < 2008082700) {
/// Add a new column to the question sessions table to record whether a
/// question has been flagged.
/// Define field flagged to be added to question_sessions
$table = new xmldb_table('question_sessions');
$field = new xmldb_field('flagged', XMLDB_TYPE_INTEGER, '2', null, XMLDB_NOTNULL, null, null, null, '0', 'manualcomment');
/// Conditionally launch add field flagged
if (!$dbman->field_exists($table, $field)) {
$dbman->add_field($table, $field);
}
/// Main savepoint reached
upgrade_main_savepoint($result, 2008082700);
}
return $result; return $result;
} }

View file

@ -94,17 +94,24 @@ define('QUESTION_PREVIEW_POPUP_OPTIONS', 'scrollbars=yes,resizable=yes,width=700
* is how question is Moodle always worked before version 1.5 * is how question is Moodle always worked before version 1.5
*/ */
define('QUESTION_ADAPTIVE', 1); define('QUESTION_ADAPTIVE', 1);
/**#@-*/
/** /**#@+
* options used in forms that move files. * Options used in forms that move files.
*
*/ */
define('QUESTION_FILENOTHINGSELECTED', 0); define('QUESTION_FILENOTHINGSELECTED', 0);
define('QUESTION_FILEDONOTHING', 1); define('QUESTION_FILEDONOTHING', 1);
define('QUESTION_FILECOPY', 2); define('QUESTION_FILECOPY', 2);
define('QUESTION_FILEMOVE', 3); define('QUESTION_FILEMOVE', 3);
define('QUESTION_FILEMOVELINKSONLY', 4); define('QUESTION_FILEMOVELINKSONLY', 4);
/**#@-*/
/**#@+
* Options for whether flags are shown/editable when rendering questions.
*/
define('QUESTION_FLAGSHIDDEN', 0);
define('QUESTION_FLAGSSHOWN', 1);
define('QUESTION_FLAGSEDITABLE', 2);
/**#@-*/ /**#@-*/
/// QTYPES INITIATION ////////////////// /// QTYPES INITIATION //////////////////
@ -909,7 +916,7 @@ function get_question_states(&$questions, $cmoptions, $attempt, $lastattemptid =
// The question field must be listed first so that it is used as the // The question field must be listed first so that it is used as the
// array index in the array returned by $DB->get_records_sql // array index in the array returned by $DB->get_records_sql
$statefields = 'n.questionid as question, s.*, n.sumpenalty, n.manualcomment'; $statefields = 'n.questionid as question, s.*, n.sumpenalty, n.manualcomment, n.flagged, n.id as questionsessionid';
// Load the newest states for the questions // Load the newest states for the questions
$sql = "SELECT $statefields $sql = "SELECT $statefields
FROM {question_states} s, {question_sessions} n FROM {question_states} s, {question_sessions} n
@ -1816,6 +1823,34 @@ function question_format_grade($cmoptions, $grade) {
return format_float($grade, $cmoptions->decimalpoints); return format_float($grade, $cmoptions->decimalpoints);
} }
/**
* @return string An inline script that creates a JavaScript object storing
* various strings and bits of configuration that the scripts in qengine.js need
* to get from PHP.
*/
function question_init_qenginejs_script() {
global $CFG;
// Get the properties we want into a PHP array first, becase that is easier
// to build.
$config = array(
'pixpath' => $CFG->pixpath,
'wwwroot' => $CFG->wwwroot,
'flagtooltip' => get_string('clicktoflag', 'question'),
'unflagtooltip' => get_string('clicktounflag', 'question'),
'flaggedalt' => get_string('flagged', 'question'),
'unflaggedalt' => get_string('notflagged', 'question'),
);
// Then generate the script tag.
$script = '<script type="text/javascript">qengine_config = {' . "\n";
foreach ($config as $property => $value) {
$script .= " $property: '" . addslashes_js($value) . "',\n";
}
$script .= "};</script>\n";
return $script;
}
/// FUNCTIONS THAT SIMPLY WRAP QUESTIONTYPE METHODS ////////////////////////////////// /// FUNCTIONS THAT SIMPLY WRAP QUESTIONTYPE METHODS //////////////////////////////////
/** /**
* Get the HTML that needs to be included in the head tag when the * Get the HTML that needs to be included in the head tag when the
@ -1830,15 +1865,23 @@ function question_format_grade($cmoptions, $grade) {
* @return string some HTML code that can go inside the head tag. * @return string some HTML code that can go inside the head tag.
*/ */
function get_html_head_contributions($questionlist, &$questions, &$states) { function get_html_head_contributions($questionlist, &$questions, &$states) {
global $QTYPES; global $CFG, $QTYPES;
$contributions = array(); // The question engine's own JavaScript.
require_js(array('yui_yahoo','yui_event', 'yui_connection'));
require_js($CFG->wwwroot . '/question/qengine.js');
// An inline script to record various lang strings, etc. that qengine.js needs.
$contributions = array(question_init_qenginejs_script());
// Anything that questions on this page need.
foreach ($questionlist as $questionid) { foreach ($questionlist as $questionid) {
$question = $questions[$questionid]; $question = $questions[$questionid];
$contributions = array_merge($contributions, $contributions = array_merge($contributions,
$QTYPES[$question->qtype]->get_html_head_contributions( $QTYPES[$question->qtype]->get_html_head_contributions(
$question, $states[$questionid])); $question, $states[$questionid]));
} }
return implode("\n", array_unique($contributions)); return implode("\n", array_unique($contributions));
} }
@ -2509,4 +2552,31 @@ function question_get_real_state($state){
} }
} }
/**
* Update the flagged state of a particular question session.
*
* @param integer $sessionid question_session id.
* @param boolean $newstate the new state for the flag.
* @return boolean success or failure.
*/
function question_update_flag($sessionid, $newstate) {
global $DB;
return $DB->set_field('question_sessions', 'flagged', $newstate, array('id' => $sessionid));
}
/**
* @param integer $attemptid the question_attempt id.
* @param integer $questionid the question id.
* @param integer $sessionid the question_session id.
* @param object $user a user, or null to use $USER.
* @return string that needs to be sent to question/toggleflag.php for it to work.
*/
function question_get_toggleflag_checksum($attemptid, $questionid, $sessionid, $user = null) {
if (is_null($user)) {
global $USER;
$user = $USER;
}
return md5($attemptid . "_" . $user->secret . "_" . $questionid . "_" . $sessionid);
}
?> ?>

View file

@ -497,7 +497,7 @@ class quiz_attempt extends quiz {
* @return object the render options for this user on this attempt. * @return object the render options for this user on this attempt.
*/ */
public function get_render_options($state) { public function get_render_options($state) {
return quiz_get_renderoptions($this->quiz->review, $state); return quiz_get_renderoptions($this->quiz, $this->attempt, $this->context, $state);
} }
/** /**
@ -534,7 +534,7 @@ class quiz_attempt extends quiz {
case QUESTION_EVENTCLOSEANDGRADE: case QUESTION_EVENTCLOSEANDGRADE:
case QUESTION_EVENTCLOSE: case QUESTION_EVENTCLOSE:
case QUESTION_EVENTMANUALGRADE: case QUESTION_EVENTMANUALGRADE:
$options = quiz_get_renderoptions($this->quiz->review, $this->states[$questionid]); $options = $this->get_render_options($this->states[$questionid]);
if ($options->scores) { if ($options->scores) {
return question_get_feedback_class($state->last_graded->raw_grade / return question_get_feedback_class($state->last_graded->raw_grade /
$this->questions[$questionid]->maxgrade); $this->questions[$questionid]->maxgrade);
@ -551,6 +551,16 @@ class quiz_attempt extends quiz {
} }
} }
/**
* @param integer $questionid question id of a question that belongs to this quiz.
* @return boolean whether this question hss been flagged by the attempter.
*/
public function is_question_flagged($questionid) {
$this->ensure_state_loaded($questionid);
$state = $this->states[$questionid];
return $state->flagged;
}
/** /**
* Return the grade obtained on a particular question, if the user is permitted to see it. * 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. * You must previously have called load_question_states to load the state data about this question.
@ -560,7 +570,7 @@ class quiz_attempt extends quiz {
*/ */
public function get_question_score($questionid) { public function get_question_score($questionid) {
$this->ensure_state_loaded($questionid); $this->ensure_state_loaded($questionid);
$options = quiz_get_renderoptions($this->quiz->review, $this->states[$questionid]); $options = $this->get_render_options($this->quiz->review, $this->states[$questionid]);
if ($options->scores) { if ($options->scores) {
return quiz_format_grade($this->quiz, $this->states[$questionid]->last_graded->grade); return quiz_format_grade($this->quiz, $this->states[$questionid]->last_graded->grade);
} else { } else {
@ -803,12 +813,20 @@ abstract class quiz_nav_panel_base {
abstract protected function get_end_bits(); abstract protected function get_end_bits();
protected function get_question_state($question) { protected function get_question_state_classes($question) {
$state = 'todo'; // TODO MDL-15653 // The current status of the question.
$classes = $this->attemptobj->get_question_status($question->id);
// Plus a marker for the current page.
if ($question->_page == $this->page) { if ($question->_page == $this->page) {
$state .= ' thispage'; $classes .= ' thispage';
} }
return $state;
// Plus a marker for flagged questions.
if ($this->attemptobj->is_question_flagged($question->id)) {
$classes .= ' flagged';
}
return $classes;
} }
public function display() { public function display() {
@ -833,7 +851,7 @@ class quiz_attempt_nav_panel extends quiz_nav_panel_base {
} }
return '<input type="submit" name="gotopage' . $question->_page . return '<input type="submit" name="gotopage' . $question->_page .
'" value="' . $number . '" class="qnbutton ' . '" value="' . $number . '" class="qnbutton ' .
$this->get_question_state($question) . '"' . $onclick . '/>'; $this->get_question_state_classes($question) . '"' . $onclick . '/>';
} }
protected function get_end_bits() { protected function get_end_bits() {
@ -853,7 +871,7 @@ class quiz_review_nav_panel extends quiz_nav_panel_base {
protected function get_question_button($number, $question) { protected function get_question_button($number, $question) {
return '<a href="' . $this->attemptobj->review_url($question->id) . return '<a href="' . $this->attemptobj->review_url($question->id) .
'" class="qnbutton ' . $this->get_question_state($question) . '" class="qnbutton ' . $this->get_question_state_classes($question) .
'">' . $number . '</a>'; '">' . $number . '</a>';
} }

View file

@ -1257,6 +1257,7 @@ function quiz_get_extra_capabilities() {
'moodle/question:movemine', 'moodle/question:movemine',
'moodle/question:moveall', 'moodle/question:moveall',
'moodle/question:managecategory', 'moodle/question:managecategory',
'moodle/question:flag',
); );
} }

View file

@ -752,15 +752,38 @@ function quiz_question_preview_button($quiz, $question) {
0, 0, $strpreview, QUESTION_PREVIEW_POPUP_OPTIONS, true); 0, 0, $strpreview, QUESTION_PREVIEW_POPUP_OPTIONS, true);
} }
/**
* @param object $attempt the attempt.
* @param object $context the quiz context.
* @return integer whether flags should be shown/editable to the current user for this attempt.
*/
function quiz_get_flag_option($attempt, $context) {
global $USER;
static $flagmode = null;
if (is_null($flagmode)) {
if (!has_capability('moodle/question:flag', $context)) {
$flagmode = QUESTION_FLAGSHIDDEN;
} else if ($attempt->userid == $USER->id) {
$flagmode = QUESTION_FLAGSEDITABLE;
} else {
$flagmode = QUESTION_FLAGSSHOWN;
}
}
return $flagmode;
}
/** /**
* Determine render options * Determine render options
* *
* @param int $reviewoptions * @param int $reviewoptions
* @param object $state * @param object $state
*/ */
function quiz_get_renderoptions($reviewoptions, $state) { function quiz_get_renderoptions($quiz, $attempt, $context, $state) {
$reviewoptions = $quiz->review;
$options = new stdClass; $options = new stdClass;
$options->flags = quiz_get_flag_option($attempt, $context);
// Show the question in readonly (review) mode if the question is in // Show the question in readonly (review) mode if the question is in
// the closed state // the closed state
$options->readonly = question_state_is_closed($state); $options->readonly = question_state_is_closed($state);
@ -791,28 +814,31 @@ function quiz_get_renderoptions($reviewoptions, $state) {
* *
* @param object $quiz the quiz instance. * @param object $quiz the quiz instance.
* @param object $attempt the attempt in question. * @param object $attempt the attempt in question.
* @param $context the roles and permissions context, * @param $context the quiz module context.
* normally the context for the quiz module instance.
* *
* @return object an object with boolean fields responses, scores, feedback, * @return object an object with boolean fields responses, scores, feedback,
* correct_responses, solutions and general feedback * correct_responses, solutions and general feedback
*/ */
function quiz_get_reviewoptions($quiz, $attempt, $context=null) { function quiz_get_reviewoptions($quiz, $attempt, $context) {
global $USER;
$options = new stdClass; $options = new stdClass;
$options->readonly = true; $options->readonly = true;
$options->flags = quiz_get_flag_option($attempt, $context);
// Provide the links to the question review and comment script // Provide the links to the question review and comment script
if (!empty($attempt->id)) { if (!empty($attempt->id)) {
$options->questionreviewlink = '/mod/quiz/reviewquestion.php?attempt=' . $attempt->id; $options->questionreviewlink = '/mod/quiz/reviewquestion.php?attempt=' . $attempt->id;
} }
// Show a link to the comment box only for closed attempts // Show a link to the comment box only for closed attempts
if ($attempt->timefinish && !is_null($context) && has_capability('mod/quiz:grade', $context)) { if ($attempt->timefinish && has_capability('mod/quiz:grade', $context)) {
$options->questioncommentlink = '/mod/quiz/comment.php'; $options->questioncommentlink = '/mod/quiz/comment.php';
} }
// Whether to display a response history. // Whether to display a response history.
$canviewreports = !is_null($context) && has_capability('mod/quiz:viewreports', $context); $canviewreports = has_capability('mod/quiz:viewreports', $context);
$options->history = ($canviewreports && !$attempt->preview) ? 'all' : 'graded'; $options->history = ($canviewreports && !$attempt->preview) ? 'all' : 'graded';
if ($canviewreports && has_capability('moodle/grade:viewhidden', $context) && !$attempt->preview) { if ($canviewreports && has_capability('moodle/grade:viewhidden', $context) && !$attempt->preview) {
@ -867,7 +893,7 @@ function quiz_get_reviewoptions($quiz, $attempt, $context=null) {
* at least one of the attempts, the other showing which options are true * at least one of the attempts, the other showing which options are true
* for all attempts. * for all attempts.
*/ */
function quiz_get_combined_reviewoptions($quiz, $attempts, $context=null) { function quiz_get_combined_reviewoptions($quiz, $attempts, $context) {
$fields = array('readonly', 'scores', 'feedback', 'correct_responses', 'solutions', 'generalfeedback', 'overallfeedback'); $fields = array('readonly', 'scores', 'feedback', 'correct_responses', 'solutions', 'generalfeedback', 'overallfeedback');
$someoptions = new stdClass; $someoptions = new stdClass;
$alloptions = new stdClass; $alloptions = new stdClass;

BIN
pix/i/flagged.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 203 B

BIN
pix/i/unflagged.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 204 B

View file

@ -68,10 +68,13 @@
$quiz->review = $CFG->quiz_review; $quiz->review = $CFG->quiz_review;
require_login($courseid, false); require_login($courseid, false);
$quiz->course = $courseid; $quiz->course = $courseid;
$context = get_context_instance(CONTEXT_COURSE, $courseid);
} else if (!$quiz = $DB->get_record('quiz', array('id' => $quizid))) { } else if (!$quiz = $DB->get_record('quiz', array('id' => $quizid))) {
print_error('invalidquizid', 'quiz', '', $quizid); print_error('invalidquizid', 'quiz', '', $quizid);
} else { } else {
require_login($quiz->course, false, get_coursemodule_from_instance('quiz', $quizid, $quiz->course)); $cm = get_coursemodule_from_instance('quiz', $quizid, $quiz->course);
require_login($quiz->course, false, $cm);
$context = get_context_instance(CONTEXT_MODULE, $cm->id);
} }
@ -187,7 +190,7 @@
} }
// TODO: should not use quiz-specific function here // TODO: should not use quiz-specific function here
$options = quiz_get_renderoptions($quiz->review, $curstate); $options = quiz_get_renderoptions($quiz, $attempt, $context, $curstate);
// Fill in the correct responses (unless the question is in readonly mode) // Fill in the correct responses (unless the question is in readonly mode)
if ($fillcorrect && !$options->readonly) { if ($fillcorrect && !$options->readonly) {

38
question/qengine.js Normal file
View file

@ -0,0 +1,38 @@
// This script, and the YUI libraries that it needs, are inluded by
// the require_js calls in get_html_head_contributions in lib/questionlib.php.
question_flag_changer = {
init_flag: function(checkboxid, postdata) {
var checkbox = document.getElementById(checkboxid);
checkbox.ajaxpostdata = postdata;
checkbox.className += ' jsworking';
question_flag_changer.update_image(checkbox);
YAHOO.util.Event.addListener(checkbox, 'change', this.checkbox_state_change);
YAHOO.util.Event.addListener(checkbox, 'focus', 'blur()');
},
checkbox_state_change: function(e) {
var checkbox = e.target ? e.target : e.srcElement;
question_flag_changer.update_image(checkbox);
var postdata = checkbox.ajaxpostdata
if (checkbox.checked) {
postdata += '&newstate=1'
} else {
postdata += '&newstate=0'
}
YAHOO.util.Connect.asyncRequest('POST', qengine_config.wwwroot + '/question/toggleflag.php', null, postdata);
},
update_image: function(checkbox) {
var img = document.getElementById(checkbox.id + 'img');
if (checkbox.checked) {
img.src = qengine_config.pixpath + '/i/flagged.png';
img.alt = qengine_config.flaggedalt;
img.title = qengine_config.unflagtooltip;
} else {
img.src = qengine_config.pixpath + '/i/unflagged.png';
img.alt = qengine_config.unflaggedalt;
img.title = qengine_config.flagtooltip;
}
}
};

48
question/toggleflag.php Normal file
View file

@ -0,0 +1,48 @@
<?php // $Id$
/**
* Used by ajax calls to toggle the flagged state of a question in an attempt.
* @license http://www.gnu.org/copyleft/gpl.html GNU Public License
* @package questionbank
*/
require_once('../config.php');
require_once($CFG->libdir.'/questionlib.php');
// Parameters
$sessionid = required_param('qsid', PARAM_INT);
$attemptid = required_param('aid', PARAM_INT);
$questionid = required_param('qid', PARAM_INT);
$newstate = required_param('newstate', PARAM_BOOL);
$checksum = required_param('checksum', PARAM_ALPHANUM);
// Check user is logged in.
require_login();
// Check the sesskey.
if (!confirm_sesskey()) {
echo 'sesskey failure';
}
// Check the checksum - it is very hard to know who a question session belongs
// to, so we require that checksum parameter is matches an md5 hash of the
// three ids and the users username. Since we are only updating a flag, that
// probably makes it sufficiently difficult for malicious users to toggle
// other users flags.
if ($checksum != md5($attemptid . "_" . $USER->secret . "_" . $questionid . "_" . $sessionid)) {
echo 'checksum failure';
}
// Check that the requested session really exists
$questionsession = $DB->get_record('question_sessions', array('id' => $sessionid,
'attemptid' => $attemptid, 'questionid' => $questionid));
if (!$questionsession) {
echo 'invalid ids';
}
// Now change state
if (!question_update_flag($sessionid, $newstate)) {
echo 'update failed';
}
echo 'OK';
?>

View file

@ -13,7 +13,8 @@
<div class="grade"> <div class="grade">
<?php echo get_string('marks', 'quiz').': '.$grade; ?> <?php echo get_string('marks', 'quiz').': '.$grade; ?>
</div> </div>
<?php } ?> <?php }
$this->print_question_flag($question, $state, $options->flags); ?>
</div> </div>
<div class="content"> <div class="content">
<?php $this->print_question_formulation_and_controls($question, $state, $cmoptions, $options); <?php $this->print_question_formulation_and_controls($question, $state, $cmoptions, $options);

View file

@ -879,7 +879,69 @@ class default_questiontype {
include "$CFG->dirroot/question/type/question.html"; include "$CFG->dirroot/question/type/question.html";
} }
/* /**
* Render the question flag, assuming $flagsoption allows it. You will probably
* never need to override this method.
*
* @param object $question the question
* @param object $state its current state
* @param integer $flagsoption the option that says whether flags should be displayed.
*/
protected function print_question_flag($question, $state, $flagsoption) {
global $CFG;
switch ($flagsoption) {
case QUESTION_FLAGSSHOWN:
$flagcontent = $this->get_question_flag_tag($state->flagged);
break;
case QUESTION_FLAGSEDITABLE:
$id = $question->name_prefix . '_flagged';
if ($state->flagged) {
$checked = 'checked="checked" ';
} else {
$checked = '';
}
$qsid = $state->questionsessionid;
$aid = $state->attempt;
$qid = $state->question;
$checksum = question_get_toggleflag_checksum($aid, $qid, $qsid);
$postdata = "qsid=$qsid&amp;aid=$aid&amp;qid=$qid&amp;checksum=$checksum&amp;sesskey=" . sesskey();
$flagcontent = '<input type="checkbox" id="' . $id . '" name="' . $id .
'" value="1" ' . $checked . ' />' .
'<label for="' . $id . '">' . $this->get_question_flag_tag(
$state->flagged, $id . 'img') . '</label>' .
"\n" . '<script type="text/javascript">question_flag_changer.init_flag(' .
"'$id', '$postdata');</script>";
break;
default:
$flagcontent = '';
}
if ($flagcontent) {
echo '<div class="questionflag">' . $flagcontent . "</div>\n";
}
}
/**
* Work out the actual img tag needed for the flag
*
* @param boolean $flagged whether the question is currently flagged.
* @param string $id an id to be added as an attribute to the img (optional).
* @return string the img tag.
*/
protected function get_question_flag_tag($flagged, $id = '') {
global $CFG;
if ($id) {
$id = 'id="' . $id . '" ';
}
if ($flagged) {
$img = 'flagged.png';
} else {
$img = 'unflagged.png';
}
return '<img ' . $id . 'src="' . $CFG->pixpath . '/i/' . $img .
'" alt="' . get_string('flagthisquestion', 'question') . '" />';
}
/**
* Print history of responses * Print history of responses
* *
* Used by print_question() * Used by print_question()

View file

@ -2623,6 +2623,15 @@ body.notes .notesgroup {
.que .info div { .que .info div {
margin-left: 1em; margin-left: 1em;
} }
.que .info .questionflag {
margin-top: 1em;
margin-right: 1em;
text-align: center;
}
.que .info .questionflag .jsworking {
position: absolute;
visibility: hidden;
}
.que .content { .que .content {
float: left; float: left;
margin: 0; margin: 0;

View file

@ -6,7 +6,7 @@
// This is compared against the values stored in the database to determine // This is compared against the values stored in the database to determine
// whether upgrades should be performed (see lib/db/*.php) // whether upgrades should be performed (see lib/db/*.php)
$version = 2008082602; // YYYYMMDD = date of the last version bump $version = 2008082702; // YYYYMMDD = date of the last version bump
// XX = daily increments // XX = daily increments
$release = '2.0 dev (Build: 20080829)'; // Human-friendly version name $release = '2.0 dev (Build: 20080829)'; // Human-friendly version name