Finished custom fields in course popup. Added optional progress bar in course popup bar. Added progress circles to manual aggregation.

This commit is contained in:
PMKuipers 2023-12-11 22:30:30 +01:00
parent 5544d57f6b
commit bfcd41dd81
23 changed files with 494 additions and 150 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,3 +1,3 @@
define("local_treestudyplan/util/date-helper",["exports"],(function(_exports){function format_date(d,short){d instanceof Date||(d=new Date(d));let monthformat="short";return short&&(monthformat="numeric"),d.toLocaleDateString(document.documentElement.lang,{year:"numeric",month:monthformat,day:"numeric"})}function studyplanDates(plan){let earliestStart=null,latestEnd=null,openEnded=!1;for(const ix in plan.pages){const page=plan.pages[ix],s=new Date(page.startdate);if(page.enddate||(openEnded=!0),(!earliestStart||s<earliestStart)&&(earliestStart=s),page.enddate){const e=new Date(page.enddate);(!latestEnd||e>latestEnd)&&(latestEnd=e)}}return{start:earliestStart,end:openEnded?null:latestEnd}}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.add_days=function(datestr,days){const date=new Date(datestr);return function(date){const d=new Date(date);let month=""+(d.getMonth()+1),day=""+d.getDate();const year=d.getFullYear();month.length<2&&(month="0"+month);day.length<2&&(day="0"+day);return[year,month,day].join("-")}(new Date(date.getTime()+864e5*days))},_exports.datespaninfo=function(first,last){first instanceof Date||(first=new Date(first));last instanceof Date||(last=new Date(last));first.setHours(0),first.setMinutes(0),first.setSeconds(0),first.setMilliseconds(0),last.setHours(23),last.setMinutes(59),last.setSeconds(59),last.setMilliseconds(999);const dayspan=Math.round((last-first+1)/864e5),years=Math.floor(dayspan/365),ydaysleft=dayspan%365,weeks=Math.floor(ydaysleft/7);return{first:first,last:last,totaldays:dayspan,years:years,weeks:weeks,days:ydaysleft%7,formatted:{first:format_date(first),last:format_date(last)}}},_exports.format_date=format_date,_exports.studyplanDates=studyplanDates,_exports.studyplanPageTiming=function(page){const now=(new Date).getTime(),start=new Date(page.startdate),end=page.enddate?new Date(page.enddate):null;return start<now?end&&now>end?"past":"present":"future"},_exports.studyplanTiming=function(plan){const now=(new Date).getTime(),dates=studyplanDates(plan);return dates.start<now?dates.end&&now>dates.end?"past":"present":"future"}})); define("local_treestudyplan/util/date-helper",["exports"],(function(_exports){function format_date(d,short){d instanceof Date||(d=new Date(d));let monthformat="short";return!0===short?monthformat="numeric":!1===short&&(monthformat="long"),d.toLocaleDateString(document.documentElement.lang,{year:"numeric",month:monthformat,day:"numeric"})}function studyplanDates(plan){let earliestStart=null,latestEnd=null,openEnded=!1;for(const ix in plan.pages){const page=plan.pages[ix],s=new Date(page.startdate);if(page.enddate||(openEnded=!0),(!earliestStart||s<earliestStart)&&(earliestStart=s),page.enddate){const e=new Date(page.enddate);(!latestEnd||e>latestEnd)&&(latestEnd=e)}}return{start:earliestStart,end:openEnded?null:latestEnd}}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.add_days=function(datestr,days){const date=new Date(datestr);return function(date){const d=new Date(date);let month=""+(d.getMonth()+1),day=""+d.getDate();const year=d.getFullYear();month.length<2&&(month="0"+month);day.length<2&&(day="0"+day);return[year,month,day].join("-")}(new Date(date.getTime()+864e5*days))},_exports.datespaninfo=function(first,last){first instanceof Date||(first=new Date(first));last instanceof Date||(last=new Date(last));first.setHours(0),first.setMinutes(0),first.setSeconds(0),first.setMilliseconds(0),last.setHours(23),last.setMinutes(59),last.setSeconds(59),last.setMilliseconds(999);const dayspan=Math.round((last-first+1)/864e5),years=Math.floor(dayspan/365),ydaysleft=dayspan%365,weeks=Math.floor(ydaysleft/7);return{first:first,last:last,totaldays:dayspan,years:years,weeks:weeks,days:ydaysleft%7,formatted:{first:format_date(first),last:format_date(last)}}},_exports.format_date=format_date,_exports.studyplanDates=studyplanDates,_exports.studyplanPageTiming=function(page){const now=(new Date).getTime(),start=new Date(page.startdate),end=page.enddate?new Date(page.enddate):null;return start<now?end&&now>end?"past":"present":"future"},_exports.studyplanTiming=function(plan){const now=(new Date).getTime(),dates=studyplanDates(plan);return dates.start<now?dates.end&&now>dates.end?"past":"present":"future"}}));
//# sourceMappingURL=date-helper.min.js.map //# sourceMappingURL=date-helper.min.js.map

File diff suppressed because one or more lines are too long

View File

