Polished up badge view for users

This commit is contained in:
PMKuipers 2023-11-01 23:47:54 +01:00
parent 3fcf0095ea
commit dd7fa5f620
9 changed files with 191 additions and 86 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -96,6 +96,8 @@ export default {
dateexpire: "dateexpire",
badgeinfo: "badgeinfo",
badgeissuedstats: "badgeissuedstats",
completion_incomplete: "completion_incomplete_badge",
completion_completed: "completion_completed_badge",
},
course: {
completion_incomplete: "completion_incomplete",
@ -2261,7 +2263,7 @@ export default {
},
data() {
return {
txt: strings
text: strings.badge,
};
},
computed: {
@ -2293,17 +2295,60 @@ export default {
} else {
return ""; // no path
}
},
arcpath_progress(){
if(this.value.badge.completion){
const fraction = this.value.badge.completion.progress/this.value.badge.completion.count;
return this.arcpath(0,fraction);
} else {
return ""; // no path
}
},
badgeinprogress(){
return ( this.value.badge.issued
|| this.teachermode
|| (this.value.badge.completion
&& this.value.badge.completion.progress >= this.value.badge.completion.count)
);
}
},
methods: {
arcpath(start, end) {
const r = 44;
const t1 = start * 2*π;
const Δ = end * 2*π;
const Δ = (end * 2*π-0.01);
return svgarcpath([50,50],[r,r],[t1,Δ], 1.5*π);
},
addTargetBlank(html) {
const m = /^([^<]*\< *a +)(.*)/.exec(html);
if(m){
return `${m[1]} target="_blank" ${m[2]}`;
} else {
return html;
}
},
completion_icon_rq(complete) {
if (complete) {
return "check-square-o";
} else {
return "square-o";
}
},
completion_icon(complete) {
if (complete) {
return "check-circle";
} else {
return "times-circle";
}
},
status(complete) {
if (complete) {
return "complete";
} else {
return "incomplete";
}
}
},
template: `
<div :class="'r-item-badge r-item-filter r-completion-'+completion" v-b-tooltip.hover :title="value.badge.name">
@ -2318,12 +2363,18 @@ export default {
</template>
<circle v-else-if="value.badge.issued" cx="50" cy="50" r="46"
style="stroke: currentcolor; stroke-width: 4; fill: currentcolor; fill-opacity: 0.5;"/>
<template v-else-if="value.badge.completion">
<circle cx="50" cy="50" r="44"
style="stroke: #ccc; stroke-width: 8; fill: #ddd; fill-opacity: 0.8;"/>
<path :d="arcpath_progress"
:style="'stroke-width: 8; stroke: var(--info); fill: none;'"/>
</template>
<circle v-else cx="50" cy="50" r="46"
stroke-dasharray="6 9"
style="stroke: #999; stroke-width: 6; fill: #ddd; fill-opacity: 0.8;"/>
<image class="badge-image" clip-path="circle() fill-box"
:href="value.badge.imageurl" x="12" y="12" width="76" height="76"
:style="(value.badge.issued||teachermode)?'':'opacity: 0.4;'" />
:style="(badgeinprogress)?'':'opacity: 0.4;'" />
</svg></a>
<b-modal
@ -2343,9 +2394,9 @@ export default {
</div>
<div class="r-course-detail-header-right" v-if="!teachermode">
<div class="r-completion-detail-header">
{{ txt.completion['completion_'+completion] }}
{{ text['completion_'+completion] }}
<i v-b-popover.hover :class="'fa fa-'+issued_icon+' r-completion-'+completion"
:title="txt.completion['completion_'+completion]"></i>
:title="text['completion_'+completion]"></i>
</div>
</div>
</template>
@ -2355,29 +2406,59 @@ export default {
</b-col><b-col cols="9">
<p>{{value.badge.description}}</p>
<ul v-if="value.badge.issued" class="list-unstyled pt-1 mb-1 border-grey border-top">
<li><strong><i class="fa fa-calendar-check-o r-completion-complete-pass"></i>
{{txt.badge.dateissued}}:</strong> {{ value.badge.dateissued }}</li>
<li v-if='value.badge.dateexpired'
><strong><i class="fa fa-calendar-times-o r-completion-complete"></i>
{{txt.badge.dateexpired}}:</strong> {{ value.badge.dateexpired }}</li>
<li><strong><i class="fa fa-share-alt r-completion-complete-pass"></i>
<a :href="value.badge.issuedlink">{{txt.badge.share_badge}}</a></strong> </li>
<li><strong><i class="fa fa-calendar-check-o r-completion-complete-pass"></i>
{{text.dateissued}}:</strong> {{ value.badge.dateissued }}</li>
<li v-if='value.badge.dateexpired'
><strong><i class="fa fa-calendar-times-o r-completion-complete"></i>
{{text.dateexpired}}:</strong> {{ value.badge.dateexpired }}</li>
<li><strong><i class="fa fa-share-alt r-completion-complete-pass"></i>
<a :href="value.badge.issuedlink">{{text.share_badge}}</a></strong> </li>
</ul>
<table v-if='value.badge.completion && !value.badge.issued' class="r-item-course-grade-details mb-2">
<tr v-if="value.badge.completion.types.length > 1">
<th colspan="2"><span v-html="value.badge.completion.title"></span></th>
</tr>
<template v-for='cgroup in value.badge.completion.types' >
<tr>
<td colspan="2" v-if="value.badge.completion.types.length > 1"
><span v-html="cgroup.title"></span></td>
<th colspan="2" v-else><span v-html="cgroup.title"></span></th>
</tr>
<template v-for='ci in cgroup.criteria'>
<tr>
<td class="pl-3"><span v-if='guestmode'><span v-html="ci.title"></span></span>
<a target='_blank' v-else-if='ci.link' :href='ci.link'
><span v-html="ci.title"></span></a>
<span v-else><span v-html="ci.title"></span></span>
<td><i :class="'fa fa-'+completion_icon(ci.completed)+' r-completion-'+status(ci.completed)"
:title="text['completion_'+status(ci.completed)]"></i>
</td>
</tr>
<template v-if="ci.requirements.length > 1">
<tr v-for="rq in ci.requirements">
<td class="pl-4" colspan="2"
><i :class="'fa fa-'+completion_icon_rq(rq.completed)+' r-completion-incomplete'"
:title="text['completion_'+status(rq.completed)]"></i>
<span class="t-badge-criteria-requirement"><span v-html="rq.title"></span></span></td>
</tr>
</template>
</template>
</template>
</table>
<ul class="list-unstyled w-100 border-grey border-top border-bottom pt-1 pb-1 mb-1"
v-if="value.badge.criteria"><li v-for="crit in value.badge.criteria"
><span v-html='crit'></span></li></ul>
<p v-if="(!guestmode)"><strong><i class="fa fa-link"></i>
<a :href="value.badge.infolink" target="_blank"
>{{ txt.badge.badgeinfo }}</a></strong></p>
>{{ text.badgeinfo }}</a></strong></p>
<p v-if="teachermode && !guestmode"
>{{txt.badge.badgeissuedstats}}:<br>
>{{text.badgeissuedstats}}:<br>
<r-completion-bar v-model="issuestats" :width="150" :height="15"></r-completion-bar>
</p>
</b-col></b-row>
</b-container>
</b-modal>
</div>
`,
});
},

