Commit 4e50df36 authored by Jenkins's avatar Jenkins
Browse files

Merge branch 'MDL-32103-master' of git://github.com/ilyatregubov/moodle into master_precheck

parents a5f0b354 70271d2a
......@@ -61,10 +61,7 @@ Feature: Enable Block Completion in a course using activity completion
And I am on "Course 1" course homepage
And I follow "Test page name"
And I am on "Course 1" course homepage
Then I should see "Status: Pending" in the "Course completion status" "block"
And I should see "0 of 1" in the "Activity completion" "table_row"
And I trigger cron
And I am on "Course 1" course homepage
Then I should see "Status: Complete" in the "Course completion status" "block"
And I should see "1 of 1" in the "Activity completion" "table_row"
And I follow "More details"
And I should see "Yes" in the "Activity completion" "table_row"
......@@ -166,7 +166,6 @@ class completion_completion extends data_object {
// Set time complete.
$this->timecompleted = $timecomplete;
// Save record.
if ($result = $this->_save()) {
$data = $this->get_record_data();
......@@ -211,17 +210,16 @@ class completion_completion extends data_object {
*
* This method creates a course_completions record if none exists
* @access private
* @return bool
* @return int|null
*/
private function _save() {
if ($this->timeenrolled === null) {
$this->timeenrolled = 0;
}
$result = false;
// Save record
if ($this->id) {
$result = $this->update();
$success = $this->update();
} else {
// Make sure reaggregate field is not null
if (!$this->reaggregate) {
......@@ -233,10 +231,10 @@ class completion_completion extends data_object {
$this->timestarted = 0;
}
$result = $this->insert();
$success = $this->insert();
}
if ($result) {
if ($success) {
// Update the cached record.
$cache = cache::make('core', 'coursecompletion');
$data = $this->get_record_data();
......@@ -244,6 +242,9 @@ class completion_completion extends data_object {
$cache->set($key, ['value' => $data]);
}
return $result;
if ($success) {
return $this->id;
}
return null;
}
}
......@@ -120,7 +120,8 @@ class completion_criteria_completion extends data_object {
'userid' => $this->userid
);
$ccompletion = new completion_completion($cc);
$ccompletion->mark_inprogress($this->timecompleted);
$result = $ccompletion->mark_inprogress($this->timecompleted);
return $result;
}
/**
......
......@@ -206,42 +206,28 @@ class completion_criteria_activity extends completion_criteria {
global $DB;
// Get all users who meet this criteria
$sql = '
$sql = "
SELECT DISTINCT
c.id AS course,
cr.id AS criteriaid,
ra.userid AS userid,
mc.timemodified AS timecompleted
FROM
{course_completion_criteria} cr
INNER JOIN
{course} c
ON cr.course = c.id
INNER JOIN
{context} con
ON con.instanceid = c.id
INNER JOIN
{role_assignments} ra
ON ra.contextid = con.id
INNER JOIN
{course_modules_completion} mc
ON mc.coursemoduleid = cr.moduleinstance
AND mc.userid = ra.userid
LEFT JOIN
{course_completion_crit_compl} cc
ON cc.criteriaid = cr.id
AND cc.userid = ra.userid
WHERE
cr.criteriatype = '.COMPLETION_CRITERIA_TYPE_ACTIVITY.'
AND con.contextlevel = '.CONTEXT_COURSE.'
AND c.enablecompletion = 1
AND cc.id IS NULL
AND (
mc.completionstate = '.COMPLETION_COMPLETE.'
OR mc.completionstate = '.COMPLETION_COMPLETE_PASS.'
OR mc.completionstate = '.COMPLETION_COMPLETE_FAIL.'
)
';
FROM {course_completion_criteria} cr
INNER JOIN {course} c ON cr.course = c.id
INNER JOIN {context} con ON con.instanceid = c.id
INNER JOIN {role_assignments} ra ON ra.contextid = con.id
INNER JOIN {course_modules_completion} mc ON mc.coursemoduleid = cr.moduleinstance AND mc.userid = ra.userid
LEFT JOIN {course_completion_crit_compl} cc ON cc.criteriaid = cr.id AND cc.userid = ra.userid
WHERE cr.criteriatype = ".COMPLETION_CRITERIA_TYPE_ACTIVITY."
AND con.contextlevel = ".CONTEXT_COURSE."
AND c.enablecompletion = 1
AND cc.id IS NULL
AND (
mc.completionstate = ".COMPLETION_COMPLETE."
OR mc.completionstate = ".COMPLETION_COMPLETE_PASS."
OR mc.completionstate = ".COMPLETION_COMPLETE_FAIL."
)
";
// Loop through completions, and mark as complete
$rs = $DB->get_recordset_sql($sql);
......@@ -252,6 +238,55 @@ class completion_criteria_activity extends completion_criteria {
$rs->close();
}
/**
* Evaluates course completion criteria for given user in given course.
* If completion conditions are met - marks user as completed a criteria.
* @param int $userid User ID
* @param int $courseid Course ID
* @return int ID of changed course completion record.
*/
public static function completion_user(int $userid, int $courseid) : int {
global $DB;
// Check if user meets this criteria.
$sql = "
SELECT DISTINCT
c.id AS course,
cr.id AS criteriaid,
ra.userid AS userid,
mc.timemodified AS timecompleted
FROM {course_completion_criteria} cr
INNER JOIN {course} c ON cr.course = c.id
INNER JOIN {context} con ON con.instanceid = c.id
INNER JOIN {role_assignments} ra ON ra.contextid = con.id
INNER JOIN {course_modules_completion} mc ON mc.coursemoduleid = cr.moduleinstance AND mc.userid = ra.userid
LEFT JOIN {course_completion_crit_compl} cc ON cc.criteriaid = cr.id AND cc.userid = ra.userid
WHERE cr.criteriatype = :criteriatype
AND con.contextlevel = :contextlevel
AND c.enablecompletion = 1
AND c.id = :courseid
AND ra.userid = :userid
AND cc.id IS NULL
AND (
mc.completionstate = :completionstate
OR mc.completionstate = :completionstatepass
OR mc.completionstate = :completionstatefail
)
";
// Mark as complete.
$record = $DB->get_record_sql($sql, array('courseid' => $courseid,
'userid' => $userid, 'criteriatype' => COMPLETION_CRITERIA_TYPE_ACTIVITY,
'contextlevel' => CONTEXT_COURSE, 'completionstate' => COMPLETION_COMPLETE,
'completionstatepass' => COMPLETION_COMPLETE_PASS, 'completionstatefail' => COMPLETION_COMPLETE_FAIL));
$result = 0;
if ($record) {
$completion = new completion_criteria_completion((array) $record, DATA_OBJECT_FETCH_BY_KEY);
$result = $completion->mark_complete($record->timecompleted);
}
return $result;
}
/**
* Return criteria progress details for display in reports
*
......
@core @core_completion
Feature: Allow to mark course as completed without cron for activity completion criteria
In order for students to see instant course completion updates
I need to be able update completion state without cron
Background:
Given the following "courses" exist:
| fullname | shortname | category |
| Completion course | CC1 | 0 |
And the following "users" exist:
| username | firstname | lastname | email |
| student1 | Student | First | student1@example.com |
| student2 | Student | Second | student2@example.com |
| teacher1 | Teacher | First | teacher1@example.com |
And the following "course enrolments" exist:
| user | course | role |
| student1 | CC1 | student |
| student2 | CC1 | student |
| teacher1 | CC1 | editingteacher |
And the following "activity" exists:
| activity | assign |
| course | CC1 |
| name | Test assignment name |
| idnumber | assign1 |
| description | Test assignment description |
And I log in as "admin"
And I am on "Completion course" course homepage
And I navigate to "Edit settings" in current page administration
And I expand all fieldsets
And I set the field "Enable completion tracking" to "Yes"
And I click on "Save and display" "button"
And I follow "Test assignment name"
And I navigate to "Edit settings" in current page administration
And I follow "Expand all"
And I set the field "Completion tracking" to "Show activity as complete when conditions are met"
And I set the field "completionusegrade" to "1"
And I press "Save and return to course"
And I navigate to "Course completion" in current page administration
And I expand all fieldsets
And I set the field "Assignment - Test assignment name" to "1"
And I press "Save changes"
And I turn editing mode on
And I add the "Course completion status" block
And I log out
@javascript
Scenario: Update course completion when student marks activity as complete
Given I log in as "teacher1"
And I am on "Completion course" course homepage
And I follow "Test assignment name"
And I navigate to "Edit settings" in current page administration
And I follow "Expand all"
And I set the field "Completion tracking" to "Students can manually mark the activity as completed"
And I press "Save and return to course"
And I log out
When I log in as "student1"
And I am on "Completion course" course homepage
And I should see "Status: Not yet started"
And I press "Mark as done"
And I wait until "Done" "button" exists
And "Mark as done" "button" should not exist
And I reload the page
Then I should see "Status: Complete"
@javascript
Scenario: Update course completion when teacher grades a single assignment
Given I log in as "teacher1"
And I am on "Completion course" course homepage
And I follow "Test assignment name"
And I navigate to "View all submissions" in current page administration
And I click on "Grade" "link" in the "student1@example.com" "table_row"
And I set the field "Grade out of 100" to "40"
And I click on "Save changes" "button"
And I am on "Completion course" course homepage
And I log out
And I log in as "student1"
When I am on "Completion course" course homepage
Then I should see "Status: Complete"
@javascript
Scenario: Update course completion with multiple activity criteria
Given I log in as "admin"
And the following "activity" exists:
| activity | assign |
| course | CC1 |
| name | Test assignment name2 |
| idnumber | assign2 |
| description | Test assignment description |
And I am on "Completion course" course homepage
And I follow "Test assignment name2"
And I navigate to "Edit settings" in current page administration
And I follow "Expand all"
And I set the field "Completion tracking" to "Show activity as complete when conditions are met"
And I set the field "completionusegrade" to "1"
And I press "Save and return to course"
And I navigate to "Course completion" in current page administration
And I expand all fieldsets
And I set the field "Assignment - Test assignment name" to "1"
And I set the field "Assignment - Test assignment name2" to "1"
And I press "Save changes"
And I am on "Completion course" course homepage
And I follow "Test assignment name"
And I navigate to "View all submissions" in current page administration
And I click on "Grade" "link" in the "student1@example.com" "table_row"
And I set the field "Grade out of 100" to "40"
And I click on "Save changes" "button"
And I am on "Completion course" course homepage
And I log out
And I log in as "student1"
And I am on "Completion course" course homepage
And I should see "Status: In progress"
And I log out
When I log in as "teacher1"
And I am on "Completion course" course homepage
And I follow "Test assignment name2"
And I navigate to "View all submissions" in current page administration
And I click on "Grade" "link" in the "student1@example.com" "table_row"
And I set the field "Grade out of 100" to "40"
And I click on "Save changes" "button"
And I am on "Completion course" course homepage
And I log out
And I log in as "student1"
And I am on "Completion course" course homepage
Then I should see "Status: Complete"
@javascript
Scenario: Course completion should not be updated when teacher grades assignment on course grader report page
Given I log in as "teacher1"
And I am on "Completion course" course homepage
And I navigate to "View > Grader report" in the course gradebook
And I press "Turn editing on"
And I give the grade "57" to the user "Student First" for the grade item "Test assignment name"
And I press "Save changes"
And I log out
And I log in as "student1"
When I am on "Completion course" course homepage
Then I should see "Status: Pending"
And I run the scheduled task "core\task\completion_regular_task"
And I wait "1" seconds
And I run the scheduled task "core\task\completion_regular_task"
And I reload the page
And I should see "Status: Complete"
@javascript
Scenario: Course completion should not be updated when teacher grades assignment on activity grader report page
Given I log in as "teacher1"
And I am on "Completion course" course homepage
And I navigate to "View > Grader report" in the course gradebook
And I follow "Single view"
And I select "Student First" from the "Select user..." singleselect
And I set the field "Override for Test assignment name" to "1"
When I set the following fields to these values:
| Grade for Test assignment name | 10.00 |
| Feedback for Test assignment name | test data |
And I press "Save"
And I press "Continue"
And I log out
And I log in as "student1"
And I am on "Completion course" course homepage
And I should see "Status: Pending"
And I run the scheduled task "core\task\completion_regular_task"
And I wait "1" seconds
And I run the scheduled task "core\task\completion_regular_task"
And I reload the page
Then I should see "Status: Complete"
@javascript @_file_upload
Scenario: Course completion should not be updated when teacher imports grades with csv file
Given I log in as "teacher1"
And I am on "Completion course" course homepage
And I navigate to "Import" in the course gradebook
And I upload "lib/tests/fixtures/upload_grades.csv" file to "File" filemanager
And I press "Upload grades"
And I set the field "Map to" to "Email address"
And I set the field "Test assignment name" to "Assignment: Test assignment name"
And I press "Upload grades"
And I press "Continue"
And I should see "10.00" in the "Student First" "table_row"
And I log out
And I log in as "student1"
And I am on "Completion course" course homepage
And I should see "Status: Pending"
When I run the scheduled task "core\task\completion_regular_task"
And I wait "1" seconds
And I run the scheduled task "core\task\completion_regular_task"
And I reload the page
Then I should see "Status: Complete"
......@@ -98,7 +98,7 @@ class core_completion_privacy_test extends \core_privacy\tests\provider_testcase
$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->assertEquals('Complete', $coursecompletion['status']);
$this->assertCount(2, $coursecompletion['criteria']);
}
......
......@@ -148,7 +148,7 @@ class core_course_privacy_testcase extends \core_privacy\tests\provider_testcase
$writer = \core_privacy\local\request\writer::with_context($this->coursecontext);
\core_course\privacy\provider::export_user_data($approvedlist);
$completiondata = $writer->get_data([get_string('privacy:completionpath', 'course')]);
$this->assertEquals('In progress', $completiondata->status);
$this->assertEquals('Complete', $completiondata->status);
$this->assertCount(2, $completiondata->criteria);
// User has a favourite course.
......@@ -272,7 +272,7 @@ class core_course_privacy_testcase extends \core_privacy\tests\provider_testcase
$records = $DB->get_records('course_modules_completion');
$this->assertCount(2, $records);
$records = $DB->get_records('course_completion_crit_compl');
$this->assertCount(2, $records);
$this->assertCount(4, $records);
// Delete data for all users in a context different than the course context (system context).
\core_course\privacy\provider::delete_data_for_all_users_in_context($systemcontext);
......
......@@ -119,7 +119,8 @@ function grade_import_commit($courseid, $importcode, $importfeedback=true, $verb
// False means do not change. See grade_itme::update_final_grade().
$grade->finalgrade = false;
}
if (!$gradeitem->update_final_grade($grade->userid, $grade->finalgrade, 'import', $grade->feedback)) {
if (!$gradeitem->update_final_grade($grade->userid, $grade->finalgrade, 'import',
$grade->feedback, FORMAT_MOODLE, null, null, true)) {
$errordata = new stdClass();
$errordata->itemname = $gradeitem->itemname;
$errordata->userid = $grade->userid;
......
......@@ -334,7 +334,8 @@ class grade_report_grader extends grade_report {
}
}
$gradeitem->update_final_grade($userid, $finalgrade, 'gradebook', $feedback, FORMAT_MOODLE);
$gradeitem->update_final_grade($userid, $finalgrade, 'gradebook', $feedback,
FORMAT_MOODLE, null, null, true);
// We can update feedback without reloading the grade item as it doesn't affect grade calculations
if ($datatype === 'feedback') {
......
......@@ -174,7 +174,8 @@ class finalgrade extends grade_attribute_format implements unique_value, be_disa
}
// Only update grades if there are no errors.
$gradeitem->update_final_grade($userid, $finalgrade, 'singleview', $feedback, FORMAT_MOODLE);
$gradeitem->update_final_grade($userid, $finalgrade, 'singleview', $feedback, FORMAT_MOODLE,
null, null, true);
return '';
}
}
......@@ -555,7 +555,7 @@ class core_grades_external extends external_api {
}
return grade_update($params['source'], $params['courseid'], $itemtype,
$itemmodule, $iteminstance, $itemnumber, $gradestructure, $params['itemdetails']);
$itemmodule, $iteminstance, $itemnumber, $gradestructure, $params['itemdetails'], true);
}
/**
......
......@@ -64,135 +64,7 @@ class completion_regular_task extends scheduled_task {
}
}
if (debugging()) {
mtrace('Aggregating completions');
}
// Save time started.
$timestarted = time();
// Grab all criteria and their associated criteria completions.
$sql = 'SELECT DISTINCT c.id AS course, cr.id AS criteriaid, crc.userid AS userid,
cr.criteriatype AS criteriatype, cc.timecompleted AS timecompleted
FROM {course_completion_criteria} cr
INNER JOIN {course} c ON cr.course = c.id
INNER JOIN {course_completions} crc ON crc.course = c.id
LEFT JOIN {course_completion_crit_compl} cc ON cc.criteriaid = cr.id AND crc.userid = cc.userid
WHERE c.enablecompletion = 1
AND crc.timecompleted IS NULL
AND crc.reaggregate > 0
AND crc.reaggregate < :timestarted
ORDER BY course, userid';
$rs = $DB->get_recordset_sql($sql, ['timestarted' => $timestarted]);
// Check if result is empty.
if (!$rs->valid()) {
$rs->close();
return;
}
$currentuser = null;
$currentcourse = null;
$completions = [];
while (1) {
// Grab records for current user/course.
foreach ($rs as $record) {
// If we are still grabbing the same users completions.
if ($record->userid === $currentuser && $record->course === $currentcourse) {
$completions[$record->criteriaid] = $record;
} else {
break;
}
}
// Aggregate.
if (!empty($completions)) {
if (debugging()) {
mtrace('Aggregating completions for user ' . $currentuser . ' in course ' . $currentcourse);
}
// Get course info object.
$info = new \completion_info((object)['id' => $currentcourse]);
// Setup aggregation.
$overall = $info->get_aggregation_method();
$activity = $info->get_aggregation_method(COMPLETION_CRITERIA_TYPE_ACTIVITY);
$prerequisite = $info->get_aggregation_method(COMPLETION_CRITERIA_TYPE_COURSE);
$role = $info->get_aggregation_method(COMPLETION_CRITERIA_TYPE_ROLE);
$overallstatus = null;
$activitystatus = null;
$prerequisitestatus = null;
$rolestatus = null;
// Get latest timecompleted.
$timecompleted = null;
// Check each of the criteria.
foreach ($completions as $params) {
$timecompleted = max($timecompleted, $params->timecompleted);
$completion = new \completion_criteria_completion((array)$params, false);
// Handle aggregation special cases.
if ($params->criteriatype == COMPLETION_CRITERIA_TYPE_ACTIVITY) {
completion_cron_aggregate($activity, $completion->is_complete(), $activitystatus);
} else if ($params->criteriatype == COMPLETION_CRITERIA_TYPE_COURSE) {
completion_cron_aggregate($prerequisite, $completion->is_complete(), $prerequisitestatus);
} else if ($params->criteriatype == COMPLETION_CRITERIA_TYPE_ROLE) {
completion_cron_aggregate($role, $completion->is_complete(), $rolestatus);
} else {
completion_cron_aggregate($overall, $completion->is_complete(), $overallstatus);
}
}
// Include role criteria aggregation in overall aggregation.
if ($rolestatus !== null) {
completion_cron_aggregate($overall, $rolestatus, $overallstatus);
}
// Include activity criteria aggregation in overall aggregation.
if ($activitystatus !== null) {
completion_cron_aggregate($overall, $activitystatus, $overallstatus);
}
// Include prerequisite criteria aggregation in overall aggregation.
if ($prerequisitestatus !== null) {
completion_cron_aggregate($overall, $prerequisitestatus, $overallstatus);
}
// If aggregation status is true, mark course complete for user.
if ($overallstatus) {
if (debugging()) {
mtrace('Marking complete');
}
$ccompletion = new \completion_completion([
'course' => $params->course,
'userid' => $params->userid
]);
$ccompletion->mark_complete($timecompleted);
}
}
// If this is the end of the recordset, break the loop.
if (!$rs->valid()) {
$rs->close();
break;
}
// New/next user, update user details, reset completions.
$currentuser = $record->userid;
$currentcourse = $record->course;
$completions = [];
$completions[$record->criteriaid] = $record;
}
// Mark all users as aggregated.
$sql = "UPDATE {course_completions}
SET reaggregate = 0
WHERE reaggregate < :timestarted
AND reaggregate > 0";
$DB->execute($sql, ['timestarted' => $timestarted]);
aggregate_completions(0);
}
}
......
......@@ -580,10 +580,12 @@ class completion_info {
* must be used; these directly set the specified state.
* @param int $userid User ID to be updated. Default 0 = current user
* @param bool $override Whether manually overriding the existing completion state.
* @param bool $isbulkupdate If bulk grade update is happening.
* @return void
* @throws moodle_exception if trying to override without permission.
*/
public function update_state($cm, $possibleresult=COMPLETION_UNKNOWN, $userid=0, $override = false) {
public function update_state($cm, $possibleresult=COMPLETION_UNKNOWN, $userid=0,