@ -1099,6 +1099,26 @@ export default {
else { else {
return this.text.noenddate; return this.text.noenddate;
} }
},
courseprogress() {
if (!this.value.course.enrolled) {
return 0;
} else if(this.value.course.completion) {
return (this.value.course.completion.progress / this.value.course.completion.count);
} else if(this.value.course.competency) {
return (this.value.course.competency.progress / this.value.course.competency.count);
} else if(this.value.course.grades) {
return (this.gradeprogress(this.value.course.grades) / this.value.course.grades.length);
} else {
return 0;
}
},
hasprogressinfo() {
if (!this.value.course.enrolled) {
return false;
} else {
return (this.value.course.completion || this.value.course.competency || this.value.course.grades);
}
} }
}, },
created(){ created(){
@ -1138,6 +1158,16 @@ export default {
return "check"; return "check";
} }
}, },
gradeprogress(grades) {
let progress = 0;
for (const ix in grades) {
const g = grades[ix];
if (["completed","excellent","good"].includes(g.completion)) {
progress++;
}
}
return progress;
},
}, },
template: ` template: `
<div :class="'r-item-competency completion-'+value.completion"> <div :class="'r-item-competency completion-'+value.completion">
@ -1156,24 +1186,10 @@ export default {
class="r-course-result fa fa-exclamation-triangle t-not-enrolled-alert" class="r-course-result fa fa-exclamation-triangle t-not-enrolled-alert"
:title="text.student_not_tracked"></i> :title="text.student_not_tracked"></i>
</template> </template>
<template v-else-if='value.course.completion'> <template v-else-if='hasprogressinfo'>
<r-progress-circle v-if='["failed", "progress","incomplete"].includes(value.completion)' <r-progress-circle v-if='["failed", "progress","incomplete"].includes(value.completion)'
:value='value.course.completion.progress' :value='courseprogress'
:max='value.course.completion.count' :max='1'
:min='0'
:class="'r-course-result r-completion-'+value.completion"
:icon='circle_icon(value.completion)'
:title="text['completion_'+value.completion]"
></r-progress-circle>
<i v-else v-b-popover.top
:class="'r-course-result fa fa-'+completion_icon(value.completion)+
' r-completion-'+value.completion"
:title="text['completion_'+value.completion]"></i>
</template>
<template v-else-if='value.course.competency'>
<r-progress-circle v-if='["failed", "progress","incomplete"].includes(value.completion)'
:value='value.course.competency.progress'
:max='value.course.competency.count'
:min='0' :min='0'
:class="'r-course-result r-completion-'+value.completion" :class="'r-course-result r-completion-'+value.completion"
:icon='circle_icon(value.completion)' :icon='circle_icon(value.completion)'
@ -1205,48 +1221,59 @@ export default {
ok-only ok-only
centered centered
scrollable scrollable
header-class="r-item-course-header"
> >
<template #modal-header> <template #modal-header >
<div> <div class="r-item-course-header-details">
<h1><a :href="(!guestmode)?('/course/view.php?id='+value.course.id):undefined" target="_blank" <div>
><i class="fa fa-graduation-cap"></i> {{ value.course.fullname }}</a></h1> <h1><a :href="(!guestmode)?('/course/view.php?id='+value.course.id):undefined" target="_blank"
{{ value.course.context.path.join(" / ")}} ><i class="fa fa-graduation-cap"></i> {{ value.course.fullname }}</a></h1>
</div> {{ value.course.context.path.join(" / ")}}
<div class="r-course-detail-header-right">
<div class="r-completion-detail-header">
<template v-if='!value.course.enrolled'>
{{text.not_enrolled}}
<i v-b-popover.top
class="r-course-result fa fa-exclamation-triangle t-not-enrolled-alert"
:title="text.student_not_tracked"></i>
</template>
<template v-else-if='value.course.completion'>
{{text['completion_'+value.completion]}}
<r-progress-circle v-if='["failed","progress","incomplete"].includes(value.completion)'
:value='value.course.completion.progress'
:max='value.course.completion.count'
:min='0'
:title="text['completion_'+value.completion]"
:class="'r-progress-circle-popup r-completion-'+value.completion"
:icon='circle_icon(value.completion)'
></r-progress-circle
><i v-else v-b-popover.top
:class="'fa fa-'+completion_icon(value.completion)+
' r-progress-icon-popup r-completion-'+value.completion"
:title="text['completion_'+value.completion]"></i>
</template>
<template v-else>
{{text['completion_'+value.completion]}}
<i :class="'fa fa-'+completion_icon(value.completion)+' r-completion-'+value.completion"
:title="text['completion_'+value.completion]"></i>
</template>
</div> </div>
<div :class="'r-timing-'+value.course.timing"> <div class="r-course-detail-header-right">
{{text['coursetiming_'+value.course.timing]}}<br> <div class="r-completion-detail-header">
{{ startdate }} - {{ enddate }} <template v-if='!value.course.enrolled'>
{{text.not_enrolled}}
<i v-b-popover.top
class="r-course-result fa fa-exclamation-triangle t-not-enrolled-alert"
:title="text.student_not_tracked"></i>
</template>
<template v-else-if='hasprogressinfo'>
<r-progress-circle v-if='["failed", "progress","incomplete"].includes(value.completion)'
:value='courseprogress'
:max='1'
:min='0'
:class="'r-course-result r-completion-'+value.completion"
:icon='circle_icon(value.completion)'
:title="text['completion_'+value.completion]"
></r-progress-circle>
<i v-else v-b-popover.top
:class="'r-course-result fa fa-'+completion_icon(value.completion)+
' r-completion-'+value.completion"
:title="text['completion_'+value.completion]"></i>
</template>
<template v-else>
{{text['completion_'+value.completion]}}
<i :class="'fa fa-'+completion_icon(value.completion)+' r-completion-'+value.completion"
:title="text['completion_'+value.completion]"></i>
</template>
</div>
<div :class="'r-timing-'+value.course.timing">
{{text['coursetiming_'+value.course.timing]}}<br>
{{ startdate }} - {{ enddate }}
</div>
</div> </div>
</div> </div>
<s-progress-bar
v-if='value.course.showprogressbar && hasprogressinfo'
v-model="courseprogress"
></s-progress-bar>
</template> </template>
<s-course-extrafields
v-if="value.course.extrafields"
v-model="value.course.extrafields"
position="above"
></s-course-extrafields>
<r-item-studentgrades <r-item-studentgrades
v-if='!!value.course.grades && value.course.grades.length > 0' v-if='!!value.course.grades && value.course.grades.length > 0'
v-model='value' v-model='value'
@ -1261,6 +1288,11 @@ export default {
v-model='value.course.competency' v-model='value.course.competency'
:item='value' :item='value'
></r-item-student-course-competency> ></r-item-student-course-competency>
<s-course-extrafields
v-if="value.course.extrafields"
v-model="value.course.extrafields"
position="below"
></s-course-extrafields>
</b-modal> </b-modal>
</b-card></div> </b-card></div>
`, `,
@ -1958,6 +1990,11 @@ export default {
</div> </div>
</div> </div>
</template> </template>
<s-course-extrafields
v-if="value.course.extrafields"
v-model="value.course.extrafields"
position="above"
></s-course-extrafields>
<r-item-teachergrades <r-item-teachergrades
v-if='!!value.course.grades && value.course.grades.length > 0' v-if='!!value.course.grades && value.course.grades.length > 0'
v-model='value.course' v-model='value.course'
@ -1973,6 +2010,11 @@ export default {
v-model='value.course.competency' v-model='value.course.competency'
:item='value' :item='value'
></r-item-teacher-course-competency> ></r-item-teacher-course-competency>
<s-course-extrafields
v-if="value.course.extrafields"
v-model="value.course.extrafields"
position="below"
></s-course-extrafields>
</b-modal> </b-modal>
</b-card> </b-card>
@ -2045,6 +2087,7 @@ export default {
{{ value.course.startdate }} - {{ value.course.enddate }} {{ value.course.startdate }} - {{ value.course.enddate }}
</div> </div>
</div> </div>
<s-
</template> </template>
<b-form-group <b-form-group
:label="text.select_grades" :label="text.select_grades"

View File

@ -152,9 +152,11 @@ export class SimpleLine {
this.mutationObserver = new MutationObserver(function(mutations_list) { this.mutationObserver = new MutationObserver(function(mutations_list) {
mutations_list.forEach(function(mutation) { mutations_list.forEach(function(mutation) {
mutation.removedNodes.forEach(function(removed_node) { mutation.removedNodes.forEach(function(removed_node) {
if(removed_node == this.start || removed_node == this.end) { if (this){
console.warning("Element removed",removed_node); if(removed_node == this.start || removed_node == this.end) {
this.remove(); console.warning("Element removed",removed_node);
this.remove();
}
} }
}); });
}); });

View File

@ -3194,6 +3194,11 @@ export default {
</div> </div>
</template> </template>
<s-course-extrafields
v-if="value.course.extrafields"
v-model="value.course.extrafields"
position="above"
></s-course-extrafields>
<t-item-course-grades <t-item-course-grades
v-if='!!value.course.grades && value.course.grades.length > 0' v-if='!!value.course.grades && value.course.grades.length > 0'
v-model='value' :plan="plan" v-model='value' :plan="plan"
@ -3208,7 +3213,11 @@ export default {
v-model='value.course.competency' v-model='value.course.competency'
:item='value' :item='value'
></t-item-course-competency> ></t-item-course-competency>
<s-course-extrafields
v-if="value.course.extrafields"
v-model="value.course.extrafields"
position="below"
></s-course-extrafields>
<template #modal-footer="{ ok, cancel, hide }" > <template #modal-footer="{ ok, cancel, hide }" >
<a href='#' @click='$emit("deleterq")' class="text-danger" <a href='#' @click='$emit("deleterq")' class="text-danger"
><i class="fa fa-trash"></i> ><i class="fa fa-trash"></i>

View File

@ -21,6 +21,9 @@ export default {
}, },
details: { details: {
details: "studyplan_details", details: "studyplan_details",
},
extrafields: {
show: "show@core"
} }
}); });
// Create new eventbus for interaction between item components // Create new eventbus for interaction between item components
@ -51,27 +54,7 @@ export default {
end: (dates.end)?format_date(dates.end):this.text.noenddate, end: (dates.end)?format_date(dates.end):this.text.noenddate,
}; };
}, },
width_completed() {
if(this.value.progress) {
return this.value.progress * 100;
} else {
return 0;
}
},
width_incomplete() {
if(this.value.progress) {
return (1-this.value.progress) * 100;
} else {
return 100;
}
},
percentage_complete() {
if(this.value.progress) {
return Math.round(this.value.progress * 100) + "%";
} else {
return "0%";
}
}
}, },
methods: { methods: {
onOpenClick(e) { onOpenClick(e) {
@ -97,20 +80,10 @@ export default {
<div class='s-studyplan-card-idnumber' v-if='value.idnumber'> <div class='s-studyplan-card-idnumber' v-if='value.idnumber'>
{{ text.idnumber }}: {{ value.idnumber }} {{ text.idnumber }}: {{ value.idnumber }}
</div> </div>
<div class='s-studyplan-card-progress' v-if='value.progress !== undefined && value.progress !== null'> <s-progress-bar
<div class="s-studyplan-card-progressbar" v-if='value.progress !== undefined && value.progress !== null'
><span v-if="width_completed > 0" v-model="value.progress"
:style="{width: width_completed+'%'}" ></s-progress-bar>
class='s-studyplan-card-progress-segment s-studyplan-card-progress-completed'
></span
><span :style="{width: width_incomplete+'%'}"
class='s-studyplan-card-progress-segment s-studyplan-card-progress-incomplete'
></span
></div>
<div class="s-studyplan-card-progresstext">
{{ percentage_complete}} {{ text.completed.toLowerCase() }}
</div>
</div>
</div> </div>
</div> </div>
@ -131,6 +104,71 @@ export default {
`, `,
}); });
Vue.component('s-progress-bar', {
props: {
value: {
type: Number,
},
min: {
type: Number,
default() { return 0;}
},
max: {
type: Number,
default() { return 1;}
}
},
data() {
return {
text: strings.studyplancard
};
},
computed: {
width_completed() {
if(this.value) {
const v = ( (this.value - this.min) / (this.max - this.min));
return v * 100;
} else {
return 0;
}
},
width_incomplete() {
if(this.value) {
const v = ( (this.value - this.min) / (this.max - this.min));
return (1-v) * 100;
} else {
return 100;
}
},
percentage_complete() {
if(this.value) {
const v = ( (this.value - this.min) / (this.max - this.min));
return Math.round(v * 100) + "%";
} else {
return "0%";
}
}
},
template: `
<div class='s-studyplan-card-progress' >
<div class="s-studyplan-card-progressbar"
><span v-if="width_completed > 0"
:style="{width: width_completed+'%'}"
class='s-studyplan-card-progress-segment s-studyplan-card-progress-completed'
></span
><span :style="{width: width_incomplete+'%'}"
class='s-studyplan-card-progress-segment s-studyplan-card-progress-incomplete'
></span
></div>
<div class="s-studyplan-card-progresstext">
{{ percentage_complete}} {{ text.completed.toLowerCase() }}
</div>
</div>
`,
});
/* /*
* S-STUDYLINE-HEADER-HEADING * S-STUDYLINE-HEADER-HEADING
* The only reasing this is not a simple empty div, is the fact that the header height * The only reasing this is not a simple empty div, is the fact that the header height
@ -293,5 +331,73 @@ export default {
`, `,
}); });
Vue.component('s-course-extrafields', {
props: {
value: {
type: Array,
},
variant: {
type: String,
default() { return "info"; }
},
position: {
type: String,
default() { return "below"; }
},
size: {
type: String,
default() { return "";}
}
},
data() {
return {
text: strings.extrafields,
};
},
computed: {
fields() {
const fields = [];
for (const ix in this.value) {
const field = this.value[ix];
if (field.position == this.position && field.value && field.value.length > 0) {
fields.push(field);
}
}
return fields;
},
},
methods: {
displaydate(field) {
return format_date(field.value,false);
},
},
template: `
<div :class="'s-course-extrafields ' + ((fields.length>0)?position:'empty')">
<table v-if="fields.length > 0">
<tr v-for="field in fields">
<td><span class='title' v-if='field.title'>{{ field.title}}</span></td>
<td>
<span v-if='field.type == "date"' class="value date">{{ displaydate(field) }}</span>
<span v-else-if='field.type == "checkbox"'
:class="'value ' + (field.checked?'true':'false')">{{ field.value }}</span>
<span v-else-if='field.type == "textarea"'>
<a class='text-info' href='#' v-b-modal="field.courseid+'_'+field.fieldname">{{text.show}}...</a>
<b-modal
:id="field.courseid+'_'+field.fieldname"
:title="field.title""
scrollable
centered
ok-only
><span v-html="field.value"></span
></b-modal>
</span>
<span v-else class="value"><span v-html="field.value"></span></span>
</td>
</tr>
</table>
</div>
`,
});
} }
}; };

View File

@ -10,8 +10,10 @@ export function format_date(d,short){
} }
let monthformat = "short"; let monthformat = "short";
if(short){ if(short === true){
monthformat = "numeric"; monthformat = "numeric";
} else if (short === false) {
monthformat = "long";
} }
return d.toLocaleDateString(document.documentElement.lang,{ return d.toLocaleDateString(document.documentElement.lang,{
year: 'numeric', month: monthformat, day: 'numeric' year: 'numeric', month: monthformat, day: 'numeric'

View File

@ -768,9 +768,6 @@ class badgeinfo {
INNER JOIN {badge_criteria_param} p on p.critid = crit.id INNER JOIN {badge_criteria_param} p on p.critid = crit.id
WHERE p.value = :courseid AND crit.criteriatype $ctypesql $conditions"; WHERE p.value = :courseid AND crit.criteriatype $ctypesql $conditions";
debug::write("Sql query courses: ");
debug::write($sql);
debug::print_r($sqlparams);
$courserelids = $DB->get_fieldset_sql($sql, $sqlparams); $courserelids = $DB->get_fieldset_sql($sql, $sqlparams);
@ -782,9 +779,6 @@ class badgeinfo {
INNER JOIN {competency_coursecomp} cc on cc.competencyid = p.value INNER JOIN {competency_coursecomp} cc on cc.competencyid = p.value
WHERE cc.courseid = :courseid AND crit.criteriatype $ctypesql $conditions"; WHERE cc.courseid = :courseid AND crit.criteriatype $ctypesql $conditions";
debug::write("Sql query through competencies: ");
debug::write($sql);
debug::print_r($sqlparams);
$competencyrelids = $DB->get_fieldset_sql($sql,$sqlparams); $competencyrelids = $DB->get_fieldset_sql($sql,$sqlparams);

View File

@ -219,6 +219,11 @@ class courseinfo {
if (strlen($idnumber) > 0) { if (strlen($idnumber) > 0) {
return $this->course->idnumber; return $this->course->idnumber;
} }
} else if ($displayfield == "fullname") {
$fullname = trim(preg_replace("/\s+/u", " ", $this->course->fullname));
if (strlen($fullname) > 0) {
return $fullname;
}
} else if (strpos( $displayfield , "customfield_") === 0) { } else if (strpos( $displayfield , "customfield_") === 0) {
$fieldname = substr($displayfield, strlen("customfield_")); $fieldname = substr($displayfield, strlen("customfield_"));
@ -292,7 +297,7 @@ class courseinfo {
"canselectgradables" => new \external_value(PARAM_BOOL, 'Requesting user can change selected gradables'), "canselectgradables" => new \external_value(PARAM_BOOL, 'Requesting user can change selected gradables'),
"numenrolled" => new \external_value(PARAM_INT, 'number of students from this studyplan enrolled in the course'), "numenrolled" => new \external_value(PARAM_INT, 'number of students from this studyplan enrolled in the course'),
"tag" => new \external_value(PARAM_TEXT, 'Tag'), "tag" => new \external_value(PARAM_TEXT, 'Tag'),
"extrafields" => self::extrafields_structure(); "extrafields" => self::extrafields_structure(),
], 'referenced course information', $value); ], 'referenced course information', $value);
} }
@ -319,16 +324,15 @@ class courseinfo {
'context' => $contextinfo->model(), 'context' => $contextinfo->model(),
'ctxid' => $this->coursecontext->id, 'ctxid' => $this->coursecontext->id,
'timing' => $timing, 'timing' => $timing,
'startdate' => date("Y-m-d", $this->course->startdate, ), 'startdate' => date("Y-m-d", $this->course->startdate),
'enddate' => date("Y-m-d", $this->course->enddate), 'enddate' => date("Y-m-d", $this->course->enddate),
'amteacher' => $this->am_teacher(), 'amteacher' => $this->am_teacher(),
'canupdatecourse' => \has_capability("moodle/course:update", $this->coursecontext), 'canupdatecourse' => \has_capability("moodle/course:update", $this->coursecontext),
'canselectgradables' => $this->i_can_select_gradables(), 'canselectgradables' => $this->i_can_select_gradables(),
'tag' => "Editormodel", 'tag' => "Editormodel",
'extrafields' => $this->extrafields_model(), 'extrafields' => $this->extrafields_model(true),
'grades' => [], 'grades' => [],
'numenrolled' => $numenrolled, 'numenrolled' => $numenrolled,
]; ];
if (isset($this->studyitem)) { if (isset($this->studyitem)) {
@ -373,7 +377,8 @@ class courseinfo {
"startdate" => new \external_value(PARAM_TEXT, 'Course start date'), "startdate" => new \external_value(PARAM_TEXT, 'Course start date'),
"enddate" => new \external_value(PARAM_TEXT, 'Course end date'), "enddate" => new \external_value(PARAM_TEXT, 'Course end date'),
"enrolled" => new \external_value(PARAM_BOOL, 'True if student is enrolled as student in this course'), "enrolled" => new \external_value(PARAM_BOOL, 'True if student is enrolled as student in this course'),
"extrafields" => self::extrafields_structure(); "extrafields" => self::extrafields_structure(),
"showprogressbar" => new \external_value(PARAM_BOOL, "Whether to show the progress bar in the header"),
], 'course information', $value); ], 'course information', $value);
} }
@ -450,6 +455,7 @@ class courseinfo {
'grades' => [], 'grades' => [],
'enrolled' => $this->is_enrolled_student($userid), 'enrolled' => $this->is_enrolled_student($userid),
'extrafields' => $this->extrafields_model(), 'extrafields' => $this->extrafields_model(),
'showprogressbar' => get_config("local_treestudyplan","courseprogressbar"),
]; ];
@ -485,27 +491,37 @@ class courseinfo {
"value" => new \external_value(PARAM_RAW, 'value'), "value" => new \external_value(PARAM_RAW, 'value'),
"position" => new \external_value(PARAM_TEXT, 'position'), "position" => new \external_value(PARAM_TEXT, 'position'),
"type" => new \external_value(PARAM_TEXT, 'value type'), "type" => new \external_value(PARAM_TEXT, 'value type'),
"fieldname" => new \external_value(PARAM_TEXT, 'field name'),
"checked" => new \external_value(PARAM_BOOL, 'checkbox value',VALUE_OPTIONAL),
"courseid" => new \external_value(PARAM_TEXT, 'course id number'),
], 'referenced course information'), $value); ], 'referenced course information'), $value);
} }
/** /**
* Webservice model for basic info * Webservice model for basic info
* @param $includeteachervisible Include custom fiel
* @return array Webservice data model * @return array Webservice data model
*/ */
public function extrafields_model() { public function extrafields_model($includeteachervisible=false) {
$list = []; $list = [];
for ($i=1; $i <= 5; $i++) { for ($i=1; $i <= 5; $i++) {
$field = get_config('local_treestudyplan','courseinfo'.$i.'_field'); $field = get_config('local_treestudyplan','courseinfo'.$i.'_field');
$title = self::extrafields_localize_title(get_config('local_treestudyplan','courseinfo'.$i.'_title')); if ($field) {
$pos = get_config('local_treestudyplan','courseinfo'.$i.'_position'); $title = self::extrafields_localize_title(get_config('local_treestudyplan','courseinfo'.$i.'_title'));
[$value,$type] = $this->extrafields_value($field); $pos = get_config('local_treestudyplan','courseinfo'.$i.'_position');
$list[] = [ [$value,$type, $raw] = $this->extrafields_value($field,$includeteachervisible);
"title" => $title, if ($type) {
"value" => $value, $list[] = [
"position" => $pos, "title" => $title,
"type" => $type, "value" => $value,
]; "position" => $pos,
"type" => $type,
"fieldname" => $field,
"courseid" => $this->course->id,
"checked" => ($type=="checkbox")?($raw?true:false):null,
];
}
}
} }
return $list; return $list;
} }
@ -514,33 +530,51 @@ class courseinfo {
$lang = trim(current_language()); $lang = trim(current_language());
$lines = explode("\n",$field); $lines = explode("\n",$field);
$title = ""; $title = "";
$fallback = ""; // Fallback to first title
foreach ($lines as $l) { foreach ($lines as $l) {
$parts = explode("|",$l,2); $parts = explode("|",$l,2);
if (count($parts) > 0) { if (count($parts) > 0) {
if (count($parts) == 1 && empty($title)) { // Set the first line as fallback.
$title = trim($parts[0]); if (empty($firsttitle) && !empty($parts[0])) {
$fallback = $parts[0];
} }
else if (trim($parts[1]) == $lang) { if (count($parts) == 1 && empty($title)) {
// 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]); return trim($parts[0]);
} }
} }
} }
return $title; // Return default title or fall back to first localizef title.
return (strlen($title) > 0)?$title:$fallback;
} }
/** /**
* Determine value and type of an extra field for this course * Determine value and type of an extra field for this course
* @return array [value,type] of the field for this * @return array [value,type] of the field for this
*/ */
protected function extrafields_value($fieldname) { protected function extrafields_value($fieldname,$includeteachervisible=false) {
$fieldname = get_config("local_treestudyplan", "display_field");
if ($fieldname == "description") { if ($fieldname == "description") {
return [$this->course()->description, "textarea"]; // Process embedded files.
$value = \file_rewrite_pluginfile_urls(
// 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
'course',
'summary',
''
);
return [$value, "textarea", $this->course()->summary];
} else if ($fieldname == "idnumber") { } else if ($fieldname == "idnumber") {
$idnumber = trim(preg_replace("/\s+/u", " ", $this->course->idnumber)); $idnumber = trim(preg_replace("/\s+/u", " ", $this->course->idnumber));
return [$idnumber, "text"]; return [$idnumber, "text", $this->course->idnumber];
} else if ($fieldname == "contacts") { } else if ($fieldname == "contacts") {
$cle = new \core_course_list_element($this->course()); $cle = new \core_course_list_element($this->course());
$contacts = $cle->get_course_contacts(); $contacts = $cle->get_course_contacts();
@ -552,24 +586,41 @@ class courseinfo {
$value .= $contact["username"] . "(".$contact["role"]["name"].")"; $value .= $contact["username"] . "(".$contact["role"]["name"].")";
} }
return [$value, "text"]; return [$value, "text", $value];
} else if (strpos( $fieldname , "customfield_") === 0) { } else if (strpos( $fieldname , "customfield_") === 0) {
$fieldname = substr($fieldname, strlen("customfield_")); $fieldshortname = substr($fieldname, strlen("customfield_"));
$handler = \core_customfield\handler::get_handler('core_course', 'course'); $handler = \core_customfield\handler::get_handler('core_course', 'course');
$datas = $handler->get_instance_data($this->course->id); $datas = $handler->get_instance_data($this->course->id);
foreach ($datas as $data) { foreach ($datas as $data) {
$field = $data->get_field(); $field = $data->get_field();
if ($field->get('shortname') == $fieldname) { $fshortname = $field->get('shortname');
$value = trim(preg_replace("/\s+/u", " ", $data->get_value())); if ($fshortname == $fieldshortname) {
$visibility = $field->get_configdata_property("visibility");
$raw = $data->get_value();
$type = $field->get('type'); $type = $field->get('type');
if (strlen($value) > 0) {
return [$value,$type]; // 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.
if ($raw == 0) {
$value = "";
} else {
// Convert to YYYY-MM-DD format.
$value = date("Y-m-d", $raw);
}
} else {
// Everything else can just use the export value.
$value = $data->export_value();
}
return [$value,$type,$raw];
} }
} }
} }
} }
// Fallback to shortname when the specified display field fails, since shortname is never empty. // Fallback to empty if finding a match fails.
return ["",""]; return [null,null,null];
} }
} }

