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-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 ( " 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-08-11 14:59:18 +00:00
/**
* The options used when popping up a question preview window in Javascript .
*/
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 );
/**#@-*/
2006-02-24 13:48:43 +00:00
/// QTYPES INITIATION //////////////////
2006-08-15 21:25:38 +00:00
// These variables get initialised via calls to question_register_questiontype
// as the question type classes are included.
global $QTYPES , $QTYPE_MENU , $QTYPE_MANUAL , $QTYPE_EXCLUDE_FROM_RANDOM ;
2006-02-24 10:21:40 +00:00
/**
2006-03-20 20:45:55 +00:00
* Array holding question type objects
*/
2006-08-15 21:25:38 +00:00
$QTYPES = array ();
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-08-15 21:25:38 +00:00
$QTYPE_MENU = array ();
/**
* String in the format " 'type1','type2' " that can be used in SQL clauses like
* " WHERE q.type IN ( $QTYPE_MANUAL ) " .
*/
$QTYPE_MANUAL = '' ;
/**
* String in the format " 'type1','type2' " that can be used in SQL clauses like
* " WHERE q.type NOT IN ( $QTYPE_EXCLUDE_FROM_RANDOM ) " .
*/
$QTYPE_EXCLUDE_FROM_RANDOM = '' ;
/**
* Add a new question type to the various global arrays above .
*
* @ param object $qtype An instance of the new question type class .
*/
function question_register_questiontype ( $qtype ) {
global $QTYPES , $QTYPE_MENU , $QTYPE_MANUAL , $QTYPE_EXCLUDE_FROM_RANDOM ;
$name = $qtype -> name ();
$QTYPES [ $name ] = $qtype ;
$menuname = $qtype -> menu_name ();
if ( $menuname ) {
$QTYPE_MENU [ $name ] = $menuname ;
}
if ( $qtype -> is_manual_graded ()) {
if ( $QTYPE_MANUAL ) {
$QTYPE_MANUAL .= ',' ;
}
$QTYPE_MANUAL .= " ' $name ' " ;
}
if ( ! $qtype -> is_usable_by_random ()) {
if ( $QTYPE_EXCLUDE_FROM_RANDOM ) {
$QTYPE_EXCLUDE_FROM_RANDOM .= ',' ;
}
$QTYPE_EXCLUDE_FROM_RANDOM .= " ' $name ' " ;
}
}
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
// These files in turn call question_register_questiontype()
// with a new instance of each qtype class.
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 .
*/
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
*
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 ) {
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 ;
}
}
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
*
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 ) {
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 );
2006-05-13 16:57:17 +00:00
delete_records ( " question_attempts " , " id " , $attemptid );
2006-03-22 18:27:28 +00:00
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-08-16 16:17:18 +00:00
if ( $child -> id != $questionid ) {
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 ) {
2006-07-18 15:34:24 +00:00
$concat = new stdClass ;
2006-03-21 09:06:34 +00:00
$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 ) {
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 ;
}
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 () .
*
* @ param object $question the question to tidy .
* @ return boolean true if successful , else false .
*/
function _tidy_question ( & $question ) {
2006-02-24 13:48:43 +00:00
global $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 );
return $QTYPES [ $question -> qtype ] -> get_question_options ( $question );
}
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 .
*
* @ param mixed $questions Either an array of question objects to be updated
* or just a single question object
* @ return bool Indicates success or failure .
*/
function get_question_options ( & $questions ) {
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 ) {
if ( ! _tidy_question ( $questions [ $i ])) {
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
2006-07-18 15:34:24 +00:00
return _tidy_question ( $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
2006-04-06 16:42:00 +00:00
$statefields = 'n.questionid as question, s.*, n.sumpenalty, n.comment' ;
2006-02-24 10:21:40 +00:00
// 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 ]);
}
} 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 ;
2006-04-08 14:20:52 +00:00
$states [ $i ] -> comment = '' ;
2006-03-26 07:59:43 +00:00
$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
2006-08-04 16:53:43 +00:00
if ( ! $QTYPES [ $questions [ $i ] -> qtype ] -> create_session_and_responses (
$questions [ $i ], $states [ $i ], $cmoptions , $attempt )) {
2006-03-26 07:59:43 +00:00
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 ()}
* @ 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
2006-08-18 22:26:04 +00:00
$state -> responses = array ( '' => addslashes ( $state -> answer ));
2006-02-24 10:21:40 +00:00
unset ( $state -> answer );
2006-04-13 00:37:14 +00:00
$state -> comment = isset ( $state -> comment ) ? addslashes ( $state -> comment ) : '' ;
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 .
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
2006-05-13 16:57:17 +00:00
if ( ! empty ( $state -> update )) { // this forces 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 ;
}
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
2006-04-30 16:15:04 +00:00
if ( ! $session = get_record ( 'question_sessions' , 'attemptid' ,
2006-08-18 22:26:04 +00:00
$state -> attempt , 'questionid' , $question -> id )) {
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 ;
$session -> comment = $state -> comment ;
if ( ! insert_record ( 'question_sessions' , $session )) {
2006-04-07 16:00:29 +00:00
error ( 'Could not insert entry in question_sessions' );
2006-02-24 10:21:40 +00:00
}
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 ;
$session -> comment = $state -> comment ;
2006-08-18 22:34:54 +00:00
} else {
$session -> comment = addslashes ( $session -> comment );
2006-02-24 10:21:40 +00:00
}
2006-04-30 16:15:04 +00:00
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
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 ) {
2006-04-07 16:00:29 +00:00
return ( $state -> event == QUESTION_EVENTCLOSE
or $state -> event == QUESTION_EVENTCLOSEANDGRADE
or $state -> event == QUESTION_EVENTMANUALGRADE );
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
}
}
2006-08-23 21:17:23 +00:00
ksort ( $actions [ $quid ] -> responses );
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-08-18 22:26:04 +00:00
" attempt = ' { $attempt -> uniqueid } ' AND question = ' { $question -> id } ' " ,
'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
// sumgrades for the attempt without this question.
$attempt -> sumgrades -= $states [ count ( $states ) - 1 ] -> grade ;
// Initialise the replaystate
$state = clone ( $states [ 0 ]);
2006-06-28 11:45:11 +00:00
$state -> comment = get_field ( 'question_sessions' , 'comment' , 'attemptid' ,
$attempt -> uniqueid , 'questionid' , $question -> id );
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-08-18 22:26:04 +00:00
or QUESTION_EVENTGRADE == $states [ $j ] -> event
or QUESTION_EVENTCLOSEANDGRADE == $states [ $j ] -> event ) {
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 ) {
2006-08-18 22:26:04 +00:00
question_process_comment ( $question , $replaystate , $attempt ,
$replaystate -> comment , $states [ $j ] -> grade );
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 ,
$action , $cmoptions , $attempt )) {
2006-05-13 16:57:17 +00:00
$verbose && notify ( " Couldn't regrade state # { $state -> id } ! " );
}
2006-02-24 10:21:40 +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
2006-04-05 05:53:18 +00:00
if (( round (( float ) $replaystate -> raw_grade , 5 ) != round (( float ) $states [ $j ] -> raw_grade , 5 ))
2006-08-18 22:26:04 +00:00
or ( round (( float ) $replaystate -> penalty , 5 ) != round (( float ) $states [ $j ] -> penalty , 5 ))
or ( round (( float ) $replaystate -> grade , 5 ) != round (( float ) $states [ $j ] -> grade , 5 ))) {
2006-04-05 05:53:18 +00:00
$changed = true ;
2006-02-24 10:21:40 +00:00
}
$replaystate -> id = $states [ $j ] -> id ;
2006-05-13 16:57:17 +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
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 ) {
2006-08-18 08:14:47 +00:00
// TODO, call a method in quiz to do this, where 'quiz' comes from
// the question_attempts table.
2006-04-05 05:53:18 +00:00
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!
2006-04-06 19:59:02 +00:00
unset ( $action -> responses [ 'submit' ], $action -> responses [ 'validate' ]);
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 )) {
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 ;
}
2006-08-04 16:53:43 +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
2006-08-04 16:53:43 +00:00
$sameresponses = ! $state -> last_graded -> event == QUESTION_EVENTOPEN &&
2006-08-23 21:17:23 +00:00
$QTYPES [ $question -> qtype ] -> compare_responses ( $question , $action , $state );
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 ;
$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-04-30 11:45:14 +00:00
} else { // grading event
2006-02-24 10:21:40 +00:00
2006-04-30 11:45:14 +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 ) {
2006-03-19 18:28:29 +00:00
// 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 );
2006-02-24 10:21:40 +00:00
2006-04-09 21:35:44 +00:00
$attempt -> sumgrades += ( float ) $state -> grade ;
2006-02-24 10:21:40 +00:00
}
2006-04-30 11:45:14 +00:00
// If the state was graded we need to update the last_graded field.
if ( question_state_is_graded ( $state )) {
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 ) {
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 {
2006-06-23 15:06:23 +00:00
$img .= " $CFG->wwwroot /file.php?file=/ $courseid / $question->image " ;
2006-02-24 10:21:40 +00:00
}
}
return $img ;
}
2006-04-07 16:00:29 +00:00
function question_print_comment_box ( $question , $state , $attempt , $url ) {
2006-04-08 08:22:18 +00:00
global $CFG ;
2006-04-09 22:48:58 +00:00
$prefix = 'response' ;
2006-04-08 08:22:18 +00:00
$usehtmleditor = can_use_richtext_editor ();
$grade = round ( $state -> last_graded -> grade , 3 );
echo '<form method="post" action="' . $url . '">' ;
include ( $CFG -> dirroot . '/question/comment.html' );
echo '<input type="hidden" name="attempt" value="' . $attempt -> uniqueid . '" />' ;
echo '<input type="hidden" name="question" value="' . $question -> id . '" />' ;
echo '<input type="hidden" name="sesskey" value="' . sesskey () . '" />' ;
echo '<input type="submit" name="submit" value="' . get_string ( 'save' , 'quiz' ) . '" />' ;
echo '</form>' ;
if ( $usehtmleditor ) {
2006-04-09 22:48:58 +00:00
use_html_editor ();
2006-04-08 08:22:18 +00:00
}
2006-04-07 16:00:29 +00:00
}
function question_process_comment ( $question , & $state , & $attempt , $comment , $grade ) {
// Update the comment and save it in the database
$state -> comment = $comment ;
if ( ! set_field ( 'question_sessions' , 'comment' , $comment , 'attemptid' , $attempt -> uniqueid , 'questionid' , $question -> id )) {
error ( " Cannot save comment " );
}
// If the teacher has changed the grade then update the attempt and the state
// The modified attempt is stored to the database, the state not yet but the
// $state->changed flag is set
if ( abs ( $state -> last_graded -> grade - $grade ) > 0.002 ) {
// the teacher has changed the grade
$attempt -> sumgrades = $attempt -> sumgrades - $state -> last_graded -> grade + $grade ;
$attempt -> timemodified = time ();
if ( ! update_record ( 'quiz_attempts' , $attempt )) {
error ( 'Failed to save the current quiz attempt!' );
}
$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-04-07 16:00:29 +00:00
// We need to indicate that the state has changed in order for it to be saved
$state -> changed = 1 ;
// We want to update existing state (rather than creating new one) if it
// was itself created by a manual grading event
$state -> update = ( $state -> event == QUESTION_EVENTMANUALGRADE ) ? 1 : 0 ;
$state -> event = QUESTION_EVENTMANUALGRADE ;
// Update the last graded state (don't simplify!)
unset ( $state -> last_graded );
$state -> last_graded = clone ( $state );
unset ( $state -> last_graded -> changed );
}
}
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 . '_' ;
}
/**
* 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
*/
2006-05-13 16:57:17 +00:00
function question_new_attempt_uniqueid ( $modulename = 'quiz' ) {
2006-02-24 10:21:40 +00:00
global $CFG ;
2006-07-18 15:34:24 +00:00
$attempt = new stdClass ;
2006-05-13 16:57:17 +00:00
$attempt -> modulename = $modulename ;
if ( ! $id = insert_record ( 'question_attempts' , $attempt )) {
error ( 'Could not create new entry in question_attempts table' );
}
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 ();
}
2006-02-24 10:21:40 +00:00
/// 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 /////////////////////////////////////////////////////////////////
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 .
*/
function sort_categories_by_tree ( & $categories , $id = 0 , $level = 1 ) {
$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)
if ( ! isset ( $categories [ $key ] -> processed ) && ! record_exists ( 'question_categories' , 'course' , $categories [ $key ] -> course , 'id' , $categories [ $key ] -> parent )) {
$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 () .
*
* Recursively adds an indentedname field to each category , starting with the category
* with id $id , and dealing with that category and all its children , and
* return a new array , with those categories in the right order .
*
* @ param array $categories an array of categories which has had childids
* 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
*/
2006-07-14 15:36:29 +00:00
function flatten_category_tree ( & $categories , $id , $depth = 0 ) {
// Indent the name of this category.
$newcategories = array ();
$newcategories [ $id ] = $categories [ $id ];
$newcategories [ $id ] -> indentedname = str_repeat ( ' ' , $depth ) . $categories [ $id ] -> name ;
// Recursively indent the children.
foreach ( $categories [ $id ] -> childids as $childid ) {
$newcategories = $newcategories + flatten_category_tree ( $categories , $childid , $depth + 1 );
2006-05-08 10:39:14 +00:00
}
2006-07-14 15:36:29 +00:00
// Remove the childids array that were temporarily added.
unset ( $newcategories [ $id ] -> childids );
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 .
*
* @ 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
*/
2006-07-14 15:36:29 +00:00
function add_indented_names ( $categories ) {
2006-05-08 10:39:14 +00:00
2006-07-14 15:36:29 +00:00
// Add an array to each category to hold the child category ids. This array will be removed
// 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.
// We have to be careful, because the categories array may include published
// 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 ) {
$newcategories = $newcategories + flatten_category_tree ( $categories , $id );
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 .
*
* Categories from this course and ( optionally ) published categories from other courses
* are included . Optionally , only categories the current user may edit can be included .
*
* @ 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 .
*/
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 ;
}
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
* @ 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 ();
2006-05-02 09:04:38 +00:00
require_once ( " { $CFG -> dirroot } /question/format.php " );
2006-02-24 15:44:53 +00:00
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-08-11 14:59:18 +00:00
?>