Proyectos de Subversion Moodle

Rev

Autoría | Ultima modificación | Ver Log |

<?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/>.

// Prevent direct access to this script.
defined('MOODLE_INTERNAL') || die();

// Include required files.
require_once($CFG->dirroot.'/question/type/ordering/question.php');

/**
 * Ordering editing form definition
 *
 * @package    qtype_ordering
 * @copyright  2013 Gordon Bateson (gordon.bateson@gmail.com)
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */
class qtype_ordering_edit_form extends question_edit_form {

    /** Rows count in answer field */
    const TEXTFIELD_ROWS = 2;

    /** Cols count in answer field */
    const TEXTFIELD_COLS = 60;

    /** Number of answers in question by default */
    const NUM_ITEMS_DEFAULT = 6;

    /** Minimum number of answers to show */
    const NUM_ITEMS_MIN = 3;

    /** Number of answers to add on demand */
    const NUM_ITEMS_ADD = 1;

    public function qtype(): string {
        return 'ordering';
    }

    /**
     * Plugin name is class name without trailing "_edit_form"
     *
     * @return string
     */
    public function plugin_name(): string {
        return 'qtype_ordering';
    }

    public function definition_inner($mform): void {
        // Field for layouttype.
        $options = qtype_ordering_question::get_layout_types();
        $mform->addElement('select', 'layouttype', get_string('layouttype', 'qtype_ordering'), $options);
        $mform->addHelpButton('layouttype', 'layouttype', 'qtype_ordering');
        $mform->setDefault('layouttype', $this->get_default_value('layouttype', qtype_ordering_question::LAYOUT_VERTICAL));

        // Field for selecttype.
        $options = qtype_ordering_question::get_select_types();
        $mform->addElement('select', 'selecttype', get_string('selecttype', 'qtype_ordering'), $options);
        $mform->addHelpButton('selecttype', 'selecttype', 'qtype_ordering');
        $mform->setDefault('selecttype', $this->get_default_value('selecttype', qtype_ordering_question::SELECT_ALL));

        // Field for selectcount.
        $mform->addElement('text', 'selectcount', get_string('selectcount', 'qtype_ordering'), ['size' => 2]);
        $mform->setDefault('selectcount', qtype_ordering_question::MIN_SUBSET_ITEMS);
        $mform->setType('selectcount', PARAM_INT);
        // Hide the field if 'Item selection type' is set to select all items.
        $mform->hideIf('selectcount', 'selecttype', 'eq', qtype_ordering_question::SELECT_ALL);
        $mform->addHelpButton('selectcount', 'selectcount', 'qtype_ordering');
        $mform->addRule('selectcount', null, 'numeric', null, 'client');

        // Field for gradingtype.
        $options = qtype_ordering_question::get_grading_types();
        $mform->addElement('select', 'gradingtype', get_string('gradingtype', 'qtype_ordering'), $options);
        $mform->addHelpButton('gradingtype', 'gradingtype', 'qtype_ordering');
        $mform->setDefault(
            'gradingtype',
            $this->get_default_value('gradingtype', qtype_ordering_question::GRADING_ABSOLUTE_POSITION)
        );

        // Field for showgrading.
        $options = [0 => get_string('hide'), 1 => get_string('show')];
        $mform->addElement('select', 'showgrading', get_string('showgrading', 'qtype_ordering'), $options);
        $mform->addHelpButton('showgrading', 'showgrading', 'qtype_ordering');
        $mform->setDefault('showgrading', $this->get_default_value('showgrading', 1));

        // Field for numberingstyle.
        $options = qtype_ordering_question::get_numbering_styles();
        $mform->addElement('select', 'numberingstyle', get_string('numberingstyle', 'qtype_ordering'), $options);
        $mform->addHelpButton('numberingstyle', 'numberingstyle', 'qtype_ordering');
        $mform->setDefault(
            'numberingstyle',
            $this->get_default_value('numberingstyle', qtype_ordering_question::NUMBERING_STYLE_DEFAULT)
        );

        $mform->addElement('header', 'answersheader', get_string('draggableitems', 'qtype_ordering'));
        $mform->setExpanded('answersheader', true);

        // Field for the answers.
        $elements = [];
        $options = [];
        $elements[] = $mform->createElement('editor', 'answer', get_string('draggableitemno', 'qtype_ordering'),
            $this->get_editor_attributes(), $this->get_editor_options());
        $elements[] = $mform->createElement('submit', 'answer' . 'removeeditor', get_string('removeeditor', 'qtype_ordering'),
            ['onclick' => 'skipClientValidation = true;']);
        $options['answer'] = ['type' => PARAM_RAW];
        $this->add_repeat_elements($mform, 'answer', $elements, $options);

        // Adjust HTML editor and removal buttons.
        $this->adjust_html_editors($mform, 'answer');

        // Adding feedback fields (=Combined feedback).
        $this->add_combined_feedback_fields(true);

        // Adding interactive settings (=Multiple tries).
        $this->add_interactive_settings(false, true);
    }

