MDL-58411 qtype_essay: Add file type validation in essay question type
authorLuca Bösch <luca.boesch@bfh.ch>
Sat, 17 Feb 2018 21:39:12 +0000 (22:39 +0100)
committerLuca Bösch <luca.boesch@bfh.ch>
Tue, 3 Apr 2018 05:27:56 +0000 (07:27 +0200)
16 files changed:
question/behaviour/manualgraded/behaviour.php
question/type/essay/backup/moodle2/backup_qtype_essay_plugin.class.php
question/type/essay/db/install.xml
question/type/essay/db/upgrade.php
question/type/essay/edit_essay_form.php
question/type/essay/lang/en/qtype_essay.php
question/type/essay/question.php
question/type/essay/questiontype.php
question/type/essay/renderer.php
question/type/essay/tests/behat/file_type_restriction.feature [new file with mode: 0644]
question/type/essay/tests/fixtures/testquestion.moodle.xml
question/type/essay/tests/helper.php
question/type/essay/tests/walkthrough_test.php
question/type/essay/version.php
question/type/questionbase.php
question/type/upgrade.txt

index e37540d..47bf3a9 100644 (file)
@@ -65,6 +65,34 @@ class qbehaviour_manualgraded extends question_behaviour_with_save {
         }
     }
 
