diff --git a/question/behaviour/manualgraded/behaviour.php b/question/behaviour/manualgraded/behaviour.php
index e37540d6fa..cb3f55fedc 100644
--- a/question/behaviour/manualgraded/behaviour.php
+++ b/question/behaviour/manualgraded/behaviour.php
@@ -65,6 +65,33 @@ 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.
+     *
+     * TODO refactor, to remove the duplication.
+     */
+    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 +108,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);
diff --git a/question/type/essay/backup/moodle2/backup_qtype_essay_plugin.class.php b/question/type/essay/backup/moodle2/backup_qtype_essay_plugin.class.php
index fb4e5f792f..842d10a487 100644
--- a/question/type/essay/backup/moodle2/backup_qtype_essay_plugin.class.php
+++ b/question/type/essay/backup/moodle2/backup_qtype_essay_plugin.class.php
@@ -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);
diff --git a/question/type/essay/db/install.xml b/question/type/essay/db/install.xml
index b7abfbd299..88314abefb 100644
--- a/question/type/essay/db/install.xml
+++ b/question/type/essay/db/install.xml
@@ -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"/>
diff --git a/question/type/essay/db/upgrade.php b/question/type/essay/db/upgrade.php
index ec0726792d..b632379876 100644
--- a/question/type/essay/db/upgrade.php
+++ b/question/type/essay/db/upgrade.php
@@ -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,20 @@ function xmldb_qtype_essay_upgrade($oldversion) {
     // Automatically generated Moodle v3.4.0 release upgrade line.
     // Put any upgrade step following this.
 
+    if ($oldversion < 2017111300) {
+
+        // Add "latest" column to submissions table to mark the latest attempt.
+        $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 latest.
+        if (!$dbman->field_exists($table, $field)) {
+            $dbman->add_field($table, $field);
+        }
+
+        // Savepoint reached.
+        upgrade_plugin_savepoint(true, 2017111300, 'qtype', 'essay');
+    }
+
     return true;
 }
diff --git a/question/type/essay/edit_essay_form.php b/question/type/essay/edit_essay_form.php
index d50437b278..7794f78508 100644
--- a/question/type/essay/edit_essay_form.php
+++ b/question/type/essay/edit_essay_form.php
@@ -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('filesofthesetypes', 'qtype_essay'), array('size' => '60'));
+        $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();
@@ -112,8 +117,8 @@ class qtype_essay_edit_form extends question_edit_form {
     }
 
     public function validation($fromform, $files) {
-        $errors = parent::validation($fromform, $files);
 
+        $errors = parent::validation($fromform, $files);
         // Don't allow both 'no inline response' and 'no attachments' to be selected,
         // as these options would result in there being no input requested from the user.
         if ($fromform['responseformat'] == 'noinline' && !$fromform['attachments']) {
@@ -131,7 +136,6 @@ class qtype_essay_edit_form extends question_edit_form {
         if ($fromform['attachments'] != -1 && $fromform['attachments'] < $fromform['attachmentsrequired'] ) {
             $errors['attachmentsrequired']  = get_string('mustrequirefewer', 'qtype_essay');
         }
-
         return $errors;
     }
 
diff --git a/question/type/essay/lang/en/qtype_essay.php b/question/type/essay/lang/en/qtype_essay.php
index 388822aabc..49a06b6116 100644
--- a/question/type/essay/lang/en/qtype_essay.php
+++ b/question/type/essay/lang/en/qtype_essay.php
@@ -23,10 +23,13 @@
  * @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';
 $string['attachmentsrequired_help'] = 'This option specifies the minimum number of attachments required for a response to be considered gradable.';
+$string['filesofthesetypes'] = 'Files of these types may be added to the submission:';
 $string['formateditor'] = 'HTML editor';
 $string['formateditorfilepicker'] = 'HTML editor with file picker';
 $string['formatmonospaced'] = 'Plain text, monospaced font';
diff --git a/question/type/essay/question.php b/question/type/essay/question.php
index 2720e1505b..e0130fd7eb 100644
--- a/question/type/essay/question.php
+++ b/question/type/essay/question.php
@@ -51,6 +51,8 @@ class qtype_essay_question extends question_with_responses {
     public $graderinfoformat;
     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 +100,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 +128,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'];
diff --git a/question/type/essay/questiontype.php b/question/type/essay/questiontype.php
index 8e5c580acd..db557b1ce9 100644
--- a/question/type/essay/questiontype.php
+++ b/question/type/essay/questiontype.php
@@ -67,6 +67,7 @@ class qtype_essay extends question_type {
         $options->responsefieldlines = $formdata->responsefieldlines;
         $options->attachments = $formdata->attachments;
         $options->attachmentsrequired = $formdata->attachmentsrequired;
+        $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 +87,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) {
diff --git a/question/type/essay/renderer.php b/question/type/essay/renderer.php
index a6352ef258..7f46afe3dc 100644
--- a/question/type/essay/renderer.php
+++ b/question/type/essay/renderer.php
@@ -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('filesofthesetypes', '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/fixtures/testquestion.moodle.xml b/question/type/essay/tests/fixtures/testquestion.moodle.xml
index 4782ff6ce4..d925ffb130 100644
--- a/question/type/essay/tests/fixtures/testquestion.moodle.xml
+++ b/question/type/essay/tests/fixtures/testquestion.moodle.xml
@@ -27,6 +27,7 @@
     <responsefieldlines>15</responsefieldlines>
     <attachments>0</attachments>
     <attachmentsrequired>0</attachmentsrequired>
+    <filetypeslist></filetypeslist>
     <graderinfo format="html">
       <text></text>
     </graderinfo>
diff --git a/question/type/essay/tests/helper.php b/question/type/essay/tests/helper.php
index 07dd4e06ca..c759bbdb17 100644
--- a/question/type/essay/tests/helper.php
+++ b/question/type/essay/tests/helper.php
@@ -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;
     }
 
diff --git a/question/type/essay/tests/question_test.php b/question/type/essay/tests/question_test.php
index 2ac7e1661d..22ecb44b59 100644
--- a/question/type/essay/tests/question_test.php
+++ b/question/type/essay/tests/question_test.php
@@ -160,6 +160,7 @@ class qtype_essay_question_test extends advanced_testcase {
         $essay->responserequired = 1;
         $essay->attachmentsrequired = 0;
         $essay->responseformat = 'editor';
+        $essay->filetypeslist = '';
 
         // The empty string should be considered an incomplete response, as should a lack of a response.
         $this->assertFalse($essay->is_complete_response(array('answer' => '')));
@@ -172,6 +173,7 @@ class qtype_essay_question_test extends advanced_testcase {
 
         // Test the case where two files are required.
         $essay->attachmentsrequired = 2;
+        $essay->filetypeslist = '';
 
         // Attaching less than two files should result in an incomplete response.
         $this->assertFalse($essay->is_complete_response(array('answer' => 'A')));
@@ -202,6 +204,7 @@ class qtype_essay_question_test extends advanced_testcase {
 
         // Test the case in which both the response and online text are optional.
         $essay->attachmentsrequired = 0;
+        $essay->filetypeslist = '';
 
         // Providing no answer and no attachment should result in an incomplete
         // response.
diff --git a/question/type/essay/tests/walkthrough_test.php b/question/type/essay/tests/walkthrough_test.php
index d39894695a..a1a9bb9963 100644
--- a/question/type/essay/tests/walkthrough_test.php
+++ b/question/type/essay/tests/walkthrough_test.php
@@ -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();
+    }
 }