View File

@ -407,7 +407,6 @@ class studentstudyplanservice extends \external_api {
if ($studyplan->exist_for_user($userid)) { if ($studyplan->exist_for_user($userid)) {
$model = $studyplan->user_model($userid); $model = $studyplan->user_model($userid);
debug::dump($model);
return $model; return $model;
} else { } else {
throw new \moodle_exception("You do not have access to this studyplan"); throw new \moodle_exception("You do not have access to this studyplan");

View File

@ -1336,6 +1336,25 @@
.features-treestudyplan table.r-item-course-competency-list td { .features-treestudyplan table.r-item-course-competency-list td {
padding-right: 1em; padding-right: 1em;
} }
.path-local-treestudyplan header.modal-header.r-item-course-header,
.features-treestudyplan header.modal-header.r-item-course-header {
display: block;
padding-bottom: 0.2rem;
}
.path-local-treestudyplan header.modal-header.r-item-course-header .s-studyplan-card-progressbar,
.features-treestudyplan header.modal-header.r-item-course-header .s-studyplan-card-progressbar {
margin-top: 0.5em;
}
.path-local-treestudyplan div.r-item-course-header-details,
.features-treestudyplan div.r-item-course-header-details {
display: flex;
align-items: flex-start;
justify-content: space-between;
}
.path-local-treestudyplan div.r-item-course-header-details:last-child,
.features-treestudyplan div.r-item-course-header-details:last-child {
margin-bottom: 0.3rem;
}
.path-local-treestudyplan .card.s-studyplan-card, .path-local-treestudyplan .card.s-studyplan-card,
.features-treestudyplan .card.s-studyplan-card { .features-treestudyplan .card.s-studyplan-card {
@ -1467,6 +1486,31 @@
width: 128px; width: 128px;
height: 128px; height: 128px;
} }
.path-local-treestudyplan .s-course-extrafields.above,
.features-treestudyplan .s-course-extrafields.above {
border-bottom: 1px solid #dee2e6;
padding-bottom: 0.5em;
margin-bottom: 0.5em;
}
.path-local-treestudyplan .s-course-extrafields.below,
.features-treestudyplan .s-course-extrafields.below {
border-top: 1px solid #dee2e6;
padding-top: 0.5em;
margin-top: 0.5em;
}
.path-local-treestudyplan .s-course-extrafields .title,
.features-treestudyplan .s-course-extrafields .title {
font-weight: bold;
padding-right: 1em;
}
.path-local-treestudyplan .s-course-extrafields .value.true,
.features-treestudyplan .s-course-extrafields .value.true {
color: var(--success);
}
.path-local-treestudyplan .s-course-extrafields .value.false,
.features-treestudyplan .s-course-extrafields .value.false {
color: var(--danger);
}
.path-local-treestudyplan .b-modal-justify-footer-between .modal-footer, .path-local-treestudyplan .b-modal-justify-footer-between .modal-footer,
.features-treestudyplan .b-modal-justify-footer-between .modal-footer { .features-treestudyplan .b-modal-justify-footer-between .modal-footer {

View File

@ -1150,4 +1150,21 @@
} }
header.modal-header.r-item-course-header {
display: block;
.s-studyplan-card-progressbar {
margin-top: 0.5em;
}
padding-bottom: 0.2rem;
}
div.r-item-course-header-details {
display: flex;
align-items: flex-start;
justify-content: space-between;
&:last-child {
margin-bottom: 0.3rem;
}
}
} }

View File

@ -129,4 +129,35 @@
} }
.s-course-extrafields {
&.above {
border-bottom: 1px solid #dee2e6;
padding-bottom: 0.5em;
margin-bottom: 0.5em;
}
&.below {
border-top: 1px solid #dee2e6;
padding-top: 0.5em;
margin-top: 0.5em;
}
.title {
font-weight: bold;
padding-right: 1em;
}
.value {
&.true {
color: var(--success);
}
&.false {
color: var(--danger);
}
}
}
} }