+    /**
+     * Like the parent method, except that when a response is gradable, but not
+     * completely, we move it to the invalid state.
+     * @param question_attempt_pending_step $pendingstep a partially initialised step
+     *      containing all the information about the action that is being performed.
+     * @return bool either {@link question_attempt::KEEP} or {@link question_attempt::DISCARD}
+     */
+    public function process_save(question_attempt_pending_step $pendingstep) {
+        if ($this->qa->get_state()->is_finished()) {
+            return question_attempt::DISCARD;
+        } else if (!$this->qa->get_state()->is_active()) {
+            throw new coding_exception('Question is not active, cannot process_actions.');
+        }
+
+        if ($this->is_same_response($pendingstep)) {
+            return question_attempt::DISCARD;
+        }
+
+        if ($this->is_complete_response($pendingstep)) {
+            $pendingstep->set_state(question_state::$complete);
+        } else if ($this->question->is_gradable_response($pendingstep->get_qt_data())) {
+            $pendingstep->set_state(question_state::$invalid);
+        } else {
+            $pendingstep->set_state(question_state::$todo);
+        }
+        return question_attempt::KEEP;
+    }
+
     public function summarise_action(question_attempt_step $step) {
         if ($step->has_behaviour_var('comment')) {
             return $this->summarise_manual_comment($step);
@@ -81,7 +109,7 @@ class qbehaviour_manualgraded extends question_behaviour_with_save {
         }
 
         $response = $this->qa->get_last_step()->get_qt_data();
-        if (!$this->question->is_complete_response($response)) {
+        if (!$this->question->is_gradable_response($response)) {
             $pendingstep->set_state(question_state::$gaveup);
         } else {
             $pendingstep->set_state(question_state::$needsgrading);
index fb4e5f7..842d10a 100644 (file)
@@ -51,7 +51,7 @@ class backup_qtype_essay_plugin extends backup_qtype_plugin {
         $essay = new backup_nested_element('essay', array('id'), array(
                 'responseformat', 'responserequired', 'responsefieldlines',
                 'attachments', 'attachmentsrequired', 'graderinfo',
-                'graderinfoformat', 'responsetemplate', 'responsetemplateformat'));
+                'graderinfoformat', 'responsetemplate', 'responsetemplateformat', 'filetypeslist'));
 
         // Now the own qtype tree.
         $pluginwrapper->add_child($essay);
index b7abfbd..88314ab 100644 (file)
@@ -17,6 +17,7 @@
         <FIELD NAME="graderinfoformat" TYPE="int" LENGTH="4" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="The text format for graderinfo."/>
         <FIELD NAME="responsetemplate" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="The template to pre-populate student's response field during attempt."/>
         <FIELD NAME="responsetemplateformat" TYPE="int" LENGTH="4" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="The text format for responsetemplate."/>
+        <FIELD NAME="filetypeslist" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="What attachment file type a student is allowed to include with their response. * or empty means unlimited."/>
       </FIELDS>
       <KEYS>
         <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
index ec07267..2c29c45 100644 (file)
@@ -30,7 +30,9 @@ defined('MOODLE_INTERNAL') || die();
  * @param int $oldversion the version we are upgrading from.
  */
 function xmldb_qtype_essay_upgrade($oldversion) {
-    global $CFG;
+    global $CFG, $DB;
+
+    $dbman = $DB->get_manager();
 
     // Automatically generated Moodle v3.2.0 release upgrade line.
     // Put any upgrade step following this.
@@ -41,5 +43,19 @@ function xmldb_qtype_essay_upgrade($oldversion) {
     // Automatically generated Moodle v3.4.0 release upgrade line.
     // Put any upgrade step following this.
 
+    if ($oldversion < 2018021800) {
+
+        // Add "filetypeslist" column to the question type options to save the allowed file types.
+        $table = new xmldb_table('qtype_essay_options');
+        $field = new xmldb_field('filetypeslist', XMLDB_TYPE_TEXT, null, null, null, null, null, 'responsetemplateformat');
+
+        // Conditionally launch add field filetypeslist.
+        if (!$dbman->field_exists($table, $field)) {
+            $dbman->add_field($table, $field);
+        }
+
+        // Essay savepoint reached.
+        upgrade_plugin_savepoint(true, 2018021800, 'qtype', 'essay');
+    }
     return true;
 }
index d50437b..3f97bd3 100644 (file)
@@ -65,6 +65,10 @@ class qtype_essay_edit_form extends question_edit_form {
         $mform->addHelpButton('attachmentsrequired', 'attachmentsrequired', 'qtype_essay');
         $mform->disabledIf('attachmentsrequired', 'attachments', 'eq', 0);
 
+        $mform->addElement('filetypes', 'filetypeslist', get_string('acceptedfiletypes', 'qtype_essay'));
+        $mform->addHelpButton('filetypeslist', 'acceptedfiletypes', 'qtype_essay');
+        $mform->disabledIf('filetypeslist', 'attachments', 'eq', 0);
+
         $mform->addElement('header', 'responsetemplateheader', get_string('responsetemplateheader', 'qtype_essay'));
         $mform->addElement('editor', 'responsetemplate', get_string('responsetemplate', 'qtype_essay'),
                 array('rows' => 10),  array_merge($this->editoroptions, array('maxfiles' => 0)));
@@ -88,6 +92,7 @@ class qtype_essay_edit_form extends question_edit_form {
         $question->responsefieldlines = $question->options->responsefieldlines;
         $question->attachments = $question->options->attachments;
         $question->attachmentsrequired = $question->options->attachmentsrequired;
+        $question->filetypeslist = $question->options->filetypeslist;
 
         $draftid = file_get_submitted_draft_itemid('graderinfo');
         $question->graderinfo = array();
index 388822a..8a4b689 100644 (file)
@@ -23,6 +23,8 @@
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
+$string['acceptedfiletypes'] = 'Accepted file types';
+$string['acceptedfiletypes_help'] = 'Accepted file types can be restricted by entering a list of file extensions. If the field is left empty, then all file types are allowed.';
 $string['allowattachments'] = 'Allow attachments';
 $string['attachmentsoptional'] = 'Attachments are optional';
 $string['attachmentsrequired'] = 'Require attachments';
@@ -38,6 +40,7 @@ $string['mustattach'] = 'When "No online text" is selected, or responses are opt
 $string['mustrequire'] = 'When "No online text" is selected, or responses are optional, you must require at least one attachment.';
 $string['mustrequirefewer'] = 'You cannot require more attachments than you allow.';
 $string['nlines'] = '{$a} lines';
+$string['nonexistentfiletypes'] = 'The following file types were not recognised: {$a}';
 $string['pluginname'] = 'Essay';
 $string['pluginname_help'] = 'In response to a question, the respondent may upload one or more files and/or enter text online. A response template may be provided. Responses must be graded manually.';
 $string['pluginname_link'] = 'question/type/essay';
index 2720e15..0e82bd3 100644 (file)
@@ -52,6 +52,9 @@ class qtype_essay_question extends question_with_responses {
     public $responsetemplate;
     public $responsetemplateformat;
 
+    /** @var array The string array of file types accepted upon file submission. */
+    public $filetypeslist;
+
     public function make_behaviour(question_attempt $qa, $preferredbehaviour) {
         return question_engine::make_behaviour('manualgraded', $qa, $preferredbehaviour);
     }
@@ -98,6 +101,18 @@ class qtype_essay_question extends question_with_responses {
 
         // Determine the number of attachments present.
         if ($hasattachments) {
+            // Check the filetypes.
+            $filetypesutil = new \core_form\filetypes_util();
+            $whitelist = $filetypesutil->normalize_file_types($this->filetypeslist);
+            $wrongfiles = array();
+            foreach ($response['attachments']->get_files() as $file) {
+                if (!$filetypesutil->is_allowed_file_type($file->get_filename(), $whitelist)) {
+                    $wrongfiles[] = $file->get_filename();
+                }
+            }
+            if ($wrongfiles) { // At least one filetype is wrong.
+                return false;
+            }
             $attachcount = count($response['attachments']->get_files());
         } else {
             $attachcount = 0;
@@ -114,6 +129,18 @@ class qtype_essay_question extends question_with_responses {
         return $hascontent && $meetsinlinereq && $meetsattachmentreq;
     }
 
+    public function is_gradable_response(array $response) {
+        // Determine if the given response has online text and attachments.
+        if (array_key_exists('answer', $response) && ($response['answer'] !== '')) {
+            return true;
+        } else if (array_key_exists('attachments', $response)
+                && $response['attachments'] instanceof question_response_files) {
+            return true;
+        } else {
+            return false;
+        }
+    }
+
     public function is_same_response(array $prevresponse, array $newresponse) {
         if (array_key_exists('answer', $prevresponse) && $prevresponse['answer'] !== $this->responsetemplate) {
             $value1 = (string) $prevresponse['answer'];
index 8e5c580..71f03d8 100644 (file)
@@ -67,6 +67,11 @@ class qtype_essay extends question_type {
         $options->responsefieldlines = $formdata->responsefieldlines;
         $options->attachments = $formdata->attachments;
         $options->attachmentsrequired = $formdata->attachmentsrequired;
+        if (!isset($formdata->filetypeslist)) {
+            $options->filetypeslist = "";
+        } else {
+            $options->filetypeslist = $formdata->filetypeslist;
+        }
         $options->graderinfo = $this->import_or_save_files($formdata->graderinfo,
                 $context, 'qtype_essay', 'graderinfo', $formdata->id);
         $options->graderinfoformat = $formdata->graderinfo['format'];
@@ -86,6 +91,8 @@ class qtype_essay extends question_type {
         $question->graderinfoformat = $questiondata->options->graderinfoformat;
         $question->responsetemplate = $questiondata->options->responsetemplate;
         $question->responsetemplateformat = $questiondata->options->responsetemplateformat;
+        $filetypesutil = new \core_form\filetypes_util();
+        $question->filetypeslist = $filetypesutil->normalize_file_types($questiondata->options->filetypeslist);
     }
 
     public function delete_question($questionid, $contextid) {
index 9b04c49..5f649f2 100644 (file)
@@ -119,12 +119,22 @@ class qtype_essay_renderer extends qtype_renderer {
 
         $pickeroptions->itemid = $qa->prepare_response_files_draft_itemid(
                 'attachments', $options->context->id);
+        $pickeroptions->accepted_types = $qa->get_question()->filetypeslist;
 
         $fm = new form_filemanager($pickeroptions);
         $filesrenderer = $this->page->get_renderer('core', 'files');
+
+        $text = '';
+        if (!empty($qa->get_question()->filetypeslist)) {
+            $text = html_writer::tag('p', get_string('acceptedfiletypes', 'qtype_essay'));
+            $filetypesutil = new \core_form\filetypes_util();
+            $filetypes = $qa->get_question()->filetypeslist;
+            $filetypedescriptions = $filetypesutil->describe_file_types($filetypes);
+            $text .= $this->render_from_template('core_form/filetypes-descriptions', $filetypedescriptions);
+        }
         return $filesrenderer->render($fm). html_writer::empty_tag(
                 'input', array('type' => 'hidden', 'name' => $qa->get_qt_field_name('attachments'),
-                'value' => $pickeroptions->itemid));
+                'value' => $pickeroptions->itemid)) . $text;
     }
 
     public function manual_comment(question_attempt $qa, question_display_options $options) {
diff --git a/question/type/essay/tests/behat/file_type_restriction.feature b/question/type/essay/tests/behat/file_type_restriction.feature
new file mode 100644 (file)
index 0000000..a8a476f
--- /dev/null
@@ -0,0 +1,75 @@
+@qtype @qtype_essay
+Feature: In a essay question, limit submittable file types
+In order to constrain student submissions for marking
+As a teacher
+I need to limit the submittable file types
+
+  Background:
+    Given the following "users" exist:
+      | username | firstname | lastname | email                |
+      | teacher1 | Teacher   | 1        | teacher1@example.com |
+      | student1 | Student   | 1        | student0@example.com |
+    And the following "courses" exist:
+      | fullname | shortname | category |
+      | Course 1 | C1        | 0        |
+    And the following "course enrolments" exist:
+      | user     | course | role           |
+      | teacher1 | C1     | editingteacher |
+      | student1 | C1     | student        |
+    And the following "question categories" exist:
+      | contextlevel | reference | name           |
+      | Course       | C1        | Test questions |
+    And the following "questions" exist:
+      | questioncategory | qtype       | name  | questiontext    | defaultmark |
+      | Test questions   | essay       | TF1   | First question  | 20          |
+    And the following "activities" exist:
+      | activity   | name   | intro              | course | idnumber | grade |
+      | quiz       | Quiz 1 | Quiz 1 description | C1     | quiz1    | 20    |
+    And quiz "Quiz 1" contains the following questions:
+      | question | page |
+      | TF1      | 1    |
+    Given I log in as "teacher1"
+    And I am on "Course 1" course homepage
+    And I follow "Quiz 1"
+    And I navigate to "Edit quiz" in current page administration
+    And I click on "Edit question TF1" "link"
+    And I set the field "Allow attachments" to "1"
+    And I set the field "Response format" to "No online text"
+    And I set the field "Require attachments" to "1"
+    And I set the field "filetypeslist[filetypes]" to ".txt"
+    And I press "Save changes"
+    Then I log out
+
+  @javascript @_file_upload
+  Scenario: Preview an Essay question and submit a response with a correct filetype.
+    When I log in as "student1"
+    And I follow "Manage private files"
+    And I upload "lib/tests/fixtures/empty.txt" file to "Files" filemanager
+    And I press "Save changes"
+    And I am on "Course 1" course homepage
+    And I follow "Quiz 1"
+    And I press "Attempt quiz now"
+    And I should see "First question"
+    And I should see "You can drag and drop files here to add them."
+    And I click on "Add..." "button"
+    And I click on "Private files" "link" in the ".fp-repo-area" "css_element"
+    And I click on "empty.txt" "link"
+    And I click on "Select this file" "button"
+    # Wait for the page to "settle".
+    And I wait until the page is ready
+    Then I should not see "These file types are not allowed here:"
+
+  @javascript @_file_upload
+  Scenario: Preview an Essay question and try to submit a response with an incorrect filetype.
+    When I log in as "student1"
+    And I follow "Manage private files"
+    And I upload "lib/tests/fixtures/upload_users.csv" file to "Files" filemanager
+    And I press "Save changes"
+    And I am on "Course 1" course homepage
+    And I follow "Quiz 1"
+    And I press "Attempt quiz now"
+    And I should see "First question"
+    And I should see "You can drag and drop files here to add them."
+    And I click on "Add..." "button"
+    And I click on "Private files" "link" in the ".fp-repo-area" "css_element"
+    Then I should see "No files available"
index 4782ff6..d925ffb 100644 (file)
@@ -27,6 +27,7 @@
     <responsefieldlines>15</responsefieldlines>
     <attachments>0</attachments>
     <attachmentsrequired>0</attachmentsrequired>
+    <filetypeslist></filetypeslist>
     <graderinfo format="html">
       <text></text>
     </graderinfo>
index 07dd4e0..c759bbd 100644 (file)
@@ -53,6 +53,7 @@ class qtype_essay_test_helper extends question_test_helper {
         $q->responsefieldlines = 10;
         $q->attachments = 0;
         $q->attachmentsrequired = 0;
+        $q->filetypeslist = '';
         $q->graderinfo = '';
         $q->graderinfoformat = FORMAT_HTML;
         $q->qtype = question_bank::get_qtype('essay');
@@ -87,6 +88,7 @@ class qtype_essay_test_helper extends question_test_helper {
         $fromform->responsefieldlines = 10;
         $fromform->attachments = 0;
         $fromform->attachmentsrequired = 0;
+        $fromform->filetypeslist = '';
         $fromform->graderinfo = array('text' => '', 'format' => FORMAT_HTML);
         $fromform->responsetemplate = array('text' => '', 'format' => FORMAT_HTML);
 
@@ -105,6 +107,19 @@ class qtype_essay_test_helper extends question_test_helper {
         return $q;
     }
 
+    /**
+     * Makes an essay question using the HTML editor allowing embedded files as
+     * input, and up to two attachments, two needed.
+     * @return qtype_essay_question
+     */
+    public function make_essay_question_editorfilepickertworequired() {
+        $q = $this->initialise_essay_question();
+        $q->responseformat = 'editorfilepicker';
+        $q->attachments = 2;
+        $q->attachmentsrequired = 2;
+        return $q;
+    }
+
     /**
      * Make the data what would be received from the editing form for an essay
      * question using the HTML editor allowing embedded files as input, and up
@@ -124,6 +139,7 @@ class qtype_essay_test_helper extends question_test_helper {
         $fromform->responsefieldlines = 10;
         $fromform->attachments = 3;
         $fromform->attachmentsrequired = 0;
+        $fromform->filetypeslist = '';
         $fromform->graderinfo = array('text' => '', 'format' => FORMAT_HTML);
         $fromform->responsetemplate = array('text' => '', 'format' => FORMAT_HTML);
 
@@ -159,6 +175,7 @@ class qtype_essay_test_helper extends question_test_helper {
         $fromform->responsefieldlines = 10;
         $fromform->attachments = 0;
         $fromform->attachmentsrequired = 0;
+        $fromform->filetypeslist = '';
         $fromform->graderinfo = array('text' => '', 'format' => FORMAT_HTML);
         $fromform->responsetemplate = array('text' => '', 'format' => FORMAT_HTML);
 
@@ -191,6 +208,7 @@ class qtype_essay_test_helper extends question_test_helper {
         $q->responseformat = 'noinline';
         $q->attachments = 3;
         $q->attachmentsrequired = 1;
+        $q->filetypeslist = '';
         return $q;
     }
 
index d398946..a1a9bb9 100644 (file)
@@ -501,4 +501,122 @@ class qtype_essay_walkthrough_testcase extends qbehaviour_walkthrough_test_base
         // Test for the hash of an empty file area.
         $this->assertNotContains('d41d8cd98f00b204e9800998ecf8427e', $this->currentoutput);
     }
+
+    public function test_deferred_feedback_html_editor_with_files_attempt_wrong_filetypes() {
+        global $CFG, $USER, $PAGE;
+
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+        // Required to init a text editor.
+        $PAGE->set_url('/');
+        $usercontextid = context_user::instance($USER->id)->id;
+        $fs = get_file_storage();
+
+        // Create an essay question in the DB.
+        $generator = $this->getDataGenerator()->get_plugin_generator('core_question');
+        $cat = $generator->create_question_category();
+        $question = $generator->create_question('essay', 'editorfilepicker', array('category' => $cat->id));
+
+        // Start attempt at the question.
+        $q = question_bank::load_question($question->id);
+        $q->filetypeslist = ("pdf, docx");
+        $this->start_attempt_at_question($q, 'deferredfeedback', 1);
+
+        $this->check_current_state(question_state::$todo);
+        $this->check_current_mark(null);
+        $this->check_step_count(1);
+
+        // Process a response and check the expected result.
+        // First we need to get the draft item ids.
+        $this->render();
+        if (!preg_match('/env=editor&amp;.*?itemid=(\d+)&amp;/', $this->currentoutput, $matches)) {
+            throw new coding_exception('Editor draft item id not found.');
+        }
+        $editordraftid = $matches[1];
+        if (!preg_match('/env=filemanager&amp;action=browse&amp;.*?itemid=(\d+)&amp;/', $this->currentoutput, $matches)) {
+            throw new coding_exception('File manager draft item id not found.');
+        }
+        $attachementsdraftid = $matches[1];
+
+        $this->save_file_to_draft_area($usercontextid, $editordraftid, 'smile.txt', ':-)');
+        $this->save_file_to_draft_area($usercontextid, $attachementsdraftid, 'greeting.txt', 'Hello world!');
+        $this->process_submission(array(
+            'answer' => 'Here is a picture: <img src="' . $CFG->wwwroot .
+                "/draftfile.php/{$usercontextid}/user/draft/{$editordraftid}/smile.txt" .
+                '" alt="smile">.',
+            'answerformat' => FORMAT_HTML,
+            'answer:itemid' => $editordraftid,
+            'attachments' => $attachementsdraftid));
+
+        $this->check_current_state(question_state::$invalid);
+        $this->check_current_mark(null);
+        $this->check_step_count(2);
+        $this->save_quba();
+
+        // Now submit all and finish.
+        $this->finish();
+        $this->check_current_state(question_state::$needsgrading);
+        $this->check_current_mark(null);
+        $this->check_step_count(3);
+        $this->save_quba();
+    }
+
+    public function test_deferred_feedback_html_editor_with_files_attempt_correct_filetypes() {
+        global $CFG, $USER, $PAGE;
+
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+        // Required to init a text editor.
+        $PAGE->set_url('/');
+        $usercontextid = context_user::instance($USER->id)->id;
+        $fs = get_file_storage();
+
+        // Create an essay question in the DB.
+        $generator = $this->getDataGenerator()->get_plugin_generator('core_question');
+        $cat = $generator->create_question_category();
+        $question = $generator->create_question('essay', 'editorfilepicker', array('category' => $cat->id));
+
+        // Start attempt at the question.
+        $q = question_bank::load_question($question->id);
+        $q->filetypeslist = ("txt, docx");
+        $this->start_attempt_at_question($q, 'deferredfeedback', 1);
+
+        $this->check_current_state(question_state::$todo);
+        $this->check_current_mark(null);
+        $this->check_step_count(1);
+
+        // Process a response and check the expected result.
+        // First we need to get the draft item ids.
+        $this->render();
+        if (!preg_match('/env=editor&amp;.*?itemid=(\d+)&amp;/', $this->currentoutput, $matches)) {
+            throw new coding_exception('Editor draft item id not found.');
+        }
+        $editordraftid = $matches[1];
+        if (!preg_match('/env=filemanager&amp;action=browse&amp;.*?itemid=(\d+)&amp;/', $this->currentoutput, $matches)) {
+            throw new coding_exception('File manager draft item id not found.');
+        }
+        $attachementsdraftid = $matches[1];
+
+        $this->save_file_to_draft_area($usercontextid, $editordraftid, 'smile.txt', ':-)');
+        $this->save_file_to_draft_area($usercontextid, $attachementsdraftid, 'greeting.txt', 'Hello world!');
+        $this->process_submission(array(
+            'answer' => 'Here is a picture: <img src="' . $CFG->wwwroot .
+                "/draftfile.php/{$usercontextid}/user/draft/{$editordraftid}/smile.txt" .
+                '" alt="smile">.',
+            'answerformat' => FORMAT_HTML,
+            'answer:itemid' => $editordraftid,
+            'attachments' => $attachementsdraftid));
+
+        $this->check_current_state(question_state::$complete);
+        $this->check_current_mark(null);
+        $this->check_step_count(2);
+        $this->save_quba();
+
+        // Now submit all and finish.
+        $this->finish();
+        $this->check_current_state(question_state::$needsgrading);
+        $this->check_current_mark(null);
+        $this->check_step_count(3);
+        $this->save_quba();
+    }
 }
index c578e51..7512f57 100644 (file)
@@ -26,7 +26,7 @@
 defined('MOODLE_INTERNAL') || die();
 
 $plugin->component = 'qtype_essay';
-$plugin->version   = 2017111300;
+$plugin->version   = 2018021800;
 
 $plugin->requires  = 2017110800;
 
index b6a4b89..32dbcf8 100644 (file)
@@ -468,6 +468,17 @@ class question_information_item extends question_definition {
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 interface question_manually_gradable {
+    /**
+     * Use by many of the behaviours to determine whether the student
+     * has provided enough of an answer for the question to be graded automatically,
+     * or whether it must be considered aborted.
+     *
+     * @param array $response responses, as returned by
+     *      {@link question_attempt_step::get_qt_data()}.
+     * @return bool whether this response can be graded.
+     */
+    public function is_gradable_response(array $response);
+
     /**
      * Used by many of the behaviours, to work out whether the student's
      * response to the question is complete. That is, whether the question attempt
@@ -554,17 +565,6 @@ class question_classified_response {
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 interface question_automatically_gradable extends question_manually_gradable {
-    /**
-     * Use by many of the behaviours to determine whether the student
-     * has provided enough of an answer for the question to be graded automatically,
-     * or whether it must be considered aborted.
-     *
-     * @param array $response responses, as returned by
-     *      {@link question_attempt_step::get_qt_data()}.
-     * @return bool whether this response can be graded.
-     */
-    public function is_gradable_response(array $response);
-
     /**
      * In situations where is_gradable_response() returns false, this method
      * should generate a description of what the problem is.
@@ -637,6 +637,10 @@ abstract class question_with_responses extends question_definition
     public function classify_response(array $response) {
         return array();
     }
+
+    public function is_gradable_response(array $response) {
+        return $this->is_complete_response($response);
+    }
 }
 
 
@@ -651,10 +655,6 @@ abstract class question_graded_automatically extends question_with_responses
     /** @var Some question types have the option to show the number of sub-parts correct. */
     public $shownumcorrect = false;
 
-    public function is_gradable_response(array $response) {
-        return $this->is_complete_response($response);
-    }
-
     public function get_right_answer_summary() {
         $correctresponse = $this->get_correct_response();
         if (empty($correctresponse)) {
index 8bb92a8..c9a70f3 100644 (file)
@@ -1,9 +1,13 @@
 This files describes API changes for question type plugins.
 
-== 3.5 ==
+=== 3.5 ===
   + Added new classes backup_qtype_extrafields_plugin and restore_qtype_extrafields_plugin
    in order to use extra fields method in backup/restore question type. Require and inherit new classes for using it. See
    backup_qtype_shortanswer_plugin and restore_qtype_shortanswer_plugin for an example of using this.
+  + The declaration of is_gradable_response has been moved from question_automatically_gradable to
+   question_manually_gradable.
+  + The default implementation of is_gradable_response has been moved from question_graded_automatically to
+   question_with_responses.
 
 === 3.1.5, 3.2.2, 3.3 ===