Merge branch 'MDL-64590' of https://github.com/timhunt/moodle
[moodle.git] / question / behaviour / behaviourbase.php
1 <?php
2 // This file is part of Moodle - http://moodle.org/
3 //
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
8 //
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 // GNU General Public License for more details.
13 //
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
17 /**
18  * Defines the question behaviour base class
19  *
20  * @package    moodlecore
21  * @subpackage questionbehaviours
22  * @copyright  2009 The Open University
23  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24  */
27 defined('MOODLE_INTERNAL') || die();
30 /**
31  * The base class for question behaviours.
32  *
33  * A question behaviour is used by the question engine, specifically by
34  * a {@link question_attempt} to manage the flow of actions a student can take
35  * as they work through a question, and later, as a teacher manually grades it.
36  * In turn, the behaviour will delegate certain processing to the
37  * relevant {@link question_definition}.
38  *
39  * @copyright  2009 The Open University
40  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
41  */
42 abstract class question_behaviour {
44     /** @var question_attempt the question attempt we are managing. */
45     protected $qa;
47     /** @var question_definition shortcut to $qa->get_question(). */
48     protected $question;
50     /**
51      * Normally you should not call this constuctor directly. The appropriate
52      * behaviour object is created automatically as part of
53      * {@link question_attempt::start()}.
54      * @param question_attempt $qa the question attempt we will be managing.
55      * @param string $preferredbehaviour the type of behaviour that was actually
56      *      requested. This information is not needed in most cases, the type of
57      *      subclass is enough, but occasionally it is needed.
58      */
59     public function __construct(question_attempt $qa, $preferredbehaviour) {
60         $this->qa = $qa;
61         $this->question = $qa->get_question();
62         if (!$this->is_compatible_question($this->question)) {
63             throw new coding_exception('This behaviour (' . $this->get_name() .
64                     ') cannot work with this question (' . get_class($this->question) . ')');
65         }
66     }
68     /**
69      * Some behaviours can only work with certing types of question. This method
70      * allows the behaviour to verify that a question is compatible.
71      *
72      * This implementation is only provided for backwards-compatibility. You should
73      * override this method if you are implementing a behaviour.
74      *
75      * @param question_definition $question the question.
76      */
77     public abstract function is_compatible_question(question_definition $question);
79     /**
80      * @return string the name of this behaviour. For example the name of
81      * qbehaviour_mymodle is 'mymodel'.
82      */
83     public function get_name() {
84         return substr(get_class($this), 11);
85     }
87     /**
88      * Whether the current attempt at this question could be completed just by the
89      * student interacting with the question, before $qa->finish() is called.
90      *
91      * @return boolean whether the attempt can finish naturally.
92      */
93     public function can_finish_during_attempt() {
94         return false;
95     }
97     /**
98      * Cause the question to be renderered. This gets the appropriate behaviour
99      * renderer using {@link get_renderer()}, and adjusts the display
100      * options using {@link adjust_display_options()} and then calls
101      * {@link core_question_renderer::question()} to do the work.
102      * @param question_display_options $options controls what should and should not be displayed.
103      * @param unknown_type $number the question number to display.
104      * @param core_question_renderer $qoutput the question renderer that will coordinate everything.
105      * @param qtype_renderer $qtoutput the question type renderer that will be helping.
106      * @return HTML fragment.
107      */
108     public function render(question_display_options $options, $number,
109             core_question_renderer $qoutput, qtype_renderer $qtoutput) {
110         $behaviouroutput = $this->get_renderer($qoutput->get_page());
111         $options = clone($options);
112         $this->adjust_display_options($options);
113         return $qoutput->question($this->qa, $behaviouroutput, $qtoutput, $options, $number);
114     }
116     /**
117      * Checks whether the users is allow to be served a particular file.
118      * @param question_display_options $options the options that control display of the question.
119      * @param string $component the name of the component we are serving files for.
120      * @param string $filearea the name of the file area.
121      * @param array $args the remaining bits of the file path.
122      * @param bool $forcedownload whether the user must be forced to download the file.
123      * @return bool true if the user can access this file.
124      */
125     public function check_file_access($options, $component, $filearea, $args, $forcedownload) {
126         $this->adjust_display_options($options);
128         if ($component == 'question' && $filearea == 'response_bf_comment') {
129             foreach ($this->qa->get_step_iterator() as $attemptstep) {
130                 if ($attemptstep->get_id() == $args[0]) {
131                     return true;
132                 }
133             }
135             return false;
136         }
138         return $this->question->check_file_access($this->qa, $options, $component,
139                 $filearea, $args, $forcedownload);
140     }
142     /**
143      * @param moodle_page $page the page to render for.
144      * @return qbehaviour_renderer get the appropriate renderer to use for this model.
145      */
146     public function get_renderer(moodle_page $page) {
147         return $page->get_renderer(get_class($this));
148     }
150     /**
151      * Make any changes to the display options before a question is rendered, so
152      * that it can be displayed in a way that is appropriate for the statue it is
153      * currently in. For example, by default, if the question is finished, we
154      * ensure that it is only ever displayed read-only.
155      * @param question_display_options $options the options to adjust. Just change
156      * the properties of this object - objects are passed by referece.
157      */
158     public function adjust_display_options(question_display_options $options) {
159         if (!$this->qa->has_marks()) {
160             $options->correctness = false;
161             $options->numpartscorrect = false;
162         }
163         if ($this->qa->get_state()->is_finished()) {
164             $options->readonly = true;
165             $options->numpartscorrect = $options->numpartscorrect &&
166                     $this->qa->get_state()->is_partially_correct() &&
167                     !empty($this->question->shownumcorrect);
168         } else {
169             $options->hide_all_feedback();
170         }
171     }
173     /**
174      * Get the most applicable hint for the question in its current state.
175      * @return question_hint the most applicable hint, or null, if none.
176      */
177     public function get_applicable_hint() {
178         return null;
179     }
181     /**
182      * What is the minimum fraction that can be scored for this question.
183      * Normally this will be based on $this->question->get_min_fraction(),
184      * but may be modified in some way by the behaviour.
185      *
186      * @return number the minimum fraction when this question is attempted under
187      * this behaviour.
188      */
189     public function get_min_fraction() {
190         return 0;
191     }
193     /**
194      * Return the maximum possible fraction that can be scored for this question.
195      * Normally this will be based on $this->question->get_max_fraction(),
196      * but may be modified in some way by the behaviour.
197      *
198      * @return number the maximum fraction when this question is attempted under
199      * this behaviour.
200      */
201     public function get_max_fraction() {
202         return $this->question->get_max_fraction();
203     }
205     /**
206      * Return an array of the behaviour variables that could be submitted
207      * as part of a question of this type, with their types, so they can be
208      * properly cleaned.
209      * @return array variable name => PARAM_... constant.
210      */
211     public function get_expected_data() {
212         if (!$this->qa->get_state()->is_finished()) {
213             return array();
214         }
216         $vars = array('comment' => question_attempt::PARAM_RAW_FILES, 'commentformat' => PARAM_INT);
217         if ($this->qa->get_max_mark()) {
218             $vars['mark'] = PARAM_RAW_TRIMMED;
219             $vars['maxmark'] = PARAM_FLOAT;
220         }
221         return $vars;
222     }
224     /**
225      * Return an array of question type variables for the question in its current
226      * state. Normally, if {@link adjust_display_options()} would set
227      * {@link question_display_options::$readonly} to true, then this method
228      * should return an empty array, otherwise it should return
229      * $this->question->get_expected_data(). Thus, there should be little need to
230      * override this method.
231      * @return array|string variable name => PARAM_... constant, or, as a special case
232      *      that should only be used in unavoidable, the constant question_attempt::USE_RAW_DATA
233      *      meaning take all the raw submitted data belonging to this question.
234      */
235     public function get_expected_qt_data() {
236         $fakeoptions = new question_display_options();
237         $fakeoptions->readonly = false;
238         $this->adjust_display_options($fakeoptions);
239         if ($fakeoptions->readonly) {
240             return array();
241         } else {
242             return $this->question->get_expected_data();
243         }
244     }
246     /**
247      * Return an array of any im variables, and the value required to get full
248      * marks.
249      * @return array variable name => value.
250      */
251     public function get_correct_response() {
252         return array();
253     }
255     /**
256      * Generate a brief, plain-text, summary of this question. This is used by
257      * various reports. This should show the particular variant of the question
258      * as presented to students. For example, the calculated quetsion type would
259      * fill in the particular numbers that were presented to the student.
260      * This method will return null if such a summary is not possible, or
261      * inappropriate.
262      *
263      * Normally, this method delegates to {question_definition::get_question_summary()}.
264      *
265      * @return string|null a plain text summary of this question.
266      */
267     public function get_question_summary() {
268         return $this->question->get_question_summary();
269     }
271     /**
272      * Generate a brief, plain-text, summary of the correct answer to this question.
273      * This is used by various reports, and can also be useful when testing.
274      * This method will return null if such a summary is not possible, or
275      * inappropriate.
276      *
277      * @return string|null a plain text summary of the right answer to this question.
278      */
279     public function get_right_answer_summary() {
280         return null;
281     }
283     /**
284      * Used by {@link start_based_on()} to get the data needed to start a new
285      * attempt from the point this attempt has go to.
286      * @return array name => value pairs.
287      */
288     public function get_resume_data() {
289         $olddata = $this->qa->get_step(0)->get_all_data();
290         $olddata = $this->qa->get_last_qt_data() + $olddata;
291         $olddata = $this->get_our_resume_data() + $olddata;
292         return $olddata;
293     }
295     /**
296      * Used by {@link start_based_on()} to get the data needed to start a new
297      * attempt from the point this attempt has go to.
298      * @return unknown_type
299      */
300     protected function get_our_resume_data() {
301         return array();
302     }
304     /**
305      * Classify responses for this question into a number of sub parts and response classes as defined by
306      * {@link \question_type::get_possible_responses} for this question type.
307      *
308      * @param string $whichtries         which tries to analyse for response analysis. Will be one of
309      *                                   question_attempt::FIRST_TRY, LAST_TRY or ALL_TRIES.
310      *                                   Defaults to question_attempt::LAST_TRY.
311      * @return (question_classified_response|array)[] If $whichtries is question_attempt::FIRST_TRY or LAST_TRY index is subpartid
312      *                                   and values are question_classified_response instances.
313      *                                   If $whichtries is question_attempt::ALL_TRIES then first key is submitted response no
314      *                                   and the second key is subpartid.
315      */
316     public function classify_response($whichtries = question_attempt::LAST_TRY) {
317         if ($whichtries == question_attempt::LAST_TRY) {
318             return $this->question->classify_response($this->qa->get_last_qt_data());
319         } else {
320             $stepswithsubmit = $this->qa->get_steps_with_submitted_response_iterator();
321             if ($whichtries == question_attempt::FIRST_TRY) {
322                 $firsttry = $stepswithsubmit[1];
323                 if ($firsttry) {
324                     return $this->question->classify_response($firsttry->get_qt_data());
325                 } else {
326                     return $this->question->classify_response(array());
327                 }
328             } else {
329                 $classifiedresponses = array();
330                 foreach ($stepswithsubmit as $submittedresponseno => $step) {
331                     $classifiedresponses[$submittedresponseno] = $this->question->classify_response($step->get_qt_data());
332                 }
333                 return $classifiedresponses;
334             }
335         }
336     }
338     /**
339      * Generate a brief textual description of the current state of the question,
340      * normally displayed under the question number.
341      *
342      * @param bool $showcorrectness Whether right/partial/wrong states should
343      * be distinguised.
344      * @return string a brief summary of the current state of the qestion attempt.
345      */
346     public function get_state_string($showcorrectness) {
347         return $this->qa->get_state()->default_string($showcorrectness);
348     }
350     public abstract function summarise_action(question_attempt_step $step);
352     /**
353      * Initialise the first step in a question attempt when a new
354      * {@link question_attempt} is being started.
355      *
356      * This method must call $this->question->start_attempt($step, $variant), and may
357      * perform additional processing if the behaviour requries it.
358      *
359      * @param question_attempt_step $step the first step of the
360      *      question_attempt being started.
361      * @param int $variant which variant of the question to use.
362      */
363     public function init_first_step(question_attempt_step $step, $variant) {
364         $this->question->start_attempt($step, $variant);
365         $step->set_state(question_state::$todo);
366     }
368     /**
369      * When an attempt is started based on a previous attempt (see
370      * {@link question_attempt::start_based_on}) this method is called to setup
371      * the new attempt.
372      *
373      * This method must call $this->question->apply_attempt_state($step), and may
374      * perform additional processing if the behaviour requries it.
375      *
376      * @param question_attempt_step The first step of the {@link question_attempt}
377      *      being loaded.
378      */
379     public function apply_attempt_state(question_attempt_step $step) {
380         $this->question->apply_attempt_state($step);
381         $step->set_state(question_state::$todo);
382     }
384     /**
385      * Checks whether two manual grading actions are the same. That is, whether
386      * the comment, and the mark (if given) is the same.
387      *
388      * @param question_attempt_step $pendingstep contains the new responses.
389      * @return bool whether the new response is the same as we already have.
390      */
391     protected function is_same_comment($pendingstep) {
392         $previouscomment = $this->qa->get_last_behaviour_var('comment');
393         $newcomment = $pendingstep->get_behaviour_var('comment');
395         // When the teacher leaves the comment empty, $previouscomment is an empty string but $newcomment is null,
396         // therefore they are not equal to each other. That's why checking if $previouscomment != $newcomment is not enough.
397         if (($previouscomment != $newcomment) && !(is_null($previouscomment) && html_is_blank($newcomment))) {
398             // The comment has changed.
399             return false;
400         }
402         if (!html_is_blank($newcomment)) {
403             // Check comment format.
404             $previouscommentformat = $this->qa->get_last_behaviour_var('commentformat');
405             $newcommentformat = $pendingstep->get_behaviour_var('commentformat');
406             if ($previouscommentformat != $newcommentformat) {
407                 return false;
408             }
409         }
411         // So, now we know the comment is the same, so check the mark, if present.
412         $previousfraction = $this->qa->get_fraction();
413         $newmark = question_utils::clean_param_mark($pendingstep->get_behaviour_var('mark'));
415         if (is_null($previousfraction)) {
416             return is_null($newmark) || $newmark === '';
417         } else if (is_null($newmark) || $newmark === '') {
418             return false;
419         }
421         $newfraction = $newmark / $pendingstep->get_behaviour_var('maxmark');
423         return abs($newfraction - $previousfraction) < 0.0000001;
424     }
426     /**
427      * The main entry point for processing an action.
428      *
429      * All the various operations that can be performed on a
430      * {@link question_attempt} get channeled through this function, except for
431      * {@link question_attempt::start()} which goes to {@link init_first_step()}.
432      * {@link question_attempt::finish()} becomes an action with im vars
433      * finish => 1, and manual comment/grade becomes an action with im vars
434      * comment => comment text, and mark => ..., max_mark => ... if the question
435      * is graded.
436      *
437      * This method should first determine whether the action is significant. For
438      * example, if no actual action is being performed, but instead the current
439      * responses are being saved, and there has been no change since the last
440      * set of responses that were saved, this the action is not significatn. In
441      * this case, this method should return {@link question_attempt::DISCARD}.
442      * Otherwise it should return {@link question_attempt::KEEP}.
443      *
444      * If the action is significant, this method should also perform any
445      * necessary updates to $pendingstep. For example, it should call
446      * {@link question_attempt_step::set_state()} to set the state that results
447      * from this action, and if this is a grading action, it should call
448      * {@link question_attempt_step::set_fraction()}.
449      *
450      * This method can also call {@link question_attempt_step::set_behaviour_var()} to
451      * store additional infomation. There are two main uses for this. This can
452      * be used to store the result of any randomisation done. It is important to
453      * store the result of randomisation once, and then in future use the same
454      * outcome if the actions are ever replayed. This is how regrading works.
455      * The other use is to cache the result of expensive computations performed
456      * on the raw response data, so that subsequent display and review of the
457      * question does not have to repeat the same expensive computations.
458      *
459      * Often this method is implemented as a dispatching method that examines
460      * the pending step to determine the kind of action being performed, and
461      * then calls a more specific method like {@link process_save()} or
462      * {@link process_comment()}. Look at some of the standard behaviours
463      * for examples.
464      *
465      * @param question_attempt_pending_step $pendingstep a partially initialised step
466      *      containing all the information about the action that is being peformed. This
467      *      information can be accessed using {@link question_attempt_step::get_behaviour_var()}.
468      * @return bool either {@link question_attempt::KEEP} or {@link question_attempt::DISCARD}
469      */
470     public abstract function process_action(question_attempt_pending_step $pendingstep);
472     /**
473      * Auto-saved data. By default this does nothing. interesting processing is
474      * done in {@link question_behaviour_with_save}.
475      *
476      * @param question_attempt_pending_step $pendingstep a partially initialised step
477      *      containing all the information about the action that is being peformed. This
478      *      information can be accessed using {@link question_attempt_step::get_behaviour_var()}.
479      * @return bool either {@link question_attempt::KEEP} or {@link question_attempt::DISCARD}
480      */
481     public function process_autosave(question_attempt_pending_step $pendingstep) {
482         return question_attempt::DISCARD;
483     }
485     /**
486      * Implementation of processing a manual comment/grade action that should
487      * be suitable for most subclasses.
488      * @param question_attempt_pending_step $pendingstep a partially initialised step
489      *      containing all the information about the action that is being peformed.
490      * @return bool either {@link question_attempt::KEEP}
491      */
492     public function process_comment(question_attempt_pending_step $pendingstep) {
493         if (!$this->qa->get_state()->is_finished()) {
494             throw new coding_exception('Cannot manually grade a question before it is finshed.');
495         }
497         if ($this->is_same_comment($pendingstep)) {
498             return question_attempt::DISCARD;
499         }
501         if ($pendingstep->has_behaviour_var('mark')) {
502             $mark = question_utils::clean_param_mark($pendingstep->get_behaviour_var('mark'));
503             if ($mark === null) {
504                 throw new coding_exception('Inalid number format ' . $pendingstep->get_behaviour_var('mark') .
505                         ' when processing a manual grading action.', 'Question ' . $this->question->id .
506                         ', slot ' . $this->qa->get_slot());
508             } else if ($mark === '') {
509                 $fraction = null;
511             } else {
512                 $fraction = $mark / $pendingstep->get_behaviour_var('maxmark');
513                 if ($fraction > $this->qa->get_max_fraction() || $fraction < $this->qa->get_min_fraction()) {
514                     throw new coding_exception('Score out of range when processing ' .
515                             'a manual grading action.', 'Question ' . $this->question->id .
516                             ', slot ' . $this->qa->get_slot() . ', fraction ' . $fraction);
517                 }
518             }
520             $pendingstep->set_fraction($fraction);
521         }
523         $pendingstep->set_state($this->qa->get_state()->corresponding_commented_state(
524                 $pendingstep->get_fraction()));
525         return question_attempt::KEEP;
526     }
528     /**
529      * @param $comment the comment text to format. If omitted,
530      *      $this->qa->get_manual_comment() is used.
531      * @param $commentformat the format of the comment, one of the FORMAT_... constants.
532      * @param $context the quiz context.
533      * @return string the comment, ready to be output.
534      */
535     public function format_comment($comment = null, $commentformat = null, $context = null) {
536         $formatoptions = new stdClass();
537         $formatoptions->noclean = true;
538         $formatoptions->para = false;
540         if (is_null($comment)) {
541             list($comment, $commentformat, $commentstep) = $this->qa->get_manual_comment();
542         }
544         if ($context !== null) {
545             $comment = $this->qa->rewrite_response_pluginfile_urls($comment, $context->id, 'bf_comment', $commentstep);
546         }
548         return format_text($comment, $commentformat, $formatoptions);
549     }
551     /**
552      * @return string a summary of a manual comment action.
553      * @param question_attempt_step $step
554      */
555     protected function summarise_manual_comment($step) {
556         $a = new stdClass();
557         if ($step->has_behaviour_var('comment')) {
558             $comment = question_utils::to_plain_text($step->get_behaviour_var('comment'),
559                     $step->get_behaviour_var('commentformat'));
560             $a->comment = shorten_text($comment, 200);
561         } else {
562             $a->comment = '';
563         }
565         $mark = question_utils::clean_param_mark($step->get_behaviour_var('mark'));
566         if (is_null($mark) || $mark === '') {
567             return get_string('commented', 'question', $a->comment);
568         } else {
569             $a->mark = $mark / $step->get_behaviour_var('maxmark') * $this->qa->get_max_mark();
570             return get_string('manuallygraded', 'question', $a);
571         }
572     }
574     public function summarise_start($step) {
575         return get_string('started', 'question');
576     }
578     public function summarise_finish($step) {
579         return get_string('attemptfinished', 'question');
580     }
582     /**
583      * Does this step include a response submitted by a student?
584      *
585      * This method should return true for any attempt explicitly submitted by a student. The question engine itself will also
586      * automatically recognise any last saved response before the attempt is finished, you don't need to return true here for these
587      * steps with responses which are not explicitly submitted by the student.
588      *
589      * @param question_attempt_step $step
590      * @return bool is this a step within a question attempt that includes a submitted response by a student.
591      */
592     public function step_has_a_submitted_response($step) {
593         return false;
594     }
598 /**
599  * A subclass of {@link question_behaviour} that implements a save
600  * action that is suitable for most questions that implement the
601  * {@link question_manually_gradable} interface.
602  *
603  * @copyright  2009 The Open University
604  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
605  */
606 abstract class question_behaviour_with_save extends question_behaviour {
607     public function required_question_definition_type() {
608         return 'question_manually_gradable';
609     }
611     public function apply_attempt_state(question_attempt_step $step) {
612         parent::apply_attempt_state($step);
613         if ($this->question->is_complete_response($step->get_qt_data())) {
614             $step->set_state(question_state::$complete);
615         }
616     }
618     /**
619      * Work out whether the response in $pendingstep are significantly different
620      * from the last set of responses we have stored.
621      * @param question_attempt_step $pendingstep contains the new responses.
622      * @return bool whether the new response is the same as we already have.
623      */
624     protected function is_same_response(question_attempt_step $pendingstep) {
625         return $this->question->is_same_response(
626                 $this->qa->get_last_step()->get_qt_data(), $pendingstep->get_qt_data());
627     }
629     /**
630      * Work out whether the response in $pendingstep represent a complete answer
631      * to the question. Normally this will call
632      * {@link question_manually_gradable::is_complete_response}, but some
633      * behaviours, for example the CBM ones, have their own parts to the
634      * response.
635      * @param question_attempt_step $pendingstep contains the new responses.
636      * @return bool whether the new response is complete.
637      */
638     protected function is_complete_response(question_attempt_step $pendingstep) {
639         return $this->question->is_complete_response($pendingstep->get_qt_data());
640     }
642     public function process_autosave(question_attempt_pending_step $pendingstep) {
643         // If already finished. Nothing to do.
644         if ($this->qa->get_state()->is_finished()) {
645             return question_attempt::DISCARD;
646         }
648         // If the new data is the same as we already have, then we don't need it.
649         if ($this->is_same_response($pendingstep)) {
650             return question_attempt::DISCARD;
651         }
653         // Repeat that test discarding any existing autosaved data.
654         if ($this->qa->has_autosaved_step()) {
655             $this->qa->discard_autosaved_step();
656             if ($this->is_same_response($pendingstep)) {
657                 return question_attempt::DISCARD;
658             }
659         }
661         // OK, we need to save.
662         return $this->process_save($pendingstep);
663     }
665     /**
666      * Implementation of processing a save action that should be suitable for
667      * most subclasses.
668      * @param question_attempt_pending_step $pendingstep a partially initialised step
669      *      containing all the information about the action that is being peformed.
670      * @return bool either {@link question_attempt::KEEP} or {@link question_attempt::DISCARD}
671      */
672     public function process_save(question_attempt_pending_step $pendingstep) {
673         if ($this->qa->get_state()->is_finished()) {
674             return question_attempt::DISCARD;
675         } else if (!$this->qa->get_state()->is_active()) {
676             throw new coding_exception('Question is not active, cannot process_actions.');
677         }
679         if ($this->is_same_response($pendingstep)) {
680             return question_attempt::DISCARD;
681         }
683         if ($this->is_complete_response($pendingstep)) {
684             $pendingstep->set_state(question_state::$complete);
685         } else {
686             $pendingstep->set_state(question_state::$todo);
687         }
688         return question_attempt::KEEP;
689     }
691     public function summarise_submit(question_attempt_step $step) {
692         return get_string('submitted', 'question',
693                 $this->question->summarise_response($step->get_qt_data()));
694     }
696     public function summarise_save(question_attempt_step $step) {
697         $data = $step->get_submitted_data();
698         if (empty($data)) {
699             return $this->summarise_start($step);
700         }
701         return get_string('saved', 'question',
702                 $this->question->summarise_response($step->get_qt_data()));
703     }
706     public function summarise_finish($step) {
707         $data = $step->get_qt_data();
708         if ($data) {
709             return get_string('attemptfinishedsubmitting', 'question',
710                     $this->question->summarise_response($data));
711         }
712         return get_string('attemptfinished', 'question');
713     }
716 abstract class question_behaviour_with_multiple_tries extends question_behaviour_with_save {
717     public function step_has_a_submitted_response($step) {
718         return $step->has_behaviour_var('submit') && $step->get_state() != question_state::$invalid;
719     }
722 /**
723  * This helper class contains the constants and methods required for
724  * manipulating scores for certainty based marking.
725  *
726  * @copyright  2009 The Open University
727  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
728  */
729 abstract class question_cbm {
730     /**#@+ @var integer named constants for the certainty levels. */
731     const LOW = 1;
732     const MED = 2;
733     const HIGH = 3;
734     /**#@-*/
736     /** @var array list of all the certainty levels. */
737     public static $certainties = array(self::LOW, self::MED, self::HIGH);
739     /**#@+ @var array coefficients used to adjust the fraction based on certainty. */
740     protected static $rightscore = array(
741         self::LOW  => 1,
742         self::MED  => 2,
743         self::HIGH => 3,
744     );
745     protected static $wrongscore = array(
746         self::LOW  =>  0,
747         self::MED  => -2,
748         self::HIGH => -6,
749     );
750     /**#@-*/
752     /**#@+ @var array upper and lower limits of the optimal window. */
753     protected static $lowlimit = array(
754         self::LOW  => 0,
755         self::MED  => 0.666666666666667,
756         self::HIGH => 0.8,
757     );
758     protected static $highlimit = array(
759         self::LOW  => 0.666666666666667,
760         self::MED  => 0.8,
761         self::HIGH => 1,
762     );
763     /**#@-*/
765     /**
766      * @return int the default certaintly level that should be assuemd if
767      * the student does not choose one.
768      */
769     public static function default_certainty() {
770         return self::LOW;
771     }
773     /**
774      * Given a fraction, and a certainty, compute the adjusted fraction.
775      * @param number $fraction the raw fraction for this question.
776      * @param int $certainty one of the certainty level constants.
777      * @return number the adjusted fraction taking the certainty into account.
778      */
779     public static function adjust_fraction($fraction, $certainty) {
780         if ($certainty == -1) {
781             // Certainty -1 has never been used in standard Moodle, but is
782             // used in Tony-Gardiner Medwin's patches to mean 'No idea' which
783             // we intend to implement: MDL-42077. In the mean time, avoid
784             // errors for people who have used TGM's patches.
785             return 0;
786         }
787         if ($fraction <= 0.00000005) {
788             return self::$wrongscore[$certainty];
789         } else {
790             return self::$rightscore[$certainty] * $fraction;
791         }
792     }
794     /**
795      * @param int $certainty one of the LOW/MED/HIGH constants.
796      * @return string a textual description of this certainty.
797      */
798     public static function get_string($certainty) {
799         return get_string('certainty' . $certainty, 'qbehaviour_deferredcbm');
800     }
802     /**
803      * @param int $certainty one of the LOW/MED/HIGH constants.
804      * @return string a short textual description of this certainty.
805      */
806     public static function get_short_string($certainty) {
807         return get_string('certaintyshort' . $certainty, 'qbehaviour_deferredcbm');
808     }
810     /**
811      * Add information about certainty to a response summary.
812      * @param string $summary the response summary.
813      * @param int $certainty the level of certainty to add.
814      * @return string the summary with information about the certainty added.
815      */
816     public static function summary_with_certainty($summary, $certainty) {
817         if (is_null($certainty)) {
818             return $summary;
819         }
820         return $summary . ' [' . self::get_short_string($certainty) . ']';
821     }
823     /**
824      * @param int $certainty one of the LOW/MED/HIGH constants.
825      * @return float the lower limit of the optimal probability range for this certainty.
826      */
827     public static function optimal_probablility_low($certainty) {
828         return self::$lowlimit[$certainty];
829     }
831     /**
832      * @param int $certainty one of the LOW/MED/HIGH constants.
833      * @return float the upper limit of the optimal probability range for this certainty.
834      */
835     public static function optimal_probablility_high($certainty) {
836         return self::$highlimit[$certainty];
837     }