Skip to content

Commit

Permalink
MDL-58411 qtype_essay: Add file type validation in essay question type
Browse files Browse the repository at this point in the history
  • Loading branch information
lucaboesch committed Apr 3, 2018
1 parent 39fab18 commit 94cb5a6
Show file tree
Hide file tree
Showing 16 changed files with 334 additions and 21 deletions.
30 changes: 29 additions & 1 deletion question/behaviour/manualgraded/behaviour.php
Expand Up @@ -65,6 +65,34 @@ public function process_action(question_attempt_pending_step $pendingstep) {
}
}

/**
* 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);
Expand All @@ -81,7 +109,7 @@ public function process_finish(question_attempt_pending_step $pendingstep) {
}

$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);
Expand Down
Expand Up @@ -51,7 +51,7 @@ protected function define_question_plugin_structure() {
$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);
Expand Down
1 change: 1 addition & 0 deletions question/type/essay/db/install.xml
Expand Up @@ -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"/>
Expand Down
18 changes: 17 additions & 1 deletion question/type/essay/db/upgrade.php
Expand Up @@ -30,7 +30,9 @@
* @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.
Expand All @@ -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;
}
5 changes: 5 additions & 0 deletions question/type/essay/edit_essay_form.php
Expand Up @@ -65,6 +65,10 @@ protected function definition_inner($mform) {
$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)));
Expand All @@ -88,6 +92,7 @@ protected function data_preprocessing($question) {
$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();
Expand Down
3 changes: 3 additions & 0 deletions question/type/essay/lang/en/qtype_essay.php
Expand Up @@ -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';
Expand All @@ -38,6 +40,7 @@
$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';
Expand Down
27 changes: 27 additions & 0 deletions question/type/essay/question.php
Expand Up @@ -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);
}
Expand Down Expand Up @@ -98,6 +101,18 @@ public function is_complete_response(array $response) {

// 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;
Expand All @@ -114,6 +129,18 @@ public function is_complete_response(array $response) {
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'];
Expand Down
7 changes: 7 additions & 0 deletions question/type/essay/questiontype.php
Expand Up @@ -67,6 +67,11 @@ public function save_question_options($formdata) {
$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'];
Expand All @@ -86,6 +91,8 @@ protected function initialise_question_instance(question_definition $question, $
$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) {
Expand Down
12 changes: 11 additions & 1 deletion question/type/essay/renderer.php
Expand Up @@ -119,12 +119,22 @@ public function files_input(question_attempt $qa, $numallowed,

$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) {
Expand Down
75 changes: 75 additions & 0 deletions question/type/essay/tests/behat/file_type_restriction.feature
@@ -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"
1 change: 1 addition & 0 deletions question/type/essay/tests/fixtures/testquestion.moodle.xml
Expand Up @@ -27,6 +27,7 @@
<responsefieldlines>15</responsefieldlines>
<attachments>0</attachments>
<attachmentsrequired>0</attachmentsrequired>
<filetypeslist></filetypeslist>
<graderinfo format="html">
<text></text>
</graderinfo>
Expand Down
18 changes: 18 additions & 0 deletions question/type/essay/tests/helper.php
Expand Up @@ -53,6 +53,7 @@ protected function initialise_essay_question() {
$q->responsefieldlines = 10;
$q->attachments = 0;
$q->attachmentsrequired = 0;
$q->filetypeslist = '';
$q->graderinfo = '';
$q->graderinfoformat = FORMAT_HTML;
$q->qtype = question_bank::get_qtype('essay');
Expand Down Expand Up @@ -87,6 +88,7 @@ public function get_essay_question_form_data_editor() {
$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);

Expand All @@ -105,6 +107,19 @@ public function make_essay_question_editorfilepicker() {
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
Expand All @@ -124,6 +139,7 @@ public function get_essay_question_form_data_editorfilepicker() {
$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);

Expand Down Expand Up @@ -159,6 +175,7 @@ public function get_essay_question_form_data_plain() {
$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);

Expand Down Expand Up @@ -191,6 +208,7 @@ public function make_essay_question_noinline() {
$q->responseformat = 'noinline';
$q->attachments = 3;
$q->attachmentsrequired = 1;
$q->filetypeslist = '';
return $q;
}

Expand Down

0 comments on commit 94cb5a6

Please sign in to comment.