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,115 @@
<?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/>.
/**
* Activities due indicator.
*
* @package core
* @copyright 2019 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_course\analytics\indicator;
defined('MOODLE_INTERNAL') || die();
require_once($CFG->dirroot . '/calendar/externallib.php');
/**
* Activities due indicator.
*
* @package core
* @copyright 2019 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class activities_due extends \core_analytics\local\indicator\binary {
/**
* Returns the name.
*
* If there is a corresponding '_help' string this will be shown as well.
*
* @return \lang_string
*/
public static function get_name(): \lang_string {
return new \lang_string('indicator:activitiesdue');
}
/**
* required_sample_data
*
* @return string[]
*/
public static function required_sample_data() {
return array('user');
}
/**
* calculate_sample
*
* @param int $sampleid
* @param string $sampleorigin
* @param int $starttime
* @param int $endtime
* @return float
*/
protected function calculate_sample($sampleid, $sampleorigin, $starttime = false, $endtime = false) {
$user = $this->retrieve('user', $sampleid);
$actionevents = \core_calendar_external::get_calendar_action_events_by_timesort($starttime, $endtime, 0, 1,
true, $user->id);
$useractionevents = [];
if ($actionevents->events) {
// We first need to check that at least one of the core_calendar_provide_event_action
// callbacks has the $userid param.
foreach ($actionevents->events as $event) {
$nparams = $this->get_provide_event_action_num_params($event->modulename);
if ($nparams > 2) {
// Just the basic info for the insight as we want a low memory usage.
$useractionevents[$event->id] = (object)[
'name' => $event->name,
'url' => $event->url,
'time' => $event->timesort,
'coursename' => $event->course->fullnamedisplay,
'icon' => $event->icon,
];
}
}
if (!empty($useractionevents)) {
$this->add_shared_calculation_info($sampleid, $useractionevents);
return self::get_max_value();
}
}
return self::get_min_value();
}
/**
* Returns the number of params declared in core_calendar_provide_event_action's implementation.
*
* @param string $modulename The module name
* @return int
*/
private function get_provide_event_action_num_params(string $modulename) {
$functionname = 'mod_' . $modulename . '_core_calendar_provide_event_action';
$reflection = new \ReflectionFunction($functionname);
return $reflection->getNumberOfParameters();
}
}
@@ -0,0 +1,87 @@
<?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 enabled set indicator.
*
* @package core_course
* @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_course\analytics\indicator;
defined('MOODLE_INTERNAL') || die();
require_once($CFG->dirroot . '/lib/completionlib.php');
/**
* Completion enabled set indicator.
*
* @package core_course
* @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class completion_enabled extends \core_analytics\local\indicator\binary {
/**
* get_name
*
* @return new \lang_string
*/
public static function get_name(): \lang_string {
return new \lang_string('indicator:completionenabled', 'moodle');
}
/**
* required_sample_data
*
* @return string[]
*/
public static function required_sample_data() {
// Minimum course although it also accepts course_modules.
return array('course');
}
/**
* Is completion enabled? Work both with courses and activities.
*
* @param int $sampleid
* @param string $sampleorigin
* @param int|false $notusedstarttime
* @param int|false $notusedendtime
* @return float
*/
public function calculate_sample($sampleid, $sampleorigin, $notusedstarttime = false, $notusedendtime = false) {
$course = $this->retrieve('course', $sampleid);
// It may not be available, but if it is the indicator checks if completion is enabled for the cm.
$cm = $this->retrieve('course_modules', $sampleid);
$completion = new \completion_info($course);
if (!$completion->is_enabled($cm)) {
$value = self::get_min_value();
} else if (!$cm && !$completion->has_criteria()) {
// Course completion enabled with no criteria counts as nothing.
$value = self::get_min_value();
} else {
$value = self::get_max_value();
}
return $value;
}
}
@@ -0,0 +1,95 @@
<?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/>.
/**
* No student indicator.
*
* @package core_course
* @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_course\analytics\indicator;
defined('MOODLE_INTERNAL') || die();
/**
* No student indicator.
*
* @package core_course
* @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class no_student extends \core_analytics\local\indicator\binary {
/**
* Student role ids.
*
* @var array|null
*/
protected $studentroleids = null;
/**
* Returns the name.
*
* If there is a corresponding '_help' string this will be shown as well.
*
* @return \lang_string
*/
public static function get_name(): \lang_string {
return new \lang_string('indicator:nostudent', 'moodle');
}
/**
* required_sample_data
*
* @return string[]
*/
public static function required_sample_data() {
// We require course because, although calculate_sample only reads context, we need the context to be course
// or below.
return array('context', 'course');
}
/**
* calculate_sample
*
* @param int $sampleid
* @param string $sampleorigin
* @param int|false $notusedstarttime
* @param int|false $notusedendtime
* @return float
*/
public function calculate_sample($sampleid, $sampleorigin, $notusedstarttime = false, $notusedendtime = false) {
$context = $this->retrieve('context', $sampleid);
if (is_null($this->studentroleids)) {
$this->studentroleids = array_keys(get_archetype_roles('student'));
}
foreach ($this->studentroleids as $role) {
// We look for roles, not enrolments as a student assigned at category level is supposed to be a
// course student.
$students = get_role_users($role, $context, false, 'u.id', 'u.id');
if ($students) {
return self::get_max_value();
}
}
return self::get_min_value();
}
}
@@ -0,0 +1,95 @@
<?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/>.
/**
* No teacher indicator.
*
* @package core_course
* @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_course\analytics\indicator;
defined('MOODLE_INTERNAL') || die();
/**
* No teacher indicator.
*
* @package core_course
* @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class no_teacher extends \core_analytics\local\indicator\binary {
/**
* Teacher role ids.
*
* @var array|null
*/
protected $teacherroleids = null;
/**
* Returns the name.
*
* If there is a corresponding '_help' string this will be shown as well.
*
* @return \lang_string
*/
public static function get_name(): \lang_string {
return new \lang_string('indicator:noteacher', 'moodle');
}
/**
* required_sample_data
*
* @return string[]
*/
public static function required_sample_data() {
// We require course because, although calculate_sample only reads context, we need the context to be course
// or below.
return array('context', 'course');
}
/**
* calculate_sample
*
* @param int $sampleid
* @param string $sampleorigin
* @param int|false $notusedstarttime
* @param int|false $notusedendtime
* @return float
*/
public function calculate_sample($sampleid, $sampleorigin, $notusedstarttime = false, $notusedendtime = false) {
$context = $this->retrieve('context', $sampleid);
if (is_null($this->teacherroleids)) {
$this->teacherroleids = array_keys(get_archetype_roles('editingteacher') + get_archetype_roles('teacher'));
}
foreach ($this->teacherroleids as $role) {
// We look for roles, not enrolments as a teacher assigned at category level is supposed to be a
// course teacher.
$teachers = get_role_users($role, $context, false, 'u.id', 'u.id');
if ($teachers) {
return self::get_max_value();
}
}
return self::get_min_value();
}
}
@@ -0,0 +1,136 @@
<?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/>.
/**
* Potential cognitive depth indicator.
*
* @package core_course
* @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_course\analytics\indicator;
defined('MOODLE_INTERNAL') || die();
use \core_analytics\local\indicator\community_of_inquiry_activity;
/**
* Potential cognitive depth indicator.
*
* It extends linear instead of discrete as there is a linear relation between
* the different cognitive levels activities can reach.
*
* @package core_course
* @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class potential_cognitive_depth extends \core_analytics\local\indicator\linear {
/**
* get_name
*
* @return \lang_string
*/
public static function get_name(): \lang_string {
return new \lang_string('indicator:potentialcognitive', 'moodle');
}
/**
* Specify the required data to process this indicator.
*
* @return string[]
*/
public static function required_sample_data() {
// We require course because, although this indicator can also work with course_modules we can't
// calculate anything without the course.
return array('course');
}
/**
* calculate_sample
*
* @throws \coding_exception
* @param int $sampleid
* @param string $sampleorigin
* @param int|false $notusedstarttime
* @param int|false $notusedendtime
* @return float
*/
public function calculate_sample($sampleid, $sampleorigin, $notusedstarttime = false, $notusedendtime = false) {
if ($sampleorigin === 'course_modules') {
$cm = $this->retrieve('course_modules', $sampleid);
$cminfo = \cm_info::create($cm);
$cognitivedepthindicator = $this->get_cognitive_indicator($cminfo->modname);
$potentiallevel = $cognitivedepthindicator->get_cognitive_depth_level($cminfo);
if ($potentiallevel > community_of_inquiry_activity::MAX_COGNITIVE_LEVEL) {
throw new \coding_exception('Maximum cognitive depth level is ' .
community_of_inquiry_activity::MAX_COGNITIVE_LEVEL . ', ' . $potentiallevel . ' provided by ' .
get_class($this));
}
} else {
$course = $this->retrieve('course', $sampleid);
$modinfo = get_fast_modinfo($course);
$cms = $modinfo->get_cms();
if (!$cms) {
return self::get_min_value();
}
$potentiallevel = 0;
foreach ($cms as $cm) {
if (!$cognitivedepthindicator = $this->get_cognitive_indicator($cm->modname)) {
continue;
}
$level = $cognitivedepthindicator->get_cognitive_depth_level($cm);
if ($level > community_of_inquiry_activity::MAX_COGNITIVE_LEVEL) {
throw new \coding_exception('Maximum cognitive depth level is ' .
community_of_inquiry_activity::MAX_COGNITIVE_LEVEL . ', ' . $level . ' provided by ' . get_class($this));
}
if ($level > $potentiallevel) {
$potentiallevel = $level;
}
}
}
// Values from -1 to 1 range split in 5 parts (the max cognitive depth level).
// Note that we divide by 4 because we start from -1.
$levelscore = round((self::get_max_value() - self::get_min_value()) / 4, 2);
// We substract $levelscore because we want to start from the lower score and there is no cognitive depth level 0.
return self::get_min_value() + ($levelscore * $potentiallevel) - $levelscore;
}
/**
* Returns the cognitive depth class of this indicator.
*
* @param string $modname
* @return \core_analytics\local\indicator\base|false
*/
protected function get_cognitive_indicator($modname) {
$indicators = \core_analytics\manager::get_all_indicators();
foreach ($indicators as $indicator) {
if ($indicator instanceof community_of_inquiry_activity &&
$indicator->get_indicator_type() === community_of_inquiry_activity::INDICATOR_COGNITIVE &&
$indicator->get_activity_type() === $modname) {
return $indicator;
}
}
return false;
}
}
@@ -0,0 +1,148 @@
<?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/>.
/**
* Potential social breadth indicator.
*
* @package core_course
* @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_course\analytics\indicator;
defined('MOODLE_INTERNAL') || die();
use \core_analytics\local\indicator\community_of_inquiry_activity;
/**
* Potential social breadth indicator.
*
* It extends linear instead of discrete as there is a linear relation between
* the different social levels activities can reach.
*
* @package core_course
* @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class potential_social_breadth extends \core_analytics\local\indicator\linear {
/**
* get_name
*
* @return \lang_string
*/
public static function get_name(): \lang_string {
return new \lang_string('indicator:potentialsocial', 'moodle');
}
/**
* Specify the required data to process this indicator.
*
* @return string[]
*/
public static function required_sample_data() {
// We require course because, although this indicator can also work with course_modules we can't
// calculate anything without the course.
return array('course');
}
/**
* calculate_sample
*
* @param int $sampleid
* @param string $sampleorigin
* @param int|false $notusedstarttime
* @param int|false $notusedendtime
* @return float
*/
public function calculate_sample($sampleid, $sampleorigin, $notusedstarttime = false, $notusedendtime = false) {
if ($sampleorigin === 'course_modules') {
$cm = $this->retrieve('course_modules', $sampleid);
$cminfo = \cm_info::create($cm);
$socialbreadthindicator = $this->get_social_indicator($cminfo->modname);
$potentiallevel = $socialbreadthindicator->get_social_breadth_level($cminfo);
if ($potentiallevel > community_of_inquiry_activity::MAX_SOCIAL_LEVEL) {
$this->level_not_accepted($potentiallevel);
}
} else {
$course = $this->retrieve('course', $sampleid);
$modinfo = get_fast_modinfo($course);
$cms = $modinfo->get_cms();
if (!$cms) {
return self::get_min_value();
}
$potentiallevel = 0;
foreach ($cms as $cm) {
if (!$socialbreadthindicator = $this->get_social_indicator($cm->modname)) {
continue;
}
$level = $socialbreadthindicator->get_social_breadth_level($cm);
if ($level > community_of_inquiry_activity::MAX_SOCIAL_LEVEL) {
$this->level_not_accepted($level);
}
if ($level > $potentiallevel) {
$potentiallevel = $level;
}
}
}
// Core activities social breadth only reaches level 2, until core activities social
// breadth do not reach level 5 we limit it to what we currently support, which is level 2.
if ($potentiallevel > 2) {
$potentiallevel = 2;
}
// Supporting only social breadth level 1 and 2 the possible values are -1 or 1.
$levelscore = round(self::get_max_value() - self::get_min_value(), 2);
// We substract $levelscore because we want to start from the lower socre and there is no cognitive depth level 0.
return self::get_min_value() + ($levelscore * $potentiallevel) - $levelscore;
}
/**
* Returns the social breadth class of this indicator.
*
* @param string $modname
* @return \core_analytics\local\indicator\base|false
*/
protected function get_social_indicator($modname) {
$indicators = \core_analytics\manager::get_all_indicators();
foreach ($indicators as $indicator) {
if ($indicator instanceof community_of_inquiry_activity &&
$indicator->get_indicator_type() === community_of_inquiry_activity::INDICATOR_SOCIAL &&
$indicator->get_activity_type() === $modname) {
return $indicator;
}
}
return false;
}
/**
* Throw a \coding_exception.
*
* @param int $level
*/
protected function level_not_accepted($level) {
throw new \coding_exception('Activities\' potential social breadth go from 1 to ' .
community_of_inquiry_activity::MAX_SOCIAL_LEVEL . '.');
}
}
@@ -0,0 +1,141 @@
<?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/>.
/**
* Course competencies achievement target.
*
* @package core_course
* @copyright 2019 Victor Deniz <victor@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_course\analytics\target;
defined('MOODLE_INTERNAL') || die();
/**
* Course competencies achievement target.
*
* @package core_course
* @copyright 2019 Victor Deniz <victor@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class course_competencies extends course_enrolments {
/**
* Number of competencies assigned per course.
* @var int[]
*/
protected $coursecompetencies = array();
/**
* Count the competencies in a course.
*
* Save the value in $coursecompetencies array to prevent new accesses to the database.
*
* @param int $courseid The course id.
* @return int Number of competencies assigned to the course.
*/
protected function get_num_competencies_in_course($courseid) {
if (!isset($this->coursecompetencies[$courseid])) {
$ccs = \core_competency\api::count_competencies_in_course($courseid);
// Save the number of competencies per course to avoid another database access in calculate_sample().
$this->coursecompetencies[$courseid] = $ccs;
} else {
$ccs = $this->coursecompetencies[$courseid];
}
return $ccs;
}
/**
* Returns the name.
*
* If there is a corresponding '_help' string this will be shown as well.
*
* @return \lang_string
*/
public static function get_name(): \lang_string {
return new \lang_string('target:coursecompetencies', 'course');
}
/**
* Returns descriptions for each of the values the target calculation can return.
*
* @return string[]
*/
protected static function classes_description() {
return array(
get_string('targetlabelstudentcompetenciesno', 'course'),
get_string('targetlabelstudentcompetenciesyes', 'course'),
);
}
/**
* Discards courses that are not yet ready to be used for training or prediction.
*
* @param \core_analytics\analysable $course
* @param bool $fortraining
* @return true|string
*/
public function is_valid_analysable(\core_analytics\analysable $course, $fortraining = true) {
$isvalid = parent::is_valid_analysable($course, $fortraining);
if (is_string($isvalid)) {
return $isvalid;
}
$ccs = $this->get_num_competencies_in_course($course->get_id());
if (!$ccs) {
return get_string('nocompetenciesincourse', 'tool_lp');
}
return true;
}
/**
* To have the proficiency or not in each of the competencies assigned to the course sets the target value.
*
* @param int $sampleid
* @param \core_analytics\analysable $course
* @param int $starttime
* @param int $endtime
* @return float|null 0 -> competencies achieved, 1 -> competencies not achieved
*/
protected function calculate_sample($sampleid, \core_analytics\analysable $course, $starttime = false, $endtime = false) {
if (!$this->enrolment_active_during_analysis_time($sampleid, $starttime, $endtime)) {
// We should not use this sample as the analysis results could be misleading.
return null;
}
$userenrol = $this->retrieve('user_enrolments', $sampleid);
$key = $course->get_id();
// Number of competencies in the course.
$ccs = $this->get_num_competencies_in_course($key);
// Number of proficient competencies in the same course for the user.
$ucs = \core_competency\api::count_proficient_competencies_in_course_for_user($key, $userenrol->userid);
// If they are the equals, the user achieved all the competencies assigned to the course.
if ($ccs == $ucs) {
return 0;
}
return 1;
}
}
@@ -0,0 +1,114 @@
<?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/>.
/**
* Course completion target.
*
* @package core_course
* @copyright 2019 Victor Deniz <victor@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_course\analytics\target;
defined('MOODLE_INTERNAL') || die();
require_once($CFG->dirroot . '/course/lib.php');
require_once($CFG->dirroot . '/lib/completionlib.php');
require_once($CFG->dirroot . '/completion/completion_completion.php');
/**
* Course completion target.
*
* @package core_course
* @copyright 2019 Victor Deniz <victor@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class course_completion extends course_enrolments {
/**
* Returns the name.
*
* If there is a corresponding '_help' string this will be shown as well.
*
* @return \lang_string
*/
public static function get_name(): \lang_string {
return new \lang_string('target:coursecompletion', 'course');
}
/**
* Returns descriptions for each of the values the target calculation can return.
*
* @return string[]
*/
protected static function classes_description() {
return array(
get_string('targetlabelstudentcompletionno', 'course'),
get_string('targetlabelstudentcompletionyes', 'course')
);
}
/**
* Discards courses that are not yet ready to be used for training or prediction.
*
* @param \core_analytics\analysable $course
* @param bool $fortraining
* @return true|string
*/
public function is_valid_analysable(\core_analytics\analysable $course, $fortraining = true) {
$isvalid = parent::is_valid_analysable($course, $fortraining);
if (is_string($isvalid)) {
return $isvalid;
}
// Not a valid target if completion is not enabled or there are not completion criteria defined.
$completion = new \completion_info($course->get_course_data());
if (!$completion->is_enabled() || !$completion->has_criteria()) {
return get_string('completionnotenabledforcourse', 'completion');
}
return true;
}
/**
* Course completion sets the target value.
*
* @param int $sampleid
* @param \core_analytics\analysable $course
* @param int $starttime
* @param int $endtime
* @return float|null 0 -> course not completed, 1 -> course completed
*/
protected function calculate_sample($sampleid, \core_analytics\analysable $course, $starttime = false, $endtime = false) {
if (!$this->enrolment_active_during_analysis_time($sampleid, $starttime, $endtime)) {
// We should not use this sample as the analysis results could be misleading.
return null;
}
$userenrol = $this->retrieve('user_enrolments', $sampleid);
// We use completion as a success metric.
$ccompletion = new \completion_completion(array('userid' => $userenrol->userid, 'course' => $course->get_id()));
if ($ccompletion->is_complete()) {
return 0;
} else {
return 1;
}
}
}
@@ -0,0 +1,154 @@
<?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/>.
/**
* Drop out course target.
*
* @package core_course
* @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_course\analytics\target;
defined('MOODLE_INTERNAL') || die();
require_once($CFG->dirroot . '/course/lib.php');
require_once($CFG->dirroot . '/lib/gradelib.php');
require_once($CFG->dirroot . '/lib/completionlib.php');
require_once($CFG->dirroot . '/completion/completion_completion.php');
/**
* Drop out course target.
*
* @package core_course
* @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class course_dropout extends course_enrolments {
/**
* Returns the name.
*
* If there is a corresponding '_help' string this will be shown as well.
*
* @return \lang_string
*/
public static function get_name(): \lang_string {
return new \lang_string('target:coursedropout', 'course');
}
/**
* classes_description
*
* @return string[]
*/
protected static function classes_description() {
return array(
get_string('targetlabelstudentdropoutno', 'course'),
get_string('targetlabelstudentdropoutyes', 'course')
);
}
/**
* Discards courses that are not yet ready to be used for training or prediction.
*
* @param \core_analytics\analysable $course
* @param bool $fortraining
* @return true|string
*/
public function is_valid_analysable(\core_analytics\analysable $course, $fortraining = true) {
global $DB;
$isvalid = parent::is_valid_analysable($course, $fortraining);
if (is_string($isvalid)) {
return $isvalid;
}
if ($fortraining) {
// Not a valid target for training if there are not enough course accesses between the course start and end dates.
$params = array('courseid' => $course->get_id(), 'anonymous' => 0, 'start' => $course->get_start(),
'end' => $course->get_end());
list($studentssql, $studentparams) = $DB->get_in_or_equal($this->students, SQL_PARAMS_NAMED);
// Using anonymous to use the db index, not filtering by timecreated to speed it up.
$select = 'courseid = :courseid AND anonymous = :anonymous AND timecreated > :start AND timecreated < :end ' .
'AND userid ' . $studentssql;
if (!$logstore = \core_analytics\manager::get_analytics_logstore()) {
throw new \coding_exception('No available log stores');
}
$nlogs = $logstore->get_events_select_count($select, array_merge($params, $studentparams));
// At least a minimum of students activity.
$nstudents = count($this->students);
if ($nlogs / $nstudents < 10) {
return get_string('nocourseactivity', 'course');
}
}
return true;
}
/**
* calculate_sample
*
* The meaning of a drop out changes depending on the settings enabled in the course. Following these priorities order:
* 1.- Course completion
* 2.- No logs during the last quarter of the course
*
* @param int $sampleid
* @param \core_analytics\analysable $course
* @param int $starttime
* @param int $endtime
* @return float|null 0 -> not at risk, 1 -> at risk
*/
protected function calculate_sample($sampleid, \core_analytics\analysable $course, $starttime = false, $endtime = false) {
if (!$this->enrolment_active_during_analysis_time($sampleid, $starttime, $endtime)) {
// We should not use this sample as the analysis results could be misleading.
return null;
}
$userenrol = $this->retrieve('user_enrolments', $sampleid);
// We use completion as a success metric only when it is enabled.
$completion = new \completion_info($course->get_course_data());
if ($completion->is_enabled() && $completion->has_criteria()) {
$ccompletion = new \completion_completion(array('userid' => $userenrol->userid, 'course' => $course->get_id()));
if ($ccompletion->is_complete()) {
return 0;
} else {
return 1;
}
}
if (!$logstore = \core_analytics\manager::get_analytics_logstore()) {
throw new \coding_exception('No available log stores');
}
// No logs during the last quarter of the course.
$courseduration = $course->get_end() - $course->get_start();
$limit = intval($course->get_end() - ($courseduration / 4));
$select = "courseid = :courseid AND userid = :userid AND timecreated > :limit";
$params = array('userid' => $userenrol->userid, 'courseid' => $course->get_id(), 'limit' => $limit);
$nlogs = $logstore->get_events_select_count($select, $params);
if ($nlogs == 0) {
return 1;
}
return 0;
}
}
@@ -0,0 +1,393 @@
<?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/>.
/**
* Base class for targets whose analysable is a course using user enrolments as samples.
*
* @package core_course
* @copyright 2019 Victor Deniz <victor@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_course\analytics\target;
defined('MOODLE_INTERNAL') || die();
/**
* Base class for targets whose analysable is a course using user enrolments as samples.
*
* @package core_course
* @copyright 2019 Victor Deniz <victor@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
abstract class course_enrolments extends \core_analytics\local\target\binary {
/**
* @var string
*/
const MESSAGE_ACTION_NAME = 'studentmessage';
/**
* @var float
*/
const ENROL_ACTIVE_PERCENT_REQUIRED = 0.7;
/**
* Students in the course.
* @var int[]
*/
protected $students;
/**
* Returns the analyser class that should be used along with this target.
*
* @return string The full class name as a string
*/
public function get_analyser_class() {
return '\core\analytics\analyser\student_enrolments';
}
/**
* Only past stuff.
*
* @param \core_analytics\local\time_splitting\base $timesplitting
* @return bool
*/
public function can_use_timesplitting(\core_analytics\local\time_splitting\base $timesplitting): bool {
return ($timesplitting instanceof \core_analytics\local\time_splitting\before_now);
}
/**
* Overwritten to show a simpler language string.
*
* @param int $modelid
* @param \context $context
* @return string
*/
public function get_insight_subject(int $modelid, \context $context) {
return get_string('studentsatriskincourse', 'course', $context->get_context_name(false));
}
/**
* Returns the body message for the insight.
*
* @param \context $context
* @param string $contextname
* @param \stdClass $user
* @param \moodle_url $insighturl
* @return string[] The plain text message and the HTML message
*/
public function get_insight_body(\context $context, string $contextname, \stdClass $user, \moodle_url $insighturl): array {
global $OUTPUT;
$a = (object)['coursename' => $contextname, 'userfirstname' => $user->firstname];
$fullmessage = get_string('studentsatriskinfomessage', 'course', $a) . PHP_EOL . PHP_EOL . $insighturl->out(false);
$fullmessagehtml = $OUTPUT->render_from_template('core_analytics/insight_info_message',
['url' => $insighturl->out(false), 'insightinfomessage' => get_string('studentsatriskinfomessage', 'course', $a)]
);
return [$fullmessage, $fullmessagehtml];
}
/**
* Discards courses that are not yet ready to be used for training or prediction.
*
* @param \core_analytics\analysable $course
* @param bool $fortraining
* @return true|string
*/
public function is_valid_analysable(\core_analytics\analysable $course, $fortraining = true) {
if (!$course->was_started()) {
return get_string('coursenotyetstarted', 'course');
}
if (!$fortraining && !$course->get_course_data()->visible) {
return get_string('hiddenfromstudents');
}
if (!$this->students = $course->get_students()) {
return get_string('nocoursestudents', 'course');
}
if (!course_format_uses_sections($course->get_course_data()->format)) {
// We can not split activities in time ranges.
return get_string('nocoursesections', 'course');
}
if ($course->get_end() == 0) {
// We require time end to be set.
return get_string('nocourseendtime', 'course');
}
if ($course->get_end() < $course->get_start()) {
return get_string('errorendbeforestart', 'course');
}
// A course that lasts longer than 1 year probably have wrong start or end dates.
if ($course->get_end() - $course->get_start() > (YEARSECS + (WEEKSECS * 4))) {
return get_string('coursetoolong', 'course');
}
// Finished courses can not be used to get predictions.
if (!$fortraining && $course->is_finished()) {
return get_string('coursealreadyfinished', 'course');
}
if ($fortraining) {
// Ongoing courses data can not be used to train.
if (!$course->is_finished()) {
return get_string('coursenotyetfinished', 'course');
}
}
return true;
}
/**
* Discard student enrolments that are invalid.
*
* Note that this method assumes that the target is only interested in enrolments that are/were active
* between the current course start and end times. Targets interested in predicting students at risk before
* their enrolment start and targets interested in getting predictions for students whose enrolment already
* finished should overwrite this method as these students are discarded by this method.
*
* @param int $sampleid
* @param \core_analytics\analysable $course
* @param bool $fortraining
* @return bool
*/
public function is_valid_sample($sampleid, \core_analytics\analysable $course, $fortraining = true) {
$now = time();
$userenrol = $this->retrieve('user_enrolments', $sampleid);
if ($userenrol->timeend && $course->get_start() > $userenrol->timeend) {
// Discard enrolments which time end is prior to the course start. This should get rid of
// old user enrolments that remain on the course.
return false;
}
$limit = $course->get_start() - (YEARSECS + (WEEKSECS * 4));
if (($userenrol->timestart && $userenrol->timestart < $limit) ||
(!$userenrol->timestart && $userenrol->timecreated < $limit)) {
// Following what we do in is_valid_analysable, we will discard enrolments that last more than 1 academic year
// because they have incorrect start and end dates or because they are reused along multiple years
// without removing previous academic years students. This may not be very accurate because some courses
// can last just some months, but it is better than nothing.
return false;
}
if ($course->get_end()) {
if (($userenrol->timestart && $userenrol->timestart > $course->get_end()) ||
(!$userenrol->timestart && $userenrol->timecreated > $course->get_end())) {
// Discard user enrolments that start after the analysable official end.
return false;
}
}
if ($now < $userenrol->timestart && $userenrol->timestart) {
// Discard enrolments whose start date is after now (no need to check timecreated > $now :P).
return false;
}
if (!$fortraining && $userenrol->timeend && $userenrol->timeend < $now) {
// We don't want to generate predictions for finished enrolments.
return false;
}
return true;
}
/**
* prediction_actions
*
* @param \core_analytics\prediction $prediction
* @param bool $includedetailsaction
* @param bool $isinsightuser
* @return \core_analytics\prediction_action[]
*/
public function prediction_actions(\core_analytics\prediction $prediction, $includedetailsaction = false,
$isinsightuser = false) {
$actions = array();
$sampledata = $prediction->get_sample_data();
$studentid = $sampledata['user']->id;
// View outline report.
$url = new \moodle_url('/report/outline/user.php', array('id' => $studentid, 'course' => $sampledata['course']->id,
'mode' => 'outline'));
$pix = new \pix_icon('i/report', get_string('outlinereport'));
$actions[] = new \core_analytics\prediction_action('viewoutlinereport', $prediction, $url, $pix,
get_string('outlinereport'), false, ['target' => '_blank']);
return array_merge(parent::prediction_actions($prediction, $includedetailsaction, $isinsightuser), $actions);
}
/**
* Suggested bulk actions for a user.
*
* @param \core_analytics\prediction[] $predictions List of predictions suitable for the bulk actions to use.
* @return \core_analytics\bulk_action[] The list of bulk actions.
*/
public function bulk_actions(array $predictions) {
$actions = [];
$userids = [];
foreach ($predictions as $prediction) {
$sampledata = $prediction->get_sample_data();
$userid = $sampledata['user']->id;
// Indexed by prediction id because we want the predictionid-userid
// mapping later when sending the message.
$userids[$prediction->get_prediction_data()->id] = $userid;
}
// Send a message for all the students.
$attrs = array(
'data-bulk-sendmessage' => '1',
'data-prediction-to-user-id' => json_encode($userids)
);
$actions[] = new \core_analytics\bulk_action(self::MESSAGE_ACTION_NAME, new \moodle_url(''),
new \pix_icon('t/message', get_string('sendmessage', 'message')),
get_string('sendmessage', 'message'), true, $attrs);
return array_merge($actions, parent::bulk_actions($predictions));
}
/**
* Adds the JS required to run the bulk actions.
*/
public function add_bulk_actions_js() {
global $PAGE;
$PAGE->requires->js_call_amd('report_insights/message_users', 'init',
['.insights-bulk-actions', self::MESSAGE_ACTION_NAME]);
parent::add_bulk_actions_js();
}
/**
* Is/was this user enrolment active during most of the analysis interval?
*
* This method discards enrolments that were not active during most of the analysis interval. It is
* important to discard these enrolments because the indicator calculations can lead to misleading
* results.
*
* Note that this method assumes that the target is interested in enrolments that are/were active
* during the analysis interval. Targets interested in predicting students at risk before
* their enrolment start should not call this method. Similarly, targets interested in getting
* predictions for students whose enrolment already finished should not call this method either.
*
* @param int $sampleid The id of the sample that is being calculated
* @param int $starttime The analysis interval start time
* @param int $endtime The analysis interval end time
* @return bool
*/
protected function enrolment_active_during_analysis_time(int $sampleid, int $starttime, int $endtime) {
$userenrol = $this->retrieve('user_enrolments', $sampleid);
if (!empty($userenrol->timestart)) {
$enrolstart = $userenrol->timestart;
} else {
// This is always set.
$enrolstart = $userenrol->timecreated;
}
if (!empty($userenrol->timeend)) {
$enrolend = $userenrol->timeend;
} else {
// Default to tre end of the world.
$enrolend = PHP_INT_MAX;
}
if ($endtime && $endtime < $enrolstart) {
/* The enrolment starts/ed after the analysis end time.
* |=========| |----------|
* A start A end E start E end
*/
return false;
}
if ($starttime && $enrolend < $starttime) {
/* The enrolment finishes/ed before the analysis start time.
* |---------| |==========|
* E start E end A start A end
*/
return false;
}
// Now we want to discard enrolments that were not active for most of the analysis interval. We
// need both a $starttime and an $endtime to calculate this.
if (!$starttime) {
// Early return. Nothing to discard if there is no start.
return true;
}
if (!$endtime) {
// We can not calculate in relative terms (percent) how far from the analysis start time
// this enrolment start is/was.
return true;
}
if ($enrolstart < $starttime && $endtime < $enrolend) {
/* The enrolment is active during all the analysis time.
* |-----------------------------|
* |========|
* E start A start A end E end
*/
return true;
}
// If we reach this point is because the enrolment is only active for a portion of the analysis interval.
// Therefore, we check that it was active for most of the analysis interval, a self::ENROL_ACTIVE_PERCENT_REQUIRED.
if ($starttime <= $enrolstart && $enrolend <= $endtime) {
/* |=============================|
* |--------|
* A start E start E end A end
*/
$activeenrolduration = $enrolend - $enrolstart;
} else if ($enrolstart <= $starttime && $enrolend <= $endtime) {
/* |===================|
* |------------------|
* E start A start E end A end
*/
$activeenrolduration = $enrolend - $starttime;
} else if ($starttime <= $enrolstart && $endtime <= $enrolend) {
/* |===================|
* |------------------|
* A start E start A end E end
*/
$activeenrolduration = $endtime - $enrolstart;
}
$analysisduration = $endtime - $starttime;
if (floatval($activeenrolduration) / floatval($analysisduration) < self::ENROL_ACTIVE_PERCENT_REQUIRED) {
// The student was not enroled in the course for most of the analysis interval.
return false;
}
// We happily return true if the enrolment was active for more than self::ENROL_ACTIVE_PERCENT_REQUIRED of
// the analysis interval.
return true;
}
}
@@ -0,0 +1,186 @@
<?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/>.
/**
* Getting the minimum grade to pass target.
*
* @package core_course
* @copyright 2019 Victor Deniz <victor@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_course\analytics\target;
defined('MOODLE_INTERNAL') || die();
/**
* Getting the minimum grade to pass target.
*
* @package core_course
* @copyright 2019 Victor Deniz <victor@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class course_gradetopass extends course_enrolments {
/**
* Courses grades to pass.
* @var mixed[]
*/
protected $coursesgradetopass = array();
/**
* Courses grades.
* @var mixed[]
*/
protected $coursesgrades = array();
/**
* Returns the grade to pass a course.
*
* Save the value in $coursesgradetopass array to prevent new accesses to the database.
*
* @param int $courseid The course id.
* @return array The courseitem id and the required grade to pass the course.
*/
protected function get_course_gradetopass($courseid) {
if (!isset($this->coursesgradetopass[$courseid])) {
// Get course grade_item.
$courseitem = \grade_item::fetch_course_item($courseid);
$ci = array();
$ci['courseitemid'] = $courseitem->id;
if ($courseitem->gradetype == GRADE_TYPE_VALUE && grade_floats_different($courseitem->gradepass, 0.0)) {
$ci['gradetopass'] = $courseitem->gradepass;
} else {
$ci['gradetopass'] = null;
}
$this->coursesgradetopass[$courseid] = $ci;
}
return $this->coursesgradetopass[$courseid];
}
/**
* Returns the grade of a user in a course.
*
* Saves the grades of all course users in $coursesgrades array to prevent new accesses to the database.
*
* @param int $courseitemid The course item id.
* @param int $userid the user whose grade is requested.
* @return array The courseitem id and the required grade to pass the course.
*/
protected function get_user_grade($courseitemid, $userid) {
// If the user grade for this course is not available, get all the grades for the course.
if (!isset($this->coursesgrades[$courseitemid])) {
// Ony a course is cached to avoid high memory usage.
unset($this->coursesgrades);
$gg = new \grade_grade(null, false);
$usersgrades = $gg->fetch_all(array('itemid' => $courseitemid));
if ($usersgrades) {
foreach ($usersgrades as $ug) {
$this->coursesgrades[$courseitemid][$ug->userid] = $ug->finalgrade;
}
}
}
if (!isset($this->coursesgrades[$courseitemid][$userid])) {
$this->coursesgrades[$courseitemid][$userid] = null;
}
return $this->coursesgrades[$courseitemid][$userid];
}
/**
* Returns the name.
*
* If there is a corresponding '_help' string this will be shown as well.
*
* @return \lang_string
*/
public static function get_name(): \lang_string {
return new \lang_string('target:coursegradetopass', 'course');
}
/**
* Returns descriptions for each of the values the target calculation can return.
*
* @return string[]
*/
protected static function classes_description() {
return array(
get_string('targetlabelstudentgradetopassno', 'course'),
get_string('targetlabelstudentgradetopassyes', 'course')
);
}
/**
* Discards courses that are not yet ready to be used for training or prediction.
*
* Only courses with "value" grade type and grade to pass set are valid.
*
* @param \core_analytics\analysable $course
* @param bool $fortraining
* @return true|string
*/
public function is_valid_analysable(\core_analytics\analysable $course, $fortraining = true) {
$isvalid = parent::is_valid_analysable($course, $fortraining);
if (is_string($isvalid)) {
return $isvalid;
}
$courseitem = $this->get_course_gradetopass ($course->get_id());
if (is_null($courseitem['gradetopass'])) {
return get_string('gradetopassnotset', 'course');
}
return true;
}
/**
* The user's grade in the course sets the target value.
*
* @param int $sampleid
* @param \core_analytics\analysable $course
* @param int $starttime
* @param int $endtime
* @return float|null 0 -> course grade to pass achieved, 1 -> course grade to pass not achieved
*/
protected function calculate_sample($sampleid, \core_analytics\analysable $course, $starttime = false, $endtime = false) {
if (!$this->enrolment_active_during_analysis_time($sampleid, $starttime, $endtime)) {
// We should not use this sample as the analysis results could be misleading.
return null;
}
$userenrol = $this->retrieve('user_enrolments', $sampleid);
// Get course grade to pass.
$courseitem = $this->get_course_gradetopass($course->get_id());
// Get the user grade.
$usergrade = $this->get_user_grade($courseitem['courseitemid'], $userenrol->userid);
if ($usergrade >= $courseitem['gradetopass']) {
return 0;
}
return 1;
}
}
@@ -0,0 +1,80 @@
<?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/>.
/**
* No accesses since the start of the course.
*
* @package core_course
* @copyright 2019 David Monllaó {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_course\analytics\target;
defined('MOODLE_INTERNAL') || die();
/**
* No accesses since the start of the course.
*
* @package core_course
* @copyright 2019 David Monllaó {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class no_access_since_course_start extends no_recent_accesses {
/**
* Only past stuff whose start matches the course start.
*
* @param \core_analytics\local\time_splitting\base $timesplitting
* @return bool
*/
public function can_use_timesplitting(\core_analytics\local\time_splitting\base $timesplitting): bool {
return ($timesplitting instanceof \core_analytics\local\time_splitting\after_start);
}
/**
* Returns the name.
*
* If there is a corresponding '_help' string this will be shown as well.
*
* @return \lang_string
*/
public static function get_name(): \lang_string {
return new \lang_string('target:noaccesssincecoursestart', 'course');
}
/**
* Returns the body message for the insight.
*
* @param \context $context
* @param string $contextname
* @param \stdClass $user
* @param \moodle_url $insighturl
* @return array The plain text message and the HTML message
*/
public function get_insight_body(\context $context, string $contextname, \stdClass $user, \moodle_url $insighturl): array {
global $OUTPUT;
$a = (object)['coursename' => $contextname, 'userfirstname' => $user->firstname];
$fullmessage = get_string('noaccesssincestartinfomessage', 'course', $a) . PHP_EOL . PHP_EOL . $insighturl->out(false);
$fullmessagehtml = $OUTPUT->render_from_template('core_analytics/insight_info_message',
['url' => $insighturl->out(false), 'insightinfomessage' => get_string('noaccesssincestartinfomessage', 'course', $a)]
);
return [$fullmessage, $fullmessagehtml];
}
}
@@ -0,0 +1,144 @@
<?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/>.
/**
* No recent accesses.
*
* @package core_course
* @copyright 2019 David Monllaó {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_course\analytics\target;
defined('MOODLE_INTERNAL') || die();
/**
* No recent accesses.
*
* @package core_course
* @copyright 2019 David Monllaó {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class no_recent_accesses extends course_enrolments {
/**
* Machine learning backends are not required to predict.
*
* @return bool
*/
public static function based_on_assumptions() {
return true;
}
/**
* Returns the name.
*
* If there is a corresponding '_help' string this will be shown as well.
*
* @return \lang_string
*/
public static function get_name(): \lang_string {
return new \lang_string('target:norecentaccesses', 'course');
}
/**
* Returns the body message for the insight.
*
* @param \context $context
* @param string $contextname
* @param \stdClass $user
* @param \moodle_url $insighturl
* @return array The plain text message and the HTML message
*/
public function get_insight_body(\context $context, string $contextname, \stdClass $user, \moodle_url $insighturl): array {
global $OUTPUT;
$a = (object)['coursename' => $contextname, 'userfirstname' => $user->firstname];
$fullmessage = get_string('norecentaccessesinfomessage', 'course', $a) . PHP_EOL . PHP_EOL . $insighturl->out(false);
$fullmessagehtml = $OUTPUT->render_from_template('core_analytics/insight_info_message',
['url' => $insighturl->out(false), 'insightinfomessage' => get_string('norecentaccessesinfomessage', 'course', $a)]
);
return [$fullmessage, $fullmessagehtml];
}
/**
* Only past stuff whose start matches the course start.
*
* @param \core_analytics\local\time_splitting\base $timesplitting
* @return bool
*/
public function can_use_timesplitting(\core_analytics\local\time_splitting\base $timesplitting): bool {
return ($timesplitting instanceof \core_analytics\local\time_splitting\past_periodic);
}
/**
* Discards courses that are not yet ready to be used for prediction.
*
* @param \core_analytics\analysable $course
* @param bool $fortraining
* @return true|string
*/
public function is_valid_analysable(\core_analytics\analysable $course, $fortraining = true) {
if (!$course->was_started()) {
return get_string('coursenotyetstarted', 'course');
}
if (!$this->students = $course->get_students()) {
return get_string('nocoursestudents', 'course');
}
if (!$fortraining && !$course->get_course_data()->visible) {
return get_string('hiddenfromstudents');
}
if ($course->get_end() && $course->get_end() < $course->get_start()) {
return get_string('errorendbeforestart', 'course');
}
// Finished courses can not be used to get predictions.
if (!$fortraining && $course->is_finished()) {
return get_string('coursealreadyfinished', 'course');
}
return true;
}
/**
* Do the user has any read action in the course?
*
* @param int $sampleid
* @param \core_analytics\analysable $analysable
* @param int $starttime
* @param int $endtime
* @return float|null 0 -> accesses, 1 -> no accesses.
*/
protected function calculate_sample($sampleid, \core_analytics\analysable $analysable, $starttime = false, $endtime = false) {
if (!$this->enrolment_active_during_analysis_time($sampleid, $starttime, $endtime)) {
// We should not use this sample as the analysis results could be misleading.
return null;
}
$readactions = $this->retrieve('\core\analytics\indicator\any_course_access', $sampleid);
if ($readactions == \core\analytics\indicator\any_course_access::get_min_value()) {
return 1;
}
return 0;
}
}
@@ -0,0 +1,212 @@
<?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/>.
/**
* No teaching target.
*
* @package core_course
* @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_course\analytics\target;
defined('MOODLE_INTERNAL') || die();
/**
* No teaching target.
*
* @package core_course
* @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class no_teaching extends \core_analytics\local\target\binary {
/**
* Machine learning backends are not required to predict.
*
* @return bool
*/
public static function based_on_assumptions() {
return true;
}
/**
* It requires a specific time-splitting method.
*
* @param \core_analytics\local\time_splitting\base $timesplitting
* @return bool
*/
public function can_use_timesplitting(\core_analytics\local\time_splitting\base $timesplitting): bool {
return (get_class($timesplitting) === \core\analytics\time_splitting\single_range::class);
}
/**
* Returns the name.
*
* If there is a corresponding '_help' string this will be shown as well.
*
* @return \lang_string
*/
public static function get_name(): \lang_string {
return new \lang_string('target:noteachingactivity', 'course');
}
/**
* Overwritten to show a simpler language string.
*
* @param int $modelid
* @param \context $context
* @return string
*/
public function get_insight_subject(int $modelid, \context $context) {
return get_string('noteachingupcomingcourses');
}
/**
* Returns the body message for the insight.
*
* @param \context $context
* @param string $contextname
* @param \stdClass $user
* @param \moodle_url $insighturl
* @return string[] The plain text message and the HTML message
*/
public function get_insight_body(\context $context, string $contextname, \stdClass $user, \moodle_url $insighturl): array {
global $OUTPUT;
$a = (object)['userfirstname' => $user->firstname];
$fullmessage = get_string('noteachinginfomessage', 'course', $a) . PHP_EOL . PHP_EOL . $insighturl->out(false);
$fullmessagehtml = $OUTPUT->render_from_template('core_analytics/insight_info_message',
['url' => $insighturl->out(false), 'insightinfomessage' => get_string('noteachinginfomessage', 'course', $a)]
);
return [$fullmessage, $fullmessagehtml];
}
/**
* prediction_actions
*
* @param \core_analytics\prediction $prediction
* @param mixed $includedetailsaction
* @param bool $isinsightuser
* @return \core_analytics\prediction_action[]
*/
public function prediction_actions(\core_analytics\prediction $prediction, $includedetailsaction = false,
$isinsightuser = false) {
global $CFG;
require_once($CFG->dirroot . '/course/lib.php');
$sampledata = $prediction->get_sample_data();
$course = $sampledata['course'];
$actions = array();
$url = new \moodle_url('/course/view.php', array('id' => $course->id));
$pix = new \pix_icon('i/course', get_string('course'));
$actions[] = new \core_analytics\prediction_action('viewcourse', $prediction,
$url, $pix, get_string('view'));
if (course_can_view_participants($sampledata['context'])) {
$url = new \moodle_url('/user/index.php', array('id' => $course->id));
$pix = new \pix_icon('i/cohort', get_string('participants'));
$actions[] = new \core_analytics\prediction_action('viewparticipants', $prediction,
$url, $pix, get_string('participants'));
}
$parentactions = parent::prediction_actions($prediction, $includedetailsaction, $isinsightuser);
return array_merge($actions, $parentactions);
}
/**
* classes_description
*
* @return string[]
*/
protected static function classes_description() {
return array(
get_string('targetlabelteachingyes', 'course'),
get_string('targetlabelteachingno', 'course'),
);
}
/**
* get_analyser_class
*
* @return string
*/
public function get_analyser_class() {
return '\core\analytics\analyser\site_courses';
}
/**
* is_valid_analysable
*
* @param \core_analytics\analysable $analysable
* @param mixed $fortraining
* @return true|string
*/
public function is_valid_analysable(\core_analytics\analysable $analysable, $fortraining = true) {
// The analysable is the site, so yes, it is always valid.
return true;
}
/**
* Only process samples which start date is getting close.
*
* @param int $sampleid
* @param \core_analytics\analysable $analysable
* @param bool $fortraining
* @return bool
*/
public function is_valid_sample($sampleid, \core_analytics\analysable $analysable, $fortraining = true) {
$course = $this->retrieve('course', $sampleid);
$now = time();
// No courses without start date, no finished courses, no predictions before start - 1 week nor
// predictions for courses that started more than 1 week ago.
if (!$course->startdate || (!empty($course->enddate) && $course->enddate < $now) ||
$course->startdate - WEEKSECS > $now || $course->startdate + WEEKSECS < $now) {
return false;
}
return true;
}
/**
* calculate_sample
*
* @param int $sampleid
* @param \core_analytics\analysable $analysable
* @param int $starttime
* @param int $endtime
* @return float
*/
protected function calculate_sample($sampleid, \core_analytics\analysable $analysable, $starttime = false, $endtime = false) {
$noteachersindicator = $this->retrieve('\core_course\analytics\indicator\no_teacher', $sampleid);
$nostudentsindicator = $this->retrieve('\core_course\analytics\indicator\no_student', $sampleid);
if ($noteachersindicator == \core_course\analytics\indicator\no_teacher::get_min_value() ||
$nostudentsindicator == \core_course\analytics\indicator\no_student::get_min_value()) {
// No teachers or no students :(.
return 1;
}
return 0;
}
}
+103
View File
@@ -0,0 +1,103 @@
<?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_course\cache;
use cache_data_source;
use cache_definition;
use moodle_url;
use core_course_list_element;
/**
* Class to describe cache data source for course image.
*
* @package core
* @subpackage course
* @author Dmitrii Metelkin <dmitriim@catalyst-au.net>
* @copyright 2021 Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class course_image implements cache_data_source {
/** @var course_image */
protected static $instance = null;
/**
* Returns an instance of the data source class that the cache can use for loading data using the other methods
* specified by this interface.
*
* @param cache_definition $definition
* @return \core_course\cache\course_image
*/
public static function get_instance_for_cache(cache_definition $definition): course_image {
if (is_null(self::$instance)) {
self::$instance = new course_image();
}
return self::$instance;
}
/**
* Loads the data for the key provided ready formatted for caching.
*
* @param string|int $key The key to load.
* @return string|bool Returns course image url as a string or false if the image is not exist
*/
public function load_for_cache($key) {
// We should use get_course() instead of get_fast_modinfo() for better performance.
$course = get_course($key);
return $this->get_image_url_from_overview_files($course);
}
/**
* Returns image URL from course overview files.
*
* @param \stdClass $course Course object.
* @return null|string Image URL or null if it's not exists.
*/
protected function get_image_url_from_overview_files(\stdClass $course): ?string {
$courseinlist = new core_course_list_element($course);
foreach ($courseinlist->get_course_overviewfiles() as $file) {
if ($file->is_valid_image()) {
return moodle_url::make_pluginfile_url(
$file->get_contextid(),
$file->get_component(),
$file->get_filearea(),
null,
$file->get_filepath(),
$file->get_filename()
)->out();
}
}
// Returning null if no image found to let it be cached
// as false is what cache API returns then a data is not found in cache.
return null;
}
/**
* Loads several keys for the cache.
*
* @param array $keys An array of keys each of which will be string|int.
* @return array An array of matching data items.
*/
public function load_many_for_cache(array $keys): array {
$records = [];
foreach ($keys as $key) {
$records[$key] = $this->load_for_cache($key);
}
return $records;
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,244 @@
<?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/>.
/**
* Course handler for custom fields
*
* @package core_course
* @copyright 2018 David Matamoros <davidmc@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_course\customfield;
defined('MOODLE_INTERNAL') || die;
use core_customfield\api;
use core_customfield\field_controller;
/**
* Course handler for custom fields
*
* @package core_course
* @copyright 2018 David Matamoros <davidmc@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class course_handler extends \core_customfield\handler {
/**
* @var course_handler
*/
static protected $singleton;
/**
* @var \context
*/
protected $parentcontext;
/** @var int Field is displayed in the course listing, visible to everybody */
const VISIBLETOALL = 2;
/** @var int Field is displayed in the course listing but only for teachers */
const VISIBLETOTEACHERS = 1;
/** @var int Field is not displayed in the course listing */
const NOTVISIBLE = 0;
/**
* Returns a singleton
*
* @param int $itemid
* @return \core_course\customfield\course_handler
*/
public static function create(int $itemid = 0): \core_customfield\handler {
if (static::$singleton === null) {
self::$singleton = new static(0);
}
return self::$singleton;
}
/**
* Run reset code after unit tests to reset the singleton usage.
*/
public static function reset_caches(): void {
if (!PHPUNIT_TEST) {
throw new \coding_exception('This feature is only intended for use in unit tests');
}
static::$singleton = null;
}
/**
* The current user can configure custom fields on this component.
*
* @return bool true if the current can configure custom fields, false otherwise
*/
public function can_configure(): bool {
return has_capability('moodle/course:configurecustomfields', $this->get_configuration_context());
}
/**
* The current user can edit custom fields on the given course.
*
* @param field_controller $field
* @param int $instanceid id of the course to test edit permission
* @return bool true if the current can edit custom fields, false otherwise
*/
public function can_edit(field_controller $field, int $instanceid = 0): bool {
if ($instanceid) {
$context = $this->get_instance_context($instanceid);
return (!$field->get_configdata_property('locked') ||
has_capability('moodle/course:changelockedcustomfields', $context));
} else {
$context = $this->get_parent_context();
if ($context->contextlevel == CONTEXT_SYSTEM) {
return (!$field->get_configdata_property('locked') ||
has_capability('moodle/course:changelockedcustomfields', $context));
} else {
return (!$field->get_configdata_property('locked') ||
guess_if_creator_will_have_course_capability('moodle/course:changelockedcustomfields', $context));
}
}
}
/**
* The current user can view custom fields on the given course.
*
* @param field_controller $field
* @param int $instanceid id of the course to test edit permission
* @return bool true if the current can edit custom fields, false otherwise
*/
public function can_view(field_controller $field, int $instanceid): bool {
$visibility = $field->get_configdata_property('visibility');
if ($visibility == self::NOTVISIBLE) {
return false;
} else if ($visibility == self::VISIBLETOTEACHERS) {
return has_capability('moodle/course:update', $this->get_instance_context($instanceid));
} else {
return true;
}
}
/**
* Sets parent context for the course
*
* This may be needed when course is being created, there is no course context but we need to check capabilities
*
* @param \context $context
*/
public function set_parent_context(\context $context) {
$this->parentcontext = $context;
}
/**
* Returns the parent context for the course
*
* @return \context
*/
protected function get_parent_context(): \context {
global $PAGE;
if ($this->parentcontext) {
return $this->parentcontext;
} else if ($PAGE->context && $PAGE->context instanceof \context_coursecat) {
return $PAGE->context;
}
return \context_system::instance();
}
/**
* Context that should be used for new categories created by this handler
*
* @return \context the context for configuration
*/
public function get_configuration_context(): \context {
return \context_system::instance();
}
/**
* URL for configuration of the fields on this handler.
*
* @return \moodle_url The URL to configure custom fields for this component
*/
public function get_configuration_url(): \moodle_url {
return new \moodle_url('/course/customfield.php');
}
/**
* Returns the context for the data associated with the given instanceid.
*
* @param int $instanceid id of the record to get the context for
* @return \context the context for the given record
*/
public function get_instance_context(int $instanceid = 0): \context {
if ($instanceid > 0) {
return \context_course::instance($instanceid);
} else {
return \context_system::instance();
}
}
/**
* Allows to add custom controls to the field configuration form that will be saved in configdata
*
* @param \MoodleQuickForm $mform
*/
public function config_form_definition(\MoodleQuickForm $mform) {
$mform->addElement('header', 'course_handler_header', get_string('customfieldsettings', 'core_course'));
$mform->setExpanded('course_handler_header', true);
// If field is locked.
$mform->addElement('selectyesno', 'configdata[locked]', get_string('customfield_islocked', 'core_course'));
$mform->addHelpButton('configdata[locked]', 'customfield_islocked', 'core_course');
// Field data visibility.
$visibilityoptions = [self::VISIBLETOALL => get_string('customfield_visibletoall', 'core_course'),
self::VISIBLETOTEACHERS => get_string('customfield_visibletoteachers', 'core_course'),
self::NOTVISIBLE => get_string('customfield_notvisible', 'core_course')];
$mform->addElement('select', 'configdata[visibility]', get_string('customfield_visibility', 'core_course'),
$visibilityoptions);
$mform->addHelpButton('configdata[visibility]', 'customfield_visibility', 'core_course');
}
/**
* Creates or updates custom field data.
*
* @param \restore_task $task
* @param array $data
*
* @return int|void Conditionally returns the ID of the created or updated record.
*/
public function restore_instance_data_from_backup(\restore_task $task, array $data) {
$courseid = $task->get_courseid();
$context = $this->get_instance_context($courseid);
$editablefields = $this->get_editable_fields($courseid);
$records = api::get_instance_fields_data($editablefields, $courseid);
$target = $task->get_target();
$override = ($target != \backup::TARGET_CURRENT_ADDING && $target != \backup::TARGET_EXISTING_ADDING);
foreach ($records as $d) {
$field = $d->get_field();
if ($field->get('shortname') === $data['shortname'] && $field->get('type') === $data['type']) {
if (!$d->get('id') || $override) {
$d->set($d->datafield(), $data['value']);
$d->set('value', $data['value']);
$d->set('valueformat', $data['valueformat']);
$d->set('valuetrust', !empty($data['valuetrust']));
$d->set('contextid', $context->id);
$d->save();
}
return $d->get('id');
}
}
}
}
+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/>.
/**
* Delete category form.
*
* @package core_course
* @copyright 2002 onwards Martin Dougiamas (http://dougiamas.com)
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die;
require_once($CFG->libdir . '/formslib.php');
require_once($CFG->libdir . '/questionlib.php');
/**
* Delete category moodleform.
* @package core_course
* @copyright 2002 onwards Martin Dougiamas (http://dougiamas.com)
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class core_course_deletecategory_form extends moodleform {
/**
* The core_course_category object for that category being deleted.
* @var core_course_category
*/
protected $coursecat;
/**
* Defines the form.
*/
public function definition() {
$mform = $this->_form;
$this->coursecat = $this->_customdata;
$categorycontext = context_coursecat::instance($this->coursecat->id);
$categoryname = $this->coursecat->get_formatted_name();
// Check permissions, to see if it OK to give the option to delete
// the contents, rather than move elsewhere.
$candeletecontent = $this->coursecat->can_delete_full();
// Get the list of categories we might be able to move to.
$displaylist = $this->coursecat->move_content_targets_list();
// Now build the options.
$options = array();
if ($displaylist) {
$options[0] = get_string('movecontentstoanothercategory');
}
if ($candeletecontent) {
$options[1] = get_string('deleteallcannotundo');
}
if (empty($options)) {
throw new \moodle_exception('youcannotdeletecategory', 'error', 'index.php', $categoryname);
}
// Now build the form.
$mform->addElement('header', 'general', get_string('categorycurrentcontents', '', $categoryname));
// Describe the contents of this category.
$contents = '';
if ($this->coursecat->has_children()) {
$contents .= html_writer::tag('li', get_string('subcategories'));
}
if ($this->coursecat->has_courses()) {
$contents .= html_writer::tag('li', get_string('courses'));
}
if (question_context_has_any_questions($categorycontext)) {
$contents .= html_writer::tag('li', get_string('questionsinthequestionbank'));
}
// Check if plugins can provide more info.
$pluginfunctions = $this->coursecat->get_plugins_callback_function('get_course_category_contents');
foreach ($pluginfunctions as $pluginfunction) {
if ($plugincontents = $pluginfunction($this->coursecat)) {
$contents .= html_writer::tag('li', $plugincontents);
}
}
if (!empty($contents)) {
$mform->addElement('static', 'emptymessage', get_string('thiscategorycontains'), html_writer::tag('ul', $contents));
} else {
$mform->addElement('static', 'emptymessage', '', get_string('deletecategoryempty'));
}
// Give the options for what to do.
$mform->addElement('select', 'fulldelete', get_string('whattodo'), $options);
if (count($options) == 1) {
// Freeze selector if only one option available.
$optionkeys = array_keys($options);
$option = reset($optionkeys);
$mform->hardFreeze('fulldelete');
$mform->setConstant('fulldelete', $option);
}
if ($displaylist) {
$mform->addElement('autocomplete', 'newparent', get_string('movecategorycontentto'), $displaylist);
if (in_array($this->coursecat->parent, $displaylist)) {
$mform->setDefault('newparent', $this->coursecat->parent);
}
$mform->hideIf('newparent', 'fulldelete', 'eq', '1');
}
$mform->addElement('hidden', 'categoryid', $this->coursecat->id);
$mform->setType('categoryid', PARAM_ALPHANUM);
$mform->addElement('hidden', 'action', 'deletecategory');
$mform->setType('action', PARAM_ALPHANUM);
$this->add_action_buttons(true, get_string('delete'));
}
/**
* Perform some extra moodle validation.
*
* @param array $data
* @param array $files
* @return array An array of errors.
*/
public function validation($data, $files) {
$errors = parent::validation($data, $files);
if (empty($data['fulldelete']) && empty($data['newparent'])) {
// When they have chosen the move option, they must specify a destination.
$errors['newparent'] = get_string('required');
return $errors;
}
if (!empty($data['newparent']) && !$this->coursecat->can_move_content_to($data['newparent'])) {
$errors['newparent'] = get_string('movecatcontentstoselected', 'error');
}
return $errors;
}
}
+136
View File
@@ -0,0 +1,136 @@
<?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/>.
/**
* Edit category form.
*
* @package core_course
* @copyright 2002 onwards Martin Dougiamas (http://dougiamas.com)
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die;
require_once($CFG->libdir.'/formslib.php');
/**
* Edit category form.
*
* @package core_course
* @copyright 2002 onwards Martin Dougiamas (http://dougiamas.com)
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class core_course_editcategory_form extends moodleform {
/**
* The form definition.
*/
public function definition() {
global $CFG, $DB;
$mform = $this->_form;
$categoryid = $this->_customdata['categoryid'];
$parent = $this->_customdata['parent'];
// Get list of categories to use as parents, with site as the first one.
$options = array();
if (has_capability('moodle/category:manage', context_system::instance()) || $parent == 0) {
$options[0] = get_string('top');
}
if ($categoryid) {
// Editing an existing category.
$options += core_course_category::make_categories_list('moodle/category:manage', $categoryid);
if (empty($options[$parent])) {
// Ensure the the category parent has been included in the options.
$options[$parent] = $DB->get_field('course_categories', 'name', array('id'=>$parent));
}
$strsubmit = get_string('savechanges');
} else {
// Making a new category.
$options += core_course_category::make_categories_list('moodle/category:manage');
$strsubmit = get_string('createcategory');
}
$mform->addElement('autocomplete', 'parent', get_string('parentcategory'), $options);
$mform->addRule('parent', null, 'required', null, 'client');
$mform->addElement('text', 'name', get_string('categoryname'), array('size' => '30'));
$mform->addRule('name', get_string('required'), 'required', null);
$mform->setType('name', PARAM_TEXT);
$mform->addElement('text', 'idnumber', get_string('idnumbercoursecategory'), 'maxlength="100" size="10"');
$mform->addHelpButton('idnumber', 'idnumbercoursecategory');
$mform->setType('idnumber', PARAM_RAW);
$mform->addElement('editor', 'description_editor', get_string('description'), null,
$this->get_description_editor_options());
$mform->setType('description_editor', PARAM_RAW);
if (!empty($CFG->allowcategorythemes)) {
$themes = array(''=>get_string('forceno'));
$allthemes = get_list_of_themes();
foreach ($allthemes as $key => $theme) {
if (empty($theme->hidefromselector)) {
$themes[$key] = get_string('pluginname', 'theme_'.$theme->name);
}
}
$mform->addElement('select', 'theme', get_string('forcetheme'), $themes);
}
$mform->addElement('hidden', 'id', 0);
$mform->setType('id', PARAM_INT);
$mform->setDefault('id', $categoryid);
$this->add_action_buttons(true, $strsubmit);
}
/**
* Returns the description editor options.
* @return array
*/
public function get_description_editor_options() {
global $CFG;
$context = $this->_customdata['context'];
$itemid = $this->_customdata['itemid'];
return array(
'maxfiles' => EDITOR_UNLIMITED_FILES,
'maxbytes' => $CFG->maxbytes,
'trusttext' => false,
'noclean' => true,
'context' => $context,
'subdirs' => file_area_contains_subdirs($context, 'coursecat', 'description', $itemid),
);
}
/**
* Validates the data submit for this form.
*
* @param array $data An array of key,value data pairs.
* @param array $files Any files that may have been submit as well.
* @return array An array of errors.
*/
public function validation($data, $files) {
global $DB;
$errors = parent::validation($data, $files);
if (!empty($data['idnumber'])) {
if ($existing = $DB->get_record('course_categories', array('idnumber' => $data['idnumber']))) {
if (!$data['id'] || $existing->id != $data['id']) {
$errors['idnumber'] = get_string('categoryidnumbertaken', 'error');
}
}
}
return $errors;
}
}
@@ -0,0 +1,85 @@
<?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/>.
/**
* Class for exporting a course module summary from an stdClass.
*
* @package core_course
* @copyright 2015 Damyon Wiese
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_course\external;
defined('MOODLE_INTERNAL') || die();
use renderer_base;
/**
* Class for exporting a course module summary from a cm_info class.
*
* @copyright 2015 Damyon Wiese
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class course_module_summary_exporter extends \core\external\exporter {
protected static function define_related() {
return array('cm' => 'cm_info');
}
protected function get_other_values(renderer_base $output) {
$cm = $this->related['cm'];
$values = array(
'id' => $cm->id,
'name' => $cm->name,
'iconurl' => $cm->get_icon_url()->out()
);
if ($cm->url) {
$values['url'] = $cm->url->out();
}
return $values;
}
/**
* Get the format parameters for name.
*
* @return array
*/
protected function get_format_parameters_for_name() {
$cm = $this->related['cm'];
$context = $cm->context;
return [
'context' => $context,
];
}
public static function define_other_properties() {
return array(
'id' => array(
'type' => PARAM_INT,
),
'name' => array(
'type' => PARAM_TEXT
),
'url' => array(
'type' => PARAM_URL,
'optional' => true,
),
'iconurl' => array(
'type' => PARAM_URL
)
);
}
}
+233
View File
@@ -0,0 +1,233 @@
<?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/>.
/**
* Class for exporting a course summary from an stdClass.
*
* @package core_course
* @copyright 2015 Damyon Wiese
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_course\external;
defined('MOODLE_INTERNAL') || die();
use renderer_base;
use moodle_url;
/**
* Class for exporting a course summary from an stdClass.
*
* @copyright 2015 Damyon Wiese
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class course_summary_exporter extends \core\external\exporter {
/**
* Constructor - saves the persistent object, and the related objects.
*
* @param mixed $data - Either an stdClass or an array of values.
* @param array $related - An optional list of pre-loaded objects related to this object.
*/
public function __construct($data, $related = array()) {
if (!array_key_exists('isfavourite', $related)) {
$related['isfavourite'] = false;
}
parent::__construct($data, $related);
}
protected static function define_related() {
// We cache the context so it does not need to be retrieved from the course.
return array('context' => '\\context', 'isfavourite' => 'bool?');
}
protected function get_other_values(renderer_base $output) {
global $CFG;
$courseimage = self::get_course_image($this->data);
if (!$courseimage) {
$courseimage = $output->get_generated_image_for_id($this->data->id);
}
$progress = self::get_course_progress($this->data);
$hasprogress = false;
if ($progress === 0 || $progress > 0) {
$hasprogress = true;
}
$progress = floor($progress ?? 0);
$coursecategory = \core_course_category::get($this->data->category, MUST_EXIST, true);
return array(
'fullnamedisplay' => get_course_display_name_for_list($this->data),
'viewurl' => (new moodle_url('/course/view.php', array('id' => $this->data->id)))->out(false),
'courseimage' => $courseimage,
'progress' => $progress,
'hasprogress' => $hasprogress,
'isfavourite' => $this->related['isfavourite'],
'hidden' => boolval(get_user_preferences('block_myoverview_hidden_course_' . $this->data->id, 0)),
'showshortname' => $CFG->courselistshortnames ? true : false,
'coursecategory' => $coursecategory->name
);
}
public static function define_properties() {
return array(
'id' => array(
'type' => PARAM_INT,
),
'fullname' => array(
'type' => PARAM_TEXT,
),
'shortname' => array(
'type' => PARAM_TEXT,
),
'idnumber' => array(
'type' => PARAM_RAW,
),
'summary' => array(
'type' => PARAM_RAW,
'null' => NULL_ALLOWED,
'default' => null,
),
'summaryformat' => array(
'type' => PARAM_INT,
'default' => FORMAT_MOODLE,
),
'startdate' => array(
'type' => PARAM_INT,
),
'enddate' => array(
'type' => PARAM_INT,
),
'visible' => array(
'type' => PARAM_BOOL,
),
'showactivitydates' => [
'type' => PARAM_BOOL,
'null' => NULL_ALLOWED
],
'showcompletionconditions' => [
'type' => PARAM_BOOL,
'null' => NULL_ALLOWED
],
'pdfexportfont' => [
'type' => PARAM_TEXT,
'null' => NULL_ALLOWED,
'default' => null,
],
);
}
/**
* Get the formatting parameters for the summary.
*
* @return array
*/
protected function get_format_parameters_for_summary() {
return [
'component' => 'course',
'filearea' => 'summary',
];
}
public static function define_other_properties() {
return array(
'fullnamedisplay' => array(
'type' => PARAM_TEXT,
),
'viewurl' => array(
'type' => PARAM_URL,
),
'courseimage' => array(
'type' => PARAM_RAW,
),
'progress' => array(
'type' => PARAM_INT,
'optional' => true
),
'hasprogress' => array(
'type' => PARAM_BOOL
),
'isfavourite' => array(
'type' => PARAM_BOOL
),
'hidden' => array(
'type' => PARAM_BOOL
),
'timeaccess' => array(
'type' => PARAM_INT,
'optional' => true
),
'showshortname' => array(
'type' => PARAM_BOOL
),
'coursecategory' => array(
'type' => PARAM_TEXT
)
);
}
/**
* Get the course image if added to course.
*
* @param object $course
* @return string|false url of course image or false if it's not exist.
*/
public static function get_course_image($course) {
$image = \cache::make('core', 'course_image')->get($course->id);
if (is_null($image)) {
$image = false;
}
return $image;
}
/**
* Get the course pattern datauri.
*
* The datauri is an encoded svg that can be passed as a url.
* @param object $course
* @return string datauri
* @deprecated 3.7
*/
public static function get_course_pattern($course) {
global $OUTPUT;
debugging('course_summary_exporter::get_course_pattern() is deprecated. ' .
'Please use $OUTPUT->get_generated_image_for_id() instead.', DEBUG_DEVELOPER);
return $OUTPUT->get_generated_image_for_id($course->id);
}
/**
* Get the course progress percentage.
*
* @param object $course
* @return int progress
*/
public static function get_course_progress($course) {
return \core_completion\progress::get_course_progress_percentage($course);
}
/**
* Get the course color.
*
* @param int $courseid
* @return string hex color code.
* @deprecated 3.7
*/
public static function coursecolor($courseid) {
global $OUTPUT;
debugging('course_summary_exporter::coursecolor() is deprecated. ' .
'Please use $OUTPUT->get_generated_color_for_id() instead.', DEBUG_DEVELOPER);
return $OUTPUT->get_generated_color_for_id($courseid);
}
}
@@ -0,0 +1,220 @@
<?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_course\external;
defined('MOODLE_INTERNAL') || die();
use context_user;
use core_calendar_external;
use core_course_external;
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;
require_once("{$CFG->dirroot}/calendar/externallib.php");
require_once("{$CFG->dirroot}/course/externallib.php");
/**
* Class for fetching courses which have action event(s) and match given filter parameters.
*
* @package core_course
* @copyright 2022 Michael Hawkins <michaelh@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class get_enrolled_courses_with_action_events_by_timeline_classification extends external_api {
/**
* Returns the description of method parameters.
*
* @return external_function_parameters
*/
public static function execute_parameters(): external_function_parameters {
return new external_function_parameters(
[
'classification' => new external_value(PARAM_ALPHA, 'future, inprogress, or past'),
'limit' => new external_value(PARAM_INT, 'Result set limit', VALUE_DEFAULT, 0),
'offset' => new external_value(PARAM_INT, 'Result set offset', VALUE_DEFAULT, 0),
'sort' => new external_value(PARAM_TEXT, 'Sort string', VALUE_DEFAULT, null),
'customfieldname' => new external_value(PARAM_ALPHANUMEXT, 'Used when classification = customfield',
VALUE_DEFAULT, null),
'customfieldvalue' => new external_value(PARAM_RAW, 'Used when classification = customfield',
VALUE_DEFAULT, null),
'searchvalue' => new external_value(PARAM_RAW, 'The value a user wishes to search against',
VALUE_DEFAULT, null),
'eventsfrom' => new external_value(PARAM_INT, 'Optional starting timestamp for action events',
VALUE_DEFAULT, null),
'eventsto' => new external_value(PARAM_INT, 'Optional ending timestamp for action events',
VALUE_DEFAULT, null),
]
);
}
/**
* Get courses matching the given timeline classification which have action event(s).
*
* Fetches courses by timeline classification which have at least one action event within the specified filtering.
*
* @param string $classification past, inprogress, or future
* @param int $limit Number of courses with events to attempt to fetch
* @param int $offset Offset the full course set before timeline classification is applied
* @param string $sort SQL sort string for results
* @param string $customfieldname Custom field name used when when classification is customfield
* @param string $customfieldvalue Custom field value used when when classification is customfield
* @param string $searchvalue Text search being applied
* @param int $eventsfrom The start timestamp (inclusive) to search from for action events in the course
* @param int $eventsto The end timestamp (inclusive) to search to for action events in the course
* @return array list of courses and any warnings
*/
public static function execute(
string $classification,
int $limit = 0,
int $offset = 0,
string $sort = null,
string $customfieldname = null,
string $customfieldvalue = null,
string $searchvalue = null,
int $eventsfrom = null,
int $eventsto = null
): array {
global $USER;
self::validate_context(context_user::instance($USER->id));
$params = self::validate_parameters(
self::execute_parameters(),
[
'classification' => $classification,
'limit' => $limit,
'offset' => $offset,
'sort' => $sort,
'customfieldname' => $customfieldname,
'customfieldvalue' => $customfieldvalue,
'searchvalue' => $searchvalue,
'eventsfrom' => $eventsfrom,
'eventsto' => $eventsto,
]
);
$classification = $params['classification'];
$limit = $params['limit'];
$offset = $params['offset'];
$sort = $params['sort'];
$customfieldname = $params['customfieldname'];
$customfieldvalue = $params['customfieldvalue'];
$searchvalue = clean_param($params['searchvalue'], PARAM_TEXT);
$eventsfrom = $params['eventsfrom'];
$eventsto = $params['eventsto'];
$morecoursestofetch = true;
$morecoursespossible = true;
$coursesfinal = [];
do {
// Fetch courses.
[
'courses' => $coursesfetched,
'nextoffset' => $offset,
] = core_course_external::get_enrolled_courses_by_timeline_classification($classification, $limit,
$offset, $sort, $customfieldname, $customfieldvalue, $searchvalue);
$courseids = array_column($coursesfetched, 'id');
$coursesfetched = array_combine($courseids, $coursesfetched);
if (!empty($courseids)) {
// Need to check this to know how many are expected (since it is possible for this to be less than the limit).
$numcoursesfetched = count($courseids);
$numfetchedwithevents = 0;
// If less courses are fetched than we requested, we know it is not possible for more courses to be available.
if ($numcoursesfetched < $limit) {
$morecoursestofetch = false;
$morecoursespossible = false;
}
// Try to fetch one action event within the time/search parameters for each course.
$events = core_calendar_external::get_calendar_action_events_by_courses($courseids, $eventsfrom, $eventsto, 1,
$searchvalue);
foreach ($events->groupedbycourse as $courseevents) {
$courseid = $courseevents->courseid;
// Only include courses which contain at least one event.
if (empty($courseevents->events)) {
unset($coursesfetched[$courseid]);
} else {
$numfetchedwithevents++;
}
}
// Add courses with events to the final course list in order.
$coursesfinal = array_merge($coursesfinal, $coursesfetched);
// If any courses did not have events, adjust the limit so we can attempt to fetch as many as are still required.
if ($numfetchedwithevents < $numcoursesfetched) {
$limit -= $numfetchedwithevents;
} else {
// If we have found as many courses as required or are available, no need to attempt fetching more.
$morecoursestofetch = false;
}
} else {
$morecoursestofetch = false;
$morecoursespossible = false;
}
} while ($morecoursestofetch);
static $isrecursivecall = false;
$morecoursesavailable = false;
// Recursively call this method to check if at least one more course is available if we know that is a possibility.
if (!$isrecursivecall && $morecoursespossible) {
// Prevent infinite recursion.
$isrecursivecall = true;
$additionalcourses = self::execute(
$classification, 1, $offset, $sort, $customfieldname, $customfieldvalue, $searchvalue, $eventsfrom, $eventsto
);
if (!empty($additionalcourses['courses'])) {
$morecoursesavailable = true;
}
}
return [
'courses' => $coursesfinal,
'nextoffset' => $offset,
'morecoursesavailable' => $morecoursesavailable,
];
}
/**
* Returns description of method result value.
*
* @return \core_external\external_description
*/
public static function execute_returns(): external_single_structure {
return new external_single_structure(
[
'courses' => new external_multiple_structure(course_summary_exporter::get_read_structure(), 'Course'),
'nextoffset' => new external_value(PARAM_INT, 'Offset for the next request'),
'morecoursesavailable' => new external_value(PARAM_BOOL,
'Whether more courses with events exist within the provided parameters'),
]
);
}
}
@@ -0,0 +1,137 @@
<?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_course\external;
use context_module;
use core_external\external_description;
use core_external\external_files;
use core_external\external_format_value;
use core_external\util as external_util;
use core_external\external_value;
/**
* This class helps implement the get_..._by_courses web service that every activity should have.
*
* It has helper methods to add the standard course-module fields to the results, and the declaration of the return value.
*
* @package core_course
* @copyright 2022 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
abstract class helper_for_get_mods_by_courses {
/**
* Add the value of all the standard fields to the results to be returned by the service.
* This is designed to be used in the implementation of the get_..._by_courses web service methods.
*
* Note that $modinstance is also updated in-place.
*
* @param \stdClass $modinstance one of the objects returned from a call to {@see get_all_instances_in_courses()}.
* @param string $component the plugin name, e.g. 'mod_book'.
* @param string $capabilityforgroups capability to check before including group/visible/section info in the results.
* @param string|null $capabilityforintro capability to check before including intro info in the results.
* null means always include (the default).
* @return array with the containing all the values declared in {@see standard_coursemodule_elements_returns()}.
*/
public static function standard_coursemodule_element_values(\stdClass $modinstance, string $component,
string $capabilityforgroups = 'moodle/course:manageactivities', string $capabilityforintro = null): array {
self::format_name_and_intro($modinstance, $component);
$context = context_module::instance($modinstance->coursemodule);
// First, we return information that any user can see in the web interface.
$moddetails['id'] = $modinstance->id;
$moddetails['coursemodule'] = $modinstance->coursemodule;
$moddetails['course'] = $modinstance->course;
$moddetails['name'] = $modinstance->name;
$moddetails['lang'] = clean_param($modinstance->lang, PARAM_LANG);
if (!$capabilityforintro || has_capability($capabilityforintro, $context)) {
$moddetails['intro'] = $modinstance->intro;
$moddetails['introformat'] = $modinstance->introformat;
$moddetails['introfiles'] = $modinstance->introfiles;
}
// Now add information only available to people who can edit.
if (has_capability($capabilityforgroups, $context)) {
$moddetails['section'] = $modinstance->section;
$moddetails['visible'] = $modinstance->visible;
$moddetails['groupmode'] = $modinstance->groupmode;
$moddetails['groupingid'] = $modinstance->groupingid;
}
return $moddetails;
}
/**
* Format the module name an introduction ready to be exported to a web service.
*
* Note that $modinstance is updated in-place.
*
* @param \stdClass $modinstance one of the objects returned from a call to {@see get_all_instances_in_courses()}.
* @param string $component the plugin name, e.g. 'mod_book'.
*/
public static function format_name_and_intro(\stdClass $modinstance, string $component) {
$context = context_module::instance($modinstance->coursemodule);
$modinstance->name = \core_external\util::format_string($modinstance->name, $context);
[$modinstance->intro, $modinstance->introformat] = \core_external\util::format_text(
$modinstance->intro, $modinstance->introformat, $context,
$component, 'intro', null, ['noclean' => true]);
$modinstance->introfiles = external_util::get_area_files($context->id, $component, 'intro', false, false);
}
/**
* Get the list of standard fields, to add to the declaration of the return values.
*
* Example usage combine the fields returned here with any extra ones your activity uses:
*
* public static function execute_returns() {
* return new external_single_structure([
* 'bigbluebuttonbns' => new external_multiple_structure(
* new external_single_structure(array_merge(
* helper_for_get_mods_by_courses::standard_coursemodule_elements_returns(),
* [
* 'meetingid' => new external_value(PARAM_RAW, 'Meeting id'),
* 'timemodified' => new external_value(PARAM_INT, 'Last time the instance was modified'),
* ]
* ))
* ),
* 'warnings' => new external_warnings(),
* ]
* );
* }
*
* @param bool $introoptional if true, the intro fields are marked as optional. Default false.
* @return external_description[] array of standard fields, to which you can add your activity-specific ones.
*/
public static function standard_coursemodule_elements_returns(bool $introoptional = false): array {
return [
'id' => new external_value(PARAM_INT, 'Activity instance id'),
'coursemodule' => new external_value(PARAM_INT, 'Course module id'),
'course' => new external_value(PARAM_INT, 'Course id'),
'name' => new external_value(PARAM_RAW, 'Activity name'),
'intro' => new external_value(PARAM_RAW, 'Activity introduction', $introoptional ? VALUE_OPTIONAL : VALUE_REQUIRED),
'introformat' => new external_format_value('intro', $introoptional ? VALUE_OPTIONAL : VALUE_REQUIRED),
'introfiles' => new external_files('Files in the introduction', VALUE_OPTIONAL),
'section' => new external_value(PARAM_INT, 'Course section id', VALUE_OPTIONAL),
'visible' => new external_value(PARAM_BOOL, 'Visible', VALUE_OPTIONAL),
'groupmode' => new external_value(PARAM_INT, 'Group mode', VALUE_OPTIONAL),
'groupingid' => new external_value(PARAM_INT, 'Group id', VALUE_OPTIONAL),
'lang' => new external_value(PARAM_SAFEDIR, 'Forced activity language', VALUE_OPTIONAL),
];
}
}
@@ -0,0 +1,43 @@
<?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_course\hook;
use stdClass;
/**
* Hook after course creation.
*
* This hook will be dispatched after the course is created and events are fired.
*
* @package core_course
* @copyright 2024 Safat Shahin <safat.shahin@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
#[\core\attribute\label('Allows plugins or features to perform actions after a course is created.')]
#[\core\attribute\tags('course')]
class after_course_created {
/**
* Constructor for the hook.
*
* @param stdClass $course The course instance.
*/
public function __construct(
/** @var stdClass The course instance */
public readonly stdClass $course,
) {
}
}
@@ -0,0 +1,47 @@
<?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_course\hook;
use stdClass;
/**
* Hook after course updates.
*
* @package core_course
* @copyright 2024 Safat Shahin <safat.shahin@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
#[\core\attribute\label('Allows plugins or features to perform actions after a course is updated.')]
#[\core\attribute\tags('course')]
class after_course_updated {
/**
* Constructor for the hook.
*
* @param stdClass $course The course instance.
* @param stdClass $oldcourse The old course instance.
* @param bool $changeincoursecat Whether the course category has changed.
*/
public function __construct(
/** @var stdClass The course instance */
public readonly stdClass $course,
/** @var stdClass The old course instance */
public readonly stdClass $oldcourse,
/** @var bool Whether the course category has changed */
public readonly bool $changeincoursecat = false,
) {
}
}
@@ -0,0 +1,48 @@
<?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_course\hook;
use core\hook\described_hook;
use course_edit_form;
use MoodleQuickForm;
/**
* Allows plugins to extend course form definition and add/remove/update form elements.
*
* @see course_edit_form::definition()
*
* @package core_course
* @copyright 2023 Dmitrii Metelkin <dmitriim@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
#[\core\attribute\label('Allows plugins to extend course editing form')]
#[\core\attribute\tags('course')]
class after_form_definition {
/**
* Creates new hook.
*
* @param course_edit_form $formwrapper Course form wrapper.
* @param MoodleQuickForm $mform Form to be extended.
*/
public function __construct(
/** @var course_edit_form The form wrapper for the edit form */
public readonly course_edit_form $formwrapper,
/** @var MoodlequickForm The form to be extended */
public readonly MoodleQuickForm $mform,
) {
}
}
@@ -0,0 +1,47 @@
<?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_course\hook;
use course_edit_form;
use MoodleQuickForm;
/**
* Allows plugins to extend course form after data is set.
*
* @see course_edit_form::definition_after_data()
*
* @package core_course
* @copyright 2023 Dmitrii Metelkin <dmitriim@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
#[\core\attribute\label('Allows plugins to extend course editing form after data is set')]
#[\core\attribute\tags('course')]
class after_form_definition_after_data {
/**
* Creates new hook.
*
* @param course_edit_form $formwrapper Course form wrapper..
* @param MoodleQuickForm $mform Form to be extended.
*/
public function __construct(
/** @var course_edit_form The form wrapper for the edit form */
public readonly course_edit_form $formwrapper,
/** @var MoodlequickForm The form to be extended */
public readonly MoodleQuickForm $mform,
) {
}
}
@@ -0,0 +1,54 @@
<?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_course\hook;
/**
* Allows plugins to extend course form submission.
*
* @see create_course()
* @see update_course()
*
* @package core_course
* @copyright 2023 Dmitrii Metelkin <dmitriim@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
#[\core\attribute\label('Allows plugins to extend saving of the course editing form')]
#[\core\attribute\tags('course')]
class after_form_submission {
/**
* Creates new hook.
*
* @param \stdClass $data Submitted data
* @param bool $isnewcourse Whether this is a new course
*/
public function __construct(
/** @var \stdClass The submitted data */
protected \stdClass $data,
/** @var bool Whether this is a new course */
public readonly bool $isnewcourse = false,
) {
}
/**
* Returns submitted data.
*
* @return \stdClass
*/
public function get_data(): \stdClass {
return $this->data;
}
}
@@ -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/>.
namespace core_course\hook;
use course_edit_form;
/**
* Allows plugins to extend course form validation.
*
* @see course_edit_form::validation()
*
* @package core_course
* @copyright 2023 Dmitrii Metelkin <dmitriim@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
#[\core\attribute\label('Allow plugins to extend a validation of the course editing form')]
#[\core\attribute\tags('course')]
class after_form_validation {
/**
* Plugin errors.
*
* @var array
*/
protected $errors = [];
/**
* Creates new hook.
*
* @param course_edit_form $formwrapper Course form wrapper..
* @param array $data Submitted data.
* @param array $files Submitted files.
*/
public function __construct(
/** @var course_edit_form Course form wrapper */
public readonly course_edit_form $formwrapper,
/** @var array The submitted data */
private array $data,
/** @var array Submitted files */
private array $files = [],
) {
}
/**
* Returns submitted data.
*
* @return array
*/
public function get_data(): array {
return $this->data;
}
/**
* Returns submitted files.
*
* @return array
*/
public function get_files(): array {
return $this->files;
}
/**
* Return plugin generated errors.
*
* @return array
*/
public function get_errors(): array {
return $this->errors;
}
/**
* Plugins implementing a callback can add validation errors.
*
* @param array $errors Validation errors generated by a plugin.
*/
public function add_errors(array $errors): void {
$this->errors = array_merge($this->errors, $errors);
}
}
@@ -0,0 +1,46 @@
<?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_course\hook;
use stdClass;
use Psr\EventDispatcher\StoppableEventInterface;
/**
* Hook before course deletion.
*
* @package core_course
* @copyright 2024 Safat Shahin <safat.shahin@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
#[\core\attribute\label('Allows plugins or features to perform actions before a course is deleted.')]
#[\core\attribute\tags('course')]
class before_course_deleted implements
StoppableEventInterface
{
use \core\hook\stoppable_trait;
/**
* Constructor for the hook.
*
* @param stdClass $course The course instance.
*/
public function __construct(
/** @var stdClass The course instance */
public readonly stdClass $course,
) {
}
}
+481
View File
@@ -0,0 +1,481 @@
<?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 core_course_list_element
*
* @package core
* @subpackage course
* @copyright 2018 Marina Glancy
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/**
* Class to store information about one course in a list of courses
*
* Not all information may be retrieved when object is created but
* it will be retrieved on demand when appropriate property or method is
* called.
*
* Instances of this class are usually returned by functions
* {@link core_course_category::search_courses()}
* and
* {@link core_course_category::get_courses()}
*
* @property-read int $id
* @property-read int $category Category ID
* @property-read int $sortorder
* @property-read string $fullname
* @property-read string $shortname
* @property-read string $idnumber
* @property-read string $summary Course summary. Field is present if core_course_category::get_courses()
* was called with option 'summary'. Otherwise will be retrieved from DB on first request
* @property-read int $summaryformat Summary format. Field is present if core_course_category::get_courses()
* was called with option 'summary'. Otherwise will be retrieved from DB on first request
* @property-read string $format Course format. Retrieved from DB on first request
* @property-read int $showgrades Retrieved from DB on first request
* @property-read int $newsitems Retrieved from DB on first request
* @property-read int $startdate
* @property-read int $enddate
* @property-read int $marker Retrieved from DB on first request
* @property-read int $maxbytes Retrieved from DB on first request
* @property-read int $legacyfiles Retrieved from DB on first request
* @property-read int $showreports Retrieved from DB on first request
* @property-read int $visible
* @property-read int $visibleold Retrieved from DB on first request
* @property-read int $groupmode Retrieved from DB on first request
* @property-read int $groupmodeforce Retrieved from DB on first request
* @property-read int $defaultgroupingid Retrieved from DB on first request
* @property-read string $lang Retrieved from DB on first request
* @property-read string $theme Retrieved from DB on first request
* @property-read int $timecreated Retrieved from DB on first request
* @property-read int $timemodified Retrieved from DB on first request
* @property-read int $requested Retrieved from DB on first request
* @property-read int $enablecompletion Retrieved from DB on first request
* @property-read int $completionnotify Retrieved from DB on first request
* @property-read int $cacherev
*
* @package core
* @subpackage course
* @copyright 2013 Marina Glancy
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class core_course_list_element implements IteratorAggregate {
/** @var stdClass record retrieved from DB, may have additional calculated property such as managers and hassummary */
protected $record;
/** @var array array of course contacts - stores result of call to get_course_contacts() */
protected $coursecontacts;
/** @var bool true if the current user can access the course, false otherwise. */
protected $canaccess = null;
/**
* Creates an instance of the class from record
*
* @param stdClass $record except fields from course table it may contain
* field hassummary indicating that summary field is not empty.
* Also it is recommended to have context fields here ready for
* context preloading
*/
public function __construct(stdClass $record) {
context_helper::preload_from_record($record);
$this->record = new stdClass();
foreach ($record as $key => $value) {
$this->record->$key = $value;
}
}
/**
* Indicates if the course has non-empty summary field
*
* @return bool
*/
public function has_summary() {
if (isset($this->record->hassummary)) {
return !empty($this->record->hassummary);
}
if (!isset($this->record->summary)) {
// We need to retrieve summary.
$this->__get('summary');
}
return !empty($this->record->summary);
}
/**
* Indicates if the course have course contacts to display
*
* @return bool
*/
public function has_course_contacts() {
if (!isset($this->record->managers)) {
$courses = array($this->id => &$this->record);
core_course_category::preload_course_contacts($courses);
}
return !empty($this->record->managers);
}
/**
* Returns list of course contacts (usually teachers) to display in course link
*
* Roles to display are set up in $CFG->coursecontact
*
* The result is the list of users where user id is the key and the value
* is an array with elements:
* - 'user' - object containing basic user information
* - 'role' - object containing basic role information (id, name, shortname, coursealias)
* - 'rolename' => role_get_name($role, $context, ROLENAME_ALIAS)
* - 'username' => fullname($user, $canviewfullnames)
*
* @return array
*/
public function get_course_contacts() {
global $CFG;
if (empty($CFG->coursecontact)) {
// No roles are configured to be displayed as course contacts.
return array();
}
if (!$this->has_course_contacts()) {
// No course contacts exist.
return array();
}
if ($this->coursecontacts === null) {
$this->coursecontacts = array();
$context = context_course::instance($this->id);
$canviewfullnames = has_capability('moodle/site:viewfullnames', $context);
$displayall = get_config('core', 'coursecontactduplicates');
foreach ($this->record->managers as $ruser) {
$processed = array_key_exists($ruser->id, $this->coursecontacts);
if (!$displayall && $processed) {
continue;
}
$role = (object)[
'id' => $ruser->roleid,
'name' => $ruser->rolename,
'shortname' => $ruser->roleshortname,
'coursealias' => $ruser->rolecoursealias,
];
$role->displayname = role_get_name($role, $context, ROLENAME_ALIAS);
if (!$processed) {
$user = username_load_fields_from_object((object)[], $ruser, null, ['id', 'username']);
$this->coursecontacts[$ruser->id] = [
'user' => $user,
'username' => fullname($user, $canviewfullnames),
// List of all roles.
'roles' => [],
// Primary role of this user.
'role' => $role,
'rolename' => $role->displayname,
];
}
$this->coursecontacts[$ruser->id]['roles'][$ruser->roleid] = $role;
}
}
return $this->coursecontacts;
}
/**
* Returns custom fields data for this course
*
* @return \core_customfield\data_controller[]
*/
public function get_custom_fields(): array {
if (!isset($this->record->customfields)) {
$this->record->customfields = \core_course\customfield\course_handler::create()->get_instance_data($this->id);
}
return $this->record->customfields;
}
/**
* Does this course have custom fields
*
* @return bool
*/
public function has_custom_fields(): bool {
$customfields = $this->get_custom_fields();
return !empty($customfields);
}
/**
* Checks if course has any associated overview files
*
* @return bool
*/
public function has_course_overviewfiles() {
global $CFG;
if (empty($CFG->courseoverviewfileslimit)) {
return false;
}
$fs = get_file_storage();
$context = context_course::instance($this->id);
return !$fs->is_area_empty($context->id, 'course', 'overviewfiles');
}
/**
* Returns all course overview files
*
* @return array array of stored_file objects
*/
public function get_course_overviewfiles() {
global $CFG;
if (empty($CFG->courseoverviewfileslimit)) {
return array();
}
require_once($CFG->libdir. '/filestorage/file_storage.php');
require_once($CFG->dirroot. '/course/lib.php');
$fs = get_file_storage();
$context = context_course::instance($this->id);
$files = $fs->get_area_files($context->id, 'course', 'overviewfiles', false, 'filename', false);
if (count($files)) {
$overviewfilesoptions = course_overviewfiles_options($this->id);
$acceptedtypes = $overviewfilesoptions['accepted_types'];
if ($acceptedtypes !== '*') {
// Filter only files with allowed extensions.
require_once($CFG->libdir. '/filelib.php');
foreach ($files as $key => $file) {
if (!file_extension_in_typegroup($file->get_filename(), $acceptedtypes)) {
unset($files[$key]);
}
}
}
if (count($files) > $CFG->courseoverviewfileslimit) {
// Return no more than $CFG->courseoverviewfileslimit files.
$files = array_slice($files, 0, $CFG->courseoverviewfileslimit, true);
}
}
return $files;
}
/**
* Magic method to check if property is set
*
* @param string $name
* @return bool
*/
public function __isset($name) {
return isset($this->record->$name);
}
/**
* Magic method to get a course property
*
* Returns any field from table course (retrieves it from DB if it was not retrieved before)
*
* @param string $name
* @return mixed
*/
public function __get($name) {
global $DB;
if (property_exists($this->record, $name)) {
return $this->record->$name;
} else if ($name === 'summary' || $name === 'summaryformat') {
// Retrieve fields summary and summaryformat together because they are most likely to be used together.
$record = $DB->get_record('course', array('id' => $this->record->id), 'summary, summaryformat', MUST_EXIST);
$this->record->summary = $record->summary;
$this->record->summaryformat = $record->summaryformat;
return $this->record->$name;
} else if (array_key_exists($name, $DB->get_columns('course'))) {
// Another field from table 'course' that was not retrieved.
$this->record->$name = $DB->get_field('course', $name, array('id' => $this->record->id), MUST_EXIST);
return $this->record->$name;
}
debugging('Invalid course property accessed! '.$name);
return null;
}
/**
* All properties are read only, sorry.
*
* @param string $name
*/
public function __unset($name) {
debugging('Can not unset '.get_class($this).' instance properties!');
}
/**
* Magic setter method, we do not want anybody to modify properties from the outside
*
* @param string $name
* @param mixed $value
*/
public function __set($name, $value) {
debugging('Can not change '.get_class($this).' instance properties!');
}
/**
* Create an iterator because magic vars can't be seen by 'foreach'.
* Exclude context fields
*
* Implementing method from interface IteratorAggregate
*
* @return ArrayIterator
*/
public function getIterator(): Traversable {
$ret = array('id' => $this->record->id);
foreach ($this->record as $property => $value) {
$ret[$property] = $value;
}
return new ArrayIterator($ret);
}
/**
* Returns the name of this course as it should be displayed within a list.
* @return string
*/
public function get_formatted_name() {
return format_string(
get_course_display_name_for_list($this),
true,
['context' => $this->get_context()],
);
}
/**
* Returns the formatted fullname for this course.
* @return string
*/
public function get_formatted_fullname() {
return format_string(
$this->__get('fullname'),
true,
['context' => $this->get_context()],
);
}
/**
* Returns the formatted shortname for this course.
* @return string
*/
public function get_formatted_shortname() {
return format_string(
$this->__get('shortname'),
true,
['context' => $this->get_context()],
);
}
/**
* Returns true if the current user can access this course.
* @return bool
*/
public function can_access() {
if ($this->canaccess === null) {
$this->canaccess = can_access_course($this->record);
}
return $this->canaccess;
}
/**
* Returns true if the user can edit this courses settings.
*
* Note: this function does not check that the current user can access the course.
* To do that please call require_login with the course, or if not possible call
* {@link core_course_list_element::can_access()}
*
* @return bool
*/
public function can_edit() {
return has_capability('moodle/course:update', $this->get_context());
}
/**
* Returns true if the user can change the visibility of this course.
*
* Note: this function does not check that the current user can access the course.
* To do that please call require_login with the course, or if not possible call
* {@link core_course_list_element::can_access()}
*
* @return bool
*/
public function can_change_visibility() {
// You must be able to both hide a course and view the hidden course.
return has_all_capabilities(array('moodle/course:visibility', 'moodle/course:viewhiddencourses'),
$this->get_context());
}
/**
* Returns the context for this course.
* @return context_course
*/
public function get_context() {
return context_course::instance($this->__get('id'));
}
/**
* Returns true if the current user can review enrolments for this course.
*
* Note: this function does not check that the current user can access the course.
* To do that please call require_login with the course, or if not possible call
* {@link core_course_list_element::can_access()}
*
* @return bool
*/
public function can_review_enrolments() {
return has_capability('moodle/course:enrolreview', $this->get_context());
}
/**
* Returns true if the current user can delete this course.
*
* Note: this function does not check that the current user can access the course.
* To do that please call require_login with the course, or if not possible call
* {@link core_course_list_element::can_access()}
*
* @return bool
*/
public function can_delete() {
return can_delete_course($this->id);
}
/**
* Returns true if the current user can backup this course.
*
* Note: this function does not check that the current user can access the course.
* To do that please call require_login with the course, or if not possible call
* {@link core_course_list_element::can_access()}
*
* @return bool
*/
public function can_backup() {
return has_capability('moodle/backup:backupcourse', $this->get_context());
}
/**
* Returns true if the current user can restore this course.
*
* Note: this function does not check that the current user can access the course.
* To do that please call require_login with the course, or if not possible call
* {@link core_course_list_element::can_access()}
*
* @return bool
*/
public function can_restore() {
return has_capability('moodle/restore:restorecourse', $this->get_context());
}
}
@@ -0,0 +1,86 @@
<?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/>.
/**
* Activity Chooser footer data class.
*
* @package core
* @subpackage course
* @copyright 2020 Mathew May <mathew.solutions>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_course\local\entity;
/**
* A class to represent the Activity Chooser footer data.
*
* @package core
* @subpackage course
* @copyright 2020 Mathew May <mathew.solutions>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class activity_chooser_footer {
/** @var string $footerjspath The path to the plugin JS file to dynamically import later. */
protected $footerjspath;
/** @var string $footertemplate The rendered template for the footer. */
protected $footertemplate;
/** @var string $carouseltemplate The rendered template for the footer. */
protected $carouseltemplate;
/**
* Constructor method.
*
* @param string $footerjspath JS file to dynamically import later.
* @param string $footertemplate Footer template that has been rendered.
* @param string|null $carouseltemplate Carousel template that may have been rendered.
*/
public function __construct(string $footerjspath, string $footertemplate, ?string $carouseltemplate = '') {
$this->footerjspath = $footerjspath;
$this->footertemplate = $footertemplate;
$this->carouseltemplate = $carouseltemplate;
}
/**
* Get the footer JS file path for this plugin.
*
* @return string The JS file to call functions from.
*/
public function get_footer_js_file(): string {
return $this->footerjspath;
}
/**
* Get the footer rendered template for this plugin.
*
* @return string The template that has been rendered for the chooser footer.
*/
public function get_footer_template(): string {
return $this->footertemplate;
}
/**
* Get the carousel rendered template for this plugin.
*
* @return string The template that has been rendered for the chooser carousel.
*/
public function get_carousel_template(): string {
return $this->carouseltemplate;
}
}
@@ -0,0 +1,182 @@
<?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 content_item class.
*
* @package core
* @subpackage course
* @copyright 2020 Jake Dallimore <jrhdallimore@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_course\local\entity;
defined('MOODLE_INTERNAL') || die();
/**
* The content_item class.
*
* @copyright 2020 Jake Dallimore <jrhdallimore@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class content_item {
/** @var int $id the id. */
private $id;
/** @var string $name the name. */
private $name;
/** @var title $title the title. */
private $title;
/** @var \moodle_url $link the url for the content item's setup page (usually mod/edit.php). */
private $link;
/** @var string $icon an html string containing the icon for this item. */
private $icon;
/** @var string $help the description/help text for this content item. */
private $help;
/** @var int $achetype a module archetype, e.g. MOD_ARCHETYPE_RESOURCE, MOD_ARCHETYPE_OTHER. */
private $archetype;
/** @var string $componentname the name of the component from which this content item originates. */
private $componentname;
/** @var string $purpose the purpose type of this component. */
private $purpose;
/** @var bool $branded whether or not this component is branded. */
private $branded;
/**
* The content_item constructor.
*
* @param int $id Id number.
* @param string $name Name of the item, not human readable.
* @param title $title Human readable title for the item.
* @param \moodle_url $link The URL to the creation page, with any item specific params
* @param string $icon HTML containing the icon for the item
* @param string $help The description of the item.
* @param int $archetype the archetype for the content item (see MOD_ARCHETYPE_X definitions in lib/moodlelib.php).
* @param string $componentname the name of the component/plugin with which this content item is associated.
* @param string $purpose the purpose type of this component.
* @param bool $branded whether or not this item is branded.
*/
public function __construct(int $id, string $name, title $title, \moodle_url $link, string $icon, string $help,
int $archetype, string $componentname, string $purpose, bool $branded = false) {
$this->id = $id;
$this->name = $name;
$this->title = $title;
$this->link = $link;
$this->icon = $icon;
$this->help = $help;
$this->archetype = $archetype;
$this->componentname = $componentname;
$this->purpose = $purpose;
$this->branded = $branded;
}
/**
* Get the name of the component with which this content item is associated.
*
* @return string
*/
public function get_component_name(): string {
return $this->componentname;
}
/**
* Get the help description of this item.
*
* @return string
*/
public function get_help(): string {
return $this->help;
}
/**
* Get the archetype of this item.
*
* @return int
*/
public function get_archetype(): int {
return $this->archetype;
}
/**
* Get the id of this item.
* @return int
*/
public function get_id(): int {
return $this->id;
}
/**
* Get the name of this item.
*
* @return string
*/
public function get_name(): string {
return $this->name;
}
/**
* Get the human readable title of this item.
*
* @return title
*/
public function get_title(): title {
return $this->title;
}
/**
* Get the link to the creation page of this item.
*
* @return \moodle_url
*/
public function get_link(): \moodle_url {
return $this->link;
}
/**
* Get the icon html for this item.
*
* @return string
*/
public function get_icon(): string {
return $this->icon;
}
/**
* Get purpose for this item.
*
* @return string
*/
public function get_purpose(): string {
return $this->purpose;
}
/**
* Whether this item is branded.
*
* @return bool true if this item is branded, false otherwise.
*/
public function is_branded(): bool {
return $this->branded;
}
}
@@ -0,0 +1,62 @@
<?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 lang_string_title class of value object, providing access to the value of a lang string.
*
* @package core
* @subpackage course
* @copyright 2020 Jake Dallimore <jrhdallimore@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_course\local\entity;
defined('MOODLE_INTERNAL') || die();
/**
* The lang_string_title class of value object, providing access to the value of a lang string.
*
* @copyright 2020 Jake Dallimore <jrhdallimore@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class lang_string_title implements title {
/** @var string $component the component name. */
private $component;
/** @var string $identifier the string identifier. */
private $identifier;
/**
* The lang_string_title constructor.
*
* @param string $identifier the component name.
* @param string $component the string identifier.
*/
public function __construct(string $identifier, string $component) {
$this->identifier = $identifier;
$this->component = $component;
}
/**
* Returns the value of the wrapped string.
*
* @return string the value of the string.
*/
public function get_value(): string {
return get_string($this->identifier, $this->component);
}
}
@@ -0,0 +1,57 @@
<?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 string_title class of value object, which provides access to a simple string.
*
* @package core
* @subpackage course
* @copyright 2020 Jake Dallimore <jrhdallimore@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_course\local\entity;
defined('MOODLE_INTERNAL') || die();
/**
* The string_title class of value object, which provides access to a simple string.
*
* @copyright 2020 Jake Dallimore <jrhdallimore@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class string_title implements title {
/** @var string $title the title string. */
private $title;
/**
* The string_title constructor.
*
* @param string $title a string.
*/
public function __construct(string $title) {
$this->title = $title;
}
/**
* Return the value of the wrapped string.
*
* @return string
*/
public function get_value(): string {
return $this->title;
}
}
+35
View File
@@ -0,0 +1,35 @@
<?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 title value object interface, which provides a basic interface to a string.
*
* @package core
* @subpackage course
* @copyright 2020 Jake Dallimore <jrhdallimore@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_course\local\entity;
defined('MOODLE_INTERNAL') || die();
interface title {
/**
* Get the value of this title.
*/
public function get_value(): string;
}
@@ -0,0 +1,161 @@
<?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 course_content_item_exporter class.
*
* @package core
* @subpackage course
* @copyright 2020 Jake Dallimore <jrhdallimore@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_course\local\exporters;
defined('MOODLE_INTERNAL') || die();
use core\external\exporter;
use core_course\local\entity\content_item;
use core_course\local\service\content_item_service;
/**
* The course_content_item_exporter class.
*
* @copyright 2020 Jake Dallimore <jrhdallimore@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class course_content_item_exporter extends exporter {
/** @var content_item $contentitem the content_item to export. */
private $contentitem;
/**
* The course_content_item_exporter constructor.
*
* @param content_item $contentitem the content item to export.
* @param array $related the array of related objects used during export.
*/
public function __construct(content_item $contentitem, array $related = []) {
$this->contentitem = $contentitem;
return parent::__construct([], $related);
}
/**
* Definition of all properties originating in the export target, \core_course\local\entity\content_item.
*
* @return array The array of property values, indexed by name.
*/
protected static function define_properties() {
return [
'id' => ['type' => PARAM_INT, 'description' => 'The id of the content item'],
'name' => ['type' => PARAM_TEXT, 'description' => 'Name of the content item'],
'title' => ['type' => PARAM_TEXT, 'description' => 'The string title of the content item, human readable'],
'link' => ['type' => PARAM_URL, 'description' => 'The link to the content item creation page'],
'icon' => ['type' => PARAM_RAW, 'description' => 'Html containing the icon for the content item'],
'help' => ['type' => PARAM_RAW, 'description' => 'Html description / help for the content item'],
'archetype' => ['type' => PARAM_RAW, 'description' => 'The archetype of the module exposing the content item'],
'componentname' => ['type' => PARAM_TEXT, 'description' => 'The name of the component exposing the content item'],
'purpose' => ['type' => PARAM_TEXT, 'description' => 'The purpose of the component exposing the content item'],
'branded' => ['type' => PARAM_BOOL, 'description' => ' Whether this content item is branded or not'],
];
}
/**
* Definition of all properties which are either calculated or originate in a related domain object.
*
* @return array The array of property values, indexed by name.
*/
protected static function define_other_properties() {
// This will hold user-dependant properties such as whether the item is starred or recommended.
return [
'favourite' => ['type' => PARAM_BOOL, 'description' => 'Has the user favourited the content item'],
'legacyitem' => [
'type' => PARAM_BOOL,
'description' => 'If this item was pulled from the old callback and has no item id.'
],
'recommended' => ['type' => PARAM_BOOL, 'description' => 'Has this item been recommended'],
];
}
/**
* Get ALL properties for the content_item DTO being exported.
*
* These properties are a mix of:
* - readonly properties of the primary object (content_item) being exported.
* - calculated values
* - properties originating from the related domain objects.
*
* Normally, those properties defined in get_properties() are added to the export automatically as part of the superclass code,
* provided they are public properties on the export target. In this case, the export target is content_item, which doesn't
* provide public access to its properties, so those are fetched via their respective getters here.
*
* @param \renderer_base $output
* @return array The array of property values, indexed by name.
*/
protected function get_other_values(\renderer_base $output) {
$favourite = false;
$itemtype = 'contentitem_' . $this->contentitem->get_component_name();
if (isset($this->related['favouriteitems'])) {
foreach ($this->related['favouriteitems'] as $favobj) {
if ($favobj->itemtype === $itemtype && in_array($this->contentitem->get_id(), $favobj->ids)) {
$favourite = true;
}
}
}
$recommended = false;
$itemtype = content_item_service::RECOMMENDATION_PREFIX . $this->contentitem->get_component_name();
if (isset($this->related['recommended'])) {
foreach ($this->related['recommended'] as $favobj) {
if ($favobj->itemtype === $itemtype && in_array($this->contentitem->get_id(), $favobj->ids)) {
$recommended = true;
}
}
}
$properties = [
'id' => $this->contentitem->get_id(),
'name' => $this->contentitem->get_name(),
'title' => $this->contentitem->get_title()->get_value(),
'link' => $this->contentitem->get_link()->out(false),
'icon' => $this->contentitem->get_icon(),
'help' => format_text($this->contentitem->get_help(), FORMAT_MARKDOWN),
'archetype' => $this->contentitem->get_archetype(),
'componentname' => $this->contentitem->get_component_name(),
'favourite' => $favourite,
'legacyitem' => ($this->contentitem->get_id() == -1),
'recommended' => $recommended,
'purpose' => $this->contentitem->get_purpose(),
'branded' => $this->contentitem->is_branded(),
];
return $properties;
}
/**
* Define the list of related objects, used by this exporter.
*
* @return array the list of related objects.
*/
protected static function define_related(): array {
return [
'context' => '\context',
'favouriteitems' => '\stdClass[]?',
'recommended' => '\stdClass[]?'
];
}
}
@@ -0,0 +1,108 @@
<?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 course_content_items_exporter class.
*
* @package core
* @subpackage course
* @copyright 2020 Jake Dallimore <jrhdallimore@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_course\local\exporters;
defined('MOODLE_INTERNAL') || die();
use core\external\exporter;
use core_course\local\entity\content_item;
/**
* The course_content_items_exporter class.
*
* @copyright 2020 Jake Dallimore <jrhdallimore@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class course_content_items_exporter extends exporter {
/** @var content_item[] the array of content items. */
private $contentitems;
/**
* The course_content_items_exporter constructor.
*
* @param array $contentitems the array of \core_course\local\entity\content_item objects to export.
* @param array $related any related objects, see define_related for what's expected.
*/
public function __construct(array $contentitems, array $related) {
$this->contentitems = $contentitems;
parent::__construct([], $related);
}
/**
* Return the properties defining this export.
*
* @return array the array of properties.
*/
public static function define_properties() {
return [
'content_items' => [
'type' => course_content_item_exporter::read_properties_definition(),
'multiple' => true
]
];
}
/**
* Generate and return the data for this export.
*
* @param \renderer_base $output
* @return array the array of course content_items
*/
protected function get_other_values(\renderer_base $output) {
$contentitemexport = function(content_item $contentitem) use ($output) {
$exporter = new course_content_item_exporter(
$contentitem,
[
'context' => $this->related['context'],
'favouriteitems' => $this->related['favouriteitems'],
'recommended' => $this->related['recommended']
]
);
return $exporter->export($output);
};
$exportedcontentitems = array_map($contentitemexport, $this->contentitems);
return [
'content_items' => $exportedcontentitems
];
}
/**
* Define the list of related objects, used by this exporter.
*
* @return array the list of related objects.
*/
protected static function define_related() {
return [
'context' => '\context',
'favouriteitems' => '\stdClass[]?',
'recommended' => '\stdClass[]?'
];
}
}
@@ -0,0 +1,56 @@
<?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 service_factory, a locator for services for course content items.
*
* Services encapsulate the business logic, and any data manipulation code, and are what clients should interact with.
*
* @package core_course
* @copyright 2020 Adrian Greeve <adrian@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_course\local\factory;
defined('MOODLE_INTERNAL') || die();
use core_course\local\repository\caching_content_item_readonly_repository;
use core_course\local\repository\content_item_readonly_repository;
use core_course\local\service\content_item_service;
/**
* Class service_factory, providing functions for location of service objects for course content items.
*
* This class is responsible for providing service objects to clients only.
*
* @copyright 2020 Adrian Greeve <adrian@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class content_item_service_factory {
/**
* Returns a basic service object providing operations for course content items.
*
* @return content_item_service
*/
public static function get_content_item_service(): content_item_service {
return new content_item_service(
new caching_content_item_readonly_repository(
\cache::make('core', 'user_course_content_items'),
new content_item_readonly_repository()
)
);
}
}
@@ -0,0 +1,88 @@
<?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 caching_content_item_repository, for fetching content_items, with additional caching.
*
* @package core
* @subpackage course
* @copyright 2020 Jake Dallimore <jrhdallimore@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_course\local\repository;
defined('MOODLE_INTERNAL') || die();
/**
* The class caching_content_item_repository, for fetching content_items, with additional caching.
*
* This class decorates the content_item_repository and uses the supplied cache to store content items for user and course
* combinations. The content items for subsequent calls are returned from the cache if present, else are retrieved from the wrapped
* content_item_repository.
*
* @copyright 2020 Jake Dallimore <jrhdallimore@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class caching_content_item_readonly_repository implements content_item_readonly_repository_interface {
/** @var \cache $cachestore the cache to use. */
private $cachestore;
/** @var content_item_readonly_repository $contentitemrepository a content item repository. */
private $contentitemrepository;
/**
* The caching_content_item_readonly_repository constructor.
*
* @param \cache $cachestore a cache to use.
* @param content_item_readonly_repository $contentitemrepository the repository to use as a fallback, after a cache miss.
*/
public function __construct(\cache $cachestore, content_item_readonly_repository $contentitemrepository) {
$this->cachestore = $cachestore;
$this->contentitemrepository = $contentitemrepository;
}
/**
* Find all the content items for a given course and user.
*
* @param \stdClass $course The course to find content items for.
* @param \stdClass $user the user to pass to plugins.
* @return array the array of content items.
*/
public function find_all_for_course(\stdClass $course, \stdClass $user): array {
global $USER;
// Try to find this data in the cache first.
$key = $USER->id . '_' . $course->id;
$contentitems = $this->cachestore->get($key);
if ($contentitems !== false) {
return $contentitems;
}
// If we can't find it there, we must get it from the slow data store, updating the cache in the process.
$contentitems = $this->contentitemrepository->find_all_for_course($course, $user);
$this->cachestore->set($key, $contentitems);
return $contentitems;
}
/**
* Find all the content items made available by core and plugins.
*
* @return array
*/
public function find_all(): array {
return $this->contentitemrepository->find_all();
}
}
@@ -0,0 +1,246 @@
<?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 content_item_repository, for fetching content_items.
*
* @package core
* @subpackage course
* @copyright 2020 Jake Dallimore <jrhdallimore@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_course\local\repository;
defined('MOODLE_INTERNAL') || die();
use core_component;
use core_course\local\entity\content_item;
use core_course\local\entity\lang_string_title;
/**
* The class content_item_repository, for reading content_items.
*
* @copyright 2020 Jake Dallimore <jrhdallimore@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class content_item_readonly_repository implements content_item_readonly_repository_interface {
/**
* Get the help string for content items representing core modules.
*
* @param string $modname the module name.
* @return string the help string, including help link.
*/
private function get_core_module_help_string(string $modname): string {
global $OUTPUT;
$help = '';
$sm = get_string_manager();
if ($sm->string_exists('modulename_help', $modname)) {
$help = get_string('modulename_help', $modname);
if ($sm->string_exists('modulename_link', $modname)) { // Link to further info in Moodle docs.
$link = get_string('modulename_link', $modname);
$linktext = get_string('morehelp');
$arialabel = get_string('morehelpaboutmodule', '', get_string('modulename', $modname));
$doclink = $OUTPUT->doc_link($link, $linktext, true, ['aria-label' => $arialabel]);
$help .= \html_writer::tag('div', $doclink, ['class' => 'helpdoclink']);
}
}
return $help;
}
/**
* Helper to get the contentitems from all subplugin hooks for a given module plugin.
*
* @param string $parentpluginname the name of the module plugin to check subplugins for.
* @param content_item $modulecontentitem the content item of the module plugin, to pass to the hooks.
* @param \stdClass $user the user object to pass to subplugins.
* @return array the array of content items.
*/
private function get_subplugin_course_content_items(string $parentpluginname, content_item $modulecontentitem,
\stdClass $user): array {
$contentitems = [];
$pluginmanager = \core_plugin_manager::instance();
foreach ($pluginmanager->get_subplugins_of_plugin($parentpluginname) as $subpluginname => $subplugin) {
// Call the hook, but with a copy of the module content item data.
$spcontentitems = component_callback($subpluginname, 'get_course_content_items', [$modulecontentitem, $user], null);
if (!is_null($spcontentitems)) {
foreach ($spcontentitems as $spcontentitem) {
$contentitems[] = $spcontentitem;
}
}
}
return $contentitems;
}
/**
* Get all the content items for a subplugin.
*
* @param string $parentpluginname
* @param content_item $modulecontentitem
* @return array
*/
private function get_subplugin_all_content_items(string $parentpluginname, content_item $modulecontentitem): array {
$contentitems = [];
$pluginmanager = \core_plugin_manager::instance();
foreach ($pluginmanager->get_subplugins_of_plugin($parentpluginname) as $subpluginname => $subplugin) {
// Call the hook, but with a copy of the module content item data.
$spcontentitems = component_callback($subpluginname, 'get_all_content_items', [$modulecontentitem], null);
if (!is_null($spcontentitems)) {
foreach ($spcontentitems as $spcontentitem) {
$contentitems[] = $spcontentitem;
}
}
}
return $contentitems;
}
/**
* Find all the available content items, not restricted to course or user.
*
* @return array the array of content items.
*/
public function find_all(): array {
global $OUTPUT, $DB, $CFG;
// Get all modules so we know which plugins are enabled and able to add content.
// Only module plugins may add content items.
$modules = $DB->get_records('modules', ['visible' => 1]);
$return = [];
// Now, generate the content_items.
foreach ($modules as $modid => $mod) {
// Exclude modules if the code doesn't exist.
if (!file_exists("$CFG->dirroot/mod/$mod->name/lib.php")) {
continue;
}
// Create the content item for the module itself.
// If the module chooses to implement the hook, this may be thrown away.
$help = $this->get_core_module_help_string($mod->name);
$archetype = plugin_supports('mod', $mod->name, FEATURE_MOD_ARCHETYPE, MOD_ARCHETYPE_OTHER);
$purpose = plugin_supports('mod', $mod->name, FEATURE_MOD_PURPOSE, MOD_PURPOSE_OTHER);
$isbranded = component_callback('mod_' . $mod->name, 'is_branded', [], false);
$contentitem = new content_item(
$mod->id,
$mod->name,
new lang_string_title("modulename", $mod->name),
new \moodle_url(''), // No course scope, so just an empty link.
$OUTPUT->pix_icon('monologo', '', $mod->name, ['class' => 'icon activityicon']),
$help,
$archetype,
'mod_' . $mod->name,
$purpose,
$isbranded,
);
$modcontentitemreference = clone($contentitem);
if (component_callback_exists('mod_' . $mod->name, 'get_all_content_items')) {
// Call the module hooks for this module.
$plugincontentitems = component_callback('mod_' . $mod->name, 'get_all_content_items',
[$modcontentitemreference], []);
if (!empty($plugincontentitems)) {
array_push($return, ...$plugincontentitems);
}
// Now, get those for subplugins of the module.
$subplugincontentitems = $this->get_subplugin_all_content_items('mod_' . $mod->name, $modcontentitemreference);
if (!empty($subplugincontentitems)) {
array_push($return, ...$subplugincontentitems);
}
} else {
// Neither callback was found, so just use the default module content item.
$return[] = $contentitem;
}
}
return $return;
}
/**
* Get the list of potential content items for the given course.
*
* @param \stdClass $course the course
* @param \stdClass $user the user, to pass to plugins implementing callbacks.
* @return array the array of content_item objects
*/
public function find_all_for_course(\stdClass $course, \stdClass $user): array {
global $OUTPUT, $DB, $CFG;
// Get all modules so we know which plugins are enabled and able to add content.
// Only module plugins may add content items.
$modules = $DB->get_records('modules', ['visible' => 1]);
$return = [];
// Now, generate the content_items.
foreach ($modules as $modid => $mod) {
// Exclude modules if the code doesn't exist.
if (!file_exists("$CFG->dirroot/mod/$mod->name/lib.php")) {
continue;
}
// Create the content item for the module itself.
// If the module chooses to implement the hook, this may be thrown away.
$help = $this->get_core_module_help_string($mod->name);
$archetype = plugin_supports('mod', $mod->name, FEATURE_MOD_ARCHETYPE, MOD_ARCHETYPE_OTHER);
$purpose = plugin_supports('mod', $mod->name, FEATURE_MOD_PURPOSE, MOD_PURPOSE_OTHER);
$isbranded = component_callback('mod_' . $mod->name, 'is_branded', [], false);
$icon = 'monologo';
// Quick check for monologo icons.
// Plugins that don't have monologo icons will be displayed as is and CSS filter will not be applied.
$hasmonologoicons = core_component::has_monologo_icon('mod', $mod->name);
$iconclass = '';
if (!$hasmonologoicons) {
$iconclass = 'nofilter';
}
$contentitem = new content_item(
$mod->id,
$mod->name,
new lang_string_title("modulename", $mod->name),
new \moodle_url('/course/mod.php', ['id' => $course->id, 'add' => $mod->name]),
$OUTPUT->pix_icon($icon, '', $mod->name, ['class' => "activityicon $iconclass"]),
$help,
$archetype,
'mod_' . $mod->name,
$purpose,
$isbranded,
);
$modcontentitemreference = clone($contentitem);
if (component_callback_exists('mod_' . $mod->name, 'get_course_content_items')) {
// Call the module hooks for this module.
$plugincontentitems = component_callback('mod_' . $mod->name, 'get_course_content_items',
[$modcontentitemreference, $user, $course], []);
if (!empty($plugincontentitems)) {
array_push($return, ...$plugincontentitems);
}
// Now, get those for subplugins of the module.
$subpluginitems = $this->get_subplugin_course_content_items('mod_' . $mod->name, $modcontentitemreference, $user);
if (!empty($subpluginitems)) {
array_push($return, ...$subpluginitems);
}
} else {
// Callback was not found, so just use the default module content item.
$return[] = $contentitem;
}
}
return $return;
}
}
@@ -0,0 +1,48 @@
<?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 interface content_item_readonly_repository_interface, defining operations for readonly content item repositories.
*
* This interface is not considered a published interface and serves to govern internal, local repository objects only.
* All calling code should use instances of the service classes, and should not interact with repositories directly.
*
* @package core
* @subpackage course
* @copyright 2020 Jake Dallimore <jrhdallimore@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_course\local\repository;
defined('MOODLE_INTERNAL') || die();
interface content_item_readonly_repository_interface {
/**
* Find all content items for a given course and user.
*
* @param \stdClass $course the course object.
* @param \stdClass $user the user object.
* @return array the array of content items.
*/
public function find_all_for_course(\stdClass $course, \stdClass $user): array;
/**
* Find all content items that can be presented, irrespective of course.
*
* @return array the array of content items.
*/
public function find_all(): array;
}
@@ -0,0 +1,386 @@
<?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 content_item_service class.
*
* @package core
* @subpackage course
* @copyright 2020 Jake Dallimore <jrhdallimore@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_course\local\service;
defined('MOODLE_INTERNAL') || die();
use core_course\local\exporters\course_content_items_exporter;
use core_course\local\repository\content_item_readonly_repository_interface;
/**
* The content_item_service class, providing the api for interacting with content items.
*
* @copyright 2020 Jake Dallimore <jrhdallimore@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class content_item_service {
/** @var content_item_readonly_repository_interface $repository a repository for content items. */
private $repository;
/** string the component for this favourite. */
public const COMPONENT = 'core_course';
/** string the favourite prefix itemtype in the favourites table. */
public const FAVOURITE_PREFIX = 'contentitem_';
/** string the recommendation prefix itemtype in the favourites table. */
public const RECOMMENDATION_PREFIX = 'recommend_';
/** string the cache name for recommendations. */
public const RECOMMENDATION_CACHE = 'recommendation_favourite_course_content_items';
/**
* The content_item_service constructor.
*
* @param content_item_readonly_repository_interface $repository a content item repository.
*/
public function __construct(content_item_readonly_repository_interface $repository) {
$this->repository = $repository;
}
/**
* Returns an array of objects representing favourited content items.
*
* Each object contains the following properties:
* itemtype: a string containing the 'itemtype' key used by the favourites subsystem.
* ids[]: an array of ids, representing the content items within a component.
*
* Since two components can return (via their hook implementation) the same id, the itemtype is used for uniqueness.
*
* @param \stdClass $user
* @return array
*/
private function get_favourite_content_items_for_user(\stdClass $user): array {
$favcache = \cache::make('core', 'user_favourite_course_content_items');
$key = $user->id;
$favmods = $favcache->get($key);
if ($favmods !== false) {
return $favmods;
}
$favourites = $this->get_content_favourites(self::FAVOURITE_PREFIX, \context_user::instance($user->id));
$favcache->set($key, $favourites);
return $favourites;
}
/**
* Returns an array of objects representing recommended content items.
*
* Each object contains the following properties:
* itemtype: a string containing the 'itemtype' key used by the favourites subsystem.
* ids[]: an array of ids, representing the content items within a component.
*
* Since two components can return (via their hook implementation) the same id, the itemtype is used for uniqueness.
*
* @return array
*/
private function get_recommendations(): array {
global $CFG;
$recommendationcache = \cache::make('core', self::RECOMMENDATION_CACHE);
$key = $CFG->siteguest;
$favmods = $recommendationcache->get($key);
if ($favmods !== false) {
return $favmods;
}
// Make sure the guest user exists in the database.
if (!\core_user::get_user($CFG->siteguest)) {
throw new \coding_exception('The guest user does not exist in the database.');
}
// Make sure the guest user context exists.
if (!$guestusercontext = \context_user::instance($CFG->siteguest, false)) {
throw new \coding_exception('The guest user context does not exist.');
}
$favourites = $this->get_content_favourites(self::RECOMMENDATION_PREFIX, $guestusercontext);
$recommendationcache->set($CFG->siteguest, $favourites);
return $favourites;
}
/**
* Gets content favourites from the favourites system depending on the area.
*
* @param string $prefix Prefix for the item type.
* @param \context_user $usercontext User context for the favourite
* @return array An array of favourite objects.
*/
private function get_content_favourites(string $prefix, \context_user $usercontext): array {
// Get all modules and any submodules which implement get_course_content_items() hook.
// This gives us the set of all itemtypes which we'll use to register favourite content items.
// The ids that each plugin returns will be used together with the itemtype to uniquely identify
// each content item for favouriting.
$pluginmanager = \core_plugin_manager::instance();
$plugins = $pluginmanager->get_plugins_of_type('mod');
$itemtypes = [];
foreach ($plugins as $plugin) {
// Add the mod itself.
$itemtypes[] = $prefix . 'mod_' . $plugin->name;
// Add any subplugins to the list of item types.
$subplugins = $pluginmanager->get_subplugins_of_plugin('mod_' . $plugin->name);
foreach ($subplugins as $subpluginname => $subplugininfo) {
try {
if (component_callback_exists($subpluginname, 'get_course_content_items')) {
$itemtypes[] = $prefix . $subpluginname;
}
} catch (\moodle_exception $e) {
debugging('Cannot get_course_content_items: ' . $e->getMessage(), DEBUG_DEVELOPER);
}
}
}
$ufservice = \core_favourites\service_factory::get_service_for_user_context($usercontext);
$favourites = [];
$favs = $ufservice->find_all_favourites(self::COMPONENT, $itemtypes);
$favsreduced = array_reduce($favs, function($carry, $item) {
$carry[$item->itemtype][$item->itemid] = 0;
return $carry;
}, []);
foreach ($itemtypes as $type) {
$favourites[] = (object) [
'itemtype' => $type,
'ids' => isset($favsreduced[$type]) ? array_keys($favsreduced[$type]) : []
];
}
return $favourites;
}
/**
* Get all content items which may be added to courses, irrespective of course caps, for site admin views, etc.
*
* @param \stdClass $user the user object.
* @return array the array of exported content items.
*/
public function get_all_content_items(\stdClass $user): array {
$allcontentitems = $this->repository->find_all();
return $this->export_content_items($user, $allcontentitems);
}
/**
* Get content items which name matches a certain pattern and may be added to courses,
* irrespective of course caps, for site admin views, etc.
*
* @param \stdClass $user The user object.
* @param string $pattern The search pattern.
* @return array The array of exported content items.
*/
public function get_content_items_by_name_pattern(\stdClass $user, string $pattern): array {
$allcontentitems = $this->repository->find_all();
$filteredcontentitems = array_filter($allcontentitems, function($contentitem) use ($pattern) {
return preg_match("/$pattern/i", $contentitem->get_title()->get_value());
});
return $this->export_content_items($user, $filteredcontentitems);
}
/**
* Export content items.
*
* @param \stdClass $user The user object.
* @param array $contentitems The content items array.
* @return array The array of exported content items.
*/
private function export_content_items(\stdClass $user, $contentitems) {
global $PAGE;
// Export the objects to get the formatted objects for transfer/display.
$favourites = $this->get_favourite_content_items_for_user($user);
$recommendations = $this->get_recommendations();
$ciexporter = new course_content_items_exporter(
$contentitems,
[
'context' => \context_system::instance(),
'favouriteitems' => $favourites,
'recommended' => $recommendations
]
);
$exported = $ciexporter->export($PAGE->get_renderer('core'));
// Sort by title for return.
\core_collator::asort_objects_by_property($exported->content_items, 'title');
return array_values($exported->content_items);
}
/**
* Return a representation of the available content items, for a user in a course.
*
* @param \stdClass $user the user to check access for.
* @param \stdClass $course the course to scope the content items to.
* @param array $linkparams the desired section to return to.
* @return \stdClass[] the content items, scoped to a course.
*/
public function get_content_items_for_user_in_course(\stdClass $user, \stdClass $course, array $linkparams = []): array {
global $PAGE;
if (!has_capability('moodle/course:manageactivities', \context_course::instance($course->id), $user)) {
return [];
}
// Get all the visible content items.
$allcontentitems = $this->repository->find_all_for_course($course, $user);
// Content items can only originate from modules or submodules.
$pluginmanager = \core_plugin_manager::instance();
$components = \core_component::get_component_list();
$parents = [];
foreach ($allcontentitems as $contentitem) {
if (!in_array($contentitem->get_component_name(), array_keys($components['mod']))) {
// It could be a subplugin.
$info = $pluginmanager->get_plugin_info($contentitem->get_component_name());
if (!is_null($info)) {
$parent = $info->get_parent_plugin();
if ($parent != false) {
if (in_array($parent, array_keys($components['mod']))) {
$parents[$contentitem->get_component_name()] = $parent;
continue;
}
}
}
throw new \moodle_exception('Only modules and submodules can generate content items. \''
. $contentitem->get_component_name() . '\' is neither.');
}
$parents[$contentitem->get_component_name()] = $contentitem->get_component_name();
}
// Now, check access to these items for the user.
$availablecontentitems = array_filter($allcontentitems, function($contentitem) use ($course, $user, $parents) {
// Check the parent module access for the user.
return course_allowed_module($course, explode('_', $parents[$contentitem->get_component_name()])[1], $user);
});
// Add the link params to the link, if any have been provided.
if (!empty($linkparams)) {
$availablecontentitems = array_map(function ($item) use ($linkparams) {
$item->get_link()->params($linkparams);
return $item;
}, $availablecontentitems);
}
// Export the objects to get the formatted objects for transfer/display.
$favourites = $this->get_favourite_content_items_for_user($user);
$recommended = $this->get_recommendations();
$ciexporter = new course_content_items_exporter(
$availablecontentitems,
[
'context' => \context_course::instance($course->id),
'favouriteitems' => $favourites,
'recommended' => $recommended
]
);
$exported = $ciexporter->export($PAGE->get_renderer('course'));
// Sort by title for return.
\core_collator::asort_objects_by_property($exported->content_items, 'title');
return array_values($exported->content_items);
}
/**
* Add a content item to a user's favourites.
*
* @param \stdClass $user the user whose favourite this is.
* @param string $componentname the name of the component from which the content item originates.
* @param int $contentitemid the id of the content item.
* @return \stdClass the exported content item.
*/
public function add_to_user_favourites(\stdClass $user, string $componentname, int $contentitemid): \stdClass {
$usercontext = \context_user::instance($user->id);
$ufservice = \core_favourites\service_factory::get_service_for_user_context($usercontext);
// Because each plugin decides its own ids for content items, a combination of
// itemtype and id is used to guarantee uniqueness across all content items.
$itemtype = self::FAVOURITE_PREFIX . $componentname;
$ufservice->create_favourite(self::COMPONENT, $itemtype, $contentitemid, $usercontext);
$favcache = \cache::make('core', 'user_favourite_course_content_items');
$favcache->delete($user->id);
$items = $this->get_all_content_items($user);
return $items[array_search($contentitemid, array_column($items, 'id'))];
}
/**
* Remove the content item from a user's favourites.
*
* @param \stdClass $user the user whose favourite this is.
* @param string $componentname the name of the component from which the content item originates.
* @param int $contentitemid the id of the content item.
* @return \stdClass the exported content item.
*/
public function remove_from_user_favourites(\stdClass $user, string $componentname, int $contentitemid): \stdClass {
$usercontext = \context_user::instance($user->id);
$ufservice = \core_favourites\service_factory::get_service_for_user_context($usercontext);
// Because each plugin decides its own ids for content items, a combination of
// itemtype and id is used to guarantee uniqueness across all content items.
$itemtype = self::FAVOURITE_PREFIX . $componentname;
$ufservice->delete_favourite(self::COMPONENT, $itemtype, $contentitemid, $usercontext);
$favcache = \cache::make('core', 'user_favourite_course_content_items');
$favcache->delete($user->id);
$items = $this->get_all_content_items($user);
return $items[array_search($contentitemid, array_column($items, 'id'))];
}
/**
* Toggle an activity to being recommended or not.
*
* @param string $itemtype The component such as mod_assign, or assignsubmission_file
* @param int $itemid The id related to this component item.
* @return bool True on creating a favourite, false on deleting it.
*/
public function toggle_recommendation(string $itemtype, int $itemid): bool {
global $CFG;
$context = \context_system::instance();
$itemtype = self::RECOMMENDATION_PREFIX . $itemtype;
// Favourites are created using a user context. We'll use the site guest user ID as that should not change and there
// can be only one.
$usercontext = \context_user::instance($CFG->siteguest);
$recommendationcache = \cache::make('core', self::RECOMMENDATION_CACHE);
$favouritefactory = \core_favourites\service_factory::get_service_for_user_context($usercontext);
if ($favouritefactory->favourite_exists(self::COMPONENT, $itemtype, $itemid, $context)) {
$favouritefactory->delete_favourite(self::COMPONENT, $itemtype, $itemid, $context);
$result = $recommendationcache->delete($CFG->siteguest);
return false;
} else {
$favouritefactory->create_favourite(self::COMPONENT, $itemtype, $itemid, $context);
$result = $recommendationcache->delete($CFG->siteguest);
return true;
}
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,157 @@
<?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_course\output;
use cm_info;
use core_availability\info;
use core_completion\cm_completion_details;
use core_user;
use core_user\fields;
use renderable;
use renderer_base;
use stdClass;
use templatable;
/**
* The activity completion renderable class.
*
* @package core_course
* @copyright 2023 Mikel Martín <mikel@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class activity_completion implements renderable, templatable {
/**
* Constructor.
*
* @param cm_info $cminfo The course module information.
* @param cm_completion_details $cmcompletion The course module information.
*/
public function __construct(
protected cm_info $cminfo,
protected cm_completion_details $cmcompletion
) {
}
/**
* Export this data so it can be used as the context for a mustache template.
*
* @param renderer_base $output Renderer base.
* @return stdClass
*/
public function export_for_template(renderer_base $output): stdClass {
global $CFG;
$overallcompletion = $this->cmcompletion->get_overall_completion();
$isoverallcomplete = $this->cmcompletion->is_overall_complete();
$overrideby = $this->get_overrideby();
$course = $this->cminfo->get_course();
// Whether the completion of this activity controls the availability of other activities/sections in the course.
// An activity with manual completion tracking which is used to enable access to other activities/sections in
// the course needs to refresh the page after having its completion state toggled. This withavailability flag will enable
// this functionality on the course homepage. Otherwise, the completion toggling will just happen normally via ajax.
if ($this->cmcompletion->has_completion() && $this->cmcompletion->is_manual()) {
$withavailability = !empty($CFG->enableavailability) && info::completion_value_used($course, $this->cminfo->id);
}
return (object) [
'cmid' => $this->cminfo->id,
'activityname' => $this->cminfo->get_formatted_name(),
'uservisible' => $this->cminfo->uservisible,
'hascompletion' => $this->cmcompletion->has_completion(),
'isautomatic' => $this->cmcompletion->is_automatic(),
'ismanual' => $this->cmcompletion->is_manual(),
'showmanualcompletion' => $this->cmcompletion->show_manual_completion(),
'istrackeduser' => $this->cmcompletion->is_tracked_user(),
'overallcomplete' => $isoverallcomplete,
'overallincomplete' => !$isoverallcomplete,
'withavailability' => $withavailability ?? false,
'overrideby' => $overrideby,
'completiondetails' => $this->get_completion_details($overrideby),
'accessibledescription' => $this->get_accessible_description($overrideby, $overallcompletion),
];
}
/**
* Returns the name of the user overriding the completion condition, if available.
*
* @return string
*/
private function get_overrideby(): string {
$overrideby = $this->cmcompletion->overridden_by();
if (!empty($overrideby)) {
$userfields = fields::for_name();
$overridebyrecord = core_user::get_user($overrideby, 'id ' . $userfields->get_sql()->selects, MUST_EXIST);
return fullname($overridebyrecord);
}
return '';
}
/**
* Returns automatic completion details
*
* @param string $overrideby The name of the user overriding the completion condition, if available.
* @return array
*/
private function get_completion_details($overrideby): array {
$details = [];
foreach ($this->cmcompletion->get_details() as $key => $detail) {
$detail->key = $key;
$detail->statuscomplete = in_array($detail->status, [COMPLETION_COMPLETE, COMPLETION_COMPLETE_PASS]);
$detail->statuscompletefail = $detail->status == COMPLETION_COMPLETE_FAIL;
// This is not used by core themes but may be needed in custom themes.
$detail->statuscompletepass = $detail->status == COMPLETION_COMPLETE_PASS;
$detail->statusincomplete = $detail->status == COMPLETION_INCOMPLETE;
// Add an accessible description to be used for title and aria-label attributes for overridden completion details.
if ($overrideby) {
$setbydata = (object)[
'condition' => $detail->description,
'setby' => $overrideby,
];
$overridestatus = $detail->statuscomplete ? 'done' : 'todo';
$detail->accessibledescription = get_string('completion_setby:auto:' . $overridestatus, 'course', $setbydata);
}
unset($detail->status);
$details[] = $detail;
}
return $details;
}
/**
* Returns the accessible description for manual completions with overridden completion state.
*
* @param string $overrideby The name of the user overriding the completion condition, if available.
* @param int $overallcompletion The overall completion state of the activity.
* @return string
*/
private function get_accessible_description($overrideby, $overallcompletion): string {
if ($this->cmcompletion->is_manual() && $overrideby) {
$setbydata = (object)[
'activityname' => $this->cminfo->get_formatted_name(),
'setby' => $overrideby,
];
$isoverallcompleted = $overallcompletion == COMPLETION_COMPLETE;
$setbylangkey = $isoverallcompleted ? 'completion_setby:manual:done' : 'completion_setby:manual:markdone';
return get_string($setbylangkey, 'course', $setbydata);
}
return '';
}
}
+72
View File
@@ -0,0 +1,72 @@
<?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_course\output;
use renderable;
use renderer_base;
use stdClass;
use templatable;
/**
* The activity dates renderable class.
*
* @package core_course
* @copyright 2023 Mikel Martín <mikel@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class activity_dates implements renderable, templatable {
/**
* Constructor.
*
* @param array $activitydates The activity dates.
*/
public function __construct(
protected array $activitydates
) {
}
/**
* Export this data so it can be used as the context for a mustache template.
*
* @param renderer_base $output Renderer base.
* @return stdClass
*/
public function export_for_template(renderer_base $output): stdClass {
$activitydates = [];
foreach ($this->activitydates as $date) {
if (empty($date['relativeto'])) {
$date['datestring'] = userdate($date['timestamp'], get_string('strftimedaydatetime', 'core_langconfig'));
} else {
$diffstr = get_time_interval_string($date['timestamp'], $date['relativeto']);
if ($date['timestamp'] >= $date['relativeto']) {
$date['datestring'] = get_string('relativedatessubmissionduedateafter', 'core_course',
['datediffstr' => $diffstr]);
} else {
$date['datestring'] = get_string('relativedatessubmissionduedatebefore', 'core_course',
['datediffstr' => $diffstr]);
}
}
$activitydates[] = $date;
}
return (object) [
'hasdates' => !empty($this->activitydates),
'activitydates' => $activitydates,
];
}
}
@@ -0,0 +1,209 @@
<?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/>.
/**
* File containing the class activity information renderable.
*
* @package core_course
* @copyright 2021 Jun Pataleta <jun@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_course\output;
defined('MOODLE_INTERNAL') || die();
use cm_info;
use completion_info;
use context;
use core\activity_dates;
use core_availability\info;
use core_completion\cm_completion_details;
use core_user;
use core_user\fields;
use renderable;
use renderer_base;
use stdClass;
use templatable;
/**
* The activity information renderable class.
*
* @deprecated since Moodle 4.3 MDL-78744
* @todo MDL-78926 This class will be deleted in Moodle 4.7
*
* @package core_course
* @copyright 2021 Jun Pataleta <jun@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class activity_information implements renderable, templatable {
/** @var cm_info The course module information. */
protected $cminfo = null;
/** @var array The array of relevant dates for this activity. */
protected $activitydates = [];
/** @var cm_completion_details The user's completion details for this activity. */
protected $cmcompletion = null;
/**
* Constructor.
*
* @deprecated since Moodle 4.3
*
* @param cm_info $cminfo The course module information.
* @param cm_completion_details $cmcompletion The course module information.
* @param array $activitydates The activity dates.
*/
public function __construct(cm_info $cminfo, cm_completion_details $cmcompletion, array $activitydates) {
debugging('activity_information class is deprecated. Use activity_completion and activity_dates instead.', DEBUG_DEVELOPER);
$this->cminfo = $cminfo;
$this->cmcompletion = $cmcompletion;
$this->activitydates = $activitydates;
}
/**
* Export this data so it can be used as the context for a mustache template.
*
* @deprecated since Moodle 4.3
*
* @param renderer_base $output Renderer base.
* @return stdClass
*/
public function export_for_template(renderer_base $output): stdClass {
debugging('activity_information class is deprecated. Use activity_completion and activity_dates instead.', DEBUG_DEVELOPER);
$data = $this->build_completion_data();
$data->cmid = $this->cminfo->id;
$data->activityname = $this->cminfo->get_formatted_name();
$this->build_dates_data($data);
$data->hasdates = !empty($this->activitydates);
return $data;
}
/**
* Builds the dates data for export.
*
* @param stdClass $data
*/
protected function build_dates_data(stdClass $data): void {
foreach ($this->activitydates as $date) {
if (empty($date['relativeto'])) {
$date['datestring'] = userdate($date['timestamp'], get_string('strftimedaydatetime', 'core_langconfig'));
} else {
$diffstr = get_time_interval_string($date['timestamp'], $date['relativeto']);
if ($date['timestamp'] >= $date['relativeto']) {
$date['datestring'] = get_string('relativedatessubmissionduedateafter', 'core_course',
['datediffstr' => $diffstr]);
} else {
$date['datestring'] = get_string('relativedatessubmissionduedatebefore', 'core_course',
['datediffstr' => $diffstr]);
}
}
$data->activitydates[] = $date;
}
}
/**
* Builds the completion data for export.
*
* @return stdClass
*/
protected function build_completion_data(): stdClass {
global $CFG;
$data = new stdClass();
$data->hascompletion = $this->cmcompletion->has_completion();
$data->isautomatic = $this->cmcompletion->is_automatic();
$data->ismanual = $this->cmcompletion->is_manual();
$data->showmanualcompletion = $this->cmcompletion->show_manual_completion();
// Get the name of the user overriding the completion condition, if available.
$data->overrideby = null;
$overrideby = $this->cmcompletion->overridden_by();
$overridebyname = null;
if (!empty($overrideby)) {
$userfields = fields::for_name();
$overridebyrecord = core_user::get_user($overrideby, 'id ' . $userfields->get_sql()->selects, MUST_EXIST);
$data->overrideby = fullname($overridebyrecord);
}
// We'll show only the completion conditions and not the completion status if we're not tracking completion for this user
// (e.g. a teacher, admin).
$data->istrackeduser = $this->cmcompletion->is_tracked_user();
// Overall completion states.
$overallcompletion = $this->cmcompletion->get_overall_completion();
$data->overallcomplete = $overallcompletion == COMPLETION_COMPLETE;
$data->overallincomplete = $overallcompletion == COMPLETION_INCOMPLETE;
// Set an accessible description for manual completions with overridden completion state.
if (!$data->isautomatic && $data->overrideby) {
$setbydata = (object)[
'activityname' => $this->cminfo->get_formatted_name(),
'setby' => $data->overrideby,
];
$setbylangkey = $data->overallcomplete ? 'completion_setby:manual:done' : 'completion_setby:manual:markdone';
$data->accessibledescription = get_string($setbylangkey, 'course', $setbydata);
}
// Whether the completion of this activity controls the availability of other activities/sections in the course.
$data->withavailability = false;
$course = $this->cminfo->get_course();
// An activity with manual completion tracking which is used to enable access to other activities/sections in
// the course needs to refresh the page after having its completion state toggled. This withavailability flag will enable
// this functionality on the course homepage. Otherwise, the completion toggling will just happen normally via ajax.
if ($this->cmcompletion->has_completion() && !$this->cmcompletion->is_automatic()) {
$data->withavailability = !empty($CFG->enableavailability) && info::completion_value_used($course, $this->cminfo->id);
}
// Whether this activity is visible to the user. If not, completion information will not be shown.
$data->uservisible = $this->cminfo->uservisible;
// Build automatic completion details.
$details = [];
foreach ($this->cmcompletion->get_details() as $key => $detail) {
// Set additional attributes for the template.
$detail->key = $key;
$detail->statuscomplete = in_array($detail->status, [COMPLETION_COMPLETE, COMPLETION_COMPLETE_PASS]);
$detail->statuscompletefail = $detail->status == COMPLETION_COMPLETE_FAIL;
// This is not used by core themes but may be needed in custom themes.
$detail->statuscompletepass = $detail->status == COMPLETION_COMPLETE_PASS;
$detail->statusincomplete = $detail->status == COMPLETION_INCOMPLETE;
// Add an accessible description to be used for title and aria-label attributes for overridden completion details.
if ($data->overrideby) {
$setbydata = (object)[
'condition' => $detail->description,
'setby' => $data->overrideby,
];
$overridestatus = $detail->statuscomplete ? 'done' : 'todo';
$detail->accessibledescription = get_string('completion_setby:auto:' . $overridestatus, 'course', $setbydata);
}
// We don't need the status in the template.
unset($detail->status);
$details[] = $detail;
}
$data->completiondetails = $details;
return $data;
}
}
@@ -0,0 +1,127 @@
<?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/>.
/**
* File containing the class activity navigation renderable.
*
* @package core_course
* @copyright 2017 Mark Nelson <markn@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_course\output;
defined('MOODLE_INTERNAL') || die();
use renderable;
use templatable;
use url_select;
/**
* The class activity navigation renderable.
*
* @package core_course
* @copyright 2017 Mark Nelson <markn@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class activity_navigation implements renderable, templatable {
/**
* @var \action_link The action link object for the prev link.
*/
public $prevlink = null;
/**
* @var \action_link The action link object for the next link.
*/
public $nextlink = null;
/**
* @var url_select The url select object for the activity selector menu.
*/
public $activitylist = null;
/**
* Constructor.
*
* @param \cm_info|null $prevmod The previous module to display, null if none.
* @param \cm_info|null $nextmod The next module to display, null if none.
* @param array $activitylist The list of activity URLs (as key) and names (as value) for the activity dropdown menu.
*/
public function __construct($prevmod, $nextmod, $activitylist = array()) {
global $OUTPUT;
// Check if there is a previous module to display.
if ($prevmod) {
$linkurl = new \moodle_url($prevmod->url, array('forceview' => 1));
$linkname = $prevmod->get_formatted_name();
if (!$prevmod->visible) {
$linkname .= ' ' . get_string('hiddenwithbrackets');
}
$attributes = [
'class' => 'btn btn-link',
'id' => 'prev-activity-link',
];
$this->prevlink = new \action_link($linkurl, $OUTPUT->larrow() . ' ' . $linkname, null, $attributes);
}
// Check if there is a next module to display.
if ($nextmod) {
$linkurl = new \moodle_url($nextmod->url, array('forceview' => 1));
$linkname = $nextmod->get_formatted_name();
if (!$nextmod->visible) {
$linkname .= ' ' . get_string('hiddenwithbrackets');
}
$attributes = [
'class' => 'btn btn-link',
'id' => 'next-activity-link',
];
$this->nextlink = new \action_link($linkurl, $linkname . ' ' . $OUTPUT->rarrow(), null, $attributes);
}
// Render the activity list dropdown menu if available.
if (!empty($activitylist)) {
$select = new url_select($activitylist, '', array('' => get_string('jumpto')));
$select->set_label(get_string('jumpto'), array('class' => 'sr-only'));
$select->attributes = array('id' => 'jump-to-activity');
$this->activitylist = $select;
}
}
/**
* Export this data so it can be used as the context for a mustache template.
*
* @param \renderer_base $output Renderer base.
* @return \stdClass
*/
public function export_for_template(\renderer_base $output) {
$data = new \stdClass();
if ($this->prevlink) {
$data->prevlink = $this->prevlink->export_for_template($output);
}
if ($this->nextlink) {
$data->nextlink = $this->nextlink->export_for_template($output);
}
if ($this->activitylist) {
$data->activitylist = $this->activitylist->export_for_template($output);
}
return $data;
}
}
@@ -0,0 +1,154 @@
<?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;
defined('MOODLE_INTERNAL') || die;
require_once($CFG->dirroot.'/course/renderer.php');
/**
* Main renderer for the bulk activity completion stuff.
*
* @package core_course
* @copyright 2017 Adrian Greeve
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class core_course_bulk_activity_completion_renderer extends plugin_renderer_base {
/**
* @deprecated since Moodle 4.0
*/
public function navigation() {
throw new coding_exception(__FUNCTION__ . '() has been removed.');
}
/**
* Render the bulk completion tab.
*
* @param Array|stdClass $data the context data to pass to the template.
* @return bool|string
*/
public function bulkcompletion($data) {
return parent::render_from_template('core_course/bulkactivitycompletion', $data);
}
/**
* Render the default completion tab.
*
* @param array|stdClass $data the context data to pass to the template.
* @param array $modules The modules that have been sent through the form.
* @param moodleform $form The current form that has been sent.
* @return bool|string
*/
public function defaultcompletion($data, $modules, $form) {
$course = get_course($data->courseid);
foreach ($data->modules as $module) {
// If the user can manage this module, then the activity completion form needs to be returned too, without the
// cancel button (so only "Save changes" button is displayed).
if ($module->canmanage) {
// Only create the form if it's different from the one that has been sent.
$modform = $form;
if (empty($form) || !in_array($module->id, array_keys($modules))) {
$modform = new \core_completion_defaultedit_form(
null,
[
'course' => $course,
'modules' => [
$module->id => $module,
],
'displaycancel' => false,
'forceuniqueid' => true,
],
);
$module->modulecollapsed = true;
}
$moduleform = manager::get_module_form($module->name, $course);
if ($moduleform) {
$module->formhtml = $modform->render();
} else {
// If the module form is not available, then display a message.
$module->formhtml = $this->output->notification(
get_string('incompatibleplugin', 'completion'),
\core\output\notification::NOTIFY_INFO,
false
);
}
}
}
$data->issite = $course->id == SITEID;
return parent::render_from_template('core_course/defaultactivitycompletion', $data);
}
/**
* Renders the form for bulk editing activities completion
*
* @param moodleform $form
* @param array $activities
* @return string
*/
public function edit_bulk_completion($form, $activities) {
ob_start();
$form->display();
$formhtml = ob_get_contents();
ob_end_clean();
$data = (object)[
'form' => $formhtml,
'activities' => array_values($activities),
'activitiescount' => count($activities),
];
return parent::render_from_template('core_course/editbulkactivitycompletion', $data);
}
/**
* Renders the form for editing default completion
*
* @param moodleform $form
* @param array $modules
* @return string
* @deprecated since Moodle 4.3 MDL-78528
* @todo MDL-78711 This will be deleted in Moodle 4.7
*/
public function edit_default_completion($form, $modules) {
debugging('edit_default_completion() is deprecated and will be removed.', DEBUG_DEVELOPER);
ob_start();
$form->display();
$formhtml = ob_get_contents();
ob_end_clean();
$data = (object)[
'form' => $formhtml,
'modules' => array_values($modules),
'modulescount' => count($modules),
];
return parent::render_from_template('core_course/editdefaultcompletion', $data);
}
/**
* Renders the course completion action bar.
*
* @param \core_course\output\completion_action_bar $actionbar
* @return string The HTML output
*/
public function render_course_completion_action_bar(\core_course\output\completion_action_bar $actionbar): string {
$data = $actionbar->export_for_template($this->output);
return $this->output->render_from_template('core_course/completion_action_bar', $data);
}
}
@@ -0,0 +1,171 @@
<?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_course\output;
use context_coursecat;
use core_course_category;
use course_request;
use moodle_page;
use moodle_url;
/**
* Class responsible for generating the action bar (tertiary nav) elements in an individual category page
*
* @package core
* @copyright 2021 Peter Dias
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class category_action_bar extends manage_categories_action_bar {
/**
* @var object The current category we are referring to.
*/
protected $category;
/**
* Constructor category_action_bar
*
* @param moodle_page $page The page object
* @param object $category
* @param object|null $course The course that we are generating the nav for
* @param string|null $searchvalue
*/
public function __construct(moodle_page $page, object $category, ?object $course = null, ?string $searchvalue = null) {
$this->category = $category;
parent::__construct($page, 'courses', $course, $searchvalue);
}
/**
* Gets the url_select to be displayed in the participants page if available.
*
* @param \renderer_base $output
* @return object|null The content required to render the url_select
*/
protected function get_category_select(\renderer_base $output): ?object {
if (!$this->searchvalue && !core_course_category::is_simple_site()) {
$categories = core_course_category::make_categories_list();
if (count($categories) > 1) {
foreach ($categories as $id => $cat) {
$url = new moodle_url($this->page->url, ['categoryid' => $id]);
$options[$url->out()] = $cat;
}
$currenturl = new moodle_url($this->page->url, ['categoryid' => $this->category->id]);
$select = new \url_select($options, $currenturl, null);
$select->set_label(get_string('categories'), ['class' => 'sr-only']);
$select->class .= ' text-truncate w-100';
return $select->export_for_template($output);
}
}
return null;
}
/**
* Gets the additional options to be displayed within a 'More' dropdown in the tertiary navigation.
* The predefined order defined by UX is:
* - Add a course
* - Add a sub cat
* - Manage course
* - Request a course
* - Course pending approval
*
* @return array
*/
protected function get_additional_category_options(): array {
global $CFG, $DB;
if ($this->category->is_uservisible()) {
$context = get_category_or_system_context($this->category->id);
if (has_capability('moodle/course:create', $context)) {
$params = [
'category' => $this->category->id ?: $CFG->defaultrequestcategory,
'returnto' => $this->category->id ? 'category' : 'topcat'
];
$options[0] = [
'url' => new moodle_url('/course/edit.php', $params),
'string' => get_string('addnewcourse')
];
}
if (!empty($CFG->enablecourserequests)) {
// Display an option to request a new course.
if (course_request::can_request($context)) {
$params = [];
if ($context instanceof context_coursecat) {
$params['category'] = $context->instanceid;
}
$options[3] = [
'url' => new moodle_url('/course/request.php', $params),
'string' => get_string('requestcourse')
];
}
// Display the manage pending requests option.
if (has_capability('moodle/site:approvecourse', $context)) {
$disabled = !$DB->record_exists('course_request', array());
if (!$disabled) {
$options[4] = [
'url' => new moodle_url('/course/pending.php'),
'string' => get_string('coursespending')
];
}
}
}
}
if ($this->category->can_create_course() || $this->category->has_manage_capability()) {
// Add 'Manage' button if user has permissions to edit this category.
$options[2] = [
'url' => new moodle_url('/course/management.php', ['categoryid' => $this->category->id]),
'string' => get_string('managecourses')
];
if ($this->category->has_manage_capability()) {
$addsubcaturl = new moodle_url('/course/editcategory.php', array('parent' => $this->category->id));
$options[1] = [
'url' => $addsubcaturl,
'string' => get_string('addsubcategory')
];
}
}
// We have stored the options in a predefined order. Sort it based on index and return.
if (isset($options)) {
sort($options);
return ['options' => $options];
}
return [];
}
/**
* Export the content to be displayed on the category page.
*
* @param \renderer_base $output
* @return array Consists of the following:
* - categoryselect A list of available categories to be fed into a urlselect
* - search The course search form
* - additionaloptions Additional actions that can be performed in a category
*/
public function export_for_template(\renderer_base $output): array {
return [
'categoryselect' => $this->get_category_select($output),
'search' => $this->get_search_form(),
'additionaloptions' => $this->get_additional_category_options()
];
}
}
@@ -0,0 +1,75 @@
<?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_course\output;
use core\output\select_menu;
use core_completion\manager;
use moodle_url;
use renderable;
use renderer_base;
use templatable;
use url_select;
/**
* Renderable class for the action bar elements in the course completion pages.
*
* @package core_course
* @copyright 2022 Mihail Geshoski <mihail@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class completion_action_bar implements templatable, renderable {
/** @var int $courseid The course id. */
private $courseid;
/** @var moodle_url $currenturl The URL of the current page. */
private $currenturl;
/**
* The class constructor.
*
* @param int $courseid The course id.
* @param moodle_url $pageurl The URL of the current page.
*/
public function __construct(int $courseid, moodle_url $pageurl) {
$this->courseid = $courseid;
$this->currenturl = $pageurl;
}
/**
* Export the data for the mustache template.
*
* @param renderer_base $output renderer to be used to render the action bar elements.
* @return array The array which contains the data required to output the tertiary navigation selector for the course
* completion pages.
*/
public function export_for_template(renderer_base $output): array {
$selectmenu = new select_menu(
'coursecompletionnavigation',
manager::get_available_completion_options($this->courseid),
$this->currenturl->out(false)
);
$selectmenu->set_label(
get_string('coursecompletionnavigation', 'completion'),
['class' => 'sr-only']
);
return [
'navigation' => $selectmenu->export_for_template($output),
];
}
}
@@ -0,0 +1,61 @@
<?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/>.
/**
* Prepares content for buttons/links to course content export/download.
*
* @package core_course
* @copyright 2020 Michael Hawkins <michaelh@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_course\output;
/**
* Prepares content for buttons/links to course content export/download.
*
* @package core_course
* @copyright 2020 Michael Hawkins <michaelh@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class content_export_link {
/**
* Prepare and return the various attributes required for a link/button to populate/trigger the download course content modal.
*
* @param \context $context The context of the content being exported.
* @return \stdClass
*/
public static function get_attributes(\context $context): \stdClass {
global $CFG;
$downloadattr = new \stdClass();
$downloadattr->url = new \moodle_url('/course/downloadcontent.php', ['contextid' => $context->id]);
$downloadattr->displaystring = get_string('downloadcoursecontent', 'course');
$maxfilesize = display_size($CFG->maxsizeperdownloadcoursefile);
$downloadlink = new \moodle_url('/course/downloadcontent.php', ['contextid' => $context->id, 'download' => 1]);
$downloadattr->elementattributes = [
'data-downloadcourse' => 1,
'data-download-body' => get_string('downloadcourseconfirmation', 'course', $maxfilesize),
'data-download-button-text' => get_string('download'),
'data-download-link' => $downloadlink->out(false),
'data-download-title' => get_string('downloadcoursecontent', 'course'),
'data-overrides-tree-activation-key-handler' => 1,
];
return $downloadattr;
}
}
@@ -0,0 +1,164 @@
<?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_course\output;
use moodle_page;
use moodle_url;
/**
* Class responsible for generating the action bar (tertiary nav) elements in the category management page
*
* @package core
* @copyright 2021 Peter Dias
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class manage_categories_action_bar implements \renderable {
/** @var object $course The course we are dealing with. */
protected $course;
/** @var moodle_page $page The current page. */
protected $page;
/** @var string|null $viewmode The viewmode of the underlying page - Course and categories, categories or courses */
protected $viewmode;
/** @var string|null $heading The heading to display */
protected $heading;
/** @var string|null $searchvalue The search value if any */
protected $searchvalue;
/**
* Constructor for the manage_categories_action_bar
*
* @param moodle_page $page The page object
* @param string $viewmode The type of page we are viewing.
* @param object|null $course The course that we are generating the nav for
* @param string|null $searchvalue The search value if applicable
*/
public function __construct(moodle_page $page, string $viewmode, ?object $course, ?string $searchvalue) {
$this->course = $course;
$this->page = $page;
$this->viewmode = $viewmode;
$this->searchvalue = $searchvalue;
if ($searchvalue) {
$this->heading = get_string('searchresults');
}
}
/**
* Gets the url_select to be displayed in the participants page if available.
*
* @param \renderer_base $output
* @return object|null The content required to render the url_select
*/
protected function get_dropdown(\renderer_base $output): ?object {
// If a search is being performed then no need to display the dropdown.
if ($this->searchvalue) {
return null;
}
$modes = \core_course\management\helper::get_management_viewmodes();
$activeurl = null;
$content = [];
foreach ($modes as $mode => $description) {
$url = new moodle_url($this->page->url, ['view' => $mode]);
$content[$url->out()] = $description;
if ($this->viewmode == $mode) {
$activeurl = $url->out();
$this->heading = get_string("manage$mode");
}
}
// Default to the first option if asking for default. This is combined.
if (!$activeurl && $this->viewmode === 'default') {
$activeurl = array_key_first($content);
$this->heading = get_string("managecombined");
}
if ($content) {
$urlselect = new \url_select($content, $activeurl, null);
$urlselect->set_label(get_string('viewing'), ['class' => 'sr-only']);
return $urlselect->export_for_template($output);
}
return null;
}
/**
* Gets the url_select to be displayed in the participants page if available.
*
* @param \renderer_base $output
* @return object|null The content required to render the url_select
*/
protected function get_category_select(\renderer_base $output): ?object {
if (!$this->searchvalue && $this->viewmode === 'courses') {
$categories = \core_course_category::make_categories_list(array('moodle/category:manage', 'moodle/course:create'));
if (!$categories) {
return null;
}
$currentcat = $this->page->url->param('categoryid');
foreach ($categories as $id => $cat) {
$url = new moodle_url($this->page->url, ['categoryid' => $id]);
if ($id == $currentcat) {
$currenturl = $url->out();
}
$options[$url->out()] = $cat;
}
$select = new \url_select($options, $currenturl);
$select->set_label(get_string('category'), ['class' => 'sr-only']);
$select->class .= ' text-truncate w-100';
return $select->export_for_template($output);
}
return null;
}
/**
* Get the search box
*
* @return array
*/
protected function get_search_form(): array {
$searchform = [
'btnclass' => 'btn-primary',
'inputname' => 'search',
'searchstring' => get_string('searchcourses'),
'query' => $this->searchvalue
];
if (\core_course_category::has_capability_on_any(['moodle/category:manage', 'moodle/course:create'])) {
$searchform['action'] = new moodle_url('/course/management.php');
} else {
$searchform['action'] = new moodle_url('/course/search.php');
}
return $searchform;
}
/**
* Export the content to be displayed on the participants page.
*
* @param \renderer_base $output
* @return array Consists of the following:
* - urlselect A stdclass representing the standard navigation options to be fed into a urlselect
* - renderedcontent Rendered content to be displayed in line with the tertiary nav
*/
public function export_for_template(\renderer_base $output): array {
return [
'urlselect' => $this->get_dropdown($output),
'categoryselect' => $this->get_category_select($output),
'search' => $this->get_search_form(),
'heading' => $this->heading,
];
}
}
@@ -0,0 +1,85 @@
<?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 activity_list renderable used for the recommended activities page.
*
* @package core_course
* @copyright 2020 Adrian Greeve
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_course\output\recommendations;
/**
* Activity list renderable.
*
* @package core_course
* @copyright 2020 Adrian Greeve
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class activity_list implements \renderable, \templatable {
/** @var array $modules activities to display in the recommendations page. */
protected $modules;
/** @var string $searchquery The search query. */
protected $searchquery;
/**
* Constructor method.
*
* @param array $modules Activities to display
* @param string $searchquery The search query if present
*/
public function __construct(array $modules, string $searchquery) {
$this->modules = $modules;
$this->searchquery = $searchquery;
}
/**
* Export method to configure information into something the template can use.
*
* @param \renderer_base $output Not actually used.
* @return array Template context information.
*/
public function export_for_template(\renderer_base $output): array {
$info = array_map(function($module) {
return [
'id' => $module->id ?? '',
'name' => $module->title,
'componentname' => $module->componentname,
'icon' => $module->icon,
'recommended' => $module->recommended ?? ''
];
}, $this->modules);
return [
'categories' => [
[
'categoryname' => get_string('activities'),
'hascategorydata' => !empty($info),
'categorydata' => $info
]
],
'search' => [
'query' => $this->searchquery,
'searchresultsnumber' => count($this->modules)
]
];
}
}
@@ -0,0 +1,46 @@
<?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 renderers for the recommendations page.
*
* @package core_course
* @copyright 2020 Adrian Greeve
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_course\output\recommendations;
/**
* Main renderer for the recommendations page.
*
* @package core_course
* @copyright 2020 Adrian Greeve
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class renderer extends \plugin_renderer_base {
/**
* Render a list of activities to recommend.
*
* @param \core_course\output\recommendations\activity_list $page activity list renderable
* @return string html for displaying.
*/
public function render_activity_list(\core_course\output\recommendations\activity_list $page): string {
$data = $page->export_for_template($this);
return parent::render_from_template('core_course/activity_list', $data);
}
}
+285
View File
@@ -0,0 +1,285 @@
<?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_course
* @copyright 2018 Adrian Greeve <adrian@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_course\privacy;
defined('MOODLE_INTERNAL') || die();
use \core_privacy\local\metadata\collection;
use \core_privacy\local\request\contextlist;
use \core_privacy\local\request\approved_contextlist;
use \core_privacy\local\request\approved_userlist;
use \core_privacy\local\request\transform;
use \core_privacy\local\request\userlist;
use \core_privacy\local\request\writer;
/**
* Privacy class for requesting user data.
*
* @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\context_aware_provider,
\core_privacy\local\request\core_userlist_provider,
\core_privacy\local\request\plugin\provider,
\core_privacy\local\request\user_preference_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_subsystem_link('core_completion', [], 'privacy:metadata:completionsummary');
$collection->add_subsystem_link('core_favourites', [], 'privacy:metadata:favouritessummary');
$collection->add_subsystem_link('core_favourites', [], 'privacy:metadata:activityfavouritessummary');
$collection->add_user_preference('coursecat_management_perpage', 'privacy:perpage');
return $collection;
}
/**
* Get the list of contexts that contain user information for the specified user.
*
* @param int $userid The user to search.
* @return contextlist $contextlist The contextlist containing the list of contexts used in this plugin.
*/
public static function get_contexts_for_userid(int $userid): contextlist {
list($join, $where, $params) = \core_completion\privacy\provider::get_course_completion_join_sql($userid, 'cc', 'c.id');
$sql = "SELECT ctx.id
FROM {context} ctx
JOIN {course} c ON ctx.instanceid = c.id AND ctx.contextlevel = :contextcourse
{$join}
WHERE {$where}";
$params['contextcourse'] = CONTEXT_COURSE;
$contextlist = new contextlist();
$contextlist->add_from_sql($sql, $params);
\core_favourites\privacy\provider::add_contexts_for_userid($contextlist, $userid, 'core_course', 'courses');
return $contextlist;
}
/**
* Get the list of users who have data within a context.
*
* @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination.
*/
public static function get_users_in_context(userlist $userlist) {
$context = $userlist->get_context();
if (!$context instanceof \context_course) {
return;
}
\core_completion\privacy\provider::add_course_completion_users_to_userlist($userlist);
\core_favourites\privacy\provider::add_userids_for_context($userlist, 'courses');
}
/**
* Export all user data for the specified user, in the specified contexts.
*
* @param approved_contextlist $contextlist The approved contexts to export information for.
*/
public static function export_user_data(approved_contextlist $contextlist) {
global $DB;
// Get the course.
list($select, $params) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED);
$params['contextcourse'] = CONTEXT_COURSE;
$sql = "SELECT c.*
FROM {course} c
JOIN {context} ctx ON c.id = ctx.instanceid AND ctx.contextlevel = :contextcourse
WHERE ctx.id $select";
$courses = $DB->get_recordset_sql($sql, $params);
foreach ($courses as $course) {
$coursecompletion = \core_completion\privacy\provider::get_course_completion_info($contextlist->get_user(), $course);
writer::with_context(\context_course::instance($course->id))->export_data(
[get_string('privacy:completionpath', 'course')], (object) $coursecompletion);
// Get user's favourites information for the particular course.
$coursefavourite = \core_favourites\privacy\provider::get_favourites_info_for_user($contextlist->get_user()->id,
\context_course::instance($course->id), 'core_course', 'courses', $course->id);
if ($coursefavourite) { // If the course has been favourited by the user, include it in the export.
writer::with_context(\context_course::instance($course->id))->export_data(
[get_string('privacy:favouritespath', 'course')], (object) $coursefavourite);
}
}
$courses->close();
}
/**
* Give the component a chance to include any contextual information deemed relevant to any child contexts which are
* exporting personal data.
*
* By giving the component access to the full list of contexts being exported across all components, it can determine whether a
* descendant context is being exported, and decide whether to add relevant contextual information about itself. Having access
* to the full list of contexts being exported is what makes this component a context aware provider.
*
* E.g.
* If, during the core export process, a course module is included in the contextlist_collection but the course containing the
* module is not (perhaps there's no longer a user enrolment), then the course should include general contextual information in
* the export so we know basic details about which course the module belongs to. This method allows the course to make that
* decision, based on the existence of any decendant module contexts in the collection.
*
* @param \core_privacy\local\request\contextlist_collection $contextlistcollection
*/
public static function export_context_data(\core_privacy\local\request\contextlist_collection $contextlistcollection) {
global $DB;
$coursecontextids = $DB->get_records_menu('context', ['contextlevel' => CONTEXT_COURSE], '', 'id, instanceid');
$courseids = [];
foreach ($contextlistcollection as $component) {
foreach ($component->get_contexts() as $context) {
// All course contexts have been accounted for, so skip all checks.
if (empty($coursecontextids)) {
break;
}
// Only course, module, and block contexts are checked.
if (in_array($context->contextlevel, [CONTEXT_USER, CONTEXT_SYSTEM, CONTEXT_COURSECAT])) {
continue;
}
// If the context is a course, then we just add it without the need to check context path.
if ($context->contextlevel == CONTEXT_COURSE) {
$courseids[$context->id] = $context->instanceid;
unset($coursecontextids[$context->id]);
continue;
}
// Otherwise, we need to check all the course context paths, to see if this context is a descendant.
foreach ($coursecontextids as $contextid => $instanceid) {
if (stripos($context->path, '/' . $contextid . '/') !== false) {
$courseids[$contextid] = $instanceid;
unset($coursecontextids[$contextid]);
}
}
}
}
if (empty($courseids)) {
return;
}
// Export general data for these contexts.
list($sql, $params) = $DB->get_in_or_equal($courseids);
$sql = 'id ' . $sql;
$coursedata = $DB->get_records_select('course', $sql, $params);
foreach ($coursedata as $course) {
$context = \context_course::instance($course->id);
$courseformat = $course->format !== 'site' ? get_string('pluginname', 'format_' . $course->format) : get_string('site');
$data = (object) [
'fullname' => format_string($course->fullname, true, ['context' => $context]),
'shortname' => $course->shortname,
'idnumber' => $course->idnumber,
'summary' => format_text(
writer::with_context($context)->rewrite_pluginfile_urls(
[],
'course',
'summary',
0,
$course->summary
), $course->summaryformat, ['context' => $context]),
'format' => $courseformat,
'startdate' => transform::datetime($course->startdate),
'enddate' => transform::datetime($course->enddate)
];
writer::with_context($context)
->export_area_files([], 'course', 'summary', 0)
->export_area_files([], 'course', 'overviewfiles', 0)
->export_data([], $data);
}
}
/**
* Export all user preferences for the plugin.
*
* @param int $userid The userid of the user whose data is to be exported.
*/
public static function export_user_preferences(int $userid) {
$perpage = get_user_preferences('coursecat_management_perpage', null, $userid);
if (isset($perpage)) {
writer::export_user_preference('core_course',
'coursecat_management_perpage',
$perpage,
get_string('privacy:perpage', 'course')
);
}
}
/**
* Delete all data for all users in the specified context.
*
* @param \context $context The specific context to delete data for.
*/
public static function delete_data_for_all_users_in_context(\context $context) {
// Check what context we've been delivered.
if (!$context instanceof \context_course) {
return;
}
// Delete course completion data.
\core_completion\privacy\provider::delete_completion(null, $context->instanceid);
// Delete course favourite data.
\core_favourites\privacy\provider::delete_favourites_for_all_users($context, 'core_course',
'courses');
}
/**
* Delete all user data for the specified user, in the specified contexts.
*
* @param approved_contextlist $contextlist The approved contexts and user information to delete information for.
*/
public static function delete_data_for_user(approved_contextlist $contextlist) {
foreach ($contextlist as $context) {
// Check what context we've been delivered.
if ($context instanceof \context_course) {
// Delete course completion data.
\core_completion\privacy\provider::delete_completion($contextlist->get_user(), $context->instanceid);
// Delete course favourite data.
\core_favourites\privacy\provider::delete_favourites_for_user($contextlist, 'core_course',
'courses');
}
}
}
/**
* Delete multiple users within a single context.
*
* @param approved_userlist $userlist The approved context and user information to delete information for.
*/
public static function delete_data_for_users(approved_userlist $userlist) {
$context = $userlist->get_context();
// Check what context we've been delivered.
if (!$context instanceof \context_course) {
return;
}
// Delete course completion data.
\core_completion\privacy\provider::delete_completion_by_approved_userlist($userlist, $context->instanceid);
// Delete course favourite data.
\core_favourites\privacy\provider::delete_favourites_for_userlist($userlist, 'courses');
}
}
@@ -0,0 +1,134 @@
<?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_course\reportbuilder\datasource;
use core_cohort\reportbuilder\local\entities\cohort;
use core_course\reportbuilder\local\entities\course_category;
use core_reportbuilder\datasource;
use core_reportbuilder\local\entities\{course, user};
use core_role\reportbuilder\local\entities\role;
/**
* Course categories datasource
*
* @package core_course
* @copyright 2023 Paul Holden <paulh@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class categories extends datasource {
/**
* Return user friendly name of the datasource
*
* @return string
*/
public static function get_name(): string {
return get_string('coursecategories');
}
/**
* Initialise report
*/
protected function initialise(): void {
$categoryentity = new course_category();
$categoryalias = $categoryentity->get_table_alias('course_categories');
$contextalias = $categoryentity->get_table_alias('context');
$this->set_main_table('course_categories', $categoryalias);
$this->add_entity($categoryentity);
// Join course entity.
$courseentity = new course();
$coursealias = $courseentity->get_table_alias('course');
$this->add_entity($courseentity
->add_join("LEFT JOIN {course} {$coursealias} ON {$coursealias}.category = {$categoryalias}.id"));
// Join cohort entity (indicate context table join alias).
$cohortentity = (new cohort())
->set_table_join_alias('context', $contextalias);
$cohort = $cohortentity->get_table_alias('cohort');
$this->add_entity($cohortentity
->add_join($categoryentity->get_context_join())
->add_join("LEFT JOIN {cohort} {$cohort} ON {$cohort}.contextid = {$contextalias}.id"));
// Join role entity.
$roleentity = (new role())
->set_table_alias('context', $contextalias);
$role = $roleentity->get_table_alias('role');
$this->add_entity($roleentity
->add_join($categoryentity->get_context_join())
->add_join("LEFT JOIN {role_assignments} ras ON ras.contextid = {$contextalias}.id")
->add_join("LEFT JOIN {role} {$role} ON {$role}.id = ras.roleid"));
// Join user entity.
$userentity = new user();
$user = $userentity->get_table_alias('user');
$this->add_entity($userentity
->add_joins($roleentity->get_joins())
->add_join("LEFT JOIN {user} {$user} ON {$user}.id = ras.userid"));
// Add all elements from entities to be available in custom reports.
$this->add_all_from_entities();
}
/**
* Return the columns that will be added to the report as part of default setup
*
* @return string[]
*/
public function get_default_columns(): array {
return [
'course_category:name',
'course_category:idnumber',
'course_category:coursecount',
];
}
/**
* Return the default sorting that will be added to the report as part of default setup
*
* @return int[]
*/
public function get_default_column_sorting(): array {
return [
'course_category:name' => SORT_ASC,
];
}
/**
* Return the filters that will be added to the report as part of default setup
*
* @return string[]
*/
public function get_default_filters(): array {
return [
'course_category:name',
];
}
/**
* Return the conditions that will be added to the report as part of default setup
*
* @return string[]
*/
public function get_default_conditions(): array {
return [];
}
}
@@ -0,0 +1,149 @@
<?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_course\reportbuilder\datasource;
use core_course\reportbuilder\local\entities\course_category;
use core_files\reportbuilder\local\entities\file;
use core_reportbuilder\datasource;
use core_reportbuilder\local\entities\course;
use core_reportbuilder\local\helpers\database;
use core_tag\reportbuilder\local\entities\tag;
use lang_string;
/**
* Courses datasource
*
* @package core_course
* @copyright 2021 Paul Holden <paulh@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class courses extends datasource {
/**
* Return user friendly name of the datasource
*
* @return string
*/
public static function get_name(): string {
return get_string('courses');
}
/**
* Initialise report
*/
protected function initialise(): void {
$courseentity = new course();
$coursetablealias = $courseentity->get_table_alias('course');
// Exclude site course.
$paramsiteid = database::generate_param_name();
$this->set_main_table('course', $coursetablealias);
$this->add_base_condition_sql("{$coursetablealias}.id != :{$paramsiteid}", [$paramsiteid => SITEID]);
$this->add_entity($courseentity);
// Join the course category entity.
$coursecatentity = new course_category();
$coursecattablealias = $coursecatentity->get_table_alias('course_categories');
$this->add_entity($coursecatentity
->add_join("JOIN {course_categories} {$coursecattablealias}
ON {$coursecattablealias}.id = {$coursetablealias}.category"));
// Join the tag entity.
$tagentity = (new tag())
->set_table_alias('tag', $courseentity->get_table_alias('tag'));
$this->add_entity($tagentity
->add_joins($courseentity->get_tag_joins()));
// Join the files entity.
$contextalias = $courseentity->get_table_alias('context');
$fileentity = (new file())
->set_entity_title(new lang_string('courseoverviewfiles'));
$filesalias = $fileentity->get_table_alias('files');
$this->add_entity($fileentity
->add_join($courseentity->get_context_join())
->add_join("LEFT JOIN {files} {$filesalias}
ON {$filesalias}.contextid = {$contextalias}.id
AND {$filesalias}.component = 'course'
AND {$filesalias}.filearea = 'overviewfiles'
AND {$filesalias}.itemid = 0
AND {$filesalias}.filename != '.'"));
// Add all columns/filters/conditions from entities to be available in custom reports.
$this->add_all_from_entity($coursecatentity->get_entity_name());
$this->add_all_from_entity($courseentity->get_entity_name());
// Add specific tag entity elements.
$this->add_columns_from_entity($tagentity->get_entity_name(), ['name', 'namewithlink']);
$this->add_filter($tagentity->get_filter('name'));
$this->add_condition($tagentity->get_condition('name'));
// Add specific file entity elements.
$this->add_columns_from_entity($fileentity->get_entity_name(), ['name', 'size', 'type', 'timecreated']);
$this->add_filters_from_entity($fileentity->get_entity_name(), ['name', 'size', 'timecreated']);
$this->add_conditions_from_entity($fileentity->get_entity_name(), ['name', 'size', 'timecreated']);
}
/**
* Return the columns that will be added to the report as part of default setup
*
* @return string[]
*/
public function get_default_columns(): array {
return [
'course_category:name',
'course:shortname',
'course:fullname',
'course:idnumber',
];
}
/**
* Return the filters that will be added to the report once is created
*
* @return string[]
*/
public function get_default_filters(): array {
return ['course_category:name', 'course:fullname', 'course:idnumber'];
}
/**
* Return the conditions that will be added to the report once is created
*
* @return string[]
*/
public function get_default_conditions(): array {
return ['course_category:name'];
}
/**
* Return the default sorting that will be added to the report once it is created
*
* @return array|int[]
*/
public function get_default_column_sorting(): array {
return [
'course_category:name' => SORT_ASC,
'course:shortname' => SORT_ASC,
'course:fullname' => SORT_ASC,
];
}
}
@@ -0,0 +1,215 @@
<?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_course\reportbuilder\datasource;
use core_course\reportbuilder\local\entities\course_category;
use core_course\reportbuilder\local\entities\access;
use core_course\reportbuilder\local\entities\completion;
use core_course\reportbuilder\local\entities\enrolment;
use core_enrol\reportbuilder\local\entities\enrol;
use core_group\reportbuilder\local\entities\group;
use core_reportbuilder\datasource;
use core_reportbuilder\local\entities\course;
use core_reportbuilder\local\entities\user;
use core_reportbuilder\local\filters\select;
use core_reportbuilder\local\helpers\database;
use core_role\reportbuilder\local\entities\role;
use core_user\output\status_field;
/**
* Course participants datasource
*
* @package core_course
* @copyright 2022 David Matamoros <davidmc@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class participants extends datasource {
/**
* Initialise report
*/
protected function initialise(): void {
$courseentity = new course();
$this->add_entity($courseentity);
$context = $courseentity->get_table_alias('context');
$course = $courseentity->get_table_alias('course');
$this->set_main_table('course', $course);
// Exclude site course.
$paramsiteid = database::generate_param_name();
$this->add_base_condition_sql("{$course}.id != :{$paramsiteid}", [$paramsiteid => SITEID]);
// Join the course category entity.
$coursecatentity = new course_category();
$categories = $coursecatentity->get_table_alias('course_categories');
$this->add_entity($coursecatentity
->add_join("JOIN {course_categories} {$categories} ON {$categories}.id = {$course}.category"));
// Join the enrolment method entity.
$enrolentity = new enrol();
$enrol = $enrolentity->get_table_alias('enrol');
$this->add_entity($enrolentity
->add_join("LEFT JOIN {enrol} {$enrol} ON {$enrol}.courseid = {$course}.id"));
// Join the enrolments entity.
$enrolmententity = (new enrolment())
->set_table_alias('enrol', $enrol);
$userenrolment = $enrolmententity->get_table_alias('user_enrolments');
$this->add_entity($enrolmententity
->add_joins($enrolentity->get_joins())
->add_join("LEFT JOIN {user_enrolments} {$userenrolment} ON {$userenrolment}.enrolid = {$enrol}.id"));
// Join user entity.
$userentity = new user();
$user = $userentity->get_table_alias('user');
$this->add_entity($userentity
->add_joins($enrolmententity->get_joins())
->add_join("LEFT JOIN {user} {$user} ON {$user}.id = {$userenrolment}.userid AND {$user}.deleted = 0"));
// Join the role entity.
$roleentity = (new role())
->set_table_alias('context', $context);
$role = $roleentity->get_table_alias('role');
$this->add_entity($roleentity
->add_joins($userentity->get_joins())
->add_join($courseentity->get_context_join())
->add_join("LEFT JOIN {role_assignments} ras ON ras.contextid = {$context}.id AND ras.userid = {$user}.id")
->add_join("LEFT JOIN {role} {$role} ON {$role}.id = ras.roleid")
);
// Join group entity.
$groupentity = (new group())
->set_table_alias('context', $context);
$groups = $groupentity->get_table_alias('groups');
// Sub-select for all course group members.
$groupsinnerselect = "
SELECT grs.*, grms.userid
FROM {groups} grs
JOIN {groups_members} grms ON grms.groupid = grs.id";
$this->add_entity($groupentity
->add_join($courseentity->get_context_join())
->add_joins($userentity->get_joins())
->add_join("
LEFT JOIN ({$groupsinnerselect}) {$groups}
ON {$groups}.courseid = {$course}.id AND {$groups}.userid = {$user}.id")
);
// Join completion entity.
$completionentity = (new completion())
->set_table_aliases([
'course' => $course,
'user' => $user,
]);
$completion = $completionentity->get_table_alias('course_completion');
$this->add_entity($completionentity
->add_joins($userentity->get_joins())
->add_join("
LEFT JOIN {course_completions} {$completion}
ON {$completion}.course = {$course}.id AND {$completion}.userid = {$user}.id")
);
// Join course access entity.
$accessentity = (new access())
->set_table_alias('user', $user);
$lastaccess = $accessentity->get_table_alias('user_lastaccess');
$this->add_entity($accessentity
->add_joins($userentity->get_joins())
->add_join("
LEFT JOIN {user_lastaccess} {$lastaccess}
ON {$lastaccess}.userid = {$user}.id AND {$lastaccess}.courseid = {$course}.id"));
// Add all entities columns/filters/conditions.
$this->add_all_from_entities();
}
/**
* Return user friendly name of the datasource
*
* @return string
*/
public static function get_name(): string {
return get_string('courseparticipants', 'course');
}
/**
* Return the columns that will be added to the report as part of default setup
*
* @return string[]
*/
public function get_default_columns(): array {
return [
'course:coursefullnamewithlink',
'user:fullnamewithlink',
'enrol:name',
];
}
/**
* Return the column sorting that will be added to the report upon creation
*
* @return int[]
*/
public function get_default_column_sorting(): array {
return [
'course:coursefullnamewithlink' => SORT_ASC,
'user:fullnamewithlink' => SORT_ASC,
'enrol:name' => SORT_ASC,
];
}
/**
* Return the filters that will be added to the report once is created
*
* @return string[]
*/
public function get_default_filters(): array {
return [
'user:suspended',
'user:confirmed',
];
}
/**
* Return the conditions that will be added to the report once is created
*
* @return string[]
*/
public function get_default_conditions(): array {
return [
'enrolment:status',
'user:suspended',
'user:confirmed',
];
}
/**
* Return the condition values that will be set for the report upon creation
*
* @return array
*/
public function get_default_condition_values(): array {
return [
'enrolment:status_operator' => select::EQUAL_TO,
'enrolment:status_value' => status_field::STATUS_ACTIVE,
];
}
}
@@ -0,0 +1,139 @@
<?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_course\reportbuilder\local\entities;
use core_reportbuilder\local\entities\base;
use core_reportbuilder\local\filters\date;
use core_reportbuilder\local\helpers\format;
use core_reportbuilder\local\report\column;
use core_reportbuilder\local\report\filter;
use lang_string;
use stdClass;
/**
* Course access entity implementation
*
* @package core_course
* @copyright 2022 David Matamoros <davidmc@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class access extends base {
/**
* Database tables that this entity uses
*
* @return string[]
*/
protected function get_default_tables(): array {
return [
'user_lastaccess',
'user',
];
}
/**
* The default title for this entity in the list of columns/conditions/filters in the report builder
*
* @return lang_string
*/
protected function get_default_entity_title(): lang_string {
return new lang_string('courseaccess', 'course');
}
/**
* Initialise the entity
*
* @return base
*/
public function initialise(): base {
foreach ($this->get_all_columns() as $column) {
$this->add_column($column);
}
// All the filters defined by the entity can also be used as conditions.
foreach ($this->get_all_filters() as $filter) {
$this
->add_filter($filter)
->add_condition($filter);
}
return $this;
}
/**
* Returns list of all available columns
*
* @return column[]
*/
protected function get_all_columns(): array {
$tablealias = $this->get_table_alias('user_lastaccess');
$user = $this->get_table_alias('user');
// Last course access column.
$columns[] = (new column(
'timeaccess',
new lang_string('lastcourseaccess', 'moodle'),
$this->get_entity_name()
))
->add_joins($this->get_joins())
->set_type(column::TYPE_TIMESTAMP)
->add_field("{$tablealias}.timeaccess")
->add_field("{$user}.id", 'userid')
->set_is_sortable(true)
->add_callback(static function(?int $value, stdClass $row, $arguments, ?string $aggregation): string {
if ($row->userid === null && $aggregation === null) {
return '';
} else if ($value === null) {
return get_string('never');
}
return format::userdate($value, $row);
});
return $columns;
}
/**
* Return list of all available filters
*
* @return filter[]
*/
protected function get_all_filters(): array {
$tablealias = $this->get_table_alias('user_lastaccess');
// Last course access filter.
$filters[] = (new filter(
date::class,
'timeaccess',
new lang_string('lastcourseaccess', 'moodle'),
$this->get_entity_name(),
"{$tablealias}.timeaccess"
))
->add_joins($this->get_joins())
->set_limited_operators([
date::DATE_ANY,
date::DATE_NOT_EMPTY,
date::DATE_EMPTY,
date::DATE_RANGE,
date::DATE_LAST,
date::DATE_CURRENT,
]);
return $filters;
}
}
@@ -0,0 +1,367 @@
<?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_course\reportbuilder\local\entities;
use core_reportbuilder\local\entities\base;
use core_course\reportbuilder\local\formatters\completion as completion_formatter;
use core_reportbuilder\local\filters\boolean_select;
use core_reportbuilder\local\filters\date;
use core_reportbuilder\local\helpers\database;
use core_reportbuilder\local\helpers\format;
use core_reportbuilder\local\report\column;
use core_reportbuilder\local\report\filter;
use completion_criteria_completion;
use completion_info;
use html_writer;
use lang_string;
use stdClass;
/**
* Course completion entity implementation
*
* @package core_course
* @copyright 2022 David Matamoros <davidmc@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class completion extends base {
/**
* Database tables that this entity uses
*
* @return string[]
*/
protected function get_default_tables(): array {
return [
'course_completion',
'course',
'grade_grades' ,
'grade_items',
'user',
];
}
/**
* The default title for this entity in the list of columns/conditions/filters in the report builder
*
* @return lang_string
*/
protected function get_default_entity_title(): lang_string {
return new lang_string('coursecompletion', 'completion');
}
/**
* Initialise the entity
*
* @return base
*/
public function initialise(): base {
foreach ($this->get_all_columns() as $column) {
$this->add_column($column);
}
// All the filters defined by the entity can also be used as conditions.
foreach ($this->get_all_filters() as $filter) {
$this
->add_filter($filter)
->add_condition($filter);
}
return $this;
}
/**
* Returns list of all available columns
*
* @return column[]
*/
protected function get_all_columns(): array {
[
'course_completion' => $coursecompletion,
'course' => $course,
'grade_grades' => $grade,
'grade_items' => $gradeitem,
'user' => $user,
] = $this->get_table_aliases();
// Completed column.
$columns[] = (new column(
'completed',
new lang_string('completed', 'completion'),
$this->get_entity_name()
))
->add_joins($this->get_joins())
->set_type(column::TYPE_BOOLEAN)
->add_field("
CASE
WHEN {$coursecompletion}.id IS NULL THEN NULL
WHEN {$coursecompletion}.timecompleted > 0 THEN 1
ELSE 0
END", 'completed')
->set_is_sortable(true)
->add_callback([format::class, 'boolean_as_text']);
// Completion criteria column.
$criterias = database::generate_alias();
$columns[] = (new column(
'criteria',
new lang_string('criteria', 'core_completion'),
$this->get_entity_name()
))
->add_joins($this->get_joins())
// Determine whether any criteria exist for the course. We also group per course, rather than report each separately.
->add_join("LEFT JOIN (
SELECT DISTINCT course FROM {course_completion_criteria}
) {$criterias} ON {$criterias}.course = {$course}.id")
->set_type(column::TYPE_TEXT)
// Select enough fields to determine user criteria for the course.
->add_field("{$criterias}.course", 'courseid')
->add_field("{$course}.enablecompletion")
->add_field("{$user}.id", 'userid')
->set_disabled_aggregation_all()
->add_callback(static function($id, stdClass $record): string {
if (!$record->courseid) {
return '';
}
$info = new completion_info((object) ['id' => $record->courseid, 'enablecompletion' => $record->enablecompletion]);
if ($info->get_aggregation_method() == COMPLETION_AGGREGATION_ALL) {
$title = get_string('criteriarequiredall', 'core_completion');
} else {
$title = get_string('criteriarequiredany', 'core_completion');
}
// Map all completion data to their criteria summaries.
$items = array_map(static function(completion_criteria_completion $completion): string {
$criteria = $completion->get_criteria();
return get_string('criteriasummary', 'core_completion', [
'type' => $criteria->get_details($completion)['type'],
'summary' => $criteria->get_title_detailed(),
]);
}, $info->get_completions((int) $record->userid));
return $title . html_writer::alist($items);
});
// Progress percentage column.
$columns[] = (new column(
'progresspercent',
new lang_string('progress', 'completion'),
$this->get_entity_name()
))
->add_joins($this->get_joins())
->set_type(column::TYPE_TEXT)
->add_field("{$course}.id", 'courseid')
->add_field("{$user}.id", 'userid')
->set_is_sortable(false)
->add_callback([completion_formatter::class, 'completion_progress']);
// Time enrolled.
$columns[] = (new column(
'timeenrolled',
new lang_string('timeenrolled', 'enrol'),
$this->get_entity_name()
))
->add_joins($this->get_joins())
->set_type(column::TYPE_TIMESTAMP)
->add_field("{$coursecompletion}.timeenrolled")
->set_is_sortable(true)
->add_callback([format::class, 'userdate']);
// Time started.
$columns[] = (new column(
'timestarted',
new lang_string('timestarted', 'enrol'),
$this->get_entity_name()
))
->add_joins($this->get_joins())
->set_type(column::TYPE_TIMESTAMP)
->add_field("{$coursecompletion}.timestarted")
->set_is_sortable(true)
->add_callback([format::class, 'userdate']);
// Time completed.
$columns[] = (new column(
'timecompleted',
new lang_string('timecompleted', 'completion'),
$this->get_entity_name()
))
->add_joins($this->get_joins())
->set_type(column::TYPE_TIMESTAMP)
->add_field("{$coursecompletion}.timecompleted")
->set_is_sortable(true)
->add_callback([format::class, 'userdate']);
// Time reaggregated.
$columns[] = (new column(
'reaggregate',
new lang_string('timereaggregated', 'enrol'),
$this->get_entity_name()
))
->add_joins($this->get_joins())
->set_type(column::TYPE_TIMESTAMP)
->add_field("{$coursecompletion}.reaggregate")
->set_is_sortable(true)
->add_callback([format::class, 'userdate']);
// Days taking course (days since course start date until completion or until current date if not completed).
$currenttime = time();
$columns[] = (new column(
'dayscourse',
new lang_string('daystakingcourse', 'course'),
$this->get_entity_name()
))
->add_joins($this->get_joins())
->set_type(column::TYPE_INTEGER)
->add_field("(
CASE
WHEN {$coursecompletion}.id IS NULL THEN NULL
ELSE (CASE WHEN {$coursecompletion}.timecompleted > 0 THEN
{$coursecompletion}.timecompleted
ELSE
{$currenttime}
END - {$course}.startdate) / " . DAYSECS . "
END)", 'dayscourse')
->set_is_sortable(true);
// Days since last completion (days since last enrolment date until completion or until current date if not completed).
$columns[] = (new column(
'daysuntilcompletion',
new lang_string('daysuntilcompletion', 'completion'),
$this->get_entity_name()
))
->add_joins($this->get_joins())
->set_type(column::TYPE_INTEGER)
->add_field("(
CASE
WHEN {$coursecompletion}.id IS NULL THEN NULL
ELSE (CASE WHEN {$coursecompletion}.timecompleted > 0 THEN
{$coursecompletion}.timecompleted
ELSE
{$currenttime}
END - {$coursecompletion}.timeenrolled) / " . DAYSECS . "
END)", 'daysuntilcompletion')
->set_is_sortable(true);
// Student course grade.
$columns[] = (new column(
'grade',
new lang_string('gradenoun'),
$this->get_entity_name()
))
->add_joins($this->get_joins())
->add_join("
LEFT JOIN {grade_items} {$gradeitem}
ON ({$gradeitem}.itemtype = 'course' AND {$course}.id = {$gradeitem}.courseid)
")
->add_join("
LEFT JOIN {grade_grades} {$grade}
ON ({$user}.id = {$grade}.userid AND {$gradeitem}.id = {$grade}.itemid)
")
->set_type(column::TYPE_FLOAT)
->add_fields("{$grade}.finalgrade")
->set_is_sortable(true)
->add_callback(function(?float $value): string {
if ($value === null) {
return '';
}
return format_float($value, 2);
});
return $columns;
}
/**
* Return list of all available filters
*
* @return filter[]
*/
protected function get_all_filters(): array {
$coursecompletion = $this->get_table_alias('course_completion');
// Completed status filter.
$filters[] = (new filter(
boolean_select::class,
'completed',
new lang_string('completed', 'completion'),
$this->get_entity_name(),
"CASE WHEN {$coursecompletion}.timecompleted > 0 THEN 1 ELSE 0 END"
))
->add_joins($this->get_joins());
// Time completed filter.
$filters[] = (new filter(
date::class,
'timecompleted',
new lang_string('timecompleted', 'completion'),
$this->get_entity_name(),
"{$coursecompletion}.timecompleted"
))
->add_joins($this->get_joins())
->set_limited_operators([
date::DATE_ANY,
date::DATE_NOT_EMPTY,
date::DATE_EMPTY,
date::DATE_RANGE,
date::DATE_LAST,
date::DATE_CURRENT,
]);
// Time enrolled/started filter and condition.
$fields = ['timeenrolled', 'timestarted'];
foreach ($fields as $field) {
$filters[] = (new filter(
date::class,
$field,
new lang_string($field, 'enrol'),
$this->get_entity_name(),
"{$coursecompletion}.{$field}"
))
->add_joins($this->get_joins())
->set_limited_operators([
date::DATE_ANY,
date::DATE_NOT_EMPTY,
date::DATE_EMPTY,
date::DATE_RANGE,
date::DATE_LAST,
date::DATE_CURRENT,
]);
}
// Time reaggregated filter and condition.
$filters[] = (new filter(
date::class,
'reaggregate',
new lang_string('timereaggregated', 'enrol'),
$this->get_entity_name(),
"{$coursecompletion}.reaggregate"
))
->add_joins($this->get_joins())
->set_limited_operators([
date::DATE_ANY,
date::DATE_NOT_EMPTY,
date::DATE_EMPTY,
date::DATE_RANGE,
date::DATE_LAST,
date::DATE_CURRENT,
]);
return $filters;
}
}
@@ -0,0 +1,306 @@
<?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_course\reportbuilder\local\entities;
use context_coursecat;
use context_helper;
use html_writer;
use lang_string;
use moodle_url;
use stdClass;
use theme_config;
use core_course_category;
use core_reportbuilder\local\entities\base;
use core_reportbuilder\local\filters\{category, select, text};
use core_reportbuilder\local\report\column;
use core_reportbuilder\local\report\filter;
/**
* Course category entity
*
* @package core_course
* @copyright 2021 Paul Holden <paulh@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class course_category extends base {
/**
* Database tables that this entity uses
*
* @return string[]
*/
protected function get_default_tables(): array {
return [
'context',
'course_categories',
];
}
/**
* The default title for this entity
*
* @return lang_string
*/
protected function get_default_entity_title(): lang_string {
return new lang_string('coursecategory');
}
/**
* Initialise the entity
*
* @return base
*/
public function initialise(): base {
$columns = $this->get_all_columns();
foreach ($columns as $column) {
$this->add_column($column);
}
// All the filters defined by the entity can also be used as conditions.
$filters = $this->get_all_filters();
foreach ($filters as $filter) {
$this
->add_filter($filter)
->add_condition($filter);
}
return $this;
}
/**
* Returns list of all available columns
*
* @return column[]
*/
protected function get_all_columns(): array {
global $DB;
$tablealias = $this->get_table_alias('course_categories');
$tablealiascontext = $this->get_table_alias('context');
// Name column.
$columns[] = (new column(
'name',
new lang_string('categoryname'),
$this->get_entity_name()
))
->add_joins($this->get_joins())
->add_join($this->get_context_join())
->set_type(column::TYPE_TEXT)
->add_fields("{$tablealias}.name, {$tablealias}.id")
->add_fields(context_helper::get_preload_record_columns_sql($tablealiascontext))
->add_callback(static function(?string $name, stdClass $category): string {
if (empty($category->id)) {
return '';
}
context_helper::preload_from_record($category);
$context = context_coursecat::instance($category->id);
return format_string($category->name, true, ['context' => $context]);
})
->set_is_sortable(true);
// Category name with link column.
$columns[] = (new column(
'namewithlink',
new lang_string('namewithlink', 'core_course'),
$this->get_entity_name()
))
->add_joins($this->get_joins())
->add_join($this->get_context_join())
->set_type(column::TYPE_TEXT)
->add_fields("{$tablealias}.name, {$tablealias}.id")
->add_fields(context_helper::get_preload_record_columns_sql($tablealiascontext))
->add_callback(static function(?string $name, stdClass $category): string {
if (empty($category->id)) {
return '';
}
context_helper::preload_from_record($category);
$context = context_coursecat::instance($category->id);
$url = new moodle_url('/course/management.php', ['categoryid' => $category->id]);
return html_writer::link($url,
format_string($category->name, true, ['context' => $context]));
})
->set_is_sortable(true);
// Path column.
$columns[] = (new column(
'path',
new lang_string('categorypath'),
$this->get_entity_name()
))
->add_joins($this->get_joins())
->set_type(column::TYPE_TEXT)
->add_fields("{$tablealias}.name, {$tablealias}.id")
->add_callback(static function(?string $name, stdClass $category): string {
return empty($category->id) ? '' :
core_course_category::get($category->id, MUST_EXIST, true)->get_nested_name(false);
})
->set_disabled_aggregation(['groupconcat', 'groupconcatdistinct'])
->set_is_sortable(true);
// ID number column.
$columns[] = (new column(
'idnumber',
new lang_string('idnumbercoursecategory'),
$this->get_entity_name()
))
->add_joins($this->get_joins())
->set_type(column::TYPE_TEXT)
->add_fields("{$tablealias}.idnumber")
->set_is_sortable(true);
// Description column (note we need to join/select from the context table in order to format the column).
$descriptionfieldsql = "{$tablealias}.description";
if ($DB->get_dbfamily() === 'oracle') {
$descriptionfieldsql = $DB->sql_order_by_text($descriptionfieldsql, 1024);
}
$columns[] = (new column(
'description',
new lang_string('description'),
$this->get_entity_name()
))
->add_joins($this->get_joins())
->add_join($this->get_context_join())
->set_type(column::TYPE_LONGTEXT)
->add_field($descriptionfieldsql, 'description')
->add_fields("{$tablealias}.descriptionformat, {$tablealias}.id")
->add_fields(context_helper::get_preload_record_columns_sql($tablealiascontext))
->add_callback(static function(?string $description, stdClass $category): string {
global $CFG;
require_once("{$CFG->libdir}/filelib.php");
if ($description === null) {
return '';
}
context_helper::preload_from_record($category);
$context = context_coursecat::instance($category->id);
$description = file_rewrite_pluginfile_urls($description, 'pluginfile.php', $context->id, 'coursecat',
'description', null);
return format_text($description, $category->descriptionformat, ['context' => $context->id]);
});
// Theme column.
$columns[] = (new column(
'theme',
new lang_string('theme'),
$this->get_entity_name()
))
->add_joins($this->get_joins())
->set_type(column::TYPE_TEXT)
->add_fields("{$tablealias}.theme")
->set_is_sortable(true)
->add_callback(static function (?string $theme): string {
if ((string) $theme === '') {
return '';
}
return get_string('pluginname', "theme_{$theme}");
});
// Course count column.
$columns[] = (new column(
'coursecount',
new lang_string('coursecount', 'core_course'),
$this->get_entity_name()
))
->add_joins($this->get_joins())
->set_type(column::TYPE_INTEGER)
->add_fields("{$tablealias}.coursecount")
->set_is_sortable(true);
return $columns;
}
/**
* Return list of all available filters
*
* @return filter[]
*/
protected function get_all_filters(): array {
$tablealias = $this->get_table_alias('course_categories');
// Select category filter.
$filters[] = (new filter(
category::class,
'name',
new lang_string('categoryselect', 'core_reportbuilder'),
$this->get_entity_name(),
"{$tablealias}.id"
))
->add_joins($this->get_joins())
->set_options([
'requiredcapabilities' => 'moodle/category:viewcourselist',
]);
// Name filter.
$filters[] = (new filter(
text::class,
'text',
new lang_string('categoryname'),
$this->get_entity_name(),
"{$tablealias}.name"
))
->add_joins($this->get_joins());
// ID number filter.
$filters[] = (new filter(
text::class,
'idnumber',
new lang_string('idnumbercoursecategory'),
$this->get_entity_name(),
"{$tablealias}.idnumber"
))
->add_joins($this->get_joins());
// Theme filter.
$filters[] = (new filter(
select::class,
'theme',
new lang_string('theme'),
$this->get_entity_name(),
"{$tablealias}.theme",
))
->set_options_callback(static function(): array {
return array_map(
fn(theme_config $theme) => $theme->get_theme_name(),
get_list_of_themes(),
);
})
->add_joins($this->get_joins());
return $filters;
}
/**
* Return context join used by columns
*
* @return string
*/
public function get_context_join(): string {
$coursecategories = $this->get_table_alias('course_categories');
$context = $this->get_table_alias('context');
return "LEFT JOIN {context} {$context} ON {$context}.instanceid = {$coursecategories}.id
AND {$context}.contextlevel = " . CONTEXT_COURSECAT;
}
}
@@ -0,0 +1,307 @@
<?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_course\reportbuilder\local\entities;
use context_course;
use core_course\reportbuilder\local\formatters\enrolment as enrolment_formatter;
use core_reportbuilder\local\entities\base;
use core_reportbuilder\local\filters\date;
use core_reportbuilder\local\filters\select;
use core_reportbuilder\local\helpers\database;
use core_reportbuilder\local\helpers\format;
use core_reportbuilder\local\report\column;
use core_reportbuilder\local\report\filter;
use core_user\output\status_field;
use enrol_plugin;
use lang_string;
use stdClass;
/**
* Course enrolment entity implementation
*
* @package core_course
* @copyright 2022 David Matamoros <davidmc@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class enrolment extends base {
/**
* Database tables that this entity uses
*
* @return string[]
*/
protected function get_default_tables(): array {
return [
'user_enrolments',
'enrol',
];
}
/**
* The default title for this entity in the list of columns/conditions/filters in the report builder
*
* @return lang_string
*/
protected function get_default_entity_title(): lang_string {
return new lang_string('enrolment', 'enrol');
}
/**
* Initialise the entity
*
* @return base
*/
public function initialise(): base {
foreach ($this->get_all_columns() as $column) {
$this->add_column($column);
}
// All the filters defined by the entity can also be used as conditions.
foreach ($this->get_all_filters() as $filter) {
$this
->add_filter($filter)
->add_condition($filter);
}
return $this;
}
/**
* Returns list of all available columns
*
* @return column[]
*/
protected function get_all_columns(): array {
$userenrolments = $this->get_table_alias('user_enrolments');
$enrol = $this->get_table_alias('enrol');
// Enrolment method column (Deprecated since Moodle 4.3, to remove in MDL-78118).
$columns[] = (new column(
'method',
new lang_string('method', 'enrol'),
$this->get_entity_name()
))
->add_joins($this->get_joins())
->set_type(column::TYPE_TEXT)
->add_fields("{$enrol}.enrol, {$enrol}.id")
->set_is_sortable(true)
->set_is_deprecated('See \'enrol:name\' for replacement')
->add_callback([enrolment_formatter::class, 'enrolment_name']);
// Enrolment time created.
$columns[] = (new column(
'timecreated',
new lang_string('timecreated', 'moodle'),
$this->get_entity_name()
))
->add_joins($this->get_joins())
->set_type(column::TYPE_TIMESTAMP)
->add_field("{$userenrolments}.timecreated")
->set_is_sortable(true)
->add_callback([format::class, 'userdate']);
// Enrolment time started.
$columns[] = (new column(
'timestarted',
new lang_string('timestarted', 'enrol'),
$this->get_entity_name()
))
->add_joins($this->get_joins())
->set_type(column::TYPE_TIMESTAMP)
->add_field("
CASE WHEN {$userenrolments}.timestart = 0
THEN {$userenrolments}.timecreated
ELSE {$userenrolments}.timestart
END", 'timestarted')
->set_is_sortable(true)
->add_callback([format::class, 'userdate']);
// Enrolment time ended.
$columns[] = (new column(
'timeended',
new lang_string('timeended', 'enrol'),
$this->get_entity_name()
))
->add_joins($this->get_joins())
->set_type(column::TYPE_TIMESTAMP)
->add_field("{$userenrolments}.timeend")
->set_is_sortable(true)
->add_callback([format::class, 'userdate']);
// Enrolment status.
$columns[] = (new column(
'status',
new lang_string('status', 'moodle'),
$this->get_entity_name()
))
->add_joins($this->get_joins())
->set_type(column::TYPE_TEXT)
->add_field($this->get_status_field_sql(), 'status')
->set_is_sortable(true)
->add_callback([enrolment_formatter::class, 'enrolment_status']);
// Role column (Deprecated since Moodle 4.3, to remove in MDL-78118).
$ctx = database::generate_alias();
$ra = database::generate_alias();
$r = database::generate_alias();
$columns[] = (new column(
'role',
new lang_string('role', 'moodle'),
$this->get_entity_name()
))
->add_joins($this->get_joins())
->add_join("LEFT JOIN {context} {$ctx}
ON {$ctx}.instanceid = {$enrol}.courseid AND {$ctx}.contextlevel = " . CONTEXT_COURSE)
->add_join("LEFT JOIN {role_assignments} {$ra}
ON {$ra}.contextid = {$ctx}.id AND {$ra}.userid = {$userenrolments}.userid")
->add_join("LEFT JOIN {role} {$r} ON {$r}.id = {$ra}.roleid")
->set_type(column::TYPE_TEXT)
->add_fields("{$r}.id, {$r}.name, {$r}.shortname, {$ctx}.instanceid")
->set_is_sortable(true, ["{$r}.shortname"])
->set_is_deprecated('See \'role:name\' for replacement')
->add_callback(static function(?string $value, stdClass $row): string {
if (!$row->id) {
return '';
}
$context = context_course::instance($row->instanceid);
return role_get_name($row, $context, ROLENAME_ALIAS);
});
return $columns;
}
/**
* Generate SQL snippet suitable for returning enrolment status field
*
* @return string
*/
private function get_status_field_sql(): string {
$time = time();
$userenrolments = $this->get_table_alias('user_enrolments');
$enrol = $this->get_table_alias('enrol');
return "
CASE WHEN {$userenrolments}.status = " . ENROL_USER_ACTIVE . "
THEN CASE WHEN ({$userenrolments}.timestart > {$time})
OR ({$userenrolments}.timeend > 0 AND {$userenrolments}.timeend < {$time})
OR ({$enrol}.status = " . ENROL_INSTANCE_DISABLED . ")
THEN " . status_field::STATUS_NOT_CURRENT . "
ELSE " . status_field::STATUS_ACTIVE . "
END
ELSE {$userenrolments}.status
END";
}
/**
* Return list of all available filters
*
* @return filter[]
*/
protected function get_all_filters(): array {
$userenrolments = $this->get_table_alias('user_enrolments');
$enrol = $this->get_table_alias('enrol');
// Enrolment method (Deprecated since Moodle 4.3, to remove in MDL-78118).
$enrolmentmethods = static function(): array {
return array_map(static function(enrol_plugin $plugin): string {
return get_string('pluginname', 'enrol_' . $plugin->get_name());
}, enrol_get_plugins(true));
};
$filters[] = (new filter(
select::class,
'method',
new lang_string('method', 'enrol'),
$this->get_entity_name(),
"{$enrol}.enrol"
))
->add_joins($this->get_joins())
->set_is_deprecated('See \'enrol:plugin\' for replacement')
->set_options_callback($enrolmentmethods);
// Enrolment time created.
$filters[] = (new filter(
date::class,
'timecreated',
new lang_string('timecreated', 'moodle'),
$this->get_entity_name(),
"{$userenrolments}.timecreated"
))
->add_joins($this->get_joins())
->set_limited_operators([
date::DATE_ANY,
date::DATE_NOT_EMPTY,
date::DATE_EMPTY,
date::DATE_RANGE,
date::DATE_LAST,
date::DATE_CURRENT,
]);
// Enrolment time started.
$filters[] = (new filter(
date::class,
'timestarted',
new lang_string('timestarted', 'enrol'),
$this->get_entity_name(),
"CASE WHEN {$userenrolments}.timestart = 0
THEN {$userenrolments}.timecreated
ELSE {$userenrolments}.timestart
END"
))
->add_joins($this->get_joins())
->set_limited_operators([
date::DATE_ANY,
date::DATE_NOT_EMPTY,
date::DATE_EMPTY,
date::DATE_RANGE,
date::DATE_LAST,
date::DATE_CURRENT,
]);
// Enrolment time ended.
$filters[] = (new filter(
date::class,
'timeended',
new lang_string('timeended', 'enrol'),
$this->get_entity_name(),
"{$userenrolments}.timeend"
))
->add_joins($this->get_joins())
->set_limited_operators([
date::DATE_ANY,
date::DATE_NOT_EMPTY,
date::DATE_EMPTY,
date::DATE_RANGE,
date::DATE_LAST,
date::DATE_CURRENT,
]);
// Enrolment status.
$filters[] = (new filter(
select::class,
'status',
new lang_string('status', 'moodle'),
$this->get_entity_name(),
$this->get_status_field_sql()
))
->add_joins($this->get_joins())
->set_options(enrolment_formatter::enrolment_values());
return $filters;
}
}
@@ -0,0 +1,77 @@
<?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_course\reportbuilder\local\formatters;
use core_completion\progress;
use core_reportbuilder\local\helpers\format;
use stdClass;
/**
* Formatters for the course completion entity
*
* @package core_course
* @copyright 2022 David Matamoros <davidmc@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class completion {
/**
* Return completion progress as a percentage
*
* @param string|null $value
* @param stdClass $row
* @return string
*/
public static function completion_progress(?string $value, stdClass $row): string {
global $CFG;
require_once($CFG->libdir . '/completionlib.php');
// Do not show progress if there is no userid.
if (!$row->userid) {
return '';
}
// Make sure courseid and userid have a value, specially userid because get_course_progress_percentage() defaults
// to the current user if this is null and the result would be wrong.
$courseid = (int) $row->courseid;
$userid = (int) $row->userid;
if ($courseid === 0 || $userid === 0) {
return format::percent(0);
}
$course = get_course($courseid);
$progress = (float) progress::get_course_progress_percentage($course, $userid);
return format::percent($progress);
}
/**
* Return number of days for methods daystakingcourse and daysuntilcompletion
*
* @param int|null $value
* @param stdClass $row
* @return int|null
*/
public static function get_days(?int $value, stdClass $row): ?int {
// Do not show anything if there is no userid.
if (!$row->userid) {
return null;
}
return $value;
}
}
@@ -0,0 +1,89 @@
<?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_course\reportbuilder\local\formatters;
use core_user\output\status_field;
use lang_string;
use stdClass;
/**
* Formatters for the course enrolment entity
*
* @package core_course
* @copyright 2022 David Matamoros <davidmc@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class enrolment {
/**
* Return enrolment plugin instance name
*
* @param string|null $value
* @param stdClass $row
* @return string
*
* @deprecated since Moodle 4.3 - please do not use this function any more (to remove in MDL-78118)
*/
public static function enrolment_name(?string $value, stdClass $row): string {
global $DB;
if (empty($value)) {
return '';
}
$instance = $DB->get_record('enrol', ['id' => $row->id, 'enrol' => $row->enrol], '*', MUST_EXIST);
$plugin = enrol_get_plugin($row->enrol);
return $plugin ? $plugin->get_instance_name($instance) : '-';
}
/**
* Returns list of enrolment statuses
*
* @return lang_string[]
*/
public static function enrolment_values(): array {
return [
status_field::STATUS_ACTIVE => new lang_string('participationactive', 'enrol'),
status_field::STATUS_SUSPENDED => new lang_string('participationsuspended', 'enrol'),
status_field::STATUS_NOT_CURRENT => new lang_string('participationnotcurrent', 'enrol'),
];
}
/**
* Return enrolment status for user
*
* @param string|null $value
* @return string|null
*/
public static function enrolment_status(?string $value): ?string {
if ($value === null) {
return null;
}
$statusvalues = self::enrolment_values();
$value = (int) $value;
if (!array_key_exists($value, $statusvalues)) {
return null;
}
return (string) $statusvalues[$value];
}
}
+198
View File
@@ -0,0 +1,198 @@
<?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/>.
/**
* Search area for Moodle courses.
*
* @package core_course
* @copyright 2016 Skylar Kelty <S.Kelty@kent.ac.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_course\search;
defined('MOODLE_INTERNAL') || die();
/**
* Search area for Moodle courses.
*
* @package core_course
* @copyright 2016 Skylar Kelty <S.Kelty@kent.ac.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class course extends \core_search\base {
/**
* The context levels the search implementation is working on.
*
* @var array
*/
protected static $levels = [CONTEXT_COURSE];
/**
* Returns recordset containing required data for indexing courses.
*
* @param int $modifiedfrom timestamp
* @param \context|null $context Restriction context
* @return \moodle_recordset|null Recordset or null if no change possible
*/
public function get_document_recordset($modifiedfrom = 0, \context $context = null) {
global $DB;
list ($contextjoin, $contextparams) = $this->get_course_level_context_restriction_sql(
$context, 'c');
if ($contextjoin === null) {
return null;
}
return $DB->get_recordset_sql("
SELECT c.*
FROM {course} c
$contextjoin
WHERE c.timemodified >= ?
ORDER BY c.timemodified ASC", array_merge($contextparams, [$modifiedfrom]));
}
/**
* Returns the document associated with this course.
*
* @param \stdClass $record
* @param array $options
* @return \core_search\document
*/
public function get_document($record, $options = array()) {
try {
$context = \context_course::instance($record->id);
} catch (\moodle_exception $ex) {
// Notify it as we run here as admin, we should see everything.
debugging('Error retrieving ' . $this->areaid . ' ' . $record->id . ' document, not all required data is available: ' .
$ex->getMessage(), DEBUG_DEVELOPER);
return false;
}
// Prepare associative array with data from DB.
$doc = \core_search\document_factory::instance($record->id, $this->componentname, $this->areaname);
$doc->set('title', content_to_text($record->fullname, false));
$doc->set('content', content_to_text($record->summary, $record->summaryformat));
$doc->set('contextid', $context->id);
$doc->set('courseid', $record->id);
$doc->set('owneruserid', \core_search\manager::NO_OWNER_ID);
$doc->set('modified', $record->timemodified);
$doc->set('description1', $record->shortname);
// Check if this document should be considered new.
if (isset($options['lastindexedtime']) && $options['lastindexedtime'] < $record->timecreated) {
// If the document was created after the last index time, it must be new.
$doc->set_is_new(true);
}
return $doc;
}
/**
* Whether the user can access the document or not.
*
* @param int $id The course instance id.
* @return int
*/
public function check_access($id) {
global $DB;
$course = $DB->get_record('course', array('id' => $id));
if (!$course) {
return \core_search\manager::ACCESS_DELETED;
}
if (\core_course_category::can_view_course_info($course)) {
return \core_search\manager::ACCESS_GRANTED;
}
return \core_search\manager::ACCESS_DENIED;
}
/**
* Link to the course.
*
* @param \core_search\document $doc
* @return \moodle_url
*/
public function get_doc_url(\core_search\document $doc) {
return $this->get_context_url($doc);
}
/**
* Link to the course.
*
* @param \core_search\document $doc
* @return \moodle_url
*/
public function get_context_url(\core_search\document $doc) {
return new \moodle_url('/course/view.php', array('id' => $doc->get('courseid')));
}
/**
* Returns true if this area uses file indexing.
*
* @return bool
*/
public function uses_file_indexing() {
return true;
}
/**
* Return the context info required to index files for
* this search area.
*
* Should be overridden by each search area.
*
* @return array
*/
public function get_search_fileareas() {
$fileareas = array(
'overviewfiles',
'summary'// Fileareas.
);
return $fileareas;
}
/**
* Returns the moodle component name.
*
* It might be the plugin name (whole frankenstyle name) or the core subsystem name.
*
* @return string
*/
public function get_component_name() {
return 'course';
}
/**
* Returns an icon instance for the document.
*
* @param \core_search\document $doc
* @return \core_search\document_icon
*/
public function get_doc_icon(\core_search\document $doc): \core_search\document_icon {
return new \core_search\document_icon('i/course');
}
/**
* Returns a list of category names associated with the area.
*
* @return array
*/
public function get_category_names() {
return [\core_search\manager::SEARCH_AREA_CATEGORY_COURSES];
}
}
+212
View File
@@ -0,0 +1,212 @@
<?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/>.
/**
* Search area for course custom fields.
*
* @package core_course
* @copyright Toni Barbera <toni@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_course\search;
use core_course\customfield\course_handler;
use core_customfield\data_controller;
use core_customfield\field_controller;
defined('MOODLE_INTERNAL') || die();
/**
* Search area for course custom fields.
*
* @package core_course
* @copyright Toni Barbera <toni@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class customfield extends \core_search\base {
/**
* Custom fields are indexed at course context.
*
* @var array
*/
protected static $levels = [CONTEXT_COURSE];
/**
* Returns recordset containing required data for indexing
* course custom fields.
*
* @param int $modifiedfrom timestamp
* @param \context|null $context Restriction context
* @return \moodle_recordset|null Recordset or null if no change possible
*/
public function get_document_recordset($modifiedfrom = 0, \context $context = null) {
global $DB;
list ($contextjoin, $contextparams) = $this->get_course_level_context_restriction_sql($context, 'c', SQL_PARAMS_NAMED);
if ($contextjoin === null) {
return null;
}
$fields = course_handler::create()->get_fields();
if (!$fields) {
$fields = array();
}
list($fieldsql, $fieldparam) = $DB->get_in_or_equal(array_keys($fields), SQL_PARAMS_NAMED, 'fld', true, 0);
// Restrict recordset to CONTEXT_COURSE (since we are implementing it to core_course\search).
$sql = "SELECT d.*
FROM {customfield_data} d
JOIN {course} c ON c.id = d.instanceid
JOIN {context} cnt ON cnt.instanceid = c.id
$contextjoin
WHERE d.timemodified >= :modifiedfrom
AND cnt.contextlevel = :contextlevel
AND d.fieldid $fieldsql
ORDER BY d.timemodified ASC";
return $DB->get_recordset_sql($sql , array_merge($contextparams,
['modifiedfrom' => $modifiedfrom, 'contextlevel' => CONTEXT_COURSE], $fieldparam));
}
/**
* Returns the document associated with this section.
*
* @param \stdClass $record
* @param array $options
* @return \core_search\document|bool
*/
public function get_document($record, $options = array()) {
global $PAGE;
try {
$context = \context_course::instance($record->instanceid);
} catch (\moodle_exception $ex) {
// Notify it as we run here as admin, we should see everything.
debugging('Error retrieving ' . $this->areaid . ' ' . $record->id . ' document, not all required data is available: ' .
$ex->getMessage(), DEBUG_DEVELOPER);
return false;
}
$handler = course_handler::create();
$field = $handler->get_fields()[$record->fieldid];
$data = data_controller::create(0, $record, $field);
// Prepare associative array with data from DB.
$doc = \core_search\document_factory::instance($record->id, $this->componentname, $this->areaname);
$doc->set('title', content_to_text($field->get('name'), false));
$doc->set('content', content_to_text($data->export_value(), FORMAT_HTML));
$doc->set('contextid', $context->id);
$doc->set('courseid', $context->instanceid);
$doc->set('owneruserid', \core_search\manager::NO_OWNER_ID);
$doc->set('modified', $record->timemodified);
// Check if this document should be considered new.
if (isset($options['lastindexedtime']) && ($options['lastindexedtime'] < $record->timecreated)) {
// If the document was created after the last index time, it must be new.
$doc->set_is_new(true);
}
return $doc;
}
/**
* Whether the user can access the document or not.
*
* @param int $id The custom field data ID
* @return int
*/
public function check_access($id) {
global $DB;
$coursesql = '
SELECT c.*
FROM {course} c
JOIN {customfield_data} d ON d.instanceid = c.id
WHERE d.id = :dataid';
// Verify both the course and data record still exist.
$course = $DB->get_record_sql($coursesql, ['dataid' => $id]);
if (!$course) {
return \core_search\manager::ACCESS_DELETED;
}
// Check whether user is enrolled and the course is visible, or user can view it while hidden.
$context = \context_course::instance($course->id);
$userenrolled = is_enrolled($context) &&
($course->visible || has_capability('moodle/course:viewhiddencourses', $context));
// Grant access if user is considered enrolled, or they can otherwise see the course info.
if ($userenrolled || \core_course_category::can_view_course_info($course)) {
return \core_search\manager::ACCESS_GRANTED;
}
return \core_search\manager::ACCESS_DENIED;
}
/**
* Link to the course.
*
* @param \core_search\document $doc
* @return \moodle_url
*/
public function get_doc_url(\core_search\document $doc) {
return $this->get_context_url($doc);
}
/**
* Link to the course.
*
* @param \core_search\document $doc
* @return \moodle_url
*/
public function get_context_url(\core_search\document $doc) {
return new \moodle_url('/course/view.php', array('id' => $doc->get('courseid')));
}
/**
* Returns the moodle component name.
*
* It might be the plugin name (whole frankenstyle name) or the core subsystem name.
*
* @return string
*/
public function get_component_name() {
return 'course';
}
/**
* Returns an icon instance for the document.
*
* @param \core_search\document $doc
* @return \core_search\document_icon
*/
public function get_doc_icon(\core_search\document $doc): \core_search\document_icon {
return new \core_search\document_icon('i/customfield');
}
/**
* Returns a list of category names associated with the area.
*
* @return array
*/
public function get_category_names() {
return [
\core_search\manager::SEARCH_AREA_CATEGORY_COURSE_CONTENT,
\core_search\manager::SEARCH_AREA_CATEGORY_COURSES
];
}
}
+217
View File
@@ -0,0 +1,217 @@
<?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/>.
/**
* Search area for course sections (title and summary).
*
* @package core_course
* @copyright 2018 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_course\search;
defined('MOODLE_INTERNAL') || die();
/**
* Search area for course sections (title and summary).
*
* Note this does not include the activities within the section, as these have their own search
* areas.
*
* @package core_course
* @copyright 2018 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class section extends \core_search\base {
/**
* Sections are indexed at course context.
*
* @var array
*/
protected static $levels = [CONTEXT_COURSE];
/**
* Returns recordset containing required data for indexing course sections.
*
* @param int $modifiedfrom timestamp
* @param \context|null $context Restriction context
* @return \moodle_recordset|null Recordset or null if no change possible
*/
public function get_document_recordset($modifiedfrom = 0, \context $context = null) {
global $DB;
list ($contextjoin, $contextparams) = $this->get_course_level_context_restriction_sql($context, 'c');
if ($contextjoin === null) {
return null;
}
$comparetext = $DB->sql_compare_text('cs.summary', 1);
return $DB->get_recordset_sql("
SELECT cs.id,
cs.course,
cs.section,
cs.name,
cs.summary,
cs.summaryformat,
cs.timemodified
FROM {course_sections} cs
JOIN {course} c ON c.id = cs.course
$contextjoin
WHERE cs.timemodified >= ?
AND (cs.name != ? OR $comparetext != ?)
ORDER BY cs.timemodified ASC", array_merge($contextparams, [$modifiedfrom, '', '']));
}
/**
* Returns the document associated with this section.
*
* @param \stdClass $record
* @param array $options
* @return \core_search\document
*/
public function get_document($record, $options = array()) {
global $CFG;
require_once($CFG->dirroot . '/course/lib.php');
// Get the context, modinfo, and section.
try {
$context = \context_course::instance($record->course);
} catch (\moodle_exception $ex) {
// Notify it as we run here as admin, we should see everything.
debugging('Error retrieving ' . $this->areaid . ' ' . $record->id .
' document, not all required data is available: ' . $ex->getMessage(),
DEBUG_DEVELOPER);
return false;
}
// Title - use default if none given.
$title = get_section_name($record->course, $record->section);
// Prepare associative array with data from DB.
$doc = \core_search\document_factory::instance($record->id, $this->componentname, $this->areaname);
$doc->set('title', content_to_text($title, false));
$doc->set('content', content_to_text($record->summary, $record->summaryformat));
$doc->set('contextid', $context->id);
$doc->set('courseid', $record->course);
$doc->set('owneruserid', \core_search\manager::NO_OWNER_ID);
$doc->set('modified', $record->timemodified);
return $doc;
}
/**
* Whether the user can access the section or not.
*
* @param int $id The course section id.
* @return int One of the \core_search\manager:ACCESS_xx constants
*/
public function check_access($id) {
global $DB;
// Check we can get the section and the course modinfo.
$sectionrec = $DB->get_record('course_sections', ['id' => $id], '*', IGNORE_MISSING);
if (!$sectionrec) {
return \core_search\manager::ACCESS_DELETED;
}
try {
$modinfo = get_fast_modinfo($sectionrec->course);
} catch (\moodle_exception $e) {
return \core_search\manager::ACCESS_DELETED;
}
$section = $modinfo->get_section_info($sectionrec->section, IGNORE_MISSING);
if (!$section) {
return \core_search\manager::ACCESS_DELETED;
}
// Check access to course and that the section is visible to current user.
if (can_access_course($modinfo->get_course()) && $section->uservisible) {
return \core_search\manager::ACCESS_GRANTED;
}
return \core_search\manager::ACCESS_DENIED;
}
/**
* Gets a link to the section.
*
* @param \core_search\document $doc
* @return \moodle_url
*/
public function get_doc_url(\core_search\document $doc) {
global $DB;
$section = $DB->get_field('course_sections', 'section', ['id' => $doc->get('itemid')], MUST_EXIST);
$format = course_get_format($doc->get('courseid'));
return $format->get_view_url($section);
}
/**
* Gets a link to the section.
*
* @param \core_search\document $doc
* @return \moodle_url
*/
public function get_context_url(\core_search\document $doc) {
return $this->get_doc_url($doc);
}
/**
* Returns true to include summary files in the index.
*
* @return bool True
*/
public function uses_file_indexing() {
return true;
}
/**
* Return the file area that is used for summary files.
*
* @return array File area name
*/
public function get_search_fileareas() {
return ['section'];
}
/**
* Returns the moodle component name, as used in the files table.
*
* @return string Component name
*/
public function get_component_name() {
return 'course';
}
/**
* Returns an icon instance for the document.
*
* @param \core_search\document $doc
* @return \core_search\document_icon
*/
public function get_doc_icon(\core_search\document $doc): \core_search\document_icon {
return new \core_search\document_icon('i/section');
}
/**
* Returns a list of category names associated with the area.
*
* @return array
*/
public function get_category_names() {
return [\core_search\manager::SEARCH_AREA_CATEGORY_COURSE_CONTENT];
}
}
@@ -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/>.
namespace core_course\task;
use core\task\adhoc_task;
/**
* Class handling course content updates notifications.
*
* @package core_course
* @copyright 2021 Juan Leyva <juan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class content_notification_task extends adhoc_task {
// Use the logging trait for better logging.
use \core\task\logging_trait;
/**
* Run the main task.
*/
public function execute() {
global $CFG, $OUTPUT;
require_once($CFG->libdir . '/enrollib.php');
$data = $this->get_custom_data();
$course = get_course($data->courseid);
$modinfo = get_fast_modinfo($course);
$cm = $modinfo->cms[$data->cmid];
$isupdate = !empty($data->update);
if (!$course->visible || !$cm->visible) {
// The course or module is hidden. We don't check if the user can see hidden courses, does not make sense here.
// Permissions may have changed since it was queued.
return;
}
// Get only active users.
$coursecontext = \context_course::instance($course->id);
$users = get_enrolled_users($coursecontext, '', 0, 'u.*', null, 0, 0, true);
if (empty($users)) {
return;
}
$userfrom = \core_user::get_user($data->userfrom);
// Now send the messages
$countusers = count($users);
$sentcount = $errorcount = 0;
$this->log_start("Sending course content update notifications to {$countusers} potential users
from user with id {$userfrom->id}.");
foreach ($users as $user) {
\core\cron::setup_user($user, $course);
// Ensure that the activity is available/visible to the user.
$cm = get_fast_modinfo($course)->cms[$cm->id];
if (!\core_availability\info_module::is_user_visible($cm, $user->id, false)) {
$this->log("Ignoring user {$user->id} (no permissions to see the module)", 1);
continue;
}
// Get module names in the user's language.
$modnames = get_module_types_names();
$a = [
'coursename' => format_string(get_course_display_name_for_list($course), true, ['context' => $coursecontext]),
'courselink' => (new \moodle_url('/course/view.php', ['id' => $course->id]))->out(false),
'modulename' => $cm->get_formatted_name(),
'moduletypename' => $modnames[$cm->modname],
'link' => (new \moodle_url('/mod/' . $cm->modname . '/view.php', ['id' => $cm->id]))->out(false),
'notificationpreferenceslink' =>
(new \moodle_url('/message/notificationpreferences.php', ['userid' => $user->id]))->out(false),
];
if ($isupdate) {
$messagesubject = get_string('coursecontentnotifupdate', 'course', $a);
$messagebody = get_string('coursecontentnotifupdatebody', 'course', $a);
} else {
$messagesubject = get_string('coursecontentnotifnew', 'course', $a);
$messagebody = get_string('coursecontentnotifnewbody', 'course', $a);
}
// Send notification.
$eventdata = new \core\message\message();
$eventdata->courseid = $course->id;
$eventdata->component = 'moodle';
$eventdata->name = 'coursecontentupdated';
$eventdata->userfrom = $userfrom;
$eventdata->userto = $user;
$eventdata->subject = $messagesubject;
$eventdata->fullmessageformat = FORMAT_HTML;
$eventdata->fullmessagehtml = $messagebody;
$eventdata->smallmessage = strip_tags($eventdata->fullmessagehtml);
$eventdata->contexturl = (new \moodle_url('/mod/' . $cm->modname . '/view.php', ['id' => $cm->id]))->out(false);
$eventdata->contexturlname = $cm->get_formatted_name();
$eventdata->notification = 1;
// Add notification custom data.
$eventcustomdata = ['notificationiconurl' => $cm->get_icon_url()->out(false)];
if ($courseimage = \core_course\external\course_summary_exporter::get_course_image($course)) {
$eventcustomdata['notificationpictureurl'] = $courseimage;
}
$eventdata->customdata = $eventcustomdata;
$activitydates = \core\activity_dates::get_dates_for_module($cm, $user->id);
if (!empty($activitydates)) {
$data = (new \core_course\output\activity_dates($activitydates))->export_for_template($OUTPUT);
foreach ($data->activitydates as $date) {
$eventdata->fullmessagehtml .= \html_writer::div($date['label'] . ' ' . $date['datestring']);
}
}
$eventdata->fullmessage = html_to_text($eventdata->fullmessagehtml);
if (message_send($eventdata)) {
$this->log("Notification sent to user with id {$user->id}", 1);
$sentcount++;
} else {
$this->log("Failed to send notification to user with id {$user->id}", 1);
$errorcount++;
}
}
$this->log_finish("Sent {$sentcount} notifications with {$errorcount} failures");
}
}
@@ -0,0 +1,99 @@
<?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/>.
/**
* Adhoc task handling course module deletion.
*
* @package core_course
* @copyright 2016 Jake Dallimore <jrhdallimore@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_course\task;
defined('MOODLE_INTERNAL') || die();
/**
* Class handling course module deletion.
*
* This task supports an array of course module object as custom_data, and calls course_delete_module() in synchronous deletion
* mode for each of them.
* This will:
* 1. call any 'mod_xxx_pre_course_module_deleted' functions (e.g. Recycle bin)
* 2. delete the module
* 3. fire the deletion event
*
* @package core_course
* @copyright 2016 Jake Dallimore <jrhdallimore@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class course_delete_modules extends \core\task\adhoc_task {
/**
* Run the deletion task.
*
* @throws \coding_exception if the module could not be removed.
*/
public function execute() {
global $CFG;
require_once($CFG->dirroot. '/course/lib.php');
// Set the proper user.
if ($this->get_custom_data()->userid !== $this->get_custom_data()->realuserid) {
$realuser = \core_user::get_user($this->get_custom_data()->realuserid, '*', MUST_EXIST);
\core\cron::setup_user($realuser);
\core\session\manager::loginas($this->get_custom_data()->userid, \context_system::instance(), false);
} else {
$user = \core_user::get_user($this->get_custom_data()->userid, '*', MUST_EXIST);
\core\cron::setup_user($user);
}
$cms = $this->get_custom_data()->cms;
$exceptions = [];
$cmsfailed = [];
foreach ($cms as $key => $cm) {
try {
course_delete_module($cm->id);
} catch (\Exception $e) {
// Keep the information instead of throw an exception and continue with next cms.
$exceptions[] = ("The course module {$cm->id} could not be deleted. "
. "{$e->getMessage()}: {$e->getFile()}({$e->getLine()}) {$e->getTraceAsString()}");
// Save the cms that has failed to set the data only with this values.
$cmsfailed[$key] = $cm;
continue;
}
}
// Throw the existing exceptions if there is any.
if (!empty($exceptions)) {
// Save the failed CMS.
$customdata = $this->get_custom_data();
$customdata->cms = $cmsfailed;
$this->set_custom_data($customdata);
throw new \coding_exception("The following course modules could not be deleted:\n " .
implode('\n', $exceptions));
}
}
/**
* Sets attemptsavailable to false.
*
* @return boolean
*/
public function retry_until_success(): bool {
return false;
}
}