libdir . '/questionlib.php'); /** * This is the base class for Moodle question types. * * There are detailed comments on each method, explaining what the method is * for, and the circumstances under which you might need to override it. * * Note: the questiontype API should NOT be considered stable yet. Very few * question tyeps have been produced yet, so we do not yet know all the places * where the current API is insufficient. I would rather learn from the * experiences of the first few question type implementors, and improve the * interface to meet their needs, rather the freeze the API prematurely and * condem everyone to working round a clunky interface for ever afterwards. * * @package questionbank * @subpackage questiontypes */ class default_questiontype { /** * Name of the question type * * The name returned should coincide with the name of the directory * in which this questiontype is located * * @return string the name of this question type. */ function name() { return 'default'; } /** * Returns a list of other question types that this one requires in order to * work. For example, the calculated question type is a subclass of the * numerical question type, which is a subclass of the shortanswer question * type; and the randomsamatch question type requires the shortanswer type * to be installed. * * @return array any other question types that this one relies on. An empty * array if none. */ function requires_qtypes() { return array(); } /** * @return string the name of this pluginfor passing to get_string, set/get_config, etc. */ function plugin_name() { return 'qtype_' . $this->name(); } /** * @return string the name of this question type in the user's language. * You should not need to override this method, the default behaviour should be fine. */ function local_name() { return get_string($this->name(), $this->plugin_name()); } /** * The name this question should appear as in the create new question * dropdown. Override this method to return false if you don't want your * question type to be createable, for example if it is an abstract base type, * otherwise, you should not need to override this method. * * @return mixed the desired string, or false to hide this question type in the menu. */ function menu_name() { return $this->local_name(); } /** * @return boolean override this to return false if this is not really a * question type, for example the description question type is not * really a question type. */ function is_real_question_type() { return true; } /** * @return boolean true if this question type may require manual grading. */ function is_manual_graded() { return false; } /** * @param object $question a question of this type. * @param string $otherquestionsinuse comma-separate list of other question ids in this attempt. * @return boolean true if a particular instance of this question requires manual grading. */ function is_question_manual_graded($question, $otherquestionsinuse) { return $this->is_manual_graded(); } /** * @return boolean true if a table analyzing responses should be shown in * the quiz statistics report. Usually if a question is manually graded * then this analysis table won't be a good idea. */ function show_analysis_of_responses() { return !$this->is_manual_graded(); } /** * @return boolean true if this question type can be used by the random question type. */ function is_usable_by_random() { return true; } /** * @param question record. * @param integer subqid this is the id of the subquestion. Usually the id * of the question record of the question record but this is dependent on * the question type. Not relevant to some question types. * @return whether the teacher supplied responses can include wildcards. Can * more than one answer be equivalent to one teacher supplied response. */ function has_wildcards_in_responses($question, $subqid) { return false; } /** * @return whether the question_answers.answer field needs to have * restore_decode_content_links_worker called on it. */ function has_html_answers() { return false; } /** * If your question type has a table that extends the question table, and * you want the base class to automatically save, backup and restore the extra fields, * override this method to return an array wherer the first element is the table name, * and the subsequent entries are the column names (apart from id and questionid). * * @return mixed array as above, or null to tell the base class to do nothing. */ function extra_question_fields() { return null; } /** * If you use extra_question_fields, overload this function to return question id field name * in case you table use another name for this column */ function questionid_column_name() { return 'questionid'; } /** * If your question type has a table that extends the question_answers table, * make this method return an array wherer the first element is the table name, * and the subsequent entries are the column names (apart from id and answerid). * * @return mixed array as above, or null to tell the base class to do nothing. */ function extra_answer_fields() { return null; } /** * Return an instance of the question editing form definition. This looks for a * class called edit_{$this->name()}_question_form in the file * {$CFG->dirroot}/question/type/{$this->name()}/edit_{$this->name()}_question_form.php * and if it exists returns an instance of it. * * @param string $submiturl passed on to the constructor call. * @return object an instance of the form definition, or null if one could not be found. */ function create_editing_form($submiturl, $question, $category, $contexts, $formeditable) { global $CFG; require_once("{$CFG->dirroot}/question/type/edit_question_form.php"); $definition_file = $CFG->dirroot.'/question/type/'.$this->name().'/edit_'.$this->name().'_form.php'; if (!(is_readable($definition_file) && is_file($definition_file))) { return null; } require_once($definition_file); $classname = 'question_edit_'.$this->name().'_form'; if (!class_exists($classname)) { return null; } return new $classname($submiturl, $question, $category, $contexts, $formeditable); } /** * @return string the full path of the folder this plugin's files live in. */ function plugin_dir() { global $CFG; return $CFG->dirroot . '/question/type/' . $this->name(); } /** * @return string the URL of the folder this plugin's files live in. */ function plugin_baseurl() { global $CFG; return $CFG->wwwroot . '/question/type/' . $this->name(); } /** * This method should be overriden if you want to include a special heading or some other * html on a question editing page besides the question editing form. * * @param question_edit_form $mform a child of question_edit_form * @param object $question * @param string $wizardnow is '' for first page. */ function display_question_editing_page(&$mform, $question, $wizardnow){ $heading = $this->get_heading(empty($question->id)); print_heading_with_help($heading, $this->name(), $this->plugin_name()); $permissionstrs = array(); if (!empty($question->id)){ if ($question->formoptions->canedit){ $permissionstrs[] = get_string('permissionedit', 'question'); } if ($question->formoptions->canmove){ $permissionstrs[] = get_string('permissionmove', 'question'); } if ($question->formoptions->cansaveasnew){ $permissionstrs[] = get_string('permissionsaveasnew', 'question'); } } if (!$question->formoptions->movecontext && count($permissionstrs)){ print_heading(get_string('permissionto', 'question'), 'center', 3); $html = ''; print_box($html, 'boxwidthnarrow boxaligncenter generalbox'); } $mform->display(); } /** * Method called by display_question_editing_page and by question.php to get heading for breadcrumbs. * * @return array a string heading and the langmodule in which it was found. */ function get_heading($adding = false){ if ($adding) { $prefix = 'adding'; } else { $prefix = 'editing'; } return get_string($prefix . $this->name(), $this->plugin_name()); } /** * Saves (creates or updates) a question. * * Given some question info and some data about the answers * this function parses, organises and saves the question * It is used by {@link question.php} when saving new data from * a form, and also by {@link import.php} when importing questions * This function in turn calls {@link save_question_options} * to save question-type specific data. * * Whether we are saving a new question or updating an existing one can be * determined by testing !empty($question->id). If it is not empty, we are updating. * * The question will be saved in category $form->category. * * @param object $question the question object which should be updated. For a new question will be mostly empty. * @param object $form the object containing the information to save, as if from the question editing form. * @param object $course not really used any more. * @return object On success, return the new question object. On failure, * return an object as follows. If the error object has an errors field, * display that as an error message. Otherwise, the editing form will be * redisplayed with validation errors, from validation_errors field, which * is itself an object, shown next to the form fields. (I don't think this is accurate any more.) */ function save_question($question, $form, $course) { global $USER, $DB; // This default implementation is suitable for most // question types. // First, save the basic question itself $question->name = trim($form->name); $question->questiontext = trim($form->questiontext); $question->questiontextformat = $form->questiontextformat; $question->parent = isset($form->parent) ? $form->parent : 0; $question->length = $this->actual_number_of_questions($question); $question->penalty = isset($form->penalty) ? $form->penalty : 0; if (empty($form->image)) { $question->image = ''; } else { $question->image = $form->image; } if (empty($form->generalfeedback)) { $question->generalfeedback = ''; } else { $question->generalfeedback = trim($form->generalfeedback); } if (empty($question->name)) { $question->name = shorten_text(strip_tags($question->questiontext), 15); if (empty($question->name)) { $question->name = '-'; } } if ($question->penalty > 1 or $question->penalty < 0) { $question->errors['penalty'] = get_string('invalidpenalty', 'quiz'); } if (isset($form->defaultgrade)) { $question->defaultgrade = $form->defaultgrade; } list($question->category) = explode(',', $form->category); if (!empty($question->id)) { /// Question already exists, update. $question->modifiedby = $USER->id; $question->timemodified = time(); $DB->update_record('question', $question); } else { /// New question. // Set the unique code $question->stamp = make_unique_id_code(); $question->createdby = $USER->id; $question->modifiedby = $USER->id; $question->timecreated = time(); $question->timemodified = time(); $question->id = $DB->insert_record('question', $question); } // Now to save all the answers and type-specific options $form->id = $question->id; $form->qtype = $question->qtype; $form->category = $question->category; $form->questiontext = $question->questiontext; $result = $this->save_question_options($form); if (!empty($result->error)) { print_error('questionsaveerror', 'question', '', $result->error); } if (!empty($result->notice)) { notice($result->notice, "question.php?id=$question->id"); } if (!empty($result->noticeyesno)) { notice_yesno($result->noticeyesno, "question.php?id=$question->id&courseid={$course->id}", "edit.php?courseid={$course->id}"); print_footer($course); exit; } // Give the question a unique version stamp determined by question_hash() $DB->set_field('question', 'version', question_hash($question), array('id' => $question->id)); return $question; } /** * Saves question-type specific options * * This is called by {@link save_question()} to save the question-type specific data * @return object $result->error or $result->noticeyesno or $result->notice * @param object $question This holds the information from the editing form, * it is not a standard question object. */ function save_question_options($question) { global $DB; $extra_question_fields = $this->extra_question_fields(); if (is_array($extra_question_fields)) { $question_extension_table = array_shift($extra_question_fields); $function = 'update_record'; $questionidcolname = $this->questionid_column_name(); $options = $DB->get_record($question_extension_table, array($questionidcolname => $question->id)); if (!$options) { $function = 'insert_record'; $options = new stdClass; $options->$questionidcolname = $question->id; } foreach ($extra_question_fields as $field) { if (!isset($question->$field)) { $result = new stdClass; $result->error = "No data for field $field when saving " . $this->name() . ' question id ' . $question->id; return $result; } $options->$field = $question->$field; } if (!$DB->{$function}($question_extension_table, $options)) { $result = new stdClass; $result->error = 'Could not save question options for ' . $this->name() . ' question id ' . $question->id; return $result; } } $extra_answer_fields = $this->extra_answer_fields(); // TODO save the answers, with any extra data. return null; } /** * Loads the question type specific options for the question. * * This function loads any question type specific options for the * question from the database into the question object. This information * is placed in the $question->options field. A question type is * free, however, to decide on a internal structure of the options field. * @return bool Indicates success or failure. * @param object $question The question object for the question. This object * should be updated to include the question type * specific information (it is passed by reference). */ function get_question_options(&$question) { global $CFG, $DB; if (!isset($question->options)) { $question->options = new object; } $extra_question_fields = $this->extra_question_fields(); if (is_array($extra_question_fields)) { $question_extension_table = array_shift($extra_question_fields); $extra_data = $DB->get_record($question_extension_table, array($this->questionid_column_name() => $question->id), implode(', ', $extra_question_fields)); if ($extra_data) { foreach ($extra_question_fields as $field) { $question->options->$field = $extra_data->$field; } } else { notify("Failed to load question options from the table $question_extension_table for questionid " . $question->id); return false; } } $extra_answer_fields = $this->extra_answer_fields(); if (is_array($extra_answer_fields)) { $answer_extension_table = array_shift($extra_answer_fields); $question->options->answers = $DB->get_records_sql(" SELECT qa.*, qax." . implode(', qax.', $extra_answer_fields) . " FROM {question_answers} qa, {$answer_extension_table} qax WHERE qa.questionid = ? AND qax.answerid = qa.id", array($question->id)); if (!$question->options->answers) { notify("Failed to load question answers from the table $answer_extension_table for questionid " . $question->id); return false; } } else { // Don't check for success or failure because some question types do not use the answers table. $question->options->answers = $DB->get_records('question_answers', array('question' => $question->id), 'id ASC'); } return true; } /** * Deletes states from the question-type specific tables * * @param string $stateslist Comma separated list of state ids to be deleted */ function delete_states($stateslist) { /// The default question type does not have any tables of its own // therefore there is nothing to delete return true; } /** * Deletes a question from the question-type specific tables * * @return boolean Success/Failure * @param object $question The question being deleted */ function delete_question($questionid) { global $CFG, $DB; $success = true; $extra_question_fields = $this->extra_question_fields(); if (is_array($extra_question_fields)) { $question_extension_table = array_shift($extra_question_fields); $success = $success && $DB->delete_records($question_extension_table, array($this->questionid_column_name() => $questionid)); } $extra_answer_fields = $this->extra_answer_fields(); if (is_array($extra_answer_fields)) { $answer_extension_table = array_shift($extra_answer_fields); $success = $success && $DB->delete_records_select($answer_extension_table, "answerid IN (SELECT qa.id FROM {question_answers} qa WHERE qa.question = ?)", array($questionid)); } $success = $success && $DB->delete_records('question_answers', array('question' => $questionid)); return $success; } /** * Returns the number of question numbers which are used by the question * * This function returns the number of question numbers to be assigned * to the question. Most question types will have length one; they will be * assigned one number. The 'description' type, however does not use up a * number and so has a length of zero. Other question types may wish to * handle a bundle of questions and hence return a number greater than one. * @return integer The number of question numbers which should be * assigned to the question. * @param object $question The question whose length is to be determined. * Question type specific information is included. */ function actual_number_of_questions($question) { // By default, each question is given one number return 1; } /** * Creates empty session and response information for the question * * This function is called to start a question session. Empty question type * specific session data (if any) and empty response data will be added to the * state object. Session data is any data which must persist throughout the * attempt possibly with updates as the user interacts with the * question. This function does NOT create new entries in the database for * the session; a call to the {@link save_session_and_responses} member will * occur to do this. * @return bool Indicates success or failure. * @param object $question The question for which the session is to be * created. Question type specific information is * included. * @param object $state The state to create the session for. Note that * this will not have been saved in the database so * there will be no id. This object will be updated * to include the question type specific information * (it is passed by reference). In particular, empty * responses will be created in the ->responses * field. * @param object $cmoptions * @param object $attempt The attempt for which the session is to be * started. Questions may wish to initialize the * session in different ways depending on the user id * or time available for the attempt. */ function create_session_and_responses(&$question, &$state, $cmoptions, $attempt) { // The default implementation should work for the legacy question types. // Most question types with only a single form field for the student's response // will use the empty string '' as the index for that one response. This will // automatically be stored in and restored from the answer field in the // question_states table. $state->responses = array( '' => '', ); return true; } /** * Restores the session data and most recent responses for the given state * * This function loads any session data associated with the question * session in the given state from the database into the state object. * In particular it loads the responses that have been saved for the given * state into the ->responses member of the state object. * * Question types with only a single form field for the student's response * will not need not restore the responses; the value of the answer * field in the question_states table is restored to ->responses[''] * before this function is called. Question types with more response fields * should override this method and set the ->responses field to an * associative array of responses. * @return bool Indicates success or failure. * @param object $question The question object for the question including any * question type specific information. * @param object $state The saved state to load the session for. This * object should be updated to include the question * type specific session information and responses * (it is passed by reference). */ function restore_session_and_responses(&$question, &$state) { // The default implementation does nothing (successfully) return true; } /** * Saves the session data and responses for the given question and state * * This function saves the question type specific session data from the * state object to the database. In particular for most question types it saves the * responses from the ->responses member of the state object. The question type * non-specific data for the state has already been saved in the question_states * table and the state object contains the corresponding id and * sequence number which may be used to index a question type specific table. * * Question types with only a single form field for the student's response * which is contained in ->responses[''] will not have to save this response, * it will already have been saved to the answer field of the question_states table. * Question types with more response fields should override this method to convert * the data the ->responses array into a single string field, and save it in the * database. The implementation in the multichoice question type is a good model to follow. * http://cvs.moodle.org/contrib/plugins/question/type/opaque/questiontype.php?view=markup * has a solution that is probably quite generally applicable. * @return bool Indicates success or failure. * @param object $question The question object for the question including * the question type specific information. * @param object $state The state for which the question type specific * data and responses should be saved. */ function save_session_and_responses(&$question, &$state) { // The default implementation does nothing (successfully) return true; } /** * Returns an array of values which will give full marks if graded as * the $state->responses field * * The correct answer to the question in the given state, or an example of * a correct answer if there are many, is returned. This is used by some question * types in the {@link grade_responses()} function but it is also used by the * question preview screen to fill in correct responses. * @return mixed A response array giving the responses corresponding * to the (or a) correct answer to the question. If there is * no correct answer that scores 100% then null is returned. * @param object $question The question for which the correct answer is to * be retrieved. Question type specific information is * available. * @param object $state The state of the question, for which a correct answer is * needed. Question type specific information is included. */ function get_correct_responses(&$question, &$state) { /* The default implementation returns the response for the first answer that gives full marks. */ if ($question->options->answers) { foreach ($question->options->answers as $answer) { if (((int) $answer->fraction) === 1) { return array('' => $answer->answer); } } } return null; } /** * Return an array of values with the texts for all possible responses stored * for the question * * All answers are found and their text values isolated * @return object A mixed object * ->id question id. Needed to manage random questions: * it's the id of the actual question presented to user in a given attempt * ->responses An array of values giving the responses corresponding * to all answers to the question. Answer ids are used as keys. * The text and partial credit are the object components * @param object $question The question for which the answers are to * be retrieved. Question type specific information is * available. */ // ULPGC ecastro function get_all_responses(&$question, &$state) { if (isset($question->options->answers) && is_array($question->options->answers)) { $answers = array(); foreach ($question->options->answers as $aid=>$answer) { $r = new stdClass; $r->answer = $answer->answer; $r->credit = $answer->fraction; $answers[$aid] = $r; } $result = new stdClass; $result->id = $question->id; $result->responses = $answers; return $result; } else { return null; } } /** * The difference between this method an get_all_responses is that this * method is not passed a state object. It is the possible answers to a * question no matter what the state. * This method is not called for random questions. * @return array of possible answers. */ function get_possible_responses(&$question) { static $responses = array(); if (!isset($responses[$question->id])){ $responses[$question->id] = $this->get_all_responses($question, new object()); } return array($question->id => $responses[$question->id]->responses); } /** * @param object $question * @return mixed either a integer score out of 1 that the average random * guess by a student might give or an empty string which means will not * calculate. */ function get_random_guess_score($question) { return 0; } /** * Return the actual response to the question in a given state * for the question. Text is not yet formatted for output. * * @return mixed An array containing the response or reponses (multiple answer, match) * given by the user in a particular attempt. * @param object $question The question for which the correct answer is to * be retrieved. Question type specific information is * available. * @param object $state The state object that corresponds to the question, * for which a correct answer is needed. Question * type specific information is included. */ // ULPGC ecastro function get_actual_response($question, $state) { if (!empty($state->responses)) { $responses[] = $state->responses['']; } else { $responses[] = ''; } return $responses; } function get_actual_response_details($question, $state) { $response = array_shift($this->get_actual_response($question, $state)); $teacherresponses = $this->get_possible_responses($question, $state); //only one response list($tsubqid, $tresponses) = each($teacherresponses); $responsedetail = new object(); $responsedetail->subqid = $tsubqid; $responsedetail->response = $response; if ($aid = $this->check_response($question, $state)){ $responsedetail->aid = $aid; } else { foreach ($tresponses as $aid => $tresponse){ if ($tresponse->answer == $response){ $responsedetail->aid = $aid; break; } } } if (isset($responsedetail->aid)){ $responsedetail->credit = $tresponses[$aid]->credit; } else { $responsedetail->aid = 0; $responsedetail->credit = 0; } return array($responsedetail); } // ULPGC ecastro function get_fractional_grade(&$question, &$state) { $grade = $state->grade; if ($question->maxgrade > 0) { return (float)($grade / $question->maxgrade); } else { return (float)$grade; } } /** * Checks if the response given is correct and returns the id * * @return int The ide number for the stored answer that matches the response * given by the user in a particular attempt. * @param object $question The question for which the correct answer is to * be retrieved. Question type specific information is * available. * @param object $state The state object that corresponds to the question, * for which a correct answer is needed. Question * type specific information is included. */ // ULPGC ecastro function check_response(&$question, &$state){ return false; } // Used by the following function, so that it only returns results once per quiz page. private $htmlheadalreadydone = false; /** * Hook to allow question types to include required JavaScrip or CSS on pages * where they are going to be printed. * * If this question type requires extra CSS or JavaScript to function, * then this method, which will be called before print_header on any page * where this question is going to be printed, is a chance to call * $PAGE->requires->js, $PAGE->requiers->css, and so on. * * The two parameters match the first two parameters of print_question. * * @param object $question The question object. * @param object $state The state object. * * @return array Deprecated. An array of bits of HTML to add to the head of * pages where this question is print_question-ed in the body. The array * should use integer array keys, which have no significance. */ function get_html_head_contributions(&$question, &$state) { // We only do this once for this question type, no matter how often this // method is called on one page. if ($this->htmlheadalreadydone) { return array(); } $this->htmlheadalreadydone = true; // By default, we link to any of the files styles.css, styles.php, // script.js or script.php that exist in the plugin folder. // Core question types should not use this mechanism. Their styles // should be included in the standard theme. $this->find_standard_scripts_and_css(); } /** * Like @see{get_html_head_contributions}, but this method is for CSS and * JavaScript required on the question editing page question/question.php. * * @return an array of bits of HTML to add to the head of pages where * this question is print_question-ed in the body. The array should use * integer array keys, which have no significance. */ function get_editing_head_contributions() { // By default, we link to any of the files styles.css, styles.php, // script.js or script.php that exist in the plugin folder. // Core question types should not use this mechanism. Their styles // should be included in the standard theme. $this->find_standard_scripts_and_css(); } /** * Utility method used by @see{get_html_head_contributions} and * @see{get_editing_head_contributions}. This looks for any of the files * styles.css, styles.php, script.js or script.php that exist in the plugin * folder and ensures they get included. * * @return array as required by get_html_head_contributions or get_editing_head_contributions. */ protected function find_standard_scripts_and_css() { global $PAGE; $plugindir = $this->plugin_dir(); $plugindirrel = 'question/type/' . $this->name(); if (file_exists($plugindir . '/script.js')) { $PAGE->requires->js($plugindirrel . '/script.js'); } if (file_exists($plugindir . '/script.php')) { $PAGE->requires->js($plugindirrel . '/script.php'); } if (file_exists($plugindir . '/styles.css')) { $PAGE->requires->css($plugindirrel . '/styles.css'); } if (file_exists($plugindir . '/styles.php')) { $PAGE->requires->css($plugindirrel . '/styles.php'); } } /** * Prints the question including the number, grading details, content, * feedback and interactions * * This function prints the question including the question number, * grading details, content for the question, any feedback for the previously * submitted responses and the interactions. The default implementation calls * various other methods to print each of these parts and most question types * will just override those methods. * @param object $question The question to be rendered. Question type * specific information is included. The * maximum possible grade is in ->maxgrade. The name * prefix for any named elements is in ->name_prefix. * @param object $state The state to render the question in. The grading * information is in ->grade, ->raw_grade and * ->penalty. The current responses are in * ->responses. This is an associative array (or the * empty string or null in the case of no responses * submitted). The last graded state is in * ->last_graded (hence the most recently graded * responses are in ->last_graded->responses). The * question type specific information is also * included. * @param integer $number The number for this question. * @param object $cmoptions * @param object $options An object describing the rendering options. */ function print_question(&$question, &$state, $number, $cmoptions, $options) { /* The default implementation should work for most question types provided the member functions it calls are overridden where required. The layout is determined by the template question.html */ global $CFG; $isgraded = question_state_is_graded($state->last_graded); if (isset($question->randomquestionid)) { $actualquestionid = $question->randomquestionid; } else { $actualquestionid = $question->id; } // For editing teachers print a link to an editing popup window $editlink = $this->get_question_edit_link($question, $cmoptions, $options); $generalfeedback = ''; if ($isgraded && $options->generalfeedback) { $generalfeedback = $this->format_text($question->generalfeedback, $question->questiontextformat, $cmoptions); } $grade = ''; if ($question->maxgrade > 0 && $options->scores) { if ($cmoptions->optionflags & QUESTION_ADAPTIVE) { if ($isgraded) { $grade = question_format_grade($cmoptions, $state->last_graded->grade).'/'; } else { $grade = '--/'; } } $grade .= question_format_grade($cmoptions, $question->maxgrade); } $comment = $state->manualcomment; $commentlink = ''; if (!empty($options->questioncommentlink)) { $strcomment = get_string('commentorgrade', 'quiz'); $commentlink = link_to_popup_window($options->questioncommentlink . '?attempt=' . $state->attempt . '&question=' . $actualquestionid, 'commentquestion', $strcomment, 480, 750, $strcomment, 'none', true); $commentlink = ''; } $history = $this->history($question, $state, $number, $cmoptions, $options); include "$CFG->dirroot/question/type/question.html"; } /** * Render the question flag, assuming $flagsoption allows it. You will probably * never need to override this method. * * @param object $question the question * @param object $state its current state * @param integer $flagsoption the option that says whether flags should be displayed. */ protected function print_question_flag($question, $state, $flagsoption) { global $CFG, $PAGE; switch ($flagsoption) { case QUESTION_FLAGSSHOWN: $flagcontent = $this->get_question_flag_tag($state->flagged); break; case QUESTION_FLAGSEDITABLE: $id = $question->name_prefix . '_flagged'; if ($state->flagged) { $checked = 'checked="checked" '; } else { $checked = ''; } $qsid = $state->questionsessionid; $aid = $state->attempt; $qid = $state->question; $checksum = question_get_toggleflag_checksum($aid, $qid, $qsid); $postdata = "qsid=$qsid&aid=$aid&qid=$qid&checksum=$checksum&sesskey=" . sesskey(); $flagcontent = '' . '' . "\n"; $PAGE->requires->js_function_call('question_flag_changer.init_flag', array($id, $postdata)); break; default: $flagcontent = ''; } if ($flagcontent) { echo '
' . $flagcontent . "
\n"; } } /** * Work out the actual img tag needed for the flag * * @param boolean $flagged whether the question is currently flagged. * @param string $id an id to be added as an attribute to the img (optional). * @return string the img tag. */ protected function get_question_flag_tag($flagged, $id = '') { global $OUTPUT; if ($id) { $id = 'id="' . $id . '" '; } if ($flagged) { $img = 'i/flagged'; } else { $img = 'i/unflagged'; } return '' . get_string('flagthisquestion', 'question') . ''; } /** * Get a link to an edit icon for this question, if the current user is allowed * to edit it. * * @param object $question the question object. * @param object $cmoptions the options from the module. If $cmoptions->thispageurl is set * then the link will be to edit the question in this browser window, then return to * $cmoptions->thispageurl. Otherwise the link will be to edit in a popup. * @return string the HTML of the link, or nothing it the currenty user is not allowed to edit. */ function get_question_edit_link($question, $cmoptions, $options) { global $CFG, $OUTPUT; /// Is this user allowed to edit this question? if (!empty($options->noeditlink) || !question_has_capability_on($question, 'edit')) { return ''; } /// Work out the right URL. $linkurl = '/question/question.php?id=' . $question->id; if (!empty($cmoptions->cmid)) { $linkurl .= '&cmid=' . $cmoptions->cmid; } else if (!empty($cmoptions->course)) { $linkurl .= '&courseid=' . $cmoptions->course; } else { print_error('missingcourseorcmidtolink', 'question'); } /// Work out the contents of the link. $stredit = get_string('edit'); $linktext = '' . $stredit . ''; if (!empty($cmoptions->thispageurl)) { /// The module allow editing in the same window, print an ordinary link. return '' . $linktext . ''; } else { /// We have to edit in a pop-up. return link_to_popup_window($linkurl . '&inpopup=1', 'editquestion', $linktext, false, false, $stredit, '', true); } } /** * Print history of responses * * Used by print_question() */ function history($question, $state, $number, $cmoptions, $options) { global $DB; $history = ''; if(isset($options->history) and $options->history) { if ($options->history == 'all') { // show all states $states = $DB->get_records_select('question_states', "attempt = ? AND question = ? AND event > '0'", array($state->attempt, $question->id), 'seq_number ASC'); } else { // show only graded states $states = $DB->get_records_select('question_states', "attempt = ? AND question = ? AND event IN (".QUESTION_EVENTS_GRADED.")", array($state->attempt, $question->id), 'seq_number ASC'); } if (count($states) > 1) { $strreviewquestion = get_string('reviewresponse', 'quiz'); $table = new stdClass; $table->width = '100%'; if ($options->scores) { $table->head = array ( get_string('numberabbr', 'quiz'), get_string('action', 'quiz'), get_string('response', 'quiz'), get_string('time'), get_string('score', 'quiz'), //get_string('penalty', 'quiz'), get_string('grade', 'quiz'), ); } else { $table->head = array ( get_string('numberabbr', 'quiz'), get_string('action', 'quiz'), get_string('response', 'quiz'), get_string('time'), ); } foreach ($states as $st) { $st->responses[''] = $st->answer; $this->restore_session_and_responses($question, $st); $b = ($state->id == $st->id) ? '' : ''; $be = ($state->id == $st->id) ? '' : ''; if ($state->id == $st->id) { $link = ''.$st->seq_number.''; } else { if(isset($options->questionreviewlink)) { $link = link_to_popup_window($options->questionreviewlink . '&question=' . $question->id . '&state=' . $st->id, 'reviewquestion', $st->seq_number, 450, 650, $strreviewquestion, 'none', true); } else { $link = $st->seq_number; } } if ($options->scores) { $table->data[] = array ( $link, $b.get_string('event'.$st->event, 'quiz').$be, $b.$this->response_summary($question, $st).$be, $b.userdate($st->timestamp, get_string('timestr', 'quiz')).$be, $b.question_format_grade($cmoptions, $st->raw_grade).$be, $b.question_format_grade($cmoptions, $st->grade).$be ); } else { $table->data[] = array ( $link, $b.get_string('event'.$st->event, 'quiz').$be, $b.$this->response_summary($question, $st).$be, $b.userdate($st->timestamp, get_string('timestr', 'quiz')).$be, ); } } $history = print_table($table, true); } } return $history; } /** * Prints the score obtained and maximum score available plus any penalty * information * * This function prints a summary of the scoring in the most recently * graded state (the question may not have been submitted for marking at * the current state). The default implementation should be suitable for most * question types. * @param object $question The question for which the grading details are * to be rendered. Question type specific information * is included. The maximum possible grade is in * ->maxgrade. * @param object $state The state. In particular the grading information * is in ->grade, ->raw_grade and ->penalty. * @param object $cmoptions * @param object $options An object describing the rendering options. */ function print_question_grading_details(&$question, &$state, $cmoptions, $options) { /* The default implementation prints the number of marks if no attempt has been made. Otherwise it displays the grade obtained out of the maximum grade available and a warning if a penalty was applied for the attempt and displays the overall grade obtained counting all previous responses (and penalties) */ if (QUESTION_EVENTDUPLICATE == $state->event) { echo ' '; print_string('duplicateresponse', 'quiz'); } if ($question->maxgrade > 0 && $options->scores) { if (question_state_is_graded($state->last_graded)) { // Display the grading details from the last graded state $grade = new stdClass; $grade->cur = question_format_grade($cmoptions, $state->last_graded->grade); $grade->max = question_format_grade($cmoptions, $question->maxgrade); $grade->raw = question_format_grade($cmoptions, $state->last_graded->raw_grade); // let student know wether the answer was correct $class = question_get_feedback_class($state->last_graded->raw_grade / $question->maxgrade); echo '
' . get_string($class, 'quiz') . '
'; echo '
'; // print grade for this submission print_string('gradingdetails', 'quiz', $grade); if ($cmoptions->penaltyscheme) { // print details of grade adjustment due to penalties if ($state->last_graded->raw_grade > $state->last_graded->grade){ echo ' '; print_string('gradingdetailsadjustment', 'quiz', $grade); } // print info about new penalty // penalty is relevant only if the answer is not correct and further attempts are possible if (($state->last_graded->raw_grade < $question->maxgrade / 1.01) and (QUESTION_EVENTCLOSEANDGRADE != $state->event)) { if ('' !== $state->last_graded->penalty && ((float)$state->last_graded->penalty) > 0.0) { // A penalty was applied so display it echo ' '; print_string('gradingdetailspenalty', 'quiz', question_format_grade($cmoptions, $state->last_graded->penalty)); } else { /* No penalty was applied even though the answer was not correct (eg. a syntax error) so tell the student that they were not penalised for the attempt */ echo ' '; print_string('gradingdetailszeropenalty', 'quiz'); } } } echo '
'; } } } /** * Prints the main content of the question including any interactions * * This function prints the main content of the question including the * interactions for the question in the state given. The last graded responses * are printed or indicated and the current responses are selected or filled in. * Any names (eg. for any form elements) are prefixed with $question->name_prefix. * This method is called from the print_question method. * @param object $question The question to be rendered. Question type * specific information is included. The name * prefix for any named elements is in ->name_prefix. * @param object $state The state to render the question in. The grading * information is in ->grade, ->raw_grade and * ->penalty. The current responses are in * ->responses. This is an associative array (or the * empty string or null in the case of no responses * submitted). The last graded state is in * ->last_graded (hence the most recently graded * responses are in ->last_graded->responses). The * question type specific information is also * included. * The state is passed by reference because some adaptive * questions may want to update it during rendering * @param object $cmoptions * @param object $options An object describing the rendering options. */ function print_question_formulation_and_controls(&$question, &$state, $cmoptions, $options) { /* This default implementation prints an error and must be overridden by all question type implementations, unless the default implementation of print_question has been overridden. */ notify('Error: Question formulation and input controls has not' .' been implemented for question type '.$this->name()); } /** * Prints the submit button(s) for the question in the given state * * This function prints the submit button(s) for the question in the * given state. The name of any button created will be prefixed with the * unique prefix for the question in $question->name_prefix. The suffix * 'submit' is reserved for the single question submit button and the suffix * 'validate' is reserved for the single question validate button (for * question types which support it). Other suffixes will result in a response * of that name in $state->responses which the printing and grading methods * can then use. * @param object $question The question for which the submit button(s) are to * be rendered. Question type specific information is * included. The name prefix for any * named elements is in ->name_prefix. * @param object $state The state to render the buttons for. The * question type specific information is also * included. * @param object $cmoptions * @param object $options An object describing the rendering options. */ function print_question_submit_buttons(&$question, &$state, $cmoptions, $options) { // The default implementation should be suitable for most question types. // It prints a mark button in the case where individual marking is allowed. if (($cmoptions->optionflags & QUESTION_ADAPTIVE) and !$options->readonly) { echo ''; } } /** * Return a summary of the student response * * This function returns a short string of no more than a given length that * summarizes the student's response in the given $state. This is used for * example in the response history table. This string should already be * formatted for output. * @return string The summary of the student response * @param object $question * @param object $state The state whose responses are to be summarized * @param int $length The maximum length of the returned string */ function response_summary($question, $state, $length=80, $formatting=true) { // This should almost certainly be overridden $responses = $this->get_actual_response($question, $state); if ($formatting){ $responses = $this->format_responses($responses, $question->questiontextformat); } $responses = implode(';', $responses); return shorten_text($responses, $length); } /** * @param array responses is an array of responses. * @return formatted responses */ function format_responses($responses, $format){ $toreturn = array(); foreach ($responses as $response){ $toreturn[] = $this->format_response($response, $format); } return $toreturn; } /** * @param string response is a response. * @return formatted response */ function format_response($response, $format){ return s($response); } /** * Renders the question for printing and returns the LaTeX source produced * * This function should render the question suitable for a printed problem * or solution sheet in LaTeX and return the rendered output. * @return string The LaTeX output. * @param object $question The question to be rendered. Question type * specific information is included. * @param object $state The state to render the question in. The * question type specific information is also * included. * @param object $cmoptions * @param string $type Indicates if the question or the solution is to be * rendered with the values 'question' and * 'solution'. */ function get_texsource(&$question, &$state, $cmoptions, $type) { // The default implementation simply returns a string stating that // the question is only available online. return get_string('onlineonly', 'texsheet'); } /** * Compares two question states for equivalence of the student's responses * * The responses for the two states must be examined to see if they represent * equivalent answers to the question by the student. This method will be * invoked for each of the previous states of the question before grading * occurs. If the student is found to have already attempted the question * with equivalent responses then the attempt at the question is ignored; * grading does not occur and the state does not change. Thus they are not * penalized for this case. * @return boolean * @param object $question The question for which the states are to be * compared. Question type specific information is * included. * @param object $state The state of the question. The responses are in * ->responses. This is the only field of $state * that it is safe to use. * @param object $teststate The state whose responses are to be * compared. The state will be of the same age or * older than $state. If possible, the method should * only use the field $teststate->responses, however * any field that is set up by restore_session_and_responses * can be used. */ function compare_responses(&$question, $state, $teststate) { // The default implementation performs a comparison of the response // arrays. The ordering of the arrays does not matter. // Question types may wish to override this (eg. to ignore trailing // white space or to make "7.0" and "7" compare equal). // In php neither == nor === compare arrays the way you want. The following // ensures that the arrays have the same keys, with the same values. $result = false; $diff1 = array_diff_assoc($state->responses, $teststate->responses); if (empty($diff1)) { $diff2 = array_diff_assoc($teststate->responses, $state->responses); $result = empty($diff2); } return $result; } /** * Checks whether a response matches a given answer * * This method only applies to questions that use teacher-defined answers * * @return boolean */ function test_response(&$question, &$state, $answer) { $response = isset($state->responses['']) ? $state->responses[''] : ''; return ($response == $answer->answer); } /** * Performs response processing and grading * * This function performs response processing and grading and updates * the state accordingly. * @return boolean Indicates success or failure. * @param object $question The question to be graded. Question type * specific information is included. * @param object $state The state of the question to grade. The current * responses are in ->responses. The last graded state * is in ->last_graded (hence the most recently graded * responses are in ->last_graded->responses). The * question type specific information is also * included. The ->raw_grade and ->penalty fields * must be updated. The method is able to * close the question session (preventing any further * attempts at this question) by setting * $state->event to QUESTION_EVENTCLOSEANDGRADE * @param object $cmoptions */ function grade_responses(&$question, &$state, $cmoptions) { // The default implementation uses the test_response method to // compare what the student entered against each of the possible // answers stored in the question, and uses the grade from the // first one that matches. It also sets the marks and penalty. // This should be good enought for most simple question types. $state->raw_grade = 0; foreach($question->options->answers as $answer) { if($this->test_response($question, $state, $answer)) { $state->raw_grade = $answer->fraction; break; } } // Make sure we don't assign negative or too high marks. $state->raw_grade = min(max((float) $state->raw_grade, 0.0), 1.0) * $question->maxgrade; // Update the penalty. $state->penalty = $question->penalty * $question->maxgrade; // mark the state as graded $state->event = ($state->event == QUESTION_EVENTCLOSE) ? QUESTION_EVENTCLOSEANDGRADE : QUESTION_EVENTGRADE; return true; } /** * Returns true if the editing wizard is finished, false otherwise. * * The default implementation returns true, which is suitable for all question- * types that only use one editing form. This function is used in * question.php to decide whether we can regrade any states of the edited * question and redirect to edit.php. * * The dataset dependent question-type, which is extended by the calculated * question-type, overwrites this method because it uses multiple pages (i.e. * a wizard) to set up the question and associated datasets. * * @param object $form The data submitted by the previous page. * * @return boolean Whether the wizard's last page was submitted or not. */ function finished_edit_wizard(&$form) { //In the default case there is only one edit page. return true; } /** * Call format_text from weblib.php with the options appropriate to question types. * * @param string $text the text to format. * @param integer $text the type of text. Normally $question->questiontextformat. * @param object $cmoptions the context the string is being displayed in. Only $cmoptions->course is used. * @return string the formatted text. */ function format_text($text, $textformat, $cmoptions = NULL) { $formatoptions = new stdClass; $formatoptions->noclean = true; $formatoptions->para = false; return format_text($text, $textformat, $formatoptions, $cmoptions === NULL ? NULL : $cmoptions->course); } /* * Find all course / site files linked from a question. * * Need to check for links to files in question_answers.answer and feedback * and in question table in generalfeedback and questiontext fields. Methods * on child classes will also check extra question specific fields. * * Needs to be overriden for child classes that have extra fields containing * html. * * @param string html the html to search * @param int courseid search for files for courseid course or set to siteid for * finding site files. * @return array of url, relative url is key and array with one item = question id as value * relative url is relative to course/site files directory root. */ function find_file_links($question, $courseid){ $urls = array(); /// Question image if ($question->image != ''){ if (substr(strtolower($question->image), 0, 7) == 'http://') { $matches = array(); //support for older questions where we have a complete url in image field if (preg_match('!^'.question_file_links_base_url($courseid).'(.*)!i', $question->image, $matches)){ if ($cleanedurl = question_url_check($urls[$matches[2]])){ $urls[$cleanedurl] = null; } } } else { if ($question->image != ''){ if ($cleanedurl = question_url_check($question->image)){ $urls[$cleanedurl] = null;//will be set later } } } } /// Questiontext and general feedback. $urls += question_find_file_links_from_html($question->questiontext, $courseid); $urls += question_find_file_links_from_html($question->generalfeedback, $courseid); /// Answers, if this question uses them. if (isset($question->options->answers)){ foreach ($question->options->answers as $answerkey => $answer){ /// URLs in the answers themselves, if appropriate. if ($this->has_html_answers()) { $urls += question_find_file_links_from_html($answer->answer, $courseid); } /// URLs in the answer feedback. $urls += question_find_file_links_from_html($answer->feedback, $courseid); } } /// Set all the values of the array to the question object if ($urls){ $urls = array_combine(array_keys($urls), array_fill(0, count($urls), array($question->id))); } return $urls; } /* * Find all course / site files linked from a question. * * Need to check for links to files in question_answers.answer and feedback * and in question table in generalfeedback and questiontext fields. Methods * on child classes will also check extra question specific fields. * * Needs to be overriden for child classes that have extra fields containing * 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 of files, file name is key and array with one item = question id as value */ function replace_file_links($question, $fromcourseid, $tocourseid, $url, $destination){ global $CFG, $DB; $updateqrec = false; /// Question image if (!empty($question->image)){ //support for older questions where we have a complete url in image field if (substr(strtolower($question->image), 0, 7) == 'http://') { $questionimage = preg_replace('!^'.question_file_links_base_url($fromcourseid).preg_quote($url, '!').'$!i', $destination, $question->image, 1); } else { $questionimage = preg_replace('!^'.preg_quote($url, '!').'$!i', $destination, $question->image, 1); } if ($questionimage != $question->image){ $question->image = $questionimage; $updateqrec = true; } } /// Questiontext and general feedback. $question->questiontext = question_replace_file_links_in_html($question->questiontext, $fromcourseid, $tocourseid, $url, $destination, $updateqrec); $question->generalfeedback = question_replace_file_links_in_html($question->generalfeedback, $fromcourseid, $tocourseid, $url, $destination, $updateqrec); /// If anything has changed, update it in the database. if ($updateqrec){ $DB->update_record('question', $question); } /// Answers, if this question uses them. if (isset($question->options->answers)){ //answers that do not need updating have been unset foreach ($question->options->answers as $answer){ $answerchanged = false; /// URLs in the answers themselves, if appropriate. if ($this->has_html_answers()) { $answer->answer = question_replace_file_links_in_html($answer->answer, $fromcourseid, $tocourseid, $url, $destination, $answerchanged); } /// URLs in the answer feedback. $answer->feedback = question_replace_file_links_in_html($answer->feedback, $fromcourseid, $tocourseid, $url, $destination, $answerchanged); /// If anything has changed, update it in the database. if ($answerchanged){ $DB->update_record('question_answers', $answer); } } } } /** * @return the best link to pass to print_error. * @param $cmoptions as passed in from outside. */ function error_link($cmoptions) { global $CFG; $cm = get_coursemodule_from_instance('quiz', $cmoptions->id); if (!empty($cm->id)) { return $CFG->wwwroot . '/mod/quiz/view.php?id=' . $cm->id; } else if (!empty($cm->course)) { return $CFG->wwwroot . '/course/view.php?id=' . $cm->course; } else { return ''; } } /// BACKUP FUNCTIONS //////////////////////////// /* * Backup the data in the question * * This is used in question/backuplib.php */ function backup($bf,$preferences,$question,$level=6) { global $DB; $status = true; $extraquestionfields = $this->extra_question_fields(); if (is_array($extraquestionfields)) { $questionextensiontable = array_shift($extraquestionfields); $record = $DB->get_record($questionextensiontable, array($this->questionid_column_name() => $question)); if ($record) { $tagname = strtoupper($this->name()); $status = $status && fwrite($bf, start_tag($tagname, $level, true)); foreach ($extraquestionfields as $field) { if (!isset($record->$field)) { echo "No data for field $field when backuping " . $this->name() . ' question id ' . $question; return false; } fwrite($bf, full_tag(strtoupper($field), $level + 1, false, $record->$field)); } $status = $status && fwrite($bf, end_tag($tagname, $level, true)); } } $extraasnwersfields = $this->extra_answer_fields(); if (is_array($extraasnwersfields)) { //TODO backup the answers, with any extra data. } else { $status = $status && question_backup_answers($bf, $preferences, $question); } return $status; } /// RESTORE FUNCTIONS ///////////////// /* * Restores the data in the question * * This is used in question/restorelib.php */ function restore($old_question_id,$new_question_id,$info,$restore) { global $DB; $status = true; $extraquestionfields = $this->extra_question_fields(); if (is_array($extraquestionfields)) { $questionextensiontable = array_shift($extraquestionfields); $tagname = strtoupper($this->name()); $recordinfo = $info['#'][$tagname][0]; $record = new stdClass; $qidcolname = $this->questionid_column_name(); $record->$qidcolname = $new_question_id; foreach ($extraquestionfields as $field) { $record->$field = backup_todb($recordinfo['#'][strtoupper($field)]['0']['#']); } $DB->insert_record($questionextensiontable, $record); } //TODO restore extra data in answers return $status; } function restore_map($old_question_id,$new_question_id,$info,$restore) { // There is nothing to decode return true; } function restore_recode_answer($state, $restore) { // There is nothing to decode return $state->answer; } /** * Abstract function implemented by each question type. It runs all the code * required to set up and save a question of any type for testing purposes. * Alternate DB table prefix may be used to facilitate data deletion. */ function generate_test($name, $courseid=null) { $form = new stdClass(); $form->name = $name; $form->questiontextformat = 1; $form->questiontext = 'test question, generated by script'; $form->defaultgrade = 1; $form->penalty = 0.1; $form->generalfeedback = "Well done"; $context = get_context_instance(CONTEXT_COURSE, $courseid); $newcategory = question_make_default_categories(array($context)); $form->category = $newcategory->id . ',1'; $question = new stdClass(); $question->courseid = $courseid; $question->qtype = $this->qtype; return array($form, $question); } } ?>