Added corecompletion detailinfo for teacher/editor

This commit is contained in:
PMKuipers 2023-05-29 23:00:45 +02:00
parent 0cf5619117
commit 5d12a6c653
2 changed files with 436 additions and 117 deletions

View file

@ -163,6 +163,7 @@ export default {
class='r-report-tabs'> class='r-report-tabs'>
<b-list-group-item <b-list-group-item
v-for="(studyplan,planindex) in value" v-for="(studyplan,planindex) in value"
:key="studyplan.id"
:active="displayedstudyplan && studyplan.id == displayedstudyplan.id" :active="displayedstudyplan && studyplan.id == displayedstudyplan.id"
button button
@click="selectedstudyplan = studyplan" @click="selectedstudyplan = studyplan"
@ -539,7 +540,7 @@ export default {
`, `,
}); });
//TAG: Item Course
Vue.component('r-item-course', { Vue.component('r-item-course', {
props: { props: {
value :{ value :{
@ -685,6 +686,7 @@ export default {
`, `,
}); });
//TAG: Selected activities dispaly
Vue.component('r-item-studentgrades',{ Vue.component('r-item-studentgrades',{
props: { props: {
value : { value : {
@ -780,6 +782,7 @@ export default {
`, `,
}); });
//TAG: Core completion version of student course info
Vue.component('r-item-studentcompletion',{ Vue.component('r-item-studentcompletion',{
props: { props: {
value : { value : {
@ -899,7 +902,7 @@ export default {
`, `,
}); });
//TODO: Implement corecompletion
Vue.component('r-item-teachercourse', { Vue.component('r-item-teachercourse', {
props: { props: {
value :{ value :{
@ -1028,49 +1031,6 @@ export default {
return "dot-circle-o"; return "dot-circle-o";
} }
}, },
is_grading_needed(grade){
if(grade.grading){
if(grade.grading.ungraded){
return 'ungraded';
}
else if(grade.grading.graded){
if(Number(grade.grading.graded) == Number(grade.grading.students)){
return 'allgraded';
}
else {
return 'graded';
}
}
else {
return 'unsubmitted';
}
} else {
return 'unknown';
}
},
grading_icon(grade){
return this.determine_grading_icon(this.is_grading_needed(grade));
},
includeChanged(newValue,g){
call([{
methodname: 'local_treestudyplan_include_grade',
args: { 'grade_id': g.id,
'item_id': this.value.id,
'include': newValue,
'required': g.required,
}
}])[0].fail(notification.exception);
},
requiredChanged(newValue,g){
call([{
methodname: 'local_treestudyplan_include_grade',
args: { 'grade_id': g.id,
'item_id': this.value.id,
'include': g.selected,
'required': newValue,
}
}])[0].fail(notification.exception);
},
}, },
template: ` template: `
<b-card no-body :class="'r-item-competency '+ (value.course.amteacher?'r-course-am-teacher':'')"> <b-card no-body :class="'r-item-competency '+ (value.course.amteacher?'r-course-am-teacher':'')">
@ -1106,10 +1066,11 @@ export default {
<div> <div>
<h1><a :href="(!guestmode)?('/course/view.php?id='+value.course.id):undefined" target="_blank" <h1><a :href="(!guestmode)?('/course/view.php?id='+value.course.id):undefined" target="_blank"
><i class="fa fa-graduation-cap"></i> {{ value.course.fullname }}</a> ><i class="fa fa-graduation-cap"></i> {{ value.course.fullname }}</a>
<a v-if="value.course.canselectgradables" href='#' <r-item-teacher-gradepicker v-model="value.course"
v-b-modal="'r-item-course-config-'+value.id" v-if="value.course.grades && value.course.grades.length > 0"
@click.prevent.stop='' :useRequiredGrades="useRequiredGrades"
><i class='fa fa-cog'></i></a> :plan="plan"
></r-item-teacher-gradepicker>
</h1> </h1>
{{ value.course.context.path.join(" / ")}} {{ value.course.context.path.join(" / ")}}
</div> </div>
@ -1125,18 +1086,210 @@ export default {
</div> </div>
</div> </div>
</template> </template>
<r-item-teachergrades
v-if='!!value.course.grades && value.course.grades.length > 0'
v-model='value.course'
:useRequiredGrades="useRequiredGrades"
></r-item-teachergrades>
<r-item-teachercompletion
v-if='!!value.course.completion'
v-model='value.course.completion'
:course='value.course'
></r-item-teachercompletion>
</b-modal>
</b-card>
`,
});
//TODO: Selecte activities to use in grade overview
Vue.component('r-item-teacher-gradepicker', {
props: {
value : {
type: Object,
default: function(){ return {};},
},
useRequiredGrades: {
type: Boolean,
default(){ return null;}
}
},
data() {
return {
};
},
computed: {
},
methods: {
},
template: `
<a v-if="value.canselectgradables" href='#'
v-b-modal="'r-item-course-config-'+value.id"
@click.prevent.stop=''
><i class='fa fa-cog'></i>
<b-modal v-if='value.canselectgradables'
:id="'r-item-course-config-'+value.id"
:title="value.displayname + ' - ' + value.fullname"
ok-only
scrollable
>
<template #modal-header>
<div>
<h1><a :href="'/course/view.php?id='+value.id" target="_blank"
><i class="fa fa-graduation-cap"></i> {{ value.fullname }}</a></h1>
{{ value.course.context.path.join(" / ")}} / {{value.displayname}}
</div>
<div class="r-course-detail-header-right">
<div :class="'r-timing-'+value.timing">
{{text['coursetiming_'+value.timing]}}<br>
{{ value.startdate }} - {{ value.enddate }}
</div>
</div>
</template>
<b-form-group
:label="text.select_grades"
><ul class="t-item-module-children">
<li class="t-item-course-gradeinfo">
<span class='t-item-course-chk-lbl'>{{text.grade_include}}</span
><span v-if="useRequiredGrades" class='t-item-course-chk-lbl'>{{text.grade_require}}</span>
</li>
<li class="t-item-course-gradeinfo" v-for="g in value.grades">
<b-form-checkbox inline
@change="includeChanged($event,g)" v-model="g.selected"
></b-form-checkbox>
<b-form-checkbox v-if="useRequiredGrades" inline :disabled="!g.selected"
@change="requiredChanged($event,g)" v-model="g.required"
></b-form-checkbox>
<span :title="g.typename" v-html="g.icon"></span><a
:href="g.link" target="_blank">{{g.name}}</a>
<s-edit-mod
:title="value.fullname"
@saved="(fd) => g.name = fd.get('name')"
v-if="g.cmid > 0"
:cmid="g.cmid"
:coursectxid="value.ctxid"
genericonly></s-edit-mod>
</li>
</ul>
</b-form-group>
</b-modal></a>
`,
});
//TODO: Selected activities dispaly
Vue.component('r-item-teachergrades',{
props: {
value : {
type: Object,
default: function(){ return {};},
},
useRequiredGrades: {
type: Boolean,
default: false,
},
},
data() {
return {
txt: {
grading: strings.grading,
}
};
},
computed: {
pendingsubmission(){
let result = false;
for(const ix in this.value.grades){
const g = this.value.grades[ix];
if(g.pendingsubmission){
result = true;
break;
}
}
return result;
},
filtered_grades(){
return this.value.grades.filter(g => g.selected);
},
},
methods: {
determine_grading_icon(gradingstate){
switch(gradingstate){
default: // "nogrades":
return "circle-o";
case "ungraded":
return "exclamation-circle";
case "unknown":
return "question-circle-o";
case "graded":
return "check";
case "allgraded":
return "check";
case "unsubmitted":
return "dot-circle-o";
}
},
grading_icon(grade){
return this.determine_grading_icon(this.is_grading_needed(grade));
},
is_grading_needed(grade){
if(grade.grading){
if(grade.grading.ungraded){
return 'ungraded';
}
else if(grade.grading.graded){
if(Number(grade.grading.graded) == Number(grade.grading.students)){
return 'allgraded';
}
else {
return 'graded';
}
}
else {
return 'unsubmitted';
}
} else {
return 'unknown';
}
},
includeChanged(newValue,g){
call([{
methodname: 'local_treestudyplan_include_grade',
args: { 'grade_id': g.id,
'item_id': this.value.id,
'include': newValue,
'required': g.required,
}
}])[0].fail(notification.exception);
},
requiredChanged(newValue,g){
call([{
methodname: 'local_treestudyplan_include_grade',
args: { 'grade_id': g.id,
'item_id': this.value.id,
'include': g.selected,
'required': newValue,
}
}])[0].fail(notification.exception);
},
},
template: `
<div>
<table class="r-item-course-grade-details"> <table class="r-item-course-grade-details">
<tr v-for="g in filtered_grades"> <tr v-for="g in filtered_grades">
<td><span class="r-activity-icon" :title="g.typename" v-html="g.icon"></span <td><span class="r-activity-icon" :title="g.typename" v-html="g.icon"></span
><a ><a
:href="(!guestmode)?(teachermode?g.gradinglink:g.link):undefined" :href="g.gradinglink"
target="_blank" :title="g.name">{{g.name}}</a> target="_blank" :title="g.name">{{g.name}}</a>
<s-edit-mod <s-edit-mod
:title="value.course.fullname" :title="value.fullname"
@saved="(fd) => g.name = fd.get('name')" @saved="(fd) => g.name = fd.get('name')"
v-if="g.cmid > 0" v-if="g.cmid > 0"
:cmid="g.cmid" :cmid="g.cmid"
:coursectxid="value.course.ctxid" :coursectxid="value.ctxid"
genericonly></s-edit-mod> genericonly></s-edit-mod>
<abbr v-if="useRequiredGrades && g.required" :title="text.required_goal" <abbr v-if="useRequiredGrades && g.required" :title="text.required_goal"
:class="'s-required ' + is_grading_needed(g)" :class="'s-required ' + is_grading_needed(g)"
@ -1152,59 +1305,131 @@ export default {
</td> </td>
</tr> </tr>
</table> </table>
</b-modal> </div>
`,
});
<b-modal v-if='value.course.canselectgradables' //TODO: Core completion version of student course info
:id="'r-item-course-config-'+value.id" Vue.component('r-item-teachercompletion',{
:title="value.course.displayname + ' - ' + value.course.fullname" props: {
value : {
type: Object,
default: function(){ return {};},
},
guestmode: {
type: Boolean,
default: false,
},
course: {
type: Object,
default: function(){ return {};},
},
},
data() {
return {
text: {
completion_incomplete: "completion_incomplete",
completion_failed: "completion_failed",
completion_pending: "completion_pending",
completion_progress: "completion_progress",
completion_completed: "completion_completed",
completion_good: "completion_good",
completion_excellent: "completion_excellent",
view_feedback: "view_feedback",
coursetiming_past: "coursetiming_past",
coursetiming_present: "coursetiming_present",
coursetiming_future: "coursetiming_future",
required_goal: "required_goal",
},
};
},
created(){
const self = this;
// Get text strings for condition settings
let stringkeys = [];
for(const key in this.text){
stringkeys.push({ key: key, component: 'local_treestudyplan'});
}
get_strings(stringkeys).then(function(strings){
let i = 0;
for(const key in self.text){
self.text[key] = strings[i];
i++;
}
});
},
computed: {
},
methods: {
completion_icon(completion) {
switch(completion){
case "progress":
return "exclamation-circle";
case "complete":
return "check-circle";
case "complete-pass":
return "check-circle";
case "complete-fail":
return "times-circle";
default: // case "incomplete"
return "circle-o";
}
},
completion_tag(cgroup){
return cgroup.completion?'completed':'incomplete';
}
},
template: `
<table class="r-item-course-grade-details">
<template v-for='cgroup in value.conditions'>
<tr>
<th colspan='2'>{{cgroup.title}}</th>
<th><r-progress-circle
:value='cgroup.progress'
:max='cgroup.count'
:class="'r-completion-'+cgroup.status"
:title="text['completion_'+cgroup.status]"
></r-progress-circle></th>
</tr>
<tr v-for='ci in cgroup.items'>
<td><span v-if='guestmode'>{{ci.title}}</span>
<span v-else v-html='ci.details.criteria'></span>
<abbr v-if="ci.details.requirement" :title="ci.details.requirement"
:class="'s-required ' + ci.status"
><i class='fa fa-questionmark' ></i
></abbr>
<td><span :class="' r-completion-'+ci.status">{{ci.grade}}</span></td>
<td><i :class="'fa fa-'+completion_icon(ci.status)+' r-completion-'+ci.status"
:title="text['completion_'+ci.status]"></i>
<i v-if='ci.pending' :title="text['completion_pending']"
class="r-pendingsubmission fa fa-clock-o"></i>
</td>
<td v-if="ci.feedback">
<a v-b-modal="'r-grade-feedback-'+ci.id"
href="#"
>{{ text["view_feedback"]}}</a>
<b-modal
:id="'r-grade-feedback-'+ci.id"
size="sm"
ok-only ok-only
centered
scrollable scrollable
> >
<template #modal-header> <template #modal-header>
<div> <h2><i class="fa fa-graduation-cap"></i>{{ course.fullname }}</h2><br>
<h1><a :href="'/course/view.php?id='+value.course.id" target="_blank" <span class="r-activity-icon" :title="ci.typename" v-html="ci.icon"></span>{{ci.name}}
><i class="fa fa-graduation-cap"></i> {{ value.course.fullname }}</a></h1>
{{ value.course.context.path.join(" / ")}} / {{value.course.displayname}}
</div>
<div class="r-course-detail-header-right">
<div :class="'r-timing-'+value.course.timing">
{{text['coursetiming_'+value.course.timing]}}<br>
{{ value.course.startdate }} - {{ value.course.enddate }}
</div>
</div>
</template> </template>
<b-form-group <span v-html="ci.feedback"></span>
:label="text.select_grades"
><ul class="t-item-module-children">
<li class="t-item-course-gradeinfo">
<span class='t-item-course-chk-lbl'>{{text.grade_include}}</span
><span v-if="useRequiredGrades" class='t-item-course-chk-lbl'>{{text.grade_require}}</span>
</li>
<li class="t-item-course-gradeinfo" v-for="g in value.course.grades">
<b-form-checkbox inline
@change="includeChanged($event,g)" v-model="g.selected"
></b-form-checkbox>
<b-form-checkbox v-if="useRequiredGrades" inline :disabled="!g.selected"
@change="requiredChanged($event,g)" v-model="g.required"
></b-form-checkbox>
<span :title="g.typename" v-html="g.icon"></span><a
:href="g.link" target="_blank">{{g.name}}</a>
<s-edit-mod
:title="value.course.fullname"
@saved="(fd) => g.name = fd.get('name')"
v-if="g.cmid > 0"
:cmid="g.cmid"
:coursectxid="value.course.ctxid"
genericonly></s-edit-mod>
</li>
</ul>
</b-form-group>
</b-modal> </b-modal>
</td>
</b-card> </tr>
</template>
</table>
`, `,
}); });
Vue.component('r-grading-bar',{ Vue.component('r-grading-bar',{
props: { props: {
value : { value : {

View file

@ -55,11 +55,12 @@ class corecompletioninfo {
return new \external_single_structure([ return new \external_single_structure([
"title" => new \external_value(PARAM_TEXT,'name of subitem',VALUE_OPTIONAL), "title" => new \external_value(PARAM_TEXT,'name of subitem',VALUE_OPTIONAL),
"link" => new \external_value(PARAM_TEXT, 'optional link to more details',VALUE_OPTIONAL), "link" => new \external_value(PARAM_TEXT, 'optional link to more details',VALUE_OPTIONAL),
// ADD BELOW IF NEEDED - try using name, description and link fields first "details" => new \external_single_structure([
/* "type" => new \external_value(PARAM_RAW, 'type',VALUE_OPTIONAL),
"required_grade" => new \external_value(PARAM_TEXT, 'required_grade',VALUE_OPTIONAL), "criteria" => new \external_value(PARAM_RAW, 'criteria',VALUE_OPTIONAL),
"course_link" => course_info::simple_structure(VALUE_OPTIONAL), "requirement" => new \external_value(PARAM_RAW, 'requirement',VALUE_OPTIONAL),
*/ "status" => new \external_value(PARAM_RAW, 'status',VALUE_OPTIONAL),
]),
], 'completion type',$value); ], 'completion type',$value);
} }
@ -92,11 +93,6 @@ class corecompletioninfo {
"status" => new \external_value(PARAM_RAW, 'status',VALUE_OPTIONAL), "status" => new \external_value(PARAM_RAW, 'status',VALUE_OPTIONAL),
]), ]),
"link" => new \external_value(PARAM_TEXT, 'optional link to more details',VALUE_OPTIONAL), "link" => new \external_value(PARAM_TEXT, 'optional link to more details',VALUE_OPTIONAL),
// ADD BELOW IF NEEDED - try using name, description and link fields first
/*
"required_grade" => new \external_value(PARAM_TEXT, 'required_grade',VALUE_OPTIONAL),
"course_link" => course_info::simple_structure(VALUE_OPTIONAL),
*/
"completed" => new \external_value(PARAM_BOOL, 'simple completed or not'), "completed" => new \external_value(PARAM_BOOL, 'simple completed or not'),
"status" => new \external_value(PARAM_TEXT, 'extended completion status ["incomplete","progress","complete", "complete-pass","complete-fail"]'), "status" => new \external_value(PARAM_TEXT, 'extended completion status ["incomplete","progress","complete", "complete-pass","complete-fail"]'),
"pending" => new \external_value(PARAM_BOOL, 'optional pending state, for submitted but not yet reviewed activities',VALUE_OPTIONAL), "pending" => new \external_value(PARAM_BOOL, 'optional pending state, for submitted but not yet reviewed activities',VALUE_OPTIONAL),
@ -164,28 +160,126 @@ class corecompletioninfo {
]; ];
foreach($criterias as $criteria){ foreach($criterias as $criteria){
$iinfo = [ // Unfortunately, we cannot easily get the criteria details with get_details() without having a
"title" => $criteria->get_title_detailed(), // user completion object involved, so'we'll have to retrieve the details per completion type
// See moodle/completion/criteria/completion_criteria_*.php::get_details() for the code that is
// in the code below is based on
if($type == COMPLETION_CRITERIA_TYPE_SELF){
$details = [
"type" => $criteria->get_title(),
"criteria" => $criteria->get_title(),
"requirement" => get_string('markingyourselfcomplete', 'completion'),
"status" => "",
]; ];
//TODO: MAKE SURE THIS DATA IS FILLED
if($type == COMPLETION_CRITERIA_TYPE_ACTIVITY){
// If it's an activity completion, add the relevant activity
//$cm = $this->modinfo->get_cm($criterias->moduleinstance);
// retrieve data for this object
//$data = $completion->get_data($cm, false, $userid);
} }
else if ($type == COMPLETION_CRITERIA_TYPE_COURSE){ else if ($type == COMPLETION_CRITERIA_TYPE_DATE){
// If it's a (sub) course dependency, add the course as a link $details = [
"type" => get_string('datepassed', 'completion'),
"criteria" => get_string('remainingenroleduntildate', 'completion'),
"requirement" => userdate($criteria->timeend, '%d %B %Y'),
"status" => "",
];
}
else if ($type == COMPLETION_CRITERIA_TYPE_UNENROL){
$details = [
"type" => get_string('unenrolment', 'completion'),
"criteria" => get_string('unenrolment', 'completion'),
"requirement" => get_string('unenrolingfromcourse', 'completion'),
"status" => "",
];
}
else if ($type == COMPLETION_CRITERIA_TYPE_ACTIVITY){
$cm = $this->modinfo->get_cm($criteria->moduleinstance);
$details = [
"type" => $criteria->get_title(),
"criteria" => "", // Will be built in a moment by code copied from completion_criteria_activity.php
"requirement" => "", // Will be built momentarily by code copied from completion_criteria_activity.php
"status" => "",
];
if ($cm->has_view()) {
$details['criteria'] = \html_writer::link($cm->url, $cm->get_formatted_name());
} else {
$details['criteria'] = $cm->get_formatted_name();
}
// Build requirements
$details['requirement'] = array();
if ($cm->completion == COMPLETION_TRACKING_MANUAL) {
$details['requirement'][] = get_string('markingyourselfcomplete', 'completion');
} elseif ($cm->completion == COMPLETION_TRACKING_AUTOMATIC) {
if ($cm->completionview) {
$modulename = core_text::strtolower(get_string('modulename', $criteria->module));
$details['requirement'][] = get_string('viewingactivity', 'completion', $modulename);
}
if (!is_null($cm->completiongradeitemnumber)) {
$details['requirement'][] = get_string('achievinggrade', 'completion');
}
if ($cm->completionpassgrade) {
$details['requirement'][] = get_string('achievingpassinggrade', 'completion');
}
}
$details['requirement'] = implode(', ', $details['requirement']);
}
else if ($type == COMPLETION_CRITERIA_TYPE_DURATION){
$details = [
"type" => get_string('periodpostenrolment', 'completion'),
"criteria" => get_string('remainingenroledfortime', 'completion'),
"requirement" => get_string('xdays', 'completion', ceil($criteria->enrolperiod / (60*60*24))),
"status" => "",
];
}
else if ($type == COMPLETION_CRITERIA_TYPE_GRADE){
$details = [
"type" => get_string('coursegrade', 'completion'),
"criteria" => get_string('graderequired', 'completion'),
// TODO: convert to selected representation (letter, percentage, etc)
"requirement" => format_float($criteria->gradepass, $decimalpoints),
"status" => "",
];
} }
else if ($type == COMPLETION_CRITERIA_TYPE_ROLE){ else if ($type == COMPLETION_CRITERIA_TYPE_ROLE){
// If it needs approval by a role, it probably already is in the title $criteria = $criteria->get_title();
$details = [
"type" => get_string('manualcompletionby', 'completion'),
"criteria" => $criteria,
"requirement" => get_string('markedcompleteby', 'completion', $criteria),
"status" => "",
];
} }
else if ($type == COMPLETION_CRITERIA_TYPE_COURSE){
$prereq = get_course($criteria->courseinstance);
$coursecontext = \context_course::instance($prereq->id, MUST_EXIST);
$fullname = format_string($prereq->fullname, true, array('context' => $coursecontext));
$details = [
"type" => $criteria->get_title(),
"criteria" => '<a href="'.$CFG->wwwroot.'/course/view.php?id='.$criteria->courseinstance.'">'.s($fullname).'</a>',
"requirement" => get_string('coursecompleted', 'completion'),
"status" => "",
];
} else {
// Moodle added a criteria type
$details = [
"type" => "",
"criteria" => "",
"requirement" => "",
"status" => "",
];
}
// only add the items list if we actually have items... // only add the items list if we actually have items...
$cinfo["items"][] = $iinfo; $cinfo["items"][] = [
"id" => $criteria->id,
"title" => $criteria->get_title_detailed(),
"details" => $details,
];
} }
$info['conditions'][] = $cinfo; $info['conditions'][] = $cinfo;