diff --git a/mod/survey/classes/privacy/provider.php b/mod/survey/classes/privacy/provider.php new file mode 100644 index 0000000000000..ac541f76d2f2b --- /dev/null +++ b/mod/survey/classes/privacy/provider.php @@ -0,0 +1,317 @@ +. + +/** + * Data provider. + * + * @package mod_survey + * @copyright 2018 Frédéric Massart + * @author Frédéric Massart + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_survey\privacy; +defined('MOODLE_INTERNAL') || die(); + +use context; +use context_helper; +use context_module; +use core_privacy\local\metadata\collection; +use core_privacy\local\request\approved_contextlist; +use core_privacy\local\request\helper; +use core_privacy\local\request\transform; +use core_privacy\local\request\writer; + +require_once($CFG->dirroot . '/mod/survey/lib.php'); + +/** + * Data provider class. + * + * @package mod_survey + * @copyright 2018 Frédéric Massart + * @author Frédéric Massart + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements + \core_privacy\local\metadata\provider, + \core_privacy\local\request\plugin\provider { + + /** + * Returns metadata. + * + * @param collection $collection The initialised collection to add items to. + * @return collection A listing of user data stored through this system. + */ + public static function get_metadata(collection $collection) : collection { + $collection->add_database_table('survey_answers', [ + 'userid' => 'privacy:metadata:answers:userid', + 'question' => 'privacy:metadata:answers:question', + 'answer1' => 'privacy:metadata:answers:answer1', + 'answer2' => 'privacy:metadata:answers:answer2', + 'time' => 'privacy:metadata:answers:time', + ], 'privacy:metadata:answers'); + + $collection->add_database_table('survey_analysis', [ + 'userid' => 'privacy:metadata:analysis:userid', + 'notes' => 'privacy:metadata:analysis:notes', + ], 'privacy:metadata:analysis'); + + return $collection; + } + + /** + * Get the list of contexts that contain user information for the specified user. + * + * @param int $userid The user to search. + * @return contextlist $contextlist The contextlist containing the list of contexts used in this plugin. + */ + public static function get_contexts_for_userid(int $userid) : \core_privacy\local\request\contextlist { + $contextlist = new \core_privacy\local\request\contextlist(); + + // While we should not have an analysis without answers, it's safer to gather contexts by looking at both tables. + $sql = " + SELECT DISTINCT ctx.id + FROM {survey} s + JOIN {modules} m + ON m.name = :survey + JOIN {course_modules} cm + ON cm.instance = s.id + AND cm.module = m.id + JOIN {context} ctx + ON ctx.instanceid = cm.id + AND ctx.contextlevel = :modulelevel + LEFT JOIN {survey_answers} sa + ON sa.survey = s.id + AND sa.userid = :userid1 + LEFT JOIN {survey_analysis} sy + ON sy.survey = s.id + AND sy.userid = :userid2 + WHERE s.template <> 0 + AND (sa.id IS NOT NULL + OR sy.id IS NOT NULL)"; + + $contextlist->add_from_sql($sql, [ + 'survey' => 'survey', + 'modulelevel' => CONTEXT_MODULE, + 'userid1' => $userid, + 'userid2' => $userid, + ]); + + return $contextlist; + } + + /** + * Export all user data for the specified user, in the specified contexts. + * + * @param approved_contextlist $contextlist The approved contexts to export information for. + */ + public static function export_user_data(approved_contextlist $contextlist) { + global $DB; + + $user = $contextlist->get_user(); + $userid = $user->id; + $cmids = array_reduce($contextlist->get_contexts(), function($carry, $context) { + if ($context->contextlevel == CONTEXT_MODULE) { + $carry[] = $context->instanceid; + } + return $carry; + }, []); + + if (empty($cmids)) { + return; + } + + // Export the answers. + list($insql, $inparams) = $DB->get_in_or_equal($cmids, SQL_PARAMS_NAMED); + $sql = " + SELECT sa.*, + sq.id as qid, + sq.text as qtext, + sq.shorttext as qshorttext, + sq.intro as qintro, + sq.options as qoptions, + sq.type as qtype, + cm.id as cmid + FROM {survey_answers} sa + JOIN {survey_questions} sq + ON sq.id = sa.question + JOIN {survey} s + ON s.id = sa.survey + JOIN {modules} m + ON m.name = :survey + JOIN {course_modules} cm + ON cm.instance = s.id + AND cm.module = m.id + WHERE cm.id $insql + AND sa.userid = :userid + ORDER BY s.id, sq.id"; + $params = array_merge($inparams, ['survey' => 'survey', 'userid' => $userid]); + + $recordset = $DB->get_recordset_sql($sql, $params); + static::recordset_loop_and_export($recordset, 'cmid', [], function($carry, $record) { + $q = survey_translate_question((object) [ + 'text' => $record->qtext, + 'shorttext' => $record->qshorttext, + 'intro' => $record->qintro, + 'options' => $record->qoptions + ]); + $qtype = $record->qtype; + $options = explode(',', $q->options); + + $carry[] = [ + 'question' => array_merge((array) $q, [ + 'options' => $qtype > 0 ? $options : '-' + ]), + 'answer' => [ + 'actual' => $qtype > 0 && !empty($record->answer1) ? $options[$record->answer1 - 1] : $record->answer1, + 'preferred' => $qtype > 0 && !empty($record->answer2) ? $options[$record->answer2 - 1] : $record->answer2, + ], + 'time' => transform::datetime($record->time), + ]; + return $carry; + + }, function($cmid, $data) use ($user) { + $context = context_module::instance($cmid); + $contextdata = helper::get_context_data($context, $user); + $contextdata = (object) array_merge((array) $contextdata, ['answers' => $data]); + helper::export_context_files($context, $user); + writer::with_context($context)->export_data([], $contextdata); + }); + + // Export the analysis. + $sql = " + SELECT sy.*, cm.id as cmid + FROM {survey_analysis} sy + JOIN {survey} s + ON s.id = sy.survey + JOIN {modules} m + ON m.name = :survey + JOIN {course_modules} cm + ON cm.instance = s.id + AND cm.module = m.id + WHERE cm.id $insql + AND sy.userid = :userid + ORDER BY s.id"; + $params = array_merge($inparams, ['survey' => 'survey', 'userid' => $userid]); + + $recordset = $DB->get_recordset_sql($sql, $params); + static::recordset_loop_and_export($recordset, 'cmid', null, function($carry, $record) { + $carry = ['notes' => $record->notes]; + return $carry; + }, function($cmid, $data) { + $context = context_module::instance($cmid); + writer::with_context($context)->export_related_data([], 'survey_analysis', (object) $data); + }); + } + + /** + * Delete all data for all users in the specified context. + * + * @param context $context The specific context to delete data for. + */ + public static function delete_data_for_all_users_in_context(context $context) { + global $DB; + + if ($context->contextlevel != CONTEXT_MODULE) { + return; + } + + $surveyid = static::get_survey_id_from_context($context); + $DB->delete_records('survey_answers', ['survey' => $surveyid]); + $DB->delete_records('survey_analysis', ['survey' => $surveyid]); + } + + /** + * Delete all user data for the specified user, in the specified contexts. + * + * @param approved_contextlist $contextlist The approved contexts and user information to delete information for. + */ + public static function delete_data_for_user(approved_contextlist $contextlist) { + global $DB; + + $userid = $contextlist->get_user()->id; + $cmids = array_reduce($contextlist->get_contexts(), function($carry, $context) { + if ($context->contextlevel == CONTEXT_MODULE) { + $carry[] = $context->instanceid; + } + return $carry; + }, []); + if (empty($cmids)) { + return; + } + + // Fetch the survey IDs. + list($insql, $inparams) = $DB->get_in_or_equal($cmids, SQL_PARAMS_NAMED); + $sql = " + SELECT s.id + FROM {survey} s + JOIN {modules} m + ON m.name = :survey + JOIN {course_modules} cm + ON cm.instance = s.id + AND cm.module = m.id + WHERE cm.id $insql"; + $params = array_merge($inparams, ['survey' => 'survey']); + $surveyids = $DB->get_fieldset_sql($sql, $params); + + // Delete all the things. + list($insql, $inparams) = $DB->get_in_or_equal($surveyids, SQL_PARAMS_NAMED); + $params = array_merge($inparams, ['userid' => $userid]); + $DB->delete_records_select('survey_answers', "survey $insql AND userid = :userid", $params); + $DB->delete_records_select('survey_analysis', "survey $insql AND userid = :userid", $params); + } + + /** + * Get a survey ID from its context. + * + * @param context_module $context The module context. + * @return int + */ + protected static function get_survey_id_from_context(context_module $context) { + $cm = get_coursemodule_from_id('survey', $context->instanceid, 0, false, MUST_EXIST); + return (int) $cm->instance; + } + /** + * Loop and export from a recordset. + * + * @param moodle_recordset $recordset The recordset. + * @param string $splitkey The record key to determine when to export. + * @param mixed $initial The initial data to reduce from. + * @param callable $reducer The function to return the dataset, receives current dataset, and the current record. + * @param callable $export The function to export the dataset, receives the last value from $splitkey and the dataset. + * @return void + */ + protected static function recordset_loop_and_export(\moodle_recordset $recordset, $splitkey, $initial, + callable $reducer, callable $export) { + + $data = $initial; + $lastid = null; + + foreach ($recordset as $record) { + if ($lastid && $record->{$splitkey} != $lastid) { + $export($lastid, $data); + $data = $initial; + } + $data = $reducer($data, $record); + $lastid = $record->{$splitkey}; + } + $recordset->close(); + + if (!empty($lastid)) { + $export($lastid, $data); + } + } +} diff --git a/mod/survey/lang/en/survey.php b/mod/survey/lang/en/survey.php index 087f576224b5f..8264fad0ec9a3 100644 --- a/mod/survey/lang/en/survey.php +++ b/mod/survey/lang/en/survey.php @@ -233,6 +233,15 @@ $string['preferred'] = 'Preferred'; $string['preferredclass'] = 'Class preferred'; $string['preferredstudent'] = '{$a} preferred'; +$string['privacy:metadata:analysis'] = 'A record of individual\'s answers analysis.'; +$string['privacy:metadata:analysis:notes'] = 'Notes saved against an individual\'s answers.'; +$string['privacy:metadata:analysis:userid'] = 'The user whose answers it is'; +$string['privacy:metadata:answers'] = 'A collection of answers to surveys.'; +$string['privacy:metadata:answers:answer1'] = 'Field to store the answer to a question.'; +$string['privacy:metadata:answers:answer2'] = 'Additional field to store the answer to a question.'; +$string['privacy:metadata:answers:question'] = 'The question.'; +$string['privacy:metadata:answers:time'] = 'The time at which the answer was posted.'; +$string['privacy:metadata:answers:userid'] = 'The user who submitted their answer.'; $string['question'] = 'Question'; $string['questions'] = 'Questions'; $string['questionsnotanswered'] = 'Some of the multiple choice questions have not been answered.'; diff --git a/mod/survey/tests/privacy_test.php b/mod/survey/tests/privacy_test.php new file mode 100644 index 0000000000000..c3380730122dc --- /dev/null +++ b/mod/survey/tests/privacy_test.php @@ -0,0 +1,416 @@ +. + +/** + * Data provider tests. + * + * @package mod_survey + * @category test + * @copyright 2018 Frédéric Massart + * @author Frédéric Massart + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); +global $CFG; + +use core_privacy\tests\provider_testcase; +use core_privacy\local\request\approved_contextlist; +use core_privacy\local\request\transform; +use core_privacy\local\request\writer; +use mod_survey\privacy\provider; + +require_once($CFG->dirroot . '/mod/survey/lib.php'); + +/** + * Data provider testcase class. + * + * @package mod_survey + * @category test + * @copyright 2018 Frédéric Massart + * @author Frédéric Massart + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class mod_survey_privacy_testcase extends provider_testcase { + + public function setUp() { + global $PAGE; + $this->resetAfterTest(); + $PAGE->get_renderer('core'); + } + + public function test_get_contexts_for_userid() { + $dg = $this->getDataGenerator(); + + $c1 = $dg->create_course(); + $c2 = $dg->create_course(); + $cm1a = $dg->create_module('survey', ['template' => 1, 'course' => $c1]); + $cm1b = $dg->create_module('survey', ['template' => 2, 'course' => $c1]); + $cm1c = $dg->create_module('survey', ['template' => 2, 'course' => $c1]); + $cm2a = $dg->create_module('survey', ['template' => 1, 'course' => $c2]); + $cm2b = $dg->create_module('survey', ['template' => 1, 'course' => $c2]); + $u1 = $dg->create_user(); + $u2 = $dg->create_user(); + + $this->create_answer($cm1a->id, 1, $u1->id); + $this->create_answer($cm1a->id, 1, $u2->id); + $this->create_answer($cm1b->id, 1, $u2->id); + $this->create_answer($cm2a->id, 1, $u1->id); + $this->create_analysis($cm2b->id, $u1->id); + $this->create_analysis($cm1c->id, $u2->id); + + $contextids = provider::get_contexts_for_userid($u1->id)->get_contextids(); + $this->assertCount(3, $contextids); + $this->assertTrue(in_array(context_module::instance($cm1a->cmid)->id, $contextids)); + $this->assertTrue(in_array(context_module::instance($cm2a->cmid)->id, $contextids)); + $this->assertTrue(in_array(context_module::instance($cm2b->cmid)->id, $contextids)); + + $contextids = provider::get_contexts_for_userid($u2->id)->get_contextids(); + $this->assertCount(3, $contextids); + $this->assertTrue(in_array(context_module::instance($cm1a->cmid)->id, $contextids)); + $this->assertTrue(in_array(context_module::instance($cm1b->cmid)->id, $contextids)); + $this->assertTrue(in_array(context_module::instance($cm1c->cmid)->id, $contextids)); + } + + public function test_delete_data_for_all_users_in_context() { + global $DB; + $dg = $this->getDataGenerator(); + + $c1 = $dg->create_course(); + $cm1a = $dg->create_module('survey', ['template' => 1, 'course' => $c1]); + $cm1b = $dg->create_module('survey', ['template' => 2, 'course' => $c1]); + $cm1c = $dg->create_module('survey', ['template' => 2, 'course' => $c1]); + $u1 = $dg->create_user(); + $u2 = $dg->create_user(); + + $this->create_answer($cm1a->id, 1, $u1->id); + $this->create_answer($cm1a->id, 1, $u2->id); + $this->create_answer($cm1b->id, 1, $u2->id); + $this->create_answer($cm1c->id, 1, $u1->id); + $this->create_analysis($cm1a->id, $u1->id); + $this->create_analysis($cm1b->id, $u1->id); + $this->create_analysis($cm1a->id, $u2->id); + $this->create_analysis($cm1c->id, $u2->id); + + $this->assertTrue($DB->record_exists('survey_answers', ['userid' => $u1->id, 'survey' => $cm1a->id])); + $this->assertTrue($DB->record_exists('survey_answers', ['userid' => $u1->id, 'survey' => $cm1c->id])); + $this->assertTrue($DB->record_exists('survey_answers', ['userid' => $u2->id, 'survey' => $cm1a->id])); + $this->assertTrue($DB->record_exists('survey_answers', ['userid' => $u2->id, 'survey' => $cm1b->id])); + $this->assertTrue($DB->record_exists('survey_analysis', ['userid' => $u1->id, 'survey' => $cm1a->id])); + $this->assertTrue($DB->record_exists('survey_analysis', ['userid' => $u1->id, 'survey' => $cm1b->id])); + $this->assertTrue($DB->record_exists('survey_analysis', ['userid' => $u2->id, 'survey' => $cm1a->id])); + $this->assertTrue($DB->record_exists('survey_analysis', ['userid' => $u2->id, 'survey' => $cm1c->id])); + + // Deleting the course does nothing. + provider::delete_data_for_all_users_in_context(context_course::instance($c1->id)); + $this->assertTrue($DB->record_exists('survey_answers', ['userid' => $u1->id, 'survey' => $cm1a->id])); + $this->assertTrue($DB->record_exists('survey_answers', ['userid' => $u1->id, 'survey' => $cm1c->id])); + $this->assertTrue($DB->record_exists('survey_answers', ['userid' => $u2->id, 'survey' => $cm1a->id])); + $this->assertTrue($DB->record_exists('survey_answers', ['userid' => $u2->id, 'survey' => $cm1b->id])); + $this->assertTrue($DB->record_exists('survey_analysis', ['userid' => $u1->id, 'survey' => $cm1a->id])); + $this->assertTrue($DB->record_exists('survey_analysis', ['userid' => $u1->id, 'survey' => $cm1b->id])); + $this->assertTrue($DB->record_exists('survey_analysis', ['userid' => $u2->id, 'survey' => $cm1a->id])); + $this->assertTrue($DB->record_exists('survey_analysis', ['userid' => $u2->id, 'survey' => $cm1c->id])); + + provider::delete_data_for_all_users_in_context(context_module::instance($cm1c->cmid)); + $this->assertTrue($DB->record_exists('survey_answers', ['userid' => $u1->id, 'survey' => $cm1a->id])); + $this->assertFalse($DB->record_exists('survey_answers', ['userid' => $u1->id, 'survey' => $cm1c->id])); + $this->assertTrue($DB->record_exists('survey_answers', ['userid' => $u2->id, 'survey' => $cm1a->id])); + $this->assertTrue($DB->record_exists('survey_answers', ['userid' => $u2->id, 'survey' => $cm1b->id])); + $this->assertTrue($DB->record_exists('survey_analysis', ['userid' => $u1->id, 'survey' => $cm1a->id])); + $this->assertTrue($DB->record_exists('survey_analysis', ['userid' => $u1->id, 'survey' => $cm1b->id])); + $this->assertTrue($DB->record_exists('survey_analysis', ['userid' => $u2->id, 'survey' => $cm1a->id])); + $this->assertFalse($DB->record_exists('survey_analysis', ['userid' => $u2->id, 'survey' => $cm1c->id])); + + provider::delete_data_for_all_users_in_context(context_module::instance($cm1a->cmid)); + $this->assertFalse($DB->record_exists('survey_answers', ['userid' => $u1->id, 'survey' => $cm1a->id])); + $this->assertFalse($DB->record_exists('survey_answers', ['userid' => $u1->id, 'survey' => $cm1c->id])); + $this->assertFalse($DB->record_exists('survey_answers', ['userid' => $u2->id, 'survey' => $cm1a->id])); + $this->assertTrue($DB->record_exists('survey_answers', ['userid' => $u2->id, 'survey' => $cm1b->id])); + $this->assertFalse($DB->record_exists('survey_analysis', ['userid' => $u1->id, 'survey' => $cm1a->id])); + $this->assertTrue($DB->record_exists('survey_analysis', ['userid' => $u1->id, 'survey' => $cm1b->id])); + $this->assertFalse($DB->record_exists('survey_analysis', ['userid' => $u2->id, 'survey' => $cm1a->id])); + $this->assertFalse($DB->record_exists('survey_analysis', ['userid' => $u2->id, 'survey' => $cm1c->id])); + } + + public function test_delete_data_for_user() { + global $DB; + $dg = $this->getDataGenerator(); + + $c1 = $dg->create_course(); + $cm1a = $dg->create_module('survey', ['template' => 1, 'course' => $c1]); + $cm1b = $dg->create_module('survey', ['template' => 2, 'course' => $c1]); + $cm1c = $dg->create_module('survey', ['template' => 2, 'course' => $c1]); + $u1 = $dg->create_user(); + $u2 = $dg->create_user(); + + $this->create_answer($cm1a->id, 1, $u1->id); + $this->create_answer($cm1a->id, 1, $u2->id); + $this->create_answer($cm1b->id, 1, $u2->id); + $this->create_answer($cm1c->id, 1, $u1->id); + $this->create_analysis($cm1a->id, $u1->id); + $this->create_analysis($cm1b->id, $u1->id); + $this->create_analysis($cm1a->id, $u2->id); + $this->create_analysis($cm1c->id, $u2->id); + + $this->assertTrue($DB->record_exists('survey_answers', ['userid' => $u1->id, 'survey' => $cm1a->id])); + $this->assertTrue($DB->record_exists('survey_answers', ['userid' => $u1->id, 'survey' => $cm1c->id])); + $this->assertTrue($DB->record_exists('survey_answers', ['userid' => $u2->id, 'survey' => $cm1a->id])); + $this->assertTrue($DB->record_exists('survey_answers', ['userid' => $u2->id, 'survey' => $cm1b->id])); + $this->assertTrue($DB->record_exists('survey_analysis', ['userid' => $u1->id, 'survey' => $cm1a->id])); + $this->assertTrue($DB->record_exists('survey_analysis', ['userid' => $u1->id, 'survey' => $cm1b->id])); + $this->assertTrue($DB->record_exists('survey_analysis', ['userid' => $u2->id, 'survey' => $cm1a->id])); + $this->assertTrue($DB->record_exists('survey_analysis', ['userid' => $u2->id, 'survey' => $cm1c->id])); + + provider::delete_data_for_user(new approved_contextlist($u1, 'mod_survey', [ + context_course::instance($c1->id)->id, + context_module::instance($cm1a->cmid)->id, + context_module::instance($cm1b->cmid)->id, + ])); + $this->assertFalse($DB->record_exists('survey_answers', ['userid' => $u1->id, 'survey' => $cm1a->id])); + $this->assertTrue($DB->record_exists('survey_answers', ['userid' => $u1->id, 'survey' => $cm1c->id])); + $this->assertTrue($DB->record_exists('survey_answers', ['userid' => $u2->id, 'survey' => $cm1a->id])); + $this->assertTrue($DB->record_exists('survey_answers', ['userid' => $u2->id, 'survey' => $cm1b->id])); + $this->assertFalse($DB->record_exists('survey_analysis', ['userid' => $u1->id, 'survey' => $cm1a->id])); + $this->assertFalse($DB->record_exists('survey_analysis', ['userid' => $u1->id, 'survey' => $cm1b->id])); + $this->assertTrue($DB->record_exists('survey_analysis', ['userid' => $u2->id, 'survey' => $cm1a->id])); + $this->assertTrue($DB->record_exists('survey_analysis', ['userid' => $u2->id, 'survey' => $cm1c->id])); + } + + public function test_export_data_for_user() { + global $DB; + $dg = $this->getDataGenerator(); + + $templates = $DB->get_records_menu('survey', array('template' => 0), 'name', 'name, id'); + + $c1 = $dg->create_course(); + $s1a = $dg->create_module('survey', ['template' => $templates['attlsname'], 'course' => $c1]); + $s1b = $dg->create_module('survey', ['template' => $templates['ciqname'], 'course' => $c1]); + $s1c = $dg->create_module('survey', ['template' => $templates['collesapname'], 'course' => $c1]); + $u1 = $dg->create_user(); + $u2 = $dg->create_user(); + + $s1actx = context_module::instance($s1a->cmid); + $s1bctx = context_module::instance($s1b->cmid); + $s1cctx = context_module::instance($s1c->cmid); + + $this->answer_survey($s1a, $u1, $c1, $s1actx); + $this->answer_survey($s1b, $u1, $c1, $s1bctx); + $this->create_analysis($s1a->id, $u1->id, 'Hello,'); + + $this->answer_survey($s1a, $u2, $c1, $s1actx); + $this->answer_survey($s1c, $u2, $c1, $s1cctx); + $this->create_analysis($s1b->id, $u2->id, 'World!'); + + provider::export_user_data(new approved_contextlist($u1, 'mod_survey', [$s1actx->id, $s1bctx->id, $s1cctx->id])); + + $data = writer::with_context($s1actx)->get_data([]); + $this->assertNotEmpty($data); + $this->assert_exported_answers($data->answers, $u1, $s1a); + $data = writer::with_context($s1actx)->get_related_data([], 'survey_analysis'); + $this->assertEquals('Hello,', $data->notes); + + $data = writer::with_context($s1bctx)->get_data([]); + $this->assertNotEmpty($data); + $this->assert_exported_answers($data->answers, $u1, $s1b); + $data = writer::with_context($s1bctx)->get_related_data([], 'survey_analysis'); + $this->assertEmpty($data); + + $data = writer::with_context($s1cctx)->get_data([]); + $this->assertEmpty($data); + $data = writer::with_context($s1cctx)->get_related_data([], 'survey_analysis'); + $this->assertEmpty($data); + + writer::reset(); + provider::export_user_data(new approved_contextlist($u2, 'mod_survey', [$s1actx->id, $s1bctx->id, $s1cctx->id])); + + $data = writer::with_context($s1actx)->get_data([]); + $this->assertNotEmpty($data); + $this->assert_exported_answers($data->answers, $u2, $s1a); + $data = writer::with_context($s1actx)->get_related_data([], 'survey_analysis'); + $this->assertEmpty($data); + + $data = writer::with_context($s1bctx)->get_data([]); + $this->assertEmpty($data); + $data = writer::with_context($s1bctx)->get_related_data([], 'survey_analysis'); + $this->assertEquals('World!', $data->notes); + + $data = writer::with_context($s1cctx)->get_data([]); + $this->assertNotEmpty($data); + $this->assert_exported_answers($data->answers, $u2, $s1c); + $data = writer::with_context($s1cctx)->get_related_data([], 'survey_analysis'); + $this->assertEmpty($data); + } + + /** + * Answer a survey in a predictable manner. + * + * @param stdClass $survey The survey. + * @param stdClass $user The user. + * @param stdClass $course The course. + * @param context_module $context The module context. + * @return void + */ + protected function answer_survey($survey, $user, $course, context_module $context) { + global $USER; + + $userid = $user->id; + $questions = survey_get_questions($survey); + $answer = function(&$answers, $q) use ($userid) { + $key = 'q' . ($q->type == 2 ? 'P' : '') . $q->id; + + if ($q->type < 1) { + $a = "A:{$q->id}:{$userid}"; + $answers[$key] = $a; + + } else if ($q->type < 3) { + $options = explode(',', get_string($q->options, 'mod_survey')); + $answers[$key] = ($q->id + $userid) % count($options) + 1; + + } else { + $options = explode(',', get_string($q->options, 'mod_survey')); + $answers["q{$q->id}"] = ($q->id + $userid) % count($options) + 1; + $answers["qP{$q->id}"] = ($q->id + $userid + 1) % count($options) + 1; + } + + }; + + foreach ($questions as $q) { + if ($q->type < 0) { + continue; + } else if ($q->type > 0 && $q->multi) { + $subquestions = survey_get_subquestions($q); + foreach ($subquestions as $sq) { + $answer($answers, $sq); + } + } else { + $answer($answers, $q); + } + } + + $origuser = $USER; + $this->setUser($user); + survey_save_answers($survey, $answers, $course, $context); + $this->setUser($origuser); + } + + /** + * Assert the answers provided to a survey. + * + * @param array $answers The answers. + * @param object $user The user. + * @param object $survey The survey. + * @return void + */ + protected function assert_exported_answers($answers, $user, $survey) { + global $DB; + + $userid = $user->id; + $questionids = explode(',', $survey->questions); + $topquestions = $DB->get_records_list('survey_questions', 'id', $questionids, 'id'); + $questions = []; + + foreach ($topquestions as $q) { + if ($q->type < 0) { + continue; + } else if ($q->type > 0 && $q->multi) { + $questionids = explode(',', $q->multi); + $subqs = $DB->get_records_list('survey_questions', 'id', $questionids, 'id'); + } else { + $subqs = [$q]; + } + foreach ($subqs as $sq) { + $questions[] = $sq; + } + } + + $this->assertCount(count($questions), $answers); + + $answer = reset($answers); + foreach ($questions as $question) { + $qtype = $question->type; + $question = survey_translate_question($question); + $options = $qtype > 0 ? explode(',', $question->options) : '-'; + $this->assertEquals($question->text, $answer['question']['text']); + $this->assertEquals($question->shorttext, $answer['question']['shorttext']); + $this->assertEquals($question->intro, $answer['question']['intro']); + $this->assertEquals($options, $answer['question']['options']); + + if ($qtype < 1) { + $this->assertEquals("A:{$question->id}:{$userid}", $answer['answer']['actual']); + + } else if ($qtype == 1 || $qtype == 2) { + $chosen = ($question->id + $userid) % count($options); + $key = $qtype == 1 ? 'actual' : 'preferred'; + $this->assertEquals($options[$chosen], $answer['answer'][$key]); + + } else { + $chosen = ($question->id + $userid) % count($options); + $this->assertEquals($options[$chosen], $answer['answer']['actual']); + $chosen = ($question->id + $userid + 1) % count($options); + $this->assertEquals($options[$chosen], $answer['answer']['preferred']); + } + + // Grab next answer, if any. + $answer = next($answers); + } + + } + + /** + * Create analysis. + * + * @param int $surveyid The survey ID. + * @param int $userid The user ID. + * @param string $notes The nodes. + * @return stdClass + */ + protected function create_analysis($surveyid, $userid, $notes = '') { + global $DB; + $record = (object) [ + 'survey' => $surveyid, + 'userid' => $userid, + 'notes' => $notes + ]; + $record->id = $DB->insert_record('survey_analysis', $record); + return $record; + } + + /** + * Create answer. + * + * @param int $surveyid The survey ID. + * @param int $questionid The question ID. + * @param int $userid The user ID. + * @param string $answer1 The first answer field. + * @param string $answer2 The second answer field. + * @return stdClass + */ + protected function create_answer($surveyid, $questionid, $userid, $answer1 = '', $answer2 = '') { + global $DB; + $record = (object) [ + 'survey' => $surveyid, + 'question' => $questionid, + 'userid' => $userid, + 'answer1' => $answer1, + 'answer2' => $answer2, + 'time' => time() + ]; + $record->id = $DB->insert_record('survey_answers', $record); + return $record; + } + +}