From b62eb9310e1e3ae6879ecc7305eae1a28d9bad9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Mudr=C3=A1k?= Date: Fri, 27 Apr 2018 10:45:15 +0200 Subject: [PATCH 1/5] MDL-61905 workshop: Drop the old 1.x era tables These tables may still contain relics of old personal data if the site was installed before Moodle 2.0. We do not need these tables any more. Following the "privacy by design" principles, we are dropping them now. --- mod/workshop/db/install.xml | 188 +----------------------------------- mod/workshop/db/upgrade.php | 17 ++++ mod/workshop/version.php | 2 +- 3 files changed, 19 insertions(+), 188 deletions(-) diff --git a/mod/workshop/db/install.xml b/mod/workshop/db/install.xml index 5884eb91394e4..fb7e7ab72e576 100644 --- a/mod/workshop/db/install.xml +++ b/mod/workshop/db/install.xml @@ -1,5 +1,5 @@ - @@ -132,191 +132,5 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - -
- - - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - -
\ No newline at end of file diff --git a/mod/workshop/db/upgrade.php b/mod/workshop/db/upgrade.php index c5fd44e070d02..0e2368c3d144a 100644 --- a/mod/workshop/db/upgrade.php +++ b/mod/workshop/db/upgrade.php @@ -71,5 +71,22 @@ function xmldb_workshop_upgrade($oldversion) { // Automatically generated Moodle v3.4.0 release upgrade line. // Put any upgrade step following this. + if ($oldversion < 2017111301) { + // Drop the old Moodle 1.x tables, thanks privacy by design for forcing me to do so finally. + + $oldtables = ['workshop_old', 'workshop_elements_old', 'workshop_rubrics_old', 'workshop_submissions_old', + 'workshop_assessments_old', 'workshop_grades_old', 'workshop_stockcomments_old', 'workshop_comments_old']; + + foreach ($oldtables as $oldtable) { + $table = new xmldb_table($oldtable); + + if ($dbman->table_exists($table)) { + $dbman->drop_table($table); + } + } + + upgrade_mod_savepoint(true, 2017111301, 'workshop'); + } + return true; } diff --git a/mod/workshop/version.php b/mod/workshop/version.php index ae2fef6fe4470..3677d834a7b80 100644 --- a/mod/workshop/version.php +++ b/mod/workshop/version.php @@ -24,7 +24,7 @@ defined('MOODLE_INTERNAL') || die(); -$plugin->version = 2017111300; // The current module version (YYYYMMDDXX) +$plugin->version = 2017111301; // The current module version (YYYYMMDDXX) $plugin->requires = 2017110800; // Requires this Moodle version. $plugin->component = 'mod_workshop'; $plugin->cron = 60; // Give as a chance every minute. From 4d829b115993b22473a4af824ffeb8b942801e3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Mudr=C3=A1k?= Date: Fri, 27 Apr 2018 10:53:58 +0200 Subject: [PATCH 2/5] MDL-61905 workshop: Implement the privacy API in the workshop core Workshop module stores personal data in its tables, via user preference and via core_files and core_plagiarism subsystems. When exporting the data, we export not only data created by users themselves (such as their submissions and provided peer-assessments) but also all relevant data that can (or must) be used to interpret created content and evaluate the user's performance and skills. On the other hand, when deleting data at user's request, we delete only those data that do not affect other users' performance evaluation. The reasoning is that one's right for privacy does not overweight someone else's right for fair assessment. For that reason, we can't fully delete whole provided peer-assessments, for example. Because they are used in cross-comparison and grading evaluation of all other peers who assessed the same submission. So instead, we replace provided texts but still keep the original record. Workshop defines the interface for its grading strategy subplugins to allow them attach personal data under their control to the exported structures. --- mod/workshop/classes/privacy/provider.php | 668 ++++++++++++++++++ .../privacy/workshopform_legacy_polyfill.php | 58 ++ .../classes/privacy/workshopform_provider.php | 47 ++ mod/workshop/lang/en/workshop.php | 42 ++ mod/workshop/tests/generator/lib.php | 2 + mod/workshop/tests/privacy_provider_test.php | 379 ++++++++++ 6 files changed, 1196 insertions(+) create mode 100644 mod/workshop/classes/privacy/provider.php create mode 100644 mod/workshop/classes/privacy/workshopform_legacy_polyfill.php create mode 100644 mod/workshop/classes/privacy/workshopform_provider.php create mode 100644 mod/workshop/tests/privacy_provider_test.php diff --git a/mod/workshop/classes/privacy/provider.php b/mod/workshop/classes/privacy/provider.php new file mode 100644 index 0000000000000..3a073abf87d60 --- /dev/null +++ b/mod/workshop/classes/privacy/provider.php @@ -0,0 +1,668 @@ +. + +/** + * Defines {@link \mod_workshop\privacy\provider} class. + * + * @package mod_workshop + * @category privacy + * @copyright 2018 David Mudrák + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_workshop\privacy; + +use core_privacy\local\metadata\collection; +use core_privacy\local\request\approved_contextlist; +use core_privacy\local\request\contextlist; +use core_privacy\local\request\deletion_criteria; +use core_privacy\local\request\helper; +use core_privacy\local\request\transform; +use core_privacy\local\request\writer; + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->dirroot.'/mod/workshop/locallib.php'); + +/** + * Privacy API implementation for the Workshop activity module. + * + * @copyright 2018 David Mudrák + * @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, + \core_privacy\local\request\plugin\provider { + + /** + * Describe all the places where the Workshop module stores some personal data. + * + * @param collection $collection Collection of items to add metadata to. + * @return collection Collection with our added items. + */ + public static function get_metadata(collection $collection) : collection { + + $collection->add_database_table('workshop_submissions', [ + 'workshopid' => 'privacy:metadata:workshopid', + 'authorid' => 'privacy:metadata:authorid', + 'example' => 'privacy:metadata:example', + 'timecreated' => 'privacy:metadata:timecreated', + 'timemodified' => 'privacy:metadata:timemodified', + 'title' => 'privacy:metadata:submissiontitle', + 'content' => 'privacy:metadata:submissioncontent', + 'contentformat' => 'privacy:metadata:submissioncontentformat', + 'grade' => 'privacy:metadata:submissiongrade', + 'gradeover' => 'privacy:metadata:submissiongradeover', + 'feedbackauthor' => 'privacy:metadata:feedbackauthor', + 'feedbackauthorformat' => 'privacy:metadata:feedbackauthorformat', + 'published' => 'privacy:metadata:published', + 'late' => 'privacy:metadata:late', + ], 'privacy:metadata:workshopsubmissions'); + + $collection->add_database_table('workshop_assessments', [ + 'submissionid' => 'privacy:metadata:submissionid', + 'reviewerid' => 'privacy:metadata:reviewerid', + 'weight' => 'privacy:metadata:weight', + 'timecreated' => 'privacy:metadata:timecreated', + 'timemodified' => 'privacy:metadata:timemodified', + 'grade' => 'privacy:metadata:assessmentgrade', + 'gradinggrade' => 'privacy:metadata:assessmentgradinggrade', + 'gradinggradeover' => 'privacy:metadata:assessmentgradinggradeover', + 'feedbackauthor' => 'privacy:metadata:feedbackauthor', + 'feedbackauthorformat' => 'privacy:metadata:feedbackauthorformat', + 'feedbackreviewer' => 'privacy:metadata:feedbackreviewer', + 'feedbackreviewerformat' => 'privacy:metadata:feedbackreviewerformat', + ], 'privacy:metadata:workshopassessments'); + + $collection->add_database_table('workshop_grades', [ + 'assessmentid' => 'privacy:metadata:assessmentid', + 'strategy' => 'privacy:metadata:strategy', + 'dimensionid' => 'privacy:metadata:dimensionid', + 'grade' => 'privacy:metadata:dimensiongrade', + 'peercomment' => 'privacy:metadata:peercomment', + 'peercommentformat' => 'privacy:metadata:peercommentformat', + ], 'privacy:metadata:workshopgrades'); + + $collection->add_database_table('workshop_aggregations', [ + 'workshopid' => 'privacy:metadata:workshopid', + 'userid' => 'privacy:metadata:userid', + 'gradinggrade' => 'privacy:metadata:aggregatedgradinggrade', + 'timegraded' => 'privacy:metadata:timeaggregated', + ], 'privacy:metadata:workshopaggregations'); + + $collection->add_subsystem_link('core_files', [], 'privacy:metadata:subsystem:corefiles'); + $collection->add_subsystem_link('core_plagiarism', [], 'privacy:metadata:subsystem:coreplagiarism'); + + $collection->add_user_preference('workshop_perpage', 'privacy:metadata:preference:perpage'); + + return $collection; + } + + /** + * Get the list of contexts that contain personal data for the specified user. + * + * User has personal data in the workshop if any of the following cases happens: + * + * - the user has submitted in the workshop + * - the user has overridden a submission grade + * - the user has been assigned as a reviewer of a submission + * - the user has overridden a grading grade + * - the user has a grading grade (existing or to be calculated) + * + * @param int $userid ID of the user. + * @return contextlist List of contexts containing the user's personal data. + */ + public static function get_contexts_for_userid(int $userid) : contextlist { + + $contextlist = new contextlist(); + + $sql = "SELECT ctx.id + FROM {course_modules} cm + JOIN {modules} m ON cm.module = m.id AND m.name = :module + JOIN {context} ctx ON ctx.contextlevel = :contextlevel AND ctx.instanceid = cm.id + JOIN {workshop} w ON cm.instance = w.id + LEFT JOIN {workshop_submissions} ws ON ws.workshopid = w.id + LEFT JOIN {workshop_assessments} wa ON wa.submissionid = ws.id + LEFT JOIN {workshop_aggregations} wr ON wr.workshopid = w.id + WHERE ws.authorid = :wsauthorid + OR ws.gradeoverby = :wsgradeoverby + OR wa.reviewerid = :wareviewerid + OR wa.gradinggradeoverby = :wagradinggradeoverby + OR wr.userid = :wruserid"; + + $params = [ + 'module' => 'workshop', + 'contextlevel' => CONTEXT_MODULE, + 'wsauthorid' => $userid, + 'wsgradeoverby' => $userid, + 'wareviewerid' => $userid, + 'wagradinggradeoverby' => $userid, + 'wruserid' => $userid, + ]; + + $contextlist->add_from_sql($sql, $params); + + return $contextlist; + } + + /** + * Export personal data stored in the given contexts. + * + * @param approved_contextlist $contextlist List of contexts approved for export. + */ + public static function export_user_data(approved_contextlist $contextlist) { + global $DB; + + if (!count($contextlist)) { + return; + } + + $user = $contextlist->get_user(); + + // Export general information about all workshops. + foreach ($contextlist->get_contexts() as $context) { + if ($context->contextlevel != CONTEXT_MODULE) { + continue; + } + $data = helper::get_context_data($context, $user); + static::append_extra_workshop_data($context, $user, $data, []); + writer::with_context($context)->export_data([], $data); + helper::export_context_files($context, $user); + } + + // Export the user's own submission and all example submissions he/she created. + static::export_submissions($contextlist); + + // Export all given assessments. + static::export_assessments($contextlist); + } + + /** + * Export user preferences controlled by this plugin. + * + * @param int $userid ID of the user we are exporting data for + */ + public static function export_user_preferences(int $userid) { + + $perpage = get_user_preferences('workshop_perpage', null, $userid); + + if ($perpage !== null) { + writer::export_user_preference('mod_workshop', 'workshop_perpage', $perpage, + get_string('privacy:metadata:preference:perpage', 'mod_workshop')); + } + } + + /** + * Append additional relevant data into the base data about the workshop instance. + * + * Relevant are data that are important for interpreting or evaluating the performance of the user expressed in + * his/her exported personal data. For example, we need to know what were the instructions for submissions or what + * was the phase of the workshop when it was exported. + * + * @param context $context Workshop module content. + * @param stdClass $user User for which we are exporting data. + * @param stdClass $data Base data about the workshop instance to append to. + * @param array $subcontext Subcontext path items to eventually write files into. + */ + protected static function append_extra_workshop_data(\context $context, \stdClass $user, \stdClass $data, array $subcontext) { + global $DB; + + if ($context->contextlevel != CONTEXT_MODULE) { + throw new \coding_exception('Unexpected context provided'); + } + + $sql = "SELECT w.instructauthors, w.instructauthorsformat, w.instructreviewers, w.instructreviewersformat, w.phase, + w.strategy, w.evaluation, w.latesubmissions, w.submissionstart, w.submissionend, w.assessmentstart, + w.assessmentend, w.conclusion, w.conclusionformat + FROM {course_modules} cm + JOIN {workshop} w ON cm.instance = w.id + WHERE cm.id = :cmid"; + + $params = [ + 'cmid' => $context->instanceid, + ]; + + $record = $DB->get_record_sql($sql, $params, MUST_EXIST); + $writer = writer::with_context($context); + + if ($record->phase >= \workshop::PHASE_SUBMISSION) { + $data->instructauthors = $writer->rewrite_pluginfile_urls($subcontext, 'mod_workshop', 'instructauthors', 0, + $record->instructauthors); + $data->instructauthorsformat = $record->instructauthorsformat; + } + + if ($record->phase >= \workshop::PHASE_ASSESSMENT) { + $data->instructreviewers = $writer->rewrite_pluginfile_urls($subcontext, 'mod_workshop', 'instructreviewers', 0, + $record->instructreviewers); + $data->instructreviewersformat = $record->instructreviewersformat; + } + + if ($record->phase >= \workshop::PHASE_CLOSED) { + $data->conclusion = $writer->rewrite_pluginfile_urls($subcontext, 'mod_workshop', 'conclusion', 0, $record->conclusion); + $data->conclusionformat = $record->conclusionformat; + } + + $data->strategy = \workshop::available_strategies_list()[$record->strategy]; + $data->evaluation = \workshop::available_evaluators_list()[$record->evaluation]; + $data->latesubmissions = transform::yesno($record->latesubmissions); + $data->submissionstart = $record->submissionstart ? transform::datetime($record->submissionstart) : null; + $data->submissionend = $record->submissionend ? transform::datetime($record->submissionend) : null; + $data->assessmentstart = $record->assessmentstart ? transform::datetime($record->assessmentstart) : null; + $data->assessmentend = $record->assessmentend ? transform::datetime($record->assessmentend) : null; + + switch ($record->phase) { + case \workshop::PHASE_SETUP: + $data->phase = get_string('phasesetup', 'mod_workshop'); + break; + case \workshop::PHASE_SUBMISSION: + $data->phase = get_string('phasesubmission', 'mod_workshop'); + break; + case \workshop::PHASE_ASSESSMENT: + $data->phase = get_string('phaseassessment', 'mod_workshop'); + break; + case \workshop::PHASE_EVALUATION: + $data->phase = get_string('phaseevaluation', 'mod_workshop'); + break; + case \workshop::PHASE_CLOSED: + $data->phase = get_string('phaseclosed', 'mod_workshop'); + break; + } + + $writer->export_area_files($subcontext, 'mod_workshop', 'instructauthors', 0); + $writer->export_area_files($subcontext, 'mod_workshop', 'instructreviewers', 0); + $writer->export_area_files($subcontext, 'mod_workshop', 'conclusion', 0); + } + + /** + * Export all user's submissions and example submissions he/she created in the given contexts. + * + * @param approved_contextlist $contextlist List of contexts approved for export. + */ + protected static function export_submissions(approved_contextlist $contextlist) { + global $DB; + + list($contextsql, $contextparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED); + $user = $contextlist->get_user(); + + $sql = "SELECT ws.id, ws.authorid, ws.example, ws.timecreated, ws.timemodified, ws.title, + ws.content, ws.contentformat, ws.grade, ws.gradeover, ws.feedbackauthor, ws.feedbackauthorformat, + ws.published, ws.late, + w.phase, w.course, cm.id AS cmid, ".\context_helper::get_preload_record_columns_sql('ctx')." + FROM {course_modules} cm + JOIN {modules} m ON cm.module = m.id AND m.name = :module + JOIN {context} ctx ON ctx.contextlevel = :contextlevel AND ctx.instanceid = cm.id + JOIN {workshop} w ON cm.instance = w.id + JOIN {workshop_submissions} ws ON ws.workshopid = w.id + WHERE ctx.id {$contextsql} + AND ws.authorid = :authorid"; + + $params = $contextparams + [ + 'module' => 'workshop', + 'contextlevel' => CONTEXT_MODULE, + 'authorid' => $user->id, + ]; + + $rs = $DB->get_recordset_sql($sql, $params); + + foreach ($rs as $record) { + \context_helper::preload_from_record($record); + $context = \context_module::instance($record->cmid); + $writer = \core_privacy\local\request\writer::with_context($context); + + if ($record->example) { + $subcontext = [get_string('examplesubmissions', 'mod_workshop'), $record->id]; + $mysubmission = null; + } else { + $subcontext = [get_string('mysubmission', 'mod_workshop')]; + $mysubmission = $record; + } + + $phase = $record->phase; + $courseid = $record->course; + + $data = (object) [ + 'example' => transform::yesno($record->example), + 'timecreated' => transform::datetime($record->timecreated), + 'timemodified' => $record->timemodified ? transform::datetime($record->timemodified) : null, + 'title' => $record->title, + 'content' => $writer->rewrite_pluginfile_urls($subcontext, 'mod_workshop', + 'submission_content', $record->id, $record->content), + 'contentformat' => $record->contentformat, + 'grade' => $record->grade, + 'gradeover' => $record->gradeover, + 'feedbackauthor' => $record->feedbackauthor, + 'feedbackauthorformat' => $record->feedbackauthorformat, + 'published' => transform::yesno($record->published), + 'late' => transform::yesno($record->late), + ]; + + $writer->export_data($subcontext, $data); + $writer->export_area_files($subcontext, 'mod_workshop', 'submission_content', $record->id); + $writer->export_area_files($subcontext, 'mod_workshop', 'submission_attachment', $record->id); + + // Export peer-assessments of my submission if the workshop was closed. We do not export received + // assessments from peers before they were actually effective. Before the workshop is closed, grades are not + // pushed into the gradebook. So peer assessments did not affect evaluation of the user's performance and + // they should not be considered as their personal data. This is different from assessments given by the + // user that are always exported. + if ($mysubmission && $phase == \workshop::PHASE_CLOSED) { + $assessments = $DB->get_records('workshop_assessments', ['submissionid' => $mysubmission->id], '', + 'id, reviewerid, weight, timecreated, timemodified, grade, feedbackauthor, feedbackauthorformat'); + + foreach ($assessments as $assessment) { + $assid = $assessment->id; + $assessment->selfassessment = transform::yesno($assessment->reviewerid == $user->id); + $assessment->timecreated = transform::datetime($assessment->timecreated); + $assessment->timemodified = $assessment->timemodified ? transform::datetime($assessment->timemodified) : null; + $assessment->feedbackauthor = $writer->rewrite_pluginfile_urls($subcontext, + 'mod_workshop', 'overallfeedback_content', $assid, $assessment->feedbackauthor); + + $assessmentsubcontext = array_merge($subcontext, [get_string('assessments', 'mod_workshop'), $assid]); + + unset($assessment->id); + unset($assessment->reviewerid); + + $writer->export_data($assessmentsubcontext, $assessment); + $writer->export_area_files($assessmentsubcontext, 'mod_workshop', 'overallfeedback_content', $assid); + $writer->export_area_files($assessmentsubcontext, 'mod_workshop', 'overallfeedback_attachment', $assid); + + // Export details of how the assessment forms were filled. + static::export_assessment_forms($user, $context, $assessmentsubcontext, $assid); + } + } + + // Export plagiarism data related to the submission content. + // The last $linkarray argument consistent with how we call {@link plagiarism_get_links()} in the renderer. + \core_plagiarism\privacy\provider::export_plagiarism_user_data($user->id, $context, $subcontext, [ + 'userid' => $user->id, + 'content' => format_text($data->content, $data->contentformat, ['overflowdiv' => true]), + 'cmid' => $context->instanceid, + 'course' => $courseid, + ]); + } + + $rs->close(); + } + + /** + * Export all assessments given by the user. + * + * @param approved_contextlist $contextlist List of contexts approved for export. + */ + protected static function export_assessments(approved_contextlist $contextlist) { + global $DB; + + list($contextsql, $contextparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED); + $user = $contextlist->get_user(); + + $sql = "SELECT ws.authorid, ws.example, ws.timecreated, ws.timemodified, ws.title, ws.content, ws.contentformat, + wa.id, wa.submissionid, wa.reviewerid, wa.weight, wa.timecreated, wa.timemodified, wa.grade, + wa.gradinggrade, wa.gradinggradeover, wa.feedbackauthor, wa.feedbackauthorformat, wa.feedbackreviewer, + wa.feedbackreviewerformat, cm.id AS cmid, ".\context_helper::get_preload_record_columns_sql('ctx')." + FROM {course_modules} cm + JOIN {modules} m ON cm.module = m.id AND m.name = :module + JOIN {context} ctx ON cm.id = ctx.instanceid AND ctx.contextlevel = :contextlevel + JOIN {workshop} w ON cm.instance = w.id + JOIN {workshop_submissions} ws ON ws.workshopid = w.id + JOIN {workshop_assessments} wa ON wa.submissionid = ws.id + WHERE ctx.id {$contextsql} + AND wa.reviewerid = :reviewerid"; + + $params = $contextparams + [ + 'module' => 'workshop', + 'contextlevel' => CONTEXT_MODULE, + 'reviewerid' => $user->id, + ]; + + $rs = $DB->get_recordset_sql($sql, $params); + + foreach ($rs as $record) { + \context_helper::preload_from_record($record); + $context = \context_module::instance($record->cmid); + $writer = \core_privacy\local\request\writer::with_context($context); + $subcontext = [get_string('myassessments', 'mod_workshop'), $record->id]; + + $data = (object) [ + 'weight' => $record->weight, + 'timecreated' => transform::datetime($record->timecreated), + 'timemodified' => $record->timemodified ? transform::datetime($record->timemodified) : null, + 'grade' => $record->grade, + 'gradinggrade' => $record->gradinggrade, + 'gradinggradeover' => $record->gradinggradeover, + 'feedbackauthor' => $writer->rewrite_pluginfile_urls($subcontext, 'mod_workshop', + 'overallfeedback_content', $record->id, $record->feedbackauthor), + 'feedbackauthorformat' => $record->feedbackauthorformat, + 'feedbackreviewer' => $record->feedbackreviewer, + 'feedbackreviewerformat' => $record->feedbackreviewerformat, + ]; + + $submission = (object) [ + 'myownsubmission' => transform::yesno($record->authorid == $user->id), + 'example' => transform::yesno($record->example), + 'timecreated' => transform::datetime($record->timecreated), + 'timemodified' => $record->timemodified ? transform::datetime($record->timemodified) : null, + 'title' => $record->title, + 'content' => $writer->rewrite_pluginfile_urls($subcontext, 'mod_workshop', + 'submission_content', $record->submissionid, $record->content), + 'contentformat' => $record->contentformat, + ]; + + $writer->export_data($subcontext, $data); + $writer->export_related_data($subcontext, 'submission', $submission); + $writer->export_area_files($subcontext, 'mod_workshop', 'overallfeedback_content', $record->id); + $writer->export_area_files($subcontext, 'mod_workshop', 'overallfeedback_attachment', $record->id); + $writer->export_area_files($subcontext, 'mod_workshop', 'submission_content', $record->submissionid); + $writer->export_area_files($subcontext, 'mod_workshop', 'submission_attachment', $record->submissionid); + + // Export details of how the assessment forms were filled. + static::export_assessment_forms($user, $context, $subcontext, $record->id); + } + + $rs->close(); + } + + /** + * Export the grading strategy data related to the particular assessment. + * + * @param stdClass $user User we are exporting for + * @param context $context Workshop activity content + * @param array $subcontext Subcontext path of the assessment + * @param int $assessmentid ID of the exported assessment + */ + protected static function export_assessment_forms(\stdClass $user, \context $context, array $subcontext, int $assessmentid) { + + foreach (\workshop::available_strategies_list() as $strategy => $title) { + $providername = '\workshopform_'.$strategy.'\privacy\provider'; + + if (is_subclass_of($providername, '\mod_workshop\privacy\workshopform_provider')) { + component_class_callback($providername, 'export_assessment_form', + [ + $user, + $context, + array_merge($subcontext, [get_string('assessmentform', 'mod_workshop'), $title]), + $assessmentid, + ] + ); + + } else { + debugging('Missing class '.$providername.' implementing workshopform_provider interface', DEBUG_DEVELOPER); + } + } + } + + /** + * Delete personal data for all users in the context. + * + * @param context $context Context to delete personal data from. + */ + public static function delete_data_for_all_users_in_context(\context $context) { + global $CFG, $DB; + require_once($CFG->libdir.'/gradelib.php'); + + if ($context->contextlevel != CONTEXT_MODULE) { + return; + } + + $cm = get_coursemodule_from_id('workshop', $context->instanceid, 0, false, IGNORE_MISSING); + + if (!$cm) { + // Probably some kind of expired context. + return; + } + + $workshop = $DB->get_record('workshop', ['id' => $cm->instance], 'id, course', MUST_EXIST); + + $submissions = $DB->get_records('workshop_submissions', ['workshopid' => $workshop->id], '', 'id'); + $assessments = $DB->get_records_list('workshop_assessments', 'submissionid', array_keys($submissions), '', 'id'); + + $DB->delete_records('workshop_aggregations', ['workshopid' => $workshop->id]); + $DB->delete_records_list('workshop_grades', 'assessmentid', array_keys($assessments)); + $DB->delete_records_list('workshop_assessments', 'id', array_keys($assessments)); + $DB->delete_records_list('workshop_submissions', 'id', array_keys($submissions)); + + $fs = get_file_storage(); + $fs->delete_area_files($context->id, 'mod_workshop', 'submission_content'); + $fs->delete_area_files($context->id, 'mod_workshop', 'submission_attachment'); + $fs->delete_area_files($context->id, 'mod_workshop', 'overallfeedback_content'); + $fs->delete_area_files($context->id, 'mod_workshop', 'overallfeedback_attachment'); + + grade_update('mod/workshop', $workshop->course, 'mod', 'workshop', $workshop->id, 0, null, ['reset' => true]); + grade_update('mod/workshop', $workshop->course, 'mod', 'workshop', $workshop->id, 1, null, ['reset' => true]); + + \core_plagiarism\privacy\provider::delete_plagiarism_for_context($context); + } + + /** + * Delete personal data for the user in a list of contexts. + * + * Removing assessments of submissions from the Workshop is not trivial. Removing one user's data can easily affect + * other users' grades and completion criteria. So we replace the non-essential contents with a "deleted" message, + * but keep the actual info in place. The argument is that one's right for privacy should not overweight others' + * right for accessing their own personal data and be evaluated on their basis. + * + * @param approved_contextlist $contextlist List of contexts to delete data from. + */ + public static function delete_data_for_user(approved_contextlist $contextlist) { + global $DB; + + list($contextsql, $contextparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED); + $user = $contextlist->get_user(); + $fs = get_file_storage(); + + // Replace sensitive data in all submissions by the user in the given contexts. + + $sql = "SELECT ws.id AS submissionid + FROM {course_modules} cm + JOIN {modules} m ON cm.module = m.id AND m.name = :module + JOIN {context} ctx ON ctx.contextlevel = :contextlevel AND ctx.instanceid = cm.id + JOIN {workshop} w ON cm.instance = w.id + JOIN {workshop_submissions} ws ON ws.workshopid = w.id + WHERE ctx.id {$contextsql} + AND ws.authorid = :authorid"; + + $params = $contextparams + [ + 'module' => 'workshop', + 'contextlevel' => CONTEXT_MODULE, + 'authorid' => $user->id, + ]; + + $submissionids = $DB->get_fieldset_sql($sql, $params); + + if ($submissionids) { + list($submissionidsql, $submissionidparams) = $DB->get_in_or_equal($submissionids, SQL_PARAMS_NAMED); + + $DB->set_field_select('workshop_submissions', 'title', get_string('privacy:request:delete:title', + 'mod_workshop'), "id $submissionidsql", $submissionidparams); + $DB->set_field_select('workshop_submissions', 'content', get_string('privacy:request:delete:content', + 'mod_workshop'), "id $submissionidsql", $submissionidparams); + $DB->set_field_select('workshop_submissions', 'feedbackauthor', get_string('privacy:request:delete:content', + 'mod_workshop'), "id $submissionidsql", $submissionidparams); + + foreach ($contextlist->get_contextids() as $contextid) { + $fs->delete_area_files_select($contextid, 'mod_workshop', 'submission_content', + $submissionidsql, $submissionidparams); + $fs->delete_area_files_select($contextid, 'mod_workshop', 'submission_attachment', + $submissionidsql, $submissionidparams); + } + } + + // Replace personal data in received assessments - feedback is seen as belonging to the recipient. + + $sql = "SELECT wa.id AS assessmentid + FROM {course_modules} cm + JOIN {modules} m ON cm.module = m.id AND m.name = :module + JOIN {context} ctx ON cm.id = ctx.instanceid AND ctx.contextlevel = :contextlevel + JOIN {workshop} w ON cm.instance = w.id + JOIN {workshop_submissions} ws ON ws.workshopid = w.id + JOIN {workshop_assessments} wa ON wa.submissionid = ws.id + WHERE ctx.id {$contextsql} + AND ws.authorid = :authorid"; + + $params = $contextparams + [ + 'module' => 'workshop', + 'contextlevel' => CONTEXT_MODULE, + 'authorid' => $user->id, + ]; + + $assessmentids = $DB->get_fieldset_sql($sql, $params); + + if ($assessmentids) { + list($assessmentidsql, $assessmentidparams) = $DB->get_in_or_equal($assessmentids, SQL_PARAMS_NAMED); + + $DB->set_field_select('workshop_assessments', 'feedbackauthor', get_string('privacy:request:delete:content', + 'mod_workshop'), "id $assessmentidsql", $assessmentidparams); + + foreach ($contextlist->get_contextids() as $contextid) { + $fs->delete_area_files_select($contextid, 'mod_workshop', 'overallfeedback_content', + $assessmentidsql, $assessmentidparams); + $fs->delete_area_files_select($contextid, 'mod_workshop', 'overallfeedback_attachment', + $assessmentidsql, $assessmentidparams); + } + } + + // Replace sensitive data in provided assessments records. + + $sql = "SELECT wa.id AS assessmentid + FROM {course_modules} cm + JOIN {modules} m ON cm.module = m.id AND m.name = :module + JOIN {context} ctx ON cm.id = ctx.instanceid AND ctx.contextlevel = :contextlevel + JOIN {workshop} w ON cm.instance = w.id + JOIN {workshop_submissions} ws ON ws.workshopid = w.id + JOIN {workshop_assessments} wa ON wa.submissionid = ws.id + WHERE ctx.id {$contextsql} + AND wa.reviewerid = :reviewerid"; + + $params = $contextparams + [ + 'module' => 'workshop', + 'contextlevel' => CONTEXT_MODULE, + 'reviewerid' => $user->id, + ]; + + $assessmentids = $DB->get_fieldset_sql($sql, $params); + + if ($assessmentids) { + list($assessmentidsql, $assessmentidparams) = $DB->get_in_or_equal($assessmentids, SQL_PARAMS_NAMED); + + $DB->set_field_select('workshop_assessments', 'feedbackreviewer', get_string('privacy:request:delete:content', + 'mod_workshop'), "id $assessmentidsql", $assessmentidparams); + } + + foreach ($contextlist as $context) { + \core_plagiarism\privacy\provider::delete_plagiarism_for_user($user->id, $context); + } + } +} diff --git a/mod/workshop/classes/privacy/workshopform_legacy_polyfill.php b/mod/workshop/classes/privacy/workshopform_legacy_polyfill.php new file mode 100644 index 0000000000000..bb0094f4cfa34 --- /dev/null +++ b/mod/workshop/classes/privacy/workshopform_legacy_polyfill.php @@ -0,0 +1,58 @@ +. + +/** + * Provides {@link mod_workshop\privacy\workshopform_legacy_polyfill} trait. + * + * @package mod_workshop + * @category privacy + * @copyright 2018 David Mudrák + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_workshop\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Trait allowing additional (contrib) plugins to have single codebase for 3.3 and 3.4. + * + * The signature of the method in the {@link \mod_workshop\privacy\workshopform_provider} interface makes use of scalar + * type hinting that is available in PHP 7.0 only. If a plugin wants to implement the interface in 3.3 (and therefore + * PHP 5.6) with the same codebase, they can make use of this trait. Instead of implementing the interface directly, the + * workshopform plugin can implement the required logic in the method (note the underscore and missing "int" hint): + * + * public static function _export_assessment_form(\stdClass $user, \context $context, array $subcontext, $assessmentid) + * + * and then simply use this trait in their provider class. + * + * @copyright 2018 David Mudrák + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +trait workshopform_legacy_polyfill { + + /** + * Return details of the filled assessment form. + * + * @param stdClass $user User we are exporting data for + * @param context $context The workshop activity context + * @param array $subcontext Subcontext within the context to export to + * @param int $assessmentid ID of the assessment + */ + public static function export_assessment_form(\stdClass $user, \context $context, array $subcontext, int $assessmentid) { + return static::_export_assessment_form($user, $context, $subcontext, $assessmentid); + } +} diff --git a/mod/workshop/classes/privacy/workshopform_provider.php b/mod/workshop/classes/privacy/workshopform_provider.php new file mode 100644 index 0000000000000..152ead34ec877 --- /dev/null +++ b/mod/workshop/classes/privacy/workshopform_provider.php @@ -0,0 +1,47 @@ +. + +/** + * Provides {@link mod_workshop\privacy\workshopform_provider} interface. + * + * @package mod_workshop + * @category privacy + * @copyright 2018 David Mudrák + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_workshop\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Interface for grading strategy subplugins implementing the privacy API. + * + * @copyright 2018 David Mudrák + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +interface workshopform_provider extends \core_privacy\local\request\plugin\subplugin_provider { + + /** + * Return details of the filled assessment form. + * + * @param stdClass $user User we are exporting data for + * @param context $context The workshop activity context + * @param array $subcontext Subcontext within the context to export to + * @param int $assessmentid ID of the assessment + */ + public static function export_assessment_form(\stdClass $user, \context $context, array $subcontext, int $assessmentid); +} diff --git a/mod/workshop/lang/en/workshop.php b/mod/workshop/lang/en/workshop.php index b09bb2a65d01f..19ece15241979 100644 --- a/mod/workshop/lang/en/workshop.php +++ b/mod/workshop/lang/en/workshop.php @@ -64,6 +64,7 @@ $string['assessmentreference'] = 'Reference assessment'; $string['assessmentreferenceconflict'] = 'It is not possible to assess an example submission for which you provided a reference assessment.'; $string['assessmentreferenceneeded'] = 'You have to assess this example submission to provide a reference assessment. Click \'Continue\' button to assess the submission.'; +$string['assessments'] = 'Assessments'; $string['assessmentsettings'] = 'Assessment settings'; $string['assessmentstart'] = 'Open for assessment from'; $string['assessmentstartevent'] = '{$a} opens for assessment'; @@ -198,6 +199,7 @@ Students obtain two grades in a workshop activity - a grade for their submission and a grade for their assessment of their peers\' submissions. Both grades are recorded in the gradebook.'; $string['modulename_link'] = 'mod/workshop/view'; $string['modulenameplural'] = 'Workshops'; +$string['myassessments'] = 'My assessments'; $string['mysubmission'] = 'My submission'; $string['nattachments'] = 'Maximum number of submission attachments'; $string['noexamples'] = 'No examples yet in this workshop'; @@ -234,6 +236,46 @@ $string['pluginname'] = 'Workshop'; $string['prepareexamples'] = 'Prepare example submissions'; $string['previewassessmentform'] = 'Preview'; +$string['privacy:metadata:aggregatedgradinggrade'] = 'Aggregated grade for all assessments made by the user in the given workshop activity'; +$string['privacy:metadata:assessmentgrade'] = 'Aggregated grade for the submission suggested by this assessment'; +$string['privacy:metadata:assessmentgradinggrade'] = 'Grade for providing this assessment'; +$string['privacy:metadata:assessmentgradinggradeover'] = 'Manually overridden value of the grade for providing this assessment'; +$string['privacy:metadata:assessmentid'] = 'Identifier of the assessment'; +$string['privacy:metadata:authorid'] = 'Identifier of the submission author'; +$string['privacy:metadata:dimensiongrade'] = 'Grade in the given assessment dimension'; +$string['privacy:metadata:dimensionid'] = 'Identifier of the assessment dimension'; +$string['privacy:metadata:example'] = 'Whether this record represents an example submission'; +$string['privacy:metadata:feedbackauthor'] = 'Feedback for the author'; +$string['privacy:metadata:feedbackauthorformat'] = 'Text format of the feedback for the author'; +$string['privacy:metadata:feedbackreviewer'] = 'Feedback for the user providing the assessment'; +$string['privacy:metadata:feedbackreviewerformat'] = 'Text format of the feedback for the user providing the assessment'; +$string['privacy:metadata:late'] = 'Whether the submission been submitted after the deadline'; +$string['privacy:metadata:peercomment'] = 'Comment on the given grade by the user providing the assessment'; +$string['privacy:metadata:peercommentformat'] = 'Text format of the comment on the given grade'; +$string['privacy:metadata:preference:perpage'] = 'Number of submissions the user prefers to see on one page'; +$string['privacy:metadata:published'] = 'Whether the submission should be published to all participants once the workshop is closed'; +$string['privacy:metadata:reviewerid'] = 'Identifier of the user providing the assessment'; +$string['privacy:metadata:strategy'] = 'Name of the grading strategy subplugin interpreting the record values'; +$string['privacy:metadata:submissioncontent'] = 'Content of the submission'; +$string['privacy:metadata:submissioncontentformat'] = 'Text format of the submission content'; +$string['privacy:metadata:submissiongrade'] = 'Aggregated grade for the submission written as a decimal number from interval 0..100'; +$string['privacy:metadata:submissiongradeover'] = 'Manually overridden value of the aggregated grade'; +$string['privacy:metadata:submissionid'] = 'Identifier of the submission'; +$string['privacy:metadata:submissiontitle'] = 'Title of the submission'; +$string['privacy:metadata:subsystem:corefiles'] = 'Workshop module stores files embedded in / attached to the submission text'; +$string['privacy:metadata:subsystem:coreplagiarism'] = 'Workshop module has inbuilt support for plagiarism prevention systems'; +$string['privacy:metadata:timeaggregated'] = 'When the aggregated grade was last calculated'; +$string['privacy:metadata:timecreated'] = 'When this record was created in the database'; +$string['privacy:metadata:timemodified'] = 'When this record was last modified in the database'; +$string['privacy:metadata:userid'] = 'Identifier of the user for which aggregated grade is calculated'; +$string['privacy:metadata:weight'] = 'Weight of the assessment'; +$string['privacy:metadata:workshopaggregations'] = 'Holds aggregated grades for assessment'; +$string['privacy:metadata:workshopassessments'] = 'Holds information about allocated assessments of workshop module submissions'; +$string['privacy:metadata:workshopgrades'] = 'Holds information about how the assessment forms were filled with grades and comments'; +$string['privacy:metadata:workshopid'] = 'Identifier of the workshop activity'; +$string['privacy:metadata:workshopsubmissions'] = 'Holds information about workshop module submissions'; +$string['privacy:request:delete:title'] = '[Deleted]'; +$string['privacy:request:delete:content'] = 'The content has been deleted at the request of the user.'; $string['publishedsubmissions'] = 'Published submissions'; $string['publishsubmission'] = 'Publish submission'; $string['publishsubmission_help'] = 'Published submissions are available to the others when the workshop is closed.'; diff --git a/mod/workshop/tests/generator/lib.php b/mod/workshop/tests/generator/lib.php index 0f893a05c9610..3fc2dcd22650d 100644 --- a/mod/workshop/tests/generator/lib.php +++ b/mod/workshop/tests/generator/lib.php @@ -152,6 +152,8 @@ public function create_assessment($submissionid, $reviewerid, $options = null) { 'timecreated' => $timenow, 'timemodified' => $timenow, 'grade' => null, + 'feedbackauthor' => '', + 'feedbackreviewer' => '', ); $id = $DB->insert_record('workshop_assessments', $record); diff --git a/mod/workshop/tests/privacy_provider_test.php b/mod/workshop/tests/privacy_provider_test.php new file mode 100644 index 0000000000000..5c6951617e0d4 --- /dev/null +++ b/mod/workshop/tests/privacy_provider_test.php @@ -0,0 +1,379 @@ +. + +/** + * Provides the {@link mod_workshop_privacy_provider_testcase} class. + * + * @package mod_workshop + * @category test + * @copyright 2018 David Mudrák + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; + +use core_privacy\local\request\writer; + +/** + * Unit tests for the privacy API implementation. + * + * @copyright 2018 David Mudrák + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class mod_workshop_privacy_provider_testcase extends advanced_testcase { + + /** @var testing_data_generator */ + protected $generator; + + /** @var mod_workshop_generator */ + protected $workshopgenerator; + + /** @var stdClass */ + protected $course1; + + /** @var stdClass */ + protected $course2; + + /** @var stdClass */ + protected $student1; + + /** @var stdClass */ + protected $student2; + + /** @var stdClass */ + protected $student3; + + /** @var stdClass */ + protected $teacher4; + + /** @var stdClass first workshop in course1 */ + protected $workshop11; + + /** @var stdClass second workshop in course1 */ + protected $workshop12; + + /** @var stdClass first workshop in course2 */ + protected $workshop21; + + /** @var int ID of the submission in workshop11 by student1 */ + protected $submission111; + + /** @var int ID of the submission in workshop12 by student1 */ + protected $submission121; + + /** @var int ID of the submission in workshop12 by student2 */ + protected $submission122; + + /** @var int ID of the submission in workshop21 by student2 */ + protected $submission212; + + /** @var int ID of the assessment of submission111 by student1 */ + protected $assessment1111; + + /** @var int ID of the assessment of submission111 by student2 */ + protected $assessment1112; + + /** @var int ID of the assessment of submission111 by student3 */ + protected $assessment1113; + + /** @var int ID of the assessment of submission121 by student2 */ + protected $assessment1212; + + /** @var int ID of the assessment of submission212 by student1 */ + protected $assessment2121; + + /** + * Set up the test environment. + * + * course1 + * | + * +--workshop11 (first digit matches the course, second is incremental) + * | | + * | +--submission111 (first two digits match the workshop, last one matches the author) + * | | + * | +--assessment1111 (first three digits match the submission, last one matches the reviewer) + * | +--assessment1112 + * | +--assessment1113 + * | + * +--workshop12 + * | + * +--submission121 + * | | + * | +--assessment1212 + * | + * +--submission122 + * + * etc. + */ + protected function setUp() { + global $DB; + $this->resetAfterTest(); + $this->setAdminUser(); + + $this->generator = $this->getDataGenerator(); + $this->workshopgenerator = $this->generator->get_plugin_generator('mod_workshop'); + + $this->course1 = $this->generator->create_course(); + $this->course2 = $this->generator->create_course(); + + $this->workshop11 = $this->generator->create_module('workshop', [ + 'course' => $this->course1, + 'name' => 'Workshop11', + ]); + $DB->set_field('workshop', 'phase', 50, ['id' => $this->workshop11->id]); + + $this->workshop12 = $this->generator->create_module('workshop', ['course' => $this->course1]); + $this->workshop21 = $this->generator->create_module('workshop', ['course' => $this->course2]); + + $this->student1 = $this->generator->create_user(); + $this->student2 = $this->generator->create_user(); + $this->student3 = $this->generator->create_user(); + $this->teacher4 = $this->generator->create_user(); + + $this->submission111 = $this->workshopgenerator->create_submission($this->workshop11->id, $this->student1->id); + $this->submission121 = $this->workshopgenerator->create_submission($this->workshop12->id, $this->student1->id, + ['gradeoverby' => $this->teacher4->id]); + $this->submission122 = $this->workshopgenerator->create_submission($this->workshop12->id, $this->student2->id); + $this->submission212 = $this->workshopgenerator->create_submission($this->workshop21->id, $this->student2->id); + + $this->assessment1111 = $this->workshopgenerator->create_assessment($this->submission111, $this->student1->id, [ + 'grade' => null, + ]); + $this->assessment1112 = $this->workshopgenerator->create_assessment($this->submission111, $this->student2->id, [ + 'grade' => 92, + ]); + $this->assessment1113 = $this->workshopgenerator->create_assessment($this->submission111, $this->student3->id); + + $this->assessment1212 = $this->workshopgenerator->create_assessment($this->submission121, $this->student2->id, [ + 'feedbackauthor' => 'This is what student 2 thinks about submission 121', + 'feedbackreviewer' => 'This is what the teacher thinks about this assessment', + ]); + + $this->assessment2121 = $this->workshopgenerator->create_assessment($this->submission212, $this->student1->id, [ + 'grade' => 68, + 'gradinggradeover' => 80, + 'gradinggradeoverby' => $this->teacher4->id, + 'feedbackauthor' => 'This is what student 1 thinks about submission 212', + 'feedbackreviewer' => 'This is what the teacher thinks about this assessment', + ]); + } + + /** + * Test {@link \mod_workshop\privacy\provider::get_contexts_for_userid()} implementation. + */ + public function test_get_contexts_for_userid() { + + $cm11 = get_coursemodule_from_instance('workshop', $this->workshop11->id); + $cm12 = get_coursemodule_from_instance('workshop', $this->workshop12->id); + $cm21 = get_coursemodule_from_instance('workshop', $this->workshop21->id); + + $context11 = context_module::instance($cm11->id); + $context12 = context_module::instance($cm12->id); + $context21 = context_module::instance($cm21->id); + + // Student1 has data in workshop11 (author + self reviewer), workshop12 (author) and workshop21 (reviewer). + $contextlist = \mod_workshop\privacy\provider::get_contexts_for_userid($this->student1->id); + $this->assertInstanceOf(\core_privacy\local\request\contextlist::class, $contextlist); + $this->assertEquals([$context11->id, $context12->id, $context21->id], $contextlist->get_contextids(), null, 0.0, 10, true); + + // Student2 has data in workshop11 (reviewer), workshop12 (reviewer) and workshop21 (author). + $contextlist = \mod_workshop\privacy\provider::get_contexts_for_userid($this->student2->id); + $this->assertEquals([$context11->id, $context12->id, $context21->id], $contextlist->get_contextids(), null, 0.0, 10, true); + + // Student3 has data in workshop11 (reviewer). + $contextlist = \mod_workshop\privacy\provider::get_contexts_for_userid($this->student3->id); + $this->assertEquals([$context11->id], $contextlist->get_contextids(), null, 0.0, 10, true); + + // Teacher4 has data in workshop12 (gradeoverby) and workshop21 (gradinggradeoverby). + $contextlist = \mod_workshop\privacy\provider::get_contexts_for_userid($this->teacher4->id); + $this->assertEquals([$context21->id, $context12->id], $contextlist->get_contextids(), null, 0.0, 10, true); + } + + /** + * Test {@link \mod_workshop\privacy\provider::export_user_data()} implementation. + */ + public function test_export_user_data_1() { + + $contextlist = new \core_privacy\local\request\approved_contextlist($this->student1, 'mod_workshop', [ + \context_module::instance($this->workshop11->cmid)->id, + \context_module::instance($this->workshop12->cmid)->id, + ]); + + \mod_workshop\privacy\provider::export_user_data($contextlist); + + $writer = writer::with_context(\context_module::instance($this->workshop11->cmid)); + + $workshop = $writer->get_data([]); + $this->assertEquals('Workshop11', $workshop->name); + $this->assertObjectHasAttribute('phase', $workshop); + + $mysubmission = $writer->get_data([ + get_string('mysubmission', 'mod_workshop'), + ]); + + $mysubmissionselfassessmentwithoutgrade = $writer->get_data([ + get_string('mysubmission', 'mod_workshop'), + get_string('assessments', 'mod_workshop'), + $this->assessment1111, + ]); + $this->assertNull($mysubmissionselfassessmentwithoutgrade->grade); + $this->assertEquals(get_string('yes'), $mysubmissionselfassessmentwithoutgrade->selfassessment); + + $mysubmissionassessmentwithgrade = $writer->get_data([ + get_string('mysubmission', 'mod_workshop'), + get_string('assessments', 'mod_workshop'), + $this->assessment1112, + ]); + $this->assertEquals(92, $mysubmissionassessmentwithgrade->grade); + $this->assertEquals(get_string('no'), $mysubmissionassessmentwithgrade->selfassessment); + + $mysubmissionassessmentwithoutgrade = $writer->get_data([ + get_string('mysubmission', 'mod_workshop'), + get_string('assessments', 'mod_workshop'), + $this->assessment1113, + ]); + $this->assertEquals(null, $mysubmissionassessmentwithoutgrade->grade); + $this->assertEquals(get_string('no'), $mysubmissionassessmentwithoutgrade->selfassessment); + + $myassessments = $writer->get_data([ + get_string('myassessments', 'mod_workshop'), + ]); + $this->assertEmpty($myassessments); + } + + /** + * Test {@link \mod_workshop\privacy\provider::export_user_data()} implementation. + */ + public function test_export_user_data_2() { + + $contextlist = new \core_privacy\local\request\approved_contextlist($this->student2, 'mod_workshop', [ + \context_module::instance($this->workshop11->cmid)->id, + ]); + + \mod_workshop\privacy\provider::export_user_data($contextlist); + + $writer = writer::with_context(\context_module::instance($this->workshop11->cmid)); + + $assessedsubmission = $writer->get_related_data([ + get_string('myassessments', 'mod_workshop'), + $this->assessment1112, + ], 'submission'); + $this->assertEquals(get_string('no'), $assessedsubmission->myownsubmission); + } + + /** + * Test {@link \mod_workshop\privacy\provider::delete_data_for_all_users_in_context()} implementation. + */ + public function test_delete_data_for_all_users_in_context() { + global $DB; + + $this->assertTrue($DB->record_exists('workshop_submissions', ['workshopid' => $this->workshop11->id])); + + // Passing a non-module context does nothing. + \mod_workshop\privacy\provider::delete_data_for_all_users_in_context(\context_course::instance($this->course1->id)); + $this->assertTrue($DB->record_exists('workshop_submissions', ['workshopid' => $this->workshop11->id])); + + // Passing a workshop context removes all data. + \mod_workshop\privacy\provider::delete_data_for_all_users_in_context(\context_module::instance($this->workshop11->cmid)); + $this->assertFalse($DB->record_exists('workshop_submissions', ['workshopid' => $this->workshop11->id])); + } + + /** + * Test {@link \mod_workshop\privacy\provider::delete_data_for_user()} implementation. + */ + public function test_delete_data_for_user() { + global $DB; + + $student1submissions = $DB->get_records('workshop_submissions', [ + 'workshopid' => $this->workshop12->id, + 'authorid' => $this->student1->id, + ]); + + $student2submissions = $DB->get_records('workshop_submissions', [ + 'workshopid' => $this->workshop12->id, + 'authorid' => $this->student2->id, + ]); + + $this->assertNotEmpty($student1submissions); + $this->assertNotEmpty($student2submissions); + + foreach ($student1submissions as $submission) { + $this->assertNotEquals(get_string('privacy:request:delete:title', 'mod_workshop'), $submission->title); + } + + foreach ($student2submissions as $submission) { + $this->assertNotEquals(get_string('privacy:request:delete:title', 'mod_workshop'), $submission->title); + } + + $contextlist = new \core_privacy\local\request\approved_contextlist($this->student1, 'mod_workshop', [ + \context_module::instance($this->workshop12->cmid)->id, + \context_module::instance($this->workshop21->cmid)->id, + ]); + + \mod_workshop\privacy\provider::delete_data_for_user($contextlist); + + $student1submissions = $DB->get_records('workshop_submissions', [ + 'workshopid' => $this->workshop12->id, + 'authorid' => $this->student1->id, + ]); + + $student2submissions = $DB->get_records('workshop_submissions', [ + 'workshopid' => $this->workshop12->id, + 'authorid' => $this->student2->id, + ]); + + $this->assertNotEmpty($student1submissions); + $this->assertNotEmpty($student2submissions); + + foreach ($student1submissions as $submission) { + $this->assertEquals(get_string('privacy:request:delete:title', 'mod_workshop'), $submission->title); + } + + foreach ($student2submissions as $submission) { + $this->assertNotEquals(get_string('privacy:request:delete:title', 'mod_workshop'), $submission->title); + } + + $student1assessments = $DB->get_records('workshop_assessments', [ + 'submissionid' => $this->submission212, + 'reviewerid' => $this->student1->id, + ]); + $this->assertNotEmpty($student1assessments); + + foreach ($student1assessments as $assessment) { + // In Moodle, feedback is seen to belong to the recipient user. + $this->assertNotEquals(get_string('privacy:request:delete:content', 'mod_workshop'), $assessment->feedbackauthor); + $this->assertEquals(get_string('privacy:request:delete:content', 'mod_workshop'), $assessment->feedbackreviewer); + // We delete what we can without affecting others' grades. + $this->assertEquals(68, $assessment->grade); + } + + $assessments = $DB->get_records_list('workshop_assessments', 'submissionid', array_keys($student1submissions)); + $this->assertNotEmpty($assessments); + + foreach ($assessments as $assessment) { + if ($assessment->reviewerid == $this->student1->id) { + $this->assertNotEquals(get_string('privacy:request:delete:content', 'mod_workshop'), $assessment->feedbackauthor); + $this->assertNotEquals(get_string('privacy:request:delete:content', 'mod_workshop'), $assessment->feedbackreviewer); + + } else { + $this->assertEquals(get_string('privacy:request:delete:content', 'mod_workshop'), $assessment->feedbackauthor); + $this->assertNotEquals(get_string('privacy:request:delete:content', 'mod_workshop'), $assessment->feedbackreviewer); + } + } + } +} From 07dc8783a940d5dcc434d11b4d79bf1398c68f6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Mudr=C3=A1k?= Date: Fri, 27 Apr 2018 11:07:41 +0200 Subject: [PATCH 3/5] MDL-61905 workshop: Implement privacy API in grading strategies On low level, standard grading strategies subplugins do not store personal themselves. They make use of the grades storage provided by the workshop itself. What they do contain though is the information about how the assessment forms were defined. And they are also responsible for correctly interpreting the values in the central grades table. Grading strategies fulfil the contract with the parent workshop module by implementing the workshopform_provider interface. That gives them a chance to export data about the assessment form to each of exported assessment. --- .../accumulative/classes/privacy/provider.php | 105 ++++++++++++ .../lang/en/workshopform_accumulative.php | 1 + .../tests/privacy_provider_test.php | 122 ++++++++++++++ .../comments/classes/privacy/provider.php | 104 ++++++++++++ .../lang/en/workshopform_comments.php | 1 + .../comments/tests/privacy_provider_test.php | 118 +++++++++++++ .../numerrors/classes/privacy/provider.php | 117 +++++++++++++ .../lang/en/workshopform_numerrors.php | 2 + .../numerrors/tests/privacy_provider_test.php | 127 ++++++++++++++ .../form/rubric/classes/privacy/provider.php | 127 ++++++++++++++ .../rubric/lang/en/workshopform_rubric.php | 1 + .../rubric/tests/privacy_provider_test.php | 155 ++++++++++++++++++ 12 files changed, 980 insertions(+) create mode 100644 mod/workshop/form/accumulative/classes/privacy/provider.php create mode 100644 mod/workshop/form/accumulative/tests/privacy_provider_test.php create mode 100644 mod/workshop/form/comments/classes/privacy/provider.php create mode 100644 mod/workshop/form/comments/tests/privacy_provider_test.php create mode 100644 mod/workshop/form/numerrors/classes/privacy/provider.php create mode 100644 mod/workshop/form/numerrors/tests/privacy_provider_test.php create mode 100644 mod/workshop/form/rubric/classes/privacy/provider.php create mode 100644 mod/workshop/form/rubric/tests/privacy_provider_test.php diff --git a/mod/workshop/form/accumulative/classes/privacy/provider.php b/mod/workshop/form/accumulative/classes/privacy/provider.php new file mode 100644 index 0000000000000..e90445e5acf06 --- /dev/null +++ b/mod/workshop/form/accumulative/classes/privacy/provider.php @@ -0,0 +1,105 @@ +. + +/** + * Provides the class {@link workshopform_accumulative\privacy\provider} + * + * @package workshopform_accumulative + * @category privacy + * @copyright 2018 David Mudrák + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace workshopform_accumulative\privacy; + +use core_privacy\local\request\writer; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy API implementation for the Accumulative grading strategy. + * + * @copyright 2018 David Mudrák + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements \core_privacy\local\metadata\null_provider, \mod_workshop\privacy\workshopform_provider { + + /** + * Explain that this plugin stores no personal data. + * + * @return string + */ + public static function get_reason() : string { + return 'privacy:metadata'; + } + + /** + * Return details of the filled assessment form. + * + * @param stdClass $user User we are exporting data for + * @param context $context The workshop activity context + * @param array $subcontext Subcontext within the context to export to + * @param int $assessmentid ID of the assessment + */ + public static function export_assessment_form(\stdClass $user, \context $context, array $subcontext, int $assessmentid) { + global $DB; + + if ($context->contextlevel != CONTEXT_MODULE) { + throw new \coding_exception('Unexpected context provided'); + } + + $sql = "SELECT dim.id, dim.description, dim.descriptionformat, dim.grade AS dimgrade, dim.weight, + wg.grade, wg.peercomment, wg.peercommentformat + FROM {course_modules} cm + JOIN {context} ctx ON ctx.contextlevel = :contextlevel AND ctx.instanceid = cm.id + JOIN {workshop} w ON cm.instance = w.id + JOIN {workshopform_accumulative} dim ON dim.workshopid = w.id + LEFT JOIN {workshop_grades} wg ON wg.strategy = :strategy + AND wg.dimensionid = dim.id AND wg.assessmentid = :assessmentid + WHERE ctx.id = :contextid + ORDER BY dim.sort"; + + $params = [ + 'strategy' => 'accumulative', + 'contextlevel' => CONTEXT_MODULE, + 'contextid' => $context->id, + 'assessmentid' => $assessmentid, + ]; + + $writer = \core_privacy\local\request\writer::with_context($context); + $data = []; + $hasdata = false; + $dimensionids = []; + + foreach ($DB->get_records_sql($sql, $params) as $record) { + if ($record->grade !== null) { + $hasdata = true; + } + $record->description = $writer->rewrite_pluginfile_urls($subcontext, 'workshopform_accumulative', + 'description', $record->id, $record->description); + $dimensionids[] = $record->id; + unset($record->id); + $data[] = $record; + } + + if ($hasdata) { + $writer->export_data($subcontext, (object) ['aspects' => $data]); + foreach ($dimensionids as $dimensionid) { + $writer->export_area_files($subcontext, 'workshopform_accumulative', 'description', $dimensionid); + } + } + } +} diff --git a/mod/workshop/form/accumulative/lang/en/workshopform_accumulative.php b/mod/workshop/form/accumulative/lang/en/workshopform_accumulative.php index 31ab57f431492..0385562f35ded 100644 --- a/mod/workshop/form/accumulative/lang/en/workshopform_accumulative.php +++ b/mod/workshop/form/accumulative/lang/en/workshopform_accumulative.php @@ -39,6 +39,7 @@ $string['pluginname'] = 'Accumulative grading'; $string['poor'] = 'Poor'; $string['present'] = 'Present'; +$string['privacy:metadata'] = 'The Accumulative grading plugin only stores the details of the assessment form. Actual personal data of how the form has been filled are stored by the Workshop module itself and are attached to exported assessments.'; $string['scalename0'] = 'Yes/No (2 point)'; $string['scalename1'] = 'Present/Absent (2 point)'; $string['scalename2'] = 'Correct/Incorrect (2 point)'; diff --git a/mod/workshop/form/accumulative/tests/privacy_provider_test.php b/mod/workshop/form/accumulative/tests/privacy_provider_test.php new file mode 100644 index 0000000000000..d4face425205e --- /dev/null +++ b/mod/workshop/form/accumulative/tests/privacy_provider_test.php @@ -0,0 +1,122 @@ +. + +/** + * Provides the {@link workshopform_accumulative_privacy_provider_testcase} class. + * + * @package workshopform_accumulative + * @category test + * @copyright 2018 David Mudrák + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; + +use core_privacy\local\request\writer; + +/** + * Unit tests for the privacy API implementation. + * + * @copyright 2018 David Mudrák + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class workshopform_accumulative_privacy_provider_testcase extends advanced_testcase { + + /** + * Test {@link workshopform_accumulative\privacy\provider::export_assessment_form()} implementation. + */ + public function test_export_assessment_form() { + global $DB; + $this->resetAfterTest(); + $this->setAdminUser(); + + $this->generator = $this->getDataGenerator(); + $this->workshopgenerator = $this->generator->get_plugin_generator('mod_workshop'); + + $this->course1 = $this->generator->create_course(); + + $this->workshop11 = $this->generator->create_module('workshop', [ + 'course' => $this->course1, + 'name' => 'Workshop11', + ]); + $DB->set_field('workshop', 'phase', 50, ['id' => $this->workshop11->id]); + + $this->dim1 = $DB->insert_record('workshopform_accumulative', [ + 'workshopid' => $this->workshop11->id, + 'sort' => 1, + 'description' => 'Aspect 1 description', + 'descriptionformat' => FORMAT_MARKDOWN, + 'grade' => 6, + 'weight' => 1, + ]); + + $this->dim2 = $DB->insert_record('workshopform_accumulative', [ + 'workshopid' => $this->workshop11->id, + 'sort' => 2, + 'description' => 'Aspect 2 description', + 'descriptionformat' => FORMAT_MARKDOWN, + 'grade' => 4, + 'weight' => 1, + ]); + + $this->student1 = $this->generator->create_user(); + $this->student2 = $this->generator->create_user(); + + $this->submission111 = $this->workshopgenerator->create_submission($this->workshop11->id, $this->student1->id); + + $this->assessment1112 = $this->workshopgenerator->create_assessment($this->submission111, $this->student2->id, [ + 'grade' => 92, + ]); + + $DB->insert_record('workshop_grades', [ + 'assessmentid' => $this->assessment1112, + 'strategy' => 'accumulative', + 'dimensionid' => $this->dim1, + 'grade' => 3, + 'peercomment' => 'Not awesome', + 'peercommentformat' => FORMAT_PLAIN, + ]); + + $DB->insert_record('workshop_grades', [ + 'assessmentid' => $this->assessment1112, + 'strategy' => 'accumulative', + 'dimensionid' => $this->dim2, + 'grade' => 4, + 'peercomment' => 'All good', + 'peercommentformat' => FORMAT_PLAIN, + ]); + + $contextlist = new \core_privacy\local\request\approved_contextlist($this->student2, 'mod_workshop', [ + \context_module::instance($this->workshop11->cmid)->id, + ]); + + \mod_workshop\privacy\provider::export_user_data($contextlist); + + $writer = writer::with_context(\context_module::instance($this->workshop11->cmid)); + + $form = $writer->get_data([ + get_string('myassessments', 'mod_workshop'), + $this->assessment1112, + get_string('assessmentform', 'mod_workshop'), + get_string('pluginname', 'workshopform_accumulative'), + ]); + + $this->assertEquals('Aspect 1 description', $form->aspects[0]->description); + $this->assertEquals(4, $form->aspects[1]->grade); + } +} diff --git a/mod/workshop/form/comments/classes/privacy/provider.php b/mod/workshop/form/comments/classes/privacy/provider.php new file mode 100644 index 0000000000000..e8defd7cd9e1d --- /dev/null +++ b/mod/workshop/form/comments/classes/privacy/provider.php @@ -0,0 +1,104 @@ +. + +/** + * Provides the class {@link workshopform_comments\privacy\provider} + * + * @package workshopform_comments + * @category privacy + * @copyright 2018 David Mudrák + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace workshopform_comments\privacy; + +use core_privacy\local\request\writer; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy API implementation for the Comments grading strategy. + * + * @copyright 2018 David Mudrák + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements \core_privacy\local\metadata\null_provider, \mod_workshop\privacy\workshopform_provider { + + /** + * Explain that this plugin stores no personal data. + * + * @return string + */ + public static function get_reason() : string { + return 'privacy:metadata'; + } + + /** + * Return details of the filled assessment form. + * + * @param stdClass $user User we are exporting data for + * @param context $context The workshop activity context + * @param array $subcontext Subcontext within the context to export to + * @param int $assessmentid ID of the assessment + */ + public static function export_assessment_form(\stdClass $user, \context $context, array $subcontext, int $assessmentid) { + global $DB; + + if ($context->contextlevel != CONTEXT_MODULE) { + throw new \coding_exception('Unexpected context provided'); + } + + $sql = "SELECT dim.id, dim.description, dim.descriptionformat, wg.peercomment, wg.peercommentformat + FROM {course_modules} cm + JOIN {context} ctx ON ctx.contextlevel = :contextlevel AND ctx.instanceid = cm.id + JOIN {workshop} w ON cm.instance = w.id + JOIN {workshopform_comments} dim ON dim.workshopid = w.id + LEFT JOIN {workshop_grades} wg ON wg.strategy = :strategy + AND wg.dimensionid = dim.id AND wg.assessmentid = :assessmentid + WHERE ctx.id = :contextid + ORDER BY dim.sort"; + + $params = [ + 'strategy' => 'comments', + 'contextlevel' => CONTEXT_MODULE, + 'contextid' => $context->id, + 'assessmentid' => $assessmentid, + ]; + + $writer = \core_privacy\local\request\writer::with_context($context); + $data = []; + $hasdata = false; + $dimensionids = []; + + foreach ($DB->get_records_sql($sql, $params) as $record) { + if ($record->peercomment !== null) { + $hasdata = true; + } + $record->description = $writer->rewrite_pluginfile_urls($subcontext, 'workshopform_comments', + 'description', $record->id, $record->description); + $dimensionids[] = $record->id; + unset($record->id); + $data[] = $record; + } + + if ($hasdata) { + $writer->export_data($subcontext, (object) ['aspects' => $data]); + foreach ($dimensionids as $dimensionid) { + $writer->export_area_files($subcontext, 'workshopform_comments', 'description', $dimensionid); + } + } + } +} diff --git a/mod/workshop/form/comments/lang/en/workshopform_comments.php b/mod/workshop/form/comments/lang/en/workshopform_comments.php index fb11fbe7dc9e6..95dd2f745d078 100644 --- a/mod/workshop/form/comments/lang/en/workshopform_comments.php +++ b/mod/workshop/form/comments/lang/en/workshopform_comments.php @@ -28,6 +28,7 @@ $string['dimensiondescription'] = 'Description'; $string['dimensionnumber'] = 'Aspect {$a}'; $string['pluginname'] = 'Comments'; +$string['privacy:metadata'] = 'The Comments grading plugin only stores the details of the assessment form. Actual personal data of how the form has been filled are stored by the Workshop module itself and are attached to exported assessments.'; // Deprecated since Moodle 3.1. $string['dimensioncomment'] = 'Comment'; diff --git a/mod/workshop/form/comments/tests/privacy_provider_test.php b/mod/workshop/form/comments/tests/privacy_provider_test.php new file mode 100644 index 0000000000000..ee78e297be7a7 --- /dev/null +++ b/mod/workshop/form/comments/tests/privacy_provider_test.php @@ -0,0 +1,118 @@ +. + +/** + * Provides the {@link workshopform_comments_privacy_provider_testcase} class. + * + * @package workshopform_comments + * @category test + * @copyright 2018 David Mudrák + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; + +use core_privacy\local\request\writer; + +/** + * Unit tests for the privacy API implementation. + * + * @copyright 2018 David Mudrák + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class workshopform_comments_privacy_provider_testcase extends advanced_testcase { + + /** + * Test {@link workshopform_comments\privacy\provider::export_assessment_form()} implementation. + */ + public function test_export_assessment_form() { + global $DB; + $this->resetAfterTest(); + $this->setAdminUser(); + + $this->generator = $this->getDataGenerator(); + $this->workshopgenerator = $this->generator->get_plugin_generator('mod_workshop'); + + $this->course1 = $this->generator->create_course(); + + $this->workshop11 = $this->generator->create_module('workshop', [ + 'course' => $this->course1, + 'name' => 'Workshop11', + ]); + $DB->set_field('workshop', 'phase', 100, ['id' => $this->workshop11->id]); + + $this->dim1 = $DB->insert_record('workshopform_comments', [ + 'workshopid' => $this->workshop11->id, + 'sort' => 1, + 'description' => 'Aspect 1 description', + 'descriptionformat' => FORMAT_MARKDOWN, + ]); + + $this->dim2 = $DB->insert_record('workshopform_comments', [ + 'workshopid' => $this->workshop11->id, + 'sort' => 2, + 'description' => 'Aspect 2 description', + 'descriptionformat' => FORMAT_MARKDOWN, + ]); + + $this->student1 = $this->generator->create_user(); + $this->student2 = $this->generator->create_user(); + + $this->submission111 = $this->workshopgenerator->create_submission($this->workshop11->id, $this->student1->id); + + $this->assessment1112 = $this->workshopgenerator->create_assessment($this->submission111, $this->student2->id, [ + 'grade' => 92, + ]); + + $DB->insert_record('workshop_grades', [ + 'assessmentid' => $this->assessment1112, + 'strategy' => 'comments', + 'dimensionid' => $this->dim1, + 'grade' => 100, + 'peercomment' => 'Not awesome', + 'peercommentformat' => FORMAT_PLAIN, + ]); + + $DB->insert_record('workshop_grades', [ + 'assessmentid' => $this->assessment1112, + 'strategy' => 'comments', + 'dimensionid' => $this->dim2, + 'grade' => 100, + 'peercomment' => 'All good', + 'peercommentformat' => FORMAT_PLAIN, + ]); + + $contextlist = new \core_privacy\local\request\approved_contextlist($this->student2, 'mod_workshop', [ + \context_module::instance($this->workshop11->cmid)->id, + ]); + + \mod_workshop\privacy\provider::export_user_data($contextlist); + + $writer = writer::with_context(\context_module::instance($this->workshop11->cmid)); + + $form = $writer->get_data([ + get_string('myassessments', 'mod_workshop'), + $this->assessment1112, + get_string('assessmentform', 'mod_workshop'), + get_string('pluginname', 'workshopform_comments'), + ]); + + $this->assertEquals('Aspect 1 description', $form->aspects[0]->description); + $this->assertEquals('All good', $form->aspects[1]->peercomment); + } +} diff --git a/mod/workshop/form/numerrors/classes/privacy/provider.php b/mod/workshop/form/numerrors/classes/privacy/provider.php new file mode 100644 index 0000000000000..fc7bbc5ed7cfc --- /dev/null +++ b/mod/workshop/form/numerrors/classes/privacy/provider.php @@ -0,0 +1,117 @@ +. + +/** + * Provides the class {@link workshopform_numerrors\privacy\provider} + * + * @package workshopform_numerrors + * @category privacy + * @copyright 2018 David Mudrák + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace workshopform_numerrors\privacy; + +use core_privacy\local\request\writer; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy API implementation for the Number of errors strategy. + * + * @copyright 2018 David Mudrák + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements \core_privacy\local\metadata\null_provider, \mod_workshop\privacy\workshopform_provider { + + /** + * Explain that this plugin stores no personal data. + * + * @return string + */ + public static function get_reason() : string { + return 'privacy:metadata'; + } + + /** + * Return details of the filled assessment form. + * + * @param stdClass $user User we are exporting data for + * @param context $context The workshop activity context + * @param array $subcontext Subcontext within the context to export to + * @param int $assessmentid ID of the assessment + */ + public static function export_assessment_form(\stdClass $user, \context $context, array $subcontext, int $assessmentid) { + global $DB; + + if ($context->contextlevel != CONTEXT_MODULE) { + throw new \coding_exception('Unexpected context provided'); + } + + $sql = "SELECT dim.id, dim.workshopid, dim.description, dim.descriptionformat, dim.grade0, dim.grade1, dim.weight, + wg.grade, wg.peercomment, wg.peercommentformat + FROM {course_modules} cm + JOIN {context} ctx ON ctx.contextlevel = :contextlevel AND ctx.instanceid = cm.id + JOIN {workshop} w ON cm.instance = w.id + JOIN {workshopform_numerrors} dim ON dim.workshopid = w.id + LEFT JOIN {workshop_grades} wg ON wg.strategy = :strategy + AND wg.dimensionid = dim.id AND wg.assessmentid = :assessmentid + WHERE ctx.id = :contextid + ORDER BY dim.sort"; + + $params = [ + 'strategy' => 'numerrors', + 'contextlevel' => CONTEXT_MODULE, + 'contextid' => $context->id, + 'assessmentid' => $assessmentid, + ]; + + $writer = \core_privacy\local\request\writer::with_context($context); + $data = []; + $workshopid = null; + $hasdata = false; + $dimensionids = []; + + foreach ($DB->get_records_sql($sql, $params) as $record) { + if ($record->grade !== null) { + $hasdata = true; + } + $record->description = $writer->rewrite_pluginfile_urls($subcontext, 'workshopform_numerrors', + 'description', $record->id, $record->description); + $workshopid = $record->workshopid; + $dimensionids[] = $record->id; + unset($record->id); + unset($record->workshopid); + $data[] = $record; + } + + if ($hasdata) { + $writer->export_data($subcontext, (object)['assertions' => $data]); + foreach ($dimensionids as $dimensionid) { + $writer->export_area_files($subcontext, 'workshopform_numerrors', 'description', $dimensionid); + } + foreach ($DB->get_records('workshopform_numerrors_map', ['workshopid' => $workshopid], 'nonegative', + 'id, nonegative, grade') as $mapping) { + $writer->export_metadata($subcontext, 'map_'.$mapping->nonegative.'_errors', $mapping->grade, + get_string('privacy:export:metadata:map', 'workshopform_numerrors', [ + 'nonegative' => $mapping->nonegative, + 'grade' => $mapping->grade + ]) + ); + } + } + } +} diff --git a/mod/workshop/form/numerrors/lang/en/workshopform_numerrors.php b/mod/workshop/form/numerrors/lang/en/workshopform_numerrors.php index 55fd534985230..0100572f40339 100644 --- a/mod/workshop/form/numerrors/lang/en/workshopform_numerrors.php +++ b/mod/workshop/form/numerrors/lang/en/workshopform_numerrors.php @@ -40,6 +40,8 @@ $string['mapgrade'] = 'Grade for submission'; $string['percents'] = '{$a} %'; $string['pluginname'] = 'Number of errors'; +$string['privacy:metadata'] = 'The Number of errors plugin only stores the details of the assessment form. Actual personal data of how the form has been filled are stored by the Workshop module itself and are attached to exported assessments.'; +$string['privacy:export:metadata:map'] = 'If the weighted number of errors reaches {$a->nonegative} then the grade is {$a->grade} percents.'; // Deprecated since Moodle 3.1. $string['dimensioncomment'] = 'Comment'; diff --git a/mod/workshop/form/numerrors/tests/privacy_provider_test.php b/mod/workshop/form/numerrors/tests/privacy_provider_test.php new file mode 100644 index 0000000000000..bb5abd57d9f58 --- /dev/null +++ b/mod/workshop/form/numerrors/tests/privacy_provider_test.php @@ -0,0 +1,127 @@ +. + +/** + * Provides the {@link workshopform_numerrors_privacy_provider_testcase} class. + * + * @package workshopform_numerrors + * @category test + * @copyright 2018 David Mudrák + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; + +use core_privacy\local\request\writer; + +/** + * Unit tests for the privacy API implementation. + * + * @copyright 2018 David Mudrák + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class workshopform_numerrors_privacy_provider_testcase extends advanced_testcase { + + /** + * Test {@link workshopform_numerrors\privacy\provider::export_assessment_form()} implementation. + */ + public function test_export_assessment_form() { + global $DB; + $this->resetAfterTest(); + $this->setAdminUser(); + + $this->generator = $this->getDataGenerator(); + $this->workshopgenerator = $this->generator->get_plugin_generator('mod_workshop'); + + $this->course1 = $this->generator->create_course(); + + $this->workshop11 = $this->generator->create_module('workshop', [ + 'course' => $this->course1, + 'name' => 'Workshop11', + ]); + $DB->set_field('workshop', 'phase', 50, ['id' => $this->workshop11->id]); + + $this->dim1 = $DB->insert_record('workshopform_numerrors', [ + 'workshopid' => $this->workshop11->id, + 'sort' => 1, + 'description' => 'Assertion 1 description', + 'descriptionformat' => FORMAT_MARKDOWN, + 'descriptiontrust' => 0, + 'grade0' => 'No', + 'grade1' => 'Yes', + 'weight' => 1, + ]); + + $this->dim2 = $DB->insert_record('workshopform_numerrors', [ + 'workshopid' => $this->workshop11->id, + 'sort' => 2, + 'description' => 'Assertion 2 description', + 'descriptionformat' => FORMAT_MARKDOWN, + 'descriptiontrust' => 0, + 'grade0' => 'Missing', + 'grade1' => 'Present', + 'weight' => 1, + ]); + + $this->student1 = $this->generator->create_user(); + $this->student2 = $this->generator->create_user(); + + $this->submission111 = $this->workshopgenerator->create_submission($this->workshop11->id, $this->student1->id); + + $this->assessment1112 = $this->workshopgenerator->create_assessment($this->submission111, $this->student2->id, [ + 'grade' => 92, + ]); + + $DB->insert_record('workshop_grades', [ + 'assessmentid' => $this->assessment1112, + 'strategy' => 'numerrors', + 'dimensionid' => $this->dim1, + 'grade' => 1, + 'peercomment' => 'Awesome', + 'peercommentformat' => FORMAT_PLAIN, + ]); + + $DB->insert_record('workshop_grades', [ + 'assessmentid' => $this->assessment1112, + 'strategy' => 'numerrors', + 'dimensionid' => $this->dim2, + 'grade' => 0, + 'peercomment' => 'Missing', + 'peercommentformat' => FORMAT_PLAIN, + ]); + + $contextlist = new \core_privacy\local\request\approved_contextlist($this->student2, 'mod_workshop', [ + \context_module::instance($this->workshop11->cmid)->id, + ]); + + \mod_workshop\privacy\provider::export_user_data($contextlist); + + $writer = writer::with_context(\context_module::instance($this->workshop11->cmid)); + + $form = $writer->get_data([ + get_string('myassessments', 'mod_workshop'), + $this->assessment1112, + get_string('assessmentform', 'mod_workshop'), + get_string('pluginname', 'workshopform_numerrors'), + ]); + + $this->assertEquals('Assertion 1 description', $form->assertions[0]->description); + $this->assertEquals(0, $form->assertions[1]->grade); + $this->assertEquals('Missing', $form->assertions[1]->peercomment); + } +} diff --git a/mod/workshop/form/rubric/classes/privacy/provider.php b/mod/workshop/form/rubric/classes/privacy/provider.php new file mode 100644 index 0000000000000..9450cb75761d4 --- /dev/null +++ b/mod/workshop/form/rubric/classes/privacy/provider.php @@ -0,0 +1,127 @@ +. + +/** + * Provides the class {@link workshopform_rubric\privacy\provider} + * + * @package workshopform_rubric + * @category privacy + * @copyright 2018 David Mudrák + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace workshopform_rubric\privacy; + +use core_privacy\local\request\writer; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy API implementation for the Rubric strategy. + * + * @copyright 2018 David Mudrák + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements \core_privacy\local\metadata\null_provider, \mod_workshop\privacy\workshopform_provider { + + /** + * Explain that this plugin stores no personal data. + * + * @return string + */ + public static function get_reason() : string { + return 'privacy:metadata'; + } + + /** + * Return details of the filled assessment form. + * + * @param stdClass $user User we are exporting data for + * @param context $context The workshop activity context + * @param array $subcontext Subcontext within the context to export to + * @param int $assessmentid ID of the assessment + */ + public static function export_assessment_form(\stdClass $user, \context $context, array $subcontext, int $assessmentid) { + global $DB; + + if ($context->contextlevel != CONTEXT_MODULE) { + throw new \coding_exception('Unexpected context provided'); + } + + $sql = "SELECT r.id, r.workshopid, r.description, r.descriptionformat, + rl.id AS levelid, rl.grade AS levelgrade, rl.definition, rl.definitionformat, + wg.grade + FROM {course_modules} cm + JOIN {context} ctx ON ctx.contextlevel = :contextlevel AND ctx.instanceid = cm.id + JOIN {workshop} w ON cm.instance = w.id + JOIN {workshopform_rubric} r ON r.workshopid = w.id + JOIN {workshopform_rubric_levels} rl ON rl.dimensionid = r.id + LEFT JOIN {workshop_grades} wg ON wg.strategy = :strategy AND wg.dimensionid = r.id AND wg.assessmentid = :assessmentid + WHERE ctx.id = :contextid + ORDER BY r.sort, rl.grade DESC"; + + $params = [ + 'strategy' => 'rubric', + 'contextlevel' => CONTEXT_MODULE, + 'contextid' => $context->id, + 'assessmentid' => $assessmentid, + ]; + + $writer = \core_privacy\local\request\writer::with_context($context); + $criteria = []; + $workshopid = null; + $hasdata = false; + + $rs = $DB->get_recordset_sql($sql, $params); + + foreach ($rs as $record) { + if (empty($criteria[$record->id])) { + $criteria[$record->id] = (object) [ + 'description' => $writer->rewrite_pluginfile_urls($subcontext, 'workshopform_rubric', 'description', + $record->id, $record->description), + 'descriptionformat' => $record->descriptionformat, + 'grade' => $record->grade, + 'levels' => [], + ]; + $workshopid = $record->workshopid; + } + $criteria[$record->id]->levels[] = (object) [ + 'grade' => $record->levelgrade, + 'definition' => $record->definition, + 'definitionformat' => $record->definitionformat, + ]; + if ($record->grade !== null) { + $hasdata = true; + } + } + + $rs->close(); + + if ($hasdata) { + $data = (object) [ + 'criteria' => array_values($criteria), + ]; + $layout = $DB->get_field('workshopform_rubric_config', 'layout', ['workshopid' => $workshopid]); + + foreach (array_keys($criteria) as $dimensionid) { + $writer->export_area_files($subcontext, 'workshopform_rubric', 'description', $dimensionid); + } + + $writer->export_data($subcontext, $data); + $writer->export_metadata($subcontext, 'layout', $layout, get_string('layout', 'workshopform_rubric')); + } + } +} diff --git a/mod/workshop/form/rubric/lang/en/workshopform_rubric.php b/mod/workshop/form/rubric/lang/en/workshopform_rubric.php index d62860fc1c89f..88f971db7dbf2 100644 --- a/mod/workshop/form/rubric/lang/en/workshopform_rubric.php +++ b/mod/workshop/form/rubric/lang/en/workshopform_rubric.php @@ -37,3 +37,4 @@ $string['mustdefinelevel'] = 'At least one level is required'; $string['mustchooseone'] = 'You have to select one of these items'; $string['pluginname'] = 'Rubric'; +$string['privacy:metadata'] = 'The Rubric plugin only stores the details of the assessment form. Actual personal data of how the form has been filled are stored by the Workshop module itself and are attached to exported assessments.'; diff --git a/mod/workshop/form/rubric/tests/privacy_provider_test.php b/mod/workshop/form/rubric/tests/privacy_provider_test.php new file mode 100644 index 0000000000000..f3a979590da44 --- /dev/null +++ b/mod/workshop/form/rubric/tests/privacy_provider_test.php @@ -0,0 +1,155 @@ +. + +/** + * Provides the {@link workshopform_rubric_privacy_provider_testcase} class. + * + * @package workshopform_rubric + * @category test + * @copyright 2018 David Mudrák + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; + +use core_privacy\local\request\writer; + +/** + * Unit tests for the privacy API implementation. + * + * @copyright 2018 David Mudrák + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class workshopform_rubric_privacy_provider_testcase extends advanced_testcase { + + /** + * Test {@link workshopform_rubric\privacy\provider::export_assessment_form()} implementation. + */ + public function test_export_assessment_form() { + global $DB; + $this->resetAfterTest(); + $this->setAdminUser(); + + $this->generator = $this->getDataGenerator(); + $this->workshopgenerator = $this->generator->get_plugin_generator('mod_workshop'); + + $this->course1 = $this->generator->create_course(); + + $this->workshop11 = $this->generator->create_module('workshop', [ + 'course' => $this->course1, + 'name' => 'Workshop11', + ]); + $DB->set_field('workshop', 'phase', 50, ['id' => $this->workshop11->id]); + + $this->dim1 = $DB->insert_record('workshopform_rubric', [ + 'workshopid' => $this->workshop11->id, + 'sort' => 1, + 'description' => 'Criterion 1 description', + 'descriptionformat' => FORMAT_MARKDOWN, + ]); + + $DB->insert_record('workshopform_rubric_levels', [ + 'dimensionid' => $this->dim1, + 'grade' => 0, + 'definition' => 'Missing', + 'definitionformat' => FORMAT_PLAIN, + ]); + + $DB->insert_record('workshopform_rubric_levels', [ + 'dimensionid' => $this->dim1, + 'grade' => 1, + 'definition' => 'Poor', + 'definitionformat' => FORMAT_PLAIN, + ]); + + $DB->insert_record('workshopform_rubric_levels', [ + 'dimensionid' => $this->dim1, + 'grade' => 2, + 'definition' => 'Good', + 'definitionformat' => FORMAT_PLAIN, + ]); + + $this->dim2 = $DB->insert_record('workshopform_rubric', [ + 'workshopid' => $this->workshop11->id, + 'sort' => 2, + 'description' => 'Criterion 2 description', + 'descriptionformat' => FORMAT_MARKDOWN, + ]); + + $DB->insert_record('workshopform_rubric_levels', [ + 'dimensionid' => $this->dim2, + 'grade' => 0, + 'definition' => 'Missing', + 'definitionformat' => FORMAT_PLAIN, + ]); + + $DB->insert_record('workshopform_rubric_levels', [ + 'dimensionid' => $this->dim2, + 'grade' => 5, + 'definition' => 'Great', + 'definitionformat' => FORMAT_PLAIN, + ]); + + $this->student1 = $this->generator->create_user(); + $this->student2 = $this->generator->create_user(); + + $this->submission111 = $this->workshopgenerator->create_submission($this->workshop11->id, $this->student1->id); + + $this->assessment1112 = $this->workshopgenerator->create_assessment($this->submission111, $this->student2->id, [ + 'grade' => 92, + ]); + + $DB->insert_record('workshop_grades', [ + 'assessmentid' => $this->assessment1112, + 'strategy' => 'rubric', + 'dimensionid' => $this->dim1, + 'grade' => 1, + 'peercomment' => '', + 'peercommentformat' => FORMAT_PLAIN, + ]); + + $DB->insert_record('workshop_grades', [ + 'assessmentid' => $this->assessment1112, + 'strategy' => 'rubric', + 'dimensionid' => $this->dim2, + 'grade' => 5, + 'peercomment' => '', + 'peercommentformat' => FORMAT_PLAIN, + ]); + + $contextlist = new \core_privacy\local\request\approved_contextlist($this->student2, 'mod_workshop', [ + \context_module::instance($this->workshop11->cmid)->id, + ]); + + \mod_workshop\privacy\provider::export_user_data($contextlist); + + $writer = writer::with_context(\context_module::instance($this->workshop11->cmid)); + + $form = $writer->get_data([ + get_string('myassessments', 'mod_workshop'), + $this->assessment1112, + get_string('assessmentform', 'mod_workshop'), + get_string('pluginname', 'workshopform_rubric'), + ]); + + $this->assertEquals('Criterion 1 description', $form->criteria[0]->description); + $this->assertEquals(3, count($form->criteria[0]->levels)); + $this->assertEquals(2, count($form->criteria[1]->levels)); + $this->assertEquals(5, $form->criteria[1]->grade); + } +} From fdcb33c5c5223ef378ab57b4d49ba4defc8d2c45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Mudr=C3=A1k?= Date: Fri, 27 Apr 2018 11:14:11 +0200 Subject: [PATCH 4/5] MDL-61905 workshop: Implement privacy API in assessment allocators Assessment allocation methods normally do not store any personal data. Their duty is to create assessment records that are then exported by the workshop core itself. Still, some allocators (such as the Manual allocation) can store certain personal data such as user preferences. --- .../manual/classes/privacy/provider.php | 68 ++++++++++++++++++ .../lang/en/workshopallocation_manual.php | 1 + .../manual/tests/privacy_provider_test.php | 69 +++++++++++++++++++ .../random/classes/privacy/provider.php | 46 +++++++++++++ .../lang/en/workshopallocation_random.php | 1 + .../scheduled/classes/privacy/provider.php | 46 +++++++++++++ .../lang/en/workshopallocation_scheduled.php | 1 + 7 files changed, 232 insertions(+) create mode 100644 mod/workshop/allocation/manual/classes/privacy/provider.php create mode 100644 mod/workshop/allocation/manual/tests/privacy_provider_test.php create mode 100644 mod/workshop/allocation/random/classes/privacy/provider.php create mode 100644 mod/workshop/allocation/scheduled/classes/privacy/provider.php diff --git a/mod/workshop/allocation/manual/classes/privacy/provider.php b/mod/workshop/allocation/manual/classes/privacy/provider.php new file mode 100644 index 0000000000000..d732ba5b7ac81 --- /dev/null +++ b/mod/workshop/allocation/manual/classes/privacy/provider.php @@ -0,0 +1,68 @@ +. + +/** + * Provides the class {@link workshopallocation_manual\privacy\provider} + * + * @package workshopallocation_manual + * @category privacy + * @copyright 2018 David Mudrák + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace workshopallocation_manual\privacy; + +use core_privacy\local\metadata\collection; +use core_privacy\local\request\writer; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy API implementation for the Manual allocation method. + * + * @copyright 2018 David Mudrák + * @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 { + + /** + * Describe all the places where this plugin stores some personal data. + * + * @param collection $collection Collection of items to add metadata to. + * @return collection Collection with our added items. + */ + public static function get_metadata(collection $collection) : collection { + + $collection->add_user_preference('workshopallocation_manual_perpage', 'privacy:metadata:preference:perpage'); + + return $collection; + } + + /** + * Export user preferences controlled by this plugin. + * + * @param int $userid ID of the user we are exporting data form. + */ + public static function export_user_preferences(int $userid) { + + $perpage = get_user_preferences('workshopallocation_manual_perpage', null, $userid); + + if ($perpage !== null) { + writer::export_user_preference('workshopallocation_manual', 'workshopallocation_manual_perpage', $perpage, + get_string('privacy:metadata:preference:perpage', 'workshopallocation_manual')); + } + } +} diff --git a/mod/workshop/allocation/manual/lang/en/workshopallocation_manual.php b/mod/workshop/allocation/manual/lang/en/workshopallocation_manual.php index 829c37262f072..69365e3922998 100644 --- a/mod/workshop/allocation/manual/lang/en/workshopallocation_manual.php +++ b/mod/workshop/allocation/manual/lang/en/workshopallocation_manual.php @@ -31,4 +31,5 @@ $string['areyousuretodeallocate'] = 'Are you sure you want deallocate the selected assessment?'; $string['areyousuretodeallocategraded'] = 'You are going to remove the assessment that has already been graded. Are you really sure you want to do it?'; $string['pluginname'] = 'Manual allocation'; +$string['privacy:metadata:preference:perpage'] = 'Number of allocated assessments the user prefers to see on one page.'; $string['showallparticipants'] = 'Show all participants'; diff --git a/mod/workshop/allocation/manual/tests/privacy_provider_test.php b/mod/workshop/allocation/manual/tests/privacy_provider_test.php new file mode 100644 index 0000000000000..39faa4c970643 --- /dev/null +++ b/mod/workshop/allocation/manual/tests/privacy_provider_test.php @@ -0,0 +1,69 @@ +. + +/** + * Provides the {@link workshopallocation_manual_privacy_provider_testcase} class. + * + * @package workshopallocation_manual + * @category test + * @copyright 2018 David Mudrák + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +use core_privacy\local\request\writer; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Unit tests for the privacy API implementation. + * + * @copyright 2018 David Mudrák + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class workshopallocation_manual_privacy_provider_testcase extends \core_privacy\tests\provider_testcase { + + /** + * When no preference exists, there should be no export. + */ + public function test_no_preference() { + global $USER; + $this->resetAfterTest(); + $this->setAdminUser(); + + \workshopallocation_manual\privacy\provider::export_user_preferences($USER->id); + $this->assertFalse(writer::with_context(\context_system::instance())->has_any_data()); + } + + /** + * Test that the recently selected perpage is exported. + */ + public function test_export_preferences() { + global $USER; + $this->resetAfterTest(); + $this->setAdminUser(); + + set_user_preference('workshopallocation_manual_perpage', 81); + + \workshopallocation_manual\privacy\provider::export_user_preferences($USER->id); + $this->assertTrue(writer::with_context(\context_system::instance())->has_any_data()); + + $prefs = writer::with_context(\context_system::instance())->get_user_preferences('workshopallocation_manual'); + $this->assertNotEmpty($prefs->workshopallocation_manual_perpage); + $this->assertEquals(81, $prefs->workshopallocation_manual_perpage->value); + $this->assertContains(get_string('privacy:metadata:preference:perpage', 'workshopallocation_manual'), + $prefs->workshopallocation_manual_perpage->description); + } +} diff --git a/mod/workshop/allocation/random/classes/privacy/provider.php b/mod/workshop/allocation/random/classes/privacy/provider.php new file mode 100644 index 0000000000000..b84b420fa6645 --- /dev/null +++ b/mod/workshop/allocation/random/classes/privacy/provider.php @@ -0,0 +1,46 @@ +. + +/** + * Provides the class {@link workshopallocation_random\privacy\provider} + * + * @package workshopallocation_random + * @category privacy + * @copyright 2018 David Mudrák + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace workshopallocation_random\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy API implementation for the Random allocation method. + * + * @copyright 2018 David Mudrák + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements \core_privacy\local\metadata\null_provider { + + /** + * Explain that this plugin stores no personal data. + * + * @return string + */ + public static function get_reason() : string { + return 'privacy:metadata'; + } +} diff --git a/mod/workshop/allocation/random/lang/en/workshopallocation_random.php b/mod/workshop/allocation/random/lang/en/workshopallocation_random.php index 86578ccae57d7..1a594f4a5450d 100644 --- a/mod/workshop/allocation/random/lang/en/workshopallocation_random.php +++ b/mod/workshop/allocation/random/lang/en/workshopallocation_random.php @@ -43,6 +43,7 @@ $string['numperauthor'] = 'per submission'; $string['numperreviewer'] = 'per reviewer'; $string['pluginname'] = 'Random allocation'; +$string['privacy:metadata'] = 'The Random allocation plugin does not store any personal data. Actual personal data about who is going to assess whom are stored by the Workshop module itself and they form basis for exporting the assessments details.'; $string['randomallocationdone'] = 'Random allocation done'; $string['resultnomorepeers'] = 'No more peers available'; $string['resultnomorepeersingroup'] = 'No more peers available in this separate group'; diff --git a/mod/workshop/allocation/scheduled/classes/privacy/provider.php b/mod/workshop/allocation/scheduled/classes/privacy/provider.php new file mode 100644 index 0000000000000..b94fcc805ffb7 --- /dev/null +++ b/mod/workshop/allocation/scheduled/classes/privacy/provider.php @@ -0,0 +1,46 @@ +. + +/** + * Provides the class {@link workshopallocation_scheduled\privacy\provider} + * + * @package workshopallocation_scheduled + * @category privacy + * @copyright 2018 David Mudrák + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace workshopallocation_scheduled\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy API implementation for the Scheduled allocation method. + * + * @copyright 2018 David Mudrák + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements \core_privacy\local\metadata\null_provider { + + /** + * Explain that this plugin stores no personal data. + * + * @return string + */ + public static function get_reason() : string { + return 'privacy:metadata'; + } +} diff --git a/mod/workshop/allocation/scheduled/lang/en/workshopallocation_scheduled.php b/mod/workshop/allocation/scheduled/lang/en/workshopallocation_scheduled.php index 4d95c3720c38a..27d7a74caf3cb 100644 --- a/mod/workshop/allocation/scheduled/lang/en/workshopallocation_scheduled.php +++ b/mod/workshop/allocation/scheduled/lang/en/workshopallocation_scheduled.php @@ -47,6 +47,7 @@ Note that the scheduled allocation is *not* executed if you manually switch the workshop into the assessment phase before the submissions deadline. You have to allocate submissions yourself in that case. The scheduled allocation method is particularly useful when used together with the automatic phase switching feature.'; $string['pluginname'] = 'Scheduled allocation'; +$string['privacy:metadata'] = 'The Scheduled allocation plugin does not store any personal data. Actual personal data about who is going to assess whom are stored by the Workshop module itself and they form basis for exporting the assessments details.'; $string['randomallocationsettings'] = 'Allocation settings'; $string['randomallocationsettings_help'] = 'Parameters for the random allocation method are defined here. They will be used by the random allocation plugin for the actual allocation of submissions.'; $string['resultdisabled'] = 'Scheduled allocation disabled'; From 0d7e2fc5ec02559cc377b3500ed004d7a3203285 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Mudr=C3=A1k?= Date: Fri, 27 Apr 2018 11:19:25 +0200 Subject: [PATCH 5/5] MDL-61905 workshop: Implement privacy API in grading evaluators Standard workshop ships with only one evaluation method and that one holds no personal data. --- .../eval/best/classes/privacy/provider.php | 46 +++++++++++++++++++ .../eval/best/lang/en/workshopeval_best.php | 1 + 2 files changed, 47 insertions(+) create mode 100644 mod/workshop/eval/best/classes/privacy/provider.php diff --git a/mod/workshop/eval/best/classes/privacy/provider.php b/mod/workshop/eval/best/classes/privacy/provider.php new file mode 100644 index 0000000000000..501727655d694 --- /dev/null +++ b/mod/workshop/eval/best/classes/privacy/provider.php @@ -0,0 +1,46 @@ +. + +/** + * Provides the class {@link workshopeval_best\privacy\provider} + * + * @package workshopeval_best + * @category privacy + * @copyright 2018 David Mudrák + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace workshopeval_best\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy API implementation for the Comparison with the best assessment method. + * + * @copyright 2018 David Mudrák + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements \core_privacy\local\metadata\null_provider { + + /** + * Explain that this plugin stores no personal data. + * + * @return string + */ + public static function get_reason() : string { + return 'privacy:metadata'; + } +} diff --git a/mod/workshop/eval/best/lang/en/workshopeval_best.php b/mod/workshop/eval/best/lang/en/workshopeval_best.php index 6af9f37dbaf2c..e6c813f266d5a 100644 --- a/mod/workshop/eval/best/lang/en/workshopeval_best.php +++ b/mod/workshop/eval/best/lang/en/workshopeval_best.php @@ -33,3 +33,4 @@ $string['comparisonlevel9'] = 'very lax'; $string['configcomparison'] = 'Default value of the factor that influence the grading evaluation.'; $string['pluginname'] = 'Comparison with the best assessment'; +$string['privacy:metadata'] = 'The Comparison with the best assessment plugin does not store any personal data. Actual personal data user\'s grades are stored by the Workshop module itself and are attached to the exported submissions and assessments data.';