MDL-58266 core_completion: Add new view table.

This commit is contained in:
hieuvu 2022-04-26 16:46:26 +07:00 committed by Thong Bui
parent e4c5a12a1c
commit c6e018e04e
15 changed files with 412 additions and 30 deletions

View File

@ -1284,7 +1284,6 @@ class backup_userscompletion_structure_step extends backup_structure_step {
protected function define_structure() {
// Define each element separated
$completions = new backup_nested_element('completions');
$completion = new backup_nested_element('completion', array('id'), array(
@ -1302,8 +1301,22 @@ class backup_userscompletion_structure_step extends backup_structure_step {
$completion->annotate_ids('user', 'userid');
// Return the root element (completions)
$completionviews = new backup_nested_element('completionviews');
$completionview = new backup_nested_element('completionview', ['id'], ['userid', 'timecreated']);
// Build the tree.
$completionviews->add_child($completionview);
// Define sources.
$completionview->set_source_table('course_modules_viewed', ['coursemoduleid' => backup::VAR_MODID]);
// Define id annotations.
$completionview->annotate_ids('user', 'userid');
$completions->add_child($completionviews);
// Return the root element (completions).
return $completions;
}
}

View File

@ -4680,8 +4680,12 @@ class restore_userscompletion_structure_step extends restore_structure_step {
$paths = array();
// Restore completion.
$paths[] = new restore_path_element('completion', '/completions/completion');
// Restore completion view.
$paths[] = new restore_path_element('completionview', '/completions/completionviews/completionview');
return $paths;
}
@ -4710,6 +4714,29 @@ class restore_userscompletion_structure_step extends restore_structure_step {
// Normal entry where it doesn't exist already
$DB->insert_record('course_modules_completion', $data);
}
// Add viewed to course_modules_viewed.
if (isset($data->viewed) && $data->viewed) {
$dataview = clone($data);
unset($dataview->id);
unset($dataview->viewed);
$dataview->timecreated = $data->timemodified;
$DB->insert_record('course_modules_viewed', $dataview);
}
}
/**
* Process the completioinview data.
* @param array $data The data from the XML file.
*/
protected function process_completionview(array $data) {
global $DB;
$data = (object)$data;
$data->coursemoduleid = $this->task->get_moduleid();
$data->userid = $this->get_mappingid('user', $data->userid);
$DB->insert_record('course_modules_viewed', $data);
}
}

View File

@ -314,6 +314,41 @@ class restore_stepslib_date_test extends \restore_date_testcase {
$this->assertEquals($coursemodulecompletion->timemodified, $newcoursemodulecompletion->timemodified);
}
/**
* Checking that the user completion of an activity relating to the view field does not change
* when doing a course restore.
* @covers ::backup_and_restore
*/
public function test_usercompletion_view_restore() {
global $DB;
// More completion...
$course = $this->getDataGenerator()->create_course(['startdate' => time(), 'enablecompletion' => 1]);
$student = $this->getDataGenerator()->create_user();
$this->getDataGenerator()->enrol_user($student->id, $course->id, 'student');
$assign = $this->getDataGenerator()->create_module('assign', [
'course' => $course->id,
'completion' => COMPLETION_TRACKING_AUTOMATIC, // Show activity as complete when conditions are met.
'completionview' => 1
]);
$cm = $DB->get_record('course_modules', ['course' => $course->id, 'instance' => $assign->id]);
// Mark the activity as completed.
$completion = new \completion_info($course);
$completion->set_module_viewed($cm, $student->id);
$coursemodulecompletion = $DB->get_record('course_modules_viewed', ['coursemoduleid' => $cm->id]);
// Back up and restore.
$newcourseid = $this->backup_and_restore($course);
$newcourse = get_course($newcourseid);
$assignid = $DB->get_field('assign', 'id', ['course' => $newcourseid]);
$cm = $DB->get_record('course_modules', ['course' => $newcourse->id, 'instance' => $assignid]);
$newcoursemodulecompletion = $DB->get_record('course_modules_viewed', ['coursemoduleid' => $cm->id]);
$this->assertEquals($coursemodulecompletion->timecreated, $newcoursemodulecompletion->timecreated);
}
/**
* Ensuring that the timemodified field of the question attempt steps table does not change when
* a course restore is done.

View File

@ -66,10 +66,14 @@ class provider implements
'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_modules_viewed', [
'userid' => 'privacy:metadata:userid',
'coursemoduleid' => 'privacy:metadata:coursemoduleid',
'timecreated' => 'privacy:metadata:timecreated',
], 'privacy:metadata:coursemodulesummary');
$collection->add_database_table('course_completion_crit_compl', [
'userid' => 'privacy:metadata:userid',
'course' => 'privacy:metadata:course',
@ -91,16 +95,18 @@ class provider implements
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.
$cmvalias = "{$prefix}_cmv"; // Course modules viewed.
$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
AND {$cmcalias}.userid = :{$prefix}_moduleuserid
LEFT JOIN {course_modules_viewed} {$cmvalias} ON {$cccalias}.moduleinstance = {$cmvalias}.coursemoduleid
AND {$cmvalias}.userid = :{$prefix}_moduleuserid2
LEFT JOIN {course_completion_crit_compl} {$ccccalias} ON {$ccccalias}.criteriaid = {$cccalias}.id
AND {$ccccalias}.userid = :{$prefix}_courseuserid";
$where = "{$cmcalias}.id IS NOT NULL OR {$ccccalias}.id IS NOT NULL";
$params = ["{$prefix}_moduleuserid" => $userid, "{$prefix}_courseuserid" => $userid];
$where = "{$cmcalias}.id IS NOT NULL OR {$ccccalias}.id IS NOT NULL OR {$cmvalias}.id IS NOT NULL";
$params = ["{$prefix}_moduleuserid" => $userid, "{$prefix}_moduleuserid2" => $userid, "{$prefix}_courseuserid" => $userid];
return [$join, $where, $params];
}
@ -126,6 +132,14 @@ class provider implements
$userlist->add_from_sql('userid', $sql, $params);
$sql = "SELECT cmv.userid
FROM {course} c
JOIN {course_completion_criteria} ccc ON ccc.course = c.id
JOIN {course_modules_viewed} cmv ON cmv.coursemoduleid = ccc.moduleinstance
WHERE c.id = :courseid";
$userlist->add_from_sql('userid', $sql, $params);
$sql = "SELECT ccc_compl.userid
FROM {course} c
JOIN {course_completion_criteria} ccc ON ccc.course = c.id
@ -231,6 +245,7 @@ class provider implements
if (isset($courseid)) {
$usersql = isset($user) ? 'AND cmc.userid = :userid' : '';
$usercmvsql = isset($user) ? 'AND cmv.userid = :userid' : '';
$params = isset($user) ? ['course' => $courseid, 'userid' => $user->id] : ['course' => $courseid];
// Find records relating to course modules.
@ -245,6 +260,19 @@ class provider implements
$deletesql = 'id ' . $deletesql;
$DB->delete_records_select('course_modules_completion', $deletesql, $deleteparams);
}
// Find records relating to course modules completion viewed.
$sql = "SELECT cmv.id
FROM {course_completion_criteria} ccc
JOIN {course_modules_viewed} cmv ON ccc.moduleinstance = cmv.coursemoduleid
WHERE ccc.course = :course $usercmvsql";
$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_viewed', $deletesql, $deleteparams);
}
$DB->delete_records('course_completion_crit_compl', $params);
$DB->delete_records('course_completions', $params);
}
@ -273,6 +301,7 @@ class provider implements
// Only delete the record for course modules completion.
$sql = "coursemoduleid = :coursemoduleid AND userid {$useridsql}";
$DB->delete_records_select('course_modules_completion', $sql, $params);
$DB->delete_records_select('course_modules_viewed', $sql, $params);
return;
}
@ -292,6 +321,19 @@ class provider implements
$DB->delete_records_select('course_modules_completion', $deletesql, $deleteparams);
}
// Find records relating to course modules.
$sql = "SELECT cmv.id
FROM {course_completion_criteria} ccc
JOIN {course_modules_viewed} cmv ON ccc.moduleinstance = cmv.coursemoduleid
WHERE ccc.course = :course AND cmv.userid {$useridsql}";
$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_viewed', $deletesql, $deleteparams);
}
$sql = "course = :course AND userid {$useridsql}";
$DB->delete_records_select('course_completion_crit_compl', $sql, $params);
$DB->delete_records_select('course_completions', $sql, $params);

View File

@ -0,0 +1,57 @@
@core @core_completion
Feature: Backup and restore the activity with the completion
Background:
Given the following "courses" exist:
| fullname | shortname | category | enablecompletion |
| Course 1 | C1 | 0 | 1 |
And the following "users" exist:
| username | firstname | lastname | email |
| student1 | Student | First | student1@example.com |
| student2 | Student | Second | student2@example.com |
And the following "course enrolments" exist:
| user | course | role |
| student1 | C1 | student |
| student2 | C1 | student |
And the following "activity" exists:
| activity | assign |
| course | C1 |
| idnumber | a1 |
| name | Test assignment name |
| intro | Submit your online text |
| assignsubmission_onlinetext_enabled | 1 |
| assignsubmission_file_enabled | 0 |
| completion | 2 |
| completionview | 1 |
| completionusegrade | 1 |
| gradepass | 50 |
| completionpassgrade | 1 |
And I am on the "Test assignment name" "assign activity" page logged in as student1
And I log out
@javascript @_file_upload
Scenario: Restore the legacy assignment with completion condition.
Given I am on the "Course 1" "restore" page logged in as "admin"
And I press "Manage backup files"
And I upload "completion/tests/fixtures/legacy_course_completion.mbz" file to "Files" filemanager
And I press "Save changes"
And I restore "legacy_course_completion.mbz" backup into a new course using this options:
| Schema | Course name | Course 2 |
| Schema | Course short name | C2 |
When I am on the "Course 2" course page logged in as student1
Then the "View" completion condition of "Test assignment name" is displayed as "done"
And I am on the "Course 2" course page logged in as student2
And the "View" completion condition of "Test assignment name" is displayed as "todo"
@javascript @_file_upload
Scenario: Backup and restore the assignment with the viewed and not-viewed completion condition
Given I am on the "Course 1" course page logged in as admin
And I backup "Course 1" course using this options:
| Confirmation | Filename | test_backup.mbz |
And I restore "test_backup.mbz" backup into a new course using this options:
| Schema | Course name | Course 2 |
| Schema | Course short name | C2 |
When I am on the "Course 2" course page logged in as student1
Then the "View" completion condition of "Test assignment name" is displayed as "done"
And I am on the "Course 2" course page logged in as student2
And the "View" completion condition of "Test assignment name" is displayed as "todo"

View File

@ -67,3 +67,25 @@ Feature: Students will be marked as completed and pass/fail
And the "View" completion condition of "Test assignment name" is displayed as "todo"
And the "Receive a grade" completion condition of "Test assignment name" is displayed as "done"
And the "Receive a passing grade" completion condition of "Test assignment name" is displayed as "failed"
@javascript
Scenario: Keep current view completion condition when the teacher does the action 'Unlock completion settings'.
Given I am on the "Course 1" course page logged in as teacher1
And I navigate to "View > Grader report" in the course gradebook
And I turn editing mode on
And I give the grade "21" to the user "Student First" for the grade item "Test assignment name"
And I give the grade "50" to the user "Student Second" for the grade item "Test assignment name"
And I press "Save changes"
And I am on the "Test assignment name" "assign activity" page logged in as teacher1
And I navigate to "Settings" in current page administration
And I expand all fieldsets
And I press "Unlock completion settings"
And I expand all fieldsets
And I should see "Completion options unlocked"
And I click on "Save and display" "button"
And I log out
When I am on the "Course 1" course page logged in as student1
Then the "View" completion condition of "Test assignment name" is displayed as "done"
And I log out
When I am on the "Course 1" course page logged in as student2
Then the "View" completion condition of "Test assignment name" is displayed as "done"

Binary file not shown.

View File

@ -933,6 +933,7 @@ function course_delete_module($cmid, $async = false) {
// features are not turned on, in case they were turned on previously (these will be
// very quick on an empty table).
$DB->delete_records('course_modules_completion', array('coursemoduleid' => $cm->id));
$DB->delete_records('course_modules_viewed', ['coursemoduleid' => $cm->id]);
$DB->delete_records('course_completion_criteria', array('moduleinstance' => $cm->id,
'course' => $cm->course,
'criteriatype' => COMPLETION_CRITERIA_TYPE_ACTIVITY));

View File

@ -208,6 +208,7 @@ $string['privacy:metadata:timecompleted'] = 'The time that the course was comple
$string['privacy:metadata:timeenrolled'] = 'The time that the user was enrolled in the course';
$string['privacy:metadata:timemodified'] = 'The time that the activity completion was modified';
$string['privacy:metadata:timestarted'] = 'The time the course was started.';
$string['privacy:metadata:timecreated'] = 'The time that the activity completion was created';
$string['privacy:metadata:viewed'] = 'If the activity was viewed';
$string['privacy:metadata:userid'] = 'The user ID of the person with course and activity completion data';
$string['privacy:metadata:unenroled'] = 'If the user has been unenrolled from the course';

View File

@ -1091,13 +1091,16 @@ class completion_info {
// If we're not caching the completion data, then just fetch the completion data for the user in this course module.
if ($usecache && $wholecourse) {
// Get whole course data for cache.
$alldatabycmc = $DB->get_records_sql("SELECT cm.id AS cmid, cmc.*
$alldatabycmc = $DB->get_records_sql("SELECT cm.id AS cmid, cmc.*,
CASE WHEN cmv.id IS NULL THEN 0 ELSE 1 END AS viewed
FROM {course_modules} cm
LEFT JOIN {course_modules_completion} cmc ON cmc.coursemoduleid = cm.id
AND cmc.userid = ?
LEFT JOIN {course_modules_completion} cmc
ON cmc.coursemoduleid = cm.id AND cmc.userid = ?
LEFT JOIN {course_modules_viewed} cmv
ON cmv.coursemoduleid = cm.id AND cmv.userid = ?
INNER JOIN {modules} m ON m.id = cm.module
WHERE m.visible = 1 AND cm.course = ?", [$userid, $this->course->id]);
WHERE m.visible = 1 AND cm.course = ?",
[$userid, $userid, $this->course->id]);
$cminfos = get_fast_modinfo($cm->course, $userid)->get_cms();
// Reindex by course module id.
@ -1125,14 +1128,7 @@ class completion_info {
$data = $cacheddata[$cminfo->id];
} else {
// Get single record
$data = $DB->get_record('course_modules_completion', array('coursemoduleid' => $cminfo->id, 'userid' => $userid));
if ($data) {
$data = (array)$data;
} else {
// Row not present counts as 'not complete'.
$data = $defaultdata;
}
$data = $this->get_completion_data($cminfo->id, $userid, $defaultdata);
// Put in cache.
$cacheddata[$cminfo->id] = $data;
}
@ -1188,10 +1184,8 @@ class completion_info {
// If view is required, try and fetch from the db. In some cases, cache can be invalid.
if ($cm->completionview == COMPLETION_VIEW_REQUIRED) {
$data['viewed'] = COMPLETION_INCOMPLETE;
$record = $DB->get_record('course_modules_completion', array('coursemoduleid' => $cm->id, 'userid' => $userid));
if ($record) {
$data['viewed'] = ($record->viewed == COMPLETION_VIEWED ? COMPLETION_COMPLETE : COMPLETION_INCOMPLETE);
}
$record = $DB->record_exists('course_modules_viewed', ['coursemoduleid' => $cm->id, 'userid' => $userid]);
$data['viewed'] = $record ? COMPLETION_COMPLETE : COMPLETION_INCOMPLETE;
}
return $data;
@ -1265,6 +1259,19 @@ class completion_info {
// Has real (nonzero) id meaning that a database row exists, update
$DB->update_record('course_modules_completion', $data);
}
$dataview = new stdClass();
$dataview->coursemoduleid = $data->coursemoduleid;
$dataview->userid = $data->userid;
$dataview->id = $DB->get_field('course_modules_viewed', 'id',
['coursemoduleid' => $dataview->coursemoduleid, 'userid' => $dataview->userid]);
if (!$data->viewed && $dataview->id) {
$DB->delete_records('course_modules_viewed', ['id' => $dataview->id]);
}
if (!$dataview->id && $data->viewed) {
$dataview->timecreated = time();
$dataview->id = $DB->insert_record('course_modules_viewed', $dataview);
}
$transaction->allow_commit();
$cmcontext = context_module::instance($data->coursemoduleid);
@ -1602,6 +1609,49 @@ class completion_info {
throw new moodle_exception('err_system','completion',
$CFG->wwwroot.'/course/view.php?id='.$this->course->id,null,$error);
}
/**
* Get completion data include viewed field.
*
* @param int $coursemoduleid The course module id.
* @param int $userid The User ID.
* @param array $defaultdata Default data completion.
* @return array Data completion retrieved.
*/
public function get_completion_data(int $coursemoduleid, int $userid, array $defaultdata): array {
global $DB;
// MySQL doesn't support FULL JOIN syntax, so we use UNION in the below SQL to help MySQL.
$sql = "SELECT cmc.*, cmv.coursemoduleid as cmvcoursemoduleid, cmv.userid as cmvuserid
FROM {course_modules_completion} cmc
LEFT JOIN {course_modules_viewed} cmv ON cmc.coursemoduleid = cmv.coursemoduleid AND cmc.userid = cmv.userid
WHERE cmc.coursemoduleid = :cmccoursemoduleid AND cmc.userid = :cmcuserid
UNION
SELECT cmc2.*, cmv2.coursemoduleid as cmvcoursemoduleid, cmv2.userid as cmvuserid
FROM {course_modules_completion} cmc2
RIGHT JOIN {course_modules_viewed} cmv2
ON cmc2.coursemoduleid = cmv2.coursemoduleid AND cmc2.userid = cmv2.userid
WHERE cmv2.coursemoduleid = :cmvcoursemoduleid AND cmv2.userid = :cmvuserid";
$data = $DB->get_record_sql($sql, ['cmccoursemoduleid' => $coursemoduleid, 'cmcuserid' => $userid,
'cmvcoursemoduleid' => $coursemoduleid, 'cmvuserid' => $userid]);
if (!$data) {
$data = $defaultdata;
} else {
if (empty($data->coursemoduleid) && empty($data->userid)) {
$data->coursemoduleid = $data->cmvcoursemoduleid;
$data->userid = $data->cmvuserid;
}
unset($data->cmvcoursemoduleid);
unset($data->cmvuserid);
// When reseting all state in the completion, we need to keep current view state.
$data->viewed = 1;
}
return (array)$data;
}
}
/**

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" ?>
<XMLDB PATH="lib/db" VERSION="20220825" COMMENT="XMLDB file for core Moodle tables"
<XMLDB PATH="lib/db" VERSION="20221013" COMMENT="XMLDB file for core Moodle tables"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="../../lib/xmldb/xmldb.xsd"
>
@ -162,7 +162,7 @@
<FIELD NAME="course" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
<FIELD NAME="criteriatype" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Type of criteria"/>
<FIELD NAME="module" TYPE="char" LENGTH="100" NOTNULL="false" SEQUENCE="false" COMMENT="Type of module (if using module criteria type)"/>
<FIELD NAME="moduleinstance" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false" COMMENT="Module instance id (if using module criteria type)"/>
<FIELD NAME="moduleinstance" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false" COMMENT="Course module id (if using module criteria type)"/>
<FIELD NAME="courseinstance" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false" COMMENT="Course instance id (if using course criteria type)"/>
<FIELD NAME="enrolperiod" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false" COMMENT="Number of days after enrolment the course is completed (if using enrolperiod criteria type)"/>
<FIELD NAME="timeend" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false" COMMENT="Timestamp of the date for course completion (if using date criteria type)"/>
@ -331,7 +331,6 @@
<FIELD NAME="coursemoduleid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="Activity that has been completed (or not)."/>
<FIELD NAME="userid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="ID of user who has (or hasn't) completed the activity."/>
<FIELD NAME="completionstate" TYPE="int" LENGTH="1" NOTNULL="true" SEQUENCE="false" COMMENT="Whether or not the user has completed the activity. Available states: 0 = not completed [if there's no row in this table, that also counts as 0] 1 = completed 2 = completed, show passed 3 = completed, show failed"/>
<FIELD NAME="viewed" TYPE="int" LENGTH="1" NOTNULL="false" SEQUENCE="false" COMMENT="Tracks whether or not this activity has been viewed. NULL = we are not tracking viewed for this activity 0 = not viewed 1 = viewed"/>
<FIELD NAME="overrideby" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false" COMMENT="Tracks whether this completion state has been set manually to override a previous state."/>
<FIELD NAME="timemodified" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="Time at which the completion state last changed."/>
</FIELDS>
@ -4745,5 +4744,20 @@
<INDEX NAME="adminpresetapplyid" UNIQUE="false" FIELDS="adminpresetapplyid"/>
</INDEXES>
</TABLE>
<TABLE NAME="course_modules_viewed" COMMENT="Tracks the completion viewed (viewed with cmid/userid and otherwise no row) of each user on each activity.">
<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
<FIELD NAME="coursemoduleid" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Activity that has been viewed (or not)."/>
<FIELD NAME="userid" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="ID of user who has (or hasn't) viewed the activity."/>
<FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Time at which the completion viewed created."/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
</KEYS>
<INDEXES>
<INDEX NAME="coursemoduleid" UNIQUE="false" FIELDS="coursemoduleid" COMMENT="For quick access via course-module (e.g. when displaying course module settings page and we need to determine whether anyone has completed it)."/>
<INDEX NAME="userid-coursemoduleid" UNIQUE="true" FIELDS="userid, coursemoduleid"/>
</INDEXES>
</TABLE>
</TABLES>
</XMLDB>

View File

@ -2925,5 +2925,60 @@ privatefiles,moodle|/user/files.php';
upgrade_main_savepoint(true, 2022092200.01);
}
if ($oldversion < 2022101300.00) {
// Define table to store completion viewed.
$table = new xmldb_table('course_modules_viewed');
// Adding fields to table course_modules_viewed.
$table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
$table->add_field('coursemoduleid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null, 'id');
$table->add_field('userid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null, 'coursemoduleid');
$table->add_field('timecreated', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0', 'userid');
// Adding keys to table course_modules_viewed.
$table->add_key('primary', XMLDB_KEY_PRIMARY, ['id']);
// Adding indexes to table course_modules_viewed.
$table->add_index('coursemoduleid', XMLDB_INDEX_NOTUNIQUE, ['coursemoduleid']);
$table->add_index('userid-coursemoduleid', XMLDB_INDEX_UNIQUE, ['userid', 'coursemoduleid']);
if (!$dbman->table_exists($table)) {
$dbman->create_table($table);
}
// Main savepoint reached.
upgrade_main_savepoint(true, 2022101300.00);
}
if ($oldversion < 2022101300.01) {
// Add legacy data to the new table.
$transaction = $DB->start_delegated_transaction();
upgrade_set_timeout(3600);
$sql = "INSERT INTO {course_modules_viewed}
(userid, coursemoduleid, timecreated)
SELECT userid, coursemoduleid, timemodified
FROM {course_modules_completion}
WHERE viewed = 1";
$DB->execute($sql);
$transaction->allow_commit();
// Main savepoint reached.
upgrade_main_savepoint(true, 2022101300.01);
}
if ($oldversion < 2022101300.02) {
// Define field viewed to be dropped from course_modules_completion.
$table = new xmldb_table('course_modules_completion');
$field = new xmldb_field('viewed');
// Conditionally launch drop field viewed.
if ($dbman->field_exists($table, $field)) {
$dbman->drop_field($table, $field);
}
// Main savepoint reached.
upgrade_main_savepoint(true, 2022101300.02);
}
return true;
}

View File

@ -5208,6 +5208,7 @@ function remove_course_contents($courseid, $showfeedback = true, array $options
// Delete cm and its context - orphaned contexts are purged in cron in case of any race condition.
context_helper::delete_instance(CONTEXT_MODULE, $cm->id);
$DB->delete_records('course_modules_completion', ['coursemoduleid' => $cm->id]);
$DB->delete_records('course_modules_viewed', ['coursemoduleid' => $cm->id]);
$DB->delete_records('course_modules', array('id' => $cm->id));
rebuild_course_cache($cm->course, true);
}
@ -5231,6 +5232,8 @@ function remove_course_contents($courseid, $showfeedback = true, array $options
// features are not enabled now, in case they were enabled previously.
$DB->delete_records_subquery('course_modules_completion', 'coursemoduleid', 'id',
'SELECT id from {course_modules} WHERE course = ?', [$courseid]);
$DB->delete_records_subquery('course_modules_viewed', 'coursemoduleid', 'id',
'SELECT id from {course_modules} WHERE course = ?', [$courseid]);
// Remove course-module data that has not been removed in modules' _delete_instance callbacks.
$cms = $DB->get_records('course_modules', array('course' => $course->id));

View File

@ -767,11 +767,16 @@ class completionlib_test extends advanced_testcase {
'coursemoduleid' => $cm->id,
'userid' => $user->id,
'completionstate' => $completion,
'viewed' => 0,
'overrideby' => null,
'timemodified' => 0,
];
$cmcompletionviewrecord = (object)[
'coursemoduleid' => $cm->id,
'userid' => $user->id,
'timecreated' => 0,
];
$DB->insert_record('course_modules_completion', $cmcompletionrecord);
$DB->insert_record('course_modules_viewed', $cmcompletionviewrecord);
}
// Whether we expect for the returned completion data to be stored in the cache.
@ -832,11 +837,16 @@ class completionlib_test extends advanced_testcase {
'coursemoduleid' => $cm->id,
'userid' => $this->user->id,
'completionstate' => COMPLETION_NOT_VIEWED,
'viewed' => 0,
'overrideby' => null,
'timemodified' => 0,
];
$cmcompletionviewrecord = (object)[
'coursemoduleid' => $cm->id,
'userid' => $this->user->id,
'timecreated' => 0,
];
$DB->insert_record('course_modules_completion', $cmcompletionrecord);
$DB->insert_record('course_modules_viewed', $cmcompletionviewrecord);
// Mock other completion data.
$completioninfo = new completion_info($this->course);
@ -856,7 +866,6 @@ class completionlib_test extends advanced_testcase {
$this->assertEquals($testcm->id, $result->coursemoduleid);
$this->assertEquals($this->user->id, $result->userid);
$this->assertEquals(0, $result->viewed);
$results[$testcm->id] = $result;
}
@ -872,6 +881,59 @@ class completionlib_test extends advanced_testcase {
}
}
/**
* Tests for get_completion_data().
*
* @covers ::get_completion_data
*/
public function test_get_completion_data() {
$this->setup_data();
$choicegenerator = $this->getDataGenerator()->get_plugin_generator('mod_choice');
$choice = $choicegenerator->create_instance([
'course' => $this->course->id,
'completion' => COMPLETION_TRACKING_AUTOMATIC,
'completionview' => true,
'completionsubmit' => true,
]);
$cm = get_coursemodule_from_instance('choice', $choice->id);
// Mock other completion data.
$completioninfo = new completion_info($this->course);
// Default data to return when no completion data is found.
$defaultdata = [
'id' => 0,
'coursemoduleid' => $cm->id,
'userid' => $this->user->id,
'completionstate' => 0,
'viewed' => 0,
'overrideby' => null,
'timemodified' => 0,
];
$completiondatabeforeview = $completioninfo->get_completion_data($cm->id, $this->user->id, $defaultdata);
$this->assertTrue(array_key_exists('viewed', $completiondatabeforeview));
$this->assertTrue(array_key_exists('coursemoduleid', $completiondatabeforeview));
$this->assertEquals(0, $completiondatabeforeview['viewed']);
$this->assertEquals($cm->id, $completiondatabeforeview['coursemoduleid']);
// Set viewed.
$completioninfo->set_module_viewed($cm, $this->user->id);
$completiondata = $completioninfo->get_completion_data($cm->id, $this->user->id, $defaultdata);
$this->assertTrue(array_key_exists('viewed', $completiondata));
$this->assertTrue(array_key_exists('coursemoduleid', $completiondata));
$this->assertEquals(1, $completiondata['viewed']);
$this->assertEquals($cm->id, $completiondatabeforeview['coursemoduleid']);
$completioninfo->reset_all_state($cm);
$completiondataafterreset = $completioninfo->get_completion_data($cm->id, $this->user->id, $defaultdata);
$this->assertTrue(array_key_exists('viewed', $completiondataafterreset));
$this->assertTrue(array_key_exists('coursemoduleid', $completiondataafterreset));
$this->assertEquals(1, $completiondataafterreset['viewed']);
$this->assertEquals($cm->id, $completiondatabeforeview['coursemoduleid']);
}
/**
* Tests for completion_info::get_other_cm_completion_data().
*

View File

@ -29,7 +29,7 @@
defined('MOODLE_INTERNAL') || die();
$version = 2022101100.00; // YYYYMMDD = weekly release date of this DEV branch.
$version = 2022101400.00; // YYYYMMDD = weekly release date of this DEV branch.
// RR = release increments - 00 in DEV branches.
// .XX = incremental changes.
$release = '4.1dev+ (Build: 20221011)'; // Human-friendly version name