]> git.mjollnir.org Git - moodle.git/commitdiff
MDL-16263 A way for students to flag/bookmark, particular questions during a quiz...
authortjhunt <tjhunt>
Fri, 29 Aug 2008 10:08:27 +0000 (10:08 +0000)
committertjhunt <tjhunt>
Fri, 29 Aug 2008 10:08:27 +0000 (10:08 +0000)
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.

18 files changed:
lang/en_utf8/question.php
lang/en_utf8/role.php
lib/db/access.php
lib/db/install.xml
lib/db/upgrade.php
lib/questionlib.php
mod/quiz/attemptlib.php
mod/quiz/lib.php
mod/quiz/locallib.php
pix/i/flagged.png [new file with mode: 0644]
pix/i/unflagged.png [new file with mode: 0644]
question/preview.php
question/qengine.js [new file with mode: 0644]
question/toggleflag.php [new file with mode: 0644]
question/type/question.html
question/type/questiontype.php
theme/standard/styles_layout.css
version.php

index b7e08fce5e3981775797fcf8df85c95c495f085a..97c5c3bc73c16c74271cd34badb5b7546355051e 100644 (file)
@@ -44,6 +44,8 @@ $string['categorycurrent'] = 'Current Category';
 $string['categorycurrentuse'] = 'Use This 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['clicktoflag'] = 'Click to flag this question';
