From 989338f34d33751e2d2defa072825833c225ea4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luca=20B=C3=B6sch?= Date: Wed, 21 Aug 2024 21:56:21 +0200 Subject: [PATCH 01/11] Circumvent number on string error, resolves #538. (#563) --- classes/question/numerical.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/classes/question/numerical.php b/classes/question/numerical.php index 238beb42..19f8318f 100644 --- a/classes/question/numerical.php +++ b/classes/question/numerical.php @@ -82,7 +82,7 @@ protected function question_survey_display($response, $descendantsdata, $blankqu // Numeric. $questiontags = new \stdClass(); $precision = $this->precise; - $a = new \StdClass(); + $a = new \stdClass(); if (isset($response->answers[$this->id][0])) { $mynumber = $response->answers[$this->id][0]->value; if ($mynumber != '') { From 2cca738ef406090095d012b26dd42d7112f22e28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luca=20B=C3=B6sch?= Date: Sun, 15 Oct 2023 23:01:46 +0200 Subject: [PATCH 02/11] Fixing Moodle Code Checker issues and prepare for Moodle 4.3 --- .../questionnaire_activity_completion.feature | 4 +- tests/csvexport_test.php | 13 +-- tests/custom_completion_test.php | 6 +- tests/generator_test.php | 18 ++-- tests/lib_test.php | 93 ++++++------------- tests/privacy_provider_test.php | 9 +- tests/questiontypes_test.php | 84 ++++++----------- tests/responsetypes_test.php | 70 ++++---------- 8 files changed, 91 insertions(+), 206 deletions(-) diff --git a/tests/behat/questionnaire_activity_completion.feature b/tests/behat/questionnaire_activity_completion.feature index a9082e39..32e9e856 100644 --- a/tests/behat/questionnaire_activity_completion.feature +++ b/tests/behat/questionnaire_activity_completion.feature @@ -108,8 +108,8 @@ Feature: View activity completion information in the questionnaire activity Then I should see "Are you still in School?" And I should see "Select one choice" And I should see "Enter some text" - And I set the field "Yes" to "1" - And I set the field "Three" to "1" + And I set the field with xpath "//input[@type='radio' and @id='auto-rb0001']" to "1" + And I set the field with xpath "//input[@type='radio' and @id='auto-rb0005']" to "1" And I press "Submit questionnaire" Then I should see "Thank you for completing this Questionnaire." And I press "Continue" diff --git a/tests/csvexport_test.php b/tests/csvexport_test.php index 714f3da9..6c58089d 100644 --- a/tests/csvexport_test.php +++ b/tests/csvexport_test.php @@ -14,15 +14,6 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . -/** - * Performance test for questionnaire module. - * @package mod_questionnaire - * @group mod_questionnaire - * @author Guy Thomas - * @copyright Copyright (c) 2015 Moodlerooms Inc. (http://www.moodlerooms.com) - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - namespace mod_questionnaire; /** @@ -59,9 +50,9 @@ private function get_csv_text(array $rows) { } /** - * Tests the CSV export. + * Test case for the csvexport method. * - * @covers \questionnaire::generate_csv + * @covers ::csvexport */ public function test_csvexport(): void { $this->resetAfterTest(); diff --git a/tests/custom_completion_test.php b/tests/custom_completion_test.php index af003e38..6d8a5b9f 100644 --- a/tests/custom_completion_test.php +++ b/tests/custom_completion_test.php @@ -116,7 +116,7 @@ public function test_get_state(string $rule, int $available, ?bool $submitted, ? /** * Test for get_defined_custom_rules(). * - * @covers \mod_questionnaire\completion\custom_completion + * @covers \mod_questionnaire\completion\custom_completion::get_defined_custom_rules */ public function test_get_defined_custom_rules(): void { $rules = custom_completion::get_defined_custom_rules(); @@ -127,7 +127,7 @@ public function test_get_defined_custom_rules(): void { /** * Test for get_defined_custom_rule_descriptions(). * - * @covers \mod_questionnaire\completion\custom_completion + * @covers \mod_questionnaire\completion\custom_completion::get_custom_rule_descriptions */ public function test_get_custom_rule_descriptions(): void { // Get defined custom rules. @@ -155,7 +155,7 @@ public function test_get_custom_rule_descriptions(): void { /** * Test for is_defined(). * - * @covers \mod_questionnaire\completion\custom_completion + * @covers \mod_questionnaire\completion\custom_completion::is_defined */ public function test_is_defined(): void { // Build a mock cm_info instance. diff --git a/tests/generator_test.php b/tests/generator_test.php index 455230e8..b90d0504 100644 --- a/tests/generator_test.php +++ b/tests/generator_test.php @@ -14,6 +14,8 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . +namespace mod_questionnaire; + /** * PHPUnit questionnaire generator tests * @@ -31,13 +33,9 @@ */ final class generator_test extends \advanced_testcase { /** - * Test generator create_instance function. + * Test case for the create_instance function. * - * @return void - * @throws coding_exception - * @throws dml_exception - * - * @covers \mod_questionnaire\generator\ + * @covers \mod_questionnaire_generator::create_instance */ public function test_create_instance(): void { global $DB; @@ -74,13 +72,9 @@ public function test_create_instance(): void { } /** - * Test generator create_content function. - * - * @return void - * @throws coding_exception - * @throws dml_exception + * Test case for the create_content function. * - * @covers \mod_questionnaire\generator\ + * @covers \mod_questionnaire_generator::create_content */ public function test_create_content(): void { global $DB; diff --git a/tests/lib_test.php b/tests/lib_test.php index 952c52a8..91b93d92 100644 --- a/tests/lib_test.php +++ b/tests/lib_test.php @@ -14,15 +14,6 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . -/** - * PHPUnit questionnaire generator tests - * - * @package mod_questionnaire - * @copyright 2015 Mike Churchward (mike@churchward.ca) - * @author Mike Churchward - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - namespace mod_questionnaire; use mod_questionnaire\question\question; @@ -34,16 +25,18 @@ require_once($CFG->dirroot . '/mod/questionnaire/classes/question/question.php'); /** - * Unit tests for questionnaire_lib_testcase. - * @group mod_questionnaire + * PHPUnit questionnaire lib tests + * + * @package mod_questionnaire + * @copyright 2015 Mike Churchward (mike@churchward.ca) + * @author Mike Churchward + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class lib_test extends \advanced_testcase { /** - * Test for questionnaire_supports. - * - * @return void + * Test case for the questionnaire_supports function. * - * @covers \questionnaire_supports + * @covers ::questionnaire_supports */ public function test_questionnaire_supports(): void { $this->assertTrue(questionnaire_supports(FEATURE_BACKUP_MOODLE2)); @@ -59,11 +52,9 @@ public function test_questionnaire_supports(): void { } /** - * Test for questionnaire_get_extra_capabilities. + * Test case for the questionnaire_get_extra_capabilities function. * - * @return void - * - * @covers \questionnaire_get_extra_capabilities + * @covers ::questionnaire_get_extra_capabilities */ public function test_questionnaire_get_extra_capabilities(): void { $caps = questionnaire_get_extra_capabilities(); @@ -73,12 +64,9 @@ public function test_questionnaire_get_extra_capabilities(): void { } /** - * Test for questionnaire_add_instance. - * - * @return void - * @throws moodle_exception + * Test case for the questionnaire_add_instance function. * - * @covers \questionnaire_add_instance + * @covers ::questionnaire_add_instance */ public function test_add_instance(): void { $this->resetAfterTest(); @@ -111,12 +99,9 @@ public function test_add_instance(): void { } /** - * Test for questionnaire_update_instance(). + * Test case for the questionnaire_update_instance function. * - * @return void - * @throws dml_exception - * - * @covers \questionnaire_update_instance + * @covers ::questionnaire_update_instance */ public function test_update_instance(): void { global $DB; @@ -175,13 +160,9 @@ public function test_update_instance(): void { /** * Test for questionnaire_delete_instance(). - * * Need to verify that delete_instance deletes all data associated with a questionnaire. * - * @return void - * @throws dml_exception - * - * @covers \questionnaire_delete_instance + * @covers ::questionnaire_delete_instance */ public function test_delete_instance(): void { global $DB; @@ -215,12 +196,9 @@ public function test_delete_instance(): void { } /** - * Test for questionnaire_user_outline(). + * Test case for the questionnaire_user_outline function. * - * @return void - * @throws coding_exception - * - * @covers \questionnaire_user_outline + * @covers ::questionnaire_user_outline */ public function test_questionnaire_user_outline(): void { $this->resetAfterTest(); @@ -243,12 +221,9 @@ public function test_questionnaire_user_outline(): void { } /** - * Test for questionnaire_user_complete(). + * Test case for the questionnaire_user_complete function. * - * @return void - * @throws coding_exception - * - * @covers \questionnaire_user_complete + * @covers ::questionnaire_user_complete */ public function test_questionnaire_user_complete(): void { $this->resetAfterTest(); @@ -263,11 +238,9 @@ public function test_questionnaire_user_complete(): void { } /** - * Test for questionnaire_print_recent_activity(). + * Test case for the questionnaire_print_recent_activity function. * - * @return void - * - * @covers \questionnaire_print_recent_activity + * @covers ::questionnaire_print_recent_activity */ public function test_questionnaire_print_recent_activity(): void { $this->resetAfterTest(); @@ -276,11 +249,9 @@ public function test_questionnaire_print_recent_activity(): void { } /** - * Test for questionnaire_grades(). - * - * @return void + * Test case for the questionnaire_grades function. * - * @covers \questionnaire_grades + * @covers ::questionnaire_grades */ public function test_questionnaire_grades(): void { $this->resetAfterTest(); @@ -289,11 +260,9 @@ public function test_questionnaire_grades(): void { } /** - * Test for questionnaire_get_user_grades(). - * - * @return void + * Test case for the questionnaire_get_user_grades function. * - * @covers \questionnaire_get_user_grades + * @covers ::questionnaire_get_user_grades */ public function test_questionnaire_get_user_grades(): void { $this->resetAfterTest(); @@ -314,11 +283,9 @@ public function test_questionnaire_get_user_grades(): void { } /** - * Test for questionnaire_update_grades(). + * Test case for the questionnaire_update_grades function. * - * @return void - * - * @covers \questionnaire_update_grades + * @covers ::questionnaire_update_grades */ public function test_questionnaire_update_grades(): void { // Don't know how to test this yet! It doesn't return anything. @@ -326,11 +293,9 @@ public function test_questionnaire_update_grades(): void { } /** - * Test for questionnaire_grade_item_update(). - * - * @return void + * Test case for the questionnaire_grade_item_update function. * - * @covers \questionnaire_grade_item_update + * @covers ::questionnaire_grade_item_update */ public function test_questionnaire_grade_item_update(): void { $this->resetAfterTest(); diff --git a/tests/privacy_provider_test.php b/tests/privacy_provider_test.php index b4aa5884..1b2e477f 100644 --- a/tests/privacy_provider_test.php +++ b/tests/privacy_provider_test.php @@ -14,14 +14,7 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . -/** - * Privacy test for the mod questionnaire. - * - * @package mod_questionnaire - * @copyright 2019, onwards Poet - * @author Mike Churchward - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ +namespace mod_questionnaire; namespace mod_questionnaire; diff --git a/tests/questiontypes_test.php b/tests/questiontypes_test.php index 6f23fa12..57bae4a7 100644 --- a/tests/questiontypes_test.php +++ b/tests/questiontypes_test.php @@ -14,15 +14,6 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . -/** - * PHPUnit questionnaire generator tests - * - * @package mod_questionnaire - * @copyright 2015 Mike Churchward (mike@churchward.ca) - * @author Mike Churchward - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - namespace mod_questionnaire; use mod_questionnaire\question\question; @@ -34,16 +25,18 @@ require_once($CFG->dirroot . '/mod/questionnaire/classes/question/question.php'); /** - * Unit tests for questionnaire_questiontypes_testcase. - * @group mod_questionnaire + * PHPUnit questionnaire questiontypes tests + * + * @package mod_questionnaire + * @copyright 2015 Mike Churchward (mike@churchward.ca) + * @author Mike Churchward + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class questiontypes_test extends \advanced_testcase { /** - * Create a check boxes test question. + * Test case for the create_test_question_with_choices function for checkbox questions. * - * @return void - * - * @covers \mod_questionnaire\questiontypes_test::create_test_question + * @covers ::create_test_question_with_choices */ public function test_create_question_checkbox(): void { $this->create_test_question_with_choices( @@ -54,33 +47,27 @@ public function test_create_question_checkbox(): void { } /** - * Create a date test question. - * - * @return void + * Test case for the create_test_question function for date questions. * - * @covers \mod_questionnaire\questiontypes_test::create_test_question + * @covers ::create_test_question */ public function test_create_question_date(): void { $this->create_test_question(QUESDATE, '\\mod_questionnaire\\question\\date', ['content' => 'Enter a date']); } /** - * Create a dropdown box test question. - * - * @return void + * Test case for the create_test_question_with_choices function for dropdown questions. * - * @covers \mod_questionnaire\questiontypes_test::create_test_question + * @covers ::create_test_question_with_choices */ public function test_create_question_dropdown(): void { $this->create_test_question_with_choices(QUESDROP, '\\mod_questionnaire\\question\\drop', ['content' => 'Select one']); } /** - * Create an essay test question. + * Test case for the create_test_question function for essay questions. * - * @return void - * - * @covers \mod_questionnaire\questiontypes_test::create_test_question + * @covers ::create_test_question */ public function test_create_question_essay(): void { $questiondata = [ @@ -92,11 +79,9 @@ public function test_create_question_essay(): void { } /** - * Create a sectiontext test question. - * - * @return void + * Test case for the create_test_question function for sectiontext questions. * - * @covers \mod_questionnaire\questiontypes_test::create_test_question + * @covers ::create_test_question */ public function test_create_question_sectiontext(): void { $this->create_test_question( @@ -107,11 +92,9 @@ public function test_create_question_sectiontext(): void { } /** - * Create a numerical test question. - * - * @return void + * Test case for the create_test_question function for numeric questions. * - * @covers \mod_questionnaire\questiontypes_test::create_test_question + * @covers ::create_test_question */ public function test_create_question_numeric(): void { $questiondata = [ @@ -123,33 +106,27 @@ public function test_create_question_numeric(): void { } /** - * Create a radio test question. + * Test case for the create_test_question_with_choices function for radiobuttons questions. * - * @return void - * - * @covers \mod_questionnaire\questiontypes_test::create_test_question + * @covers ::create_test_question_with_choices */ public function test_create_question_radiobuttons(): void { $this->create_test_question_with_choices(QUESRADIO, '\\mod_questionnaire\\question\\radio', ['content' => 'Choose one']); } /** - * Create a rate test question. - * - * @return void + * Test case for the create_test_question_with_choices function for ratescale questions. * - * @covers \mod_questionnaire\questiontypes_test::create_test_question + * @covers ::create_test_question_with_choices */ public function test_create_question_ratescale(): void { $this->create_test_question_with_choices(QUESRATE, '\\mod_questionnaire\\question\\rate', ['content' => 'Rate these']); } /** - * Create a text test question. - * - * @return void + * Test case for the create_test_question function for textbox questions. * - * @covers \mod_questionnaire\questiontypes_test::create_test_question + * @covers ::create_test_question */ public function test_create_question_textbox(): void { $questiondata = [ @@ -161,11 +138,9 @@ public function test_create_question_textbox(): void { } /** - * Create a slider test question. + * Test case for the create_test_question function for slider questions. * - * @return void - * - * @covers \mod_questionnaire\questiontypes_test::create_test_question + * @covers ::create_test_question */ public function test_create_question_slider(): void { $questiondata = ['content' => 'Enter a number']; @@ -173,17 +148,14 @@ public function test_create_question_slider(): void { } /** - * Create a yes/no test question. - * - * @return void + * Test case for the create_test_question function for yesno questions. * - * @covers \mod_questionnaire\questiontypes_test::create_test_question + * @covers ::create_test_question */ public function test_create_question_yesno(): void { $this->create_test_question(QUESYESNO, '\\mod_questionnaire\\question\\yesno', ['content' => 'Enter yes or no']); } - // General tests to call from specific tests above. /** diff --git a/tests/responsetypes_test.php b/tests/responsetypes_test.php index 88995ff7..1fc6f5e2 100644 --- a/tests/responsetypes_test.php +++ b/tests/responsetypes_test.php @@ -14,21 +14,8 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . -/** - * PHPUnit questionnaire generator tests - * - * @package mod_questionnaire - * @copyright 2015 Mike Churchward (mike@churchward.ca) - * @author Mike Churchward - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - namespace mod_questionnaire; -use mod_questionnaire\question\question; - -defined('MOODLE_INTERNAL') || die(); - global $CFG; require_once($CFG->dirroot . '/mod/questionnaire/locallib.php'); require_once($CFG->dirroot . '/mod/questionnaire/tests/generator_test.php'); @@ -36,17 +23,18 @@ require_once($CFG->dirroot . '/mod/questionnaire/classes/question/question.php'); /** - * Unit tests for questionnaire_responsetypes_testcase. - * @group mod_questionnaire + * PHPUnit questionnaire generator tests + * + * @package mod_questionnaire + * @copyright 2015 Mike Churchward (mike@churchward.ca) + * @author Mike Churchward + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class responsetypes_test extends \advanced_testcase { /** - * Test responses in a yes/no question. - * - * @return void - * @throws dml_exception + * Test case for the create_response_boolean function. * - * @covers \mod_questionnaire\question\yesno + * @covers \mod_questionnaire_generator::create_question_response */ public function test_create_response_boolean(): void { global $DB; @@ -75,12 +63,9 @@ public function test_create_response_boolean(): void { } /** - * Test responses in a essay question. + * Test case for the create_response_text function. * - * @return void - * @throws dml_exception - * - * @covers \mod_questionnaire\question\essay + * @covers \mod_questionnaire_generator::create_question_response */ public function test_create_response_text(): void { global $DB; @@ -110,12 +95,9 @@ public function test_create_response_text(): void { } /** - * Test responses in a slider question. - * - * @return void - * @throws dml_exception + * Test case for the create_response_slider function. * - * @covers \mod_questionnaire\question\slider + * @covers \mod_questionnaire_generator::create_question_response */ public function test_create_response_slider(): void { global $DB; @@ -145,12 +127,9 @@ public function test_create_response_slider(): void { } /** - * Test responses in a date question. - * - * @return void - * @throws dml_exception + * Test case for the create_response_date function. * - * @covers \mod_questionnaire\question\date + * @covers \mod_questionnaire_generator::create_question_response */ public function test_create_response_date(): void { global $DB; @@ -181,12 +160,9 @@ public function test_create_response_date(): void { } /** - * Test responses in a single choice radio question. + * Test case for the create_response_single function. * - * @return void - * @throws dml_exception - * - * @covers \mod_questionnaire\question\radio + * @covers \mod_questionnaire_generator::create_question_response */ public function test_create_response_single(): void { global $DB; @@ -260,12 +236,9 @@ public function test_create_response_single(): void { } /** - * Test responses in a multiple choices question. - * - * @return void - * @throws dml_exception + * Test case for the create_response_multiple function. * - * @covers \mod_questionnaire\question\rate + * @covers \mod_questionnaire_generator::create_question_response */ public function test_create_response_multiple(): void { global $DB; @@ -325,12 +298,9 @@ public function test_create_response_multiple(): void { } /** - * Test response's ranks in a rate question. - * - * @return void - * @throws dml_exception + * Test case for the create_response_rank function. * - * @covers \mod_questionnaire\question\rate + * @covers \mod_questionnaire_generator::create_question_response */ public function test_create_response_rank(): void { global $DB; From d5da11c7eaa668199e71aee97be2091e6dba3851 Mon Sep 17 00:00:00 2001 From: Laurent David Date: Wed, 15 Feb 2023 12:11:49 +0100 Subject: [PATCH 03/11] Add file question type --- classes/output/renderer.php | 1 + classes/question/file.php | 206 ++++++++++++ classes/question/question.php | 2 + classes/responsetype/file.php | 348 +++++++++++++++++++++ classes/responsetype/response/response.php | 1 + db/install.php | 7 + db/install.xml | 15 + db/upgrade.php | 33 ++ lang/en/questionnaire.php | 2 + lib.php | 11 +- locallib.php | 2 + questionnaire.class.php | 1 + tests/behat/add_questions.feature | 4 + tests/behat/behat_mod_questionnaire.php | 197 +++++++++++- tests/behat/file_question.feature | 44 +++ tests/fixtures/testfilequestion.pdf | Bin 0 -> 16338 bytes 16 files changed, 870 insertions(+), 4 deletions(-) create mode 100644 classes/question/file.php create mode 100644 classes/responsetype/file.php create mode 100644 tests/behat/file_question.feature create mode 100644 tests/fixtures/testfilequestion.pdf diff --git a/classes/output/renderer.php b/classes/output/renderer.php index 74c81484..dd198b55 100755 --- a/classes/output/renderer.php +++ b/classes/output/renderer.php @@ -384,6 +384,7 @@ public function print_preview_formend($url, $submitstr, $resetstr) { $output .= \html_writer::start_tag('div'); $output .= \html_writer::empty_tag('input', ['type' => 'submit', 'name' => 'submit', 'value' => $submitstr, 'class' => 'btn btn-primary']); + $output .= \html_writer::empty_tag('input', ['type' => 'hidden', 'name' => 'sesskey', 'value' => sesskey()]); $output .= ' '; $output .= \html_writer::tag('a', $resetstr, ['href' => $url, 'class' => 'btn btn-secondary me-1']); $output .= \html_writer::end_tag('div') . "\n"; diff --git a/classes/question/file.php b/classes/question/file.php new file mode 100644 index 00000000..1370bcdd --- /dev/null +++ b/classes/question/file.php @@ -0,0 +1,206 @@ +. +namespace mod_questionnaire\question; +use core_media_manager; +use form_filemanager; +use mod_questionnaire\responsetype\response\response; +use moodle_url; +use MoodleQuickForm; + +/** + * This file contains the parent class for text question types. + * + * @author Laurent David + * @author Martin Cornu-Mansuy + * @copyright 2023 onward CALL Learning + * @license http://www.gnu.org/copyleft/gpl.html GNU Public License + * @package mod_questionnaire + */ +class file extends question { + + /** + * Get name. + * + * @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; + } + + /** + * Get response class. + * + * @return object|string + */ + protected function responseclass() { + return '\\mod_questionnaire\\responsetype\\file'; + } + + /** + * Survey display output. + * + * @param response $response + * @param object $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; + } + + /** + * Get file manager options + * + * @return array + */ + private function get_file_manager_option() { + return [ + 'mainfile' => '', + 'subdirs' => false, + 'accepted_types' => array('image', '.pdf') + ]; + } + + /** + * Response display output. + * + * @param 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; + } + + /** + * Add the length element as hidden. + * + * @param \MoodleQuickForm $mform + * @param string $helpname + * @return \MoodleQuickForm + */ + protected function form_length(MoodleQuickForm $mform, $helpname = '') { + return question::form_length_hidden($mform); + } + + /** + * Add the precise element as hidden. + * + * @param \MoodleQuickForm $mform + * @param string $helpname + * @return \MoodleQuickForm + */ + 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 5f987d89..02212586 100644 --- a/classes/question/question.php +++ b/classes/question/question.php @@ -42,6 +42,7 @@ define('QUESDATE', 9); define('QUESNUMERIC', 10); define('QUESSLIDER', 11); +define('QUESFILE', 12); define('QUESPAGEBREAK', 99); define('QUESSECTIONTEXT', 100); @@ -121,6 +122,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..3ee0d5aa --- /dev/null +++ b/classes/responsetype/file.php @@ -0,0 +1,348 @@ +. +namespace mod_questionnaire\responsetype; + +use mod_questionnaire\db\bulk_sql_config; +use moodle_url; + +/** + * Class for text response types. + * + * @author Laurent David + * @author Martin Cornu-Mansuy + * @copyright 2023 onward CALL Learning + * @license http://www.gnu.org/copyleft/gpl.html GNU Public License + * @package mod_questionnaire + */ +class file extends responsetype { + /** + * 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; + } + + /** + * 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 + */ + public static 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 + */ + public static 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; + } + + /** + * Insert a provided response to the question. + * + * @param \mod_questionnaire\responsetype\response\response|\stdClass $responsedata + * @return bool|int + */ + 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; + } + } + + /** + * Provide the necessary response data table name. Should probably always be used with late static binding 'static::' form + * rather than 'self::' form to allow for class extending. + * + * @return string response table name. + */ + public static function response_table() { + return 'questionnaire_response_file'; + } + + /** + * 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'; + } + } + + /** + * Provide the result information for the specified result records. + * + * @param int|array $rids - A single response id, or array. + * @param string $sort - Optional display sort. + * @param boolean $anonymous - Whether or not responses are anonymous. + * @return string - Display output. + */ + 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 = ""; + } + return $pagetags; + } + + /** + * Provide the result information for the specified result records. + * + * @param int|array $rids - A single response id, or array. + * @param boolean $anonymous - Whether or not responses are anonymous. + * @return array - Array of data records. + */ + 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); + } + + /** + * Override the results tags function for templates for questions with dates. + * + * @param array $weights + * @param int $participants Number of questionnaire participants. + * @param int $respondents Number of question respondents. + * @param bool $showtotals + * @param string $sort + * @return \stdClass + */ + 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; + } + + /** + * 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 001a3ff8..82e61698 100644 --- a/classes/responsetype/response/response.php +++ b/classes/responsetype/response/response.php @@ -181,5 +181,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); } } diff --git a/db/install.php b/db/install.php index 79de7308..8cc9130d 100644 --- a/db/install.php +++ b/db/install.php @@ -100,6 +100,13 @@ function xmldb_questionnaire_install() { $questiontype->response_table = 'response_text'; $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); + $questiontype = new stdClass(); $questiontype->typeid = 99; $questiontype->type = 'Page Break'; diff --git a/db/install.xml b/db/install.xml index c9c5ba8c..7c8fd976 100644 --- a/db/install.xml +++ b/db/install.xml @@ -226,6 +226,21 @@ + + + + + + + + + + + + + + +
diff --git a/db/upgrade.php b/db/upgrade.php index c190d142..c01da96d 100644 --- a/db/upgrade.php +++ b/db/upgrade.php @@ -1015,6 +1015,39 @@ function xmldb_questionnaire_upgrade($oldversion = 0) { upgrade_mod_savepoint(true, 2022121600.02, 'questionnaire'); } + if ($oldversion < 2022121600.03) { + $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); + + // 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, 2022121600.03, 'questionnaire'); + } + if ($oldversion < 2022121601.01) { // Add removeafter fields. $table = new xmldb_table('questionnaire'); diff --git a/lang/en/questionnaire.php b/lang/en/questionnaire.php index 22c2ae7e..eaf48a2d 100644 --- a/lang/en/questionnaire.php +++ b/lang/en/questionnaire.php @@ -264,6 +264,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 6b6de8cf..a71f5c4e 100644 --- a/lib.php +++ b/lib.php @@ -551,11 +551,12 @@ function questionnaire_scale_used_anywhere($scaleid) { * @param string $filearea * @param array $args * @param bool $forcedownload + * @param mixed $options * @return bool false if file not found, does not return if found - justsend the file * * $forcedownload is unused, but API requires it. Suppress PHPMD warning. */ -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) { @@ -564,7 +565,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; } @@ -583,6 +584,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; @@ -601,7 +606,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 dffc1026..0f1eb611 100644 --- a/locallib.php +++ b/locallib.php @@ -608,6 +608,8 @@ function questionnaire_get_type($id) { return get_string('numeric', 'questionnaire'); case 11: return get_string('slider', 'questionnaire'); + case 12: + return get_string('file', 'questionnaire'); case 100: return get_string('sectiontext', 'questionnaire'); case 99: diff --git a/questionnaire.class.php b/questionnaire.class.php index c71e70f5..15660634 100644 --- a/questionnaire.class.php +++ b/questionnaire.class.php @@ -29,6 +29,7 @@ * @author Mike Churchward * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +#[AllowDynamicProperties] class questionnaire { // Class Properties. 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 2c8bca19..f455900e 100644 --- a/tests/behat/behat_mod_questionnaire.php +++ b/tests/behat/behat_mod_questionnaire.php @@ -33,7 +33,6 @@ Behat\Gherkin\Node\PyStringNode, Behat\Mink\Exception\ExpectationException; -#[\AllowDynamicProperties] /** * Questionnaire-related steps definitions. * @@ -128,6 +127,7 @@ public function i_add_a_question_and_i_fill_the_form_with($questiontype, TableNo 'Text Box', 'Yes/No', 'Slider', + 'File', ]; if (!in_array($questiontype, $validtypes)) { @@ -565,4 +565,199 @@ protected function get_cm_by_questionnaire_name(string $name): stdClass { $questionnaire = $this->get_questionnaire_by_name($name); return get_coursemodule_from_instance('questionnaire', $questionnaire->id, $questionnaire->course); } + + /** + * 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$/ + * @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. + * + * @return NodeElement + */ + 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 + * + * @param string $filepath Normally a path relative to $CFG->dirroot, but can be an absolute path too. + * @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") + * @throws DriverException + * @throws ExpectationException Thrown by behat_base::find + */ + 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 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. + * + * @param NodeElement $filemanagernode The filemanager or filepicker form element DOM node. + * @param mixed $repositoryname The repo name. + * @return void + * @throws ExpectationException Thrown by behat_base::find + */ + 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..0ff43934 --- /dev/null +++ b/tests/behat/file_question.feature @@ -0,0 +1,44 @@ +@mod @mod_questionnaire +Feature: In questionnaire, we can add a question requiring a file upload. + + Background: + 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 | + + @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 I log in as "teacher1" + When I am on the "Test questionnaire" "questionnaire activity" page + And I navigate to "Questions" in current page administration + And 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 + And I log in as "student1" + And I am on the "Test questionnaire" "questionnaire activity" page + And I navigate to "Answer the questions..." in current page administration + And I upload "mod/questionnaire/tests/fixtures/testfilequestion.pdf" to questionnaire filemanager + And I press "Submit questionnaire" + And I should see "Thank you for completing this Questionnaire" + And I press "Continue" + And I should see "View your response(s)" + And ".resourcecontent.resourcepdf" "css_element" should exist + And I log out + And I log in as "teacher1" + And I am on the "Test questionnaire" "questionnaire activity" page + And I navigate to "View all responses" in current page administration + Then I should see "testfilequestion.pdf" diff --git a/tests/fixtures/testfilequestion.pdf b/tests/fixtures/testfilequestion.pdf new file mode 100644 index 0000000000000000000000000000000000000000..90588c3015a7864cb0ee78639de3b564b569a1a6 GIT binary patch literal 16338 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 Date: Sun, 15 Oct 2023 11:27:01 +0200 Subject: [PATCH 04/11] Update version to 2023101500. Fix upload when several file upload questions are used. - Adjust siganture of methods to their parent - Limit the files to upload to one for each question - Use questionaire id for file draft area instead of question id in the questionaire. Fix handling of file uploads - allow upload of one file only. - new submit does not load previously existing files. Change refence of file.itemid to questionnaire_response_file.id update existing files.itemid to questionnaire_response_file.id --- classes/feedback_section_form.php | 4 + classes/file_storage.php | 2 +- classes/question/file.php | 113 +++++++++++---------- classes/responsetype/file.php | 93 ++++++++++++----- classes/responsetype/response/response.php | 4 +- db/upgrade.php | 40 +++++--- lib.php | 2 +- questionnaire.class.php | 7 +- tests/behat/behat_mod_questionnaire.php | 1 + 9 files changed, 166 insertions(+), 100 deletions(-) diff --git a/classes/feedback_section_form.php b/classes/feedback_section_form.php index 5c030b3c..9ab00ceb 100644 --- a/classes/feedback_section_form.php +++ b/classes/feedback_section_form.php @@ -21,6 +21,7 @@ require_once($CFG->libdir . '/formslib.php'); require_once($CFG->dirroot . '/mod/questionnaire/lib.php'); +#[\AllowDynamicProperties] /** * Print the form to add or edit a questionnaire-instance * @@ -37,6 +38,9 @@ class feedback_section_form extends \moodleform { */ public $context; + /** @var int $sid The section id. */ + protected $sid; + /** * Form definition. */ diff --git a/classes/file_storage.php b/classes/file_storage.php index 976336d7..483448ed 100644 --- a/classes/file_storage.php +++ b/classes/file_storage.php @@ -17,7 +17,7 @@ namespace mod_questionnaire; /** - * Defines the file stoeage class for questionnaire. + * Defines the file storage class for questionnaire. * @package mod_questionnaire * @copyright 2020 onwards Mike Churchward (mike.churchward@poetopensource.org) * @author Mike Churchward diff --git a/classes/question/file.php b/classes/question/file.php index 1370bcdd..023433c1 100644 --- a/classes/question/file.php +++ b/classes/question/file.php @@ -70,19 +70,23 @@ protected function responseclass() { /** * Survey display output. * - * @param response $response + * @param \stdClass $formdata * @param object $descendantsdata * @param bool $blankquestionnaire - * @return object|string + * @return string */ - protected function question_survey_display($response, $descendantsdata, $blankquestionnaire = false) { + protected function question_survey_display($formdata, $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); + $options = self::get_file_manager_option(); + if ($draftitemid > 0) { + file_prepare_draft_area($draftitemid, $this->context->id, $component, 'file', $this->id, $options); + } else { + $draftitemid = file_get_unused_draft_itemid(); + } // 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"); @@ -110,75 +114,74 @@ protected function question_survey_display($response, $descendantsdata, $blankqu * * @return array */ - private function get_file_manager_option() { + public static function get_file_manager_option() { return [ 'mainfile' => '', 'subdirs' => false, - 'accepted_types' => array('image', '.pdf') + 'accepted_types' => array('image', '.pdf'), + 'maxfiles' => 1, ]; } /** * Response display output. * - * @param response $response - * @return object|string + * @param \stdClass $data + * @return string */ - protected function response_survey_display($response) { + protected function response_survey_display($data) { 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]); + if (isset($data->answers[$this->id])) { + $answer = reset($data->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); + $code = ''; + + if ($file) { + // There is a file. + $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 = ''; + + $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; + return '
' . $code . '
'; } /** diff --git a/classes/responsetype/file.php b/classes/responsetype/file.php index 3ee0d5aa..253e01e9 100644 --- a/classes/responsetype/file.php +++ b/classes/responsetype/file.php @@ -43,14 +43,19 @@ public static function answers_from_webform($responsedata, $question) { $record->responseid = $responsedata->rid; $record->questionid = $question->id; - file_save_draft_area_files($val, $question->context->id, 'mod_questionnaire', 'file', $responsedata->rid); + file_save_draft_area_files($val, $question->context->id, + 'mod_questionnaire', 'file', $val, + \mod_questionnaire\question\file::get_file_manager_option()); $fs = get_file_storage(); - $files = $fs->get_area_files($question->context->id, 'mod_questionnaire', 'file', $responsedata->rid, + $files = $fs->get_area_files($question->context->id, 'mod_questionnaire', + 'file', $val, "itemid, filepath, filename", false); - $file = reset($files); - $record->value = $file->get_id(); - $answers[] = answer\answer::create_from_data($record); + if (!empty($files)) { + $file = reset($files); + $record->value = $file->get_id(); + $answers[] = answer\answer::create_from_data($record); + } } return $answers; } @@ -130,10 +135,47 @@ public function insert_response($responsedata) { $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; + // When saving the draft file, the itemid was the same as the draftitemid. This must now be + // corrected to the primary key that is questionaire_response_file.id to have a correct reference. + $recordid = $DB->insert_record(static::response_table(), $record); + if ($recordid) { + $olditem = $DB->get_record('files', ['id' => $record->fileid], 'itemid'); + if (!$olditem) { + return false; + } + $siblings = $DB->get_records('files', + ['component' => 'mod_questionnaire', 'itemid' => $olditem->itemid]); + foreach ($siblings as $sibling) { + if (!self::fix_file_itemid($recordid, $sibling)) { + return false; + } + } + return $recordid; + } + } + return false; + } + + /** + * Update records in the table file with the new given itemid. To do this, the pathnamehash + * needs to be recalculated as well. + * @param int $recordid + * @param \stdClass $filerecord + * @return bool + * @throws \dml_exception + */ + public static function fix_file_itemid(int $recordid, \stdClass $filerecord): bool { + global $DB; + if ((int)$filerecord->itemid === $recordid) { + return true; // Reference is already good, nothing to do. } + $fs = get_file_storage(); + $file = $fs->get_file_instance($filerecord); + $newhash = $fs->get_pathname_hash($filerecord->contextid, $filerecord->component, + $filerecord->filearea, $recordid, $file->get_filepath(), $file->get_filename()); + $filerecord->itemid = $recordid; + $filerecord->pathnamehash = $newhash; + return $DB->update_record('files', $filerecord); } /** @@ -259,23 +301,26 @@ public function get_results_tags($weights, $participants, $respondents, $showtot $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]); + if ($file) { + // There is a file. + $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]) . ''; } - $response->respondent = - '' . fullname($users[$row->userid]) . ''; } else { $response->respondent = ''; } diff --git a/classes/responsetype/response/response.php b/classes/responsetype/response/response.php index 82e61698..90246870 100644 --- a/classes/responsetype/response/response.php +++ b/classes/responsetype/response/response.php @@ -111,9 +111,9 @@ public static function create_from_data($responsedata) { /** * Provide a response object from web form data to the question. * - * @param \stdClass $responsedata All of the responsedata as an object. + * @param \stdClass $responsedata All the responsedata as an object. * @param array $questions - * @return bool|response A response object. + * @return response A response object. */ public static function response_from_webform($responsedata, $questions) { global $USER; diff --git a/db/upgrade.php b/db/upgrade.php index c01da96d..a7dfdfab 100644 --- a/db/upgrade.php +++ b/db/upgrade.php @@ -1015,7 +1015,21 @@ function xmldb_questionnaire_upgrade($oldversion = 0) { upgrade_mod_savepoint(true, 2022121600.02, 'questionnaire'); } - if ($oldversion < 2022121600.03) { + if ($oldversion < 2022121601.01) { + // Add removeafter fields. + $table = new xmldb_table('questionnaire'); + $field = new xmldb_field('removeafter', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, 0, 'progressbar'); + + // Conditionally launch add field. + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // Questionnaire savepoint reached. + upgrade_mod_savepoint(true, 2022121601.01, 'questionnaire'); + } + + if ($oldversion < 2023101500) { $questiontype = new stdClass(); $questiontype->typeid = 12; $questiontype->type = 'File'; @@ -1045,21 +1059,21 @@ function xmldb_questionnaire_upgrade($oldversion = 0) { } // Questionnaire savepoint reached. - upgrade_mod_savepoint(true, 2022121600.03, 'questionnaire'); + upgrade_mod_savepoint(true, 2023101500, 'questionnaire'); } - if ($oldversion < 2022121601.01) { - // Add removeafter fields. - $table = new xmldb_table('questionnaire'); - $field = new xmldb_field('removeafter', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, 0, 'progressbar'); - - // Conditionally launch add field. - if (!$dbman->field_exists($table, $field)) { - $dbman->add_field($table, $field); + if ($oldversion < 2023101501) { + // Upgrade files.itemid with questionnaire_response_file.id + $filesresponses = $DB->get_records('questionnaire_response_file', [], '', 'id,fileid'); + $idmap = []; + foreach ($filesresponses as $fileresponse) { + $idmap[(int)$fileresponse->fileid] = (int)$fileresponse->id; } - - // Questionnaire savepoint reached. - upgrade_mod_savepoint(true, 2022121601.01, 'questionnaire'); + $filerecords = $DB->get_records_list('files', 'id', array_keys($idmap), 'id desc'); + foreach ($filerecords as $filerecord) { + \mod_questionnaire\responsetype\file::fix_file_itemid($idmap[(int)$filerecord->id], $filerecord); + } + upgrade_mod_savepoint(true, 2023101501, 'questionnaire'); } if ($oldversion < 2025041400.01) { diff --git a/lib.php b/lib.php index a71f5c4e..d5b7be41 100644 --- a/lib.php +++ b/lib.php @@ -585,7 +585,7 @@ function questionnaire_pluginfile($course, $cm, $context, $filearea, $args, $for return false; } } else if ($filearea == 'file') { - if (!$DB->record_exists('questionnaire_response_file', ['response_id' => $componentid])) { + if (!$DB->record_exists('questionnaire_response_file', ['id' => $componentid])) { return false; } } else { diff --git a/questionnaire.class.php b/questionnaire.class.php index 15660634..b7c13936 100644 --- a/questionnaire.class.php +++ b/questionnaire.class.php @@ -29,7 +29,6 @@ * @author Mike Churchward * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -#[AllowDynamicProperties] class questionnaire { // Class Properties. @@ -151,12 +150,12 @@ public function add_survey($sid = 0, $survey = null) { /** * Adding questions to the object. - * @param bool $sid + * @param int $sid */ - public function add_questions($sid = false) { + public function add_questions($sid = 0) { global $DB; - if ($sid === false) { + if ($sid === 0) { $sid = $this->sid; } diff --git a/tests/behat/behat_mod_questionnaire.php b/tests/behat/behat_mod_questionnaire.php index f455900e..c3e55961 100644 --- a/tests/behat/behat_mod_questionnaire.php +++ b/tests/behat/behat_mod_questionnaire.php @@ -33,6 +33,7 @@ Behat\Gherkin\Node\PyStringNode, Behat\Mink\Exception\ExpectationException; +#[\AllowDynamicProperties] /** * Questionnaire-related steps definitions. * From e31d31e4465ac9506718c40531d59f176ef99341 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luca=20B=C3=B6sch?= Date: Thu, 18 Jan 2024 18:52:36 +0100 Subject: [PATCH 05/11] Adding savepoint. Improvements to satisfy code checker. Make the file upload work with required setting, fix delete of records Add file question type --- classes/question/file.php | 47 ++++++++++++++--- classes/responsetype/file.php | 30 ++++++++++- db/upgrade.php | 3 +- locallib.php | 2 + tests/behat/behat_mod_questionnaire.php | 68 +++++++++---------------- 5 files changed, 95 insertions(+), 55 deletions(-) diff --git a/classes/question/file.php b/classes/question/file.php index 023433c1..3860b84c 100644 --- a/classes/question/file.php +++ b/classes/question/file.php @@ -70,20 +70,25 @@ protected function responseclass() { /** * Survey display output. * - * @param \stdClass $formdata + * @param response $response * @param object $descendantsdata * @param bool $blankquestionnaire * @return string */ - protected function question_survey_display($formdata, $descendantsdata, $blankquestionnaire = false) { + 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'; + // Make sure there is a response, fetch the draft id from the original request. + if (isset($response->answers[$this->id]) && !empty($response->answers[$this->id]) && isset($_REQUEST[$elname . 'draft'])) { + $draftitemid = (int)$_REQUEST[$elname . 'draft']; + } else { + $draftitemid = file_get_submitted_draft_itemid($elname); + } $options = self::get_file_manager_option(); if ($draftitemid > 0) { - file_prepare_draft_area($draftitemid, $this->context->id, $component, 'file', $this->id, $options); + file_prepare_draft_area($draftitemid, $this->context->id, 'mod_questionnaire', 'file', $this->id, $options); } else { $draftitemid = file_get_unused_draft_itemid(); } @@ -101,14 +106,40 @@ protected function question_survey_display($formdata, $descendantsdata, $blankqu ); $fm = new form_filemanager((object) $fmoptions); $output = $PAGE->get_renderer('core', 'files'); - $html = $output->render($fm); - $html .= ''; - $html .= ''; + $html = '
' . + $output->render($fm) . + '' . + '' . + '
'; return $html; } + /** + * Check question's form data for complete response. + * @param \stdClass $responsedata The data entered into the response. + * @return bool + */ + public function response_complete($responsedata) { + $answered = false; + // If $responsedata is a response object, look through the answers. + if (is_a($responsedata, 'mod_questionnaire\responsetype\response\response') && + isset($responsedata->answers[$this->id]) && !empty($responsedata->answers[$this->id]) + ) { + $answer = reset($responsedata->answers[$this->id]); + $answered = ((int)$answer->value > 0); + // If $responsedata is webform data, check that it is not empty. + } else if (isset($responsedata->{'q'.$this->id})) { + $draftitemid = (int)$responsedata->{'q' . $this->id}; + if ($draftitemid > 0) { + $info = file_get_draft_area_info($draftitemid); + $answered = $info['filecount'] > 0; + } + } + return !($this->required() && ($this->deleted == 'n') && !$answered); + } + /** * Get file manager options * diff --git a/classes/responsetype/file.php b/classes/responsetype/file.php index 253e01e9..b6024025 100644 --- a/classes/responsetype/file.php +++ b/classes/responsetype/file.php @@ -55,6 +55,8 @@ public static function answers_from_webform($responsedata, $question) { $file = reset($files); $record->value = $file->get_id(); $answers[] = answer\answer::create_from_data($record); + } else { + self::delete_old_response((int)$question->id, (int)$record->responseid); } } return $answers; @@ -114,11 +116,34 @@ public static function response_answers_by_question($rid) { return $answers; } + /** + * Delete old entries from the questionnaire_response_file table and also the corresponding entries + * in the files table. + * @param int $question_id + * @param int $response_id + * @return void + * @throws \dml_exception + */ + public static function delete_old_response(int $question_id, int $response_id) { + global $DB; + // Check, if we have an old response file from a former attempt. + $record = $DB->get_record(static::response_table(), [ + 'response_id' => $response_id, + 'question_id' => $question_id, + ]); + if ($record) { + // Old record found, then delete all referenced entries in the files table and then delete this entry. + $DB->delete_records('files', ['component' => 'mod_questionnaire', 'itemid' => $record->itemid]); + $DB->delete_records(self::response_table(), ['id' => $record->id]); + } + } + /** * Insert a provided response to the question. * * @param \mod_questionnaire\responsetype\response\response|\stdClass $responsedata * @return bool|int + * @throws \dml_exception */ public function insert_response($responsedata) { global $DB; @@ -135,6 +160,9 @@ public function insert_response($responsedata) { $record->question_id = $this->question->id; $record->fileid = intval(clean_text($response->answers[$this->question->id][0]->value)); + // Delete any previous attempts. + self::delete_old_response((int)$this->question->id, (int)$response->id); + // When saving the draft file, the itemid was the same as the draftitemid. This must now be // corrected to the primary key that is questionaire_response_file.id to have a correct reference. $recordid = $DB->insert_record(static::response_table(), $record); @@ -302,7 +330,7 @@ public function get_results_tags($weights, $participants, $respondents, $showtot $file = $fs->get_file_by_id($row->fileid); if ($file) { - // There is a file. + // There is a file. $imageurl = moodle_url::make_pluginfile_url( $file->get_contextid(), $file->get_component(), diff --git a/db/upgrade.php b/db/upgrade.php index a7dfdfab..3b9f291a 100644 --- a/db/upgrade.php +++ b/db/upgrade.php @@ -1063,7 +1063,7 @@ function xmldb_questionnaire_upgrade($oldversion = 0) { } if ($oldversion < 2023101501) { - // Upgrade files.itemid with questionnaire_response_file.id + // Upgrade files.itemid with questionnaire_response_file.id. $filesresponses = $DB->get_records('questionnaire_response_file', [], '', 'id,fileid'); $idmap = []; foreach ($filesresponses as $fileresponse) { @@ -1073,6 +1073,7 @@ function xmldb_questionnaire_upgrade($oldversion = 0) { foreach ($filerecords as $filerecord) { \mod_questionnaire\responsetype\file::fix_file_itemid($idmap[(int)$filerecord->id], $filerecord); } + // Questionnaire savepoint reached. upgrade_mod_savepoint(true, 2023101501, 'questionnaire'); } diff --git a/locallib.php b/locallib.php index 0f1eb611..8006d5e1 100644 --- a/locallib.php +++ b/locallib.php @@ -414,6 +414,7 @@ function questionnaire_delete_response($response, $questionnaire = '') { $DB->delete_records('questionnaire_response_rank', ['response_id' => $rid]); $DB->delete_records('questionnaire_resp_single', ['response_id' => $rid]); $DB->delete_records('questionnaire_response_text', ['response_id' => $rid]); + $DB->delete_records('questionnaire_response_file', ['response_id' => $rid]); $status = $status && $DB->delete_records('questionnaire_response', ['id' => $rid]); @@ -444,6 +445,7 @@ function questionnaire_delete_responses($qid) { $DB->delete_records('questionnaire_response_rank', ['question_id' => $qid]); $DB->delete_records('questionnaire_resp_single', ['question_id' => $qid]); $DB->delete_records('questionnaire_response_text', ['question_id' => $qid]); + $DB->delete_records('questionnaire_response_file', ['question_id' => $qid]); return true; } diff --git a/tests/behat/behat_mod_questionnaire.php b/tests/behat/behat_mod_questionnaire.php index c3e55961..f4ddc7e2 100644 --- a/tests/behat/behat_mod_questionnaire.php +++ b/tests/behat/behat_mod_questionnaire.php @@ -572,45 +572,57 @@ protected function get_cm_by_questionnaire_name(string $name): stdClass { * * The paths should be relative to moodle codebase. * - * @When /^I upload "(?P(?:[^"]|\\")*)" to questionnaire filemanager$/ + * @When /^I upload "(?P(?:[^"]|\\")*)" to questionnaire "(?P(?:[^"]|\\")*)" filemanager$/ * @param string $filepath + * @param string $question */ - public function i_upload_file_to_questionnaire_filemanager($filepath) { - $this->upload_file_to_filemanager_questionnaire($filepath, new TableNode(array())); + public function i_upload_file_to_questionnaire_question_filemanager($filepath, $question) { + $this->upload_file_to_question_filemanager_questionnaire($filepath, $question, new TableNode([]), false); } /** - * Try to get the filemanager node. + * Try to get the filemanager node of a given question. * - * @return NodeElement + * @param $question + * @return \Behat\Mink\Element\NodeElement|null */ - protected function get_filemanager() { + protected function get_filepicker_node($question) { + // More info about the problem (in case there is a problem). + $exception = new ExpectationException('The filepicker for the question with text "' . $question . + '" can not be found', $this->getSession()); - // If no file picker label is mentioned take the first file picker from the page. - return $this->find( + $filepickercontainer = $this->find( 'xpath', - '//div[contains(concat(" ", normalize-space(@class), " "), " filemanager ")]' + "//p[contains(.,'" . $question . "')]" . + "//parent::div[contains(concat(' ', normalize-space(@class), ' '), ' no-overflow ')]" . + "//parent::div[contains(concat(' ', normalize-space(@class), ' '), ' qn-question ')]" . + "//following::div[contains(concat(' ', normalize-space(@class), ' '), ' qn-answer ')]" . + "//descendant::*[@data-fieldtype = 'filemanager' or @data-fieldtype = 'filepicker']", + $exception ); + + return $filepickercontainer; } /** * Uploads a file to filemanager * * @param string $filepath Normally a path relative to $CFG->dirroot, but can be an absolute path too. + * @param string $question A question text. * @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") * @throws DriverException * @throws ExpectationException Thrown by behat_base::find */ - protected function upload_file_to_filemanager_questionnaire($filepath, TableNode $data, $overwriteaction = false) { + protected function upload_file_to_question_filemanager_questionnaire($filepath, $question, 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(); + $filemanagernode = $this->get_filepicker_node($question); // Opening the select repository window and selecting the upload repository. $this->open_add_file_window($filemanagernode, get_string('pluginname', 'repository_upload')); @@ -677,40 +689,6 @@ protected function upload_file_to_filemanager_questionnaire($filepath, TableNode } - /** - * Try to get the filemanager node specified by the element - * - * @param string $filepickerelement - * @return 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. * From 22b1f3ee935422e08a25d2b0a290b77a489334f5 Mon Sep 17 00:00:00 2001 From: Laurent David Date: Wed, 15 Feb 2023 12:11:49 +0100 Subject: [PATCH 06/11] Add file question type --- classes/question/file.php | 27 ++++++------- tests/behat/file_question.feature | 64 ++++++++++++++++++++++++------- 2 files changed, 61 insertions(+), 30 deletions(-) diff --git a/classes/question/file.php b/classes/question/file.php index 3860b84c..52d5156c 100644 --- a/classes/question/file.php +++ b/classes/question/file.php @@ -95,16 +95,11 @@ protected function question_survey_display($response, $descendantsdata, $blankqu // 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); + $options->client_id = uniqid(); + $options->itemid = $draftitemid; + $options->target = $this->id; + $options->name = $elname; + $fm = new form_filemanager($options); $output = $PAGE->get_renderer('core', 'files'); $html = '
' . @@ -146,12 +141,12 @@ public function response_complete($responsedata) { * @return array */ public static function get_file_manager_option() { - return [ - 'mainfile' => '', - 'subdirs' => false, - 'accepted_types' => array('image', '.pdf'), - 'maxfiles' => 1, - ]; + $options = new \stdClass(); + $options->mainfile = ''; + $options->subdirs = false; + $options->accepted_types = ['image', '.pdf']; + $options->maxfiles = 1; + return $options; } /** diff --git a/tests/behat/file_question.feature b/tests/behat/file_question.feature index 0ff43934..855d8350 100644 --- a/tests/behat/file_question.feature +++ b/tests/behat/file_question.feature @@ -1,37 +1,41 @@ @mod @mod_questionnaire -Feature: In questionnaire, we can add a question requiring a file upload. +Feature: Add a question requiring a file upload in questionnaire. + In order to use this plugin + As a teacher + I need to add a a file question to a questionnaire created in my course + and a student answers to it. Then the file has to be accessible. Background: Given the following "users" exist: - | username | firstname | lastname | email | - | teacher1 | Teacher | 1 | teacher1@example.com | - | student1 | Student | 1 | student1@example.com | + | 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 | + | Course 1 | C1 | 0 | And the following "course enrolments" exist: - | user | course | role | - | teacher1 | C1 | editingteacher | - | student1 | C1 | student | + | 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 | + | activity | name | description | course | idnumber | resume | navigate | + | questionnaire | Test questionnaire | Test questionnaire description | C1 | questionnaire0 | 1 | 1 | @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. + Scenario: Add a single file question to a questionnaire and view an answer with an uploaded file. Given I log in as "teacher1" When I am on the "Test questionnaire" "questionnaire activity" page And I navigate to "Questions" in current page administration And I should see "Add questions" And I add a "File" question and I fill the form with: - | Question Name | File question | - | Yes | Yes | + | Question Name | File question | + | Yes | Yes | | Question Text | Add a file as an answer | And I log out And I log in as "student1" And I am on the "Test questionnaire" "questionnaire activity" page And I navigate to "Answer the questions..." in current page administration - And I upload "mod/questionnaire/tests/fixtures/testfilequestion.pdf" to questionnaire filemanager + And I upload "mod/questionnaire/tests/fixtures/testfilequestion.pdf" to questionnaire "Add a file as an answer" filemanager And I press "Submit questionnaire" And I should see "Thank you for completing this Questionnaire" And I press "Continue" @@ -42,3 +46,35 @@ Feature: In questionnaire, we can add a question requiring a file upload. And I am on the "Test questionnaire" "questionnaire activity" page And I navigate to "View all responses" in current page administration Then I should see "testfilequestion.pdf" + + @javascript @_file_upload + Scenario: Add two file questions to a questionnaire and view an answer with two uploaded file. + Given I log in as "teacher1" + When I am on the "Test questionnaire" "questionnaire activity" page + And I navigate to "Questions" in current page administration + And I should see "Add questions" + And I add a "File" question and I fill the form with: + | Question Name | File question one | + | Yes | Yes | + | Question Text | Add a first file as an answer | + And I add a "File" question and I fill the form with: + | Question Name | File question two | + | Yes | Yes | + | Question Text | Add a second file as an answer | + And I log out + And I log in as "student1" + And I am on the "Test questionnaire" "questionnaire activity" page + And I navigate to "Answer the questions..." in current page administration + And I upload "mod/questionnaire/tests/fixtures/testfilequestion.pdf" to questionnaire "Add a first file as an answer" filemanager + And I upload "mod/questionnaire/tests/fixtures/testfilequestion2.pdf" to questionnaire "Add a second file as an answer" filemanager + And I press "Submit questionnaire" + And I should see "Thank you for completing this Questionnaire" + And I press "Continue" + And I should see "View your response(s)" + And ".resourcecontent.resourcepdf" "css_element" should exist + And I log out + And I log in as "teacher1" + And I am on the "Test questionnaire" "questionnaire activity" page + And I navigate to "View all responses" in current page administration + Then I should see "testfilequestion.pdf" + And I should see "testfilequestion2.pdf" From bebcec1d80903ad630aadc8fa24223cc6fcd7793 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luca=20B=C3=B6sch?= Date: Sat, 20 Jan 2024 11:43:13 +0100 Subject: [PATCH 07/11] File question type Behat tests. --- classes/question/file.php | 2 +- classes/responsetype/file.php | 10 +++++----- tests/behat/behat_mod_questionnaire.php | 22 +++++++++++++++++----- tests/fixtures/testfilequestion2.pdf | Bin 0 -> 16338 bytes 4 files changed, 23 insertions(+), 11 deletions(-) create mode 100644 tests/fixtures/testfilequestion2.pdf diff --git a/classes/question/file.php b/classes/question/file.php index 52d5156c..6a0cd4a8 100644 --- a/classes/question/file.php +++ b/classes/question/file.php @@ -124,8 +124,8 @@ public function response_complete($responsedata) { ) { $answer = reset($responsedata->answers[$this->id]); $answered = ((int)$answer->value > 0); - // If $responsedata is webform data, check that it is not empty. } else if (isset($responsedata->{'q'.$this->id})) { + // If $responsedata is webform data, check that it is not empty. $draftitemid = (int)$responsedata->{'q' . $this->id}; if ($draftitemid > 0) { $info = file_get_draft_area_info($draftitemid); diff --git a/classes/responsetype/file.php b/classes/responsetype/file.php index b6024025..0af324c7 100644 --- a/classes/responsetype/file.php +++ b/classes/responsetype/file.php @@ -119,17 +119,17 @@ public static function response_answers_by_question($rid) { /** * Delete old entries from the questionnaire_response_file table and also the corresponding entries * in the files table. - * @param int $question_id - * @param int $response_id + * @param int $questionid + * @param int $responseid * @return void * @throws \dml_exception */ - public static function delete_old_response(int $question_id, int $response_id) { + public static function delete_old_response(int $questionid, int $responseid) { global $DB; // Check, if we have an old response file from a former attempt. $record = $DB->get_record(static::response_table(), [ - 'response_id' => $response_id, - 'question_id' => $question_id, + 'response_id' => $responseid, + 'question_id' => $questionid, ]); if ($record) { // Old record found, then delete all referenced entries in the files table and then delete this entry. diff --git a/tests/behat/behat_mod_questionnaire.php b/tests/behat/behat_mod_questionnaire.php index f4ddc7e2..5c520d2a 100644 --- a/tests/behat/behat_mod_questionnaire.php +++ b/tests/behat/behat_mod_questionnaire.php @@ -591,7 +591,7 @@ protected function get_filepicker_node($question) { $exception = new ExpectationException('The filepicker for the question with text "' . $question . '" can not be found', $this->getSession()); - $filepickercontainer = $this->find( + $filepickercontainer = $this->find( 'xpath', "//p[contains(.,'" . $question . "')]" . "//parent::div[contains(concat(' ', normalize-space(@class), ' '), ' no-overflow ')]" . @@ -615,7 +615,8 @@ protected function get_filepicker_node($question) { * @throws DriverException * @throws ExpectationException Thrown by behat_base::find */ - protected function upload_file_to_question_filemanager_questionnaire($filepath, $question, TableNode $data, $overwriteaction = false) { + protected function upload_file_to_question_filemanager_questionnaire($filepath, $question, TableNode $data, + $overwriteaction = false) { global $CFG; if (!$this->has_tag('_file_upload')) { @@ -701,7 +702,7 @@ 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 + // 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' @@ -723,7 +724,9 @@ protected function open_add_file_window($filemanagernode, $repositoryname) { $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( + // Apparently there are some of these repo elements. So if the first one is not visible, check out + // the next one. + $repositorylinks = $this->find_all( 'xpath', "//div[contains(concat(' ', normalize-space(@class), ' '), ' fp-repo-area ')]" . "//descendant::span[contains(concat(' ', normalize-space(@class), ' '), ' fp-repo-name ')]" . @@ -731,8 +734,17 @@ protected function open_add_file_window($filemanagernode, $repositoryname) { $repoexception ); + foreach ($repositorylinks as $repositorylink) { + try { + $this->ensure_node_is_visible($repositorylink); + } catch (Exception $exception) { + $repositorylink = $exception; + } + } + if ($repositorylink instanceof \Exception) { + throw new $repositorylink; + } // 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). diff --git a/tests/fixtures/testfilequestion2.pdf b/tests/fixtures/testfilequestion2.pdf new file mode 100644 index 0000000000000000000000000000000000000000..90588c3015a7864cb0ee78639de3b564b569a1a6 GIT binary patch literal 16338 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 Date: Fri, 5 Apr 2024 08:01:21 +0200 Subject: [PATCH 08/11] Make sid public, because its already used via #[\AllowDynamicProperties] Fix cleanup of old file responses. Add backup/restore handling for file response type. --- .../moodle2/backup_questionnaire_stepslib.php | 8 ++ .../restore_questionnaire_stepslib.php | 20 ++++ classes/feedback_section_form.php | 2 +- classes/question/drop.php | 3 +- classes/question/file.php | 63 ++++++------- classes/question/question.php | 19 ++-- classes/responsetype/file.php | 49 +++++++--- classes/responsetype/response/response.php | 2 +- myreport.php | 2 +- print.php | 2 +- report.php | 3 +- tests/behat/behat_mod_questionnaire.php | 21 +++-- tests/csvexport_test.php | 13 ++- tests/generator_test.php | 18 ++-- tests/lib_test.php | 93 +++++++++++++------ tests/privacy_provider_test.php | 9 +- tests/questiontypes_test.php | 84 +++++++++++------ tests/responsetypes_test.php | 70 ++++++++++---- view.php | 2 +- 19 files changed, 327 insertions(+), 156 deletions(-) diff --git a/backup/moodle2/backup_questionnaire_stepslib.php b/backup/moodle2/backup_questionnaire_stepslib.php index 92dcbfec..e2e89fc3 100644 --- a/backup/moodle2/backup_questionnaire_stepslib.php +++ b/backup/moodle2/backup_questionnaire_stepslib.php @@ -149,6 +149,10 @@ protected function define_structure() { $responsedate = new backup_nested_element('response_date', ['id'], ['response_id', 'question_id', 'response']); + $responsefiles = new backup_nested_element('response_files'); + + $responsefile = new backup_nested_element('response_file', ['id'], ['response_id', 'question_id', 'fileid']); + $responsemultiples = new backup_nested_element('response_multiples'); $responsemultiple = new backup_nested_element('response_multiple', ['id'], [ @@ -215,6 +219,9 @@ protected function define_structure() { $response->add_child($responsedates); $responsedates->add_child($responsedate); + $response->add_child($responsefiles); + $responsefiles->add_child($responsefile); + $response->add_child($responsemultiples); $responsemultiples->add_child($responsemultiple); @@ -256,6 +263,7 @@ protected function define_structure() { $response->set_source_table('questionnaire_response', ['questionnaireid' => backup::VAR_PARENTID]); $responsebool->set_source_table('questionnaire_response_bool', ['response_id' => backup::VAR_PARENTID]); $responsedate->set_source_table('questionnaire_response_date', ['response_id' => backup::VAR_PARENTID]); + $responsefile->set_source_table('questionnaire_response_file', ['response_id' => backup::VAR_PARENTID]); $responsemultiple->set_source_table('questionnaire_resp_multiple', ['response_id' => backup::VAR_PARENTID]); $responseother->set_source_table('questionnaire_response_other', ['response_id' => backup::VAR_PARENTID]); $responserank->set_source_table('questionnaire_response_rank', ['response_id' => backup::VAR_PARENTID]); diff --git a/backup/moodle2/restore_questionnaire_stepslib.php b/backup/moodle2/restore_questionnaire_stepslib.php index 82bfc8d8..2377668e 100644 --- a/backup/moodle2/restore_questionnaire_stepslib.php +++ b/backup/moodle2/restore_questionnaire_stepslib.php @@ -127,6 +127,10 @@ protected function define_structure() { 'questionnaire_response_date', '/activity/questionnaire/responses/response/response_dates/response_date' ); + $paths[] = new restore_path_element( + 'questionnaire_response_file', + '/activity/questionnaire/responses/response/response_files/response_file' + ); $paths[] = new restore_path_element( 'questionnaire_response_multiple', '/activity/questionnaire/responses/response/response_multiples/response_multiple' @@ -381,6 +385,22 @@ protected function process_questionnaire_response_date($data) { $DB->insert_record('questionnaire_response_date', $data); } + /** + * Process file responses. + * @param array $data + * @throws dml_exception + */ + protected function process_questionnaire_response_file($data) { + global $DB; + + $data = (object)$data; + $data->response_id = $this->get_new_parentid('questionnaire_response'); + $data->question_id = $this->get_mappingid('questionnaire_question', $data->question_id); + + // Insert the questionnaire_response_file record. + $DB->insert_record('questionnaire_response_file', $data); + } + /** * Process multiple responses. * @param array $data diff --git a/classes/feedback_section_form.php b/classes/feedback_section_form.php index 9ab00ceb..dc605a12 100644 --- a/classes/feedback_section_form.php +++ b/classes/feedback_section_form.php @@ -39,7 +39,7 @@ class feedback_section_form extends \moodleform { public $context; /** @var int $sid The section id. */ - protected $sid; + public $sid; /** * Form definition. diff --git a/classes/question/drop.php b/classes/question/drop.php index f611aaad..27c2a1d5 100644 --- a/classes/question/drop.php +++ b/classes/question/drop.php @@ -17,6 +17,7 @@ namespace mod_questionnaire\question; use html_writer; use mod_questionnaire\question\choice; +use mod_questionnaire\responsetype\answer\answer; use mod_questionnaire\responsetype\response\response; /** @@ -137,7 +138,7 @@ protected function response_survey_display($response) { $resptags->options[] = (object)['value' => '', 'label' => get_string('choosedots')]; if (!isset($response->answers[$this->id])) { - $response->answers[$this->id][] = new \mod_questionnaire\responsetype\answer\answer(); + $response->answers[$this->id][] = new answer(); } foreach ($this->choices as $id => $choice) { diff --git a/classes/question/file.php b/classes/question/file.php index 6a0cd4a8..c24c6227 100644 --- a/classes/question/file.php +++ b/classes/question/file.php @@ -13,6 +13,7 @@ // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . + namespace mod_questionnaire\question; use core_media_manager; use form_filemanager; @@ -30,7 +31,6 @@ * @package mod_questionnaire */ class file extends question { - /** * Get name. * @@ -86,20 +86,28 @@ protected function question_survey_display($response, $descendantsdata, $blankqu } else { $draftitemid = file_get_submitted_draft_itemid($elname); } - $options = self::get_file_manager_option(); if ($draftitemid > 0) { - file_prepare_draft_area($draftitemid, $this->context->id, 'mod_questionnaire', 'file', $this->id, $options); + file_prepare_draft_area( + $draftitemid, + $this->context->id, + 'mod_questionnaire', + 'file', + $this->id, + self::get_file_manager_option() + ); } else { $draftitemid = file_get_unused_draft_itemid(); } // 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"); - $options->client_id = uniqid(); - $options->itemid = $draftitemid; - $options->target = $this->id; - $options->name = $elname; - $fm = new form_filemanager($options); + $options = array_merge(self::get_file_manager_option(), [ + 'client_id' => uniqid(), + 'itemid' => $draftitemid, + 'target' => $this->id, + 'name' => $elname, + ]); + $fm = new form_filemanager((object)$options); $output = $PAGE->get_renderer('core', 'files'); $html = '
' . @@ -119,13 +127,14 @@ protected function question_survey_display($response, $descendantsdata, $blankqu public function response_complete($responsedata) { $answered = false; // If $responsedata is a response object, look through the answers. - if (is_a($responsedata, 'mod_questionnaire\responsetype\response\response') && - isset($responsedata->answers[$this->id]) && !empty($responsedata->answers[$this->id]) + if ( + is_a($responsedata, 'mod_questionnaire\responsetype\response\response') && + isset($responsedata->answers[$this->id]) && + !empty($responsedata->answers[$this->id]) ) { $answer = reset($responsedata->answers[$this->id]); $answered = ((int)$answer->value > 0); - } else if (isset($responsedata->{'q'.$this->id})) { - // If $responsedata is webform data, check that it is not empty. + } else if (isset($responsedata->{'q' . $this->id})) { // If $responsedata is webform data, check that it is not empty. $draftitemid = (int)$responsedata->{'q' . $this->id}; if ($draftitemid > 0) { $info = file_get_draft_area_info($draftitemid); @@ -141,12 +150,12 @@ public function response_complete($responsedata) { * @return array */ public static function get_file_manager_option() { - $options = new \stdClass(); - $options->mainfile = ''; - $options->subdirs = false; - $options->accepted_types = ['image', '.pdf']; - $options->maxfiles = 1; - return $options; + return [ + 'mainfile' => '', + 'subdirs' => false, + 'accepted_types' => ['image', '.pdf'], + 'maxfiles' => 1, + ]; } /** @@ -183,26 +192,19 @@ protected function response_survey_display($data) { $title = ''; $mediamanager = core_media_manager::instance($PAGE); - $embedoptions = array( + $embedoptions = [ 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. + } 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. + } 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. + } 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); } @@ -231,5 +233,4 @@ protected function form_length(MoodleQuickForm $mform, $helpname = '') { 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 02212586..0ae9937a 100644 --- a/classes/question/question.php +++ b/classes/question/question.php @@ -14,14 +14,6 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . -namespace mod_questionnaire\question; -use mod_questionnaire\edit_question_form; -use mod_questionnaire\responsetype\response\response; -use questionnaire; - -defined('MOODLE_INTERNAL') || die(); -use html_writer; - /** * This file contains the parent class for questionnaire question types. * @@ -30,7 +22,16 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU Public License * @package mod_questionnaire */ - // Constants. + +namespace mod_questionnaire\question; +use mod_questionnaire\edit_question_form; +use mod_questionnaire\responsetype\response\response; +use questionnaire; +use html_writer; + +defined('MOODLE_INTERNAL') || die(); + +// Constants. define('QUESCHOOSE', 0); define('QUESYESNO', 1); define('QUESTEXT', 2); diff --git a/classes/responsetype/file.php b/classes/responsetype/file.php index 0af324c7..1005a18f 100644 --- a/classes/responsetype/file.php +++ b/classes/responsetype/file.php @@ -13,6 +13,7 @@ // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . + namespace mod_questionnaire\responsetype; use mod_questionnaire\db\bulk_sql_config; @@ -43,14 +44,23 @@ public static function answers_from_webform($responsedata, $question) { $record->responseid = $responsedata->rid; $record->questionid = $question->id; - file_save_draft_area_files($val, $question->context->id, - 'mod_questionnaire', 'file', $val, - \mod_questionnaire\question\file::get_file_manager_option()); + file_save_draft_area_files( + $val, + $question->context->id, + 'mod_questionnaire', + 'file', + $val, + \mod_questionnaire\question\file::get_file_manager_option() + ); $fs = get_file_storage(); - $files = $fs->get_area_files($question->context->id, 'mod_questionnaire', - 'file', $val, + $files = $fs->get_area_files( + $question->context->id, + 'mod_questionnaire', + 'file', + $val, "itemid, filepath, filename", - false); + false + ); if (!empty($files)) { $file = reset($files); $record->value = $file->get_id(); @@ -133,7 +143,7 @@ public static function delete_old_response(int $questionid, int $responseid) { ]); if ($record) { // Old record found, then delete all referenced entries in the files table and then delete this entry. - $DB->delete_records('files', ['component' => 'mod_questionnaire', 'itemid' => $record->itemid]); + $DB->delete_records('files', ['component' => 'mod_questionnaire', 'itemid' => $record->id]); $DB->delete_records(self::response_table(), ['id' => $record->id]); } } @@ -171,8 +181,13 @@ public function insert_response($responsedata) { if (!$olditem) { return false; } - $siblings = $DB->get_records('files', - ['component' => 'mod_questionnaire', 'itemid' => $olditem->itemid]); + $siblings = $DB->get_records( + 'files', + [ + 'component' => 'mod_questionnaire', + 'itemid' => $olditem->itemid, + ] + ); foreach ($siblings as $sibling) { if (!self::fix_file_itemid($recordid, $sibling)) { return false; @@ -199,8 +214,14 @@ public static function fix_file_itemid(int $recordid, \stdClass $filerecord): bo } $fs = get_file_storage(); $file = $fs->get_file_instance($filerecord); - $newhash = $fs->get_pathname_hash($filerecord->contextid, $filerecord->component, - $filerecord->filearea, $recordid, $file->get_filepath(), $file->get_filename()); + $newhash = $fs->get_pathname_hash( + $filerecord->contextid, + $filerecord->component, + $filerecord->filearea, + $recordid, + $file->get_filepath(), + $file->get_filename() + ); $filerecord->itemid = $recordid; $filerecord->pathnamehash = $newhash; return $DB->update_record('files', $filerecord); @@ -266,7 +287,7 @@ public function get_results($rids = false, $anonymous = false) { $rsql = ''; if (!empty($rids)) { - list($rsql, $params) = $DB->get_in_or_equal($rids); + [$rsql, $params] = $DB->get_in_or_equal($rids); $rsql = ' AND response_id ' . $rsql; } @@ -337,7 +358,8 @@ public function get_results_tags($weights, $participants, $respondents, $showtot $file->get_filearea(), $file->get_itemid(), $file->get_filepath(), - $file->get_filename()); + $file->get_filename() + ); $response->text = \html_writer::link($imageurl, $file->get_filename()); if ($viewsingleresponse && $nonanonymous) { @@ -418,4 +440,3 @@ 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 90246870..b24378a2 100644 --- a/classes/responsetype/response/response.php +++ b/classes/responsetype/response/response.php @@ -111,7 +111,7 @@ public static function create_from_data($responsedata) { /** * Provide a response object from web form data to the question. * - * @param \stdClass $responsedata All the responsedata as an object. + * @param \stdClass $responsedata All of the responsedata as an object. * @param array $questions * @return response A response object. */ diff --git a/myreport.php b/myreport.php index faf95861..514e89d9 100644 --- a/myreport.php +++ b/myreport.php @@ -17,7 +17,7 @@ /** * This page shows results of a questionnaire to a student. * - * @package mod_questionnaire + * @package mod_questionnaire * @copyright 2016 Mike Churchward (mike.churchward@poetgroup.org) * @author Mike Churchward * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later diff --git a/print.php b/print.php index 5f879813..1aab5a73 100644 --- a/print.php +++ b/print.php @@ -17,7 +17,7 @@ /** * The main page to print a questionnaire. * - * @package mod_questionnaire + * @package mod_questionnaire * @copyright 2016 Mike Churchward (mike.churchward@poetgroup.org) * @author Mike Churchward * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later diff --git a/report.php b/report.php index 241a3b08..ba4adc83 100755 --- a/report.php +++ b/report.php @@ -17,12 +17,13 @@ /** * The main report page for a questionnaire. * - * @package mod_questionnaire + * @package mod_questionnaire * @copyright 2016 Mike Churchward (mike.churchward@poetgroup.org) * @author Mike Churchward * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * */ + require_once("../../config.php"); require_once($CFG->dirroot . '/mod/questionnaire/questionnaire.class.php'); diff --git a/tests/behat/behat_mod_questionnaire.php b/tests/behat/behat_mod_questionnaire.php index 5c520d2a..a0d42e20 100644 --- a/tests/behat/behat_mod_questionnaire.php +++ b/tests/behat/behat_mod_questionnaire.php @@ -583,10 +583,10 @@ public function i_upload_file_to_questionnaire_question_filemanager($filepath, $ /** * Try to get the filemanager node of a given question. * - * @param $question + * @param string $question * @return \Behat\Mink\Element\NodeElement|null */ - protected function get_filepicker_node($question) { + protected function get_filepicker_node(string $question) { // More info about the problem (in case there is a problem). $exception = new ExpectationException('The filepicker for the question with text "' . $question . '" can not be found', $this->getSession()); @@ -615,8 +615,12 @@ protected function get_filepicker_node($question) { * @throws DriverException * @throws ExpectationException Thrown by behat_base::find */ - protected function upload_file_to_question_filemanager_questionnaire($filepath, $question, TableNode $data, - $overwriteaction = false) { + protected function upload_file_to_question_filemanager_questionnaire( + $filepath, + $question, + TableNode $data, + $overwriteaction = false + ) { global $CFG; if (!$this->has_tag('_file_upload')) { @@ -665,7 +669,6 @@ protected function upload_file_to_question_filemanager_questionnaire($filepath, // 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. @@ -687,7 +690,6 @@ protected function upload_file_to_question_filemanager_questionnaire($filepath, // We wait for all the JS to finish. $this->getSession()->wait(self::get_timeout(), self::PAGE_READY_JS); } - } /** @@ -705,7 +707,7 @@ protected function open_add_file_window($filemanagernode, $repositoryname) { // 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' + $filemanagernode, 'NodeElement', ]); // Wait for the default repository (if any) to load. This checks that @@ -714,7 +716,8 @@ protected function open_add_file_window($filemanagernode, $repositoryname) { "//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'); + 'xpath_element' + ); // Getting the repository link and opening it. $repoexception = @@ -742,7 +745,7 @@ protected function open_add_file_window($filemanagernode, $repositoryname) { } } if ($repositorylink instanceof \Exception) { - throw new $repositorylink; + throw new $repositorylink(); } // Selecting the repo. if (!$repositorylink->getParent()->getParent()->hasClass('active')) { diff --git a/tests/csvexport_test.php b/tests/csvexport_test.php index 6c58089d..714f3da9 100644 --- a/tests/csvexport_test.php +++ b/tests/csvexport_test.php @@ -14,6 +14,15 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . +/** + * Performance test for questionnaire module. + * @package mod_questionnaire + * @group mod_questionnaire + * @author Guy Thomas + * @copyright Copyright (c) 2015 Moodlerooms Inc. (http://www.moodlerooms.com) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + namespace mod_questionnaire; /** @@ -50,9 +59,9 @@ private function get_csv_text(array $rows) { } /** - * Test case for the csvexport method. + * Tests the CSV export. * - * @covers ::csvexport + * @covers \questionnaire::generate_csv */ public function test_csvexport(): void { $this->resetAfterTest(); diff --git a/tests/generator_test.php b/tests/generator_test.php index b90d0504..455230e8 100644 --- a/tests/generator_test.php +++ b/tests/generator_test.php @@ -14,8 +14,6 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . -namespace mod_questionnaire; - /** * PHPUnit questionnaire generator tests * @@ -33,9 +31,13 @@ */ final class generator_test extends \advanced_testcase { /** - * Test case for the create_instance function. + * Test generator create_instance function. * - * @covers \mod_questionnaire_generator::create_instance + * @return void + * @throws coding_exception + * @throws dml_exception + * + * @covers \mod_questionnaire\generator\ */ public function test_create_instance(): void { global $DB; @@ -72,9 +74,13 @@ public function test_create_instance(): void { } /** - * Test case for the create_content function. + * Test generator create_content function. + * + * @return void + * @throws coding_exception + * @throws dml_exception * - * @covers \mod_questionnaire_generator::create_content + * @covers \mod_questionnaire\generator\ */ public function test_create_content(): void { global $DB; diff --git a/tests/lib_test.php b/tests/lib_test.php index 91b93d92..952c52a8 100644 --- a/tests/lib_test.php +++ b/tests/lib_test.php @@ -14,6 +14,15 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . +/** + * PHPUnit questionnaire generator tests + * + * @package mod_questionnaire + * @copyright 2015 Mike Churchward (mike@churchward.ca) + * @author Mike Churchward + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + namespace mod_questionnaire; use mod_questionnaire\question\question; @@ -25,18 +34,16 @@ require_once($CFG->dirroot . '/mod/questionnaire/classes/question/question.php'); /** - * PHPUnit questionnaire lib tests - * - * @package mod_questionnaire - * @copyright 2015 Mike Churchward (mike@churchward.ca) - * @author Mike Churchward - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * Unit tests for questionnaire_lib_testcase. + * @group mod_questionnaire */ final class lib_test extends \advanced_testcase { /** - * Test case for the questionnaire_supports function. + * Test for questionnaire_supports. + * + * @return void * - * @covers ::questionnaire_supports + * @covers \questionnaire_supports */ public function test_questionnaire_supports(): void { $this->assertTrue(questionnaire_supports(FEATURE_BACKUP_MOODLE2)); @@ -52,9 +59,11 @@ public function test_questionnaire_supports(): void { } /** - * Test case for the questionnaire_get_extra_capabilities function. + * Test for questionnaire_get_extra_capabilities. * - * @covers ::questionnaire_get_extra_capabilities + * @return void + * + * @covers \questionnaire_get_extra_capabilities */ public function test_questionnaire_get_extra_capabilities(): void { $caps = questionnaire_get_extra_capabilities(); @@ -64,9 +73,12 @@ public function test_questionnaire_get_extra_capabilities(): void { } /** - * Test case for the questionnaire_add_instance function. + * Test for questionnaire_add_instance. + * + * @return void + * @throws moodle_exception * - * @covers ::questionnaire_add_instance + * @covers \questionnaire_add_instance */ public function test_add_instance(): void { $this->resetAfterTest(); @@ -99,9 +111,12 @@ public function test_add_instance(): void { } /** - * Test case for the questionnaire_update_instance function. + * Test for questionnaire_update_instance(). * - * @covers ::questionnaire_update_instance + * @return void + * @throws dml_exception + * + * @covers \questionnaire_update_instance */ public function test_update_instance(): void { global $DB; @@ -160,9 +175,13 @@ public function test_update_instance(): void { /** * Test for questionnaire_delete_instance(). + * * Need to verify that delete_instance deletes all data associated with a questionnaire. * - * @covers ::questionnaire_delete_instance + * @return void + * @throws dml_exception + * + * @covers \questionnaire_delete_instance */ public function test_delete_instance(): void { global $DB; @@ -196,9 +215,12 @@ public function test_delete_instance(): void { } /** - * Test case for the questionnaire_user_outline function. + * Test for questionnaire_user_outline(). * - * @covers ::questionnaire_user_outline + * @return void + * @throws coding_exception + * + * @covers \questionnaire_user_outline */ public function test_questionnaire_user_outline(): void { $this->resetAfterTest(); @@ -221,9 +243,12 @@ public function test_questionnaire_user_outline(): void { } /** - * Test case for the questionnaire_user_complete function. + * Test for questionnaire_user_complete(). * - * @covers ::questionnaire_user_complete + * @return void + * @throws coding_exception + * + * @covers \questionnaire_user_complete */ public function test_questionnaire_user_complete(): void { $this->resetAfterTest(); @@ -238,9 +263,11 @@ public function test_questionnaire_user_complete(): void { } /** - * Test case for the questionnaire_print_recent_activity function. + * Test for questionnaire_print_recent_activity(). * - * @covers ::questionnaire_print_recent_activity + * @return void + * + * @covers \questionnaire_print_recent_activity */ public function test_questionnaire_print_recent_activity(): void { $this->resetAfterTest(); @@ -249,9 +276,11 @@ public function test_questionnaire_print_recent_activity(): void { } /** - * Test case for the questionnaire_grades function. + * Test for questionnaire_grades(). + * + * @return void * - * @covers ::questionnaire_grades + * @covers \questionnaire_grades */ public function test_questionnaire_grades(): void { $this->resetAfterTest(); @@ -260,9 +289,11 @@ public function test_questionnaire_grades(): void { } /** - * Test case for the questionnaire_get_user_grades function. + * Test for questionnaire_get_user_grades(). + * + * @return void * - * @covers ::questionnaire_get_user_grades + * @covers \questionnaire_get_user_grades */ public function test_questionnaire_get_user_grades(): void { $this->resetAfterTest(); @@ -283,9 +314,11 @@ public function test_questionnaire_get_user_grades(): void { } /** - * Test case for the questionnaire_update_grades function. + * Test for questionnaire_update_grades(). * - * @covers ::questionnaire_update_grades + * @return void + * + * @covers \questionnaire_update_grades */ public function test_questionnaire_update_grades(): void { // Don't know how to test this yet! It doesn't return anything. @@ -293,9 +326,11 @@ public function test_questionnaire_update_grades(): void { } /** - * Test case for the questionnaire_grade_item_update function. + * Test for questionnaire_grade_item_update(). + * + * @return void * - * @covers ::questionnaire_grade_item_update + * @covers \questionnaire_grade_item_update */ public function test_questionnaire_grade_item_update(): void { $this->resetAfterTest(); diff --git a/tests/privacy_provider_test.php b/tests/privacy_provider_test.php index 1b2e477f..b4aa5884 100644 --- a/tests/privacy_provider_test.php +++ b/tests/privacy_provider_test.php @@ -14,7 +14,14 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . -namespace mod_questionnaire; +/** + * Privacy test for the mod questionnaire. + * + * @package mod_questionnaire + * @copyright 2019, onwards Poet + * @author Mike Churchward + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ namespace mod_questionnaire; diff --git a/tests/questiontypes_test.php b/tests/questiontypes_test.php index 57bae4a7..6f23fa12 100644 --- a/tests/questiontypes_test.php +++ b/tests/questiontypes_test.php @@ -14,6 +14,15 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . +/** + * PHPUnit questionnaire generator tests + * + * @package mod_questionnaire + * @copyright 2015 Mike Churchward (mike@churchward.ca) + * @author Mike Churchward + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + namespace mod_questionnaire; use mod_questionnaire\question\question; @@ -25,18 +34,16 @@ require_once($CFG->dirroot . '/mod/questionnaire/classes/question/question.php'); /** - * PHPUnit questionnaire questiontypes tests - * - * @package mod_questionnaire - * @copyright 2015 Mike Churchward (mike@churchward.ca) - * @author Mike Churchward - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * Unit tests for questionnaire_questiontypes_testcase. + * @group mod_questionnaire */ final class questiontypes_test extends \advanced_testcase { /** - * Test case for the create_test_question_with_choices function for checkbox questions. + * Create a check boxes test question. * - * @covers ::create_test_question_with_choices + * @return void + * + * @covers \mod_questionnaire\questiontypes_test::create_test_question */ public function test_create_question_checkbox(): void { $this->create_test_question_with_choices( @@ -47,27 +54,33 @@ public function test_create_question_checkbox(): void { } /** - * Test case for the create_test_question function for date questions. + * Create a date test question. + * + * @return void * - * @covers ::create_test_question + * @covers \mod_questionnaire\questiontypes_test::create_test_question */ public function test_create_question_date(): void { $this->create_test_question(QUESDATE, '\\mod_questionnaire\\question\\date', ['content' => 'Enter a date']); } /** - * Test case for the create_test_question_with_choices function for dropdown questions. + * Create a dropdown box test question. + * + * @return void * - * @covers ::create_test_question_with_choices + * @covers \mod_questionnaire\questiontypes_test::create_test_question */ public function test_create_question_dropdown(): void { $this->create_test_question_with_choices(QUESDROP, '\\mod_questionnaire\\question\\drop', ['content' => 'Select one']); } /** - * Test case for the create_test_question function for essay questions. + * Create an essay test question. * - * @covers ::create_test_question + * @return void + * + * @covers \mod_questionnaire\questiontypes_test::create_test_question */ public function test_create_question_essay(): void { $questiondata = [ @@ -79,9 +92,11 @@ public function test_create_question_essay(): void { } /** - * Test case for the create_test_question function for sectiontext questions. + * Create a sectiontext test question. + * + * @return void * - * @covers ::create_test_question + * @covers \mod_questionnaire\questiontypes_test::create_test_question */ public function test_create_question_sectiontext(): void { $this->create_test_question( @@ -92,9 +107,11 @@ public function test_create_question_sectiontext(): void { } /** - * Test case for the create_test_question function for numeric questions. + * Create a numerical test question. + * + * @return void * - * @covers ::create_test_question + * @covers \mod_questionnaire\questiontypes_test::create_test_question */ public function test_create_question_numeric(): void { $questiondata = [ @@ -106,27 +123,33 @@ public function test_create_question_numeric(): void { } /** - * Test case for the create_test_question_with_choices function for radiobuttons questions. + * Create a radio test question. * - * @covers ::create_test_question_with_choices + * @return void + * + * @covers \mod_questionnaire\questiontypes_test::create_test_question */ public function test_create_question_radiobuttons(): void { $this->create_test_question_with_choices(QUESRADIO, '\\mod_questionnaire\\question\\radio', ['content' => 'Choose one']); } /** - * Test case for the create_test_question_with_choices function for ratescale questions. + * Create a rate test question. + * + * @return void * - * @covers ::create_test_question_with_choices + * @covers \mod_questionnaire\questiontypes_test::create_test_question */ public function test_create_question_ratescale(): void { $this->create_test_question_with_choices(QUESRATE, '\\mod_questionnaire\\question\\rate', ['content' => 'Rate these']); } /** - * Test case for the create_test_question function for textbox questions. + * Create a text test question. + * + * @return void * - * @covers ::create_test_question + * @covers \mod_questionnaire\questiontypes_test::create_test_question */ public function test_create_question_textbox(): void { $questiondata = [ @@ -138,9 +161,11 @@ public function test_create_question_textbox(): void { } /** - * Test case for the create_test_question function for slider questions. + * Create a slider test question. * - * @covers ::create_test_question + * @return void + * + * @covers \mod_questionnaire\questiontypes_test::create_test_question */ public function test_create_question_slider(): void { $questiondata = ['content' => 'Enter a number']; @@ -148,14 +173,17 @@ public function test_create_question_slider(): void { } /** - * Test case for the create_test_question function for yesno questions. + * Create a yes/no test question. + * + * @return void * - * @covers ::create_test_question + * @covers \mod_questionnaire\questiontypes_test::create_test_question */ public function test_create_question_yesno(): void { $this->create_test_question(QUESYESNO, '\\mod_questionnaire\\question\\yesno', ['content' => 'Enter yes or no']); } + // General tests to call from specific tests above. /** diff --git a/tests/responsetypes_test.php b/tests/responsetypes_test.php index 1fc6f5e2..88995ff7 100644 --- a/tests/responsetypes_test.php +++ b/tests/responsetypes_test.php @@ -14,8 +14,21 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . +/** + * PHPUnit questionnaire generator tests + * + * @package mod_questionnaire + * @copyright 2015 Mike Churchward (mike@churchward.ca) + * @author Mike Churchward + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + namespace mod_questionnaire; +use mod_questionnaire\question\question; + +defined('MOODLE_INTERNAL') || die(); + global $CFG; require_once($CFG->dirroot . '/mod/questionnaire/locallib.php'); require_once($CFG->dirroot . '/mod/questionnaire/tests/generator_test.php'); @@ -23,18 +36,17 @@ require_once($CFG->dirroot . '/mod/questionnaire/classes/question/question.php'); /** - * PHPUnit questionnaire generator tests - * - * @package mod_questionnaire - * @copyright 2015 Mike Churchward (mike@churchward.ca) - * @author Mike Churchward - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * Unit tests for questionnaire_responsetypes_testcase. + * @group mod_questionnaire */ final class responsetypes_test extends \advanced_testcase { /** - * Test case for the create_response_boolean function. + * Test responses in a yes/no question. + * + * @return void + * @throws dml_exception * - * @covers \mod_questionnaire_generator::create_question_response + * @covers \mod_questionnaire\question\yesno */ public function test_create_response_boolean(): void { global $DB; @@ -63,9 +75,12 @@ public function test_create_response_boolean(): void { } /** - * Test case for the create_response_text function. + * Test responses in a essay question. * - * @covers \mod_questionnaire_generator::create_question_response + * @return void + * @throws dml_exception + * + * @covers \mod_questionnaire\question\essay */ public function test_create_response_text(): void { global $DB; @@ -95,9 +110,12 @@ public function test_create_response_text(): void { } /** - * Test case for the create_response_slider function. + * Test responses in a slider question. + * + * @return void + * @throws dml_exception * - * @covers \mod_questionnaire_generator::create_question_response + * @covers \mod_questionnaire\question\slider */ public function test_create_response_slider(): void { global $DB; @@ -127,9 +145,12 @@ public function test_create_response_slider(): void { } /** - * Test case for the create_response_date function. + * Test responses in a date question. + * + * @return void + * @throws dml_exception * - * @covers \mod_questionnaire_generator::create_question_response + * @covers \mod_questionnaire\question\date */ public function test_create_response_date(): void { global $DB; @@ -160,9 +181,12 @@ public function test_create_response_date(): void { } /** - * Test case for the create_response_single function. + * Test responses in a single choice radio question. * - * @covers \mod_questionnaire_generator::create_question_response + * @return void + * @throws dml_exception + * + * @covers \mod_questionnaire\question\radio */ public function test_create_response_single(): void { global $DB; @@ -236,9 +260,12 @@ public function test_create_response_single(): void { } /** - * Test case for the create_response_multiple function. + * Test responses in a multiple choices question. + * + * @return void + * @throws dml_exception * - * @covers \mod_questionnaire_generator::create_question_response + * @covers \mod_questionnaire\question\rate */ public function test_create_response_multiple(): void { global $DB; @@ -298,9 +325,12 @@ public function test_create_response_multiple(): void { } /** - * Test case for the create_response_rank function. + * Test response's ranks in a rate question. + * + * @return void + * @throws dml_exception * - * @covers \mod_questionnaire_generator::create_question_response + * @covers \mod_questionnaire\question\rate */ public function test_create_response_rank(): void { global $DB; diff --git a/view.php b/view.php index 1036d32a..687ef0cb 100644 --- a/view.php +++ b/view.php @@ -17,7 +17,7 @@ /** * This main view page for a questionnaire. * - * @package mod_questionnaire + * @package mod_questionnaire * @copyright 2016 Mike Churchward (mike.churchward@poetgroup.org) * @author Mike Churchward * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later From 8d9e7802bfb9ff618b01af1a475772e6947a55b4 Mon Sep 17 00:00:00 2001 From: Stephan Robotta Date: Thu, 27 Nov 2025 13:36:57 +0100 Subject: [PATCH 09/11] Adjust code after feedback for PR. --- .../moodle2/backup_questionnaire_stepslib.php | 1 + classes/feedback_section_form.php | 3 - classes/question/drop.php | 3 +- classes/question/file.php | 59 +++++------- classes/responsetype/file.php | 14 ++- db/upgrade.php | 15 --- lib.php | 4 +- myreport.php | 2 +- print.php | 2 +- questionnaire.class.php | 4 +- report.php | 2 +- tests/behat/file_question_multiple.feature | 94 +++++++++++++++++++ .../questionnaire_activity_completion.feature | 4 +- 13 files changed, 141 insertions(+), 66 deletions(-) create mode 100644 tests/behat/file_question_multiple.feature diff --git a/backup/moodle2/backup_questionnaire_stepslib.php b/backup/moodle2/backup_questionnaire_stepslib.php index e2e89fc3..3d5b4974 100644 --- a/backup/moodle2/backup_questionnaire_stepslib.php +++ b/backup/moodle2/backup_questionnaire_stepslib.php @@ -264,6 +264,7 @@ protected function define_structure() { $responsebool->set_source_table('questionnaire_response_bool', ['response_id' => backup::VAR_PARENTID]); $responsedate->set_source_table('questionnaire_response_date', ['response_id' => backup::VAR_PARENTID]); $responsefile->set_source_table('questionnaire_response_file', ['response_id' => backup::VAR_PARENTID]); + $responsefile->annotate_files('mod_questionnaire', 'response_file', null); $responsemultiple->set_source_table('questionnaire_resp_multiple', ['response_id' => backup::VAR_PARENTID]); $responseother->set_source_table('questionnaire_response_other', ['response_id' => backup::VAR_PARENTID]); $responserank->set_source_table('questionnaire_response_rank', ['response_id' => backup::VAR_PARENTID]); diff --git a/classes/feedback_section_form.php b/classes/feedback_section_form.php index dc605a12..88cc173b 100644 --- a/classes/feedback_section_form.php +++ b/classes/feedback_section_form.php @@ -38,9 +38,6 @@ class feedback_section_form extends \moodleform { */ public $context; - /** @var int $sid The section id. */ - public $sid; - /** * Form definition. */ diff --git a/classes/question/drop.php b/classes/question/drop.php index 27c2a1d5..f611aaad 100644 --- a/classes/question/drop.php +++ b/classes/question/drop.php @@ -17,7 +17,6 @@ namespace mod_questionnaire\question; use html_writer; use mod_questionnaire\question\choice; -use mod_questionnaire\responsetype\answer\answer; use mod_questionnaire\responsetype\response\response; /** @@ -138,7 +137,7 @@ protected function response_survey_display($response) { $resptags->options[] = (object)['value' => '', 'label' => get_string('choosedots')]; if (!isset($response->answers[$this->id])) { - $response->answers[$this->id][] = new answer(); + $response->answers[$this->id][] = new \mod_questionnaire\responsetype\answer\answer(); } foreach ($this->choices as $id => $choice) { diff --git a/classes/question/file.php b/classes/question/file.php index c24c6227..5622b4d4 100644 --- a/classes/question/file.php +++ b/classes/question/file.php @@ -80,42 +80,31 @@ protected function question_survey_display($response, $descendantsdata, $blankqu require_once($CFG->libdir . '/filelib.php'); $elname = 'q' . $this->id; - // Make sure there is a response, fetch the draft id from the original request. - if (isset($response->answers[$this->id]) && !empty($response->answers[$this->id]) && isset($_REQUEST[$elname . 'draft'])) { - $draftitemid = (int)$_REQUEST[$elname . 'draft']; - } else { - $draftitemid = file_get_submitted_draft_itemid($elname); - } - if ($draftitemid > 0) { - file_prepare_draft_area( - $draftitemid, - $this->context->id, - 'mod_questionnaire', - 'file', - $this->id, - self::get_file_manager_option() - ); - } else { - $draftitemid = file_get_unused_draft_itemid(); - } + // If there is a response and the response is resumed, get the original itemid. + $itemid = ( + isset($response->answers[$this->id]) && + !empty($response->answers[$this->id]) && + isset($_REQUEST['resume']) && + $_REQUEST['resume'] === '1' + ) ? (int)$response->answers[$this->id][0]->id : 0; + // Prepare the draft area. + $draftitemid = file_get_submitted_draft_itemid($elname); + file_prepare_draft_area( + $draftitemid, + $this->context->id, + 'mod_questionnaire', + 'response_file', + $itemid, + self::get_file_manager_option() + ); // 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"); - - $options = array_merge(self::get_file_manager_option(), [ - 'client_id' => uniqid(), - 'itemid' => $draftitemid, - 'target' => $this->id, - 'name' => $elname, - ]); - $fm = new form_filemanager((object)$options); + $fm = new form_filemanager((object)self::get_file_manager_option($draftitemid)); $output = $PAGE->get_renderer('core', 'files'); - $html = '
' . $output->render($fm) . '' . - '' . '
'; - return $html; } @@ -145,21 +134,23 @@ public function response_complete($responsedata) { } /** - * Get file manager options + * Get file manager options, here allow only one image or pdf file, without + * creating subdirectories. * + * @param ?int $draftitemid * @return array */ - public static function get_file_manager_option() { + public static function get_file_manager_option(?int $draftitemid = 0) { return [ - 'mainfile' => '', 'subdirs' => false, - 'accepted_types' => ['image', '.pdf'], + 'accepted_types' => ['image', 'document'], 'maxfiles' => 1, + 'itemid' => $draftitemid, ]; } /** - * Response display output. + * Response display output, e.g. render the file image/pdf or show a linkt to download it. * * @param \stdClass $data * @return string diff --git a/classes/responsetype/file.php b/classes/responsetype/file.php index 1005a18f..116afa16 100644 --- a/classes/responsetype/file.php +++ b/classes/responsetype/file.php @@ -48,7 +48,7 @@ public static function answers_from_webform($responsedata, $question) { $val, $question->context->id, 'mod_questionnaire', - 'file', + 'response_file', $val, \mod_questionnaire\question\file::get_file_manager_option() ); @@ -56,7 +56,7 @@ public static function answers_from_webform($responsedata, $question) { $files = $fs->get_area_files( $question->context->id, 'mod_questionnaire', - 'file', + 'response_file', $val, "itemid, filepath, filename", false @@ -143,7 +143,14 @@ public static function delete_old_response(int $questionid, int $responseid) { ]); if ($record) { // Old record found, then delete all referenced entries in the files table and then delete this entry. - $DB->delete_records('files', ['component' => 'mod_questionnaire', 'itemid' => $record->id]); + $DB->delete_records( + 'files', + [ + 'component' => 'mod_questionnaire', + 'filearea' => 'response_file', + 'itemid' => $record->id, + ] + ); $DB->delete_records(self::response_table(), ['id' => $record->id]); } } @@ -185,6 +192,7 @@ public function insert_response($responsedata) { 'files', [ 'component' => 'mod_questionnaire', + 'filearea' => 'response_file', 'itemid' => $olditem->itemid, ] ); diff --git a/db/upgrade.php b/db/upgrade.php index 3b9f291a..7895602f 100644 --- a/db/upgrade.php +++ b/db/upgrade.php @@ -1062,21 +1062,6 @@ function xmldb_questionnaire_upgrade($oldversion = 0) { upgrade_mod_savepoint(true, 2023101500, 'questionnaire'); } - if ($oldversion < 2023101501) { - // Upgrade files.itemid with questionnaire_response_file.id. - $filesresponses = $DB->get_records('questionnaire_response_file', [], '', 'id,fileid'); - $idmap = []; - foreach ($filesresponses as $fileresponse) { - $idmap[(int)$fileresponse->fileid] = (int)$fileresponse->id; - } - $filerecords = $DB->get_records_list('files', 'id', array_keys($idmap), 'id desc'); - foreach ($filerecords as $filerecord) { - \mod_questionnaire\responsetype\file::fix_file_itemid($idmap[(int)$filerecord->id], $filerecord); - } - // Questionnaire savepoint reached. - upgrade_mod_savepoint(true, 2023101501, 'questionnaire'); - } - if ($oldversion < 2025041400.01) { $table = new xmldb_table('questionnaire_question'); $index = new xmldb_index('quest_question_sididx', XMLDB_INDEX_NOTUNIQUE, ['surveyid', 'deleted']); diff --git a/lib.php b/lib.php index d5b7be41..ae680763 100644 --- a/lib.php +++ b/lib.php @@ -565,7 +565,7 @@ function questionnaire_pluginfile($course, $cm, $context, $filearea, $args, $for require_course_login($course, true, $cm); - $fileareas = ['intro', 'info', 'thankbody', 'question', 'feedbacknotes', 'sectionheading', 'feedback', 'file']; + $fileareas = ['intro', 'info', 'thankbody', 'question', 'feedbacknotes', 'sectionheading', 'feedback', 'response_file']; if (!in_array($filearea, $fileareas)) { return false; } @@ -584,7 +584,7 @@ function questionnaire_pluginfile($course, $cm, $context, $filearea, $args, $for if (!$DB->record_exists('questionnaire_feedback', ['id' => $componentid])) { return false; } - } else if ($filearea == 'file') { + } else if ($filearea == 'response_file') { if (!$DB->record_exists('questionnaire_response_file', ['id' => $componentid])) { return false; } diff --git a/myreport.php b/myreport.php index 514e89d9..faf95861 100644 --- a/myreport.php +++ b/myreport.php @@ -17,7 +17,7 @@ /** * This page shows results of a questionnaire to a student. * - * @package mod_questionnaire + * @package mod_questionnaire * @copyright 2016 Mike Churchward (mike.churchward@poetgroup.org) * @author Mike Churchward * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later diff --git a/print.php b/print.php index 1aab5a73..5f879813 100644 --- a/print.php +++ b/print.php @@ -17,7 +17,7 @@ /** * The main page to print a questionnaire. * - * @package mod_questionnaire + * @package mod_questionnaire * @copyright 2016 Mike Churchward (mike.churchward@poetgroup.org) * @author Mike Churchward * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later diff --git a/questionnaire.class.php b/questionnaire.class.php index b7c13936..d9cf9539 100644 --- a/questionnaire.class.php +++ b/questionnaire.class.php @@ -150,7 +150,7 @@ public function add_survey($sid = 0, $survey = null) { /** * Adding questions to the object. - * @param int $sid + * @param bool $sid */ public function add_questions($sid = 0) { global $DB; @@ -1745,7 +1745,7 @@ public function survey_print_render($courseid, $message = '', $referer = '', $ri $this->questions[$questionid]->set_isprint($referer === 'print'); $output .= $this->renderer->question_output( $this->questions[$questionid], - $this->responses[0] ?? [], + $this->responses[0] ?? new \mod_questionnaire\responsetype\response\response(), $i++, null, $dependants diff --git a/report.php b/report.php index ba4adc83..f1d92d38 100755 --- a/report.php +++ b/report.php @@ -17,7 +17,7 @@ /** * The main report page for a questionnaire. * - * @package mod_questionnaire + * @package mod_questionnaire * @copyright 2016 Mike Churchward (mike.churchward@poetgroup.org) * @author Mike Churchward * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later diff --git a/tests/behat/file_question_multiple.feature b/tests/behat/file_question_multiple.feature new file mode 100644 index 00000000..dfff7c6b --- /dev/null +++ b/tests/behat/file_question_multiple.feature @@ -0,0 +1,94 @@ +@mod @mod_questionnaire +Feature: Add a question requiring more than one file upload questions in a questionnaire. + In order to use this plugin + As a student + I need to add two files in a questionnaire and be able to save and resume the questionnaire. + + Background: + 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 multiple files | Test questionnaire description | C1 | questionnaire0 | 1 | 1 | + And I log in as "teacher1" + And I am on "Course 1" course homepage + And I follow "Test questionnaire multiple files" + And I navigate to "Questions" in current page administration + And I add a "Check Boxes" question and I fill the form with: + | Question Name | Q1 | + | Yes | Yes | + | Question Text | Please answer Q1 | + | Possible answers | yes,no,may be | + And I add a "File" question and I fill the form with: + | Question Name | File question One | + | Yes | Yes | + | Question Text | Add file1 as an answer | + And I add a "File" question and I fill the form with: + | Question Name | File question Two | + | Yes | No | + | Question Text | Add file2 as an answer | + And I log out + + @javascript @_file_upload + Scenario: Add one file to the questionnaire and verify that the uploaded files exists in the filepicker. + Given I log in as "student1" + When I am on the "Test questionnaire multiple files" "questionnaire activity" page + And I navigate to "Answer the questions..." in current page administration + And I upload "mod/questionnaire/tests/fixtures/testfilequestion.pdf" to questionnaire "Add file1 as an answer" filemanager + And I press "Submit questionnaire" + And I should not see "Thank you for completing this Questionnaire" + And I should see "Please answer required question #1." + And I should see "testfilequestion.pdf" in the "(//*[contains(@class, 'filemanager-container')])[1]//*[contains(@class, 'fp-filename')]" "xpath" + And I should see "You can drag and drop files here to add them." in the "(//*[contains(@class, 'filemanager-container')])[2]//*[contains(@class, 'dndupload-message')]" "xpath" + And I upload "mod/questionnaire/tests/fixtures/testfilequestion2.pdf" to questionnaire "Add file2 as an answer" filemanager + And I press "Submit questionnaire" + And I should not see "Thank you for completing this Questionnaire" + And I should see "Please answer required question #1." + And I set the field "may be" to "checked" + And I press "Submit questionnaire" + And I should see "Thank you for completing this Questionnaire" + And I press "Continue" + And I should see "View your response(s)" + And ".resourcecontent.resourcepdf" "css_element" should exist + And I log out + And I log in as "teacher1" + And I am on the "Test questionnaire multiple files" "questionnaire activity" page + And I navigate to "View all responses" in current page administration + Then I should see "testfilequestion.pdf" + And I should see "testfilequestion2.pdf" + + @javascript @_file_upload + Scenario: Add one file at a time when the questionnaire is saved and resumed and check that the filepicker contains the uploaded files. + Given I log in as "student1" + When I am on the "Test questionnaire multiple files" "questionnaire activity" page + And I navigate to "Answer the questions..." in current page administration + And I upload "mod/questionnaire/tests/fixtures/testfilequestion.pdf" to questionnaire "Add file2 as an answer" filemanager + And I press "Save and exit" + And I should see "Your progress has been saved." + And I click on "//a[contains(@class, 'btn-primary')][contains(@href, 'resume=1')]" "xpath_element" + And I should see "testfilequestion.pdf" in the "(//*[contains(@class, 'filemanager-container')])[2]//*[contains(@class, 'fp-filename')]" "xpath" + And I should see "You can drag and drop files here to add them." in the "(//*[contains(@class, 'filemanager-container')])[1]//*[contains(@class, 'dndupload-message')]" "xpath" + And I upload "mod/questionnaire/tests/fixtures/testfilequestion2.pdf" to questionnaire "Add file1 as an answer" filemanager + And I press "Save and exit" + And I should see "Your progress has been saved." + And I click on "//a[contains(@class, 'btn-primary')][contains(@href, 'resume=1')]" "xpath_element" + And I should see "testfilequestion2.pdf" in the "(//*[contains(@class, 'filemanager-container')])[1]//*[contains(@class, 'fp-filename')]" "xpath" + And I should see "testfilequestion.pdf" in the "(//*[contains(@class, 'filemanager-container')])[2]//*[contains(@class, 'fp-filename')]" "xpath" + And I set the field "may be" to "checked" + And I press "Submit questionnaire" + And I should see "Thank you for completing this Questionnaire" + And I log out + And I log in as "teacher1" + And I am on the "Test questionnaire multiple files" "questionnaire activity" page + And I navigate to "View all responses" in current page administration + Then I should see "testfilequestion.pdf" + And I should see "testfilequestion2.pdf" diff --git a/tests/behat/questionnaire_activity_completion.feature b/tests/behat/questionnaire_activity_completion.feature index 32e9e856..a9082e39 100644 --- a/tests/behat/questionnaire_activity_completion.feature +++ b/tests/behat/questionnaire_activity_completion.feature @@ -108,8 +108,8 @@ Feature: View activity completion information in the questionnaire activity Then I should see "Are you still in School?" And I should see "Select one choice" And I should see "Enter some text" - And I set the field with xpath "//input[@type='radio' and @id='auto-rb0001']" to "1" - And I set the field with xpath "//input[@type='radio' and @id='auto-rb0005']" to "1" + And I set the field "Yes" to "1" + And I set the field "Three" to "1" And I press "Submit questionnaire" Then I should see "Thank you for completing this Questionnaire." And I press "Continue" From b5f304569208882aa9d3b944e4bb7737079df523 Mon Sep 17 00:00:00 2001 From: Stephan Robotta Date: Tue, 13 Jan 2026 17:31:42 +0100 Subject: [PATCH 10/11] Fix restore of files in backup. --- .../restore_questionnaire_stepslib.php | 68 ++++++++++++++++++- 1 file changed, 66 insertions(+), 2 deletions(-) diff --git a/backup/moodle2/restore_questionnaire_stepslib.php b/backup/moodle2/restore_questionnaire_stepslib.php index 2377668e..ea6563de 100644 --- a/backup/moodle2/restore_questionnaire_stepslib.php +++ b/backup/moodle2/restore_questionnaire_stepslib.php @@ -42,6 +42,11 @@ class restore_questionnaire_activity_structure_step extends restore_activity_str */ protected $olddependencies = []; + /** + * @var array $responsefileids Contains new questionnaire_response_file.id in key and old id in value. + */ + protected $responsefileids = []; + /** * Implementation of define_structure. * @return mixed @@ -396,9 +401,9 @@ protected function process_questionnaire_response_file($data) { $data = (object)$data; $data->response_id = $this->get_new_parentid('questionnaire_response'); $data->question_id = $this->get_mappingid('questionnaire_question', $data->question_id); - // Insert the questionnaire_response_file record. - $DB->insert_record('questionnaire_response_file', $data); + $newid = $DB->insert_record('questionnaire_response_file', $data); + $this->responsefileids[$newid] = $data->id; } /** @@ -490,6 +495,64 @@ protected function process_questionnaire_response_text($data) { $DB->insert_record('questionnaire_response_text', $data); } + /** + * Handle related files for response file assignments and update the references between the + * records in the questionnaire_response_file and the files table. + */ + protected function handle_releated_files_for_response_file_assignments() { + global $DB; + // If there are no response file IDs mapped before, then there are no response files to handle. + if (empty($this->responsefileids)) { + return; + } + $newctx = restore_dbops::get_backup_ids_record($this->get_restoreid(), 'context', $this->task->get_old_contextid()); + restore_dbops::send_files_to_pool( + $this->get_basepath(), + $this->get_restoreid(), + 'mod_questionnaire', + 'response_file', + $this->task->get_old_contextid(), + $this->task->get_userid() + ); + // Lookup all file records that where just created for this context and component/filearea. + [$sql, $params] = $DB->get_in_or_equal(array_values($this->responsefileids), SQL_PARAMS_NAMED, 'id'); + $params['component'] = 'mod_questionnaire'; + $params['filearea'] = 'response_file'; + $params['ctx'] = $newctx->newitemid; + $filerecords = $DB->get_records_select( + 'files', + 'contextid = :ctx AND component = :component AND filearea = :filearea AND itemid ' . $sql, + $params + ); + $oldtonew = array_flip($this->responsefileids); + foreach ($filerecords as $filerecord) { + if (array_key_exists($filerecord->itemid, $oldtonew)) { + if ($filerecord->filename != '.') { + // We have a real file, use that file.id to update the questionnaire_response_file.fileid. + $DB->set_field( + 'questionnaire_response_file', + 'fileid', + $filerecord->id, + ['id' => $oldtonew[$filerecord->itemid]] + ); + } + // Update the itemid to the new response_file.id. + $filerecord->itemid = $oldtonew[$filerecord->itemid]; + // Calculate new pathnamehash. + $filerecord->pathnamehash = \file_storage::get_pathname_hash( + $filerecord->contextid, + $filerecord->component, + $filerecord->filearea, + $filerecord->itemid, + $filerecord->filepath, + $filerecord->filename + ); + // Adjust the files record with the new itemid and pathnamehash. + $DB->update_record('files', $filerecord); + } + } + } + /** * Stuff to do after execution. */ @@ -540,6 +603,7 @@ protected function after_execute() { $this->add_related_files('mod_questionnaire', 'question', 'questionnaire_question'); $this->add_related_files('mod_questionnaire', 'sectionheading', 'questionnaire_fb_sections'); $this->add_related_files('mod_questionnaire', 'feedback', 'questionnaire_feedback'); + $this->handle_releated_files_for_response_file_assignments(); // Process any old rate question named degree choices after all questions and choices have been restored. if ($this->task->get_old_moduleversion() < 2018110103) { From 8c82448b200f3c20fcc11741f6205efc81801972 Mon Sep 17 00:00:00 2001 From: Stephan Robotta Date: Tue, 13 Jan 2026 17:37:29 +0100 Subject: [PATCH 11/11] Update version as requested in PR. --- db/upgrade.php | 4 ++-- version.php | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/db/upgrade.php b/db/upgrade.php index 7895602f..51d4b2ca 100644 --- a/db/upgrade.php +++ b/db/upgrade.php @@ -1062,7 +1062,7 @@ function xmldb_questionnaire_upgrade($oldversion = 0) { upgrade_mod_savepoint(true, 2023101500, 'questionnaire'); } - if ($oldversion < 2025041400.01) { + if ($oldversion < 2025041400.02) { $table = new xmldb_table('questionnaire_question'); $index = new xmldb_index('quest_question_sididx', XMLDB_INDEX_NOTUNIQUE, ['surveyid', 'deleted']); if ($dbman->index_exists($table, $index)) { @@ -1085,7 +1085,7 @@ function xmldb_questionnaire_upgrade($oldversion = 0) { } unset($field); // Questionnaire savepoint reached. - upgrade_mod_savepoint(true, 2025041400.01, 'questionnaire'); + upgrade_mod_savepoint(true, 2025041400.02, 'questionnaire'); } return true; diff --git a/version.php b/version.php index bd30f4da..3f808c07 100644 --- a/version.php +++ b/version.php @@ -25,7 +25,7 @@ defined('MOODLE_INTERNAL') || die(); -$plugin->version = 2025041400.01; +$plugin->version = 2025041400.02; $plugin->requires = 2025041400.00; // Moodle 5.0 and up. $plugin->component = 'mod_questionnaire';