From 9f6296e5de8bba2c0115e2e41d1310336c3048bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Massart?= Date: Thu, 5 Apr 2018 18:10:59 +0800 Subject: [PATCH] MDL-61862 mod_feedback: Implement core_privacy API --- mod/feedback/classes/privacy/provider.php | 403 ++++++++++++++++++++ mod/feedback/lang/en/feedback.php | 8 + mod/feedback/tests/privacy_test.php | 432 ++++++++++++++++++++++ 3 files changed, 843 insertions(+) create mode 100644 mod/feedback/classes/privacy/provider.php create mode 100644 mod/feedback/tests/privacy_test.php diff --git a/mod/feedback/classes/privacy/provider.php b/mod/feedback/classes/privacy/provider.php new file mode 100644 index 00000000000..717e409ec6b --- /dev/null +++ b/mod/feedback/classes/privacy/provider.php @@ -0,0 +1,403 @@ +. + +/** + * Data provider. + * + * @package mod_feedback + * @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_feedback\privacy; +defined('MOODLE_INTERNAL') || die(); + +use context; +use context_helper; +use stdClass; +use core_privacy\local\metadata\collection; +use core_privacy\local\request\approved_contextlist; +use core_privacy\local\request\contextlist; +use core_privacy\local\request\helper; +use core_privacy\local\request\transform; +use core_privacy\local\request\writer; + +require_once($CFG->dirroot . '/mod/feedback/lib.php'); + +/** + * Data provider class. + * + * @package mod_feedback + * @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 { + $completedfields = [ + 'userid' => 'privacy:metadata:completed:userid', + 'timemodified' => 'privacy:metadata:completed:timemodified', + 'anonymous_response' => 'privacy:metadata:completed:anonymousresponse', + ]; + + $collection->add_database_table('feedback_completed', $completedfields, 'privacy:metadata:completed'); + $collection->add_database_table('feedback_completedtmp', $completedfields, 'privacy:metadata:completedtmp'); + + $valuefields = [ + 'value' => 'privacy:metadata:value:value' + ]; + + $collection->add_database_table('feedback_value', $valuefields, 'privacy:metadata:value'); + $collection->add_database_table('feedback_valuetmp', $valuefields, 'privacy:metadata:valuetmp'); + + 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) : contextlist { + $sql = " + SELECT DISTINCT ctx.id + FROM {%s} fc + JOIN {modules} m + ON m.name = :feedback + JOIN {course_modules} cm + ON cm.instance = fc.feedback + AND cm.module = m.id + JOIN {context} ctx + ON ctx.instanceid = cm.id + AND ctx.contextlevel = :modlevel + WHERE fc.userid = :userid"; + $params = ['feedback' => 'feedback', 'modlevel' => CONTEXT_MODULE, 'userid' => $userid]; + $contextlist = new contextlist(); + $contextlist->add_from_sql(sprintf($sql, 'feedback_completed'), $params); + $contextlist->add_from_sql(sprintf($sql, 'feedback_completedtmp'), $params); + 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; + $contextids = array_map(function($context) { + return $context->id; + }, array_filter($contextlist->get_contexts(), function($context) { + return $context->contextlevel == CONTEXT_MODULE; + })); + + if (empty($contextids)) { + return; + } + + $flushdata = function($context, $data) use ($user) { + $contextdata = helper::get_context_data($context, $user); + helper::export_context_files($context, $user); + $mergeddata = array_merge((array) $contextdata, (array) $data); + + // Drop the temporary keys. + if (array_key_exists('submissions', $mergeddata)) { + $mergeddata['submissions'] = array_values($mergeddata['submissions']); + } + + writer::with_context($context)->export_data([], (object) $mergeddata); + }; + + $lastctxid = null; + $data = (object) []; + list($sql, $params) = static::prepare_export_query($contextids, $userid); + $recordset = $DB->get_recordset_sql($sql, $params); + foreach ($recordset as $record) { + if ($lastctxid && $lastctxid != $record->contextid) { + $flushdata(context::instance_by_id($lastctxid), $data); + $data = (object) []; + } + + context_helper::preload_from_record($record); + $id = ($record->istmp ? 'tmp' : 'notmp') . $record->submissionid; + + if (!isset($data->submissions)) { + $data->submissions = []; + } + + if (!isset($data->submissions[$id])) { + $data->submissions[$id] = [ + 'inprogress' => transform::yesno($record->istmp), + 'anonymousresponse' => transform::yesno($record->anonymousresponse == FEEDBACK_ANONYMOUS_YES), + 'timemodified' => transform::datetime($record->timemodified), + 'answers' => [] + ]; + } + $item = static::extract_item_record_from_record($record); + $value = static::extract_value_record_from_record($record); + $itemobj = feedback_get_item_class($record->itemtyp); + $data->submissions[$id]['answers'][] = [ + 'question' => format_text($record->itemname, FORMAT_HTML, [ + 'context' => context::instance_by_id($record->contextid), + 'para' => false, + 'noclean' => true, + ]), + 'answer' => $itemobj->get_printval($item, $value) + ]; + + $lastctxid = $record->contextid; + } + + if (!empty($lastctxid)) { + $flushdata(context::instance_by_id($lastctxid), $data); + } + + $recordset->close(); + } + + /** + * 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; + + // This should not happen, but just in case. + if ($context->contextlevel != CONTEXT_MODULE) { + return; + } + + // Prepare SQL to gather all completed IDs. + + $completedsql = " + SELECT fc.id + FROM {%s} fc + JOIN {modules} m + ON m.name = :feedback + JOIN {course_modules} cm + ON cm.instance = fc.feedback + AND cm.module = m.id + WHERE cm.id = :cmid"; + $completedparams = ['cmid' => $context->instanceid, 'feedback' => 'feedback']; + + // Delete temp answers and submissions. + $completedtmpids = $DB->get_fieldset_sql(sprintf($completedsql, 'feedback_completedtmp'), $completedparams); + if (!empty($completedtmpids)) { + list($insql, $inparams) = $DB->get_in_or_equal($completedtmpids, SQL_PARAMS_NAMED); + $DB->delete_records_select('feedback_valuetmp', "completed $insql", $inparams); + $DB->delete_records_select('feedback_completedtmp', "id $insql", $inparams); + } + + // Delete answers and submissions. + $completedids = $DB->get_fieldset_sql(sprintf($completedsql, 'feedback_completed'), $completedparams); + if (!empty($completedids)) { + list($insql, $inparams) = $DB->get_in_or_equal($completedids, SQL_PARAMS_NAMED); + $DB->delete_records_select('feedback_value', "completed $insql", $inparams); + $DB->delete_records_select('feedback_completed', "id $insql", $inparams); + } + } + + /** + * 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; + + // Ensure that we only act on module contexts. + $contextids = array_map(function($context) { + return $context->instanceid; + }, array_filter($contextlist->get_contexts(), function($context) { + return $context->contextlevel == CONTEXT_MODULE; + })); + + // Prepare SQL to gather all completed IDs. + list($insql, $inparams) = $DB->get_in_or_equal($contextids, SQL_PARAMS_NAMED); + $completedsql = " + SELECT fc.id + FROM {%s} fc + JOIN {modules} m + ON m.name = :feedback + JOIN {course_modules} cm + ON cm.instance = fc.feedback + AND cm.module = m.id + WHERE fc.userid = :userid + AND cm.id $insql"; + $completedparams = array_merge($inparams, ['userid' => $userid, 'feedback' => 'feedback']); + + // Delete all submissions in progress. + $completedtmpids = $DB->get_fieldset_sql(sprintf($completedsql, 'feedback_completedtmp'), $completedparams); + if (!empty($completedtmpids)) { + list($insql, $inparams) = $DB->get_in_or_equal($completedtmpids, SQL_PARAMS_NAMED); + $DB->delete_records_select('feedback_valuetmp', "completed $insql", $inparams); + $DB->delete_records_select('feedback_completedtmp', "id $insql", $inparams); + } + + // Delete all final submissions. + $completedids = $DB->get_fieldset_sql(sprintf($completedsql, 'feedback_completed'), $completedparams); + if (!empty($completedids)) { + list($insql, $inparams) = $DB->get_in_or_equal($completedids, SQL_PARAMS_NAMED); + $DB->delete_records_select('feedback_value', "completed $insql", $inparams); + $DB->delete_records_select('feedback_completed', "id $insql", $inparams); + } + } + + /** + * Extract an item record from a database record. + * + * @param stdClass $record The record. + * @return The item record. + */ + protected static function extract_item_record_from_record(stdClass $record) { + $newrec = new stdClass(); + foreach ($record as $key => $value) { + if (strpos($key, 'item') !== 0) { + continue; + } + $key = substr($key, 4); + $newrec->{$key} = $value; + } + return $newrec; + } + + /** + * Extract a value record from a database record. + * + * @param stdClass $record The record. + * @return The value record. + */ + protected static function extract_value_record_from_record(stdClass $record) { + $newrec = new stdClass(); + foreach ($record as $key => $value) { + if (strpos($key, 'value') !== 0) { + continue; + } + $key = substr($key, 5); + $newrec->{$key} = $value; + } + return $newrec; + } + + /** + * Prepare the query to export all data. + * + * Doing it this way allows for merging all records from both the temporary and final tables + * as most of their columns are shared. It is a lot easier to deal with the records when + * exporting as we do not need to try to manually group the two types of submissions in the + * same reported dataset. + * + * The ordering may affect performance on large datasets. + * + * @param array $contextids The context IDs. + * @param int $userid The user ID. + * @return array With SQL and params. + */ + protected static function prepare_export_query(array $contextids, $userid) { + global $DB; + + $makefetchsql = function($istmp) use ($DB, $contextids, $userid) { + $ctxfields = context_helper::get_preload_record_columns_sql('ctx'); + list($insql, $inparams) = $DB->get_in_or_equal($contextids, SQL_PARAMS_NAMED); + + $i = $istmp ? 0 : 1; + $istmpsqlval = $istmp ? 1 : 0; + $prefix = $istmp ? 'idtmp' : 'id'; + $uniqid = $DB->sql_concat("'$prefix'", 'fc.id'); + + $sql = " + SELECT $uniqid AS uniqid, + f.id AS feedbackid, + ctx.id AS contextid, + + $istmpsqlval AS istmp, + fc.id AS submissionid, + fc.anonymous_response AS anonymousresponse, + fc.timemodified AS timemodified, + + fv.id AS valueid, + fv.course_id AS valuecourse_id, + fv.item AS valueitem, + fv.completed AS valuecompleted, + fv.tmp_completed AS valuetmp_completed, + fv.value AS valuevalue, + + fi.id AS itemid, + fi.feedback AS itemfeedback, + fi.template AS itemtemplate, + fi.name AS itemname, + fi.label AS itemlabel, + fi.presentation AS itempresentation, + fi.typ AS itemtyp, + fi.hasvalue AS itemhasvalue, + fi.position AS itemposition, + fi.required AS itemrequired, + fi.dependitem AS itemdependitem, + fi.dependvalue AS itemdependvalue, + fi.options AS itemoptions, + + $ctxfields + FROM {context} ctx + JOIN {course_modules} cm + ON cm.id = ctx.instanceid + JOIN {feedback} f + ON f.id = cm.instance + JOIN {%s} fc + ON fc.feedback = f.id + JOIN {%s} fv + ON fv.completed = fc.id + JOIN {feedback_item} fi + ON fi.id = fv.item + WHERE ctx.id $insql + AND fc.userid = :userid{$i}"; + + $params = array_merge($inparams, [ + 'userid' . $i => $userid, + ]); + + $completedtbl = $istmp ? 'feedback_completedtmp' : 'feedback_completed'; + $valuetbl = $istmp ? 'feedback_valuetmp' : 'feedback_value'; + return [sprintf($sql, $completedtbl, $valuetbl), $params]; + }; + + list($nontmpsql, $nontmpparams) = $makefetchsql(false); + list($tmpsql, $tmpparams) = $makefetchsql(true); + + $sql = " + SELECT q.* + FROM ($nontmpsql UNION $tmpsql) q + ORDER BY q.contextid, q.istmp, q.submissionid, q.valueid"; + $params = array_merge($nontmpparams, $tmpparams); + + return [$sql, $params]; + } +} diff --git a/mod/feedback/lang/en/feedback.php b/mod/feedback/lang/en/feedback.php index 9f98fde8dcf..06f2c66e9c3 100644 --- a/mod/feedback/lang/en/feedback.php +++ b/mod/feedback/lang/en/feedback.php @@ -218,6 +218,14 @@ $string['pluginadministration'] = 'Feedback administration'; $string['pluginname'] = 'Feedback'; $string['position'] = 'Position'; $string['previous_page'] = 'Previous page'; +$string['privacy:metadata:completed'] = 'A record of the submissions to the feedback'; +$string['privacy:metadata:completed:anonymousresponse'] = 'Whether the submission is to be used anonymously.'; +$string['privacy:metadata:completed:timemodified'] = 'The time at which the submission was last modified.'; +$string['privacy:metadata:completed:userid'] = 'The user ID'; +$string['privacy:metadata:completedtmp'] = 'A record of the submissions which are still in progress.'; +$string['privacy:metadata:value'] = 'A record of the answer to a question.'; +$string['privacy:metadata:value:value'] = 'The chosen answer.'; +$string['privacy:metadata:valuetmp'] = 'A record of the answer to a question in a submission in progress.'; $string['public'] = 'Public'; $string['question'] = 'Question'; $string['questionandsubmission'] = 'Question and submission settings'; diff --git a/mod/feedback/tests/privacy_test.php b/mod/feedback/tests/privacy_test.php new file mode 100644 index 00000000000..97fe6c82431 --- /dev/null +++ b/mod/feedback/tests/privacy_test.php @@ -0,0 +1,432 @@ +. + +/** + * Data provider tests. + * + * @package mod_feedback + * @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_feedback\privacy\provider; + +require_once($CFG->dirroot . '/mod/feedback/lib.php'); + +/** + * Data provider testcase class. + * + * @package mod_feedback + * @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_feedback_privacy_testcase extends provider_testcase { + + public function setUp() { + $this->resetAfterTest(); + } + + /** + * Test getting the contexts for a user. + */ + public function test_get_contexts_for_userid() { + global $DB; + $dg = $this->getDataGenerator(); + $fg = $dg->get_plugin_generator('mod_feedback'); + + $c1 = $dg->create_course(); + $c2 = $dg->create_course(); + $cm0a = $dg->create_module('feedback', ['course' => SITEID]); + $cm1a = $dg->create_module('feedback', ['course' => $c1, 'anonymous' => FEEDBACK_ANONYMOUS_NO]); + $cm1b = $dg->create_module('feedback', ['course' => $c1]); + $cm2a = $dg->create_module('feedback', ['course' => $c2]); + $cm2b = $dg->create_module('feedback', ['course' => $c2]); + $cm2c = $dg->create_module('feedback', ['course' => $c2]); + + $u1 = $dg->create_user(); + $u2 = $dg->create_user(); + + foreach ([$cm0a, $cm1a, $cm1b, $cm2a] as $feedback) { + $i1 = $fg->create_item_numeric($feedback); + $i2 = $fg->create_item_multichoice($feedback); + $answers = ['numeric_' . $i1->id => '1', 'multichoice_' . $i2->id => [1]]; + + if ($feedback == $cm1b) { + $this->create_submission_with_answers($feedback, $u2, $answers); + } else { + $this->create_submission_with_answers($feedback, $u1, $answers); + } + } + + // Unsaved submission for u1 in cm2b. + $feedback = $cm2b; + $i1 = $fg->create_item_numeric($feedback); + $i2 = $fg->create_item_multichoice($feedback); + $answers = ['numeric_' . $i1->id => '1', 'multichoice_' . $i2->id => [1]]; + $this->create_tmp_submission_with_answers($feedback, $u1, $answers); + + // Unsaved submission for u2 in cm2c. + $feedback = $cm2c; + $i1 = $fg->create_item_numeric($feedback); + $i2 = $fg->create_item_multichoice($feedback); + $answers = ['numeric_' . $i1->id => '1', 'multichoice_' . $i2->id => [1]]; + $this->create_tmp_submission_with_answers($feedback, $u2, $answers); + + $contextids = provider::get_contexts_for_userid($u1->id)->get_contextids(); + $this->assertCount(4, $contextids); + $this->assertTrue(in_array(context_module::instance($cm0a->cmid)->id, $contextids)); + $this->assertTrue(in_array(context_module::instance($cm1a->cmid)->id, $contextids)); + $this->assertTrue(in_array(context_module::instance($cm2a->cmid)->id, $contextids)); + $this->assertFalse(in_array(context_module::instance($cm1b->cmid)->id, $contextids)); + $this->assertTrue(in_array(context_module::instance($cm2b->cmid)->id, $contextids)); + $this->assertFalse(in_array(context_module::instance($cm2c->cmid)->id, $contextids)); + + $contextids = provider::get_contexts_for_userid($u2->id)->get_contextids(); + $this->assertCount(2, $contextids); + $this->assertFalse(in_array(context_module::instance($cm0a->cmid)->id, $contextids)); + $this->assertFalse(in_array(context_module::instance($cm1a->cmid)->id, $contextids)); + $this->assertFalse(in_array(context_module::instance($cm2a->cmid)->id, $contextids)); + $this->assertTrue(in_array(context_module::instance($cm1b->cmid)->id, $contextids)); + $this->assertFalse(in_array(context_module::instance($cm2b->cmid)->id, $contextids)); + $this->assertTrue(in_array(context_module::instance($cm2c->cmid)->id, $contextids)); + } + + /** + * Test deleting user data. + */ + public function test_delete_data_for_user() { + global $DB; + $dg = $this->getDataGenerator(); + $fg = $dg->get_plugin_generator('mod_feedback'); + + $c1 = $dg->create_course(); + $c2 = $dg->create_course(); + $cm0a = $dg->create_module('feedback', ['course' => SITEID]); + $cm1a = $dg->create_module('feedback', ['course' => $c1, 'anonymous' => FEEDBACK_ANONYMOUS_NO]); + $cm2a = $dg->create_module('feedback', ['course' => $c2]); + + $u1 = $dg->create_user(); + $u2 = $dg->create_user(); + + // Create a bunch of data. + foreach ([$cm1a, $cm0a, $cm2a] as $feedback) { + $i1 = $fg->create_item_numeric($feedback); + $i2 = $fg->create_item_multichoice($feedback); + $answers = ['numeric_' . $i1->id => '1', 'multichoice_' . $i2->id => [1]]; + + // Create u2 user data for this module. + if ($feedback == $cm1a) { + $this->create_submission_with_answers($feedback, $u2, $answers); + $this->create_tmp_submission_with_answers($feedback, $u2, $answers); + } + + $this->create_submission_with_answers($feedback, $u1, $answers); + $this->create_tmp_submission_with_answers($feedback, $u1, $answers); + } + + $appctx = new approved_contextlist($u1, 'mod_feedback', [ + context_module::instance($cm0a->cmid)->id, + context_module::instance($cm1a->cmid)->id + ]); + provider::delete_data_for_user($appctx); + + // Confirm all data is gone in those, except for u2. + foreach ([$cm0a, $cm1a] as $feedback) { + $this->assert_no_feedback_data_for_user($feedback, $u1); + if ($feedback == $cm1a) { + $this->assert_feedback_data_for_user($feedback, $u2); + $this->assert_feedback_tmp_data_for_user($feedback, $u2); + } + } + + // Confirm cm2a wasn't affected. + $this->assert_feedback_data_for_user($cm2a, $u1); + $this->assert_feedback_tmp_data_for_user($cm2a, $u1); + + } + + /** + * Test deleting a whole context. + */ + public function test_delete_data_for_all_users_in_context() { + global $DB; + $dg = $this->getDataGenerator(); + $fg = $dg->get_plugin_generator('mod_feedback'); + + $c1 = $dg->create_course(); + $c2 = $dg->create_course(); + $cm0a = $dg->create_module('feedback', ['course' => SITEID]); + $cm1a = $dg->create_module('feedback', ['course' => $c1, 'anonymous' => FEEDBACK_ANONYMOUS_NO]); + + $u1 = $dg->create_user(); + $u2 = $dg->create_user(); + + // Create a bunch of data. + foreach ([$cm1a, $cm0a] as $feedback) { + $i1 = $fg->create_item_numeric($feedback); + $i2 = $fg->create_item_multichoice($feedback); + $answers = ['numeric_' . $i1->id => '1', 'multichoice_' . $i2->id => [1]]; + + $this->create_submission_with_answers($feedback, $u1, $answers); + $this->create_tmp_submission_with_answers($feedback, $u1, $answers); + + $this->create_submission_with_answers($feedback, $u2, $answers); + $this->create_tmp_submission_with_answers($feedback, $u2, $answers); + } + + provider::delete_data_for_all_users_in_context(context_module::instance($cm1a->cmid)); + + $this->assert_no_feedback_data_for_user($cm1a, $u1); + $this->assert_no_feedback_data_for_user($cm1a, $u2); + $this->assert_feedback_data_for_user($cm0a, $u1); + $this->assert_feedback_data_for_user($cm0a, $u2); + $this->assert_feedback_tmp_data_for_user($cm0a, $u1); + $this->assert_feedback_tmp_data_for_user($cm0a, $u2); + } + + /** + * Test exporting data. + */ + public function test_export_user_data() { + global $DB; + $dg = $this->getDataGenerator(); + $fg = $dg->get_plugin_generator('mod_feedback'); + + $c1 = $dg->create_course(); + $c2 = $dg->create_course(); + $cm0a = $dg->create_module('feedback', ['course' => SITEID]); + $cm1a = $dg->create_module('feedback', ['course' => $c1, 'anonymous' => FEEDBACK_ANONYMOUS_NO]); + $cm2a = $dg->create_module('feedback', ['course' => $c2, 'anonymous' => FEEDBACK_ANONYMOUS_YES, 'multiple_submit' => 1]); + $cm2b = $dg->create_module('feedback', ['course' => $c2]); + $cm2c = $dg->create_module('feedback', ['course' => $c2]); + + $u1 = $dg->create_user(); + $u2 = $dg->create_user(); + + // Create a bunch of data. + foreach ([$cm0a, $cm1a, $cm2a, $cm2b] as $feedback) { + $i1 = $fg->create_item_numeric($feedback, ['name' => 'Q1', 'label' => 'L1']); + $i2 = $fg->create_item_multichoice($feedback, ['name' => 'Q2', 'label' => 'L2']); + $answersu1 = ['numeric_' . $i1->id => '1', 'multichoice_' . $i2->id => [1]]; + $answersu2 = ['numeric_' . $i1->id => '2', 'multichoice_' . $i2->id => [2]]; + + if ($cm0a == $feedback) { + $this->create_submission_with_answers($feedback, $u1, $answersu1); + $this->create_tmp_submission_with_answers($feedback, $u1, $answersu1); + } else if ($cm1a == $feedback) { + $this->create_tmp_submission_with_answers($feedback, $u1, $answersu1); + } else if ($cm2a == $feedback) { + $this->create_submission_with_answers($feedback, $u1, $answersu1); + $this->create_submission_with_answers($feedback, $u1, ['numeric_' . $i1->id => '1337'], 2); + } else if ($cm2c == $feedback) { + $this->create_submission_with_answers($feedback, $u1, $answersu1); + $this->create_tmp_submission_with_answers($feedback, $u1, $answersu1); + } + + $this->create_submission_with_answers($feedback, $u2, $answersu2); + $this->create_tmp_submission_with_answers($feedback, $u2, $answersu2); + } + + $appctx = new approved_contextlist($u1, 'mod_feedback', [ + context_module::instance($cm0a->cmid)->id, + context_module::instance($cm1a->cmid)->id, + context_module::instance($cm2a->cmid)->id, + context_module::instance($cm2b->cmid)->id, + ]); + provider::export_user_data($appctx); + + // CM0A. + $data = writer::with_context(context_module::instance($cm0a->cmid))->get_data(); + $this->assertCount(2, $data->submissions); + $submission = $data->submissions[0]; + $this->assertEquals(transform::yesno(false), $submission['inprogress']); + $this->assertEquals(transform::yesno(true), $submission['anonymousresponse']); + $this->assertCount(2, $submission['answers']); + $this->assertEquals('Q1', $submission['answers'][0]['question']); + $this->assertEquals('1', $submission['answers'][0]['answer']); + $this->assertEquals('Q2', $submission['answers'][1]['question']); + $this->assertEquals('a', $submission['answers'][1]['answer']); + $submission = $data->submissions[1]; + $this->assertEquals(transform::yesno(true), $submission['inprogress']); + $this->assertEquals(transform::yesno(true), $submission['anonymousresponse']); + $this->assertCount(2, $submission['answers']); + $this->assertEquals('Q1', $submission['answers'][0]['question']); + $this->assertEquals('1', $submission['answers'][0]['answer']); + $this->assertEquals('Q2', $submission['answers'][1]['question']); + $this->assertEquals('a', $submission['answers'][1]['answer']); + + // CM1A. + $data = writer::with_context(context_module::instance($cm1a->cmid))->get_data(); + $this->assertCount(1, $data->submissions); + $submission = $data->submissions[0]; + $this->assertEquals(transform::yesno(true), $submission['inprogress']); + $this->assertEquals(transform::yesno(false), $submission['anonymousresponse']); + $this->assertCount(2, $submission['answers']); + $this->assertEquals('Q1', $submission['answers'][0]['question']); + $this->assertEquals('1', $submission['answers'][0]['answer']); + $this->assertEquals('Q2', $submission['answers'][1]['question']); + $this->assertEquals('a', $submission['answers'][1]['answer']); + + // CM2A. + $data = writer::with_context(context_module::instance($cm2a->cmid))->get_data(); + $this->assertCount(2, $data->submissions); + $submission = $data->submissions[0]; + $this->assertEquals(transform::yesno(false), $submission['inprogress']); + $this->assertEquals(transform::yesno(true), $submission['anonymousresponse']); + $this->assertCount(2, $submission['answers']); + $this->assertEquals('Q1', $submission['answers'][0]['question']); + $this->assertEquals('1', $submission['answers'][0]['answer']); + $this->assertEquals('Q2', $submission['answers'][1]['question']); + $this->assertEquals('a', $submission['answers'][1]['answer']); + $submission = $data->submissions[1]; + $this->assertEquals(transform::yesno(false), $submission['inprogress']); + $this->assertEquals(transform::yesno(true), $submission['anonymousresponse']); + $this->assertCount(1, $submission['answers']); + $this->assertEquals('Q1', $submission['answers'][0]['question']); + $this->assertEquals('1337', $submission['answers'][0]['answer']); + + // CM2B (no data). + $data = writer::with_context(context_module::instance($cm2b->cmid))->get_data(); + $this->assertEmpty($data); + + // CM2C (not exported). + $data = writer::with_context(context_module::instance($cm2b->cmid))->get_data(); + $this->assertEmpty($data); + } + + /** + * Assert there is no feedback data for a user. + * + * @param object $feedback The feedback. + * @param object $user The user. + * @return void + */ + protected function assert_no_feedback_data_for_user($feedback, $user) { + global $DB; + $this->assertFalse($DB->record_exists('feedback_completed', ['feedback' => $feedback->id, 'userid' => $user->id])); + $this->assertFalse($DB->record_exists('feedback_completedtmp', ['feedback' => $feedback->id, 'userid' => $user->id])); + + // Check that there aren't orphan values because we can't check by userid. + $sql = " + SELECT fv.id + FROM {%s} fv + LEFT JOIN {%s} fc + ON fc.id = fv.completed + WHERE fc.id IS NULL"; + $this->assertFalse($DB->record_exists_sql(sprintf($sql, 'feedback_value', 'feedback_completed'), [])); + $this->assertFalse($DB->record_exists_sql(sprintf($sql, 'feedback_valuetmp', 'feedback_completedtmp'), [])); + } + + /** + * Assert there are submissions and answers for user. + * + * @param object $feedback The feedback. + * @param object $user The user. + * @param int $submissioncount The number of submissions. + * @param int $valuecount The number of values per submission. + * @return void + */ + protected function assert_feedback_data_for_user($feedback, $user, $submissioncount = 1, $valuecount = 2) { + global $DB; + $completeds = $DB->get_records('feedback_completed', ['feedback' => $feedback->id, 'userid' => $user->id]); + $this->assertCount($submissioncount, $completeds); + foreach ($completeds as $record) { + $this->assertEquals($valuecount, $DB->count_records('feedback_value', ['completed' => $record->id])); + } + } + + /** + * Assert there are temporary submissions and answers for user. + * + * @param object $feedback The feedback. + * @param object $user The user. + * @param int $submissioncount The number of submissions. + * @param int $valuecount The number of values per submission. + * @return void + */ + protected function assert_feedback_tmp_data_for_user($feedback, $user, $submissioncount = 1, $valuecount = 2) { + global $DB; + $completedtmps = $DB->get_records('feedback_completedtmp', ['feedback' => $feedback->id, 'userid' => $user->id]); + $this->assertCount($submissioncount, $completedtmps); + foreach ($completedtmps as $record) { + $this->assertEquals($valuecount, $DB->count_records('feedback_valuetmp', ['completed' => $record->id])); + } + } + + /** + * Create an submission with answers. + * + * @param object $feedback The feedback. + * @param object $user The user. + * @param array $answers Answers. + * @param int $submissioncount The number of submissions expected after this entry. + * @return void + */ + protected function create_submission_with_answers($feedback, $user, $answers, $submissioncount = 1) { + global $DB, $USER; + $origuser = $USER; + $this->setUser($user); + + $modinfo = get_fast_modinfo($feedback->course); + $cm = $modinfo->get_cm($feedback->cmid); + + $feedbackcompletion = new mod_feedback_completion($feedback, $cm, $feedback->course); + $feedbackcompletion->save_response_tmp((object) $answers); + $feedbackcompletion->save_response(); + $this->assertEquals($submissioncount, $DB->count_records('feedback_completed', ['feedback' => $feedback->id, + 'userid' => $user->id])); + $this->assertEquals(count($answers), $DB->count_records('feedback_value', [ + 'completed' => $feedbackcompletion->get_completed()->id])); + + $this->setUser($origuser); + } + + /** + * Create a temporary submission with answers. + * + * @param object $feedback The feedback. + * @param object $user The user. + * @param array $answers Answers. + * @return void + */ + protected function create_tmp_submission_with_answers($feedback, $user, $answers) { + global $DB, $USER; + $origuser = $USER; + $this->setUser($user); + + $modinfo = get_fast_modinfo($feedback->course); + $cm = $modinfo->get_cm($feedback->cmid); + + $feedbackcompletion = new mod_feedback_completion($feedback, $cm, $feedback->course); + $feedbackcompletion->save_response_tmp((object) $answers); + $this->assertEquals(1, $DB->count_records('feedback_completedtmp', ['feedback' => $feedback->id, 'userid' => $user->id])); + $this->assertEquals(2, $DB->count_records('feedback_valuetmp', [ + 'completed' => $feedbackcompletion->get_current_completed_tmp()->id])); + + $this->setUser($origuser); + } +}