AutorÃa | Ultima modificación | Ver Log |
<?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 Progress block.** @package block_completion_progress* @copyright 2016 Michael de Raadt* @copyright 2021 Jonathon Fowler <fowlerj@usq.edu.au>* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later*/namespace block_completion_progress;use stdClass;use completion_info;use context_course;use coding_exception;/*** Completion Progress.** @package block_completion_progress* @copyright 2016 Michael de Raadt* @copyright 2021 Jonathon Fowler <fowlerj@usq.edu.au>* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later*/class completion_progress implements \renderable {/*** Sort activities by course order.*/const ORDERBY_COURSE = 'orderbycourse';/*** Sort activities by expected time order.*/const ORDERBY_TIME = 'orderbytime';/*** The course.* @var object*/protected $course;/*** The course context.* @var context_course*/protected $context;/*** Completion info for the course.* @var completion_info*/protected $completioninfo;/*** The user.* @var object*/protected $user;/*** Block instance record.* @var stdClass*/protected $blockinstance;/*** Block instance config.* @var stdClass*/protected $blockconfig;/*** List of activities.* @var array cmid => obj*/protected $activities = null;/*** List of visible activities.* @var array cmid => obj*/protected $visibleactivities = null;/*** List of grade exclusions.* @var array of: [module-instance-userid, ...]*/protected $exclusions = null;/*** List of submissions.* @var array of arrays: userid => [cmid => obj]*/protected $submissions = null;/*** List of computed completions.* @var array of arrays: userid => [cmid => state]*/protected $completions = null;/*** Whether exclusions have been loaded for all course users already.* @var boolean*/protected $exclusionsforall = false;/*** Whether submissions have been loaded for all course users already.* @var boolean*/protected $submissionsforall = false;/*** Whether completions have been loaded for all course users already.* @var boolean*/protected $completionsforall = false;/*** Simple bar mode (for overview).* @var boolean*/protected $simplebar = false;/*** Constructor.* @param object|int $courseorid*/public function __construct($courseorid) {global $CFG;require_once($CFG->libdir.'/completionlib.php');if (is_object($courseorid)) {$this->course = $courseorid;} else {$this->course = get_course($courseorid);}$this->context = context_course::instance($this->course->id);$this->completioninfo = new completion_info($this->course);}/*** Specialise for a specific user.* @param stdClass $user containing minimum of core_user\fields::for_name()* @return self*/public function for_user(stdClass $user): self {$this->user = $user;$this->load_exclusions();$this->load_submissions();$this->load_completions();$this->filter_visible_activities();return $this;}/*** Specialise for overview page use.* @return self*/public function for_overview() {if ($this->user) {throw new coding_exception('cannot re-specialise for overview');}$this->user = null;$this->simplebar = true;$this->load_exclusions();$this->load_submissions();$this->load_completions();return $this;}/*** Specialise for a particular block instance.* @param stdClass $instance Instance record.* @param boolean $selectedonly Whether to filter by configured selected items.* @return self*/public function for_block_instance(stdClass $instance, $selectedonly = true): self {if ($this->blockinstance) {throw new coding_exception('cannot re-specialise for a different block instance');}$this->blockinstance = $instance;$this->blockconfig = (object)(array)unserialize(base64_decode($instance->configdata ?? ''));$this->load_activities($selectedonly);$this->filter_visible_activities();return $this;}/*** Return the course object.* @return object*/public function get_course(): stdClass {return $this->course;}/*** Return the course object.* @return context_course*/public function get_context(): context_course {return $this->context;}/*** Return the completion info object.* @return completion_info*/public function get_completion_info(): completion_info {return $this->completioninfo;}/*** Return the user.* @return stdClass*/public function get_user(): ?stdClass {return $this->user;}/*** Return the simple bar mode.* @return boolean*/public function is_simple_bar(): bool {return $this->simplebar;}/*** Check whether any activities are available.* @return boolean*/public function has_activities(): bool {if ($this->activities === null) {throw new coding_exception('activities not loaded until for_block_instance() is called');}return !empty($this->activities);}/*** Return the activities in presentation order.* @param string|null $orderoverride* @return array*/public function get_activities($orderoverride = null): array {if ($this->activities === null) {throw new coding_exception('activities not loaded until for_block_instance() is called');}$order = $orderoverride ?? $this->blockconfig->orderby ?? self::ORDERBY_COURSE;usort($this->activities, [$this, 'sorter_' . $order]);return $this->activities;}/*** Check whether any visible activities are available.* @return boolean*/public function has_visible_activities(): bool {if ($this->visibleactivities === null) {throw new coding_exception('visible activities not computed until for_block_instance() is called');}return !empty($this->visibleactivities);}/*** Return the activities visible to the user in presentation order.* @param string|null $orderoverride* @return array*/public function get_visible_activities($orderoverride = null): array {if ($this->visibleactivities === null) {throw new coding_exception('visible activities not computed until for_block_instance() is called');}$order = $orderoverride ?? $this->blockconfig->orderby ?? self::ORDERBY_COURSE;usort($this->visibleactivities, [$this, 'sorter_' . $order]);return $this->visibleactivities;}/*** Return the exclusions.* @return array of modname-modinstance-userid formatted items*/public function get_exclusions(): array {return $this->exclusions;}/*** Get block instance.* @return stdClass|null*/public function get_block_instance(): ?stdClass {return $this->blockinstance;}/*** Get block configuration.* @return stdClass*/public function get_block_config(): stdClass {return $this->blockconfig;}/*** Get user activity submissions.* @return array cmid => info*/public function get_submissions(): array {if ($this->submissions === null) {throw new coding_exception('submissions not computed until for_user() or for_overview() is called');}if ($this->user) {return $this->submissions[$this->user->id] ?? [];} else {throw new coding_exception('unimplemented');}}/*** Get user activity completion states.* @return array cmid => status*/public function get_completions() {if ($this->completions === null) {throw new coding_exception('completions not computed until for_user() or for_overview() is called');}if ($this->user) {// Filter to visible activities and fill in gaps.$completions = $this->completions[$this->user->id] ?? [];$ret = [];foreach ($this->visibleactivities as $activity) {$ret[$activity->id] = $completions[$activity->id] ?? COMPLETION_INCOMPLETE;}return $ret;} else {throw new coding_exception('unimplemented');}}/*** Calculates an overall percentage of progress.* @return integer Progress value as a percentage*/public function get_percentage(): ?int {$completions = $this->get_completions();if (count($completions) == 0) {return null;}$completecount = 0;foreach ($completions as $complete) {if ($complete == COMPLETION_COMPLETE || $complete == COMPLETION_COMPLETE_PASS) {$completecount++;}}return (int)round(100 * $completecount / count($this->visibleactivities));}/*** Used to compare two activity entries based on order on course page.** @param array $a* @param array $b* @return integer*/private function sorter_orderbycourse($a, $b): int {if ($a->section != $b->section) {return $a->section <=> $b->section;} else {return $a->position <=> $b->position;}}/*** Used to compare two activity entries based their expected completion times** @param array $a* @param array $b* @return integer*/private function sorter_orderbytime($a, $b): int {if ($a->expected != 0 && $b->expected != 0 && $a->expected != $b->expected) {return $a->expected <=> $b->expected;} else if ($a->expected != 0 && $b->expected == 0) {return -1;} else if ($a->expected == 0 && $b->expected != 0) {return 1;} else {return $this->sorter_orderbycourse($a, $b);}}/*** Loads activities with completion set in current course.** @param boolean $selectedonly Whether to filter by configured selected items.* @return array*/protected function load_activities($selectedonly) {$modinfo = get_fast_modinfo($this->course, -1);$sections = $modinfo->get_sections();$selectedonly = $selectedonly && ($this->blockconfig->activitiesincluded ?? '') === 'selectedactivities';$selectedcms = $this->blockconfig->selectactivities ?? [];$this->activities = [];foreach ($modinfo->instances as $module => $cms) {$modulename = get_string('pluginname', $module);foreach ($cms as $cm) {if ($cm->completion == COMPLETION_TRACKING_NONE) {continue;}if ($selectedonly && !in_array($module.'-'.$cm->instance, $selectedcms)) {continue;}$this->activities[$cm->id] = (object)['type' => $module,'modulename' => $modulename,'id' => $cm->id,'instance' => $cm->instance,'name' => $cm->get_formatted_name(),'expected' => $cm->completionexpected,'section' => $cm->sectionnum,'position' => array_search($cm->id, $sections[$cm->sectionnum]),'url' => $cm->url instanceof \moodle_url ? $cm->url->out() : '','onclick' => $cm->onclick,'context' => $cm->context,'icon' => $cm->get_icon_url(),'available' => $cm->available,];}}}/*** Filter down the activities to those a user can see.*/protected function filter_visible_activities() {global $CFG, $USER;if (!$this->user || $this->activities === null) {return;}$this->visibleactivities = [];$modinfo = get_fast_modinfo($this->course, $this->user->id);$canviewhidden = has_capability('moodle/course:viewhiddenactivities', $this->context, $this->user);// Keep only activities that are visible.foreach ($this->activities as $key => $activity) {$cm = $modinfo->cms[$activity->id];// Check visibility in course.if (!$cm->visible && !$canviewhidden) {continue;}// Check availability, allowing for visible, but not accessible items.if (!empty($CFG->enableavailability)) {if ($canviewhidden) {$activity->available = true;} else {if (isset($cm->available) && !$cm->available && empty($cm->availableinfo)) {continue;}$activity->available = $cm->available;}}// Check for exclusions.if (in_array($activity->type.'-'.$activity->instance.'-'.$this->user->id, $this->exclusions)) {continue;}// Save the visible event.$this->visibleactivities[$key] = $activity;}}/*** Finds gradebook exclusions for students in the course.*/protected function load_exclusions() {global $DB;if ($this->exclusionsforall) {// Already loaded.return;}$query = "SELECT g.id, i.itemmodule, i.iteminstance, g.useridFROM {grade_grades} g, {grade_items} iWHERE i.courseid = :courseidAND i.id = g.itemidAND g.excluded <> 0";$params = ['courseid' => $this->course->id];if ($this->user) {$query .= " AND g.userid = :userid";$params['userid'] = $this->user->id;} else {// Avoid refetching this info if specialising for user later.$this->exclusionsforall = true;}$this->exclusions = [];foreach ($DB->get_records_sql($query, $params) as $rec) {$this->exclusions[] = $rec->itemmodule . '-' . $rec->iteminstance . '-' . $rec->userid;}}/*** Loads completion information for enrolled users in the course.*/protected function load_completions() {global $DB;if ($this->completionsforall) {// Already loaded.return;}// Somewhat faster than lots of calls to completion_info::get_data($cm, true, $userid)// where its cache can't be used because the userid is different.$enrolsql = get_enrolled_join($this->context, 'u.id', false);$query = "SELECT DISTINCT " . $DB->sql_concat('cm.id', "'-'", 'u.id') . " AS id,u.id AS userid, cm.id AS cmid,COALESCE(cmc.completionstate, :incomplete) AS completionstateFROM {user} u {$enrolsql->joins}CROSS JOIN {course_modules} cmLEFT JOIN {course_modules_completion} cmc ON cmc.coursemoduleid = cm.id AND cmc.userid = u.idWHERE {$enrolsql->wheres}AND cm.course = :courseidAND cm.completion <> :none";$params = $enrolsql->params + ['courseid' => $this->course->id,'incomplete' => COMPLETION_INCOMPLETE,'none' => COMPLETION_TRACKING_NONE,];if ($this->user) {$query .= " AND u.id = :userid";$params['userid'] = $this->user->id;} else {// Avoid refetching this info if specialising for user later.$this->completionsforall = true;}$rset = $DB->get_recordset_sql($query, $params);$this->completions = [];foreach ($rset as $compl) {$submission = $this->submissions[$compl->userid][$compl->cmid] ?? null;if ($compl->completionstate == COMPLETION_INCOMPLETE && $submission) {$this->completions[$compl->userid][$compl->cmid] = 'submitted';} else if ($compl->completionstate == COMPLETION_COMPLETE_FAIL && $submission&& !$submission->graded) {$this->completions[$compl->userid][$compl->cmid] = 'submitted';} else {$this->completions[$compl->userid][$compl->cmid] = $compl->completionstate;}}$rset->close();}/*** Find submissions for students in the course.*/protected function load_submissions() {global $DB, $CFG;if ($this->submissionsforall) {// Already loaded.return;}require_once($CFG->dirroot . '/mod/quiz/lib.php');$params = ['courseid' => $this->course->id,];if ($this->user) {$assignwhere = 'AND s.userid = :userid';$workshopwhere = 'AND s.authorid = :userid';$quizwhere = 'AND qa.userid = :userid';$params += ['userid' => $this->user->id,];} else {$assignwhere = '';$workshopwhere = '';$quizwhere = '';// Avoid refetching this info if specialising for user later.$this->submissionsforall = true;}// Queries to deliver instance IDs of activities with submissions by user.$queries = array ([// Assignments with individual submission, or groups requiring a submission per user,// or ungrouped users in a group submission situation.'module' => 'assign','query' => "SELECT ". $DB->sql_concat('s.userid', "'-'", 'c.id') ." AS id,s.userid, c.id AS cmid,MAX(CASE WHEN ag.grade IS NULL OR ag.grade = -1 THEN 0 ELSE 1 END) AS gradedFROM {assign_submission} sINNER JOIN {assign} a ON s.assignment = a.idINNER JOIN {course_modules} c ON c.instance = a.idINNER JOIN {modules} m ON m.name = 'assign' AND m.id = c.moduleLEFT JOIN {assign_grades} ag ON ag.assignment = s.assignmentAND ag.attemptnumber = s.attemptnumberAND ag.userid = s.useridWHERE s.latest = 1AND s.status = 'submitted'AND a.course = :courseidAND (a.teamsubmission = 0 OR(a.teamsubmission <> 0 AND a.requireallteammemberssubmit <> 0 AND s.groupid = 0) OR(a.teamsubmission <> 0 AND a.preventsubmissionnotingroup = 0 AND s.groupid = 0))$assignwhereGROUP BY s.userid, c.id",'params' => [ ],],[// Assignments with groups requiring only one submission per group.'module' => 'assign','query' => "SELECT ". $DB->sql_concat('s.userid', "'-'", 'c.id') ." AS id,s.userid, c.id AS cmid,MAX(CASE WHEN ag.grade IS NULL OR ag.grade = -1 THEN 0 ELSE 1 END) AS gradedFROM {assign_submission} gsINNER JOIN {assign} a ON gs.assignment = a.idINNER JOIN {course_modules} c ON c.instance = a.idINNER JOIN {modules} m ON m.name = 'assign' AND m.id = c.moduleINNER JOIN {groups_members} s ON s.groupid = gs.groupidLEFT JOIN {assign_grades} ag ON ag.assignment = gs.assignmentAND ag.attemptnumber = gs.attemptnumberAND ag.userid = s.useridWHERE gs.latest = 1AND gs.status = 'submitted'AND gs.userid = 0AND a.course = :courseidAND (a.teamsubmission <> 0 AND a.requireallteammemberssubmit = 0)$assignwhereGROUP BY s.userid, c.id",'params' => [ ],],['module' => 'workshop','query' => "SELECT ". $DB->sql_concat('s.authorid', "'-'", 'c.id') ." AS id,s.authorid AS userid, c.id AS cmid,1 AS gradedFROM {workshop_submissions} s, {workshop} w, {modules} m, {course_modules} cWHERE s.workshopid = w.idAND w.course = :courseidAND m.name = 'workshop'AND m.id = c.moduleAND c.instance = w.id$workshopwhereGROUP BY s.authorid, c.id",'params' => [ ],],[// Quizzes with 'first' and 'last attempt' grading methods.'module' => 'quiz','query' => "SELECT ". $DB->sql_concat('qa.userid', "'-'", 'c.id') ." AS id,qa.userid, c.id AS cmid,(CASE WHEN qa.sumgrades IS NULL THEN 0 ELSE 1 END) AS gradedFROM {quiz_attempts} qaINNER JOIN {quiz} q ON q.id = qa.quizINNER JOIN {course_modules} c ON c.instance = q.idINNER JOIN {modules} m ON m.name = 'quiz' AND m.id = c.moduleWHERE qa.state = 'finished'AND q.course = :courseidAND qa.attempt = (SELECT CASE WHEN q.grademethod = :gmfirst THEN MIN(qa1.attempt)WHEN q.grademethod = :gmlast THEN MAX(qa1.attempt) ENDFROM {quiz_attempts} qa1WHERE qa1.quiz = qa.quizAND qa1.userid = qa.useridAND qa1.state = 'finished')$quizwhere",'params' => ['gmfirst' => QUIZ_ATTEMPTFIRST,'gmlast' => QUIZ_ATTEMPTLAST,],],[// Quizzes with 'maximum' and 'average' grading methods.'module' => 'quiz','query' => "SELECT ". $DB->sql_concat('qa.userid', "'-'", 'c.id') ." AS id,qa.userid, c.id AS cmid,MIN(CASE WHEN qa.sumgrades IS NULL THEN 0 ELSE 1 END) AS gradedFROM {quiz_attempts} qaINNER JOIN {quiz} q ON q.id = qa.quizINNER JOIN {course_modules} c ON c.instance = q.idINNER JOIN {modules} m ON m.name = 'quiz' AND m.id = c.moduleWHERE (q.grademethod = :gmmax OR q.grademethod = :gmavg)AND qa.state = 'finished'AND q.course = :courseid$quizwhereGROUP BY qa.userid, c.id",'params' => ['gmmax' => QUIZ_GRADEHIGHEST,'gmavg' => QUIZ_GRADEAVERAGE,],],);$this->submissions = [];foreach ($queries as $spec) {$results = $DB->get_records_sql($spec['query'], $params + $spec['params']);foreach ($results as $obj) {unset($obj->id);$this->submissions[$obj->userid][$obj->cmid] = $obj;}}}}