diff --git a/backup/moodle2/backup_stepslib.php b/backup/moodle2/backup_stepslib.php index 2d2552dbe4b..e98b92ec558 100644 --- a/backup/moodle2/backup_stepslib.php +++ b/backup/moodle2/backup_stepslib.php @@ -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; + } } diff --git a/backup/moodle2/restore_stepslib.php b/backup/moodle2/restore_stepslib.php index 4d4cd316de6..8fb41b35232 100644 --- a/backup/moodle2/restore_stepslib.php +++ b/backup/moodle2/restore_stepslib.php @@ -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); } } diff --git a/backup/moodle2/tests/restore_stepslib_date_test.php b/backup/moodle2/tests/restore_stepslib_date_test.php index cdbc491abf1..8ff878c55d3 100644 --- a/backup/moodle2/tests/restore_stepslib_date_test.php +++ b/backup/moodle2/tests/restore_stepslib_date_test.php @@ -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. diff --git a/completion/classes/privacy/provider.php b/completion/classes/privacy/provider.php index fb3a4f9a870..93a7a692c2e 100644 --- a/completion/classes/privacy/provider.php +++ b/completion/classes/privacy/provider.php @@ -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); diff --git a/completion/tests/behat/backup_restore_completion.feature b/completion/tests/behat/backup_restore_completion.feature new file mode 100644 index 00000000000..c3bbd98ec21 --- /dev/null +++ b/completion/tests/behat/backup_restore_completion.feature @@ -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" diff --git a/completion/tests/behat/enable_completion_on_view_and_grade.feature b/completion/tests/behat/enable_completion_on_view_and_grade.feature index 69fe918b26a..f762c49dd91 100644 --- a/completion/tests/behat/enable_completion_on_view_and_grade.feature +++ b/completion/tests/behat/enable_completion_on_view_and_grade.feature @@ -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" diff --git a/completion/tests/fixtures/legacy_course_completion.mbz b/completion/tests/fixtures/legacy_course_completion.mbz new file mode 100644 index 00000000000..4d84d4e8fad Binary files /dev/null and b/completion/tests/fixtures/legacy_course_completion.mbz differ diff --git a/course/lib.php b/course/lib.php index 4298b24caab..b6745221c70 100644 --- a/course/lib.php +++ b/course/lib.php @@ -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)); diff --git a/lang/en/completion.php b/lang/en/completion.php index 041a1bd372a..41aec434b90 100644 --- a/lang/en/completion.php +++ b/lang/en/completion.php @@ -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'; diff --git a/lib/completionlib.php b/lib/completionlib.php index eb8057da8dc..94cd4b0c924 100644 --- a/lib/completionlib.php +++ b/lib/completionlib.php @@ -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; + } } /** diff --git a/lib/db/install.xml b/lib/db/install.xml index 12e63fa531c..8dd7a7324d2 100644 --- a/lib/db/install.xml +++ b/lib/db/install.xml @@ -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> diff --git a/lib/db/upgrade.php b/lib/db/upgrade.php index 97776ccba2f..9cf4044c21e 100644 --- a/lib/db/upgrade.php +++ b/lib/db/upgrade.php @@ -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; } diff --git a/lib/moodlelib.php b/lib/moodlelib.php index 5cc89ea432c..a1369b34d17 100644 --- a/lib/moodlelib.php +++ b/lib/moodlelib.php @@ -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)); diff --git a/lib/tests/completionlib_test.php b/lib/tests/completionlib_test.php index ecd75696b2f..e34f69dc8d9 100644 --- a/lib/tests/completionlib_test.php +++ b/lib/tests/completionlib_test.php @@ -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(). * diff --git a/version.php b/version.php index 38b6beea2d2..8dbbc34e95e 100644 --- a/version.php +++ b/version.php @@ -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