    /**
     * Returns answer repeats count
     *
     * @param stdClass $question
     * @return int
     */
    protected function get_answer_repeats(stdClass $question): int {
        if (isset($question->id)) {
            $repeats = count($question->options->answers);
        } else {
            $repeats = self::NUM_ITEMS_DEFAULT;
        }
        if ($repeats < self::NUM_ITEMS_MIN) {
            $repeats = self::NUM_ITEMS_MIN;
        }
        return $repeats;
    }

    /**
     * Returns editor attributes
     *
     * @return array
     */
    protected function get_editor_attributes(): array {
        return [
            'rows' => self::TEXTFIELD_ROWS,
            'cols' => self::TEXTFIELD_COLS,
        ];
    }

    /**
     * Returns editor options
     *
     * @return array
     */
    protected function get_editor_options(): array {
        return [
            'context' => $this->context,
            'maxfiles' => EDITOR_UNLIMITED_FILES,
            'noclean' => true,
        ];
    }

    /**
     * Resets editor format to specified
     *
     * @param MoodleQuickForm_editor $editor
     * @param int|string $format
     * @return int
     */
    protected function reset_editor_format(MoodleQuickForm_editor $editor, int|string $format = FORMAT_MOODLE): int {
        $value = $editor->getValue();
        $value['format'] = $format;
        $editor->setValue($value);
        return $format;
    }

    /**
     * Adjust HTML editor and removal buttons.
     *
     * @param MoodleQuickForm $mform
     * @param string $name
     */
    protected function adjust_html_editors(MoodleQuickForm $mform, string $name): void {

        // Cache the number of formats supported
        // by the preferred editor for each format.
        $count = [];

        if (isset($this->question->options->answers)) {
            $ids = array_keys($this->question->options->answers);
        } else {
            $ids = [];
        }

        $defaultanswerformat = get_config('qtype_ordering', 'defaultanswerformat');

        $repeats = 'count'.$name.'s'; // E.g. countanswers.
        if ($mform->elementExists($repeats)) {
            // Use mform element to get number of repeats.
            $repeats = $mform->getElement($repeats)->getValue();
        } else {
            // Determine number of repeats by object sniffing.
            $repeats = 0;
            while ($mform->elementExists($name."[$repeats]")) {
                $repeats++;
            }
        }

        for ($i = 0; $i < $repeats; $i++) {
            $editor = $mform->getElement($name."[$i]");
            $id = $ids[$i] ?? 0;

            // The old/new name of the button to remove the HTML editor
            // old : the name of the button when added by repeat_elements
            // new : the simplified name of the button to satisfy "no_submit_button_pressed()" in lib/formslib.php.
            $oldname = $name.'removeeditor['.$i.']';
            $newname = $name.'removeeditor_'.$i;

            // Remove HTML editor, if necessary.
            if (optional_param($newname, 0, PARAM_RAW)) {
                $format = $this->reset_editor_format($editor);
                $_POST['answer'][$i]['format'] = $format; // Overwrite incoming data.
            } else if ($id) {
                $format = $this->question->options->answers[$id]->answerformat;
            } else {
                $format = $this->reset_editor_format($editor, $defaultanswerformat);
            }

            // Check we have a submit button - it should always be there !!
            if ($mform->elementExists($oldname)) {
                if (!isset($count[$format])) {
                    $editor = editors_get_preferred_editor($format);
                    $count[$format] = $editor->get_supported_formats();
                    $count[$format] = count($count[$format]);
                }

                if ($count[$format] > 1) {
                    $mform->removeElement($oldname);
                } else {
                    $submit = $mform->getElement($oldname);
                    $submit->setName($newname);
                }
                $mform->registerNoSubmitButton($newname);
            }
        }
    }

