diff --git a/dta.zip b/dta.zip
index 1dc2cab2e4b76667b091b2d6c98f629ee9e195a3..afca1bf23f47123d6decf766493a73598665084d 100644
Binary files a/dta.zip and b/dta.zip differ
diff --git a/dta/models/DtaResult.php b/dta/models/DtaResult.php
index 85e1ee7ac854335464a7604f502be043f3cc366c..a06e33d2976082a5c7c3280d70fa67a020761842 100644
--- a/dta/models/DtaResult.php
+++ b/dta/models/DtaResult.php
@@ -69,7 +69,7 @@ class DtaResult {
 
     /**
      * @var $failurereason Reason of test failure if applicable, "" otherwise.
-     */    
+     */
     public $failurereason;
 
     /**
@@ -119,7 +119,7 @@ class DtaResultSummary {
 
     /**
      * @var $timestamp Result timestamp for chronological ordering and deletion of previous results.
-     */    
+     */
     public $timestamp;
 
     /**
diff --git a/dta/privacy/provider.php b/dta/privacy/provider.php
new file mode 100644
index 0000000000000000000000000000000000000000..7f30ade97fe221410ec11be12d0f2e4d215f26ca
--- /dev/null
+++ b/dta/privacy/provider.php
@@ -0,0 +1,264 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * provider for data privacy
+ *
+ * @package assignsubmission_dta
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @copyright Gero Lueckemeyer and student project teams
+ */
+namespace assignsubmission_dta\privacy;
+
+defined('MOODLE_INTERNAL') || die();
+
+use core_privacy\local\metadata\collection;
+use \core_privacy\local\request\writer;
+use \core_privacy\local\request\contextlist;
+use \mod_assign\privacy\assign_plugin_request_data;
+
+class provider implements
+        // This plugin does store personal user data.
+        \core_privacy\local\metadata\provider,
+        \mod_assign\privacy\assignsubmission_provider,
+        \mod_assign\privacy\assignsubmission_user_provider
+        {
+
+    /**
+     * File area for dta submission assignment.
+     */
+    const ASSIGNSUBMISSION_DTA_FILEAREA_SUBMISSION = "submissions_dta";
+
+    public static function get_metadata(collection $collection): collection {
+        $collection->add_subsystem_link(
+            'core_files',
+            [],
+            'privacy:metadata:core_files'
+        );
+        
+        $collection->add_database_table(
+            'assignsubmission_dta_summary',
+            [
+                'assignmentid' => 'privacy:metadata:assignsubmission_dta_summary:assignmentid',
+                'submissionid' => 'privacy:metadata:assignsubmission_dta_summary:submissionid',
+                'timestamp' => 'privacy:metadata:assignsubmission_dta_summary:timestamp',
+                'global_stacktrace' => 'privacy:metadata:assignsubmission_dta_summary:global_stacktrace',
+                'successful_competencies' => 'privacy:metadata:assignsubmission_dta_summary:successful_competencies',
+                'tested_competencies' => 'privacy:metadata:assignsubmission_dta_summary:tested_competencies',
+            ],
+            'privacy:metadata:assignsubmission_dta_summary'
+        );
+
+        $collection->add_database_table(
+            'assignsubmission_dta_result',
+            [
+                'assignmentid' => 'privacy:metadata:assignsubmission_dta_result:assignmentid',
+                'submissionid' => 'privacy:metadata:assignsubmission_dta_result:submissionid',
+                'package_name' => 'privacy:metadata:assignsubmission_dta_result:package_name',
+                'class_name' => 'privacy:metadata:assignsubmission_dta_result:class_name',
+                'name' => 'privacy:metadata:assignsubmission_dta_result:name',
+                'state' => 'privacy:metadata:assignsubmission_dta_result:state',
+                'failure_type' => 'privacy:metadata:assignsubmission_dta_result:failure_type',
+                'failure_reason' => 'privacy:metadata:assignsubmission_dta_result:failure_reason',
+                'stacktrace' => 'privacy:metadata:assignsubmission_dta_result:stacktrace',
+                'column_number' => 'privacy:metadata:assignsubmission_dta_result:column_number',
+                'line_number' => 'privacy:metadata:assignsubmission_dta_result:line_number',
+                'position' => 'privacy:metadata:assignsubmission_dta_result:position',
+            ],
+            'privacy:metadata:assignsubmission_dta_result'
+        );
+        
+        $collection->add_external_location_link('dta_backend', [
+                'assignmentid' => 'privacy:metadata:assignsubmission_dta_result:assignmentid',
+                'submissionid' => 'privacy:metadata:assignsubmission_dta_result:submissionid',
+                'submissioncontent' => 'privacy:metadata:core_files',
+            ], 
+            'privacy:metadata:dta_backend'
+        );
+
+        return $collection;
+    }
+
+    /**
+     * This is covered by mod_assign provider and the query on assign_submissions.
+     *
+     * @param  int $userid The user ID that we are finding contexts for.
+     * @param  contextlist $contextlist A context list to add sql and params to for contexts.
+     */
+    public static function get_context_for_userid_within_submission(int $userid, contextlist $contextlist) {
+        // This is already fetched from mod_assign.
+    }
+
+    /**
+     * This is also covered by the mod_assign provider and its queries.
+     *
+     * @param  \mod_assign\privacy\useridlist $useridlist An object for obtaining user IDs of students.
+     */
+    public static function get_student_user_ids(\mod_assign\privacy\useridlist $useridlist) {
+        // This is already fetched from mod_assign.
+    }
+
+    /**
+     * If you have tables that contain userids and you can generate entries in your tables without creating an
+     * entry in the assign_submission table then please fill in this method.
+     *
+     * @param  userlist $userlist The userlist object
+     */
+    public static function get_userids_from_context(\core_privacy\local\request\userlist $userlist) {
+        // Not required.
+    }
+
+    /**
+     * Export all user data for this plugin.
+     *
+     * @param  assign_plugin_request_data $exportdata Data used to determine which context and user to export and other useful
+     * information to help with exporting.
+     */
+    public static function export_submission_user_data(assign_plugin_request_data $exportdata) {
+        // We currently don't show submissions to teachers when exporting their data.
+        $context = $exportdata->get_context();
+        if ($exportdata->get_user() != null) {
+            return null;
+        }
+        $user = new \stdClass();
+        $assign = $exportdata->get_assign();
+        $submission = $exportdata->get_pluginobject();
+        $files = get_files($submission, $user);
+        foreach ($files as $file) {
+            $userid = $exportdata->get_pluginobject()->userid;
+            $dtaresultsummary=DBUtils::getresultsummaryfromdatabase($assign->id, $submission->id);
+            // Submitted file.
+            writer::with_context($exportdata->get_context())->export_file($exportdata->get_subcontext(), $file)
+            // DTA result.
+            ->export_related_data($dtaresultsummary);             
+
+            // Plagiarism data.
+            $coursecontext = $context->get_course_context();
+            \core_plagiarism\privacy\provider::export_plagiarism_user_data($userid, $context, $exportdata->get_subcontext(), [
+                'cmid' => $context->instanceid,
+                'course' => $coursecontext->instanceid,
+                'userid' => $userid,
+                'file' => $file
+            ]);
+        }
+    }
+
+    /**
+     * Any call to this method should delete all user data for the context defined in the deletion_criteria.
+     *
+     * @param  assign_plugin_request_data $requestdata Information useful for deleting user data.
+     */
+    public static function delete_submission_for_context(assign_plugin_request_data $requestdata) {
+        global $DB;
+
+        \core_plagiarism\privacy\provider::delete_plagiarism_for_context($requestdata->get_context());
+
+        $fs = get_file_storage();
+        $fs->delete_area_files($requestdata->get_context()->id, 'assignsubmission_dta', ASSIGNSUBMISSION_DTA_FILEAREA_SUBMISSION);
+
+        $assignmentid = $requestdata->get_assign()->get_instance()->id;
+                
+        // Delete records from assignsubmission_dta tables.
+        $DB->delete_records('assignsubmission_dta_result', ['assignmentid' => $assignmentid]);
+        $DB->delete_records('assignsubmission_dta_summary', ['assignmentid' => $assignmentid]);
+    }
+
+    /**
+     * A call to this method should delete user data (where practical) using the userid and submission.
+     *
+     * @param  assign_plugin_request_data $deletedata Details about the user and context to focus the deletion.
+     */
+    public static function delete_submission_for_userid(assign_plugin_request_data $deletedata) {
+        global $DB;
+
+        \core_plagiarism\privacy\provider::delete_plagiarism_for_user($deletedata->get_user()->id, $deletedata->get_context());
+
+        $assignmentid = $deletedata->get_assign()->get_instance()->id;
+        $submissionid = $deletedata->get_pluginobject()->id;
+
+        $fs = get_file_storage();
+        $fs->delete_area_files($deletedata->get_context()->id, 'assignsubmission_dta', ASSIGNSUBMISSION_DTA_FILEAREA_SUBMISSION,
+                $submissionid);
+
+        // Delete records from assignsubmission_dta tables. Also possible with a list as below.
+        $DB->delete_records('assignsubmission_dta_result', [
+            'assignmentid' => $assignmentid, 
+            'submissionid' => $submissionid,
+            ]);
+        $DB->delete_records('assignsubmission_dta_summary', [
+            'assignmentid' => $assignmentid, 
+            'submissionid' => $submissionid,
+            ]);
+    }
+    
+    /**
+     * Deletes all submissions for the submission ids / userids provided in a context.
+     * assign_plugin_request_data contains:
+     * - context
+     * - assign object
+     * - submission ids (pluginids)
+     * - user ids
+     * @param  assign_plugin_request_data $deletedata A class that contains the relevant information required for deletion.
+     */
+    public static function delete_submissions(assign_plugin_request_data $deletedata) {
+        global $DB;
+
+        \core_plagiarism\privacy\provider::delete_plagiarism_for_users($deletedata->get_userids(), $deletedata->get_context());
+
+        if (empty($deletedata->get_submissionids())) {
+            return;
+        }
+        $fs = get_file_storage();
+        list($sql, $params) = $DB->get_in_or_equal($deletedata->get_submissionids(), SQL_PARAMS_NAMED);
+        $fs->delete_area_files_select($deletedata->get_context()->id, 'assignsubmission_file', ASSIGNSUBMISSION_FILE_FILEAREA,
+                $sql, $params);
+
+        $params['assignid'] = $deletedata->get_assignid();
+        $DB->delete_records_select('assignsubmission_dta_result', "assignmentid = :assignid AND submissionid $sql", $params);
+        $DB->delete_records_select('assignsubmission_dta_summary', "assignmentid = :assignid AND submissionid $sql", $params);
+    }
+
+    /**
+     * Produce a list of files suitable for export that represent this feedback or submission
+     *
+     * @param stdClass $submission The submission
+     * @param stdClass $user The user record - unused
+     * @return array - return an array of files indexed by filename
+     */
+    public function get_files(stdClass $submission, stdClass $user) {
+        $result = array();
+        $fs = get_file_storage();
+
+        $files = $fs->get_area_files($this->assignment->get_context()->id,
+                                     'assignsubmission_file',
+                                     ASSIGNSUBMISSION_DTA_FILEAREA_SUBMISSION,
+                                     $submission->id,
+                                     'timemodified',
+                                     false);
+
+        foreach ($files as $file) {
+            // Do we return the full folder path or just the file name?
+            if (isset($submission->exportfullpath) && $submission->exportfullpath == false) {
+                $result[$file->get_filename()] = $file;
+            } else {
+                $result[$file->get_filepath().$file->get_filename()] = $file;
+            }
+        }
+        return $result;
+    }
+
+
+}
\ No newline at end of file
diff --git a/dta/utils/view.php b/dta/utils/view.php
index 34d0881ca2a56ac8681547a173f3a72cc1b2bd88..ebd223a3c6b4df92cfee2b9f57a6ca956fc2e0e2 100644
--- a/dta/utils/view.php
+++ b/dta/utils/view.php
@@ -241,7 +241,7 @@ class view_submission_utils {
                 // New copy of base attributes array.
                 $resultrowattributes = $tablerowattributes;
                 $tmp = "";
-                $tmp .= html_writer::tag("td", get_string("comp" . $index, self::COMPONENT_NAME), $resultrowattributes);                
+                $tmp .= html_writer::tag("td", get_string("comp" . $index, self::COMPONENT_NAME), $resultrowattributes);
                 $tmp .= html_writer::tag("td", 100 * floatval($shown) / floatval($comp) . "% " .
                     "(" . $shown . " / " . $comp . ")", $resultrowattributes);
                 $tmp .= html_writer::tag("td", get_string("comp_expl" . $index, self::COMPONENT_NAME), $resultrowattributes);
diff --git a/test/privacy/apicompliance.php b/test/privacy/apicompliance.php
new file mode 100644
index 0000000000000000000000000000000000000000..121ad630786845a1575b9cb058284d6dd6279770
--- /dev/null
+++ b/test/privacy/apicompliance.php
@@ -0,0 +1,99 @@
+<?php
+
+// Set this if you want to run the script for one component only. Otherwise leave empty.
+$CHECK_COMPONENT = '';
+
+define('CLI_SCRIPT', true);
+
+require_once('config.php');
+
+$user = \core_user::get_user(2);
+
+\core\session\manager::init_empty_session();
+\core\session\manager::set_user($user);
+
+$rc = new \ReflectionClass(\core_privacy\manager::class);
+$rcm = $rc->getMethod('get_component_list');
+$rcm->setAccessible(true);
+
+$manager = new \core_privacy\manager();
+$components = $rcm->invoke($manager);
+
+$list = (object) [
+    'good' => [],
+    'bad' => [],
+];
+
+foreach ($components as $component) {
+    if ($CHECK_COMPONENT && $component !== $CHECK_COMPONENT) {
+        continue;
+    }
+    $compliant = $manager->component_is_compliant($component);
+    if ($compliant) {
+        $list->good[] = $component;
+    } else {
+        $list->bad[] = $component;
+    }
+}
+
+echo "The following plugins are not compliant:\n";
+echo "=> " . implode("\n=> ", array_values($list->bad)) . "\n";
+
+echo "\n";
+echo "Testing the compliant plugins:\n";
+foreach ($list->good as $component) {
+    $classname = \core_privacy\manager::get_provider_classname_for_component($component);
+    echo "== {$component} ($classname) ==\n";
+    if (check_implements($component, \core_privacy\local\metadata\null_provider::class)) {
+        echo "    Claims not to store any data with reason:\n";
+        echo "      '" . get_string($classname::get_reason(), $component) . "'\n";
+    }
+    else if (check_implements($component, \core_privacy\local\metadata\provider::class)) {
+        $collection = new \core_privacy\local\metadata\collection($component);
+        $classname::get_metadata($collection);
+        $count = count($collection->get_collection());
+        echo "    Found {$count} items of metadata\n";
+        if (empty($count)) {
+            echo "!!! No metadata found!!! This an error.\n";
+        }
+
+        if (check_implements($component, \core_privacy\local\request\user_preference_provider::class)) {
+            $userprefdescribed = false;
+            foreach ($collection->get_collection() as $item) {
+                if ($item instanceof \core_privacy\local\metadata\types\user_preference) {
+                    $userprefdescribed = true;
+                    echo "     ".$item->get_name()." : ".get_string($item->get_summary(), $component) . "\n";
+                }
+            }
+            if (!$userprefdescribed) {
+                echo "!!! User preference found, but was not described in metadata\n";
+            }
+        }
+
+        if (check_implements($component, \core_privacy\local\request\core_user_data_provider::class)) {
+            // No need to check the return type - it's enforced by the interface.
+            $contextlist = $classname::get_contexts_for_userid($user->id);
+            $approvedcontextlist = new \core_privacy\local\request\approved_contextlist($user, $contextlist->get_component(), $contextlist->get_contextids());
+            if (count($approvedcontextlist)) {
+                $classname::export_user_data($approvedcontextlist);
+                echo "    Successfully ran a test export\n";
+            } else {
+                echo "    Nothing to export.\n";
+            }
+        }
+        if (check_implements($component, \core_privacy\local\request\shared_data_provider::class)) {
+            echo "    This is a shared data provider\n";
+        }
+    }
+}
+
+echo "\n\n== Done ==\n";
+
+function check_implements($component, $interface) {
+    $manager = new \core_privacy\manager();
+    $rc = new \ReflectionClass(\core_privacy\manager::class);
+    $rcm = $rc->getMethod('component_implements');
+    $rcm->setAccessible(true);
+
+    return $rcm->invoke($manager, $component, $interface);
+}
\ No newline at end of file
diff --git a/test/privacy/deletion.php b/test/privacy/deletion.php
new file mode 100644
index 0000000000000000000000000000000000000000..8fdb75ce0023ad73b692d4cae61f6dbdb40a58ba
--- /dev/null
+++ b/test/privacy/deletion.php
@@ -0,0 +1,70 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+define('CLI_SCRIPT', true);
+require_once('config.php');
+require_once("$CFG->libdir/clilib.php");
+
+list($options, $unrecognized) = cli_get_params(
+    [
+        'username' => '',
+        'userid' => '',
+    ],
+    []
+);
+
+$user = null;
+$username = $options['username'];
+$userid = $options['userid'];
+
+if (!empty($options['username'])) {
+    $user = \core_user::get_user_by_username($options['username']);
+} else if (!empty($options['userid'])) {
+    $user = \core_user::get_user($options['userid']);
+}
+
+while (empty($user)) {
+    if (!empty($username)) {
+        echo "Unable to find a user with username '{$username}'.\n";
+        echo "Try again.\n";
+    } else if (!empty($userid)) {
+        echo "Unable to find a user with userid '{$userid}'.\n";
+        echo "Try again.\n";
+    }
+    $username = readline("Username: ");
+    $user = \core_user::get_user_by_username($username);
+}
+
+echo "Processing delete for " . fullname($user) . "\n";
+
+\core\session\manager::init_empty_session();
+\core\session\manager::set_user($user);
+
+$manager = new \core_privacy\manager();
+
+$approvedlist = new \core_privacy\local\request\contextlist_collection($user->id);
+
+$trace = new text_progress_trace();
+$contextlists = $manager->get_contexts_for_userid($user->id, $trace);
+foreach ($contextlists as $contextlist) {
+    $approvedlist->add_contextlist(new \core_privacy\local\request\approved_contextlist(
+        $user,
+        $contextlist->get_component(),
+        $contextlist->get_contextids()
+    ));
+}
+
+$manager->delete_data_for_user($approvedlist, $trace);
\ No newline at end of file
diff --git a/test/privacy/export.php b/test/privacy/export.php
new file mode 100644
index 0000000000000000000000000000000000000000..f56d61711f9fac857730470774ac5551a29d5ecd
--- /dev/null
+++ b/test/privacy/export.php
@@ -0,0 +1,87 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Helper utility to perform a test export.
+ *
+ * @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+define('CLI_SCRIPT', true);
+require_once('config.php');
+require_once("$CFG->libdir/clilib.php");
+
+list($options, $unrecognized) = cli_get_params(
+    [
+        'username' => '',
+        'userid' => '',
+    ],
+    []
+);
+
+$user = null;
+$username = $options['username'];
+$userid = $options['userid'];
+
+if (!empty($options['username'])) {
+    $user = \core_user::get_user_by_username($options['username']);
+} else if (!empty($options['userid'])) {
+    $user = \core_user::get_user($options['userid']);
+}
+
+while (empty($user)) {
+    if (!empty($username)) {
+        echo "Unable to find a user with username '{$username}'.\n";
+        echo "Try again.\n";
+    } else if (!empty($userid)) {
+        echo "Unable to find a user with userid '{$userid}'.\n";
+        echo "Try again.\n";
+    }
+    $username = readline("Username: ");
+    $user = \core_user::get_user_by_username($username);
+}
+
+echo "Processing export for " . fullname($user) . "\n";
+
+\core\session\manager::init_empty_session();
+\core\session\manager::set_user($user);
+
+$PAGE = new moodle_page();
+$OUTPUT = new core_renderer($PAGE, RENDERER_TARGET_GENERAL);
+
+$manager = new \core_privacy\manager();
+
+$approvedlist = new \core_privacy\local\request\contextlist_collection($user->id);
+
+$contextlists = $manager->get_contexts_for_userid($user->id);
+foreach ($contextlists as $contextlist) {
+    $approvedlist->add_contextlist(new \core_privacy\local\request\approved_contextlist(
+        $user,
+        $contextlist->get_component(),
+        $contextlist->get_contextids()
+    ));
+}
+
+$exportedcontent = $manager->export_user_data($approvedlist);
+$basedir = make_temp_directory('privacy');
+$exportpath = make_unique_writable_directory($basedir, true);
+$fp = get_file_packer();
+$fp->extract_to_pathname($exportedcontent, $exportpath);
+
+echo "\n";
+echo "== File export was uncompressed to {$exportpath}\n";
+echo "============================================================================\n";
\ No newline at end of file