mirror of
https://github.com/moodle/moodle.git
synced 2025-01-17 21:49:15 +01:00
29a541724f
This has been generated running the following Sniff, part of the Moodle's CodeSniffer standard: - PSR2.Methods.MethodDeclaration It just ensures all the function declarations have the correct order for: - abstract and final. - visibility (public, protected, private). - static. So, all the lines modified by this commit are function declarations and the only changes are in the positions of those keywords.
839 lines
34 KiB
PHP
839 lines
34 KiB
PHP
<?php
|
|
// This file is part of Moodle - http://moodle.org/
|
|
//
|
|
// Moodle is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// Moodle is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU General Public License
|
|
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
/**
|
|
* Defines the question behaviour base class
|
|
*
|
|
* @package moodlecore
|
|
* @subpackage questionbehaviours
|
|
* @copyright 2009 The Open University
|
|
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
|
*/
|
|
|
|
|
|
defined('MOODLE_INTERNAL') || die();
|
|
|
|
|
|
/**
|
|
* The base class for question behaviours.
|
|
*
|
|
* A question behaviour is used by the question engine, specifically by
|
|
* a {@link question_attempt} to manage the flow of actions a student can take
|
|
* as they work through a question, and later, as a teacher manually grades it.
|
|
* In turn, the behaviour will delegate certain processing to the
|
|
* relevant {@link question_definition}.
|
|
*
|
|
* @copyright 2009 The Open University
|
|
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
|
*/
|
|
abstract class question_behaviour {
|
|
|
|
/** @var question_attempt the question attempt we are managing. */
|
|
protected $qa;
|
|
|
|
/** @var question_definition shortcut to $qa->get_question(). */
|
|
protected $question;
|
|
|
|
/**
|
|
* Normally you should not call this constuctor directly. The appropriate
|
|
* behaviour object is created automatically as part of
|
|
* {@link question_attempt::start()}.
|
|
* @param question_attempt $qa the question attempt we will be managing.
|
|
* @param string $preferredbehaviour the type of behaviour that was actually
|
|
* requested. This information is not needed in most cases, the type of
|
|
* subclass is enough, but occasionally it is needed.
|
|
*/
|
|
public function __construct(question_attempt $qa, $preferredbehaviour) {
|
|
$this->qa = $qa;
|
|
$this->question = $qa->get_question(false);
|
|
if (!$this->is_compatible_question($this->question)) {
|
|
throw new coding_exception('This behaviour (' . $this->get_name() .
|
|
') cannot work with this question (' . get_class($this->question) . ')');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Some behaviours can only work with certing types of question. This method
|
|
* allows the behaviour to verify that a question is compatible.
|
|
*
|
|
* This implementation is only provided for backwards-compatibility. You should
|
|
* override this method if you are implementing a behaviour.
|
|
*
|
|
* @param question_definition $question the question.
|
|
*/
|
|
abstract public function is_compatible_question(question_definition $question);
|
|
|
|
/**
|
|
* @return string the name of this behaviour. For example the name of
|
|
* qbehaviour_mymodle is 'mymodel'.
|
|
*/
|
|
public function get_name() {
|
|
return substr(get_class($this), 11);
|
|
}
|
|
|
|
/**
|
|
* Whether the current attempt at this question could be completed just by the
|
|
* student interacting with the question, before $qa->finish() is called.
|
|
*
|
|
* @return boolean whether the attempt can finish naturally.
|
|
*/
|
|
public function can_finish_during_attempt() {
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Cause the question to be renderered. This gets the appropriate behaviour
|
|
* renderer using {@link get_renderer()}, and adjusts the display
|
|
* options using {@link adjust_display_options()} and then calls
|
|
* {@link core_question_renderer::question()} to do the work.
|
|
* @param question_display_options $options controls what should and should not be displayed.
|
|
* @param string|null $number the question number to display.
|
|
* @param core_question_renderer $qoutput the question renderer that will coordinate everything.
|
|
* @param qtype_renderer $qtoutput the question type renderer that will be helping.
|
|
* @return string HTML fragment.
|
|
*/
|
|
public function render(question_display_options $options, $number,
|
|
core_question_renderer $qoutput, qtype_renderer $qtoutput) {
|
|
$behaviouroutput = $this->get_renderer($qoutput->get_page());
|
|
$options = clone($options);
|
|
$this->adjust_display_options($options);
|
|
return $qoutput->question($this->qa, $behaviouroutput, $qtoutput, $options, $number);
|
|
}
|
|
|
|
/**
|
|
* Checks whether the users is allow to be served a particular file.
|
|
* @param question_display_options $options the options that control display of the question.
|
|
* @param string $component the name of the component we are serving files for.
|
|
* @param string $filearea the name of the file area.
|
|
* @param array $args the remaining bits of the file path.
|
|
* @param bool $forcedownload whether the user must be forced to download the file.
|
|
* @return bool true if the user can access this file.
|
|
*/
|
|
public function check_file_access($options, $component, $filearea, $args, $forcedownload) {
|
|
$this->adjust_display_options($options);
|
|
|
|
if ($component == 'question' && $filearea == 'response_bf_comment') {
|
|
foreach ($this->qa->get_step_iterator() as $attemptstep) {
|
|
if ($attemptstep->get_id() == $args[0]) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
return $this->question->check_file_access($this->qa, $options, $component,
|
|
$filearea, $args, $forcedownload);
|
|
}
|
|
|
|
/**
|
|
* @param moodle_page $page the page to render for.
|
|
* @return qbehaviour_renderer get the appropriate renderer to use for this model.
|
|
*/
|
|
public function get_renderer(moodle_page $page) {
|
|
return $page->get_renderer(get_class($this));
|
|
}
|
|
|
|
/**
|
|
* Make any changes to the display options before a question is rendered, so
|
|
* that it can be displayed in a way that is appropriate for the statue it is
|
|
* currently in. For example, by default, if the question is finished, we
|
|
* ensure that it is only ever displayed read-only.
|
|
* @param question_display_options $options the options to adjust. Just change
|
|
* the properties of this object - objects are passed by referece.
|
|
*/
|
|
public function adjust_display_options(question_display_options $options) {
|
|
if (!$this->qa->has_marks()) {
|
|
$options->correctness = false;
|
|
$options->numpartscorrect = false;
|
|
}
|
|
if ($this->qa->get_state()->is_finished()) {
|
|
$options->readonly = true;
|
|
$options->numpartscorrect = $options->numpartscorrect &&
|
|
$this->qa->get_state()->is_partially_correct() &&
|
|
!empty($this->question->shownumcorrect);
|
|
} else {
|
|
$options->hide_all_feedback();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the most applicable hint for the question in its current state.
|
|
* @return question_hint the most applicable hint, or null, if none.
|
|
*/
|
|
public function get_applicable_hint() {
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* What is the minimum fraction that can be scored for this question.
|
|
* Normally this will be based on $this->question->get_min_fraction(),
|
|
* but may be modified in some way by the behaviour.
|
|
*
|
|
* @return number the minimum fraction when this question is attempted under
|
|
* this behaviour.
|
|
*/
|
|
public function get_min_fraction() {
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Return the maximum possible fraction that can be scored for this question.
|
|
* Normally this will be based on $this->question->get_max_fraction(),
|
|
* but may be modified in some way by the behaviour.
|
|
*
|
|
* @return number the maximum fraction when this question is attempted under
|
|
* this behaviour.
|
|
*/
|
|
public function get_max_fraction() {
|
|
return $this->question->get_max_fraction();
|
|
}
|
|
|
|
/**
|
|
* Return an array of the behaviour variables that could be submitted
|
|
* as part of a question of this type, with their types, so they can be
|
|
* properly cleaned.
|
|
* @return array variable name => PARAM_... constant.
|
|
*/
|
|
public function get_expected_data() {
|
|
if (!$this->qa->get_state()->is_finished()) {
|
|
return array();
|
|
}
|
|
|
|
$vars = array('comment' => question_attempt::PARAM_RAW_FILES, 'commentformat' => PARAM_INT);
|
|
if ($this->qa->get_max_mark()) {
|
|
$vars['mark'] = PARAM_RAW_TRIMMED;
|
|
$vars['maxmark'] = PARAM_FLOAT;
|
|
}
|
|
return $vars;
|
|
}
|
|
|
|
/**
|
|
* Return an array of question type variables for the question in its current
|
|
* state. Normally, if {@link adjust_display_options()} would set
|
|
* {@link question_display_options::$readonly} to true, then this method
|
|
* should return an empty array, otherwise it should return
|
|
* $this->question->get_expected_data(). Thus, there should be little need to
|
|
* override this method.
|
|
* @return array|string variable name => PARAM_... constant, or, as a special case
|
|
* that should only be used in unavoidable, the constant question_attempt::USE_RAW_DATA
|
|
* meaning take all the raw submitted data belonging to this question.
|
|
*/
|
|
public function get_expected_qt_data() {
|
|
$fakeoptions = new question_display_options();
|
|
$fakeoptions->readonly = false;
|
|
$this->adjust_display_options($fakeoptions);
|
|
if ($fakeoptions->readonly) {
|
|
return array();
|
|
} else {
|
|
return $this->question->get_expected_data();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Return an array of any im variables, and the value required to get full
|
|
* marks.
|
|
* @return array variable name => value.
|
|
*/
|
|
public function get_correct_response() {
|
|
return array();
|
|
}
|
|
|
|
/**
|
|
* Generate a brief, plain-text, summary of this question. This is used by
|
|
* various reports. This should show the particular variant of the question
|
|
* as presented to students. For example, the calculated quetsion type would
|
|
* fill in the particular numbers that were presented to the student.
|
|
* This method will return null if such a summary is not possible, or
|
|
* inappropriate.
|
|
*
|
|
* Normally, this method delegates to {question_definition::get_question_summary()}.
|
|
*
|
|
* @return string|null a plain text summary of this question.
|
|
*/
|
|
public function get_question_summary() {
|
|
return $this->question->get_question_summary();
|
|
}
|
|
|
|
/**
|
|
* Generate a brief, plain-text, summary of the correct answer to this question.
|
|
* This is used by various reports, and can also be useful when testing.
|
|
* This method will return null if such a summary is not possible, or
|
|
* inappropriate.
|
|
*
|
|
* @return string|null a plain text summary of the right answer to this question.
|
|
*/
|
|
public function get_right_answer_summary() {
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Used by {@link start_based_on()} to get the data needed to start a new
|
|
* attempt from the point this attempt has go to.
|
|
* @return array name => value pairs.
|
|
*/
|
|
public function get_resume_data() {
|
|
$olddata = $this->qa->get_step(0)->get_all_data();
|
|
$olddata = $this->qa->get_last_qt_data() + $olddata;
|
|
$olddata = $this->get_our_resume_data() + $olddata;
|
|
return $olddata;
|
|
}
|
|
|
|
/**
|
|
* Used by {@link start_based_on()} to get the data needed to start a new
|
|
* attempt from the point this attempt has go to.
|
|
* @return unknown_type
|
|
*/
|
|
protected function get_our_resume_data() {
|
|
return array();
|
|
}
|
|
|
|
/**
|
|
* Classify responses for this question into a number of sub parts and response classes as defined by
|
|
* {@link \question_type::get_possible_responses} for this question type.
|
|
*
|
|
* @param string $whichtries which tries to analyse for response analysis. Will be one of
|
|
* question_attempt::FIRST_TRY, LAST_TRY or ALL_TRIES.
|
|
* Defaults to question_attempt::LAST_TRY.
|
|
* @return (question_classified_response|array)[] If $whichtries is question_attempt::FIRST_TRY or LAST_TRY index is subpartid
|
|
* and values are question_classified_response instances.
|
|
* If $whichtries is question_attempt::ALL_TRIES then first key is submitted response no
|
|
* and the second key is subpartid.
|
|
*/
|
|
public function classify_response($whichtries = question_attempt::LAST_TRY) {
|
|
if ($whichtries == question_attempt::LAST_TRY) {
|
|
return $this->question->classify_response($this->qa->get_last_qt_data());
|
|
} else {
|
|
$stepswithsubmit = $this->qa->get_steps_with_submitted_response_iterator();
|
|
if ($whichtries == question_attempt::FIRST_TRY) {
|
|
$firsttry = $stepswithsubmit[1];
|
|
if ($firsttry) {
|
|
return $this->question->classify_response($firsttry->get_qt_data());
|
|
} else {
|
|
return $this->question->classify_response(array());
|
|
}
|
|
} else {
|
|
$classifiedresponses = array();
|
|
foreach ($stepswithsubmit as $submittedresponseno => $step) {
|
|
$classifiedresponses[$submittedresponseno] = $this->question->classify_response($step->get_qt_data());
|
|
}
|
|
return $classifiedresponses;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generate a brief textual description of the current state of the question,
|
|
* normally displayed under the question number.
|
|
*
|
|
* @param bool $showcorrectness Whether right/partial/wrong states should
|
|
* be distinguised.
|
|
* @return string a brief summary of the current state of the qestion attempt.
|
|
*/
|
|
public function get_state_string($showcorrectness) {
|
|
return $this->qa->get_state()->default_string($showcorrectness);
|
|
}
|
|
|
|
abstract public function summarise_action(question_attempt_step $step);
|
|
|
|
/**
|
|
* Initialise the first step in a question attempt when a new
|
|
* {@link question_attempt} is being started.
|
|
*
|
|
* This method must call $this->question->start_attempt($step, $variant), and may
|
|
* perform additional processing if the behaviour requries it.
|
|
*
|
|
* @param question_attempt_step $step the first step of the
|
|
* question_attempt being started.
|
|
* @param int $variant which variant of the question to use.
|
|
*/
|
|
public function init_first_step(question_attempt_step $step, $variant) {
|
|
$this->question->start_attempt($step, $variant);
|
|
$step->set_state(question_state::$todo);
|
|
}
|
|
|
|
/**
|
|
* When an attempt is started based on a previous attempt (see
|
|
* {@link question_attempt::start_based_on}) this method is called to setup
|
|
* the new attempt.
|
|
*
|
|
* This method must call $this->question->apply_attempt_state($step), and may
|
|
* perform additional processing if the behaviour requries it.
|
|
*
|
|
* @param question_attempt_step The first step of the {@link question_attempt}
|
|
* being loaded.
|
|
*/
|
|
public function apply_attempt_state(question_attempt_step $step) {
|
|
$this->question->apply_attempt_state($step);
|
|
$step->set_state(question_state::$todo);
|
|
}
|
|
|
|
/**
|
|
* Checks whether two manual grading actions are the same. That is, whether
|
|
* the comment, and the mark (if given) is the same.
|
|
*
|
|
* @param question_attempt_step $pendingstep contains the new responses.
|
|
* @return bool whether the new response is the same as we already have.
|
|
*/
|
|
protected function is_same_comment($pendingstep) {
|
|
$previouscomment = $this->qa->get_last_behaviour_var('comment');
|
|
$newcomment = $pendingstep->get_behaviour_var('comment');
|
|
|
|
// When the teacher leaves the comment empty, $previouscomment is an empty string but $newcomment is null,
|
|
// therefore they are not equal to each other. That's why checking if $previouscomment != $newcomment is not enough.
|
|
if (($previouscomment != $newcomment) && !(is_null($previouscomment) && html_is_blank($newcomment))) {
|
|
// The comment has changed.
|
|
return false;
|
|
}
|
|
|
|
if (!html_is_blank($newcomment)) {
|
|
// Check comment format.
|
|
$previouscommentformat = $this->qa->get_last_behaviour_var('commentformat');
|
|
$newcommentformat = $pendingstep->get_behaviour_var('commentformat');
|
|
if ($previouscommentformat != $newcommentformat) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// So, now we know the comment is the same, so check the mark, if present.
|
|
$previousfraction = $this->qa->get_fraction();
|
|
$newmark = question_utils::clean_param_mark($pendingstep->get_behaviour_var('mark'));
|
|
|
|
if (is_null($previousfraction)) {
|
|
return is_null($newmark) || $newmark === '';
|
|
} else if (is_null($newmark) || $newmark === '') {
|
|
return false;
|
|
}
|
|
|
|
$newfraction = $newmark / $pendingstep->get_behaviour_var('maxmark');
|
|
|
|
return abs($newfraction - $previousfraction) < 0.0000001;
|
|
}
|
|
|
|
/**
|
|
* The main entry point for processing an action.
|
|
*
|
|
* All the various operations that can be performed on a
|
|
* {@link question_attempt} get channeled through this function, except for
|
|
* {@link question_attempt::start()} which goes to {@link init_first_step()}.
|
|
* {@link question_attempt::finish()} becomes an action with im vars
|
|
* finish => 1, and manual comment/grade becomes an action with im vars
|
|
* comment => comment text, and mark => ..., max_mark => ... if the question
|
|
* is graded.
|
|
*
|
|
* This method should first determine whether the action is significant. For
|
|
* example, if no actual action is being performed, but instead the current
|
|
* responses are being saved, and there has been no change since the last
|
|
* set of responses that were saved, this the action is not significatn. In
|
|
* this case, this method should return {@link question_attempt::DISCARD}.
|
|
* Otherwise it should return {@link question_attempt::KEEP}.
|
|
*
|
|
* If the action is significant, this method should also perform any
|
|
* necessary updates to $pendingstep. For example, it should call
|
|
* {@link question_attempt_step::set_state()} to set the state that results
|
|
* from this action, and if this is a grading action, it should call
|
|
* {@link question_attempt_step::set_fraction()}.
|
|
*
|
|
* This method can also call {@link question_attempt_step::set_behaviour_var()} to
|
|
* store additional infomation. There are two main uses for this. This can
|
|
* be used to store the result of any randomisation done. It is important to
|
|
* store the result of randomisation once, and then in future use the same
|
|
* outcome if the actions are ever replayed. This is how regrading works.
|
|
* The other use is to cache the result of expensive computations performed
|
|
* on the raw response data, so that subsequent display and review of the
|
|
* question does not have to repeat the same expensive computations.
|
|
*
|
|
* Often this method is implemented as a dispatching method that examines
|
|
* the pending step to determine the kind of action being performed, and
|
|
* then calls a more specific method like {@link process_save()} or
|
|
* {@link process_comment()}. Look at some of the standard behaviours
|
|
* for examples.
|
|
*
|
|
* @param question_attempt_pending_step $pendingstep a partially initialised step
|
|
* containing all the information about the action that is being peformed. This
|
|
* information can be accessed using {@link question_attempt_step::get_behaviour_var()}.
|
|
* @return bool either {@link question_attempt::KEEP} or {@link question_attempt::DISCARD}
|
|
*/
|
|
abstract public function process_action(question_attempt_pending_step $pendingstep);
|
|
|
|
/**
|
|
* Auto-saved data. By default this does nothing. interesting processing is
|
|
* done in {@link question_behaviour_with_save}.
|
|
*
|
|
* @param question_attempt_pending_step $pendingstep a partially initialised step
|
|
* containing all the information about the action that is being peformed. This
|
|
* information can be accessed using {@link question_attempt_step::get_behaviour_var()}.
|
|
* @return bool either {@link question_attempt::KEEP} or {@link question_attempt::DISCARD}
|
|
*/
|
|
public function process_autosave(question_attempt_pending_step $pendingstep) {
|
|
return question_attempt::DISCARD;
|
|
}
|
|
|
|
/**
|
|
* Implementation of processing a manual comment/grade action that should
|
|
* be suitable for most subclasses.
|
|
* @param question_attempt_pending_step $pendingstep a partially initialised step
|
|
* containing all the information about the action that is being peformed.
|
|
* @return bool either {@link question_attempt::KEEP}
|
|
*/
|
|
public function process_comment(question_attempt_pending_step $pendingstep) {
|
|
if (!$this->qa->get_state()->is_finished()) {
|
|
throw new coding_exception('Cannot manually grade a question before it is finshed.');
|
|
}
|
|
|
|
if ($this->is_same_comment($pendingstep)) {
|
|
return question_attempt::DISCARD;
|
|
}
|
|
|
|
if ($pendingstep->has_behaviour_var('mark')) {
|
|
$mark = question_utils::clean_param_mark($pendingstep->get_behaviour_var('mark'));
|
|
if ($mark === null) {
|
|
throw new coding_exception('Inalid number format ' . $pendingstep->get_behaviour_var('mark') .
|
|
' when processing a manual grading action.', 'Question ' . $this->question->id .
|
|
', slot ' . $this->qa->get_slot());
|
|
|
|
} else if ($mark === '') {
|
|
$fraction = null;
|
|
|
|
} else {
|
|
$fraction = $mark / $pendingstep->get_behaviour_var('maxmark');
|
|
if ($fraction > $this->qa->get_max_fraction() || $fraction < $this->qa->get_min_fraction()) {
|
|
throw new coding_exception('Score out of range when processing ' .
|
|
'a manual grading action.', 'Question ' . $this->question->id .
|
|
', slot ' . $this->qa->get_slot() . ', fraction ' . $fraction);
|
|
}
|
|
}
|
|
|
|
$pendingstep->set_fraction($fraction);
|
|
}
|
|
|
|
$pendingstep->set_state($this->qa->get_state()->corresponding_commented_state(
|
|
$pendingstep->get_fraction()));
|
|
return question_attempt::KEEP;
|
|
}
|
|
|
|
/**
|
|
* @param $comment the comment text to format. If omitted,
|
|
* $this->qa->get_manual_comment() is used.
|
|
* @param $commentformat the format of the comment, one of the FORMAT_... constants.
|
|
* @param $context the quiz context.
|
|
* @return string the comment, ready to be output.
|
|
*/
|
|
public function format_comment($comment = null, $commentformat = null, $context = null) {
|
|
$formatoptions = new stdClass();
|
|
$formatoptions->noclean = true;
|
|
$formatoptions->para = false;
|
|
|
|
if (is_null($comment)) {
|
|
list($comment, $commentformat, $commentstep) = $this->qa->get_manual_comment();
|
|
}
|
|
|
|
if ($context !== null) {
|
|
$comment = $this->qa->rewrite_response_pluginfile_urls($comment, $context->id, 'bf_comment', $commentstep);
|
|
}
|
|
|
|
return format_text($comment, $commentformat, $formatoptions);
|
|
}
|
|
|
|
/**
|
|
* @return string a summary of a manual comment action.
|
|
* @param question_attempt_step $step
|
|
*/
|
|
protected function summarise_manual_comment($step) {
|
|
$a = new stdClass();
|
|
if ($step->has_behaviour_var('comment')) {
|
|
$comment = question_utils::to_plain_text($step->get_behaviour_var('comment'),
|
|
$step->get_behaviour_var('commentformat'));
|
|
$a->comment = shorten_text($comment, 200);
|
|
} else {
|
|
$a->comment = '';
|
|
}
|
|
|
|
$mark = question_utils::clean_param_mark($step->get_behaviour_var('mark'));
|
|
if (is_null($mark) || $mark === '') {
|
|
return get_string('commented', 'question', $a->comment);
|
|
} else {
|
|
$a->mark = $mark / $step->get_behaviour_var('maxmark') * $this->qa->get_max_mark();
|
|
return get_string('manuallygraded', 'question', $a);
|
|
}
|
|
}
|
|
|
|
public function summarise_start($step) {
|
|
return get_string('started', 'question');
|
|
}
|
|
|
|
public function summarise_finish($step) {
|
|
return get_string('attemptfinished', 'question');
|
|
}
|
|
|
|
/**
|
|
* Does this step include a response submitted by a student?
|
|
*
|
|
* This method should return true for any attempt explicitly submitted by a student. The question engine itself will also
|
|
* automatically recognise any last saved response before the attempt is finished, you don't need to return true here for these
|
|
* steps with responses which are not explicitly submitted by the student.
|
|
*
|
|
* @param question_attempt_step $step
|
|
* @return bool is this a step within a question attempt that includes a submitted response by a student.
|
|
*/
|
|
public function step_has_a_submitted_response($step) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* A subclass of {@link question_behaviour} that implements a save
|
|
* action that is suitable for most questions that implement the
|
|
* {@link question_manually_gradable} interface.
|
|
*
|
|
* @copyright 2009 The Open University
|
|
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
|
*/
|
|
abstract class question_behaviour_with_save extends question_behaviour {
|
|
public function required_question_definition_type() {
|
|
return 'question_manually_gradable';
|
|
}
|
|
|
|
public function apply_attempt_state(question_attempt_step $step) {
|
|
parent::apply_attempt_state($step);
|
|
if ($this->question->is_complete_response($step->get_qt_data())) {
|
|
$step->set_state(question_state::$complete);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Work out whether the response in $pendingstep are significantly different
|
|
* from the last set of responses we have stored.
|
|
* @param question_attempt_step $pendingstep contains the new responses.
|
|
* @return bool whether the new response is the same as we already have.
|
|
*/
|
|
protected function is_same_response(question_attempt_step $pendingstep) {
|
|
return $this->question->is_same_response(
|
|
$this->qa->get_last_step()->get_qt_data(), $pendingstep->get_qt_data());
|
|
}
|
|
|
|
/**
|
|
* Work out whether the response in $pendingstep represent a complete answer
|
|
* to the question. Normally this will call
|
|
* {@link question_manually_gradable::is_complete_response}, but some
|
|
* behaviours, for example the CBM ones, have their own parts to the
|
|
* response.
|
|
* @param question_attempt_step $pendingstep contains the new responses.
|
|
* @return bool whether the new response is complete.
|
|
*/
|
|
protected function is_complete_response(question_attempt_step $pendingstep) {
|
|
return $this->question->is_complete_response($pendingstep->get_qt_data());
|
|
}
|
|
|
|
public function process_autosave(question_attempt_pending_step $pendingstep) {
|
|
// If already finished. Nothing to do.
|
|
if ($this->qa->get_state()->is_finished()) {
|
|
return question_attempt::DISCARD;
|
|
}
|
|
|
|
// If the new data is the same as we already have, then we don't need it.
|
|
if ($this->is_same_response($pendingstep)) {
|
|
return question_attempt::DISCARD;
|
|
}
|
|
|
|
// Repeat that test discarding any existing autosaved data.
|
|
if ($this->qa->has_autosaved_step()) {
|
|
$this->qa->discard_autosaved_step();
|
|
if ($this->is_same_response($pendingstep)) {
|
|
return question_attempt::DISCARD;
|
|
}
|
|
}
|
|
|
|
// OK, we need to save.
|
|
return $this->process_save($pendingstep);
|
|
}
|
|
|
|
/**
|
|
* Implementation of processing a save action that should be suitable for
|
|
* most subclasses.
|
|
* @param question_attempt_pending_step $pendingstep a partially initialised step
|
|
* containing all the information about the action that is being peformed.
|
|
* @return bool either {@link question_attempt::KEEP} or {@link question_attempt::DISCARD}
|
|
*/
|
|
public function process_save(question_attempt_pending_step $pendingstep) {
|
|
if ($this->qa->get_state()->is_finished()) {
|
|
return question_attempt::DISCARD;
|
|
} else if (!$this->qa->get_state()->is_active()) {
|
|
throw new coding_exception('Question is not active, cannot process_actions.');
|
|
}
|
|
|
|
if ($this->is_same_response($pendingstep)) {
|
|
return question_attempt::DISCARD;
|
|
}
|
|
|
|
if ($this->is_complete_response($pendingstep)) {
|
|
$pendingstep->set_state(question_state::$complete);
|
|
} else {
|
|
$pendingstep->set_state(question_state::$todo);
|
|
}
|
|
return question_attempt::KEEP;
|
|
}
|
|
|
|
public function summarise_submit(question_attempt_step $step) {
|
|
return get_string('submitted', 'question',
|
|
$this->question->summarise_response($step->get_qt_data()));
|
|
}
|
|
|
|
public function summarise_save(question_attempt_step $step) {
|
|
$data = $step->get_submitted_data();
|
|
if (empty($data)) {
|
|
return $this->summarise_start($step);
|
|
}
|
|
return get_string('saved', 'question',
|
|
$this->question->summarise_response($step->get_qt_data()));
|
|
}
|
|
|
|
|
|
public function summarise_finish($step) {
|
|
$data = $step->get_qt_data();
|
|
if ($data) {
|
|
return get_string('attemptfinishedsubmitting', 'question',
|
|
$this->question->summarise_response($data));
|
|
}
|
|
return get_string('attemptfinished', 'question');
|
|
}
|
|
}
|
|
|
|
abstract class question_behaviour_with_multiple_tries extends question_behaviour_with_save {
|
|
public function step_has_a_submitted_response($step) {
|
|
return $step->has_behaviour_var('submit') && $step->get_state() != question_state::$invalid;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* This helper class contains the constants and methods required for
|
|
* manipulating scores for certainty based marking.
|
|
*
|
|
* @copyright 2009 The Open University
|
|
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
|
*/
|
|
abstract class question_cbm {
|
|
/**#@+ @var integer named constants for the certainty levels. */
|
|
const LOW = 1;
|
|
const MED = 2;
|
|
const HIGH = 3;
|
|
/**#@-*/
|
|
|
|
/** @var array list of all the certainty levels. */
|
|
public static $certainties = array(self::LOW, self::MED, self::HIGH);
|
|
|
|
/**#@+ @var array coefficients used to adjust the fraction based on certainty. */
|
|
protected static $rightscore = array(
|
|
self::LOW => 1,
|
|
self::MED => 2,
|
|
self::HIGH => 3,
|
|
);
|
|
protected static $wrongscore = array(
|
|
self::LOW => 0,
|
|
self::MED => -2,
|
|
self::HIGH => -6,
|
|
);
|
|
/**#@-*/
|
|
|
|
/**#@+ @var array upper and lower limits of the optimal window. */
|
|
protected static $lowlimit = array(
|
|
self::LOW => 0,
|
|
self::MED => 0.666666666666667,
|
|
self::HIGH => 0.8,
|
|
);
|
|
protected static $highlimit = array(
|
|
self::LOW => 0.666666666666667,
|
|
self::MED => 0.8,
|
|
self::HIGH => 1,
|
|
);
|
|
/**#@-*/
|
|
|
|
/**
|
|
* @return int the default certaintly level that should be assuemd if
|
|
* the student does not choose one.
|
|
*/
|
|
public static function default_certainty() {
|
|
return self::LOW;
|
|
}
|
|
|
|
/**
|
|
* Given a fraction, and a certainty, compute the adjusted fraction.
|
|
* @param number $fraction the raw fraction for this question.
|
|
* @param int $certainty one of the certainty level constants.
|
|
* @return number the adjusted fraction taking the certainty into account.
|
|
*/
|
|
public static function adjust_fraction($fraction, $certainty) {
|
|
if ($certainty == -1) {
|
|
// Certainty -1 has never been used in standard Moodle, but is
|
|
// used in Tony-Gardiner Medwin's patches to mean 'No idea' which
|
|
// we intend to implement: MDL-42077. In the mean time, avoid
|
|
// errors for people who have used TGM's patches.
|
|
return 0;
|
|
}
|
|
if ($fraction <= question_utils::MARK_TOLERANCE) {
|
|
return self::$wrongscore[$certainty];
|
|
} else {
|
|
return self::$rightscore[$certainty] * $fraction;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param int $certainty one of the LOW/MED/HIGH constants.
|
|
* @return string a textual description of this certainty.
|
|
*/
|
|
public static function get_string($certainty) {
|
|
return get_string('certainty' . $certainty, 'qbehaviour_deferredcbm');
|
|
}
|
|
|
|
/**
|
|
* @param int $certainty one of the LOW/MED/HIGH constants.
|
|
* @return string a short textual description of this certainty.
|
|
*/
|
|
public static function get_short_string($certainty) {
|
|
return get_string('certaintyshort' . $certainty, 'qbehaviour_deferredcbm');
|
|
}
|
|
|
|
/**
|
|
* Add information about certainty to a response summary.
|
|
* @param string $summary the response summary.
|
|
* @param int $certainty the level of certainty to add.
|
|
* @return string the summary with information about the certainty added.
|
|
*/
|
|
public static function summary_with_certainty($summary, $certainty) {
|
|
if (is_null($certainty)) {
|
|
return $summary;
|
|
}
|
|
return $summary . ' [' . self::get_short_string($certainty) . ']';
|
|
}
|
|
|
|
/**
|
|
* @param int $certainty one of the LOW/MED/HIGH constants.
|
|
* @return float the lower limit of the optimal probability range for this certainty.
|
|
*/
|
|
public static function optimal_probablility_low($certainty) {
|
|
return self::$lowlimit[$certainty];
|
|
}
|
|
|
|
/**
|
|
* @param int $certainty one of the LOW/MED/HIGH constants.
|
|
* @return float the upper limit of the optimal probability range for this certainty.
|
|
*/
|
|
public static function optimal_probablility_high($certainty) {
|
|
return self::$highlimit[$certainty];
|
|
}
|
|
}
|