2009-05-26 03:23:32 +00:00
< ? php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
2006-02-24 10:21:40 +00:00
/**
2006-03-20 20:45:55 +00:00
* Code for handling and processing questions
*
* This is code that is module independent , i . e . , can be used by any module that
* uses questions , like quiz , lesson , ..
* This script also loads the questiontype classes
* Code for handling the editing of questions is in { @ link question / editlib . php }
*
* TODO : separate those functions which form part of the API
* from the helper functions .
*
2009-05-26 03:23:32 +00:00
* Major Contributors
* - Alex Smith , Julian Sedding and Gustav Delius { @ link http :// maths . york . ac . uk / serving_maths }
*
* @ package moodlecore
* @ subpackage question
* @ copyright 1999 onwards Martin Dougiamas { @ link http :// moodle . com }
* @ license http :// www . gnu . org / copyleft / gpl . html GNU GPL v3 or later
2006-03-20 20:45:55 +00:00
*/
/// CONSTANTS ///////////////////////////////////
2006-02-24 10:21:40 +00:00
2006-02-24 10:25:16 +00:00
/** #@+
2006-03-20 20:45:55 +00:00
* The different types of events that can create question states
*/
2006-03-19 18:28:29 +00:00
define ( 'QUESTION_EVENTOPEN' , '0' ); // The state was created by Moodle
define ( 'QUESTION_EVENTNAVIGATE' , '1' ); // The responses were saved because the student navigated to another page (this is not currently used)
define ( 'QUESTION_EVENTSAVE' , '2' ); // The student has requested that the responses should be saved but not submitted or validated
define ( 'QUESTION_EVENTGRADE' , '3' ); // Moodle has graded the responses. A SUBMIT event can be changed to a GRADE event by Moodle.
define ( 'QUESTION_EVENTDUPLICATE' , '4' ); // The responses submitted were the same as previously
define ( 'QUESTION_EVENTVALIDATE' , '5' ); // The student has requested a validation. This causes the responses to be saved as well, but not graded.
define ( 'QUESTION_EVENTCLOSEANDGRADE' , '6' ); // Moodle has graded the responses. A CLOSE event can be changed to a CLOSEANDGRADE event by Moodle.
define ( 'QUESTION_EVENTSUBMIT' , '7' ); // The student response has been submitted but it has not yet been marked
define ( 'QUESTION_EVENTCLOSE' , '8' ); // The response has been submitted and the session has been closed, either because the student requested it or because Moodle did it (e.g. because of a timelimit). The responses have not been graded.
2006-03-27 17:38:30 +00:00
define ( 'QUESTION_EVENTMANUALGRADE' , '9' ); // Grade was entered by teacher
2008-05-26 11:39:51 +00:00
define ( 'QUESTION_EVENTS_GRADED' , QUESTION_EVENTGRADE . ',' .
QUESTION_EVENTCLOSEANDGRADE . ',' .
QUESTION_EVENTMANUALGRADE );
2008-09-23 09:52:55 +00:00
define ( 'QUESTION_EVENTS_CLOSED' , QUESTION_EVENTCLOSE . ',' .
QUESTION_EVENTCLOSEANDGRADE . ',' .
QUESTION_EVENTMANUALGRADE );
define ( 'QUESTION_EVENTS_CLOSED_OR_GRADED' , QUESTION_EVENTGRADE . ',' .
QUESTION_EVENTS_CLOSED );
2008-07-15 15:30:39 +00:00
2006-02-24 10:25:16 +00:00
/**#@-*/
/** #@+
2006-08-15 21:25:38 +00:00
* The core question types .
2006-03-20 20:45:55 +00:00
*/
2006-03-21 23:08:36 +00:00
define ( " SHORTANSWER " , " shortanswer " );
define ( " TRUEFALSE " , " truefalse " );
define ( " MULTICHOICE " , " multichoice " );
define ( " RANDOM " , " random " );
define ( " MATCH " , " match " );
define ( " RANDOMSAMATCH " , " randomsamatch " );
define ( " DESCRIPTION " , " description " );
define ( " NUMERICAL " , " numerical " );
define ( " MULTIANSWER " , " multianswer " );
define ( " CALCULATED " , " calculated " );
define ( " ESSAY " , " essay " );
2006-02-24 10:25:16 +00:00
/**#@-*/
2006-03-20 20:45:55 +00:00
/**
* Constant determines the number of answer boxes supplied in the editing
* form for multiple choice and similar question types .
*/
2006-02-28 09:26:00 +00:00
define ( " QUESTION_NUMANS " , " 10 " );
2006-02-24 10:25:16 +00:00
2007-01-07 12:46:47 +00:00
/**
* Constant determines the number of answer boxes supplied in the editing
* form for multiple choice and similar question types to start with , with
* the option of adding QUESTION_NUMANS_ADD more answers .
*/
define ( " QUESTION_NUMANS_START " , 3 );
/**
* Constant determines the number of answer boxes to add in the editing
* form for multiple choice and similar question types when the user presses
* 'add form fields button' .
*/
define ( " QUESTION_NUMANS_ADD " , 3 );
2006-08-11 14:59:18 +00:00
/**
* The options used when popping up a question preview window in Javascript .
*/
2007-04-24 16:59:32 +00:00
define ( 'QUESTION_PREVIEW_POPUP_OPTIONS' , 'scrollbars=yes,resizable=yes,width=700,height=540' );
2006-02-24 10:21:40 +00:00
2006-03-20 20:45:55 +00:00
/** #@+
* Option flags for -> optionflags
* The options are read out via bitwise operation using these constants
*/
/**
* Whether the questions is to be run in adaptive mode . If this is not set then
* a question closes immediately after the first submission of responses . This
* is how question is Moodle always worked before version 1.5
*/
define ( 'QUESTION_ADAPTIVE' , 1 );
2008-08-29 10:08:27 +00:00
/**#@-*/
2006-03-20 20:45:55 +00:00
2008-08-29 10:08:27 +00:00
/** #@+
* Options used in forms that move files .
2007-08-09 21:50:59 +00:00
*/
define ( 'QUESTION_FILENOTHINGSELECTED' , 0 );
define ( 'QUESTION_FILEDONOTHING' , 1 );
define ( 'QUESTION_FILECOPY' , 2 );
define ( 'QUESTION_FILEMOVE' , 3 );
define ( 'QUESTION_FILEMOVELINKSONLY' , 4 );
2008-08-29 10:08:27 +00:00
/**#@-*/
2007-08-09 21:50:59 +00:00
2008-08-29 10:08:27 +00:00
/** #@+
* Options for whether flags are shown / editable when rendering questions .
*/
define ( 'QUESTION_FLAGSHIDDEN' , 0 );
define ( 'QUESTION_FLAGSSHOWN' , 1 );
define ( 'QUESTION_FLAGSEDITABLE' , 2 );
2006-03-20 20:45:55 +00:00
/**#@-*/
2009-05-26 03:23:32 +00:00
/**
* GLOBAL VARAIBLES
* @ global array $QTYPES
* @ name $QTYPES
*/
2008-11-28 06:07:11 +00:00
global $QTYPES ;
2006-02-24 10:21:40 +00:00
/**
2008-11-28 06:07:11 +00:00
* Array holding question type objects . Initialised via calls to
* question_register_questiontype as the question type classes are included .
2006-03-20 20:45:55 +00:00
*/
2006-08-15 21:25:38 +00:00
$QTYPES = array ();
/**
* Add a new question type to the various global arrays above .
2007-01-07 12:46:47 +00:00
*
2009-05-26 03:23:32 +00:00
* @ global object
2006-08-15 21:25:38 +00:00
* @ param object $qtype An instance of the new question type class .
*/
function question_register_questiontype ( $qtype ) {
2008-11-28 06:07:11 +00:00
global $QTYPES ;
2007-01-07 12:46:47 +00:00
2006-08-15 21:25:38 +00:00
$name = $qtype -> name ();
$QTYPES [ $name ] = $qtype ;
}
2006-02-24 10:21:40 +00:00
2006-03-24 19:31:46 +00:00
require_once ( " $CFG->dirroot /question/type/questiontype.php " );
2006-02-24 10:21:40 +00:00
2006-08-15 21:25:38 +00:00
// Load the questiontype.php file for each question type
2007-01-07 12:46:47 +00:00
// These files in turn call question_register_questiontype()
2006-08-15 21:25:38 +00:00
// with a new instance of each qtype class.
2009-06-19 14:25:56 +00:00
$qtypenames = get_plugin_list ( 'qtype' );
foreach ( $qtypenames as $qtypename => $qdir ) {
2006-02-24 13:48:43 +00:00
// Instanciates all plug-in question types
2009-06-19 14:25:56 +00:00
$qtypefilepath = " $qdir /questiontype.php " ;
2006-02-24 13:48:43 +00:00
// echo "Loading $qtypename<br/>"; // Uncomment for debugging
if ( is_readable ( $qtypefilepath )) {
require_once ( $qtypefilepath );
2006-02-24 10:21:40 +00:00
}
}
2008-02-28 12:53:02 +00:00
/**
* An array of question type names translated to the user ' s language , suitable for use when
* creating a drop - down menu of options .
*
* Long - time Moodle programmers will realise that this replaces the old $QTYPE_MENU array .
* The array returned will only hold the names of all the question types that the user should
* be able to create directly . Some internal question types like random questions are excluded .
2008-06-12 09:15:16 +00:00
*
2009-05-26 03:23:32 +00:00
* @ global object
2008-02-28 12:53:02 +00:00
* @ return array an array of question type names translated to the user ' s language .
*/
function question_type_menu () {
global $QTYPES ;
2009-03-03 05:13:53 +00:00
static $menuoptions = null ;
if ( is_null ( $menuoptions )) {
2009-03-03 07:47:32 +00:00
$config = get_config ( 'question' );
2009-03-03 05:13:53 +00:00
$menuoptions = array ();
2008-02-28 12:53:02 +00:00
foreach ( $QTYPES as $name => $qtype ) {
2009-03-03 07:47:32 +00:00
// Get the name if this qtype is enabled.
2008-02-28 12:53:02 +00:00
$menuname = $qtype -> menu_name ();
2009-03-03 07:47:32 +00:00
$enabledvar = $name . '_disabled' ;
if ( $menuname && ! isset ( $config -> $enabledvar )) {
2009-03-03 05:13:53 +00:00
$menuoptions [ $name ] = $menuname ;
2008-02-28 12:53:02 +00:00
}
}
2009-03-03 07:47:32 +00:00
$menuoptions = question_sort_qtype_array ( $menuoptions , $config );
2008-02-28 12:53:02 +00:00
}
2009-03-03 05:13:53 +00:00
return $menuoptions ;
2008-02-28 12:53:02 +00:00
}
2009-03-03 07:47:32 +00:00
/**
* Sort an array of question type names according to the question type sort order stored in
* config_plugins . Entries for which there is no xxx_sortorder defined will go
* at the end , sorted according to asort ( $inarray , SORT_LOCALE_STRING ) .
* @ param $inarray an array $qtype => $QTYPES [ $qtype ] -> local_name () .
* @ param $config get_config ( 'question' ), if you happen to have it around , to save one DB query .
* @ return array the sorted version of $inarray .
*/
function question_sort_qtype_array ( $inarray , $config = null ) {
if ( is_null ( $config )) {
$config = get_config ( 'question' );
}
$sortorder = array ();
foreach ( $inarray as $name => $notused ) {
$sortvar = $name . '_sortorder' ;
if ( isset ( $config -> $sortvar )) {
$sortorder [ $config -> $sortvar ] = $name ;
}
}
ksort ( $sortorder );
$outarray = array ();
foreach ( $sortorder as $name ) {
$outarray [ $name ] = $inarray [ $name ];
unset ( $inarray [ $name ]);
}
asort ( $inarray , SORT_LOCALE_STRING );
return array_merge ( $outarray , $inarray );
}
/**
* Move one question type in a list of question types . If you try to move one element
* off of the end , nothing will change .
*
* @ param array $sortedqtypes An array $qtype => anything .
* @ param string $tomove one of the keys from $sortedqtypes
* @ param integer $direction + 1 or - 1
* @ return array an array $index => $qtype , with $index from 0 to n in order , and
* the $qtypes in the same order as $sortedqtypes , except that $tomove will
* have been moved one place .
*/
function question_reorder_qtypes ( $sortedqtypes , $tomove , $direction ) {
$neworder = array_keys ( $sortedqtypes );
// Find the element to move.
$key = array_search ( $tomove , $neworder );
if ( $key === false ) {
return $neworder ;
}
// Work out the other index.
$otherkey = $key + $direction ;
if ( ! isset ( $neworder [ $otherkey ])) {
return $neworder ;
}
// Do the swap.
$swap = $neworder [ $otherkey ];
$neworder [ $otherkey ] = $neworder [ $key ];
$neworder [ $key ] = $swap ;
return $neworder ;
}
/**
* Save a new question type order to the config_plugins table .
2009-05-26 03:23:32 +00:00
* @ global object
2009-03-03 07:47:32 +00:00
* @ param $neworder An arra $index => $qtype . Indices should start at 0 and be in order .
* @ param $config get_config ( 'question' ), if you happen to have it around , to save one DB query .
*/
function question_save_qtype_order ( $neworder , $config = null ) {
global $DB ;
if ( is_null ( $config )) {
$config = get_config ( 'question' );
}
foreach ( $neworder as $index => $qtype ) {
$sortvar = $qtype . '_sortorder' ;
if ( ! isset ( $config -> $sortvar ) || $config -> $sortvar != $index + 1 ) {
set_config ( $sortvar , $index + 1 , 'question' );
}
}
}
2006-02-24 10:21:40 +00:00
/// OTHER CLASSES /////////////////////////////////////////////////////////
/**
2006-03-20 20:45:55 +00:00
* This holds the options that are set by the course module
2009-05-26 03:23:32 +00:00
*
* @ package moodlecore
* @ subpackage question
* @ copyright 1999 onwards Martin Dougiamas { @ link http :// moodle . com }
* @ license http :// www . gnu . org / copyleft / gpl . html GNU GPL v3 or later
2006-03-20 20:45:55 +00:00
*/
2006-02-24 10:21:40 +00:00
class cmoptions {
/**
* Whether a new attempt should be based on the previous one . If true
* then a new attempt will start in a state where all responses are set
* to the last responses from the previous attempt .
*/
var $attemptonlast = false ;
/**
* Various option flags . The flags are accessed via bitwise operations
* using the constants defined in the CONSTANTS section above .
*/
2006-03-20 20:45:55 +00:00
var $optionflags = QUESTION_ADAPTIVE ;
2006-02-24 10:21:40 +00:00
/**
* Determines whether in the calculation of the score for a question
* penalties for earlier wrong responses within the same attempt will
* be subtracted .
*/
var $penaltyscheme = true ;
/**
* The maximum time the user is allowed to answer the questions withing
* an attempt . This is measured in minutes so needs to be multiplied by
* 60 before compared to timestamps . If set to 0 no timelimit will be applied
*/
var $timelimit = 0 ;
/**
* Timestamp for the closing time . Responses submitted after this time will
* be saved but no credit will be given for them .
*/
var $timeclose = 9999999999 ;
/**
* The id of the course from withing which the question is currently being used
*/
var $course = SITEID ;
/**
* Whether the answers in a multiple choice question should be randomly
* shuffled when a new attempt is started .
*/
2006-04-30 08:11:49 +00:00
var $shuffleanswers = true ;
2006-02-24 10:21:40 +00:00
/**
* The number of decimals to be shown when scores are printed
*/
var $decimalpoints = 2 ;
}
/// FUNCTIONS //////////////////////////////////////////////////////
2006-03-20 23:04:22 +00:00
/**
2006-03-21 09:06:34 +00:00
* Returns an array of names of activity modules that use this question
2006-03-20 23:04:22 +00:00
*
2009-05-26 03:23:32 +00:00
* @ global object
* @ global object
2006-03-21 09:06:34 +00:00
* @ param object $questionid
* @ return array of strings
2006-03-20 23:04:22 +00:00
*/
2006-03-21 09:06:34 +00:00
function question_list_instances ( $questionid ) {
2008-06-09 12:16:54 +00:00
global $CFG , $DB ;
2006-03-20 23:04:22 +00:00
$instances = array ();
2008-06-09 12:16:54 +00:00
$modules = $DB -> get_records ( 'modules' );
2006-03-20 23:04:22 +00:00
foreach ( $modules as $module ) {
2008-05-12 17:29:56 +00:00
$fullmod = $CFG -> dirroot . '/mod/' . $module -> name ;
if ( file_exists ( $fullmod . '/lib.php' )) {
include_once ( $fullmod . '/lib.php' );
$fn = $module -> name . '_question_list_instances' ;
if ( function_exists ( $fn )) {
$instances = $instances + $fn ( $questionid );
}
2006-03-20 23:04:22 +00:00
}
}
return $instances ;
}
2006-02-24 10:21:40 +00:00
2008-05-13 21:52:38 +00:00
/**
* Determine whether there arey any questions belonging to this context , that is whether any of its
* question categories contain any questions . This will return true even if all the questions are
* hidden .
*
2009-05-26 03:23:32 +00:00
* @ global object
2008-05-13 21:52:38 +00:00
* @ param mixed $context either a context object , or a context id .
* @ return boolean whether any of the question categories beloning to this context have
* any questions in them .
*/
function question_context_has_any_questions ( $context ) {
2008-06-09 12:16:54 +00:00
global $DB ;
2008-05-13 21:52:38 +00:00
if ( is_object ( $context )) {
$contextid = $context -> id ;
} else if ( is_numeric ( $context )) {
$contextid = $context ;
} else {
print_error ( 'invalidcontextinhasanyquestions' , 'question' );
}
2008-06-09 12:16:54 +00:00
return $DB -> record_exists_sql ( " SELECT *
FROM { question } q
JOIN { question_categories } qc ON qc . id = q . category
WHERE qc . contextid = ? AND q . parent = 0 " , array( $contextid ));
2008-05-13 21:52:38 +00:00
}
2007-01-07 12:46:47 +00:00
/**
2006-03-22 14:43:55 +00:00
* Returns list of 'allowed' grades for grade selection
* formatted suitably for dropdown box function
* @ return object -> gradeoptionsfull full array -> gradeoptions + ve only
*/
function get_grade_options () {
2008-09-26 03:14:05 +00:00
// define basic array of grades. This list comprises all fractions of the form:
// a. p/q for q <= 6, 0 <= p <= q
// b. p/10 for 0 <= p <= 10
// c. 1/q for 1 <= q <= 10
// d. 1/20
2006-03-22 14:43:55 +00:00
$grades = array (
2008-09-26 03:14:05 +00:00
1.0000000 ,
0.9000000 ,
0.8333333 ,
0.8000000 ,
0.7500000 ,
0.7000000 ,
0.6666667 ,
0.6000000 ,
0.5000000 ,
0.4000000 ,
0.3333333 ,
0.3000000 ,
0.2500000 ,
0.2000000 ,
0.1666667 ,
0.1428571 ,
0.1250000 ,
0.1111111 ,
0.1000000 ,
0.0500000 ,
0.0000000 );
2006-03-22 14:43:55 +00:00
// iterate through grades generating full range of options
$gradeoptionsfull = array ();
$gradeoptions = array ();
foreach ( $grades as $grade ) {
$percentage = 100 * $grade ;
$neggrade = - $grade ;
$gradeoptions [ " $grade " ] = " $percentage % " ;
$gradeoptionsfull [ " $grade " ] = " $percentage % " ;
$gradeoptionsfull [ " $neggrade " ] = - $percentage . " % " ;
}
$gradeoptionsfull [ " 0 " ] = $gradeoptions [ " 0 " ] = get_string ( " none " );
// sort lists
arsort ( $gradeoptions , SORT_NUMERIC );
arsort ( $gradeoptionsfull , SORT_NUMERIC );
// construct return object
$grades = new stdClass ;
$grades -> gradeoptions = $gradeoptions ;
$grades -> gradeoptionsfull = $gradeoptionsfull ;
return $grades ;
}
2006-03-22 16:27:46 +00:00
/**
* match grade options
* if no match return error or match nearest
* @ param array $gradeoptionsfull list of valid options
* @ param int $grade grade to be tested
* @ param string $matchgrades 'error' or 'nearest'
* @ return mixed either 'fixed' value or false if erro
*/
function match_grade_options ( $gradeoptionsfull , $grade , $matchgrades = 'error' ) {
// if we just need an error...
if ( $matchgrades == 'error' ) {
foreach ( $gradeoptionsfull as $value => $option ) {
2006-04-13 10:29:20 +00:00
// slightly fuzzy test, never check floats for equality :-)
if ( abs ( $grade - $value ) < 0.00001 ) {
2006-03-22 16:27:46 +00:00
return $grade ;
}
}
// didn't find a match so that's an error
return false ;
}
// work out nearest value
else if ( $matchgrades == 'nearest' ) {
$hownear = array ();
foreach ( $gradeoptionsfull as $value => $option ) {
if ( $grade == $value ) {
return $grade ;
}
$hownear [ $value ] = abs ( $grade - $value );
}
// reverse sort list of deltas and grab the last (smallest)
asort ( $hownear , SORT_NUMERIC );
reset ( $hownear );
return key ( $hownear );
}
else {
return false ;
2007-01-07 12:46:47 +00:00
}
2006-03-22 16:27:46 +00:00
}
2006-03-21 09:06:34 +00:00
/**
* Tests whether a category is in use by any activity module
*
2009-05-26 03:23:32 +00:00
* @ global object
2006-03-21 09:06:34 +00:00
* @ return boolean
2007-01-07 12:46:47 +00:00
* @ param integer $categoryid
2006-03-21 09:06:34 +00:00
* @ param boolean $recursive Whether to examine category children recursively
*/
function question_category_isused ( $categoryid , $recursive = false ) {
2008-06-09 12:16:54 +00:00
global $DB ;
2006-03-21 09:06:34 +00:00
//Look at each question in the category
2009-02-13 06:27:08 +00:00
if ( $questions = $DB -> get_records ( 'question' , array ( 'category' => $categoryid ), '' , 'id,qtype' )) {
2006-03-21 09:06:34 +00:00
foreach ( $questions as $question ) {
if ( count ( question_list_instances ( $question -> id ))) {
return true ;
}
}
}
//Look under child categories recursively
if ( $recursive ) {
2008-06-09 12:16:54 +00:00
if ( $children = $DB -> get_records ( 'question_categories' , array ( 'parent' => $categoryid ))) {
2006-03-21 09:06:34 +00:00
foreach ( $children as $child ) {
if ( question_category_isused ( $child -> id , $recursive )) {
return true ;
}
}
}
}
return false ;
}
2006-03-22 18:27:28 +00:00
/**
* Deletes all data associated to an attempt from the database
*
2009-05-26 03:23:32 +00:00
* @ global object
* @ global object
2006-08-10 21:33:53 +00:00
* @ param integer $attemptid The id of the attempt being deleted
2006-03-22 18:27:28 +00:00
*/
function delete_attempt ( $attemptid ) {
2008-06-09 12:16:54 +00:00
global $QTYPES , $DB ;
2006-03-22 18:27:28 +00:00
2008-06-09 12:16:54 +00:00
$states = $DB -> get_records ( 'question_states' , array ( 'attempt' => $attemptid ));
2007-03-30 10:17:16 +00:00
if ( $states ) {
$stateslist = implode ( ',' , array_keys ( $states ));
2007-08-09 21:50:59 +00:00
2007-03-30 10:17:16 +00:00
// delete question-type specific data
foreach ( $QTYPES as $qtype ) {
$qtype -> delete_states ( $stateslist );
}
2006-03-22 18:27:28 +00:00
}
// delete entries from all other question tables
// It is important that this is done only after calling the questiontype functions
2008-06-09 12:16:54 +00:00
$DB -> delete_records ( " question_states " , array ( " attempt " => $attemptid ));
$DB -> delete_records ( " question_sessions " , array ( " attemptid " => $attemptid ));
$DB -> delete_records ( " question_attempts " , array ( " id " => $attemptid ));
2006-03-22 18:27:28 +00:00
}
2006-03-21 09:06:34 +00:00
2006-02-24 10:21:40 +00:00
/**
2006-03-20 20:45:55 +00:00
* Deletes question and all associated data from the database
*
2006-03-20 23:04:22 +00:00
* It will not delete a question if it is used by an activity module
2009-05-26 03:23:32 +00:00
*
* @ global object
* @ global object
2006-03-20 20:45:55 +00:00
* @ param object $question The question being deleted
*/
2006-03-20 23:04:22 +00:00
function delete_question ( $questionid ) {
2008-06-09 12:16:54 +00:00
global $QTYPES , $DB ;
2007-01-07 12:46:47 +00:00
2008-12-11 05:31:33 +00:00
if ( ! $question = $DB -> get_record ( 'question' , array ( 'id' => $questionid ))) {
// In some situations, for example if this was a child of a
// Cloze question that was previously deleted, the question may already
// have gone. In this case, just do nothing.
return ;
}
2006-03-20 23:04:22 +00:00
// Do not delete a question if it is used by an activity module
2006-03-21 09:06:34 +00:00
if ( count ( question_list_instances ( $questionid ))) {
2006-03-20 23:04:22 +00:00
return ;
}
// delete questiontype-specific data
2007-08-09 21:50:59 +00:00
question_require_capability_on ( $question , 'edit' );
if ( $question ) {
2006-03-21 15:33:30 +00:00
if ( isset ( $QTYPES [ $question -> qtype ])) {
$QTYPES [ $question -> qtype ] -> delete_question ( $questionid );
}
} else {
echo " Question with id $questionid does not exist.<br /> " ;
2006-02-24 10:21:40 +00:00
}
2006-03-20 23:04:22 +00:00
2008-06-09 12:16:54 +00:00
if ( $states = $DB -> get_records ( 'question_states' , array ( 'question' => $questionid ))) {
2006-03-25 21:07:11 +00:00
$stateslist = implode ( ',' , array_keys ( $states ));
2007-01-07 12:46:47 +00:00
2006-03-25 21:07:11 +00:00
// delete questiontype-specific data
foreach ( $QTYPES as $qtype ) {
$qtype -> delete_states ( $stateslist );
}
2006-03-22 18:27:28 +00:00
}
2006-03-20 23:04:22 +00:00
// delete entries from all other question tables
// It is important that this is done only after calling the questiontype functions
2008-06-09 12:16:54 +00:00
$DB -> delete_records ( " question_answers " , array ( " question " => $questionid ));
$DB -> delete_records ( " question_states " , array ( " question " => $questionid ));
$DB -> delete_records ( " question_sessions " , array ( " questionid " => $questionid ));
2006-03-20 23:04:22 +00:00
// Now recursively delete all child questions
2009-02-13 06:27:08 +00:00
if ( $children = $DB -> get_records ( 'question' , array ( 'parent' => $questionid ), '' , 'id,qtype' )) {
2006-02-24 10:21:40 +00:00
foreach ( $children as $child ) {
2006-08-16 16:17:18 +00:00
if ( $child -> id != $questionid ) {
delete_question ( $child -> id );
}
2006-02-24 10:21:40 +00:00
}
}
2007-01-07 12:46:47 +00:00
2006-03-20 23:04:22 +00:00
// Finally delete the question record itself
2008-06-09 12:16:54 +00:00
$DB -> delete_records ( 'question' , array ( 'id' => $questionid ));
2006-03-20 23:04:22 +00:00
return ;
2006-02-24 10:21:40 +00:00
}
2006-03-21 09:06:34 +00:00
/**
2007-08-09 21:50:59 +00:00
* All question categories and their questions are deleted for this course .
2006-03-21 09:06:34 +00:00
*
2009-05-26 03:23:32 +00:00
* @ global object
2007-08-09 21:50:59 +00:00
* @ param object $mod an object representing the activity
2006-03-21 09:06:34 +00:00
* @ param boolean $feedback to specify if the process must output a summary of its work
* @ return boolean
*/
function question_delete_course ( $course , $feedback = true ) {
2008-06-09 12:16:54 +00:00
global $DB ;
2006-03-21 09:06:34 +00:00
//To store feedback to be showed at the end of the process
$feedbackdata = array ();
//Cache some strings
$strcatdeleted = get_string ( 'unusedcategorydeleted' , 'quiz' );
2007-08-09 21:50:59 +00:00
$coursecontext = get_context_instance ( CONTEXT_COURSE , $course -> id );
2008-06-09 12:16:54 +00:00
$categoriescourse = $DB -> get_records ( 'question_categories' , array ( 'contextid' => $coursecontext -> id ), 'parent' , 'id, parent, name' );
2006-03-21 09:06:34 +00:00
2007-08-09 21:50:59 +00:00
if ( $categoriescourse ) {
2006-03-21 09:06:34 +00:00
//Sort categories following their tree (parent-child) relationships
2007-08-09 21:50:59 +00:00
//this will make the feedback more readable
$categoriescourse = sort_categories_by_tree ( $categoriescourse );
foreach ( $categoriescourse as $category ) {
//Delete it completely (questions and category itself)
//deleting questions
2009-02-13 06:27:08 +00:00
if ( $questions = $DB -> get_records ( 'question' , array ( 'category' => $category -> id ), '' , 'id,qtype' )) {
2007-08-09 21:50:59 +00:00
foreach ( $questions as $question ) {
delete_question ( $question -> id );
2006-03-21 09:06:34 +00:00
}
2008-06-09 12:16:54 +00:00
$DB -> delete_records ( " question " , array ( " category " => $category -> id ));
2007-08-09 21:50:59 +00:00
}
//delete the category
2008-06-09 12:16:54 +00:00
$DB -> delete_records ( 'question_categories' , array ( 'id' => $category -> id ));
2006-03-21 09:06:34 +00:00
2007-08-09 21:50:59 +00:00
//Fill feedback
$feedbackdata [] = array ( $category -> name , $strcatdeleted );
}
//Inform about changes performed if feedback is enabled
if ( $feedback ) {
$table = new stdClass ;
$table -> head = array ( get_string ( 'category' , 'quiz' ), get_string ( 'action' ));
$table -> data = $feedbackdata ;
print_table ( $table );
}
}
return true ;
}
2006-03-21 09:06:34 +00:00
2008-05-13 21:52:38 +00:00
/**
* Category is about to be deleted ,
* 1 / All question categories and their questions are deleted for this course category .
* 2 / All questions are moved to new category
*
2009-05-26 03:23:32 +00:00
* @ global object
2008-05-13 21:52:38 +00:00
* @ param object $category course category object
* @ param object $newcategory empty means everything deleted , otherwise id of category where content moved
* @ param boolean $feedback to specify if the process must output a summary of its work
* @ return boolean
*/
function question_delete_course_category ( $category , $newcategory , $feedback = true ) {
2009-08-18 05:20:12 +00:00
global $DB , $OUTPUT ;
2008-06-01 15:52:12 +00:00
2008-05-13 21:52:38 +00:00
$context = get_context_instance ( CONTEXT_COURSECAT , $category -> id );
if ( empty ( $newcategory )) {
$feedbackdata = array (); // To store feedback to be showed at the end of the process
$rescueqcategory = null ; // See the code around the call to question_save_from_deletion.
$strcatdeleted = get_string ( 'unusedcategorydeleted' , 'quiz' );
// Loop over question categories.
2008-06-09 12:16:54 +00:00
if ( $categories = $DB -> get_records ( 'question_categories' , array ( 'contextid' => $context -> id ), 'parent' , 'id, parent, name' )) {
2008-05-13 21:52:38 +00:00
foreach ( $categories as $category ) {
2008-06-12 09:15:16 +00:00
2008-05-13 21:52:38 +00:00
// Deal with any questions in the category.
2009-02-13 06:27:08 +00:00
if ( $questions = $DB -> get_records ( 'question' , array ( 'category' => $category -> id ), '' , 'id,qtype' )) {
2008-05-13 21:52:38 +00:00
// Try to delete each question.
foreach ( $questions as $question ) {
delete_question ( $question -> id );
}
// Check to see if there were any questions that were kept because they are
// still in use somehow, even though quizzes in courses in this category will
// already have been deteted. This could happen, for example, if questions are
// added to a course, and then that course is moved to another category (MDL-14802).
2008-06-01 15:52:12 +00:00
$questionids = $DB -> get_records_menu ( 'question' , array ( 'category' => $category -> id ), '' , 'id,1' );
2008-05-13 21:52:38 +00:00
if ( ! empty ( $questionids )) {
if ( ! $rescueqcategory = question_save_from_deletion ( implode ( ',' , array_keys ( $questionids )),
get_parent_contextid ( $context ), print_context_name ( $context ), $rescueqcategory )) {
return false ;
}
$feedbackdata [] = array ( $category -> name , get_string ( 'questionsmovedto' , 'question' , $rescueqcategory -> name ));
}
}
// Now delete the category.
2008-06-09 12:16:54 +00:00
if ( ! $DB -> delete_records ( 'question_categories' , array ( 'id' => $category -> id ))) {
2008-05-13 21:52:38 +00:00
return false ;
}
$feedbackdata [] = array ( $category -> name , $strcatdeleted );
} // End loop over categories.
}
// Output feedback if requested.
if ( $feedback and $feedbackdata ) {
$table = new stdClass ;
$table -> head = array ( get_string ( 'questioncategory' , 'question' ), get_string ( 'action' ));
$table -> data = $feedbackdata ;
print_table ( $table );
}
} else {
// Move question categories ot the new context.
if ( ! $newcontext = get_context_instance ( CONTEXT_COURSECAT , $newcategory -> id )) {
return false ;
}
2008-06-09 12:16:54 +00:00
if ( ! $DB -> set_field ( 'question_categories' , 'contextid' , $newcontext -> id , array ( 'contextid' => $context -> id ))) {
2008-05-13 21:52:38 +00:00
return false ;
}
if ( $feedback ) {
$a = new stdClass ;
$a -> oldplace = print_context_name ( $context );
$a -> newplace = print_context_name ( $newcontext );
2009-08-18 05:20:12 +00:00
echo $OUTPUT -> notification ( get_string ( 'movedquestionsandcategories' , 'question' , $a ), 'notifysuccess' );
2008-05-13 21:52:38 +00:00
}
}
return true ;
}
/**
* Enter description here ...
*
2009-05-26 03:23:32 +00:00
* @ global object
2008-05-13 21:52:38 +00:00
* @ param string $questionids list of questionids
* @ param object $newcontext the context to create the saved category in .
2008-06-12 09:15:16 +00:00
* @ param string $oldplace a textual description of the think being deleted , e . g . from get_context_name
2008-05-13 21:52:38 +00:00
* @ param object $newcategory
2008-06-12 09:15:16 +00:00
* @ return mixed false on
2008-05-13 21:52:38 +00:00
*/
function question_save_from_deletion ( $questionids , $newcontextid , $oldplace , $newcategory = null ) {
2008-06-09 12:16:54 +00:00
global $DB ;
2008-05-13 21:52:38 +00:00
// Make a category in the parent context to move the questions to.
if ( is_null ( $newcategory )) {
$newcategory = new object ();
$newcategory -> parent = 0 ;
$newcategory -> contextid = $newcontextid ;
2008-06-09 12:16:54 +00:00
$newcategory -> name = get_string ( 'questionsrescuedfrom' , 'question' , $oldplace );
$newcategory -> info = get_string ( 'questionsrescuedfrominfo' , 'question' , $oldplace );
2008-05-13 21:52:38 +00:00
$newcategory -> sortorder = 999 ;
$newcategory -> stamp = make_unique_id_code ();
2009-06-13 18:16:08 +00:00
$newcategory -> id = $DB -> insert_record ( 'question_categories' , $newcategory );
2008-05-13 21:52:38 +00:00
}
// Move any remaining questions to the 'saved' category.
if ( ! question_move_questions_to_category ( $questionids , $newcategory -> id )) {
return false ;
}
return $newcategory ;
}
2007-08-09 21:50:59 +00:00
/**
* All question categories and their questions are deleted for this activity .
*
2009-05-26 03:23:32 +00:00
* @ global object
2007-08-09 21:50:59 +00:00
* @ param object $cm the course module object representing the activity
* @ param boolean $feedback to specify if the process must output a summary of its work
* @ return boolean
*/
function question_delete_activity ( $cm , $feedback = true ) {
2008-06-09 12:16:54 +00:00
global $DB ;
2007-08-09 21:50:59 +00:00
//To store feedback to be showed at the end of the process
$feedbackdata = array ();
2006-03-21 09:06:34 +00:00
2007-08-09 21:50:59 +00:00
//Cache some strings
$strcatdeleted = get_string ( 'unusedcategorydeleted' , 'quiz' );
$modcontext = get_context_instance ( CONTEXT_MODULE , $cm -> id );
2008-06-09 12:16:54 +00:00
if ( $categoriesmods = $DB -> get_records ( 'question_categories' , array ( 'contextid' => $modcontext -> id ), 'parent' , 'id, parent, name' )){
2007-08-09 21:50:59 +00:00
//Sort categories following their tree (parent-child) relationships
//this will make the feedback more readable
$categoriesmods = sort_categories_by_tree ( $categoriesmods );
2006-03-21 09:06:34 +00:00
2007-08-09 21:50:59 +00:00
foreach ( $categoriesmods as $category ) {
2006-03-21 09:06:34 +00:00
2007-08-09 21:50:59 +00:00
//Delete it completely (questions and category itself)
//deleting questions
2009-02-13 06:27:08 +00:00
if ( $questions = $DB -> get_records ( 'question' , array ( 'category' => $category -> id ), '' , 'id,qtype' )) {
2007-08-09 21:50:59 +00:00
foreach ( $questions as $question ) {
delete_question ( $question -> id );
}
2008-06-09 12:16:54 +00:00
$DB -> delete_records ( " question " , array ( " category " => $category -> id ));
2006-03-21 09:06:34 +00:00
}
2007-08-09 21:50:59 +00:00
//delete the category
2008-06-09 12:16:54 +00:00
$DB -> delete_records ( 'question_categories' , array ( 'id' => $category -> id ));
2007-08-09 21:50:59 +00:00
//Fill feedback
$feedbackdata [] = array ( $category -> name , $strcatdeleted );
2006-03-21 09:06:34 +00:00
}
//Inform about changes performed if feedback is enabled
if ( $feedback ) {
2006-07-18 15:34:24 +00:00
$table = new stdClass ;
2006-03-21 09:06:34 +00:00
$table -> head = array ( get_string ( 'category' , 'quiz' ), get_string ( 'action' ));
$table -> data = $feedbackdata ;
print_table ( $table );
}
}
return true ;
}
2008-05-09 15:05:36 +00:00
/**
* This function should be considered private to the question bank , it is called from
* question / editlib . php question / contextmoveq . php and a few similar places to to the work of
* acutally moving questions and associated data . However , callers of this function also have to
* do other work , which is why you should not call this method directly from outside the questionbank .
*
2009-05-26 03:23:32 +00:00
* @ global object
2008-05-09 15:05:36 +00:00
* @ param string $questionids a comma - separated list of question ids .
* @ param integer $newcategory the id of the category to move to .
*/
function question_move_questions_to_category ( $questionids , $newcategory ) {
2008-06-01 16:14:59 +00:00
global $DB ;
2008-05-09 15:05:36 +00:00
$result = true ;
// Move the questions themselves.
2008-06-01 16:14:59 +00:00
$result = $result && $DB -> set_field_select ( 'question' , 'category' , $newcategory , " id IN ( $questionids ) " );
2008-05-09 15:05:36 +00:00
// Move any subquestions belonging to them.
2008-06-01 16:14:59 +00:00
$result = $result && $DB -> set_field_select ( 'question' , 'category' , $newcategory , " parent IN ( $questionids ) " );
2008-05-09 15:05:36 +00:00
// TODO Deal with datasets.
return $result ;
}
2007-08-09 21:50:59 +00:00
/**
2009-05-26 03:23:32 +00:00
* @ global object
* @ global object
2007-08-09 21:50:59 +00:00
* @ param array $row tab objects
* @ param question_edit_contexts $contexts object representing contexts available from this context
* @ param string $querystring to append to urls
* */
function questionbank_navigation_tabs ( & $row , $contexts , $querystring ) {
global $CFG , $QUESTION_EDITTABCAPS ;
$tabs = array (
'questions' => array ( " $CFG->wwwroot /question/edit.php? $querystring " , get_string ( 'questions' , 'quiz' ), get_string ( 'editquestions' , 'quiz' )),
'categories' => array ( " $CFG->wwwroot /question/category.php? $querystring " , get_string ( 'categories' , 'quiz' ), get_string ( 'editqcats' , 'quiz' )),
'import' => array ( " $CFG->wwwroot /question/import.php? $querystring " , get_string ( 'import' , 'quiz' ), get_string ( 'importquestions' , 'quiz' )),
'export' => array ( " $CFG->wwwroot /question/export.php? $querystring " , get_string ( 'export' , 'quiz' ), get_string ( 'exportquestions' , 'quiz' )));
foreach ( $tabs as $tabname => $tabparams ){
if ( $contexts -> have_one_edit_tab_cap ( $tabname )) {
$row [] = new tabobject ( $tabname , $tabparams [ 0 ], $tabparams [ 1 ], $tabparams [ 2 ]);
}
2007-03-21 10:41:28 +00:00
}
}
2008-04-04 02:54:20 +00:00
/**
2008-07-08 16:33:47 +00:00
* Given a list of ids , load the basic information about a set of questions from the questions table .
* The $join and $extrafields arguments can be used together to pull in extra data .
* See , for example , the usage in mod / quiz / attemptlib . php , and
* read the code below to see how the SQL is assembled . Throws exceptions on error .
2008-04-04 02:54:20 +00:00
*
2009-05-26 03:23:32 +00:00
* @ global object
* @ global object
2008-07-08 16:33:47 +00:00
* @ param array $questionids array of question ids .
* @ param string $extrafields extra SQL code to be added to the query .
* @ param string $join extra SQL code to be added to the query .
* @ param array $extraparams values for any placeholders in $join .
* You are strongly recommended to use named placeholder .
2008-04-04 02:54:20 +00:00
*
2008-07-08 16:33:47 +00:00
* @ return array partially complete question objects . You need to call get_question_options
* on them before they can be properly used .
2008-04-04 02:54:20 +00:00
*/
2008-07-08 16:33:47 +00:00
function question_preload_questions ( $questionids , $extrafields = '' , $join = '' , $extraparams = array ()) {
2008-06-09 12:16:54 +00:00
global $CFG , $DB ;
2008-07-25 14:18:02 +00:00
if ( empty ( $questionids )) {
return array ();
}
2008-04-04 02:54:20 +00:00
if ( $join ) {
2008-07-08 16:33:47 +00:00
$join = ' JOIN ' . $join ;
2008-04-04 02:54:20 +00:00
}
if ( $extrafields ) {
$extrafields = ', ' . $extrafields ;
}
2008-07-08 16:33:47 +00:00
list ( $questionidcondition , $params ) = $DB -> get_in_or_equal (
$questionids , SQL_PARAMS_NAMED , 'qid0000' );
2008-06-09 12:16:54 +00:00
$sql = 'SELECT q.*' . $extrafields . ' FROM {question} q' . $join .
2008-07-08 16:33:47 +00:00
' WHERE q.id ' . $questionidcondition ;
2008-04-04 02:54:20 +00:00
// Load the questions
2008-07-08 16:33:47 +00:00
if ( ! $questions = $DB -> get_records_sql ( $sql , $extraparams + $params )) {
2008-04-04 02:54:20 +00:00
return 'Could not load questions.' ;
}
2008-07-08 16:33:47 +00:00
foreach ( $questions as $question ) {
$question -> _partiallyloaded = true ;
}
2008-07-15 15:30:39 +00:00
// Note, a possible optimisation here would be to not load the TEXT fields
// (that is, questiontext and generalfeedback) here, and instead load them in
// question_load_questions. That would add one DB query, but reduce the amount
// of data transferred most of the time. I am not going to do this optimisation
// until it is shown to be worthwhile.
2008-07-08 16:33:47 +00:00
return $questions ;
}
/**
* Load a set of questions , given a list of ids . The $join and $extrafields arguments can be used
* together to pull in extra data . See , for example , the usage in mod / quiz / attempt . php , and
* read the code below to see how the SQL is assembled . Throws exceptions on error .
*
* @ param array $questionids array of question ids .
* @ param string $extrafields extra SQL code to be added to the query .
* @ param string $join extra SQL code to be added to the query .
* @ param array $extraparams values for any placeholders in $join .
* You are strongly recommended to use named placeholder .
*
* @ return array question objects .
*/
function question_load_questions ( $questionids , $extrafields = '' , $join = '' ) {
$questions = question_preload_questions ( $questionids , $extrafields , $join );
2008-04-04 02:54:20 +00:00
// Load the question type specific information
if ( ! get_question_options ( $questions )) {
return 'Could not load the question options' ;
}
return $questions ;
}
2006-02-24 10:21:40 +00:00
/**
2006-07-18 15:34:24 +00:00
* Private function to factor common code out of get_question_options () .
2007-01-07 12:46:47 +00:00
*
2009-05-26 03:23:32 +00:00
* @ global object
* @ global object
2006-07-18 15:34:24 +00:00
* @ param object $question the question to tidy .
2009-01-16 08:00:06 +00:00
* @ param boolean $loadtags load the question tags from the tags table . Optional , default false .
2007-01-07 12:46:47 +00:00
* @ return boolean true if successful , else false .
2006-07-18 15:34:24 +00:00
*/
2009-01-16 08:00:06 +00:00
function _tidy_question ( & $question , $loadtags = false ) {
global $CFG , $QTYPES ;
2006-07-18 15:34:24 +00:00
if ( ! array_key_exists ( $question -> qtype , $QTYPES )) {
$question -> qtype = 'missingtype' ;
$question -> questiontext = '<p>' . get_string ( 'warningmissingtype' , 'quiz' ) . '</p>' . $question -> questiontext ;
}
$question -> name_prefix = question_make_name_prefix ( $question -> id );
2008-07-08 16:33:47 +00:00
if ( $success = $QTYPES [ $question -> qtype ] -> get_question_options ( $question )) {
if ( isset ( $question -> _partiallyloaded )) {
unset ( $question -> _partiallyloaded );
}
}
2009-01-16 08:00:06 +00:00
if ( $loadtags && ! empty ( $CFG -> usetags )) {
require_once ( $CFG -> dirroot . '/tag/lib.php' );
$question -> tags = tag_get_tags_array ( 'question' , $question -> id );
}
2008-07-08 16:33:47 +00:00
return $success ;
2006-07-18 15:34:24 +00:00
}
2006-02-24 10:21:40 +00:00
2006-07-18 15:34:24 +00:00
/**
* Updates the question objects with question type specific
* information by calling { @ link get_question_options ()}
*
* Can be called either with an array of question objects or with a single
* question object .
2007-01-07 12:46:47 +00:00
*
2006-07-18 15:34:24 +00:00
* @ param mixed $questions Either an array of question objects to be updated
* or just a single question object
2009-01-16 08:00:06 +00:00
* @ param boolean $loadtags load the question tags from the tags table . Optional , default false .
2006-07-18 15:34:24 +00:00
* @ return bool Indicates success or failure .
*/
2009-01-16 08:00:06 +00:00
function get_question_options ( & $questions , $loadtags = false ) {
2006-02-24 10:21:40 +00:00
if ( is_array ( $questions )) { // deal with an array of questions
2006-07-18 15:34:24 +00:00
foreach ( $questions as $i => $notused ) {
2009-01-16 08:00:06 +00:00
if ( ! _tidy_question ( $questions [ $i ], $loadtags )) {
2006-02-24 10:21:40 +00:00
return false ;
2006-07-18 15:34:24 +00:00
}
2006-02-24 10:21:40 +00:00
}
return true ;
} else { // deal with single question
2009-01-16 08:00:06 +00:00
return _tidy_question ( $questions , $loadtags );
2006-02-24 10:21:40 +00:00
}
}
/**
2008-09-04 06:37:43 +00:00
* Load the basic state information for
*
2009-05-26 03:23:32 +00:00
* @ global object
2008-09-04 06:37:43 +00:00
* @ param integer $attemptid the attempt id to load the states for .
* @ return array an array of state data from the database , you will subsequently
* need to call question_load_states to get fully loaded states that can be
* used by the question types . The states here should be sufficient for
* basic tasks like rendering navigation .
*/
function question_preload_states ( $attemptid ) {
global $DB ;
2008-09-18 07:39:10 +00:00
// Note, changes here probably also need to be reflected in
// regrade_question_in_attempt and question_load_specific_state.
2006-02-24 10:21:40 +00:00
2008-09-04 06:37:43 +00:00
// The questionid field must be listed first so that it is used as the
2008-06-09 12:16:54 +00:00
// array index in the array returned by $DB->get_records_sql
2008-08-29 10:08:27 +00:00
$statefields = 'n.questionid as question, s.*, n.sumpenalty, n.manualcomment, n.flagged, n.id as questionsessionid' ;
2008-09-04 06:37:43 +00:00
2006-02-24 10:21:40 +00:00
// Load the newest states for the questions
2008-06-09 12:16:54 +00:00
$sql = " SELECT $statefields
FROM { question_states } s , { question_sessions } n
2008-09-04 06:37:43 +00:00
WHERE s . id = n . newest AND n . attemptid = ? " ;
$states = $DB -> get_records_sql ( $sql , array ( $attemptid ));
if ( ! $states ) {
return false ;
}
2006-02-24 10:21:40 +00:00
// Load the newest graded states for the questions
2008-06-09 12:16:54 +00:00
$sql = " SELECT $statefields
FROM { question_states } s , { question_sessions } n
2008-09-04 06:37:43 +00:00
WHERE s . id = n . newgraded AND n . attemptid = ? " ;
$gradedstates = $DB -> get_records_sql ( $sql , array ( $attemptid ));
// Hook the two together.
foreach ( $states as $questionid => $state ) {
$states [ $questionid ] -> _partiallyloaded = true ;
if ( $gradedstates [ $questionid ]) {
$states [ $questionid ] -> last_graded = $gradedstates [ $questionid ];
2008-09-04 07:05:14 +00:00
$states [ $questionid ] -> last_graded -> _partiallyloaded = true ;
2008-09-04 06:37:43 +00:00
}
}
return $states ;
}
/**
* Finish loading the question states that were extracted from the database with
* question_preload_states , creating new states for any question where there
* is not a state in the database .
*
2009-05-26 03:23:32 +00:00
* @ global object
* @ global object
2008-09-04 06:37:43 +00:00
* @ param array $questions the questions to load state for .
2008-09-04 07:05:14 +00:00
* @ param array $states the partially loaded states this array is updated .
2008-09-04 06:37:43 +00:00
* @ param object $cmoptions options from the module we are loading the states for . E . g . $quiz .
* @ param object $attempt The attempt for which the question sessions are
* to be restored or created .
* @ param mixed either the id of a previous attempt , if this attmpt is
* building on a previous one , or false for a clean attempt .
2008-09-04 07:05:14 +00:00
* @ return true or false for success or failure .
2008-09-04 06:37:43 +00:00
*/
function question_load_states ( & $questions , & $states , $cmoptions , $attempt , $lastattemptid = false ) {
global $QTYPES , $DB ;
2006-02-24 10:21:40 +00:00
// loop through all questions and set the last_graded states
2008-09-04 06:37:43 +00:00
foreach ( array_keys ( $questions ) as $qid ) {
if ( isset ( $states [ $qid ])) {
restore_question_state ( $questions [ $qid ], $states [ $qid ]);
if ( isset ( $states [ $qid ] -> _partiallyloaded )) {
unset ( $states [ $qid ] -> _partiallyloaded );
}
2008-09-04 07:05:14 +00:00
if ( isset ( $states [ $qid ] -> last_graded )) {
restore_question_state ( $questions [ $qid ], $states [ $qid ] -> last_graded );
if ( isset ( $states [ $qid ] -> last_graded -> _partiallyloaded )) {
unset ( $states [ $qid ] -> last_graded -> _partiallyloaded );
}
} else {
$states [ $qid ] -> last_graded = clone ( $states [ $qid ]);
2006-02-24 10:21:40 +00:00
}
} else {
2007-03-29 16:36:16 +00:00
if ( $lastattemptid ) {
2008-12-12 02:50:36 +00:00
// If the new attempt is to be based on this previous attempt.
// Find the responses from the previous attempt and save them to the new session
2007-03-29 16:36:16 +00:00
// Load the last graded state for the question
$statefields = 'n.questionid as question, s.*, n.sumpenalty' ;
2008-06-09 12:16:54 +00:00
$sql = " SELECT $statefields
FROM { question_states } s , { question_sessions } n
WHERE s . id = n . newgraded
AND n . attemptid = ?
AND n . questionid = ? " ;
2008-09-04 06:37:43 +00:00
if ( ! $laststate = $DB -> get_record_sql ( $sql , array ( $lastattemptid , $qid ))) {
2007-03-29 16:36:16 +00:00
// Only restore previous responses that have been graded
continue ;
}
// Restore the state so that the responses will be restored
2008-09-04 06:37:43 +00:00
restore_question_state ( $questions [ $qid ], $laststate );
$states [ $qid ] = clone ( $laststate );
unset ( $states [ $qid ] -> id );
2007-03-29 16:36:16 +00:00
} else {
2007-08-10 15:22:28 +00:00
// create a new empty state
2008-09-04 06:37:43 +00:00
$states [ $qid ] = new object ;
$states [ $qid ] -> question = $qid ;
$states [ $qid ] -> responses = array ( '' => '' );
$states [ $qid ] -> raw_grade = 0 ;
2007-03-29 16:36:16 +00:00
}
// now fill/overide initial values
2008-09-04 06:37:43 +00:00
$states [ $qid ] -> attempt = $attempt -> uniqueid ;
$states [ $qid ] -> seq_number = 0 ;
$states [ $qid ] -> timestamp = $attempt -> timestart ;
$states [ $qid ] -> event = ( $attempt -> timefinish ) ? QUESTION_EVENTCLOSE : QUESTION_EVENTOPEN ;
$states [ $qid ] -> grade = 0 ;
$states [ $qid ] -> penalty = 0 ;
$states [ $qid ] -> sumpenalty = 0 ;
$states [ $qid ] -> manualcomment = '' ;
$states [ $qid ] -> flagged = 0 ;
2007-03-29 16:36:16 +00:00
2006-03-26 07:59:43 +00:00
// Prevent further changes to the session from incrementing the
// sequence number
2008-09-04 06:37:43 +00:00
$states [ $qid ] -> changed = true ;
2006-03-26 07:59:43 +00:00
2007-03-29 16:36:16 +00:00
if ( $lastattemptid ) {
// prepare the previous responses for new processing
$action = new stdClass ;
$action -> responses = $laststate -> responses ;
$action -> timestamp = $laststate -> timestamp ;
$action -> event = QUESTION_EVENTSAVE ; //emulate save of questions from all pages MDL-7631
// Process these responses ...
2008-09-04 06:37:43 +00:00
question_process_responses ( $questions [ $qid ], $states [ $qid ], $action , $cmoptions , $attempt );
2007-03-29 16:36:16 +00:00
// Fix for Bug #5506: When each attempt is built on the last one,
2007-08-09 21:50:59 +00:00
// preserve the options from any previous attempt.
2007-03-29 16:36:16 +00:00
if ( isset ( $laststate -> options ) ) {
2008-09-04 06:37:43 +00:00
$states [ $qid ] -> options = $laststate -> options ;
2007-03-29 16:36:16 +00:00
}
} else {
// Create the empty question type specific information
2008-09-04 06:37:43 +00:00
if ( ! $QTYPES [ $questions [ $qid ] -> qtype ] -> create_session_and_responses (
$questions [ $qid ], $states [ $qid ], $cmoptions , $attempt )) {
2007-03-29 16:36:16 +00:00
return false ;
}
2006-02-24 10:21:40 +00:00
}
2008-09-04 06:37:43 +00:00
$states [ $qid ] -> last_graded = clone ( $states [ $qid ]);
2006-02-24 10:21:40 +00:00
}
}
2008-09-04 07:05:14 +00:00
return true ;
2006-02-24 10:21:40 +00:00
}
2008-09-04 06:37:43 +00:00
/**
* Loads the most recent state of each question session from the database
* or create new one .
*
* For each question the most recent session state for the current attempt
* is loaded from the question_states table and the question type specific data and
* responses are added by calling { @ link restore_question_state ()} which in turn
* calls { @ link restore_session_and_responses ()} for each question .
* If no states exist for the question instance an empty state object is
* created representing the start of a session and empty question
* type specific information and responses are created by calling
* { @ link create_session_and_responses ()} .
*
* @ return array An array of state objects representing the most recent
* states of the question sessions .
* @ param array $questions The questions for which sessions are to be restored or
* created .
* @ param object $cmoptions
* @ param object $attempt The attempt for which the question sessions are
* to be restored or created .
* @ param mixed either the id of a previous attempt , if this attmpt is
* building on a previous one , or false for a clean attempt .
*/
function get_question_states ( & $questions , $cmoptions , $attempt , $lastattemptid = false ) {
// Preload the states.
$states = question_preload_states ( $attempt -> uniqueid );
if ( ! $states ) {
$states = array ();
}
// Then finish the job.
2008-09-04 07:05:14 +00:00
if ( ! question_load_states ( $questions , $states , $cmoptions , $attempt , $lastattemptid )) {
return false ;
}
2008-09-04 06:37:43 +00:00
return $states ;
}
2008-07-15 15:30:39 +00:00
/**
* Load a particular previous state of a question .
*
2009-05-26 03:23:32 +00:00
* @ global object
2008-07-15 15:30:39 +00:00
* @ param array $question The question to load the state for .
* @ param object $cmoptions Options from the specifica activity module , e . g . $quiz .
* @ param object $attempt The attempt for which the question sessions are to be loaded .
* @ param integer $stateid The id of a specific state of this question .
* @ return object the requested state . False on error .
*/
function question_load_specific_state ( $question , $cmoptions , $attempt , $stateid ) {
2008-09-23 09:52:55 +00:00
global $DB ;
2008-07-15 15:30:39 +00:00
// Load specified states for the question.
2008-09-18 07:39:10 +00:00
// sess.sumpenalty is probably wrong here shoul really be a sum of penalties from before the one we are asking for.
$sql = ' SELECT st .* , sess . sumpenalty , sess . manualcomment , sess . flagged , sess . id as questionsessionid
2008-07-15 15:30:39 +00:00
FROM { question_states } st , { question_sessions } sess
WHERE st . id = ?
AND st . attempt = ?
AND sess . attemptid = st . attempt
AND st . question = ?
AND sess . questionid = st . question ' ;
$state = $DB -> get_record_sql ( $sql , array ( $stateid , $attempt -> id , $question -> id ));
if ( ! $state ) {
return false ;
}
restore_question_state ( $question , $state );
// Load the most recent graded states for the questions before the specified one.
2008-09-18 07:39:10 +00:00
$sql = ' SELECT st .* , sess . sumpenalty , sess . manualcomment , sess . flagged , sess . id as questionsessionid
2008-07-15 15:30:39 +00:00
FROM { question_states } st , { question_sessions } sess
WHERE st . seq_number <= ?
AND st . attempt = ?
AND sess . attemptid = st . attempt
AND st . question = ?
AND sess . questionid = st . question
2008-09-23 09:52:55 +00:00
AND st . event IN ( '.QUESTION_EVENTS_GRADED.' ) ' .
2008-07-15 15:30:39 +00:00
'ORDER BY st.seq_number DESC' ;
2008-09-23 09:52:55 +00:00
$gradedstates = $DB -> get_records_sql ( $sql , array ( $state -> seq_number , $attempt -> id , $question -> id ), 0 , 1 );
2008-07-15 15:30:39 +00:00
if ( empty ( $gradedstates )) {
$state -> last_graded = clone ( $state );
} else {
$gradedstate = reset ( $gradedstates );
restore_question_state ( $question , $gradedstate );
$state -> last_graded = $gradedstate ;
}
return $state ;
}
2006-02-24 10:21:40 +00:00
/**
* Creates the run - time fields for the states
*
* Extends the state objects for a question by calling
* { @ link restore_session_and_responses ()}
2009-05-26 03:23:32 +00:00
*
* @ global object
2006-02-24 10:21:40 +00:00
* @ param object $question The question for which the state is needed
2006-08-18 22:26:04 +00:00
* @ param object $state The state as loaded from the database
* @ return boolean Represents success or failure
2006-02-24 10:21:40 +00:00
*/
2006-02-28 09:26:00 +00:00
function restore_question_state ( & $question , & $state ) {
2006-02-24 13:48:43 +00:00
global $QTYPES ;
2006-02-24 10:21:40 +00:00
// initialise response to the value in the answer field
2008-06-09 16:53:30 +00:00
$state -> responses = array ( '' => $state -> answer );
2006-02-24 10:21:40 +00:00
unset ( $state -> answer );
2008-06-09 16:53:30 +00:00
$state -> manualcomment = isset ( $state -> manualcomment ) ? $state -> manualcomment : '' ;
2006-02-24 10:21:40 +00:00
// Set the changed field to false; any code which changes the
// question session must set this to true and must increment
2006-02-28 09:26:00 +00:00
// ->seq_number. The save_question_session
2006-02-24 10:21:40 +00:00
// function will save the new state object to the database if the field is
// set to true.
$state -> changed = false ;
// Load the question type specific data
2006-02-24 13:48:43 +00:00
return $QTYPES [ $question -> qtype ]
2006-08-18 22:26:04 +00:00
-> restore_session_and_responses ( $question , $state );
2006-02-24 10:21:40 +00:00
}
/**
* Saves the current state of the question session to the database
*
* The state object representing the current state of the session for the
2006-02-28 09:26:00 +00:00
* question is saved to the question_states table with -> responses [ '' ] saved
2006-02-24 10:21:40 +00:00
* to the answer field of the database table . The information in the
* question_sessions table is updated .
* The question type specific data is then saved .
2009-05-26 03:23:32 +00:00
*
* @ global array
* @ global object
2006-03-27 17:38:30 +00:00
* @ return mixed The id of the saved or updated state or false
2006-02-24 10:21:40 +00:00
* @ param object $question The question for which session is to be saved .
* @ param object $state The state information to be saved . In particular the
* most recent responses are in -> responses . The object
* is updated to hold the new -> id .
*/
2008-07-08 18:22:18 +00:00
function save_question_session ( $question , $state ) {
2008-06-09 12:16:54 +00:00
global $QTYPES , $DB ;
2008-09-03 07:11:59 +00:00
2006-02-24 10:21:40 +00:00
// Check if the state has changed
if ( ! $state -> changed && isset ( $state -> id )) {
2008-09-03 07:11:59 +00:00
if ( isset ( $state -> newflaggedstate ) && $state -> flagged != $state -> newflaggedstate ) {
// If this fails, don't worry too much, it is not critical data.
question_update_flag ( $state -> questionsessionid , $state -> newflaggedstate );
}
2006-03-27 17:38:30 +00:00
return $state -> id ;
2006-02-24 10:21:40 +00:00
}
// Set the legacy answer field
$state -> answer = isset ( $state -> responses [ '' ]) ? $state -> responses [ '' ] : '' ;
// Save the state
2006-05-13 16:57:17 +00:00
if ( ! empty ( $state -> update )) { // this forces the old state record to be overwritten
2008-06-09 12:16:54 +00:00
$DB -> update_record ( 'question_states' , $state );
2006-02-24 10:21:40 +00:00
} else {
2009-06-13 18:16:08 +00:00
$state -> id = $DB -> insert_record ( 'question_states' , $state );
2006-04-07 16:00:29 +00:00
}
2006-02-24 10:21:40 +00:00
2006-04-07 16:00:29 +00:00
// create or update the session
2008-06-12 09:15:16 +00:00
if ( ! $session = $DB -> get_record ( 'question_sessions' , array ( 'attemptid' => $state -> attempt , 'questionid' => $question -> id ))) {
2008-09-03 07:11:59 +00:00
$session = new stdClass ;
2006-04-30 16:15:04 +00:00
$session -> attemptid = $state -> attempt ;
$session -> questionid = $question -> id ;
$session -> newest = $state -> id ;
// The following may seem weird, but the newgraded field needs to be set
// already even if there is no graded state yet.
$session -> newgraded = $state -> id ;
$session -> sumpenalty = $state -> sumpenalty ;
2006-08-24 16:44:15 +00:00
$session -> manualcomment = $state -> manualcomment ;
2008-09-03 07:11:59 +00:00
$session -> flagged = ! empty ( $state -> newflaggedstate );
2009-06-13 18:34:43 +00:00
$DB -> insert_record ( 'question_sessions' , $session );
2006-04-07 16:00:29 +00:00
} else {
2006-04-30 16:15:04 +00:00
$session -> newest = $state -> id ;
if ( question_state_is_graded ( $state ) or $state -> event == QUESTION_EVENTOPEN ) {
// this state is graded or newly opened, so it goes into the lastgraded field as well
$session -> newgraded = $state -> id ;
$session -> sumpenalty = $state -> sumpenalty ;
2006-08-24 16:44:15 +00:00
$session -> manualcomment = $state -> manualcomment ;
2006-08-18 22:34:54 +00:00
} else {
2008-06-09 12:16:54 +00:00
$session -> manualcomment = $session -> manualcomment ;
2006-02-24 10:21:40 +00:00
}
2008-09-03 07:11:59 +00:00
$session -> flagged = ! empty ( $state -> newflaggedstate );
2009-06-13 17:17:10 +00:00
$DB -> update_record ( 'question_sessions' , $session );
2006-02-24 10:21:40 +00:00
}
unset ( $state -> answer );
// Save the question type specific state information and responses
2008-09-03 07:11:59 +00:00
if ( ! $QTYPES [ $question -> qtype ] -> save_session_and_responses ( $question , $state )) {
2006-02-24 10:21:40 +00:00
return false ;
}
2008-09-03 07:11:59 +00:00
2006-02-24 10:21:40 +00:00
// Reset the changed flag
$state -> changed = false ;
2006-03-27 17:38:30 +00:00
return $state -> id ;
2006-02-24 10:21:40 +00:00
}
/**
* Determines whether a state has been graded by looking at the event field
*
* @ return boolean true if the state has been graded
* @ param object $state
*/
2006-02-28 09:26:00 +00:00
function question_state_is_graded ( $state ) {
2008-09-23 09:52:55 +00:00
static $question_events_graded = array ();
if ( ! $question_events_graded ){
$question_events_graded = explode ( ',' , QUESTION_EVENTS_GRADED );
}
return ( in_array ( $state -> event , $question_events_graded ));
2006-03-19 18:28:29 +00:00
}
/**
* Determines whether a state has been closed by looking at the event field
*
* @ return boolean true if the state has been closed
* @ param object $state
*/
function question_state_is_closed ( $state ) {
2008-09-23 09:52:55 +00:00
static $question_events_closed = array ();
if ( ! $question_events_closed ){
$question_events_closed = explode ( ',' , QUESTION_EVENTS_CLOSED );
}
return ( in_array ( $state -> event , $question_events_closed ));
2006-02-24 10:21:40 +00:00
}
/**
2006-03-22 17:22:36 +00:00
* Extracts responses from submitted form
*
* This can extract the responses given to one or several questions present on a page
* It returns an array with one entry for each question , indexed by question id
* Each entry is an object with the properties
* -> event The event that has triggered the submission . This is determined by which button
* the user has pressed .
* -> responses An array holding the responses to an individual question , indexed by the
* name of the corresponding form element .
* -> timestamp A unix timestamp
* @ return array array of action objects , indexed by question ids .
* @ param array $questions an array containing at least all questions that are used on the form
* @ param array $formdata the data submitted by the form on the question page
* @ param integer $defaultevent the event type used if no 'mark' or 'validate' is submitted
*/
function question_extract_responses ( $questions , $formdata , $defaultevent = QUESTION_EVENTSAVE ) {
2006-02-24 10:21:40 +00:00
2006-03-22 17:22:36 +00:00
$time = time ();
2006-02-24 10:21:40 +00:00
$actions = array ();
2006-03-22 17:22:36 +00:00
foreach ( $formdata as $key => $response ) {
2006-02-24 10:21:40 +00:00
// Get the question id from the response name
2006-02-28 09:26:00 +00:00
if ( false !== ( $quid = question_get_id_from_name_prefix ( $key ))) {
2006-02-24 10:21:40 +00:00
// check if this is a valid id
if ( ! isset ( $questions [ $quid ])) {
2008-05-14 08:37:38 +00:00
print_error ( 'formquestionnotinids' , 'question' );
2006-02-24 10:21:40 +00:00
}
// Remove the name prefix from the name
//decrypt trying
$key = substr ( $key , strlen ( $questions [ $quid ] -> name_prefix ));
if ( false === $key ) {
$key = '' ;
}
// Check for question validate and mark buttons & set events
if ( $key === 'validate' ) {
2006-02-28 09:26:00 +00:00
$actions [ $quid ] -> event = QUESTION_EVENTVALIDATE ;
2006-03-22 17:22:36 +00:00
} else if ( $key === 'submit' ) {
2006-03-19 18:28:29 +00:00
$actions [ $quid ] -> event = QUESTION_EVENTSUBMIT ;
2006-02-24 10:21:40 +00:00
} else {
$actions [ $quid ] -> event = $defaultevent ;
}
// Update the state with the new response
$actions [ $quid ] -> responses [ $key ] = $response ;
2007-01-07 12:46:47 +00:00
2006-03-22 17:22:36 +00:00
// Set the timestamp
$actions [ $quid ] -> timestamp = $time ;
2006-02-24 10:21:40 +00:00
}
}
2006-08-24 11:37:10 +00:00
foreach ( $actions as $quid => $notused ) {
ksort ( $actions [ $quid ] -> responses );
}
2006-02-24 10:21:40 +00:00
return $actions ;
}
2006-12-18 06:07:44 +00:00
/**
* Returns the html for question feedback image .
2009-05-26 03:23:32 +00:00
*
* @ global object
2006-12-18 06:07:44 +00:00
* @ param float $fraction value representing the correctness of the user ' s
* response to a question .
* @ param boolean $selected whether or not the answer is the one that the
* user picked .
* @ return string
*/
function question_get_feedback_image ( $fraction , $selected = true ) {
2009-07-09 07:35:03 +00:00
global $CFG , $OUTPUT ;
2008-06-30 16:56:49 +00:00
static $icons = array ( 'correct' => 'tick_green' , 'partiallycorrect' => 'tick_amber' ,
'incorrect' => 'cross_red' );
2006-12-18 06:07:44 +00:00
2008-06-30 16:56:49 +00:00
if ( $selected ) {
$size = 'big' ;
2006-12-18 06:07:44 +00:00
} else {
2008-06-30 16:56:49 +00:00
$size = 'small' ;
2006-12-18 06:07:44 +00:00
}
2008-06-30 16:56:49 +00:00
$class = question_get_feedback_class ( $fraction );
2009-07-03 06:19:25 +00:00
return '<img src="' . $OUTPUT -> old_icon_url ( 'i/' . $icons [ $class ] . '_' . $size ) .
'" alt="' . get_string ( $class , 'quiz' ) . '" class="icon" />' ;
2006-12-18 06:07:44 +00:00
}
/**
* Returns the class name for question feedback .
* @ param float $fraction value representing the correctness of the user ' s
* response to a question .
* @ return string
*/
function question_get_feedback_class ( $fraction ) {
2008-06-30 16:56:49 +00:00
if ( $fraction >= 1 / 1.01 ) {
return 'correct' ;
} else if ( $fraction > 0.0 ) {
return 'partiallycorrect' ;
2006-12-18 06:07:44 +00:00
} else {
2008-06-30 16:56:49 +00:00
return 'incorrect' ;
2006-12-18 06:07:44 +00:00
}
}
2006-02-24 10:21:40 +00:00
/**
* For a given question in an attempt we walk the complete history of states
* and recalculate the grades as we go along .
*
* This is used when a question is changed and old student
* responses need to be marked with the new version of a question .
*
2009-05-26 03:23:32 +00:00
* @ todo Make sure this is not quiz - specific
2006-02-28 09:26:00 +00:00
*
2009-05-26 03:23:32 +00:00
* @ global object
2006-04-05 05:53:18 +00:00
* @ return boolean Indicates whether the grade has changed
2006-02-24 10:21:40 +00:00
* @ param object $question A question object
* @ param object $attempt The attempt , in which the question needs to be regraded .
* @ param object $cmoptions
* @ param boolean $verbose Optional . Whether to print progress information or not .
2008-07-11 07:27:14 +00:00
* @ param boolean $dryrun Optional . Whether to make changes to grades records
* or record that changes need to be made for a later regrade .
2006-02-24 10:21:40 +00:00
*/
2008-07-11 07:27:14 +00:00
function regrade_question_in_attempt ( $question , $attempt , $cmoptions , $verbose = false , $dryrun = false ) {
2009-08-18 05:20:12 +00:00
global $DB , $OUTPUT ;
2006-02-24 10:21:40 +00:00
// load all states for this question in this attempt, ordered in sequence
2008-06-09 12:16:54 +00:00
if ( $states = $DB -> get_records ( 'question_states' ,
array ( 'attempt' => $attempt -> uniqueid , 'question' => $question -> id ),
2006-08-18 22:26:04 +00:00
'seq_number ASC' )) {
2006-02-24 10:21:40 +00:00
$states = array_values ( $states );
// Subtract the grade for the latest state from $attempt->sumgrades to get the
2007-01-07 12:46:47 +00:00
// sumgrades for the attempt without this question.
2006-02-24 10:21:40 +00:00
$attempt -> sumgrades -= $states [ count ( $states ) - 1 ] -> grade ;
// Initialise the replaystate
2008-09-18 07:39:10 +00:00
$replaystate = question_load_specific_state ( $question , $cmoptions , $attempt , $states [ 0 ] -> id );
$replaystate -> sumpenalty = 0 ;
$replaystate -> last_graded -> sumpenalty = 0 ;
2006-02-24 10:21:40 +00:00
2006-04-05 05:53:18 +00:00
$changed = false ;
2006-02-24 10:21:40 +00:00
for ( $j = 1 ; $j < count ( $states ); $j ++ ) {
2006-02-28 09:26:00 +00:00
restore_question_state ( $question , $states [ $j ]);
2006-02-24 10:21:40 +00:00
$action = new stdClass ;
$action -> responses = $states [ $j ] -> responses ;
$action -> timestamp = $states [ $j ] -> timestamp ;
2006-03-19 18:28:29 +00:00
// Change event to submit so that it will be reprocessed
2008-09-18 07:39:10 +00:00
if ( in_array ( $states [ $j ] -> event , array ( QUESTION_EVENTCLOSE ,
QUESTION_EVENTGRADE , QUESTION_EVENTCLOSEANDGRADE ))) {
2006-03-19 18:28:29 +00:00
$action -> event = QUESTION_EVENTSUBMIT ;
2006-02-24 10:21:40 +00:00
// By default take the event that was saved in the database
} else {
$action -> event = $states [ $j ] -> event ;
}
2006-05-13 16:57:17 +00:00
2006-06-26 11:23:32 +00:00
if ( $action -> event == QUESTION_EVENTMANUALGRADE ) {
2008-06-12 09:15:16 +00:00
// Ensure that the grade is in range - in the past this was not checked,
2008-05-15 16:02:12 +00:00
// but now it is (MDL-14835) - so we need to ensure the data is valid before
// proceeding.
if ( $states [ $j ] -> grade < 0 ) {
$states [ $j ] -> grade = 0 ;
2008-07-11 07:27:14 +00:00
$changed = true ;
2008-05-15 16:02:12 +00:00
} else if ( $states [ $j ] -> grade > $question -> maxgrade ) {
$states [ $j ] -> grade = $question -> maxgrade ;
2008-07-11 07:27:14 +00:00
$changed = true ;
2008-05-15 16:02:12 +00:00
}
2008-07-11 07:27:14 +00:00
if ( ! $dryrun ){
$error = question_process_comment ( $question , $replaystate , $attempt ,
$replaystate -> manualcomment , $states [ $j ] -> grade );
if ( is_string ( $error )) {
2009-08-18 05:20:12 +00:00
echo $OUTPUT -> notification ( $error );
2008-07-11 07:27:14 +00:00
}
} else {
$replaystate -> grade = $states [ $j ] -> grade ;
2008-05-15 16:02:12 +00:00
}
2006-05-13 16:57:17 +00:00
} else {
// Reprocess (regrade) responses
2006-08-18 22:26:04 +00:00
if ( ! question_process_responses ( $question , $replaystate ,
2008-09-18 07:39:10 +00:00
$action , $cmoptions , $attempt ) && $verbose ) {
$a = new stdClass ;
$a -> qid = $question -> id ;
$a -> stateid = $states [ $j ] -> id ;
2009-08-18 05:20:12 +00:00
echo $OUTPUT -> notification ( get_string ( 'errorduringregrade' , 'question' , $a ));
2006-05-13 16:57:17 +00:00
}
2008-07-11 07:27:14 +00:00
// We need rounding here because grades in the DB get truncated
// e.g. 0.33333 != 0.3333333, but we want them to be equal here
if (( round (( float ) $replaystate -> raw_grade , 5 ) != round (( float ) $states [ $j ] -> raw_grade , 5 ))
or ( round (( float ) $replaystate -> penalty , 5 ) != round (( float ) $states [ $j ] -> penalty , 5 ))
or ( round (( float ) $replaystate -> grade , 5 ) != round (( float ) $states [ $j ] -> grade , 5 ))) {
$changed = true ;
}
2008-09-18 07:39:10 +00:00
// If this was previously a closed state, and it has been knoced back to
// graded, then fix up the state again.
if ( $replaystate -> event == QUESTION_EVENTGRADE &&
( $states [ $j ] -> event == QUESTION_EVENTCLOSE ||
$states [ $j ] -> event == QUESTION_EVENTCLOSEANDGRADE )) {
$replaystate -> event = $states [ $j ] -> event ;
}
2006-02-24 10:21:40 +00:00
}
$replaystate -> id = $states [ $j ] -> id ;
2007-01-07 12:46:47 +00:00
$replaystate -> changed = true ;
2006-03-26 07:59:43 +00:00
$replaystate -> update = true ; // This will ensure that the existing database entry is updated rather than a new one created
2008-07-11 07:27:14 +00:00
if ( ! $dryrun ){
save_question_session ( $question , $replaystate );
}
2006-02-24 10:21:40 +00:00
}
2006-04-05 05:53:18 +00:00
if ( $changed ) {
2008-07-11 07:27:14 +00:00
if ( ! $dryrun ){
// TODO, call a method in quiz to do this, where 'quiz' comes from
// the question_attempts table.
$DB -> update_record ( 'quiz_attempts' , $attempt );
}
}
if ( $changed ){
$toinsert = new object ();
$toinsert -> oldgrade = round (( float ) $states [ count ( $states ) - 1 ] -> grade , 5 );
$toinsert -> newgrade = round (( float ) $replaystate -> grade , 5 );
$toinsert -> attemptid = $attempt -> uniqueid ;
$toinsert -> questionid = $question -> id ;
//the grade saved is the old grade if the new grade is saved
//it is the new grade if this is a dry run.
$toinsert -> regraded = $dryrun ? 0 : 1 ;
$toinsert -> timemodified = time ();
$DB -> insert_record ( 'quiz_question_regrade' , $toinsert );
return true ;
} else {
return false ;
2006-02-24 10:21:40 +00:00
}
}
2006-04-05 05:53:18 +00:00
return false ;
2006-02-24 10:21:40 +00:00
}
/**
* Processes an array of student responses , grading and saving them as appropriate
*
2009-05-26 03:23:32 +00:00
* @ global array
2006-02-24 10:21:40 +00:00
* @ param object $question Full question object , passed by reference
* @ param object $state Full state object , passed by reference
* @ param object $action object with the fields -> responses which
* is an array holding the student responses ,
2006-02-28 09:26:00 +00:00
* -> action which specifies the action , e . g . , QUESTION_EVENTGRADE ,
2006-02-24 10:21:40 +00:00
* and -> timestamp which is a timestamp from when the responses
* were submitted by the student .
* @ param object $cmoptions
* @ param object $attempt The attempt is passed by reference so that
* during grading its -> sumgrades field can be updated
2008-07-01 13:34:09 +00:00
* @ return boolean Indicates success / failure
2006-02-24 10:21:40 +00:00
*/
2008-07-08 18:22:18 +00:00
function question_process_responses ( $question , & $state , $action , $cmoptions , & $attempt ) {
2006-02-24 13:48:43 +00:00
global $QTYPES ;
2006-02-24 10:21:40 +00:00
// if no responses are set initialise to empty response
if ( ! isset ( $action -> responses )) {
$action -> responses = array ( '' => '' );
}
2008-09-03 07:11:59 +00:00
$state -> newflaggedstate = ! empty ( $action -> responses [ '_flagged' ]);
2006-02-24 10:21:40 +00:00
// make sure these are gone!
2008-09-03 07:11:59 +00:00
unset ( $action -> responses [ 'submit' ], $action -> responses [ 'validate' ], $action -> responses [ '_flagged' ]);
2006-02-24 10:21:40 +00:00
// Check the question session is still open
2006-03-19 18:28:29 +00:00
if ( question_state_is_closed ( $state )) {
2006-02-24 10:21:40 +00:00
return true ;
}
2006-03-19 18:28:29 +00:00
2006-02-24 10:21:40 +00:00
// If $action->event is not set that implies saving
if ( ! isset ( $action -> event )) {
2007-03-29 16:05:55 +00:00
debugging ( 'Ambiguous action in question_process_responses.' , DEBUG_DEVELOPER );
2006-02-28 09:26:00 +00:00
$action -> event = QUESTION_EVENTSAVE ;
2006-02-24 10:21:40 +00:00
}
2006-03-19 18:28:29 +00:00
// If submitted then compare against last graded
2006-02-24 10:21:40 +00:00
// responses, not last given responses in this case
2006-02-28 09:26:00 +00:00
if ( question_isgradingevent ( $action -> event )) {
2006-02-24 10:21:40 +00:00
$state -> responses = $state -> last_graded -> responses ;
}
2007-01-07 12:46:47 +00:00
2006-02-24 10:21:40 +00:00
// Check for unchanged responses (exactly unchanged, not equivalent).
// We also have to catch questions that the student has not yet attempted
2007-01-11 12:18:10 +00:00
$sameresponses = $QTYPES [ $question -> qtype ] -> compare_responses ( $question , $action , $state );
2007-07-17 15:27:59 +00:00
if ( ! empty ( $state -> last_graded ) && $state -> last_graded -> event == QUESTION_EVENTOPEN &&
question_isgradingevent ( $action -> event )) {
2007-01-11 12:18:10 +00:00
$sameresponses = false ;
}
2006-02-24 10:21:40 +00:00
2006-03-19 18:28:29 +00:00
// If the response has not been changed then we do not have to process it again
// unless the attempt is closing or validation is requested
2006-02-28 09:26:00 +00:00
if ( $sameresponses and QUESTION_EVENTCLOSE != $action -> event
2006-08-04 16:53:43 +00:00
and QUESTION_EVENTVALIDATE != $action -> event ) {
2006-02-24 10:21:40 +00:00
return true ;
}
// Roll back grading information to last graded state and set the new
// responses
$newstate = clone ( $state -> last_graded );
$newstate -> responses = $action -> responses ;
$newstate -> seq_number = $state -> seq_number + 1 ;
$newstate -> changed = true ; // will assure that it gets saved to the database
2006-04-30 11:45:14 +00:00
$newstate -> last_graded = clone ( $state -> last_graded );
2006-02-24 10:21:40 +00:00
$newstate -> timestamp = $action -> timestamp ;
2008-09-03 07:11:59 +00:00
$newstate -> newflaggedstate = $state -> newflaggedstate ;
$newstate -> flagged = $state -> flagged ;
$newstate -> questionsessionid = $state -> questionsessionid ;
2006-02-24 10:21:40 +00:00
$state = $newstate ;
// Set the event to the action we will perform. The question type specific
2006-02-28 09:26:00 +00:00
// grading code may override this by setting it to QUESTION_EVENTCLOSE if the
2006-02-24 10:21:40 +00:00
// attempt at the question causes the session to close
$state -> event = $action -> event ;
2006-02-28 09:26:00 +00:00
if ( ! question_isgradingevent ( $action -> event )) {
2006-02-24 10:21:40 +00:00
// Grade the response but don't update the overall grade
2008-07-01 13:34:09 +00:00
if ( ! $QTYPES [ $question -> qtype ] -> grade_responses ( $question , $state , $cmoptions )) {
return false ;
}
2007-08-09 21:50:59 +00:00
2007-04-25 11:20:22 +00:00
// Temporary hack because question types are not given enough control over what is going
// on. Used by Opaque questions.
// TODO fix this code properly.
if ( ! empty ( $state -> believeevent )) {
// If the state was graded we need to ...
if ( question_state_is_graded ( $state )) {
question_apply_penalty_and_timelimit ( $question , $state , $attempt , $cmoptions );
// update the attempt grade
$attempt -> sumgrades -= ( float ) $state -> last_graded -> grade ;
$attempt -> sumgrades += ( float ) $state -> grade ;
2007-08-09 21:50:59 +00:00
2007-04-25 11:20:22 +00:00
// and update the last_graded field.
unset ( $state -> last_graded );
$state -> last_graded = clone ( $state );
unset ( $state -> last_graded -> changed );
}
} else {
// Don't allow the processing to change the event type
$state -> event = $action -> event ;
}
2007-08-09 21:50:59 +00:00
2006-04-30 11:45:14 +00:00
} else { // grading event
2006-02-24 10:21:40 +00:00
2007-01-07 12:46:47 +00:00
// Unless the attempt is closing, we want to work out if the current responses
// (or equivalent responses) were already given in the last graded attempt.
2006-08-04 16:53:43 +00:00
if ( QUESTION_EVENTCLOSE != $action -> event && QUESTION_EVENTOPEN != $state -> last_graded -> event &&
$QTYPES [ $question -> qtype ] -> compare_responses ( $question , $state , $state -> last_graded )) {
2006-03-19 18:28:29 +00:00
$state -> event = QUESTION_EVENTDUPLICATE ;
2006-02-24 10:21:40 +00:00
}
2006-03-19 18:28:29 +00:00
2006-04-30 11:45:14 +00:00
// If we did not find a duplicate or if the attempt is closing, perform grading
2006-08-04 16:53:43 +00:00
if (( ! $sameresponses and QUESTION_EVENTDUPLICATE != $state -> event ) or
QUESTION_EVENTCLOSE == $action -> event ) {
2008-07-01 13:34:09 +00:00
if ( ! $QTYPES [ $question -> qtype ] -> grade_responses ( $question , $state , $cmoptions )) {
return false ;
}
2006-03-19 18:28:29 +00:00
// Calculate overall grade using correct penalty method
question_apply_penalty_and_timelimit ( $question , $state , $attempt , $cmoptions );
2006-02-24 10:21:40 +00:00
}
2007-04-25 11:20:22 +00:00
// If the state was graded we need to ...
2006-04-30 11:45:14 +00:00
if ( question_state_is_graded ( $state )) {
2007-04-25 11:20:22 +00:00
// update the attempt grade
$attempt -> sumgrades -= ( float ) $state -> last_graded -> grade ;
$attempt -> sumgrades += ( float ) $state -> grade ;
// and update the last_graded field.
2006-04-30 11:45:14 +00:00
unset ( $state -> last_graded );
$state -> last_graded = clone ( $state );
unset ( $state -> last_graded -> changed );
}
2006-02-24 10:21:40 +00:00
}
$attempt -> timemodified = $action -> timestamp ;
return true ;
}
/**
* Determine if event requires grading
*/
2006-02-28 09:26:00 +00:00
function question_isgradingevent ( $event ) {
2006-03-19 18:28:29 +00:00
return ( QUESTION_EVENTSUBMIT == $event || QUESTION_EVENTCLOSE == $event );
2006-02-24 10:21:40 +00:00
}
/**
* Applies the penalty from the previous graded responses to the raw grade
* for the current responses
*
* The grade for the question in the current state is computed by subtracting the
* penalty accumulated over the previous graded responses at the question from the
* raw grade . If the timestamp is more than 1 minute beyond the end of the attempt
* the grade is set to zero . The -> grade field of the state object is modified to
* reflect the new grade but is never allowed to decrease .
* @ param object $question The question for which the penalty is to be applied .
* @ param object $state The state for which the grade is to be set from the
* raw grade and the cumulative penalty from the last
* graded state . The -> grade field is updated by applying
* the penalty scheme determined in $cmoptions to the -> raw_grade and
* -> last_graded -> penalty fields .
* @ param object $cmoptions The options set by the course module .
* The -> penaltyscheme field determines whether penalties
* for incorrect earlier responses are subtracted .
*/
2006-02-28 09:26:00 +00:00
function question_apply_penalty_and_timelimit ( & $question , & $state , $attempt , $cmoptions ) {
2007-04-12 22:16:47 +00:00
// TODO. Quiz dependancy. The fact that the attempt that is passed in here
// is from quiz_attempts, and we use things like $cmoptions->timelimit.
2007-08-09 21:50:59 +00:00
2006-03-19 18:28:29 +00:00
// deal with penalty
2006-02-24 10:21:40 +00:00
if ( $cmoptions -> penaltyscheme ) {
2007-04-25 11:20:22 +00:00
$state -> grade = $state -> raw_grade - $state -> sumpenalty ;
$state -> sumpenalty += ( float ) $state -> penalty ;
2006-02-24 10:21:40 +00:00
} else {
$state -> grade = $state -> raw_grade ;
}
// deal with timelimit
if ( $cmoptions -> timelimit ) {
// We allow for 5% uncertainty in the following test
quiz settings: MDL-18485 Improve quiz settings form
* Reorder form fields to group things more logically.
** and on the corresponding admin page too.
* Set some options to be 'Advanced' by default:
** Apply penalties.
** Each attempt builds on the last.
** Decimal places for question grades.
** The five 'Extra restrictions on attempts' settings. (password, etc.)
* Admins can still change this to suit their institiution at Administration > Plugins > Activity modules > Quiz.
* These new defaults are applied if the admin had not previously set any fields to be advanced.
* Disable some filds when they are not applicable:
** Grading method, if num attempts = 1
** Penaly scheme, if adaptive mode = no
** Each attempt builds of last, if num attempts = 1
** Review after quiz closed options, if no close date.
** Delay between 1st and 2nd attempts, if num attempts = 1
** Delay between later attempts, if num attempts < 3
* Convert quiz.timelimit to be in seconds, for consistency, and ready for the new duration field type (MDL 18500).
** Including ensuring that backup and restore is backwards compatible.
* MDL-5537 New setting, questiondecimalpoints, so, for example, you can show the quiz grade as an integer, but have fractional question grades.
** There is a 'Same as overall decimal points' option, which is the default.
* Improve some field labels.
* Make corresponding changes in the help files.
2009-03-10 08:39:51 +00:00
if ( $state -> timestamp - $attempt -> timestart > $cmoptions -> timelimit * 1.05 ) {
2007-04-13 10:03:10 +00:00
$cm = get_coursemodule_from_instance ( 'quiz' , $cmoptions -> id );
if ( ! has_capability ( 'mod/quiz:ignoretimelimits' , get_context_instance ( CONTEXT_MODULE , $cm -> id ),
$attempt -> userid , false )) {
$state -> grade = 0 ;
}
2006-02-24 10:21:40 +00:00
}
}
// deal with closing time
if ( $cmoptions -> timeclose and $state -> timestamp > ( $cmoptions -> timeclose + 60 ) // allowing 1 minute lateness
and ! $attempt -> preview ) { // ignore closing time for previews
$state -> grade = 0 ;
}
// Ensure that the grade does not go down
$state -> grade = max ( $state -> grade , $state -> last_graded -> grade );
}
/**
* Print the icon for the question type
*
2009-05-26 03:23:32 +00:00
* @ global array
* @ global object
2008-09-09 08:35:38 +00:00
* @ param object $question The question object for which the icon is required
* only $question -> qtype is used .
* @ param boolean $return If true the functions returns the link as a string
2006-02-24 10:21:40 +00:00
*/
2007-04-11 22:57:46 +00:00
function print_question_icon ( $question , $return = false ) {
2006-03-01 07:03:57 +00:00
global $QTYPES , $CFG ;
2006-02-24 10:21:40 +00:00
2008-07-17 12:49:25 +00:00
if ( array_key_exists ( $question -> qtype , $QTYPES )) {
2008-09-09 08:43:40 +00:00
$namestr = $QTYPES [ $question -> qtype ] -> local_name ();
2008-07-17 12:49:25 +00:00
} else {
$namestr = 'missingtype' ;
}
2007-04-11 22:57:46 +00:00
$html = '<img src="' . $CFG -> wwwroot . '/question/type/' .
$question -> qtype . '/icon.gif" alt="' .
$namestr . '" title="' . $namestr . '" />' ;
2006-02-24 10:21:40 +00:00
if ( $return ) {
return $html ;
} else {
echo $html ;
}
}
/**
* Returns a html link to the question image if there is one
*
2009-05-26 03:23:32 +00:00
* @ global object
* @ global object
2006-02-24 10:21:40 +00:00
* @ return string The html image tag or the empy string if there is no image .
* @ param object $question The question object
*/
2007-09-04 11:55:10 +00:00
function get_question_image ( $question ) {
2008-06-09 12:16:54 +00:00
global $CFG , $DB ;
2006-02-24 10:21:40 +00:00
$img = '' ;
2008-06-09 12:16:54 +00:00
if ( ! $category = $DB -> get_record ( 'question_categories' , array ( 'id' => $question -> category ))) {
2008-05-14 08:37:38 +00:00
print_error ( 'invalidcategory' );
2007-09-04 11:55:10 +00:00
}
$coursefilesdir = get_filesdir_from_context ( get_context_instance_by_id ( $category -> contextid ));
2006-02-24 10:21:40 +00:00
if ( $question -> image ) {
if ( substr ( strtolower ( $question -> image ), 0 , 7 ) == 'http://' ) {
$img .= $question -> image ;
} else {
2008-07-10 09:55:11 +00:00
require_once ( $CFG -> libdir . '/filelib.php' );
2008-07-23 10:32:35 +00:00
$img = get_file_url ( " $coursefilesdir / { $question -> image } " );
2008-07-10 09:55:11 +00:00
}
2006-02-24 10:21:40 +00:00
}
return $img ;
}
2006-04-07 16:00:29 +00:00
2009-05-26 03:23:32 +00:00
/**
* @ global array
*/
2008-12-10 09:11:30 +00:00
function question_print_comment_fields ( $question , $state , $prefix , $cmoptions , $caption = '' ) {
2008-12-12 09:00:11 +00:00
global $QTYPES ;
2008-09-18 07:39:10 +00:00
$idprefix = preg_replace ( '/[^-_a-zA-Z0-9]/' , '' , $prefix );
2009-03-26 01:54:40 +00:00
$otherquestionsinuse = '' ;
if ( ! empty ( $cmoptions -> questions )) {
$otherquestionsinuse = $cmoptions -> questions ;
}
if ( ! question_state_is_graded ( $state ) && $QTYPES [ $question -> qtype ] -> is_question_manual_graded ( $question , $otherquestionsinuse )) {
2008-12-12 09:00:11 +00:00
$grade = '' ;
} else {
$grade = question_format_grade ( $cmoptions , $state -> last_graded -> grade );
}
2008-09-18 07:39:10 +00:00
$maxgrade = question_format_grade ( $cmoptions , $question -> maxgrade );
$fieldsize = strlen ( $maxgrade ) - 1 ;
2008-12-10 09:11:30 +00:00
if ( empty ( $caption )) {
$caption = format_string ( $question -> name );
}
?>
< fieldset class = " que comment clearfix " >
< legend class = " ftoggler " >< ? php echo $caption ; ?> </legend>
< div class = " fcontainer clearfix " >
< div class = " fitem " >
< div class = " fitemtitle " >
< label for = " <?php echo $idprefix ; ?>_comment_box " >< ? php print_string ( 'comment' , 'quiz' ); ?> </label>
</ div >
< div class = " felement fhtmleditor " >
< ? php print_textarea ( can_use_html_editor (), 15 , 60 , 630 , 300 , $prefix . '[comment]' ,
$state -> manualcomment , 0 , false , $idprefix . '_comment_box' ); ?>
</ div >
</ div >
< div class = " fitem " >
< div class = " fitemtitle " >
< label for = " <?php echo $idprefix ; ?>_grade_field " >< ? php print_string ( 'grade' , 'quiz' ); ?> </label>
</ div >
< div class = " felement ftext " >
< input type = " text " name = " <?php echo $prefix ; ?>[grade] " size = " <?php echo $fieldsize ; ?> " id = " <?php echo $idprefix ; ?>_grade_field " value = " <?php echo $grade ; ?> " /> / < ? php echo $maxgrade ; ?>
</ div >
</ div >
</ div >
</ fieldset >
< ? php
2006-04-07 16:00:29 +00:00
}
2008-05-15 16:02:12 +00:00
/**
* Process a manual grading action . That is , use $comment and $grade to update
* $state and $attempt . The attempt and the comment text are stored in the
* database . $state is only updated in memory , it is up to the call to store
* that , if appropriate .
*
2009-05-26 03:23:32 +00:00
* @ global object
2008-05-15 16:02:12 +00:00
* @ param object $question the question
* @ param object $state the state to be updated .
* @ param object $attempt the attempt the state belongs to , to be updated .
2008-12-12 09:00:11 +00:00
* @ param string $comment the new comment from the teacher .
* @ param mixed $grade the grade the teacher assigned , or '' to not change the grade .
2008-05-15 16:02:12 +00:00
* @ return mixed true on success , a string error message if a problem is detected
* ( for example score out of range ) .
*/
2006-04-07 16:00:29 +00:00
function question_process_comment ( $question , & $state , & $attempt , $comment , $grade ) {
2008-06-09 12:16:54 +00:00
global $DB ;
2008-12-12 09:00:11 +00:00
$grade = trim ( $grade );
2008-05-15 16:02:12 +00:00
if ( $grade < 0 || $grade > $question -> maxgrade ) {
$a = new stdClass ;
$a -> grade = $grade ;
$a -> maxgrade = $question -> maxgrade ;
$a -> name = $question -> name ;
return get_string ( 'errormanualgradeoutofrange' , 'question' , $a );
}
2006-04-07 16:00:29 +00:00
// Update the comment and save it in the database
2007-09-19 10:56:24 +00:00
$comment = trim ( $comment );
2006-08-24 16:44:15 +00:00
$state -> manualcomment = $comment ;
2008-06-09 12:16:54 +00:00
if ( ! $DB -> set_field ( 'question_sessions' , 'manualcomment' , $comment , array ( 'attemptid' => $attempt -> uniqueid , 'questionid' => $question -> id ))) {
2008-05-15 16:02:12 +00:00
return get_string ( 'errorsavingcomment' , 'question' , $question );
2006-04-07 16:00:29 +00:00
}
2006-10-17 23:23:18 +00:00
// Update the attempt if the score has changed.
2008-12-12 09:00:11 +00:00
if ( $grade !== '' && ( abs ( $state -> last_graded -> grade - $grade ) > 0.002 || $state -> last_graded -> event != QUESTION_EVENTMANUALGRADE )) {
2006-04-07 16:00:29 +00:00
$attempt -> sumgrades = $attempt -> sumgrades - $state -> last_graded -> grade + $grade ;
$attempt -> timemodified = time ();
2009-06-13 17:17:10 +00:00
$DB -> update_record ( 'quiz_attempts' , $attempt );
2006-11-15 17:32:49 +00:00
// We want to update existing state (rather than creating new one) if it
// was itself created by a manual grading event.
2008-12-12 09:00:11 +00:00
$state -> update = $state -> event == QUESTION_EVENTMANUALGRADE ;
2006-11-15 17:32:49 +00:00
// Update the other parts of the state object.
2006-04-07 16:00:29 +00:00
$state -> raw_grade = $grade ;
$state -> grade = $grade ;
$state -> penalty = 0 ;
$state -> timestamp = time ();
2006-05-13 16:57:17 +00:00
$state -> seq_number ++ ;
2006-10-17 23:23:18 +00:00
$state -> event = QUESTION_EVENTMANUALGRADE ;
2006-04-07 16:00:29 +00:00
// Update the last graded state (don't simplify!)
unset ( $state -> last_graded );
$state -> last_graded = clone ( $state );
2006-10-17 23:23:18 +00:00
// We need to indicate that the state has changed in order for it to be saved.
$state -> changed = 1 ;
2006-04-07 16:00:29 +00:00
}
2008-05-15 16:02:12 +00:00
return true ;
2006-04-07 16:00:29 +00:00
}
2006-02-24 10:21:40 +00:00
/**
* Construct name prefixes for question form element names
*
* Construct the name prefix that should be used for example in the
* names of form elements created by questions .
2006-02-28 09:26:00 +00:00
* This is called by { @ link get_question_options ()}
2006-02-24 10:21:40 +00:00
* to set $question -> name_prefix .
* This name prefix includes the question id which can be
2006-02-28 09:26:00 +00:00
* extracted from it with { @ link question_get_id_from_name_prefix ()} .
2006-02-24 10:21:40 +00:00
*
* @ return string
* @ param integer $id The question id
*/
2006-02-28 09:26:00 +00:00
function question_make_name_prefix ( $id ) {
2006-02-24 10:21:40 +00:00
return 'resp' . $id . '_' ;
}
/**
2008-09-03 08:32:29 +00:00
* Extract question id from the prefix of form element names
*
* @ return integer The question id
* @ param string $name The name that contains a prefix that was
* constructed with { @ link question_make_name_prefix ()}
*/
2006-02-28 09:26:00 +00:00
function question_get_id_from_name_prefix ( $name ) {
2008-09-03 08:32:29 +00:00
if ( ! preg_match ( '/^resp([0-9]+)_/' , $name , $matches )) {
2006-02-24 10:21:40 +00:00
return false ;
2008-09-03 08:32:29 +00:00
}
2006-02-24 10:21:40 +00:00
return ( integer ) $matches [ 1 ];
}
2008-09-03 08:32:29 +00:00
/**
* Extract question id from the prefix of form element names
*
* @ return integer The question id
* @ param string $name The name that contains a prefix that was
* constructed with { @ link question_make_name_prefix ()}
*/
function question_id_and_key_from_post_name ( $name ) {
if ( ! preg_match ( '/^resp([0-9]+)_(.*)$/' , $name , $matches )) {
return array ( false , false );
}
return array (( integer ) $matches [ 1 ], $matches [ 2 ]);
}
2006-02-28 09:26:00 +00:00
/**
2006-03-22 17:22:36 +00:00
* Returns the unique id for a new attempt
*
* Every module can keep their own attempts table with their own sequential ids but
* the question code needs to also have a unique id by which to identify all these
* attempts . Hence a module , when creating a new attempt , calls this function and
* stores the return value in the 'uniqueid' field of its attempts table .
2009-05-26 03:23:32 +00:00
*
* @ global object
2006-02-28 09:26:00 +00:00
*/
2006-05-13 16:57:17 +00:00
function question_new_attempt_uniqueid ( $modulename = 'quiz' ) {
2008-06-09 12:16:54 +00:00
global $DB ;
2006-07-18 15:34:24 +00:00
$attempt = new stdClass ;
2006-05-13 16:57:17 +00:00
$attempt -> modulename = $modulename ;
2009-06-03 20:16:20 +00:00
$id = $DB -> insert_record ( 'question_attempts' , $attempt );
2006-05-13 16:57:17 +00:00
return $id ;
2006-02-24 10:21:40 +00:00
}
2006-05-13 16:57:17 +00:00
/**
* Creates a stamp that uniquely identifies this version of the question
2006-04-10 22:10:32 +00:00
*
* In future we want this to use a hash of the question data to guarantee that
* identical versions have the same version stamp .
*
* @ param object $question
* @ return string A unique version stamp
*/
function question_hash ( $question ) {
return make_unique_id_code ();
}
2008-08-15 06:42:38 +00:00
/**
* Round a grade to to the correct number of decimal places , and format it for display .
quiz settings: MDL-18485 Improve quiz settings form
* Reorder form fields to group things more logically.
** and on the corresponding admin page too.
* Set some options to be 'Advanced' by default:
** Apply penalties.
** Each attempt builds on the last.
** Decimal places for question grades.
** The five 'Extra restrictions on attempts' settings. (password, etc.)
* Admins can still change this to suit their institiution at Administration > Plugins > Activity modules > Quiz.
* These new defaults are applied if the admin had not previously set any fields to be advanced.
* Disable some filds when they are not applicable:
** Grading method, if num attempts = 1
** Penaly scheme, if adaptive mode = no
** Each attempt builds of last, if num attempts = 1
** Review after quiz closed options, if no close date.
** Delay between 1st and 2nd attempts, if num attempts = 1
** Delay between later attempts, if num attempts < 3
* Convert quiz.timelimit to be in seconds, for consistency, and ready for the new duration field type (MDL 18500).
** Including ensuring that backup and restore is backwards compatible.
* MDL-5537 New setting, questiondecimalpoints, so, for example, you can show the quiz grade as an integer, but have fractional question grades.
** There is a 'Same as overall decimal points' option, which is the default.
* Improve some field labels.
* Make corresponding changes in the help files.
2009-03-10 08:39:51 +00:00
* If $cmoptions -> questiondecimalpoints is set , that is used , otherwise
* else if $cmoptions -> decimalpoints is used ,
* otherwise a default of 2 is used , but this should not be relied upon , and generated a developer debug warning .
* However , if $cmoptions -> questiondecimalpoints is - 1 , the means use $cmoptions -> decimalpoints .
2008-08-15 06:42:38 +00:00
*
quiz settings: MDL-18485 Improve quiz settings form
* Reorder form fields to group things more logically.
** and on the corresponding admin page too.
* Set some options to be 'Advanced' by default:
** Apply penalties.
** Each attempt builds on the last.
** Decimal places for question grades.
** The five 'Extra restrictions on attempts' settings. (password, etc.)
* Admins can still change this to suit their institiution at Administration > Plugins > Activity modules > Quiz.
* These new defaults are applied if the admin had not previously set any fields to be advanced.
* Disable some filds when they are not applicable:
** Grading method, if num attempts = 1
** Penaly scheme, if adaptive mode = no
** Each attempt builds of last, if num attempts = 1
** Review after quiz closed options, if no close date.
** Delay between 1st and 2nd attempts, if num attempts = 1
** Delay between later attempts, if num attempts < 3
* Convert quiz.timelimit to be in seconds, for consistency, and ready for the new duration field type (MDL 18500).
** Including ensuring that backup and restore is backwards compatible.
* MDL-5537 New setting, questiondecimalpoints, so, for example, you can show the quiz grade as an integer, but have fractional question grades.
** There is a 'Same as overall decimal points' option, which is the default.
* Improve some field labels.
* Make corresponding changes in the help files.
2009-03-10 08:39:51 +00:00
* @ param object $cmoptions The modules settings .
2008-08-15 06:42:38 +00:00
* @ param float $grade The grade to round .
*/
function question_format_grade ( $cmoptions , $grade ) {
quiz settings: MDL-18485 Improve quiz settings form
* Reorder form fields to group things more logically.
** and on the corresponding admin page too.
* Set some options to be 'Advanced' by default:
** Apply penalties.
** Each attempt builds on the last.
** Decimal places for question grades.
** The five 'Extra restrictions on attempts' settings. (password, etc.)
* Admins can still change this to suit their institiution at Administration > Plugins > Activity modules > Quiz.
* These new defaults are applied if the admin had not previously set any fields to be advanced.
* Disable some filds when they are not applicable:
** Grading method, if num attempts = 1
** Penaly scheme, if adaptive mode = no
** Each attempt builds of last, if num attempts = 1
** Review after quiz closed options, if no close date.
** Delay between 1st and 2nd attempts, if num attempts = 1
** Delay between later attempts, if num attempts < 3
* Convert quiz.timelimit to be in seconds, for consistency, and ready for the new duration field type (MDL 18500).
** Including ensuring that backup and restore is backwards compatible.
* MDL-5537 New setting, questiondecimalpoints, so, for example, you can show the quiz grade as an integer, but have fractional question grades.
** There is a 'Same as overall decimal points' option, which is the default.
* Improve some field labels.
* Make corresponding changes in the help files.
2009-03-10 08:39:51 +00:00
if ( isset ( $cmoptions -> questiondecimalpoints ) && $cmoptions -> questiondecimalpoints != - 1 ) {
$decimalplaces = $cmoptions -> questiondecimalpoints ;
} else if ( isset ( $cmoptions -> decimalpoints )) {
$decimalplaces = $cmoptions -> decimalpoints ;
} else {
$decimalplaces = 2 ;
debugging ( 'Code that leads to question_format_grade being called should set ' .
'$cmoptions->questiondecimalpoints or $cmoptions->decimalpoints' , DEBUG_DEVELOPER );
}
return format_float ( $grade , $decimalplaces );
2008-08-15 06:42:38 +00:00
}
2006-02-24 10:21:40 +00:00
2008-08-29 10:08:27 +00:00
/**
* @ 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 .
*/
2009-07-03 06:19:25 +00:00
function question_init_qengine_js () {
global $CFG , $PAGE , $OUTPUT ;
2008-08-29 10:08:27 +00:00
$config = array (
2009-07-03 06:19:25 +00:00
'actionurl' => $CFG -> wwwroot . '/question/toggleflag.php' ,
'flagicon' => $OUTPUT -> old_icon_url ( 'i/flagged' ),
'unflagicon' => $OUTPUT -> old_icon_url ( 'i/unflagged' ),
2008-08-29 10:08:27 +00:00
'flagtooltip' => get_string ( 'clicktoflag' , 'question' ),
'unflagtooltip' => get_string ( 'clicktounflag' , 'question' ),
'flaggedalt' => get_string ( 'flagged' , 'question' ),
'unflaggedalt' => get_string ( 'notflagged' , 'question' ),
);
ajaxlib/require_js: MDL-16693 $PAGE->requires->... deprecates require_js etc.
There is a new implementation of require_js in lib/deprecatedlib.php,
based on $PAGE->requires.
There were a few other recently introduced functions in lib/weblib.php,
namely print_js_call, print_delayed_js_call, print_js_config and
standard_js_config. These have been removed, since they were never in
a stable branch, and all the places that used them have been changed
to use the newer $PAGE->requires->... methods.
get_require_js_code is also gone, and the evil places that were calling
it, even though it is an internal function, have been fixed.
Also, I made some minor improvements to the code I committed yesterday
for MDL-16695.
All that remains is to update all the places in core code that are
still using require_js.
(This commit also fixes the problem where the admin tree would not
start with the right categories expanded.)
2009-06-12 12:13:07 +00:00
$PAGE -> requires -> data_for_js ( 'qengine_config' , $config );
2008-08-29 10:08:27 +00:00
}
2006-02-24 10:21:40 +00:00
/// FUNCTIONS THAT SIMPLY WRAP QUESTIONTYPE METHODS //////////////////////////////////
2007-04-12 15:58:32 +00:00
/**
2009-06-15 07:57:53 +00:00
* Give the questions in $questionlist a chance to request the CSS or JavaScript
* they need , before the header is printed .
*
* If your code is going to call the print_question function , it must call this
* funciton before print_header .
2007-04-12 15:58:32 +00:00
*
* @ param array $questionlist a list of questionids of the questions what will appear on this page .
* @ param array $questions an array of question objects , whose keys are question ids .
* Must contain all the questions in $questionlist
* @ param array $states an array of question state objects , whose keys are question ids .
* Must contain the state of all the questions in $questionlist
*/
2008-08-29 03:34:44 +00:00
function get_html_head_contributions ( $questionlist , & $questions , & $states ) {
2009-06-15 05:37:57 +00:00
global $CFG , $PAGE , $QTYPES ;
2008-08-29 10:08:27 +00:00
// The question engine's own JavaScript.
2009-06-15 05:37:57 +00:00
$PAGE -> requires -> yui_lib ( 'connection' );
$PAGE -> requires -> js ( 'question/qengine.js' );
2009-07-03 06:19:25 +00:00
question_init_qengine_js ();
2006-02-24 10:21:40 +00:00
2008-08-29 10:08:27 +00:00
// Anything that questions on this page need.
2007-04-12 15:58:32 +00:00
foreach ( $questionlist as $questionid ) {
$question = $questions [ $questionid ];
2009-06-26 09:06:16 +00:00
$QTYPES [ $question -> qtype ] -> get_html_head_contributions ( $question , $states [ $questionid ]);
2007-04-12 15:58:32 +00:00
}
}
2009-03-30 09:15:46 +00:00
/**
2009-06-15 07:57:53 +00:00
* Like { @ link get_html_head_contributions ()} but for the editing page
2009-03-30 09:15:46 +00:00
* question / question . php .
*
* @ param $question A question object . Only $question -> qtype is used .
2009-06-15 07:57:53 +00:00
* @ return string Deprecated . Some HTML code that can go inside the head tag .
2009-03-30 09:15:46 +00:00
*/
function get_editing_head_contributions ( $question ) {
global $QTYPES ;
2009-06-26 09:06:16 +00:00
$QTYPES [ $question -> qtype ] -> get_editing_head_contributions ();
2009-03-30 09:15:46 +00:00
}
2007-04-12 15:58:32 +00:00
/**
2006-03-22 17:22:36 +00:00
* Prints a question
*
* Simply calls the question type specific print_question () method .
2009-05-26 03:23:32 +00:00
*
* @ global array
2006-03-22 17:22:36 +00:00
* @ param object $question The question to be rendered .
* @ param object $state The state to render the question in .
* @ param integer $number The number for this question .
* @ param object $cmoptions The options specified by the course module
* @ param object $options An object specifying the rendering options .
*/
2006-02-28 09:26:00 +00:00
function print_question ( & $question , & $state , $number , $cmoptions , $options = null ) {
2006-02-24 13:48:43 +00:00
global $QTYPES ;
2008-12-10 05:32:51 +00:00
$QTYPES [ $question -> qtype ] -> print_question ( $question , $state , $number , $cmoptions , $options );
2006-02-24 10:21:40 +00:00
}
2006-03-01 07:36:09 +00:00
/**
2007-04-12 15:58:32 +00:00
* Saves question options
*
* Simply calls the question type specific save_question_options () method .
2009-05-26 03:23:32 +00:00
*
* @ global array
2007-04-12 15:58:32 +00:00
*/
2006-03-01 07:36:09 +00:00
function save_question_options ( $question ) {
global $QTYPES ;
$QTYPES [ $question -> qtype ] -> save_question_options ( $question );
}
2006-02-24 10:21:40 +00:00
/**
* Gets all teacher stored answers for a given question
*
* Simply calls the question type specific get_all_responses () method .
2009-05-26 03:23:32 +00:00
*
* @ global array
2006-02-24 10:21:40 +00:00
*/
// ULPGC ecastro
2006-02-28 09:26:00 +00:00
function get_question_responses ( $question , $state ) {
2006-02-24 13:48:43 +00:00
global $QTYPES ;
$r = $QTYPES [ $question -> qtype ] -> get_all_responses ( $question , $state );
2006-02-24 10:21:40 +00:00
return $r ;
}
/**
2006-03-01 07:03:57 +00:00
* Gets the response given by the user in a particular state
2006-02-24 10:21:40 +00:00
*
* Simply calls the question type specific get_actual_response () method .
2009-05-26 03:23:32 +00:00
*
* @ global array
2006-02-24 10:21:40 +00:00
*/
// ULPGC ecastro
2006-02-28 09:26:00 +00:00
function get_question_actual_response ( $question , $state ) {
2006-02-24 13:48:43 +00:00
global $QTYPES ;
2006-02-24 10:21:40 +00:00
2006-02-24 13:48:43 +00:00
$r = $QTYPES [ $question -> qtype ] -> get_actual_response ( $question , $state );
2006-02-24 10:21:40 +00:00
return $r ;
}
/**
2006-03-01 07:03:57 +00:00
* TODO : document this
2009-05-26 03:23:32 +00:00
*
* @ global array
2006-02-24 10:21:40 +00:00
*/
// ULPGc ecastro
2006-02-28 09:26:00 +00:00
function get_question_fraction_grade ( $question , $state ) {
2006-02-24 13:48:43 +00:00
global $QTYPES ;
2006-02-24 10:21:40 +00:00
2006-02-24 13:48:43 +00:00
$r = $QTYPES [ $question -> qtype ] -> get_fractional_grade ( $question , $state );
2006-02-24 10:21:40 +00:00
return $r ;
}
2008-06-16 13:29:00 +00:00
/**
2009-05-26 03:23:32 +00:00
* @ global array
2008-06-16 13:29:00 +00:00
* @ return integer grade out of 1 that a random guess by a student might score .
*/
// ULPGc ecastro
2008-06-16 13:59:30 +00:00
function question_get_random_guess_score ( $question ) {
2008-06-16 13:29:00 +00:00
global $QTYPES ;
$r = $QTYPES [ $question -> qtype ] -> get_random_guess_score ( $question );
return $r ;
}
2006-02-24 10:21:40 +00:00
/// CATEGORY FUNCTIONS /////////////////////////////////////////////////////////////////
2006-05-08 10:39:14 +00:00
/**
* returns the categories with their names ordered following parent - child relationships
* finally it tries to return pending categories ( those being orphaned , whose parent is
* incorrect ) to avoid missing any category from original array .
2009-05-26 03:23:32 +00:00
*
* @ global object
2006-05-08 10:39:14 +00:00
*/
function sort_categories_by_tree ( & $categories , $id = 0 , $level = 1 ) {
2008-06-09 12:16:54 +00:00
global $DB ;
2006-05-08 10:39:14 +00:00
$children = array ();
$keys = array_keys ( $categories );
foreach ( $keys as $key ) {
if ( ! isset ( $categories [ $key ] -> processed ) && $categories [ $key ] -> parent == $id ) {
$children [ $key ] = $categories [ $key ];
$categories [ $key ] -> processed = true ;
$children = $children + sort_categories_by_tree ( $categories , $children [ $key ] -> id , $level + 1 );
}
}
//If level = 1, we have finished, try to look for non processed categories (bad parent) and sort them too
if ( $level == 1 ) {
foreach ( $keys as $key ) {
//If not processed and it's a good candidate to start (because its parent doesn't exist in the course)
2008-06-09 12:16:54 +00:00
if ( ! isset ( $categories [ $key ] -> processed ) && ! $DB -> record_exists ( 'question_categories' , array ( 'course' => $categories [ $key ] -> course , 'id' => $categories [ $key ] -> parent ))) {
2006-05-08 10:39:14 +00:00
$children [ $key ] = $categories [ $key ];
$categories [ $key ] -> processed = true ;
$children = $children + sort_categories_by_tree ( $categories , $children [ $key ] -> id , $level + 1 );
}
}
}
return $children ;
}
/**
2006-07-14 15:36:29 +00:00
* Private method , only for the use of add_indented_names () .
2007-01-07 12:46:47 +00:00
*
2006-07-14 15:36:29 +00:00
* Recursively adds an indentedname field to each category , starting with the category
2007-01-07 12:46:47 +00:00
* with id $id , and dealing with that category and all its children , and
2006-07-14 15:36:29 +00:00
* return a new array , with those categories in the right order .
*
2007-01-07 12:46:47 +00:00
* @ param array $categories an array of categories which has had childids
2006-07-14 15:36:29 +00:00
* fields added by flatten_category_tree () . Passed by reference for
* performance only . It is not modfied .
* @ param int $id the category to start the indenting process from .
* @ param int $depth the indent depth . Used in recursive calls .
* @ return array a new array of categories , in the right order for the tree .
2006-05-08 10:39:14 +00:00
*/
2007-08-09 21:50:59 +00:00
function flatten_category_tree ( & $categories , $id , $depth = 0 , $nochildrenof = - 1 ) {
2007-01-07 12:46:47 +00:00
2006-07-14 15:36:29 +00:00
// Indent the name of this category.
$newcategories = array ();
$newcategories [ $id ] = $categories [ $id ];
$newcategories [ $id ] -> indentedname = str_repeat ( ' ' , $depth ) . $categories [ $id ] -> name ;
2007-01-07 12:46:47 +00:00
2006-07-14 15:36:29 +00:00
// Recursively indent the children.
foreach ( $categories [ $id ] -> childids as $childid ) {
2007-08-09 21:50:59 +00:00
if ( $childid != $nochildrenof ){
$newcategories = $newcategories + flatten_category_tree ( $categories , $childid , $depth + 1 , $nochildrenof );
}
2006-05-08 10:39:14 +00:00
}
2007-01-07 12:46:47 +00:00
2006-07-14 15:36:29 +00:00
// Remove the childids array that were temporarily added.
unset ( $newcategories [ $id ] -> childids );
2007-01-07 12:46:47 +00:00
2006-07-14 15:36:29 +00:00
return $newcategories ;
2006-05-08 10:39:14 +00:00
}
/**
2006-07-14 15:36:29 +00:00
* Format categories into an indented list reflecting the tree structure .
2007-01-07 12:46:47 +00:00
*
2006-07-14 15:36:29 +00:00
* @ param array $categories An array of category objects , for example from the .
* @ return array The formatted list of categories .
2006-05-08 10:39:14 +00:00
*/
2007-08-09 21:50:59 +00:00
function add_indented_names ( $categories , $nochildrenof = - 1 ) {
2006-05-08 10:39:14 +00:00
2007-01-07 12:46:47 +00:00
// Add an array to each category to hold the child category ids. This array will be removed
2006-07-14 15:36:29 +00:00
// again by flatten_category_tree(). It should not be used outside these two functions.
foreach ( array_keys ( $categories ) as $id ) {
$categories [ $id ] -> childids = array ();
2006-05-08 10:39:14 +00:00
}
2006-07-14 15:36:29 +00:00
// Build the tree structure, and record which categories are top-level.
2007-01-07 12:46:47 +00:00
// We have to be careful, because the categories array may include published
2006-07-14 15:36:29 +00:00
// categories from other courses, but not their parents.
$toplevelcategoryids = array ();
foreach ( array_keys ( $categories ) as $id ) {
if ( ! empty ( $categories [ $id ] -> parent ) && array_key_exists ( $categories [ $id ] -> parent , $categories )) {
$categories [ $categories [ $id ] -> parent ] -> childids [] = $id ;
} else {
$toplevelcategoryids [] = $id ;
2006-05-08 10:39:14 +00:00
}
}
2006-07-14 15:36:29 +00:00
// Flatten the tree to and add the indents.
$newcategories = array ();
foreach ( $toplevelcategoryids as $id ) {
2007-08-09 21:50:59 +00:00
$newcategories = $newcategories + flatten_category_tree ( $categories , $id , 0 , $nochildrenof );
2006-05-08 10:39:14 +00:00
}
2006-07-14 15:36:29 +00:00
return $newcategories ;
2006-05-08 10:39:14 +00:00
}
2006-03-02 14:13:42 +00:00
2006-02-24 10:21:40 +00:00
/**
2006-07-14 15:36:29 +00:00
* Output a select menu of question categories .
2007-01-07 12:46:47 +00:00
*
2006-07-14 15:36:29 +00:00
* Categories from this course and ( optionally ) published categories from other courses
2007-01-07 12:46:47 +00:00
* are included . Optionally , only categories the current user may edit can be included .
2006-07-14 15:36:29 +00:00
*
* @ param integer $courseid the id of the course to get the categories for .
* @ param integer $published if true , include publised categories from other courses .
* @ param integer $only_editable if true , exclude categories this user is not allowed to edit .
* @ param integer $selected optionally , the id of a category to be selected by default in the dropdown .
*/
2007-08-09 21:50:59 +00:00
function question_category_select_menu ( $contexts , $top = false , $currentcat = 0 , $selected = " " , $nochildrenof = - 1 ) {
2009-08-06 00:35:06 +00:00
global $OUTPUT ;
2007-08-09 21:50:59 +00:00
$categoriesarray = question_category_options ( $contexts , $top , $currentcat , false , $nochildrenof );
2007-03-01 09:45:53 +00:00
if ( $selected ) {
$nothing = '' ;
} else {
2009-08-06 00:35:06 +00:00
$nothing = 'choosedots' ;
2006-02-24 10:21:40 +00:00
}
2009-08-10 08:38:45 +00:00
$select = html_select :: make ( $categoriesarray , 'category' , $selected );
2009-08-06 00:35:06 +00:00
$select -> nothingvalue = $nothing ;
$select -> nested = true ;
echo $OUTPUT -> select ( $select );
2006-02-24 10:21:40 +00:00
}
2008-09-10 05:28:25 +00:00
/**
2009-05-26 03:23:32 +00:00
* @ global object
2008-09-10 05:28:25 +00:00
* @ param integer $contextid a context id .
* @ return object the default question category for that context , or false if none .
*/
function question_get_default_category ( $contextid ) {
global $DB ;
$category = $DB -> get_records ( 'question_categories' , array ( 'contextid' => $contextid ), 'id' , '*' , 0 , 1 );
if ( ! empty ( $category )) {
return reset ( $category );
} else {
return false ;
}
}
/**
2009-05-26 03:23:32 +00:00
* @ global object
* @ global object
2008-09-10 05:28:25 +00:00
* @ param object $context a context
* @ return string A URL for editing questions in this context .
*/
function question_edit_url ( $context ) {
global $CFG , $SITE ;
if ( ! has_any_capability ( question_get_question_capabilities (), $context )) {
return false ;
}
$baseurl = $CFG -> wwwroot . '/question/edit.php?' ;
$defaultcategory = question_get_default_category ( $context -> id );
if ( $defaultcategory ) {
2008-09-10 05:34:09 +00:00
$baseurl .= 'cat=' . $defaultcategory -> id . ',' . $context -> id . '&' ;
2008-09-10 05:28:25 +00:00
}
switch ( $context -> contextlevel ) {
case CONTEXT_SYSTEM :
return $baseurl . 'courseid=' . $SITE -> id ;
case CONTEXT_COURSECAT :
// This is nasty, becuase we can only edit questions in a course
// context at the moment, so for now we just return false.
return false ;
case CONTEXT_COURSE :
return $baseurl . 'courseid=' . $context -> instanceid ;
case CONTEXT_MODULE :
return $baseurl . 'cmid=' . $context -> instanceid ;
}
}
2007-09-18 11:23:29 +00:00
/**
* Gets the default category in the most specific context .
* If no categories exist yet then default ones are created in all contexts .
*
2009-05-26 03:23:32 +00:00
* @ global object
2007-09-18 11:23:29 +00:00
* @ param array $contexts The context objects for this context and all parent contexts .
* @ return object The default category - the category in the course context
*/
function question_make_default_categories ( $contexts ) {
2008-06-09 12:16:54 +00:00
global $DB ;
2009-03-18 05:02:27 +00:00
static $preferredlevels = array (
CONTEXT_COURSE => 4 ,
CONTEXT_MODULE => 3 ,
CONTEXT_COURSECAT => 2 ,
CONTEXT_SYSTEM => 1 ,
);
2008-06-09 12:16:54 +00:00
2007-09-18 11:23:29 +00:00
$toreturn = null ;
2009-03-18 05:02:27 +00:00
$preferredness = 0 ;
2007-09-18 11:23:29 +00:00
// If it already exists, just return it.
foreach ( $contexts as $key => $context ) {
2009-03-18 05:02:27 +00:00
if ( ! $exists = $DB -> record_exists ( " question_categories " , array ( 'contextid' => $context -> id ))) {
2008-06-09 12:16:54 +00:00
// Otherwise, we need to make one
$category = new stdClass ;
$contextname = print_context_name ( $context , false , true );
$category -> name = get_string ( 'defaultfor' , 'question' , $contextname );
$category -> info = get_string ( 'defaultinfofor' , 'question' , $contextname );
$category -> contextid = $context -> id ;
$category -> parent = 0 ;
$category -> sortorder = 999 ; // By default, all categories get this number, and are sorted alphabetically.
$category -> stamp = make_unique_id_code ();
2009-06-03 20:00:08 +00:00
$category -> id = $DB -> insert_record ( 'question_categories' , $category );
2008-06-12 09:15:16 +00:00
} else {
2008-09-10 05:28:25 +00:00
$category = question_get_default_category ( $context -> id );
2007-09-18 11:23:29 +00:00
}
2009-03-18 05:33:56 +00:00
if ( $preferredlevels [ $context -> contextlevel ] > $preferredness &&
has_any_capability ( array ( 'moodle/question:usemine' , 'moodle/question:useall' ), $context )) {
2009-03-18 05:02:27 +00:00
$toreturn = $category ;
$preferredness = $preferredlevels [ $context -> contextlevel ];
2007-09-18 11:23:29 +00:00
}
}
2009-03-18 05:02:27 +00:00
if ( ! is_null ( $toreturn )) {
$toreturn = clone ( $toreturn );
}
2007-09-18 11:23:29 +00:00
return $toreturn ;
}
2007-01-07 13:54:36 +00:00
/**
2007-08-09 21:50:59 +00:00
* Get all the category objects , including a count of the number of questions in that category ,
* for all the categories in the lists $contexts .
2007-01-07 13:54:36 +00:00
*
2009-05-26 03:23:32 +00:00
* @ global object
2007-08-09 21:50:59 +00:00
* @ param mixed $contexts either a single contextid , or a comma - separated list of context ids .
* @ param string $sortorder used as the ORDER BY clause in the select statement .
* @ return array of category objects .
2007-01-07 13:54:36 +00:00
*/
2007-08-09 21:50:59 +00:00
function get_categories_for_contexts ( $contexts , $sortorder = 'parent, sortorder, name ASC' ) {
2008-06-09 12:16:54 +00:00
global $DB ;
return $DB -> get_records_sql ( "
2008-09-02 06:55:32 +00:00
SELECT c .* , ( SELECT count ( 1 ) FROM { question } q
2008-06-09 12:16:54 +00:00
WHERE c . id = q . category AND q . hidden = '0' AND q . parent = '0' ) AS questioncount
FROM { question_categories } c
WHERE c . contextid IN ( $contexts )
ORDER BY $sortorder " );
2007-08-09 21:50:59 +00:00
}
2007-05-20 16:17:48 +00:00
2007-08-09 21:50:59 +00:00
/**
* Output an array of question categories .
2009-05-26 03:23:32 +00:00
* @ global object
2007-08-09 21:50:59 +00:00
*/
function question_category_options ( $contexts , $top = false , $currentcat = 0 , $popupform = false , $nochildrenof = - 1 ) {
global $CFG ;
$pcontexts = array ();
foreach ( $contexts as $context ){
$pcontexts [] = $context -> id ;
2007-05-25 05:49:51 +00:00
}
2007-08-09 21:50:59 +00:00
$contextslist = join ( $pcontexts , ', ' );
$categories = get_categories_for_contexts ( $contextslist );
2007-01-07 13:54:36 +00:00
2007-08-09 21:50:59 +00:00
$categories = question_add_context_in_key ( $categories );
if ( $top ){
$categories = question_add_tops ( $categories , $pcontexts );
}
$categories = add_indented_names ( $categories , $nochildrenof );
2007-03-01 09:45:53 +00:00
2007-08-09 21:50:59 +00:00
//sort cats out into different contexts
2007-01-07 13:54:36 +00:00
$categoriesarray = array ();
2007-08-09 21:50:59 +00:00
foreach ( $pcontexts as $pcontext ){
$contextstring = print_context_name ( get_context_instance_by_id ( $pcontext ), true , true );
foreach ( $categories as $category ) {
if ( $category -> contextid == $pcontext ){
$cid = $category -> id ;
if ( $currentcat != $cid || $currentcat == 0 ) {
2007-08-15 12:06:21 +00:00
$countstring = ( ! empty ( $category -> questioncount )) ? " ( $category->questioncount ) " : '' ;
2007-08-09 21:50:59 +00:00
$categoriesarray [ $contextstring ][ $cid ] = $category -> indentedname . $countstring ;
}
}
2007-01-07 13:54:36 +00:00
}
}
2007-08-09 21:50:59 +00:00
if ( $popupform ){
$popupcats = array ();
foreach ( $categoriesarray as $contextstring => $optgroup ){
$popupcats [] = '--' . $contextstring ;
$popupcats = array_merge ( $popupcats , $optgroup );
$popupcats [] = '--' ;
}
return $popupcats ;
} else {
return $categoriesarray ;
}
2007-01-07 13:54:36 +00:00
}
2007-08-09 21:50:59 +00:00
function question_add_context_in_key ( $categories ){
$newcatarray = array ();
foreach ( $categories as $id => $category ) {
$category -> parent = " $category->parent , $category->contextid " ;
$category -> id = " $category->id , $category->contextid " ;
$newcatarray [ " $id , $category->contextid " ] = $category ;
2006-02-24 10:21:40 +00:00
}
2007-08-09 21:50:59 +00:00
return $newcatarray ;
}
function question_add_tops ( $categories , $pcontexts ){
$topcats = array ();
foreach ( $pcontexts as $context ){
$newcat = new object ();
$newcat -> id = " 0, $context " ;
$newcat -> name = get_string ( 'top' );
$newcat -> parent = - 1 ;
$newcat -> contextid = $context ;
$topcats [ " 0, $context " ] = $newcat ;
}
//put topcats in at beginning of array - they'll be sorted into different contexts later.
return array_merge ( $topcats , $categories );
2006-02-24 10:21:40 +00:00
}
/**
2006-10-06 16:48:54 +00:00
* Returns a comma separated list of ids of the category and all subcategories
2009-05-26 03:23:32 +00:00
* @ global object
2006-10-06 16:48:54 +00:00
*/
2006-03-01 07:03:57 +00:00
function question_categorylist ( $categoryid ) {
2008-06-09 12:16:54 +00:00
global $DB ;
2006-02-24 10:21:40 +00:00
// returns a comma separated list of ids of the category and all subcategories
$categorylist = $categoryid ;
2008-11-27 11:50:29 +00:00
if ( $subcategories = $DB -> get_records ( 'question_categories' , array ( 'parent' => $categoryid ), 'sortorder ASC' , 'id, 1' )) {
2006-02-24 10:21:40 +00:00
foreach ( $subcategories as $subcategory ) {
2006-03-01 07:03:57 +00:00
$categorylist .= ',' . question_categorylist ( $subcategory -> id );
2006-02-24 10:21:40 +00:00
}
}
return $categorylist ;
}
2006-11-29 13:29:00 +00:00
2007-08-09 21:50:59 +00:00
2006-02-24 10:21:40 +00:00
2006-05-02 09:04:38 +00:00
//===========================
// Import/Export Functions
//===========================
2006-02-24 15:44:53 +00:00
/**
* Get list of available import or export formats
2009-05-26 03:23:32 +00:00
*
* @ global object
2006-02-24 15:44:53 +00:00
* @ param string $type 'import' if import list , otherwise export list assumed
2007-01-07 12:46:47 +00:00
* @ return array sorted list of import / export formats available
2009-05-26 03:23:32 +00:00
*/
2006-02-24 15:44:53 +00:00
function get_import_export_formats ( $type ) {
global $CFG ;
2009-06-19 14:25:56 +00:00
$fileformats = get_plugin_list ( " qformat " );
2006-02-24 15:44:53 +00:00
$fileformatname = array ();
2006-05-02 09:04:38 +00:00
require_once ( " { $CFG -> dirroot } /question/format.php " );
2009-06-19 14:25:56 +00:00
foreach ( $fileformats as $fileformat => $fdir ) {
$format_file = " $fdir /format.php " ;
if ( file_exists ( $format_file ) ) {
require_once ( $format_file );
2006-02-24 15:44:53 +00:00
}
else {
continue ;
}
2006-03-01 09:30:21 +00:00
$classname = " qformat_ $fileformat " ;
2006-02-24 15:44:53 +00:00
$format_class = new $classname ();
if ( $type == 'import' ) {
$provided = $format_class -> provide_import ();
}
else {
$provided = $format_class -> provide_export ();
}
if ( $provided ) {
$formatname = get_string ( $fileformat , 'quiz' );
if ( $formatname == " [[ $fileformat ]] " ) {
2009-06-21 04:29:37 +00:00
$formatname = get_string ( $fileformat , 'qformat_' . $fileformat );
if ( $formatname == " [[ $fileformat ]] " ) {
$formatname = $fileformat ; // Just use the raw folder name
}
2006-02-24 15:44:53 +00:00
}
$fileformatnames [ $fileformat ] = $formatname ;
}
}
natcasesort ( $fileformatnames );
return $fileformatnames ;
}
2006-02-24 18:49:50 +00:00
/**
* Create default export filename
*
* @ return string default export filename
* @ param object $course
* @ param object $category
*/
function default_export_filename ( $course , $category ) {
//Take off some characters in the filename !!
$takeoff = array ( " " , " : " , " / " , " \\ " , " | " );
2006-10-07 20:47:54 +00:00
$export_word = str_replace ( $takeoff , " _ " , moodle_strtolower ( get_string ( " exportfilename " , " quiz " )));
2006-02-24 18:49:50 +00:00
//If non-translated, use "export"
if ( substr ( $export_word , 0 , 1 ) == " [ " ) {
$export_word = " export " ;
}
//Calculate the date format string
$export_date_format = str_replace ( " " , " _ " , get_string ( " exportnameformat " , " quiz " ));
//If non-translated, use "%Y%m%d-%H%M"
if ( substr ( $export_date_format , 0 , 1 ) == " [ " ) {
$export_date_format = " %%Y%%m%%d-%%H%%M " ;
}
//Calculate the shortname
$export_shortname = clean_filename ( $course -> shortname );
if ( empty ( $export_shortname ) or $export_shortname == '_' ) {
$export_shortname = $course -> id ;
}
//Calculate the category name
$export_categoryname = clean_filename ( $category -> name );
//Calculate the final export filename
//The export word
$export_name = $export_word . " - " ;
//The shortname
2006-10-07 20:47:54 +00:00
$export_name .= moodle_strtolower ( $export_shortname ) . " - " ;
2006-02-24 18:49:50 +00:00
//The category name
2006-10-07 20:47:54 +00:00
$export_name .= moodle_strtolower ( $export_categoryname ) . " - " ;
2006-02-24 18:49:50 +00:00
//The date format
$export_name .= userdate ( time (), $export_date_format , 99 , false );
2007-10-30 19:29:45 +00:00
//Extension is supplied by format later.
2006-02-24 18:49:50 +00:00
return $export_name ;
}
2009-05-26 03:23:32 +00:00
/**
* @ package moodlecore
* @ subpackage question
* @ copyright 1999 onwards Martin Dougiamas { @ link http :// moodle . com }
* @ license http :// www . gnu . org / copyleft / gpl . html GNU GPL v3 or later
*/
2007-08-09 21:50:59 +00:00
class context_to_string_translator {
/**
* @ var array used to translate between contextids and strings for this context .
*/
var $contexttostringarray = array ();
function context_to_string_translator ( $contexts ){
$this -> generate_context_to_string_array ( $contexts );
}
function context_to_string ( $contextid ){
return $this -> contexttostringarray [ $contextid ];
}
function string_to_context ( $contextname ){
$contextid = array_search ( $contextname , $this -> contexttostringarray );
return $contextid ;
}
function generate_context_to_string_array ( $contexts ){
if ( ! $this -> contexttostringarray ){
$catno = 1 ;
foreach ( $contexts as $context ){
switch ( $context -> contextlevel ){
case CONTEXT_MODULE :
$contextstring = 'module' ;
break ;
case CONTEXT_COURSE :
$contextstring = 'course' ;
break ;
case CONTEXT_COURSECAT :
$contextstring = " cat $catno " ;
$catno ++ ;
break ;
case CONTEXT_SYSTEM :
$contextstring = 'system' ;
break ;
}
$this -> contexttostringarray [ $context -> id ] = $contextstring ;
}
}
}
}
2006-02-24 18:49:50 +00:00
2008-09-10 05:28:25 +00:00
/**
* @ return array all the capabilities that relate to accessing particular questions .
*/
function question_get_question_capabilities () {
return array (
'moodle/question:add' ,
'moodle/question:editmine' ,
'moodle/question:editall' ,
'moodle/question:viewmine' ,
'moodle/question:viewall' ,
'moodle/question:usemine' ,
'moodle/question:useall' ,
'moodle/question:movemine' ,
'moodle/question:moveall' ,
);
}
/**
* @ return array all the question bank capabilities .
*/
function question_get_all_capabilities () {
$caps = question_get_question_capabilities ();
$caps [] = 'moodle/question:managecategory' ;
$caps [] = 'moodle/question:flag' ;
return $caps ;
}
2007-08-09 21:50:59 +00:00
/**
* Check capability on category
2009-05-26 03:23:32 +00:00
*
* @ global object
* @ global object
2007-08-09 21:50:59 +00:00
* @ param mixed $question object or id
* @ param string $cap 'add' , 'edit' , 'view' , 'use' , 'move'
* @ param integer $cachecat useful to cache all question records in a category
* @ return boolean this user has the capability $cap for this question $question ?
*/
function question_has_capability_on ( $question , $cap , $cachecat = - 1 ){
2008-06-09 12:16:54 +00:00
global $USER , $DB ;
2008-06-12 12:55:13 +00:00
// nicolasconnault@gmail.com In some cases I get $question === false. Since no such object exists, it can't be deleted, we can safely return true
if ( $question === false ) {
return true ;
}
2007-08-09 21:50:59 +00:00
// these are capabilities on existing questions capabilties are
//set per category. Each of these has a mine and all version. Append 'mine' and 'all'
$question_questioncaps = array ( 'edit' , 'view' , 'use' , 'move' );
static $questions = array ();
static $categories = array ();
static $cachedcat = array ();
2007-08-16 05:19:33 +00:00
if ( $cachecat != - 1 && ( array_search ( $cachecat , $cachedcat ) === FALSE )){
2008-06-09 12:16:54 +00:00
$questions += $DB -> get_records ( 'question' , array ( 'category' => $cachecat ));
2007-08-09 21:50:59 +00:00
$cachedcat [] = $cachecat ;
}
if ( ! is_object ( $question )){
if ( ! isset ( $questions [ $question ])){
2009-02-25 06:12:55 +00:00
if ( ! $questions [ $question ] = $DB -> get_record ( 'question' , array ( 'id' => $question ), 'id,category,createdby' )) {
2007-11-26 13:32:23 +00:00
print_error ( 'questiondoesnotexist' , 'question' );
2007-08-09 21:50:59 +00:00
}
}
$question = $questions [ $question ];
}
if ( ! isset ( $categories [ $question -> category ])){
2008-06-09 12:16:54 +00:00
if ( ! $categories [ $question -> category ] = $DB -> get_record ( 'question_categories' , array ( 'id' => $question -> category ))) {
2007-08-09 21:50:59 +00:00
print_error ( 'invalidcategory' , 'quiz' );
}
}
$category = $categories [ $question -> category ];
if ( array_search ( $cap , $question_questioncaps ) !== FALSE ){
if ( ! has_capability ( 'moodle/question:' . $cap . 'all' , get_context_instance_by_id ( $category -> contextid ))){
if ( $question -> createdby == $USER -> id ){
return has_capability ( 'moodle/question:' . $cap . 'mine' , get_context_instance_by_id ( $category -> contextid ));
} else {
return false ;
}
} else {
return true ;
}
} else {
return has_capability ( 'moodle/question:' . $cap , get_context_instance_by_id ( $category -> contextid ));
}
}
/**
* Require capability on question .
*/
function question_require_capability_on ( $question , $cap ){
if ( ! question_has_capability_on ( $question , $cap )){
print_error ( 'nopermissions' , '' , '' , $cap );
}
return true ;
}
2009-05-26 03:23:32 +00:00
/**
* @ global object
*/
2007-08-09 21:50:59 +00:00
function question_file_links_base_url ( $courseid ){
global $CFG ;
$baseurl = preg_quote ( " $CFG->wwwroot /file.php " , '!' );
$baseurl .= '(' . preg_quote ( '?file=' , '!' ) . ')?' ; //may or may not
//be using slasharguments, accept either
$baseurl .= " / $courseid / " ; //course directory
return $baseurl ;
}
2009-05-26 03:23:32 +00:00
/**
2007-08-09 21:50:59 +00:00
* Find all course / site files linked to in a piece of html .
2009-05-26 03:23:32 +00:00
* @ global object
2007-08-09 21:50:59 +00:00
* @ param string html the html to search
* @ param int course search for files for courseid course or set to siteid for
* finding site files .
* @ return array files with keys being files .
*/
function question_find_file_links_from_html ( $html , $courseid ){
global $CFG ;
$baseurl = question_file_links_base_url ( $courseid );
$searchfor = '!' .
'(<\s*(a|img)\s[^>]*(href|src)\s*=\s*")' . $baseurl . '([^"]*)"' .
'|' .
'(<\s*(a|img)\s[^>]*(href|src)\s*=\s*\')' . $baseurl . '([^\']*)\'' .
'!i' ;
$matches = array ();
$no = preg_match_all ( $searchfor , $html , $matches );
if ( $no ){
$rawurls = array_filter ( array_merge ( $matches [ 5 ], $matches [ 10 ])); //array_filter removes empty elements
//remove any links that point somewhere they shouldn't
foreach ( array_keys ( $rawurls ) as $rawurlkey ){
if ( ! $cleanedurl = question_url_check ( $rawurls [ $rawurlkey ])){
unset ( $rawurls [ $rawurlkey ]);
} else {
$rawurls [ $rawurlkey ] = $cleanedurl ;
}
}
$urls = array_flip ( $rawurls ); // array_flip removes duplicate files
// and when we merge arrays will continue to automatically remove duplicates
} else {
$urls = array ();
}
return $urls ;
}
2008-06-09 12:16:54 +00:00
/**
2007-08-09 21:50:59 +00:00
* Check that url doesn 't point anywhere it shouldn' t
*
2009-05-26 03:23:32 +00:00
* @ global object
2007-08-09 21:50:59 +00:00
* @ param $url string relative url within course files directory
* @ return mixed boolean false if not OK or cleaned URL as string if OK
*/
function question_url_check ( $url ){
global $CFG ;
if (( substr ( strtolower ( $url ), 0 , strlen ( $CFG -> moddata )) == strtolower ( $CFG -> moddata )) ||
( substr ( strtolower ( $url ), 0 , 10 ) == 'backupdata' )){
return false ;
} else {
return clean_param ( $url , PARAM_PATH );
}
}
2008-06-09 12:16:54 +00:00
/**
2007-08-09 21:50:59 +00:00
* Find all course / site files linked to in a piece of html .
2009-05-26 03:23:32 +00:00
*
* @ global object
2007-08-09 21:50:59 +00:00
* @ param string html the html to search
* @ param int course search for files for courseid course or set to siteid for
* finding site files .
* @ return array files with keys being files .
*/
function question_replace_file_links_in_html ( $html , $fromcourseid , $tocourseid , $url , $destination , & $changed ){
global $CFG ;
2008-07-10 09:55:11 +00:00
require_once ( $CFG -> libdir . '/filelib.php' );
$tourl = get_file_url ( " $tocourseid / $destination " );
2007-08-09 21:50:59 +00:00
$fromurl = question_file_links_base_url ( $fromcourseid ) . preg_quote ( $url , '!' );
$searchfor = array ( '!(<\s*(a|img)\s[^>]*(href|src)\s*=\s*")' . $fromurl . '(")!i' ,
'!(<\s*(a|img)\s[^>]*(href|src)\s*=\s*\')' . $fromurl . '(\')!i' );
$newhtml = preg_replace ( $searchfor , '\\1' . $tourl . '\\5' , $html );
if ( $newhtml != $html ){
$changed = true ;
}
return $newhtml ;
}
2009-05-26 03:23:32 +00:00
/**
* @ global object
*/
2007-08-09 21:50:59 +00:00
function get_filesdir_from_context ( $context ){
2008-06-09 12:16:54 +00:00
global $DB ;
2007-08-09 21:50:59 +00:00
switch ( $context -> contextlevel ){
case CONTEXT_COURSE :
$courseid = $context -> instanceid ;
break ;
case CONTEXT_MODULE :
2008-06-09 12:16:54 +00:00
$courseid = $DB -> get_field ( 'course_modules' , 'course' , array ( 'id' => $context -> instanceid ));
2007-08-09 21:50:59 +00:00
break ;
case CONTEXT_COURSECAT :
case CONTEXT_SYSTEM :
$courseid = SITEID ;
break ;
default :
2008-05-14 08:37:38 +00:00
print_error ( 'invalidcontext' );
2007-08-09 21:50:59 +00:00
}
return $courseid ;
}
2008-07-18 14:36:24 +00:00
/**
2008-07-30 09:02:44 +00:00
* Get the real state - the correct question id and answer - for a random
* question .
2008-07-18 14:36:24 +00:00
* @ param object $state with property answer .
* @ return mixed return integer real question id or false if there was an
* error ..
*/
2008-07-30 09:02:44 +00:00
function question_get_real_state ( $state ){
2009-08-18 05:20:12 +00:00
global $OUTPUT ;
2008-07-30 09:02:44 +00:00
$realstate = clone ( $state );
2008-07-18 14:36:24 +00:00
$matches = array ();
2008-11-10 07:03:25 +00:00
if ( ! preg_match ( '|^random([0-9]+)-(.*)|' , $state -> answer , $matches )){
2009-08-18 05:20:12 +00:00
echo $OUTPUT -> notification ( get_string ( 'errorrandom' , 'quiz_statistics' ));
2008-07-18 14:36:24 +00:00
return false ;
} else {
2008-07-30 09:02:44 +00:00
$realstate -> question = $matches [ 1 ];
$realstate -> answer = $matches [ 2 ];
return $realstate ;
2008-07-18 14:36:24 +00:00
}
}
2008-07-30 09:02:44 +00:00
2008-08-29 10:08:27 +00:00
/**
* Update the flagged state of a particular question session .
*
2009-05-26 03:23:32 +00:00
* @ global object
2008-08-29 10:08:27 +00:00
* @ 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 ));
}
2008-09-03 08:32:29 +00:00
/**
* Update the flagged state of all the questions in an attempt , where a new .
*
2009-05-26 03:23:32 +00:00
* @ global object
2008-09-03 08:32:29 +00:00
* @ param integer $sessionid question_session id .
* @ param boolean $newstate the new state for the flag .
* @ return boolean success or failure .
*/
function question_save_flags ( $formdata , $attemptid , $questionids ) {
global $DB ;
$donequestionids = array ();
foreach ( $formdata as $postvariable => $value ) {
list ( $qid , $key ) = question_id_and_key_from_post_name ( $postvariable );
if ( $qid !== false && in_array ( $qid , $questionids )) {
if ( $key == '_flagged' ) {
$DB -> set_field ( 'question_sessions' , 'flagged' , ! empty ( $value ),
array ( 'attemptid' => $attemptid , 'questionid' => $qid ));
$donequestionids [ $qid ] = 1 ;
}
}
}
foreach ( $questionids as $qid ) {
if ( ! isset ( $donequestionids [ $qid ])) {
$DB -> set_field ( 'question_sessions' , 'flagged' , 0 ,
array ( 'attemptid' => $attemptid , 'questionid' => $qid ));
}
}
}
2008-08-29 10:08:27 +00:00
/**
2009-05-26 03:23:32 +00:00
*
* @ global object
2008-08-29 10:08:27 +00:00
* @ 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 );
}