+$string['clicktounflag'] = 'Click to un-flag this question';
 $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
 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['filesareacourse']= 'the course files area';
 $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['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';
@@ -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['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['notflagged'] = 'Not flagged';
 $string['novirtualquestiontype'] = 'No virtual question type for question type $a';
 $string['parenthesisinproperstart'] = 'Parenthesis before ** is not properly started in $a**';
 $string['parenthesisinproperclose'] = 'Parenthesis before ** is not properly closed in $a**';
index 2f6ba30357c609566b8b408fa24cf16b98b2b2b9..471ec5212b2c2b08670ba6859f8c9f6811cb1d62 100644 (file)
@@ -110,6 +110,7 @@ $string['question:add'] = 'Add new questions';
 $string['question:config'] = 'Configure question types';
 $string['question:editall'] = 'Edit all questions';
 $string['question:editmine'] = 'Edit your own questions';
+$string['question:flag'] = 'Flag questions while attempting them';
 $string['question:managecategory'] = 'Edit question categories';
 $string['question:moveall'] = 'Move all questions';
 $string['question:movemine'] = 'Move your own questions';
index c4c5c938a5408014769a0c4145884902284bffd1..5a9ccfc415a48590db9bb0ef80cb7e401b345425 100644 (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',
         'contextlevel' => CONTEXT_SYSTEM,
         'legacy' => array(
index 707fa3318bbc3740197cc64143bfd9196f33af34..dda246b28408233f173f896426eb0db3f4c74fdf 100644 (file)
         <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="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>
       <KEYS>
         <KEY NAME="primary" TYPE="primary" FIELDS="id" NEXT="attemptid"/>
index ff96c0a0111b1cf25f486f00d3e0773f32f3a9dc..00f20b31d3486d720d53cba17cc51568283072bc 100644 (file)
@@ -721,6 +721,22 @@ function xmldb_main_upgrade($oldversion) {
         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;
 }
index d5474a22c18fd5d4b92c5b2efc5c4e407e942d58..d157c36eb8a1b4a1124ab1fadd67d0d947104d7e 100644 (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
  */
 define('QUESTION_ADAPTIVE', 1);
+/**#@-*/
 
-/**
- * options used in forms that move files.
- *
+/**#@+
+ * Options used in forms that move files.
  */
 define('QUESTION_FILENOTHINGSELECTED', 0);
 define('QUESTION_FILEDONOTHING', 1);
 define('QUESTION_FILECOPY', 2);
 define('QUESTION_FILEMOVE', 3);
 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 //////////////////
@@ -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
     // 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
     $sql = "SELECT $statefields
               FROM {question_states} s, {question_sessions} n
@@ -1816,6 +1823,34 @@ function question_format_grade($cmoptions, $grade) {
     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 //////////////////////////////////
 /**
  * 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.
  */
 function get_html_head_contributions($questionlist, &$questions, &$states) {
-    global $QTYPES;
+    global $CFG, $QTYPES;
+
+    // The question engine's own JavaScript.
+    require_js(array('yui_yahoo','yui_event', 'yui_connection'));
+    require_js($CFG->wwwroot . '/question/qengine.js');
 
-    $contributions = array();
+    // 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) {
         $question = $questions[$questionid];
         $contributions = array_merge($contributions,
                 $QTYPES[$question->qtype]->get_html_head_contributions(
                 $question, $states[$questionid]));
     }
+
     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);
+}
+
 ?>
index 7045b7fb8389e541cda4617d64a5764301806fad..53a9942c08c0054861a64ff28a9cb0d6d54da7a8 100644 (file)
@@ -497,7 +497,7 @@ class quiz_attempt extends quiz {
      * @return object the render options for this user on this attempt.
      */
     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_EVENTCLOSE:
             case QUESTION_EVENTMANUALGRADE:
-                $options = quiz_get_renderoptions($this->quiz->review, $this->states[$questionid]);
+                $options = $this->get_render_options($this->states[$questionid]);
                 if ($options->scores) {
                     return question_get_feedback_class($state->last_graded->raw_grade /
                             $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.
      * 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) {
         $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) {
             return quiz_format_grade($this->quiz, $this->states[$questionid]->last_graded->grade);
         } else {
@@ -803,12 +813,20 @@ abstract class quiz_nav_panel_base {
 
     abstract protected function get_end_bits();
 
-    protected function get_question_state($question) {
-        $state = 'todo'; // TODO MDL-15653
+    protected function get_question_state_classes($question) {
+        // 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) {
-            $state .= ' thispage';
+            $classes .= ' thispage';
+        }
+
+        // Plus a marker for flagged questions.
+        if ($this->attemptobj->is_question_flagged($question->id)) {
+            $classes .= ' flagged';
         }
-        return $state;
+        return $classes;
     }
 
     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 .
                 '" value="' . $number . '" class="qnbutton ' .
-                $this->get_question_state($question) . '"' . $onclick . '/>';
+                $this->get_question_state_classes($question) . '"' . $onclick . '/>';
     }
 
     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) {
         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>';
     }
 
index 3d966e5ceaaa99e051fa2052375ee64c24025ff8..6d2c6a32afc029e1d4d1add41b4505997e5ba309 100644 (file)
@@ -1257,6 +1257,7 @@ function quiz_get_extra_capabilities() {
         'moodle/question:movemine',
         'moodle/question:moveall',
         'moodle/question:managecategory',
+        'moodle/question:flag',
     );
 }
 
index 61565909ed2e2eb7144eae6558e28e6eab6eebc7..17dbc0dbd0adab72fde461fc19b096143330ed93 100644 (file)
@@ -752,15 +752,38 @@ function quiz_question_preview_button($quiz, $question) {
             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
  *
  * @param int $reviewoptions
  * @param object $state
  */
-function quiz_get_renderoptions($reviewoptions, $state) {
+function quiz_get_renderoptions($quiz, $attempt, $context, $state) {
+    $reviewoptions = $quiz->review;
     $options = new stdClass;
 
+    $options->flags = quiz_get_flag_option($attempt, $context);
+
     // Show the question in readonly (review) mode if the question is in
     // the 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 $attempt the attempt in question.
- * @param $context the roles and permissions context,
- *          normally the context for the quiz module instance.
+ * @param $context the quiz module context.
  *
  * @return object an object with boolean fields responses, scores, 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->readonly = true;
 
+    $options->flags = quiz_get_flag_option($attempt, $context);
+
     // Provide the links to the question review and comment script
     if (!empty($attempt->id)) {
         $options->questionreviewlink = '/mod/quiz/reviewquestion.php?attempt=' . $attempt->id;
     }
 
     // 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';
     }
 
     // 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';
 
     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
  *          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');
     $someoptions = new stdClass;
     $alloptions = new stdClass;
diff --git a/pix/i/flagged.png b/pix/i/flagged.png
new file mode 100644 (file)
index 0000000..1e3bed1
Binary files /dev/null and b/pix/i/flagged.png differ
diff --git a/pix/i/unflagged.png b/pix/i/unflagged.png
new file mode 100644 (file)
index 0000000..16d9362
Binary files /dev/null and b/pix/i/unflagged.png differ
index 19779cb9e3b1b0c86be74593753ef89208e1b203..1cbdf50ba7771d38b156df6066867a2a38f1dbee 100644 (file)
         $quiz->review = $CFG->quiz_review;
         require_login($courseid, false);
         $quiz->course = $courseid;
+        $context = get_context_instance(CONTEXT_COURSE, $courseid);
     } else if (!$quiz = $DB->get_record('quiz', array('id' => $quizid))) {
         print_error('invalidquizid', 'quiz', '', $quizid);
     } 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);
     }
 
 
     }
 
     // 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)
     if ($fillcorrect && !$options->readonly) {
diff --git a/question/qengine.js b/question/qengine.js
new file mode 100644 (file)
index 0000000..a72804e
--- /dev/null
@@ -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;
+        }
+    }
+};
diff --git a/question/toggleflag.php b/question/toggleflag.php
new file mode 100644 (file)
index 0000000..d095674
--- /dev/null
@@ -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';
+?>
\ No newline at end of file
index 9a076528aef9408e175c0cc2f1085a42b3201919..12154dc84e58ccf6ef64968db25b4708acb58899 100644 (file)
@@ -13,7 +13,8 @@
       <div class="grade">
         <?php echo get_string('marks', 'quiz').': '.$grade; ?>
       </div>
-    <?php } ?>
+    <?php }
+    $this->print_question_flag($question, $state, $options->flags); ?>
   </div>
   <div class="content">
     <?php $this->print_question_formulation_and_controls($question, $state, $cmoptions, $options);
index 9eda7b8f8a838b22ed501ef890ae2dc2f50572be..296b98fed1b935b72ad6558702f13291ef11b9ef 100644 (file)
@@ -879,7 +879,69 @@ class default_questiontype {
         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
      *
      * Used by print_question()
index 60ca5ec6db70beb4bde65d07c5dddc1d639fbcd9..c29cf6c6d9db06d9ca1626696d2f774c80510e6e 100644 (file)
@@ -2623,6 +2623,15 @@ body.notes .notesgroup {
 .que .info div {
   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 {
   float: left;
   margin: 0;
index 3a91811a51b485a6c7b5200933c73250a8840cf9..581230faaa2a561ec9259d92c0ff7a8171a39cdd 100644 (file)
@@ -6,7 +6,7 @@
 // This is compared against the values stored in the database to determine
 // 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
 
     $release = '2.0 dev (Build: 20080829)';  // Human-friendly version name