/*eslint no-var: "error"*/
/*eslint no-console: "off"*/
/*eslint no-unused-vars: warn */
/*eslint max-len: ["error", { "code": 160 }] */
/*eslint-disable no-trailing-spaces */
/*eslint-env es6*/
// Put this file in path/to/plugin/amd/src
import {SimpleLine} from './simpleline/simpleline';
import {get_strings} from 'core/str';
import {load_strings} from './util/string-helper';
import {format_date, format_datetime, studyplanPageTiming,studyplanTiming} from './util/date-helper';
import {call} from 'core/ajax';
import notification from 'core/notification';
import {svgarcpath} from './util/svgarc';
import Debugger from './util/debugger';
import Config from 'core/config';
import {ProcessStudyplan, ProcessStudyplanPage, objCopy} from './studyplan-processor';
import TSComponents from './treestudyplan-components';
import {eventTypes as editSwEventTypes} from 'core/edit_switch';
import { premiumenabled, premiumstatus } from "./util/premium";
import FitTextVue from './util/fittext-vue';
// Make π available as a constant
const π = Math.PI;
// Gravity value for arrow lines - determines how much a line is pulled in the direction of the start/end before changing direction
const LINE_GRAVITY = 1.3;
/**
* Studyline is not enrollable
* @var int
*/
const ENROLLABLE_NONE = 0;
/**
* Studyline can be enrolled into by the student
* @var int
*/
const ENROLLABLE_SELF = 1;
/**
* Studyline can be enrolled into by specific role(s)
* @var int
*/
const ENROLLABLE_ROLE = 2;
/**
* Studyline can be enrolled by user and/or role
* @var int
*/
const ENROLLABLE_SELF_ROLE = 3;
export default {
install(Vue/*,options*/){
Vue.use(TSComponents);
Vue.use(FitTextVue);
let debug = new Debugger("treestudyplan-viewer");
let lastCaller = null;
/**
* Scroll current period into view
* @param {*} handle A key to pass so subsequent calls with the same key won't trigger (always triggers when null or undefined)
*/
function scrollCurrentIntoView(handle){
const elScrollContainer = document.querySelector(".r-studyplan-scrollable");
const elCurrentHeader = elScrollContainer.querySelector(".s-studyline-header-period.current");
if(elCurrentHeader && ((!handle) || (handle != lastCaller))){
lastCaller = handle;
elCurrentHeader.scrollIntoView({
behavior: "smooth",
block: "start",
inline: "center",
});
}
}
let strings = load_strings({
report: {
loading: "loadinghelp@core",
studyplan_past: "studyplan_past",
studyplan_present: "studyplan_present",
studyplan_future: "studyplan_future",
back: "back",
},
invalid: {
error: 'error',
},
grading: {
ungraded: "ungraded",
graded: "graded",
allgraded: "allgraded",
unsubmitted: "unsubmitted",
nogrades: "nogrades",
unknown: "unknown",
},
completion: {
completed: "completion_completed",
incomplete: "completion_incomplete",
completed_pass: "completion_passed",
completed_fail: "completion_failed",
ungraded: "ungraded",
aggregation_all: "aggregation_all",
aggregation_any: "aggregation_any",
aggregation_one: "aggregation_one",
aggregation_overall_all: "aggregation_overall_all",
aggregation_overall_any: "aggregation_overall_any",
aggregation_overall_one: "aggregation_overall_one",
completion_not_configured: "completion_not_configured",
configure_completion: "configure_completion",
view_completion_report: "view_completion_report",
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",
student_not_tracked: "student_not_tracked",
completion_not_enabled: "completion_not_enabled",
},
badge: {
share_badge: "share_badge",
dateissued: "dateissued",
dateexpire: "dateexpire",
badgeinfo: "badgeinfo",
badgeissuedstats: "badgeissuedstats",
completion_incomplete: "completion_incomplete_badge",
completion_completed: "completion_completed_badge",
completioninfo: "completioninfo",
badgedisabled: "badgedisabled"
},
course: {
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",
student_not_tracked: "student_not_tracked",
not_enrolled: "not_enrolled",
},
teachercourse: {
select_conditions: "select_conditions",
select_grades: "select_grades",
coursetiming_past: "coursetiming_past",
coursetiming_present: "coursetiming_present",
coursetiming_future: "coursetiming_future",
grade_include: "grade_include",
grade_require: "grade_require",
required_goal: "required_goal",
student_from_plan_enrolled: "student_from_plan_enrolled",
students_from_plan_enrolled: "students_from_plan_enrolled",
},
competency: {
competency_not_configured: "competency_not_configured",
configure_competency: "configure_competency",
when: "when",
required: "required",
points: "points@core_grades",
heading: "competency_heading",
details: "competency_details",
results: "results",
unrated: "unrated",
progress: "completion_progress",
view_feedback: "view_feedback",
},
pageinfo: {
edit: 'period_edit',
fullname: 'studyplan_name',
shortname: 'studyplan_shortname',
startdate: 'studyplan_startdate',
enddate: 'studyplan_enddate',
description: 'studyplan_description',
duration: 'studyplan_duration',
details: 'studyplan_details',
overview: 'overviewreport:all',
oveviewperiod: 'overviewreport:period'
},
lineheader: {
cannot_enrol: 'line_cannot_enrol',
can_enrol: 'line_can_enrol',
is_enrolled: 'line_is_enrolled',
enrol: 'line_enrol',
unenrol: 'line_unenrol',
enrolled: 'line_enrolled',
notenrolled: 'line_notenrolled',
enrol_question: 'line_enrol_question',
enrollments: 'line_enrollments',
enrollment: 'line_enrollment',
info: 'info@core',
confirm: 'confirm@core',
yes: 'yes@core',
no: 'no@core',
enrolled_in: 'line_enrolled_in',
since: 'since@core',
byname: 'byname@core',
students: 'students@core',
firstname: 'firstname@core',
lastname: 'lastname@core',
email: 'email@core',
enrol_student_question: 'line_enrol_student_question',
unenrol_student_question: 'line_unenrol_student_question',
}
});
/************************************
* *
* Treestudyplan Viewer components *
* *
************************************/
/**
* Check if element is visible
* @param {Object} elem The element to check
* @returns {boolean} True if visible
*/
function isVisible(elem){
return !!( elem.offsetWidth || elem.offsetHeight || elem.getClientRects().length );
}
// Create new eventbus for interaction between item components
const ItemEventBus = new Vue();
// Add event listener for the edit mode event so we can react to it, or at the very least ignore it
document.addEventListener(editSwEventTypes.editModeSet,(e) => {
e.preventDefault();
ItemEventBus.$emit('editModeSet',e.detail.editMode);
});
Vue.component('r-progress-circle',{
props: {
value: {
type: Number,
},
max: {
type: Number,
default: 100,
},
min: {
type: Number,
default: 0,
},
stroke: {
type: Number,
default: 0.2,
},
bgopacity: {
type: Number,
default: 0.2,
},
title: {
type: String,
default: "",
},
icon: {
type: String,
}
},
data() {
return {
selectedstudyplan: null,
};
},
computed: {
range() {
return this.max - this.min;
},
fraction(){
if(this.max - this.min == 0){
return 0;
// 0 size is always empty :)
} else {
return (this.value - this.min)/(this.max - this.min);
}
},
radius() {
return 50 - (50*this.stroke);
},
arcpath() {
let fraction = 0;
const r = 50 - (50*this.stroke);
if(this.max - this.min != 0){
fraction = (this.value - this.min)/(this.max - this.min);
}
const Δ = fraction * 2*π;
return svgarcpath([50,50],[r,r],[0,Δ], 1.5*π);
},
},
methods: {
},
template: `
`,
});
Vue.component('r-report', {
props: {
invitekey: {
type: String,
default() { return null;},
},
userid: {
type: Number,
default() { return 0;},
},
type: {
type: String,
default() { return "own";},
},
},
data() {
return {
text: strings.report,
studyplans: {
past: [],
present: [],
future: [],
},
selectedstudyplan: null,
loadingstudyplan: false,
loading: true,
};
},
computed: {
teachermode() {
return (this.type =="teaching");
},
guestmode() {
return (this.type == "invited");
},
verified_type() {
if (! ["invited","other","teaching","own"].includes(this.type)) {
return "own";
} else {
return this.type;
}
},
studyplancount() {
return this.studyplans.past.length + this.studyplans.present.length + this.studyplans.future.length;
}
},
updated() {
},
mounted() {
this.loadStudyplans();
},
methods: {
call_args(o) {
const args = {};
if (typeof o == 'object' && !Array.isArray(o) && o !== null) {
objCopy(args,o);
}
if(this.verified_type == "invited") {
args["invitekey"] = this.invitekey;
} else if(this.verified_type == "other") {
args["userid"] = this.userid;
}
return args;
},
loadStudyplans() {
const self = this;
this.loading = true;
call([{
methodname: `local_treestudyplan_list_${this.verified_type}_studyplans`,
args: this.call_args(),
}])[0].then(function(response){
console.info("Loaded: plans",response);
const plans = { future: [], present: [], past: [], };
for (const ix in response) {
const plan = response[ix];
const timing = studyplanTiming(plan);
plans[timing].push(plan);
}
for (const ix in plans) {
plans[ix].sort((a,b) => {
const t = new Date(b.startdate).getTime() - new Date(a.startdate).getTime();
if (t == 0) {
// sort by name if timing is equal
t = a.name.localeCompare(b.name);
}
return t;
});
}
self.studyplans = plans;
self.loading = false;
// load studyplan from hash if applicable
const hash = window.location.hash.replace('#','');
const parts = hash.split("-");
if (!!parts && parts.length > 0) {
for (const k in self.studyplans) {
const list = self.studyplans[k];
for (const idx in list) {
const plan = list[idx];
if (plan.id == parts[0]){
self.selectStudyplan(plan);
return;
}
}
}
}
if (self.studyplans.present.length == 1) {
// Directly show the current study plan if it's the only current one
self.selectStudyplan(self.studyplans.present[0]);
} else {
// If there is but a single studyplan, select it anyway, even if it is not current...
if (this.studyplancount == 1) {
if(self.studyplans.future.lengh > 0) {
self.selectStudyplan(self.studyplans.future[0]);
} else {
self.selectStudyplan(self.studyplans.past[0]);
}
}
}
}).catch(notification.exception);
},
selectStudyplan(plan) {
const self = this;
this.loadingstudyplan = true;
call([{
methodname: `local_treestudyplan_get_${this.verified_type}_studyplan`,
args: this.call_args({
studyplanid: plan.id,
}),
}])[0].then(function(response){
self.selectedstudyplan = ProcessStudyplan(response);
self.loadingstudyplan = false;
window.location.hash = self.selectedstudyplan.id;
}).catch(notification.exception);
},
deselectStudyplan() {
this.selectedstudyplan = null;
this.loadStudyplans(); // Reload the list of studyplans.
window.location.hash = '';
}
},
template: `
{{ text.loading }}
{{ text["studyplan_"+timing]}}:
`,
});
Vue.component('r-studyplan', {
props: {
value: {
type: Object,
},
guestmode: {
type: Boolean,
default: false,
},
teachermode: {
type: Boolean,
default: false,
},
coaching: {
type: Boolean,
default: false,
},
},
data() {
return {
selectedpageindex: -1,
text: strings.pageinfo,
};
},
computed: {
selectedpage() {
return this.value.pages[this.selectedpageindex];
},
startpageindex() {
let startpageindex = 0;
let firststart = null;
for(const ix in this.value.pages) {
const page = this.value.pages[ix];
if(studyplanPageTiming(page) == "present") {
const s = new Date(page.startdate);
if( (!firststart) || firststart > s) {
startpageindex = ix;
firststart = s;
}
}
}
return startpageindex;
},
wwwroot() {
return Config.wwwroot;
},
},
methods: {
pageduration(page){
return format_date(page.startdate,false) + " - " + format_date(page.enddate,false);
},
columns(page) {
return 1+ (page.periods * 2);
},
columns_stylerule(page) {
// Uses css variables, so width for slots and filters can be configured in css
let s = "grid-template-columns: var(--studyplan-filter-width)"; // use css variable here
for(let i=0; i maxLayer){
maxLayer = item.layer;
}
}
for(const ix in line.slots[i].filters){
const item = line.slots[i].filters[ix];
if(item.layer > maxLayer){
maxLayer = item.layer;
}
}
}
return (maxLayer >= 0)?(maxLayer+1):1;
},
showslot(page,line,index, layeridx, type){
// check if the slot should be hidden because a previous slot has an item with a span
// so big that it hides this slot
const forGradable = (type == 'gradable')?true:false;
const periods = page.periods;
let show = true;
for(let i = 0; i < periods; i++){
if(line.slots[index-i] && line.slots[index-i].courses){
const list = line.slots[index-i].courses;
for(const ix in list){ // Really wish that 'for of' would work with the minifier moodle uses
const item = list[ix];
if(item.layer == layeridx){
if(forGradable){
if(i > 0 && (item.span - i) > 0){
show = false;
}
} else {
if((item.span - i) > 1){
show = false;
}
}
}
}
}
}
return show;
},
selectedpageChanged(newTabIndex,prevTabIndex) {
ItemEventBus.$emit('redrawLines', null);
scrollCurrentIntoView(this.selectedpage.id);
},
},
mounted() {
// scrollCurrentIntoView(this.selectedpage.id);
this.$root.$emit('redrawLines');
},
updated() {
scrollCurrentIntoView(this.selectedpage.id);
ItemEventBus.$emit('lineHeightChange', null);
this.$root.$emit('redrawLines');
ItemEventBus.$emit('redrawLines');
},
template: `
{{page.shortname}}
{{page.fullname}}
{{ text.shortname}}
{{ page.shortname }}
{{ text.duration}}
{{ pageduration(page) }}
{{ text.description}}
`,
});
/*
* R-STUDYLINE-HEADER
*/
Vue.component('r-studyline-heading', {
props: {
value : {
type: Object, // Studyline
default: function(){ return {};},
},
guestmode: {
type: Boolean,
default: false,
},
teachermode: {
type: Boolean,
default: false,
},
layers: {
type: Number,
default: 1,
},
studentid: {
type: Number,
},
},
data() {
return {
layerHeights: {},
text: strings.lineheader,
students: null,
can_unenrol: false,
sorting: {
asc: false,
field: 'enrolled_time'
}
};
},
created() {
// Listener for the signal that a new connection was made and needs to be drawn
// Sent by the incoming item - By convention, outgoing items are responsible for drawing the lines
ItemEventBus.$on('lineHeightChange', this.onLineHeightChange);
},
computed: {
enrollable() {
return this.value.enrol.enrollable > ENROLLABLE_NONE;
},
enrollable_self() {
return [ENROLLABLE_SELF,ENROLLABLE_SELF_ROLE].includes(this.value.enrol.enrollable);
},
enrollable_role() {
return [ENROLLABLE_ROLE,ENROLLABLE_SELF_ROLE].includes(this.value.enrol.enrollable);
},
enrolled() {
return this.value.enrol.enrolled?true:false;
},
can_enrol() {
return this.value.enrol.can_enrol?true:false;
},
enrol_question() {
return this.text.enrol_question.replace('{$a}',this.value.name);
},
enrolled_in() {
return this.text.enrolled_in.replace('{$a}',this.value.name);
},
by() {
return this.text.byname.replace('{$a}','');
},
enrolldate() {
return format_datetime(this.value.enrol.enrolled_time);
},
sorted_students() {
const self =this;
const list = Array.isArray(this.students)?this.students:[];
list.sort((a,b) => {
let d = a;
let e = b;
if (!self.sorting.asc) {
d = b;
e = a;
}
let df = d;
let ef = e;
const field = self.sorting.field;
if (d.user && d.user.hasOwnProperty(field)) {
df = d.user;
ef = e.user;
} else if (d.enrol && d.enrol.hasOwnProperty(field)) {
df = d.enrol;
ef = e.enrol;
}
if (field == 'enrolled') {
return ((df.enrolled)?1:0) - ((ef.enrolled)?1:0);
} else if (field == "enrolled_time") {
const dvalue = (df[field] && d.enrol.enrolled)?df[field]:0;
const evalue = (ef[field] && e.enrol.enrolled)?ef[field]:0;
return dvalue - evalue;
} else {
return String(df[this.sorting.field]).localeCompare(String(ef[this.sorting.field]));
}
});
return list;
}
},
methods: {
premiumenabled,
format_datetime,
onLineHeightChange(lineid){
// All layers for this line have the first slot send an update message on layer height change.
// When one of those updates is received, record the height and recalculate the total height of the
// header
if(this.$refs.mainEl && (lineid == this.value.id || lineid === null)){
const items = document.querySelectorAll(
`.r-studyline-slot-0[data-studyline='${this.value.id}']`);
// determine the height of all the lines and add them up.
let heightSum = 0;
items.forEach((el) => {
// getBoundingClientRect() Gets the actual fractional height instead of rounded to integer pixels
const r = el.getBoundingClientRect();
const height = r.height;
heightSum += height;
});
const heightStyle=`${heightSum}px`;
this.$refs.mainEl.style.height = heightStyle;
}
},
enrol_self() {
const self=this;
call([{
methodname: 'local_treestudyplan_line_enrol_self',
args: { id: self.value.id},
}])[0].then(function(response){
self.$emit('enrolupdate',response);
}).catch(notification.exception);
},
enrol_student(student) {
const self=this;
const user = student.user;
let question = self.text.enrol_student_question.replace('{$a}',`${user.firstname} ${user.lastname}`);
const options = {
okTitle: self.text.yes,
cancelTitle: self.text.no,
okVariant: "success",
cancelVariant: "danger",
};
this.$bvModal.msgBoxConfirm(question, options).then(reply => {
if(reply) {
call([{
methodname: 'local_treestudyplan_line_enrol_students',
args: { id: self.value.id, users: [user.id] },
}])[0].then(function(response){
student.enrol = response[0].enrol;
}).catch(notification.exception);
}
});
},
unenrol_student(student) {
const self=this;
const user = student.user;
let question = self.text.enrol_student_question.replace('{$a}',`${user.firstname} ${user.lastname}`);
const options = {
okTitle: self.text.yes,
cancelTitle: self.text.no,
okVariant: "success",
cancelVariant: "danger",
};
this.$bvModal.msgBoxConfirm(question, options).then(reply => {
if(reply) {
call([{
methodname: 'local_treestudyplan_line_unenrol_students',
args: { id: self.value.id, users: [user.id] },
}])[0].then(function(response){
student.enrol = response[0].enrol;
}).catch(notification.exception);
}
});
},
load_students() {
const self=this;
self.students=null;
self.can_unenrol=false;
call([{
methodname: 'local_treestudyplan_list_line_enrolled_students',
args: { id: self.value.id },
}])[0].then(function(response){
self.students = response.userinfo;
self.can_unenrol = response.can_unenrol;
}).catch(notification.exception);
},
toggleSort(header) {
if (this.sorting.field == header) {
this.sorting.asc = !this.sorting.asc;
} else {
this.sorting.field = header;
this.sorting.asc = true;
}
},
},
template: `
`,
});
Vue.component('r-studyline-slot', {
props: {
value: {
type: Array, // item to display
default(){ return [];},
},
type : {
type: String,
default: 'gradable',
},
slotindex : {
type: Number,
default: 0,
},
line : {
type: Object,
default(){ return null;},
},
layer : {
type: Number,
},
plan: {
type: Object,
default(){ return null;},
},
page: {
type: Object,
default(){ return null;},
},
guestmode: {
type: Boolean,
default: false,
},
teachermode: {
type: Boolean,
default: false,
},
period: {
type: Object,
default(){ return null;},
}
},
mounted() {
const self=this;
if(self.type == "gradable" && self.slotindex == 1){
self.resizeListener = new ResizeObserver(() => {
if(self.$refs.sizeElement){
const height = self.$refs.sizeElement.getBoundingClientRect().height;
ItemEventBus.$emit('lineHeightChange', self.line.id, self.layer, height);
}
}).observe(self.$refs.sizeElement);
}
},
computed: {
item(){
for(const ix in this.value){
const itm = this.value[ix];
if(itm.layer == this.layer){
return itm;
}
}
return null;
},
spanCss(){
if(this.item && this.item.span > 1){
const span = (2 * this.item.span) - 1;
return `width: 100%; grid-column: span ${span};`;
} else {
return "";
}
},
cloud() {
const enrol = this.line.enrol;
return (!this.teachermode) && (enrol.enrollable > 0) && (!enrol.enrolled);
}
},
data() {
return {
};
},
methods: {
},
template: `
`,
});
Vue.component('r-item', {
props: {
value :{
type: Object,
default: function(){ return null;},
},
plan: {
type: Object,
default(){ return null;}
},
guestmode: {
type: Boolean,
default: false,
},
teachermode: {
type: Boolean,
default: false,
},
cloud: {
type: Boolean,
default: false,
}
},
data() {
return {
lines: [],
};
},
methods: {
lineColor(){
if(this.teachermode) {
return "var(--gray)";
} else if (this.cloud) {
return "#ccc";
} else {
switch(this.value.completion){
default: // "incomplete"
return "var(--gray)";
case "failed":
return "var(--danger)";
case "progress":
return "var(--warning)";
case "completed":
return "var(--success)";
case "good":
return "var(--info)";
case "excellent":
return "var(--blue)";
}
}
},
redrawLine(conn){
let lineColor = this.lineColor();
// draw new line...
let start = document.getElementById('studyitem-'+conn.from_id);
let end= document.getElementById('studyitem-'+conn.to_id);
// delete old line
if(this.lines[conn.to_id]){
this.lines[conn.to_id].remove();
delete this.lines[conn.to_id];
}
if(start !== null && end !== null && isVisible(start) && isVisible(end)){
this.lines[conn.to_id] = new SimpleLine(start,end,{
color: lineColor,
gravity: {
start: LINE_GRAVITY,
end: LINE_GRAVITY,
},
class: (this.cloud?"r-dummy-line":""),
});
}
},
redrawLines(){
// Clean all old lines
for(let ix in this.lines){
let lineinfo = this.lines[ix];
if(lineinfo && lineinfo.line){
lineinfo.line.remove();
lineinfo.line = undefined;
}
}
// Create new lines
for(let i in this.value.connections.out){
let conn = this.value.connections.out[i];
this.redrawLine(conn);
}
},
onWindowResize(){
this.redrawLines();
},
onRedrawLines(){
this.redrawLines();
},
removeLine(conn){
if(this.lines[conn.to_id]){
this.lines[conn.to_id].remove();
delete this.lines[conn.to_id];
}
},
},
computed: {
hasConnectionsOut() {
return !(["finish",].includes(this.value.type));
},
hasConnectionsIn() {
return !(["start",].includes(this.value.type));
},
hasContext() {
return ['start','junction','finish'].includes(this.value.type);
}
},
created(){
ItemEventBus.$on('redrawLines', this.onRedrawLines);
},
mounted(){
// Initialize connection lines when mounting
this.redrawLines();
setTimeout(()=>{
this.redrawLines();
},50);
// Add resize event listener
window.addEventListener('resize',this.onWindowResize);
},
beforeDestroy(){
for(let i in this.value.connections.out){
let conn = this.value.connections.out[i];
this.removeLine(conn);
}
// Remove resize event listener
window.removeEventListener('resize',this.onWindowResize);
ItemEventBus.$off('redrawLines', this.onRedrawLines);
},
beforeUpdate(){
},
updated(){
if(!this.dummy) {
this.redrawLines();
}
},
template: `
`,
});
Vue.component('r-item-invalid', {
props: {
'value' :{
type: Object,
default: function(){ return null;},
},
},
data() {
return {
text: strings.invalid,
};
},
methods: {
},
template: `
{{ text.error }}
`,
});
//TAG: Item Course
Vue.component('r-item-course', {
props: {
value :{
type: Object,
default(){ return null;},
},
guestmode: {
type: Boolean,
default(){ return false;}
},
teachermode: {
type: Boolean,
default(){ return false;}
},
plan: {
type: Object,
default(){ return null;}
}
},
data() {
return {
text: strings.course,
};
},
computed: {
startdate(){
return format_date(this.value.course.startdate);
},
enddate(){
if(this.value.course.enddate){
return format_date(this.value.course.enddate);
}
else {
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);
}
},
wwwroot() {
return Config.wwwroot;
}
},
created(){
},
methods: {
completion_icon(completion) {
switch(completion){
default: // case "incomplete"
return "circle-o";
case "pending":
return "question-circle";
case "failed":
return "times-circle";
case "progress":
return "exclamation-circle";
case "completed":
return "check-circle";
case "good":
return "check-circle";
case "excellent":
return "check-circle";
}
},
circle_icon(completion) {
switch(completion){
default: // case "incomplete"
return null;
case "failed":
return "times";
case "progress":
return "";
case "completed":
return "check";
case "good":
return "check";
case "excellent":
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: `
`,
});
//Selected activities dispaly
Vue.component('r-item-studentgrades',{
props: {
value : {
type: Object,
default: function(){ return {};},
},
guestmode: {
type: Boolean,
default: false,
},
},
data() {
return {
text: strings.course,
};
},
computed: {
pendingsubmission(){
let result = false;
for(const ix in this.value.course.grades){
const g = this.value.course.grades[ix];
if(g.pendingsubmission){
result = true;
break;
}
}
return result;
},
useRequiredGrades() {
if(this.plan && this.plan.aggregation_info && this.plan.aggregation_info.useRequiredGrades !== undefined){
return this.plan.aggregation_info.useRequiredGrades;
}
else {
return false;
}
},
},
methods: {
completion_icon(completion) {
switch(completion){
default: // case "incomplete"
return "circle-o";
case "pending":
return "question-circle";
case "failed":
return "times-circle";
case "progress":
return "exclamation-circle";
case "completed":
return "check-circle";
case "good":
return "check-circle";
case "excellent":
return "check-circle";
}
},
},
template: `
`,
});
// Core completion version of student course info
Vue.component('r-item-studentcompletion',{
props: {
value : {
type: Object,
default: function(){ return {};},
},
guestmode: {
type: Boolean,
default: false,
},
course: {
type: Object,
default: function(){ return {};},
},
},
data() {
return {
text: strings.completion,
};
},
created(){
},
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';
},
hasCompletions() {
if(this.value.conditions) {
for(const cgroup of this.value.conditions){
if(cgroup.items && cgroup.items.length > 0){
return true;
}
}
}
return false;
},
requirementHTML(requirements) {
const rqs = requirements.split(/, */);
let html = "";
for(const ix in rqs) {
const rq = rqs[ix];
html += `${rq}
`;
}
return html;
},
addTargetBlank(html) {
const m = /^([^<]*\< *a +)(.*)/.exec(html);
if(m){
return `${m[1]} target="_blank" ${m[2]}`;
} else {
return html;
}
}
},
template: `
{{ text.aggregation_overall_one }}{{ text.aggregation_overall_all}}{{ text.aggregation_overall_any }} |
{{text.completion_not_configured}}!
|
{{ text.aggregation_all}}{{ text.aggregation_any}}{{ text.aggregation_one }}
{{ cgroup.title.toLowerCase() }}:
|
|
| {{ci.grade}}
|
|
{{ text["view_feedback"]}}
{{ course.fullname }}
{{ci.name}}
|
{{text.completion_not_enabled}} |
{{text.student_not_tracked}} |
`,
});
//TAG: STUDENT Course competency
Vue.component('r-item-student-course-competency',{
props: {
value : {
type: Object,
default: function(){ return {};},
},
guestmode: {
type: Boolean,
default: false,
},
item: {
type: Object,
default: function(){ return { id: null};},
}
},
data() {
return {
text: strings.competency,
};
},
created(){
},
computed: {
hasCompletions() {
if(this.value.conditions) {
for(const cgroup of this.value.conditions){
if(cgroup.items && cgroup.items.length > 0){
return true;
}
}
}
return false;
},
wwwroot() {
return Config.wwwroot;
}
},
methods: {
completion_icon(competency) {
if (competency.proficient && competency.courseproficient) {
return "check-circle";
} else if (competency.proficient) {
return "check";
} else if (competency.proficient === false) {
return "times-circle";
} else {
return "circle-o";
}
},
completion_tag(competency){
if (competency.proficient && competency.courseproficient) {
return "completed";
} else if (competency.proficient) {
return "completed";
} else if (competency.proficient === false) {
return "failed";
} else if (competency.progress) {
return "progress";
} else {
return "incomplete";
}
},
pathtags(competency) {
const path = competency.path;
let s = "";
for (const ix in path) {
const p = path[ix];
if ( ix > 0) {
s += " / ";
}
let url;
if (p.type =='competency') {
url = Config.wwwroot+`/admin/tool/lp/user_competency_in_course.php?courseid=${this.item.course.id}&competencyid=${p.id}`;
} else {
url = this.competencyurl(p);
}
s += `${p.title}`;
}
return s;
},
competencyurl(c) {
return Config.wwwroot+`/admin/tool/lp/user_competency_in_course.php?courseid=${this.item.course.id}&competencyid=${c.id}`;
},
usercompetencyurl(c) {
return Config.wwwroot+`/admin/tool/lp/user_competency.php?id=${c.ucid}`;
}
},
template: `
`,
});
//TAG: Teacher course
Vue.component('r-item-teachercourse', {
props: {
value :{
type: Object,
default(){ return null;}
},
guestmode: {
type: Boolean,
default(){ return false;}
},
teachermode: {
type: Boolean,
default(){ return false;}
},
plan: {
type: Object,
default(){ return null;}
}
},
data() {
return {
text: strings.teachercourse,
txt: {
grading: strings.grading,
}
};
},
computed: {
course_grading_needed(){
return this.course_grading_state();
},
course_grading_icon(){
return this.determine_grading_icon(this.course_grading_state());
},
filtered_grades(){
return this.value.course.grades.filter(g => g.selected);
},
useRequiredGrades() {
if(this.plan && this.plan.aggregation_info && this.plan.aggregation_info.useRequiredGrades !== undefined){
return this.plan.aggregation_info.useRequiredGrades;
}
else {
return false;
}
},
isCompletable() {
let completable = false;
if(this.value.course.completion){
if(this.value.course.completion.conditions.length > 0){
completable = true;
}
} else if (this.value.course.grades){
if(this.value.course.grades.length > 0){
completable = true;
}
}
return completable;
},
progress_circle() {
const status = {
students: 0,
completed: 0,
completed_pass: 0,
completed_fail: 0,
ungraded: 0,
};
if(this.value.course.completion) {
for(const cond of this.value.course.completion.conditions) {
for(const itm of cond.items) {
if(itm.progress) {
status.students += itm.progress.students;
status.completed += itm.progress.completed;
status.completed_pass += itm.progress.completed_pass;
status.completed_fail += itm.progress.completed_fail;
status.ungraded += itm.progress.ungraded;
}
}
}
} else if (this.value.course.competency) {
status.students = this.value.course.competency.total;
status.completed = this.value.course.competency.proficient;
} else if (this.value.course.grades) {
for( const g of this.value.course.grades) {
if(g.grading) {
status.students += g.grading.students;
status.completed += g.grading.completed;
status.completed_pass += g.grading.completed_pass;
status.completed_fail += g.grading.completed_fail;
status.ungraded += g.grading.ungraded;
}
}
}
return status;
},
startdate(){
return format_date(this.value.course.startdate);
},
enddate(){
if(this.value.course.enddate){
return format_date(this.value.course.enddate);
}
else {
return this.text.noenddate;
}
},
wwwroot() {
return Config.wwwroot;
}
},
created(){
},
methods: {
course_grading_state(){
let ungraded = 0;
let unknown = 0;
let graded = 0;
let allgraded = 0;
const grades = this.filtered_grades;
if(!Array.isArray(grades) || grades == 0){
return 'nogrades';
}
for(const ix in grades){
const grade = grades[ix];
if(grade.grading){
if(Number(grade.grading.ungraded) > 0){
ungraded++;
}
else if(Number(grade.grading.graded) > 0) {
if(Number(grade.grading.graded) == Number(grade.grading.students)){
allgraded++;
} else {
graded++;
}
}
} else {
unknown = true;
}
}
if(ungraded > 0){
return 'ungraded';
} else if(unknown) {
return 'unknown';
} else if(graded){
return 'graded';
} else if(allgraded){
return 'allgraded';
} else {
return 'unsubmitted';
}
},
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";
}
},
},
template: `
{{ value.course.context.path.join(" / ") }}
{{ value.course.numenrolled }} {{ text.students_from_plan_enrolled }}
1 {{ text.student_from_plan_enrolled }}
`,
});
//Select activities to use in grade overview
Vue.component('r-item-teacher-gradepicker', {
props: {
value : {
type: Object, // Item
default: function(){ return {};},
},
useRequiredGrades: {
type: Boolean,
default(){ return null;}
}
},
data() {
return {
text: strings.teachercourse,
};
},
computed: {
wwwroot() {
return Config.wwwroot;
}
},
methods: {
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].catch(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].catch(notification.exception);
},
},
template: `
{{ value.course.context.path.join(" / ")}} / {{value.course.displayname}}
-
{{text.grade_include}}{{text.grade_require}}
-
{{g.name}}
g.name = fd.get('name')"
v-if="g.cmid > 0"
:cmid="g.cmid"
:coursectxid="value.course.ctxid"
genericonly>
`,
});
//Selected activities dispaly
Vue.component('r-item-teachergrades',{
props: {
value : {
type: Object,
default: function(){ return {};},
},
useRequiredGrades: {
type: Boolean,
default: false,
},
},
data() {
return {
text: strings.teachercourse,
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.completed_pass || grade.grading.completed || grade.grading.completed_fail) {
if(Number(grade.grading.completed) + Number(grade.grading.completed_pass)
+ Number(grade.grading.completed_fail)
== Number(grade.grading.students)){
return 'allgraded';
}
else {
return 'graded';
}
}
else {
return 'unsubmitted';
}
} else {
return 'unknown';
}
},
},
template: `
g.name = fd.get('name')"
v-if="g.cmid > 0"
:cmid="g.cmid"
:coursectxid="value.ctxid"
genericonly>
|
|
|
`,
});
// Core completion version of student course info
Vue.component('r-item-teachercompletion',{
props: {
value : {
type: Object,
default: function(){ return {};},
},
guestmode: {
type: Boolean,
default: false,
},
course: {
type: Object,
default: function(){ return {};},
},
},
data() {
return {
text: strings.completion,
};
},
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: {
completionreport(){
return `${Config.wwwroot}/report/completion/index.php?course=${this.course.id}`;
},
wwwroot() {
return Config.wwwroot;
}
},
methods: {
hasCompletions() {
if(this.value.conditions) {
for(const cgroup of this.value.conditions){
if(cgroup.items && cgroup.items.length > 0){
return true;
}
}
}
return false;
},
},
template: `
{{ text.aggregation_overall_one }}{{ text.aggregation_overall_all}}{{ text.aggregation_overall_any }} |
{{text.completion_not_configured}}!
{{text.configure_completion}}
|
{{ text.aggregation_all}}{{ text.aggregation_any}}{{ text.aggregation_one }}
{{ cgroup.title.toLowerCase() }}:
|
|
|
{{ text.view_completion_report}}
|
`,
});
//TAG: Teacher Course competency
Vue.component('r-item-teacher-course-competency',{
props: {
value : {
type: Object,
default: function(){ return {};},
},
guestmode: {
type: Boolean,
default: false,
},
item: {
type: Object,
default: function(){ return { id: null};},
}
},
data() {
return {
text: strings.competency,
};
},
created(){
},
computed: {
hasCompletions() {
if(this.value.conditions) {
for(const cgroup of this.value.conditions){
if(cgroup.items && cgroup.items.length > 0){
return true;
}
}
}
return false;
},
wwwroot() {
return Config.wwwroot;
}
},
methods: {
completion_icon(competency) {
if (competency.proficient && competency.courseproficient) {
return "check-circle";
} else if (competency.proficient) {
return "check";
} else if (competency.proficient === false) {
return "times-circle";
} else {
return "circle-o";
}
},
completion_tag(competency){
if (competency.proficient && competency.courseproficient) {
return "completed";
} else if (competency.proficient) {
return "completed";
} else if (competency.proficient === false) {
return "failed";
} else if (competency.progress) {
return "progress";
} else {
return "incomplete";
}
},
pathtags(competency) {
const path = competency.path;
let s = "";
for (const ix in path) {
const p = path[ix];
if ( ix > 0) {
s += " / ";
}
let url;
if (p.type =='competency') {
url = Config.wwwroot+`/admin/tool/lp/user_competency_in_course.php?courseid=${this.item.course.id}&competencyid=${p.id}`;
} else {
url = this.competencyurl(p);
}
s += `${p.title}`;
}
return s;
},
competencyurl(c) {
return Config.wwwroot+`/admin/tool/lp/user_competency_in_course.php?courseid=${this.item.course.id}&competencyid=${c.id}`;
},
},
template: `
`,
});
Vue.component('r-grading-bar',{
props: {
value : {
type: Object,
default: function(){ return {};},
},
width: {
type: Number,
default: 150,
},
height: {
type: Number,
default: 15,
}
},
data() {
return {
text: strings.grading,
};
},
computed: {
width_unsubmitted() {
return this.width * this.fraction_unsubmitted();
},
width_graded() {
return this.width * this.fraction_graded();
},
width_ungraded() {
return this.width * this.fraction_ungraded();
},
count_unsubmitted(){
return (this.value.students - this.value.graded - this.value.ungraded);
}
},
methods: {
fraction_unsubmitted() {
if(this.value.students > 0){
return 1 - ((this.value.graded + this.value.ungraded) / this.value.students);
} else {
return 1;
}
},
fraction_graded() {
if(this.value.students > 0){
return this.value.graded / this.value.students;
} else {
return 0;
}
},
fraction_ungraded() {
if(this.value.students > 0){
return this.value.ungraded / this.value.students;
} else {
return 0;
}
},
},
template: `
`,
});
Vue.component('r-completion-bar',{
props: {
value : {
type: Object,
default: function(){ return {
students: 0,
completed: 0,
completed_pass: 0,
completed_fail: 0,
ungraded: 0,
};},
},
width: {
type: Number,
default: 150,
},
height: {
type: Number,
default: 15,
}
},
data() {
return {
text: strings.completion,
};
},
computed: {
width_incomplete() {
return this.width * this.fraction_incomplete();
},
width_completed() {
return this.width * this.fraction_completed();
},
width_completed_pass() {
return this.width * this.fraction_completed_pass();
},
width_completed_fail() {
return this.width * this.fraction_completed_fail();
},
width_ungraded() {
return this.width * this.fraction_ungraded();
},
count_incomplete(){
return (this.value.students - this.value.completed - this.value.completed_pass
- this.value.completed_fail - this.value.ungraded);
}
},
methods: {
fraction_incomplete() {
if(this.value.students > 0){
return 1 - (
(this.value.completed + this.value.completed_pass +
this.value.completed_fail + this.value.ungraded) / this.value.students);
} else {
return 1;
}
},
fraction_completed() {
if(this.value.students > 0){
return this.value.completed / this.value.students;
} else {
return 0;
}
},
fraction_completed_pass() {
if(this.value.students > 0){
return this.value.completed_pass / this.value.students;
} else {
return 0;
}
},
fraction_completed_fail() {
if(this.value.students > 0){
return this.value.completed_fail / this.value.students;
} else {
return 0;
}
},
fraction_ungraded() {
if(this.value.students > 0){
return this.value.ungraded / this.value.students;
} else {
return 0;
}
},
},
template: `
`,
});
Vue.component('r-completion-circle',{
props: {
value : {
type: Object,
default: function(){ return {
students: 10,
completed: 2,
completed_pass: 2,
completed_fail: 2,
ungraded: 2,
};},
},
stroke: {
type: Number,
default: 0.2,
},
disabled: {
type: Boolean,
default: false,
},
title: {
type: String,
default: "",
}
},
computed: {
radius() {
return 50 - (50*this.stroke);
},
arcpath_ungraded() {
const begin = 0;
return this.arcpath(begin,this.fraction_ungraded());
},
arcpath_completed() {
const begin = this.fraction_ungraded();
return this.arcpath(begin,this.fraction_completed());
},
arcpath_completed_pass() {
const begin = this.fraction_ungraded()
+ this.fraction_completed();
return this.arcpath(begin,this.fraction_completed_pass());
},
arcpath_completed_fail() {
const begin = this.fraction_ungraded()
+ this.fraction_completed()
+ this.fraction_completed_pass();
return this.arcpath(begin,this.fraction_completed_fail());
},
arcpath_incomplete() {
const begin = this.fraction_ungraded()
+ this.fraction_completed()
+ this.fraction_completed_pass()
+ this.fraction_completed_fail();
return this.arcpath(begin,this.fraction_incomplete());
},
},
methods: {
arcpath(start, end) {
const r = 50 - (50*this.stroke);
const t1 = start * 2*π;
const Δ = end * 2*π;
return svgarcpath([50,50],[r,r],[t1,Δ], 1.5*π);
},
fraction_incomplete() {
if(this.value.students > 0){
return 1 - (
(this.value.completed + this.value.completed_pass +
this.value.completed_fail + this.value.ungraded) / this.value.students);
} else {
return 1;
}
},
fraction_completed() {
if(this.value.students > 0){
return this.value.completed / this.value.students;
} else {
return 0;
}
},
fraction_completed_pass() {
if(this.value.students > 0){
return this.value.completed_pass / this.value.students;
} else {
return 0;
}
},
fraction_completed_fail() {
if(this.value.students > 0){
return this.value.completed_fail / this.value.students;
} else {
return 0;
}
},
fraction_ungraded() {
if(this.value.students > 0){
return this.value.ungraded / this.value.students;
} else {
return 0;
}
},
},
template: `
`,
});
Vue.component('r-item-junction',{
props: {
value : {
type: Object,
default: function(){ return {};},
},
guestmode: {
type: Boolean,
default: false,
},
teachermode: {
type: Boolean,
default: false,
}
},
data() {
return {
};
},
computed: {
completion(){
if(this.value.completion){
return this.value.completion;
} else {
return "incomplete";
}
}
},
methods: {
},
template: `
`,
});
Vue.component('r-item-finish',{
props: {
value : {
type: Object,
default: function(){ return {};},
},
guestmode: {
type: Boolean,
default: false,
},
teachermode: {
type: Boolean,
default: false,
}
},
data() {
return {
};
},
computed: {
completion(){
if(this.value.completion){
return this.value.completion;
} else {
return "incomplete";
}
}
},
methods: {
},
template: `
`,
});
Vue.component('r-item-start',{
props: {
value : {
type: Object,
default: function(){ return {};},
},
guestmode: {
type: Boolean,
default: false,
},
teachermode: {
type: Boolean,
default: false,
}
},
data() {
return {
};
},
computed: {
completion(){
if(this.value.completion){
return this.value.completion;
} else {
return "incomplete";
}
}
},
created(){
},
methods: {
},
template: `
`,
});
Vue.component('r-item-badge',{
props: {
value : {
type: Object,
default: function(){ return {};},
},
guestmode: {
type: Boolean,
default: false,
},
teachermode: {
type: Boolean,
default: false,
}
},
data() {
return {
text: strings.badge,
};
},
computed: {
completion() {
return this.value.badge.issued?"completed":"incomplete";
},
issued_icon(){
switch(this.value.badge.issued){
default: // "nogrades":
return "circle-o";
case true:
return "check";
}
},
issuestats(){
// so the r-completion-bar can be used to show issuing stats
return {
students: (this.value.badge.studentcount)?this.value.badge.studentcount:0,
completed: (this.value.badge.issuedcount)?this.value.badge.issuedcount:0,
completed_pass: 0,
completed_fail: 0,
ungraded: 0,
};
},
arcpath_issued(){
if(this.value.badge.studentcount){
const fraction = this.value.badge.issuedcount/this.value.badge.studentcount;
return this.arcpath(0,fraction);
} 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*π-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: `
`,
});
Vue.component('r-item-dummy-course', {
props: {
'value' :{
type: Object,
default: function(){ return null;},
},
},
data() {
return {
text: strings.invalid,
};
},
methods: {
},
template: `
`,
});
Vue.component('r-item-dummy-filter',{
props: {
},
data() {
return {};
},
computed: {
},
created(){
},
methods: {
},
template: `
`,
});
Vue.component('r-item-dummy-badge',{
props: {
},
data() {
return {};
},
computed: {
},
methods: {
},
template: `
`,
});
},
};