diff --git a/classes/question/file.php b/classes/question/file.php
new file mode 100644
index 00000000..4196a798
--- /dev/null
+++ b/classes/question/file.php
@@ -0,0 +1,183 @@
+.
+
+/**
+ * This file contains the parent class for text question types.
+ *
+ * @author Laurent David
+ * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
+ * @package questiontypes
+ */
+
+namespace mod_questionnaire\question;
+
+use context_module;
+use core_media_manager;
+use form_filemanager;
+use mod_questionnaire\file_storage;
+use moodle_url;
+use stdClass;
+
+defined('MOODLE_INTERNAL') || die();
+
+class file extends question {
+
+ /**
+ * @return string
+ */
+ public function helpname() {
+ return 'file';
+ }
+
+ /**
+ * Override and return a form template if provided. Output of question_survey_display is iterpreted based on this.
+ *
+ * @return boolean | string
+ */
+ public function question_template() {
+ return false;
+ }
+
+ /**
+ * Override and return a response template if provided. Output of response_survey_display is iterpreted based on this.
+ *
+ * @return boolean | string
+ */
+ public function response_template() {
+ return false;
+ }
+
+ /**
+ * @return object|string
+ */
+ protected function responseclass() {
+ return '\\mod_questionnaire\\responsetype\\file';
+ }
+
+ /**
+ * @param \mod_questionnaire\responsetype\response\response $response
+ * @param $descendantsdata
+ * @param bool $blankquestionnaire
+ * @return object|string
+ */
+ protected function question_survey_display($response, $descendantsdata, $blankquestionnaire = false) {
+ global $CFG, $PAGE;
+ require_once($CFG->libdir . '/filelib.php');
+ $elname = 'q' . $this->id;
+ $draftitemid = file_get_submitted_draft_itemid($elname);
+ $component = 'mod_questionnaire';
+ $options = $this->get_file_manager_option();
+ file_prepare_draft_area($draftitemid, $this->context->id, $component, 'file', $this->id, $options);
+ // Filemanager form element implementation is far from optimal, we need to rework this if we ever fix it...
+ require_once("$CFG->dirroot/lib/form/filemanager.php");
+
+ $fmoptions = array_merge(
+ $options,
+ [
+ 'client_id' => uniqid(),
+ 'itemid' => $draftitemid,
+ 'target' => $this->id,
+ 'name' => $elname
+ ]
+ );
+ $fm = new form_filemanager((object) $fmoptions);
+ $output = $PAGE->get_renderer('core', 'files');
+ $html = $output->render($fm);
+
+ $html .= '';
+ $html .= '';
+
+ return $html;
+ }
+
+ private function get_file_manager_option() {
+ return [
+ 'mainfile' => '',
+ 'subdirs' => false,
+ 'accepted_types' => array('image', '.pdf')
+ ];
+ }
+
+ /**
+ * @param \mod_questionnaire\responsetype\response\response $response
+ * @return object|string
+ */
+ protected function response_survey_display($response) {
+ global $PAGE, $CFG;
+ require_once($CFG->libdir . '/filelib.php');
+ require_once($CFG->libdir . '/resourcelib.php');
+ if (isset($response->answers[$this->id])) {
+ $answer = reset($response->answers[$this->id]);
+ } else {
+ return '';
+ }
+ $fs = get_file_storage();
+ $file = $fs->get_file_by_id($answer->value);
+
+ $moodleurl = moodle_url::make_pluginfile_url(
+ $file->get_contextid(),
+ $file->get_component(),
+ $file->get_filearea(),
+ $file->get_itemid(),
+ $file->get_filepath(),
+ $file->get_filename());
+
+ $mimetype = $file->get_mimetype();
+ $title = '';
+
+ $extension = resourcelib_get_extension($file->get_filename());
+
+ $mediamanager = core_media_manager::instance($PAGE);
+ $embedoptions = array(
+ core_media_manager::OPTION_TRUSTED => true,
+ core_media_manager::OPTION_BLOCK => true,
+ );
+
+ if (file_mimetype_in_typegroup($mimetype, 'web_image')) { // It's an image
+ $code = resourcelib_embed_image($moodleurl->out(), $title);
+
+ } else if ($mimetype === 'application/pdf') {
+ // PDF document
+ $code = resourcelib_embed_pdf($moodleurl->out(), $title, get_string('view'));
+
+ } else if ($mediamanager->can_embed_url($moodleurl, $embedoptions)) {
+ // Media (audio/video) file.
+ $code = $mediamanager->embed_url($moodleurl, $title, 0, 0, $embedoptions);
+
+ } else {
+ // We need a way to discover if we are loading remote docs inside an iframe.
+ $moodleurl->param('embed', 1);
+
+ // anything else - just try object tag enlarged as much as possible
+ $code = resourcelib_embed_general($moodleurl, $title, get_string('view'), $mimetype);
+ }
+
+ $output = '';
+ $output .= '
';
+ $output .= $code;
+ $output .= '
';
+ return $output;
+ }
+
+ protected function form_length(\MoodleQuickForm $mform, $helpname = '') {
+ return question::form_length_hidden($mform);
+ }
+
+ protected function form_precise(\MoodleQuickForm $mform, $helpname = '') {
+ return question::form_precise_hidden($mform);
+ }
+
+}
diff --git a/classes/question/question.php b/classes/question/question.php
index b3270f96..f997b9db 100644
--- a/classes/question/question.php
+++ b/classes/question/question.php
@@ -47,6 +47,7 @@
define('QUESRATE', 8);
define('QUESDATE', 9);
define('QUESNUMERIC', 10);
+define('QUESFILE', 12);
define('QUESPAGEBREAK', 99);
define('QUESSECTIONTEXT', 100);
@@ -113,6 +114,7 @@ abstract class question {
QUESDROP => 'drop',
QUESRATE => 'rate',
QUESDATE => 'date',
+ QUESFILE => 'file',
QUESNUMERIC => 'numerical',
QUESPAGEBREAK => 'pagebreak',
QUESSECTIONTEXT => 'sectiontext'
diff --git a/classes/responsetype/file.php b/classes/responsetype/file.php
new file mode 100644
index 00000000..c17cfa3e
--- /dev/null
+++ b/classes/responsetype/file.php
@@ -0,0 +1,350 @@
+.
+
+/**
+ * This file contains the parent class for questionnaire question types.
+ *
+ * @author Laurent David
+ * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
+ * @package questiontypes
+ */
+
+namespace mod_questionnaire\responsetype;
+defined('MOODLE_INTERNAL') || die();
+
+use mod_questionnaire\db\bulk_sql_config;
+use moodle_url;
+
+/**
+ * Class for text response types.
+ *
+ * @author Laurent David
+ * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
+ * @package questiontypes
+ */
+
+class file extends responsetype {
+ /**
+ * @return string
+ */
+ public static function response_table() {
+ return 'questionnaire_response_file';
+ }
+
+ /**
+ * Provide an array of answer objects from web form data for the question.
+ *
+ * @param \stdClass $responsedata All of the responsedata as an object.
+ * @param \mod_questionnaire\question\question $question
+ * @return array \mod_questionnaire\responsetype\answer\answer An array of answer objects.
+ */
+ public static function answers_from_webform($responsedata, $question) {
+ $answers = [];
+ if (isset($responsedata->{'q'.$question->id}) && (strlen($responsedata->{'q'.$question->id}) > 0)) {
+ $val = $responsedata->{'q' . $question->id};
+ $record = new \stdClass();
+ $record->responseid = $responsedata->rid;
+ $record->questionid = $question->id;
+
+ file_save_draft_area_files($val, $question->context->id, 'mod_questionnaire', 'file', $responsedata->rid);
+ $fs = get_file_storage();
+ $files = $fs->get_area_files($question->context->id, 'mod_questionnaire', 'file', $responsedata->rid, "itemid, filepath, filename",
+ false);
+ $file = reset($files);
+ $record->value = $file->get_id();
+ $answers[] = answer\answer::create_from_data($record);
+ }
+ return $answers;
+ }
+
+ /**
+ * @param \mod_questionnaire\responsetype\response\response|\stdClass $responsedata
+ * @return bool|int
+ * @throws \coding_exception
+ * @throws \dml_exception
+ */
+ public function insert_response($responsedata) {
+ global $DB;
+
+ if (!$responsedata instanceof \mod_questionnaire\responsetype\response\response) {
+ $response = \mod_questionnaire\responsetype\response\response::response_from_webform($responsedata, [$this->question]);
+ } else {
+ $response = $responsedata;
+ }
+
+ if (!empty($response) && isset($response->answers[$this->question->id][0])) {
+ $record = new \stdClass();
+ $record->response_id = $response->id;
+ $record->question_id = $this->question->id;
+ $record->fileid = intval(clean_text($response->answers[$this->question->id][0]->value));
+
+ return $DB->insert_record(static::response_table(), $record);
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * @param bool $rids
+ * @param bool $anonymous
+ * @return array
+ * @throws \coding_exception
+ * @throws \dml_exception
+ */
+ public function get_results($rids=false, $anonymous=false) {
+ global $DB;
+
+ $rsql = '';
+ if (!empty($rids)) {
+ list($rsql, $params) = $DB->get_in_or_equal($rids);
+ $rsql = ' AND response_id ' . $rsql;
+ }
+
+ if ($anonymous) {
+ $sql = 'SELECT t.id, t.fileid, r.submitted AS submitted, ' .
+ 'r.questionnaireid, r.id AS rid ' .
+ 'FROM {'.static::response_table().'} t, ' .
+ '{questionnaire_response} r ' .
+ 'WHERE question_id=' . $this->question->id . $rsql .
+ ' AND t.response_id = r.id ' .
+ 'ORDER BY r.submitted DESC';
+ } else {
+ $sql = 'SELECT t.id, t.fileid, r.submitted AS submitted, r.userid, u.username AS username, ' .
+ 'u.id as usrid, ' .
+ 'r.questionnaireid, r.id AS rid ' .
+ 'FROM {'.static::response_table().'} t, ' .
+ '{questionnaire_response} r, ' .
+ '{user} u ' .
+ 'WHERE question_id=' . $this->question->id . $rsql .
+ ' AND t.response_id = r.id' .
+ ' AND u.id = r.userid ' .
+ 'ORDER BY u.lastname, u.firstname, r.submitted';
+ }
+ return $DB->get_records_sql($sql, $params);
+ }
+
+ /**
+ * Provide a template for results screen if defined.
+ * @param bool $pdf
+ * @return mixed The template string or false/
+ */
+ public function results_template($pdf = false) {
+ if ($pdf) {
+ return 'mod_questionnaire/resultspdf_text';
+ } else {
+ return 'mod_questionnaire/results_text';
+ }
+ }
+
+ /**
+ * @param bool $rids
+ * @param string $sort
+ * @param bool $anonymous
+ * @return string
+ * @throws \coding_exception
+ */
+ public function display_results($rids=false, $sort='', $anonymous=false) {
+ if (is_array($rids)) {
+ $prtotal = 1;
+ } else if (is_int($rids)) {
+ $prtotal = 0;
+ }
+ if ($rows = $this->get_results($rids, $anonymous)) {
+ $numrespondents = count($rids);
+ $numresponses = count($rows);
+ $pagetags = $this->get_results_tags($rows, $numrespondents, $numresponses, $prtotal);
+ } else {
+ $pagetags = new \stdClass();
+ }
+ return $pagetags;
+ }
+
+ /**
+ * Override the results tags function for templates for questions with dates.
+ *
+ * @param $weights
+ * @param $participants Number of questionnaire participants.
+ * @param $respondents Number of question respondents.
+ * @param $showtotals
+ * @param string $sort
+ * @return \stdClass
+ * @throws \coding_exception
+ */
+ public function get_results_tags($weights, $participants, $respondents, $showtotals = 1, $sort = '') {
+ $pagetags = new \stdClass();
+ if ($respondents == 0) {
+ return $pagetags;
+ }
+
+ // If array element is an object, outputting non-numeric responses.
+ if (is_object(reset($weights))) {
+ global $CFG, $SESSION, $questionnaire, $DB;
+ $viewsingleresponse = $questionnaire->capabilities->viewsingleresponse;
+ $nonanonymous = $questionnaire->respondenttype != 'anonymous';
+ if ($viewsingleresponse && $nonanonymous) {
+ $currentgroupid = '';
+ if (isset($SESSION->questionnaire->currentgroupid)) {
+ $currentgroupid = $SESSION->questionnaire->currentgroupid;
+ }
+ $url = $CFG->wwwroot.'/mod/questionnaire/report.php?action=vresp&sid='.$questionnaire->survey->id.
+ '¤tgroupid='.$currentgroupid;
+ }
+ $users = [];
+ $evencolor = false;
+ foreach ($weights as $row) {
+ $response = new \stdClass();
+ $fs = get_file_storage();
+ $file = $fs->get_file_by_id($row->fileid);
+
+ $imageurl = moodle_url::make_pluginfile_url(
+ $file->get_contextid(),
+ $file->get_component(),
+ $file->get_filearea(),
+ $file->get_itemid(),
+ $file->get_filepath(),
+ $file->get_filename());
+
+ $response->text = \html_writer::link($imageurl, $file->get_filename());
+ if ($viewsingleresponse && $nonanonymous) {
+ $rurl = $url.'&rid='.$row->rid.'&individualresponse=1';
+ $title = userdate($row->submitted);
+ if (!isset($users[$row->userid])) {
+ $users[$row->userid] = $DB->get_record('user', ['id' => $row->userid]);
+ }
+ $response->respondent = ''.fullname($users[$row->userid]).'';
+ } else {
+ $response->respondent = '';
+ }
+ // The 'evencolor' attribute is used by the PDF template.
+ $response->evencolor = $evencolor;
+ $pagetags->responses[] = (object)['response' => $response];
+ $evencolor = !$evencolor;
+ }
+
+ if ($showtotals == 1) {
+ $pagetags->total = new \stdClass();
+ $pagetags->total->total = "$respondents/$participants";
+ }
+ } else {
+ $nbresponses = 0;
+ $sum = 0;
+ $strtotal = get_string('totalofnumbers', 'questionnaire');
+ $straverage = get_string('average', 'questionnaire');
+
+ if (!empty($weights) && is_array($weights)) {
+ ksort($weights);
+ $evencolor = false;
+ foreach ($weights as $text => $num) {
+ $response = new \stdClass();
+ $response->text = $text;
+ $response->respondent = $num;
+ // The 'evencolor' attribute is used by the PDF template.
+ $response->evencolor = $evencolor;
+ $nbresponses += $num;
+ $sum += $text * $num;
+ $evencolor = !$evencolor;
+ $pagetags->responses[] = (object)['response' => $response];
+ }
+
+ $response = new \stdClass();
+ $response->text = $sum;
+ $response->respondent = $strtotal;
+ $response->evencolor = $evencolor;
+ $pagetags->responses[] = (object)['response' => $response];
+ $evencolor = !$evencolor;
+
+ $response = new \stdClass();
+ $response->respondent = $straverage;
+ $avg = $sum / $nbresponses;
+ $response->text = sprintf('%.' . $this->question->precise . 'f', $avg);
+ $response->evencolor = $evencolor;
+ $pagetags->responses[] = (object)['response' => $response];
+ $evencolor = !$evencolor;
+
+ if ($showtotals == 1) {
+ $pagetags->total = new \stdClass();
+ $pagetags->total->total = "$respondents/$participants";
+ $pagetags->total->evencolor = $evencolor;
+ }
+ }
+ }
+
+ return $pagetags;
+ }
+
+ /**
+ * Return an array of answers by question/choice for the given response. Must be implemented by the subclass.
+ *
+ * @param int $rid The response id.
+ * @return array
+ */
+ static public function response_select($rid) {
+ global $DB;
+
+ $values = [];
+ $sql = 'SELECT q.id, q.content, a.fileid as aresponse '.
+ 'FROM {'.static::response_table().'} a, {questionnaire_question} q '.
+ 'WHERE a.response_id=? AND a.question_id=q.id ';
+ $records = $DB->get_records_sql($sql, [$rid]);
+ foreach ($records as $qid => $row) {
+ unset($row->id);
+ $row = (array)$row;
+ $newrow = [];
+ foreach ($row as $key => $val) {
+ if (!is_numeric($key)) {
+ $newrow[] = $val;
+ }
+ }
+ $values[$qid] = $newrow;
+ $val = array_pop($values[$qid]);
+ array_push($values[$qid], $val, $val);
+ }
+
+ return $values;
+ }
+
+ /**
+ * Return an array of answer objects by question for the given response id.
+ * THIS SHOULD REPLACE response_select.
+ *
+ * @param int $rid The response id.
+ * @return array array answer
+ * @throws \dml_exception
+ */
+ static public function response_answers_by_question($rid) {
+ global $DB;
+
+ $answers = [];
+ $sql = 'SELECT id, response_id as responseid, question_id as questionid, 0 as choiceid, fileid as value ' .
+ 'FROM {' . static::response_table() .'} ' .
+ 'WHERE response_id = ? ';
+ $records = $DB->get_records_sql($sql, [$rid]);
+ foreach ($records as $record) {
+ $answers[$record->questionid][] = answer\answer::create_from_data($record);
+ }
+
+ return $answers;
+ }
+
+ /**
+ * Configure bulk sql
+ * @return bulk_sql_config
+ */
+ protected function bulk_sql_config() {
+ return new bulk_sql_config(static::response_table(), 'qrt', false, false, false);
+ }
+}
+
diff --git a/classes/responsetype/response/response.php b/classes/responsetype/response/response.php
index 6dcd24e8..f79e2162 100644
--- a/classes/responsetype/response/response.php
+++ b/classes/responsetype/response/response.php
@@ -174,5 +174,6 @@ public function add_questions_answers() {
$this->answers += \mod_questionnaire\responsetype\boolean::response_answers_by_question($this->id);
$this->answers += \mod_questionnaire\responsetype\date::response_answers_by_question($this->id);
$this->answers += \mod_questionnaire\responsetype\text::response_answers_by_question($this->id);
+ $this->answers += \mod_questionnaire\responsetype\file::response_answers_by_question($this->id);
}
}
\ No newline at end of file
diff --git a/db/install.php b/db/install.php
index 0a459a11..c63e33bf 100644
--- a/db/install.php
+++ b/db/install.php
@@ -107,4 +107,10 @@ function xmldb_questionnaire_install() {
$questiontype->response_table = '';
$id = $DB->insert_record('questionnaire_question_type', $questiontype);
+ $questiontype = new stdClass();
+ $questiontype->typeid = 12;
+ $questiontype->type = 'File';
+ $questiontype->has_choices = 'n';
+ $questiontype->response_table = 'response_file';
+ $id = $DB->insert_record('questionnaire_question_type', $questiontype);
}
\ No newline at end of file
diff --git a/db/install.xml b/db/install.xml
index a1b7af0b..4fdc7ff7 100644
--- a/db/install.xml
+++ b/db/install.xml
@@ -225,6 +225,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/db/upgrade.php b/db/upgrade.php
index ca290ae7..4d36eb4b 100644
--- a/db/upgrade.php
+++ b/db/upgrade.php
@@ -978,6 +978,43 @@ function xmldb_questionnaire_upgrade($oldversion=0) {
// Questionnaire savepoint reached.
upgrade_mod_savepoint(true, 2020062301, 'questionnaire');
}
+ if ($oldversion < 2023021402) {
+ $questiontype = new stdClass();
+ $questiontype->typeid = 11;
+ $questiontype->type = 'File';
+ $questiontype->has_choices = 'n';
+ $questiontype->response_table = 'response_file';
+ $id = $DB->insert_record('questionnaire_question_type', $questiontype);
+
+ // Questionnaire savepoint reached.
+ upgrade_mod_savepoint(true, 2023021402, 'questionnaire');
+ }
+ if ($oldversion < 2023021403) {
+
+ // Define table questionnaire_response_file to be created.
+ $table = new xmldb_table('questionnaire_response_file');
+
+ // Adding fields to table questionnaire_response_file.
+ $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
+ $table->add_field('response_id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
+ $table->add_field('question_id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
+ $table->add_field('fileid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
+
+ // Adding keys to table questionnaire_response_file.
+ $table->add_key('primary', XMLDB_KEY_PRIMARY, ['id']);
+ $table->add_key('file_fk', XMLDB_KEY_FOREIGN, ['fileid'], 'files', ['id']);
+
+ // Adding indexes to table questionnaire_response_file.
+ $table->add_index('response_question', XMLDB_INDEX_NOTUNIQUE, ['response_id', 'question_id']);
+
+ // Conditionally launch create table for questionnaire_response_file.
+ if (!$dbman->table_exists($table)) {
+ $dbman->create_table($table);
+ }
+
+ // Questionnaire savepoint reached.
+ upgrade_mod_savepoint(true, 2023021403, 'questionnaire');
+ }
return $result;
}
diff --git a/lang/en/questionnaire.php b/lang/en/questionnaire.php
index e4e86c17..92a172e9 100644
--- a/lang/en/questionnaire.php
+++ b/lang/en/questionnaire.php
@@ -243,6 +243,8 @@
respondent.
Default values are 20 characters for the Input Box width and 25 characters for the maximum length of text entered.';
+$string['file'] = 'File';
+$string['file_help'] = 'Allow user to submit a file (only simple, virus safe formats are allowed)';
$string['finished'] = 'You have answered all the questions in this questionnaire!';
$string['firstrespondent'] = 'First Respondent';
$string['formateditor'] = 'HTML editor';
diff --git a/lib.php b/lib.php
index ff0958e2..6eabea1c 100644
--- a/lib.php
+++ b/lib.php
@@ -469,7 +469,7 @@ function questionnaire_scale_used_anywhere($scaleid) {
*
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
-function questionnaire_pluginfile($course, $cm, $context, $filearea, $args, $forcedownload) {
+function questionnaire_pluginfile($course, $cm, $context, $filearea, $args, $forcedownload, $options) {
global $DB;
if ($context->contextlevel != CONTEXT_MODULE) {
@@ -478,7 +478,7 @@ function questionnaire_pluginfile($course, $cm, $context, $filearea, $args, $for
require_course_login($course, true, $cm);
- $fileareas = ['intro', 'info', 'thankbody', 'question', 'feedbacknotes', 'sectionheading', 'feedback'];
+ $fileareas = ['intro', 'info', 'thankbody', 'question', 'feedbacknotes', 'sectionheading', 'feedback', 'file'];
if (!in_array($filearea, $fileareas)) {
return false;
}
@@ -497,6 +497,10 @@ function questionnaire_pluginfile($course, $cm, $context, $filearea, $args, $for
if (!$DB->record_exists('questionnaire_feedback', ['id' => $componentid])) {
return false;
}
+ } else if ($filearea == 'file') {
+ if (!$DB->record_exists('questionnaire_response_file', ['response_id' => $componentid])) {
+ return false;
+ }
} else {
if (!$DB->record_exists('questionnaire_survey', ['id' => $componentid])) {
return false;
@@ -515,7 +519,7 @@ function questionnaire_pluginfile($course, $cm, $context, $filearea, $args, $for
}
// Finally send the file.
- send_stored_file($file, 0, 0, true); // Download MUST be forced - security!
+ send_stored_file($file, null, 0, $forcedownload, $options); // Download MUST be forced - security!
}
/**
* Adds module specific settings to the settings block
diff --git a/locallib.php b/locallib.php
index 6287a7c6..d7de15ce 100644
--- a/locallib.php
+++ b/locallib.php
@@ -445,6 +445,8 @@ function questionnaire_get_type ($id) {
return get_string('date', 'questionnaire');
case 10:
return get_string('numeric', 'questionnaire');
+ case 12:
+ return get_string('file', 'questionnaire');
case 100:
return get_string('sectiontext', 'questionnaire');
case 99:
diff --git a/tests/behat/add_questions.feature b/tests/behat/add_questions.feature
index 54b4fdf0..4f020693 100644
--- a/tests/behat/add_questions.feature
+++ b/tests/behat/add_questions.feature
@@ -99,4 +99,8 @@ Feature: Add questions to a questionnaire activity
And I should see "Choose yes or no"
And I set the field "id_type_id" to "----- Page Break -----"
And I press "Add selected question type"
+ And I add a "File" question and I fill the form with:
+ | Question Name | Q10 |
+ | Yes | Yes |
+ | Question Text | Add a file as an answer |
Then I should see "[----- Page Break -----]"
diff --git a/tests/behat/behat_mod_questionnaire.php b/tests/behat/behat_mod_questionnaire.php
index 88769895..e7bacd90 100644
--- a/tests/behat/behat_mod_questionnaire.php
+++ b/tests/behat/behat_mod_questionnaire.php
@@ -63,7 +63,8 @@ public function i_add_a_question_and_i_fill_the_form_with($questiontype, TableNo
'Radio Buttons',
'Rate (scale 1..5)',
'Text Box',
- 'Yes/No');
+ 'Yes/No',
+ 'File');
if (!in_array($questiontype, $validtypes)) {
throw new ExpectationException('Invalid question type specified.', $this->getSession());
@@ -388,4 +389,200 @@ private function add_data(array $data, $datatable, $mapvar = '', array $replvars
}
}
+
+ /**
+ * Uploads a file to the specified filemanager leaving other fields in upload form default. The paths should be relative to moodle codebase.
+ *
+ * @When /^I upload "(?P(?:[^"]|\\")*)" to questionnaire filemanager$/
+ * @throws DriverException
+ * @throws ExpectationException Thrown by behat_base::find
+ * @param string $filepath
+ */
+ public function i_upload_file_to_questionnaire_filemanager($filepath) {
+ $this->upload_file_to_filemanager_questionnaire($filepath, new TableNode(array()));
+ }
+
+ /**
+ * Try to get the filemanager node specified by the element
+ *
+ * @param string $filepickerelement
+ * @return \Behat\Mink\Element\NodeElement
+ * @throws ExpectationException
+ */
+ protected function get_filemanager() {
+
+ // If no file picker label is mentioned take the first file picker from the page.
+ return $this->find(
+ 'xpath',
+ '//div[contains(concat(" ", normalize-space(@class), " "), " filemanager ")]'
+ );
+ }
+
+ /**
+ * Uploads a file to filemanager
+ *
+ * @throws DriverException
+ * @throws ExpectationException Thrown by behat_base::find
+ * @param string $filepath Normally a path relative to $CFG->dirroot, but can be an absolute path too.
+ * @param string $filemanagerelement
+ * @param TableNode $data Data to fill in upload form
+ * @param false|string $overwriteaction false if we don't expect that file with the same name already exists,
+ * or button text in overwrite dialogue ("Overwrite", "Rename to ...", "Cancel")
+ */
+ protected function upload_file_to_filemanager_questionnaire($filepath, TableNode $data, $overwriteaction = false) {
+ global $CFG;
+
+ if (!$this->has_tag('_file_upload')) {
+ throw new DriverException('File upload tests must have the @_file_upload tag on either the scenario or feature.');
+ }
+
+ $filemanagernode = $this->get_filemanager();
+
+ // Opening the select repository window and selecting the upload repository.
+ $this->open_add_file_window($filemanagernode, get_string('pluginname', 'repository_upload'));
+
+ // Ensure all the form is ready.
+ $noformexception = new ExpectationException('The upload file form is not ready', $this->getSession());
+ $this->find(
+ 'xpath',
+ "//div[contains(concat(' ', normalize-space(@class), ' '), ' container ')]" .
+ "[contains(concat(' ', normalize-space(@class), ' '), ' repository_upload ')]" .
+ "/descendant::div[contains(concat(' ', normalize-space(@class), ' '), ' file-picker ')]" .
+ "/descendant::div[contains(concat(' ', normalize-space(@class), ' '), ' fp-content ')]" .
+ "/descendant::div[contains(concat(' ', normalize-space(@class), ' '), ' fp-upload-form ')]" .
+ "/descendant::form",
+ $noformexception
+ );
+ // After this we have the elements we want to interact with.
+
+ // Form elements to interact with.
+ $file = $this->find_file('repo_upload_file');
+
+ // Attaching specified file to the node.
+ // Replace 'admin/' if it is in start of path with $CFG->admin .
+ if (substr($filepath, 0, 6) === 'admin/') {
+ $filepath = $CFG->dirroot . DIRECTORY_SEPARATOR . $CFG->admin .
+ DIRECTORY_SEPARATOR . substr($filepath, 6);
+ }
+ $filepath = str_replace('/', DIRECTORY_SEPARATOR, $filepath);
+ if (!is_readable($filepath)) {
+ $filepath = $CFG->dirroot . DIRECTORY_SEPARATOR . $filepath;
+ if (!is_readable($filepath)) {
+ throw new ExpectationException('The file to be uploaded does not exist.', $this->getSession());
+ }
+ }
+ $file->attachFile($filepath);
+
+ // Fill the form in Upload window.
+ $datahash = $data->getRowsHash();
+
+ // The action depends on the field type.
+ foreach ($datahash as $locator => $value) {
+
+ $field = behat_field_manager::get_form_field_from_label($locator, $this);
+
+ // Delegates to the field class.
+ $field->set_value($value);
+ }
+
+ // Submit the file.
+ $submit = $this->find_button(get_string('upload', 'repository'));
+ $submit->press();
+
+ // We wait for all the JS to finish as it is performing an action.
+ $this->getSession()->wait(self::get_timeout(), self::PAGE_READY_JS);
+
+ if ($overwriteaction !== false) {
+ $overwritebutton = $this->find_button($overwriteaction);
+ $this->ensure_node_is_visible($overwritebutton);
+ $overwritebutton->click();
+
+ // We wait for all the JS to finish.
+ $this->getSession()->wait(self::get_timeout(), self::PAGE_READY_JS);
+ }
+
+ }
+
+ /**
+ * Try to get the filemanager node specified by the element
+ *
+ * @param string $filepickerelement
+ * @return \Behat\Mink\Element\NodeElement
+ * @throws ExpectationException
+ */
+ protected function get_filepicker_node($filepickerelement) {
+
+ // More info about the problem (in case there is a problem).
+ $exception = new ExpectationException('"' . $filepickerelement . '" filepicker can not be found', $this->getSession());
+
+ // If no file picker label is mentioned take the first file picker from the page.
+ if (empty($filepickerelement)) {
+ $filepickercontainer = $this->find(
+ 'xpath',
+ "//*[@class=\"form-filemanager\"]",
+ $exception
+ );
+ } else {
+ // Gets the filemanager node specified by the locator which contains the filepicker container
+ // either for filepickers created by mform or by admin config.
+ $filepickerelement = behat_context_helper::escape($filepickerelement);
+ $filepickercontainer = $this->find(
+ 'xpath',
+ "//input[./@id = substring-before(//p[normalize-space(.)=$filepickerelement]/@id, '_label')]" .
+ "//ancestor::*[@data-fieldtype = 'filemanager' or @data-fieldtype = 'filepicker']",
+ $exception
+ );
+ }
+
+ return $filepickercontainer;
+ }
+ /**
+ * Opens the filepicker modal window and selects the repository.
+ *
+ * @throws ExpectationException Thrown by behat_base::find
+ * @param NodeElement $filemanagernode The filemanager or filepicker form element DOM node.
+ * @param mixed $repositoryname The repo name.
+ * @return void
+ */
+ protected function open_add_file_window($filemanagernode, $repositoryname) {
+ $exception = new ExpectationException('No files can be added to the specified filemanager', $this->getSession());
+
+ // We should deal with single-file and multiple-file filemanagers,
+ // catching the exception thrown by behat_base::find() in case is not multiple
+ $this->execute('behat_general::i_click_on_in_the', [
+ 'div.fp-btn-add a, input.fp-btn-choose', 'css_element',
+ $filemanagernode, 'NodeElement'
+ ]);
+
+ // Wait for the default repository (if any) to load. This checks that
+ // the relevant div exists and that it does not include the loading image.
+ $this->ensure_element_exists(
+ "//div[contains(concat(' ', normalize-space(@class), ' '), ' file-picker ')]" .
+ "//div[contains(concat(' ', normalize-space(@class), ' '), ' fp-content ')]" .
+ "[not(descendant::div[contains(concat(' ', normalize-space(@class), ' '), ' fp-content-loading ')])]",
+ 'xpath_element');
+
+ // Getting the repository link and opening it.
+ $repoexception = new ExpectationException('The "' . $repositoryname . '" repository has not been found', $this->getSession());
+
+ // Avoid problems with both double and single quotes in the same string.
+ $repositoryname = behat_context_helper::escape($repositoryname);
+
+ // Here we don't need to look inside the selected element because there can only be one modal window.
+ $repositorylink = $this->find(
+ 'xpath',
+ "//div[contains(concat(' ', normalize-space(@class), ' '), ' fp-repo-area ')]" .
+ "//descendant::span[contains(concat(' ', normalize-space(@class), ' '), ' fp-repo-name ')]" .
+ "[normalize-space(.)=$repositoryname]",
+ $repoexception
+ );
+
+ // Selecting the repo.
+ $this->ensure_node_is_visible($repositorylink);
+ if (!$repositorylink->getParent()->getParent()->hasClass('active')) {
+ // If the repository link is active, then the repository is already loaded.
+ // Clicking it while it's active causes issues, so only click it when it isn't (see MDL-51014).
+ $this->execute('behat_general::i_click_on', [$repositorylink, 'NodeElement']);
+ }
+ }
}
diff --git a/tests/behat/file_question.feature b/tests/behat/file_question.feature
new file mode 100644
index 00000000..d067f117
--- /dev/null
+++ b/tests/behat/file_question.feature
@@ -0,0 +1,50 @@
+@mod @mod_questionnaire
+Feature: In questionnaire, we can add a question requiring a file upload.
+
+ @javascript @_file_upload
+ Scenario: As a teacher, I create a questionnaire in my course with a file question and a student answers to it. Then the file has to be accessible.
+ Given the following "users" exist:
+ | username | firstname | lastname | email |
+ | teacher1 | Teacher | 1 | teacher1@example.com |
+ | student1 | Student | 1 | student1@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 "activities" exist:
+ | activity | name | description | course | idnumber | resume | navigate |
+ | questionnaire | Test questionnaire | Test questionnaire description | C1 | questionnaire0 | 1 | 1 |
+
+ When I log in as "teacher1"
+ And I am on "Course 1" course homepage
+ And I follow "Test questionnaire"
+ And I navigate to "Questions" in current page administration
+ Then I should see "Add questions"
+ And I add a "File" question and I fill the form with:
+ | Question Name | File question |
+ | Yes | Yes |
+ | Question Text | Add a file as an answer |
+ And I log out
+
+ Given I log in as "student1"
+ And I am on "Course 1" course homepage
+ And I follow "Test questionnaire"
+ And I navigate to "Answer the questions..." in current page administration
+ When I upload "mod/questionnaire/tests/fixtures/testfilequestion.pdf" to questionnaire filemanager
+ And I press "Submit questionnaire"
+ Then I should see "Thank you for completing this Questionnaire."
+ And I press "Continue"
+ And I should see "Your response"
+ And I log out
+
+ Given I log in as "teacher1"
+ And I am on "Course 1" course homepage
+ And I follow "Test questionnaire"
+ When I navigate to "View All Responses" in current page administration
+ Then I should see "testfilequestion.pdf"
+ And I follow "student1"
+ # Todo find how to check if the pdf viewer is there.
+ And I log out
diff --git a/tests/fixtures/testfilequestion.pdf b/tests/fixtures/testfilequestion.pdf
new file mode 100644
index 00000000..90588c30
Binary files /dev/null and b/tests/fixtures/testfilequestion.pdf differ
diff --git a/tests/questiontypes_test.php b/tests/questiontypes_test.php
index cc9e95f9..9302893e 100644
--- a/tests/questiontypes_test.php
+++ b/tests/questiontypes_test.php
@@ -29,6 +29,7 @@
global $CFG;
require_once($CFG->dirroot.'/mod/questionnaire/locallib.php');
+require_once($CFG->dirroot.'/mod/questionnaire/classes/question/question.php'); // Import type question ids.
/**
* Unit tests for {@link questionnaire_questiontypes_testcase}.
@@ -90,6 +91,9 @@ public function test_create_question_yesno() {
$this->create_test_question(QUESYESNO, '\\mod_questionnaire\\question\\yesno', array('content' => 'Enter yes or no'));
}
+ public function test_create_question_file() {
+ $this->create_test_question(QUESFILE, '\\mod_questionnaire\\question\\file', []);
+ }
// General tests to call from specific tests above.
diff --git a/version.php b/version.php
index 92f2be6e..371a0954 100644
--- a/version.php
+++ b/version.php
@@ -24,7 +24,7 @@
defined('MOODLE_INTERNAL') || die();
-$plugin->version = 2020062302; // The current module version (Date: YYYYMMDDXX)
+$plugin->version = 2023021403; // The current module version (Date: YYYYMMDDXX)
$plugin->requires = 2019052000; // Moodle version (3.7).
$plugin->component = 'mod_questionnaire';