Merge branch 'MDL-58411-master' of https://github.com/lucaboesch/moodle

This commit is contained in:
Jun Pataleta 2018-04-03 16:44:43 +08:00
commit 5a0dd0e50d
16 changed files with 334 additions and 21 deletions

View 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);

View 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);

View 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"/>

View 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;
}

View 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();

View 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';

View 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'];

View 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) {

View 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) {

View File

@ -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"

View File

@ -27,6 +27,7 @@
<responsefieldlines>15</responsefieldlines>
<attachments>0</attachments>
<attachmentsrequired>0</attachmentsrequired>
<filetypeslist></filetypeslist>
<graderinfo format="html">
<text></text>
</graderinfo>

View 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;
}

View 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();
}
}

View File

@ -26,7 +26,7 @@
defined('MOODLE_INTERNAL') || die();
$plugin->component = 'qtype_essay';
$plugin->version = 2017111300;
$plugin->version = 2018021800;
$plugin->requires = 2017110800;

View 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)) {

View 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 ===