    protected function get_hint_fields($withclearwrong = false, $withshownumpartscorrect = false): array {
        $mform = $this->_form;

        $repeated = [];
        $repeated[] = $mform->createElement('editor', 'hint', get_string('hintn', 'question'),
            ['rows' => 5], $this->editoroptions);
        $repeatedoptions['hint']['type'] = PARAM_RAW;

        $optionelements = [];

        if ($withshownumpartscorrect) {
            $optionelements[] = $mform->createElement('advcheckbox', 'hintshownumcorrect', '',
                get_string('shownumpartscorrect', 'question'));
        }

        $optionelements[] = $mform->createElement('advcheckbox', 'hintoptions', '',
            get_string('highlightresponse', 'qtype_ordering'));

        if (count($optionelements)) {
            $repeated[] = $mform->createElement('group', 'hintoptions',
                get_string('hintnoptions', 'question'), $optionelements, null, false);
        }

        return [$repeated, $repeatedoptions];
    }

    public function data_preprocessing($question): stdClass {

        $question = parent::data_preprocessing($question);
        $question = $this->data_preprocessing_answers($question, true);

        // Preprocess feedback.
        $question = $this->data_preprocessing_combined_feedback($question, true);

        $question = $this->data_preprocessing_hints($question, false, true);

        // Preprocess answers and fractions.
        $question->answer = [];
        $question->fraction = [];

        if (empty($question->options->answers)) {
            $answerids = [];
        } else {
            $answerids = array_keys($question->options->answers);
        }

        $defaultanswerformat = get_config('qtype_ordering', 'defaultanswerformat');
        $repeats = $this->get_answer_repeats($question);
        for ($i = 0; $i < $repeats; $i++) {

            if ($answerid = array_shift($answerids)) {
                $answer = $question->options->answers[$answerid];
            } else {
                $answer = (object) ['answer' => '', 'answerformat' => $defaultanswerformat];
                $answerid = 0;
            }

            if (empty($question->id)) {
                $question->answer[$i] = $answer->answer;
            } else {
                $itemid = file_get_submitted_draft_itemid("answer[$i]");
                $format = $answer->answerformat;
                $text = file_prepare_draft_area($itemid, $this->context->id, 'question', 'answer',
                    $answerid, $this->editoroptions, $answer->answer);
                $question->answer[$i] = [
                    'text' => $text,
                    'format' => $format,
                    'itemid' => $itemid,
                ];
            }
            $question->fraction[$i] = ($i + 1);
        }

        // Defining default values.
        $names = [
            'layouttype' => qtype_ordering_question::LAYOUT_VERTICAL,
            'selecttype' => qtype_ordering_question::SELECT_ALL,
            'selectcount' => qtype_ordering_question::MIN_SUBSET_ITEMS,
            'gradingtype' => qtype_ordering_question::GRADING_ABSOLUTE_POSITION,
            'showgrading' => 1,  // 1 means SHOW.
            'numberingstyle' => qtype_ordering_question::NUMBERING_STYLE_DEFAULT,
        ];
        foreach ($names as $name => $default) {
            $question->$name = $question->options->$name ?? $this->get_default_value($name, $default);
        }

        return $question;
    }

    protected function data_preprocessing_hints($question, $withclearwrong = false, $withshownumpartscorrect = false): stdClass {
        if (empty($question->hints)) {
            return $question;
        }
        parent::data_preprocessing_hints($question, $withclearwrong, $withshownumpartscorrect);

        $question->hintoptions = [];
        foreach ($question->hints as $hint) {
            $question->hintoptions[] = $hint->options;
        }

        return $question;
    }

