Implemented badge subriteria parsing...

This commit is contained in:
PMKuipers 2023-10-28 21:49:48 +02:00
parent 8af1152ae2
commit 3fcf0095ea
3 changed files with 432 additions and 30 deletions

View file

@ -24,6 +24,8 @@ namespace local_treestudyplan;
defined('MOODLE_INTERNAL') || die(); defined('MOODLE_INTERNAL') || die();
require_once($CFG->libdir.'/externallib.php'); require_once($CFG->libdir.'/externallib.php');
use award_criteria;
use \core_badges\badge; use \core_badges\badge;
/** /**
@ -166,23 +168,7 @@ class badgeinfo {
"id" => new \external_value(PARAM_INT, 'id of badge'), "id" => new \external_value(PARAM_INT, 'id of badge'),
"infolink" => new \external_value(PARAM_TEXT, 'badge issue information link', VALUE_OPTIONAL), "infolink" => new \external_value(PARAM_TEXT, 'badge issue information link', VALUE_OPTIONAL),
"name" => new \external_value(PARAM_TEXT, 'badge name'), "name" => new \external_value(PARAM_TEXT, 'badge name'),
"completion" => new \external_multiple_structure(new \external_single_structure([ "completion" => self::badge_completion_structure(VALUE_OPTIONAL),
"types" => new \external_multiple_structure(new \external_single_structure([
'criteria' => new \external_multiple_structure(new \external_single_structure([
"title" => new \external_value(PARAM_TEXT, 'criterion title'),
"details" => new \external_value(PARAM_TEXT, 'criterion details'),
"completed" => new \external_value(PARAM_BOOL, 'criterion is completed or not'),
]),'specific criteria'),
"aggregation" => new \external_value(PARAM_TEXT, 'any|all'),
"count" => new \external_value(PARAM_INT, 'effective number of critera for type progress'),
"progress" => new \external_value(PARAM_INT, 'effective number of completed criteria for type progress'),
"fraction" => new \external_value(PARAM_FLOAT, 'fraction of completed type criteria as float'),
]), "criteria types"),
"aggregation" => new \external_value(PARAM_TEXT, 'any|all'),
"count" => new \external_value(PARAM_INT, 'total number of critera for progress'),
"progress" => new \external_value(PARAM_INT, 'number of completed criteria for progress'),
"fraction" => new \external_value(PARAM_FLOAT, 'fraction of completed criteria as float'),
]),'badge criteria', VALUE_OPTIONAL),
"description" => new \external_value(PARAM_TEXT, 'badge description'), "description" => new \external_value(PARAM_TEXT, 'badge description'),
"imageurl" => new \external_value(PARAM_TEXT, 'url of badge image'), "imageurl" => new \external_value(PARAM_TEXT, 'url of badge image'),
"issued" => new \external_value(PARAM_BOOL, 'badge is issued'), "issued" => new \external_value(PARAM_BOOL, 'badge is issued'),
@ -224,6 +210,7 @@ class badgeinfo {
'infolink' => (new \moodle_url('/badges/overview.php', ['id' => $this->badge->id]))->out(false), 'infolink' => (new \moodle_url('/badges/overview.php', ['id' => $this->badge->id]))->out(false),
]; ];
if ($issued) { if ($issued) {
$issueinfo = $DB->get_record('badge_issued', array('badgeid' => $this->badge->id, 'userid' => $userid)); $issueinfo = $DB->get_record('badge_issued', array('badgeid' => $this->badge->id, 'userid' => $userid));
$badge['dateissued'] = date("Y-m-d", $issueinfo->dateissued); $badge['dateissued'] = date("Y-m-d", $issueinfo->dateissued);
@ -234,10 +221,40 @@ class badgeinfo {
$badge['issuedlink'] = (new \moodle_url('/badges/badge.php', ['hash' => $issueinfo->uniquehash]))->out(false); $badge['issuedlink'] = (new \moodle_url('/badges/badge.php', ['hash' => $issueinfo->uniquehash]))->out(false);
} }
$f = fopen("/tmp/badgedebug","a");
fputs($f,date("Y-m-d H:M:S")." Badge info\n");
fputs($f,print_r($badge,true)."\n\n");
fclose($f);
return $badge; return $badge;
} }
protected function badge_completion_data($userid) {
protected function badge_completion_structure($value) {
return new \external_single_structure([
"types" => new \external_multiple_structure(new \external_single_structure([
'criteria' => new \external_multiple_structure(new \external_single_structure([
"title" => new \external_value(PARAM_RAW, 'criterion title'),
"description" => new \external_value(PARAM_RAW, 'criterion description'),
"requirements" => new \external_multiple_structure(new \external_single_structure([
"title" => new \external_value(PARAM_RAW, 'requirment title'),
"completed" => new \external_value(PARAM_BOOL, 'criterion is completed or not'),
]), "criterion specific requirements", VALUE_OPTIONAL),
"completed" => new \external_value(PARAM_BOOL, 'criterion is completed or not'),
]),'specific criteria'),
"type" => new \external_value(PARAM_TEXT, 'any|all'),
"aggregation" => new \external_value(PARAM_TEXT, 'any|all'),
"count" => new \external_value(PARAM_INT, 'effective number of critera for type progress'),
"progress" => new \external_value(PARAM_INT, 'effective number of completed criteria for type progress'),
"fraction" => new \external_value(PARAM_FLOAT, 'fraction of completed type criteria as float'),
]), "criteria types",VALUE_OPTIONAL),
"aggregation" => new \external_value(PARAM_TEXT, 'any|all'),
"count" => new \external_value(PARAM_INT, 'total number of critera for progress'),
"progress" => new \external_value(PARAM_INT, 'number of completed criteria for progress'),
"fraction" => new \external_value(PARAM_FLOAT, 'fraction of completed criteria as float'),
],'badge completion information', $value);
}
protected function badge_completion_data($userid) : array {
$count = 0; $count = 0;
$progress = 0; $progress = 0;
@ -245,27 +262,31 @@ class badgeinfo {
$badgeagg = $this->badge->get_aggregation_method(); $badgeagg = $this->badge->get_aggregation_method();
$types = []; $types = [];
foreach ($this->badge->criteria as $type => $c) { $f = fopen("/tmp/badgedebug","a");
fputs($f,date("Y-m-d H:M:S")." Badge criteria\n");
fputs($f,print_r($this->badge->criteria,true)."\n\n");
fclose($f);
foreach ($this->badge->criteria as $type => $bc) {
$typeagg = $this->badge->get_aggregation_method($type); $typeagg = $this->badge->get_aggregation_method($type);
$typecrit = []; $typecrit = $this->get_award_subcriteria($bc,$userid);
$typecount = count($typecrit);
$typeprogress = 0; $typeprogress = 0;
$typecount = count($c); foreach ($typecrit as $subcrit) {
foreach ($c as $bc) { // Quickly check if the subcriteria are completed.
$iscompleted = $bc->review($userid); if ($subcrit["completed"]) {
$typecrit[] = [
"title" => $bc->get_title(),
"details" => $bc->get_details(),
"completed" => $iscompleted,
];
if ($iscompleted) {
$typeprogress++; $typeprogress++;
} }
} }
// Determine how to process the progress data, depending on the TYPE's aggregation.
if ($typeagg == BADGE_CRITERIA_AGGREGATION_ANY) { if ($typeagg == BADGE_CRITERIA_AGGREGATION_ANY) {
$typecount = 1; $typecount = 1;
$typeprogress = ($typeprogress > 0)?1:0; $typeprogress = ($typeprogress > 0)?1:0;
} }
// Determine how to patch this data into the overall progress numbers, depending on the OVERALL aggregation.
if($badgeagg == BADGE_CRITERIA_AGGREGATION_ANY) { if($badgeagg == BADGE_CRITERIA_AGGREGATION_ANY) {
/* If ANY completion overall, count only the criteria type with the highest completion percentage -. /* If ANY completion overall, count only the criteria type with the highest completion percentage -.
Overwrite data if current type is more complete */ Overwrite data if current type is more complete */
@ -282,6 +303,7 @@ class badgeinfo {
} }
$typeinfo = [ $typeinfo = [
'type' => $type,
'aggregation' => ($typeagg == BADGE_CRITERIA_AGGREGATION_ALL)?"all":"any", 'aggregation' => ($typeagg == BADGE_CRITERIA_AGGREGATION_ALL)?"all":"any",
'criteria' => $typecrit, 'criteria' => $typecrit,
'count' => $typecount, 'count' => $typecount,
@ -291,7 +313,7 @@ class badgeinfo {
$types[] = $typeinfo; $types[] = $typeinfo;
} }
$c = [ return [
"types" => $types, "types" => $types,
"aggregation" => ($typeagg == BADGE_CRITERIA_AGGREGATION_ALL)?"all":"any", "aggregation" => ($typeagg == BADGE_CRITERIA_AGGREGATION_ALL)?"all":"any",
"count" => $count, "count" => $count,
@ -317,4 +339,380 @@ class badgeinfo {
return $issuecount; return $issuecount;
} }
/**
* Gets the module instance from the database and returns it.
* If no module instance exists this function returns false.
*
* @return stdClass|bool
*/
private static function get_mod_instance($cmid) {
global $DB;
$rec = $DB->get_record_sql("SELECT md.name
FROM {course_modules} cm,
{modules} md
WHERE cm.id = ? AND
md.id = cm.module", array($cmid));
if ($rec) {
return get_coursemodule_from_id($rec->name, $cmid);
} else {
return null;
}
}
/**
* Gets role name.
* If no such role exists this function returns null.
*
* @return string|null
*/
private static function get_role_name($rid) {
global $DB, $PAGE;
$rec = $DB->get_record('role', array('id' => $rid));
if ($rec) {
return role_get_name($rec, $PAGE->context, ROLENAME_BOTH);
} else {
return null;
}
}
/**
* [Description for get_award_subcriteria]
*
* @param award_criteria $crit
*
* @return array
*
*/
protected function get_award_subcriteria(award_criteria $crit, $userid = null) : array {
global $DB;
$list = [];
if ($crit->criteriatype == BADGE_CRITERIA_TYPE_ACTIVITY) {
foreach ($crit->params as $p) {
$mod = self::get_mod_instance($p["module"]);
if(!$mod) {
$title = get_string('error:nosuchmod', 'badges');
$description = get_string('error:nosuchmod', 'badges');
} else {
$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')));
}
}
$subcrit = [
"title" => $title,
"description" => $description,
"requirements" => [
'completion' => [
'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'))),
];
}
if(isset($userid)) {
$info = new \completion_info($crit->course);
$cm = new \stdClass();
$cm->id = $p['module'];
$data = $info->get_data($cm, false, $userid);
// Successfull completion states depend on the completion settings.
if (isset($data->passgrade)) {
// Passing grade is required. Don't issue a badge when state is COMPLETION_COMPLETE_FAIL.
$completionstates = [COMPLETION_COMPLETE, COMPLETION_COMPLETE_PASS];
} else {
// Any grade is required. Issue a badge even when state is COMPLETION_COMPLETE_FAIL.
$completionstates = [COMPLETION_COMPLETE, COMPLETION_COMPLETE_PASS, COMPLETION_COMPLETE_FAIL];
}
$modcompleted = in_array($data->completionstate, $completionstates);
$subcrit["requirements"]["completion"]["completed"] = $modcompleted;
$check_date = true;
if (isset($p["bydate"])) {
$date = $data->timemodified;
$check_date = ($date <= $p['bydate']);
$subcrit["requirements"]["completion"]["completed"] = $check_date;
}
$subcrit["completed"] = $modcompleted && $check_date;
}
$list[] = $subcrit;
}
} else if ($crit->criteriatype == BADGE_CRITERIA_TYPE_MANUAL) {
foreach ($crit->params as $p) {
$role = self::get_role_name($p['role']);
if(!$role) {
$title = get_string('error:nosuchrole', 'badges');
$description = get_string('error:nosuchrole', 'badges');
} else {
$title = $role;
$description = $role;
}
$subcrit = [
"title" => $title,
"description" => $description,
"requirements" => []
];
if(isset($userid)) {
$crit = $DB->get_record('badge_manual_award', array('issuerrole' => $p['role'], 'recipientid' => $userid, 'badgeid' => $crit->badgeid));
$subcrit["completed"] = $crit;
}
$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
} else {
$params = $crit->params;
}
foreach ($params as $p) {
$course = get_course($p["course"]);
if(!$course) {
$description = get_string('error:nosuchcourse',"badges");
} 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')));
}
if (isset($p['grade'])) {
$description .= get_string('criteria_descr_grade', 'badges', $p['grade']);
}
}
$subcrit = [
"title" => $course->fullname,
"description" => $description,
"requirements" => [
'completion' => [
'title' => get_string('coursecompleted','core'),
]
]
];
if (isset($p["grade"])) {
$subcrit["requirements"]["grade"] = [
'title' => get_string('criteria_descr_bydate','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'))),
];
}
if(isset($userid)) {
$info = new \completion_info($course);
$coursecompleted = $info->is_course_complete($userid);
$subcrit["requirements"]["completion"]["completed"] = $coursecompleted;
$check_grade = true;
if (isset($p["grade"])) {
$grade = \grade_get_course_grade($userid,$course->id);
$check_grade = ($grade->grade >= $p['grade']);
$subcrit["requirements"]["completion"]["completed"] = $check_grade;
}
$check_date = true;
if (isset($p["bydate"])) {
$completion = new \completion_completion(["userid" => $userid, "course" => $course->id]);
$check_date = ($completion->timecompleted <= $p["bydate"]);
$subcrit["requirements"]["completion"]["completed"] = $check_date;
}
$subcrit["completed"] = $coursecompleted && $check_grade && $check_date;
}
$list[] = $subcrit;
}
} else if ($crit->criteriatype == BADGE_CRITERIA_TYPE_PROFILE) {
foreach ($crit->params as $p) {
if (is_numeric($p['field'])) {
$fields = profile_get_custom_fields();
// Get formatted field name if such field exists.
$fieldname = isset($fields[$p['field']]->name) ?
format_string($fields[$p['field']]->name) : null;
} else {
$fieldname = \core_user\fields::get_display_name($p['field']);
}
if(!$fieldname) {
$title = get_string('error:nosuchfield', 'badges');
$description = get_string('error:nosuchfield', 'badges');
} else {
$title = $fieldname;
$description = $fieldname;
}
$subcrit = [
"title" => $title,
"description" => $description,
"requirements" => []
];
if(isset($userid)) {
$join = '';
$where = '';
$sqlparams = [ 'userid'=> $userid];
if (is_numeric($p['field'])) {
// This is a custom field.
$join .= " LEFT JOIN {user_info_data} uid ON uid.userid = u.id AND uid.fieldid = :fieldid ";
$sqlparams["fieldid"] = $p['field'];
$where = "uid.id IS NOT NULL";
} else if (in_array($p['field'], $this->allowed_default_fields)) {
// This is a valid field from {user} table.
if ($p['field'] == 'picture') {
// The picture field is numeric and requires special handling.
$where = "u.{$p['field']} != 0";
} else {
$where = $DB->sql_isnotempty('u', "u.{$p['field']}", false, true);
}
}
$sql = "SELECT 1 FROM {user} u " . $join . " WHERE u.id = :userid $where";
$completed = $DB->record_exists_sql($sql, $sqlparams);
$subcrit["completed"] = $completed;
}
$list[] = $subcrit;
}
} else if ($crit->criteriatype == BADGE_CRITERIA_TYPE_BADGE) {
foreach ($crit->params as $p) {
$badgename = $DB->get_field('badge', 'name', array('id' => $p['badge']));
if(!$badgename) {
$title = get_string('error:nosuchbadge', 'badges');
$description = get_string('error:nosuchbadge', 'badges');
} else {
$title = \html_writer::tag('b', '"' . $badgename . '"');;
$description = \html_writer::tag('b', '"' . $badgename . '"');;
}
$subcrit = [
"title" => $title,
"description" => $description,
"requirements" => []
];
if(isset($userid)) {
$badge = $DB->get_record('badge', array('id' => $p['badge']));
// See if the user has earned this badge.
if($badge) {
$awarded = $DB->get_record('badge_issued', array('badgeid' => $p['badge'], 'userid' => $userid));
} else {
$awarded = false;
}
$subcrit["completed"] = $awarded;
}
$list[] = $subcrit;
}
} else if ($crit->criteriatype == BADGE_CRITERIA_TYPE_COHORT) {
foreach ($crit->params as $p) {
$cohortname = $DB->get_field('cohort', 'name', array('id' => $p['cohort']));
if(!$cohortname) {
$title = get_string('error:nosuchcohort', 'badges');
$description = get_string('error:nosuchcohort', 'badges');
} else {
$title = \html_writer::tag('b', '"' . format_string($cohortname, true) . '"');
$description = \html_writer::tag('b', '"' . format_string($cohortname, true) . '"');
}
$subcrit = [
"title" => $title,
"description" => $description,
"requirements" => []
];
if(isset($userid)) {
$cohort = $DB->get_record('cohort', array('id' => $p['cohort']));
if(\cohort_is_member($cohort->id, $userid)) {
$ismember = true;
} else {
$ismember = false;
}
$subcrit["completed"] = $ismember;
}
$list[] = $subcrit;
}
} else if ($crit->criteriatype == BADGE_CRITERIA_TYPE_COMPETENCY) {
foreach ($crit->params as $p) {
$competency = new \core_competency\competency($p['competency']);
$competency->set('description', ''); // Will use 'short' description.
/* Below must be one of the most convoluted ways of calling tool_lp_render_competency_summary.
Since I'm pretty sure it's not an actually good idea to implement <plugin>_render_competency_summary
in a custom plugin, even though the system appearently allows you to add competency descriptions,
it's probably a case of premature optimization. Still, since this is the way that
'badges/criteria/award_criteria_competency.php does it, we'll copy its example, to avoid
differences in display....
*/
$output = [];
// Render the competency even if competencies are not currently enabled.
\core_competency\api::skip_enabled();
if ($pluginsfunction = get_plugins_with_function('render_competency_summary')) {
foreach ($pluginsfunction as $plugintype => $plugins) {
foreach ($plugins as $pluginfunction) {
$output[] = $pluginfunction($competency, $competency->get_framework(), false, false, true);
}
}
}
\core_competency\api::check_enabled();
if(count($output) < 1) {
$title = get_string('error:nosuchcompetency', 'local_treestudyplan');
$description = get_string('error:nosuchcompetency', 'local_treestudyplan');
} else {
/* Might as wel implode the output, just in case someone was actually stupid enough to implement
<plugin>_render_competency_summary in a custom plugin...
*/
$title = implode(" ",$output);
$description = implode(" ",$output);
}
$subcrit = [
"title" => $title,
"description" => $description,
"requirements" => []
];
if(isset($userid)) {
/* Slightly modified from award_criteria_competency.php::review()
to use criteria api class instead of direct calls....
*/
$proficiency = false;
$badge = $DB->get_record('badge', array('id' => $crit->badgeid));
if ($badge->type == BADGE_TYPE_SITE) {
$uc = \core_competency\api::get_user_competency($userid,$p['competency']);
$proficiency = $uc->get('proficiency');
} else if ($badge->type == BADGE_TYPE_COURSE) {
$uc = \core_competency\api::get_user_competency_in_course($badge->courseid,$userid,$p['competency']);
$proficiency = $uc->get('proficiency');
}
$subcrit["completed"] = $proficiency;
}
$list[] = $subcrit;
}
}
return $list;
}
} }

