2006-02-24 10:21:40 +00:00
< ? php // $Id$
/**
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 .
*
* @ version $Id $
* @ author Martin Dougiamas and many others . This has recently been completely
* rewritten by Alex Smith , Julian Sedding and Gustav Delius as part of
* the Serving Mathematics project
* { @ link http :// maths . york . ac . uk / serving_maths }
* @ license http :// www . gnu . org / copyleft / gpl . html GNU Public License
* @ package question
*/
/// 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
2006-02-24 10:25:16 +00:00
/**#@-*/
/** #@+
2006-03-20 20:45:55 +00:00
* The core question types
*/
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 ( " RQP " , " rqp " );
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
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 );
/** When processing responses the code checks that the new responses at
* a question differ from those given on the previous submission . If
* furthermore this flag is set to true
* then the code goes through the whole history of responses and checks if
* ANY of them are identical to the current response in which case the
* current response is ignored .
*/
define ( 'QUESTION_IGNORE_DUPRESP' , 2 );
/**#@-*/
2006-02-24 13:48:43 +00:00
/// QTYPES INITIATION //////////////////
2006-02-24 10:21:40 +00:00
/**
2006-03-20 20:45:55 +00:00
* Array holding question type objects
*/
2006-03-18 14:14:55 +00:00
global $QTYPES ;
2006-03-20 20:45:55 +00:00
$QTYPES = array (); // This array will be populated when the questiontype.php files are loaded below
2006-03-18 14:14:55 +00:00
/**
2006-03-20 20:45:55 +00:00
* Array of question types names translated to the user ' s language
*
* The $QTYPE_MENU array holds 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 .
* The complete list of question types can be found in { @ link $QTYPES } .
*/
2006-03-18 14:14:55 +00:00
$QTYPE_MENU = array (); // This array will be populated when the questiontype.php files are loaded
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-02-24 13:48:43 +00:00
/*
* Load the questiontype . php file for each question type
2006-02-24 10:21:40 +00:00
* These files in turn instantiate the corresponding question type class
2006-03-18 14:14:55 +00:00
* and add them to the $QTYPES array
2006-02-24 10:21:40 +00:00
*/
2006-03-24 19:54:13 +00:00
$qtypenames = get_list_of_plugins ( 'question/type' );
2006-02-24 13:48:43 +00:00
foreach ( $qtypenames as $qtypename ) {
// Instanciates all plug-in question types
2006-03-24 19:31:46 +00:00
$qtypefilepath = " $CFG->dirroot /question/type/ $qtypename /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
}
}
/// OTHER CLASSES /////////////////////////////////////////////////////////
/**
2006-03-20 20:45:55 +00:00
* This holds the options that are set by the course module
*/
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 .
*/
var $shuffleanswers = false ;
/**
* 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
*
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 ) {
2006-03-20 23:04:22 +00:00
$instances = array ();
$modules = get_records ( 'modules' );
foreach ( $modules as $module ) {
2006-03-21 09:06:34 +00:00
$fn = $module -> name . '_question_list_instances' ;
2006-03-20 23:04:22 +00:00
if ( function_exists ( $fn )) {
2006-03-21 09:06:34 +00:00
$instances = $instances + $fn ( $questionid );
2006-03-20 23:04:22 +00:00
}
}
return $instances ;
}
2006-02-24 10:21:40 +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 () {
// define basic array of grades
$grades = array (
1 ,
0.9 ,
0.8 ,
0.75 ,
0.70 ,
0.66666 ,
0.60 ,
0.50 ,
0.40 ,
0.33333 ,
0.30 ,
0.25 ,
0.20 ,
0.16666 ,
0.142857 ,
0.125 ,
0.11111 ,
0.10 ,
0.05 ,
0 );
// 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 ) {
if ( $grade == $value ) {
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 ;
}
}
2006-03-21 09:06:34 +00:00
/**
* Tests whether a category is in use by any activity module
*
* @ return boolean
* @ param integer $categoryid
* @ param boolean $recursive Whether to examine category children recursively
*/
function question_category_isused ( $categoryid , $recursive = false ) {
//Look at each question in the category
if ( $questions = get_records ( 'question' , 'category' , $categoryid )) {
foreach ( $questions as $question ) {
if ( count ( question_list_instances ( $question -> id ))) {
return true ;
}
}
}
//Look under child categories recursively
if ( $recursive ) {
if ( $children = get_records ( 'question_categories' , 'parent' , $categoryid )) {
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
*
* @ param object $question The question being deleted
*/
function delete_attempt ( $attemptid ) {
global $QTYPES ;
$states = get_records ( 'question_states' , 'attempt' , $attemptid );
$stateslist = implode ( ',' , array_keys ( $states ));
// delete questiontype-specific data
foreach ( $QTYPES as $qtype ) {
$qtype -> delete_states ( $stateslist );
}
// delete entries from all other question tables
// It is important that this is done only after calling the questiontype functions
delete_records ( " question_states " , " attempt " , $attemptid );
delete_records ( " question_sessions " , " attemptid " , $attemptid );
return ;
}
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
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 ) {
2006-02-24 13:48:43 +00:00
global $QTYPES ;
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
2006-03-21 15:33:30 +00:00
if ( $question = get_record ( 'question' , 'id' , $questionid )) {
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
2006-03-25 21:07:11 +00:00
if ( $states = get_records ( 'question_states' , 'question' , $questionid )) {
$stateslist = implode ( ',' , array_keys ( $states ));
// 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
delete_records ( " question_answers " , " question " , $questionid );
delete_records ( " question_states " , " question " , $questionid );
delete_records ( " question_sessions " , " questionid " , $questionid );
// Now recursively delete all child questions
if ( $children = get_records ( 'question' , 'parent' , $questionid )) {
2006-02-24 10:21:40 +00:00
foreach ( $children as $child ) {
2006-03-20 23:04:22 +00:00
delete_question ( $child -> id );
2006-02-24 10:21:40 +00:00
}
}
2006-03-20 23:04:22 +00:00
// Finally delete the question record itself
delete_records ( 'question' , 'id' , $questionid );
return ;
2006-02-24 10:21:40 +00:00
}
2006-03-21 09:06:34 +00:00
/**
* All non - used question categories and their questions are deleted and
* categories still used by other courses are moved to the site course .
*
* @ param object $course an object representing the course
* @ param boolean $feedback to specify if the process must output a summary of its work
* @ return boolean
*/
function question_delete_course ( $course , $feedback = true ) {
global $CFG , $QTYPES ;
//To detect if we have created the "container category"
$concatid = 0 ;
//The "container" category we'll create if we need if
$contcat = new object ;
//To temporary store changes performed with parents
$parentchanged = array ();
//To store feedback to be showed at the end of the process
$feedbackdata = array ();
//Cache some strings
$strcatcontainer = get_string ( 'containercategorycreated' , 'quiz' );
$strcatmoved = get_string ( 'usedcategorymoved' , 'quiz' );
$strcatdeleted = get_string ( 'unusedcategorydeleted' , 'quiz' );
if ( $categories = get_records ( 'question_categories' , 'course' , $course -> id , 'parent' , 'id, parent, name, course' )) {
//Sort categories following their tree (parent-child) relationships
$categories = sort_categories_by_tree ( $categories );
foreach ( $categories as $cat ) {
//Get the full record
$category = get_record ( 'question_categories' , 'id' , $cat -> id );
//Check if the category is being used anywhere
if ( question_category_isused ( $category -> id , true )) {
//It's being used. Cannot delete it, so:
//Create a container category in SITEID course if it doesn't exist
if ( ! $concatid ) {
$concat -> course = SITEID ;
if ( ! isset ( $course -> shortname )) {
$course -> shortname = 'id=' . $course -> id ;
}
$concat -> name = get_string ( 'savedfromdeletedcourse' , 'quiz' , $course -> shortname );
$concat -> info = $concat -> name ;
$concat -> publish = 1 ;
$concat -> stamp = make_unique_id_code ();
$concatid = insert_record ( 'question_categories' , $concat );
//Fill feedback
$feedbackdata [] = array ( $concat -> name , $strcatcontainer );
}
//Move the category to the container category in SITEID course
$category -> course = SITEID ;
//Assign to container if the category hasn't parent or if the parent is wrong (not belongs to the course)
if ( ! $category -> parent || ! isset ( $categories [ $category -> parent ])) {
$category -> parent = $concatid ;
}
//If it's being used, its publish field should be 1
$category -> publish = 1 ;
//Let's update it
update_record ( 'question_categories' , $category );
//Save this parent change for future use
$parentchanged [ $category -> id ] = $category -> parent ;
//Fill feedback
$feedbackdata [] = array ( $category -> name , $strcatmoved );
} else {
//Category isn't being used so:
//Delete it completely (questions and category itself)
//deleting questions
if ( $questions = get_records ( " question " , " category " , $category -> id )) {
foreach ( $questions as $question ) {
delete_question ( $question -> id );
}
delete_records ( " question " , " category " , $category -> id );
}
//delete the category
delete_records ( 'question_categories' , 'id' , $category -> id );
//Save this parent change for future use
if ( ! empty ( $category -> parent )) {
$parentchanged [ $category -> id ] = $category -> parent ;
} else {
$parentchanged [ $category -> id ] = $concatid ;
}
//Update all its child categories to re-parent them to grandparent.
set_field ( 'question_categories' , 'parent' , $parentchanged [ $category -> id ], 'parent' , $category -> id );
//Fill feedback
$feedbackdata [] = array ( $category -> name , $strcatdeleted );
}
}
//Inform about changes performed if feedback is enabled
if ( $feedback ) {
$table -> head = array ( get_string ( 'category' , 'quiz' ), get_string ( 'action' ));
$table -> data = $feedbackdata ;
print_table ( $table );
}
}
return true ;
}
2006-02-24 10:21:40 +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 .
* @ return bool Indicates success or failure .
* @ param mixed $questions Either an array of question objects to be updated
* or just a single question object
*/
2006-02-28 09:26:00 +00:00
function get_question_options ( & $questions ) {
2006-02-24 13:48:43 +00:00
global $QTYPES ;
2006-02-24 10:21:40 +00:00
if ( is_array ( $questions )) { // deal with an array of questions
// get the keys of the input array
$keys = array_keys ( $questions );
// update each question object
foreach ( $keys as $i ) {
2006-03-24 21:21:59 +00:00
if ( ! array_key_exists ( $questions [ $i ] -> qtype , $QTYPES )) {
$questions [ $i ] -> qtype = 'missingtype' ;
}
2006-02-24 10:21:40 +00:00
// set name prefix
2006-02-28 09:26:00 +00:00
$questions [ $i ] -> name_prefix = question_make_name_prefix ( $i );
2006-02-24 10:21:40 +00:00
2006-02-24 13:48:43 +00:00
if ( ! $QTYPES [ $questions [ $i ] -> qtype ] -> get_question_options ( $questions [ $i ]))
2006-02-24 10:21:40 +00:00
return false ;
}
return true ;
} else { // deal with single question
2006-03-24 21:21:59 +00:00
if ( ! array_key_exists ( $questions -> qtype , $QTYPES )) {
$questions -> qtype = 'missingtype' ;
}
2006-02-28 09:26:00 +00:00
$questions -> name_prefix = question_make_name_prefix ( $questions -> id );
2006-02-24 13:48:43 +00:00
return $QTYPES [ $questions -> qtype ] -> get_question_options ( $questions );
2006-02-24 10:21:40 +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
2006-02-28 09:26:00 +00:00
* 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
2006-02-24 10:21:40 +00:00
* 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 .
*/
2006-02-28 09:26:00 +00:00
function get_question_states ( & $questions , $cmoptions , $attempt ) {
2006-02-24 13:48:43 +00:00
global $CFG , $QTYPES ;
2006-02-24 10:21:40 +00:00
// get the question ids
$ids = array_keys ( $questions );
$questionlist = implode ( ',' , $ids );
// The question field must be listed first so that it is used as the
// array index in the array returned by get_records_sql
$statefields = 'n.questionid as question, s.*, n.sumpenalty' ;
// Load the newest states for the questions
$sql = " SELECT $statefields " .
2006-02-28 09:26:00 +00:00
" FROM { $CFG -> prefix } question_states s, " .
2006-02-24 10:21:40 +00:00
" { $CFG -> prefix } question_sessions n " .
" WHERE s.id = n.newest " .
" AND n.attemptid = ' $attempt->uniqueid ' " .
" AND n.questionid IN ( $questionlist ) " ;
$states = get_records_sql ( $sql );
// Load the newest graded states for the questions
$sql = " SELECT $statefields " .
2006-02-28 09:26:00 +00:00
" FROM { $CFG -> prefix } question_states s, " .
2006-02-24 10:21:40 +00:00
" { $CFG -> prefix } question_sessions n " .
" WHERE s.id = n.newgraded " .
" AND n.attemptid = ' $attempt->uniqueid ' " .
" AND n.questionid IN ( $questionlist ) " ;
$gradedstates = get_records_sql ( $sql );
// loop through all questions and set the last_graded states
foreach ( $ids as $i ) {
if ( isset ( $states [ $i ])) {
2006-02-28 09:26:00 +00:00
restore_question_state ( $questions [ $i ], $states [ $i ]);
2006-02-24 10:21:40 +00:00
if ( isset ( $gradedstates [ $i ])) {
2006-02-28 09:26:00 +00:00
restore_question_state ( $questions [ $i ], $gradedstates [ $i ]);
2006-02-24 10:21:40 +00:00
$states [ $i ] -> last_graded = $gradedstates [ $i ];
} else {
$states [ $i ] -> last_graded = clone ( $states [ $i ]);
2006-03-22 17:22:36 +00:00
$states [ $i ] -> last_graded -> responses = array ( '' => '' );
2006-02-24 10:21:40 +00:00
}
} else {
2006-03-26 07:59:43 +00:00
// create a new empty state
$states [ $i ] = new object ;
$states [ $i ] -> attempt = $attempt -> uniqueid ;
$states [ $i ] -> question = ( int ) $i ;
$states [ $i ] -> seq_number = 0 ;
$states [ $i ] -> timestamp = $attempt -> timestart ;
$states [ $i ] -> event = ( $attempt -> timefinish ) ? QUESTION_EVENTCLOSE : QUESTION_EVENTOPEN ;
$states [ $i ] -> grade = 0 ;
$states [ $i ] -> raw_grade = 0 ;
$states [ $i ] -> penalty = 0 ;
$states [ $i ] -> sumpenalty = 0 ;
$states [ $i ] -> responses = array ( '' => '' );
// Prevent further changes to the session from incrementing the
// sequence number
$states [ $i ] -> changed = true ;
// Create the empty question type specific information
if ( ! $QTYPES [ $questions [ $i ] -> qtype ]
-> create_session_and_responses ( $questions [ $i ], $states [ $i ], $cmoptions , $attempt )) {
return false ;
2006-02-24 10:21:40 +00:00
}
2006-03-26 07:59:43 +00:00
$states [ $i ] -> last_graded = clone ( $states [ $i ]);
2006-02-24 10:21:40 +00:00
}
}
return $states ;
}
/**
* Creates the run - time fields for the states
*
* Extends the state objects for a question by calling
* { @ link restore_session_and_responses ()}
* @ return boolean Represents success or failure
* @ param object $question The question for which the state is needed
* @ param object $state The state as loaded from the database
*/
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
$state -> responses = array ( '' => $state -> answer );
unset ( $state -> answer );
// 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-02-24 10:21:40 +00:00
-> restore_session_and_responses ( $question , $state );
}
/**
* 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 .
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 .
*/
2006-02-28 09:26:00 +00:00
function save_question_session ( & $question , & $state ) {
2006-02-24 13:48:43 +00:00
global $QTYPES ;
2006-02-24 10:21:40 +00:00
// Check if the state has changed
if ( ! $state -> changed && isset ( $state -> id )) {
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
if ( isset ( $state -> update )) { // this ->update field is only used by the
// regrading function to force the old state record to be overwritten
2006-02-28 09:26:00 +00:00
update_record ( 'question_states' , $state );
2006-02-24 10:21:40 +00:00
} else {
2006-02-28 09:26:00 +00:00
if ( ! $state -> id = insert_record ( 'question_states' , $state )) {
2006-02-24 10:21:40 +00:00
unset ( $state -> id );
unset ( $state -> answer );
return false ;
}
// this is the most recent state
if ( ! record_exists ( 'question_sessions' , 'attemptid' ,
$state -> attempt , 'questionid' , $question -> id )) {
$new -> attemptid = $state -> attempt ;
$new -> questionid = $question -> id ;
$new -> newest = $state -> id ;
$new -> sumpenalty = $state -> sumpenalty ;
if ( ! insert_record ( 'question_sessions' , $new )) {
error ( 'Could not insert entry in question_sessions' );
}
} else {
set_field ( 'question_sessions' , 'newest' , $state -> id , 'attemptid' ,
$state -> attempt , 'questionid' , $question -> id );
}
2006-02-28 09:26:00 +00:00
if ( question_state_is_graded ( $state )) {
2006-02-24 10:21:40 +00:00
// this is also the most recent graded state
if ( $newest = get_record ( 'question_sessions' , 'attemptid' ,
$state -> attempt , 'questionid' , $question -> id )) {
$newest -> newgraded = $state -> id ;
$newest -> sumpenalty = $state -> sumpenalty ;
update_record ( 'question_sessions' , $newest );
}
}
}
unset ( $state -> answer );
// Save the question type specific state information and responses
2006-02-24 13:48:43 +00:00
if ( ! $QTYPES [ $question -> qtype ] -> save_session_and_responses (
2006-02-24 10:21:40 +00:00
$question , $state )) {
return false ;
}
// 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 ) {
2006-03-27 17:38:30 +00:00
return ( $state -> event == QUESTION_EVENTGRADE
or $state -> event == QUESTION_EVENTCLOSEANDGRADE
or $state -> event == QUESTION_EVENTMANUALGRADE );
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 ) {
return ( $state -> event == QUESTION_EVENTCLOSE or $state -> event == QUESTION_EVENTCLOSEANDGRADE );
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 ])) {
error ( 'Form contained question that is not in questionids' );
}
// 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 ;
2006-03-22 17:22:36 +00:00
// Set the timestamp
$actions [ $quid ] -> timestamp = $time ;
2006-02-24 10:21:40 +00:00
}
}
return $actions ;
}
/**
* 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 .
*
2006-02-28 09:26:00 +00:00
* TODO : Make sure this is not quiz - specific
*
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 .
*/
2006-02-28 09:26:00 +00:00
function regrade_question_in_attempt ( $question , $attempt , $cmoptions , $verbose = false ) {
2006-02-24 10:21:40 +00:00
// load all states for this question in this attempt, ordered in sequence
2006-02-28 09:26:00 +00:00
if ( $states = get_records_select ( 'question_states' ,
2006-02-24 10:21:40 +00:00
" attempt = ' { $attempt -> uniqueid } ' AND question = ' { $question -> id } ' " , 'seq_number ASC' )) {
$states = array_values ( $states );
// Subtract the grade for the latest state from $attempt->sumgrades to get the
// sumgrades for the attempt without this question.
$attempt -> sumgrades -= $states [ count ( $states ) - 1 ] -> grade ;
// Initialise the replaystate
$state = clone ( $states [ 0 ]);
2006-02-28 09:26:00 +00:00
restore_question_state ( $question , $state );
2006-02-24 10:21:40 +00:00
$state -> sumpenalty = 0.0 ;
$replaystate = clone ( $state );
$replaystate -> last_graded = $state ;
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
2006-04-05 05:53:18 +00:00
if ( QUESTION_EVENTCLOSE == $states [ $j ] -> event
2006-03-19 18:28:29 +00:00
or QUESTION_EVENTGRADE == $states [ $j ] -> event
or QUESTION_EVENTCLOSEANDGRADE == $states [ $j ] -> event ) {
$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 ;
}
// Reprocess (regrade) responses
2006-02-28 09:26:00 +00:00
if ( ! question_process_responses ( $question , $replaystate , $action , $cmoptions ,
2006-02-24 10:21:40 +00:00
$attempt )) {
$verbose && notify ( " Couldn't regrade state # { $state -> id } ! " );
}
// 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
2006-04-05 05:53:18 +00:00
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 ;
2006-02-24 10:21:40 +00:00
}
$replaystate -> id = $states [ $j ] -> id ;
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
2006-02-28 09:26:00 +00:00
save_question_session ( $question , $replaystate );
2006-02-24 10:21:40 +00:00
}
2006-04-05 05:53:18 +00:00
if ( $changed ) {
update_record ( 'quiz_attempts' , $attempt );
2006-02-24 10:21:40 +00:00
}
2006-04-05 05:53:18 +00:00
return $changed ;
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
*
* @ return boolean Indicates success / failure
* @ 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
*/
2006-02-28 09:26:00 +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 ( '' => '' );
}
// make sure these are gone!
unset ( $action -> responses [ 'mark' ], $action -> responses [ 'validate' ]);
// 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 )) {
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 ;
}
// Check for unchanged responses (exactly unchanged, not equivalent).
// We also have to catch questions that the student has not yet attempted
$sameresponses = (( $state -> responses == $action -> responses ) or
( $state -> responses == array ( '' => '' ) && array_keys ( array_count_values ( $action -> responses )) === array ( '' )));
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
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
$newstate -> last_graded = $state -> last_graded ;
$newstate -> timestamp = $action -> timestamp ;
$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
2006-02-24 13:48:43 +00:00
$QTYPES [ $question -> qtype ] -> grade_responses (
2006-02-24 10:21:40 +00:00
$question , $state , $cmoptions );
2006-03-19 18:28:29 +00:00
// Don't allow the processing to change the event type
2006-02-24 10:21:40 +00:00
$state -> event = $action -> event ;
2006-03-19 18:28:29 +00:00
} else if ( QUESTION_EVENTSUBMIT == $action -> event ) {
2006-02-24 10:21:40 +00:00
// Work out if the current responses (or equivalent responses) were
// already given in
// a. the last graded attempt
// b. any other graded attempt
2006-02-24 13:48:43 +00:00
if ( $QTYPES [ $question -> qtype ] -> compare_responses (
2006-02-24 10:21:40 +00:00
$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
} else {
2006-03-20 20:45:55 +00:00
if ( $cmoptions -> optionflags & QUESTION_IGNORE_DUPRESP ) {
2006-02-24 10:21:40 +00:00
/* Walk back through the previous graded states looking for
one where the responses are equivalent to the current
responses . If such a state is found , set the current grading
details to those of that state and set the event to
2006-03-19 18:28:29 +00:00
QUESTION_EVENTDUPLICATE */
2006-02-28 09:26:00 +00:00
question_search_for_duplicate_responses ( $question , $state );
2006-02-24 10:21:40 +00:00
}
}
2006-03-19 18:28:29 +00:00
// If we did not find a duplicate, perform grading
if ( QUESTION_EVENTDUPLICATE != $state -> event ) {
// Decrease sumgrades by previous grade and then later add new grade
$attempt -> sumgrades -= ( float ) $state -> last_graded -> grade ;
$QTYPES [ $question -> qtype ] -> grade_responses (
$question , $state , $cmoptions );
// Calculate overall grade using correct penalty method
question_apply_penalty_and_timelimit ( $question , $state , $attempt , $cmoptions );
// Update the last graded state (don't simplify!)
unset ( $state -> last_graded );
$state -> last_graded = clone ( $state );
unset ( $state -> last_graded -> changed );
$attempt -> sumgrades += ( float ) $state -> last_graded -> grade ;
}
2006-02-28 09:26:00 +00:00
} else if ( QUESTION_EVENTCLOSE == $action -> event ) {
2006-02-24 10:21:40 +00:00
// decrease sumgrades by previous grade and then later add new grade
$attempt -> sumgrades -= ( float ) $state -> last_graded -> grade ;
// Only mark if they haven't been marked already
if ( ! $sameresponses ) {
2006-02-24 13:48:43 +00:00
$QTYPES [ $question -> qtype ] -> grade_responses (
2006-02-24 10:21:40 +00:00
$question , $state , $cmoptions );
// Calculate overall grade using correct penalty method
2006-02-28 09:26:00 +00:00
question_apply_penalty_and_timelimit ( $question , $state , $attempt , $cmoptions );
2006-02-24 10:21:40 +00:00
}
// Update the last graded state (don't simplify!)
unset ( $state -> last_graded );
$state -> last_graded = clone ( $state );
unset ( $state -> last_graded -> changed );
$attempt -> sumgrades += ( float ) $state -> last_graded -> grade ;
}
$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
}
/**
* Compare current responses to all previous graded responses
*
2006-02-28 09:26:00 +00:00
* This is used by { @ link question_process_responses ()} to determine whether
2006-02-24 10:21:40 +00:00
* to ignore the marking request for the current response . However this
* check against all previous graded responses is only performed if
2006-03-20 20:45:55 +00:00
* the QUESTION_IGNORE_DUPRESP bit in $cmoptions -> optionflags is set
2006-03-19 18:28:29 +00:00
* If the current response is a duplicate of a previously graded response then
* $STATE -> event is set to QUESTION_EVENTDUPLICATE .
2006-02-24 10:21:40 +00:00
* @ return boolean Indicates if a state with duplicate responses was
* found .
* @ param object $question
* @ param object $state
*/
2006-02-28 09:26:00 +00:00
function question_search_for_duplicate_responses ( & $question , & $state ) {
2006-02-24 10:21:40 +00:00
// get all previously graded question states
2006-02-24 13:48:43 +00:00
global $QTYPES ;
2006-03-01 07:03:57 +00:00
if ( ! $oldstates = get_records ( 'question_states' , " event = ' " .
2006-02-28 09:26:00 +00:00
QUESTION_EVENTGRADE . " ' AND " . " question = ' " . $question -> id .
2006-02-24 10:21:40 +00:00
" ' " , 'seq_number DESC' )) {
return false ;
}
foreach ( $oldstates as $oldstate ) {
2006-02-24 13:48:43 +00:00
if ( $QTYPES [ $question -> qtype ] -> restore_session_and_responses (
2006-02-24 10:21:40 +00:00
$question , $oldstate )) {
2006-02-24 13:48:43 +00:00
if ( ! $QTYPES [ $question -> qtype ] -> compare_responses (
2006-02-24 10:21:40 +00:00
$question , $state , $oldstate )) {
2006-03-19 18:28:29 +00:00
$state -> event = QUESTION_EVENTDUPLICATE ;
2006-02-24 10:21:40 +00:00
break ;
}
}
}
2006-03-19 18:28:29 +00:00
return ( QUESTION_EVENTDUPLICATE == $state -> 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 ) {
2006-03-19 18:28:29 +00:00
// deal with penalty
2006-02-24 10:21:40 +00:00
if ( $cmoptions -> penaltyscheme ) {
$state -> grade = $state -> raw_grade - $state -> sumpenalty ;
$state -> sumpenalty += ( float ) $state -> penalty ;
} else {
$state -> grade = $state -> raw_grade ;
}
// deal with timelimit
if ( $cmoptions -> timelimit ) {
// We allow for 5% uncertainty in the following test
if (( $state -> timestamp - $attempt -> timestart ) > ( $cmoptions -> timelimit * 63 )) {
$state -> grade = 0 ;
}
}
// 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
*
* @ param object $question The question object for which the icon is required
* @ param boolean $editlink If true then the icon is a link to the question
* edit page .
* @ param boolean $return If true the functions returns the link as a string
*/
2006-02-28 09:26:00 +00:00
function print_question_icon ( $question , $editlink = true , $return = false ) {
2006-02-24 10:21:40 +00:00
// returns a question icon
2006-03-01 07:03:57 +00:00
global $QTYPES , $CFG ;
2006-02-24 10:21:40 +00:00
2006-03-24 20:02:42 +00:00
$namestr = get_string ( $question -> qtype , 'quiz' );
2006-03-24 19:31:46 +00:00
$html = '<img border="0" height="16" width="16" src="' . $CFG -> wwwroot . '/question/type/' .
2006-03-24 21:21:59 +00:00
$question -> qtype . '/icon.gif" alt="' .
2006-03-24 20:02:42 +00:00
$namestr . '" title="' . $namestr . '" />' ;
2006-02-24 10:21:40 +00:00
if ( $editlink ) {
2006-02-24 10:25:16 +00:00
$html = " <a href= \" $CFG->wwwroot /question/question.php?id= $question->id\ " title = \ " "
2006-03-24 21:21:59 +00:00
. $question -> qtype . " \" > " .
2006-02-24 10:21:40 +00:00
$html . " </a> \n " ;
}
if ( $return ) {
return $html ;
} else {
echo $html ;
}
}
/**
* Returns a html link to the question image if there is one
*
* @ return string The html image tag or the empy string if there is no image .
* @ param object $question The question object
*/
2006-02-28 09:26:00 +00:00
function get_question_image ( $question , $courseid ) {
2006-02-24 10:21:40 +00:00
global $CFG ;
$img = '' ;
if ( $question -> image ) {
if ( substr ( strtolower ( $question -> image ), 0 , 7 ) == 'http://' ) {
$img .= $question -> image ;
} else if ( $CFG -> slasharguments ) { // Use this method if possible for better caching
$img .= " $CFG->wwwroot /file.php/ $courseid / $question->image " ;
} else {
$img .= " $CFG->wwwroot /file.php?file= $courseid / $question->image " ;
}
}
return $img ;
}
/**
* 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 . '_' ;
}
/**
* 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
2006-02-28 09:26:00 +00:00
* constructed with { @ link question_make_name_prefix ()}
2006-02-24 10:21:40 +00:00
*/
2006-02-28 09:26:00 +00:00
function question_get_id_from_name_prefix ( $name ) {
2006-02-24 10:21:40 +00:00
if ( ! preg_match ( '/^resp([0-9]+)_/' , $name , $matches ))
return false ;
return ( integer ) $matches [ 1 ];
}
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 .
2006-02-28 09:26:00 +00:00
*/
function question_new_attempt_uniqueid () {
2006-02-24 10:21:40 +00:00
global $CFG ;
set_config ( 'attemptuniqueid' , $CFG -> attemptuniqueid + 1 );
return $CFG -> attemptuniqueid ;
}
/// FUNCTIONS THAT SIMPLY WRAP QUESTIONTYPE METHODS //////////////////////////////////
2006-03-22 17:22:36 +00:00
/**
* Prints a question
*
* Simply calls the question type specific print_question () method .
* @ 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 ;
2006-02-24 10:21:40 +00:00
2006-02-24 13:48:43 +00:00
$QTYPES [ $question -> qtype ] -> print_question ( $question , $state , $number ,
2006-02-24 10:21:40 +00:00
$cmoptions , $options );
}
2006-03-01 07:36:09 +00:00
/**
* Saves question options
*
* Simply calls the question type specific save_question_options () method .
*/
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 .
*/
// 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 .
*/
// 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
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 ;
}
/// CATEGORY FUNCTIONS /////////////////////////////////////////////////////////////////
/**
* Gets the default category in a course
*
* It returns the first category with no parent category . If no categories
* exist yet then one is created .
* @ return object The default category
* @ param integer $courseid The id of the course whose default category is wanted
*/
2006-02-28 09:26:00 +00:00
function get_default_question_category ( $courseid ) {
2006-02-24 10:21:40 +00:00
/// Returns the current category
2006-03-01 07:03:57 +00:00
if ( $categories = get_records_select ( " question_categories " , " course = ' $courseid ' AND parent = '0' " , " id " )) {
2006-02-24 10:21:40 +00:00
foreach ( $categories as $category ) {
return $category ; // Return the first one (lowest id)
}
}
// Otherwise, we need to make one
$category -> name = get_string ( " default " , " quiz " );
$category -> info = get_string ( " defaultinfo " , " quiz " );
$category -> course = $courseid ;
$category -> parent = 0 ;
2006-02-28 09:26:00 +00:00
// TODO: Figure out why we use 999 below
$category -> sortorder = 999 ;
2006-02-24 10:21:40 +00:00
$category -> publish = 0 ;
$category -> stamp = make_unique_id_code ();
2006-03-01 07:03:57 +00:00
if ( ! $category -> id = insert_record ( " question_categories " , $category )) {
2006-02-24 10:21:40 +00:00
notify ( " Error creating a default category! " );
return false ;
}
return $category ;
}
2006-03-01 07:03:57 +00:00
function question_category_menu ( $courseid , $published = false ) {
2006-02-24 10:21:40 +00:00
/// Returns the list of categories
$publish = " " ;
if ( $published ) {
$publish = " OR publish = '1' " ;
}
if ( ! isadmin ()) {
2006-03-01 07:03:57 +00:00
$categories = get_records_select ( " question_categories " , " course = ' $courseid ' $publish " , 'parent, sortorder, name ASC' );
2006-02-24 10:21:40 +00:00
} else {
2006-03-01 07:03:57 +00:00
$categories = get_records_select ( " question_categories " , '' , 'parent, sortorder, name ASC' );
2006-02-24 10:21:40 +00:00
}
if ( ! $categories ) {
return false ;
}
$categories = add_indented_names ( $categories );
foreach ( $categories as $category ) {
if ( $catcourse = get_record ( " course " , " id " , $category -> course )) {
if ( $category -> publish && ( $category -> course != $courseid )) {
$category -> indentedname .= " ( $catcourse->shortname ) " ;
}
$catmenu [ $category -> id ] = $category -> indentedname ;
}
}
return $catmenu ;
}
function sort_categories_by_tree ( & $categories , $id = 0 , $level = 1 ) {
// 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.
$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)
2006-03-01 07:03:57 +00:00
if ( ! isset ( $categories [ $key ] -> processed ) && ! record_exists ( 'question_categories' , 'course' , $categories [ $key ] -> course , 'id' , $categories [ $key ] -> parent )) {
2006-02-24 10:21:40 +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-03-02 14:13:42 +00:00
function flatten_category_tree ( $cats , $depth = 0 ) {
// flattens tree structure created by add_indented_named
// (adding the names)
$newcats = array ();
2006-02-24 10:21:40 +00:00
$fillstr = ' ' ;
2006-03-02 14:13:42 +00:00
foreach ( $cats as $key => $cat ) {
$newcats [ $key ] = $cat ;
$newcats [ $key ] -> indentedname = str_repeat ( $fillstr , $depth ) . $cat -> name ;
// recurse if the category has children
if ( ! empty ( $cat -> children )) {
$newcats += flatten_category_tree ( $cat -> children , $depth + 1 );
2006-02-24 10:21:40 +00:00
}
}
2006-03-02 14:13:42 +00:00
return $newcats ;
2006-02-24 10:21:40 +00:00
}
2006-03-02 14:13:42 +00:00
function add_indented_names ( $categories ) {
// iterate through categories adding new fields
// and creating references
foreach ( $categories as $key => $category ) {
$categories [ $key ] -> children = array ();
$categories [ $key ] -> link = & $categories [ $key ];
}
// create tree structure of children
// link field is used to track 'new' place of category in tree
foreach ( $categories as $key => $category ) {
if ( ! empty ( $category -> parent )) {
$categories [ $category -> parent ] -> link -> children [ $key ] = $categories [ $key ];
$categories [ $key ] -> link = & $categories [ $category -> parent ] -> link -> children [ $key ];
}
}
// remove top level categories with parents
$newcats = array ();
foreach ( $categories as $key => $category ) {
unset ( $category -> link );
if ( empty ( $category -> parent )) {
$newcats [ $key ] = $category ;
}
}
// walk the tree to flatten revised structure
$categories = flatten_category_tree ( $newcats );
return $categories ;
}
2006-02-24 10:21:40 +00:00
/**
* Displays a select menu of categories with appended course names
*
* Optionaly non editable categories may be excluded .
* @ author Howard Miller June ' 04
*/
2006-03-01 07:03:57 +00:00
function question_category_select_menu ( $courseid , $published = false , $only_editable = false , $selected = " " ) {
2006-02-24 10:21:40 +00:00
// get sql fragment for published
$publishsql = " " ;
if ( $published ) {
$publishsql = " or publish=1 " ;
}
2006-03-01 07:03:57 +00:00
$categories = get_records_select ( " question_categories " , " course= $courseid $publishsql " , 'parent, sortorder, name ASC' );
2006-02-24 10:21:40 +00:00
$categories = add_indented_names ( $categories );
echo " <select name= \" category \" > \n " ;
foreach ( $categories as $category ) {
$cid = $category -> id ;
2006-03-01 07:03:57 +00:00
$cname = question_category_coursename ( $category , $courseid );
2006-02-24 10:21:40 +00:00
$seltxt = " " ;
if ( $cid == $selected ) {
$seltxt = " selected= \" selected \" " ;
}
if (( ! $only_editable ) || isteacheredit ( $category -> course )) {
echo " <option value= \" $cid\ " $seltxt > $cname </ option > \n " ;
}
}
echo " </select> \n " ;
}
2006-03-01 07:03:57 +00:00
function question_category_coursename ( $category , $courseid = 0 ) {
2006-02-24 10:21:40 +00:00
/// if the category is not from this course and is published , adds on the course
/// name
$cname = ( isset ( $category -> indentedname )) ? $category -> indentedname : $category -> name ;
if ( $category -> course != $courseid && $category -> publish ) {
if ( $catcourse = get_record ( " course " , " id " , $category -> course )) {
$cname .= " ( $catcourse->shortname ) " ;
}
}
return $cname ;
}
/**
* Returns a comma separated list of ids of the category and all subcategories
*/
2006-03-01 07:03:57 +00:00
function question_categorylist ( $categoryid ) {
2006-02-24 10:21:40 +00:00
// returns a comma separated list of ids of the category and all subcategories
$categorylist = $categoryid ;
2006-03-01 07:03:57 +00:00
if ( $subcategories = get_records ( 'question_categories' , 'parent' , $categoryid , 'sortorder ASC' , 'id, id' )) {
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 ;
}
/**
* Function to read all questions for category into big array
*
* @ param int $category category number
* @ param bool @ noparent if true only questions with NO parent will be selected
* @ author added by Howard Miller June 2004
*/
function get_questions_category ( $category , $noparent = false ) {
2006-02-24 13:48:43 +00:00
global $QTYPES ;
2006-02-24 10:21:40 +00:00
// questions will be added to an array
$qresults = array ();
// build sql bit for $noparent
$npsql = '' ;
if ( $noparent ) {
$npsql = " and parent='0' " ;
}
// get the list of questions for the category
2006-03-01 07:03:57 +00:00
if ( $questions = get_records_select ( " question " , " category= { $category -> id } $npsql " , " qtype, name ASC " )) {
2006-02-24 10:21:40 +00:00
// iterate through questions, getting stuff we need
foreach ( $questions as $question ) {
2006-02-24 13:48:43 +00:00
$questiontype = $QTYPES [ $question -> qtype ];
2006-02-24 10:21:40 +00:00
$questiontype -> get_question_options ( $question );
$qresults [] = $question ;
}
}
return $qresults ;
}
2006-02-24 15:44:53 +00:00
/**
* Get list of available import or export formats
* @ param string $type 'import' if import list , otherwise export list assumed
* @ return array sorted list of import / export formats available
**/
function get_import_export_formats ( $type ) {
global $CFG ;
2006-03-01 07:03:57 +00:00
$fileformats = get_list_of_plugins ( " question/format " );
2006-02-24 15:44:53 +00:00
$fileformatname = array ();
require_once ( " format.php " );
foreach ( $fileformats as $key => $fileformat ) {
$format_file = $CFG -> dirroot . " /question/format/ $fileformat /format.php " ;
if ( file_exists ( $format_file ) ) {
require_once ( $format_file );
}
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 ]] " ) {
$formatname = $fileformat ; // Just use the raw folder name
}
$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 ( " " , " : " , " / " , " \\ " , " | " );
$export_word = str_replace ( $takeoff , " _ " , strtolower ( get_string ( " exportfilename " , " quiz " )));
//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
$export_name .= strtolower ( $export_shortname ) . " - " ;
//The category name
$export_name .= strtolower ( $export_categoryname ) . " - " ;
//The date format
$export_name .= userdate ( time (), $export_date_format , 99 , false );
//The extension - no extension, supplied by format
// $export_name .= ".txt";
return $export_name ;
}
2006-02-24 10:21:40 +00:00
?>