From 3fcf0095eae93a0e6d222614dac84b5a240b20f1 Mon Sep 17 00:00:00 2001 From: PMKuipers Date: Sat, 28 Oct 2023 21:49:48 +0200 Subject: [PATCH] Implemented badge subriteria parsing... --- classes/badgeinfo.php | 458 +++++++++++++++++++++++++++++--- lang/en/local_treestudyplan.php | 2 + lang/nl/local_treestudyplan.php | 2 + 3 files changed, 432 insertions(+), 30 deletions(-) diff --git a/classes/badgeinfo.php b/classes/badgeinfo.php index ecfeb8f..4e66aec 100644 --- a/classes/badgeinfo.php +++ b/classes/badgeinfo.php @@ -24,6 +24,8 @@ namespace local_treestudyplan; defined('MOODLE_INTERNAL') || die(); require_once($CFG->libdir.'/externallib.php'); + +use award_criteria; use \core_badges\badge; /** @@ -166,23 +168,7 @@ class badgeinfo { "id" => new \external_value(PARAM_INT, 'id of badge'), "infolink" => new \external_value(PARAM_TEXT, 'badge issue information link', VALUE_OPTIONAL), "name" => new \external_value(PARAM_TEXT, 'badge name'), - "completion" => new \external_multiple_structure(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_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), + "completion" => self::badge_completion_structure(VALUE_OPTIONAL), "description" => new \external_value(PARAM_TEXT, 'badge description'), "imageurl" => new \external_value(PARAM_TEXT, 'url of badge image'), "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), ]; + if ($issued) { $issueinfo = $DB->get_record('badge_issued', array('badgeid' => $this->badge->id, 'userid' => $userid)); $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); } + $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; } - 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; $progress = 0; @@ -245,27 +262,31 @@ class badgeinfo { $badgeagg = $this->badge->get_aggregation_method(); $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); - $typecrit = []; + $typecrit = $this->get_award_subcriteria($bc,$userid); + + $typecount = count($typecrit); $typeprogress = 0; - $typecount = count($c); - foreach ($c as $bc) { - $iscompleted = $bc->review($userid); - $typecrit[] = [ - "title" => $bc->get_title(), - "details" => $bc->get_details(), - "completed" => $iscompleted, - ]; - if ($iscompleted) { + foreach ($typecrit as $subcrit) { + // Quickly check if the subcriteria are completed. + if ($subcrit["completed"]) { $typeprogress++; } } + + // Determine how to process the progress data, depending on the TYPE's aggregation. if ($typeagg == BADGE_CRITERIA_AGGREGATION_ANY) { $typecount = 1; $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 ANY completion overall, count only the criteria type with the highest completion percentage -. Overwrite data if current type is more complete */ @@ -282,6 +303,7 @@ class badgeinfo { } $typeinfo = [ + 'type' => $type, 'aggregation' => ($typeagg == BADGE_CRITERIA_AGGREGATION_ALL)?"all":"any", 'criteria' => $typecrit, 'count' => $typecount, @@ -291,7 +313,7 @@ class badgeinfo { $types[] = $typeinfo; } - $c = [ + return [ "types" => $types, "aggregation" => ($typeagg == BADGE_CRITERIA_AGGREGATION_ALL)?"all":"any", "count" => $count, @@ -317,4 +339,380 @@ class badgeinfo { 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 _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 + _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; + } + } diff --git a/lang/en/local_treestudyplan.php b/lang/en/local_treestudyplan.php index 667b2c5..e434c9b 100644 --- a/lang/en/local_treestudyplan.php +++ b/lang/en/local_treestudyplan.php @@ -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_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'; \ No newline at end of file diff --git a/lang/nl/local_treestudyplan.php b/lang/nl/local_treestudyplan.php index a466cb9..6d7ab04 100644 --- a/lang/nl/local_treestudyplan.php +++ b/lang/nl/local_treestudyplan.php @@ -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_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. '; \ No newline at end of file