    public function validation($data, $files): array {
        $errors = [];

        $minsubsetitems = qtype_ordering_question::MIN_SUBSET_ITEMS;
        // Make sure the entered size of the subset is no less than the defined minimum.
        if ($data['selecttype'] != qtype_ordering_question::SELECT_ALL && $data['selectcount'] < $minsubsetitems) {
            $errors['selectcount'] = get_string('notenoughsubsetitems', 'qtype_ordering', $minsubsetitems);
        }

        // Identify duplicates and report as an error.
        $answers = [];
        $answercount = 0;
        foreach ($data['answer'] as $answer) {
            if (is_array($answer)) {
                $answer = $answer['text'];
            }
            if ($answer = trim($answer)) {
                if (in_array($answer, $answers)) {
                    $i = array_search($answer, $answers);
                    $item = get_string('draggableitemno', 'qtype_ordering');
                    $item = str_replace('{no}', $i + 1, $item);
                    $item = html_writer::link("#id_answer_$i", $item);
                    $a = (object) ['text' => $answer, 'item' => $item];
                    $errors["answer[$answercount]"] = get_string('duplicatesnotallowed', 'qtype_ordering', $a);
                } else {
                    $answers[] = $answer;
                }
                $answercount++;
            }
        }

        // If there are no answers provided, show error message under first 2 answer boxes
        // If only 1 answer provided, show error message under second answer box.
        if ($answercount < 2) {
            $errors['answer[1]'] = get_string('notenoughanswers', 'qtype_ordering', 2);

            if ($answercount == 0) {
                $errors['answer[0]'] = get_string('notenoughanswers', 'qtype_ordering', 2);
            }
        }

        // If adding a new ordering question, update defaults.
        if (empty($errors) && empty($data['id'])) {
            $fields = [
                'layouttype', 'selecttype', 'selectcount',
                'gradingtype', 'showgrading', 'numberingstyle',
            ];
            foreach ($fields as $field) {
                if (array_key_exists($field, $data)) {
                    question_bank::get_qtype($this->qtype())->set_default_value($field, $data[$field]);
                }
            }
        }

        return $errors;
    }

    /**
     * Get array of countable item types
     *
     * @param string $type
     * @param int $max
     * @return array (type => description)
     */
    protected function get_addcount_options(string $type, int $max = 10): array {
        // Generate options.
        $options = [];
        for ($i = 1; $i <= $max; $i++) {
            if ($i == 1) {
                $options[$i] = get_string('addsingle'.$type, 'qtype_ordering');
            } else {
                $options[$i] = get_string('addmultiple'.$type.'s', 'qtype_ordering', $i);
            }
        }
        return $options;
    }

    /**
     * Add repeated elements with a button allowing a selectable number of new elements
     *
     * @param MoodleQuickForm $mform the Moodle form object
     * @param string $type
     * @param array $elements
     * @param array $options
     * @return void, but will update $mform
     */
    protected function add_repeat_elements(MoodleQuickForm $mform, string $type, array $elements, array $options): void {

        // Cache element names.
        $types = $type.'s';
        $addtypes = 'add'.$types;
        $counttypes = 'count'.$types;
        $addtypescount = $addtypes.'count';
        $addtypesgroup = $addtypes.'group';

        $repeats = $this->get_answer_repeats($this->question);

        $count = optional_param($addtypescount, self::NUM_ITEMS_ADD, PARAM_INT);

        $label = ($count == 1 ? 'addsingle'.$type : 'addmultiple'.$types);
        $label = get_string($label, 'qtype_ordering', $count);

        $this->repeat_elements($elements, $repeats, $options, $counttypes, $addtypes, $count, $label, true);

        // Remove the original "Add xxx" button ...
        $mform->removeElement($addtypes);

        // ... and replace it with "Add" button + select group.
        $options = $this->get_addcount_options($type);
        $mform->addGroup([
            $mform->createElement('submit', $addtypes, get_string('add')),
            $mform->createElement('select', $addtypescount, '', $options),
        ], $addtypesgroup, '', ' ', false);

        // Set default value and type of select element.
        $mform->setDefault($addtypescount, $count);
        $mform->setType($addtypescount, PARAM_INT);
    }
}