From f9f59314e9c0367d01ba73e4da27857d5f0936d4 Mon Sep 17 00:00:00 2001 From: Andrew Nicols Date: Sun, 15 Apr 2018 17:33:03 +0800 Subject: [PATCH 01/10] MDL-61407 privacy: Track changes in any context --- .../classes/tests/request/content_writer.php | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/privacy/classes/tests/request/content_writer.php b/privacy/classes/tests/request/content_writer.php index 59dfa2d56ef..4531a2fbf6a 100644 --- a/privacy/classes/tests/request/content_writer.php +++ b/privacy/classes/tests/request/content_writer.php @@ -73,6 +73,8 @@ class content_writer implements \core_privacy\local\request\content_writer { /** * Whether any data has been exported at all within the current context. + * + * @return bool */ public function has_any_data() { $hasdata = !empty($this->data->{$this->context->id}); @@ -96,6 +98,32 @@ class content_writer implements \core_privacy\local\request\content_writer { return $hasanydata; } + /** + * Whether any data has been exported for any context. + * + * @return bool + */ + public function has_any_data_in_any_context() { + $checkfordata = function($location) { + foreach ($location as $context => $data) { + if (!empty($data)) { + return true; + } + } + + return false; + }; + + $hasanydata = $checkfordata($this->data); + $hasanydata = $hasanydata || $checkfordata($this->relateddata); + $hasanydata = $hasanydata || $checkfordata($this->metadata); + $hasanydata = $hasanydata || $checkfordata($this->files); + $hasanydata = $hasanydata || $checkfordata($this->customfiles); + $hasanydata = $hasanydata || $checkfordata($this->userprefs); + + return $hasanydata; + } + /** * Constructor for the content writer. * From 7597722505d85468ba9c826609f704398f18bb4e Mon Sep 17 00:00:00 2001 From: Andrew Nicols Date: Sun, 15 Apr 2018 17:29:04 +0800 Subject: [PATCH 02/10] MDL-61407 question: Allow formatters to ignore caps --- lib/questionlib.php | 2 +- question/format.php | 12 +++++++----- question/upgrade.txt | 6 ++++++ 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/lib/questionlib.php b/lib/questionlib.php index af4b916bbb5..5347ef81012 100644 --- a/lib/questionlib.php +++ b/lib/questionlib.php @@ -1789,7 +1789,7 @@ function question_pluginfile($course, $context, $component, $filearea, $args, $f } // export data to moodle file pool - if (!$content = $qformat->exportprocess(true)) { + if (!$content = $qformat->exportprocess()) { send_file_not_found(); } diff --git a/question/format.php b/question/format.php index 20b92fec95a..8c41d05cafc 100644 --- a/question/format.php +++ b/question/format.php @@ -759,11 +759,13 @@ class qformat_default { } /** - * Do the export - * For most types this should not need to be overrided - * @return stored_file + * Perform the export. + * For most types this should not need to be overrided. + * + * @param bool $checkcapabilities Whether to check capabilities when exporting the questions. + * @return string The content of the export. */ - public function exportprocess() { + public function exportprocess($checkcapabilities = true) { global $CFG, $OUTPUT, $DB, $USER; // get the questions (from database) in this category @@ -823,7 +825,7 @@ class qformat_default { // export the question displaying message $count++; - if (question_has_capability_on($question, 'view', $question->category)) { + if (!$checkcapabilities || question_has_capability_on($question, 'view', $question->category)) { $expout .= $this->writequestion($question, $contextid) . "\n"; } } diff --git a/question/upgrade.txt b/question/upgrade.txt index 7d0bbe35ff3..f34bd6a4626 100644 --- a/question/upgrade.txt +++ b/question/upgrade.txt @@ -1,5 +1,11 @@ This files describes API changes for code that uses the question API. +=== 3.5 === + +1) The question format exportprocess function now adds a + $checkcapabilities boolean to allow questions to be exported in + certain circumstances. + === 3.2 === 1) The following renderers have been deprecated in favour of the renderable From 5b2b0e66c125deb2d8d3ae11d84bbb0f5139dd7f Mon Sep 17 00:00:00 2001 From: Andrew Nicols Date: Wed, 21 Feb 2018 17:37:49 +0800 Subject: [PATCH 03/10] MDL-61407 question: Add initial privacy implementation --- lang/en/question.php | 24 +- question/classes/privacy/provider.php | 467 +++++++++++++++++++++++ question/tests/generator/lib.php | 63 +++ question/tests/privacy_helper.php | 103 +++++ question/tests/privacy_provider_test.php | 420 ++++++++++++++++++++ 5 files changed, 1076 insertions(+), 1 deletion(-) create mode 100644 question/classes/privacy/provider.php create mode 100644 question/tests/privacy_helper.php create mode 100644 question/tests/privacy_provider_test.php diff --git a/lang/en/question.php b/lang/en/question.php index b3c377dc7d9..4444be6ceaa 100644 --- a/lang/en/question.php +++ b/lang/en/question.php @@ -380,6 +380,29 @@ $string['penaltyforeachincorrecttry_help'] = 'When questions are run using the \ The penalty is a proportion of the total question grade, so if the question is worth three marks, and the penalty is 0.3333333, then the student will score 3 if they get the question right first time, 2 if they get it right second try, and 1 of they get it right on the third try.'; $string['previewquestion'] = 'Preview question: {$a}'; +$string['privacy:metadata:database:question'] = 'The details about an specific question.'; +$string['privacy:metadata:database:question:createdby'] = 'The person who created the question.'; +$string['privacy:metadata:database:question:generalfeedback'] = 'The general feedback for this question.'; +$string['privacy:metadata:database:question:modifiedby'] = 'The person who last updated the question.'; +$string['privacy:metadata:database:question:name'] = 'The name of the question.'; +$string['privacy:metadata:database:question:questiontext'] = 'The question text.'; +$string['privacy:metadata:database:question:timecreated'] = 'The date and time when this question was created.'; +$string['privacy:metadata:database:question:timemodified'] = 'The date and time when this question was updated.'; +$string['privacy:metadata:database:question_attempt_step_data'] = 'Question attempt steps may have additional data specific to that step. The data is stored in the step_data table.'; +$string['privacy:metadata:database:question_attempt_step_data:name'] = 'The name of the data item.'; +$string['privacy:metadata:database:question_attempt_step_data:value'] = 'The value of the data item.'; +$string['privacy:metadata:database:question_attempt_steps'] = 'Each question attempt has a number of steps to indicate the different phases from beginning to completion to marking. This table stores the information for each of these steps.'; +$string['privacy:metadata:database:question_attempt_steps:fraction'] = 'The grade that was awarded to this question attempt scaled to a value out of 1.'; +$string['privacy:metadata:database:question_attempt_steps:state'] = 'The state of this question attempt step at the end of the step transition.'; +$string['privacy:metadata:database:question_attempt_steps:timecreated'] = 'The date and time that this step transition begun.'; +$string['privacy:metadata:database:question_attempt_steps:userid'] = 'The user who performed the step transition.'; +$string['privacy:metadata:database:question_attempts'] = 'The information about an attempt at a specific question.'; +$string['privacy:metadata:database:question_attempts:flagged'] = 'An indication that the user has flagged this question within the attempt.'; +$string['privacy:metadata:database:question_attempts:responsesummary'] = 'A summary of the question response.'; +$string['privacy:metadata:database:question_attempts:timemodified'] = 'The time that the question attempt was updated.'; +$string['privacy:metadata:link:qbehaviour'] = 'The Question subsystem makes use of the Question Behaviour plugintype.'; +$string['privacy:metadata:link:qformat'] = 'The Question subsystem makes use of the Question Format plugintype for the purpose of importing and exporting questions in different formats.'; +$string['privacy:metadata:link:qtype'] = 'The Question subsystem interacts with the Question Type plugintype which contains the different types of questions.'; $string['questionbehaviouradminsetting'] = 'Question behaviour settings'; $string['questionbehavioursdisabled'] = 'Question behaviours to disable'; $string['questionbehavioursdisabledexplained'] = 'Enter a comma separated list of behaviours you do not want to appear in dropdown menu'; @@ -442,4 +465,3 @@ $string['whichtries'] = 'Which tries'; $string['withselected'] = 'With selected'; $string['xoutofmax'] = '{$a->mark} out of {$a->max}'; $string['yougotnright'] = 'You have correctly selected {$a->num}.'; - diff --git a/question/classes/privacy/provider.php b/question/classes/privacy/provider.php new file mode 100644 index 00000000000..7b23ec54c7e --- /dev/null +++ b/question/classes/privacy/provider.php @@ -0,0 +1,467 @@ +. + +/** + * Privacy Subsystem implementation for core_question. + * + * @package core_question + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_question\privacy; + +use \core_privacy\local\metadata\collection; +use \core_privacy\local\request\writer; +use \core_privacy\local\request\transform; +use \core_privacy\local\request\contextlist; +use \core_privacy\local\request\approved_contextlist; + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->libdir . '/questionlib.php'); +require_once($CFG->dirroot . '/question/format.php'); +require_once($CFG->dirroot . '/question/editlib.php'); +require_once($CFG->dirroot . '/question/engine/datalib.php'); + +/** + * Privacy Subsystem implementation for core_question. + * + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements + // This component has data. + // We need to return all question information where the user is + // listed in either the question.createdby or question.modifiedby fields. + // We may also need to fetch this informtion from individual plugins in some cases. + // e.g. to fetch the full and other question-specific meta-data. + \core_privacy\local\metadata\provider, + + // This is a subsysytem which provides information to core. + \core_privacy\local\request\subsystem\provider, + + // This is a subsysytem which provides information to plugins. + \core_privacy\local\request\subsystem\plugin_provider +{ + + /** + * Describe the types of data stored by the question subsystem. + * + * @param collection $items The collection to add metadata to. + * @return collection The array of metadata + */ + public static function get_metadata(collection $items) { + // Other tables link against it. + + // The 'question_usages' table does not contain any user data. + // The table links the but doesn't store itself. + + // The 'question_attempts' table contains data about question attempts. + // It does not contain any user ids - these are stored by the caller. + $items->add_database_table('question_attempts', [ + 'flagged' => 'privacy:metadata:database:question_attempts:flagged', + 'responsesummary' => 'privacy:metadata:database:question_attempts:responsesummary', + 'timemodified' => 'privacy:metadata:database:question_attempts:timemodified', + ], 'privacy:metadata:database:question_attempts');; + + // The 'question_attempt_steps' table contains data about changes to the state of a question attempt. + $items->add_database_table('question_attempt_steps', [ + 'state' => 'privacy:metadata:database:question_attempt_steps:state', + 'timecreated' => 'privacy:metadata:database:question_attempt_steps:timecreated', + 'fraction' => 'privacy:metadata:database:question_attempt_steps:fraction', + 'userid' => 'privacy:metadata:database:question_attempt_steps:userid', + ], 'privacy:metadata:database:question_attempt_steps'); + + // The 'question_attempt_step_data' table contains specific all metadata for each state. + $items->add_database_table('question_attempt_step_data', [ + 'name' => 'privacy:metadata:database:question_attempt_step_data:name', + 'value' => 'privacy:metadata:database:question_attempt_step_data:value', + ], 'privacy:metadata:database:question_attempt_step_data'); + + // These are all part of the set of the question definition + // The 'question' table is used to store instances of each question. + // It contains a createdby and modifiedby which related to specific users. + $items->add_database_table('question', [ + 'name' => 'privacy:metadata:database:question:name', + 'questiontext' => 'privacy:metadata:database:question:questiontext', + 'generalfeedback' => 'privacy:metadata:database:question:generalfeedback', + 'timecreated' => 'privacy:metadata:database:question:timecreated', + 'timemodified' => 'privacy:metadata:database:question:timemodified', + 'createdby' => 'privacy:metadata:database:question:createdby', + 'modifiedby' => 'privacy:metadata:database:question:modifiedby', + ], 'privacy:metadata:database:question'); + + // The 'question_answers' table is used to store the set of answers, with appropriate feedback for each question. + // It does not contain user data. + + // The 'question_hints' table is used to store hints about the correct answer for a question. + // It does not contain user data. + + // The 'question_categories' table contains structural information about how questions are presented in the UI. + // It does not contain user data. + + // The 'question_statistics' table contains aggregated statistics about responses. + // It does not contain any identifiable user data. + + // The question subsystem makes use of the qtype, qformat, and qbehaviour plugin types. + $items->add_plugintype_link('qtype', [], 'privacy:metadata:link:qtype'); + $items->add_plugintype_link('qformat', [], 'privacy:metadata:link:qformat'); + $items->add_plugintype_link('qbehaviour', [], 'privacy:metadata:link:qbehaviour'); + + return $items; + } + + /** + * Export the data for all question attempts on this question usage. + * + * Where a user is the owner of the usage, then the full detail of that usage will be included. + * Where a user has been involved in the usage, but it is not their own usage, then only their specific + * involvement will be exported. + * + * @param int $userid The userid to export. + * @param \context $context The context that the question was used within. + * @param array $usagecontext The subcontext of this usage. + * @param int $usage The question usage ID. + * @param \question_display_options $options The display options used for formatting. + * @param bool $isowner Whether the user being exported is the user who used the question. + */ + public static function export_question_usage( + $userid, + \context $context, + array $usagecontext, + $usage, + \question_display_options $options, + $isowner + ) { + // Determine the questions in this usage. + $quba = \question_engine::load_questions_usage_by_activity($usage); + + $basepath = $usagecontext; + $questionscontext = array_merge($usagecontext, [ + get_string('questions', 'core_question'), + ]); + + foreach ($quba->get_attempt_iterator() as $qa) { + $question = $qa->get_question(); + $slotno = $qa->get_slot(); + $questionnocontext = array_merge($questionscontext, [$slotno]); + + if ($isowner) { + // This user is the overal owner of the question attempt and all data wil therefore be exported. + // + // Respect _some_ of the question_display_options to ensure that they don't have access to + // generalfeedback and mark if the display options prevent this. + // This is defensible because they can submit questions without completing a quiz and perform an SAR to + // get prior access to the feedback and mark to improve upon it. + // Export the response. + $data = (object) [ + 'name' => $question->name, + 'question' => $qa->get_question_summary(), + 'answer' => $qa->get_response_summary(), + 'timemodified' => transform::datetime($qa->timemodified), + ]; + + if ($options->marks >= \question_display_options::MARK_AND_MAX) { + $data->mark = $qa->format_mark($options->markdp); + } + + if ($options->flags != \question_display_options::HIDDEN) { + $data->flagged = transform::yesno($qa->is_flagged()); + } + + if ($options->generalfeedback != \question_display_options::HIDDEN) { + $data->generalfeedback = $question->format_generalfeedback($qa); + } + + if ($options->manualcomment != \question_display_options::HIDDEN) { + $behaviour = $qa->get_behaviour(); + if ($qa->has_manual_comment()) { + // Note - the export of the step data will ensure that the files are exported. + // No need to do it again here. + list($comment, $commentformat, $step) = $qa->get_manual_comment(); + + $comment = writer::with_context($context) + ->rewrite_pluginfile_urls( + $questionnocontext, + 'question', + 'response_bf_comment', + $step->get_id(), + $comment + ); + $data->comment = $behaviour->format_comment($comment, $commentformat); + } + } + + writer::with_context($context) + ->export_data($questionnocontext, $data); + + // Export the step data. + static::export_question_attempt_steps($userid, $context, $questionnocontext, $qa, $options, $isowner); + } + } + } + + /** + * Export the data for each step transition for each question in each question attempt. + * + * Where a user is the owner of the usage, then all steps in the question usage will be exported. + * Where a user is not the owner, but has been involved in the usage, then only their specific + * involvement will be exported. + * + * @param int $userid The user to export for + * @param \context $context The context that the question was used within. + * @param array $questionnocontext The subcontext of this question number. + * @param \question_attempt $qa The attempt being checked + * @param \question_display_options $options The display options used for formatting. + * @param bool $isowner Whether the user being exported is the user who used the question. + */ + public static function export_question_attempt_steps( + $userid, + \context $context, + array $questionnocontext, + \question_attempt $qa, + \question_display_options $options, + $isowner + ) { + $attemptdata = (object) [ + 'steps' => [], + ]; + $stepno = 0; + foreach ($qa->get_step_iterator() as $i => $step) { + $stepno++; + + if ($isowner || ($step->get_user_id() != $userid)) { + // The user is the owner, or the author of the step. + + $restrictedqa = new \question_attempt_with_restricted_history($qa, $i, null); + $stepdata = (object) [ + // Note: Do not include the user here. + 'time' => transform::datetime($step->get_timecreated()), + 'action' => $qa->summarise_action($step), + ]; + + if ($options->marks >= \question_display_options::MARK_AND_MAX) { + $stepdata->mark = $qa->format_fraction_as_mark($step->get_fraction(), $options->markdp); + } + + if ($options->correctness != \question_display_options::HIDDEN) { + $stepdata->state = $restrictedqa->get_state_string($options->correctness); + } + + if ($step->has_behaviour_var('comment')) { + $behaviour = $qa->get_behaviour(); + $comment = $step->get_behaviour_var('comment'); + $commentformat = $step->get_behaviour_var('commentformat'); + + if (empty(trim($comment))) { + // Skip empty comments. + continue; + } + + // Format the comment. + $comment = writer::with_context($context) + ->rewrite_pluginfile_urls( + $questionnocontext, + 'question', + 'response_bf_comment', + $step->get_id(), + $comment + ); + + writer::with_context($context) + ->export_area_files( + $questionnocontext, + 'question', + "response_bf_comment", + $step->get_id() + ); + + $stepdata->comment = $behaviour->format_comment($comment, $commentformat); + } + + $attemptdata->steps[$stepno] = $stepdata; + } + } + + if (!empty($attemptdata->steps)) { + writer::with_context($context) + ->export_related_data($questionnocontext, 'steps', $attemptdata); + } + } + + /** + * Get the list of contexts where the specified user has either created, or edited a question. + * + * To export usage of a question, please call {@link provider::export_question_usage()} from the module which + * instantiated the usage of the question. + * + * @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($userid) { + $contextlist = new contextlist(); + + // A user may have created or updated a question. + // Questions are linked against a question category, which has a contextid field. + $sql = "SELECT cat.contextid + FROM {question} q + INNER JOIN {question_categories} cat ON cat.id = q.category + WHERE + q.createdby = :useridcreated OR + q.modifiedby = :useridmodified"; + $params = [ + 'useridcreated' => $userid, + 'useridmodified' => $userid, + ]; + $contextlist->add_from_sql($sql, $params); + + return $contextlist; + } + + /** + * Determine related question usages for a user. + * + * @param string $prefix A unique prefix to add to the table alias + * @param string $component The name of the component to fetch usages for. + * @param string $joinfield The SQL field name to use in the JOIN ON - e.g. q.usageid + * @param int $userid The user to search. + * @return \qubaid_join + */ + public static function get_related_question_usages_for_user($prefix, $component, $joinfield, $userid) { + return new \qubaid_join(" + JOIN {question_usages} {$prefix}_qu ON {$prefix}_qu.id = {$joinfield} + AND {$prefix}_qu.component = :{$prefix}_usagecomponent + JOIN {question_attempts} {$prefix}_qa ON {$prefix}_qa.questionusageid = {$prefix}_qu.id + JOIN {question_attempt_steps} {$prefix}_qas ON {$prefix}_qas.questionattemptid = {$prefix}_qa.id", + "{$prefix}_qu.id", + "{$prefix}_qas.userid = :{$prefix}_stepuserid", + [ + "{$prefix}_stepuserid" => $userid, + "{$prefix}_usagecomponent" => $component, + ]); + } + + /** + * 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 $CFG, $DB, $SITE; + if (empty($contextlist)) { + return; + } + + // Use the Moodle XML Data format. + // It is the only lossless format that we support. + $format = "xml"; + require_once($CFG->dirroot . "/question/format/{$format}/format.php"); + + // THe export system needs questions in a particular format. + // The easiest way to fetch these is with get_questions_category() which takes the details of a question + // category. + // We fetch the root question category for each context and the get_questions_category function recurses to + // After fetching them, we filter out any not created or modified by the requestor. + $user = $contextlist->get_user(); + $userid = $user->id; + + list($contextsql, $contextparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED); + $categories = $DB->get_records_select('question_categories', "contextid {$contextsql} AND parent = 0", $contextparams); + + $classname = "qformat_{$format}"; + foreach ($categories as $category) { + $context = \context::instance_by_id($category->contextid); + + $questions = get_questions_category($category, true); + $questions = array_filter($questions, function($question) use ($userid) { + return ($question->createdby == $userid) || ($question->modifiedby == $userid); + }, ARRAY_FILTER_USE_BOTH); + + if (empty($questions)) { + continue; + } + + $qformat = new $classname(); + $qformat->setQuestions($questions); + + $qformat->setContexts([$context]); + $qformat->setContexttofile(true); + + // We do not know which course this belongs to, and it's not actually used except in error, so use Site. + $qformat->setCourse($SITE); + $content = ''; + if ($qformat->exportpreprocess()) { + $content = $qformat->exportprocess(false); + } + + $subcontext = [ + get_string('questionbank', 'core_question'), + ]; + writer::with_context($context)->export_custom_file($subcontext, 'questions.xml', $content); + } + } + + /** + * 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; + + // Questions are considered to be 'owned' by the institution, even if they were originally written by a specific + // user. They are still exported in the list of a users data, but they are not removed. + // The userid is instead anonymised. + + $DB->set_field_select('question', 'createdby', 0, + 'category IN (SELECT id FROM {question_categories} WHERE contextid = :contextid)', + [ + 'contextid' => $context->id, + ]); + + $DB->set_field_select('question', 'modifiedby', 0, + 'category IN (SELECT id FROM {question_categories} WHERE contextid = :contextid)', + [ + 'contextid' => $context->id, + ]); + } + + /** + * 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; + + // Questions are considered to be 'owned' by the institution, even if they were originally written by a specific + // user. They are still exported in the list of a users data, but they are not removed. + // The userid is instead anonymised. + + list($contextsql, $contextparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED); + $contextparams['createdby'] = $contextlist->get_user()->id; + $DB->set_field_select('question', 'createdby', 0, " + category IN (SELECT id FROM {question_categories} WHERE contextid {$contextsql}) + AND createdby = :createdby", $contextparams); + + list($contextsql, $contextparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED); + $contextparams['modifiedby'] = $contextlist->get_user()->id; + $DB->set_field_select('question', 'modifiedby', 0, " + category IN (SELECT id FROM {question_categories} WHERE contextid {$contextsql}) + AND modifiedby = :modifiedby", $contextparams); + } +} diff --git a/question/tests/generator/lib.php b/question/tests/generator/lib.php index 0b8a0e5b831..cc9250a65b0 100644 --- a/question/tests/generator/lib.php +++ b/question/tests/generator/lib.php @@ -82,6 +82,69 @@ class core_question_generator extends component_generator_base { $question->category = $fromform->category; $question->qtype = $qtype; $question->createdby = 0; + + return $this->update_question($question, $which, $overrides); + } + + /** + * Update an existing question. + * + * @param stdClass $question the question data to update. + * @param string $which as for the corresponding argument of + * {@link question_test_helper::get_question_form_data}. null for the default one. + * @param array|stdClass $overrides any fields that should be different from the base example. + */ + public function update_question($question, $which = null, $overrides = null) { + global $CFG; + require_once($CFG->dirroot . '/question/engine/tests/helpers.php'); + + $qtype = $question->qtype; + + $fromform = test_question_maker::get_question_form_data($qtype, $which); + $fromform = (object) $this->datagenerator->combine_defaults_and_record( + (array) $question, $fromform); + $fromform = (object) $this->datagenerator->combine_defaults_and_record( + (array) $fromform, $overrides); + return question_bank::get_qtype($qtype)->save_question($question, $fromform); } + + /** + * Setup a course category, course, a question category, and 2 questions + * for testing. + * + * @param string $type The type of question category to create. + * @return array The created data objects + */ + public function setup_course_and_questions($type = 'course') { + $datagenerator = $this->datagenerator; + $category = $datagenerator->create_category(); + $course = $datagenerator->create_course([ + 'numsections' => 5, + 'category' => $category->id + ]); + + switch ($type) { + case 'category': + $context = context_coursecat::instance($category->id); + break; + + case 'system': + $context = context_system::instance(); + break; + + default: + $context = context_course::instance($course->id); + break; + } + + $qcat = $this->create_question_category(['contextid' => $context->id]); + + $questions = array( + $this->create_question('shortanswer', null, ['category' => $qcat->id]), + $this->create_question('shortanswer', null, ['category' => $qcat->id]), + ); + + return array($category, $course, $qcat, $questions); + } } diff --git a/question/tests/privacy_helper.php b/question/tests/privacy_helper.php new file mode 100644 index 00000000000..f564175112c --- /dev/null +++ b/question/tests/privacy_helper.php @@ -0,0 +1,103 @@ +. + +/** + * Helper for privacy tests. + * + * @package core_question + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +use \core_privacy\local\request\writer; + +/** + * Helper for privacy tests. + * + * @package core_question + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +trait core_question_privacy_helper { + /** + * Assert that the question usage in the supplied slot matches the expected format + * and usage for a question. + * + * @param \question_usage_by_activity $quba The Question Usage to test against. + * @param int $slotno The slot number to compare + * @param \question_display_options $options The display options used for formatting. + * @param \stdClass $data The data to check. + */ + public function assert_question_slot_equals( + \question_usage_by_activity $quba, + $slotno, + \question_display_options $options, + $data + ) { + $attempt = $quba->get_question_attempt($slotno); + $question = $attempt->get_question(); + + // Check the question data exported. + $this->assertEquals($data->name, $question->name); + $this->assertEquals($data->question, $question->questiontext); + + // Check the answer exported. + $this->assertEquals($attempt->get_response_summary(), $data->answer); + + if ($options->marks != \question_display_options::HIDDEN) { + $this->assertEquals($attempt->get_mark(), $data->mark); + } else { + $this->assertFalse(isset($data->mark)); + } + + if ($options->flags != \question_display_options::HIDDEN) { + $this->assertEquals($attempt->is_flagged(), (int) $data->flagged); + } else { + $this->assertFalse(isset($data->flagged)); + } + + if ($options->generalfeedback != \question_display_options::HIDDEN) { + $this->assertEquals($question->format_generalfeedback($attempt), $data->generalfeedback); + } else { + $this->assertFalse(isset($data->generalfeedback)); + } + } + + /** + * Assert that a question attempt was exported. + * + * @param \context $context The context which the attempt should be in + * @param array $subcontext The base of the export + * @param question_usage_by_activity $quba The question usage expected + * @param \question_display_options $options The display options used for formatting. + * @param \stdClass $user The user exported + */ + public function assert_question_attempt_exported(\context $context, array $subcontext, $quba, $options, $user) { + $usagecontext = array_merge( + $subcontext, + [get_string('questions', 'core_question')] + ); + + $writer = writer::with_context($context); + + foreach ($quba->get_slots() as $slotno) { + $data = $writer->get_data(array_merge($usagecontext, [$slotno])); + $this->assert_question_slot_equals($quba, $slotno, $options, $data); + } + } +} diff --git a/question/tests/privacy_provider_test.php b/question/tests/privacy_provider_test.php new file mode 100644 index 00000000000..6760e7d8bc8 --- /dev/null +++ b/question/tests/privacy_provider_test.php @@ -0,0 +1,420 @@ +. + +/** + * Privacy provider tests. + * + * @package core_question + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +use core_privacy\local\metadata\collection; +use core_privacy\local\request\deletion_criteria; +use core_privacy\local\request\writer; +use core_question\privacy\provider; + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->libdir . '/xmlize.php'); +require_once(__DIR__ . '/privacy_helper.php'); +require_once(__DIR__ . '/../engine/tests/helpers.php'); + +/** + * Privacy provider tests class. + * + * @package core_question + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class core_question_privacy_provider_testcase extends \core_privacy\tests\provider_testcase { + + // Include the privacy helper which has assertions on it. + use core_question_privacy_helper; + + /** + * Prepare a question attempt. + * + * @return question_usage_by_activity + */ + protected function prepare_question_attempt() { + // Create a question with a usage from the current user. + $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question'); + $cat = $questiongenerator->create_question_category(); + $quba = question_engine::make_questions_usage_by_activity('core_question_preview', context_system::instance()); + $quba->set_preferred_behaviour('deferredfeedback'); + $questiondata = $questiongenerator->create_question('numerical', null, ['category' => $cat->id]); + $question = question_bank::load_question($questiondata->id); + $quba->add_question($question); + $quba->start_all_questions(); + + question_engine::save_questions_usage_by_activity($quba); + + return $quba; + } + + /** + * Test that calling export_question_usage on a usage belonging to a + * different user does not export any data. + */ + public function test_export_question_usage_no_usage() { + $this->resetAfterTest(); + + $quba = $this->prepare_question_attempt(); + + // Create a question with a usage from the current user. + $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question'); + $cat = $questiongenerator->create_question_category(); + $quba = question_engine::make_questions_usage_by_activity('core_question_preview', context_system::instance()); + $quba->set_preferred_behaviour('deferredfeedback'); + $questiondata = $questiongenerator->create_question('numerical', null, ['category' => $cat->id]); + $question = question_bank::load_question($questiondata->id); + $quba->add_question($question); + $quba->start_all_questions(); + + question_engine::save_questions_usage_by_activity($quba); + + // Set the user. + $testuser = $this->getDataGenerator()->create_user(); + $this->setUser($testuser); + $context = $quba->get_owning_context(); + $options = new \question_display_options(); + + provider::export_question_usage($testuser->id, $context, [], $quba->get_id(), $options, false); + $writer = writer::with_context($context); + + $this->assertFalse($writer->has_any_data_in_any_context()); + } + + /** + * Test that calling export_question_usage on a usage belonging to a + * different user but ignoring the user match + */ + public function test_export_question_usage_with_usage() { + $this->resetAfterTest(); + + $quba = $this->prepare_question_attempt(); + + // Create a question with a usage from the current user. + $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question'); + $cat = $questiongenerator->create_question_category(); + $quba = question_engine::make_questions_usage_by_activity('core_question_preview', context_system::instance()); + $quba->set_preferred_behaviour('deferredfeedback'); + + $questiondata = $questiongenerator->create_question('truefalse', 'true', ['category' => $cat->id]); + $quba->add_question(question_bank::load_question($questiondata->id)); + $questiondata = $questiongenerator->create_question('shortanswer', null, ['category' => $cat->id]); + $quba->add_question(question_bank::load_question($questiondata->id)); + + // Set the user and answer the questions. + $testuser = $this->getDataGenerator()->create_user(); + $this->setUser($testuser); + + $quba->start_all_questions(); + $quba->process_action(1, ['answer' => 1]); + $quba->process_action(2, ['answer' => 'cat']); + $quba->finish_all_questions(); + + question_engine::save_questions_usage_by_activity($quba); + + $context = $quba->get_owning_context(); + + // Export all questions for this attempt. + $options = new \question_display_options(); + provider::export_question_usage($testuser->id, $context, [], $quba->get_id(), $options, true); + $writer = writer::with_context($context); + + $this->assertTrue($writer->has_any_data_in_any_context()); + $this->assertTrue($writer->has_any_data()); + + $slots = $quba->get_slots(); + $this->assertCount(2, $slots); + + foreach ($slots as $slotno) { + $data = $writer->get_data([get_string('questions', 'core_question'), $slotno]); + $this->assertNotNull($data); + $this->assert_question_slot_equals($quba, $slotno, $options, $data); + } + + $this->assertEmpty($writer->get_data([get_string('questions', 'core_question'), $quba->next_slot_number()])); + + // Disable some options and re-export. + writer::reset(); + $options = new \question_display_options(); + $options->hide_all_feedback(); + $options->flags = \question_display_options::HIDDEN; + $options->marks = \question_display_options::HIDDEN; + + provider::export_question_usage($testuser->id, $context, [], $quba->get_id(), $options, true); + $writer = writer::with_context($context); + + $this->assertTrue($writer->has_any_data_in_any_context()); + $this->assertTrue($writer->has_any_data()); + + $slots = $quba->get_slots(); + $this->assertCount(2, $slots); + + foreach ($slots as $slotno) { + $data = $writer->get_data([get_string('questions', 'core_question'), $slotno]); + $this->assertNotNull($data); + $this->assert_question_slot_equals($quba, $slotno, $options, $data); + } + + $this->assertEmpty($writer->get_data([get_string('questions', 'core_question'), $quba->next_slot_number()])); + } + + /** + * Test that questions owned by a user are exported and never deleted. + */ + public function test_question_owned_is_handled() { + global $DB; + $this->resetAfterTest(); + + $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question'); + + // Create the two test users. + $user = $this->getDataGenerator()->create_user(); + $otheruser = $this->getDataGenerator()->create_user(); + + // Create one question as each user in diferent contexts. + $this->setUser($user); + $userdata = $questiongenerator->setup_course_and_questions(); + $expectedcontext = \context_course::instance($userdata[1]->id); + + $this->setUser($otheruser); + $otheruserdata = $questiongenerator->setup_course_and_questions(); + $unexpectedcontext = \context_course::instance($otheruserdata[1]->id); + + // And create another one where we'll update a question as the test user. + $moreotheruserdata = $questiongenerator->setup_course_and_questions(); + $otherexpectedcontext = \context_course::instance($moreotheruserdata[1]->id); + $morequestions = $moreotheruserdata[3]; + + // Update the third set of questions. + $this->setUser($user); + + foreach ($morequestions as $question) { + $questiongenerator->update_question($question); + } + + // Run the get_contexts_for_userid as default user. + $this->setUser(); + + // There should be two contexts returned - the first course, and the third. + $contextlist = provider::get_contexts_for_userid($user->id); + $this->assertCount(2, $contextlist); + + $expectedcontexts = [ + $expectedcontext->id, + $otherexpectedcontext->id, + ]; + $this->assertEquals($expectedcontexts, $contextlist->get_contextids(), 'Contexts not equal', 0.0, 10, true); + + // Run the export_user_Data as the test user. + $this->setUser($user); + + $approvedcontextlist = new \core_privacy\tests\request\approved_contextlist( + \core_user::get_user($user->id), + 'core_question', + $expectedcontexts + ); + provider::export_user_data($approvedcontextlist); + + // There should be data for the user's question context. + $writer = writer::with_context($expectedcontext); + $this->assertTrue($writer->has_any_data()); + + // And for the course we updated. + $otherwriter = writer::with_context($otherexpectedcontext); + $this->assertTrue($otherwriter->has_any_data()); + + // But not for the other user's course. + $otherwriter = writer::with_context($unexpectedcontext); + $this->assertFalse($otherwriter->has_any_data()); + + // The question data is exported as an XML export in custom files. + $writer = writer::with_context($expectedcontext); + $subcontext = [get_string('questionbank', 'core_question')]; + + $exportfile = $writer->get_custom_file($subcontext, 'questions.xml'); + $this->assertNotEmpty($exportfile); + + $xmlized = xmlize($exportfile); + $xmlquestions = $xmlized['quiz']['#']['question']; + + $this->assertCount(2, $xmlquestions); + + // Run the delete functions as default user. + $this->setUser(); + + // The delete functions should do nothing here. + $this->assertCount(6, $DB->get_records('question')); + + // Delete for all users in context. + provider::delete_data_for_all_users_in_context($expectedcontext); + $this->assertCount(6, $DB->get_records('question')); + + provider::delete_data_for_user($approvedcontextlist); + $this->assertCount(6, $DB->get_records('question')); + } + + /** + * Deleting questions should only unset their created and modified user. + */ + public function test_question_delete_data_for_user_anonymised() { + global $DB; + $this->resetAfterTest(true); + + $user = \core_user::get_user_by_username('admin'); + $otheruser = $this->getDataGenerator()->create_user(); + + $course = $this->getDataGenerator()->create_course(); + $context = \context_course::instance($course->id); + $othercourse = $this->getDataGenerator()->create_course(); + $othercontext = \context_course::instance($othercourse->id); + + // Create a couple of questions. + $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question'); + $cat = $questiongenerator->create_question_category([ + 'contextid' => $context->id, + ]); + $othercat = $questiongenerator->create_question_category([ + 'contextid' => $othercontext->id, + ]); + + // Create questions: + // Q1 - Created by the UUT, Modified by UUT. + // Q2 - Created by the UUT, Modified by the other user. + // Q3 - Created by the other user, Modified by UUT + // Q4 - Created by the other user, Modified by the other user. + // Q5 - Created by the UUT, Modified by the UUT, but in a different context. + $this->setUser($user); + $q1 = $questiongenerator->create_question('shortanswer', null, array('category' => $cat->id)); + $q2 = $questiongenerator->create_question('shortanswer', null, array('category' => $cat->id)); + + $this->setUser($otheruser); + $questiongenerator->update_question($q2); + $q3 = $questiongenerator->create_question('shortanswer', null, array('category' => $cat->id)); + $q4 = $questiongenerator->create_question('shortanswer', null, array('category' => $cat->id)); + + $this->setUser($user); + $questiongenerator->update_question($q3); + $q5 = $questiongenerator->create_question('shortanswer', null, array('category' => $othercat->id)); + + $approvedcontextlist = new \core_privacy\tests\request\approved_contextlist( + $user, + 'core_question', + [$context->id] + ); + + // Delete the data and check it is removed. + $this->setUser(); + provider::delete_data_for_user($approvedcontextlist); + + $this->assertCount(5, $DB->get_records('question')); + + $qrecord = $DB->get_record('question', ['id' => $q1->id]); + $this->assertEquals(0, $qrecord->createdby); + $this->assertEquals(0, $qrecord->modifiedby); + + $qrecord = $DB->get_record('question', ['id' => $q2->id]); + $this->assertEquals(0, $qrecord->createdby); + $this->assertEquals($otheruser->id, $qrecord->modifiedby); + + $qrecord = $DB->get_record('question', ['id' => $q3->id]); + $this->assertEquals($otheruser->id, $qrecord->createdby); + $this->assertEquals(0, $qrecord->modifiedby); + + $qrecord = $DB->get_record('question', ['id' => $q4->id]); + $this->assertEquals($otheruser->id, $qrecord->createdby); + $this->assertEquals($otheruser->id, $qrecord->modifiedby); + + $qrecord = $DB->get_record('question', ['id' => $q5->id]); + $this->assertEquals($user->id, $qrecord->createdby); + $this->assertEquals($user->id, $qrecord->modifiedby); + } + + /** + * Deleting questions should only unset their created and modified user for all questions in a context. + */ + public function test_question_delete_data_for_all_users_in_context_anonymised() { + global $DB; + $this->resetAfterTest(true); + + $user = \core_user::get_user_by_username('admin'); + $otheruser = $this->getDataGenerator()->create_user(); + + $course = $this->getDataGenerator()->create_course(); + $context = \context_course::instance($course->id); + $othercourse = $this->getDataGenerator()->create_course(); + $othercontext = \context_course::instance($othercourse->id); + + // Create a couple of questions. + $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question'); + $cat = $questiongenerator->create_question_category([ + 'contextid' => $context->id, + ]); + $othercat = $questiongenerator->create_question_category([ + 'contextid' => $othercontext->id, + ]); + + // Create questions: + // Q1 - Created by the UUT, Modified by UUT. + // Q2 - Created by the UUT, Modified by the other user. + // Q3 - Created by the other user, Modified by UUT + // Q4 - Created by the other user, Modified by the other user. + // Q5 - Created by the UUT, Modified by the UUT, but in a different context. + $this->setUser($user); + $q1 = $questiongenerator->create_question('shortanswer', null, array('category' => $cat->id)); + $q2 = $questiongenerator->create_question('shortanswer', null, array('category' => $cat->id)); + + $this->setUser($otheruser); + $questiongenerator->update_question($q2); + $q3 = $questiongenerator->create_question('shortanswer', null, array('category' => $cat->id)); + $q4 = $questiongenerator->create_question('shortanswer', null, array('category' => $cat->id)); + + $this->setUser($user); + $questiongenerator->update_question($q3); + $q5 = $questiongenerator->create_question('shortanswer', null, array('category' => $othercat->id)); + + // Delete the data and check it is removed. + $this->setUser(); + provider::delete_data_for_all_users_in_context($context); + + $this->assertCount(5, $DB->get_records('question')); + + $qrecord = $DB->get_record('question', ['id' => $q1->id]); + $this->assertEquals(0, $qrecord->createdby); + $this->assertEquals(0, $qrecord->modifiedby); + + $qrecord = $DB->get_record('question', ['id' => $q2->id]); + $this->assertEquals(0, $qrecord->createdby); + $this->assertEquals(0, $qrecord->modifiedby); + + $qrecord = $DB->get_record('question', ['id' => $q3->id]); + $this->assertEquals(0, $qrecord->createdby); + $this->assertEquals(0, $qrecord->modifiedby); + + $qrecord = $DB->get_record('question', ['id' => $q4->id]); + $this->assertEquals(0, $qrecord->createdby); + $this->assertEquals(0, $qrecord->modifiedby); + + $qrecord = $DB->get_record('question', ['id' => $q5->id]); + $this->assertEquals($user->id, $qrecord->createdby); + $this->assertEquals($user->id, $qrecord->modifiedby); + } +} From 84b5f665ee8244651ee780ca4ab3d7e7c289dcab Mon Sep 17 00:00:00 2001 From: Andrew Nicols Date: Wed, 21 Feb 2018 17:37:53 +0800 Subject: [PATCH 04/10] MDL-61407 mod_quiz: Add initial privacy implementation --- lang/en/question.php | 1 + mod/quiz/classes/privacy/helper.php | 65 +++ .../privacy/legacy_quizaccess_polyfill.php | 66 +++ mod/quiz/classes/privacy/provider.php | 442 ++++++++++++++++++ .../classes/privacy/quizaccess_provider.php | 63 +++ mod/quiz/lang/en/quiz.php | 25 + mod/quiz/lib.php | 66 ++- mod/quiz/locallib.php | 36 +- ...rivacy_legacy_quizaccess_polyfill_test.php | 164 +++++++ mod/quiz/tests/privacy_provider_test.php | 279 +++++++++++ 10 files changed, 1184 insertions(+), 23 deletions(-) create mode 100644 mod/quiz/classes/privacy/helper.php create mode 100644 mod/quiz/classes/privacy/legacy_quizaccess_polyfill.php create mode 100644 mod/quiz/classes/privacy/provider.php create mode 100644 mod/quiz/classes/privacy/quizaccess_provider.php create mode 100644 mod/quiz/tests/privacy_legacy_quizaccess_polyfill_test.php create mode 100644 mod/quiz/tests/privacy_provider_test.php diff --git a/lang/en/question.php b/lang/en/question.php index 4444be6ceaa..cbf0afe6ec1 100644 --- a/lang/en/question.php +++ b/lang/en/question.php @@ -437,6 +437,7 @@ $string['specificfeedback_help'] = 'Feedback that depends on what response the s $string['started'] = 'Started'; $string['state'] = 'State'; $string['step'] = 'Step'; +$string['steps'] = 'Steps'; $string['submissionoutofsequence'] = 'Access out of sequence. Please do not click the back button when working on quiz questions.'; $string['submissionoutofsequencefriendlymessage'] = "You have entered data outside the normal sequence. This can occur if you use your browser's Back or Forward buttons; please don't use these during the test. It can also happen if you click on something while a page is loading. Click Continue to resume."; $string['submit'] = 'Submit'; diff --git a/mod/quiz/classes/privacy/helper.php b/mod/quiz/classes/privacy/helper.php new file mode 100644 index 00000000000..b58e32c2f88 --- /dev/null +++ b/mod/quiz/classes/privacy/helper.php @@ -0,0 +1,65 @@ +. + +/** + * Privacy Subsystem helper for mod_quiz. + * + * @package mod_quiz + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_quiz\privacy; + +use \core_privacy\local\request\writer; +use \core_privacy\local\request\transform; +use \core_privacy\local\request\contextlist; +use \core_privacy\local\request\approved_contextlist; +use \core_privacy\local\request\deletion_criteria; +use \core_privacy\local\metadata\collection; +use \core_privacy\manager; + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->dirroot . '/mod/quiz/lib.php'); +require_once($CFG->dirroot . '/mod/quiz/locallib.php'); + +/** + * Privacy Subsystem implementation for mod_quiz. + * + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class helper { + /** + * Determine the subcontext for the specified quiz attempt. + * + * @param \stdClass $attempt The attempt data retrieved from the database. + * @param \stdClass $user The user record. + * @return \array The calculated subcontext. + */ + public static function get_quiz_attempt_subcontext(\stdClass $attempt, \stdClass $user) { + $subcontext = [ + get_string('attempts', 'mod_quiz'), + ]; + if ($attempt->userid != $user->id) { + $subcontext[] = fullname($user); + } + $subcontext[] = $attempt->attempt; + + return $subcontext; + } +} diff --git a/mod/quiz/classes/privacy/legacy_quizaccess_polyfill.php b/mod/quiz/classes/privacy/legacy_quizaccess_polyfill.php new file mode 100644 index 00000000000..adb373ac832 --- /dev/null +++ b/mod/quiz/classes/privacy/legacy_quizaccess_polyfill.php @@ -0,0 +1,66 @@ +. + +/** + * This file contains the polyfil to allow a plugin to operate with Moodle 3.3 up. + * + * @package mod_quiz + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace mod_quiz\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * The trait used to provide a backwards compatibility for third-party plugins. + * + * @package mod_quiz + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +trait legacy_quizaccess_polyfill { + + /** + * Export all user data for the specified user, for the specified quiz. + * + * @param \quiz $quiz The quiz being exported + * @param \stdClass $user The user to export data for + * @return \stdClass The data to be exported for this access rule. + */ + public static function export_quizaccess_user_data(\quiz $quiz, \stdClass $user) { + return static::_export_quizaccess_user_data($quiz, $user); + } + + /** + * Delete all data for all users in the specified quiz. + * + * @param \quiz $quiz The quiz being deleted + */ + public static function delete_quizaccess_data_for_all_users_in_context(\quiz $quiz) { + static::_delete_quizaccess_data_for_all_users_in_context($quiz); + } + + /** + * Delete all user data for the specified user, in the specified quiz. + * + * @param \quiz $quiz The quiz being deleted + * @param \stdClass $user The user to export data for + */ + public static function delete_quizaccess_data_for_user(\quiz $quiz, \stdClass $user) { + static::_delete_quizaccess_data_for_user($quiz, $user); + } +} diff --git a/mod/quiz/classes/privacy/provider.php b/mod/quiz/classes/privacy/provider.php new file mode 100644 index 00000000000..a678a1088a3 --- /dev/null +++ b/mod/quiz/classes/privacy/provider.php @@ -0,0 +1,442 @@ +. + +/** + * Privacy Subsystem implementation for mod_quiz. + * + * @package mod_quiz + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_quiz\privacy; + +use \core_privacy\local\request\writer; +use \core_privacy\local\request\transform; +use \core_privacy\local\request\contextlist; +use \core_privacy\local\request\approved_contextlist; +use \core_privacy\local\request\deletion_criteria; +use \core_privacy\local\metadata\collection; +use \core_privacy\manager; + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->dirroot . '/mod/quiz/lib.php'); +require_once($CFG->dirroot . '/mod/quiz/locallib.php'); + +/** + * Privacy Subsystem implementation for mod_quiz. + * + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements + // This plugin has data. + \core_privacy\local\metadata\provider, + + // This plugin currently implements the original plugin_provider interface. + \core_privacy\local\request\plugin\provider { + + /** + * Get the list of contexts that contain user information for the specified user. + * + * @param collection $items The collection to add metadata to. + * @return collection The array of metadata + */ + public static function get_metadata(collection $items) { + // The table 'quiz' stores a record for each quiz. + // It does not contain user personal data, but data is returned from it for contextual requirements. + + // The table 'quiz_attempts' stores a record of each quiz attempt. + // It contains a userid which links to the user making the attempt and contains information about that attempt. + $items->add_database_table('quiz_attempts', [ + 'attempt' => 'privacy:metadata:quiz_attempts:attempt', + 'currentpage' => 'privacy:metadata:quiz_attempts:currentpage', + 'preview' => 'privacy:metadata:quiz_attempts:preview', + 'state' => 'privacy:metadata:quiz_attempts:state', + 'timestart' => 'privacy:metadata:quiz_attempts:timestart', + 'timefinish' => 'privacy:metadata:quiz_attempts:timefinish', + 'timemodified' => 'privacy:metadata:quiz_attempts:timemodified', + 'timemodifiedoffline' => 'privacy:metadata:quiz_attempts:timemodifiedoffline', + 'timecheckstate' => 'privacy:metadata:quiz_attempts:timecheckstate', + 'sumgrades' => 'privacy:metadata:quiz_attempts:sumgrades', + ], 'privacy:metadata:quiz_attempts'); + + // The table 'quiz_feedback' contains the feedback responses which will be shown to users depending upon the + // grade they achieve in the quiz. + // It does not identify the user who wrote the feedback item so cannot be returned directly and is not + // described, but relevant feedback items will be included with the quiz export for a user who has a grade. + + // The table 'quiz_grades' contains the current grade for each quiz/user combination. + $items->add_database_table('quiz_grades', [ + 'quiz' => 'privacy:metadata:quiz_grades:quiz', + 'userid' => 'privacy:metadata:quiz_grades:userid', + 'grade' => 'privacy:metadata:quiz_grades:grade', + 'timemodified' => 'privacy:metadata:quiz_grades:timemodified', + ], 'privacy:metadata:quiz_grades'); + + // The table 'quiz_overrides' contains any user or group overrides for users. + // It should be included where data exists for a user. + $items->add_database_table('quiz_overrides', [ + 'quiz' => 'privacy:metadata:quiz_overrides:quiz', + 'userid' => 'privacy:metadata:quiz_overrides:userid', + 'timeopen' => 'privacy:metadata:quiz_overrides:timeopen', + 'timeclose' => 'privacy:metadata:quiz_overrides:timeclose', + 'timelimit' => 'privacy:metadata:quiz_overrides:timelimit', + ], 'privacy:metadata:quiz_overrides'); + + // These define the structure of the quiz. + + // The table 'quiz_sections' contains data about the structure of a quiz. + // It does not contain any user identifying data and does not need a mapping. + + // The table 'quiz_slots' contains data about the structure of a quiz. + // It does not contain any user identifying data and does not need a mapping. + + // The table 'quiz_reports' does not contain any user identifying data and does not need a mapping. + + // The table 'quiz_statistics' contains abstract statistics about question usage and cannot be mapped to any + // specific user. + // It does not contain any user identifying data and does not need a mapping. + + // The quiz links to the 'core_question' subsystem for all question functionality. + $items->add_subsystem_link('core_question', [], 'privacy:metadata:core_question'); + + // The quiz has two subplugins.. + $items->add_plugintype_link('quiz', [], 'privacy:metadata:quiz'); + $items->add_plugintype_link('quizaccess', [], 'privacy:metadata:quizaccess'); + + // Although the quiz supports the core_completion API and defines custom completion items, these will be + // noted by the manager as all activity modules are capable of supporting this functionality. + + return $items; + } + + /** + * Get the list of contexts where the specified user has attempted a quiz, or been involved with manual marking + * and/or grading of a quiz. + * + * @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($userid) { + // Get the SQL used to link indirect question usages for the user. + // This includes where a user is the manual marker on a question attempt. + $qubaid = \core_question\privacy\provider::get_related_question_usages_for_user('rel', 'mod_quiz', 'qa.uniqueid', $userid); + + // Select the context of any quiz attempt where a user has an attempt, plus the related usages. + $sql = "SELECT c.id + FROM {context} c + JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = :contextlevel + JOIN {modules} m ON m.id = cm.module AND m.name = :modname + JOIN {quiz} q ON q.id = cm.instance + JOIN {quiz_attempts} qa ON qa.quiz = q.id + LEFT JOIN {quiz_overrides} qo ON qo.quiz = q.id AND qo.userid = :qouserid + " . $qubaid->from . " + WHERE ( + qa.userid = :qauserid OR + " . $qubaid->where() . " + ) AND qa.preview = 0 + "; + + $params = array_merge( + [ + 'contextlevel' => CONTEXT_MODULE, + 'modname' => 'quiz', + 'qauserid' => $userid, + 'qouserid' => $userid, + ], + $qubaid->from_where_params() + ); + + $resultset = new contextlist(); + $resultset->add_from_sql($sql, $params); + + return $resultset; + } + + /** + * Delete all data for all users in the specified context. + * + * @param approved_contextlist $contextlist The approved contexts to export information for. + */ + public static function export_user_data(approved_contextlist $contextlist) { + global $DB; + + if (!count($contextlist)) { + return; + } + + $user = $contextlist->get_user(); + $userid = $user->id; + list($contextsql, $contextparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED); + + $sql = "SELECT + q.*, + qg.id AS hasgrade, + qg.grade AS bestgrade, + qg.timemodified AS grademodified, + qo.id AS hasoverride, + qo.timeopen AS override_timeopen, + qo.timeclose AS override_timeclose, + qo.timelimit AS override_timelimit, + c.id AS contextid, + cm.id AS cmid + FROM {context} c + INNER JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = :contextlevel + INNER JOIN {modules} m ON m.id = cm.module AND m.name = 'quiz' + INNER JOIN {quiz} q ON q.id = cm.instance + LEFT JOIN {quiz_overrides} qo ON qo.quiz = q.id AND qo.userid = :qouserid + LEFT JOIN {quiz_grades} qg ON qg.quiz = q.id AND qg.userid = :qguserid + WHERE c.id {$contextsql}"; + + $params = [ + 'contextlevel' => CONTEXT_MODULE, + 'qguserid' => $userid, + 'qouserid' => $userid, + ]; + $params += $contextparams; + + // Fetch the individual quizzes. + $quizzes = $DB->get_recordset_sql($sql, $params); + foreach ($quizzes as $quiz) { + list($course, $cm) = get_course_and_cm_from_cmid($quiz->cmid, 'quiz'); + $quizobj = new \quiz($quiz, $cm, $course); + $context = $quizobj->get_context(); + + $quizdata = \core_privacy\local\request\helper::get_context_data($context, $contextlist->get_user()); + \core_privacy\local\request\helper::export_context_files($context, $contextlist->get_user()); + + if (!empty($quizdata->timeopen)) { + $quizdata->timeopen = transform::datetime($quiz->timeopen); + } + if (!empty($quizdata->timeclose)) { + $quizdata->timeclose = transform::datetime($quiz->timeclose); + } + if (!empty($quizdata->timelimit)) { + $quizdata->timelimit = $quiz->timelimit; + } + + if (!empty($quiz->hasoverride)) { + $quizdata->override = (object) []; + + if (!empty($quizdata->override_override_timeopen)) { + $quizdata->override->timeopen = transform::datetime($quiz->override_timeopen); + } + if (!empty($quizdata->override_timeclose)) { + $quizdata->override->timeclose = transform::datetime($quiz->override_timeclose); + } + if (!empty($quizdata->override_timelimit)) { + $quizdata->override->timelimit = $quiz->override_timelimit; + } + } + + $quizdata->accessdata = (object) []; + + $components = \core_component::get_plugin_list('quizaccess'); + $exportparams = [ + $quizobj, + $user, + ]; + foreach (array_keys($components) as $component) { + $classname = manager::get_provider_classname_for_component("quizaccess_$component"); + if (class_exists($classname) && is_subclass_of($classname, quizaccess_provider::class)) { + $result = component_class_callback($classname, 'export_quizaccess_user_data', $exportparams); + if (count((array) $result)) { + $quizdata->accessdata->$component = $result; + } + } + } + + if (empty((array) $quizdata->accessdata)) { + unset($quizdata->accessdata); + } + + writer::with_context($context) + ->export_data([], $quizdata); + } + $quizzes->close(); + + // Store all quiz attempt data. + static::export_quiz_attempts($contextlist); + } + + /** + * 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) { + $cm = get_coursemodule_from_id('quiz', $context->instanceid); + if (!$cm) { + // Only quiz module will be handled. + return; + } + $quiz = \quiz::create($cm->instance); + + // Handle the 'quizaccess' subplugin. + manager::plugintype_class_callback( + 'quizaccess', + quizaccess_provider::class, + 'delete_subplugin_data_for_all_users_in_context', + [$quiz] + ); + + // Delete all overrides - do not log. + quiz_delete_all_overrides($quiz, false); + + // This will delete all question attempts, quiz attempts, and quiz grades for this quiz. + quiz_delete_all_attempts($quiz); + } + + /** + * 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; + + foreach ($contextlist as $context) { + $cm = get_coursemodule_from_id('quiz', $context->instanceid); + $quiz = \quiz::create($cm->instance); + $user = $contextlist->get_user(); + + // Handle the 'quizaccess' quizaccess. + manager::plugintype_class_callback( + 'quizaccess', + quizaccess_provider::class, + 'delete_quizaccess_data_for_user', + [$quiz, $user] + ); + + $overrides = $DB->get_records('quiz_overrides' , [ + 'quiz' => $quiz->get_quizid(), + 'userid' => $user->id, + ]); + + foreach ($overrides as $override) { + quiz_delete_override($quiz, $override->id, false); + } + + // This will delete all question attempts, quiz attempts, and quiz grades for this quiz. + quiz_delete_user_attempts($quiz, $user); + } + } + + /** + * Store all quiz attempts for the contextlist. + * + * @param approved_contextlist $contextlist + */ + protected static function export_quiz_attempts(approved_contextlist $contextlist) { + global $DB; + + $userid = $contextlist->get_user()->id; + list($contextsql, $contextparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED); + $qubaid = \core_question\privacy\provider::get_related_question_usages_for_user('rel', 'mod_quiz', 'qa.uniqueid', $userid); + + $sql = "SELECT + c.id AS contextid, + cm.id AS cmid, + qa.* + FROM {context} c + JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = :contextlevel + JOIN {modules} m ON m.id = cm.module AND m.name = :modname + JOIN {quiz} q ON q.id = cm.instance + JOIN {quiz_attempts} qa ON qa.quiz = q.id + " . $qubaid->from. " + WHERE ( + qa.userid = :qauserid OR + " . $qubaid->where() . " + ) AND qa.preview = 0 + "; + + $params = array_merge( + [ + 'contextlevel' => CONTEXT_MODULE, + 'modname' => 'quiz', + 'qauserid' => $userid, + ], + $qubaid->from_where_params() + ); + + $attempts = $DB->get_recordset_sql($sql, $params); + foreach ($attempts as $attempt) { + $quiz = $DB->get_record('quiz', ['id' => $attempt->quiz]); + $context = \context_module::instance($attempt->cmid); + $attemptsubcontext = helper::get_quiz_attempt_subcontext($attempt, $contextlist->get_user()); + $options = quiz_get_review_options($quiz, $attempt, $context); + + if ($attempt->userid == $userid) { + // This attempt was made by the user. + // They 'own' all data on it. + // Store the question usage data. + \core_question\privacy\provider::export_question_usage($userid, + $context, + $attemptsubcontext, + $attempt->uniqueid, + $options, + true + ); + + // Store the quiz attempt data. + $data = (object) [ + 'state' => \quiz_attempt::state_name($attempt->state), + ]; + + if (!empty($attempt->timestart)) { + $data->timestart = transform::datetime($attempt->timestart); + } + if (!empty($attempt->timefinish)) { + $data->timefinish = transform::datetime($attempt->timefinish); + } + if (!empty($attempt->timemodified)) { + $data->timemodified = transform::datetime($attempt->timemodified); + } + if (!empty($attempt->timemodifiedoffline)) { + $data->timemodifiedoffline = transform::datetime($attempt->timemodifiedoffline); + } + if (!empty($attempt->timecheckstate)) { + $data->timecheckstate = transform::datetime($attempt->timecheckstate); + } + + if ($options->marks == \question_display_options::MARK_AND_MAX) { + $grade = quiz_rescale_grade($attempt->sumgrades, $quiz, false); + $data->grade = (object) [ + 'grade' => quiz_format_grade($quiz, $grade), + 'feedback' => quiz_feedback_for_grade($grade, $quiz, $context), + ]; + } + + writer::with_context($context) + ->export_data($attemptsubcontext, $data); + } else { + // This attempt was made by another user. + // The current user may have marked part of the quiz attempt. + \core_question\privacy\provider::export_question_usage( + $userid, + $context, + $attemptsubcontext, + $attempt->uniqueid, + $options, + false + ); + } + } + $attempts->close(); + } +} diff --git a/mod/quiz/classes/privacy/quizaccess_provider.php b/mod/quiz/classes/privacy/quizaccess_provider.php new file mode 100644 index 00000000000..2cc6cbe5ed8 --- /dev/null +++ b/mod/quiz/classes/privacy/quizaccess_provider.php @@ -0,0 +1,63 @@ +. + +/** + * The quizaccess_provider interface provides the expected interface for all 'quizaccess' quizaccesss. + * + * @package mod_quiz + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace mod_quiz\privacy; + +defined('MOODLE_INTERNAL') || die(); + +use \core_privacy\local\request\contextlist; +use \core_privacy\local\request\approved_contextlist; + +/** + * The quizaccess_provider interface provides the expected interface for all 'quizaccess' quizaccesss. + * + * @package mod_quiz + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +interface quizaccess_provider extends \core_privacy\local\request\plugin\subplugin_provider { + + /** + * Export all user data for the specified user, for the specified quiz. + * + * @param \quiz $quiz The quiz being exported + * @param \stdClass $user The user to export data for + * @return \stdClass The data to be exported for this access rule. + */ + public static function export_quizaccess_user_data(\quiz $quiz, \stdClass $user); + + /** + * Delete all data for all users in the specified quiz. + * + * @param \quiz $quiz The quiz being deleted + */ + public static function delete_quizaccess_data_for_all_users_in_context(\quiz $quiz); + + /** + * Delete all user data for the specified user, in the specified quiz. + * + * @param \quiz $quiz The quiz being deleted + * @param \stdClass $user The user to export data for + */ + public static function delete_quizaccess_data_for_user(\quiz $quiz, \stdClass $user); +} diff --git a/mod/quiz/lang/en/quiz.php b/mod/quiz/lang/en/quiz.php index 65a488c051d..26d59977643 100644 --- a/mod/quiz/lang/en/quiz.php +++ b/mod/quiz/lang/en/quiz.php @@ -626,6 +626,31 @@ $string['previewquestion'] = 'Preview question'; $string['previewquiz'] = 'Preview {$a}'; $string['previewquiznow'] = 'Preview quiz now'; $string['previous'] = 'Previous state'; +$string['privacy:metadata:core_question'] = 'The quiz activity stores question usage information in the core_question subsystem.'; +$string['privacy:metadata:quiz'] = 'The quiz activity makes use of quiz reports.'; +$string['privacy:metadata:quiz_attempts'] = 'Details about each attempt on a quiz.'; +$string['privacy:metadata:quiz_attempts:attempt'] = 'The attempt number.'; +$string['privacy:metadata:quiz_attempts:currentpage'] = 'The current page that the user is on.'; +$string['privacy:metadata:quiz_attempts:preview'] = 'Whether this is a preview of the quiz.'; +$string['privacy:metadata:quiz_attempts:state'] = 'The current state of the attempt.'; +$string['privacy:metadata:quiz_attempts:sumgrades'] = 'The sum of grades in the attempt.'; +$string['privacy:metadata:quiz_attempts:timecheckstate'] = 'The time that the state was checked.'; +$string['privacy:metadata:quiz_attempts:timefinish'] = 'The time that the attempt was completed.'; +$string['privacy:metadata:quiz_attempts:timemodified'] = 'The time that the attempt was updated.'; +$string['privacy:metadata:quiz_attempts:timemodifiedoffline'] = 'The time that the attempt was updated via an offline update.'; +$string['privacy:metadata:quiz_attempts:timestart'] = 'The time that the attempt was started.'; +$string['privacy:metadata:quiz_grades'] = 'Details about the overall grade for this quiz.'; +$string['privacy:metadata:quiz_grades:grade'] = 'The overall grade for this quiz.'; +$string['privacy:metadata:quiz_grades:quiz'] = 'The quiz that was graded.'; +$string['privacy:metadata:quiz_grades:timemodified'] = 'The time that the grade was modified.'; +$string['privacy:metadata:quiz_grades:userid'] = 'The user who was graded.'; +$string['privacy:metadata:quiz_overrides'] = 'Details about overrides for this quiz.'; +$string['privacy:metadata:quiz_overrides:quiz'] = 'The quiz with override information.'; +$string['privacy:metadata:quiz_overrides:timeclose'] = 'The new close time for the quiz.'; +$string['privacy:metadata:quiz_overrides:timelimit'] = 'The new time limit for the quiz.'; +$string['privacy:metadata:quiz_overrides:timeopen'] = 'The new open time for the quiz.'; +$string['privacy:metadata:quiz_overrides:userid'] = 'The user being overridden.'; +$string['privacy:metadata:quizaccess'] = 'The quiz activity makes use of quiz access rules.'; $string['publish'] = 'Publish'; $string['publishedit'] = 'You must have permission in the publishing course to add or edit questions in this category'; $string['qname'] = 'name'; diff --git a/mod/quiz/lib.php b/mod/quiz/lib.php index 6f383ff10d3..67600ab95b6 100644 --- a/mod/quiz/lib.php +++ b/mod/quiz/lib.php @@ -212,9 +212,10 @@ function quiz_delete_instance($id) { * * @param object $quiz The quiz object. * @param int $overrideid The id of the override being deleted + * @param bool $log Whether to trigger logs. * @return bool true on success */ -function quiz_delete_override($quiz, $overrideid) { +function quiz_delete_override($quiz, $overrideid, $log = true) { global $DB; if (!isset($quiz->cmid)) { @@ -242,26 +243,28 @@ function quiz_delete_override($quiz, $overrideid) { $DB->delete_records('quiz_overrides', array('id' => $overrideid)); - // Set the common parameters for one of the events we will be triggering. - $params = array( - 'objectid' => $override->id, - 'context' => context_module::instance($quiz->cmid), - 'other' => array( - 'quizid' => $override->quiz - ) - ); - // Determine which override deleted event to fire. - if (!empty($override->userid)) { - $params['relateduserid'] = $override->userid; - $event = \mod_quiz\event\user_override_deleted::create($params); - } else { - $params['other']['groupid'] = $override->groupid; - $event = \mod_quiz\event\group_override_deleted::create($params); - } + if ($log) { + // Set the common parameters for one of the events we will be triggering. + $params = array( + 'objectid' => $override->id, + 'context' => context_module::instance($quiz->cmid), + 'other' => array( + 'quizid' => $override->quiz + ) + ); + // Determine which override deleted event to fire. + if (!empty($override->userid)) { + $params['relateduserid'] = $override->userid; + $event = \mod_quiz\event\user_override_deleted::create($params); + } else { + $params['other']['groupid'] = $override->groupid; + $event = \mod_quiz\event\group_override_deleted::create($params); + } - // Trigger the override deleted event. - $event->add_record_snapshot('quiz_overrides', $override); - $event->trigger(); + // Trigger the override deleted event. + $event->add_record_snapshot('quiz_overrides', $override); + $event->trigger(); + } return true; } @@ -270,13 +273,14 @@ function quiz_delete_override($quiz, $overrideid) { * Deletes all quiz overrides from the database and clears any corresponding calendar events * * @param object $quiz The quiz object. + * @param bool $log Whether to trigger logs. */ -function quiz_delete_all_overrides($quiz) { +function quiz_delete_all_overrides($quiz, $log = true) { global $DB; $overrides = $DB->get_records('quiz_overrides', array('quiz' => $quiz->id), 'id'); foreach ($overrides as $override) { - quiz_delete_override($quiz, $override->id); + quiz_delete_override($quiz, $override->id, $log); } } @@ -403,6 +407,24 @@ function quiz_delete_all_attempts($quiz) { $DB->delete_records('quiz_grades', array('quiz' => $quiz->id)); } +/** + * Delete all the attempts belonging to a user in a particular quiz. + * + * @param object $quiz The quiz object. + * @param object $user The user object. + */ +function quiz_delete_user_attempts($quiz, $user) { + global $CFG, $DB; + require_once($CFG->dirroot . '/mod/quiz/locallib.php'); + question_engine::delete_questions_usage_by_activities(new qubaids_for_quiz_user($quiz->get_quizid(), $user->id)); + $params = [ + 'quiz' => $quiz->get_quizid(), + 'userid' => $user->id, + ]; + $DB->delete_records('quiz_attempts', $params); + $DB->delete_records('quiz_grades', $params); +} + /** * Get the best current grade for a particular user in a quiz. * diff --git a/mod/quiz/locallib.php b/mod/quiz/locallib.php index 4072b84c120..bdf80037aff 100644 --- a/mod/quiz/locallib.php +++ b/mod/quiz/locallib.php @@ -1970,7 +1970,6 @@ class mod_quiz_display_options extends question_display_options { } } - /** * A {@link qubaid_condition} for finding all the question usages belonging to * a particular quiz. @@ -1996,6 +1995,41 @@ class qubaids_for_quiz extends qubaid_join { } } +/** + * A {@link qubaid_condition} for finding all the question usages belonging to a particular user and quiz combination. + * + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class qubaids_for_quiz_user extends qubaid_join { + /** + * Constructor for this qubaid. + * + * @param int $quizid The quiz to search. + * @param int $userid The user to filter on + * @param bool $includepreviews Whether to include preview attempts + * @param bool $onlyfinished Whether to only include finished attempts or not + */ + public function __construct($quizid, $userid, $includepreviews = true, $onlyfinished = false) { + $where = 'quiza.quiz = :quizaquiz AND quiza.userid = :quizauserid'; + $params = [ + 'quizaquiz' => $quizid, + 'quizauserid' => $userid, + ]; + + if (!$includepreviews) { + $where .= ' AND preview = 0'; + } + + if ($onlyfinished) { + $where .= ' AND state = :statefinished'; + $params['statefinished'] = quiz_attempt::FINISHED; + } + + parent::__construct('{quiz_attempts} quiza', 'quiza.uniqueid', $where, $params); + } +} + /** * Creates a textual representation of a question for display. * diff --git a/mod/quiz/tests/privacy_legacy_quizaccess_polyfill_test.php b/mod/quiz/tests/privacy_legacy_quizaccess_polyfill_test.php new file mode 100644 index 00000000000..ac272cf10a5 --- /dev/null +++ b/mod/quiz/tests/privacy_legacy_quizaccess_polyfill_test.php @@ -0,0 +1,164 @@ +. + +/** + * Unit tests for the privacy legacy polyfill for quiz access rules. + * + * @package mod_quiz + * @category test + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->dirroot . '/mod/quiz/attemptlib.php'); + +/** + * Unit tests for the privacy legacy polyfill for quiz access rules. + * + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class core_privacy_legacy_quizaccess_polyfill_test extends advanced_testcase { + /** + * Test that the core_quizaccess\privacy\legacy_polyfill works and that the static _export_quizaccess_user_data can + * be called. + */ + public function test_export_quizaccess_user_data() { + $quiz = $this->createMock(quiz::class); + $user = (object) []; + $returnvalue = (object) []; + + $mock = $this->createMock(test_privacy_legacy_quizaccess_polyfill_mock_wrapper::class); + $mock->expects($this->once()) + ->method('get_return_value') + ->with('_export_quizaccess_user_data', [$quiz, $user]) + ->willReturn($returnvalue); + + test_privacy_legacy_quizaccess_polyfill_provider::$mock = $mock; + $result = test_privacy_legacy_quizaccess_polyfill_provider::export_quizaccess_user_data($quiz, $user); + $this->assertSame($returnvalue, $result); + } + + /** + * Test the _delete_quizaccess_for_context shim. + */ + public function test_delete_quizaccess_for_context() { + $context = context_system::instance(); + + $quiz = $this->createMock(quiz::class); + + $mock = $this->createMock(test_privacy_legacy_quizaccess_polyfill_mock_wrapper::class); + $mock->expects($this->once()) + ->method('get_return_value') + ->with('_delete_quizaccess_data_for_all_users_in_context', [$quiz]); + + test_privacy_legacy_quizaccess_polyfill_provider::$mock = $mock; + test_privacy_legacy_quizaccess_polyfill_provider::delete_quizaccess_data_for_all_users_in_context($quiz); + } + + /** + * Test the _delete_quizaccess_for_context shim. + */ + public function test_delete_quizaccess_for_user() { + $context = context_system::instance(); + + $quiz = $this->createMock(quiz::class); + $user = (object) []; + + $mock = $this->createMock(test_privacy_legacy_quizaccess_polyfill_mock_wrapper::class); + $mock->expects($this->once()) + ->method('get_return_value') + ->with('_delete_quizaccess_data_for_user', [$quiz, $user]); + + test_privacy_legacy_quizaccess_polyfill_provider::$mock = $mock; + test_privacy_legacy_quizaccess_polyfill_provider::delete_quizaccess_data_for_user($quiz, $user); + } +} + +/** + * Legacy polyfill test class for the quizaccess_provider. + * + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class test_privacy_legacy_quizaccess_polyfill_provider implements + \core_privacy\local\metadata\provider, + \mod_quiz\privacy\quizaccess_provider { + + use \mod_quiz\privacy\legacy_quizaccess_polyfill; + use \core_privacy\local\legacy_polyfill; + + /** + * @var test_privacy_legacy_quizaccess_polyfill_provider $mock. + */ + public static $mock = null; + + /** + * Export all user data for the quizaccess plugin. + * + * @param \quiz $quiz + * @param \stdClass $user + */ + protected static function _export_quizaccess_user_data($quiz, $user) { + return static::$mock->get_return_value(__FUNCTION__, func_get_args()); + } + + /** + * Deletes all user data for the given context. + * + * @param \quiz $quiz + */ + protected static function _delete_quizaccess_data_for_all_users_in_context($quiz) { + static::$mock->get_return_value(__FUNCTION__, func_get_args()); + } + + /** + * Delete personal data for the given user and context. + * + * @param \quiz $quiz The quiz being deleted + * @param \stdClass $user The user to export data for + */ + protected static function _delete_quizaccess_data_for_user($quiz, $user) { + static::$mock->get_return_value(__FUNCTION__, func_get_args()); + } + + /** + * Returns metadata about this plugin. + * + * @param \core_privacy\local\metadata\collection $collection The initialised collection to add items to. + * @return \core_privacy\local\metadata\collection A listing of user data stored through this system. + */ + protected static function _get_metadata(\core_privacy\local\metadata\collection $collection) { + return $collection; + } +} + +/** + * Called inside the polyfill methods in the test polyfill provider, allowing us to ensure these are called with correct params. + * + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class test_privacy_legacy_quizaccess_polyfill_mock_wrapper { + /** + * Get the return value for the specified item. + */ + public function get_return_value() { + } +} diff --git a/mod/quiz/tests/privacy_provider_test.php b/mod/quiz/tests/privacy_provider_test.php new file mode 100644 index 00000000000..13a43eb2bab --- /dev/null +++ b/mod/quiz/tests/privacy_provider_test.php @@ -0,0 +1,279 @@ +. + +/** + * Privacy provider tests. + * + * @package mod_quiz + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +use core_privacy\local\metadata\collection; +use core_privacy\local\request\deletion_criteria; +use core_privacy\local\request\writer; +use mod_quiz\privacy\provider; +use mod_quiz\privacy\helper; + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->dirroot . '/question/tests/privacy_helper.php'); + +/** + * Privacy provider tests class. + * + * @package mod_quiz + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class mod_quiz_privacy_provider_testcase extends \core_privacy\tests\provider_testcase { + + use core_question_privacy_helper; + + /** + * Test that a user who has no data gets no contexts + */ + public function test_get_contexts_for_userid_no_data() { + global $USER; + $this->resetAfterTest(); + $this->setAdminUser(); + + $contextlist = provider::get_contexts_for_userid($USER->id); + $this->assertEmpty($contextlist); + } + + /** + * The export function should handle an empty contextlist properly. + */ + public function test_export_user_data_no_data() { + global $USER; + $this->resetAfterTest(); + $this->setAdminUser(); + + $approvedcontextlist = new \core_privacy\tests\request\approved_contextlist( + \core_user::get_user($USER->id), + 'mod_quiz', + [] + ); + + provider::export_user_data($approvedcontextlist); + $this->assertDebuggingNotCalled(); + + // No data should have been exported. + $writer = \core_privacy\local\request\writer::with_context(\context_system::instance()); + $this->assertFalse($writer->has_any_data_in_any_context()); + } + + /** + * The delete function should handle an empty contextlist properly. + */ + public function test_delete_data_for_user_no_data() { + global $USER; + $this->resetAfterTest(); + $this->setAdminUser(); + + $approvedcontextlist = new \core_privacy\tests\request\approved_contextlist( + \core_user::get_user($USER->id), + 'mod_quiz', + [] + ); + + provider::delete_data_for_user($approvedcontextlist); + $this->assertDebuggingNotCalled(); + } + + /** + * Export + Delete quiz data for a user who has made a single attempt. + */ + public function test_user_with_data() { + global $DB; + $this->resetAfterTest(true); + + // Make a quiz. + $course = $this->getDataGenerator()->create_course(); + $user = $this->getDataGenerator()->create_user(); + $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz'); + + $quiz = $quizgenerator->create_instance([ + 'course' => $course->id, + 'questionsperpage' => 0, + 'grade' => 100.0, + 'sumgrades' => 2, + ]); + + // Create a couple of questions. + $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question'); + $cat = $questiongenerator->create_question_category(); + + $saq = $questiongenerator->create_question('shortanswer', null, array('category' => $cat->id)); + quiz_add_quiz_question($saq->id, $quiz); + $numq = $questiongenerator->create_question('numerical', null, array('category' => $cat->id)); + quiz_add_quiz_question($numq->id, $quiz); + + // Run as the user and make an attempt on the quiz. + $this->setUser($user); + $starttime = time(); + $quizobj = quiz::create($quiz->id, $user->id); + $context = $quizobj->get_context(); + + $quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context()); + $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour); + + // Start the attempt. + $attempt = quiz_create_attempt($quizobj, 1, false, $starttime, false, $user->id); + quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $starttime); + quiz_attempt_save_started($quizobj, $quba, $attempt); + + // Answer the questions. + $attemptobj = quiz_attempt::create($attempt->id); + + $tosubmit = [ + 1 => ['answer' => 'frog'], + 2 => ['answer' => '3.14'], + ]; + + $attemptobj->process_submitted_actions($starttime, false, $tosubmit); + + // Finish the attempt. + $attemptobj = quiz_attempt::create($attempt->id); + $this->assertTrue($attemptobj->has_response_to_at_least_one_graded_question()); + $attemptobj->process_finish($starttime, false); + + // Fetch the contexts -only one context should be returned. + $this->setUser(); + $contextlist = provider::get_contexts_for_userid($user->id); + $this->assertCount(1, $contextlist); + $this->assertEquals($context, $contextlist->current()); + + // Perform the export and check the data. + $this->setUser($user); + $approvedcontextlist = new \core_privacy\tests\request\approved_contextlist( + \core_user::get_user($user->id), + 'mod_quiz', + $contextlist->get_contextids() + ); + provider::export_user_data($approvedcontextlist); + + // Ensure that the quiz data was exported correctly. + $writer = writer::with_context($context); + $this->assertTrue($writer->has_any_data()); + + $quizdata = $writer->get_data([]); + $this->assertEquals($quizobj->get_quiz_name(), $quizdata->name); + + // Every module has an intro. + $this->assertTrue(isset($quizdata->intro)); + + // Fetch the attempt data. + $attemptsubcontext = [ + get_string('attempts', 'mod_quiz'), + $attempt->attempt, + ]; + $attemptdata = writer::with_context($context)->get_data($attemptsubcontext); + + $attempt = $attemptobj->get_attempt(); + $this->assertTrue(isset($attemptdata->state)); + $this->assertEquals(\quiz_attempt::state_name($attemptobj->get_state()), $attemptdata->state); + $this->assertTrue(isset($attemptdata->timestart)); + $this->assertTrue(isset($attemptdata->timefinish)); + $this->assertTrue(isset($attemptdata->timemodified)); + $this->assertFalse(isset($attemptdata->timemodifiedoffline)); + $this->assertFalse(isset($attemptdata->timecheckstate)); + + $this->assertTrue(isset($attemptdata->grade)); + $this->assertEquals(100.00, $attemptdata->grade->grade); + + // Check that the exported question attempts are correct. + $attemptsubcontext = helper::get_quiz_attempt_subcontext($attemptobj->get_attempt(), $user); + $this->assert_question_attempt_exported( + $context, + $attemptsubcontext, + \question_engine::load_questions_usage_by_activity($attemptobj->get_uniqueid()), + quiz_get_review_options($quiz, $attemptobj->get_attempt(), $context), + $user + ); + + // Delete the data and check it is removed. + $this->setUser(); + provider::delete_data_for_user($approvedcontextlist); + $this->expectException(\dml_missing_record_exception::class); + \quiz_attempt::create($attemptobj->get_quizid()); + } + + /** + * Export + Delete quiz data for a user who has made a single attempt. + */ + public function test_user_with_preview() { + global $DB; + $this->resetAfterTest(true); + + // Make a quiz. + $course = $this->getDataGenerator()->create_course(); + $user = $this->getDataGenerator()->create_user(); + $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz'); + + $quiz = $quizgenerator->create_instance([ + 'course' => $course->id, + 'questionsperpage' => 0, + 'grade' => 100.0, + 'sumgrades' => 2, + ]); + + // Create a couple of questions. + $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question'); + $cat = $questiongenerator->create_question_category(); + + $saq = $questiongenerator->create_question('shortanswer', null, array('category' => $cat->id)); + quiz_add_quiz_question($saq->id, $quiz); + $numq = $questiongenerator->create_question('numerical', null, array('category' => $cat->id)); + quiz_add_quiz_question($numq->id, $quiz); + + // Run as the user and make an attempt on the quiz. + $this->setUser($user); + $starttime = time(); + $quizobj = quiz::create($quiz->id, $user->id); + $context = $quizobj->get_context(); + + $quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context()); + $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour); + + // Start the attempt. + $attempt = quiz_create_attempt($quizobj, 1, false, $starttime, true, $user->id); + quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $starttime); + quiz_attempt_save_started($quizobj, $quba, $attempt); + + // Answer the questions. + $attemptobj = quiz_attempt::create($attempt->id); + + $tosubmit = [ + 1 => ['answer' => 'frog'], + 2 => ['answer' => '3.14'], + ]; + + $attemptobj->process_submitted_actions($starttime, false, $tosubmit); + + // Finish the attempt. + $attemptobj = quiz_attempt::create($attempt->id); + $this->assertTrue($attemptobj->has_response_to_at_least_one_graded_question()); + $attemptobj->process_finish($starttime, false); + + // Fetch the contexts - no context should be returned. + $this->setUser(); + $contextlist = provider::get_contexts_for_userid($user->id); + $this->assertCount(0, $contextlist); + } +} From a6a8ecc369ca82281f9d1e169c49aae7a7b654c1 Mon Sep 17 00:00:00 2001 From: Andrew Nicols Date: Sat, 7 Apr 2018 14:02:38 +0800 Subject: [PATCH 05/10] MDL-61407 question: Add null_writer for all behaviours Behaviours do not store any personal data. Any contrib behaviour which does store personal data should utilise the standard export interfaces as this subsystem will not call them directly. --- .../adaptive/classes/privacy/provider.php | 46 +++++++++++++++++++ .../adaptive/lang/en/qbehaviour_adaptive.php | 1 + .../classes/privacy/provider.php | 46 +++++++++++++++++++ .../lang/en/qbehaviour_adaptivenopenalty.php | 1 + .../deferredcbm/classes/privacy/provider.php | 46 +++++++++++++++++++ .../lang/en/qbehaviour_deferredcbm.php | 1 + .../classes/privacy/provider.php | 46 +++++++++++++++++++ .../lang/en/qbehaviour_deferredfeedback.php | 1 + .../immediatecbm/classes/privacy/provider.php | 46 +++++++++++++++++++ .../lang/en/qbehaviour_immediatecbm.php | 1 + .../classes/privacy/provider.php | 46 +++++++++++++++++++ .../lang/en/qbehaviour_immediatefeedback.php | 1 + .../classes/privacy/provider.php | 46 +++++++++++++++++++ .../lang/en/qbehaviour_informationitem.php | 3 +- .../interactive/classes/privacy/provider.php | 46 +++++++++++++++++++ .../lang/en/qbehaviour_interactive.php | 1 + .../classes/privacy/provider.php | 46 +++++++++++++++++++ .../en/qbehaviour_interactivecountback.php | 1 + .../manualgraded/classes/privacy/provider.php | 46 +++++++++++++++++++ .../lang/en/qbehaviour_manualgraded.php | 1 + .../missing/classes/privacy/provider.php | 46 +++++++++++++++++++ .../missing/lang/en/qbehaviour_missing.php | 1 + 22 files changed, 518 insertions(+), 1 deletion(-) create mode 100644 question/behaviour/adaptive/classes/privacy/provider.php create mode 100644 question/behaviour/adaptivenopenalty/classes/privacy/provider.php create mode 100644 question/behaviour/deferredcbm/classes/privacy/provider.php create mode 100644 question/behaviour/deferredfeedback/classes/privacy/provider.php create mode 100644 question/behaviour/immediatecbm/classes/privacy/provider.php create mode 100644 question/behaviour/immediatefeedback/classes/privacy/provider.php create mode 100644 question/behaviour/informationitem/classes/privacy/provider.php create mode 100644 question/behaviour/interactive/classes/privacy/provider.php create mode 100644 question/behaviour/interactivecountback/classes/privacy/provider.php create mode 100644 question/behaviour/manualgraded/classes/privacy/provider.php create mode 100644 question/behaviour/missing/classes/privacy/provider.php diff --git a/question/behaviour/adaptive/classes/privacy/provider.php b/question/behaviour/adaptive/classes/privacy/provider.php new file mode 100644 index 00000000000..3a565fc2409 --- /dev/null +++ b/question/behaviour/adaptive/classes/privacy/provider.php @@ -0,0 +1,46 @@ +. + +/** + * Privacy Subsystem implementation for qbehaviour_adaptive. + * + * @package qbehaviour_adaptive + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace qbehaviour_adaptive\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem for qbehaviour_adaptive implementing null_provider. + * + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements \core_privacy\local\metadata\null_provider { + + /** + * Get the language string identifier with the component's language + * file to explain why this plugin stores no data. + * + * @return string + */ + public static function get_reason() { + return 'privacy:metadata'; + } +} diff --git a/question/behaviour/adaptive/lang/en/qbehaviour_adaptive.php b/question/behaviour/adaptive/lang/en/qbehaviour_adaptive.php index 2dc2e53bc66..fa92c04e96c 100644 --- a/question/behaviour/adaptive/lang/en/qbehaviour_adaptive.php +++ b/question/behaviour/adaptive/lang/en/qbehaviour_adaptive.php @@ -32,6 +32,7 @@ $string['gradingdetailswithpenalty'] = 'Marks for this submission: {$a->raw}/{$a $string['gradingdetailswithtotalpenalty'] = 'Marks for this submission: {$a->raw}/{$a->max}. This submission attracted a penalty of {$a->penalty}. Total penalties so far: {$a->totalpenalty}.'; $string['notcomplete'] = 'Not complete'; $string['pluginname'] = 'Adaptive mode'; +$string['privacy:metadata'] = 'The Adaptive mode question behaviour plugin does not store any personal data.'; // Old strings these are currently only used in the unit tests, to verify that the new // strings give the same results as the old strings. diff --git a/question/behaviour/adaptivenopenalty/classes/privacy/provider.php b/question/behaviour/adaptivenopenalty/classes/privacy/provider.php new file mode 100644 index 00000000000..826af400466 --- /dev/null +++ b/question/behaviour/adaptivenopenalty/classes/privacy/provider.php @@ -0,0 +1,46 @@ +. + +/** + * Privacy Subsystem implementation for qbehaviour_adaptivenopenalty. + * + * @package qbehaviour_adaptivenopenalty + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace qbehaviour_adaptivenopenalty\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem for qbehaviour_adaptivenopenalty implementing null_provider. + * + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements \core_privacy\local\metadata\null_provider { + + /** + * Get the language string identifier with the component's language + * file to explain why this plugin stores no data. + * + * @return string + */ + public static function get_reason() { + return 'privacy:metadata'; + } +} diff --git a/question/behaviour/adaptivenopenalty/lang/en/qbehaviour_adaptivenopenalty.php b/question/behaviour/adaptivenopenalty/lang/en/qbehaviour_adaptivenopenalty.php index d15c66830e5..733ddbcfa02 100644 --- a/question/behaviour/adaptivenopenalty/lang/en/qbehaviour_adaptivenopenalty.php +++ b/question/behaviour/adaptivenopenalty/lang/en/qbehaviour_adaptivenopenalty.php @@ -24,3 +24,4 @@ */ $string['pluginname'] = 'Adaptive mode (no penalties)'; +$string['privacy:metadata'] = 'The Adaptive mode (no penalties) question behaviour plugin does not store any personal data.'; diff --git a/question/behaviour/deferredcbm/classes/privacy/provider.php b/question/behaviour/deferredcbm/classes/privacy/provider.php new file mode 100644 index 00000000000..b11417b72a6 --- /dev/null +++ b/question/behaviour/deferredcbm/classes/privacy/provider.php @@ -0,0 +1,46 @@ +. + +/** + * Privacy Subsystem implementation for qbehaviour_deferredcbm. + * + * @package qbehaviour_deferredcbm + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace qbehaviour_deferredcbm\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem for qbehaviour_deferredcbm implementing null_provider. + * + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements \core_privacy\local\metadata\null_provider { + + /** + * Get the language string identifier with the component's language + * file to explain why this plugin stores no data. + * + * @return string + */ + public static function get_reason() { + return 'privacy:metadata'; + } +} diff --git a/question/behaviour/deferredcbm/lang/en/qbehaviour_deferredcbm.php b/question/behaviour/deferredcbm/lang/en/qbehaviour_deferredcbm.php index 47a4a9760d6..a2d691f157d 100644 --- a/question/behaviour/deferredcbm/lang/en/qbehaviour_deferredcbm.php +++ b/question/behaviour/deferredcbm/lang/en/qbehaviour_deferredcbm.php @@ -66,6 +66,7 @@ $string['howcertainareyou'] = 'Certainty{$a->help}: {$a->choices}'; $string['noquestions'] = 'No responses'; $string['overconfident'] = 'over-confident'; $string['pluginname'] = 'Deferred feedback with CBM'; +$string['privacy:metadata'] = 'The Deferred feedback with CBM question behaviour plugin does not store any personal data.'; $string['slightlyoverconfident'] = 'a bit over-confident'; $string['slightlyunderconfident'] = 'a bit under-confident'; $string['underconfident'] = 'under-confident'; diff --git a/question/behaviour/deferredfeedback/classes/privacy/provider.php b/question/behaviour/deferredfeedback/classes/privacy/provider.php new file mode 100644 index 00000000000..45b9850563e --- /dev/null +++ b/question/behaviour/deferredfeedback/classes/privacy/provider.php @@ -0,0 +1,46 @@ +. + +/** + * Privacy Subsystem implementation for qbehaviour_deferredfeedback. + * + * @package qbehaviour_deferredfeedback + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace qbehaviour_deferredfeedback\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem for qbehaviour_deferredfeedback implementing null_provider. + * + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements \core_privacy\local\metadata\null_provider { + + /** + * Get the language string identifier with the component's language + * file to explain why this plugin stores no data. + * + * @return string + */ + public static function get_reason() { + return 'privacy:metadata'; + } +} diff --git a/question/behaviour/deferredfeedback/lang/en/qbehaviour_deferredfeedback.php b/question/behaviour/deferredfeedback/lang/en/qbehaviour_deferredfeedback.php index 61be24286d3..266cd8b62c0 100644 --- a/question/behaviour/deferredfeedback/lang/en/qbehaviour_deferredfeedback.php +++ b/question/behaviour/deferredfeedback/lang/en/qbehaviour_deferredfeedback.php @@ -24,3 +24,4 @@ */ $string['pluginname'] = 'Deferred feedback'; +$string['privacy:metadata'] = 'The Deferred feedback behaviour plugin does not store any personal data.'; diff --git a/question/behaviour/immediatecbm/classes/privacy/provider.php b/question/behaviour/immediatecbm/classes/privacy/provider.php new file mode 100644 index 00000000000..dd662a0a414 --- /dev/null +++ b/question/behaviour/immediatecbm/classes/privacy/provider.php @@ -0,0 +1,46 @@ +. + +/** + * Privacy Subsystem implementation for qbehaviour_immediatecbm. + * + * @package qbehaviour_immediatecbm + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace qbehaviour_immediatecbm\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem for qbehaviour_immediatecbm implementing null_provider. + * + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements \core_privacy\local\metadata\null_provider { + + /** + * Get the language string identifier with the component's language + * file to explain why this plugin stores no data. + * + * @return string + */ + public static function get_reason() { + return 'privacy:metadata'; + } +} diff --git a/question/behaviour/immediatecbm/lang/en/qbehaviour_immediatecbm.php b/question/behaviour/immediatecbm/lang/en/qbehaviour_immediatecbm.php index cc0fe79fccf..f09a1d2595d 100644 --- a/question/behaviour/immediatecbm/lang/en/qbehaviour_immediatecbm.php +++ b/question/behaviour/immediatecbm/lang/en/qbehaviour_immediatecbm.php @@ -25,3 +25,4 @@ $string['pleaseselectacertainty'] = 'Please select a certainty.'; $string['pluginname'] = 'Immediate feedback with CBM'; +$string['privacy:metadata'] = 'The Immediate feedback with CBM question behaviour plugin does not store any personal data.'; diff --git a/question/behaviour/immediatefeedback/classes/privacy/provider.php b/question/behaviour/immediatefeedback/classes/privacy/provider.php new file mode 100644 index 00000000000..8c72f849917 --- /dev/null +++ b/question/behaviour/immediatefeedback/classes/privacy/provider.php @@ -0,0 +1,46 @@ +. + +/** + * Privacy Subsystem implementation for qbehaviour_immediatefeedback. + * + * @package qbehaviour_immediatefeedback + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace qbehaviour_immediatefeedback\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem for qbehaviour_immediatefeedback implementing null_provider. + * + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements \core_privacy\local\metadata\null_provider { + + /** + * Get the language string identifier with the component's language + * file to explain why this plugin stores no data. + * + * @return string + */ + public static function get_reason() { + return 'privacy:metadata'; + } +} diff --git a/question/behaviour/immediatefeedback/lang/en/qbehaviour_immediatefeedback.php b/question/behaviour/immediatefeedback/lang/en/qbehaviour_immediatefeedback.php index 84e36886f91..55abc46819b 100644 --- a/question/behaviour/immediatefeedback/lang/en/qbehaviour_immediatefeedback.php +++ b/question/behaviour/immediatefeedback/lang/en/qbehaviour_immediatefeedback.php @@ -25,3 +25,4 @@ $string['notcomplete'] = 'Not complete'; $string['pluginname'] = 'Immediate feedback'; +$string['privacy:metadata'] = 'The Immediate feedback question behaviour plugin does not store any personal data.'; diff --git a/question/behaviour/informationitem/classes/privacy/provider.php b/question/behaviour/informationitem/classes/privacy/provider.php new file mode 100644 index 00000000000..c5e2b03525c --- /dev/null +++ b/question/behaviour/informationitem/classes/privacy/provider.php @@ -0,0 +1,46 @@ +. + +/** + * Privacy Subsystem implementation for qbehaviour_informationitem. + * + * @package qbehaviour_informationitem + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace qbehaviour_informationitem\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem for qbehaviour_informationitem implementing null_provider. + * + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements \core_privacy\local\metadata\null_provider { + + /** + * Get the language string identifier with the component's language + * file to explain why this plugin stores no data. + * + * @return string + */ + public static function get_reason() { + return 'privacy:metadata'; + } +} diff --git a/question/behaviour/informationitem/lang/en/qbehaviour_informationitem.php b/question/behaviour/informationitem/lang/en/qbehaviour_informationitem.php index f0869b86c50..df79792f184 100644 --- a/question/behaviour/informationitem/lang/en/qbehaviour_informationitem.php +++ b/question/behaviour/informationitem/lang/en/qbehaviour_informationitem.php @@ -24,4 +24,5 @@ */ $string['pluginname'] = 'Behaviour for information items'; -$string['seen'] = 'Seen'; \ No newline at end of file +$string['privacy:metadata'] = 'The Information items question behaviour plugin does not store any personal data.'; +$string['seen'] = 'Seen'; diff --git a/question/behaviour/interactive/classes/privacy/provider.php b/question/behaviour/interactive/classes/privacy/provider.php new file mode 100644 index 00000000000..53681a90ea3 --- /dev/null +++ b/question/behaviour/interactive/classes/privacy/provider.php @@ -0,0 +1,46 @@ +. + +/** + * Privacy Subsystem implementation for qbehaviour_interactive. + * + * @package qbehaviour_interactive + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace qbehaviour_interactive\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem for qbehaviour_interactive implementing null_provider. + * + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements \core_privacy\local\metadata\null_provider { + + /** + * Get the language string identifier with the component's language + * file to explain why this plugin stores no data. + * + * @return string + */ + public static function get_reason() { + return 'privacy:metadata'; + } +} diff --git a/question/behaviour/interactive/lang/en/qbehaviour_interactive.php b/question/behaviour/interactive/lang/en/qbehaviour_interactive.php index 8a4ebf63246..be53c41798d 100644 --- a/question/behaviour/interactive/lang/en/qbehaviour_interactive.php +++ b/question/behaviour/interactive/lang/en/qbehaviour_interactive.php @@ -24,5 +24,6 @@ */ $string['pluginname'] = 'Interactive with multiple tries'; +$string['privacy:metadata'] = 'The Interactive with multiple tries question behaviour plugin does not store any personal data.'; $string['triesremaining'] = 'Tries remaining: {$a}'; $string['tryagain'] = 'Try again'; diff --git a/question/behaviour/interactivecountback/classes/privacy/provider.php b/question/behaviour/interactivecountback/classes/privacy/provider.php new file mode 100644 index 00000000000..d425e5661d2 --- /dev/null +++ b/question/behaviour/interactivecountback/classes/privacy/provider.php @@ -0,0 +1,46 @@ +. + +/** + * Privacy Subsystem implementation for qbehaviour_interactivecountback. + * + * @package qbehaviour_interactivecountback + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace qbehaviour_interactivecountback\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem for qbehaviour_interactivecountback implementing null_provider. + * + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements \core_privacy\local\metadata\null_provider { + + /** + * Get the language string identifier with the component's language + * file to explain why this plugin stores no data. + * + * @return string + */ + public static function get_reason() { + return 'privacy:metadata'; + } +} diff --git a/question/behaviour/interactivecountback/lang/en/qbehaviour_interactivecountback.php b/question/behaviour/interactivecountback/lang/en/qbehaviour_interactivecountback.php index c02023e17bf..35cd3eef046 100644 --- a/question/behaviour/interactivecountback/lang/en/qbehaviour_interactivecountback.php +++ b/question/behaviour/interactivecountback/lang/en/qbehaviour_interactivecountback.php @@ -24,3 +24,4 @@ */ $string['pluginname'] = 'Interactive with multiple tries (credit for earlier tries)'; +$string['privacy:metadata'] = 'The Interactive with multiple tries (credit for earlier tries) question behaviour plugin does not store any personal data.'; diff --git a/question/behaviour/manualgraded/classes/privacy/provider.php b/question/behaviour/manualgraded/classes/privacy/provider.php new file mode 100644 index 00000000000..a9bfb83131e --- /dev/null +++ b/question/behaviour/manualgraded/classes/privacy/provider.php @@ -0,0 +1,46 @@ +. + +/** + * Privacy Subsystem implementation for qbehaviour_manualgraded. + * + * @package qbehaviour_manualgraded + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace qbehaviour_manualgraded\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem for qbehaviour_manualgraded implementing null_provider. + * + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements \core_privacy\local\metadata\null_provider { + + /** + * Get the language string identifier with the component's language + * file to explain why this plugin stores no data. + * + * @return string + */ + public static function get_reason() { + return 'privacy:metadata'; + } +} diff --git a/question/behaviour/manualgraded/lang/en/qbehaviour_manualgraded.php b/question/behaviour/manualgraded/lang/en/qbehaviour_manualgraded.php index 5ff907a5927..c9bc6b2eeb9 100644 --- a/question/behaviour/manualgraded/lang/en/qbehaviour_manualgraded.php +++ b/question/behaviour/manualgraded/lang/en/qbehaviour_manualgraded.php @@ -24,3 +24,4 @@ */ $string['pluginname'] = 'Manually graded'; +$string['privacy:metadata'] = 'The Manually graded question behaviour plugin does not store any personal data.'; diff --git a/question/behaviour/missing/classes/privacy/provider.php b/question/behaviour/missing/classes/privacy/provider.php new file mode 100644 index 00000000000..c8eb5bd9183 --- /dev/null +++ b/question/behaviour/missing/classes/privacy/provider.php @@ -0,0 +1,46 @@ +. + +/** + * Privacy Subsystem implementation for qbehaviour_missing. + * + * @package qbehaviour_missing + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace qbehaviour_missing\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem for qbehaviour_missing implementing null_provider. + * + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements \core_privacy\local\metadata\null_provider { + + /** + * Get the language string identifier with the component's language + * file to explain why this plugin stores no data. + * + * @return string + */ + public static function get_reason() { + return 'privacy:metadata'; + } +} diff --git a/question/behaviour/missing/lang/en/qbehaviour_missing.php b/question/behaviour/missing/lang/en/qbehaviour_missing.php index a7795b0dfdf..bca723f726e 100644 --- a/question/behaviour/missing/lang/en/qbehaviour_missing.php +++ b/question/behaviour/missing/lang/en/qbehaviour_missing.php @@ -25,3 +25,4 @@ $string['pluginname'] = 'Missing behaviour'; $string['questionusedunknownmodel'] = 'This question was attempted with a behaviour that is not currently available. The question is being displayed as well as possible, but some parts may be missing or wrong.'; +$string['privacy:metadata'] = 'The Missing question behaviour plugin does not store any personal data.'; From 1b03a7f54d8494fc58220bc0113c4282422b0a77 Mon Sep 17 00:00:00 2001 From: Andrew Nicols Date: Sat, 7 Apr 2018 14:10:29 +0800 Subject: [PATCH 06/10] MDL-61407 qformat: Add null_writer for all qformats Questions formats do not store any personal data. Any contrib format which does store personal data should utilise the standard export interfaces as this subsystem will not call them directly. --- .../format/aiken/classes/privacy/provider.php | 46 +++++++++++++++++++ .../format/aiken/lang/en/qformat_aiken.php | 1 + .../classes/privacy/provider.php | 46 +++++++++++++++++++ .../lang/en/qformat_blackboard_six.php | 1 + .../examview/classes/privacy/provider.php | 46 +++++++++++++++++++ .../examview/lang/en/qformat_examview.php | 1 + .../format/gift/classes/privacy/provider.php | 46 +++++++++++++++++++ question/format/gift/lang/en/qformat_gift.php | 1 + .../missingword/classes/privacy/provider.php | 46 +++++++++++++++++++ .../lang/en/qformat_missingword.php | 1 + .../multianswer/classes/privacy/provider.php | 46 +++++++++++++++++++ .../lang/en/qformat_multianswer.php | 1 + .../format/webct/classes/privacy/provider.php | 46 +++++++++++++++++++ .../format/webct/lang/en/qformat_webct.php | 1 + .../format/xhtml/classes/privacy/provider.php | 46 +++++++++++++++++++ .../format/xhtml/lang/en/qformat_xhtml.php | 1 + .../format/xml/classes/privacy/provider.php | 46 +++++++++++++++++++ question/format/xml/lang/en/qformat_xml.php | 1 + 18 files changed, 423 insertions(+) create mode 100644 question/format/aiken/classes/privacy/provider.php create mode 100644 question/format/blackboard_six/classes/privacy/provider.php create mode 100644 question/format/examview/classes/privacy/provider.php create mode 100644 question/format/gift/classes/privacy/provider.php create mode 100644 question/format/missingword/classes/privacy/provider.php create mode 100644 question/format/multianswer/classes/privacy/provider.php create mode 100644 question/format/webct/classes/privacy/provider.php create mode 100644 question/format/xhtml/classes/privacy/provider.php create mode 100644 question/format/xml/classes/privacy/provider.php diff --git a/question/format/aiken/classes/privacy/provider.php b/question/format/aiken/classes/privacy/provider.php new file mode 100644 index 00000000000..882efc84d3a --- /dev/null +++ b/question/format/aiken/classes/privacy/provider.php @@ -0,0 +1,46 @@ +. + +/** + * Privacy Subsystem implementation for qformat_aiken. + * + * @package qformat_aiken + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace qformat_aiken\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem for qformat_aiken implementing null_provider. + * + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements \core_privacy\local\metadata\null_provider { + + /** + * Get the language string identifier with the component's language + * file to explain why this plugin stores no data. + * + * @return string + */ + public static function get_reason() { + return 'privacy:metadata'; + } +} diff --git a/question/format/aiken/lang/en/qformat_aiken.php b/question/format/aiken/lang/en/qformat_aiken.php index 9fc5ede4b39..44cf96f17de 100644 --- a/question/format/aiken/lang/en/qformat_aiken.php +++ b/question/format/aiken/lang/en/qformat_aiken.php @@ -25,3 +25,4 @@ $string['pluginname'] = 'Aiken format'; $string['pluginname_help'] = 'This is a simple format for importing multiple choice questions from a text file.'; $string['pluginname_link'] = 'qformat/aiken'; +$string['privacy:metadata'] = 'The Aiken question format plugin does not store any personal data.'; diff --git a/question/format/blackboard_six/classes/privacy/provider.php b/question/format/blackboard_six/classes/privacy/provider.php new file mode 100644 index 00000000000..addabd80e12 --- /dev/null +++ b/question/format/blackboard_six/classes/privacy/provider.php @@ -0,0 +1,46 @@ +. + +/** + * Privacy Subsystem implementation for qformat_blackboard_six. + * + * @package qformat_blackboard_six + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace qformat_blackboard_six\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem for qformat_blackboard_six implementing null_provider. + * + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements \core_privacy\local\metadata\null_provider { + + /** + * Get the language string identifier with the component's language + * file to explain why this plugin stores no data. + * + * @return string + */ + public static function get_reason() { + return 'privacy:metadata'; + } +} diff --git a/question/format/blackboard_six/lang/en/qformat_blackboard_six.php b/question/format/blackboard_six/lang/en/qformat_blackboard_six.php index c87aa2cd5b9..99112dd7e50 100644 --- a/question/format/blackboard_six/lang/en/qformat_blackboard_six.php +++ b/question/format/blackboard_six/lang/en/qformat_blackboard_six.php @@ -31,4 +31,5 @@ $string['importedcategory'] = 'Imported category {$a}'; $string['notenoughtsubans'] = 'Unable to import matching question \'{$a}\' because a matching question must comprise at least two questions and three answers.'; $string['pluginname'] = 'Blackboard'; $string['pluginname_help'] = 'Blackboard format enables questions saved in all Blackboard export formats to be imported via a dat or zip file. For zip files, images import is supported.'; +$string['privacy:metadata'] = 'The Blackbard question format plugin does not store any personal data.'; $string['unhandledpresblock'] = 'Unhandled presentation block'; diff --git a/question/format/examview/classes/privacy/provider.php b/question/format/examview/classes/privacy/provider.php new file mode 100644 index 00000000000..0cf83db3e49 --- /dev/null +++ b/question/format/examview/classes/privacy/provider.php @@ -0,0 +1,46 @@ +. + +/** + * Privacy Subsystem implementation for qformat_examview. + * + * @package qformat_examview + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace qformat_examview\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem for qformat_examview implementing null_provider. + * + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements \core_privacy\local\metadata\null_provider { + + /** + * Get the language string identifier with the component's language + * file to explain why this plugin stores no data. + * + * @return string + */ + public static function get_reason() { + return 'privacy:metadata'; + } +} diff --git a/question/format/examview/lang/en/qformat_examview.php b/question/format/examview/lang/en/qformat_examview.php index ce51ff1b71d..f40b151644d 100644 --- a/question/format/examview/lang/en/qformat_examview.php +++ b/question/format/examview/lang/en/qformat_examview.php @@ -24,3 +24,4 @@ $string['pluginname'] = 'Examview'; $string['pluginname_help'] = 'Examview format enables the import of questions from Examview 4 XML files. For newer versions of Examview, Blackboard format may be used.'; +$string['privacy:metadata'] = 'The Examview question format plugin does not store any personal data.'; diff --git a/question/format/gift/classes/privacy/provider.php b/question/format/gift/classes/privacy/provider.php new file mode 100644 index 00000000000..7d46ab094ea --- /dev/null +++ b/question/format/gift/classes/privacy/provider.php @@ -0,0 +1,46 @@ +. + +/** + * Privacy Subsystem implementation for qformat_gift. + * + * @package qformat_gift + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace qformat_gift\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem for qformat_gift implementing null_provider. + * + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements \core_privacy\local\metadata\null_provider { + + /** + * Get the language string identifier with the component's language + * file to explain why this plugin stores no data. + * + * @return string + */ + public static function get_reason() { + return 'privacy:metadata'; + } +} diff --git a/question/format/gift/lang/en/qformat_gift.php b/question/format/gift/lang/en/qformat_gift.php index f0ccd6020ce..6bad7cbaa8d 100644 --- a/question/format/gift/lang/en/qformat_gift.php +++ b/question/format/gift/lang/en/qformat_gift.php @@ -34,3 +34,4 @@ $string['nohandler'] = 'No handler for question type {$a}'; $string['pluginname'] = 'Gift format'; $string['pluginname_help'] = 'Gift format enables multiple-choice, true-false, short answer, matching, missing word, numerical and essay questions to be imported or exported via text file.'; $string['pluginname_link'] = 'qformat/gift'; +$string['privacy:metadata'] = 'The Gift question format plugin does not store any personal data.'; diff --git a/question/format/missingword/classes/privacy/provider.php b/question/format/missingword/classes/privacy/provider.php new file mode 100644 index 00000000000..1e48d7eb279 --- /dev/null +++ b/question/format/missingword/classes/privacy/provider.php @@ -0,0 +1,46 @@ +. + +/** + * Privacy Subsystem implementation for qformat_missingword. + * + * @package qformat_missingword + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace qformat_missingword\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem for qformat_missingword implementing null_provider. + * + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements \core_privacy\local\metadata\null_provider { + + /** + * Get the language string identifier with the component's language + * file to explain why this plugin stores no data. + * + * @return string + */ + public static function get_reason() { + return 'privacy:metadata'; + } +} diff --git a/question/format/missingword/lang/en/qformat_missingword.php b/question/format/missingword/lang/en/qformat_missingword.php index e6b66a9660d..f468d311126 100644 --- a/question/format/missingword/lang/en/qformat_missingword.php +++ b/question/format/missingword/lang/en/qformat_missingword.php @@ -28,3 +28,4 @@ $string['pluginname_link'] = 'Missing word format'; $string['beginanswernotfound'] = 'Could not find a required "{" character in imported file content.'; $string['endanswernotfound'] = 'Could not find a required "}" character in imported file content.'; $string['noanswerfound'] = 'No answers found in question'; +$string['privacy:metadata'] = 'The Missing word question format plugin does not store any personal data.'; diff --git a/question/format/multianswer/classes/privacy/provider.php b/question/format/multianswer/classes/privacy/provider.php new file mode 100644 index 00000000000..d30b6ecb7f7 --- /dev/null +++ b/question/format/multianswer/classes/privacy/provider.php @@ -0,0 +1,46 @@ +. + +/** + * Privacy Subsystem implementation for qformat_multianswer. + * + * @package qformat_multianswer + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace qformat_multianswer\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem for qformat_multianswer implementing null_provider. + * + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements \core_privacy\local\metadata\null_provider { + + /** + * Get the language string identifier with the component's language + * file to explain why this plugin stores no data. + * + * @return string + */ + public static function get_reason() { + return 'privacy:metadata'; + } +} diff --git a/question/format/multianswer/lang/en/qformat_multianswer.php b/question/format/multianswer/lang/en/qformat_multianswer.php index 4df7fecd2f3..48d4e2c439e 100644 --- a/question/format/multianswer/lang/en/qformat_multianswer.php +++ b/question/format/multianswer/lang/en/qformat_multianswer.php @@ -25,3 +25,4 @@ $string['pluginname'] = 'Embedded answers (Cloze)'; $string['pluginname_help'] = 'Embedded answers (Cloze) format enables the import of a passage of text with questions such as multiple-choice and short answer embedded within it.'; $string['pluginname_link'] = 'question/type/multianswer'; +$string['privacy:metadata'] = 'The Embedded answers question plugin does not store any personal data.'; diff --git a/question/format/webct/classes/privacy/provider.php b/question/format/webct/classes/privacy/provider.php new file mode 100644 index 00000000000..63eddf7dc18 --- /dev/null +++ b/question/format/webct/classes/privacy/provider.php @@ -0,0 +1,46 @@ +. + +/** + * Privacy Subsystem implementation for qformat_webct. + * + * @package qformat_webct + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace qformat_webct\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem for qformat_webct implementing null_provider. + * + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements \core_privacy\local\metadata\null_provider { + + /** + * Get the language string identifier with the component's language + * file to explain why this plugin stores no data. + * + * @return string + */ + public static function get_reason() { + return 'privacy:metadata'; + } +} diff --git a/question/format/webct/lang/en/qformat_webct.php b/question/format/webct/lang/en/qformat_webct.php index f79a98eae49..96a52d6dafc 100644 --- a/question/format/webct/lang/en/qformat_webct.php +++ b/question/format/webct/lang/en/qformat_webct.php @@ -28,6 +28,7 @@ $string['missingquestion'] = 'Missing question label after line {$a}'; $string['pluginname'] = 'WebCT format'; $string['pluginname_help'] = 'WebCT format enables multiple-choice and short answer questions saved in WebCT\'s text-based format to be imported.'; $string['pluginname_link'] = 'qformat/webct'; +$string['privacy:metadata'] = 'The WebCT question format plugin does not store any personal data.'; $string['questionnametoolong'] = 'Question name too long at line {$a} (255 char. max). It has been truncated.'; $string['unknowntype'] = 'Unknown question type after line {$a}'; $string['warningsdetected'] = '{$a} warning(s) detected'; diff --git a/question/format/xhtml/classes/privacy/provider.php b/question/format/xhtml/classes/privacy/provider.php new file mode 100644 index 00000000000..e88693af2bb --- /dev/null +++ b/question/format/xhtml/classes/privacy/provider.php @@ -0,0 +1,46 @@ +. + +/** + * Privacy Subsystem implementation for qformat_xhtml. + * + * @package qformat_xhtml + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace qformat_xhtml\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem for qformat_xhtml implementing null_provider. + * + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements \core_privacy\local\metadata\null_provider { + + /** + * Get the language string identifier with the component's language + * file to explain why this plugin stores no data. + * + * @return string + */ + public static function get_reason() { + return 'privacy:metadata'; + } +} diff --git a/question/format/xhtml/lang/en/qformat_xhtml.php b/question/format/xhtml/lang/en/qformat_xhtml.php index 32d464bd5bb..aac94bb6cc7 100644 --- a/question/format/xhtml/lang/en/qformat_xhtml.php +++ b/question/format/xhtml/lang/en/qformat_xhtml.php @@ -25,3 +25,4 @@ $string['pluginname'] = 'XHTML format'; $string['pluginname_help'] = 'XHTML format enables all questions in the category to be exported to a single page of strict XHTML for possible use in another application.'; $string['pluginname_link'] = 'qformat/xhtml'; +$string['privacy:metadata'] = 'The XHTML question format plugin does not store any personal data.'; diff --git a/question/format/xml/classes/privacy/provider.php b/question/format/xml/classes/privacy/provider.php new file mode 100644 index 00000000000..a3138583c9e --- /dev/null +++ b/question/format/xml/classes/privacy/provider.php @@ -0,0 +1,46 @@ +. + +/** + * Privacy Subsystem implementation for qformat_xml. + * + * @package qformat_xml + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace qformat_xml\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem for qformat_xml implementing null_provider. + * + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements \core_privacy\local\metadata\null_provider { + + /** + * Get the language string identifier with the component's language + * file to explain why this plugin stores no data. + * + * @return string + */ + public static function get_reason() { + return 'privacy:metadata'; + } +} diff --git a/question/format/xml/lang/en/qformat_xml.php b/question/format/xml/lang/en/qformat_xml.php index 04b9c6cd36d..57fb5549174 100644 --- a/question/format/xml/lang/en/qformat_xml.php +++ b/question/format/xml/lang/en/qformat_xml.php @@ -26,6 +26,7 @@ $string['invalidxml'] = 'Invalid XML file - string expected (use CDATA?)'; $string['pluginname'] = 'Moodle XML format'; $string['pluginname_help'] = 'This is a Moodle-specific format for importing and exporting questions.'; $string['pluginname_link'] = 'qformat/xml'; +$string['privacy:metadata'] = 'The XML question format plugin does not store any personal data.'; $string['truefalseimporterror'] = 'Warning: The true/false question \'{$a->questiontext}\' could not be imported properly. It was not clear whether the correct answer is true or false. The question has been imported assuming that the answer is \'{$a->answer}\'. If this is not correct, you will need to edit the question.'; $string['unsupportedexport'] = 'Question type {$a} is not supported by XML export'; $string['xmlimportnoname'] = 'Missing question name in XML file'; From 9c9d3ab247cc785fcb216a82b60682219902c59f Mon Sep 17 00:00:00 2001 From: Andrew Nicols Date: Sat, 7 Apr 2018 14:16:52 +0800 Subject: [PATCH 07/10] MDL-61407 qtype: Add null_writer for all qtypes --- .../calculated/classes/privacy/provider.php | 46 +++++++++++++++++++ .../calculated/lang/en/qtype_calculated.php | 1 + .../classes/privacy/provider.php | 46 +++++++++++++++++++ .../lang/en/qtype_calculatedmulti.php | 1 + .../classes/privacy/provider.php | 46 +++++++++++++++++++ .../lang/en/qtype_calculatedsimple.php | 1 + .../classes/privacy/provider.php | 46 +++++++++++++++++++ .../lang/en/qtype_ddimageortext.php | 1 + .../ddmarker/classes/privacy/provider.php | 46 +++++++++++++++++++ .../type/ddmarker/lang/en/qtype_ddmarker.php | 1 + .../type/ddwtos/classes/privacy/provider.php | 46 +++++++++++++++++++ question/type/ddwtos/lang/en/qtype_ddwtos.php | 1 + .../description/classes/privacy/provider.php | 46 +++++++++++++++++++ .../description/lang/en/qtype_description.php | 1 + .../type/essay/classes/privacy/provider.php | 46 +++++++++++++++++++ question/type/essay/lang/en/qtype_essay.php | 1 + .../gapselect/classes/privacy/provider.php | 46 +++++++++++++++++++ .../gapselect/lang/en/qtype_gapselect.php | 1 + .../type/match/classes/privacy/provider.php | 46 +++++++++++++++++++ question/type/match/lang/en/qtype_match.php | 1 + .../missingtype/classes/privacy/provider.php | 46 +++++++++++++++++++ .../missingtype/lang/en/qtype_missingtype.php | 1 + .../multianswer/classes/privacy/provider.php | 46 +++++++++++++++++++ .../multianswer/lang/en/qtype_multianswer.php | 1 + .../multichoice/classes/privacy/provider.php | 46 +++++++++++++++++++ .../multichoice/lang/en/qtype_multichoice.php | 1 + .../numerical/classes/privacy/provider.php | 46 +++++++++++++++++++ .../numerical/lang/en/qtype_numerical.php | 1 + .../type/random/classes/privacy/provider.php | 46 +++++++++++++++++++ question/type/random/lang/en/qtype_random.php | 1 + .../classes/privacy/provider.php | 46 +++++++++++++++++++ .../lang/en/qtype_randomsamatch.php | 1 + .../shortanswer/classes/privacy/provider.php | 46 +++++++++++++++++++ .../shortanswer/lang/en/qtype_shortanswer.php | 1 + .../truefalse/classes/privacy/provider.php | 46 +++++++++++++++++++ .../truefalse/lang/en/qtype_truefalse.php | 1 + 36 files changed, 846 insertions(+) create mode 100644 question/type/calculated/classes/privacy/provider.php create mode 100644 question/type/calculatedmulti/classes/privacy/provider.php create mode 100644 question/type/calculatedsimple/classes/privacy/provider.php create mode 100644 question/type/ddimageortext/classes/privacy/provider.php create mode 100644 question/type/ddmarker/classes/privacy/provider.php create mode 100644 question/type/ddwtos/classes/privacy/provider.php create mode 100644 question/type/description/classes/privacy/provider.php create mode 100644 question/type/essay/classes/privacy/provider.php create mode 100644 question/type/gapselect/classes/privacy/provider.php create mode 100644 question/type/match/classes/privacy/provider.php create mode 100644 question/type/missingtype/classes/privacy/provider.php create mode 100644 question/type/multianswer/classes/privacy/provider.php create mode 100644 question/type/multichoice/classes/privacy/provider.php create mode 100644 question/type/numerical/classes/privacy/provider.php create mode 100644 question/type/random/classes/privacy/provider.php create mode 100644 question/type/randomsamatch/classes/privacy/provider.php create mode 100644 question/type/shortanswer/classes/privacy/provider.php create mode 100644 question/type/truefalse/classes/privacy/provider.php diff --git a/question/type/calculated/classes/privacy/provider.php b/question/type/calculated/classes/privacy/provider.php new file mode 100644 index 00000000000..1d3f3451746 --- /dev/null +++ b/question/type/calculated/classes/privacy/provider.php @@ -0,0 +1,46 @@ +. + +/** + * Privacy Subsystem implementation for qtype_calculated. + * + * @package qtype_calculated + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace qtype_calculated\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem for qtype_calculated implementing null_provider. + * + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements \core_privacy\local\metadata\null_provider { + + /** + * Get the language string identifier with the component's language + * file to explain why this plugin stores no data. + * + * @return string + */ + public static function get_reason() { + return 'privacy:metadata'; + } +} diff --git a/question/type/calculated/lang/en/qtype_calculated.php b/question/type/calculated/lang/en/qtype_calculated.php index f56e297ed8e..82be4ec2b96 100644 --- a/question/type/calculated/lang/en/qtype_calculated.php +++ b/question/type/calculated/lang/en/qtype_calculated.php @@ -115,6 +115,7 @@ $string['pluginname_link'] = 'question/type/calculated'; $string['pluginnameadding'] = 'Adding a Calculated question'; $string['pluginnameediting'] = 'Editing a Calculated question'; $string['pluginnamesummary'] = 'Calculated questions are like numerical questions but with the numbers used selected randomly from a set when the quiz is taken.'; +$string['privacy:metadata'] = 'The Calculated question type plugin does not store any personal data.'; $string['possiblehdr'] = 'Possible wild cards present only in the question text'; $string['questiondatasets'] = 'Question datasets'; $string['questiondatasets_help'] = 'Question datasets of wild cards that will be used in each individual question'; diff --git a/question/type/calculatedmulti/classes/privacy/provider.php b/question/type/calculatedmulti/classes/privacy/provider.php new file mode 100644 index 00000000000..07738b62594 --- /dev/null +++ b/question/type/calculatedmulti/classes/privacy/provider.php @@ -0,0 +1,46 @@ +. + +/** + * Privacy Subsystem implementation for qtype_calculatedmulti. + * + * @package qtype_calculatedmulti + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace qtype_calculatedmulti\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem for qtype_calculatedmulti implementing null_provider. + * + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements \core_privacy\local\metadata\null_provider { + + /** + * Get the language string identifier with the component's language + * file to explain why this plugin stores no data. + * + * @return string + */ + public static function get_reason() { + return 'privacy:metadata'; + } +} diff --git a/question/type/calculatedmulti/lang/en/qtype_calculatedmulti.php b/question/type/calculatedmulti/lang/en/qtype_calculatedmulti.php index a675114de37..c0b4b1926d1 100644 --- a/question/type/calculatedmulti/lang/en/qtype_calculatedmulti.php +++ b/question/type/calculatedmulti/lang/en/qtype_calculatedmulti.php @@ -30,3 +30,4 @@ $string['pluginname_link'] = 'question/type/calculatedmulti'; $string['pluginnameadding'] = 'Adding a Calculated multichoice question'; $string['pluginnameediting'] = 'Editing a Calculated multichoice question'; $string['pluginnamesummary'] = 'Calculated multichoice questions are like multichoice questions which choice elements can include formula results from numeric values that are selected randomly from a set when the quiz is taken.'; +$string['privacy:metadata'] = 'The Calculated multichoice question type plugin does not store any personal data.'; diff --git a/question/type/calculatedsimple/classes/privacy/provider.php b/question/type/calculatedsimple/classes/privacy/provider.php new file mode 100644 index 00000000000..c8e309f05a1 --- /dev/null +++ b/question/type/calculatedsimple/classes/privacy/provider.php @@ -0,0 +1,46 @@ +. + +/** + * Privacy Subsystem implementation for qtype_calculatedsimple. + * + * @package qtype_calculatedsimple + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace qtype_calculatedsimple\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem for qtype_calculatedsimple implementing null_provider. + * + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements \core_privacy\local\metadata\null_provider { + + /** + * Get the language string identifier with the component's language + * file to explain why this plugin stores no data. + * + * @return string + */ + public static function get_reason() { + return 'privacy:metadata'; + } +} diff --git a/question/type/calculatedsimple/lang/en/qtype_calculatedsimple.php b/question/type/calculatedsimple/lang/en/qtype_calculatedsimple.php index 955962fe754..e3f4bff0d67 100644 --- a/question/type/calculatedsimple/lang/en/qtype_calculatedsimple.php +++ b/question/type/calculatedsimple/lang/en/qtype_calculatedsimple.php @@ -34,6 +34,7 @@ $string['pluginname_link'] = 'question/type/calculatedsimple'; $string['pluginnameadding'] = 'Adding a Simple calculated question'; $string['pluginnameediting'] = 'Editing a Simple calculated question'; $string['pluginnamesummary'] = 'A simpler version of calculated questions which are like numerical questions but with the numbers used selected randomly from a set when the quiz is taken.'; +$string['privacy:metadata'] = 'The Calculated simple question type plugin does not store any personal data.'; $string['setno'] = 'Set {$a}'; $string['setwildcardvalues'] = 'set(s) of wild card(s) values'; $string['showitems'] = 'Display'; diff --git a/question/type/ddimageortext/classes/privacy/provider.php b/question/type/ddimageortext/classes/privacy/provider.php new file mode 100644 index 00000000000..0eb544a19d9 --- /dev/null +++ b/question/type/ddimageortext/classes/privacy/provider.php @@ -0,0 +1,46 @@ +. + +/** + * Privacy Subsystem implementation for qtype_ddimageortext. + * + * @package qtype_ddimageortext + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace qtype_ddimageortext\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem for qtype_ddimageortext implementing null_provider. + * + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements \core_privacy\local\metadata\null_provider { + + /** + * Get the language string identifier with the component's language + * file to explain why this plugin stores no data. + * + * @return string + */ + public static function get_reason() { + return 'privacy:metadata'; + } +} diff --git a/question/type/ddimageortext/lang/en/qtype_ddimageortext.php b/question/type/ddimageortext/lang/en/qtype_ddimageortext.php index 0d676dd8ec5..3f590b375eb 100644 --- a/question/type/ddimageortext/lang/en/qtype_ddimageortext.php +++ b/question/type/ddimageortext/lang/en/qtype_ddimageortext.php @@ -61,6 +61,7 @@ $string['pluginnameediting'] = 'Editing drag and drop onto image'; $string['pluginnamesummary'] = 'Images or text labels are dragged and dropped into drop zones on a background image.'; $string['previewareaheader'] = 'Preview'; $string['previewareamessage'] = 'Select a background image, specify draggable items and define drop zones on the background image into which they must be dragged.'; +$string['privacy:metadata'] = 'The Drag and drop onto image question type does not store any personal data.'; $string['refresh'] = 'Refresh preview'; $string['shuffleimages'] = 'Shuffle drag items each time question is attempted'; $string['summarisechoice'] = '{$a->no}. {$a->text}'; diff --git a/question/type/ddmarker/classes/privacy/provider.php b/question/type/ddmarker/classes/privacy/provider.php new file mode 100644 index 00000000000..f56ba48c2af --- /dev/null +++ b/question/type/ddmarker/classes/privacy/provider.php @@ -0,0 +1,46 @@ +. + +/** + * Privacy Subsystem implementation for qtype_ddmarker. + * + * @package qtype_ddmarker + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace qtype_ddmarker\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem for qtype_ddmarker implementing null_provider. + * + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements \core_privacy\local\metadata\null_provider { + + /** + * Get the language string identifier with the component's language + * file to explain why this plugin stores no data. + * + * @return string + */ + public static function get_reason() { + return 'privacy:metadata'; + } +} diff --git a/question/type/ddmarker/lang/en/qtype_ddmarker.php b/question/type/ddmarker/lang/en/qtype_ddmarker.php index 054763a6565..d0bf8599449 100644 --- a/question/type/ddmarker/lang/en/qtype_ddmarker.php +++ b/question/type/ddmarker/lang/en/qtype_ddmarker.php @@ -69,6 +69,7 @@ $string['pluginnameediting'] = 'Editing drag and drop markers'; $string['pluginnamesummary'] = 'Markers are dragged and dropped onto a background image.'; $string['previewareaheader'] = 'Preview'; $string['previewareamessage'] = 'Select a background image file, enter text labels for markers and define the drop zones on the background image to which they must be dragged.'; +$string['privacy:metadata'] = 'The Drag and drop markers question type does not store any personal data.'; $string['refresh'] = 'Refresh preview'; $string['clearwrongparts'] = 'Move incorrectly placed markers back to default start position below image'; $string['shape'] = 'Shape'; diff --git a/question/type/ddwtos/classes/privacy/provider.php b/question/type/ddwtos/classes/privacy/provider.php new file mode 100644 index 00000000000..46f28354941 --- /dev/null +++ b/question/type/ddwtos/classes/privacy/provider.php @@ -0,0 +1,46 @@ +. + +/** + * Privacy Subsystem implementation for qtype_ddwtos. + * + * @package qtype_ddwtos + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace qtype_ddwtos\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem for qtype_ddwtos implementing null_provider. + * + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements \core_privacy\local\metadata\null_provider { + + /** + * Get the language string identifier with the component's language + * file to explain why this plugin stores no data. + * + * @return string + */ + public static function get_reason() { + return 'privacy:metadata'; + } +} diff --git a/question/type/ddwtos/lang/en/qtype_ddwtos.php b/question/type/ddwtos/lang/en/qtype_ddwtos.php index c5025f6c8e9..088f9d9e2d4 100644 --- a/question/type/ddwtos/lang/en/qtype_ddwtos.php +++ b/question/type/ddwtos/lang/en/qtype_ddwtos.php @@ -34,3 +34,4 @@ $string['pluginname_link'] = 'question/type/ddwtos'; $string['pluginnameadding'] = 'Adding a drag and drop into text'; $string['pluginnameediting'] = 'Editing a drag and drop into text'; $string['pluginnamesummary'] = 'Missing words in the question text are filled in using drag and drop.'; +$string['privacy:metadata'] = 'The Drag and drop into text question type does not store any personal data.'; diff --git a/question/type/description/classes/privacy/provider.php b/question/type/description/classes/privacy/provider.php new file mode 100644 index 00000000000..58996b61c82 --- /dev/null +++ b/question/type/description/classes/privacy/provider.php @@ -0,0 +1,46 @@ +. + +/** + * Privacy Subsystem implementation for qtype_description. + * + * @package qtype_description + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace qtype_description\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem for qtype_description implementing null_provider. + * + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements \core_privacy\local\metadata\null_provider { + + /** + * Get the language string identifier with the component's language + * file to explain why this plugin stores no data. + * + * @return string + */ + public static function get_reason() { + return 'privacy:metadata'; + } +} diff --git a/question/type/description/lang/en/qtype_description.php b/question/type/description/lang/en/qtype_description.php index cf917834508..48f18040442 100644 --- a/question/type/description/lang/en/qtype_description.php +++ b/question/type/description/lang/en/qtype_description.php @@ -31,3 +31,4 @@ The question text is displayed both during the attempt and on the review page. A $string['pluginnameadding'] = 'Adding a description'; $string['pluginnameediting'] = 'Editing a Description'; $string['pluginnamesummary'] = 'This is not actually a question. Instead it is a way to add some instructions, rubric or other content to the activity. This is similar to the way that labels can be used to add content to the course page.'; +$string['privacy:metadata'] = 'The Description question type plugin does not store any personal data.'; diff --git a/question/type/essay/classes/privacy/provider.php b/question/type/essay/classes/privacy/provider.php new file mode 100644 index 00000000000..6163ffab71f --- /dev/null +++ b/question/type/essay/classes/privacy/provider.php @@ -0,0 +1,46 @@ +. + +/** + * Privacy Subsystem implementation for qtype_essay. + * + * @package qtype_essay + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace qtype_essay\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem for qtype_essay implementing null_provider. + * + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements \core_privacy\local\metadata\null_provider { + + /** + * Get the language string identifier with the component's language + * file to explain why this plugin stores no data. + * + * @return string + */ + public static function get_reason() { + return 'privacy:metadata'; + } +} diff --git a/question/type/essay/lang/en/qtype_essay.php b/question/type/essay/lang/en/qtype_essay.php index 388822aabc9..edbbe596b57 100644 --- a/question/type/essay/lang/en/qtype_essay.php +++ b/question/type/essay/lang/en/qtype_essay.php @@ -44,6 +44,7 @@ $string['pluginname_link'] = 'question/type/essay'; $string['pluginnameadding'] = 'Adding an Essay question'; $string['pluginnameediting'] = 'Editing an Essay question'; $string['pluginnamesummary'] = 'Allows a response of a file upload and/or online text. This must then be graded manually.'; +$string['privacy:metadata'] = 'The Essay question type does not store any personal data.'; $string['responsefieldlines'] = 'Input box size'; $string['responseformat'] = 'Response format'; $string['responseoptions'] = 'Response Options'; diff --git a/question/type/gapselect/classes/privacy/provider.php b/question/type/gapselect/classes/privacy/provider.php new file mode 100644 index 00000000000..d09a535ef0c --- /dev/null +++ b/question/type/gapselect/classes/privacy/provider.php @@ -0,0 +1,46 @@ +. + +/** + * Privacy Subsystem implementation for qtype_gapselect. + * + * @package qtype_gapselect + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace qtype_gapselect\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem for qtype_gapselect implementing null_provider. + * + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements \core_privacy\local\metadata\null_provider { + + /** + * Get the language string identifier with the component's language + * file to explain why this plugin stores no data. + * + * @return string + */ + public static function get_reason() { + return 'privacy:metadata'; + } +} diff --git a/question/type/gapselect/lang/en/qtype_gapselect.php b/question/type/gapselect/lang/en/qtype_gapselect.php index 6ec72e912e7..c743a0eeb62 100644 --- a/question/type/gapselect/lang/en/qtype_gapselect.php +++ b/question/type/gapselect/lang/en/qtype_gapselect.php @@ -41,6 +41,7 @@ $string['pluginname_link'] = 'question/type/gapselect'; $string['pluginnameadding'] = 'Adding a select missing words question'; $string['pluginnameediting'] = 'Editing a select missing words question'; $string['pluginnamesummary'] = 'Missing words in the question text are filled in using dropdown menus.'; +$string['privacy:metadata'] = 'The Select missing words plugin does not store any personal data.'; $string['shuffle'] = 'Shuffle'; $string['tagsnotallowed'] = '{$a->tag} is not allowed. (Only {$a->allowed} are permitted.)'; $string['tagsnotallowedatall'] = '{$a->tag} is not allowed. (No HTML is allowed here.)'; diff --git a/question/type/match/classes/privacy/provider.php b/question/type/match/classes/privacy/provider.php new file mode 100644 index 00000000000..3f1d42cc83b --- /dev/null +++ b/question/type/match/classes/privacy/provider.php @@ -0,0 +1,46 @@ +. + +/** + * Privacy Subsystem implementation for qtype_match. + * + * @package qtype_match + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace qtype_match\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem for qtype_match implementing null_provider. + * + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements \core_privacy\local\metadata\null_provider { + + /** + * Get the language string identifier with the component's language + * file to explain why this plugin stores no data. + * + * @return string + */ + public static function get_reason() { + return 'privacy:metadata'; + } +} diff --git a/question/type/match/lang/en/qtype_match.php b/question/type/match/lang/en/qtype_match.php index cfdc5ada455..9334ae57de8 100644 --- a/question/type/match/lang/en/qtype_match.php +++ b/question/type/match/lang/en/qtype_match.php @@ -42,3 +42,4 @@ $string['pluginname_link'] = 'question/type/match'; $string['pluginnameadding'] = 'Adding a Matching question'; $string['pluginnameediting'] = 'Editing a Matching question'; $string['pluginnamesummary'] = 'The answer to each of a number of sub-question must be selected from a list of possibilities.'; +$string['privacy:metadata'] = 'The Matching question type does not store any personal data.'; diff --git a/question/type/missingtype/classes/privacy/provider.php b/question/type/missingtype/classes/privacy/provider.php new file mode 100644 index 00000000000..c78840fa66e --- /dev/null +++ b/question/type/missingtype/classes/privacy/provider.php @@ -0,0 +1,46 @@ +. + +/** + * Privacy Subsystem implementation for qtype_missingtype. + * + * @package qtype_missingtype + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace qtype_missingtype\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem for qtype_missingtype implementing null_provider. + * + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements \core_privacy\local\metadata\null_provider { + + /** + * Get the language string identifier with the component's language + * file to explain why this plugin stores no data. + * + * @return string + */ + public static function get_reason() { + return 'privacy:metadata'; + } +} diff --git a/question/type/missingtype/lang/en/qtype_missingtype.php b/question/type/missingtype/lang/en/qtype_missingtype.php index 5aa684472dd..885598fab42 100644 --- a/question/type/missingtype/lang/en/qtype_missingtype.php +++ b/question/type/missingtype/lang/en/qtype_missingtype.php @@ -32,4 +32,5 @@ $string['missing'] = 'Question of a type that is not installed on this system'; $string['pluginname'] = 'Missing type'; $string['pluginnameadding'] = 'Adding a question of a type that is not installed on this system'; $string['pluginnameediting'] = 'Editing a question of a type that is not installed on this system'; +$string['privacy:metadata'] = 'The Missing type question type does not store any personal data.'; $string['warningmissingtype'] = 'This question is of a type that has not been installed on your Moodle yet.
Please alert your Moodle administrator.
'; diff --git a/question/type/multianswer/classes/privacy/provider.php b/question/type/multianswer/classes/privacy/provider.php new file mode 100644 index 00000000000..4aed279826a --- /dev/null +++ b/question/type/multianswer/classes/privacy/provider.php @@ -0,0 +1,46 @@ +. + +/** + * Privacy Subsystem implementation for qtype_multianswer. + * + * @package qtype_multianswer + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace qtype_multianswer\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem for qtype_multianswer implementing null_provider. + * + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements \core_privacy\local\metadata\null_provider { + + /** + * Get the language string identifier with the component's language + * file to explain why this plugin stores no data. + * + * @return string + */ + public static function get_reason() { + return 'privacy:metadata'; + } +} diff --git a/question/type/multianswer/lang/en/qtype_multianswer.php b/question/type/multianswer/lang/en/qtype_multianswer.php index ddbe942a83a..9a32ef19e13 100644 --- a/question/type/multianswer/lang/en/qtype_multianswer.php +++ b/question/type/multianswer/lang/en/qtype_multianswer.php @@ -44,6 +44,7 @@ $string['pluginname_link'] = 'question/type/multianswer'; $string['pluginnameadding'] = 'Adding an Embedded answers (Cloze) question'; $string['pluginnameediting'] = 'Editing an Embedded answers (Cloze) question'; $string['pluginnamesummary'] = 'Questions of this type are very flexible, but can only be created by entering text containing special codes that create embedded multiple-choice, short answers and numerical questions.'; +$string['privacy:metadata'] = 'The Embedded answers (Cloze) question type plugin does not store any personal data.'; $string['qtypenotrecognized'] = 'questiontype {$a} not recognized'; $string['questiondefinition'] = 'Question definition'; $string['questiondeleted'] = 'Question deleted'; diff --git a/question/type/multichoice/classes/privacy/provider.php b/question/type/multichoice/classes/privacy/provider.php new file mode 100644 index 00000000000..95bf5c26715 --- /dev/null +++ b/question/type/multichoice/classes/privacy/provider.php @@ -0,0 +1,46 @@ +. + +/** + * Privacy Subsystem implementation for qtype_multichoice. + * + * @package qtype_multichoice + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace qtype_multichoice\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem for qtype_multichoice implementing null_provider. + * + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements \core_privacy\local\metadata\null_provider { + + /** + * Get the language string identifier with the component's language + * file to explain why this plugin stores no data. + * + * @return string + */ + public static function get_reason() { + return 'privacy:metadata'; + } +} diff --git a/question/type/multichoice/lang/en/qtype_multichoice.php b/question/type/multichoice/lang/en/qtype_multichoice.php index 73055521d4d..493173c7b04 100644 --- a/question/type/multichoice/lang/en/qtype_multichoice.php +++ b/question/type/multichoice/lang/en/qtype_multichoice.php @@ -62,6 +62,7 @@ $string['pluginname_link'] = 'question/type/multichoice'; $string['pluginnameadding'] = 'Adding a Multiple choice question'; $string['pluginnameediting'] = 'Editing a Multiple choice question'; $string['pluginnamesummary'] = 'Allows the selection of a single or multiple responses from a pre-defined list.'; +$string['privacy:metadata'] = 'The Multiple choice question type does not store any personal data.'; $string['selectmulti'] = 'Select one or more:'; $string['selectone'] = 'Select one:'; $string['shuffleanswers'] = 'Shuffle the choices?'; diff --git a/question/type/numerical/classes/privacy/provider.php b/question/type/numerical/classes/privacy/provider.php new file mode 100644 index 00000000000..faa7f0d55a9 --- /dev/null +++ b/question/type/numerical/classes/privacy/provider.php @@ -0,0 +1,46 @@ +. + +/** + * Privacy Subsystem implementation for qtype_numerical. + * + * @package qtype_numerical + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace qtype_numerical\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem for qtype_numerical implementing null_provider. + * + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements \core_privacy\local\metadata\null_provider { + + /** + * Get the language string identifier with the component's language + * file to explain why this plugin stores no data. + * + * @return string + */ + public static function get_reason() { + return 'privacy:metadata'; + } +} diff --git a/question/type/numerical/lang/en/qtype_numerical.php b/question/type/numerical/lang/en/qtype_numerical.php index 2757b2f7384..21eab957ba3 100644 --- a/question/type/numerical/lang/en/qtype_numerical.php +++ b/question/type/numerical/lang/en/qtype_numerical.php @@ -67,6 +67,7 @@ $string['pluginname_link'] = 'question/type/numerical'; $string['pluginnameadding'] = 'Adding a Numerical question'; $string['pluginnameediting'] = 'Editing a Numerical question'; $string['pluginnamesummary'] = 'Allows a numerical response, possibly with units, that is graded by comparing against various model answers, possibly with tolerances.'; +$string['privacy:metadata'] = 'The Numerical question type plugin does not store any personal data.'; $string['relative'] = 'Relative'; $string['rightexample'] = 'on the right, for example 1.00cm or 1.00km'; $string['selectunits'] = 'Select units'; diff --git a/question/type/random/classes/privacy/provider.php b/question/type/random/classes/privacy/provider.php new file mode 100644 index 00000000000..79b3ecfc50d --- /dev/null +++ b/question/type/random/classes/privacy/provider.php @@ -0,0 +1,46 @@ +. + +/** + * Privacy Subsystem implementation for qtype_random. + * + * @package qtype_random + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace qtype_random\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem for qtype_random implementing null_provider. + * + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements \core_privacy\local\metadata\null_provider { + + /** + * Get the language string identifier with the component's language + * file to explain why this plugin stores no data. + * + * @return string + */ + public static function get_reason() { + return 'privacy:metadata'; + } +} diff --git a/question/type/random/lang/en/qtype_random.php b/question/type/random/lang/en/qtype_random.php index 0e15605c38f..d411e463100 100644 --- a/question/type/random/lang/en/qtype_random.php +++ b/question/type/random/lang/en/qtype_random.php @@ -28,6 +28,7 @@ $string['includingsubcategories'] = 'Including subcategories'; $string['pluginname'] = 'Random'; $string['pluginname_help'] = 'A random question is not a question type as such, but is a way of inserting a randomly-chosen question from a specified category into an activity.'; $string['pluginnameediting'] = 'Editing a random question'; +$string['privacy:metadata'] = 'The Random question type plugin does not store any personal data.'; $string['randomqname'] = 'Random ({$a})'; $string['randomqplusname'] = 'Random ({$a} and subcategories)'; $string['selectedby'] = '{$a->questionname} selected by {$a->randomname}'; diff --git a/question/type/randomsamatch/classes/privacy/provider.php b/question/type/randomsamatch/classes/privacy/provider.php new file mode 100644 index 00000000000..94585de49a2 --- /dev/null +++ b/question/type/randomsamatch/classes/privacy/provider.php @@ -0,0 +1,46 @@ +. + +/** + * Privacy Subsystem implementation for qtype_randomsamatch. + * + * @package qtype_randomsamatch + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace qtype_randomsamatch\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem for qtype_randomsamatch implementing null_provider. + * + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements \core_privacy\local\metadata\null_provider { + + /** + * Get the language string identifier with the component's language + * file to explain why this plugin stores no data. + * + * @return string + */ + public static function get_reason() { + return 'privacy:metadata'; + } +} diff --git a/question/type/randomsamatch/lang/en/qtype_randomsamatch.php b/question/type/randomsamatch/lang/en/qtype_randomsamatch.php index 82848d94c43..69813ad2a2e 100644 --- a/question/type/randomsamatch/lang/en/qtype_randomsamatch.php +++ b/question/type/randomsamatch/lang/en/qtype_randomsamatch.php @@ -32,6 +32,7 @@ $string['pluginname_link'] = 'question/type/randomsamatch'; $string['pluginnameadding'] = 'Adding a Random short-answer matching question'; $string['pluginnameediting'] = 'Editing a Random short-answer matching question'; $string['pluginnamesummary'] = 'Like a Matching question, but created randomly from the short answer questions in a particular category.'; +$string['privacy:metadata'] = 'The Random short-answer matching question type does not store any personal data.'; $string['randomsamatchnumber'] = 'Number of questions to select'; $string['randomsamatch'] = 'Random short-answer matching'; $string['randomsamatchintro'] = 'For each of the following questions, select the matching answer from the menu.'; diff --git a/question/type/shortanswer/classes/privacy/provider.php b/question/type/shortanswer/classes/privacy/provider.php new file mode 100644 index 00000000000..b4395a3732b --- /dev/null +++ b/question/type/shortanswer/classes/privacy/provider.php @@ -0,0 +1,46 @@ +. + +/** + * Privacy Subsystem implementation for qtype_shortanswer. + * + * @package qtype_shortanswer + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace qtype_shortanswer\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem for qtype_shortanswer implementing null_provider. + * + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements \core_privacy\local\metadata\null_provider { + + /** + * Get the language string identifier with the component's language + * file to explain why this plugin stores no data. + * + * @return string + */ + public static function get_reason() { + return 'privacy:metadata'; + } +} diff --git a/question/type/shortanswer/lang/en/qtype_shortanswer.php b/question/type/shortanswer/lang/en/qtype_shortanswer.php index 0f42cf6a4ab..fca0040aeb6 100644 --- a/question/type/shortanswer/lang/en/qtype_shortanswer.php +++ b/question/type/shortanswer/lang/en/qtype_shortanswer.php @@ -41,3 +41,4 @@ $string['pluginname_link'] = 'question/type/shortanswer'; $string['pluginnameadding'] = 'Adding a short answer question'; $string['pluginnameediting'] = 'Editing a Short answer question'; $string['pluginnamesummary'] = 'Allows a response of one or a few words that is graded by comparing against various model answers, which may contain wildcards.'; +$string['privacy:metadata'] = 'The Short answer question type does not store any personal data.'; diff --git a/question/type/truefalse/classes/privacy/provider.php b/question/type/truefalse/classes/privacy/provider.php new file mode 100644 index 00000000000..0d9a940c92a --- /dev/null +++ b/question/type/truefalse/classes/privacy/provider.php @@ -0,0 +1,46 @@ +. + +/** + * Privacy Subsystem implementation for qtype_truefalse. + * + * @package qtype_truefalse + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace qtype_truefalse\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem for qtype_truefalse implementing null_provider. + * + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements \core_privacy\local\metadata\null_provider { + + /** + * Get the language string identifier with the component's language + * file to explain why this plugin stores no data. + * + * @return string + */ + public static function get_reason() { + return 'privacy:metadata'; + } +} diff --git a/question/type/truefalse/lang/en/qtype_truefalse.php b/question/type/truefalse/lang/en/qtype_truefalse.php index 42ee0706505..872d858662b 100644 --- a/question/type/truefalse/lang/en/qtype_truefalse.php +++ b/question/type/truefalse/lang/en/qtype_truefalse.php @@ -38,3 +38,4 @@ $string['pluginname_link'] = 'question/type/truefalse'; $string['pluginnameadding'] = 'Adding a True/False question'; $string['pluginnameediting'] = 'Editing a True/False question'; $string['pluginnamesummary'] = 'A simple form of multiple choice question with just the two choices \'True\' and \'False\'.'; +$string['privacy:metadata'] = 'The True/False question type plugin does not store any personal data.'; From 0a8a57943c0c5d2434acc49ee0ac46f05729cffc Mon Sep 17 00:00:00 2001 From: Andrew Nicols Date: Sun, 8 Apr 2018 13:21:47 +0800 Subject: [PATCH 08/10] MDL-61407 quiz: Add reports for quiz --- .../grading/classes/privacy/provider.php | 46 ++++++ .../report/grading/lang/en/quiz_grading.php | 1 + .../overview/classes/privacy/provider.php | 78 ++++++++++ .../report/overview/lang/en/quiz_overview.php | 3 + .../overview/tests/privacy_provider_test.php | 99 +++++++++++++ .../responses/classes/privacy/provider.php | 99 +++++++++++++ .../responses/lang/en/quiz_responses.php | 7 + .../responses/tests/privacy_provider_test.php | 139 ++++++++++++++++++ .../statistics/classes/privacy/provider.php | 46 ++++++ .../statistics/lang/en/quiz_statistics.php | 2 +- 10 files changed, 519 insertions(+), 1 deletion(-) create mode 100644 mod/quiz/report/grading/classes/privacy/provider.php create mode 100644 mod/quiz/report/overview/classes/privacy/provider.php create mode 100644 mod/quiz/report/overview/tests/privacy_provider_test.php create mode 100644 mod/quiz/report/responses/classes/privacy/provider.php create mode 100644 mod/quiz/report/responses/tests/privacy_provider_test.php create mode 100644 mod/quiz/report/statistics/classes/privacy/provider.php diff --git a/mod/quiz/report/grading/classes/privacy/provider.php b/mod/quiz/report/grading/classes/privacy/provider.php new file mode 100644 index 00000000000..797cc02cc77 --- /dev/null +++ b/mod/quiz/report/grading/classes/privacy/provider.php @@ -0,0 +1,46 @@ +. + +/** + * Privacy Subsystem implementation for quiz_grading. + * + * @package quiz_grading + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace quiz_grading\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem for quiz_grading implementing null_provider. + * + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements \core_privacy\local\metadata\null_provider { + + /** + * Get the language string identifier with the component's language + * file to explain why this plugin stores no data. + * + * @return string + */ + public static function get_reason() { + return 'privacy:metadata'; + } +} diff --git a/mod/quiz/report/grading/lang/en/quiz_grading.php b/mod/quiz/report/grading/lang/en/quiz_grading.php index 2fda255e22e..6f53c956909 100644 --- a/mod/quiz/report/grading/lang/en/quiz_grading.php +++ b/mod/quiz/report/grading/lang/en/quiz_grading.php @@ -68,6 +68,7 @@ $string['nothingfound'] = 'Nothing to display'; $string['options'] = 'Options'; $string['orderattempts'] = 'Order attempts'; $string['pluginname'] = 'Manual grading'; +$string['privacy:metadata'] = 'The Quiz Manual grading plugin does not store any personal data. It provides an interface for users to store data without storing any data itself.'; $string['qno'] = 'Q #'; $string['questionname'] = 'Question name'; $string['questionsperpage'] = 'Questions per page'; diff --git a/mod/quiz/report/overview/classes/privacy/provider.php b/mod/quiz/report/overview/classes/privacy/provider.php new file mode 100644 index 00000000000..c2077fc5320 --- /dev/null +++ b/mod/quiz/report/overview/classes/privacy/provider.php @@ -0,0 +1,78 @@ +. + +/** + * Privacy Subsystem implementation for quiz_overview.. + * + * @package quiz_overview + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace quiz_overview\privacy; + +use \core_privacy\local\request\writer; +use \core_privacy\local\request\transform; +use \core_privacy\local\metadata\collection; +use \core_privacy\manager; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem implementation for quiz_overview.. + * + * @copyright 2018 Andrew Nicols + * @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\user_preference_provider { + + /** + * Returns meta data about this system. + * + * @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->add_user_preference('quiz_overview_slotmarks', 'privacy:metadata:preference:quiz_overview_slotmarks'); + + return $collection; + } + + /** + * Export all user preferences for the plugin. + * + * @param int $userid The userid of the user whose data is to be exported. + */ + public static function export_user_preferences($userid) { + $preference = get_user_preferences('quiz_overview_slotmarks', null); + if (null !== $preference) { + if (empty($preference)) { + $description = get_string('privacy:preference:slotmarks:no', 'quiz_overview'); + } else { + $description = get_string('privacy:preference:slotmarks:yes', 'quiz_overview'); + } + + writer::export_user_preference( + 'quiz_overview', + 'slotmarks', + transform::yesno($preference), + $description + ); + } + } +} diff --git a/mod/quiz/report/overview/lang/en/quiz_overview.php b/mod/quiz/report/overview/lang/en/quiz_overview.php index dc188c22a72..60d89679a8e 100644 --- a/mod/quiz/report/overview/lang/en/quiz_overview.php +++ b/mod/quiz/report/overview/lang/en/quiz_overview.php @@ -53,6 +53,9 @@ $string['pluginname'] = 'Grades'; $string['preferencespage'] = 'Preferences just for this page'; $string['preferencessave'] = 'Show report'; $string['preferencesuser'] = 'Your preferences for this report'; +$string['privacy:metadata:preference:quiz_overview_slotmarks'] = 'Whether to show marks for each question slot.'; +$string['privacy:preference:slotmarks:yes'] = 'Marks are shown alongside the question slot.'; +$string['privacy:preference:slotmarks:no'] = 'Marks are not shown alongside the question slot.'; $string['regrade'] = 'Regrade'; $string['regradeall'] = 'Regrade all'; $string['regradealldry'] = 'Dry run a full regrade'; diff --git a/mod/quiz/report/overview/tests/privacy_provider_test.php b/mod/quiz/report/overview/tests/privacy_provider_test.php new file mode 100644 index 00000000000..de59fcaa31f --- /dev/null +++ b/mod/quiz/report/overview/tests/privacy_provider_test.php @@ -0,0 +1,99 @@ +. + +/** + * Privacy provider tests. + * + * @package quiz_overview + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +use core_privacy\local\metadata\collection; +use quiz_overview\privacy\provider; +use core_privacy\local\request\writer; +use core_privacy\local\request\transform; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy provider tests class. + * + * @package quiz_overview + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class quiz_overview_privacy_provider_testcase extends \core_privacy\tests\provider_testcase { + /** + * When no preference exists, there should be no export. + */ + public function test_preference_unset() { + global $USER; + + $this->resetAfterTest(); + $this->setAdminUser(); + + provider::export_user_preferences($USER->id); + + $this->assertFalse(writer::with_context(\context_system::instance())->has_any_data()); + } + + /** + * Preference does exist. + */ + public function test_preference_yes() { + global $USER; + + $this->resetAfterTest(); + $this->setAdminUser(); + + set_user_preference('quiz_overview_slotmarks', 1); + + provider::export_user_preferences($USER->id); + + $writer = writer::with_context(\context_system::instance()); + $this->assertTrue($writer->has_any_data()); + + $preferences = $writer->get_user_preferences('quiz_overview'); + $this->assertNotEmpty($preferences->slotmarks); + $this->assertEquals(transform::yesno(1), $preferences->slotmarks->value); + $description = get_string('privacy:preference:slotmarks:yes', 'quiz_overview'); + $this->assertEquals($description, $preferences->slotmarks->description); + } + + /** + * Preference does exist and is no. + */ + public function test_preference_no() { + global $USER; + + $this->resetAfterTest(); + $this->setAdminUser(); + + set_user_preference('quiz_overview_slotmarks', 0); + + provider::export_user_preferences($USER->id); + + $writer = writer::with_context(\context_system::instance()); + $this->assertTrue($writer->has_any_data()); + + $preferences = $writer->get_user_preferences('quiz_overview'); + $this->assertNotEmpty($preferences->slotmarks); + $this->assertEquals(transform::yesno(0), $preferences->slotmarks->value); + $description = get_string('privacy:preference:slotmarks:no', 'quiz_overview'); + $this->assertEquals($description, $preferences->slotmarks->description); + } +} diff --git a/mod/quiz/report/responses/classes/privacy/provider.php b/mod/quiz/report/responses/classes/privacy/provider.php new file mode 100644 index 00000000000..95b21c9405b --- /dev/null +++ b/mod/quiz/report/responses/classes/privacy/provider.php @@ -0,0 +1,99 @@ +. + +/** + * Privacy Subsystem implementation for quiz_responses. + * + * @package quiz_responses + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace quiz_responses\privacy; + +use core_privacy\local\metadata\collection; +use core_privacy\local\request\writer; +use core_privacy\local\request\transform; + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->dirroot . '/question/engine/questionattempt.php'); + +/** + * Privacy Subsystem for quiz_responses with user preferences. + * + * @copyright 2018 Andrew Nicols + * @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\user_preference_provider { + + /** + * Returns meta data about this system. + * + * @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->add_user_preference('quiz_report_responses_qtext', 'privacy:preference:qtext'); + $collection->add_user_preference('quiz_report_responses_resp', 'privacy:preference:resp'); + $collection->add_user_preference('quiz_report_responses_right', 'privacy:preference:right'); + $collection->add_user_preference('quiz_report_responses_which_tries', 'privacy:preference:which_tries'); + + return $collection; + } + + /** + * Export all user preferences for the plugin. + * + * @param int $userid The userid of the user whose data is to be exported. + */ + public static function export_user_preferences($userid) { + $preferences = [ + 'qtext', + 'resp', + 'right', + ]; + + foreach ($preferences as $key) { + $preference = get_user_preferences("quiz_report_responses_{$key}", null, $userid); + if (null !== $preference) { + $desc = get_string("privacy:preference:{$key}", 'quiz_responses'); + writer::export_user_preference('quiz_responses', $key, transform::yesno($preference), $desc); + } + } + + $preference = get_user_preferences("quiz_report_responses_which_tries", null, $userid); + if (null !== $preference) { + switch($preference) { + case \question_attempt::FIRST_TRY: + $value = get_string("privacy:preference:which_tries:first", 'quiz_responses'); + break; + case \question_attempt::LAST_TRY: + $value = get_string("privacy:preference:which_tries:last", 'quiz_responses'); + break; + case \question_attempt::ALL_TRIES: + $value = get_string("privacy:preference:which_tries:all", 'quiz_responses'); + break; + } + $desc = get_string("privacy:preference:which_tries", 'quiz_responses'); + + writer::export_user_preference('quiz_responses', 'which_tries', $value, $desc); + } + } +} diff --git a/mod/quiz/report/responses/lang/en/quiz_responses.php b/mod/quiz/report/responses/lang/en/quiz_responses.php index 7afc8a6973a..7bc319b993e 100644 --- a/mod/quiz/report/responses/lang/en/quiz_responses.php +++ b/mod/quiz/report/responses/lang/en/quiz_responses.php @@ -27,6 +27,13 @@ $string['include'] = 'Include the'; $string['mustselectcols'] = 'You must include something.'; $string['pagesize'] = 'Page size'; $string['pluginname'] = 'Responses'; +$string['privacy:preference:qtext'] = 'Whether to show the question text columns.'; +$string['privacy:preference:resp'] = 'Whether to show the students\' response columns.'; +$string['privacy:preference:right'] = 'Whether to show the correct response columns.'; +$string['privacy:preference:which_tries'] = 'Which tries to show responses from.'; +$string['privacy:preference:which_tries:first'] = 'The first try at a question during an attempt by a user.'; +$string['privacy:preference:which_tries:last'] = 'The last try at a question during an attempt by a user.'; +$string['privacy:preference:which_tries:all'] = 'All tries at a question during an attempt by a user.'; $string['questiontext'] = 'question text'; $string['reportresponses'] = 'Responses'; $string['response'] = 'response'; diff --git a/mod/quiz/report/responses/tests/privacy_provider_test.php b/mod/quiz/report/responses/tests/privacy_provider_test.php new file mode 100644 index 00000000000..d39aa5b36d0 --- /dev/null +++ b/mod/quiz/report/responses/tests/privacy_provider_test.php @@ -0,0 +1,139 @@ +. + +/** + * Privacy provider tests. + * + * @package quiz_responses + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +use core_privacy\local\metadata\collection; +use quiz_responses\privacy\provider; +use core_privacy\local\request\writer; +use core_privacy\local\request\transform; + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->dirroot . '/question/engine/questionattempt.php'); + +/** + * Privacy provider tests class. + * + * @package quiz_responses + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class quiz_responses_privacy_provider_testcase extends \core_privacy\tests\provider_testcase { + /** + * When no preference exists, there should be no export. + */ + public function test_preference_unset() { + global $USER; + + $this->resetAfterTest(); + $this->setAdminUser(); + + provider::export_user_preferences($USER->id); + + $this->assertFalse(writer::with_context(\context_system::instance())->has_any_data()); + } + + /** + * Preference does exist. + */ + public function test_preference_bool_true() { + global $USER; + + $this->resetAfterTest(); + $this->setAdminUser(); + + set_user_preference('quiz_report_responses_qtext', true); + set_user_preference('quiz_report_responses_resp', true); + set_user_preference('quiz_report_responses_right', true); + + provider::export_user_preferences($USER->id); + + $writer = writer::with_context(\context_system::instance()); + $this->assertTrue($writer->has_any_data()); + + $preferences = $writer->get_user_preferences('quiz_responses'); + + $this->assertNotEmpty($preferences->qtext); + $this->assertEquals(transform::yesno(1), $preferences->qtext->value); + + $this->assertNotEmpty($preferences->resp); + $this->assertEquals(transform::yesno(1), $preferences->resp->value); + + $this->assertNotEmpty($preferences->right); + $this->assertEquals(transform::yesno(1), $preferences->right->value); + } + + /** + * Preference does exist. + */ + public function test_preference_bool_false() { + global $USER; + + $this->resetAfterTest(); + $this->setAdminUser(); + + set_user_preference('quiz_report_responses_qtext', false); + set_user_preference('quiz_report_responses_resp', false); + set_user_preference('quiz_report_responses_right', false); + + provider::export_user_preferences($USER->id); + + $writer = writer::with_context(\context_system::instance()); + $this->assertTrue($writer->has_any_data()); + + $preferences = $writer->get_user_preferences('quiz_responses'); + + $this->assertNotEmpty($preferences->qtext); + $this->assertEquals(transform::yesno(0), $preferences->qtext->value); + + $this->assertNotEmpty($preferences->resp); + $this->assertEquals(transform::yesno(0), $preferences->resp->value); + + $this->assertNotEmpty($preferences->right); + $this->assertEquals(transform::yesno(0), $preferences->right->value); + } + + /** + * Preference does exist. + */ + public function test_preference_bool_which_first() { + global $USER; + + $this->resetAfterTest(); + $this->setAdminUser(); + + set_user_preference('quiz_report_responses_which_tries', question_attempt::FIRST_TRY); + + provider::export_user_preferences($USER->id); + + $writer = writer::with_context(\context_system::instance()); + $this->assertTrue($writer->has_any_data()); + + $preferences = $writer->get_user_preferences('quiz_responses'); + + $expected = get_string("privacy:preference:which_tries:first", 'quiz_responses'); + $this->assertNotEmpty($preferences->which_tries); + $this->assertEquals($expected, $preferences->which_tries->value); + } +} diff --git a/mod/quiz/report/statistics/classes/privacy/provider.php b/mod/quiz/report/statistics/classes/privacy/provider.php new file mode 100644 index 00000000000..f083a890668 --- /dev/null +++ b/mod/quiz/report/statistics/classes/privacy/provider.php @@ -0,0 +1,46 @@ +. + +/** + * Privacy Subsystem implementation for quiz_statistics. + * + * @package quiz_statistics + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace quiz_statistics\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem for quiz_statistics implementing null_provider. + * + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements \core_privacy\local\metadata\null_provider { + + /** + * Get the language string identifier with the component's language + * file to explain why this plugin stores no data. + * + * @return string + */ + public static function get_reason() { + return 'privacy:metadata'; + } +} diff --git a/mod/quiz/report/statistics/lang/en/quiz_statistics.php b/mod/quiz/report/statistics/lang/en/quiz_statistics.php index 64f562cbadc..38895d02e13 100644 --- a/mod/quiz/report/statistics/lang/en/quiz_statistics.php +++ b/mod/quiz/report/statistics/lang/en/quiz_statistics.php @@ -86,6 +86,7 @@ $string['nostudentsingroup'] = 'There are no students in this group yet'; $string['optiongrade'] = 'Partial credit'; $string['partofquestion'] = 'Part of question'; $string['pluginname'] = 'Statistics'; +$string['privacy:metadata'] = 'Although the Quiz Statistics plugin has database tables, the data is aggregate data and does not describe a unique indidividual.'; $string['position'] = 'Position'; $string['positions'] = 'Position(s)'; $string['questioninformation'] = 'Question information'; @@ -115,4 +116,3 @@ $string['statistics:view'] = 'View statistics report'; $string['statsfor'] = 'Quiz statistics (for {$a})'; $string['variant'] = 'Variant'; $string['whichtries'] = 'Analyze responses for'; - From 07049468e6366531e486f749a2bede425c2bb763 Mon Sep 17 00:00:00 2001 From: Andrew Nicols Date: Tue, 24 Apr 2018 13:35:05 +0800 Subject: [PATCH 09/10] MDL-61407 quizaccess: Add null_writer implementations --- .../classes/privacy/provider.php | 46 +++++++++++++++++++ .../en/quizaccess_delaybetweenattempts.php | 1 + .../ipaddress/classes/privacy/provider.php | 46 +++++++++++++++++++ .../lang/en/quizaccess_ipaddress.php | 1 + .../numattempts/classes/privacy/provider.php | 46 +++++++++++++++++++ .../lang/en/quizaccess_numattempts.php | 1 + .../classes/privacy/provider.php | 46 +++++++++++++++++++ .../lang/en/quizaccess_offlineattempts.php | 2 +- .../classes/privacy/provider.php | 46 +++++++++++++++++++ .../lang/en/quizaccess_openclosedate.php | 1 + .../password/classes/privacy/provider.php | 46 +++++++++++++++++++ .../password/lang/en/quizaccess_password.php | 1 + .../safebrowser/classes/privacy/provider.php | 46 +++++++++++++++++++ .../lang/en/quizaccess_safebrowser.php | 1 + .../securewindow/classes/privacy/provider.php | 46 +++++++++++++++++++ .../lang/en/quizaccess_securewindow.php | 1 + .../timelimit/classes/privacy/provider.php | 46 +++++++++++++++++++ .../lang/en/quizaccess_timelimit.php | 1 + 18 files changed, 423 insertions(+), 1 deletion(-) create mode 100644 mod/quiz/accessrule/delaybetweenattempts/classes/privacy/provider.php create mode 100644 mod/quiz/accessrule/ipaddress/classes/privacy/provider.php create mode 100644 mod/quiz/accessrule/numattempts/classes/privacy/provider.php create mode 100644 mod/quiz/accessrule/offlineattempts/classes/privacy/provider.php create mode 100644 mod/quiz/accessrule/openclosedate/classes/privacy/provider.php create mode 100644 mod/quiz/accessrule/password/classes/privacy/provider.php create mode 100644 mod/quiz/accessrule/safebrowser/classes/privacy/provider.php create mode 100644 mod/quiz/accessrule/securewindow/classes/privacy/provider.php create mode 100644 mod/quiz/accessrule/timelimit/classes/privacy/provider.php diff --git a/mod/quiz/accessrule/delaybetweenattempts/classes/privacy/provider.php b/mod/quiz/accessrule/delaybetweenattempts/classes/privacy/provider.php new file mode 100644 index 00000000000..a0a2f675403 --- /dev/null +++ b/mod/quiz/accessrule/delaybetweenattempts/classes/privacy/provider.php @@ -0,0 +1,46 @@ +. + +/** + * Privacy Subsystem implementation for quizaccess_delaybetweenattempts. + * + * @package quizaccess_delaybetweenattempts + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace quizaccess_delaybetweenattempts\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem for quizaccess_delaybetweenattempts implementing null_provider. + * + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements \core_privacy\local\metadata\null_provider { + + /** + * Get the language string identifier with the component's language + * file to explain why this plugin stores no data. + * + * @return string + */ + public static function get_reason() { + return 'privacy:metadata'; + } +} diff --git a/mod/quiz/accessrule/delaybetweenattempts/lang/en/quizaccess_delaybetweenattempts.php b/mod/quiz/accessrule/delaybetweenattempts/lang/en/quizaccess_delaybetweenattempts.php index 8b9fdcdce98..07715178b63 100644 --- a/mod/quiz/accessrule/delaybetweenattempts/lang/en/quizaccess_delaybetweenattempts.php +++ b/mod/quiz/accessrule/delaybetweenattempts/lang/en/quizaccess_delaybetweenattempts.php @@ -28,5 +28,6 @@ defined('MOODLE_INTERNAL') || die(); $string['pluginname'] = 'Delay between attempts quiz access rule'; +$string['privacy:metadata'] = 'The Delay between attempts quiz access rule plugin does not store any personal data.'; $string['youcannotwait'] = 'This quiz closes before you will be allowed to start another attempt.'; $string['youmustwait'] = 'You must wait before you may re-attempt this quiz. You will be allowed to start another attempt after {$a}.'; diff --git a/mod/quiz/accessrule/ipaddress/classes/privacy/provider.php b/mod/quiz/accessrule/ipaddress/classes/privacy/provider.php new file mode 100644 index 00000000000..b468237746d --- /dev/null +++ b/mod/quiz/accessrule/ipaddress/classes/privacy/provider.php @@ -0,0 +1,46 @@ +. + +/** + * Privacy Subsystem implementation for quizaccess_ipaddress. + * + * @package quizaccess_ipaddress + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace quizaccess_ipaddress\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem for quizaccess_ipaddress implementing null_provider. + * + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements \core_privacy\local\metadata\null_provider { + + /** + * Get the language string identifier with the component's language + * file to explain why this plugin stores no data. + * + * @return string + */ + public static function get_reason() { + return 'privacy:metadata'; + } +} diff --git a/mod/quiz/accessrule/ipaddress/lang/en/quizaccess_ipaddress.php b/mod/quiz/accessrule/ipaddress/lang/en/quizaccess_ipaddress.php index 984b26632db..cb8b66fb520 100644 --- a/mod/quiz/accessrule/ipaddress/lang/en/quizaccess_ipaddress.php +++ b/mod/quiz/accessrule/ipaddress/lang/en/quizaccess_ipaddress.php @@ -28,4 +28,5 @@ defined('MOODLE_INTERNAL') || die(); $string['pluginname'] = 'IP address quiz access rule'; +$string['privacy:metadata'] = 'The IP address quiz access rule plugin does not store any personal data.'; $string['subnetwrong'] = 'This quiz is only accessible from certain locations, and this computer is not on the allowed list.'; diff --git a/mod/quiz/accessrule/numattempts/classes/privacy/provider.php b/mod/quiz/accessrule/numattempts/classes/privacy/provider.php new file mode 100644 index 00000000000..f9e344d4db3 --- /dev/null +++ b/mod/quiz/accessrule/numattempts/classes/privacy/provider.php @@ -0,0 +1,46 @@ +. + +/** + * Privacy Subsystem implementation for quizaccess_numattempts. + * + * @package quizaccess_numattempts + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace quizaccess_numattempts\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem for quizaccess_numattempts implementing null_provider. + * + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements \core_privacy\local\metadata\null_provider { + + /** + * Get the language string identifier with the component's language + * file to explain why this plugin stores no data. + * + * @return string + */ + public static function get_reason() { + return 'privacy:metadata'; + } +} diff --git a/mod/quiz/accessrule/numattempts/lang/en/quizaccess_numattempts.php b/mod/quiz/accessrule/numattempts/lang/en/quizaccess_numattempts.php index b778ba03e7d..d1a8f2e1c12 100644 --- a/mod/quiz/accessrule/numattempts/lang/en/quizaccess_numattempts.php +++ b/mod/quiz/accessrule/numattempts/lang/en/quizaccess_numattempts.php @@ -29,3 +29,4 @@ defined('MOODLE_INTERNAL') || die(); $string['attemptsallowedn'] = 'Attempts allowed: {$a}'; $string['pluginname'] = 'Number of attempts quiz access rule'; +$string['privacy:metadata'] = 'The Number of attempts quiz access rule plugin does not store any personal data.'; diff --git a/mod/quiz/accessrule/offlineattempts/classes/privacy/provider.php b/mod/quiz/accessrule/offlineattempts/classes/privacy/provider.php new file mode 100644 index 00000000000..80802e4431a --- /dev/null +++ b/mod/quiz/accessrule/offlineattempts/classes/privacy/provider.php @@ -0,0 +1,46 @@ +. + +/** + * Privacy Subsystem implementation for quizaccess_offlineattempts. + * + * @package quizaccess_offlineattempts + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace quizaccess_offlineattempts\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem for quizaccess_offlineattempts implementing null_provider. + * + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements \core_privacy\local\metadata\null_provider { + + /** + * Get the language string identifier with the component's language + * file to explain why this plugin stores no data. + * + * @return string + */ + public static function get_reason() { + return 'privacy:metadata'; + } +} diff --git a/mod/quiz/accessrule/offlineattempts/lang/en/quizaccess_offlineattempts.php b/mod/quiz/accessrule/offlineattempts/lang/en/quizaccess_offlineattempts.php index 2ae58be00aa..69e71008cf4 100644 --- a/mod/quiz/accessrule/offlineattempts/lang/en/quizaccess_offlineattempts.php +++ b/mod/quiz/accessrule/offlineattempts/lang/en/quizaccess_offlineattempts.php @@ -34,4 +34,4 @@ $string['offlineattemptserror'] = 'It is not possible for a quiz to be attempted $string['offlinedatamessage'] = 'You have worked on this attempt using a mobile device. Data was last saved to this site {$a} ago.'; $string['pleaseconfirm'] = 'Please check and confirm that you do not have any unsaved work.'; $string['pluginname'] = 'Offline attempts access rule'; - +$string['privacy:metadata'] = 'The Offline attempts quiz access rule plugin does not store any personal data.'; diff --git a/mod/quiz/accessrule/openclosedate/classes/privacy/provider.php b/mod/quiz/accessrule/openclosedate/classes/privacy/provider.php new file mode 100644 index 00000000000..e3d75e20e70 --- /dev/null +++ b/mod/quiz/accessrule/openclosedate/classes/privacy/provider.php @@ -0,0 +1,46 @@ +. + +/** + * Privacy Subsystem implementation for quizaccess_openclosedate. + * + * @package quizaccess_openclosedate + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace quizaccess_openclosedate\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem for quizaccess_openclosedate implementing null_provider. + * + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements \core_privacy\local\metadata\null_provider { + + /** + * Get the language string identifier with the component's language + * file to explain why this plugin stores no data. + * + * @return string + */ + public static function get_reason() { + return 'privacy:metadata'; + } +} diff --git a/mod/quiz/accessrule/openclosedate/lang/en/quizaccess_openclosedate.php b/mod/quiz/accessrule/openclosedate/lang/en/quizaccess_openclosedate.php index a7bdcc32246..8e5191d3591 100644 --- a/mod/quiz/accessrule/openclosedate/lang/en/quizaccess_openclosedate.php +++ b/mod/quiz/accessrule/openclosedate/lang/en/quizaccess_openclosedate.php @@ -29,4 +29,5 @@ defined('MOODLE_INTERNAL') || die(); $string['notavailable'] = 'This quiz is not currently available'; $string['pluginname'] = 'Open and close date access rule'; +$string['privacy:metadata'] = 'The Open and close date quiz access rule plugin does not store any personal data.'; $string['quiznotavailable'] = 'The quiz will not be available until {$a}'; diff --git a/mod/quiz/accessrule/password/classes/privacy/provider.php b/mod/quiz/accessrule/password/classes/privacy/provider.php new file mode 100644 index 00000000000..fc9b81d055c --- /dev/null +++ b/mod/quiz/accessrule/password/classes/privacy/provider.php @@ -0,0 +1,46 @@ +. + +/** + * Privacy Subsystem implementation for quizaccess_password. + * + * @package quizaccess_password + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace quizaccess_password\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem for quizaccess_password implementing null_provider. + * + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements \core_privacy\local\metadata\null_provider { + + /** + * Get the language string identifier with the component's language + * file to explain why this plugin stores no data. + * + * @return string + */ + public static function get_reason() { + return 'privacy:metadata'; + } +} diff --git a/mod/quiz/accessrule/password/lang/en/quizaccess_password.php b/mod/quiz/accessrule/password/lang/en/quizaccess_password.php index 16721144381..6a58602a966 100644 --- a/mod/quiz/accessrule/password/lang/en/quizaccess_password.php +++ b/mod/quiz/accessrule/password/lang/en/quizaccess_password.php @@ -28,5 +28,6 @@ defined('MOODLE_INTERNAL') || die(); $string['passworderror'] = 'The password entered was incorrect'; $string['pluginname'] = 'Password quiz access rule'; +$string['privacy:metadata'] = 'The Password quiz access rule plugin does not store any personal data.'; $string['quizpassword'] = 'Quiz password'; $string['requirepasswordmessage'] = 'To attempt this quiz you need to know the quiz password'; diff --git a/mod/quiz/accessrule/safebrowser/classes/privacy/provider.php b/mod/quiz/accessrule/safebrowser/classes/privacy/provider.php new file mode 100644 index 00000000000..2795736a1bd --- /dev/null +++ b/mod/quiz/accessrule/safebrowser/classes/privacy/provider.php @@ -0,0 +1,46 @@ +. + +/** + * Privacy Subsystem implementation for quizaccess_safebrowser. + * + * @package quizaccess_safebrowser + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace quizaccess_safebrowser\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem for quizaccess_safebrowser implementing null_provider. + * + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements \core_privacy\local\metadata\null_provider { + + /** + * Get the language string identifier with the component's language + * file to explain why this plugin stores no data. + * + * @return string + */ + public static function get_reason() { + return 'privacy:metadata'; + } +} diff --git a/mod/quiz/accessrule/safebrowser/lang/en/quizaccess_safebrowser.php b/mod/quiz/accessrule/safebrowser/lang/en/quizaccess_safebrowser.php index 0847d5fd274..f92976b22c5 100644 --- a/mod/quiz/accessrule/safebrowser/lang/en/quizaccess_safebrowser.php +++ b/mod/quiz/accessrule/safebrowser/lang/en/quizaccess_safebrowser.php @@ -28,6 +28,7 @@ defined('MOODLE_INTERNAL') || die(); $string['pluginname'] = 'Safe Exam Browser quiz access rule'; +$string['privacy:metadata'] = 'The Safe Exam Browser quiz access rule plugin does not store any personal data.'; $string['requiresafeexambrowser'] = 'Require the use of Safe Exam Browser'; $string['safebrowsererror'] = 'This quiz has been set up so that it may only be attempted using the Safe Exam Browser. You cannot attempt it from this web browser.'; $string['safebrowsernotice'] = 'This quiz has been configured so that students may only attempt it using the Safe Exam Browser.'; diff --git a/mod/quiz/accessrule/securewindow/classes/privacy/provider.php b/mod/quiz/accessrule/securewindow/classes/privacy/provider.php new file mode 100644 index 00000000000..f5c250e04ae --- /dev/null +++ b/mod/quiz/accessrule/securewindow/classes/privacy/provider.php @@ -0,0 +1,46 @@ +. + +/** + * Privacy Subsystem implementation for quizaccess_securewindow. + * + * @package quizaccess_securewindow + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace quizaccess_securewindow\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem for quizaccess_securewindow implementing null_provider. + * + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements \core_privacy\local\metadata\null_provider { + + /** + * Get the language string identifier with the component's language + * file to explain why this plugin stores no data. + * + * @return string + */ + public static function get_reason() { + return 'privacy:metadata'; + } +} diff --git a/mod/quiz/accessrule/securewindow/lang/en/quizaccess_securewindow.php b/mod/quiz/accessrule/securewindow/lang/en/quizaccess_securewindow.php index 74897c91953..73d9ece9430 100644 --- a/mod/quiz/accessrule/securewindow/lang/en/quizaccess_securewindow.php +++ b/mod/quiz/accessrule/securewindow/lang/en/quizaccess_securewindow.php @@ -29,3 +29,4 @@ defined('MOODLE_INTERNAL') || die(); $string['pluginname'] = 'JavaScript security quiz access rule'; $string['popupwithjavascriptsupport'] = 'Full screen pop-up with some JavaScript security'; +$string['privacy:metadata'] = 'The JavaScript security quiz access rule plugin does not store any personal data.'; diff --git a/mod/quiz/accessrule/timelimit/classes/privacy/provider.php b/mod/quiz/accessrule/timelimit/classes/privacy/provider.php new file mode 100644 index 00000000000..a1284ad6a54 --- /dev/null +++ b/mod/quiz/accessrule/timelimit/classes/privacy/provider.php @@ -0,0 +1,46 @@ +. + +/** + * Privacy Subsystem implementation for quizaccess_timelimit. + * + * @package quizaccess_timelimit + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace quizaccess_timelimit\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem for quizaccess_timelimit implementing null_provider. + * + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements \core_privacy\local\metadata\null_provider { + + /** + * Get the language string identifier with the component's language + * file to explain why this plugin stores no data. + * + * @return string + */ + public static function get_reason() { + return 'privacy:metadata'; + } +} diff --git a/mod/quiz/accessrule/timelimit/lang/en/quizaccess_timelimit.php b/mod/quiz/accessrule/timelimit/lang/en/quizaccess_timelimit.php index 106dfe9ee9d..3b67a43475b 100644 --- a/mod/quiz/accessrule/timelimit/lang/en/quizaccess_timelimit.php +++ b/mod/quiz/accessrule/timelimit/lang/en/quizaccess_timelimit.php @@ -30,4 +30,5 @@ defined('MOODLE_INTERNAL') || die(); $string['confirmstartheader'] = 'Timed quiz'; $string['confirmstart'] = 'The quiz has a time limit of {$a}. Time will count down from the moment you start your attempt and you must submit before it expires. Are you sure that you wish to start now?'; $string['pluginname'] = 'Time limit quiz access rule'; +$string['privacy:metadata'] = 'The Time limit quiz access rule plugin does not store any personal data.'; $string['quiztimelimit'] = 'Time limit: {$a}'; From c73a9d5cbe1deba60b4e34280c6f8d6d92b09887 Mon Sep 17 00:00:00 2001 From: Andrew Nicols Date: Thu, 3 May 2018 07:21:01 +0800 Subject: [PATCH 10/10] MDL-61407 privacy: Reset the writer after writer tests The writer was not reset after these tests which had the potential to affect the next test after these were run. --- privacy/tests/writer_test.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/privacy/tests/writer_test.php b/privacy/tests/writer_test.php index ebef5b9b221..45fbfd99e09 100644 --- a/privacy/tests/writer_test.php +++ b/privacy/tests/writer_test.php @@ -40,6 +40,14 @@ use \core_privacy\local\request\writer; * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class writer_test extends advanced_testcase { + /** + * Ensure that the writer is cleared away as appropriate after each + * test. + */ + public function tearDown() { + writer::reset(); + } + /** * Test that calling with_context multiple times will return the same write instance. */