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 .
*
* @ 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
2008-05-26 11:39:51 +00:00
define ( 'QUESTION_EVENTS_GRADED' , QUESTION_EVENTGRADE . ',' .
QUESTION_EVENTCLOSEANDGRADE . ',' .
QUESTION_EVENTMANUALGRADE );
2006-02-24 10:25:16 +00:00
/**#@-*/
/** #@+
2006-08-15 21:25:38 +00:00
* The core question types .
2006-03-20 20:45:55 +00:00
*/
2006-03-21 23:08:36 +00:00
define ( " SHORTANSWER " , " shortanswer " );
define ( " TRUEFALSE " , " truefalse " );
define ( " MULTICHOICE " , " multichoice " );
define ( " RANDOM " , " random " );
define ( " MATCH " , " match " );
define ( " RANDOMSAMATCH " , " randomsamatch " );
define ( " DESCRIPTION " , " description " );
define ( " NUMERICAL " , " numerical " );
define ( " MULTIANSWER " , " multianswer " );
define ( " CALCULATED " , " calculated " );
define ( " ESSAY " , " essay " );
2006-02-24 10:25:16 +00:00
/**#@-*/
2006-03-20 20:45:55 +00:00
/**
* Constant determines the number of answer boxes supplied in the editing
* form for multiple choice and similar question types .
*/
2006-02-28 09:26:00 +00:00
define ( " QUESTION_NUMANS " , " 10 " );
2006-02-24 10:25:16 +00:00
2007-01-07 12:46:47 +00:00
/**
* Constant determines the number of answer boxes supplied in the editing
* form for multiple choice and similar question types to start with , with
* the option of adding QUESTION_NUMANS_ADD more answers .
*/
define ( " QUESTION_NUMANS_START " , 3 );
/**
* Constant determines the number of answer boxes to add in the editing
* form for multiple choice and similar question types when the user presses
* 'add form fields button' .
*/
define ( " QUESTION_NUMANS_ADD " , 3 );
2006-08-11 14:59:18 +00:00
/**
* The options used when popping up a question preview window in Javascript .
*/
2007-04-24 16:59:32 +00:00
define ( 'QUESTION_PREVIEW_POPUP_OPTIONS' , 'scrollbars=yes,resizable=yes,width=700,height=540' );
2006-02-24 10:21:40 +00:00
2006-03-20 20:45:55 +00:00
/** #@+
* Option flags for -> optionflags
* The options are read out via bitwise operation using these constants
*/
/**
* Whether the questions is to be run in adaptive mode . If this is not set then
* a question closes immediately after the first submission of responses . This
* is how question is Moodle always worked before version 1.5
*/
define ( 'QUESTION_ADAPTIVE' , 1 );
2007-08-09 21:50:59 +00:00
/**
* options used in forms that move files .
*
*/
define ( 'QUESTION_FILENOTHINGSELECTED' , 0 );
define ( 'QUESTION_FILEDONOTHING' , 1 );
define ( 'QUESTION_FILECOPY' , 2 );
define ( 'QUESTION_FILEMOVE' , 3 );
define ( 'QUESTION_FILEMOVELINKSONLY' , 4 );
2006-03-20 20:45:55 +00:00
/**#@-*/
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.
2008-02-28 12:53:02 +00:00
global $QTYPES , $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 ();
/**
* String in the format " 'type1','type2' " that can be used in SQL clauses like
* " WHERE q.type IN ( $QTYPE_MANUAL ) " .
*/
2007-01-07 12:46:47 +00:00
$QTYPE_MANUAL = '' ;
2006-08-15 21:25:38 +00:00
/**
* 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 .
2007-01-07 12:46:47 +00:00
*
2006-08-15 21:25:38 +00:00
* @ param object $qtype An instance of the new question type class .
*/
function question_register_questiontype ( $qtype ) {
2008-02-28 12:53:02 +00:00
global $QTYPES , $QTYPE_MANUAL , $QTYPE_EXCLUDE_FROM_RANDOM ;
2007-01-07 12:46:47 +00:00
2006-08-15 21:25:38 +00:00
$name = $qtype -> name ();
$QTYPES [ $name ] = $qtype ;
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
2007-01-07 12:46:47 +00:00
// These files in turn call question_register_questiontype()
2006-08-15 21:25:38 +00:00
// with a new instance of each qtype class.
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
}
}
2008-02-28 12:53:02 +00:00
/**
* An array of question type names translated to the user ' s language , suitable for use when
* creating a drop - down menu of options .
*
* Long - time Moodle programmers will realise that this replaces the old $QTYPE_MENU array .
* The array returned will only hold the names of all the question types that the user should
* be able to create directly . Some internal question types like random questions are excluded .
2008-06-12 09:15:16 +00:00
*
2008-02-28 12:53:02 +00:00
* @ return array an array of question type names translated to the user ' s language .
*/
function question_type_menu () {
global $QTYPES ;
static $menu_options = null ;
if ( is_null ( $menu_options )) {
$menu_options = array ();
foreach ( $QTYPES as $name => $qtype ) {
$menuname = $qtype -> menu_name ();
if ( $menuname ) {
$menu_options [ $name ] = $menuname ;
}
}
}
return $menu_options ;
}
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 ) {
2008-06-09 12:16:54 +00:00
global $CFG , $DB ;
2006-03-20 23:04:22 +00:00
$instances = array ();
2008-06-09 12:16:54 +00:00
$modules = $DB -> get_records ( 'modules' );
2006-03-20 23:04:22 +00:00
foreach ( $modules as $module ) {
2008-05-12 17:29:56 +00:00
$fullmod = $CFG -> dirroot . '/mod/' . $module -> name ;
if ( file_exists ( $fullmod . '/lib.php' )) {
include_once ( $fullmod . '/lib.php' );
$fn = $module -> name . '_question_list_instances' ;
if ( function_exists ( $fn )) {
$instances = $instances + $fn ( $questionid );
}
2006-03-20 23:04:22 +00:00
}
}
return $instances ;
}
2006-02-24 10:21:40 +00:00
2008-05-13 21:52:38 +00:00
/**
* Determine whether there arey any questions belonging to this context , that is whether any of its
* question categories contain any questions . This will return true even if all the questions are
* hidden .
*
* @ param mixed $context either a context object , or a context id .
* @ return boolean whether any of the question categories beloning to this context have
* any questions in them .
*/
function question_context_has_any_questions ( $context ) {
2008-06-09 12:16:54 +00:00
global $DB ;
2008-05-13 21:52:38 +00:00
if ( is_object ( $context )) {
$contextid = $context -> id ;
} else if ( is_numeric ( $context )) {
$contextid = $context ;
} else {
print_error ( 'invalidcontextinhasanyquestions' , 'question' );
}
2008-06-09 12:16:54 +00:00
return $DB -> record_exists_sql ( " SELECT *
FROM { question } q
JOIN { question_categories } qc ON qc . id = q . category
WHERE qc . contextid = ? AND q . parent = 0 " , array( $contextid ));
2008-05-13 21:52:38 +00:00
}
2007-01-07 12:46:47 +00:00
/**
2006-03-22 14:43:55 +00:00
* Returns list of 'allowed' grades for grade selection
* formatted suitably for dropdown box function
* @ return object -> gradeoptionsfull full array -> gradeoptions + ve only
*/
function get_grade_options () {
// 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 ;
2007-01-07 12:46:47 +00:00
}
2006-03-22 16:27:46 +00:00
}
2006-03-21 09:06:34 +00:00
/**
* Tests whether a category is in use by any activity module
*
* @ return boolean
2007-01-07 12:46:47 +00:00
* @ param integer $categoryid
2006-03-21 09:06:34 +00:00
* @ param boolean $recursive Whether to examine category children recursively
*/
function question_category_isused ( $categoryid , $recursive = false ) {
2008-06-09 12:16:54 +00:00
global $DB ;
2006-03-21 09:06:34 +00:00
//Look at each question in the category
2008-06-09 12:16:54 +00:00
if ( $questions = $DB -> get_records ( 'question' , array ( 'category' => $categoryid ))) {
2006-03-21 09:06:34 +00:00
foreach ( $questions as $question ) {
if ( count ( question_list_instances ( $question -> id ))) {
return true ;
}
}
}
//Look under child categories recursively
if ( $recursive ) {
2008-06-09 12:16:54 +00:00
if ( $children = $DB -> get_records ( 'question_categories' , array ( 'parent' => $categoryid ))) {
2006-03-21 09:06:34 +00:00
foreach ( $children as $child ) {
if ( question_category_isused ( $child -> id , $recursive )) {
return true ;
}
}
}
}
return false ;
}
2006-03-22 18:27:28 +00:00
/**
* Deletes all data associated to an attempt from the database
*
2006-08-10 21:33:53 +00:00
* @ param integer $attemptid The id of the attempt being deleted
2006-03-22 18:27:28 +00:00
*/
function delete_attempt ( $attemptid ) {
2008-06-09 12:16:54 +00:00
global $QTYPES , $DB ;
2006-03-22 18:27:28 +00:00
2008-06-09 12:16:54 +00:00
$states = $DB -> get_records ( 'question_states' , array ( 'attempt' => $attemptid ));
2007-03-30 10:17:16 +00:00
if ( $states ) {
$stateslist = implode ( ',' , array_keys ( $states ));
2007-08-09 21:50:59 +00:00
2007-03-30 10:17:16 +00:00
// delete question-type specific data
foreach ( $QTYPES as $qtype ) {
$qtype -> delete_states ( $stateslist );
}
2006-03-22 18:27:28 +00:00
}
// delete entries from all other question tables
// It is important that this is done only after calling the questiontype functions
2008-06-09 12:16:54 +00:00
$DB -> delete_records ( " question_states " , array ( " attempt " => $attemptid ));
$DB -> delete_records ( " question_sessions " , array ( " attemptid " => $attemptid ));
$DB -> delete_records ( " question_attempts " , array ( " id " => $attemptid ));
2006-03-22 18:27:28 +00:00
}
2006-03-21 09:06:34 +00:00
2006-02-24 10:21:40 +00:00
/**
2006-03-20 20:45:55 +00:00
* Deletes question and all associated data from the database
*
2006-03-20 23:04:22 +00:00
* It will not delete a question if it is used by an activity module
2006-03-20 20:45:55 +00:00
* @ param object $question The question being deleted
*/
2006-03-20 23:04:22 +00:00
function delete_question ( $questionid ) {
2008-06-09 12:16:54 +00:00
global $QTYPES , $DB ;
2007-01-07 12:46:47 +00:00
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
2008-06-09 12:16:54 +00:00
$question = $DB -> get_record ( 'question' , array ( 'id' => $questionid ));
2007-08-09 21:50:59 +00:00
question_require_capability_on ( $question , 'edit' );
if ( $question ) {
2006-03-21 15:33:30 +00:00
if ( isset ( $QTYPES [ $question -> qtype ])) {
$QTYPES [ $question -> qtype ] -> delete_question ( $questionid );
}
} else {
echo " Question with id $questionid does not exist.<br /> " ;
2006-02-24 10:21:40 +00:00
}
2006-03-20 23:04:22 +00:00
2008-06-09 12:16:54 +00:00
if ( $states = $DB -> get_records ( 'question_states' , array ( 'question' => $questionid ))) {
2006-03-25 21:07:11 +00:00
$stateslist = implode ( ',' , array_keys ( $states ));
2007-01-07 12:46:47 +00:00
2006-03-25 21:07:11 +00:00
// delete questiontype-specific data
foreach ( $QTYPES as $qtype ) {
$qtype -> delete_states ( $stateslist );
}
2006-03-22 18:27:28 +00:00
}
2006-03-20 23:04:22 +00:00
// delete entries from all other question tables
// It is important that this is done only after calling the questiontype functions
2008-06-09 12:16:54 +00:00
$DB -> delete_records ( " question_answers " , array ( " question " => $questionid ));
$DB -> delete_records ( " question_states " , array ( " question " => $questionid ));
$DB -> delete_records ( " question_sessions " , array ( " questionid " => $questionid ));
2006-03-20 23:04:22 +00:00
// Now recursively delete all child questions
2008-06-09 12:16:54 +00:00
if ( $children = $DB -> get_records ( 'question' , array ( '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
}
}
2007-01-07 12:46:47 +00:00
2006-03-20 23:04:22 +00:00
// Finally delete the question record itself
2008-06-09 12:16:54 +00:00
$DB -> delete_records ( 'question' , array ( 'id' => $questionid ));
2006-03-20 23:04:22 +00:00
return ;
2006-02-24 10:21:40 +00:00
}
2006-03-21 09:06:34 +00:00
/**
2007-08-09 21:50:59 +00:00
* All question categories and their questions are deleted for this course .
2006-03-21 09:06:34 +00:00
*
2007-08-09 21:50:59 +00:00
* @ param object $mod an object representing the activity
2006-03-21 09:06:34 +00:00
* @ param boolean $feedback to specify if the process must output a summary of its work
* @ return boolean
*/
function question_delete_course ( $course , $feedback = true ) {
2008-06-09 12:16:54 +00:00
global $DB ;
2006-03-21 09:06:34 +00:00
//To store feedback to be showed at the end of the process
$feedbackdata = array ();
//Cache some strings
$strcatdeleted = get_string ( 'unusedcategorydeleted' , 'quiz' );
2007-08-09 21:50:59 +00:00
$coursecontext = get_context_instance ( CONTEXT_COURSE , $course -> id );
2008-06-09 12:16:54 +00:00
$categoriescourse = $DB -> get_records ( 'question_categories' , array ( 'contextid' => $coursecontext -> id ), 'parent' , 'id, parent, name' );
2006-03-21 09:06:34 +00:00
2007-08-09 21:50:59 +00:00
if ( $categoriescourse ) {
2006-03-21 09:06:34 +00:00
//Sort categories following their tree (parent-child) relationships
2007-08-09 21:50:59 +00:00
//this will make the feedback more readable
$categoriescourse = sort_categories_by_tree ( $categoriescourse );
foreach ( $categoriescourse as $category ) {
//Delete it completely (questions and category itself)
//deleting questions
2008-06-09 12:16:54 +00:00
if ( $questions = $DB -> get_records ( " question " , array ( " category " => $category -> id ))) {
2007-08-09 21:50:59 +00:00
foreach ( $questions as $question ) {
delete_question ( $question -> id );
2006-03-21 09:06:34 +00:00
}
2008-06-09 12:16:54 +00:00
$DB -> delete_records ( " question " , array ( " category " => $category -> id ));
2007-08-09 21:50:59 +00:00
}
//delete the category
2008-06-09 12:16:54 +00:00
$DB -> delete_records ( 'question_categories' , array ( 'id' => $category -> id ));
2006-03-21 09:06:34 +00:00
2007-08-09 21:50:59 +00:00
//Fill feedback
$feedbackdata [] = array ( $category -> name , $strcatdeleted );
}
//Inform about changes performed if feedback is enabled
if ( $feedback ) {
$table = new stdClass ;
$table -> head = array ( get_string ( 'category' , 'quiz' ), get_string ( 'action' ));
$table -> data = $feedbackdata ;
print_table ( $table );
}
}
return true ;
}
2006-03-21 09:06:34 +00:00
2008-05-13 21:52:38 +00:00
/**
* Category is about to be deleted ,
* 1 / All question categories and their questions are deleted for this course category .
* 2 / All questions are moved to new category
*
* @ param object $category course category object
* @ param object $newcategory empty means everything deleted , otherwise id of category where content moved
* @ param boolean $feedback to specify if the process must output a summary of its work
* @ return boolean
*/
function question_delete_course_category ( $category , $newcategory , $feedback = true ) {
2008-06-01 15:52:12 +00:00
global $DB ;
2008-05-13 21:52:38 +00:00
$context = get_context_instance ( CONTEXT_COURSECAT , $category -> id );
if ( empty ( $newcategory )) {
$feedbackdata = array (); // To store feedback to be showed at the end of the process
$rescueqcategory = null ; // See the code around the call to question_save_from_deletion.
$strcatdeleted = get_string ( 'unusedcategorydeleted' , 'quiz' );
// Loop over question categories.
2008-06-09 12:16:54 +00:00
if ( $categories = $DB -> get_records ( 'question_categories' , array ( 'contextid' => $context -> id ), 'parent' , 'id, parent, name' )) {
2008-05-13 21:52:38 +00:00
foreach ( $categories as $category ) {
2008-06-12 09:15:16 +00:00
2008-05-13 21:52:38 +00:00
// Deal with any questions in the category.
2008-06-09 12:16:54 +00:00
if ( $questions = $DB -> get_records ( 'question' , array ( 'category' => $category -> id ))) {
2008-05-13 21:52:38 +00:00
// Try to delete each question.
foreach ( $questions as $question ) {
delete_question ( $question -> id );
}
// Check to see if there were any questions that were kept because they are
// still in use somehow, even though quizzes in courses in this category will
// already have been deteted. This could happen, for example, if questions are
// added to a course, and then that course is moved to another category (MDL-14802).
2008-06-01 15:52:12 +00:00
$questionids = $DB -> get_records_menu ( 'question' , array ( 'category' => $category -> id ), '' , 'id,1' );
2008-05-13 21:52:38 +00:00
if ( ! empty ( $questionids )) {
if ( ! $rescueqcategory = question_save_from_deletion ( implode ( ',' , array_keys ( $questionids )),
get_parent_contextid ( $context ), print_context_name ( $context ), $rescueqcategory )) {
return false ;
}
$feedbackdata [] = array ( $category -> name , get_string ( 'questionsmovedto' , 'question' , $rescueqcategory -> name ));
}
}
// Now delete the category.
2008-06-09 12:16:54 +00:00
if ( ! $DB -> delete_records ( 'question_categories' , array ( 'id' => $category -> id ))) {
2008-05-13 21:52:38 +00:00
return false ;
}
$feedbackdata [] = array ( $category -> name , $strcatdeleted );
} // End loop over categories.
}
// Output feedback if requested.
if ( $feedback and $feedbackdata ) {
$table = new stdClass ;
$table -> head = array ( get_string ( 'questioncategory' , 'question' ), get_string ( 'action' ));
$table -> data = $feedbackdata ;
print_table ( $table );
}
} else {
// Move question categories ot the new context.
if ( ! $newcontext = get_context_instance ( CONTEXT_COURSECAT , $newcategory -> id )) {
return false ;
}
2008-06-09 12:16:54 +00:00
if ( ! $DB -> set_field ( 'question_categories' , 'contextid' , $newcontext -> id , array ( 'contextid' => $context -> id ))) {
2008-05-13 21:52:38 +00:00
return false ;
}
if ( $feedback ) {
$a = new stdClass ;
$a -> oldplace = print_context_name ( $context );
$a -> newplace = print_context_name ( $newcontext );
notify ( get_string ( 'movedquestionsandcategories' , 'question' , $a ), 'notifysuccess' );
}
}
return true ;
}
/**
* Enter description here ...
*
* @ param string $questionids list of questionids
* @ param object $newcontext the context to create the saved category in .
2008-06-12 09:15:16 +00:00
* @ param string $oldplace a textual description of the think being deleted , e . g . from get_context_name
2008-05-13 21:52:38 +00:00
* @ param object $newcategory
2008-06-12 09:15:16 +00:00
* @ return mixed false on
2008-05-13 21:52:38 +00:00
*/
function question_save_from_deletion ( $questionids , $newcontextid , $oldplace , $newcategory = null ) {
2008-06-09 12:16:54 +00:00
global $DB ;
2008-05-13 21:52:38 +00:00
// Make a category in the parent context to move the questions to.
if ( is_null ( $newcategory )) {
$newcategory = new object ();
$newcategory -> parent = 0 ;
$newcategory -> contextid = $newcontextid ;
2008-06-09 12:16:54 +00:00
$newcategory -> name = get_string ( 'questionsrescuedfrom' , 'question' , $oldplace );
$newcategory -> info = get_string ( 'questionsrescuedfrominfo' , 'question' , $oldplace );
2008-05-13 21:52:38 +00:00
$newcategory -> sortorder = 999 ;
$newcategory -> stamp = make_unique_id_code ();
2008-06-09 12:16:54 +00:00
if ( ! $newcategory -> id = $DB -> insert_record ( 'question_categories' , $newcategory )) {
2008-05-13 21:52:38 +00:00
return false ;
}
}
// Move any remaining questions to the 'saved' category.
if ( ! question_move_questions_to_category ( $questionids , $newcategory -> id )) {
return false ;
}
return $newcategory ;
}
2007-08-09 21:50:59 +00:00
/**
* All question categories and their questions are deleted for this activity .
*
* @ param object $cm the course module object representing the activity
* @ param boolean $feedback to specify if the process must output a summary of its work
* @ return boolean
*/
function question_delete_activity ( $cm , $feedback = true ) {
2008-06-09 12:16:54 +00:00
global $DB ;
2007-08-09 21:50:59 +00:00
//To store feedback to be showed at the end of the process
$feedbackdata = array ();
2006-03-21 09:06:34 +00:00
2007-08-09 21:50:59 +00:00
//Cache some strings
$strcatdeleted = get_string ( 'unusedcategorydeleted' , 'quiz' );
$modcontext = get_context_instance ( CONTEXT_MODULE , $cm -> id );
2008-06-09 12:16:54 +00:00
if ( $categoriesmods = $DB -> get_records ( 'question_categories' , array ( 'contextid' => $modcontext -> id ), 'parent' , 'id, parent, name' )){
2007-08-09 21:50:59 +00:00
//Sort categories following their tree (parent-child) relationships
//this will make the feedback more readable
$categoriesmods = sort_categories_by_tree ( $categoriesmods );
2006-03-21 09:06:34 +00:00
2007-08-09 21:50:59 +00:00
foreach ( $categoriesmods as $category ) {
2006-03-21 09:06:34 +00:00
2007-08-09 21:50:59 +00:00
//Delete it completely (questions and category itself)
//deleting questions
2008-06-09 12:16:54 +00:00
if ( $questions = $DB -> get_records ( " question " , array ( " category " => $category -> id ))) {
2007-08-09 21:50:59 +00:00
foreach ( $questions as $question ) {
delete_question ( $question -> id );
}
2008-06-09 12:16:54 +00:00
$DB -> delete_records ( " question " , array ( " category " => $category -> id ));
2006-03-21 09:06:34 +00:00
}
2007-08-09 21:50:59 +00:00
//delete the category
2008-06-09 12:16:54 +00:00
$DB -> delete_records ( 'question_categories' , array ( 'id' => $category -> id ));
2007-08-09 21:50:59 +00:00
//Fill feedback
$feedbackdata [] = array ( $category -> name , $strcatdeleted );
2006-03-21 09:06:34 +00:00
}
//Inform about changes performed if feedback is enabled
if ( $feedback ) {
2006-07-18 15:34:24 +00:00
$table = new stdClass ;
2006-03-21 09:06:34 +00:00
$table -> head = array ( get_string ( 'category' , 'quiz' ), get_string ( 'action' ));
$table -> data = $feedbackdata ;
print_table ( $table );
}
}
return true ;
}
2008-05-09 15:05:36 +00:00
/**
* This function should be considered private to the question bank , it is called from
* question / editlib . php question / contextmoveq . php and a few similar places to to the work of
* acutally moving questions and associated data . However , callers of this function also have to
* do other work , which is why you should not call this method directly from outside the questionbank .
*
* @ param string $questionids a comma - separated list of question ids .
* @ param integer $newcategory the id of the category to move to .
*/
function question_move_questions_to_category ( $questionids , $newcategory ) {
2008-06-01 16:14:59 +00:00
global $DB ;
2008-05-09 15:05:36 +00:00
$result = true ;
// Move the questions themselves.
2008-06-01 16:14:59 +00:00
$result = $result && $DB -> set_field_select ( 'question' , 'category' , $newcategory , " id IN ( $questionids ) " );
2008-05-09 15:05:36 +00:00
// Move any subquestions belonging to them.
2008-06-01 16:14:59 +00:00
$result = $result && $DB -> set_field_select ( 'question' , 'category' , $newcategory , " parent IN ( $questionids ) " );
2008-05-09 15:05:36 +00:00
// TODO Deal with datasets.
return $result ;
}
2007-08-09 21:50:59 +00:00
/**
* @ param array $row tab objects
* @ param question_edit_contexts $contexts object representing contexts available from this context
* @ param string $querystring to append to urls
* */
function questionbank_navigation_tabs ( & $row , $contexts , $querystring ) {
global $CFG , $QUESTION_EDITTABCAPS ;
$tabs = array (
'questions' => array ( " $CFG->wwwroot /question/edit.php? $querystring " , get_string ( 'questions' , 'quiz' ), get_string ( 'editquestions' , 'quiz' )),
'categories' => array ( " $CFG->wwwroot /question/category.php? $querystring " , get_string ( 'categories' , 'quiz' ), get_string ( 'editqcats' , 'quiz' )),
'import' => array ( " $CFG->wwwroot /question/import.php? $querystring " , get_string ( 'import' , 'quiz' ), get_string ( 'importquestions' , 'quiz' )),
'export' => array ( " $CFG->wwwroot /question/export.php? $querystring " , get_string ( 'export' , 'quiz' ), get_string ( 'exportquestions' , 'quiz' )));
foreach ( $tabs as $tabname => $tabparams ){
if ( $contexts -> have_one_edit_tab_cap ( $tabname )) {
$row [] = new tabobject ( $tabname , $tabparams [ 0 ], $tabparams [ 1 ], $tabparams [ 2 ]);
}
2007-03-21 10:41:28 +00:00
}
}
2008-04-04 02:54:20 +00:00
/**
2008-07-08 16:33:47 +00:00
* Given a list of ids , load the basic information about a set of questions from the questions table .
* The $join and $extrafields arguments can be used together to pull in extra data .
* See , for example , the usage in mod / quiz / attemptlib . php , and
* read the code below to see how the SQL is assembled . Throws exceptions on error .
2008-04-04 02:54:20 +00:00
*
2008-07-08 16:33:47 +00:00
* @ param array $questionids array of question ids .
* @ param string $extrafields extra SQL code to be added to the query .
* @ param string $join extra SQL code to be added to the query .
* @ param array $extraparams values for any placeholders in $join .
* You are strongly recommended to use named placeholder .
2008-04-04 02:54:20 +00:00
*
2008-07-08 16:33:47 +00:00
* @ return array partially complete question objects . You need to call get_question_options
* on them before they can be properly used .
2008-04-04 02:54:20 +00:00
*/
2008-07-08 16:33:47 +00:00
function question_preload_questions ( $questionids , $extrafields = '' , $join = '' , $extraparams = array ()) {
2008-06-09 12:16:54 +00:00
global $CFG , $DB ;
2008-04-04 02:54:20 +00:00
if ( $join ) {
2008-07-08 16:33:47 +00:00
$join = ' JOIN ' . $join ;
2008-04-04 02:54:20 +00:00
}
if ( $extrafields ) {
$extrafields = ', ' . $extrafields ;
}
2008-07-08 16:33:47 +00:00
list ( $questionidcondition , $params ) = $DB -> get_in_or_equal (
$questionids , SQL_PARAMS_NAMED , 'qid0000' );
2008-06-09 12:16:54 +00:00
$sql = 'SELECT q.*' . $extrafields . ' FROM {question} q' . $join .
2008-07-08 16:33:47 +00:00
' WHERE q.id ' . $questionidcondition ;
2008-04-04 02:54:20 +00:00
// Load the questions
2008-07-08 16:33:47 +00:00
if ( ! $questions = $DB -> get_records_sql ( $sql , $extraparams + $params )) {
2008-04-04 02:54:20 +00:00
return 'Could not load questions.' ;
}
2008-07-08 16:33:47 +00:00
foreach ( $questions as $question ) {
$question -> _partiallyloaded = true ;
}
return $questions ;
}
/**
* Load a set of questions , given a list of ids . The $join and $extrafields arguments can be used
* together to pull in extra data . See , for example , the usage in mod / quiz / attempt . php , and
* read the code below to see how the SQL is assembled . Throws exceptions on error .
*
* @ param array $questionids array of question ids .
* @ param string $extrafields extra SQL code to be added to the query .
* @ param string $join extra SQL code to be added to the query .
* @ param array $extraparams values for any placeholders in $join .
* You are strongly recommended to use named placeholder .
*
* @ return array question objects .
*/
function question_load_questions ( $questionids , $extrafields = '' , $join = '' ) {
$questions = question_preload_questions ( $questionids , $extrafields , $join );
2008-04-04 02:54:20 +00:00
// Load the question type specific information
if ( ! get_question_options ( $questions )) {
return 'Could not load the question options' ;
}
return $questions ;
}
2006-02-24 10:21:40 +00:00
/**
2006-07-18 15:34:24 +00:00
* Private function to factor common code out of get_question_options () .
2007-01-07 12:46:47 +00:00
*
2006-07-18 15:34:24 +00:00
* @ param object $question the question to tidy .
2007-01-07 12:46:47 +00:00
* @ return boolean true if successful , else false .
2006-07-18 15:34:24 +00:00
*/
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 );
2008-07-08 16:33:47 +00:00
if ( $success = $QTYPES [ $question -> qtype ] -> get_question_options ( $question )) {
if ( isset ( $question -> _partiallyloaded )) {
unset ( $question -> _partiallyloaded );
}
}
return $success ;
2006-07-18 15:34:24 +00:00
}
2006-02-24 10:21:40 +00:00
2006-07-18 15:34:24 +00:00
/**
* Updates the question objects with question type specific
* information by calling { @ link get_question_options ()}
*
* Can be called either with an array of question objects or with a single
* question object .
2007-01-07 12:46:47 +00:00
*
2006-07-18 15:34:24 +00:00
* @ param mixed $questions Either an array of question objects to be updated
* or just a single question object
* @ 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 .
2007-03-30 09:34:05 +00:00
* @ param mixed either the id of a previous attempt , if this attmpt is
* building on a previous one , or false for a clean attempt .
2006-02-24 10:21:40 +00:00
*/
2007-03-30 09:34:05 +00:00
function get_question_states ( & $questions , $cmoptions , $attempt , $lastattemptid = false ) {
2008-06-09 12:16:54 +00:00
global $CFG , $QTYPES , $DB ;
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
2008-06-09 12:16:54 +00:00
// array index in the array returned by $DB->get_records_sql
2006-08-24 16:44:15 +00:00
$statefields = 'n.questionid as question, s.*, n.sumpenalty, n.manualcomment' ;
2006-02-24 10:21:40 +00:00
// Load the newest states for the questions
2008-06-09 12:16:54 +00:00
$sql = " SELECT $statefields
FROM { question_states } s , { question_sessions } n
WHERE s . id = n . newest
AND n . attemptid = ?
AND n . questionid IN ( $questionlist ) " ;
$states = $DB -> get_records_sql ( $sql , array ( $attempt -> uniqueid ));
2006-02-24 10:21:40 +00:00
// Load the newest graded states for the questions
2008-06-09 12:16:54 +00:00
$sql = " SELECT $statefields
FROM { question_states } s , { question_sessions } n
WHERE s . id = n . newgraded
AND n . attemptid = ?
AND n . questionid IN ( $questionlist ) " ;
$gradedstates = $DB -> get_records_sql ( $sql , array ( $attempt -> uniqueid ));
2006-02-24 10:21:40 +00:00
// 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 {
2007-03-29 16:36:16 +00:00
// If the new attempt is to be based on a previous attempt get it and clean things
// Having lastattemptid filled implies that (should we double check?):
// $attempt->attempt > 1 and $cmoptions->attemptonlast and !$attempt->preview
if ( $lastattemptid ) {
// find the responses from the previous attempt and save them to the new session
// Load the last graded state for the question
$statefields = 'n.questionid as question, s.*, n.sumpenalty' ;
2008-06-09 12:16:54 +00:00
$sql = " SELECT $statefields
FROM { question_states } s , { question_sessions } n
WHERE s . id = n . newgraded
AND n . attemptid = ?
AND n . questionid = ? " ;
if ( ! $laststate = $DB -> get_record_sql ( $sql , array ( $lastattemptid , $i ))) {
2007-03-29 16:36:16 +00:00
// Only restore previous responses that have been graded
continue ;
}
// Restore the state so that the responses will be restored
restore_question_state ( $questions [ $i ], $laststate );
2007-08-10 15:22:28 +00:00
$states [ $i ] = clone ( $laststate );
2007-12-17 15:29:48 +00:00
unset ( $states [ $i ] -> id );
2007-03-29 16:36:16 +00:00
} else {
2007-08-10 15:22:28 +00:00
// create a new empty state
$states [ $i ] = new object ;
$states [ $i ] -> question = $i ;
$states [ $i ] -> responses = array ( '' => '' );
$states [ $i ] -> raw_grade = 0 ;
2007-03-29 16:36:16 +00:00
}
// now fill/overide initial values
2006-03-26 07:59:43 +00:00
$states [ $i ] -> attempt = $attempt -> uniqueid ;
$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 ] -> penalty = 0 ;
$states [ $i ] -> sumpenalty = 0 ;
2006-08-24 16:44:15 +00:00
$states [ $i ] -> manualcomment = '' ;
2007-03-29 16:36:16 +00:00
2006-03-26 07:59:43 +00:00
// Prevent further changes to the session from incrementing the
// sequence number
$states [ $i ] -> changed = true ;
2007-03-29 16:36:16 +00:00
if ( $lastattemptid ) {
// prepare the previous responses for new processing
$action = new stdClass ;
$action -> responses = $laststate -> responses ;
$action -> timestamp = $laststate -> timestamp ;
$action -> event = QUESTION_EVENTSAVE ; //emulate save of questions from all pages MDL-7631
// Process these responses ...
question_process_responses ( $questions [ $i ], $states [ $i ], $action , $cmoptions , $attempt );
// Fix for Bug #5506: When each attempt is built on the last one,
2007-08-09 21:50:59 +00:00
// preserve the options from any previous attempt.
2007-03-29 16:36:16 +00:00
if ( isset ( $laststate -> options ) ) {
$states [ $i ] -> options = $laststate -> options ;
}
} else {
// 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 ()}
* @ param object $question The question for which the state is needed
2006-08-18 22:26:04 +00:00
* @ param object $state The state as loaded from the database
* @ return boolean Represents success or failure
2006-02-24 10:21:40 +00:00
*/
2006-02-28 09:26:00 +00:00
function restore_question_state ( & $question , & $state ) {
2006-02-24 13:48:43 +00:00
global $QTYPES ;
2006-02-24 10:21:40 +00:00
// initialise response to the value in the answer field
2008-06-09 16:53:30 +00:00
$state -> responses = array ( '' => $state -> answer );
2006-02-24 10:21:40 +00:00
unset ( $state -> answer );
2008-06-09 16:53:30 +00:00
$state -> manualcomment = isset ( $state -> manualcomment ) ? $state -> manualcomment : '' ;
2006-02-24 10:21:40 +00:00
// Set the changed field to false; any code which changes the
// question session must set this to true and must increment
2006-02-28 09:26:00 +00:00
// ->seq_number. The save_question_session
2006-02-24 10:21:40 +00:00
// function will save the new state object to the database if the field is
// set to true.
$state -> changed = false ;
// Load the question type specific data
2006-02-24 13:48:43 +00:00
return $QTYPES [ $question -> qtype ]
2006-08-18 22:26:04 +00:00
-> restore_session_and_responses ( $question , $state );
2006-02-24 10:21:40 +00:00
}
/**
* Saves the current state of the question session to the database
*
* The state object representing the current state of the session for the
2006-02-28 09:26:00 +00:00
* question is saved to the question_states table with -> responses [ '' ] saved
2006-02-24 10:21:40 +00:00
* to the answer field of the database table . The information in the
* question_sessions table is updated .
* The question type specific data is then saved .
2006-03-27 17:38:30 +00:00
* @ return mixed The id of the saved or updated state or false
2006-02-24 10:21:40 +00:00
* @ param object $question The question for which session is to be saved .
* @ param object $state The state information to be saved . In particular the
* most recent responses are in -> responses . The object
* is updated to hold the new -> id .
*/
2008-07-08 18:22:18 +00:00
function save_question_session ( $question , $state ) {
2008-06-09 12:16:54 +00:00
global $QTYPES , $DB ;
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
2008-06-09 12:16:54 +00:00
$DB -> update_record ( 'question_states' , $state );
2006-02-24 10:21:40 +00:00
} else {
2008-06-09 12:16:54 +00:00
if ( ! $state -> id = $DB -> 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
2008-06-12 09:15:16 +00:00
if ( ! $session = $DB -> get_record ( 'question_sessions' , array ( 'attemptid' => $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 ;
2006-08-24 16:44:15 +00:00
$session -> manualcomment = $state -> manualcomment ;
2008-06-09 12:16:54 +00:00
if ( ! $DB -> insert_record ( 'question_sessions' , $session )) {
2008-05-14 08:37:38 +00:00
print_error ( 'cannotinsert' , 'question' );
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 ;
2006-08-24 16:44:15 +00:00
$session -> manualcomment = $state -> manualcomment ;
2006-08-18 22:34:54 +00:00
} else {
2008-06-09 12:16:54 +00:00
$session -> manualcomment = $session -> manualcomment ;
2006-02-24 10:21:40 +00:00
}
2008-06-09 12:16:54 +00:00
$DB -> update_record ( 'question_sessions' , $session );
2006-02-24 10:21:40 +00:00
}
unset ( $state -> answer );
// Save the question type specific state information and responses
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 ) {
2008-05-26 11:39:51 +00:00
$gradedevents = explode ( ',' , QUESTION_EVENTS_GRADED );
return ( in_array ( $state -> event , $gradedevents ));
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 ])) {
2008-05-14 08:37:38 +00:00
print_error ( 'formquestionnotinids' , 'question' );
2006-02-24 10:21:40 +00:00
}
// Remove the name prefix from the name
//decrypt trying
$key = substr ( $key , strlen ( $questions [ $quid ] -> name_prefix ));
if ( false === $key ) {
$key = '' ;
}
// Check for question validate and mark buttons & set events
if ( $key === 'validate' ) {
2006-02-28 09:26:00 +00:00
$actions [ $quid ] -> event = QUESTION_EVENTVALIDATE ;
2006-03-22 17:22:36 +00:00
} else if ( $key === 'submit' ) {
2006-03-19 18:28:29 +00:00
$actions [ $quid ] -> event = QUESTION_EVENTSUBMIT ;
2006-02-24 10:21:40 +00:00
} else {
$actions [ $quid ] -> event = $defaultevent ;
}
// Update the state with the new response
$actions [ $quid ] -> responses [ $key ] = $response ;
2007-01-07 12:46:47 +00:00
2006-03-22 17:22:36 +00:00
// Set the timestamp
$actions [ $quid ] -> timestamp = $time ;
2006-02-24 10:21:40 +00:00
}
}
2006-08-24 11:37:10 +00:00
foreach ( $actions as $quid => $notused ) {
ksort ( $actions [ $quid ] -> responses );
}
2006-02-24 10:21:40 +00:00
return $actions ;
}
2006-12-18 06:07:44 +00:00
/**
* Returns the html for question feedback image .
* @ param float $fraction value representing the correctness of the user ' s
* response to a question .
* @ param boolean $selected whether or not the answer is the one that the
* user picked .
* @ return string
*/
function question_get_feedback_image ( $fraction , $selected = true ) {
global $CFG ;
2008-06-30 16:56:49 +00:00
static $icons = array ( 'correct' => 'tick_green' , 'partiallycorrect' => 'tick_amber' ,
'incorrect' => 'cross_red' );
2006-12-18 06:07:44 +00:00
2008-06-30 16:56:49 +00:00
if ( $selected ) {
$size = 'big' ;
2006-12-18 06:07:44 +00:00
} else {
2008-06-30 16:56:49 +00:00
$size = 'small' ;
2006-12-18 06:07:44 +00:00
}
2008-06-30 16:56:49 +00:00
$class = question_get_feedback_class ( $fraction );
return '<img src="' . $CFG -> pixpath . '/i/' . $icons [ $class ] . '_' . $size . '.gif" ' .
'alt="' . get_string ( $class , 'quiz' ) . '" class="icon" />' ;
2006-12-18 06:07:44 +00:00
}
/**
* Returns the class name for question feedback .
* @ param float $fraction value representing the correctness of the user ' s
* response to a question .
* @ return string
*/
function question_get_feedback_class ( $fraction ) {
2008-06-30 16:56:49 +00:00
if ( $fraction >= 1 / 1.01 ) {
return 'correct' ;
} else if ( $fraction > 0.0 ) {
return 'partiallycorrect' ;
2006-12-18 06:07:44 +00:00
} else {
2008-06-30 16:56:49 +00:00
return 'incorrect' ;
2006-12-18 06:07:44 +00:00
}
}
2006-02-24 10:21:40 +00:00
/**
* For a given question in an attempt we walk the complete history of states
* and recalculate the grades as we go along .
*
* This is used when a question is changed and old student
* responses need to be marked with the new version of a question .
*
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 .
2008-07-11 07:27:14 +00:00
* @ param boolean $dryrun Optional . Whether to make changes to grades records
* or record that changes need to be made for a later regrade .
2006-02-24 10:21:40 +00:00
*/
2008-07-11 07:27:14 +00:00
function regrade_question_in_attempt ( $question , $attempt , $cmoptions , $verbose = false , $dryrun = false ) {
2008-06-09 12:16:54 +00:00
global $DB ;
2006-02-24 10:21:40 +00:00
// load all states for this question in this attempt, ordered in sequence
2008-06-09 12:16:54 +00:00
if ( $states = $DB -> get_records ( 'question_states' ,
array ( 'attempt' => $attempt -> uniqueid , 'question' => $question -> id ),
2006-08-18 22:26:04 +00:00
'seq_number ASC' )) {
2006-02-24 10:21:40 +00:00
$states = array_values ( $states );
// Subtract the grade for the latest state from $attempt->sumgrades to get the
2007-01-07 12:46:47 +00:00
// sumgrades for the attempt without this question.
2006-02-24 10:21:40 +00:00
$attempt -> sumgrades -= $states [ count ( $states ) - 1 ] -> grade ;
// Initialise the replaystate
$state = clone ( $states [ 0 ]);
2008-06-09 12:16:54 +00:00
$state -> manualcomment = $DB -> get_field ( 'question_sessions' , 'manualcomment' ,
array ( '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 ) {
2008-06-12 09:15:16 +00:00
// Ensure that the grade is in range - in the past this was not checked,
2008-05-15 16:02:12 +00:00
// but now it is (MDL-14835) - so we need to ensure the data is valid before
// proceeding.
if ( $states [ $j ] -> grade < 0 ) {
$states [ $j ] -> grade = 0 ;
2008-07-11 07:27:14 +00:00
$changed = true ;
2008-05-15 16:02:12 +00:00
} else if ( $states [ $j ] -> grade > $question -> maxgrade ) {
$states [ $j ] -> grade = $question -> maxgrade ;
2008-07-11 07:27:14 +00:00
$changed = true ;
2008-05-15 16:02:12 +00:00
}
2008-07-11 07:27:14 +00:00
if ( ! $dryrun ){
$error = question_process_comment ( $question , $replaystate , $attempt ,
$replaystate -> manualcomment , $states [ $j ] -> grade );
if ( is_string ( $error )) {
notify ( $error );
}
} else {
$replaystate -> grade = $states [ $j ] -> grade ;
2008-05-15 16:02:12 +00:00
}
2006-05-13 16:57:17 +00:00
} else {
// Reprocess (regrade) responses
2006-08-18 22:26:04 +00:00
if ( ! question_process_responses ( $question , $replaystate ,
$action , $cmoptions , $attempt )) {
2006-05-13 16:57:17 +00:00
$verbose && notify ( " Couldn't regrade state # { $state -> id } ! " );
}
2008-07-11 07:27:14 +00:00
// We need rounding here because grades in the DB get truncated
// e.g. 0.33333 != 0.3333333, but we want them to be equal here
if (( round (( float ) $replaystate -> raw_grade , 5 ) != round (( float ) $states [ $j ] -> raw_grade , 5 ))
or ( round (( float ) $replaystate -> penalty , 5 ) != round (( float ) $states [ $j ] -> penalty , 5 ))
or ( round (( float ) $replaystate -> grade , 5 ) != round (( float ) $states [ $j ] -> grade , 5 ))) {
$changed = true ;
}
2006-02-24 10:21:40 +00:00
}
2008-07-11 07:27:14 +00:00
2006-02-24 10:21:40 +00:00
$replaystate -> id = $states [ $j ] -> id ;
2007-01-07 12:46:47 +00:00
$replaystate -> changed = true ;
2006-03-26 07:59:43 +00:00
$replaystate -> update = true ; // This will ensure that the existing database entry is updated rather than a new one created
2008-07-11 07:27:14 +00:00
if ( ! $dryrun ){
save_question_session ( $question , $replaystate );
}
2006-02-24 10:21:40 +00:00
}
2006-04-05 05:53:18 +00:00
if ( $changed ) {
2008-07-11 07:27:14 +00:00
if ( ! $dryrun ){
// TODO, call a method in quiz to do this, where 'quiz' comes from
// the question_attempts table.
$DB -> update_record ( 'quiz_attempts' , $attempt );
}
}
if ( $changed ){
$toinsert = new object ();
$toinsert -> oldgrade = round (( float ) $states [ count ( $states ) - 1 ] -> grade , 5 );
$toinsert -> newgrade = round (( float ) $replaystate -> grade , 5 );
$toinsert -> attemptid = $attempt -> uniqueid ;
$toinsert -> questionid = $question -> id ;
//the grade saved is the old grade if the new grade is saved
//it is the new grade if this is a dry run.
$toinsert -> regraded = $dryrun ? 0 : 1 ;
$toinsert -> timemodified = time ();
$DB -> insert_record ( 'quiz_question_regrade' , $toinsert );
return true ;
} else {
return false ;
2006-02-24 10:21:40 +00:00
}
}
2006-04-05 05:53:18 +00:00
return false ;
2006-02-24 10:21:40 +00:00
}
/**
* Processes an array of student responses , grading and saving them as appropriate
*
* @ param object $question Full question object , passed by reference
* @ param object $state Full state object , passed by reference
* @ param object $action object with the fields -> responses which
* is an array holding the student responses ,
2006-02-28 09:26:00 +00:00
* -> action which specifies the action , e . g . , QUESTION_EVENTGRADE ,
2006-02-24 10:21:40 +00:00
* and -> timestamp which is a timestamp from when the responses
* were submitted by the student .
* @ param object $cmoptions
* @ param object $attempt The attempt is passed by reference so that
* during grading its -> sumgrades field can be updated
2008-07-01 13:34:09 +00:00
* @ return boolean Indicates success / failure
2006-02-24 10:21:40 +00:00
*/
2008-07-08 18:22:18 +00:00
function question_process_responses ( $question , & $state , $action , $cmoptions , & $attempt ) {
2006-02-24 13:48:43 +00:00
global $QTYPES ;
2006-02-24 10:21:40 +00:00
// if no responses are set initialise to empty response
if ( ! isset ( $action -> responses )) {
$action -> responses = array ( '' => '' );
}
// 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 )) {
2007-03-29 16:05:55 +00:00
debugging ( 'Ambiguous action in question_process_responses.' , DEBUG_DEVELOPER );
2006-02-28 09:26:00 +00:00
$action -> event = QUESTION_EVENTSAVE ;
2006-02-24 10:21:40 +00:00
}
2006-03-19 18:28:29 +00:00
// If submitted then compare against last graded
2006-02-24 10:21:40 +00:00
// responses, not last given responses in this case
2006-02-28 09:26:00 +00:00
if ( question_isgradingevent ( $action -> event )) {
2006-02-24 10:21:40 +00:00
$state -> responses = $state -> last_graded -> responses ;
}
2007-01-07 12:46:47 +00:00
2006-02-24 10:21:40 +00:00
// Check for unchanged responses (exactly unchanged, not equivalent).
// We also have to catch questions that the student has not yet attempted
2007-01-11 12:18:10 +00:00
$sameresponses = $QTYPES [ $question -> qtype ] -> compare_responses ( $question , $action , $state );
2007-07-17 15:27:59 +00:00
if ( ! empty ( $state -> last_graded ) && $state -> last_graded -> event == QUESTION_EVENTOPEN &&
question_isgradingevent ( $action -> event )) {
2007-01-11 12:18:10 +00:00
$sameresponses = false ;
}
2006-02-24 10:21:40 +00:00
2006-03-19 18:28:29 +00:00
// If the response has not been changed then we do not have to process it again
// unless the attempt is closing or validation is requested
2006-02-28 09:26:00 +00:00
if ( $sameresponses and QUESTION_EVENTCLOSE != $action -> event
2006-08-04 16:53:43 +00:00
and QUESTION_EVENTVALIDATE != $action -> event ) {
2006-02-24 10:21:40 +00:00
return true ;
}
// Roll back grading information to last graded state and set the new
// responses
$newstate = clone ( $state -> last_graded );
$newstate -> responses = $action -> responses ;
$newstate -> seq_number = $state -> seq_number + 1 ;
$newstate -> changed = true ; // will assure that it gets saved to the database
2006-04-30 11:45:14 +00:00
$newstate -> last_graded = clone ( $state -> last_graded );
2006-02-24 10:21:40 +00:00
$newstate -> timestamp = $action -> timestamp ;
$state = $newstate ;
// Set the event to the action we will perform. The question type specific
2006-02-28 09:26:00 +00:00
// grading code may override this by setting it to QUESTION_EVENTCLOSE if the
2006-02-24 10:21:40 +00:00
// attempt at the question causes the session to close
$state -> event = $action -> event ;
2006-02-28 09:26:00 +00:00
if ( ! question_isgradingevent ( $action -> event )) {
2006-02-24 10:21:40 +00:00
// Grade the response but don't update the overall grade
2008-07-01 13:34:09 +00:00
if ( ! $QTYPES [ $question -> qtype ] -> grade_responses ( $question , $state , $cmoptions )) {
return false ;
}
2007-08-09 21:50:59 +00:00
2007-04-25 11:20:22 +00:00
// Temporary hack because question types are not given enough control over what is going
// on. Used by Opaque questions.
// TODO fix this code properly.
if ( ! empty ( $state -> believeevent )) {
// If the state was graded we need to ...
if ( question_state_is_graded ( $state )) {
question_apply_penalty_and_timelimit ( $question , $state , $attempt , $cmoptions );
// update the attempt grade
$attempt -> sumgrades -= ( float ) $state -> last_graded -> grade ;
$attempt -> sumgrades += ( float ) $state -> grade ;
2007-08-09 21:50:59 +00:00
2007-04-25 11:20:22 +00:00
// and update the last_graded field.
unset ( $state -> last_graded );
$state -> last_graded = clone ( $state );
unset ( $state -> last_graded -> changed );
}
} else {
// Don't allow the processing to change the event type
$state -> event = $action -> event ;
}
2007-08-09 21:50:59 +00:00
2006-04-30 11:45:14 +00:00
} else { // grading event
2006-02-24 10:21:40 +00:00
2007-01-07 12:46:47 +00:00
// Unless the attempt is closing, we want to work out if the current responses
// (or equivalent responses) were already given in the last graded attempt.
2006-08-04 16:53:43 +00:00
if ( QUESTION_EVENTCLOSE != $action -> event && QUESTION_EVENTOPEN != $state -> last_graded -> event &&
$QTYPES [ $question -> qtype ] -> compare_responses ( $question , $state , $state -> last_graded )) {
2006-03-19 18:28:29 +00:00
$state -> event = QUESTION_EVENTDUPLICATE ;
2006-02-24 10:21:40 +00:00
}
2006-03-19 18:28:29 +00:00
2006-04-30 11:45:14 +00:00
// If we did not find a duplicate or if the attempt is closing, perform grading
2006-08-04 16:53:43 +00:00
if (( ! $sameresponses and QUESTION_EVENTDUPLICATE != $state -> event ) or
QUESTION_EVENTCLOSE == $action -> event ) {
2008-07-01 13:34:09 +00:00
if ( ! $QTYPES [ $question -> qtype ] -> grade_responses ( $question , $state , $cmoptions )) {
return false ;
}
2006-03-19 18:28:29 +00:00
// Calculate overall grade using correct penalty method
question_apply_penalty_and_timelimit ( $question , $state , $attempt , $cmoptions );
2006-02-24 10:21:40 +00:00
}
2007-04-25 11:20:22 +00:00
// If the state was graded we need to ...
2006-04-30 11:45:14 +00:00
if ( question_state_is_graded ( $state )) {
2007-04-25 11:20:22 +00:00
// update the attempt grade
$attempt -> sumgrades -= ( float ) $state -> last_graded -> grade ;
$attempt -> sumgrades += ( float ) $state -> grade ;
// and update the last_graded field.
2006-04-30 11:45:14 +00:00
unset ( $state -> last_graded );
$state -> last_graded = clone ( $state );
unset ( $state -> last_graded -> changed );
}
2006-02-24 10:21:40 +00:00
}
$attempt -> timemodified = $action -> timestamp ;
return true ;
}
/**
* Determine if event requires grading
*/
2006-02-28 09:26:00 +00:00
function question_isgradingevent ( $event ) {
2006-03-19 18:28:29 +00:00
return ( QUESTION_EVENTSUBMIT == $event || QUESTION_EVENTCLOSE == $event );
2006-02-24 10:21:40 +00:00
}
/**
* Applies the penalty from the previous graded responses to the raw grade
* for the current responses
*
* The grade for the question in the current state is computed by subtracting the
* penalty accumulated over the previous graded responses at the question from the
* raw grade . If the timestamp is more than 1 minute beyond the end of the attempt
* the grade is set to zero . The -> grade field of the state object is modified to
* reflect the new grade but is never allowed to decrease .
* @ param object $question The question for which the penalty is to be applied .
* @ param object $state The state for which the grade is to be set from the
* raw grade and the cumulative penalty from the last
* graded state . The -> grade field is updated by applying
* the penalty scheme determined in $cmoptions to the -> raw_grade and
* -> last_graded -> penalty fields .
* @ param object $cmoptions The options set by the course module .
* The -> penaltyscheme field determines whether penalties
* for incorrect earlier responses are subtracted .
*/
2006-02-28 09:26:00 +00:00
function question_apply_penalty_and_timelimit ( & $question , & $state , $attempt , $cmoptions ) {
2007-04-12 22:16:47 +00:00
// TODO. Quiz dependancy. The fact that the attempt that is passed in here
// is from quiz_attempts, and we use things like $cmoptions->timelimit.
2007-08-09 21:50:59 +00:00
2006-03-19 18:28:29 +00:00
// deal with penalty
2006-02-24 10:21:40 +00:00
if ( $cmoptions -> penaltyscheme ) {
2007-04-25 11:20:22 +00:00
$state -> grade = $state -> raw_grade - $state -> sumpenalty ;
$state -> sumpenalty += ( float ) $state -> penalty ;
2006-02-24 10:21:40 +00:00
} else {
$state -> grade = $state -> raw_grade ;
}
// deal with timelimit
if ( $cmoptions -> timelimit ) {
// We allow for 5% uncertainty in the following test
2007-04-13 10:03:10 +00:00
if ( $state -> timestamp - $attempt -> timestart > $cmoptions -> timelimit * 63 ) {
$cm = get_coursemodule_from_instance ( 'quiz' , $cmoptions -> id );
if ( ! has_capability ( 'mod/quiz:ignoretimelimits' , get_context_instance ( CONTEXT_MODULE , $cm -> id ),
$attempt -> userid , false )) {
$state -> grade = 0 ;
}
2006-02-24 10:21:40 +00:00
}
}
// deal with closing time
if ( $cmoptions -> timeclose and $state -> timestamp > ( $cmoptions -> timeclose + 60 ) // allowing 1 minute lateness
and ! $attempt -> preview ) { // ignore closing time for previews
$state -> grade = 0 ;
}
// Ensure that the grade does not go down
$state -> grade = max ( $state -> grade , $state -> last_graded -> grade );
}
/**
* Print the icon for the question type
*
* @ param object $question The question object for which the icon is required
* @ param boolean $return If true the functions returns the link as a string
*/
2007-04-11 22:57:46 +00:00
function print_question_icon ( $question , $return = false ) {
2006-03-01 07:03:57 +00:00
global $QTYPES , $CFG ;
2006-02-24 10:21:40 +00:00
2007-01-30 11:09:32 +00:00
$namestr = $QTYPES [ $question -> qtype ] -> menu_name ();
2007-04-11 22:57:46 +00:00
$html = '<img src="' . $CFG -> wwwroot . '/question/type/' .
$question -> qtype . '/icon.gif" alt="' .
$namestr . '" title="' . $namestr . '" />' ;
2006-02-24 10:21:40 +00:00
if ( $return ) {
return $html ;
} else {
echo $html ;
}
}
/**
* Returns a html link to the question image if there is one
*
* @ return string The html image tag or the empy string if there is no image .
* @ param object $question The question object
*/
2007-09-04 11:55:10 +00:00
function get_question_image ( $question ) {
2008-06-09 12:16:54 +00:00
global $CFG , $DB ;
2006-02-24 10:21:40 +00:00
$img = '' ;
2008-06-09 12:16:54 +00:00
if ( ! $category = $DB -> get_record ( 'question_categories' , array ( 'id' => $question -> category ))) {
2008-05-14 08:37:38 +00:00
print_error ( 'invalidcategory' );
2007-09-04 11:55:10 +00:00
}
$coursefilesdir = get_filesdir_from_context ( get_context_instance_by_id ( $category -> contextid ));
2006-02-24 10:21:40 +00:00
if ( $question -> image ) {
if ( substr ( strtolower ( $question -> image ), 0 , 7 ) == 'http://' ) {
$img .= $question -> image ;
} else {
2008-07-10 09:55:11 +00:00
require_once ( $CFG -> libdir . '/filelib.php' );
$img = get_file_url ( " $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' ;
2008-05-07 07:40:50 +00:00
$usehtmleditor = can_use_html_editor ();
2006-04-08 08:22:18 +00:00
$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
}
2008-05-15 16:02:12 +00:00
/**
* Process a manual grading action . That is , use $comment and $grade to update
* $state and $attempt . The attempt and the comment text are stored in the
* database . $state is only updated in memory , it is up to the call to store
* that , if appropriate .
*
* @ param object $question the question
* @ param object $state the state to be updated .
* @ param object $attempt the attempt the state belongs to , to be updated .
* @ param string $comment the comment the teacher added
* @ param float $grade the grade the teacher assigned .
* @ return mixed true on success , a string error message if a problem is detected
* ( for example score out of range ) .
*/
2006-04-07 16:00:29 +00:00
function question_process_comment ( $question , & $state , & $attempt , $comment , $grade ) {
2008-06-09 12:16:54 +00:00
global $DB ;
2008-05-15 16:02:12 +00:00
if ( $grade < 0 || $grade > $question -> maxgrade ) {
$a = new stdClass ;
$a -> grade = $grade ;
$a -> maxgrade = $question -> maxgrade ;
$a -> name = $question -> name ;
return get_string ( 'errormanualgradeoutofrange' , 'question' , $a );
}
2006-04-07 16:00:29 +00:00
// Update the comment and save it in the database
2007-09-19 10:56:24 +00:00
$comment = trim ( $comment );
2006-08-24 16:44:15 +00:00
$state -> manualcomment = $comment ;
2008-06-09 12:16:54 +00:00
if ( ! $DB -> set_field ( 'question_sessions' , 'manualcomment' , $comment , array ( 'attemptid' => $attempt -> uniqueid , 'questionid' => $question -> id ))) {
2008-05-15 16:02:12 +00:00
return get_string ( 'errorsavingcomment' , 'question' , $question );
2006-04-07 16:00:29 +00:00
}
2006-10-17 23:23:18 +00:00
// Update the attempt if the score has changed.
2006-04-07 16:00:29 +00:00
if ( abs ( $state -> last_graded -> grade - $grade ) > 0.002 ) {
$attempt -> sumgrades = $attempt -> sumgrades - $state -> last_graded -> grade + $grade ;
$attempt -> timemodified = time ();
2008-06-09 12:16:54 +00:00
if ( ! $DB -> update_record ( 'quiz_attempts' , $attempt )) {
2008-05-15 16:02:12 +00:00
return get_string ( 'errorupdatingattempt' , 'question' , $attempt );
2006-04-07 16:00:29 +00:00
}
2006-10-17 23:23:18 +00:00
}
2006-04-07 16:00:29 +00:00
2006-10-17 23:23:18 +00:00
// Update the state if either the score has changed, or this is the first
2007-09-19 10:56:24 +00:00
// manual grade event and there is actually a grade of comment to process.
2006-10-17 23:23:18 +00:00
// We don't need to store the modified state in the database, we just need
// to set the $state->changed flag.
if ( abs ( $state -> last_graded -> grade - $grade ) > 0.002 ||
2007-09-19 10:56:24 +00:00
( $state -> last_graded -> event != QUESTION_EVENTMANUALGRADE && ( $grade > 0.002 || $comment != '' ))) {
2006-11-15 17:32:49 +00:00
// We want to update existing state (rather than creating new one) if it
// was itself created by a manual grading event.
$state -> update = ( $state -> event == QUESTION_EVENTMANUALGRADE ) ? 1 : 0 ;
// Update the other parts of the state object.
2006-04-07 16:00:29 +00:00
$state -> raw_grade = $grade ;
$state -> grade = $grade ;
$state -> penalty = 0 ;
$state -> timestamp = time ();
2006-05-13 16:57:17 +00:00
$state -> seq_number ++ ;
2006-10-17 23:23:18 +00:00
$state -> event = QUESTION_EVENTMANUALGRADE ;
2006-04-07 16:00:29 +00:00
// Update the last graded state (don't simplify!)
unset ( $state -> last_graded );
$state -> last_graded = clone ( $state );
2006-10-17 23:23:18 +00:00
// We need to indicate that the state has changed in order for it to be saved.
$state -> changed = 1 ;
2006-04-07 16:00:29 +00:00
}
2008-05-15 16:02:12 +00:00
return true ;
2006-04-07 16:00:29 +00:00
}
2006-02-24 10:21:40 +00:00
/**
* Construct name prefixes for question form element names
*
* Construct the name prefix that should be used for example in the
* names of form elements created by questions .
2006-02-28 09:26:00 +00:00
* This is called by { @ link get_question_options ()}
2006-02-24 10:21:40 +00:00
* to set $question -> name_prefix .
* This name prefix includes the question id which can be
2006-02-28 09:26:00 +00:00
* extracted from it with { @ link question_get_id_from_name_prefix ()} .
2006-02-24 10:21:40 +00:00
*
* @ return string
* @ param integer $id The question id
*/
2006-02-28 09:26:00 +00:00
function question_make_name_prefix ( $id ) {
2006-02-24 10:21:40 +00:00
return 'resp' . $id . '_' ;
}
/**
* 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' ) {
2008-06-09 12:16:54 +00:00
global $DB ;
2006-07-18 15:34:24 +00:00
$attempt = new stdClass ;
2006-05-13 16:57:17 +00:00
$attempt -> modulename = $modulename ;
2008-06-09 12:16:54 +00:00
if ( ! $id = $DB -> insert_record ( 'question_attempts' , $attempt )) {
2008-05-14 08:37:38 +00:00
print_error ( 'cannotcreate' , 'question' );
2006-05-13 16:57:17 +00:00
}
return $id ;
2006-02-24 10:21:40 +00:00
}
2006-05-13 16:57:17 +00:00
/**
* Creates a stamp that uniquely identifies this version of the question
2006-04-10 22:10:32 +00:00
*
* In future we want this to use a hash of the question data to guarantee that
* identical versions have the same version stamp .
*
* @ param object $question
* @ return string A unique version stamp
*/
function question_hash ( $question ) {
return make_unique_id_code ();
}
2006-02-24 10:21:40 +00:00
/// FUNCTIONS THAT SIMPLY WRAP QUESTIONTYPE METHODS //////////////////////////////////
2007-04-12 15:58:32 +00:00
/**
* Get the HTML that needs to be included in the head tag when the
* questions in $questionlist are printed in the gives states .
*
* @ param array $questionlist a list of questionids of the questions what will appear on this page .
* @ param array $questions an array of question objects , whose keys are question ids .
* Must contain all the questions in $questionlist
* @ param array $states an array of question state objects , whose keys are question ids .
* Must contain the state of all the questions in $questionlist
*
* @ return string some HTML code that can go inside the head tag .
*/
function get_html_head_contributions ( & $questionlist , & $questions , & $states ) {
global $QTYPES ;
2006-02-24 10:21:40 +00:00
2007-04-12 15:58:32 +00:00
$contributions = array ();
foreach ( $questionlist as $questionid ) {
$question = $questions [ $questionid ];
$contributions = array_merge ( $contributions ,
$QTYPES [ $question -> qtype ] -> get_html_head_contributions (
$question , $states [ $questionid ]));
}
return implode ( " \n " , array_unique ( $contributions ));
}
/**
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
/**
2007-04-12 15:58:32 +00:00
* Saves question options
*
* Simply calls the question type specific save_question_options () method .
*/
2006-03-01 07:36:09 +00:00
function save_question_options ( $question ) {
global $QTYPES ;
$QTYPES [ $question -> qtype ] -> save_question_options ( $question );
}
2006-02-24 10:21:40 +00:00
/**
* Gets all teacher stored answers for a given question
*
* Simply calls the question type specific get_all_responses () method .
*/
// 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 ;
}
2008-06-16 13:29:00 +00:00
/**
* @ return integer grade out of 1 that a random guess by a student might score .
*/
// ULPGc ecastro
2008-06-16 13:59:30 +00:00
function question_get_random_guess_score ( $question ) {
2008-06-16 13:29:00 +00:00
global $QTYPES ;
$r = $QTYPES [ $question -> qtype ] -> get_random_guess_score ( $question );
return $r ;
}
2006-02-24 10:21:40 +00:00
/// CATEGORY FUNCTIONS /////////////////////////////////////////////////////////////////
2006-05-08 10:39:14 +00:00
/**
* returns the categories with their names ordered following parent - child relationships
* finally it tries to return pending categories ( those being orphaned , whose parent is
* incorrect ) to avoid missing any category from original array .
*/
function sort_categories_by_tree ( & $categories , $id = 0 , $level = 1 ) {
2008-06-09 12:16:54 +00:00
global $DB ;
2006-05-08 10:39:14 +00:00
$children = array ();
$keys = array_keys ( $categories );
foreach ( $keys as $key ) {
if ( ! isset ( $categories [ $key ] -> processed ) && $categories [ $key ] -> parent == $id ) {
$children [ $key ] = $categories [ $key ];
$categories [ $key ] -> processed = true ;
$children = $children + sort_categories_by_tree ( $categories , $children [ $key ] -> id , $level + 1 );
}
}
//If level = 1, we have finished, try to look for non processed categories (bad parent) and sort them too
if ( $level == 1 ) {
foreach ( $keys as $key ) {
//If not processed and it's a good candidate to start (because its parent doesn't exist in the course)
2008-06-09 12:16:54 +00:00
if ( ! isset ( $categories [ $key ] -> processed ) && ! $DB -> record_exists ( 'question_categories' , array ( 'course' => $categories [ $key ] -> course , 'id' => $categories [ $key ] -> parent ))) {
2006-05-08 10:39:14 +00:00
$children [ $key ] = $categories [ $key ];
$categories [ $key ] -> processed = true ;
$children = $children + sort_categories_by_tree ( $categories , $children [ $key ] -> id , $level + 1 );
}
}
}
return $children ;
}
/**
2006-07-14 15:36:29 +00:00
* Private method , only for the use of add_indented_names () .
2007-01-07 12:46:47 +00:00
*
2006-07-14 15:36:29 +00:00
* Recursively adds an indentedname field to each category , starting with the category
2007-01-07 12:46:47 +00:00
* with id $id , and dealing with that category and all its children , and
2006-07-14 15:36:29 +00:00
* return a new array , with those categories in the right order .
*
2007-01-07 12:46:47 +00:00
* @ param array $categories an array of categories which has had childids
2006-07-14 15:36:29 +00:00
* fields added by flatten_category_tree () . Passed by reference for
* performance only . It is not modfied .
* @ param int $id the category to start the indenting process from .
* @ param int $depth the indent depth . Used in recursive calls .
* @ return array a new array of categories , in the right order for the tree .
2006-05-08 10:39:14 +00:00
*/
2007-08-09 21:50:59 +00:00
function flatten_category_tree ( & $categories , $id , $depth = 0 , $nochildrenof = - 1 ) {
2007-01-07 12:46:47 +00:00
2006-07-14 15:36:29 +00:00
// Indent the name of this category.
$newcategories = array ();
$newcategories [ $id ] = $categories [ $id ];
$newcategories [ $id ] -> indentedname = str_repeat ( ' ' , $depth ) . $categories [ $id ] -> name ;
2007-01-07 12:46:47 +00:00
2006-07-14 15:36:29 +00:00
// Recursively indent the children.
foreach ( $categories [ $id ] -> childids as $childid ) {
2007-08-09 21:50:59 +00:00
if ( $childid != $nochildrenof ){
$newcategories = $newcategories + flatten_category_tree ( $categories , $childid , $depth + 1 , $nochildrenof );
}
2006-05-08 10:39:14 +00:00
}
2007-01-07 12:46:47 +00:00
2006-07-14 15:36:29 +00:00
// Remove the childids array that were temporarily added.
unset ( $newcategories [ $id ] -> childids );
2007-01-07 12:46:47 +00:00
2006-07-14 15:36:29 +00:00
return $newcategories ;
2006-05-08 10:39:14 +00:00
}
/**
2006-07-14 15:36:29 +00:00
* Format categories into an indented list reflecting the tree structure .
2007-01-07 12:46:47 +00:00
*
2006-07-14 15:36:29 +00:00
* @ param array $categories An array of category objects , for example from the .
* @ return array The formatted list of categories .
2006-05-08 10:39:14 +00:00
*/
2007-08-09 21:50:59 +00:00
function add_indented_names ( $categories , $nochildrenof = - 1 ) {
2006-05-08 10:39:14 +00:00
2007-01-07 12:46:47 +00:00
// Add an array to each category to hold the child category ids. This array will be removed
2006-07-14 15:36:29 +00:00
// again by flatten_category_tree(). It should not be used outside these two functions.
foreach ( array_keys ( $categories ) as $id ) {
$categories [ $id ] -> childids = array ();
2006-05-08 10:39:14 +00:00
}
2006-07-14 15:36:29 +00:00
// Build the tree structure, and record which categories are top-level.
2007-01-07 12:46:47 +00:00
// We have to be careful, because the categories array may include published
2006-07-14 15:36:29 +00:00
// categories from other courses, but not their parents.
$toplevelcategoryids = array ();
foreach ( array_keys ( $categories ) as $id ) {
if ( ! empty ( $categories [ $id ] -> parent ) && array_key_exists ( $categories [ $id ] -> parent , $categories )) {
$categories [ $categories [ $id ] -> parent ] -> childids [] = $id ;
} else {
$toplevelcategoryids [] = $id ;
2006-05-08 10:39:14 +00:00
}
}
2006-07-14 15:36:29 +00:00
// Flatten the tree to and add the indents.
$newcategories = array ();
foreach ( $toplevelcategoryids as $id ) {
2007-08-09 21:50:59 +00:00
$newcategories = $newcategories + flatten_category_tree ( $categories , $id , 0 , $nochildrenof );
2006-05-08 10:39:14 +00:00
}
2006-07-14 15:36:29 +00:00
return $newcategories ;
2006-05-08 10:39:14 +00:00
}
2006-03-02 14:13:42 +00:00
2006-02-24 10:21:40 +00:00
/**
2006-07-14 15:36:29 +00:00
* Output a select menu of question categories .
2007-01-07 12:46:47 +00:00
*
2006-07-14 15:36:29 +00:00
* Categories from this course and ( optionally ) published categories from other courses
2007-01-07 12:46:47 +00:00
* are included . Optionally , only categories the current user may edit can be included .
2006-07-14 15:36:29 +00:00
*
* @ param integer $courseid the id of the course to get the categories for .
* @ param integer $published if true , include publised categories from other courses .
* @ param integer $only_editable if true , exclude categories this user is not allowed to edit .
* @ param integer $selected optionally , the id of a category to be selected by default in the dropdown .
*/
2007-08-09 21:50:59 +00:00
function question_category_select_menu ( $contexts , $top = false , $currentcat = 0 , $selected = " " , $nochildrenof = - 1 ) {
$categoriesarray = question_category_options ( $contexts , $top , $currentcat , false , $nochildrenof );
2007-03-01 09:45:53 +00:00
if ( $selected ) {
$nothing = '' ;
} else {
$nothing = 'choose' ;
2006-02-24 10:21:40 +00:00
}
2007-08-09 21:50:59 +00:00
choose_from_menu_nested ( $categoriesarray , 'category' , $selected , $nothing );
2006-02-24 10:21:40 +00:00
}
2007-09-18 11:23:29 +00:00
/**
* Gets the default category in the most specific context .
* If no categories exist yet then default ones are created in all contexts .
*
* @ param array $contexts The context objects for this context and all parent contexts .
* @ return object The default category - the category in the course context
*/
function question_make_default_categories ( $contexts ) {
2008-06-09 12:16:54 +00:00
global $DB ;
2007-09-18 11:23:29 +00:00
$toreturn = null ;
// If it already exists, just return it.
foreach ( $contexts as $key => $context ) {
2008-06-12 09:15:16 +00:00
if ( ! $exists = $DB -> record_exists ( " question_categories " , array ( 'contextid' => $context -> id ))){
2008-06-09 12:16:54 +00:00
// Otherwise, we need to make one
$category = new stdClass ;
$contextname = print_context_name ( $context , false , true );
$category -> name = get_string ( 'defaultfor' , 'question' , $contextname );
$category -> info = get_string ( 'defaultinfofor' , 'question' , $contextname );
$category -> contextid = $context -> id ;
$category -> parent = 0 ;
$category -> sortorder = 999 ; // By default, all categories get this number, and are sorted alphabetically.
$category -> stamp = make_unique_id_code ();
if ( ! $category -> id = $DB -> insert_record ( 'question_categories' , $category )) {
print_error ( 'cannotcreatedefaultcat' , '' , '' , print_context_name ( $context ));
2007-09-18 11:23:29 +00:00
}
2008-06-12 09:15:16 +00:00
} else {
2008-06-13 11:13:31 +00:00
$category = $DB -> get_record ( 'question_categories' , array ( 'contextid' => $context -> id ), '*' , true );
2007-09-18 11:23:29 +00:00
}
2008-06-12 09:15:16 +00:00
2007-09-18 11:23:29 +00:00
if ( $context -> contextlevel == CONTEXT_COURSE ){
$toreturn = clone ( $category );
}
}
return $toreturn ;
}
2007-01-07 13:54:36 +00:00
/**
2007-08-09 21:50:59 +00:00
* Get all the category objects , including a count of the number of questions in that category ,
* for all the categories in the lists $contexts .
2007-01-07 13:54:36 +00:00
*
2007-08-09 21:50:59 +00:00
* @ param mixed $contexts either a single contextid , or a comma - separated list of context ids .
* @ param string $sortorder used as the ORDER BY clause in the select statement .
* @ return array of category objects .
2007-01-07 13:54:36 +00:00
*/
2007-08-09 21:50:59 +00:00
function get_categories_for_contexts ( $contexts , $sortorder = 'parent, sortorder, name ASC' ) {
2008-06-09 12:16:54 +00:00
global $DB ;
return $DB -> get_records_sql ( "
SELECT * , ( SELECT count ( 1 ) FROM { question } q
WHERE c . id = q . category AND q . hidden = '0' AND q . parent = '0' ) AS questioncount
FROM { question_categories } c
WHERE c . contextid IN ( $contexts )
ORDER BY $sortorder " );
2007-08-09 21:50:59 +00:00
}
2007-05-20 16:17:48 +00:00
2007-08-09 21:50:59 +00:00
/**
* Output an array of question categories .
*/
function question_category_options ( $contexts , $top = false , $currentcat = 0 , $popupform = false , $nochildrenof = - 1 ) {
global $CFG ;
$pcontexts = array ();
foreach ( $contexts as $context ){
$pcontexts [] = $context -> id ;
2007-05-25 05:49:51 +00:00
}
2007-08-09 21:50:59 +00:00
$contextslist = join ( $pcontexts , ', ' );
$categories = get_categories_for_contexts ( $contextslist );
2007-01-07 13:54:36 +00:00
2007-08-09 21:50:59 +00:00
$categories = question_add_context_in_key ( $categories );
if ( $top ){
$categories = question_add_tops ( $categories , $pcontexts );
}
$categories = add_indented_names ( $categories , $nochildrenof );
2007-03-01 09:45:53 +00:00
2007-08-09 21:50:59 +00:00
//sort cats out into different contexts
2007-01-07 13:54:36 +00:00
$categoriesarray = array ();
2007-08-09 21:50:59 +00:00
foreach ( $pcontexts as $pcontext ){
$contextstring = print_context_name ( get_context_instance_by_id ( $pcontext ), true , true );
foreach ( $categories as $category ) {
if ( $category -> contextid == $pcontext ){
$cid = $category -> id ;
if ( $currentcat != $cid || $currentcat == 0 ) {
2007-08-15 12:06:21 +00:00
$countstring = ( ! empty ( $category -> questioncount )) ? " ( $category->questioncount ) " : '' ;
2007-08-09 21:50:59 +00:00
$categoriesarray [ $contextstring ][ $cid ] = $category -> indentedname . $countstring ;
}
}
2007-01-07 13:54:36 +00:00
}
}
2007-08-09 21:50:59 +00:00
if ( $popupform ){
$popupcats = array ();
foreach ( $categoriesarray as $contextstring => $optgroup ){
$popupcats [] = '--' . $contextstring ;
$popupcats = array_merge ( $popupcats , $optgroup );
$popupcats [] = '--' ;
}
return $popupcats ;
} else {
return $categoriesarray ;
}
2007-01-07 13:54:36 +00:00
}
2007-08-09 21:50:59 +00:00
function question_add_context_in_key ( $categories ){
$newcatarray = array ();
foreach ( $categories as $id => $category ) {
$category -> parent = " $category->parent , $category->contextid " ;
$category -> id = " $category->id , $category->contextid " ;
$newcatarray [ " $id , $category->contextid " ] = $category ;
2006-02-24 10:21:40 +00:00
}
2007-08-09 21:50:59 +00:00
return $newcatarray ;
}
function question_add_tops ( $categories , $pcontexts ){
$topcats = array ();
foreach ( $pcontexts as $context ){
$newcat = new object ();
$newcat -> id = " 0, $context " ;
$newcat -> name = get_string ( 'top' );
$newcat -> parent = - 1 ;
$newcat -> contextid = $context ;
$topcats [ " 0, $context " ] = $newcat ;
}
//put topcats in at beginning of array - they'll be sorted into different contexts later.
return array_merge ( $topcats , $categories );
2006-02-24 10:21:40 +00:00
}
/**
2006-10-06 16:48:54 +00:00
* Returns a comma separated list of ids of the category and all subcategories
*/
2006-03-01 07:03:57 +00:00
function question_categorylist ( $categoryid ) {
2008-06-09 12:16:54 +00:00
global $DB ;
2006-02-24 10:21:40 +00:00
// returns a comma separated list of ids of the category and all subcategories
$categorylist = $categoryid ;
2008-06-09 12:16:54 +00:00
if ( $subcategories = $DB -> get_records ( 'question_categories' , array ( '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-11-29 13:29:00 +00:00
2007-08-09 21:50:59 +00:00
2006-02-24 10:21:40 +00:00
2006-05-02 09:04:38 +00:00
//===========================
// Import/Export Functions
//===========================
2006-02-24 15:44:53 +00:00
/**
* Get list of available import or export formats
* @ param string $type 'import' if import list , otherwise export list assumed
2007-01-07 12:46:47 +00:00
* @ return array sorted list of import / export formats available
2006-02-24 15:44:53 +00:00
**/
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 ( " " , " : " , " / " , " \\ " , " | " );
2006-10-07 20:47:54 +00:00
$export_word = str_replace ( $takeoff , " _ " , moodle_strtolower ( get_string ( " exportfilename " , " quiz " )));
2006-02-24 18:49:50 +00:00
//If non-translated, use "export"
if ( substr ( $export_word , 0 , 1 ) == " [ " ) {
$export_word = " export " ;
}
//Calculate the date format string
$export_date_format = str_replace ( " " , " _ " , get_string ( " exportnameformat " , " quiz " ));
//If non-translated, use "%Y%m%d-%H%M"
if ( substr ( $export_date_format , 0 , 1 ) == " [ " ) {
$export_date_format = " %%Y%%m%%d-%%H%%M " ;
}
//Calculate the shortname
$export_shortname = clean_filename ( $course -> shortname );
if ( empty ( $export_shortname ) or $export_shortname == '_' ) {
$export_shortname = $course -> id ;
}
//Calculate the category name
$export_categoryname = clean_filename ( $category -> name );
//Calculate the final export filename
//The export word
$export_name = $export_word . " - " ;
//The shortname
2006-10-07 20:47:54 +00:00
$export_name .= moodle_strtolower ( $export_shortname ) . " - " ;
2006-02-24 18:49:50 +00:00
//The category name
2006-10-07 20:47:54 +00:00
$export_name .= moodle_strtolower ( $export_categoryname ) . " - " ;
2006-02-24 18:49:50 +00:00
//The date format
$export_name .= userdate ( time (), $export_date_format , 99 , false );
2007-10-30 19:29:45 +00:00
//Extension is supplied by format later.
2006-02-24 18:49:50 +00:00
return $export_name ;
}
2007-08-09 21:50:59 +00:00
class context_to_string_translator {
/**
* @ var array used to translate between contextids and strings for this context .
*/
var $contexttostringarray = array ();
function context_to_string_translator ( $contexts ){
$this -> generate_context_to_string_array ( $contexts );
}
function context_to_string ( $contextid ){
return $this -> contexttostringarray [ $contextid ];
}
function string_to_context ( $contextname ){
$contextid = array_search ( $contextname , $this -> contexttostringarray );
return $contextid ;
}
function generate_context_to_string_array ( $contexts ){
if ( ! $this -> contexttostringarray ){
$catno = 1 ;
foreach ( $contexts as $context ){
switch ( $context -> contextlevel ){
case CONTEXT_MODULE :
$contextstring = 'module' ;
break ;
case CONTEXT_COURSE :
$contextstring = 'course' ;
break ;
case CONTEXT_COURSECAT :
$contextstring = " cat $catno " ;
$catno ++ ;
break ;
case CONTEXT_SYSTEM :
$contextstring = 'system' ;
break ;
}
$this -> contexttostringarray [ $context -> id ] = $contextstring ;
}
}
}
}
2006-02-24 18:49:50 +00:00
2007-08-09 21:50:59 +00:00
/**
* Check capability on category
* @ param mixed $question object or id
* @ param string $cap 'add' , 'edit' , 'view' , 'use' , 'move'
* @ param integer $cachecat useful to cache all question records in a category
* @ return boolean this user has the capability $cap for this question $question ?
*/
function question_has_capability_on ( $question , $cap , $cachecat = - 1 ){
2008-06-09 12:16:54 +00:00
global $USER , $DB ;
2008-06-12 12:55:13 +00:00
// nicolasconnault@gmail.com In some cases I get $question === false. Since no such object exists, it can't be deleted, we can safely return true
if ( $question === false ) {
return true ;
}
2007-08-09 21:50:59 +00:00
// these are capabilities on existing questions capabilties are
//set per category. Each of these has a mine and all version. Append 'mine' and 'all'
$question_questioncaps = array ( 'edit' , 'view' , 'use' , 'move' );
static $questions = array ();
static $categories = array ();
static $cachedcat = array ();
2007-08-16 05:19:33 +00:00
if ( $cachecat != - 1 && ( array_search ( $cachecat , $cachedcat ) === FALSE )){
2008-06-09 12:16:54 +00:00
$questions += $DB -> get_records ( 'question' , array ( 'category' => $cachecat ));
2007-08-09 21:50:59 +00:00
$cachedcat [] = $cachecat ;
}
if ( ! is_object ( $question )){
if ( ! isset ( $questions [ $question ])){
2008-06-09 12:16:54 +00:00
if ( ! $questions [ $question ] = $DB -> get_record ( 'question' , array ( 'id' => $question ))) {
2007-11-26 13:32:23 +00:00
print_error ( 'questiondoesnotexist' , 'question' );
2007-08-09 21:50:59 +00:00
}
}
$question = $questions [ $question ];
}
if ( ! isset ( $categories [ $question -> category ])){
2008-06-09 12:16:54 +00:00
if ( ! $categories [ $question -> category ] = $DB -> get_record ( 'question_categories' , array ( 'id' => $question -> category ))) {
2007-08-09 21:50:59 +00:00
print_error ( 'invalidcategory' , 'quiz' );
}
}
$category = $categories [ $question -> category ];
if ( array_search ( $cap , $question_questioncaps ) !== FALSE ){
if ( ! has_capability ( 'moodle/question:' . $cap . 'all' , get_context_instance_by_id ( $category -> contextid ))){
if ( $question -> createdby == $USER -> id ){
return has_capability ( 'moodle/question:' . $cap . 'mine' , get_context_instance_by_id ( $category -> contextid ));
} else {
return false ;
}
} else {
return true ;
}
} else {
return has_capability ( 'moodle/question:' . $cap , get_context_instance_by_id ( $category -> contextid ));
}
}
/**
* Require capability on question .
*/
function question_require_capability_on ( $question , $cap ){
if ( ! question_has_capability_on ( $question , $cap )){
print_error ( 'nopermissions' , '' , '' , $cap );
}
return true ;
}
function question_file_links_base_url ( $courseid ){
global $CFG ;
$baseurl = preg_quote ( " $CFG->wwwroot /file.php " , '!' );
$baseurl .= '(' . preg_quote ( '?file=' , '!' ) . ')?' ; //may or may not
//be using slasharguments, accept either
$baseurl .= " / $courseid / " ; //course directory
return $baseurl ;
}
/*
* Find all course / site files linked to in a piece of html .
* @ param string html the html to search
* @ param int course search for files for courseid course or set to siteid for
* finding site files .
* @ return array files with keys being files .
*/
function question_find_file_links_from_html ( $html , $courseid ){
global $CFG ;
$baseurl = question_file_links_base_url ( $courseid );
$searchfor = '!' .
'(<\s*(a|img)\s[^>]*(href|src)\s*=\s*")' . $baseurl . '([^"]*)"' .
'|' .
'(<\s*(a|img)\s[^>]*(href|src)\s*=\s*\')' . $baseurl . '([^\']*)\'' .
'!i' ;
$matches = array ();
$no = preg_match_all ( $searchfor , $html , $matches );
if ( $no ){
$rawurls = array_filter ( array_merge ( $matches [ 5 ], $matches [ 10 ])); //array_filter removes empty elements
//remove any links that point somewhere they shouldn't
foreach ( array_keys ( $rawurls ) as $rawurlkey ){
if ( ! $cleanedurl = question_url_check ( $rawurls [ $rawurlkey ])){
unset ( $rawurls [ $rawurlkey ]);
} else {
$rawurls [ $rawurlkey ] = $cleanedurl ;
}
}
$urls = array_flip ( $rawurls ); // array_flip removes duplicate files
// and when we merge arrays will continue to automatically remove duplicates
} else {
$urls = array ();
}
return $urls ;
}
2008-06-09 12:16:54 +00:00
/**
2007-08-09 21:50:59 +00:00
* Check that url doesn 't point anywhere it shouldn' t
*
* @ param $url string relative url within course files directory
* @ return mixed boolean false if not OK or cleaned URL as string if OK
*/
function question_url_check ( $url ){
global $CFG ;
if (( substr ( strtolower ( $url ), 0 , strlen ( $CFG -> moddata )) == strtolower ( $CFG -> moddata )) ||
( substr ( strtolower ( $url ), 0 , 10 ) == 'backupdata' )){
return false ;
} else {
return clean_param ( $url , PARAM_PATH );
}
}
2008-06-09 12:16:54 +00:00
/**
2007-08-09 21:50:59 +00:00
* Find all course / site files linked to in a piece of html .
* @ param string html the html to search
* @ param int course search for files for courseid course or set to siteid for
* finding site files .
* @ return array files with keys being files .
*/
function question_replace_file_links_in_html ( $html , $fromcourseid , $tocourseid , $url , $destination , & $changed ){
global $CFG ;
2008-07-10 09:55:11 +00:00
require_once ( $CFG -> libdir . '/filelib.php' );
$tourl = get_file_url ( " $tocourseid / $destination " );
2007-08-09 21:50:59 +00:00
$fromurl = question_file_links_base_url ( $fromcourseid ) . preg_quote ( $url , '!' );
$searchfor = array ( '!(<\s*(a|img)\s[^>]*(href|src)\s*=\s*")' . $fromurl . '(")!i' ,
'!(<\s*(a|img)\s[^>]*(href|src)\s*=\s*\')' . $fromurl . '(\')!i' );
$newhtml = preg_replace ( $searchfor , '\\1' . $tourl . '\\5' , $html );
if ( $newhtml != $html ){
$changed = true ;
}
return $newhtml ;
}
function get_filesdir_from_context ( $context ){
2008-06-09 12:16:54 +00:00
global $DB ;
2007-08-09 21:50:59 +00:00
switch ( $context -> contextlevel ){
case CONTEXT_COURSE :
$courseid = $context -> instanceid ;
break ;
case CONTEXT_MODULE :
2008-06-09 12:16:54 +00:00
$courseid = $DB -> get_field ( 'course_modules' , 'course' , array ( 'id' => $context -> instanceid ));
2007-08-09 21:50:59 +00:00
break ;
case CONTEXT_COURSECAT :
case CONTEXT_SYSTEM :
$courseid = SITEID ;
break ;
default :
2008-05-14 08:37:38 +00:00
print_error ( 'invalidcontext' );
2007-08-09 21:50:59 +00:00
}
return $courseid ;
}
2006-11-29 13:29:00 +00:00
?>