/* eslint no-var: "error"*/ /* eslint no-console: "off"*/ /* eslint no-unused-vars: warn */ /* eslint max-len: ["error", { "code": 160 }] */ /* eslint max-depth: ["error", 6] */ /* eslint no-trailing-spaces: warn */ /* eslint-env es6*/ import {SimpleLine} from './simpleline/simpleline'; import {loadStrings} from './util/string-helper'; import {formatDate, formatDatetime, studyplanPageTiming, studyplanTiming} from './util/date-helper'; import {addBrowserButtonEvent} from './util/browserbuttonevents'; 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, objCopy} from './studyplan-processor'; import TSComponents from './treestudyplan-components'; import {premiumenabled} 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 = loadStrings({ 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", 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"); }, verifiedType() { 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; } }, mounted() { this.loadStudyplans(); addBrowserButtonEvent(this.backPressed); }, methods: { backPressed() { debug.log("Back button pressed"); if (this.selectedstudyplan) { debug.log("Closing studyplan"); this.deselectStudyplan(); } }, callArgs(o) { const args = {}; if (typeof o == 'object' && !Array.isArray(o) && o !== null) { objCopy(args, o); } if (this.verifiedType == "invited") { args.invitekey = this.invitekey; } else if (this.verifiedType == "other") { args.userid = this.userid; } return args; }, loadStudyplans() { const self = this; this.loading = true; call([{ methodname: `local_treestudyplan_list_${this.verifiedType}_studyplans`, args: this.callArgs(), }])[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); } } } return; }).catch(notification.exception); }, selectStudyplan(plan) { const self = this; this.loadingstudyplan = true; call([{ methodname: `local_treestudyplan_get_${this.verifiedType}_studyplan`, args: this.callArgs({ studyplanid: plan.id, }), }])[0].then(function(response) { self.selectedstudyplan = processStudyplan(response); self.loadingstudyplan = false; window.location.hash = self.selectedstudyplan.id; return; }).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 formatDate(page.startdate, false) + " - " + formatDate(page.enddate, false); }, columns(page) { return 1 + (page.periods * 2); }, columnsStylerule(page) { // Uses css variables, so width for slots and filters can be configured in css let s = "grid-template-columns: var(--studyplan-filter-width)"; // Ese css variable here for (let i = 0; i < page.periods; i++) { s += " var(--studyplan-course-width) var(--studyplan-filter-width)"; } return s + ";"; }, countLineLayers(line, page) { let maxLayer = -1; for (let i = 0; i <= page.periods; i++) { // Determine the amount of used layers in a studyline slot for (const ix in line.slots[i].courses) { const item = line.slots[i].courses[ix]; if (item.layer > 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(/* Params: newTabIndex, prevTabIndex */) { ItemEventBus.$emit('redrawLines', null); scrollCurrentIntoView(this.selectedpage.id); }, }, mounted() { 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() { 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, canUnenrol: 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; }, enrollableSelf() { return [ENROLLABLE_SELF, ENROLLABLE_SELF_ROLE].includes(this.value.enrol.enrollable); }, enrollableRole() { return [ENROLLABLE_ROLE, ENROLLABLE_SELF_ROLE].includes(this.value.enrol.enrollable); }, enrolled() { return this.value.enrol.enrolled ? true : false; }, canEnrol() { return this.value.enrol.can_enrol ? true : false; }, enrolQuestion() { return this.text.enrol_question.replace('{$a}', this.value.name); }, enrolledIn() { return this.text.enrolled_in.replace('{$a}', this.value.name); }, by() { return this.text.byname.replace('{$a}', ''); }, enrolldate() { return formatDatetime(this.value.enrol.enrolled_time); }, sortedStudents() { 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, formatDatetime, 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) => { // Function 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; } }, enrolSelf() { const self = this; call([{ methodname: 'local_treestudyplan_line_enrol_self', args: { id: self.value.id, }, }])[0].then(function(response) { self.$emit('enrolupdate', response); return; }).catch(notification.exception); }, enrolStudent(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; return; }).catch(notification.exception); } return; }).catch(notification.exception); }, unenrolStudent(student) { const self = this; const user = student.user; let question = self.text.unenrol_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; return; }).catch(notification.exception); } return; }).catch(notification.exception); }, loadStudents() { const self = this; self.students = null; self.canUnenrol = false; call([{ methodname: 'local_treestudyplan_list_line_enrolled_students', args: { id: self.value.id, }, }])[0].then(function(response) { self.students = response.userinfo; self.canUnenrol = response.can_unenrol; return; }).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() { 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); }, updated() { if (!this.dummy) { this.redrawLines(); } }, template: `
`, }); Vue.component('r-item-invalid', { props: { 'value': { type: Object, default() { 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 formatDate(this.value.course.startdate); }, enddate() { if (this.value.course.enddate > 0) { return formatDate(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; } }, methods: { completionIcon(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"; } }, circleIcon(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() { 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: { completionIcon(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() { return {}; }, }, guestmode: { type: Boolean, 'default': false, }, course: { type: Object, default() { return {}; }, }, }, data() { return { text: strings.completion, }; }, computed: { }, methods: { completionIcon(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"; } }, completionTag(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() { return {}; }, }, guestmode: { type: Boolean, 'default': false, }, item: { type: Object, default() { return {id: null}; }, } }, data() { return { text: strings.competency, }; }, 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: { completionIcon(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"; } }, completionTag(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: { courseGradingNeeded() { return this.courseGradingState(); }, courseGradingIcon() { return this.determineGradingIcon(this.courseGradingState()); }, filteredGrades() { 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; }, progressCircle() { const status = { students: 0, completed: 0, completedPass: 0, completedFail: 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.completedPass += itm.progress.completed_pass; status.completedFail += 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.completedPass += g.grading.completed_pass; status.completedFail += g.grading.completed_fail; status.ungraded += g.grading.ungraded; } } } return status; }, startdate() { return formatDate(this.value.course.startdate); }, enddate() { if (this.value.course.enddate > 0) { return formatDate(this.value.course.enddate); } else { return this.text.noenddate; } }, wwwroot() { return Config.wwwroot; } }, methods: { courseGradingState() { let ungraded = 0; let unknown = 0; let graded = 0; let allgraded = 0; const grades = this.filteredGrades; 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'; } }, determineGradingIcon(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() { return {}; }, }, useRequiredGrades: { type: Boolean, default() { return null; } } }, data() { return { text: strings.teachercourse, }; }, computed: { startdate() { return formatDate(this.value.course.startdate); }, enddate() { if (this.value.course.enddate > 0) { return formatDate(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() { 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; }, filteredGrades() { return this.value.grades.filter(g => g.selected); }, }, methods: { determineGradingIcon(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"; } }, gradingIcon(grade) { return this.determineGradingIcon(this.isGradingNeeded(grade)); }, isGradingNeeded(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() { return {}; }, }, guestmode: { type: Boolean, 'default': false, }, course: { type: Object, default() { return {}; }, }, }, data() { return { text: strings.completion, }; }, 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() { return {}; }, }, guestmode: { type: Boolean, 'default': false, }, item: { type: Object, default() { return {id: null}; }, } }, data() { return { text: strings.competency, }; }, 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: { completionIcon(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"; } }, completionTag(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() { return {}; }, }, width: { type: Number, 'default': 150, }, height: { type: Number, 'default': 15, } }, data() { return { text: strings.grading, }; }, computed: { widthUnsubmitted() { return this.width * this.fractionUnsubmitted(); }, widthGraded() { return this.width * this.fractionGraded(); }, widthUngraded() { return this.width * this.fractionUngraded(); }, countUnsubmitted() { return (this.value.students - this.value.graded - this.value.ungraded); } }, methods: { fractionUnsubmitted() { if (this.value.students > 0) { return 1 - ((this.value.graded + this.value.ungraded) / this.value.students); } else { return 1; } }, fractionGraded() { if (this.value.students > 0) { return this.value.graded / this.value.students; } else { return 0; } }, fractionUngraded() { 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() { 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: { widthIncomplete() { return this.width * this.fractionIncomplete(); }, widthCompleted() { return this.width * this.fractionCompleted(); }, widthCompletedPass() { return this.width * this.fractionCompletedPass(); }, widthCompletedFail() { return this.width * this.fractionCompletedFail(); }, widthUngraded() { return this.width * this.fractionUngraded(); }, countIncomplete() { return (this.value.students - this.value.completed - this.value.completed_pass - this.value.completed_fail - this.value.ungraded); } }, methods: { fractionIncomplete() { 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; } }, fractionCompleted() { if (this.value.students > 0) { return this.value.completed / this.value.students; } else { return 0; } }, fractionCompletedPass() { if (this.value.students > 0) { return this.value.completed_pass / this.value.students; } else { return 0; } }, fractionCompletedFail() { if (this.value.students > 0) { return this.value.completed_fail / this.value.students; } else { return 0; } }, fractionUngraded() { 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() { return { students: 10, completed: 2, completedPass: 2, completedFail: 2, ungraded: 2, }; }, }, stroke: { type: Number, 'default': 0.2, }, disabled: { type: Boolean, 'default': false, }, title: { type: String, 'default': "", } }, computed: { completedPass() { if (this.value.completed_pass) { return this.value.completed_pass; } else { return this.value.completedPass; } }, completedFail() { if (this.value.completed_fail) { return this.value.completed_fail; } else { return this.value.completedFail; } }, radius() { return 50 - (50 * this.stroke); }, arcpathUngraded() { const begin = 0; return this.arcpath(begin, this.fractionUngraded()); }, arcpathCompleted() { const begin = this.fractionUngraded(); return this.arcpath(begin, this.fractionCompleted()); }, arcpathCompletedPass() { const begin = this.fractionUngraded() + this.fractionCompleted(); return this.arcpath(begin, this.fractionCompletedPass()); }, arcpathCompletedFail() { const begin = this.fractionUngraded() + this.fractionCompleted() + this.fractionCompletedPass(); return this.arcpath(begin, this.fractionCompletedFail()); }, arcpathIncomplete() { const begin = this.fractionUngraded() + this.fractionCompleted() + this.fractionCompletedPass() + this.fractionCompletedFail(); return this.arcpath(begin, this.fractionIncomplete()); }, }, 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 * π); }, fractionIncomplete() { if (this.value.students > 0) { return 1 - ( (this.value.completed + this.completedPass + this.completedFail + this.value.ungraded) / this.value.students); } else { return 1; } }, fractionCompleted() { if (this.value.students > 0) { return this.value.completed / this.value.students; } else { return 0; } }, fractionCompletedPass() { if (this.value.students > 0) { return this.completedPass / this.value.students; } else { return 0; } }, fractionCompletedFail() { if (this.value.students > 0) { return this.completedFail / this.value.students; } else { return 0; } }, fractionUngraded() { 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() { 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() { 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() { 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-badge', { props: { value: { type: Object, default() { 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"; }, issuedIcon() { 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, }; }, arcpathIssued() { if (this.value.badge.studentcount) { const fraction = this.value.badge.issuedcount / this.value.badge.studentcount; return this.arcpath(0, fraction); } else { return ""; // No path } }, arcpathProgress() { 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; } }, completionIconRq(complete) { if (complete) { return "check-square-o"; } else { return "square-o"; } }, completionIcon(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() { return null; }, }, }, data() { return { text: strings.invalid, }; }, methods: { }, template: `
`, }); Vue.component('r-item-dummy-filter', { props: { }, data() { return {}; }, computed: { }, methods: { }, template: `
`, }); Vue.component('r-item-dummy-badge', { props: { }, data() { return {}; }, computed: { }, methods: { }, template: `
`, }); }, };