View File

@ -27,6 +27,7 @@ require_once($CFG->libdir.'/externallib.php');
use award_criteria;
use \core_badges\badge;
use moodle_url;
/**
* Handle badge information in the same style as the other classes
@ -176,6 +177,7 @@ class badgeinfo {
"dateexpire" => new \external_value(PARAM_TEXT, 'date the badge will expire', VALUE_OPTIONAL),
"uniquehash" => new \external_value(PARAM_TEXT, 'badge issue hash', VALUE_OPTIONAL),
"issuedlink" => new \external_value(PARAM_TEXT, 'badge issue information link', VALUE_OPTIONAL),
"active" => new \external_value(PARAM_BOOL, 'badge is available'),
], "Badge info", $value);
}
@ -208,6 +210,7 @@ class badgeinfo {
'completion' => $this->badge_completion_data($userid),
'issued' => $issued,
'infolink' => (new \moodle_url('/badges/overview.php', ['id' => $this->badge->id]))->out(false),
"active" => $this->badge->is_active(),
];
@ -235,13 +238,14 @@ class badgeinfo {
'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'),
"link"=> new \external_value(PARAM_RAW, 'link to criterion resource',VALUE_OPTIONAL),
"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'),
"completed" => new \external_value(PARAM_BOOL, 'criterion is completed or not', VALUE_OPTIONAL),
]), "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'),
"title" => new \external_value(PARAM_RAW, 'type title'),
"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'),
@ -251,6 +255,7 @@ class badgeinfo {
"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'),
"title" => new \external_value(PARAM_RAW, 'completion title'),
],'badge completion information', $value);
}
@ -262,60 +267,61 @@ class badgeinfo {
$badgeagg = $this->badge->get_aggregation_method();
$types = [];
$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 = $this->get_award_subcriteria($bc,$userid);
$typecount = count($typecrit);
$typeprogress = 0;
foreach ($typecrit as $subcrit) {
// Quickly check if the subcriteria are completed.
if ($subcrit["completed"]) {
$typeprogress++;
if ($type != BADGE_CRITERIA_TYPE_OVERALL) {
$typeagg = $this->badge->get_aggregation_method($type);
$typecrit = $this->get_award_subcriteria($bc,$userid);
$typecount = count($typecrit);
$typeprogress = 0;
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 */
$typefraction = $typeprogress / $typecount;
if ($typefraction > $fraction) {
$fraction = $typefraction;
$count = $typecount;
$progress = $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;
}
} else {
/* If ALL completion overall, just add it up to the total */
$count += $typecount;
$progress += $typeprogress;
}
$typeinfo = [
'type' => $type,
'aggregation' => ($typeagg == BADGE_CRITERIA_AGGREGATION_ALL)?"all":"any",
'criteria' => $typecrit,
'count' => $typecount,
'progress' => $typeprogress,
"fraction" => $typefraction,
];
$types[] = $typeinfo;
// 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 */
$typefraction = ($typecount > 0)?($typeprogress / $typecount):0;
if ($typefraction > $fraction || ($fraction == 0 && $typecount > $count)) {
$fraction = $typefraction;
$count = $typecount;
$progress = $typeprogress;
}
} else {
/* If ALL completion overall, just add it up to the total */
$count += $typecount;
$progress += $typeprogress;
}
$aggrgation_handle = ($typeagg == BADGE_CRITERIA_AGGREGATION_ALL)?"all":"any";
$typeinfo = [
'title' => ucfirst(get_string("criteria_descr_$type","badges", get_string($aggrgation_handle,"core"))),
'aggregation' => $aggrgation_handle,
'criteria' => $typecrit,
'count' => $typecount,
'progress' => $typeprogress,
"fraction" => $typefraction,
];
$types[] = $typeinfo;
}
}
$aggrgation_handle = ($badgeagg == BADGE_CRITERIA_AGGREGATION_ALL)?"all":"any";
return [
"types" => $types,
"aggregation" => ($typeagg == BADGE_CRITERIA_AGGREGATION_ALL)?"all":"any",
"title" => ucfirst(get_string("criteria_descr_0","badges", mb_strtolower(get_string($aggrgation_handle,"core")))),
"aggregation" => $aggrgation_handle,
"count" => $count,
"progress" => $progress,
"fraction" => $fraction,
@ -388,7 +394,7 @@ class badgeinfo {
*
*/
protected function get_award_subcriteria(award_criteria $crit, $userid = null) : array {
global $DB;
global $DB, $CFG;
$list = [];
if ($crit->criteriatype == BADGE_CRITERIA_TYPE_ACTIVITY) {
@ -444,7 +450,7 @@ class badgeinfo {
if (isset($p["bydate"])) {
$date = $data->timemodified;
$check_date = ($date <= $p['bydate']);
$subcrit["requirements"]["completion"]["completed"] = $check_date;
$subcrit["requirements"]["bydate"]["completed"] = $check_date;
}
$subcrit["completed"] = $modcompleted && $check_date;
@ -471,7 +477,7 @@ class badgeinfo {
if(isset($userid)) {
$crit = $DB->get_record('badge_manual_award', array('issuerrole' => $p['role'], 'recipientid' => $userid, 'badgeid' => $crit->badgeid));
$subcrit["completed"] = $crit;
$subcrit["completed"] = $crit !== false;
}
$list[] = $subcrit;
}
@ -500,17 +506,18 @@ class badgeinfo {
$subcrit = [
"title" => $course->fullname,
"link" => (new moodle_url($CFG->wwwroot."/course/view.php",["id" => $course->id]))->out(),
"description" => $description,
"requirements" => [
'completion' => [
'title' => get_string('coursecompleted','core'),
'title' => get_string('coursecompleted','completion'),
]
]
];
if (isset($p["grade"])) {
$subcrit["requirements"]["grade"] = [
'title' => get_string('criteria_descr_bydate','badges',$p["grade"]),
'title' => get_string('criteria_descr_grade','badges',$p["grade"]),
];
}
@ -521,21 +528,24 @@ class badgeinfo {
}
if(isset($userid)) {
$info = new \completion_info($course);
$coursecompleted = $info->is_course_complete($userid);
$subcrit["requirements"]["completion"]["completed"] = $coursecompleted;
$coursecompletion = new \completion_completion(["userid" => $userid, "course" => $course->id]);
$coursecompleted = $coursecompletion->is_complete();
$f = fopen("/tmp/debug","a+");
fputs($f,$course->fullname." ".($coursecompleted?"(COMPLETED)":"(NOT completed)")."\n");
fputs($f,print_r($coursecompletion,true));
fclose($f);
$subcrit["requirements"]["completion"]["completed"] = (bool) $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;
$subcrit["requirements"]["grade"]["completed"] = (bool) $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;
$check_date = ((bool) $coursecompletion->timecompleted) && ($coursecompletion->timecompleted <= $p["bydate"]);
$subcrit["requirements"]["bydate"]["completed"] = (bool) $check_date;
}
$subcrit["completed"] = $coursecompleted && $check_grade && $check_date;
@ -590,7 +600,7 @@ class badgeinfo {
$sql = "SELECT 1 FROM {user} u " . $join . " WHERE u.id = :userid $where";
$completed = $DB->record_exists_sql($sql, $sqlparams);
$subcrit["completed"] = $completed;
$subcrit["completed"] = (bool) $completed;
}
$list[] = $subcrit;
}
@ -601,13 +611,14 @@ class badgeinfo {
$title = get_string('error:nosuchbadge', 'badges');
$description = get_string('error:nosuchbadge', 'badges');
} else {
$title = \html_writer::tag('b', '"' . $badgename . '"');;
$title = $badgename;
$description = \html_writer::tag('b', '"' . $badgename . '"');;
}
$subcrit = [
"title" => $title,
"description" => $description,
"link" => (new \moodle_url($CFG->wwwroot."/badges/overview.php",["id" => $p["badge"]]))->out(),
"requirements" => []
];
@ -616,10 +627,11 @@ class badgeinfo {
// See if the user has earned this badge.
if($badge) {
$awarded = $DB->get_record('badge_issued', array('badgeid' => $p['badge'], 'userid' => $userid));
$awarded = isset($awarded);
} else {
$awarded = false;
}
$subcrit["completed"] = $awarded;
$subcrit["completed"] = (bool)$awarded;
}
$list[] = $subcrit;
}
@ -642,11 +654,7 @@ class badgeinfo {
if(isset($userid)) {
$cohort = $DB->get_record('cohort', array('id' => $p['cohort']));
if(\cohort_is_member($cohort->id, $userid)) {
$ismember = true;
} else {
$ismember = false;
}
$ismember = (bool) \cohort_is_member($cohort->id, $userid);
$subcrit["completed"] = $ismember;
}
$list[] = $subcrit;
@ -706,7 +714,7 @@ class badgeinfo {
$proficiency = $uc->get('proficiency');
}
$subcrit["completed"] = $proficiency;
$subcrit["completed"] = (bool) $proficiency;
}
$list[] = $subcrit;
}

View File

@ -707,6 +707,10 @@
left: 50%;
transform: translate(-50%, -50%);
}
.path-local-treestudyplan .t-badge-criteria-requirement,
.features-treestudyplan .t-badge-criteria-requirement {
color: var(--gray);
}
.path-local-treestudyplan .r-report-tabs .list-group-item-action,
.features-treestudyplan .r-report-tabs .list-group-item-action {
width: inherit;

View File

@ -180,6 +180,8 @@ $string["completion_completed"] = "Completed";
$string["completion_good"] = "Good";
$string["completion_excellent"] = "Excellent";
$string["completion_passed"] = "Passed";
$string["completion_incomplete_badge"] = "Not issued";
$string["completion_completed_badge"] = "Issued";
$string["incomplete"] = 'Not started';
$string["completed"] = 'Completed';

View File

@ -177,6 +177,8 @@ $string["completion_completed"] = "Voltooid";
$string["completion_good"] = "Goed";
$string["completion_excellent"] = "Uitstekend";
$string["completion_passed"] = "Behaald";
$string["completion_incomplete_badge"] = "Niet uitgegeven";
$string["completion_completed_badge"] = "Uitgegeven";
$string["incomplete"] = 'Niet gestart';
$string["completed"] = 'Voltooid';

View File

@ -603,6 +603,10 @@
transform: translate(-50%, -50%);
}
.t-badge-criteria-requirement {
color: var(--gray);
}
.r-report-tabs .list-group-item-action {
width: inherit;
}

View File

@ -707,6 +707,10 @@
left: 50%;
transform: translate(-50%, -50%);
}
.path-local-treestudyplan .t-badge-criteria-requirement,
.features-treestudyplan .t-badge-criteria-requirement {
color: var(--gray);
}
.path-local-treestudyplan .r-report-tabs .list-group-item-action,
.features-treestudyplan .r-report-tabs .list-group-item-action {
width: inherit;