diff --git a/renderer.php b/renderer.php
index 20cca9be..c2b5f3d4 100644
--- a/renderer.php
+++ b/renderer.php
@@ -22,7 +22,6 @@
* @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-
/**
* Base class for generating the bits of output for formulas questions.
*
@@ -31,11 +30,17 @@
*/
class qtype_formulas_renderer extends qtype_with_combined_feedback_renderer {
+ /** @var string */
+ const UNIT_FIELD = 'u';
+
+ /** @var string */
+ const COMBINED_FIELD = '';
+
/**
- * Generate the display of the formulation part of the question. This is the
- * area that contains the question text, and the controls for students to
- * input their answers. Once the question is answered, it will contain the green tick
- * or the red cross and the part's general / combined feedback.
+ * Generate the display of the formulation part of the question. This is the area that
+ * contains the question text and the controls for students to input their answers.
+ * Once the question is answered, it will contain the green tick or the red cross and
+ * the part's general / combined feedback.
*
* @param question_attempt $qa the question attempt to display.
* @param question_display_options $options controls what should and should not be displayed.
@@ -52,28 +57,23 @@ public function formulation_and_controls(question_attempt $qa, question_display_
}
$questiontext = '';
+ // First, iterate over all parts, put the corresponding fragment of the main question text at the
+ // right position, followed by the part's text, input and (if applicable) feedback elements.
foreach ($question->parts as $part) {
$questiontext .= $question->format_text(
- $question->textfragments[$part->partindex],
- $question->questiontextformat,
- $qa,
- 'question',
- 'questiontext',
- $question->id,
- false
+ $question->textfragments[$part->partindex], $question->questiontextformat,
+ $qa, 'question', 'questiontext', $question->id, false
);
$questiontext .= $this->part_formulation_and_controls($qa, $options, $part);
}
+ // All parts are done. We now append the final fragment of the main question text. Note that this fragment
+ // might be empty.
$questiontext .= $question->format_text(
- $question->textfragments[$question->numparts],
- $question->questiontextformat,
- $qa,
- 'question',
- 'questiontext',
- $question->id,
- false
+ end($question->textfragments), $question->questiontextformat, $qa, 'question', 'questiontext', $question->id, false
);
+ // Pack everything in a
and, if the question is in an invalid state, append the appropriate error message
+ // at the very end.
$result = html_writer::tag('div', $questiontext, ['class' => 'qtext']);
if ($qa->get_state() == question_state::$invalid) {
$result .= html_writer::nonempty_tag(
@@ -82,6 +82,7 @@ public function formulation_and_controls(question_attempt $qa, question_display_
['class' => 'validationerror']
);
}
+
return $result;
}
@@ -92,7 +93,7 @@ public function formulation_and_controls(question_attempt $qa, question_display_
* @param question_attempt $qa question attempt that will be displayed on the page
* @return string HTML fragment
*/
- public function head_code(question_attempt $qa) {
+ public function head_code(question_attempt $qa): string {
global $CFG;
$this->page->requires->js_call_amd('qtype_formulas/answervalidation', 'init');
@@ -109,91 +110,82 @@ public function head_code(question_attempt $qa) {
* Return the part text, controls, grading details and feedbacks.
*
* @param question_attempt $qa question attempt that will be displayed on the page
- * @param question_display_options $options
- * @param qtype_formulas_part $part
+ * @param question_display_options $options controls what should and should not be displayed
+ * @param qtype_formulas_part $part question part
* @return void
*/
public function part_formulation_and_controls(question_attempt $qa, question_display_options $options,
- qtype_formulas_part $part) {
+ qtype_formulas_part $part): string {
+ // The behaviour might change the display options per part, so it is safer to clone them here.
$partoptions = clone $options;
- // If using adaptivemultipart behaviour, adjust feedback display options for this part.
if ($qa->get_behaviour_name() === 'adaptivemultipart') {
$qa->get_behaviour()->adjust_display_options_for_part($part->partindex, $partoptions);
}
- $sub = $this->get_part_image_and_class($qa, $partoptions, $part);
- $output = $this->get_part_formulation(
- $qa,
- $partoptions,
- $part->partindex,
- $sub
- );
- // Place for the right/wrong feeback image or appended at part's end.
- // TODO: this is not documented anywhere.
+ // Fetch information about the outcome: grade, feedback symbol, CSS class to be used.
+ $outcomedata = $this->get_part_feedback_class_and_symbol($qa, $partoptions, $part);
+
+ // First of all, we take the part's question text and its input fields.
+ $output = $this->get_part_formulation($qa, $partoptions, $part, $outcomedata);
+
+ // If the user has requested the feedback symbol to be placed at a special position, we
+ // do that now. Otherwise, we just append it after the part's text and input boxes.
if (strpos($output, '{_m}') !== false) {
- $output = str_replace('{_m}', $sub->feedbackimage, $output);
+ $output = str_replace('{_m}', $outcomedata->feedbacksymbol, $output);
} else {
- $output .= $sub->feedbackimage;
+ $output .= $outcomedata->feedbacksymbol;
}
- $feedback = $this->part_combined_feedback($qa, $partoptions, $part, $sub->fraction);
+ // The part's feedback consists of the combined feedback (correct, partially correct, incorrect -- depending on the
+ // outcome) and the general feedback which is given in all cases.
+ $feedback = $this->part_combined_feedback($qa, $partoptions, $part, $outcomedata->fraction);
$feedback .= $this->part_general_feedback($qa, $partoptions, $part);
- // If one of the part's coordinates is a MC or select question, the correct answer
- // stored in the database is not the right answer, but the index of the right answer,
- // so in that case, we need to calculate the right answer.
+
+ // If requested, the correct answer should be appended to the feedback.
if ($partoptions->rightanswer) {
- $feedback .= $this->part_correct_response($part->partindex, $qa);
+ $feedback .= $this->part_correct_response($part);
}
- $output .= html_writer::nonempty_tag(
- 'div',
- $feedback,
- ['class' => 'formulaspartoutcome']
- );
+
+ // Put all feedback into a
with the appropriate CSS class and append it to the output.
+ $output .= html_writer::nonempty_tag('div', $feedback, ['class' => 'formulaspartoutcome outcome']);
+
return html_writer::tag('div', $output , ['class' => 'formulaspart']);
}
/**
- * Return class and image for the part feedback.
+ * Return class and symbol for the part feedback.
*
- * @param question_attempt $qa
- * @param question_display_options $options
- * @param qtype_formulas_part $part
- * @return object
+ * @param question_attempt $qa question attempt that will be displayed on the page
+ * @param question_display_options $options controls what should and should not be displayed
+ * @param qtype_formulas_part $part question part
+ * @return stdClass
*/
- public function get_part_image_and_class($qa, $options, $part) {
- $question = $qa->get_question();
-
- $sub = new StdClass;
+ public function get_part_feedback_class_and_symbol(question_attempt $qa, question_display_options $options,
+ qtype_formulas_part $part): stdClass {
+ // Prepare a new object to hold the different elements.
+ $result = new stdClass;
+ // Fetch the last response data and grade it.
$response = $qa->get_last_qt_data();
- $response = $question->normalize_response($response);
-
list('answer' => $answergrade, 'unit' => $unitcorrect) = $part->grade($response);
- $sub->fraction = $answergrade;
+ // The fraction will later be used to determine which feedback (correct, partially correct or incorrect)
+ // to use. We have to take into account a possible deduction for a wrong unit.
+ $result->fraction = $answergrade;
if ($unitcorrect === false) {
- $sub->fraction *= (1 - $part->unitpenalty);
+ $result->fraction *= (1 - $part->unitpenalty);
}
- // Get the class and image for the feedback.
+ // By default, we add no feedback at all...
+ $result->feedbacksymbol = '';
+ $result->feedbackclass = '';
+ // ... unless correctness is requested in the display options.
if ($options->correctness) {
- $sub->feedbackimage = $this->feedback_image($sub->fraction);
- $sub->feedbackclass = $this->feedback_class($sub->fraction);
- if ($part->unitpenalty >= 1) { // All boxes must be correct at the same time, so they are of the same color.
- $sub->unitfeedbackclass = $sub->feedbackclass;
- $sub->boxfeedbackclass = $sub->feedbackclass;
- } else { // Show individual color, all four color combinations are possible.
- $sub->unitfeedbackclass = $this->feedback_class($unitcorrect);
- $sub->boxfeedbackclass = $this->feedback_class($answergrade);
- }
- } else { // There should be no feedback if options->correctness is not set for this part.
- $sub->feedbackimage = '';
- $sub->feedbackclass = '';
- $sub->unitfeedbackclass = '';
- $sub->boxfeedbackclass = '';
+ $result->feedbacksymbol = $this->feedback_image($result->fraction);
+ $result->feedbackclass = $this->feedback_class($result->fraction);
}
- return $sub;
+ return $result;
}
/**
@@ -203,7 +195,7 @@ public function get_part_image_and_class($qa, $options, $part) {
* @param string $style style to render the number in, acccording to {@see qtype_multichoice::get_numbering_styles()}
* @return string number $num in the requested style
*/
- protected function number_in_style($num, $style) {
+ protected static function number_in_style(int $num, string $style): string {
switch ($style) {
case 'abc':
$number = chr(ord('a') + $num);
@@ -229,19 +221,311 @@ protected function number_in_style($num, $style) {
return $number . '. ';
}
+ /**
+ * Create a set of radio boxes for a multiple choice answer input.
+ *
+ * @param qtype_formulas_part $part question part
+ * @param int|string $answerindex index of the answer (starting at 0) or special value for combined/separate unit field
+ * @param question_attempt $qa question attempt that will be displayed on the page
+ * @param array $answeroptions array of strings containing the answer options to choose from
+ * @param question_display_options $displayoptions controls what should and should not be displayed
+ * @param string $feedbackclass
+ * @return string HTML fragment
+ */
+ protected function create_radio_mc_answer(qtype_formulas_part $part, $answerindex, question_attempt $qa,
+ array $answeroptions, question_display_options $displayoptions, string $feedbackclass = ''): string {
+ /** @var qype_formulas_question $question */
+ $question = $qa->get_question();
+
+ $variablename = "{$part->partindex}_{$answerindex}";
+ $currentanswer = $qa->get_last_qt_var($variablename);
+ $inputname = $qa->get_qt_field_name($variablename);
+
+ $inputattributes['type'] = 'radio';
+ $inputattributes['name'] = $inputname;
+ if ($displayoptions->readonly) {
+ $inputattributes['disabled'] = 'disabled';
+ }
+
+ // First, we open a