first commit

This commit is contained in:
CHIEFSOFT\ameye
2024-09-30 18:11:26 -04:00
commit e592ca6823
27270 changed files with 5002257 additions and 0 deletions
@@ -0,0 +1,208 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
declare(strict_types = 1);
namespace core_completion;
use cm_info;
use coding_exception;
use moodle_exception;
/**
* Base class for defining an activity module's custom completion rules.
*
* Class for defining an activity module's custom completion rules and fetching the completion statuses
* of the custom completion rules for a given module instance and a user.
*
* @package core_completion
* @copyright 2021 Jun Pataleta <jun@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
abstract class activity_custom_completion {
/** @var cm_info The course module information object. */
protected $cm;
/** @var int The user's ID. */
protected $userid;
/** @var array The current state of core completion */
protected $completionstate;
/**
* activity_custom_completion constructor.
*
* @param cm_info $cm
* @param int $userid
* @param array|null $completionstate The current state of the core completion criteria
*/
public function __construct(cm_info $cm, int $userid, ?array $completionstate = null) {
$this->cm = $cm;
$this->userid = $userid;
$this->completionstate = $completionstate;
}
/**
* Validates that the custom rule is defined by this plugin and is enabled for this activity instance.
*
* @param string $rule The custom completion rule.
*/
public function validate_rule(string $rule): void {
// Check that this custom completion rule is defined.
if (!$this->is_defined($rule)) {
throw new coding_exception("Undefined custom completion rule '$rule'");
}
// Check that this custom rule is included in the course module's custom completion rules.
if (!$this->is_available($rule)) {
throw new moodle_exception("Custom completion rule '$rule' is not used by this activity.");
}
}
/**
* Whether this module defines this custom rule.
*
* @param string $rule The custom completion rule.
* @return bool
*/
public function is_defined(string $rule): bool {
return in_array($rule, static::get_defined_custom_rules());
}
/**
* Checks whether the custom completion rule is being used by the activity module instance.
*
* @param string $rule The custom completion rule.
* @return bool
*/
public function is_available(string $rule): bool {
return in_array($rule, $this->get_available_custom_rules());
}
/**
* Fetches the list of custom completion rules that are being used by this activity module instance.
*
* @return array
*/
public function get_available_custom_rules(): array {
$rules = static::get_defined_custom_rules();
$availablerules = [];
$customdata = (array)$this->cm->customdata;
foreach ($rules as $rule) {
$customrule = $customdata['customcompletionrules'][$rule] ?? false;
if (!empty($customrule)) {
$availablerules[] = $rule;
}
}
return $availablerules;
}
/**
* Fetches the overall completion status of this activity instance for a user based on its available custom completion rules.
*
* @return int The completion state (e.g. COMPLETION_COMPLETE, COMPLETION_INCOMPLETE, COMPLETION_COMPLETE_FAIL).
*/
public function get_overall_completion_state(): int {
$iscompletefail = false;
foreach ($this->get_available_custom_rules() as $rule) {
$state = $this->get_state($rule);
// Return early if one of the custom completion rules is not yet complete.
if ($state == COMPLETION_INCOMPLETE) {
return $state;
}
if (!$iscompletefail) {
$iscompletefail = ($state === COMPLETION_COMPLETE_FAIL || $state === COMPLETION_COMPLETE_FAIL_HIDDEN);
}
}
if ($iscompletefail) {
return COMPLETION_COMPLETE_FAIL;
}
// If this was reached, then all custom rules have been marked complete.
return COMPLETION_COMPLETE;
}
/**
* Fetches the description for a given custom completion rule.
*
* @param string $rule The custom completion rule.
* @return string
*/
public function get_custom_rule_description(string $rule): string {
$descriptions = $this->get_custom_rule_descriptions();
if (!isset($descriptions[$rule])) {
// Lang string not found for this custom completion rule. Just return it.
return $rule;
}
return $descriptions[$rule];
}
/**
* Show the manual completion or not regardless of the course's showcompletionconditions setting.
* Returns false by default for plugins that don't need to override the course's showcompletionconditions setting.
* Activity plugins that need to always show manual completion need to override this function.
*
* @return bool
*/
public function manual_completion_always_shown(): bool {
return false;
}
/**
* Fetches the module's custom completion class implementation if it's available.
*
* @param string $modname The activity module name. Usually from cm_info::modname.
* @return string|null
*/
public static function get_cm_completion_class(string $modname): ?string {
$cmcompletionclass = "mod_{$modname}\\completion\\custom_completion";
if (class_exists($cmcompletionclass) && is_subclass_of($cmcompletionclass, self::class)) {
return $cmcompletionclass;
}
return null;
}
/**
* Fetches the completion state for a given completion rule.
*
* @param string $rule The completion rule.
* @return int The completion state.
*/
abstract public function get_state(string $rule): int;
/**
* Fetch the list of custom completion rules that this module defines.
*
* @return array
*/
abstract public static function get_defined_custom_rules(): array;
/**
* Returns an associative array of the descriptions of custom completion rules.
*
* @return array
*/
abstract public function get_custom_rule_descriptions(): array;
/**
* Returns an array of all completion rules, in the order they should be displayed to users.
*
* @return array
*/
abstract public function get_sort_order(): array;
}
+190
View File
@@ -0,0 +1,190 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Contains class containing completion API.
*
* @package core_completion
* @copyright 2017 Mark Nelson <markn@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_completion;
defined('MOODLE_INTERNAL') || die();
/**
* Class containing completion API.
*
* @package core_completion
* @copyright 2017 Mark Nelson <markn@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class api {
/**
* @var string The completion expected on event.
*/
const COMPLETION_EVENT_TYPE_DATE_COMPLETION_EXPECTED = 'expectcompletionon';
/**
* Creates, updates or deletes an event for the expected completion date.
*
* @param int $cmid The course module id
* @param string $modulename The name of the module (eg. assign, quiz)
* @param \stdClass|int $instanceorid The instance object or ID.
* @param int|null $completionexpectedtime The time completion is expected, null if not set
* @return bool
*/
public static function update_completion_date_event($cmid, $modulename, $instanceorid, $completionexpectedtime) {
global $CFG, $DB;
// Required for calendar constant CALENDAR_EVENT_TYPE_ACTION.
require_once($CFG->dirroot . '/calendar/lib.php');
$instance = null;
if (is_object($instanceorid)) {
$instance = $instanceorid;
} else {
$instance = $DB->get_record($modulename, array('id' => $instanceorid), '*', IGNORE_MISSING);
}
if (!$instance) {
return false;
}
$course = get_course($instance->course);
$completion = new \completion_info($course);
// Can not create/update an event if completion is disabled.
if (!$completion->is_enabled() && $completionexpectedtime !== null) {
return true;
}
// Create the \stdClass we will be using for our language strings.
$lang = new \stdClass();
$lang->modulename = get_string('pluginname', $modulename);
$lang->instancename = $instance->name;
// Create the calendar event.
$event = new \stdClass();
$event->type = CALENDAR_EVENT_TYPE_ACTION;
$event->eventtype = self::COMPLETION_EVENT_TYPE_DATE_COMPLETION_EXPECTED;
if ($event->id = $DB->get_field('event', 'id', array('modulename' => $modulename,
'instance' => $instance->id, 'eventtype' => $event->eventtype))) {
if ($completionexpectedtime !== null) {
// Calendar event exists so update it.
$event->name = get_string('completionexpectedfor', 'completion', $lang);
$event->description = format_module_intro($modulename, $instance, $cmid, false);
$event->format = FORMAT_HTML;
$event->timestart = $completionexpectedtime;
$event->timesort = $completionexpectedtime;
$event->visible = instance_is_visible($modulename, $instance);
$event->timeduration = 0;
$calendarevent = \calendar_event::load($event->id);
$calendarevent->update($event, false);
} else {
// Calendar event is no longer needed.
$calendarevent = \calendar_event::load($event->id);
$calendarevent->delete();
}
} else {
// Event doesn't exist so create one.
if ($completionexpectedtime !== null) {
$event->name = get_string('completionexpectedfor', 'completion', $lang);
$event->description = format_module_intro($modulename, $instance, $cmid, false);
$event->format = FORMAT_HTML;
$event->courseid = $instance->course;
$event->groupid = 0;
$event->userid = 0;
$event->modulename = $modulename;
$event->instance = $instance->id;
$event->timestart = $completionexpectedtime;
$event->timesort = $completionexpectedtime;
$event->visible = instance_is_visible($modulename, $instance);
$event->timeduration = 0;
\calendar_event::create($event, false);
}
}
return true;
}
/**
* Mark users who completed course based on activity criteria.
* @param array $userdata If set only marks specified user in given course else checks all courses/users.
* @return int Completion record id if $userdata is set, 0 else.
* @since Moodle 4.0
*/
public static function mark_course_completions_activity_criteria($userdata = null): int {
global $DB;
// Get all users who meet 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} cm ON cm.id = cr.moduleinstance
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 cc.id IS NULL
AND (
mc.completionstate = :completionstate
OR (cm.completionpassgrade = 1 AND mc.completionstate = :completionstatepass1)
OR (cm.completionpassgrade = 0 AND (mc.completionstate = :completionstatepass2
OR mc.completionstate = :completionstatefail))
)";
$params = [
'criteriatype' => COMPLETION_CRITERIA_TYPE_ACTIVITY,
'contextlevel' => CONTEXT_COURSE,
'completionstate' => COMPLETION_COMPLETE,
'completionstatepass1' => COMPLETION_COMPLETE_PASS,
'completionstatepass2' => COMPLETION_COMPLETE_PASS,
'completionstatefail' => COMPLETION_COMPLETE_FAIL
];
if ($userdata) {
$params['courseid'] = $userdata['courseid'];
$params['userid'] = $userdata['userid'];
$sql .= " AND c.id = :courseid AND ra.userid = :userid";
// Mark as complete.
$record = $DB->get_record_sql($sql, $params);
if ($record) {
$completion = new \completion_criteria_completion((array) $record, DATA_OBJECT_FETCH_BY_KEY);
$result = $completion->mark_complete($record->timecompleted);
return $result;
}
} else {
// Loop through completions, and mark as complete.
$rs = $DB->get_recordset_sql($sql, $params);
foreach ($rs as $record) {
$completion = new \completion_criteria_completion((array) $record, DATA_OBJECT_FETCH_BY_KEY);
$completion->mark_complete($record->timecompleted);
}
$rs->close();
}
return 0;
}
}
+150
View File
@@ -0,0 +1,150 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
use core_completion\manager;
/**
* Bulk edit activity completion form
*
* @package core_completion
* @copyright 2017 Marina Glancy
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class core_completion_bulkedit_form extends core_completion_edit_base_form {
/** @var cm_info[] list of selected course modules */
protected $cms = [];
/** @var array Do not use directly, call $this->get_module_names() */
protected $_modnames = null;
/**
* Returns list of types of selected modules
*
* @return array modname=>modfullname
*/
protected function get_module_names() {
if ($this->_modnames !== null) {
return $this->_modnames;
}
$this->_modnames = [];
foreach ($this->cms as $cm) {
$this->_modnames[$cm->modname] = $cm->modfullname;
}
return $this->_modnames;
}
/**
* It will return the course module when $cms has only one course module; otherwise, null will be returned.
*
* @return cm_info|null
*/
protected function get_cm(): ?cm_info {
if (count($this->cms) === 1) {
return reset($this->cms);
}
// If there are multiple modules, so none will be selected.
return null;
}
/**
* Returns an instance of component-specific module form for the first selected module
*
* @return moodleform_mod|null
*/
protected function get_module_form() {
global $CFG, $PAGE;
if ($this->_moduleform) {
return $this->_moduleform;
}
$cm = reset($this->cms);
$this->_moduleform = manager::get_module_form(
modname: $cm->modname,
course: $this->course,
cm: $cm,
);
return $this->_moduleform;
}
/**
* Form definition
*/
public function definition() {
$this->cms = $this->_customdata['cms'];
$cm = reset($this->cms); // First selected course module.
$this->course = $cm->get_course();
$mform = $this->_form;
$idx = 0;
foreach ($this->cms as $cm) {
$mform->addElement('hidden', 'cmid['.$idx.']', $cm->id);
$mform->setType('cmid['.$idx.']', PARAM_INT);
$idx++;
}
parent::definition();
$modform = $this->get_module_form();
if ($modform) {
// Pre-fill the form with the current completion rules of the first selected module.
list($cmrec, $context, $module, $data, $cw) = get_moduleinfo_data($cm->get_course_module_record(), $this->course);
$data = (array)$data;
$modform->data_preprocessing($data);
// Unset fields that will conflict with this form and set data to this form.
unset($data['cmid']);
unset($data['id']);
$this->set_data($data);
}
}
/**
* Form validation
*
* @param array $data array of ("fieldname"=>value) of submitted data
* @param array $files array of uploaded files "element_name"=>tmp_file_path
* @return array of "element_name"=>"error_description" if there are errors,
* or an empty array if everything is OK (true allowed for backwards compatibility too).
*/
public function validation($data, $files) {
global $CFG;
$errors = parent::validation($data, $files);
// Completion: Don't let them choose automatic completion without turning
// on some conditions.
if (array_key_exists('completion', $data) &&
$data['completion'] == COMPLETION_TRACKING_AUTOMATIC &&
(!empty($data['completionusegrade']) || !empty($data['completionpassgrade']))) {
require_once($CFG->libdir.'/gradelib.php');
$moduleswithoutgradeitem = [];
foreach ($this->cms as $cm) {
$item = grade_item::fetch(array('courseid' => $cm->course, 'itemtype' => 'mod',
'itemmodule' => $cm->modname, 'iteminstance' => $cm->instance,
'itemnumber' => 0));
if (!$item) {
$moduleswithoutgradeitem[] = $cm->get_formatted_name();
}
}
if ($moduleswithoutgradeitem) {
$errors['completionusegrade'] = get_string('nogradeitem', 'completion', join(', ', $moduleswithoutgradeitem));
}
}
return $errors;
}
}
@@ -0,0 +1,319 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Contains the class for building the user's activity completion details.
*
* @package core_completion
* @copyright 2021 Jun Pataleta <jun@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
declare(strict_types = 1);
namespace core_completion;
use cm_info;
use completion_info;
/**
* Class for building the user's activity completion details.
*
* @package core_completion
* @copyright 2021 Jun Pataleta <jun@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class cm_completion_details {
/** @var completion_info The completion info instance for this cm's course. */
protected $completioninfo = null;
/** @var object The completion data. */
protected $completiondata = null;
/** @var cm_info The course module information. */
protected $cminfo = null;
/** @var int The user ID. */
protected $userid = 0;
/** @var bool Whether to return automatic completion details. */
protected $returndetails = true;
/** @var activity_custom_completion Activity custom completion object. */
protected $cmcompletion = null;
/**
* Constructor.
*
* @param completion_info $completioninfo The completion info instance for this cm's course.
* @param cm_info $cminfo The course module information.
* @param int $userid The user ID.
* @param bool $returndetails Whether to return completion details or not.
*/
public function __construct(completion_info $completioninfo, cm_info $cminfo, int $userid, bool $returndetails = true) {
$this->completioninfo = $completioninfo;
// We need to pass wholecourse = true here for better performance. All the course's completion data for the current
// logged-in user will get in a single query instead of multiple queries and loaded to cache.
$this->completiondata = $completioninfo->get_data($cminfo, true, $userid);
$this->cminfo = $cminfo;
$this->userid = $userid;
$this->returndetails = $returndetails;
$cmcompletionclass = activity_custom_completion::get_cm_completion_class($this->cminfo->modname);
if ($cmcompletionclass) {
$this->cmcompletion = new $cmcompletionclass(
$this->cminfo,
$this->userid,
$completioninfo->get_core_completion_state($cminfo, $userid)
);
}
}
/**
* Fetches the completion details for a user.
*
* @return array An array of completion details for a user containing the completion requirement's description and status.
* @throws \coding_exception
*/
public function get_details(): array {
if (!$this->is_automatic()) {
// No details need to be returned for modules that don't have automatic completion tracking enabled.
return [];
}
if (!$this->returndetails) {
// We don't need to return completion details.
return [];
}
$completiondata = $this->completiondata;
$hasoverride = !empty($this->overridden_by());
$details = [];
// Completion rule: Student must view this activity.
if (!empty($this->cminfo->completionview)) {
if (!$hasoverride) {
$status = COMPLETION_INCOMPLETE;
if ($completiondata->viewed == COMPLETION_VIEWED) {
$status = COMPLETION_COMPLETE;
}
} else {
$status = $completiondata->completionstate;
}
$details['completionview'] = (object)[
'status' => $status,
'description' => get_string('detail_desc:view', 'completion'),
];
}
// Completion rule: Student must receive a grade.
if (!is_null($this->cminfo->completiongradeitemnumber)) {
if (!$hasoverride) {
$status = $completiondata->completiongrade ?? COMPLETION_INCOMPLETE;
} else {
$status = $completiondata->completionstate;
}
$details['completionusegrade'] = (object)[
'status' => $status,
'description' => get_string('detail_desc:receivegrade', 'completion'),
];
if (!is_null($this->cminfo->completionpassgrade) && $this->cminfo->completionpassgrade) {
$details['completionpassgrade'] = (object)[
'status' => $completiondata->passgrade ?? COMPLETION_INCOMPLETE,
'description' => get_string('detail_desc:receivepassgrade', 'completion'),
];
}
}
if ($this->cmcompletion) {
if (isset($completiondata->customcompletion)) {
foreach ($completiondata->customcompletion as $rule => $status) {
$details[$rule] = (object)[
'status' => !$hasoverride ? $status : $completiondata->completionstate,
'description' => $this->cmcompletion->get_custom_rule_description($rule),
];
}
$details = $this->sort_completion_details($details);
}
}
return $details;
}
/**
* Sort completion details in the order specified by the activity's custom completion implementation.
*
* @param array $details The completion details to be sorted.
* @return array
* @throws \coding_exception
*/
protected function sort_completion_details(array $details): array {
$sortorder = $this->cmcompletion->get_sort_order();
$sorteddetails = [];
foreach ($sortorder as $sortedkey) {
if (isset($details[$sortedkey])) {
$sorteddetails[$sortedkey] = $details[$sortedkey];
}
}
// Make sure the sorted list includes all of the conditions that were set.
if (count($sorteddetails) < count($details)) {
$exceptiontext = get_class($this->cmcompletion) .'::get_sort_order() is missing one or more completion conditions.' .
' All custom and standard conditions that apply to this activity must be listed.';
throw new \coding_exception($exceptiontext);
}
return $sorteddetails;
}
/**
* Fetches the overall completion state of this course module.
*
* @return int The overall completion state for this course module.
*/
public function get_overall_completion(): int {
return (int)$this->completiondata->completionstate;
}
/**
* Returns whether the overall completion state of this course module should be marked as complete or not.
* This is based on the completion settings of the course module, so when the course module requires a passing grade,
* it will only be marked as complete when the user has passed the course module. Otherwise, it will be marked as complete
* even when the user has failed the course module.
*
* @return bool True when the module can be marked as completed.
*/
public function is_overall_complete(): bool {
$completionstates = [];
if ($this->is_manual()) {
$completionstates = [COMPLETION_COMPLETE];
} else if ($this->is_automatic()) {
// Successfull completion states depend on the completion settings.
if (property_exists($this->completiondata, 'customcompletion') && !empty($this->completiondata->customcompletion)) {
// If the module has any failed custom completion rule the state could be COMPLETION_COMPLETE_FAIL.
$completionstates = [COMPLETION_COMPLETE, COMPLETION_COMPLETE_PASS];
} else if (isset($this->completiondata->passgrade)) {
// Passing grade is required. Don't mark it as complete when state is COMPLETION_COMPLETE_FAIL.
$completionstates = [COMPLETION_COMPLETE, COMPLETION_COMPLETE_PASS];
} else {
// Any grade is required. Mark it as complete even when state is COMPLETION_COMPLETE_FAIL.
$completionstates = [COMPLETION_COMPLETE, COMPLETION_COMPLETE_PASS, COMPLETION_COMPLETE_FAIL];
}
}
return in_array($this->get_overall_completion(), $completionstates);
}
/**
* Whether this activity module has completion enabled.
*
* @return bool
*/
public function has_completion(): bool {
return $this->completioninfo->is_enabled($this->cminfo) != COMPLETION_DISABLED;
}
/**
* Whether this activity module instance tracks completion automatically.
*
* @return bool
*/
public function is_automatic(): bool {
return $this->cminfo->completion == COMPLETION_TRACKING_AUTOMATIC;
}
/**
* Whether this activity module instance tracks completion manually.
*
* @return bool
*/
public function is_manual(): bool {
return $this->cminfo->completion == COMPLETION_TRACKING_MANUAL;
}
/**
* Fetches the user ID that has overridden the completion state of this activity for the user.
*
* @return int|null
*/
public function overridden_by(): ?int {
return isset($this->completiondata->overrideby) ? (int)$this->completiondata->overrideby : null;
}
/**
* Checks whether completion is being tracked for this user.
*
* @return bool
*/
public function is_tracked_user(): bool {
return $this->completioninfo->is_tracked_user($this->userid);
}
/**
* Determine whether to show the manual completion or not.
*
* @return bool
*/
public function show_manual_completion(): bool {
global $PAGE;
if (!$this->is_manual()) {
return false;
}
if ($PAGE->context->contextlevel == CONTEXT_MODULE) {
// Manual completion should always be shown on the activity page.
return true;
} else {
$course = $this->cminfo->get_course();
if ($course->showcompletionconditions == COMPLETION_SHOW_CONDITIONS) {
return true;
} else if ($this->cmcompletion) {
return $this->cmcompletion->manual_completion_always_shown();
}
}
return false;
}
/**
* Completion state timemodified
*
* @return int timestamp
*/
public function get_timemodified(): int {
return (int)$this->completiondata->timemodified;
}
/**
* Generates an instance of this class.
*
* @param cm_info $cminfo The course module info instance.
* @param int $userid The user ID that we're fetching completion details for.
* @param bool $returndetails Whether to return completion details or not.
* @return cm_completion_details
*/
public static function get_instance(cm_info $cminfo, int $userid, bool $returndetails = true): cm_completion_details {
$course = $cminfo->get_course();
$completioninfo = new \completion_info($course);
return new self($completioninfo, $cminfo, $userid, $returndetails);
}
}
+140
View File
@@ -0,0 +1,140 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
use core_completion\manager;
/**
* Default activity completion form
*
* @package core_completion
* @copyright 2017 Marina Glancy
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class core_completion_defaultedit_form extends core_completion_edit_base_form {
/** @var array */
protected $modules;
/** @var array */
protected $_modnames;
public function __construct(
$action = null,
$customdata = null,
$method = 'post',
$target = '',
$attributes = null,
$editable = true,
$ajaxformdata = null
) {
$this->modules = $customdata['modules'];
if ($modname = $this->get_module_name()) {
// Set the form suffix to the module name so that the form identifier is unique for each module type.
$this->set_suffix('_' . $modname);
}
parent::__construct($action, $customdata, $method, $target, $attributes, $editable, $ajaxformdata);
}
/**
* Returns list of types of selected modules
*
* @return array modname=>modfullname
*/
protected function get_module_names() {
if ($this->_modnames !== null) {
return $this->_modnames;
}
$this->_modnames = [];
foreach ($this->modules as $module) {
$this->_modnames[$module->name] = $module->formattedname;
}
return $this->_modnames;
}
/**
* Returns an instance of component-specific module form for the first selected module
*
* @return moodleform_mod|null
*/
protected function get_module_form() {
if ($this->_moduleform) {
return $this->_moduleform;
}
$modnames = array_keys($this->get_module_names());
$this->_moduleform = manager::get_module_form(
modname: $modnames[0],
course: $this->course,
suffix: $this->get_suffix(),
);
return $this->_moduleform;
}
/**
* Form definition,
*/
public function definition() {
$course = $this->_customdata['course'];
$this->course = is_numeric($course) ? get_course($course) : $course;
$this->modules = $this->_customdata['modules'];
$mform = $this->_form;
foreach ($this->modules as $modid => $module) {
$mform->addElement('hidden', 'modids['.$modid.']', $modid);
$mform->setType('modids['.$modid.']', PARAM_INT);
}
parent::definition();
$modform = $this->get_module_form();
if ($modform) {
$modnames = array_keys($this->get_module_names());
$modname = $modnames[0];
// Pre-fill the form with the current completion rules of the first selected module type.
list($module, $context, $cw, $cmrec, $data) = prepare_new_moduleinfo_data(
$this->course,
$modname,
0,
$this->get_suffix()
);
$data = (array)$data;
try {
$modform->data_preprocessing($data);
} catch (moodle_exception $e) {
debugging(
'data_preprocessing function of module ' . $modnames[0] .
' should be fixed so it can be shown together with other Default activity completion forms',
DEBUG_DEVELOPER
);
}
// Unset fields that will conflict with this form and set data to this form.
unset($data['cmid']);
unset($data['modids']);
unset($data['id']);
$this->set_data($data);
}
}
/**
* This method has been overridden because the form identifier must be unique for each module type.
* Otherwise, the form will display the same data for each module type once it's submitted.
*/
protected function get_form_identifier() {
return parent::get_form_identifier() . $this->get_suffix();
}
}
+322
View File
@@ -0,0 +1,322 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
defined('MOODLE_INTERNAL') || die;
require_once($CFG->libdir.'/formslib.php');
require_once($CFG->dirroot.'/course/modlib.php');
/**
* Base form for changing completion rules. Used in bulk editing activity completion and editing default activity completion
*
* @package core_completion
* @copyright 2017 Marina Glancy
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
abstract class core_completion_edit_base_form extends moodleform {
use \core_completion\form\form_trait;
/** @var moodleform_mod Do not use directly, call $this->get_module_form() */
protected $_moduleform = null;
/** @var bool */
protected $hascustomrules = false;
/** @var stdClass */
protected $course;
/**
* Returns list of types of selected module types
*
* @return array modname=>modfullname
*/
abstract protected function get_module_names();
/**
* Get the module name. If the form have more than one modules, it will return the first one.
*
* @return string|null The module name or null if there is no modules associated to this form.
*/
protected function get_module_name(): ?string {
$modnames = $this->get_module_names();
if (empty($modnames)) {
return null;
}
$modnamekeys = array_keys($modnames);
return reset($modnamekeys);
}
/**
* Returns true if all selected modules support tracking view.
*
* @return bool
*/
protected function support_views() {
foreach ($this->get_module_names() as $modname => $modfullname) {
if (!plugin_supports('mod', $modname, FEATURE_COMPLETION_TRACKS_VIEWS, false)) {
return false;
}
}
return true;
}
/**
* Returns true if all selected modules support grading.
*
* @return bool
*/
protected function support_grades() {
foreach ($this->get_module_names() as $modname => $modfullname) {
if (!plugin_supports('mod', $modname, FEATURE_GRADE_HAS_GRADE, false)) {
return false;
}
}
return true;
}
/**
* Returns an instance of component-specific module form for the first selected module
*
* @return moodleform_mod|null
*/
abstract protected function get_module_form();
/**
* If all selected modules are of the same module type, adds custom completion rules from this module type
*
* @return array
*/
protected function add_custom_completion(string $function): array {
$modnames = array_keys($this->get_module_names());
if (count($modnames) != 1 || !plugin_supports('mod', $modnames[0], FEATURE_COMPLETION_HAS_RULES, false)) {
return [false, []];
}
$component = "mod_{$modnames[0]}";
$itemnames = \core_grades\component_gradeitems::get_itemname_mapping_for_component($component);
$hascustomrules = count($itemnames) > 1;
$customcompletionelements = [];
try {
// Add completion rules from the module form to this form.
$moduleform = $this->get_module_form();
// If the module doesn't return a form for any reason, we don't continue checking.
if (!$moduleform) {
return [false, []];
}
$moduleform->_form = $this->_form;
if ($customcompletionelements = $moduleform->{$function}()) {
$hascustomrules = true;
foreach ($customcompletionelements as $customcompletionelement) {
// Instead of checking for the suffix at the end of the element name, we need to check for its presence
// because some modules, like SCORM, are adding things at the end.
if (!str_contains($customcompletionelement, $this->get_suffix())) {
debugging(
'Custom completion rule ' . $customcompletionelement . ' of module ' . $modnames[0] .
' has wrong suffix and has been removed from the form. This has to be fixed by the developer',
DEBUG_DEVELOPER
);
if ($moduleform->_form->elementExists($customcompletionelement)) {
$moduleform->_form->removeElement($customcompletionelement);
}
}
}
}
return [$hascustomrules, $customcompletionelements];
} catch (Exception $e) {
debugging('Could not add custom completion rule of module ' . $modnames[0] .
' to this form, this has to be fixed by the developer', DEBUG_DEVELOPER);
return [false, $customcompletionelements];
}
}
/**
* If all selected modules are of the same module type, adds custom completion rules from this module type
*
* @return array
*/
protected function add_completion_rules() {
list($hascustomrules, $customcompletionelements) = $this->add_custom_completion('add_completion_rules');
if (!$this->hascustomrules && $hascustomrules) {
$this->hascustomrules = true;
}
$component = "mod_{$this->get_module_name()}";
$itemnames = \core_grades\component_gradeitems::get_itemname_mapping_for_component($component);
if (count($itemnames) > 1) {
$customcompletionelements[] = 'completiongradeitemnumber';
}
return $customcompletionelements;
}
/**
* Checks if at least one of the custom completion rules is enabled
*
* @param array $data Input data (not yet validated)
* @return bool True if one or more rules is enabled, false if none are;
* default returns false
*/
protected function completion_rule_enabled($data) {
if ($this->hascustomrules) {
return $this->get_module_form()->completion_rule_enabled($data);
}
return false;
}
/**
* If all selected modules are of the same module type, adds custom completion rules from this module type
*
* @return array
*/
public function add_completiongrade_rules(): array {
list($hascustomrules, $customcompletionelements) = $this->add_custom_completion('add_completiongrade_rules');
if (!$this->hascustomrules && $hascustomrules) {
$this->hascustomrules = true;
}
return $customcompletionelements;
}
/**
* Returns list of modules that have automatic completion rules that are not shown on this form
* (because they are not present in at least one other selected module).
*
* @return array
*/
protected function get_modules_with_hidden_rules() {
$modnames = $this->get_module_names();
if (count($modnames) <= 1) {
// No rules definitions conflicts if there is only one module type.
return [];
}
$conflicts = [];
if (!$this->support_views()) {
// If we don't display views rule but at least one module supports it - we have conflicts.
foreach ($modnames as $modname => $modfullname) {
if (empty($conflicts[$modname]) && plugin_supports('mod', $modname, FEATURE_COMPLETION_TRACKS_VIEWS, false)) {
$conflicts[$modname] = $modfullname;
}
}
}
if (!$this->support_grades()) {
// If we don't display grade rule but at least one module supports it - we have conflicts.
foreach ($modnames as $modname => $modfullname) {
if (empty($conflicts[$modname]) && plugin_supports('mod', $modname, FEATURE_GRADE_HAS_GRADE, false)) {
$conflicts[$modname] = $modfullname;
}
}
}
foreach ($modnames as $modname => $modfullname) {
// We do not display any custom completion rules, find modules that define them and add to conflicts list.
if (empty($conflicts[$modname]) && plugin_supports('mod', $modname, FEATURE_COMPLETION_HAS_RULES, false)) {
$conflicts[$modname] = $modfullname;
}
}
return $conflicts;
}
/**
* Form definition
*/
public function definition() {
$mform = $this->_form;
// Course id.
$mform->addElement('hidden', 'id', $this->course->id);
$mform->setType('id', PARAM_INT);
// Add the completion elements to the form.
$this->add_completion_elements(
$this->get_module_name(),
$this->support_views(),
$this->support_grades(),
false,
$this->course->id
);
if ($conflicts = $this->get_modules_with_hidden_rules()) {
$mform->addElement('static', 'qwerty', '', get_string('hiddenrules', 'completion', join(', ', $conflicts)));
}
// Whether to show the cancel button or not in the form.
$displaycancel = $this->_customdata['displaycancel'] ?? true;
$this->add_action_buttons($displaycancel);
}
/**
* Return the course module of the form, if any.
*
* @return cm_info|null
*/
protected function get_cm(): ?cm_info {
return null;
}
/**
* Each module which defines definition_after_data() must call this method.
*/
public function definition_after_data() {
$this->definition_after_data_completion($this->get_cm());
}
/**
* Form validation
*
* @param array $data array of ("fieldname"=>value) of submitted data
* @param array $files array of uploaded files "element_name"=>tmp_file_path
* @return array of "element_name"=>"error_description" if there are errors,
* or an empty array if everything is OK (true allowed for backwards compatibility too).
*/
public function validation($data, $files) {
$errors = parent::validation($data, $files);
// Completion: Check completion fields don't have errors.
$errors = array_merge($errors, $this->validate_completion($data));
return $errors;
}
/**
* Returns if this form has custom completion rules. This is only possible if all selected modules have the same
* module type and this module type supports custom completion rules
*
* @return bool
*/
public function has_custom_completion_rules() {
return $this->hascustomrules;
}
/**
* Return submitted data if properly submitted or returns NULL if validation fails or
* if there is no submitted data.
*
* @return object submitted data; NULL if not valid or not submitted or cancelled
*/
public function get_data() {
$data = parent::get_data();
if ($data && $this->hascustomrules) {
$this->get_module_form()->data_postprocessing($data);
}
return $data;
}
}
+587
View File
@@ -0,0 +1,587 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Completion external API
*
* @package core_completion
* @category external
* @copyright 2015 Juan Leyva <juan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @since Moodle 2.9
*/
use core_external\external_api;
use core_external\external_function_parameters;
use core_external\external_multiple_structure;
use core_external\external_single_structure;
use core_external\external_value;
use core_external\external_warnings;
defined('MOODLE_INTERNAL') || die;
require_once("$CFG->libdir/completionlib.php");
/**
* Completion external functions
*
* @package core_completion
* @category external
* @copyright 2015 Juan Leyva <juan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @since Moodle 2.9
*/
class core_completion_external extends external_api {
/**
* Describes the parameters for update_activity_completion_status_manually.
*
* @return external_function_parameters
* @since Moodle 2.9
*/
public static function update_activity_completion_status_manually_parameters() {
return new external_function_parameters (
array(
'cmid' => new external_value(PARAM_INT, 'course module id'),
'completed' => new external_value(PARAM_BOOL, 'activity completed or not'),
)
);
}
/**
* Update completion status for the current user in an activity, only for activities with manual tracking.
* @param int $cmid Course module id
* @param bool $completed Activity completed or not
* @return array Result and possible warnings
* @since Moodle 2.9
* @throws moodle_exception
*/
public static function update_activity_completion_status_manually($cmid, $completed) {
// Validate and normalize parameters.
$params = self::validate_parameters(self::update_activity_completion_status_manually_parameters(),
array('cmid' => $cmid, 'completed' => $completed));
$cmid = $params['cmid'];
$completed = $params['completed'];
$warnings = array();
$context = context_module::instance($cmid);
self::validate_context($context);
require_capability('moodle/course:togglecompletion', $context);
list($course, $cm) = get_course_and_cm_from_cmid($cmid);
// Set up completion object and check it is enabled.
$completion = new completion_info($course);
if (!$completion->is_enabled()) {
throw new moodle_exception('completionnotenabled', 'completion');
}
// Check completion state is manual.
if ($cm->completion != COMPLETION_TRACKING_MANUAL) {
throw new moodle_exception('cannotmanualctrack', 'error');
}
$targetstate = ($completed) ? COMPLETION_COMPLETE : COMPLETION_INCOMPLETE;
$completion->update_state($cm, $targetstate);
$result = array();
$result['status'] = true;
$result['warnings'] = $warnings;
return $result;
}
/**
* Describes the update_activity_completion_status_manually return value.
*
* @return external_single_structure
* @since Moodle 2.9
*/
public static function update_activity_completion_status_manually_returns() {
return new external_single_structure(
array(
'status' => new external_value(PARAM_BOOL, 'status, true if success'),
'warnings' => new external_warnings(),
)
);
}
/**
* Describes the parameters for override_activity_completion_status.
*
* @return external_external_function_parameters
* @since Moodle 3.4
*/
public static function override_activity_completion_status_parameters() {
return new external_function_parameters (
array(
'userid' => new external_value(PARAM_INT, 'user id'),
'cmid' => new external_value(PARAM_INT, 'course module id'),
'newstate' => new external_value(PARAM_INT, 'the new activity completion state'),
)
);
}
/**
* Update completion status for a user in an activity.
* @param int $userid User id
* @param int $cmid Course module id
* @param int $newstate Activity completion
* @return array Array containing the current (updated) completion status.
* @since Moodle 3.4
* @throws moodle_exception
*/
public static function override_activity_completion_status($userid, $cmid, $newstate) {
// Validate and normalize parameters.
$params = self::validate_parameters(self::override_activity_completion_status_parameters(),
array('userid' => $userid, 'cmid' => $cmid, 'newstate' => $newstate));
$userid = $params['userid'];
$cmid = $params['cmid'];
$newstate = $params['newstate'];
$context = context_module::instance($cmid);
self::validate_context($context);
list($course, $cm) = get_course_and_cm_from_cmid($cmid);
// Set up completion object and check it is enabled.
$completion = new completion_info($course);
if (!$completion->is_enabled()) {
throw new moodle_exception('completionnotenabled', 'completion');
}
// Update completion state and get the new state back.
$completion->update_state($cm, $newstate, $userid, true);
$completiondata = $completion->get_data($cm, false, $userid);
// Return the current state of completion.
return [
'cmid' => $completiondata->coursemoduleid,
'userid' => $completiondata->userid,
'state' => $completiondata->completionstate,
'timecompleted' => $completiondata->timemodified,
'overrideby' => $completiondata->overrideby,
'tracking' => $completion->is_enabled($cm)
];
}
/**
* Describes the override_activity_completion_status return value.
*
* @return external_single_structure
* @since Moodle 3.4
*/
public static function override_activity_completion_status_returns() {
return new external_single_structure(
array(
'cmid' => new external_value(PARAM_INT, 'The course module id'),
'userid' => new external_value(PARAM_INT, 'The user id to which the completion info belongs'),
'state' => new external_value(PARAM_INT, 'The current completion state.'),
'timecompleted' => new external_value(PARAM_INT, 'time of completion'),
'overrideby' => new external_value(PARAM_INT, 'The user id who has overriden the status, or null'),
'tracking' => new external_value(PARAM_INT, 'type of tracking:
0 means none, 1 manual, 2 automatic'),
)
);
}
/**
* Returns description of method parameters
*
* @return external_function_parameters
* @since Moodle 2.9
*/
public static function get_activities_completion_status_parameters() {
return new external_function_parameters(
array(
'courseid' => new external_value(PARAM_INT, 'Course ID'),
'userid' => new external_value(PARAM_INT, 'User ID'),
)
);
}
/**
* Get Activities completion status
*
* @param int $courseid ID of the Course
* @param int $userid ID of the User
* @return array of activities progress and warnings
* @throws moodle_exception
* @since Moodle 2.9
* @throws moodle_exception
*/
public static function get_activities_completion_status($courseid, $userid) {
global $CFG, $USER, $PAGE;
require_once($CFG->libdir . '/grouplib.php');
$warnings = array();
$arrayparams = array(
'courseid' => $courseid,
'userid' => $userid,
);
$params = self::validate_parameters(self::get_activities_completion_status_parameters(), $arrayparams);
$course = get_course($params['courseid']);
$user = core_user::get_user($params['userid'], '*', MUST_EXIST);
core_user::require_active_user($user);
$context = context_course::instance($course->id);
self::validate_context($context);
// Check that current user have permissions to see this user's activities.
if ($user->id != $USER->id) {
require_capability('report/progress:view', $context);
if (!groups_user_groups_visible($course, $user->id)) {
// We are not in the same group!
throw new moodle_exception('accessdenied', 'admin');
}
}
$completion = new completion_info($course);
$activities = $completion->get_activities();
$results = array();
foreach ($activities as $activity) {
// Check if current user has visibility on this activity.
if (!$activity->uservisible) {
continue;
}
// Get progress information and state (we must use get_data because it works for all user roles in course).
$exporter = new \core_completion\external\completion_info_exporter(
$course,
$activity,
$userid,
);
$renderer = $PAGE->get_renderer('core');
$data = (array)$exporter->export($renderer);
$results[] = array_merge([
'cmid' => $activity->id,
'modname' => $activity->modname,
'instance' => $activity->instance,
'tracking' => $activity->completion,
], $data);
}
$results = array(
'statuses' => $results,
'warnings' => $warnings
);
return $results;
}
/**
* Returns description of method result value
*
* @return \core_external\external_description
* @since Moodle 2.9
*/
public static function get_activities_completion_status_returns() {
return new external_single_structure(
array(
'statuses' => new external_multiple_structure(
new external_single_structure(
[
'cmid' => new external_value(PARAM_INT, 'course module ID'),
'modname' => new external_value(PARAM_PLUGIN, 'activity module name'),
'instance' => new external_value(PARAM_INT, 'instance ID'),
'state' => new external_value(PARAM_INT,
"Completion state value:
0 means incomplete,
1 complete,
2 complete pass,
3 complete fail"
),
'timecompleted' => new external_value(PARAM_INT,
'timestamp for completed activity'),
'tracking' => new external_value(PARAM_INT,
"type of tracking:
0 means none,
1 manual,
2 automatic"
),
'overrideby' => new external_value(PARAM_INT,
'The user id who has overriden the status, or null', VALUE_OPTIONAL),
'valueused' => new external_value(PARAM_BOOL,
'Whether the completion status affects the availability of another activity.',
VALUE_OPTIONAL),
'hascompletion' => new external_value(PARAM_BOOL,
'Whether this activity module has completion enabled',
VALUE_OPTIONAL),
'isautomatic' => new external_value(PARAM_BOOL,
'Whether this activity module instance tracks completion automatically.',
VALUE_OPTIONAL),
'istrackeduser' => new external_value(PARAM_BOOL,
'Whether completion is being tracked for this user.',
VALUE_OPTIONAL),
'uservisible' => new external_value(PARAM_BOOL,
'Whether this activity is visible to the user.',
VALUE_OPTIONAL),
'details' => new external_multiple_structure(
new external_single_structure(
[
'rulename' => new external_value(PARAM_TEXT, 'Rule name'),
'rulevalue' => new external_single_structure(
[
'status' => new external_value(PARAM_INT, 'Completion status'),
'description' => new external_value(PARAM_TEXT, 'Completion description'),
]
)
]
),
'Completion status details',
VALUE_DEFAULT,
[]
),
'isoverallcomplete' => new external_value(PARAM_BOOL,
'Whether the overall completion state of this course module should be marked as complete or not.',
VALUE_OPTIONAL),
], 'Activity'
), 'List of activities status'
),
'warnings' => new external_warnings()
)
);
}
/**
* Returns description of method parameters
*
* @return external_function_parameters
* @since Moodle 2.9
*/
public static function get_course_completion_status_parameters() {
return new external_function_parameters(
array(
'courseid' => new external_value(PARAM_INT, 'Course ID'),
'userid' => new external_value(PARAM_INT, 'User ID'),
)
);
}
/**
* Get Course completion status
*
* @param int $courseid ID of the Course
* @param int $userid ID of the User
* @return array of course completion status and warnings
* @since Moodle 2.9
* @throws moodle_exception
*/
public static function get_course_completion_status($courseid, $userid) {
global $CFG, $USER;
require_once($CFG->libdir . '/grouplib.php');
$warnings = array();
$arrayparams = array(
'courseid' => $courseid,
'userid' => $userid,
);
$params = self::validate_parameters(self::get_course_completion_status_parameters(), $arrayparams);
$course = get_course($params['courseid']);
$user = core_user::get_user($params['userid'], '*', MUST_EXIST);
core_user::require_active_user($user);
$context = context_course::instance($course->id);
self::validate_context($context);
// Can current user see user's course completion status?
// This check verifies if completion is enabled because $course is mandatory.
if (!completion_can_view_data($user->id, $course)) {
throw new moodle_exception('cannotviewreport');
}
// The previous function doesn't check groups.
if ($user->id != $USER->id) {
if (!groups_user_groups_visible($course, $user->id)) {
// We are not in the same group!
throw new moodle_exception('accessdenied', 'admin');
}
}
$info = new completion_info($course);
// Check this user is enroled.
if (!$info->is_tracked_user($user->id)) {
if ($USER->id == $user->id) {
throw new moodle_exception('notenroled', 'completion');
} else {
throw new moodle_exception('usernotenroled', 'completion');
}
}
$completions = $info->get_completions($user->id);
if (empty($completions)) {
throw new moodle_exception('nocriteriaset', 'completion');
}
// Load course completion.
$completionparams = array(
'userid' => $user->id,
'course' => $course->id,
);
$ccompletion = new completion_completion($completionparams);
$completionrows = array();
// Loop through course criteria.
foreach ($completions as $completion) {
$criteria = $completion->get_criteria();
$completionrow = array();
$completionrow['type'] = $criteria->criteriatype;
$completionrow['title'] = $criteria->get_title();
$completionrow['status'] = $completion->get_status();
$completionrow['complete'] = $completion->is_complete();
$completionrow['timecompleted'] = $completion->timecompleted;
$completionrow['details'] = $criteria->get_details($completion);
$completionrows[] = $completionrow;
}
$result = array(
'completed' => $info->is_course_complete($user->id),
'aggregation' => $info->get_aggregation_method(),
'completions' => $completionrows
);
$results = array(
'completionstatus' => $result,
'warnings' => $warnings
);
return $results;
}
/**
* Returns description of method result value
*
* @return \core_external\external_description
* @since Moodle 2.9
*/
public static function get_course_completion_status_returns() {
return new external_single_structure(
array(
'completionstatus' => new external_single_structure(
array(
'completed' => new external_value(PARAM_BOOL, 'true if the course is complete, false otherwise'),
'aggregation' => new external_value(PARAM_INT, 'aggregation method 1 means all, 2 means any'),
'completions' => new external_multiple_structure(
new external_single_structure(
array(
'type' => new external_value(PARAM_INT, 'Completion criteria type'),
'title' => new external_value(PARAM_TEXT, 'Completion criteria Title'),
'status' => new external_value(PARAM_NOTAGS, 'Completion status (Yes/No) a % or number'),
'complete' => new external_value(PARAM_BOOL, 'Completion status (true/false)'),
'timecompleted' => new external_value(PARAM_INT, 'Timestamp for criteria completetion'),
'details' => new external_single_structure(
array(
'type' => new external_value(PARAM_TEXT, 'Type description'),
'criteria' => new external_value(PARAM_RAW, 'Criteria description'),
'requirement' => new external_value(PARAM_TEXT, 'Requirement description'),
'status' => new external_value(PARAM_RAW, 'Status description, can be anything'),
), 'details'),
), 'Completions'
), ''
)
), 'Course status'
),
'warnings' => new external_warnings()
), 'Course completion status'
);
}
/**
* Describes the parameters for mark_course_self_completed.
*
* @return external_function_parameters
* @since Moodle 3.0
*/
public static function mark_course_self_completed_parameters() {
return new external_function_parameters (
array(
'courseid' => new external_value(PARAM_INT, 'Course ID')
)
);
}
/**
* Update the course completion status for the current user (if course self-completion is enabled).
*
* @param int $courseid Course id
* @return array Result and possible warnings
* @since Moodle 3.0
* @throws moodle_exception
*/
public static function mark_course_self_completed($courseid) {
global $USER;
$warnings = array();
$params = self::validate_parameters(self::mark_course_self_completed_parameters(),
array('courseid' => $courseid));
$course = get_course($params['courseid']);
$context = context_course::instance($course->id);
self::validate_context($context);
// Set up completion object and check it is enabled.
$completion = new completion_info($course);
if (!$completion->is_enabled()) {
throw new moodle_exception('completionnotenabled', 'completion');
}
if (!$completion->is_tracked_user($USER->id)) {
throw new moodle_exception('nottracked', 'completion');
}
$completion = $completion->get_completion($USER->id, COMPLETION_CRITERIA_TYPE_SELF);
// Self completion criteria not enabled.
if (!$completion) {
throw new moodle_exception('noselfcompletioncriteria', 'completion');
}
// Check if the user has already marked himself as complete.
if ($completion->is_complete()) {
throw new moodle_exception('useralreadymarkedcomplete', 'completion');
}
// Mark the course complete.
$completion->mark_complete();
$result = array();
$result['status'] = true;
$result['warnings'] = $warnings;
return $result;
}
/**
* Describes the mark_course_self_completed return value.
*
* @return external_single_structure
* @since Moodle 3.0
*/
public static function mark_course_self_completed_returns() {
return new external_single_structure(
array(
'status' => new external_value(PARAM_BOOL, 'status, true if success'),
'warnings' => new external_warnings(),
)
);
}
}
+158
View File
@@ -0,0 +1,158 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
declare(strict_types=1);
namespace core_completion\external;
defined('MOODLE_INTERNAL') || die();
use renderer_base;
/**
* Completion info exporter
*
* @package core_completion
* @copyright 2021 Dongsheng Cai
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class completion_info_exporter extends \core\external\exporter {
/**
* @var object $course moodle course object
*/
private $course;
/**
* @var object|cm_info $cm course module info
*/
private $cminfo;
/**
* @var int $userid user id
*/
private $userid;
/**
* Constructor for the completion info exporter.
*
* @param object $course course object
* @param object|cm_info $cm course module info
* @param int $userid user id
* @param array $related related values
*/
public function __construct(object $course, object $cm, int $userid, array $related = []) {
$this->course = $course;
$this->cminfo = \cm_info::create($cm);
$this->userid = $userid;
parent::__construct([], $related);
}
/**
* Get the additional values to inject while exporting.
*
* @param renderer_base $output The renderer.
* @return array Keys are the property names, values are their values.
*/
protected function get_other_values(renderer_base $output): array {
$cmcompletion = \core_completion\cm_completion_details::get_instance($this->cminfo, $this->userid);
$cmcompletiondetails = $cmcompletion->get_details();
$details = [];
foreach ($cmcompletiondetails as $rulename => $rulevalue) {
$details[] = [
'rulename' => $rulename,
'rulevalue' => (array)$rulevalue,
];
}
return [
'state' => $cmcompletion->get_overall_completion(),
'timecompleted' => $cmcompletion->get_timemodified(),
'overrideby' => $cmcompletion->overridden_by(),
'valueused' => \core_availability\info::completion_value_used($this->course, $this->cminfo->id),
'hascompletion' => $cmcompletion->has_completion(),
'isautomatic' => $cmcompletion->is_automatic(),
'istrackeduser' => $cmcompletion->is_tracked_user(),
'uservisible' => $this->cminfo->uservisible,
'details' => $details,
'isoverallcomplete' => $cmcompletion->is_overall_complete(),
];
}
/**
* Return the list of additional properties used only for display.
*
* @return array Keys are the property names, and value their definition.
*/
public static function define_other_properties(): array {
return [
'state' => [
'type' => PARAM_INT,
'description' => 'overall completion state of this course module.',
],
'timecompleted' => [
'type' => PARAM_INT,
'description' => 'course completion timestamp.',
],
'overrideby' => [
'type' => PARAM_INT,
'description' => 'user ID that has overridden the completion state of this activity for the user.',
'null' => NULL_ALLOWED,
],
'valueused' => [
'type' => PARAM_BOOL,
'description' => 'True if module is used in a condition, false otherwise.',
],
'hascompletion' => [
'type' => PARAM_BOOL,
'description' => 'Whether this activity module has completion enabled.'
],
'isautomatic' => [
'type' => PARAM_BOOL,
'description' => 'Whether this activity module instance tracks completion automatically.'
],
'istrackeduser' => [
'type' => PARAM_BOOL,
'description' => 'Checks whether completion is being tracked for this user.'
],
'uservisible' => [
'type' => PARAM_BOOL,
'description' => 'Whether this activity is visible to user.'
],
'details' => [
'multiple' => true,
'description' => 'An array of completion details containing the description and status.',
'type' => [
'rulename' => [
'type' => PARAM_TEXT,
],
'rulevalue' => [
'type' => [
'status' => [
'type' => PARAM_INT,
],
'description' => [
'type' => PARAM_TEXT,
]
]
]
]
],
'isoverallcomplete' => [
'type' => PARAM_BOOL,
'description' => 'Whether the overall completion state of this course module should be marked as complete or not.',
'optional' => true,
],
];
}
}
+513
View File
@@ -0,0 +1,513 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core_completion\form;
use core_grades\component_gradeitems;
use cm_info;
/**
* Completion trait helper, with methods to add completion elements and validate them.
*
* @package core_completion
* @since Moodle 4.3
* @copyright 2023 Sara Arjona (sara@moodle.com)
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
trait form_trait {
/** @var string The suffix to be added to the completion elements when creating them (for example, 'completion_assign'). */
protected $suffix = '';
/**
* Called during validation.
* Override this method to indicate, based on the data, whether a custom completion rule is selected or not.
*
* @param array $data Input data (not yet validated)
* @return bool True if one or more rules are enabled; false if none are.
*/
abstract protected function completion_rule_enabled($data);
/**
* Add completion elements to the form and return the list of element ids.
*
* @return array Array of string IDs of added items, empty array if none
*/
abstract protected function add_completion_rules();
/**
* Get the form associated to this class, where the completion elements will be added.
* This method must be overriden by the class using this trait if it doesn't include a _form property.
*
* @return \MoodleQuickForm
* @throws \coding_exception If the class does not have a _form property.
*/
protected function get_form(): \MoodleQuickForm {
if (property_exists($this, '_form')) {
return $this->_form;
}
throw new \coding_exception('This class does not have a _form property. Please, add it or override the get_form() method.');
}
/**
* Set the suffix to be added to the completion elements when creating them (for example, 'completion_assign').
*
* @param string $suffix
*/
public function set_suffix(string $suffix): void {
$this->suffix = $suffix;
}
/**
* Get the suffix to be added to the completion elements when creating them (for example, 'completion_assign').
*
* @return string The suffix
*/
public function get_suffix(): string {
return $this->suffix;
}
/**
* Add completion elements to the form.
*
* @param string|null $modname The module name (for example, 'assign'). If null and form is moodleform_mod, the parameters are
* overriden with the expected values from the form.
* @param bool $supportviews True if the module supports views and false otherwise.
* @param bool $supportgrades True if the module supports grades and false otherwise.
* @param bool $rating True if the rating feature is enabled and false otherwise.
* @param int|null $courseid Course where to add completion elements.
* @throws \coding_exception If the form is not moodleform_mod and $modname is null.
*/
protected function add_completion_elements(
string $modname = null,
bool $supportviews = false,
bool $supportgrades = false,
bool $rating = false,
?int $courseid = null
): void {
global $SITE;
$mform = $this->get_form();
if ($modname === null) {
if ($this instanceof \moodleform_mod) {
// By default, all the modules can be initiatized with the same parameters.
$modname = $this->_modname;
$supportviews = plugin_supports('mod', $modname, FEATURE_COMPLETION_TRACKS_VIEWS, false);
$supportgrades = plugin_supports('mod', $modname, FEATURE_GRADE_HAS_GRADE, false);
$rating = $this->_features->rating;
} else {
throw new \coding_exception('You must specify the modname parameter if you are not using a moodleform_mod.');
}
}
// Unlock button if people have completed it. The button will be removed later in definition_after_data if they haven't.
// The unlock buttons don't need suffix because they are only displayed in the module settings page.
$mform->addElement('submit', 'unlockcompletion', get_string('unlockcompletion', 'completion'));
$mform->registerNoSubmitButton('unlockcompletion');
$mform->addElement('hidden', 'completionunlocked', 0);
$mform->setType('completionunlocked', PARAM_INT);
$trackingdefault = COMPLETION_TRACKING_NONE;
// Get the sufix to add to the completion elements name.
$suffix = $this->get_suffix();
$completionel = 'completion' . $suffix;
$mform->addElement(
'radio',
$completionel,
'',
get_string('completion_none', 'completion'),
COMPLETION_TRACKING_NONE,
['class' => 'left-indented']
);
$mform->addElement(
'radio',
$completionel,
'',
get_string('completion_manual', 'completion'),
COMPLETION_TRACKING_MANUAL,
['class' => 'left-indented']
);
$allconditionsel = 'allconditions' . $suffix;
$allconditions = $mform->createElement(
'static',
$allconditionsel,
'',
get_string('allconditions', 'completion'));
$conditionsgroupel = 'conditionsgroup' . $suffix;
$mform->addGroup([$allconditions], $conditionsgroupel, '', null, false);
$mform->hideIf($conditionsgroupel, $completionel, 'ne', COMPLETION_TRACKING_AUTOMATIC);
$mform->setType($completionel, PARAM_INT);
$mform->setDefault($completionel, COMPLETION_TRACKING_NONE);
// Automatic completion once you view it.
if ($supportviews) {
$completionviewel = 'completionview' . $suffix;
$mform->addElement(
'checkbox',
$completionviewel,
'',
get_string('completionview_desc', 'completion')
);
$mform->hideIf($completionviewel, $completionel, 'ne', COMPLETION_TRACKING_AUTOMATIC);
// Check by default if automatic completion tracking is set.
if ($trackingdefault == COMPLETION_TRACKING_AUTOMATIC) {
$mform->setDefault($completionviewel, 1);
}
}
// Automatic completion according to module-specific rules.
$customcompletionelements = $this->add_completion_rules();
if (property_exists($this, '_customcompletionelements')) {
$this->_customcompletionelements = $customcompletionelements;
}
if ($customcompletionelements !== null) {
foreach ($customcompletionelements as $element) {
$mform->hideIf($element, $completionel, 'ne', COMPLETION_TRACKING_AUTOMATIC);
}
}
// If the activity supports grading, the grade elements must be added.
if ($supportgrades) {
$this->add_completiongrade_elements($modname, $rating);
}
$autocompletionpossible = $supportviews || $supportgrades || (count($customcompletionelements) > 0);
// Automatic option only appears if possible.
if ($autocompletionpossible) {
$automatic = $mform->createElement(
'radio',
$completionel,
'',
get_string('completion_automatic', 'completion'),
COMPLETION_TRACKING_AUTOMATIC,
['class' => 'left-indented']
);
$mform->insertElementBefore($automatic, $conditionsgroupel);
}
// Completion expected at particular date? (For progress tracking).
// We don't show completion expected at site level default completion.
if ($courseid != $SITE->id) {
$completionexpectedel = 'completionexpected' . $suffix;
$mform->addElement('date_time_selector', $completionexpectedel, get_string('completionexpected', 'completion'),
['optional' => true]);
$a = get_string('pluginname', $modname);
$mform->addHelpButton($completionexpectedel, 'completionexpected', 'completion', '', false, $a);
$mform->hideIf($completionexpectedel, $completionel, 'eq', COMPLETION_TRACKING_NONE);
}
}
/**
* Add completion grade elements to the form.
*
* @param string $modname The name of the module (for example, 'assign').
* @param bool $rating True if the rating feature is enabled and false otherwise.
*/
protected function add_completiongrade_elements(
string $modname,
bool $rating = false
): void {
$mform = $this->get_form();
// Get the sufix to add to the completion elements name.
$suffix = $this->get_suffix();
$completionel = 'completion' . $suffix;
$completionelementexists = $mform->elementExists($completionel);
$component = "mod_{$modname}";
$itemnames = component_gradeitems::get_itemname_mapping_for_component($component);
$indentation = ['parentclass' => 'ml-2'];
$receiveagradeel = 'receiveagrade' . $suffix;
$completionusegradeel = 'completionusegrade' . $suffix;
$completionpassgradeel = 'completionpassgrade' . $suffix;
if (count($itemnames) === 1) {
// Only one gradeitem in this activity.
// We use the completionusegrade field here.
$mform->addElement(
'checkbox',
$completionusegradeel,
'',
get_string('completionusegrade_desc', 'completion')
);
// Complete if the user has reached any grade.
$mform->addElement(
'radio',
$completionpassgradeel,
null,
get_string('completionanygrade_desc', 'completion'),
0,
$indentation
);
// Complete if the user has reached the pass grade.
$mform->addElement(
'radio',
$completionpassgradeel,
null,
get_string('completionpassgrade_desc', 'completion'),
1,
$indentation
);
$mform->hideIf($completionpassgradeel, $completionusegradeel, 'notchecked');
if ($completionelementexists) {
$mform->hideIf($completionpassgradeel, $completionel, 'ne', COMPLETION_TRACKING_AUTOMATIC);
$mform->hideIf($completionusegradeel, $completionel, 'ne', COMPLETION_TRACKING_AUTOMATIC);
}
// The disabledIf logic differs between ratings and other grade items due to different field types.
if ($rating) {
// If using the rating system, there is no grade unless ratings are enabled.
$mform->hideIf($completionusegradeel, 'assessed', 'eq', 0);
$mform->hideIf($completionusegradeel, 'assessed', 'eq', 0);
} else {
// All other field types use the '$gradefieldname' field's modgrade_type.
$itemnumbers = array_keys($itemnames);
$itemnumber = array_shift($itemnumbers);
$gradefieldname = component_gradeitems::get_field_name_for_itemnumber($component, $itemnumber, 'grade');
$mform->hideIf($completionusegradeel, "{$gradefieldname}[modgrade_type]", 'eq', 'none');
$mform->hideIf($completionusegradeel, "{$gradefieldname}[modgrade_type]", 'eq', 'none');
}
} else if (count($itemnames) > 1) {
// There are multiple grade items in this activity.
// Show them all.
$options = [];
foreach ($itemnames as $itemnumber => $itemname) {
$options[$itemnumber] = get_string("grade_{$itemname}_name", $component);
}
$group = [$mform->createElement(
'checkbox',
$completionusegradeel,
null,
get_string('completionusegrade_desc', 'completion')
)];
$completiongradeitemnumberel = 'completiongradeitemnumber' . $suffix;
$group[] =& $mform->createElement(
'select',
$completiongradeitemnumberel,
'',
$options
);
$receiveagradegroupel = 'receiveagradegroup' . $suffix;
$mform->addGroup($group, $receiveagradegroupel, '', [' '], false);
if ($completionelementexists) {
$mform->hideIf($completionusegradeel, $completionel, 'ne', COMPLETION_TRACKING_AUTOMATIC);
$mform->hideIf($receiveagradegroupel, $completionel, 'ne', COMPLETION_TRACKING_AUTOMATIC);
}
$mform->hideIf($completiongradeitemnumberel, $completionusegradeel, 'notchecked');
// Complete if the user has reached any grade.
$mform->addElement(
'radio',
$completionpassgradeel,
null,
get_string('completionanygrade_desc', 'completion'),
0,
$indentation
);
// Complete if the user has reached the pass grade.
$mform->addElement(
'radio',
$completionpassgradeel,
null,
get_string('completionpassgrade_desc', 'completion'),
1,
$indentation
);
$mform->hideIf($completionpassgradeel, $completionusegradeel, 'notchecked');
if ($completionelementexists) {
$mform->hideIf($completiongradeitemnumberel, $completionel, 'ne', COMPLETION_TRACKING_AUTOMATIC);
$mform->hideIf($completionpassgradeel, $completionel, 'ne', COMPLETION_TRACKING_AUTOMATIC);
}
}
$customgradingelements = $this->add_completiongrade_rules();
if (property_exists($this, '_customcompletionelements')) {
$this->_customcompletionelements = array_merge($this->_customcompletionelements, $customgradingelements);
}
if ($completionelementexists) {
foreach ($customgradingelements as $customgradingelement) {
$mform->hideIf($customgradingelement, $completionel, 'ne', COMPLETION_TRACKING_AUTOMATIC);
}
}
}
/**
* Add completion grading elements to the form and return the list of element ids.
*
* @return array Array of string IDs of added items, empty array if none
*/
abstract public function add_completiongrade_rules(): array;
/**
* Perform some extra validation for completion settings.
*
* @param array $data Array of ["fieldname" => value] of submitted data.
* @return array List of ["element_name" => "error_description"] if there are errors or an empty array if everything is OK.
*/
protected function validate_completion(array $data): array {
$errors = [];
// Get the sufix to add to the completion elements name.
$suffix = $this->get_suffix();
$completionel = 'completion' . $suffix;
// Completion: Don't let them choose automatic completion without turning on some conditions.
$automaticcompletion = array_key_exists($completionel, $data) && $data[$completionel] == COMPLETION_TRACKING_AUTOMATIC;
// Ignore this check when completion settings are locked, as the options are then disabled.
// The unlock buttons don't need suffix because they are only displayed in the module settings page.
$automaticcompletion = $automaticcompletion && !empty($data['completionunlocked']);
if ($automaticcompletion) {
// View to complete.
$completionviewel = 'completionview' . $suffix;
$rulesenabled = !empty($data[$completionviewel]);
// Use grade to complete (only one grade item).
$completionusegradeel = 'completionusegrade' . $suffix;
$completionpassgradeel = 'completionpassgrade' . $suffix;
$rulesenabled = $rulesenabled || !empty($data[$completionusegradeel]) || !empty($data[$completionpassgradeel]);
// Use grade to complete (specific grade item).
$completiongradeitemnumberel = 'completiongradeitemnumber' . $suffix;
if (!$rulesenabled && isset($data[$completiongradeitemnumberel])) {
$rulesenabled = $data[$completiongradeitemnumberel] != '';
}
// Module-specific completion rules.
$rulesenabled = $rulesenabled || $this->completion_rule_enabled($data);
if (!$rulesenabled) {
// No rules are enabled. Can't set automatically completed without rules.
$errors[$completionel] = get_string('badautocompletion', 'completion');
}
}
return $errors;
}
/**
* It should be called from the definition_after_data() to setup the completion settings in the form.
*
* @param cm_info|null $cm The course module associated to this form.
*/
protected function definition_after_data_completion(?cm_info $cm = null): void {
global $COURSE, $SITE;
$mform = $this->get_form();
$completion = new \completion_info($COURSE);
// We use $SITE course for site default activity completion,
// so users could set default values regardless of whether completion is enabled or not.".
if ($completion->is_enabled() || $COURSE->id == $SITE->id) {
$suffix = $this->get_suffix();
// If anybody has completed the activity, these options will be 'locked'.
// We use $SITE course for site default activity completion, so we don't need any unlock button.
$completedcount = (empty($cm) || $COURSE->id == $SITE->id) ? 0 : $completion->count_user_data($cm);
$freeze = false;
if (!$completedcount) {
// The unlock buttons don't need suffix because they are only displayed in the module settings page.
if ($mform->elementExists('unlockcompletion')) {
$mform->removeElement('unlockcompletion');
}
// Automatically set to unlocked. Note: this is necessary in order to make it recalculate completion once
// the option is changed, maybe someone has completed it now.
if ($mform->elementExists('completionunlocked')) {
$mform->getElement('completionunlocked')->setValue(1);
}
} else {
// Has the element been unlocked, either by the button being pressed in this request, or the field already
// being set from a previous one?
if ($mform->exportValue('unlockcompletion') || $mform->exportValue('completionunlocked')) {
// Yes, add in warning text and set the hidden variable.
$completedunlockedel = $mform->createElement(
'static',
'completedunlocked',
get_string('completedunlocked', 'completion'),
get_string('completedunlockedtext', 'completion')
);
$mform->insertElementBefore($completedunlockedel, 'unlockcompletion');
$mform->removeElement('unlockcompletion');
$mform->getElement('completionunlocked')->setValue(1);
} else {
// No, add in the warning text with the count (now we know it) before the unlock button.
$completedwarningel = $mform->createElement(
'static',
'completedwarning',
get_string('completedwarning', 'completion'),
get_string('completedwarningtext', 'completion', $completedcount)
);
$mform->insertElementBefore($completedwarningel, 'unlockcompletion');
$freeze = true;
}
}
if ($freeze) {
$completionel = 'completion' . $suffix;
$mform->freeze($completionel);
$completionviewel = 'completionview' . $suffix;
if ($mform->elementExists($completionviewel)) {
// Don't use hardFreeze or checkbox value gets lost.
$mform->freeze($completionviewel);
}
$completionusegradeel = 'completionusegrade' . $suffix;
if ($mform->elementExists($completionusegradeel)) {
$mform->freeze($completionusegradeel);
}
$completionpassgradeel = 'completionpassgrade' . $suffix;
if ($mform->elementExists($completionpassgradeel)) {
$mform->freeze($completionpassgradeel);
// Has the completion pass grade completion criteria been set? If it has, then we shouldn't change
// any of the modules "gradepass" type fields.
if ($mform->exportValue($completionpassgradeel)) {
// Some modules define separate "gradepass" fields for each of their grade items.
$gradepassfieldels = array_merge(['gradepass'], array_map(
fn(string $gradeitem) => "{$gradeitem}gradepass",
component_gradeitems::get_itemname_mapping_for_component("mod_{$this->_modname}"),
));
foreach ($gradepassfieldels as $gradepassfieldel) {
if ($mform->elementExists($gradepassfieldel)) {
$mform->freeze($gradepassfieldel);
}
}
}
}
$completiongradeitemnumberel = 'completiongradeitemnumber' . $suffix;
if ($mform->elementExists($completiongradeitemnumberel)) {
$mform->freeze($completiongradeitemnumberel);
}
if (property_exists($this, '_customcompletionelements')) {
$mform->freeze($this->_customcompletionelements);
}
}
}
}
}
+652
View File
@@ -0,0 +1,652 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Bulk activity completion manager class
*
* @package core_completion
* @category completion
* @copyright 2017 Adrian Greeve
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_completion;
use core\context;
use stdClass;
use context_course;
use cm_info;
use tabobject;
use lang_string;
use moodle_url;
defined('MOODLE_INTERNAL') || die;
/**
* Bulk activity completion manager class
*
* @package core_completion
* @category completion
* @copyright 2017 Adrian Greeve
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class manager {
/**
* @var int $courseid the course id.
*/
protected $courseid;
/**
* manager constructor.
* @param int $courseid the course id.
*/
public function __construct($courseid) {
$this->courseid = $courseid;
}
/**
* Returns current course context or system level for $SITE courseid.
*
* @return context The course based on current courseid or system context.
*/
protected function get_context(): context {
global $SITE;
if ($this->courseid && $this->courseid != $SITE->id) {
return context_course::instance($this->courseid);
}
return \context_system::instance();
}
/**
* Gets the data (context) to be used with the bulkactivitycompletion template.
*
* @return stdClass data for use with the bulkactivitycompletion template.
*/
public function get_activities_and_headings() {
global $OUTPUT;
$moduleinfo = get_fast_modinfo($this->courseid);
$sections = $moduleinfo->get_sections();
$data = new stdClass;
$data->courseid = $this->courseid;
$data->sesskey = sesskey();
$data->helpicon = $OUTPUT->help_icon('bulkcompletiontracking', 'core_completion');
$data->sections = [];
foreach ($sections as $sectionnumber => $section) {
$sectioninfo = $moduleinfo->get_section_info($sectionnumber);
$sectionobject = new stdClass();
$sectionobject->sectionnumber = $sectionnumber;
$sectionobject->name = get_section_name($this->courseid, $sectioninfo);
$sectionobject->activities = $this->get_activities($section, true);
$data->sections[] = $sectionobject;
}
return $data;
}
/**
* Gets the data (context) to be used with the activityinstance template
*
* @param array $cmids list of course module ids
* @param bool $withcompletiondetails include completion details
* @return array
*/
public function get_activities($cmids, $withcompletiondetails = false) {
$moduleinfo = get_fast_modinfo($this->courseid);
$activities = [];
foreach ($cmids as $cmid) {
$mod = $moduleinfo->get_cm($cmid);
if (!$mod->uservisible) {
continue;
}
$moduleobject = new stdClass();
$moduleobject->cmid = $cmid;
$moduleobject->modname = $mod->get_formatted_name();
$moduleobject->icon = $mod->get_icon_url()->out();
$moduleobject->url = $mod->url;
$moduleobject->canmanage = $withcompletiondetails && self::can_edit_bulk_completion($this->courseid, $mod);
// Get activity completion information.
if ($moduleobject->canmanage) {
$moduleobject->completionstatus = $this->get_completion_detail($mod);
} else {
$moduleobject->completionstatus = ['icon' => null, 'string' => null];
}
if (self::can_edit_bulk_completion($this->courseid, $mod)) {
$activities[] = $moduleobject;
}
}
return $activities;
}
/**
* Get completion information on the selected module or module type
*
* @param cm_info|stdClass $mod either instance of cm_info (with 'customcompletionrules' in customdata) or
* object with fields ->completion, ->completionview, ->completionexpected, ->completionusegrade
* and ->customdata['customcompletionrules']
* @return array
*/
private function get_completion_detail($mod) {
global $OUTPUT;
$strings = [];
switch ($mod->completion) {
case COMPLETION_TRACKING_NONE:
$strings['string'] = get_string('none');
break;
case COMPLETION_TRACKING_MANUAL:
$strings['string'] = get_string('manual', 'completion');
$strings['icon'] = $OUTPUT->pix_icon('i/completion-manual-y', get_string('completion_manual', 'completion'));
break;
case COMPLETION_TRACKING_AUTOMATIC:
$strings['string'] = get_string('withconditions', 'completion');
$strings['icon'] = $OUTPUT->pix_icon('i/completion-auto-y', get_string('completion_automatic', 'completion'));
break;
default:
$strings['string'] = get_string('none');
break;
}
// Get the descriptions for all the active completion rules for the module.
if ($ruledescriptions = $this->get_completion_active_rule_descriptions($mod)) {
foreach ($ruledescriptions as $ruledescription) {
$strings['string'] .= \html_writer::empty_tag('br') . $ruledescription;
}
}
return $strings;
}
/**
* Get the descriptions for all active conditional completion rules for the current module.
*
* @param cm_info|stdClass $moduledata either instance of cm_info (with 'customcompletionrules' in customdata) or
* object with fields ->completion, ->completionview, ->completionexpected, ->completionusegrade
* and ->customdata['customcompletionrules']
* @return array $activeruledescriptions an array of strings describing the active completion rules.
*/
protected function get_completion_active_rule_descriptions($moduledata) {
$activeruledescriptions = [];
if ($moduledata->completion == COMPLETION_TRACKING_AUTOMATIC) {
// Generate the description strings for the core conditional completion rules (if set).
if (!empty($moduledata->completionview)) {
$activeruledescriptions[] = get_string('completionview_desc', 'completion');
}
if ($moduledata instanceof cm_info && !is_null($moduledata->completiongradeitemnumber) ||
($moduledata instanceof stdClass && !empty($moduledata->completionusegrade))) {
$description = 'completionusegrade_desc';
if (!empty($moduledata->completionpassgrade)) {
$description = 'completionpassgrade_desc';
}
$activeruledescriptions[] = get_string($description, 'completion');
}
// Now, ask the module to provide descriptions for its custom conditional completion rules.
if ($customruledescriptions = component_callback($moduledata->modname,
'get_completion_active_rule_descriptions', [$moduledata])) {
$activeruledescriptions = array_merge($activeruledescriptions, $customruledescriptions);
}
}
if ($moduledata->completion != COMPLETION_TRACKING_NONE) {
if (!empty($moduledata->completionexpected)) {
$activeruledescriptions[] = get_string('completionexpecteddesc', 'completion',
userdate($moduledata->completionexpected));
}
}
return $activeruledescriptions;
}
/**
* Gets the course modules for the current course.
*
* @param bool $includedefaults Whether the default values should be included or not.
* @return stdClass $data containing the modules
*/
public function get_activities_and_resources(bool $includedefaults = true) {
global $DB, $OUTPUT, $CFG;
require_once($CFG->dirroot.'/course/lib.php');
// Get enabled activities and resources.
$modules = $DB->get_records('modules', ['visible' => 1], 'name ASC');
$data = new stdClass();
$data->courseid = $this->courseid;
$data->sesskey = sesskey();
$data->helpicon = $OUTPUT->help_icon('bulkcompletiontracking', 'core_completion');
// Add icon information.
$data->modules = array_values($modules);
$context = $this->get_context();
$canmanage = has_capability('moodle/course:manageactivities', $context);
$course = get_course($this->courseid);
$availablemodules = [];
foreach ($data->modules as $module) {
$libfile = "$CFG->dirroot/mod/$module->name/lib.php";
if (!file_exists($libfile)) {
continue;
}
$module->icon = $OUTPUT->image_url('monologo', $module->name)->out();
$module->formattedname = format_string(get_string('modulename', 'mod_' . $module->name),
true, ['context' => $context]);
$module->canmanage = $canmanage && course_allowed_module($course, $module->name);
if ($includedefaults) {
$defaults = self::get_default_completion($course, $module, false);
$defaults->modname = $module->name;
$module->completionstatus = $this->get_completion_detail($defaults);
}
$availablemodules[] = $module;
}
// Order modules by displayed name.
usort($availablemodules, function($a, $b) {
return strcmp($a->formattedname, $b->formattedname);
});
$data->modules = $availablemodules;
return $data;
}
/**
* Checks if current user can edit activity completion
*
* @param int|stdClass $courseorid
* @param \cm_info|null $cm if specified capability for a given coursemodule will be check,
* if not specified capability to edit at least one activity is checked.
*/
public static function can_edit_bulk_completion($courseorid, $cm = null) {
if ($cm) {
return $cm->uservisible && has_capability('moodle/course:manageactivities', $cm->context);
}
$coursecontext = context_course::instance(is_object($courseorid) ? $courseorid->id : $courseorid);
if (has_capability('moodle/course:manageactivities', $coursecontext)) {
return true;
}
$modinfo = get_fast_modinfo($courseorid);
foreach ($modinfo->cms as $mod) {
if ($mod->uservisible && has_capability('moodle/course:manageactivities', $mod->context)) {
return true;
}
}
return false;
}
/**
* @deprecated since Moodle 4.0
*/
public static function get_available_completion_tabs() {
throw new \coding_exception(__FUNCTION__ . '() has been removed.');
}
/**
* Returns an array with the available completion options (url => name) for the current course and user.
*
* @param int $courseid The course id.
* @return array
*/
public static function get_available_completion_options(int $courseid): array {
$coursecontext = context_course::instance($courseid);
$options = [];
if (has_capability('moodle/course:update', $coursecontext)) {
$completionlink = new moodle_url('/course/completion.php', ['id' => $courseid]);
$options[$completionlink->out(false)] = get_string('coursecompletionsettings', 'completion');
}
if (has_capability('moodle/course:manageactivities', $coursecontext)) {
$defaultcompletionlink = new moodle_url('/course/defaultcompletion.php', ['id' => $courseid]);
$options[$defaultcompletionlink->out(false)] = get_string('defaultcompletion', 'completion');
}
if (self::can_edit_bulk_completion($courseid)) {
$bulkcompletionlink = new moodle_url('/course/bulkcompletion.php', ['id' => $courseid]);
$options[$bulkcompletionlink->out(false)] = get_string('bulkactivitycompletion', 'completion');
}
return $options;
}
/**
* Applies completion from the bulk edit form to all selected modules
*
* @param stdClass $data data received from the core_completion_bulkedit_form
* @param bool $updateinstances if we need to update the instance tables of the module (i.e. 'assign', 'forum', etc.) -
* if no module-specific completion rules were added to the form, update of the module table is not needed.
*/
public function apply_completion($data, $updateinstances) {
$updated = false;
$needreset = [];
$modinfo = get_fast_modinfo($this->courseid);
$cmids = $data->cmid;
$data = (array)$data;
unset($data['id']); // This is a course id, we don't want to confuse it with cmid or instance id.
unset($data['cmid']);
unset($data['submitbutton']);
foreach ($cmids as $cmid) {
$cm = $modinfo->get_cm($cmid);
if (self::can_edit_bulk_completion($this->courseid, $cm) && $this->apply_completion_cm($cm, $data, $updateinstances)) {
$updated = true;
if ($cm->completion != COMPLETION_TRACKING_MANUAL || $data['completion'] != COMPLETION_TRACKING_MANUAL) {
// If completion was changed we will need to reset it's state. Exception is when completion was and remains as manual.
$needreset[] = $cm->id;
}
}
// Update completion calendar events.
$completionexpected = ($data['completionexpected']) ? $data['completionexpected'] : null;
\core_completion\api::update_completion_date_event($cm->id, $cm->modname, $cm->instance, $completionexpected);
}
if ($updated) {
// Now that modules are fully updated, also update completion data if required.
// This will wipe all user completion data and recalculate it.
rebuild_course_cache($this->courseid, true);
$modinfo = get_fast_modinfo($this->courseid);
$completion = new \completion_info($modinfo->get_course());
foreach ($needreset as $cmid) {
$completion->reset_all_state($modinfo->get_cm($cmid));
}
// And notify the user of the result.
\core\notification::add(get_string('activitycompletionupdated', 'core_completion'), \core\notification::SUCCESS);
}
}
/**
* Applies new completion rules to one course module
*
* @param \cm_info $cm
* @param array $data
* @param bool $updateinstance if we need to update the instance table of the module (i.e. 'assign', 'forum', etc.) -
* if no module-specific completion rules were added to the form, update of the module table is not needed.
* @return bool if module was updated
*/
protected function apply_completion_cm(\cm_info $cm, $data, $updateinstance) {
global $DB;
$defaults = [
'completion' => COMPLETION_DISABLED, 'completionview' => COMPLETION_VIEW_NOT_REQUIRED,
'completionexpected' => 0, 'completiongradeitemnumber' => null,
'completionpassgrade' => 0
];
$data += ['completion' => $cm->completion,
'completionexpected' => $cm->completionexpected,
'completionview' => $cm->completionview];
if ($cm->completion == $data['completion'] && $cm->completion == COMPLETION_TRACKING_NONE) {
// If old and new completion are both "none" - no changes are needed.
return false;
}
if ($cm->completion == $data['completion'] && $cm->completion == COMPLETION_TRACKING_NONE &&
$cm->completionexpected == $data['completionexpected']) {
// If old and new completion are both "manual" and completion expected date is not changed - no changes are needed.
return false;
}
if (array_key_exists('completionusegrade', $data)) {
// Convert the 'use grade' checkbox into a grade-item number: 0 if checked, null if not.
$data['completiongradeitemnumber'] = !empty($data['completionusegrade']) ? 0 : null;
unset($data['completionusegrade']);
} else {
// Completion grade item number is classified in mod_edit forms as 'use grade'.
$data['completionusegrade'] = is_null($cm->completiongradeitemnumber) ? 0 : 1;
$data['completiongradeitemnumber'] = $cm->completiongradeitemnumber;
}
// Update module instance table.
if ($updateinstance) {
$moddata = ['id' => $cm->instance, 'timemodified' => time()] + array_diff_key($data, $defaults);
$DB->update_record($cm->modname, $moddata);
}
// Update course modules table.
$cmdata = ['id' => $cm->id, 'timemodified' => time()] + array_intersect_key($data, $defaults);
$DB->update_record('course_modules', $cmdata);
\core\event\course_module_updated::create_from_cm($cm, $cm->context)->trigger();
// We need to reset completion data for this activity.
return true;
}
/**
* Saves default completion from edit form to all selected module types
*
* @param stdClass $data data received from the core_completion_bulkedit_form
* @param bool $updatecustomrules if we need to update the custom rules of the module -
* if no module-specific completion rules were added to the form, update of the module table is not needed.
* @param string $suffix the suffix to add to the name of the completion rules.
*/
public function apply_default_completion($data, $updatecustomrules, string $suffix = '') {
global $DB;
if (!empty($suffix)) {
// Fields were renamed to avoid conflicts, but they need to be stored in DB with the original name.
$modules = property_exists($data, 'modules') ? $data->modules : null;
if ($modules !== null) {
unset($data->modules);
$data = (array)$data;
foreach ($data as $name => $value) {
if (str_ends_with($name, $suffix)) {
$data[substr($name, 0, strpos($name, $suffix))] = $value;
unset($data[$name]);
} else if ($name == 'customdata') {
$customrules = $value['customcompletionrules'];
foreach ($customrules as $rulename => $rulevalue) {
if (str_ends_with($rulename, $suffix)) {
$customrules[substr($rulename, 0, strpos($rulename, $suffix))] = $rulevalue;
unset($customrules[$rulename]);
}
}
$data['customdata'] = $customrules;
}
}
$data = (object)$data;
}
}
$courseid = $data->id;
// MDL-72375 Unset the id here, it should not be stored in customrules.
unset($data->id);
$coursecontext = context_course::instance($courseid);
if (!$modids = $data->modids) {
return;
}
$defaults = [
'completion' => COMPLETION_DISABLED,
'completionview' => COMPLETION_VIEW_NOT_REQUIRED,
'completionexpected' => 0,
'completionusegrade' => 0,
'completionpassgrade' => 0
];
$data = (array)$data;
if (!array_key_exists('completionusegrade', $data)) {
$data['completionusegrade'] = 0;
}
if (!array_key_exists('completionpassgrade', $data)) {
$data['completionpassgrade'] = 0;
}
if ($data['completionusegrade'] == 0) {
$data['completionpassgrade'] = 0;
}
if ($updatecustomrules) {
$customdata = array_diff_key($data, $defaults);
$data['customrules'] = $customdata ? json_encode($customdata) : null;
$defaults['customrules'] = null;
}
$data = array_merge($defaults, $data);
// Get names of the affected modules.
list($modidssql, $params) = $DB->get_in_or_equal($modids);
$params[] = 1;
$modules = $DB->get_records_select_menu('modules', 'id ' . $modidssql . ' and visible = ?', $params, '', 'id, name');
// Get an associative array of [module_id => course_completion_defaults_id].
list($in, $params) = $DB->get_in_or_equal($modids);
$params[] = $courseid;
$defaultsids = $DB->get_records_select_menu('course_completion_defaults', 'module ' . $in . ' and course = ?', $params, '',
'module, id');
foreach ($modids as $modid) {
if (!array_key_exists($modid, $modules)) {
continue;
}
if (isset($defaultsids[$modid])) {
$DB->update_record('course_completion_defaults', $data + ['id' => $defaultsids[$modid]]);
} else {
$defaultsids[$modid] = $DB->insert_record('course_completion_defaults', $data + ['course' => $courseid,
'module' => $modid]);
}
// Trigger event.
\core\event\completion_defaults_updated::create([
'objectid' => $defaultsids[$modid],
'context' => $coursecontext,
'other' => ['modulename' => $modules[$modid]],
])->trigger();
}
// Add notification.
\core\notification::add(get_string('defaultcompletionupdated', 'completion'), \core\notification::SUCCESS);
}
/**
* Returns default completion rules for given module type in the given course
*
* @param stdClass $course
* @param stdClass $module
* @param bool $flatten if true all module custom completion rules become properties of the same object,
* otherwise they can be found as array in ->customdata['customcompletionrules']
* @param string $suffix the suffix to add to the name of the completion rules.
* @return stdClass
*/
public static function get_default_completion($course, $module, $flatten = true, string $suffix = '') {
global $DB, $CFG, $SITE;
$fields = 'completion, completionview, completionexpected, completionusegrade, completionpassgrade, customrules';
// Check course default completion values.
$params = ['course' => $course->id, 'module' => $module->id];
$data = $DB->get_record('course_completion_defaults', $params, $fields);
if (!$data && $course->id != $SITE->id) {
// If there is no course default completion, check site level default completion values ($SITE->id).
$params['course'] = $SITE->id;
$data = $DB->get_record('course_completion_defaults', $params, $fields);
}
if ($data) {
if ($data->customrules && ($customrules = @json_decode($data->customrules, true))) {
// MDL-72375 This will override activity id for new mods. Skip this field, it is already exposed as courseid.
unset($customrules['id']);
if ($flatten) {
foreach ($customrules as $key => $value) {
$data->$key = $value;
}
} else {
$data->customdata['customcompletionrules'] = $customrules;
}
}
unset($data->customrules);
} else {
$data = new stdClass();
$data->completion = COMPLETION_TRACKING_NONE;
}
// If the suffix is not empty, the completion rules need to be renamed to avoid conflicts.
if (!empty($suffix)) {
$data = (array)$data;
foreach ($data as $name => $value) {
if (str_starts_with($name, 'completion')) {
$data[$name . $suffix] = $value;
unset($data[$name]);
} else if ($name == 'customdata') {
$customrules = $value['customcompletionrules'];
foreach ($customrules as $rulename => $rulevalue) {
if (str_starts_with($rulename, 'completion')) {
$customrules[$rulename . $suffix] = $rulevalue;
unset($customrules[$rulename]);
}
}
$data['customdata'] = $customrules;
}
}
$data = (object)$data;
}
return $data;
}
/**
* Return a mod_form of the given module.
*
* @param string $modname Module to get the form from.
* @param stdClass $course Course object.
* @param ?cm_info $cm cm_info object to use.
* @param string $suffix The suffix to add to the name of the completion rules.
* @return ?\moodleform_mod The moodleform_mod object if everything goes fine. Null otherwise.
*/
public static function get_module_form(
string $modname,
stdClass $course,
?cm_info $cm = null,
string $suffix = ''
): ?\moodleform_mod {
global $CFG, $PAGE;
$modmoodleform = "$CFG->dirroot/mod/$modname/mod_form.php";
if (file_exists($modmoodleform)) {
require_once($modmoodleform);
} else {
throw new \moodle_exception('noformdesc');
}
if ($cm) {
[$cmrec, $context, $module, $data, $cw] = get_moduleinfo_data($cm, $course);
$data->update = $modname;
} else {
[$module, $context, $cw, $cmrec, $data] = prepare_new_moduleinfo_data($course, $modname, 0, $suffix);
$data->add = $modname;
}
$data->return = 0;
$data->sr = 0;
// Initialise the form but discard all JS requirements it adds, our form has already added them.
$mformclassname = 'mod_'.$modname.'_mod_form';
$PAGE->start_collecting_javascript_requirements();
try {
$moduleform = new $mformclassname($data, 0, $cmrec, $course);
if (!$cm) {
$moduleform->set_suffix('_' . $modname);
}
} catch (\Exception $e) {
// The form class has thrown an error when instantiating.
// This could happen because some conditions for the module are not met.
$moduleform = null;
} finally {
$PAGE->end_collecting_javascript_requirements();
}
return $moduleform;
}
}
+342
View File
@@ -0,0 +1,342 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Privacy class for requesting user data.
*
* @package core_completion
* @copyright 2018 Adrian Greeve <adrian@moodle.com>
* @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\approved_userlist;
use core_privacy\local\request\contextlist;
use core_privacy\local\request\transform;
use core_privacy\local\request\userlist;
require_once($CFG->dirroot . '/comment/lib.php');
/**
* Privacy class for requesting user data.
*
* @package core_completion
* @copyright 2018 Adrian Greeve <adrian@moodle.com>
* @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,
\core_privacy\local\request\shared_userlist_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',
'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',
'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.
$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 OR {$cmvalias}.id IS NOT NULL";
$params = ["{$prefix}_moduleuserid" => $userid, "{$prefix}_moduleuserid2" => $userid, "{$prefix}_courseuserid" => $userid];
return [$join, $where, $params];
}
/**
* Find users' course completion by context and add to the provided userlist.
*
* @param userlist $userlist The userlist to add to.
*/
public static function add_course_completion_users_to_userlist(userlist $userlist) {
$context = $userlist->get_context();
if (!$context instanceof \context_course) {
return;
}
$params = ['courseid' => $context->instanceid];
$sql = "SELECT cmc.userid
FROM {course} c
JOIN {course_completion_criteria} ccc ON ccc.course = c.id
JOIN {course_modules_completion} cmc ON cmc.coursemoduleid = ccc.moduleinstance
WHERE c.id = :courseid";
$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
JOIN {course_completion_crit_compl} ccc_compl ON ccc_compl.criteriaid = ccc.id
WHERE c.id = :courseid";
$userlist->add_from_sql('userid', $sql, $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) {
return [];
}
$coursecomplete = $completioninfo->is_course_complete($user->id);
if ($coursecomplete) {
$status = get_string('complete');
} else {
$criteriacomplete = $completioninfo->count_course_user_data($user->id);
$ccompletion = new \completion_completion(['userid' => $user->id, 'course' => $course->id]);
if (!$criteriacomplete && !$ccompletion->timestarted) {
$status = get_string('notyetstarted', 'completion');
} else {
$status = 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' : '';
$usercmvsql = isset($user) ? 'AND cmv.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);
}
// 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);
}
}
/**
* Delete completion information for users within an approved userlist.
*
* @param approved_userlist $userlist The approved userlist of users to delete completion information for.
* @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_by_approved_userlist(approved_userlist $userlist, int $courseid = null, int $cmid = null) {
global $DB;
$userids = $userlist->get_userids();
if (empty($userids)) {
return;
}
list($useridsql, $params) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED);
if (isset($cmid)) {
$params['coursemoduleid'] = $cmid;
// 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;
}
if (isset($courseid)) {
$params['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 AND cmc.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_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);
}
}
}
+92
View File
@@ -0,0 +1,92 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Contains class used to return completion progress information.
*
* @package core_completion
* @copyright 2017 Mark Nelson <markn@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_completion;
defined('MOODLE_INTERNAL') || die();
require_once($CFG->libdir . '/completionlib.php');
/**
* Class used to return completion progress information.
*
* @package core_completion
* @copyright 2017 Mark Nelson <markn@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class progress {
/**
* Returns the course percentage completed by a certain user, returns null if no completion data is available.
*
* @param \stdClass $course Moodle course object
* @param int $userid The id of the user, 0 for the current user
* @return null|float The percentage, or null if completion is not supported in the course,
* or there are no activities that support completion.
*/
public static function get_course_progress_percentage($course, $userid = 0) {
global $USER;
// Make sure we continue with a valid userid.
if (empty($userid)) {
$userid = $USER->id;
}
$completion = new \completion_info($course);
// First, let's make sure completion is enabled.
if (!$completion->is_enabled()) {
return null;
}
if (!$completion->is_tracked_user($userid)) {
return null;
}
// Before we check how many modules have been completed see if the course has.
if ($completion->is_course_complete($userid)) {
return 100;
}
// Get the number of modules that support completion.
$modules = $completion->get_activities();
$count = count($modules);
if (!$count) {
return null;
}
// Get the number of modules that have been completed.
$completed = 0;
foreach ($modules as $module) {
$data = $completion->get_data($module, true, $userid);
if (($data->completionstate == COMPLETION_INCOMPLETE) || ($data->completionstate == COMPLETION_COMPLETE_FAIL)) {
$completed += 0;
} else {
$completed += 1;
};
}
return ($completed / $count) * 100;
}
}