View file

@ -354,3 +354,5 @@ $string["not_enrolled"] = "Not enrolled";
$string["warning_incomplete_pass"] = 'Completion data was reset for this activity, causing your passing grade not to be registered. Ask your teacher to re-grade this grade to remedy this.'; $string["warning_incomplete_pass"] = 'Completion data was reset for this activity, causing your passing grade not to be registered. Ask your teacher to re-grade this grade to remedy this.';
$string["warning_incomplete_nograderq"] = 'Because the grade is not marked as a requirement, your passing grade is not registering completion.'; $string["warning_incomplete_nograderq"] = 'Because the grade is not marked as a requirement, your passing grade is not registering completion.';
$string["error:nosuchcompetency"] = 'Warning: This competency no longer exists';

View file

@ -352,3 +352,5 @@ $string["not_enrolled"] = "Niet ingeschreven";
$string["warning_incomplete_pass"] = 'Door een storing wordt je voldoende resultaat niet als voltooid meegenomen. Vraag je docent om opnieuw je beoordeling vast te stellen. '; $string["warning_incomplete_pass"] = 'Door een storing wordt je voldoende resultaat niet als voltooid meegenomen. Vraag je docent om opnieuw je beoordeling vast te stellen. ';
$string["warning_incomplete_nograderq"] = 'Omdat het behalen van een cijfer niet als voorwaarde is aangegeven, telt het behalen van een voldoende resultaat niet mee voor voitooiing'; $string["warning_incomplete_nograderq"] = 'Omdat het behalen van een cijfer niet als voorwaarde is aangegeven, telt het behalen van een voldoende resultaat niet mee voor voitooiing';
$string["error:nosuchcompetency"] = 'Waarschuwing: deze competentie is niet langer beschikbaar. ';