diff --git a/completion/classes/privacy/provider.php b/completion/classes/privacy/provider.php new file mode 100644 index 0000000000000..e8d2f9e1fa702 --- /dev/null +++ b/completion/classes/privacy/provider.php @@ -0,0 +1,205 @@ +. + +/** + * Privacy class for requesting user data. + * + * @package core_completion + * @copyright 2018 Adrian Greeve + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_completion\privacy; + +defined('MOODLE_INTERNAL') || die(); + +use \core_privacy\local\metadata\collection; +use \core_privacy\local\request\transform; +use \core_privacy\local\request\contextlist; + +require_once($CFG->dirroot . '/comment/lib.php'); + +/** + * Privacy class for requesting user data. + * + * @package core_completion + * @copyright 2018 Adrian Greeve + * @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\subsystem\plugin_provider { + + /** + * Returns meta data about this system. + * + * @param collection $collection The initialised collection to add items to. + * @return collection A listing of user data stored through this system. + */ + public static function get_metadata(collection $collection) : collection { + $collection->add_database_table('course_completions', [ + 'userid' => 'privacy:metadata:userid', + 'course' => 'privacy:metadata:course', + 'timeenrolled' => 'privacy:metadata:timeenrolled', + 'timestarted' => 'privacy:metadata:timestarted', + 'timecompleted' => 'privacy:metadata:timecompleted', + 'reaggregate' => 'privacy:metadata:reaggregate' + ], 'privacy:metadata:coursesummary'); + $collection->add_database_table('course_modules_completion', [ + 'userid' => 'privacy:metadata:userid', + 'coursemoduleid' => 'privacy:metadata:coursemoduleid', + 'completionstate' => 'privacy:metadata:completionstate', + 'viewed' => 'privacy:metadata:viewed', + 'overrideby' => 'privacy:metadata:overrideby', + 'timemodified' => 'privacy:metadata:timemodified' + ], 'privacy:metadata:coursemodulesummary'); + $collection->add_database_table('course_completion_crit_compl', [ + 'userid' => 'privacy:metadata:userid', + 'course' => 'privacy:metadata:course', + 'gradefinal' => 'privacy:metadata:gradefinal', + 'unenroled' => 'privacy:metadata:unenroled', + 'timecompleted' => 'privacy:metadata:timecompleted' + ], 'privacy:metadata:coursecompletedsummary'); + return $collection; + } + + /** + * Get join sql to retrieve courses the user is in. + * + * @param int $userid The user ID + * @param string $prefix A unique prefix for these joins. + * @param string $joinfield A field to join these tables to. Joins to course ID. + * @return array The join, where, and params for this join. + */ + public static function get_course_completion_join_sql(int $userid, string $prefix, string $joinfield) : array { + $cccalias = "{$prefix}_ccc"; // Course completion criteria. + $cmcalias = "{$prefix}_cmc"; // Course modules completion. + $ccccalias = "{$prefix}_cccc"; // Course completion criteria completion. + + $join = "JOIN {course_completion_criteria} {$cccalias} ON {$joinfield} = {$cccalias}.course + LEFT JOIN {course_modules_completion} {$cmcalias} ON {$cccalias}.moduleinstance = {$cmcalias}.coursemoduleid + LEFT JOIN {course_completion_crit_compl} {$ccccalias} ON {$ccccalias}.criteriaid = {$cccalias}.id"; + $where = "{$cmcalias}.userid = :{$prefix}_moduleuserid OR {$ccccalias}.userid = :{$prefix}_courseuserid"; + $params = ["{$prefix}_moduleuserid" => $userid, "{$prefix}_courseuserid" => $userid]; + + return [$join, $where, $params]; + } + + /** + * Returns activity completion information about a user. + * + * @param \stdClass $user The user to return information about. + * @param \stdClass $course The course the user is in. + * @param \stdClass $cm Course module information. + * @return \stdClass Activity completion information. + */ + public static function get_activity_completion_info(\stdClass $user, \stdClass $course, $cm) : \stdClass { + $completioninfo = new \completion_info($course); + $completion = $completioninfo->is_enabled($cm); + return ($completion != COMPLETION_TRACKING_NONE) ? $completioninfo->get_data($cm, true, $user->id) : new \stdClass(); + } + + /** + * Returns course completion information for a user. + * + * @param \stdClass $user The user that we are getting completion information for. + * @param \stdClass $course The course we are interested in. + * @return \stdClass Course completion information. + */ + public static function get_course_completion_info(\stdClass $user, \stdClass $course) : array { + $completioninfo = new \completion_info($course); + $completion = $completioninfo->is_enabled(); + + if ($completion == COMPLETION_ENABLED) { + + $coursecomplete = $completioninfo->is_course_complete($user->id); + $criteriacomplete = $completioninfo->count_course_user_data($user->id); + $ccompletion = new \completion_completion(['userid' => $user->id, 'course' => $course->id]); + + $status = ($coursecomplete) ? get_string('complete') : ''; + $status = (!$criteriacomplete && !$ccompletion->timestarted) ? get_string('notyetstarted', 'completion') : + get_string('inprogress', 'completion'); + + $completions = $completioninfo->get_completions($user->id); + $overall = get_string('nocriteriaset', 'completion'); + if (!empty($completions)) { + if ($completioninfo->get_aggregation_method() == COMPLETION_AGGREGATION_ALL) { + $overall = get_string('criteriarequiredall', 'completion'); + } else { + $overall = get_string('criteriarequiredany', 'completion'); + } + } + + $coursecompletiondata = [ + 'status' => $status, + 'required' => $overall, + ]; + + $coursecompletiondata['criteria'] = array_map(function($completion) use ($completioninfo) { + $criteria = $completion->get_criteria(); + $aggregation = $completioninfo->get_aggregation_method($criteria->criteriatype); + $required = ($aggregation == COMPLETION_AGGREGATION_ALL) ? get_string('all', 'completion') : + get_string('any', 'completion'); + $data = [ + 'required' => $required, + 'completed' => transform::yesno($completion->is_complete()), + 'timecompleted' => isset($completion->timecompleted) ? transform::datetime($completion->timecompleted) : '' + ]; + $details = $criteria->get_details($completion); + $data = array_merge($data, $details); + return $data; + }, $completions); + return $coursecompletiondata; + } + } + + /** + * Delete completion information for users. + * + * @param \stdClass $user The user. If provided will delete completion information for just this user. Else all users. + * @param int $courseid The course id. Provide this if you want course completion and activity completion deleted. + * @param int $cmid The course module id. Provide this if you only want activity completion deleted. + */ + public static function delete_completion(\stdClass $user = null, int $courseid = null, int $cmid = null) { + global $DB; + + if (isset($cmid)) { + $params = (isset($user)) ? ['userid' => $user->id, 'coursemoduleid' => $cmid] : ['coursemoduleid' => $cmid]; + // Only delete the record for course modules completion. + $DB->delete_records('course_modules_completion', $params); + return; + } + + if (isset($courseid)) { + + $usersql = isset($user) ? 'AND cmc.userid = :userid' : ''; + $params = isset($user) ? ['course' => $courseid, 'userid' => $user->id] : ['course' => $courseid]; + + // Find records relating to course modules. + $sql = "SELECT cmc.id + FROM {course_completion_criteria} ccc + JOIN {course_modules_completion} cmc ON ccc.moduleinstance = cmc.coursemoduleid + WHERE ccc.course = :course $usersql"; + $recordids = $DB->get_records_sql($sql, $params); + $ids = array_keys($recordids); + if (!empty($ids)) { + list($deletesql, $deleteparams) = $DB->get_in_or_equal($ids); + $deletesql = 'id ' . $deletesql; + $DB->delete_records_select('course_modules_completion', $deletesql, $deleteparams); + } + $DB->delete_records('course_completion_crit_compl', $params); + $DB->delete_records('course_completions', $params); + } + } +} diff --git a/completion/tests/fixtures/completion_creation.php b/completion/tests/fixtures/completion_creation.php new file mode 100644 index 0000000000000..b064d5a451500 --- /dev/null +++ b/completion/tests/fixtures/completion_creation.php @@ -0,0 +1,118 @@ +. + +/** + * Trait for course completion creation in unit tests + * + * @package core_completion + * @category test + * @copyright 2018 Adrian Greeve + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; + +require_once($CFG->dirroot . '/completion/criteria/completion_criteria.php'); +require_once($CFG->dirroot . '/completion/criteria/completion_criteria_activity.php'); +require_once($CFG->dirroot . '/completion/criteria/completion_criteria_role.php'); + +/** + * Trait for unit tests and completion. + * + * @copyright 2018 Adrian Greeve + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +trait completion_creation { + + /** @var stdClass The course object. */ + public $course; + + /** @var context The course context object. */ + public $coursecontext; + + /** @var stdClass The course module object */ + public $cm; + + /** + * Create completion information. + */ + public function create_course_completion() { + $this->resetAfterTest(); + + $course = $this->getDataGenerator()->create_course(['enablecompletion' => 1]); + $coursecontext = context_course::instance($course->id); + + $assign = $this->getDataGenerator()->create_module('assign', ['course' => $course->id, 'completion' => 1]); + $modulecontext = context_module::instance($assign->cmid); + $cm = get_coursemodule_from_id('assign', $assign->cmid); + + // Set completion rules. + $completion = new \completion_info($course); + + $criteriadata = (object) [ + 'id' => $course->id, + 'criteria_activity' => [ + $cm->id => 1 + ] + ]; + $criterion = new \completion_criteria_activity(); + $criterion->update_config($criteriadata); + + $criteriadata = (object) [ + 'id' => $course->id, + 'criteria_role' => [3 => 3] + ]; + $criterion = new \completion_criteria_role(); + $criterion->update_config($criteriadata); + + // Handle overall aggregation. + $aggdata = array( + 'course' => $course->id, + 'criteriatype' => COMPLETION_CRITERIA_TYPE_ACTIVITY + ); + $aggregation = new \completion_aggregation($aggdata); + $aggregation->setMethod(COMPLETION_AGGREGATION_ALL); + $aggregation->save(); + $aggdata['criteriatype'] = COMPLETION_CRITERIA_TYPE_ROLE; + $aggregation = new \completion_aggregation($aggdata); + $aggregation->setMethod(COMPLETION_AGGREGATION_ANY); + $aggregation->save(); + + // Set variables for access in tests. + $this->course = $course; + $this->coursecontext = $coursecontext; + $this->cm = $cm; + } + + /** + * Complete some of the course completion criteria. + * + * @param stdClass $user The user object + */ + public function complete_course($user) { + $this->getDataGenerator()->enrol_user($user->id, $this->course->id, 'student'); + $completion = new \completion_info($this->course); + $criteriacompletions = $completion->get_completions($user->id, COMPLETION_CRITERIA_TYPE_ROLE); + $criteria = completion_criteria::factory(['id' => 3, 'criteriatype' => COMPLETION_CRITERIA_TYPE_ROLE]); + foreach ($criteriacompletions as $ccompletion) { + $criteria->complete($ccompletion); + } + // Set activity as complete. + $completion->update_state($this->cm, COMPLETION_COMPLETE, $user->id); + } +} diff --git a/completion/tests/privacy_test.php b/completion/tests/privacy_test.php new file mode 100644 index 0000000000000..7e1939ad7b4b2 --- /dev/null +++ b/completion/tests/privacy_test.php @@ -0,0 +1,118 @@ +. + +/** + * Unit Tests for the request helper. + * + * @package core_completion + * @category test + * @copyright 2018 Adrian Greeve + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; + +require_once($CFG->dirroot . '/completion/tests/fixtures/completion_creation.php'); + +/** + * Tests for the \core_completion API's provider functionality. + * + * @copyright 2018 Adrian Greeve + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class core_completion_privacy_test extends \core_privacy\tests\provider_testcase { + + use completion_creation; + + /** + * Test joining course completion data to an sql statement. + */ + public function test_get_course_completion_join_sql() { + global $DB; + $this->resetAfterTest(); + $user = $this->getDataGenerator()->create_user(); + $this->create_course_completion(); + $this->complete_course($user); + + list($join, $where, $params) = \core_completion\privacy\provider::get_course_completion_join_sql($user->id, 'comp', 'c.id'); + $sql = "SELECT DISTINCT c.id + FROM {course} c + {$join} + WHERE {$where}"; + $records = $DB->get_records_sql($sql, $params); + $data = array_shift($records); + $this->assertEquals($this->course->id, $data->id); + } + + /** + * Test getting course completion information. + */ + public function test_get_course_completion_info() { + $this->resetAfterTest(); + $user = $this->getDataGenerator()->create_user(); + $this->create_course_completion(); + $this->complete_course($user); + $coursecompletion = \core_completion\privacy\provider::get_course_completion_info($user, $this->course); + $this->assertEquals('In progress', $coursecompletion['status']); + $this->assertCount(2, $coursecompletion['criteria']); + } + + /** + * Test getting activity completion information. + */ + public function test_get_activity_completion_info() { + $this->resetAfterTest(); + $user = $this->getDataGenerator()->create_user(); + $this->create_course_completion(); + $this->complete_course($user); + $activitycompletion = \core_completion\privacy\provider::get_activity_completion_info($user, $this->course, + $this->cm); + $this->assertEquals($user->id, $activitycompletion->userid); + $this->assertEquals($this->cm->id, $activitycompletion->coursemoduleid); + $this->assertEquals(1, $activitycompletion->completionstate); + } + + /** + * Test deleting activity completion information for a user. + */ + public function test_delete_completion_activity_user() { + $this->resetAfterTest(); + $user = $this->getDataGenerator()->create_user(); + $this->create_course_completion(); + $this->complete_course($user); + \core_completion\privacy\provider::delete_completion($user, null, $this->cm->id); + $activitycompletion = \core_completion\privacy\provider::get_activity_completion_info($user, $this->course, + $this->cm); + $this->assertEquals(0, $activitycompletion->completionstate); + } + + /** + * Test deleting course completion information. + */ + public function test_delete_completion_course() { + $this->resetAfterTest(); + $user = $this->getDataGenerator()->create_user(); + $this->create_course_completion(); + $this->complete_course($user); + \core_completion\privacy\provider::delete_completion(null, $this->course->id); + $coursecompletion = \core_completion\privacy\provider::get_course_completion_info($user, $this->course); + foreach ($coursecompletion['criteria'] as $criterion) { + $this->assertEquals('No', $criterion['completed']); + } + } +} diff --git a/lang/en/completion.php b/lang/en/completion.php index 5d303c636748a..0bbf46da70fc2 100644 --- a/lang/en/completion.php +++ b/lang/en/completion.php @@ -181,6 +181,22 @@ $string['overallaggregation_any'] = 'Course is complete when ANY of the conditions are met'; $string['pending'] = 'Pending'; $string['periodpostenrolment'] = 'Period post enrolment'; +$string['privacy:metadata:completionstate'] = 'If the course module has been completed.'; +$string['privacy:metadata:course'] = 'A course identifier.'; +$string['privacy:metadata:coursecompletedsummary'] = 'Holds information about students that have completed criteria in a course.'; +$string['privacy:metadata:coursemoduleid'] = 'The identifier to the course module.'; +$string['privacy:metadata:coursemodulesummary'] = 'Stores the course module completion data for a user.'; +$string['privacy:metadata:coursesummary'] = 'Stores the course completion data for a user.'; +$string['privacy:metadata:gradefinal'] = 'Final grade recieved for the course completion.'; +$string['privacy:metadata:overrideby'] = 'The user ID of the person who overrode the module completion.'; +$string['privacy:metadata:reaggregate'] = 'If the course completion was reaggregated.'; +$string['privacy:metadata:timecompleted'] = 'The time that the course was completed.'; +$string['privacy:metadata:timeenrolled'] = 'The time that the user was enrolled into the course.'; +$string['privacy:metadata:timemodified'] = 'The time the course module completion was modified.'; +$string['privacy:metadata:timestarted'] = 'The time the course was started.'; +$string['privacy:metadata:viewed'] = 'If the course module was viewed.'; +$string['privacy:metadata:userid'] = 'Course and activity completion is stored against this user.'; +$string['privacy:metadata:unenroled'] = 'If the user has been unenroled from the course.'; $string['progress'] = 'Student progress'; $string['progress-title'] = '{$a->user}, {$a->activity}: {$a->state} {$a->date}'; $string['progresstotal'] = 'Progress: {$a->complete} / {$a->total}';