/*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", noenddate: "noenddate", }, 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", noenddate: "noenddate", }, 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: `
{{title}}
`, }); 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] && !plan.suspended){ self.selectStudyplan(plan); return; } } } } // If there is but a single studyplan, select it anyway, even if it is not current... if (this.studyplancount == 1) { if (self.studyplans.present.length > 0) { // Directly show the current study plan if it's the only current one const plan = self.studyplans.present[0]; if (!plan.suspended) { self.selectStudyplan(plan); } } else if(self.studyplans.future.lengh > 0) { const plan = self.studyplans.future[0]; if (!plan.suspended) { self.selectStudyplan(plan); } } else { const plan = self.studyplans.past[0]; if (!plan.suspended) { self.selectStudyplan(plan); } } } }).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: `
`, }); 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){ 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: `
{{ 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: `
{{ value.shortname }}
`, }); 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 > 0){ 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: `
{{ value.course.displayname }}
`, }); //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: `
{{g.grade}} {{ text["view_feedback"]}}
`, }); // 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}}!
`, }); //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: `
{{text.competencies_not_configured}}!
{{text.configure_competencies}}
`, }); //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 > 0){ 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: `
`, }); //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: { startdate(){ return format_date(this.value.course.startdate); }, enddate(){ if(this.value.course.enddate > 0){ return format_date(this.value.course.enddate); } else { return this.text.noenddate; } }, 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: ` `, }); //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: `
`, }); // 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.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: `
{{text.competencies_not_configured}}!
{{text.configure_competencies}}
`, }); 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: ` {{title}} `, }); 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: `
{{value.badge.name}}

{{value.badge.description}}

  • {{text.dateissued}}: {{ value.badge.dateissued }}
  • {{text.dateexpired}}: {{ value.badge.dateexpired }}
  • {{text.share_badge}}
{{text.badgedisabled}}

{{ text.badgeinfo }}

{{text.badgeissuedstats}}:

`, }); 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: `
`, }); }, };