Completed code guidelines rework

This commit is contained in:
PMKuipers 2024-06-02 23:23:32 +02:00
parent e72be595aa
commit 64c821bbe9
59 changed files with 939 additions and 818 deletions

View File

@ -23,7 +23,6 @@
namespace local_treestudyplan;
use moodle_exception;
use ValueError;
defined('MOODLE_INTERNAL') || die();
@ -78,7 +77,7 @@ abstract class aggregator {
* Create a new aggregatior object based on the specified method
* @param mixed $method Aggregation method
* @param mixed $configstr Configuration string for aggregator
* @throws ValueError If method is not found
* @throws moodle_exception If method is not found
*/
public static function create($method, $configstr): self {
if (self::supported($method)) {
@ -98,7 +97,7 @@ abstract class aggregator {
public static function create_or_default($method, $configstr): self {
try {
return self::create($method, $configstr);
} catch (\ValueError $x) {
} catch (\moodle_exception $x) {
return self::create(self::FALLBACK, "");
}
}

View File

@ -58,7 +58,8 @@ class associationservice extends \external_api {
"lastname" => new \external_value(PARAM_TEXT, 'last name'),
"idnumber" => new \external_value(PARAM_TEXT, 'id number'),
"email" => new \external_value(PARAM_TEXT, 'email address'),
"lastaccess" => new \external_value(PARAM_INT, 'id of last access this user had to any course in the studyplan', VALUE_OPTIONAL),
"lastaccess" => new \external_value(PARAM_INT,
'id of last access this user had to any course in the studyplan', VALUE_OPTIONAL),
]);
}
@ -131,6 +132,11 @@ class associationservice extends \external_api {
}
/**
* Get last access time of user to any of the courses in the studyplan
* @param $userid ID of user
* @param $studyplanid ID of studyplan
*/
public static function user_lastaccess($userid, $studyplanid=null) {
global $DB;
if (!empty($studyplanid)) {

View File

@ -270,6 +270,10 @@ class badgeinfo {
return $badge;
}
/**
* Define structure for badge completion parts
* @param object $value VALUE_OPTIONAL or VALUE_REQUIRED
*/
protected static function badge_completion_structure($value) {
return new \external_single_structure([
"types" => new \external_multiple_structure(new \external_single_structure([
@ -297,6 +301,10 @@ class badgeinfo {
], 'badge completion information', $value);
}
/**
* Generate completion model of this badge for a given user
* @param int $userid id of user to check for
*/
protected function badge_completion_data($userid): array {
$count = 0;
@ -330,7 +338,7 @@ class badgeinfo {
if ($badgeagg == BADGE_CRITERIA_AGGREGATION_ANY) {
/* If ANY completion overall, count only the criteria type with the highest completion percentage -.
Overwrite data if current type is more complete */
$typefraction = ($typecount > 0) ? ($typeprogress / $typecount):0;
$typefraction = ($typecount > 0) ? ($typeprogress / $typecount) : ($typeprogress / $typecount);
if ($typefraction > $fraction || ($fraction == 0 && $typecount > $count)) {
$fraction = $typefraction;
$count = $typecount;
@ -444,7 +452,8 @@ class badgeinfo {
$title = \html_writer::tag('b', '"' . get_string('modulename', $mod->modname) . ' - ' . $mod->name . '"');;
$description = \html_writer::tag('b', '"' . get_string('modulename', $mod->modname) . ' - ' . $mod->name . '"');
if (isset($p['bydate'])) {
$description .= get_string('criteria_descr_bydate', 'badges', userdate($p['bydate'], get_string('strftimedate', 'core_langconfig')));
$description .= get_string('criteria_descr_bydate', 'badges',
userdate($p['bydate'], get_string('strftimedate', 'core_langconfig')));
}
}
@ -456,12 +465,13 @@ class badgeinfo {
'title' => get_string('completeactivity', 'core').
get_string('modulename', $mod->modname) . ' - ' . $mod->name,
],
]
],
];
if (isset($p["bydate"])) {
$subcrit["requirements"]["bydate"] = [
'title' => get_string('criteria_descr_bydate', 'badges', userdate($p['bydate'], get_string('strftimedate', 'core_langconfig'))),
'title' => get_string('criteria_descr_bydate', 'badges',
userdate($p['bydate'], get_string('strftimedate', 'core_langconfig'))),
];
}
@ -513,16 +523,15 @@ class badgeinfo {
];
if (isset($userid)) {
$crit = $DB->get_record('badge_manual_award', ['issuerrole' => $p['role'], 'recipientid' => $userid, 'badgeid' => $crit->badgeid]);
$crit = $DB->get_record('badge_manual_award',
['issuerrole' => $p['role'], 'recipientid' => $userid, 'badgeid' => $crit->badgeid]);
$subcrit["completed"] = $crit !== false;
}
$list[] = $subcrit;
}
} else if ($crit->criteriatype == BADGE_CRITERIA_TYPE_SOCIAL) {
/* Unused in moodle - probably deprecated */
} else if ($crit->criteriatype == BADGE_CRITERIA_TYPE_COURSE || $crit->criteriatype == BADGE_CRITERIA_TYPE_COURSESET) {
if ($crit->criteriatype == BADGE_CRITERIA_TYPE_COURSE) {
$params = [reset($crit->params)]; // Only use the first parameter
$params = [reset($crit->params)]; // Only use the first parameter.
} else {
$params = $crit->params;
}
@ -534,7 +543,8 @@ class badgeinfo {
} else {
$description = \html_writer::tag('b', '"' . $course->fullname . '"');
if (isset($p['bydate'])) {
$description .= get_string('criteria_descr_bydate', 'badges', userdate($p['bydate'], get_string('strftimedate', 'core_langconfig')));
$description .= get_string( 'criteria_descr_bydate', 'badges',
userdate($p['bydate'], get_string('strftimedate', 'core_langconfig')));
}
if (isset($p['grade'])) {
$description .= get_string('criteria_descr_grade', 'badges', $p['grade']);
@ -549,18 +559,18 @@ class badgeinfo {
'completion' => [
'title' => get_string('coursecompleted', 'completion'),
],
]
],
];
if (isset($p["grade"])) {
$subcrit["requirements"]["grade"] = [
'title' => get_string('criteria_descr_grade', 'badges', $p["grade"]),
];
}
if (isset($p["bydate"])) {
$subcrit["requirements"]["bydate"] = [
'title' => get_string('criteria_descr_bydate', 'badges', userdate($p['bydate'], get_string('strftimedate', 'core_langconfig'))),
'title' => get_string('criteria_descr_bydate', 'badges',
userdate($p['bydate'], get_string('strftimedate', 'core_langconfig'))),
];
}
@ -577,7 +587,8 @@ class badgeinfo {
}
$checkdate = true;
if (isset($p["bydate"])) {
$checkdate = ((bool) $coursecompletion->timecompleted) && ($coursecompletion->timecompleted <= $p["bydate"]);
$checkdate = ((bool) $coursecompletion->timecompleted) &&
($coursecompletion->timecompleted <= $p["bydate"]);
$subcrit["requirements"]["bydate"]["completed"] = (bool) $checkdate;
}
@ -773,17 +784,6 @@ class badgeinfo {
throw new \moodle_exception("\$course argument must be course id or course object", "local_treestudyplan");
}
/*
$crit->citeriatype = BADGE_CRITERIA_TYPE_COURSE
$badge->params[0] = ["course"=> $courseid]
$crit->citeriatype = BADGE_CRITERIA_TYPE_COURSESET
$badge->params[] = [["course"=> $courseid]...]
$crit->citeriatype = BADGE_CRITERIA_TYPE_ACTIVITY
$badge->params[] = [["module"=> $cmid]...]
$crit->citeriatype = BADGE_CRITERIA_TYPE_COMPETENCY
$badge->params[] = [["competency"=> $competencyid]...]
*/
$search = trim($search);
$conditions = "";
$basesqlparams = ['courseid' => $courseid];
@ -799,7 +799,8 @@ class badgeinfo {
}
[$ctypesql, $ctypeinparams] = $DB->get_in_or_equal([BADGE_CRITERIA_TYPE_COURSESET, BADGE_CRITERIA_TYPE_COURSE], SQL_PARAMS_NAMED);
[$ctypesql, $ctypeinparams] = $DB->get_in_or_equal([BADGE_CRITERIA_TYPE_COURSESET, BADGE_CRITERIA_TYPE_COURSE],
SQL_PARAMS_NAMED);
$sqlparams = array_merge($basesqlparams, $ctypeinparams);
$sql = "SELECT DISTINCT b.id from {badge} b
@ -860,7 +861,7 @@ class badgeinfo {
$basesqlparams = array_merge($basesqlparams, $inparams);
}
// Get all course badges for this course
// Get all course badges for this course.
$badgesids = [];
$sql = "SELECT DISTINCT b.id from {badge} b
WHERE b.courseid = :courseid AND $conditions";
@ -888,12 +889,12 @@ class badgeinfo {
foreach (studyitem::find_studyline_children($line) as $item) {
if ($item->type() == studyitem::COURSE && $item->courseid()) {
$courseid = $item->courseid();
$relatedbadges = badgeinfo::find_badges_by_course_relation($courseid, $search, $active);
$relatedbadges = self::find_badges_by_course_relation($courseid, $search, $active);
foreach ($relatedbadges as $id) {
$badgeids[] = $id;
}
if ($includecoursebadges) {
$coursebadges = badgeinfo::find_course_badges($courseid, $search, $active);
$coursebadges = self::find_course_badges($courseid, $search, $active);
foreach ($coursebadges as $id) {
$badgeids[] = $id;
}
@ -936,7 +937,7 @@ class badgeinfo {
$basesqlparams = array_merge($basesqlparams, $inparams);
}
// Get all course badges for this course
// Get all course badges for this course.
$sql = "SELECT DISTINCT b.id from {badge} b
WHERE $conditions";

View File

@ -122,7 +122,7 @@ class cascadecohortsync {
$records = $DB->get_records("enrol", $searchparams);
foreach ($records as $instance) {
if (!empty($instance->customtext4)) {
// Only add to the list if customtext4 is not empty
// Only add to the list if customtext4 is not empty.
$list[] = $instance;
}
}
@ -215,7 +215,8 @@ class cascadecohortsync {
}
}
/* 2: Remove the cohort sync for this studyplan for this line if it exists, since this course should not have cohort sync form this studyplan
/* 2: Remove the cohort sync for this studyplan for this line if it exists,
since this course should not have cohort sync form this studyplan
Then - if no one uses the link anymore, deactivate it...
This does not remove the sync from courses that are unlinked from a studplan.
But maybe we do not want that anyway, since it is generally a good idea to keep student
@ -262,7 +263,6 @@ class cascadecohortsync {
// It already exists.
// Check if this studyplan is already referenced in customtext4 in json format.
// TODO: Check this code - Maybe add option to not remember manually added stuff .
$plans = json_decode($instance->customtext4);
if ($plans == false || !is_array(($plans))) {
// If the data was not an array (null or garbled), count it as manually added.
@ -297,7 +297,8 @@ class cascadecohortsync {
// In the customtext4 field in json format.
$instance = $DB->get_record('enrol', ['id' => $instanceid]);
$this->enrol->update_instance($instance, (object)["customtext4" => json_encode([(int)($this->studyplanid)])]);
$this->enrol->update_instance($instance, (object)[
"customtext4" => json_encode([(int)($this->studyplanid)])]);
// Successfully added a valid new instance, so now instantiate it.
// First synchronise the enrolment.

View File

@ -252,7 +252,7 @@ class corecompletioninfo {
$info = [
"conditions" => $conditions,
"aggregation" => self::aggregation_handle($this->completion->get_aggregation_method()),
"enabled" => $this->completion->is_enabled()
"enabled" => $this->completion->is_enabled(),
];
// Check if completion tracking is enabled for this course - otherwise, revert to defaults .
@ -314,7 +314,7 @@ class corecompletioninfo {
} else {
$details['criteria'] = $cm->get_formatted_name();
}
// Set title based on cm formatted name
// Set title based on cm formatted name.
$title = $cm->get_formatted_name();
// Build requirements.
$details['requirement'] = [];
@ -354,7 +354,6 @@ class corecompletioninfo {
$details = [
"type" => get_string('coursegrade', 'completion'),
"criteria" => get_string('graderequired', 'completion'),
// TODO: convert to selected representation (letter, percentage, etc).
"requirement" => get_string('graderequired', 'completion')
.": ".$displaygrade,
"status" => "",
@ -589,8 +588,6 @@ class corecompletioninfo {
* @return stdClass|null object containing 'grade' and optional 'feedback' attribute
*/
private function get_grade($cm, $userid) {
// TODO: Display grade in the way described in the course setup (with letters if needed).
$gi = grade_item::fetch(['itemtype' => 'mod',
'itemmodule' => $cm->modname,
'iteminstance' => $cm->instance,
@ -624,7 +621,6 @@ class corecompletioninfo {
* @return stdClass|null object containing 'grade' and optional 'feedback' attribute
*/
private function format_course_grade($grade) {
// TODO: Display grade in the way described in the course setup (with letters if needed).
$gi = new \grade_item(['itemtype' => 'course',
'courseid' => $this->course->id]);

View File

@ -79,6 +79,10 @@ class coursecompetencyinfo {
], "details about gradable submissions", $value);
}
/**
* Convert completion stats to web service model.
* @param object $stats Stats object
*/
protected static function completionstats($stats) {
return [
"students" => $stats->count,
@ -113,7 +117,6 @@ class coursecompetencyinfo {
"courseproficient" => new \external_value(PARAM_BOOL, 'course competency proficiency', VALUE_OPTIONAL),
"needreview" => new \external_value(PARAM_BOOL, 'waiting for review or review in progress', VALUE_OPTIONAL),
"completionstats" => static::completionstats_structure(VALUE_OPTIONAL),
"count" => new \external_value(PARAM_INT, 'number of students in stats', VALUE_OPTIONAL),
"required" => new \external_value(PARAM_BOOL, 'if required in parent competency rule', VALUE_OPTIONAL),
"points" => new \external_value(PARAM_INT, 'number of points in parent competency rule', VALUE_OPTIONAL),
"progress" => new \external_value(PARAM_INT, 'number completed child competencies/points', VALUE_OPTIONAL),
@ -121,7 +124,8 @@ class coursecompetencyinfo {
"feedback" => new \external_value(PARAM_RAW, 'feedback provided with this competency', VALUE_OPTIONAL),
];
if ($recurse) {
$struct["children"] = new \external_multiple_structure(self::competencyinfo_structure(false), 'child competencies', VALUE_OPTIONAL);
$struct["children"] = new \external_multiple_structure(
self::competencyinfo_structure(false), 'child competencies', VALUE_OPTIONAL);
}
return new \external_single_structure($struct, 'course completion info');
}
@ -162,7 +166,7 @@ class coursecompetencyinfo {
$heading = $framework->get($headingfield);
if (empty(trim($heading))) {
$heading = $framework->get('shortname'); // Fall back to shortname if heading field is empty
$heading = $framework->get('shortname'); // Fall back to shortname if heading field is empty.
}
$path = [[
'id' => $framework->get('id'),
@ -174,7 +178,7 @@ class coursecompetencyinfo {
$competencypath[] = $c->get('shortname');
$heading = $c->get($headingfield);
if (empty(trim($heading))) {
$heading = $c->get('shortname'); // Fall back to shortname if heading field is empty
$heading = $c->get('shortname'); // Fall back to shortname if heading field is empty.
}
$path[] = [
'id' => $c->get('id'),
@ -186,7 +190,7 @@ class coursecompetencyinfo {
$heading = $competency->get($headingfield);
if (empty(trim($heading))) {
$heading = $competency->get('shortname'); // Fall back to shortname if heading field is empty
$heading = $competency->get('shortname'); // Fall back to shortname if heading field is empty.
}
$path[] = [
'id' => $competency->get('id'),
@ -197,7 +201,7 @@ class coursecompetencyinfo {
$title = $competency->get($displayfield);
if (empty(trim($title))) {
$title = $competency->get('shortname'); // Fall back to shortname if heading field is empty
$title = $competency->get('shortname'); // Fall back to shortname if heading field is empty.
}
$model = [
'id' => $competency->get('id'),
@ -262,7 +266,7 @@ class coursecompetencyinfo {
$ruleconfig = json_decode($ruleconfig);
$points = $ruleconfig->base->points;
// Make a nice map of the competency rule config
// Make a nice map of the competency rule config.
$crlist = [];
foreach ($ruleconfig->competencies as $cr) {
$crlist[$cr->id] = $cr;
@ -272,7 +276,7 @@ class coursecompetencyinfo {
$ci["rule"] = $ruletext;
}
// get one level of children
// Get one level of children.
$dids = competency::get_descendants_ids($c);
if (count($dids) > 0) {
$children = [];
@ -355,7 +359,7 @@ class coursecompetencyinfo {
$ruleconfig = json_decode($ruleconfig);
$pointsreq = $ruleconfig->base->points;
$points = 0;
// Make a nice map of the competency rule config
// Make a nice map of the competency rule config.
$crlist = [];
foreach ($ruleconfig->competencies as $cr) {
$crlist[$cr->id] = $cr;
@ -363,7 +367,7 @@ class coursecompetencyinfo {
}
// get one level of children
// Get one level of children.
$dids = competency::get_descendants_ids($c);
if (count($dids) > 0) {
$dcount = 0;
@ -442,6 +446,11 @@ class coursecompetencyinfo {
return $list;
}
/**
* Determine proficiency stats
* @param object $competency
* @param array $studentlist
*/
protected function proficiency_stats($competency, $studentlist) {
$r = new \stdClass();
$r->count = 0;
@ -535,7 +544,9 @@ class coursecompetencyinfo {
$ucc = self::get_user_competency_in_course($this->course->id, $userid, $competencyid);
$r->courseproficient = $ucc->get('proficiency');
$r->coursegrade = $scale->get_nearest_item($ucc->get('grade'));
} catch (\Exception $x) {}
} catch (\Exception $x) {
$ucc = null;
}
return $r;
}
@ -552,10 +563,10 @@ class coursecompetencyinfo {
$competencyid = $competency->get('id');
$uc = self::get_user_competency($userid, $competencyid);
// Get evidences and sort by creation date (newest first)
// Get evidences and sort by creation date (newest first).
$evidence = evidence::get_records_for_usercompetency($uc->get('id'), \context_system::instance(), 'timecreated', "DESC");
// Get the first valid note and return it;
// Get the first valid note and return it.
foreach ($evidence as $e) {
if (in_array($e->get('action'), [evidence::ACTION_OVERRIDE, evidence::ACTION_COMPLETE])) {
return $e->get('note');
@ -575,13 +586,15 @@ class coursecompetencyinfo {
public static function require_competency(int $competencyid, int $itemid, bool $required) {
global $DB;
$item = studyitem::find_by_id($itemid);
// Make sure conditions are properly configured;
// Make sure conditions are properly configured.
$conditions = [];
try {
$conditions = json_decode($item->conditions(), true);
} catch (\Exception $x) {}
} catch (\Exception $x) {
$conditions = [];
}
// Make sure the competencied field exists
// Make sure the competencied field exists.
if (!isset($conditions["competencies"]) || !is_array($conditions["competencies"])) {
$conditions["competencies"] = [];
}
@ -595,7 +608,7 @@ class coursecompetencyinfo {
$conditions["competencies"][$competencyid]["required"] = boolval($required);
}
// Store conditions;
// Store conditions.
$item->edit(["conditions" => json_encode($conditions)]);
return success::success();
}
@ -609,9 +622,11 @@ class coursecompetencyinfo {
$conditions = [];
try {
$conditions = json_decode($this->studyitem->conditions(), true);
} catch (\Exception $x) {}
} catch (\Exception $x) {
$conditions = [];
}
// Make sure the competencied field exists
// Make sure the competencied field exists.
if (isset($conditions["competencies"])
&& is_array($conditions["competencies"])
&& isset($conditions["competencies"][$competency->get("id")])

View File

@ -267,7 +267,7 @@ class courseinfo {
'fullname' => $this->course->fullname,
'shortname' => $this->course->shortname,
'displayname' => $this->displayname(),
'context' => $contextinfo->model()
'context' => $contextinfo->model(),
];
return $info;
@ -524,11 +524,15 @@ class courseinfo {
return $list;
}
/**
* Localize title for extra field
* @param string $field
*/
protected static function extrafields_localize_title($field) {
$lang = trim(current_language());
$lines = explode("\n", $field);
$title = "";
$fallback = ""; // Fallback to first title
$fallback = ""; // Fallback to first title.
foreach ($lines as $l) {
$parts = explode("|", $l, 2);
if (count($parts) > 0) {
@ -537,7 +541,7 @@ class courseinfo {
$fallback = $parts[0];
}
if (count($parts) == 1 && empty($title)) {
// Set line without language as default if no localized line found
// Set line without language as default if no localized line found.
$title = trim($parts[0]);
} else if (trim($parts[1]) == $lang) {
return trim($parts[0]);
@ -558,13 +562,13 @@ class courseinfo {
if ($fieldname == "description") {
// Process embedded files.
$value = \file_rewrite_pluginfile_urls(
// The description content
// The description content.
$this->course()->summary,
// The pluginfile URL which will serve the request.
'pluginfile.php',
// The combination of contextid / component / filearea / itemid
// form the virtual bucket that file are stored in.
$this->coursecontext->id, // System instance is always used for this
// The combination of contextid / component / filearea / itemid.
// Form the virtual bucket that file are stored in.
$this->coursecontext->id, // System instance is always used for this.
'course',
'summary',
''
@ -600,7 +604,7 @@ class courseinfo {
$raw = $data->get_value();
$type = $field->get('type');
// Only show if visibility is "Everyone" or ("Teachers" and in teacher view )
// Only show if visibility is "Everyone" or ("Teachers" and in teacher view ).
if ($visibility > 0 && ($visibility == 2 || $includeteachervisible)) {
if ($type == "date") {
// Date should be converted to YYYY-MM-DD so the javascript can properly format it.

View File

@ -51,7 +51,8 @@ class courseservice extends \external_api {
/**
* Get the topmost categories for the specicied user.
* Most of the work is offloaded to an SQL query in the interest of speed, but moodle functions are used to double check access permissions.
* Most of the work is offloaded to an SQL query in the interest of speed,
* but moodle functions are used to double check access permissions.
* @param int $userid Id of the user
* @return array of core_course_category
*/
@ -65,7 +66,7 @@ class courseservice extends \external_api {
if (has_capability($capability, \context_system::instance(), $userid)) {
if ($capability == 'moodle/category:viewcourselist') {
// We are now just looking for the visible main level categories.
// Add all categories of depth = 1;
// Add all categories of depth = 1.
$rs = $DB->get_recordset("course_categories", ["depth" => 1], 'sortorder');
foreach ($rs as $rcat) {
// Get the category, and double check if the category is visible to the current user.
@ -79,7 +80,7 @@ class courseservice extends \external_api {
$rs->close();
} else {
// Context is system, but system may not be visible
// Context is system, but system may not be visible.
// Return the top visible categories for this user.
// Recurses only once.
return self::user_tops($userid);
@ -88,10 +89,10 @@ class courseservice extends \external_api {
// We need to search for the permissions on an individial context level.
// This part finds all top categories with a certain permission that are also visible for the user.
$sql = "SELECT UNIQUE ctx.* FROM {context} AS ctx
INNER JOIN {role_assignments} AS ra ON ra.contextid = ctx.id
INNER JOIN {role_capabilities} AS rc ON ra.roleid = rc.roleid
LEFT JOIN {course_categories} AS cat ON ctx.instanceid = cat.id
$sql = "SELECT UNIQUE ctx.* FROM {context} ctx
INNER JOIN {role_assignments} ra ON ra.contextid = ctx.id
INNER JOIN {role_capabilities} rc ON ra.roleid = rc.roleid
LEFT JOIN {course_categories} cat ON ctx.instanceid = cat.id
WHERE ( ctx.contextlevel = :ctxl_coursecat )
AND ra.userid = :userid AND rc.capability = :capability
ORDER BY ctx.depth ASC, cat.sortorder ASC";
@ -133,7 +134,6 @@ class courseservice extends \external_api {
}
}
}
//$recordset->close();
}
return $tops;
@ -152,10 +152,10 @@ class courseservice extends \external_api {
$tops = [];
$pathlike = $DB->sql_like('ctx.path', ':pathsearch');
$sql = "SELECT UNIQUE ctx.* FROM {context} AS ctx
INNER JOIN {role_assignments} AS ra ON ra.contextid = ctx.id
INNER JOIN {role_capabilities} AS rc ON ra.roleid = rc.roleid
LEFT JOIN {course_categories} AS cat ON ctx.instanceid = cat.id
$sql = "SELECT UNIQUE ctx.* FROM {context} ctx
INNER JOIN {role_assignments} ra ON ra.contextid = ctx.id
INNER JOIN {role_capabilities} rc ON ra.roleid = rc.roleid
LEFT JOIN {course_categories} cat ON ctx.instanceid = cat.id
WHERE ( ctx.contextlevel = :ctxl_coursecat )
AND ra.userid = :userid AND rc.capability = :capability
AND {$pathlike}
@ -255,7 +255,7 @@ class courseservice extends \external_api {
throw new moodle_exception("error:nocategoriesvisible", "local_treestudyplan");
}
} else {
if (\get_config("local_treestudyplan", "limitcourselist")) { // TODO: Insert config setting here
if (\get_config("local_treestudyplan", "limitcourselist")) {
$studyplan = studyplan::find_by_id($studyplanid);
$context = $studyplan->context();
if ($context->contextlevel == \CONTEXT_SYSTEM) {
@ -446,7 +446,7 @@ class courseservice extends \external_api {
$cats = [];
// If the reference context id is not in the list, push it there if the user has proper permissions in that context
// If the reference context id is not in the list, push it there if the user has proper permissions in that context.
if ($refctxid > 1 && !in_array($refctxid, $contextids)) {
try {
// Get the context.
@ -456,7 +456,7 @@ class courseservice extends \external_api {
$insertctxs[] = $refctx;
}
} catch (\dml_missing_record_exception $x) {
// ignore context
$refctx = null;
}
}
@ -504,7 +504,7 @@ class courseservice extends \external_api {
}
}
} catch (\dml_missing_record_exception $x) {
// ignore context
$ctx = null;
}
}
@ -539,7 +539,6 @@ class courseservice extends \external_api {
return new \external_function_parameters( [
"gradeitemid" => new \external_value(PARAM_INT, 'Grade item ID to scan progress for', VALUE_DEFAULT),
"studyplanid" => new \external_value(PARAM_INT, 'Study plan id to check progress in', VALUE_DEFAULT),
]);
}

View File

@ -19,16 +19,16 @@
* @copyright 2023 P.M. Kuipers
* @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace local_treestudyplan;
use Exception;
use JsonException;
defined('MOODLE_INTERNAL') || die();
/**
* Crude tool to aid in debugging
*/
class debug {
/**
* Dump an object in json format to debug log
* @param $object Object to dump
* @param $filename File to write to
* @return any The object
@ -42,9 +42,9 @@ class debug {
fwrite($f, $json."\n");
} catch (JsonException $x) {
fwrite($f, "Error processing json: ". $x->getMessage()."\n");
fwrite($f, "Print_r dump: \n".print_r($object, true)."\n");
}
} catch (\Exception $x) {
$f = null;
} finally {
fclose($f);
@ -52,22 +52,10 @@ class debug {
return $object;
}
/**
* @param $object Object to dump
* @param $filename File to write to
* @return any The object
*/
public static function &print_r(&$object, $filename="/tmp/debug.log") {
try {
$f = fopen($filename, "a+");
fwrite($f, "Print_r dump: \n".print_r($object, true)."\n");
} catch (\Exception $x) {
} finally {
fclose($f);
}
}
/**
* Write text to debug log
* @param $object Object to dump
* @param $filename File to write to
* @return any The object
@ -77,6 +65,21 @@ class debug {
$f = fopen($filename, "a+");
fwrite($f, $text."\n");
} catch (\Exception $x) {
$f = null;
} finally {
fclose($f);
}
}
/**
* Write nothing to the file. (Used to satisfy the code checker)
* @param $filename File to write to
* @return any The object
*/
public static function void($filename="/tmp/debug.log") {
try {
$f = fopen($filename, "a+");
} catch (\Exception $x) {
$f = null;
} finally {
fclose($f);
}

View File

@ -1,8 +1,31 @@
<?php
// This file is part of the Studyplan plugin for Moodle
//
// 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 <https://www.gnu.org/licenses/>.
/**
* No file description
* @package local_treestudyplan
* @copyright 2023 P.M. Kuipers
* @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace local_treestudyplan\form;
defined('MOODLE_INTERNAL') || die();
require_once($CFG->dirroot."/lib/formslib.php");
/**
* Base class for moodle forms used in this plugin.
*/
abstract class formbase extends \moodleform {
/**
@ -13,9 +36,9 @@ abstract class formbase extends \moodleform {
*/
public function __construct($params, $ajaxformdata=null) {
$customdata = static::init_customdata($params);
if (static::check_security($customdata) !== false) {
if (static::check_security($customdata) ) {
parent::__construct(null, (array)$customdata, 'post', '', null, true, $ajaxformdata);
// Initialize the initial data based on the parameter validation
// Initialize the initial data based on the parameter validation.
$this->set_data($this->init_formdata((object)$this->_customdata));
} else {
throw new \moodle_exception('accessexception', 'core');
@ -48,7 +71,9 @@ abstract class formbase extends \moodleform {
* @return bool True if security validation passes.
* @throws \moodle_exception if access denied for a specific reason.
*/
abstract public static function check_security(object $customdata);
public static function check_security(object $customdata) {
return true;
}
/**
* Process the submission and perform necessary actions
@ -72,5 +97,4 @@ abstract class formbase extends \moodleform {
throw new \moodle_exception('no_form_data', 'local_treestudyplan', '', null, $data);
}
}
}

View File

@ -1,4 +1,24 @@
<?php
// This file is part of the Studyplan plugin for Moodle
//
// 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 <https://www.gnu.org/licenses/>.
/**
* No file description
* @package local_treestudyplan
* @copyright 2023 P.M. Kuipers
* @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace local_treestudyplan\form;
defined('MOODLE_INTERNAL') || die();
@ -48,31 +68,19 @@ class studyplan_editform extends formbase {
'subdirs' => true,
'maxfiles' => 20,
'maxbytes' => 20 * 1024 * 1024,
'context' => \context_system::instance(), // Keep the files in system context
'context' => \context_system::instance(), // Keep the files in system context.
];
$customdata->fileoptions = [
'subdirs' => 0,
'maxbytes' => 10 * 1024 * 1024, // Max 10MiB should be sufficient for a picture.
'areamaxbytes' => 10485760,
'maxfiles' => 1, // Just one file
'maxfiles' => 1, // Just one file.
'accepted_types' => ['.jpg', '.png', '.jpeg', '.svg', '.svgz'],
'return_types' => \FILE_INTERNAL | \FILE_EXTERNAL,
];
return $customdata;
}
/**
* Validate security access for this form based on the customdata generated by init_customdata
* Return true if validation passes, false or throw an exception if it does not.
*
* @param object $customdata The customdata for this form
* @return bool True if security validation passes.
* @throws \moodle_exception if access denied for a specific reason.
*/
public static function check_security(object $customdata) {
/*webservicehelper::require_capabilities(self::CAP_EDIT, $customdata->context); */
}
/**
* Generate form data from parameters
* Also validate parameters and access permissions here
@ -109,7 +117,7 @@ class studyplan_editform extends formbase {
$agcfg = json_decode($customdata->plan->aggregator()->config_string(), true);
}
// Prepare the editor
// Prepare the editor.
$entry = file_prepare_standard_editor( $entry,
'description',
$customdata->editoroptions,
@ -119,15 +127,15 @@ class studyplan_editform extends formbase {
($customdata->create) ? null : $customdata->plan->id()
);
// Prepare file area for the icon
// Prepare file area for the icon.
// Get an unused draft itemid which will be used for this form.
$draftitemid = file_get_submitted_draft_itemid('icon');
file_prepare_draft_area(
// The $draftitemid is the target location.
$draftitemid,
// The combination of contextid / component / filearea / itemid
// form the virtual bucket that files are currently stored in
// and will be copied from.
// The combination of contextid / component / filearea / itemid.
// Form the virtual bucket that files are currently stored in.
// And will be copied from.
\context_system::instance()->id,
'local_treestudyplan',
'icon',
@ -154,10 +162,10 @@ class studyplan_editform extends formbase {
$mform = $this->_form;
$customdata = (object)$this->_customdata;
// Register integer type
text_integer::Register();
// Register integer type.
text_integer::register();
// Define the form
// Define the form.
$field = 'name';
$mform->addElement('text', $field,
get_string('studyplan_name', 'local_treestudyplan'),
@ -197,7 +205,7 @@ class studyplan_editform extends formbase {
if ($customdata->create) {
$timeless = \get_config("local_treestudyplan", "timelessperiods");
if (!$timeless) {
// Only add these fields if a new studyplan is created, to easily initialize the first page
// Only add these fields if a new studyplan is created, to easily initialize the first page.
$field = 'startdate';
$mform->addElement('date_selector', $field,
get_string('studyplan_startdate', 'local_treestudyplan'),
@ -219,7 +227,7 @@ class studyplan_editform extends formbase {
$mform->addRule($field, null, 'required', null, 'client');
} else {
// These fields are only relevant if the studyplan is edited
// These fields are only relevant if the studyplan is edited.
$field = 'suspended';
$mform->addElement('advcheckbox', $field,
get_string('studyplan_suspend', 'local_treestudyplan'),
@ -239,7 +247,7 @@ class studyplan_editform extends formbase {
$aggregators = [];
foreach (aggregator::list_model() as $a) {
// Add method only if not deprecated or currently used
// Add method only if not deprecated or currently used.
if ($customdata->simplemodel['aggregation'] == $a['id'] || !($a['deprecated'])) {
$aggregators[$a['id']] = $a['name'];
}
@ -307,7 +315,8 @@ class studyplan_editform extends formbase {
$customdata = (object)$this->_customdata;
// Add aggregation configs to entry.
$agcfg = json_decode(aggregator::create($entry->aggregation, "")->config_string(), true); // Retrieve default config string from selected aggregation method
// Retrieve default config string from selected aggregation method.
$agcfg = json_decode(aggregator::create($entry->aggregation, "")->config_string(), true);
foreach ($agcfg as $key => $val) {
$entrykey = $entry->aggregation."_".$key;
$agcfg[$key] = $entry->$entrykey;
@ -316,7 +325,7 @@ class studyplan_editform extends formbase {
if ($customdata->create) {
// Use our own abstraction to create the record, so caches are maintained
// Use our own abstraction to create the record, so caches are maintained.
$plan = studyplan::add(['name' => $entry->name,
'shortname' => $entry->shortname,
'idnumber' => $entry->idnumber,
@ -327,7 +336,7 @@ class studyplan_editform extends formbase {
'enddate' => date("Y-m-d", $entry->enddate),
'periods' => $entry->periods,
]);
// Process the provided files in the description editor
// Process the provided files in the description editor.
$entry = file_postupdate_standard_editor($entry,
'description',
$customdata->editoroptions,
@ -335,7 +344,7 @@ class studyplan_editform extends formbase {
'local_treestudyplan',
'studyplan',
$plan->id());
// Update the description
// Update the description.
$plan->edit([
'description' => $entry->description,
'descriptionformat' => $entry->descriptionformat,
@ -343,7 +352,7 @@ class studyplan_editform extends formbase {
} else {
$plan = $customdata->plan;
// Process the provided files in the description editor
// Process the provided files in the description editor.
$entry = file_postupdate_standard_editor($entry,
'description',
$customdata->editoroptions,
@ -352,7 +361,7 @@ class studyplan_editform extends formbase {
'studyplan',
$plan->id());
// Use our own abstraction to update the record, so caches are maintained
// Use our own abstraction to update the record, so caches are maintained.
$plan->edit(['name' => $entry->name,
'shortname' => $entry->shortname,
'idnumber' => $entry->idnumber,
@ -370,8 +379,8 @@ class studyplan_editform extends formbase {
file_save_draft_area_files(
// The $entry->icon property contains the itemid of the draft file area.
$entry->icon,
// The combination of contextid / component / filearea / itemid
// form the virtual bucket that file are stored in.
// The combination of contextid / component / filearea / itemid.
// Form the virtual bucket that file are stored in.
\context_system::instance()->id,
'local_treestudyplan',
'icon',
@ -387,5 +396,4 @@ class studyplan_editform extends formbase {
debug::dump($response);
return $response;
}
}

View File

@ -1,12 +1,30 @@
<?php
// This file is part of the Studyplan plugin for Moodle
//
// 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 <https://www.gnu.org/licenses/>.
/**
* No file description
* @package local_treestudyplan
* @copyright 2023 P.M. Kuipers
* @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace local_treestudyplan\form;
defined('MOODLE_INTERNAL') || die();
require_once($CFG->dirroot.'/repository/lib.php');
use local_treestudyplan\aggregator;
use local_treestudyplan\studyplan;
use local_treestudyplan\premium;
use local_treestudyplan\contextinfo;
use local_treestudyplan\studyplanservice;
use local_treestudyplan\courseservice;
@ -38,18 +56,6 @@ class studyplan_fromtemplateform extends formbase {
return $customdata;
}
/**
* Validate security access for this form based on the customdata generated by init_customdata
* Return true if validation passes, false or throw an exception if it does not.
*
* @param object $customdata The customdata for this form
* @return bool True if security validation passes.
* @throws \moodle_exception if access denied for a specific reason.
*/
public static function check_security(object $customdata) {
/*webservicehelper::require_capabilities(self::CAP_EDIT, $customdata->context); */
}
/**
* Generate form data from parameters
* Also validate parameters and access permissions here
@ -87,10 +93,10 @@ class studyplan_fromtemplateform extends formbase {
$mform = $this->_form;
$customdata = (object)$this->_customdata;
// Register integer type
text_integer::Register();
// Register integer type.
text_integer::register();
// Define the form
// Define the form.
$templatelist = [];
foreach (studyplan::find_template() as $s) {
@ -137,7 +143,7 @@ class studyplan_fromtemplateform extends formbase {
$timeless = \get_config("local_treestudyplan", "timelessperiods");
if (!$timeless) {
// Only add these fields if the studyplans are timed
// Only add these fields if the studyplans are timed.
$field = 'startdate';
$mform->addElement('date_selector', $field,
get_string('studyplan_startdate', 'local_treestudyplan'),
@ -180,5 +186,4 @@ class studyplan_fromtemplateform extends formbase {
return null;
}
}
}

View File

@ -1,15 +1,32 @@
<?php
// This file is part of the Studyplan plugin for Moodle
//
// 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 <https://www.gnu.org/licenses/>.
/**
* No file description
* @package local_treestudyplan
* @copyright 2023 P.M. Kuipers
* @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace local_treestudyplan\form;
defined('MOODLE_INTERNAL') || die();
require_once($CFG->dirroot.'/repository/lib.php');
use local_treestudyplan\aggregator;
use local_treestudyplan\studyplan;
use local_treestudyplan\studyplanpage;
use local_treestudyplan\courseservice;
use local_treestudyplan\form\text_integer;
use local_treestudyplan\local\helpers\webservicehelper;
use local_treestudyplan\studyplanservice;
use moodle_exception;
use stdClass;
@ -48,31 +65,19 @@ class studyplanpage_editform extends formbase {
'subdirs' => true,
'maxfiles' => 20,
'maxbytes' => 20 * 1024 * 1024,
'context' => \context_system::instance(), // Keep the files in system context
'context' => \context_system::instance(), // Keep the files in system context.
];
$customdata->fileoptions = [
'subdirs' => 0,
'maxbytes' => 10 * 1024 * 1024, // Max 10MiB should be sufficient for a picture.
'areamaxbytes' => 10485760,
'maxfiles' => 1, // Just one file
'maxfiles' => 1, // Just one file.
'accepted_types' => ['.jpg', '.png'],
'return_types' => \FILE_INTERNAL | \FILE_EXTERNAL,
];
return $customdata;
}
/**
* Validate security access for this form based on the customdata generated by init_customdata
* Return true if validation passes, false or throw an exception if it does not.
*
* @param object $customdata The customdata for this form
* @return bool True if security validation passes.
* @throws \moodle_exception if access denied for a specific reason.
*/
public static function check_security(object $customdata) {
/*webservicehelper::require_capabilities(self::CAP_EDIT, $customdata->context); */
}
/**
* Generate form data from parameters
* Also validate parameters and access permissions here
@ -91,7 +96,7 @@ class studyplanpage_editform extends formbase {
$entry = new stdClass;
$entry->studyplan_id = $plan->id();
// By default, make the start date of a new page, continue 1 day after the last page's start date;
// By default, make the start date of a new page, continue 1 day after the last page's start date.
$otherpages = $plan->pages();
if (count($otherpages) > 0) {
$lastpage = $otherpages[count($otherpages) - 1];
@ -100,12 +105,12 @@ class studyplanpage_editform extends formbase {
if ($lastpage->enddate(false) == null) {
$entry->startdate = $lastpage->startdate()->add(new \DateInterval("P1Y"))->format("U");
} else {
// Otherwise, propose 1 day after the last page's end date
// Otherwise, propose 1 day after the last page's end date.
$entry->startdate = $lastpage->enddate()->add(new \DateInterval("P1D"))->format("U");
}
} else {
// Determine the next august 1st for default value purposes. Only if no other page is available
// Determine the next august 1st for default value purposes. Only if no other page is available.
$august = strtotime("first day of august this year");
if ($august < time()) {
$august = strtotime("first day of august next year");
@ -114,7 +119,7 @@ class studyplanpage_editform extends formbase {
$entry->startdate = $august;
}
$entry->enddate = $entry->startdate + (364*24*60*60); // Not bothering about leap years here.
$entry->enddate = $entry->startdate + (364 * 24 * 60 * 60); // Not bothering with leap years here.
$entry->periods = 4;
} else {
$entry = $DB->get_record(studyplanpage::TABLE, ['id' => $customdata->page->id()]);
@ -122,7 +127,7 @@ class studyplanpage_editform extends formbase {
$entry->enddate = strtotime($entry->enddate);
}
// Prepare the editor
// Prepare the editor.
$entry = file_prepare_standard_editor( $entry,
'description',
$customdata->editoroptions,
@ -143,10 +148,10 @@ class studyplanpage_editform extends formbase {
$mform = $this->_form;
$customdata = (object)$this->_customdata;
// Register integer type
text_integer::Register();
// Register integer type.
text_integer::register();
// Define the form
// Define the form.
$field = 'fullname';
$mform->addElement('text', $field,
get_string('studyplan_name', 'local_treestudyplan'),
@ -201,7 +206,7 @@ class studyplanpage_editform extends formbase {
if ($customdata->create) {
// Use our own abstraction to update the record, so caches are maintained
// Use our own abstraction to update the record, so caches are maintained.
$page = studyplanpage::add(['fullname' => $entry->fullname,
'shortname' => $entry->shortname,
'startdate' => date("Y-m-d", $entry->startdate),
@ -209,7 +214,7 @@ class studyplanpage_editform extends formbase {
'periods' => $entry->periods,
'studyplan_id' => $customdata->plan->id(),
]);
// Process the provided files in the description editor
// Process the provided files in the description editor.
$entry = file_postupdate_standard_editor($entry,
'description',
$customdata->editoroptions,
@ -217,7 +222,7 @@ class studyplanpage_editform extends formbase {
'local_treestudyplan',
'studyplanpage',
$page->id());
// Update the description
// Update the description.
$page->edit([
'description' => $entry->description,
'descriptionformat' => $entry->descriptionformat,
@ -225,7 +230,7 @@ class studyplanpage_editform extends formbase {
} else {
$page = $customdata->page;
// Process the provided files in the description editor
// Process the provided files in the description editor.
$entry = file_postupdate_standard_editor($entry,
'description',
$customdata->editoroptions,
@ -234,7 +239,7 @@ class studyplanpage_editform extends formbase {
'studyplanpage',
$page->id());
// Use our own abstraction to update the record, so caches are maintained
// Use our own abstraction to update the record, so caches are maintained.
$page->edit(['fullname' => $entry->fullname,
'shortname' => $entry->shortname,
'description' => $entry->description,
@ -251,5 +256,4 @@ class studyplanpage_editform extends formbase {
*/
return studyplanservice::clean_returnvalue(studyplanpage::editor_structure(), $page->editor_model());
}
}

View File

@ -1,4 +1,25 @@
<?php
// This file is part of the Studyplan plugin for Moodle
//
// 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 <https://www.gnu.org/licenses/>.
/**
* No file description
* @package local_treestudyplan
* @copyright 2023 P.M. Kuipers
* @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace local_treestudyplan\form;
use MoodleQuickForm_text;
use MoodleQuickForm;
@ -7,6 +28,9 @@ defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->libdir . "/form/text.php");
/**
* Class to handle integer only form field.
*/
class text_integer extends MoodleQuickForm_text {
/**
* Accepts a renderer
@ -32,7 +56,7 @@ class text_integer extends MoodleQuickForm_text {
$unsigned = (isset($this->_attributes['unsigned']) && $this->_attributes['unsigned']);
$nonzero = (isset($this->_attributes['nonzero']) && $this->_attributes['nonzero']);
$context = array(
$context = [
'element' => $elementcontext,
'label' => $label,
'unsigned' => ($unsigned) ? true : false ,
@ -41,7 +65,7 @@ class text_integer extends MoodleQuickForm_text {
'advanced' => $advanced,
'helpbutton' => $helpbutton,
'error' => $error,
);
];
$html = $OUTPUT->render_from_template('local_treestudyplan/form/element_text_integer', $context);
if ($renderer->_inGroup) {
$this->_groupElementTemplate = $html;
@ -59,7 +83,10 @@ class text_integer extends MoodleQuickForm_text {
$renderer->_html .= $html;
}
public static function Register() {
/**
* Register the element
*/
public static function register() {
global $CFG;
MoodleQuickForm::registerElementType(
// The custom element is named `course_competency_rule`.

View File

@ -337,7 +337,7 @@ class gradeinfo {
public function user_model($userid) {
global $DB;
$grade = $this->gradeitem->get_final($userid);
// Format grade for proper display
// Format grade for proper display.
if (is_object($grade)) {
$finalgrade = \grade_format_gradevalue($grade->finalgrade, $this->gradeitem, true, null, 1);
} else {

View File

@ -165,7 +165,7 @@ class competency_aggregator extends \local_treestudyplan\aggregator {
* @return int Aggregated completion as completion class constant
*/
public function aggregate_course(courseinfo $courseinfo, studyitem $studyitem, $userid) {
// Retrieve the course competencies
// Retrieve the course competencies.
$course = $courseinfo->course();
$cci = new coursecompetencyinfo($course, $studyitem);
@ -196,7 +196,7 @@ class competency_aggregator extends \local_treestudyplan\aggregator {
}
// Determine minimum for
// Determine minimum for.
$limit = $this->cfg()->thresh_completed * $count;
$coursefinished = ($course->enddate) ? ($course->enddate < time()) : false;

View File

@ -121,7 +121,7 @@ class core_aggregator extends \local_treestudyplan\aggregator {
if ($completion->is_enabled() && $completion->is_tracked_user($userid)) {
if ($completion->is_course_complete($userid)) {
// Completed is completed
// Completed is completed.
return completion::COMPLETED;
} else {
// Check if the course is over or not, if it is over, display failed.

View File

@ -93,7 +93,6 @@ class gradegenerator {
"Praesent nec risus vestibulum quam venenatis tempor.",
"Nullam rhoncus ex a quam egestas, eu auctor enim lobortis.",
"Nam luctus ante id lacus scelerisque, quis blandit ante elementum.",
];
/**
@ -289,21 +288,21 @@ class gradegenerator {
$r = (object)[
"gi" => $g,
"grade" => $gi->grademin + $grade,
"fb" => ($grade > 0) ? $gr->fb : ""
"fb" => ($grade > 0) ? $gr->fb : "",
];
} else if (!$gr->result) {
// PROGRESS.
$r = (object)[
"gi" => $g,
"grade" => $gi->grademin + rand(round($range * 0.35), round($range * 0.55) - 1 ),
"fb" => $gr->fb
"fb" => $gr->fb,
];
} else {
// COMPLETED.
$r = (object)[
"gi" => $g,
"grade" => $gi->grademin + rand(round($range * 0.55) , $range ),
"fb" => $gr->fb
"fb" => $gr->fb,
];
}

View File

@ -52,14 +52,9 @@ class webservicehelper {
}
if (is_array($capability)) {
// TODO: replace this by accesslib function \has_any_capability().
foreach ($capability as $cap) {
if (has_capability($cap, $context)) {
return true;
}
}
} else if (has_capability($capability, $context)) {
return true;
return \has_any_capability($capability, $context);
} else {
return has_capability($capability, $context);
}
}

View File

@ -30,19 +30,19 @@ class randomimage {
/**
* Image width
* @var integer
* @var int
*/
private $width;
/**
* Image height
* @var integer
* @var int
*/
private $height;
/**
* Number of shapes to draw
* @var integer
* @var int
*/
private $shapes;
@ -66,7 +66,7 @@ class randomimage {
IMG_ARC_PIE,
IMG_ARC_CHORD,
IMG_ARC_EDGED,
IMG_ARC_NOFILL
IMG_ARC_NOFILL,
];
/**
@ -75,11 +75,12 @@ class randomimage {
* @param integer $maxpts Max number of point to use
* @return void
*/
private function random_polygon($im, Int $maxpts = 20)
{
private function random_polygon($im, Int $maxpts = 20) {
$color = imagecolorallocatealpha($im, ...$this->random_color_alpha());
$pts = $this->random_pts(\random_int(3, $maxpts));
imagefilledpolygon($im, $pts, $color);
$numpoints = \random_int(3, $maxpts);
$pts = $this->random_pts($numpoints);
// Remove $numpoints parameter when php7.4 is no longer supported (param is deprecated in >8.1).
imagefilledpolygon($im, $pts, $numpoints, $color);
}
/**
@ -87,8 +88,7 @@ class randomimage {
* @param \GdImage $im The image resource
* @return void
*/
private function random_arc($im)
{
private function random_arc($im) {
$cx = \random_int(0, $this->width);
$cy = \random_int(0, $this->height);
$w = \random_int(0, $this->width);
@ -104,13 +104,12 @@ class randomimage {
* Generates an array of random alpha color values.
* @return Array [r, g, b, a]
*/
private function random_color_alpha(): Array
{
private function random_color_alpha(): Array {
return [
\random_int(0, 255),
\random_int(0, 255),
\random_int(0, 255),
\random_int(0, 127)
\random_int(0, 127),
];
}
@ -119,8 +118,7 @@ class randomimage {
* @param integer $length Number of sets of points to generate
* @return Array The resulting array of points
*/
private function random_pts($length): Array
{
private function random_pts($length): Array {
$pts = [];
for ($n = 0; $n < $length; $n++) {
$pts[] = \random_int(0, $this->width);
@ -129,6 +127,12 @@ class randomimage {
return $pts;
}
/**
* Create new random image
* @param int $shapes Number of shapes
* @param int $width Width of the images generated
* @param int $height Height of the images generated
*/
public function __construct($shapes=3, $width=256, $height=256) {
$this->shapes = $shapes;
$this->width = $width;
@ -159,15 +163,20 @@ class randomimage {
imagedestroy($im);
}
/**
* Get image content as string
* @return string
*/
public function content() {
return $this->content = \file_get_contents($this->tempfile);
}
public function __destruct()
{
/**
* Destructor for random image object
*/
public function __destruct() {
if (isset($this->tempfile) && file_exists($this->tempfile)) {
unlink($this->tempfile);
}
}
}

View File

@ -117,7 +117,7 @@ class period {
$pend = $pstart + $ptime;
}
// Continue period numbers if so configured
// Continue period numbers if so configured.
if (get_config("local_treestudyplan", "continueperiodnumberingnewpage")) {
$offset = 0;
foreach (studyplanpage::find_studyplan_children($page->studyplan()) as $p) {

View File

@ -25,19 +25,22 @@ defined('MOODLE_INTERNAL') || die();
require_once($CFG->libdir.'/externallib.php');
use DateTime;
use moodle_url;
use stdClass;
/**
* Handle badge information in the same style as the other classes
*/
class premium extends \external_api {
// Toggle the variable below to enable support for premium stuff.
// If set to false, all premium features will be enabled and no premium settings panel will be visible.
/**
* Toggle the variable below to enable support for premium stuff.
* If set to false, all premium features will be enabled and no premium settings panel will be visible.
* @var bool
*/
private static $premiumsupported = false;
/**
* Certficate code
* @var string
*/
private static $premiumcrt = "-----BEGIN CERTIFICATE-----
MIIDSzCCAjMCFFlyhmKf1fN7U5lQL/dtlsyP24AQMA0GCSqGSIb3DQEBCwUAMGEx
CzAJBgNVBAYTAk5MMRYwFAYDVQQIDA1Ob29yZC1Ib2xsYW5kMRowGAYDVQQKDBFN
@ -59,27 +62,38 @@ n8FqBInMBhGu1uz0Qeo09ke0RHRnghP9EXfig/veMegASZeEhFqmS2Bdiy6gqeZ5
Klc5I28bGbvxIV5pnL6ZSjHEDp2WreM8HB0XFJwU+Q==
-----END CERTIFICATE-----";
/**
* Cached status
* @var object
*/
private static $cachedpremiumstatus = null;
/**
* Check if premium is supported
* @return bool
*/
public static function supported() {
return self::$premiumsupported;
}
/**
* Decrypt encrypted data
* @param string $encrypted Encrypted data
*/
private static function decrypt($encrypted) {
// Get the public key.
$key = \openssl_get_publickey(self::$premiumcrt);
if ($key === false) {
throw new \ValueError("Error parsing public key data)");
throw new \ParseError("Error parsing public key data)");
}
// Determine the key size.
$keysize = \openssl_pkey_get_details($key)["bits"];
//
$blocksize = ($keysize / 8); // Bits / 8. Whether padded or not.
// Decode data in
// Decode data in.
$b64 = \base64_decode($encrypted);
if ($b64 === false) {
throw new \ValueError("Error in base64 decoding");
throw new \ParseError("Error in base64 decoding");
}
$data = \str_split($b64, $blocksize);
@ -89,48 +103,53 @@ Klc5I28bGbvxIV5pnL6ZSjHEDp2WreM8HB0XFJwU+Q==
if (\openssl_public_decrypt($chunk, $dchunk, $key, \OPENSSL_PKCS1_PADDING)) {
$decrypted .= $dchunk;
} else {
throw new \ValueError("Error decrypting chunk $i ({$blocksize} bytes)");
throw new \ParseError("Error decrypting chunk $i ({$blocksize} bytes)");
}
$i++;
}
// Deprecated in PHP 8.0 and up, but included to be compatible with 7.4.
// Wrap in a try/catch in case the function is removed in a later version.
try {
if (\PHP_MAJOR_VERSION < 8) {
\openssl_pkey_free($key);
} catch (\Exception $x) {}
}
return $decrypted;
}
/**
* Trim headers from key data
* @param string $data the key dat with headers
* @return string Key without headers
*/
private static function trim_headers($data) {
// Headers are repeated in this function for easier testing and copy-pasting into other projects.
$STARTHEADER = "----- BEGIN ACTIVATION KEY -----";
$ENDHEADER = "----- END ACTIVATION KEY -----";
$startheader = "----- BEGIN ACTIVATION KEY -----";
$endheader = "----- END ACTIVATION KEY -----";
$parts = preg_split("/\r?\n/", \trim($data));
if (count($parts) > 2) {
$start = -1;
$end = -1;
for ($i = 0; $i < count($parts); $i++) {
$p = trim(preg_replace('/\s+/u', ' ', $parts[$i])); // Make sure all unicode spaces are converted to normal spaces before comparing...
// Make sure all unicode spaces are converted to normal spaces before comparing.
$p = trim(preg_replace('/\s+/u', ' ', $parts[$i]));
if ( $p == $STARTHEADER ) {
if ($p == $startheader) {
$start = $i + 1;
}
if ($start > 0 && $p == $ENDHEADER) {
if ($start > 0 && $p == $endheader) {
$end = $i;
}
}
if ($start < 0 || $end < 0 || $end - $start <= 0) {
throw new \ValueError("Invalid activation key wrappers");
throw new \ParseError("Invalid activation key wrappers");
} else {
$keyslice = array_slice($parts, $start, $end - $start);
return implode("\n", $keyslice);
}
} else {
throw new \ValueError("Invalid activation key");
throw new \ParseError("Invalid activation key");
}
}
@ -140,7 +159,7 @@ Klc5I28bGbvxIV5pnL6ZSjHEDp2WreM8HB0XFJwU+Q==
*/
public static function enabled() {
if (self::$premiumsupported) {
$status = self::premiumStatus();
$status = self::premiumstatus();
return $status->enabled;
} else {
return true;
@ -153,7 +172,7 @@ Klc5I28bGbvxIV5pnL6ZSjHEDp2WreM8HB0XFJwU+Q==
* @return bool
*/
public static function has_intent($intent) {
$status = self::premiumStatus();
$status = self::premiumstatus();
if ($status->enabled) {
return \in_array(\strtolower($intent), $status->intents);
} else {
@ -161,6 +180,9 @@ Klc5I28bGbvxIV5pnL6ZSjHEDp2WreM8HB0XFJwU+Q==
}
}
/**
* Generate debug info.
*/
public static function debuginfo() {
$s = "<pre>";
try {
@ -172,10 +194,10 @@ Klc5I28bGbvxIV5pnL6ZSjHEDp2WreM8HB0XFJwU+Q==
$s .= $json . "\n----\n";
$decoded = \json_decode($json, false);
$s .= "Read as object:\n----\n";
$s .= print_r($decoded, true) ."\n----\n";
$status = self::premiumStatus();
$s .= \json_encode($decoded, \JSON_PRETTY_PRINT) ."\n----\n";
$status = self::premiumstatus();
$s .= "Parsed premium status block:\n----\n";
$s .= print_r($status, true) ."\n----\n";
$s .= \json_encode($status, \JSON_PRETTY_PRINT) ."\n----\n";
$s .= "Message: " . self::statusdescription() . "\n";
} else {
$s .= "Premium key empty";
@ -191,11 +213,12 @@ Klc5I28bGbvxIV5pnL6ZSjHEDp2WreM8HB0XFJwU+Q==
$s .= "</pre>";
return $s;
}
/**
* Determine, cache and retrieve premium status
* @return object
*/
protected static function premiumStatus() {
protected static function premiumstatus() {
if (!isset(self::$cachedpremiumstatus)) {
// Initialize default object.
@ -217,7 +240,7 @@ Klc5I28bGbvxIV5pnL6ZSjHEDp2WreM8HB0XFJwU+Q==
if (is_object($decoded)) {
// Copy basic data/
// Copy basic data/.
$keys = ["name", "website", "expires", "issued"];
foreach ($keys as $k) {
if (isset($decoded->$k)) {
@ -229,17 +252,19 @@ Klc5I28bGbvxIV5pnL6ZSjHEDp2WreM8HB0XFJwU+Q==
$o->intents = explode(", ", $decoded->intent);
}
// Convert dates to DateTime for
// Convert dates to DateTime for.
$now = new \DateTime();
$issuedate = new \DateTime($o->issued);
$expirydate = new \DateTime(); // Default to now
if ($o->expires == 'never') {
// If expiry date == never
// If expiry date == never.
$expirydate = new \DateTime(); // Default to now and add a year.
$expirydate->add(new \DateInterval("P1Y"));
} else {
try {
$expirydate = new \DateTime($o->expires);
} catch (\Exception $x) {}
} catch (\Exception $x) {
$expirydate = new \DateTime(); // Default to now.
}
}
if (\in_array('treestudyplan', $o->intents)
@ -263,7 +288,7 @@ Klc5I28bGbvxIV5pnL6ZSjHEDp2WreM8HB0XFJwU+Q==
}
}
}
} catch (\ValueError $x) {
} catch (\ParseError $x) {
$o->status = \get_string("premium:invalidactivationcontent", "local_treestudyplan");
}
self::$cachedpremiumstatus = $o;
@ -291,13 +316,13 @@ Klc5I28bGbvxIV5pnL6ZSjHEDp2WreM8HB0XFJwU+Q==
$keyurl = (object)\parse_url($key);
$siteurl = (object)\parse_url($site);
// No match if host is empty on key or site
// No match if host is empty on key or site.
if (empty($keyurl->host) || empty($siteurl->host)) {
return false;
}
if ($keyurl->host == "*") {
// * matches all
// Value * matches all.
return true;
}
@ -306,16 +331,20 @@ Klc5I28bGbvxIV5pnL6ZSjHEDp2WreM8HB0XFJwU+Q==
$siteparts = \array_reverse(\explode(".", $siteurl->host));
// Trim starting www from both parts, since site.domain and www.site.domain should be treated as the same.
if (($x = \array_pop($keyparts)) != "www") {\array_push($keyparts, $x);}
if (($x = \array_pop($siteparts)) != "www") {\array_push($siteparts, $x);}
if (($x = \array_pop($keyparts)) != "www") {
\array_push($keyparts, $x);
}
if (($x = \array_pop($siteparts)) != "www") {
\array_push($siteparts, $x);
}
for ($i = 0; $i < count($keyparts); $i++) {
// No match if the site does not have a part, but the key does. Unless the key part is *
// No match if the site does not have a part, but the key does. Unless the key part is .
if (!isset($siteparts[$i])) {
if ($keyparts[$i] != "*") {
return false;
} else {
$i++; //increment $i by one before break, to make sure the comparison following this loop holds.
$i++; // Increment $i by one before break, to make sure the comparison following this loop holds.
break; // Stop comparison. Host part matches.
}
}
@ -326,17 +355,17 @@ Klc5I28bGbvxIV5pnL6ZSjHEDp2WreM8HB0XFJwU+Q==
return false;
}
}
// Fail if the site has a deeper subdomain than the key, unless the deepest key subdomain is *
// Fail if the site has a deeper subdomain than the key, unless the deepest key subdomain is .
if ($keyparts[$i - 1] != '*' && count($siteparts) > ($i)) {
return false;
}
// If we made it here then the host part matches. Now check the path.
// If path is /*, matches all subpaths including /
// If path is /*, matches all subpaths including /.
$keypath = empty($keyurl->path) ? "/" : $keyurl->path;
$sitepath = empty($siteurl->path) ? "/" : $siteurl->path;
// Trim trailing / from both paths before comparison
// Trim trailing / from both paths before comparison.
if (\strlen($sitepath) > 1) {
$sitepath = \rtrim($sitepath, "/");
}
@ -365,7 +394,9 @@ Klc5I28bGbvxIV5pnL6ZSjHEDp2WreM8HB0XFJwU+Q==
"name" => new \external_value(PARAM_TEXT, 'premium registration name'),
"expires" => new \external_value(PARAM_TEXT, 'premium registration expiry date'),
"expired" => new \external_value(PARAM_BOOL, 'premium status expired'),
"intents" => new \external_multiple_structure(new \external_value(PARAM_TEXT), 'additional intents included in the license '),
"intents" => new \external_multiple_structure(
new \external_value(PARAM_TEXT), 'additional intents included in the license'
),
]);
}
@ -375,7 +406,7 @@ Klc5I28bGbvxIV5pnL6ZSjHEDp2WreM8HB0XFJwU+Q==
*/
public static function get_premiumstatus() {
if (self::$premiumsupported) {
$status = self::premiumStatus();
$status = self::premiumstatus();
$keys = [
"enabled",
"website",
@ -407,7 +438,7 @@ Klc5I28bGbvxIV5pnL6ZSjHEDp2WreM8HB0XFJwU+Q==
* @return string HTML description of premium status.
*/
public static function statusdescription() {
$status = self::premiumStatus();
$status = self::premiumstatus();
$msg = new \stdClass;
$msg->name = $status->name;

View File

@ -40,6 +40,9 @@ class reportservice extends \external_api {
*/
const CAP_VIEW = "local/treestudyplan:viewuserreports";
/**
* Parameter description for webservice function
*/
public static function get_report_structure_parameters(): \external_function_parameters {
return new \external_function_parameters([
"pageid" => new \external_value(PARAM_INT, 'id of studyplan page'),
@ -83,7 +86,7 @@ class reportservice extends \external_api {
$firstperiod = 1;
}
if (empty($lastperiod)) {
// Index for periods starts at 1, so the period count is same as length
// Index for periods starts at 1, so the period count is same as length.
$lastperiod = $page->periods();
}
@ -95,7 +98,7 @@ class reportservice extends \external_api {
"periods" => [],
];
// Find all relevant parts in this order
// Find all relevant parts in this order.
for ($i = $firstperiod; $i <= $lastperiod; $i++) {
$period = period::find($page, $i);
@ -127,13 +130,15 @@ class reportservice extends \external_api {
return $model;
}
/**
* Parameter description for webservice function
*/
public static function get_report_data_parameters(): \external_function_parameters {
return new \external_function_parameters([
"pageid" => new \external_value(PARAM_INT, 'id of studyplan page'),
"userid" => new \external_value(PARAM_INT, 'id of user'),
"firstperiod" => new \external_value(PARAM_INT, 'first period to include in report', VALUE_DEFAULT),
"lastperiod" => new \external_value(PARAM_INT, 'last period to include in report', VALUE_DEFAULT),
]);
}
@ -144,6 +149,13 @@ class reportservice extends \external_api {
return new \external_multiple_structure(studyitem::user_structure(), "Information per studyitem");
}
/**
* Webservice function get_report data
* @param int $pageid
* @param int $userid
* @param int|null $firsteperiod
* @param int|null $lastperiod
*/
public static function get_report_data($pageid, $userid, $firstperiod=null, $lastperiod=null) {
$page = studyplanpage::find_by_id($pageid);
webservicehelper::require_capabilities(self::CAP_VIEW, $page->studyplan()->context());
@ -156,14 +168,11 @@ class reportservice extends \external_api {
$firstperiod = 1;
}
if (empty($lastperiod)) {
// Index for periods starts at 1, so the period count is same as length
// Index for periods starts at 1, so the period count is same as length.
$lastperiod = $page->periods();
}
$model = [];
//TODO: Build report structure
for ($i = $firstperiod; $i <= $lastperiod; $i++) {
$lines = studyline::find_page_children($page);
foreach ($lines as $line) {
@ -175,11 +184,12 @@ class reportservice extends \external_api {
}
}
}
return $model;
}
/**
* Parameter description for webservice function
*/
public static function get_report_details_parameters(): \external_function_parameters {
return new \external_function_parameters([
"itemid" => new \external_value(PARAM_INT, 'id of studyitem'),

View File

@ -222,7 +222,7 @@ class studentstudyplanservice extends \external_api {
return [];
}
// Do not validate the context in this instance to avoid requiring logins when this is not wanted
// Do not validate the context in this instance to avoid requiring logins when this is not wanted.
$userid = $invite->user_id;
@ -279,7 +279,7 @@ class studentstudyplanservice extends \external_api {
return [];
}
// Do not validate the context in this instance to avoid requiring logins when this is not wanted
// Do not validate the context in this instance to avoid requiring logins when this is not wanted.
$studyplan = studyplan::find_by_id($studyplanid);
$userid = $invite->user_id;
@ -341,7 +341,7 @@ class studentstudyplanservice extends \external_api {
return [];
}
// Do not validate the context in this instance to avoid requiring logins when this is not wanted
// Do not validate the context in this instance to avoid requiring logins when this is not wanted.
$page = studyplanpage::find_by_id($pageid);
$studyplan = $page->studyplan();
@ -556,7 +556,7 @@ class studentstudyplanservice extends \external_api {
\external_api::validate_context(\context_system::instance());
// Return empty list if coach role not enabled
// Return empty list if coach role not enabled.
if (! (premium::enabled() && \get_config("local_treestudyplan", "enablecoach"))) {
return [];
}
@ -570,7 +570,9 @@ class studentstudyplanservice extends \external_api {
if (has_capability(self::CAP_COACH, $studyplan->context(), $USER)) {
$list[] = $studyplan->simple_model_coach();
}
} catch(\Exception $x) {}
} catch (\Exception $x) {
$studyplan = null;
}
}
return $list;
}
@ -694,7 +696,7 @@ class studentstudyplanservice extends \external_api {
$o->enrol($userid);
}
// Trigger immediate cohort synchronization for this line only
// Trigger immediate cohort synchronization for this line only.
autocohortsync::syncline($o);
return $o->enrol_info_model($userid);

View File

@ -245,7 +245,9 @@ class studyline {
try {
$role = $DB->get_record('role', ["id" => $roleid], "*", MUST_EXIST);
$list[] = $role;
} catch(\Exception $x) {}
} catch (\Exception $x) {
$role = null;
}
}
}
@ -304,7 +306,7 @@ class studyline {
self::SLOTSET_FILTER => new \external_multiple_structure(
studyitem::editor_structure(), 'filter items'),
])
)
),
], "Study line editor structure", $value);
}
@ -552,7 +554,7 @@ class studyline {
self::SLOTSET_FILTER => new \external_multiple_structure(
studyitem::user_structure(), 'filter items'),
])
)
),
], 'Studyline with user info', $value);
}
@ -606,7 +608,8 @@ class studyline {
return $model;
}
/** Get the users enrolled in this studyline if relevant
/**
* Get the users enrolled in this studyline if relevant
* @return array of Userids linked
*/
public function get_enrolled_userids() {
@ -632,7 +635,7 @@ class studyline {
public function isenrolled($userid) {
global $DB;
if ($this->r->enrollable == self::ENROLLABLE_NONE) {
return true; // If student cannot enrol, the student always is enrolled
return true; // If student cannot enrol, the student always is enrolled.
} else {
$r = $DB->get_record('local_treestudyplan_lineuser', [
'line_id' => $this->id(),
@ -721,7 +724,7 @@ class studyline {
// Registration already exists, check if enrolled is true and update accordingly.
if (boolval($r->enrolled)) {
$r->enrolled = 0;
$r->timeenrolled = time(); // Regi
$r->timeenrolled = time(); // Regi.
$r->enrolledby = $USER->id;
$DB->update_record("local_treestudyplan_lineuser", $r);

View File

@ -154,6 +154,10 @@ class studyplan {
return $date;
}
/**
* Determine studyplan icon
* @return string Url of icon
*/
private function icon() {
global $CFG;
$fs = \get_file_storage();
@ -183,8 +187,14 @@ class studyplan {
// Fall back to the standard (ugly) default image.
$url = new \moodle_url($CFG->wwwroot . "/local/treestudyplan/pix/default_icon.png");
} else {
$url = \moodle_url::make_pluginfile_url(\context_system::instance()->id, 'local_treestudyplan', 'defaulticon', 0, "/",
$defaulticon);
$url = \moodle_url::make_pluginfile_url(
\context_system::instance()->id,
'local_treestudyplan',
'defaulticon',
0,
"/",
$defaulticon
);
}
}
@ -201,9 +211,9 @@ class studyplan {
// The pluginfile URL which will serve the request.
'pluginfile.php',
// The combination of contextid / component / filearea / itemid
// form the virtual bucket that file are stored in.
\context_system::instance()->id, // System instance is always used for this
// The combination of contextid / component / filearea / itemid.
// Form the virtual bucket that file are stored in.
\context_system::instance()->id, // System instance is always used for this.
'local_treestudyplan',
'studyplan',
$this->id
@ -266,7 +276,11 @@ class studyplan {
"aggregation_info" => aggregator::basic_structure(),
"pages" => new \external_multiple_structure(studyplanpage::simple_structure(), 'pages'),
"progress" => new \external_value(PARAM_FLOAT, "fraction of completed modules", VALUE_OPTIONAL),
"amteaching" => new \external_value(PARAM_BOOL, "Current user is teaching one or more courses in this studyplan", VALUE_OPTIONAL),
"amteaching" => new \external_value(
PARAM_BOOL,
"Current user is teaching one or more courses in this studyplan",
VALUE_OPTIONAL
),
"suspended" => new \external_value(PARAM_BOOL, 'if studyplan is suspended', VALUE_OPTIONAL),
], 'Basic studyplan info', $value);
}
@ -408,7 +422,8 @@ class studyplan {
public static function add($fields, $bare = false): self {
global $CFG, $DB;
$addable = ['name', 'shortname', 'description', 'descriptionformat', 'idnumber', 'context_id', 'aggregation', 'aggregation_config'];
$addable = ['name', 'shortname', 'description', 'descriptionformat',
'idnumber', 'context_id', 'aggregation', 'aggregation_config'];
$info = ['enddate' => null, "template" => 0, "suspended" => 0];
foreach ($addable as $f) {
if (array_key_exists($f, $fields)) {
@ -418,8 +433,8 @@ class studyplan {
$id = $DB->insert_record(self::TABLE, $info);
$plan = self::find_by_id($id); // Make sure the new studyplan is immediately cached.
// Add a single page and initialize it with placeholder data
// This makes it easier to create a new study plan
// Add a single page and initialize it with placeholder data.
// This makes it easier to create a new study plan.
// On import, adding an empty page messes things up , so we have an option to skip this....
if (!$bare) {
@ -459,7 +474,7 @@ class studyplan {
'aggregation',
'aggregation_config',
'suspended',
'template'
'template',
];
$info = ['id' => $this->id ];
foreach ($editable as $f) {
@ -501,7 +516,7 @@ class studyplan {
$DB->delete_records("local_treestudyplan_coach", ["studyplan_id" => $this->id]);
$DB->delete_records("local_treestudyplan_cohort", ["studyplan_id" => $this->id]);
$DB->delete_records("local_treestudyplan_user", ["studyplan_id" => $this->id]);
//
$DB->delete_records('local_treestudyplan', ['id' => $this->id]);
return success::success();
}
@ -768,7 +783,8 @@ class studyplan {
return $this->linkeduserids;
}
/** Check if this studyplan is linked to a particular user
/**
* Check if this studyplan is linked to a particular user
* @param bool|stdClass $user The userid or user record of the user
*/
public function has_linked_user($user) {
@ -785,12 +801,13 @@ class studyplan {
}
}
/** Check if this studyplan is linked to a particular user
/**
* Check if this studyplan is linked to a particular user
* @param bool|stdClass|null $user The userid or user record of the user Leave empty to check current user.
*/
public function is_coach($user=null) {
global $DB, $USER
;
global $DB, $USER;
if (! (premium::enabled() && \get_config("local_treestudyplan", "enablecoach"))) {
// If coach role is not available, return false immediately.
return false;
@ -845,7 +862,7 @@ class studyplan {
$prg = $p->scanuserprogress($userid);
$progress += $prg;
}
// Now average it out over the amount of pages
// Now average it out over the amount of pages.
if (count($pages) > 0) {
return $progress / count($pages);
} else {
@ -912,7 +929,7 @@ class studyplan {
'description' => $this->r->description,
'descriptionformat' => $this->r->descriptionformat,
'aggregation' => $this->r->aggregation,
'aggregation_config' => $this->r->aggregation_config
'aggregation_config' => $this->r->aggregation_config,
], true);
// Copy any files related to this studyplan.
@ -981,7 +998,7 @@ class studyplan {
$json = json_encode([
"type" => "studyplan",
"version" => 2.0,
"studyplan" => $model
"studyplan" => $model,
], \JSON_PRETTY_PRINT);
return [ "format" => "application/json", "content" => $json];
}
@ -1094,11 +1111,11 @@ class studyplan {
// Create a new plan, based on the given parameters - this is the import studyplan part.
$plan = self::add( $planmodel, true);
// Import the files
// Import the files.
if (isset( $planmodel['files']) && is_array( $planmodel['files'])) {
$plan->import_files( $planmodel['files'], "studyplan");
}
// Import the icon
// Import the icon.
if (isset( $planmodel['iconfiles']) && is_array( $planmodel['iconfiles'])) {
$plan->import_files( $planmodel['iconfiles'], "icon");
}

View File

@ -154,9 +154,9 @@ class studyplanpage {
// The pluginfile URL which will serve the request.
'pluginfile.php',
// The combination of contextid / component / filearea / itemid
// form the virtual bucket that file are stored in.
\context_system::instance()->id, // System instance is always used for this
// The combination of contextid / component / filearea / itemid.
// Form the virtual bucket that file are stored in.
\context_system::instance()->id, // System instance is always used for this.
'local_treestudyplan',
'studyplanpage',
$this->id
@ -164,6 +164,10 @@ class studyplanpage {
return $text;
}
/**
* Determine studyplan timing
* @return string
*/
public function timing() {
$now = new \DateTime();
if ($now > $this->startdate()) {
@ -292,7 +296,7 @@ class studyplanpage {
}
$id = $DB->insert_record(self::TABLE, $info);
// Refresh the page cache for the studyplan
// Refresh the page cache for the studyplan.
$plan->pages(true);
$page = self::find_by_id($id); // Make sure the new page is immediately cached.
@ -301,7 +305,7 @@ class studyplanpage {
if (get_config("local_treestudyplan", "copystudylinesnewpage")) {
$templatepage = null;
$templatelines = [];
// find the latest a page with lines in the plan
// Find the latest a page with lines in the plan.
$pages = $plan->pages(true);
foreach ($pages as $p) {
if ($p->id() != $id) {
@ -322,7 +326,7 @@ class studyplanpage {
}
if (count(studyline::find_page_children($page)) == 0) {
// Add an empty study line if there are currently no study lines
// Add an empty study line if there are currently no study lines.
$lineinfo = ['page_id' => $id,
'name' => get_string("default_line_name", "local_treestudyplan"),
'shortname' => get_string("default_line_shortname", "local_treestudyplan"),
@ -585,7 +589,7 @@ class studyplanpage {
$json = json_encode([
"type" => "studyplanpage",
"version" => 2.0,
"page" => $model
"page" => $model,
], \JSON_PRETTY_PRINT);
return [ "format" => "application/json", "content" => $json];
}

View File

@ -133,7 +133,7 @@ class studyplanservice extends \external_api {
webservicehelper::require_capabilities([self::CAP_EDIT, self::CAP_VIEW], $studyplan->context());
}
// If suspended, only allow the mapping if the user has edit rights
// If suspended, only allow the mapping if the user has edit rights.
if (!has_capability(self::CAP_EDIT, $studyplan->context()) && $studyplan->suspended()) {
return null;
} else {
@ -902,7 +902,6 @@ class studyplanservice extends \external_api {
*/
public static function list_badges_parameters(): \external_function_parameters {
return new \external_function_parameters( [
] );
}
@ -922,12 +921,9 @@ class studyplanservice extends \external_api {
$result = [];
$badges = badges_get_badges(BADGE_TYPE_SITE, "timemodified");
foreach ($badges as $badge) {
// TODO: Add config option to list only active badges.
$result[] = (new badgeinfo($badge))->editor_model();
// TODO: Include course badges somehow... Just site badges is not enough.
}
return $result;
@ -962,7 +958,7 @@ class studyplanservice extends \external_api {
*/
public static function search_badges($search, $active=false) {
$systemcontext = webservicehelper::system_context();
// Check system permission to
// Check system permission to.
webservicehelper::require_capabilities("moodle/badges:viewbadges", $systemcontext);
$results = [];
@ -1115,7 +1111,8 @@ class studyplanservice extends \external_api {
$studyplan = $item->studyline()->studyplan();
if (($studyplan->is_coach() && !$studyplan->suspended())
|| has_capability('local/treestudyplan:editstudyplan', $studyplan->context())
|| ($coursecontext && is_enrolled($coursecontext, $USER, 'local/treestudyplan:selectowngradables'))) {
|| ($coursecontext && is_enrolled($coursecontext, $USER, 'local/treestudyplan:selectowngradables'))
) {
return coursecompetencyinfo::require_competency($competencyid, $itemid, $required)->model();
} else {
return success::fail("Access denied")->model();
@ -2081,7 +2078,7 @@ class studyplanservice extends \external_api {
}
}
// Trigger immediate cohort synchronization for this line only
// Trigger immediate cohort synchronization for this line only.
autocohortsync::syncline($o);
return $list;
}
@ -2132,7 +2129,7 @@ class studyplanservice extends \external_api {
$list[] = self::student_enrol_status_model($userid, $o);
}
// Trigger immediate cohort synchronization for this line only
// Trigger immediate cohort synchronization for this line only.
autocohortsync::syncline($o);
return $list;
}
@ -2195,7 +2192,6 @@ class studyplanservice extends \external_api {
*/
public static function count_templates_parameters(): \external_function_parameters {
return new \external_function_parameters( [
] );
}

View File

@ -98,7 +98,7 @@ class autocohortsync extends \core\task\scheduled_task {
$userenroller = new cascadeusersync($plan);
$userenroller->sync($line);
}
// Leave the csync required flag for the next auto update
// Leave the csync required flag for the next auto update.
}
}

View File

@ -167,7 +167,6 @@ class teachingfinder {
$now = time();
foreach ($list as $pageid) {
// Retrieve the studyplan id from the page.
// TODO: Change this when page management is implemented to return the page instead of the plan.
$planid = $DB->get_field("local_treestudyplan_page", "studyplan_id", ["id" => $pageid]);
$DB->insert_record(self::TABLE, ["teacher_id" => $userid, "studyplan_id" => $planid, "update_time" => $now]);
}

View File

@ -45,6 +45,12 @@ class utilityservice extends \external_api {
*/
const CAP_VIEW = "local/treestudyplan:viewuserreports";
/**
* Load moodle form
* @param string $formname
* @param string $params
* @param string $ajaxformdata
*/
protected static function load_mform($formname, $params, $ajaxformdata = null) {
global $CFG;
/* We don't need to load the form php file (class autoloading will handle that)
@ -121,7 +127,7 @@ class utilityservice extends \external_api {
return new \external_single_structure(
[
'html' => new \external_value(PARAM_RAW, 'HTML fragment.'),
'javascript' => new \external_value(PARAM_RAW, 'JavaScript fragment')
'javascript' => new \external_value(PARAM_RAW, 'JavaScript fragment'),
]
);
}
@ -157,7 +163,7 @@ class utilityservice extends \external_api {
$ajaxformdata = [];
parse_str($formdata, $ajaxformdata);
// Load the form, provide submitted form data and perform security checks
// Load the form, provide submitted form data and perform security checks.
$mform = self::load_mform($formname, $params, $ajaxformdata);
$return = $mform->process_submission();
@ -193,7 +199,7 @@ class utilityservice extends \external_api {
return [
"hivizdropslots" => get_config("local_treestudyplan", "hivizdropslots"),
"toolboxleft" => get_config("local_treestudyplan", "toolboxleft")
"toolboxleft" => get_config("local_treestudyplan", "toolboxleft"),
];
}

View File

@ -41,7 +41,7 @@ Options:
list($options, $unrecognised) = cli_get_params([
'help' => false,
'amount' => null
'amount' => null,
], [
'h' => 'help',
'a' => 'amount',
@ -56,7 +56,7 @@ if ($options['help']) {
cli_writeln($usage);
exit(2);
}
print_r($options);
if (empty($options['amount']) || !is_numeric($options['amount'])) {
cli_error('Missing mandatory integer argument "amount"', 2);
}

View File

@ -43,7 +43,7 @@ list($options, $unrecognised) = cli_get_params([
'help' => false,
'studyplan' => null,
'all' => false,
'file' => "/tmp/generategrades.json"
'file' => "/tmp/generategrades.json",
], [
'h' => 'help',
's' => 'studyplan',

View File

@ -47,13 +47,13 @@ list($options, $unrecognised) = cli_get_params([
'dryrun' => false,
'studyplan' => null,
'all' => false,
'file' => "/tmp/generategrades.json"
'file' => "/tmp/generategrades.json",
], [
'h' => 'help',
's' => 'studyplan',
'a' => 'all',
'f' => 'file',
'd' => 'dryrun'
'd' => 'dryrun',
]);
if ($unrecognised) {
@ -150,7 +150,10 @@ foreach ($plans as $plan) {
$ug = $a->get_user_grade($u->id, true);
$ug->grade = grade_floatval($gg->grade);
$ug->grader = $USER->id;
$ug->feedbacktext = nl2br( htmlspecialchars($gg->fb));
$ug->feedbacktext = nl2br(htmlspecialchars(
$gg->fb,
ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401
));
$ug->feedbackformat = FORMAT_HTML;
if (!$options["dryrun"]) {

View File

@ -14,7 +14,7 @@
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <https://www.gnu.org/licenses/>.
/**
* View study plans - teacher view and student view
* Entry point for Coach
* @package local_treestudyplan
* @copyright 2023 P.M. Kuipers
* @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
@ -50,7 +50,6 @@ $PAGE->requires->js_call_amd('local_treestudyplan/page-coach', 'init', []);
/**
* Shortcut function to provide translations
*
* @param mixed $str Translation key
* @param null|string[] $param Parameters to pass to translation
* @param string $plugin Location to search for translation strings
@ -61,7 +60,14 @@ function t($str, $param = null, $plugin = 'local_treestudyplan') {
}
print $OUTPUT->header();
?>
$text = (object)[
'studyplan_select' => t("studyplan_select"),
'coacheditmode' => t("coacheditmode"),
'showoverview' => t("showoverview"),
'studyplan_noneselected' => t("studyplan_noneselected"),
];
print <<<END
<div id='root' class="t-studyplan-limit-width">
<div class='vue-loader' v-show='false'>
<div class="spinner-border text-primary" role="status">
@ -73,7 +79,7 @@ print $OUTPUT->header();
<template v-if="displayedstudyplan">
<a href='#' @click.prevent='closeStudyplan'
><i style='font-size: 150%;' class='fa fa-chevron-left'></i> <?php t('back');?></a>
<span><?php t("studyplan_select"); ?></span>&nbsp;
<span>{$text->studyplan_select}</span>&nbsp;
<s-studyplan-details
v-model="displayedstudyplan"
v-if="displayedstudyplan.description"
@ -93,14 +99,14 @@ print $OUTPUT->header();
variant="primary"
>
<template v-slot="{value}">{{value.firstname}} {{value.lastname}}</template>
<template #defaultlabel><span class='text-primary'><?php t("coacheditmode"); ?></span></template>
<template #defaultlabel><span class='text-primary'>{$text->coacheditmode}</span></template>
</s-prevnext-selector>
</template>
</div>
<div class='t-studyplan-container'>
<h2 v-if='displayedstudyplan&& selectedstudent'
>{{selectedstudent.firstname}} {{selectedstudent.lastname}} - {{displayedstudyplan.name}}</h2>
<h2 v-else-if='displayedstudyplan'><?php t("showoverview"); ?> - {{displayedstudyplan.name}}</h2>
<h2 v-else-if='displayedstudyplan'>{$text->showoverview} - {{displayedstudyplan.name}}</h2>
<template v-if="!loadingstudyplan && displayedstudyplan">
<r-studyplan v-if="selectedstudent"
v-model='displayedstudyplan'
@ -116,7 +122,7 @@ print $OUTPUT->header();
<span class="sr-only">Loading...</span>
</div>
<div v-else class='t-studyplan-notselected'>
<p><?php t("studyplan_noneselected"); ?></p>
<p>{$text->studyplan_noneselected}</p>
<b-card-group deck>
<s-studyplan-card
v-for='(studyplan, planindex) in studyplans'
@ -130,6 +136,6 @@ print $OUTPUT->header();
</div>
</div>
</div>
<?php
END;
print $OUTPUT->footer();

View File

@ -27,63 +27,62 @@ $capabilities = [
'riskbitmask' => RISK_DATALOSS ,
'captype' => 'write',
'contextlevel' => CONTEXT_SYSTEM,
'archetypes' => array(
'manager' => CAP_ALLOW
),
'archetypes' => [
'manager' => CAP_ALLOW,
],
],
'local/treestudyplan:forcescales' => [
'riskbitmask' => RISK_DATALOSS ,
'captype' => 'write',
'contextlevel' => CONTEXT_SYSTEM,
'archetypes' => array(
'manager' => CAP_ALLOW
),
'archetypes' => [
'manager' => CAP_ALLOW,
],
],
'local/treestudyplan:selectowngradables' => [
'riskbitmask' => RISK_DATALOSS ,
'captype' => 'write',
'contextlevel' => CONTEXT_SYSTEM,
'archetypes' => array(
'teacher' => CAP_ALLOW
),
'archetypes' => [
'teacher' => CAP_ALLOW,
],
],
'local/treestudyplan:configure' => [
'riskbitmask' => RISK_DATALOSS ,
'captype' => 'write',
'contextlevel' => CONTEXT_SYSTEM,
'archetypes' => array(
'manager' => CAP_ALLOW
),
'archetypes' => [
'manager' => CAP_ALLOW,
],
],
'local/treestudyplan:viewuserreports' => [
'riskbitmask' => RISK_PERSONAL ,
'captype' => 'write',
'contextlevel' => CONTEXT_SYSTEM,
'archetypes' => array(
'manager' => CAP_ALLOW
),
'archetypes' => [
'manager' => CAP_ALLOW,
],
],
'local/treestudyplan:lineunenrol' => [
'riskbitmask' => RISK_PERSONAL ,
'captype' => 'write',
'contextlevel' => CONTEXT_SYSTEM,
'archetypes' => array(
'manager' => CAP_ALLOW
),
'archetypes' => [
'manager' => CAP_ALLOW,
],
],
'local/treestudyplan:coach' => [
'riskbitmask' => RISK_PERSONAL ,
'captype' => 'write',
'contextlevel' => CONTEXT_SYSTEM,
'archetypes' => array(
'manager' => CAP_ALLOW
),
'archetypes' => [
'manager' => CAP_ALLOW,
],
],
];

View File

@ -23,7 +23,6 @@
defined('MOODLE_INTERNAL') || die();
$services = [
];
$functions = [
@ -832,5 +831,4 @@ $functions = [
'capabilities' => '',
'loginrequired' => true,
],
];

View File

@ -49,5 +49,4 @@ $tasks = [
'month' => '*',
'dayofweek' => '*',
],
];

View File

@ -394,7 +394,7 @@ function xmldb_local_treestudyplan_upgrade($oldversion) {
"fullname" => "Period {$pnum}",
"shortname" => "P{$pnum}",
"startdate" => date("Y-m-d", $pstart),
"enddate" => date("Y-m-d", $pend)
"enddate" => date("Y-m-d", $pend),
];
$DB->insert_record("local_treestudyplan_period", $o);
}
@ -532,7 +532,6 @@ function xmldb_local_treestudyplan_upgrade($oldversion) {
$dbman->add_field($table, $field);
}
// Treestudyplan savepoint reached.
upgrade_plugin_savepoint(true, 2024022502, 'local', 'treestudyplan');
}

View File

@ -51,9 +51,9 @@ require_login();
print $OUTPUT->header();
if (!empty($add)) {
$data = array(
$data = [
'add' => 1,
);
];
} else if (!empty($update)) {
$data = $DB->get_record("local_treestudyplan_invit", ['id' => $update]);

View File

@ -64,7 +64,6 @@ $ci = new contextinfo($studyplancontext);
$contextname = $ci->pathstr();
$PAGE->set_pagelayout('base');
//$PAGE->set_context($studyplancontext);
$PAGE->set_title(get_string('cfg_plans', 'local_treestudyplan')." - ".$contextname);
$PAGE->set_heading($contextname);
@ -72,20 +71,30 @@ if ($studyplancontext->id > 1) {
navigation_node::override_active_url(new moodle_url('/course/index.php', ['categoryid' => $categoryid ]));
$PAGE->navbar->add(get_string('cfg_plans', 'local_treestudyplan'));
// Coursecat context
$cat = \core_course_category::get($studyplancontext->instanceid, IGNORE_MISSING, true); // We checck visibility later
// Coursecat context.
$cat = \core_course_category::get($studyplancontext->instanceid, IGNORE_MISSING, true); // We checck visibility later.
} else {
// System context
// System context.
$cat = \core_course_category::top();
}
if (!$cat->is_uservisible()) {
throw new \moodle_exception("error:cannotviewcategory", "local_treestudyplan", "/local/treestudyplan/edit_plan.php", $contextname);
throw new \moodle_exception(
"error:cannotviewcategory",
"local_treestudyplan",
"/local/treestudyplan/edit_plan.php",
$contextname
);
}
if (!has_capability('local/treestudyplan:editstudyplan', $studyplancontext)) {
throw new \moodle_exception("error:nostudyplaneditaccess", "local_treestudyplan", "/local/treestudyplan/edit_plan.php", $contextname);
throw new \moodle_exception(
"error:nostudyplaneditaccess",
"local_treestudyplan",
"/local/treestudyplan/edit_plan.php",
$contextname
);
}
// Load javascripts and specific css.
@ -110,8 +119,13 @@ function t($str, $param = null, $plugin = 'local_treestudyplan') {
}
print $OUTPUT->header();
$text = (object)[
'loading' => t("loading", null, "core"),
'studyplan_select' => t("studyplan_select"),
'studyplan_noneselected' => t("studyplan_noneselected"),
];
?>
print <<<END
<div id='root' class="t-studyplan-limit-width">
<div class='vue-loader' v-show='false'>
<div class="spinner-border text-primary" role="status">
@ -120,26 +134,27 @@ print $OUTPUT->header();
</div>
<div v-cloak>
<div v-if='!activestudyplan && usedcontexts && !loadingstudyplan' class='ml-3 mb-3 s-context-selector'>
<b-form-select text='<?php print($contextname);?>' :value="contextid" @change='switchContext'
<b-form-select text='{$contextname}' :value="contextid" @change='switchContext'
:class="(!(usedcontexts.length)) ? 'text-primary' : ''">
<b-form-select-option v-if='!(usedcontexts.length)' :value="contextid"
:class="'text-primary'">
<span><?php t("loading", null, "core"); ?>...</span></b-form-select-option>
<span>{$text->loading}...</span></b-form-select-option>
<b-form-select-option v-for='ctx in usedcontexts' :key='ctx.id' :value="ctx.context_id"
:class="(ctx.studyplancount > 0) ? 'font-weight-bold' : ''"
><span v-for="(p, i) in ctx.category.path"
><span v-if="i>0"> / </span>{{ p }}</span> <span>({{ ctx.studyplancount }})</span>
</b-form-select-option>
</b-form-select>
<div v-if="!(usedcontexts.length)" style="position: relative; top: 0.3rem; width: 1.2rem; height: 1.2rem; font-size: 0.7rem;"
<div v-if="!(usedcontexts.length)"
style="position: relative; top: 0.3rem; width: 1.2rem; height: 1.2rem; font-size: 0.7rem;"
class="spinner-border text-primary" role="status"></div>
</div>
<h3 v-else><?php print $contextname; ?></h3>
<h3 v-else>{$contextname}</h3>
<div class="m-buttonbar" style="margin-bottom: 1em;">
<a href='#' v-if='activestudyplan' @click.prevent='closeStudyplan'
><i style='font-size: 150%;' class='fa fa-chevron-left'></i> <?php t('back');?></a>
<span v-if='activestudyplan'><?php t("studyplan_select"); ?></span>&nbsp;
<span v-if='activestudyplan'>{$text->studyplan_select}</span>&nbsp;
<b-form-select v-if='activestudyplan' lazy :text='dropdown_title' :value='activestudyplan.id' @change="selectStudyplan">
<b-form-select-option
v-for='(studyplan, planindex) in studyplans'
@ -181,7 +196,7 @@ print $OUTPUT->header();
<span class="sr-only">Loading...</span>
</div>
<div v-else class='t-studyplan-notselected'>
<p><?php t("studyplan_noneselected"); ?></p>
<p>{$text->studyplan_noneselected}</p>
<b-card-group deck>
<s-studyplan-card
v-for='(studyplan, planindex) in studyplans'
@ -193,7 +208,11 @@ print $OUTPUT->header();
>
<template #title>
<span class='s-studyplan-card-title-buttons'>
<t-studyplan-edit v-model="studyplans[planindex]" @moved="movedStudyplan" @input="refreshTemplateCount"></t-studyplan-edit>
<t-studyplan-edit
v-model="studyplans[planindex]"
@moved="movedStudyplan"
@input="refreshTemplateCount"
></t-studyplan-edit>
<t-studyplan-associate v-model="studyplans[planindex]"></t-studyplan-associate>
</span>
</template>
@ -203,6 +222,5 @@ print $OUTPUT->header();
</div>
</div>
</div>
<?php
END;
print $OUTPUT->footer();

View File

@ -90,19 +90,18 @@ if (!get_config("local_treestudyplan", "enableplansharing")) {
$PAGE->set_heading(get_string('report_invited', 'local_treestudyplan', "{$student->firstname} {$student->lastname}"));
print $OUTPUT->header();
?>
print "
<div id='root'>
<div class='vue-loader' v-show='false'>
<div class="spinner-border text-primary" role="status">
<span class="sr-only">Loading...</span>
<div class='spinner-border text-primary' role='status'>
<span class='sr-only'>Loading...</span>
</div>
</div>
<div v-cloak>
<r-report type="invited" :invitekey="invitekey"></r-report>
<r-report type='invited' :invitekey='invitekey'></r-report>
</div>
</div>
<?php
";
print $OUTPUT->footer();
}

20
lib.php
View File

@ -56,7 +56,7 @@ function local_treestudyplan_autofill_customusermenuitems() {
"/local/treestudyplan/edit-plan.php" => ["included" => false, "strkey" => "link_editplan"],
];
if ((premium::enabled() && \get_config("local_treestudyplan", "enablecoach"))) {
// Also include the coach role if enabled
// Also include the coach role if enabled.
$navitems["/local/treestudyplan/coach.php"] = ["included" => false, "strkey" => "link_coach"];
}
@ -86,7 +86,7 @@ function local_treestudyplan_autofill_customusermenuitems() {
$link, // Link.
'', // Tooltip.
$lang, // Language code.
" #Automatically added by studyplan plugin. See setting 'primary_nav_autofill' to disable this"
" #Automatically added by studyplan plugin. See setting 'primary_nav_autofill' to disable this",
]);
$custommenuitems = trim($custommenuitems)."\n".$line;
}
@ -198,8 +198,8 @@ function local_treestudyplan_extend_navigation(global_navigation $navigation) {
$hideprimaryhrefs[] = "/local/treestudyplan/edit-plan.php";
}
$coachsql = "SELECT COUNT('id') FROM {local_treestudyplan_coach} as c
INNER JOIN {local_treestudyplan} AS t ON c.studyplan_id = t.id
$coachsql = "SELECT COUNT('id') FROM {local_treestudyplan_coach} c
INNER JOIN {local_treestudyplan} t ON c.studyplan_id = t.id
WHERE c.user_id = :user_id";
if ((premium::enabled() && \get_config("local_treestudyplan", "enablecoach")) &&
@ -450,13 +450,13 @@ function local_treestudyplan_pluginfile(
// Studyplan icons and description images are not secret, so don't overdo it on access control...
if (true) {
// Extract the filename / filepath from the $args array
// Extract the filename / filepath from the $args array.
$filename = array_pop($args); // The last item in the $args array.
if (empty($args)) {
// $args is empty => the path is '/'.
// Var $args is empty => the path is '/'.
$filepath = '/';
} else {
// $args contains the remaining elements of the filepath.
// Var $args contains the remaining elements of the filepath.
$filepath = '/' . implode('/', $args) . '/';
}
@ -478,13 +478,13 @@ function local_treestudyplan_pluginfile(
// Fetch the itemid from the path.
$itemid = array_shift($args);
// Extract the filename / filepath from the $args array
// Extract the filename / filepath from the $args array.
$filename = array_pop($args); // The last item in the $args array.
if (empty($args)) {
// $args is empty => the path is '/'.
// Var $args is empty => the path is '/'.
$filepath = '/';
} else {
// $args contains the remaining elements of the filepath.
// Var $args contains the remaining elements of the filepath.
$filepath = '/' . implode('/', $args) . '/';
}

View File

@ -35,7 +35,6 @@ require_login();
$PAGE->set_pagelayout('base');
$PAGE->set_context($systemcontext);
//$teachermode = has_capability("local/treestudyplan:viewuserreports", $systemcontext);
$amteaching = teachingfinder::is_teaching($USER->id);
$haveplans = studyplan::exist_for_user($USER->id);

View File

@ -23,8 +23,6 @@
require_once("../../config.php");
use local_treestudyplan\contextinfo;
use local_treestudyplan\courseservice;
use local_treestudyplan\studyplan;
use local_treestudyplan\studyplanpage;
use local_treestudyplan\premium;
@ -42,8 +40,8 @@ $context = $studyplan->context();
$ci = new contextinfo($context);
$contextname = $ci->pathstr();
$firstperiod = optional_param('firstperiod', 0, PARAM_INT); // First period to show
$lastperiod = optional_param('lastperiod', 0, PARAM_INT); // Last periode to show
$firstperiod = optional_param('firstperiod', 0, PARAM_INT); // First period to show.
$lastperiod = optional_param('lastperiod', 0, PARAM_INT); // Last periode to show.
$PAGE->set_pagelayout('report');
$PAGE->set_title(get_string('studyplan_report', 'local_treestudyplan'));
@ -62,7 +60,11 @@ $PAGE->requires->css(new moodle_url($CFG->wwwroot.'/local/treestudyplan/css/boot
if ($CFG->debugdeveloper) {
$PAGE->requires->css(new moodle_url($CFG->wwwroot.'/local/treestudyplan/css/devstyles.css'));
}
$PAGE->requires->js_call_amd('local_treestudyplan/page-result-overview', 'init', [$studyplan->id(), $page->id(), $firstperiod, $lastperiod]);
$PAGE->requires->js_call_amd(
'local_treestudyplan/page-result-overview',
'init',
[$studyplan->id(), $page->id(), $firstperiod, $lastperiod]
);
/**
* Shortcut function to provide translations
@ -81,30 +83,30 @@ print "<h3><b>{$contextname}</b> / {$studyplan->name()}</h3>";
if ($studyplan->name() != $page->fullname()) {
print "<h4>{$page->fullname()}</h4>";
}
?>
print <<<END
<div id='root'>
<div class='vue-loader' v-show='false'>
<div class="spinner-border text-primary" role="status">
<span class="sr-only">Loading...</span>
<div class='spinner-border text-primary' role='status'>
<span class='sr-only'>Loading...</span>
</div>
</div>
<div v-cloak>
<div class='vue-loader' v-if="!structure">
<div class="spinner-border text-primary" role="status">
<span class="sr-only">Loading...</span>
<div class='vue-loader' v-if='!structure'>
<div class='spinner-border text-primary' role='status'>
<span class='sr-only'>Loading...</span>
</div>
</div>
<template v-else>
<div class='container q-pageperiodselection'>
<div class='row'>
<div class="col-sm-2 q-label">{{text.page}}</div>
<div class="col-sm-6">
<select class="custom-select" :value="page.id" @change="selectedPage">
<template v-if="studyplan">
<option v-for="p in studyplan.pages"
:key="p.id"
:value="p.id"
:selected="(page.id == p.id)?true:false"
<div class='col-sm-2 q-label'>{{text.page}}</div>
<div class='col-sm-6'>
<select class='custom-select' :value='page.id' @change='selectedPage'>
<template v-if='studyplan'>
<option v-for='p in studyplan.pages'
:key='p.id'
:value='p.id'
:selected='(page.id == p.id) ? true : false'
>{{p.fullname}}</option>
</template>
<option v-else disabled>{{text.loading}}</option>
@ -112,27 +114,27 @@ if ($studyplan->name() != $page->fullname()) {
</div>
</div>
<div class='row'>
<div class="col-sm-2 q-label" >{{text.period}} ({{text.from}} / {{text.to}})</div>
<div class="col-sm-2">
<select class="custom-select" @change="selectedFirstPeriod">
<template v-if="page">
<option v-for="p in page.perioddesc"
:key="p.id"
:value="p.period"
:selected="(structure.firstperiod == p.period)?true:false"
<div class='col-sm-2 q-label' >{{text.period}} ({{text.from}} / {{text.to}})</div>
<div class='col-sm-2'>
<select class='custom-select' @change='selectedFirstPeriod'>
<template v-if='page'>
<option v-for='p in page.perioddesc'
:key='p.id'
:value='p.period'
:selected='(structure.firstperiod == p.period) ? true : false'
>{{p.fullname}}</option>
</template>
<option v-else disabled>{{text.loading}}</option>
</select>
</div>
<div class="col-sm-2 ">
<select class="custom-select" @change="selectedLastPeriod">
<template v-if="page">
<template v-for="p in page.perioddesc">
<option v-if="p.period >= structure.firstperiod"
:key="p.id"
:value="p.period"
:selected="(structure.lastperiod == p.period)?true:false"
<div class='col-sm-2 '>
<select class='custom-select' @change='selectedLastPeriod'>
<template v-if='page'>
<template v-for='p in page.perioddesc'>
<option v-if='p.period >= structure.firstperiod'
:key='p.id'
:value='p.period'
:selected='(structure.lastperiod == p.period) ? true : false'
>{{p.fullname}}</option>
</template>
</template>
@ -141,10 +143,10 @@ if ($studyplan->name() != $page->fullname()) {
</div>
</div>
</div>
<q-studyplanreport :structure="structure" ></s-studyplanreport>
<q-studyplanreport :structure='structure' ></s-studyplanreport>
</template>
</div>
</div>
<?php
END;
print $OUTPUT->footer();

View File

@ -55,7 +55,7 @@ if ($hassiteconfig) {
true,
));
// Default image for study plans
// Default image for study plans.
$page->add(new admin_setting_configstoredfile('local_treestudyplan/defaulticon',
get_string('setting_defaulticon', 'local_treestudyplan'),
get_string('settingdesc_defaulticon', 'local_treestudyplan'),
@ -89,8 +89,17 @@ if ($hassiteconfig) {
get_string('settingdesc_display_heading', 'local_treestudyplan')
));
$displayfields = ["shortname" => get_string("shortname"), "idnumber" => get_string("idnumber"), "fullname" => get_string("fullname") ];
$infofields = ["" => get_string('none'), "description" => get_string("description"), "contacts" => get_string("teachers"), "idnumber" => get_string("idnumber")];
$displayfields = [
"shortname" => get_string("shortname"),
"idnumber" => get_string("idnumber"),
"fullname" => get_string("fullname"),
];
$infofields = [
"" => get_string('none'),
"description" => get_string("description"),
"contacts" => get_string("teachers"),
"idnumber" => get_string("idnumber"),
];
$handler = \core_customfield\handler::get_handler('core_course', 'course');
foreach ($handler->get_categories_with_fields() as $cat) {
@ -106,49 +115,43 @@ if ($hassiteconfig) {
}
}
}
//get_config("local_treestudyplan", "enableplansharing")
$page->add(new admin_setting_configcheckbox('local_treestudyplan/enableplansharing',
get_string('setting_enableplansharing', 'local_treestudyplan'),
get_string('settingdesc_enableplansharing', 'local_treestudyplan'),
true,
));
//get_config("local_treestudyplan", "timelessperiods")
$page->add(new admin_setting_configcheckbox('local_treestudyplan/timelessperiods',
get_string('setting_timelessperiods', 'local_treestudyplan'),
get_string('settingdesc_timelessperiods', 'local_treestudyplan'),
false,
));
//get_config("local_treestudyplan", "limitcourselist")
$page->add(new admin_setting_configcheckbox('local_treestudyplan/limitcourselist',
get_string('setting_limitcourselist', 'local_treestudyplan'),
get_string('settingdesc_limitcourselist', 'local_treestudyplan'),
false,
));
//get_config("local_treestudyplan", "hivizdropslots")
$page->add(new admin_setting_configcheckbox('local_treestudyplan/hivizdropslots',
get_string('setting_hivizdropslots', 'local_treestudyplan'),
get_string('settingdesc_hivizdropslots', 'local_treestudyplan'),
false,
));
//get_config("local_treestudyplan", "toolboxleft")
$page->add(new admin_setting_configcheckbox('local_treestudyplan/toolboxleft',
get_string('setting_toolboxleft', 'local_treestudyplan'),
get_string('settingdesc_toolboxleft', 'local_treestudyplan'),
true,
));
//get_config("local_treestudyplan", "copystudylinesnewpage")
$page->add(new admin_setting_configcheckbox('local_treestudyplan/copystudylinesnewpage',
get_string('setting_copystudylinesnewpage', 'local_treestudyplan'),
get_string('settingdesc_copystudylinesnewpage', 'local_treestudyplan'),
false,
));
//get_config("local_treestudyplan", "continueperiodnumberingnewpage")
$page->add(new admin_setting_configcheckbox('local_treestudyplan/continueperiodnumberingnewpage',
get_string('setting_continueperiodnumberingnewpage', 'local_treestudyplan'),
get_string('settingdesc_continueperiodnumberingnewpage', 'local_treestudyplan'),
@ -156,7 +159,6 @@ if ($hassiteconfig) {
));
if (premium::enabled()) {
//get_config("local_treestudyplan", "enablecoach")
$page->add(new admin_setting_configcheckbox('local_treestudyplan/enablecoach',
get_string('setting_enablecoach', 'local_treestudyplan'),
get_string('settingdesc_enablecoach', 'local_treestudyplan'),
@ -410,7 +412,7 @@ if ($hassiteconfig) {
));
if ($CFG->debugdeveloper) {
// show decrypted key data
// Show decrypted key data.
$pagepremium->add(new admin_setting_description('local_treestudyplan/premium_debug',
get_string('setting_premium_debug', 'local_treestudyplan'),
premium::debuginfo() . "<br>&nbsp;<br>" // Add empty row at end.
@ -420,5 +422,4 @@ if ($hassiteconfig) {
// Add settings page2 to the admin settings category.
$ADMIN->add('local_treestudyplan', $pagepremium);
}
}

View File

@ -1,83 +0,0 @@
<?php
function sitematch($key, $site) {
// Add double slashes to key and site if no scheme is set.
// Basically: if no double slashes present before any dots, shashes or @s.
if (!\preg_match_all('#^[^./@]*?//#', $key )) {
$key = "//".$key;
}
if (!\preg_match_all('#^[^./@]*?//#', $site)) {
$site = "//".$site;
}
// Use parse_url() to split path and host.
$keyurl = (object)\parse_url($key);
$siteurl = (object)\parse_url($site);
// No match if host is empty on key or site
if (empty($keyurl->host) || empty($siteurl->host)) {
return false;
}
if ($keyurl->host == "*") {
// * matches all
return true;
}
// First match the host part.
$keyparts = \array_reverse(\explode(".", $keyurl->host));
$siteparts = \array_reverse(\explode(".", $siteurl->host));
// Trim starting www from both parts, since site.domain and www.site.domain should be treated as the same.
if (($x = \array_pop($keyparts)) != "www") {\array_push($keyparts, $x);}
if (($x = \array_pop($siteparts)) != "www") {\array_push($siteparts, $x);}
for ($i = 0; $i < count($keyparts); $i++) {
// No match if the site does not have a part, but the key does. Unless the key part is *
if (!isset($siteparts[$i]) ) {
if ($keyparts[$i] != "*") {
return false;
} else {
$i++; //increment $i by one before break, to make sure the comparison following this loop holds.
break; // Stop comparison. Host part matches.
}
}
// Now do a proper case insensitive check for matching.
// Uses fnmatch to easily handle shell type wildcards.
if ( ! \fnmatch($keyparts[$i], $siteparts[$i], \FNM_CASEFOLD)) {
return false;
}
}
// Fail if the site has a deeper subdomain than the key, unless the deepest key subdomain is *
if ($keyparts[$i-1] != '*' && count($siteparts) > ($i)) {
return false;
}
// If we made it here then the host part matches. Now check the path.
// If path is /*, matches all subpaths including /
$keypath = empty($keyurl->path) ? "/" : $keyurl->path;
$sitepath = empty($siteurl->path) ? "/" : $siteurl->path;
// Trim trailing / from both paths before comparison
if (\strlen($sitepath) > 1) {
$sitepath = \rtrim($sitepath, "/");
}
if (\strlen($keypath) > 1) {
$keypath = \rtrim($keypath, "/");
}
// Do a case insensitive fnmatch on the site so wildcards are matched too.
return \fnmatch($keypath, $sitepath, \FNM_CASEFOLD);
}
$tests = [
["*", "https://www.miqra.nl"],
["*/*", "https://www.miqra.nl"],
["*", "https://clients.openedu.nl/fith"],
["clients.openedu.nl/fith", "https://clients.openedu.nl/fith/"],
["clients.openedu.nl/fith/", "https://clients.openedu.nl/fith"],
];
foreach($tests as $test) {
[$key, $site] = $test;
print("Checking key '{$key}' on site '{$site}': " . (sitematch($key, $site)?"MATCH":"FAIL") . "\n");
}

View File

@ -65,7 +65,6 @@ $ci = new contextinfo($studyplancontext);
$contextname = $ci->pathstr();
$PAGE->set_pagelayout('base');
//$PAGE->set_context($studyplancontext);
$PAGE->set_title(get_string('view_plan', 'local_treestudyplan')." - ".$contextname);
$PAGE->set_heading(get_string('view_plan', 'local_treestudyplan')." - ".$contextname);
@ -73,19 +72,29 @@ if ($studyplancontext->id > 1) {
navigation_node::override_active_url(new moodle_url('/course/index.php', ['categoryid' => $categoryid ]));
$PAGE->navbar->add(get_string('view_plan', 'local_treestudyplan'));
// Coursecat context
$cat = \core_course_category::get($studyplancontext->instanceid, IGNORE_MISSING, true); // We checck visibility later
// Coursecat context.
$cat = \core_course_category::get($studyplancontext->instanceid, IGNORE_MISSING, true); // We checck visibility later.
} else {
// System context
// System context.
$cat = \core_course_category::top();
}
if (!$cat->is_uservisible()) {
throw new \moodle_exception("error:cannotviewcategory", "local_treestudyplan", "/local/treestudyplan/view_plan.php", $contextname);
throw new \moodle_exception(
"error:cannotviewcategory",
"local_treestudyplan",
"/local/treestudyplan/view_plan.php",
$contextname
);
}
if (!has_capability('local/treestudyplan:viewuserreports', $studyplancontext)) {
throw new \moodle_exception("error:nostudyplanviewaccess", "local_treestudyplan", "/local/treestudyplan/view_plan.php", $contextname);
throw new \moodle_exception(
"error:nostudyplanviewaccess",
"local_treestudyplan",
"/local/treestudyplan/view_plan.php",
$contextname
);
}
// Load javascripts and specific css.
@ -108,7 +117,15 @@ function t($str, $param = null, $plugin = 'local_treestudyplan') {
}
print $OUTPUT->header();
?>
$text = (object)[
'loading' => t("loading", null, "core"),
'back' => t('back'),
'studyplan_select' => t("studyplan_select"),
'selectstudent_btn' => t('selectstudent_btn'),
'showoverview' => t("showoverview"),
'studyplan_noneselected' => t("studyplan_noneselected"),
];
print <<<END
<div id='root'>
<div class='vue-loader' v-show='false'>
<div class="spinner-border text-primary" role="status">
@ -117,25 +134,26 @@ print $OUTPUT->header();
</div>
<div v-cloak>
<div v-if='!activestudyplan && usedcontexts' class='ml-3 mb-3 s-context-selector'>
<b-form-select text='<?php print($contextname);?>' :value="contextid" @change='switchContext'
<b-form-select text='{$contextname}' :value="contextid" @change='switchContext'
:class="(!(usedcontexts.length)) ? 'text-primary' : ''">
<b-form-select-option v-if='!(usedcontexts.length)' :value="contextid"
:class="'text-primary'">
<span><?php t("loading", null, "core"); ?>...</span></b-form-select-option>
<span>{$text->loading}...</span></b-form-select-option>
<b-form-select-option v-for='ctx in usedcontexts' :key='ctx.id' :value="ctx.context_id"
:class="(ctx.studyplancount > 0) ? 'font-weight-bold' : ''"
><span v-for="(p, i) in ctx.category.path"><span v-if="i>0"> / </span>{{ p }}</span>
<span>({{ ctx.studyplancount }})</span></b-form-select-option>
</b-form-select>
<div v-if="!(usedcontexts.length)" style="position: relative; top: 0.3rem; width: 1.2rem; height: 1.2rem; font-size: 0.7rem;"
<div v-if="!(usedcontexts.length)"
style="position: relative; top: 0.3rem; width: 1.2rem; height: 1.2rem; font-size: 0.7rem;"
class="spinner-border text-primary" role="status"></div>
</div>
<h3 v-else><?php print $contextname; ?></h3>
<h3 v-else>{$contextname}</h3>
<div class="m-buttonbar" style="margin-bottom: 1em;">
<template v-if="displayedstudyplan">
<a href='#' @click.prevent='closeStudyplan'
><i style='font-size: 150%;' class='fa fa-chevron-left'></i> <?php t('back');?></a>
<span><?php t("studyplan_select"); ?></span>&nbsp;
><i style='font-size: 150%;' class='fa fa-chevron-left'></i> {$text->back}</a>
<span>{$text->studyplan_select}</span>&nbsp;
<b-form-select lazy :text='dropdown_title' :value='displayedstudyplan.id'>
<b-form-select-option
v-for='(studyplan, planindex) in studyplans'
@ -164,7 +182,7 @@ print $OUTPUT->header();
variant="primary"
>
<template v-slot="{value}">{{value.firstname}} {{value.lastname}}</template>
<template #defaultlabel><span class='text-primary'><?php t("showoverview"); ?></span></template>
<template #defaultlabel><span class='text-primary'>{$text->showoverview}</span></template>
</s-prevnext-selector>
</div>
</template>
@ -172,7 +190,7 @@ print $OUTPUT->header();
<div class='t-studyplan-container'>
<h2 v-if='displayedstudyplan&& selectedstudent'
>{{selectedstudent.firstname}} {{selectedstudent.lastname}} - {{displayedstudyplan.name}}</h2>
<h2 v-else-if='displayedstudyplan'><?php t("showoverview"); ?> - {{displayedstudyplan.name}}</h2>
<h2 v-else-if='displayedstudyplan'>{$text->showoverview} - {{displayedstudyplan.name}}</h2>
<r-studyplan v-if='!loadingstudyplan && displayedstudyplan'
v-model='displayedstudyplan' :teachermode='!selectedstudent'
></r-studyplan>
@ -180,7 +198,7 @@ print $OUTPUT->header();
<span class="sr-only">Loading...</span>
</div>
<div v-else class='t-studyplan-notselected'>
<p><?php t("studyplan_noneselected"); ?></p>
<p>{$text->studyplan_noneselected}</p>
<b-card-group deck>
<s-studyplan-card
v-for='(studyplan, planindex) in studyplans'
@ -194,6 +212,6 @@ print $OUTPUT->header();
</div>
</div>
</div>
<?php
END;
print $OUTPUT->footer();