View File

@ -88,17 +88,21 @@ if ($hassiteconfig) {
get_string('settingdesc_display_heading', 'local_treestudyplan') get_string('settingdesc_display_heading', 'local_treestudyplan')
)); ));
$displayfields = ["shortname" => get_string("shortname"), "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")]; $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'); $handler = \core_customfield\handler::get_handler('core_course', 'course');
foreach ($handler->get_categories_with_fields() as $cat) { foreach ($handler->get_categories_with_fields() as $cat) {
$catname = $cat->get_formatted_name(); $catname = $cat->get_formatted_name();
foreach ($cat->get_fields() as $field) { foreach ($cat->get_fields() as $field) {
$fieldname = $field->get_formatted_name(); $visibility = $field->get_configdata_property("visibility");
$fieldid = $field->get("shortname"); if ($visibility > 0) {
$displayfields["customfield_".$fieldid] = $catname.": ".$fieldname; // Only include fields that are visible to Teachers, or Everyone.
$infofields["customfield_".$fieldid] = $catname.": ".$fieldname; $fieldname = $field->get_formatted_name();
$fieldid = $field->get("shortname");
$displayfields["customfield_".$fieldid] = $catname.": ".$fieldname;
$infofields["customfield_".$fieldid] = $catname.": ".$fieldname;
}
} }
} }
@ -122,8 +126,6 @@ if ($hassiteconfig) {
$positions = [ "above" => get_string('infofield_position_above', 'local_treestudyplan'), $positions = [ "above" => get_string('infofield_position_above', 'local_treestudyplan'),
"below" => get_string("infofield_position_below", 'local_treestudyplan'), "below" => get_string("infofield_position_below", 'local_treestudyplan'),
"header" => get_string("infofield_position_header", 'local_treestudyplan'),
"footer" => get_string("infofield_position_footer", 'local_treestudyplan')
]; ];
for ($i=1;$i<=5;$i++) { for ($i=1;$i<=5;$i++) {

View File

@ -1336,6 +1336,25 @@
.features-treestudyplan table.r-item-course-competency-list td { .features-treestudyplan table.r-item-course-competency-list td {
padding-right: 1em; padding-right: 1em;
} }
.path-local-treestudyplan header.modal-header.r-item-course-header,
.features-treestudyplan header.modal-header.r-item-course-header {
display: block;
padding-bottom: 0.2rem;
}
.path-local-treestudyplan header.modal-header.r-item-course-header .s-studyplan-card-progressbar,
.features-treestudyplan header.modal-header.r-item-course-header .s-studyplan-card-progressbar {
margin-top: 0.5em;
}
.path-local-treestudyplan div.r-item-course-header-details,
.features-treestudyplan div.r-item-course-header-details {
display: flex;
align-items: flex-start;
justify-content: space-between;
}
.path-local-treestudyplan div.r-item-course-header-details:last-child,
.features-treestudyplan div.r-item-course-header-details:last-child {
margin-bottom: 0.3rem;
}
.path-local-treestudyplan .card.s-studyplan-card, .path-local-treestudyplan .card.s-studyplan-card,
.features-treestudyplan .card.s-studyplan-card { .features-treestudyplan .card.s-studyplan-card {
@ -1467,6 +1486,31 @@
width: 128px; width: 128px;
height: 128px; height: 128px;
} }
.path-local-treestudyplan .s-course-extrafields.above,
.features-treestudyplan .s-course-extrafields.above {
border-bottom: 1px solid #dee2e6;
padding-bottom: 0.5em;
margin-bottom: 0.5em;
}
.path-local-treestudyplan .s-course-extrafields.below,
.features-treestudyplan .s-course-extrafields.below {
border-top: 1px solid #dee2e6;
padding-top: 0.5em;
margin-top: 0.5em;
}
.path-local-treestudyplan .s-course-extrafields .title,
.features-treestudyplan .s-course-extrafields .title {
font-weight: bold;
padding-right: 1em;
}
.path-local-treestudyplan .s-course-extrafields .value.true,
.features-treestudyplan .s-course-extrafields .value.true {
color: var(--success);
}
.path-local-treestudyplan .s-course-extrafields .value.false,
.features-treestudyplan .s-course-extrafields .value.false {
color: var(--danger);
}
.path-local-treestudyplan .b-modal-justify-footer-between .modal-footer, .path-local-treestudyplan .b-modal-justify-footer-between .modal-footer,
.features-treestudyplan .b-modal-justify-footer-between .modal-footer { .features-treestudyplan .b-modal-justify-footer-between .modal-footer {