Skip to content
Snippets Groups Projects
renderer.php 23.3 KiB
Newer Older
  • Learn to ignore specific revisions
  • // This file is part of Stack - http://stack.maths.ed.ac.uk/
    
    // Stack 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.
    //
    
    // Stack 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 Stack.  If not, see <http://www.gnu.org/licenses/>.
    
    defined('MOODLE_INTERNAL') || die();
    
    
    /**
     * Stack question renderer class.
     *
    
     * @package   qtype_stack
     * @copyright 2012 The Open University
     * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
    
    require_once(__DIR__ . '/vle_specific.php');
    
    
    /**
     * Generates the output for Stack questions.
     *
    
     * @copyright 2012 The Open University
     * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
    
     */
    class qtype_stack_renderer extends qtype_renderer {
    
        public function formulation_and_controls(question_attempt $qa, question_display_options $options) {
    
    Chris Sangwin's avatar
    Chris Sangwin committed
            /* Return type should be @var qtype_stack_question $question. */
    
            $question = $qa->get_question();
    
    
            $response = $qa->get_last_qt_data();
    
    Chris Sangwin's avatar
    Chris Sangwin committed
            // We need to provide a processor for the CASText2 post-processing,
    
            // basically for targetting pluginfiles.
            $question->castextprocessor = new castext2_qa_processor($qa);
    
            if (is_string($question->questiontextinstantiated)) {
                // The question has not been instantiated successfully, at this level it is likely
                // a failure at compilation and that means invalid teacher code.
                return $question->questiontextinstantiated;
            }
    
            $questiontext = $question->questiontextinstantiated->get_rendered($question->castextprocessor);
    
    
            // Replace inputs.
    
            $inputstovaldiate = array();
    
    
            // Get the list of placeholders before format_text.
    
            $originalinputplaceholders = array_unique(stack_utils::extract_placeholders($questiontext, 'input'));
            sort($originalinputplaceholders);
            $originalfeedbackplaceholders = array_unique(stack_utils::extract_placeholders($questiontext, 'feedback'));
            sort($originalfeedbackplaceholders);
    
    
            // Now format the questiontext.
            $questiontext = $question->format_text(
                    stack_maths::process_display_castext($questiontext, $this),
    
                    FORMAT_HTML, // All CASText2 processed content has already been formatted to HTML.
    
                    $qa, 'question', 'questiontext', $question->id);
    
            // Get the list of placeholders after format_text.
            $formatedinputplaceholders = stack_utils::extract_placeholders($questiontext, 'input');
    
            $formatedfeedbackplaceholders = stack_utils::extract_placeholders($questiontext, 'feedback');
    
    
            // We need to check that if the list has changed.
    
            // Have we lost some of the placeholders entirely?
            // Duplicates may have been removed by multi-lang,
            // No duplicates should remain.
    
            if ($formatedinputplaceholders !== $originalinputplaceholders ||
                    $formatedfeedbackplaceholders !== $originalfeedbackplaceholders) {
    
                throw new coding_exception('Inconsistent placeholders. Possibly due to multi-lang filtter not being active.');
    
            foreach ($question->inputs as $name => $input) {
    
                // Get the actual value of the teacher's answer at this point.
    
                $tavalue = $question->get_ta_for_input($name);
    
                $fieldname = $qa->get_qt_field_name($name);
    
                $state = $question->get_input_state($name, $response);
    
    Tim Hunt's avatar
    Tim Hunt committed
                $questiontext = str_replace("[[input:{$name}]]",
    
                        $input->render($state, $fieldname, $options->readonly, $tavalue),
    
                $questiontext = $input->replace_validation_tags($state, $fieldname, $questiontext);
    
                if ($input->requires_validation()) {
                    $inputstovaldiate[] = $name;
    
    Chris Sangwin's avatar
    Chris Sangwin committed
            // Replace PRTs.
            foreach ($question->prts as $index => $prt) {
                $feedback = '';
                if ($options->feedback) {
    
                    $feedback = $this->prt_feedback($index, $response, $qa, $options, $prt->get_feedbackstyle());
    
    Chris Sangwin's avatar
    Chris Sangwin committed
                } else if (in_array($qa->get_behaviour_name(), array('interactivecountback', 'adaptivemulipart'))) {
                    // The behaviour name test here is a hack. The trouble is that interactive
                    // behaviour or adaptivemulipart does not show feedback if the input
                    // is invalid, but we want to show the CAS errors from the PRT.
                    $result = $question->get_prt_result($index, $response, $qa->get_state()->is_finished());
    
                    $errors = implode(' ', $result->get_errors());
                    $feedback = html_writer::nonempty_tag('span', $errors,
    
    Chris Sangwin's avatar
    Chris Sangwin committed
                            array('class' => 'stackprtfeedback stackprtfeedback-' . $name));
                }
                $questiontext = str_replace("[[feedback:{$index}]]", $feedback, $questiontext);
            }
    
            // Initialise automatic validation, if enabled.
    
            if (stack_utils::get_config()->ajaxvalidation) {
    
    Chris Sangwin's avatar
    Chris Sangwin committed
                // Once we cen rely on everyone being on a Moodle version that includes the fix for
                // MDL-65029 (3.5.6+, 3.6.4+, 3.7+) we can remove this if and just call the method.
    
                if (method_exists($qa, 'get_outer_question_div_unique_id')) {
                    $questiondivid = $qa->get_outer_question_div_unique_id();
                } else {
                    $questiondivid = 'q' . $qa->get_slot();
                }
    
                $this->page->requires->js_call_amd('qtype_stack/input', 'initInputs',
    
                        [$questiondivid, $qa->get_field_prefix(),
    
                                $qa->get_database_id(), $inputstovaldiate]);
    
    Tim Hunt's avatar
    Tim Hunt committed
            $result .= $this->question_tests_link($question, $options) . $questiontext;
    
    
            if ($qa->get_state() == question_state::$invalid) {
    
                $result .= html_writer::nonempty_tag('span',
    
                        $question->get_validation_error($response),
                        array('class' => 'validationerror'));
            }
    
            return $result;
    
        /**
         * Displays a link to run the question tests, if applicable.
         * @param qtype_stack_question $question
         * @param question_display_options $options
         * @return string HTML fragment.
         */
        protected function question_tests_link(qtype_stack_question $question, question_display_options $options) {
    
    Tim Hunt's avatar
    Tim Hunt committed
            if (!empty($options->suppressruntestslink)) {
    
            if (!stack_user_can_view_question($question)) {
    
            $urlparams = array('questionid' => $question->id);
            $urlparams['seed'] = $question->seed;
    
    
            // Quite honestly fellow developers I'm getting fed up of fixing live questions written by colleagues!
            // Especially when the questions do not have tests or deployed variants which would have revealed the problem.
            // Make these problems more obvious to authors, who don't yet understand what tests/variants are for.
            // Alert a teacher to questions without tests or deployed variants.
            $testscases = question_bank::get_qtype('stack')->load_question_tests($question->id);
    
            $links = array();
    
    Chris Sangwin's avatar
    Chris Sangwin committed
            if (($question->has_random_variants() && count($question->deployedseeds) == 0) ||
    
                count($testscases) == 0) {
                $links[] = html_writer::link(
                    $question->qtype->get_question_test_url($question),
    
                    stack_string_error('runquestiontests_alert'));
            } else {
                $links[] = html_writer::link(
                        $question->qtype->get_question_test_url($question),
                        stack_string('runquestiontests'));
    
            return html_writer::tag('div', implode(' | ', $links), array('class' => 'questiontestslink'));
    
        public function feedback(question_attempt $qa, question_display_options $options) {
    
            $output = '';
            if ($options->feedback) {
                $output .= $this->stack_specific_feedback($qa, $options);
    
            } else if ($qa->get_behaviour_name() == 'interactivecountback') {
                // The behaviour name test here is a hack. The trouble is that interactive
                // behaviour does not show feedback if the input is invalid, but we want
                // to show the CAS errors from the PRT.
                $output .= $this->stack_specific_feedback_errors_only($qa);
            }
    
            $output .= parent::feedback($qa, $options);
    
            return $output;
        }
    
        protected function stack_specific_feedback_errors_only(question_attempt $qa) {
            $question = $qa->get_question();
            $response = $qa->get_last_qt_data();
    
            // If called out of order.
            if ($question->castextprocessor === null) {
                $question->castextprocessor = new castext2_qa_processor($qa);
            }
    
    Chris Sangwin's avatar
    Chris Sangwin committed
    
    
            if ($question->specificfeedbackinstantiated === null) {
                // Invalid question, otherwise this would be here.
                return '';
            }
    
            $feedbacktext = $question->specificfeedbackinstantiated->get_rendered($question->castextprocessor);
    
            $feedbacktext = stack_maths::process_display_castext($feedbacktext, $this);
    
    Chris Sangwin's avatar
    Chris Sangwin committed
            $feedbacktext = $question->format_text($feedbacktext,
    
                    FORMAT_HTML, // All CASText2 processed content has already been formatted to HTML.
    
    Tim Hunt's avatar
    Tim Hunt committed
                    $qa, 'qtype_stack', 'specificfeedback', $question->id);
    
    
            // Replace any PRT feedback.
            $allempty = true;
            foreach ($question->prts as $name => $prt) {
                $feedback = '';
                $result = $question->get_prt_result($name, $response, $qa->get_state()->is_finished());
    
                if ($result->get_errors() != array()) {
                    $errors = implode(' ', $result->get_errors());
                    $feedback = html_writer::nonempty_tag('span', $errors,
    
                            array('class' => 'stackprtfeedback stackprtfeedback-' . $name));
                }
                $allempty = $allempty && !$feedback;
                $feedbacktext = str_replace("[[feedback:{$name}]]", $feedback, $feedbacktext);
            }
    
            if ($allempty) {
                return '';
            }
    
    
    Tim Hunt's avatar
    Tim Hunt committed
            return $feedbacktext;
    
        }
    
        /**
         * Generate the specific feedback. This has to be a stack-specific method
         * since the standard specific_feedback method does not get given $options.
         * @param question_attempt $qa the question attempt to display.
         * @param question_display_options $options controls what should and should not be displayed.
         * @return string HTML fragment.
         */
        protected function stack_specific_feedback(question_attempt $qa, question_display_options $options) {
    
    
            $question = $qa->get_question();
            $response = $qa->get_last_qt_data();
    
            // If called out of order.
            if ($question->castextprocessor === null) {
                $question->castextprocessor = new castext2_qa_processor($qa);
            }
            if ($question->specificfeedbackinstantiated === null) {
                // Invalid question, otherwise this would be here.
                return '';
            }
            $feedbacktext = $question->specificfeedbackinstantiated->get_rendered($question->castextprocessor);
    
            if (!$feedbacktext) {
                return '';
            }
    
    
            $feedbacktext = stack_maths::process_display_castext($feedbacktext, $this);
    
    Chris Sangwin's avatar
    Chris Sangwin committed
            $feedbacktext = $question->format_text($feedbacktext,
    
                    FORMAT_HTML, // All CASText2 processed content has already been formatted to HTML.
    
    Tim Hunt's avatar
    Tim Hunt committed
                    $qa, 'qtype_stack', 'specificfeedback', $question->id);
    
    
            $individualfeedback = count($question->prts) == 1;
            if ($individualfeedback) {
                $overallfeedback = '';
            } else {
                $overallfeedback = $this->overall_standard_prt_feedback($qa, $question, $response);
            }
    
    
            // Replace any PRT feedback.
    
            foreach ($question->prts as $index => $prt) {
    
                $feedback = $this->prt_feedback($index, $response, $qa, $options, $prt->get_feedbackstyle());
    
                $allempty = $allempty && !$feedback;
    
                $feedbacktext = str_replace("[[feedback:{$index}]]",
    
                        stack_maths::process_display_castext($feedback, $this), $feedbacktext);
    
            if ($allempty && !$overallfeedback) {
    
            return $overallfeedback . $feedbacktext;
    
        /**
         * Get the appropriate response to use for generating the feedback to a PRT.
         * @param string $name PRT name
    
    Tim Hunt's avatar
    Tim Hunt committed
         * @param array $response the current response.
    
         * @param question_attempt $qa the question_attempt we are displaying.
         */
        protected function get_applicable_response_for_prt($name, $response, question_attempt $qa) {
            if ($qa->get_behaviour_name() != 'adaptivemultipart') {
                return $response;
            }
    
            // The behaviour name test above is a hack. The trouble is that
            // for adaptive behaviour, exactly what feedback to display is
            // is complex, so we need to ask the behaviour.
            $step = $qa->get_behaviour()->get_last_graded_response_step_for_part($name);
    
            if (!$step) {
                return null;
            }
    
            $lastgradedresponse = $step->get_qt_data();
            if (!$qa->get_question()->is_same_response_for_part($name, $lastgradedresponse, $response)) {
                return null;
            }
    
            return $response;
        }
    
    
    Chris Sangwin's avatar
    Chris Sangwin committed
         * Slightly complex rules for what feedback to display.
    
         * @param string $name the PRT name.
         * @param array $response the most recent student response.
         * @param question_attempt $qa the question attempt to display.
         * @param question_display_options $options controls what should and should not be displayed.
    
         * @param int feedbackstyle whether and how to include the standard.
    
         *      'Your answer is partially correct' bit at the start of the feedback.
    
         * @return string nicely formatted feedback, for display.
         */
    
        protected function prt_feedback($name, $response, question_attempt $qa,
    
                question_display_options $options, int $feedbackstyle) {
    
            $question = $qa->get_question();
    
    
            $relevantresponse = $this->get_applicable_response_for_prt($name, $response, $qa);
            if (is_null($relevantresponse)) {
                return '';
    
            $result = $question->get_prt_result($name, $relevantresponse, $qa->get_state()->is_finished());
    
            return $this->prt_feedback_display($name, $qa, $question, $result, $options, $feedbackstyle);
    
        }
    
        /**
         * Actually generate the display of the PRT feedback.
    
         * @param string $name the PRT name.
         * @param question_attempt $qa the question attempt to display.
         * @param question_definition $question the question being displayed.
    
         * @param prt_evaluatable $result the results to display.
    
         * @param question_display_options $options controls what should and should not be displayed.
    
         * @param feedbackstyle styles the type of feedback.
    
         * @return string nicely formatted feedback, for display.
    
        protected function prt_feedback_display($name, question_attempt $qa,
    
                question_definition $question, prt_evaluatable $result,
    
                question_display_options $options, $feedbackstyle) {
    
            if ($result->get_errors()) {
    
                $err = stack_string('prtruntimeerror',
                    array('prt' => $name, 'error' => implode(' ', $result->get_errors())));
    
            $feedback = $result->get_feedback(new castext2_qa_processor($qa));
            // The feedback does not come as bits anymore the whole thing is concatenated in CAS
            // and CASText converts any formats to HTML already, pluginfiles as well.
            $feedback = format_text(stack_maths::process_display_castext($feedback, $this),
                FORMAT_HTML, array('noclean' => true, 'para' => false));
    
            if ($result->get_valid() && $qa->get_behaviour_name() == 'adaptivemultipart'
    
    Chris Sangwin's avatar
    Chris Sangwin committed
                    && $options->marks >= question_display_options::MARK_AND_MAX) {
    
                $renderer = $this->page->get_renderer('qbehaviour_adaptivemultipart');
                $gradingdetails = $renderer->render_adaptive_marks(
    
    Chris Sangwin's avatar
    Chris Sangwin committed
                    $qa->get_behaviour()->get_part_mark_details($name), $options);
    
            $standardfeedback = $this->standard_prt_feedback($qa, $question, $result, $feedbackstyle);
    
            switch ($feedbackstyle) {
    
                case 0:
                    // Formative PRT.
                    $fb = $err . $feedback;
                    break;
                case 1:
                    $fb = $standardfeedback . $err . $feedback . $gradingdetails;
                    break;
                case 2:
                    // Compact.
                    $fb = $standardfeedback . $err . $feedback;
                    $tag = 'span';
                    break;
                case 3:
                    // Symbolic.
                    $fb = $standardfeedback . $err;
                    $tag = 'span';
                    break;
                default:
                    echo "i is not equal to 0, 1 or 2";
    
    
            return html_writer::nonempty_tag($tag, $fb, array('class' => 'stackprtfeedback stackprtfeedback-' . $name));
    
    Chris Sangwin's avatar
    Chris Sangwin committed
         * Generate the standard PRT feedback for a particular score.
    
         * @param question_attempt $qa the question attempt to display.
         * @param question_definition $question the question being displayed.
    
         * @param prt_evaluatable $result the results to display.
    
         * @param feedbackstyle styles the type of feedback.
    
         * @return string nicely standard feedback, for display.
         */
    
        protected function standard_prt_feedback(question_attempt $qa, question_definition $question,
                prt_evaluatable $result, $feedbackstyle) {
    
            if (!$result->is_evaluated()) {
    
            // Don't give standard feedback when we have errors.
            if (count($result->get_errors()) != 0) {
                return '';
            }
    
            $state = question_state::graded_state_for_fraction($result->get_score());
    
            $class = $state->get_feedback_class();
    
            if ($feedbackstyle === 2 || $feedbackstyle === 3) {
    
                $s = get_string('symbolicprt' . $class . 'feedback', 'qtype_stack');
                return html_writer::tag('span', $s, array('class' => $class));
            }
    
    
            // If called out of order.
            if ($question->castextprocessor === null) {
                $question->castextprocessor = new castext2_qa_processor($qa);
            }
    
    
            $field = 'prt' . $class . 'instantiated';
    
            if ($question->$field) {
    
                return html_writer::tag('div', $question->format_text(
    
                        stack_maths::process_display_castext($question->$field->get_rendered($question->castextprocessor), $this),
                        FORMAT_HTML, // All CASText2 processed content has already been formatted to HTML.
                        $qa, 'qtype_stack', $field, $question->id), array('class' => $class));
    
    
        /**
         * Display and appropriate piece of standard PRT feedback given the overall
         * state of the question.
         * @param question_attempt $qa
         * @param qtype_stack_question $question the question being displayed.
         * @param array $response the current response.
         * @return string HTML fragment.
         */
        protected function overall_standard_prt_feedback(question_attempt $qa,
                qtype_stack_question $question, $response) {
    
            $fraction = null;
            foreach ($question->prts as $name => $prt) {
                $relevantresponse = $this->get_applicable_response_for_prt($name, $response, $qa);
                if (is_null($relevantresponse)) {
                    continue;
                }
    
                $result = $question->get_prt_result($name, $relevantresponse, $qa->get_state()->is_finished());
    
            $result = new prt_evaluatable('', $fraction, new castext2_static_replacer([]), array());
    
            // This is overall, so we fix the PRT feedbackstyle style = 1 to get the default type of feedback.
    
            return $this->standard_prt_feedback($qa, $qa->get_question(), $result, 1);
    
        protected function hint(question_attempt $qa, question_hint $hint) {
            if (empty($hint->hint)) {
                return '';
            }
    
            $question = $qa->get_question();
            $hinttext = $question->get_hint_castext($hint);
    
            // If called out of order.
            if ($question->castextprocessor === null) {
                $question->castextprocessor = new castext2_qa_processor($qa);
            }
    
    
            $newhint = new question_hint($hint->id,
    
                    stack_maths::process_display_castext($hinttext->get_rendered($question->castextprocessor), $this),
                    FORMAT_HTML // All CASText2 processed content has already been formatted to HTML.
                );
    
    
            return html_writer::nonempty_tag('div',
    
                $question->format_hint($newhint, $qa), array('class' => 'hint'));
    
        public function correct_response(question_attempt $qa) {
            $question = $qa->get_question();
            return '<hr />'.$question->format_correct_response($qa);
        }
    
    
        public function general_feedback(question_attempt $qa) {
            $question = $qa->get_question();
            if (empty($question->generalfeedback)) {
                return '';
            }
    
    
            // If called out of order.
            if ($question->castextprocessor === null) {
                $question->castextprocessor = new castext2_qa_processor($qa);
            }
    
    
            return $qa->get_question()->format_text(stack_maths::process_display_castext(
    
                    $question->get_generalfeedback_castext()->get_rendered($question->castextprocessor), $this),
    
    Chris Sangwin's avatar
    Chris Sangwin committed
                    FORMAT_HTML, // All CASText2 processed content has already been formatted to HTML.
    
                    $qa, 'question', 'generalfeedback', $question->id);
    
    Chris Sangwin's avatar
    Chris Sangwin committed
        public function question_description(question_attempt $qa) {
            $question = $qa->get_question();
            if (empty($question->questiondescription)) {
                return '';
            }
    
            // If called out of order.
            if ($question->castextprocessor === null) {
                $question->castextprocessor = new castext2_qa_processor($qa);
            }
    
            return $qa->get_question()->format_text(stack_maths::process_display_castext(
                $question->get_questiondescription_castext()->get_rendered($question->castextprocessor), $this),
                FORMAT_HTML, // All CASText2 processed content has already been formatted to HTML.
                $qa, 'question', 'questiondescription', $question->id);
        }
    
    
        /**
         * Render a fact sheet.
         * @param string $name the title of the fact sheet.
         * @param string $fact the contents of the fact sheet.
         */
        public function fact_sheet($name, $fact) {
    
            $name = html_writer::tag('h5', $name);
            return html_writer::tag('div', $name.$fact, array('class' => 'factsheet'));