/*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} 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 TSComponents from './treestudyplan-components'; // 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; export default { install(Vue/*,options*/){ Vue.use(TSComponents); let debug = new Debugger("treestudyplan-viewer"); debug.warn(Config); 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({ 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", }, course: { completion_incomplete: "completion_incomplete", completion_failed: "completion_failed", completion_pending: "completion_pending", completion_progress: "completion_progress", completion_completed: "completion_completed", completion_good: "completion_good", completion_excellent: "completion_excellent", view_feedback: "view_feedback", coursetiming_past: "coursetiming_past", coursetiming_present: "coursetiming_present", coursetiming_future: "coursetiming_future", required_goal: "required_goal", student_not_tracked: "student_not_tracked", not_enrolled: "not_enrolled", }, teachercourse: { select_conditions: "select_conditions", select_grades: "select_grades", coursetiming_past: "coursetiming_past", coursetiming_present: "coursetiming_present", coursetiming_future: "coursetiming_future", grade_include: "grade_include", grade_require: "grade_require", required_goal: "required_goal", student_from_plan_enrolled: "student_from_plan_enrolled", students_from_plan_enrolled: "students_from_plan_enrolled", } }); /************************************ * * * 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(); 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: { value: { type: Array, }, guestmode: { type: Boolean, default: false, }, teachermode: { type: Boolean, default: false, } }, data() { return { selectedstudyplan: null, }; }, computed: { displayedstudyplan(){ if(this.selectedstudyplan){ return this.selectedstudyplan; } else if(this.value && this.value.length > 0){ return this.value[0]; } else { return null; } } }, methods: { selectstudyplan(plan) { this.selectedstudyplan = plan; }, }, template: `
{{studyplan.name}}
`, }); Vue.component('r-studyplan', { props: { value: { type: Object, }, guestmode: { type: Boolean, default: false, }, teachermode: { type: Boolean, default: false, } }, data() { return { }; }, computed: { columns() { return 1+ (this.page.periods * 2); }, columns_stylerule() { // 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(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 = this.page.periods; let show = true; for(let i = 0; i < periods; i++){ if(line.slots[index-i] && line.slots[index-i].courses){ const list = line.slots[index-i].courses; for(const ix in list){ // Really wish that 'for of' would work with the minifier moodle uses const item = list[ix]; if(item.layer == layeridx){ if(forGradable){ if(i > 0 && (item.span - i) > 0){ show = false; } } else { if((item.span - i) > 1){ show = false; } } } } } } return show; } }, mounted() { scrollCurrentIntoView(this.value.id); }, updated() { scrollCurrentIntoView(this.value.id); ItemEventBus.$emit('lineHeightChange', null); }, template: `
`, }); /* * R-STUDYLINE-HEADER */ Vue.component('r-studyline-heading', { props: { value : { type: Object, // Studyline default: function(){ return {};}, }, layers: { type: Number, default: 1, }, }, data() { return { layerHeights: {} }; }, 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: { }, methods: { 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; } } }, 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; }, current(){ if( this.period && this.period.startdate && this.period.enddate){ const now = new Date(); const pstart = new Date(this.period.startdate); const pend = new Date(this.period.enddate); return (now >= pstart && now < pend); } else { return false; } }, spanCss(){ if(this.item && this.item.span > 1){ const span = (2 * this.item.span) - 1; return `width: 100%; grid-column: span ${span};`; } else { return ""; } } }, 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, } }, data() { return { lines: [], }; }, methods: { lineColor(){ if(this.teachermode){ return "var(--gray)"; } 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(); // prepare lineinfo link or delete old line let lineinfo = this.lines[conn.to_id]; if(lineinfo){ if(lineinfo.line){ lineinfo.line.remove(); lineinfo.line = undefined; } } else { lineinfo = {}; this.lines[conn.to_id] = lineinfo; } // draw new line... let start = document.getElementById('studyitem-'+conn.from_id); let end= document.getElementById('studyitem-'+conn.to_id); if(start !== null && end !== null && isVisible(start) && isVisible(end)){ lineinfo.line = new SimpleLine(start,end,{ color: lineColor, gravity: { start: LINE_GRAVITY, end: LINE_GRAVITY, }, }); } }, 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(); } }, 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(){ }, 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]; let lineinfo = this.lines[conn.to_id]; if(lineinfo){ if(lineinfo.line){ if(lineinfo.lineElm ){ lineinfo.lineElm.parentNode.removeChild(lineinfo.lineElm); lineinfo.lineElm = undefined; } else { lineinfo.line.remove(); } lineinfo.line = undefined; } } } // Remove resize event listener window.removeEventListener('resize',this.onWindowResize); }, beforeUpdate(){ }, updated(){ if(!this.dummy) { this.redrawLines(); } }, template: `
`, }); Vue.component('r-item-invalid', { props: { 'value' :{ type: Object, default: function(){ return null;}, }, }, data() { return { text: strings.invalid, }; }, methods: { }, template: `
{{ text.error }}
`, }); //TAG: Item Course Vue.component('r-item-course', { props: { value :{ type: Object, default(){ return null;}, }, guestmode: { type: Boolean, default(){ return false;} }, teachermode: { type: Boolean, default(){ return false;} }, plan: { type: Object, default(){ return null;} } }, data() { return { text: strings.course, }; }, computed: { startdate(){ return format_date(this.value.course.startdate); }, enddate(){ if(this.value.course.enddate){ return format_date(this.value.course.enddate); } else { return this.text.noenddate; } } }, 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 "exclamation"; case "completed": return "check"; case "good": return "check"; case "excellent": return "check"; } }, }, template: `
{{ value.course.displayname }}
`, }); //TAG: 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"]}}
`, }); //TAG: 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: 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() { //INFO: 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.grades){ for( const g of this.value.course.grades){ if(g.grading){ status.students += g.grading.students; status.completed += g.grading.completed; status.completed_pass += g.grading.completed_pass; status.completed_fail += g.grading.completed_fail; status.ungraded += g.grading.ungraded; } } } return status; }, startdate(){ return format_date(this.value.course.startdate); }, enddate(){ if(this.value.course.enddate){ return format_date(this.value.course.enddate); } else { return this.text.noenddate; } } }, 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: `
`, }); //TAG: 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: { }, 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].fail(notification.exception); }, requiredChanged(newValue,g){ call([{ methodname: 'local_treestudyplan_include_grade', args: { 'grade_id': g.id, 'item_id': this.value.id, 'include': g.selected, 'required': newValue, } }])[0].fail(notification.exception); }, }, template: ` `, }); //TAG: 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: `
`, }); //TAG: 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}`; } }, 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}}
`, }); 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.badgeinfo }}

{{text.badgeissuedstats}}:

`, }); }, };