/*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,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'; // 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"); let lastCaller = null; /** * Scroll current period into view * @param {*} handle A key to pass so subsequent calls with the same key won't trigger (always triggers when null or undefined) */ function scrollCurrentIntoView(handle){ const elScrollContainer = document.querySelector(".r-studyplan-scrollable"); const elCurrentHeader = elScrollContainer.querySelector(".s-studyline-header-period.current"); if(elCurrentHeader && ((!handle) || (handle != lastCaller))){ lastCaller = handle; elCurrentHeader.scrollIntoView({ behavior: "smooth", block: "start", inline: "center", }); } } let strings = load_strings({ report: { loading: "loadinghelp@core", studyplan_past: "studyplan_past", studyplan_present: "studyplan_present", studyplan_future: "studyplan_future", back: "back", }, invalid: { error: 'error', }, grading: { ungraded: "ungraded", graded: "graded", allgraded: "allgraded", unsubmitted: "unsubmitted", nogrades: "nogrades", unknown: "unknown", }, completion: { completed: "completion_completed", incomplete: "completion_incomplete", completed_pass: "completion_passed", completed_fail: "completion_failed", ungraded: "ungraded", aggregation_all: "aggregation_all", aggregation_any: "aggregation_any", aggregation_one: "aggregation_one", aggregation_overall_all: "aggregation_overall_all", aggregation_overall_any: "aggregation_overall_any", aggregation_overall_one: "aggregation_overall_one", completion_not_configured: "completion_not_configured", configure_completion: "configure_completion", view_completion_report: "view_completion_report", completion_incomplete: "completion_incomplete", completion_failed: "completion_failed", completion_pending: "completion_pending", completion_progress: "completion_progress", completion_completed: "completion_completed", completion_good: "completion_good", completion_excellent: "completion_excellent", view_feedback: "view_feedback", coursetiming_past: "coursetiming_past", coursetiming_present: "coursetiming_present", coursetiming_future: "coursetiming_future", required_goal: "required_goal", student_not_tracked: "student_not_tracked", completion_not_enabled: "completion_not_enabled", }, badge: { share_badge: "share_badge", dateissued: "dateissued", dateexpire: "dateexpire", badgeinfo: "badgeinfo", badgeissuedstats: "badgeissuedstats", completion_incomplete: "completion_incomplete_badge", completion_completed: "completion_completed_badge", completioninfo: "completioninfo", badgedisabled: "badgedisabled" }, course: { completion_incomplete: "completion_incomplete", completion_failed: "completion_failed", completion_pending: "completion_pending", completion_progress: "completion_progress", completion_completed: "completion_completed", completion_good: "completion_good", completion_excellent: "completion_excellent", view_feedback: "view_feedback", coursetiming_past: "coursetiming_past", coursetiming_present: "coursetiming_present", coursetiming_future: "coursetiming_future", required_goal: "required_goal", student_not_tracked: "student_not_tracked", not_enrolled: "not_enrolled", }, teachercourse: { select_conditions: "select_conditions", select_grades: "select_grades", coursetiming_past: "coursetiming_past", coursetiming_present: "coursetiming_present", coursetiming_future: "coursetiming_future", grade_include: "grade_include", grade_require: "grade_require", required_goal: "required_goal", student_from_plan_enrolled: "student_from_plan_enrolled", students_from_plan_enrolled: "students_from_plan_enrolled", }, competency: { competency_not_configured: "competency_not_configured", configure_competency: "configure_competency", when: "when", required: "required", points: "points@core_grades", heading: "competency_heading", details: "competency_details", results: "results", unrated: "unrated", progress: "completion_progress", view_feedback: "view_feedback", }, pageinfo: { edit: 'period_edit', fullname: 'studyplan_name', shortname: 'studyplan_shortname', startdate: 'studyplan_startdate', enddate: 'studyplan_enddate', description: 'studyplan_description', duration: 'studyplan_duration', details: 'studyplan_details', overview: 'overviewreport:all', oveviewperiod: 'overviewreport:period' } }); /************************************ * * * 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; if (self.studyplans.present.length == 1) { // Directly show the current study plan if it's the only one self.selectStudyplan(self.studyplans.present[0]); } else { // If there is but a single studyplan, select it anyway, even if it is not current... if (this.studyplancount == 1) { if(self.studyplans.future.lengh > 0) { self.selectStudyplan(self.studyplans.future[0]); } else { self.selectStudyplan(self.studyplans.past[0]); } } } }).catch(notification.exception); }, selectStudyplan(plan) { const self = this; this.loadingstudyplan = true; call([{ methodname: `local_treestudyplan_get_${this.verified_type}_studyplan`, args: this.call_args({ studyplanid: plan.id, }), }])[0].then(function(response){ self.selectedstudyplan = ProcessStudyplan(response); self.loadingstudyplan = false; }).catch(notification.exception); }, deselectStudyplan() { this.selectedstudyplan = null; this.loadStudyplans(); // Reload the list of studyplans. } }, template: `
`, }); Vue.component('r-studyplan', { props: { value: { type: Object, }, guestmode: { type: Boolean, default() { return false;}, }, teachermode: { type: Boolean, default() { return false;}, }, }, data() { return { selectedpageindex: -1, text: strings.pageinfo, }; }, computed: { selectedpage() { return this.value.pages[this.selectedpageindex]; }, startpageindex() { let startpageindex = 0; let firststart = null; for(const ix in this.value.pages) { const page = this.value.pages[ix]; if(studyplanPageTiming(page) == "present") { const s = new Date(page.startdate); if( (!firststart) || firststart > s) { startpageindex = ix; firststart = s; } } } return startpageindex; }, wwwroot() { return Config.wwwroot; }, }, methods: { pageduration(page){ return format_date(page.startdate,false) + " - " + format_date(page.enddate,false); }, columns(page) { return 1+ (page.periods * 2); }, columns_stylerule(page) { // Uses css variables, so width for slots and filters can be configured in css let s = "grid-template-columns: var(--studyplan-filter-width)"; // use css variable here for(let i=0; i maxLayer){ maxLayer = item.layer; } } for(const ix in line.slots[i].filters){ const item = line.slots[i].filters[ix]; if(item.layer > maxLayer){ maxLayer = item.layer; } } } return (maxLayer >= 0)?(maxLayer+1):1; }, showslot(page,line,index, layeridx, type){ // check if the slot should be hidden because a previous slot has an item with a span // so big that it hides this slot const forGradable = (type == 'gradable')?true:false; const periods = page.periods; let show = true; for(let i = 0; i < periods; i++){ if(line.slots[index-i] && line.slots[index-i].courses){ const list = line.slots[index-i].courses; for(const ix in list){ // Really wish that 'for of' would work with the minifier moodle uses const item = list[ix]; if(item.layer == layeridx){ if(forGradable){ if(i > 0 && (item.span - i) > 0){ show = false; } } else { if((item.span - i) > 1){ show = false; } } } } } } return show; }, selectedpageChanged(newTabIndex,prevTabIndex) { ItemEventBus.$emit('redrawLines', null); scrollCurrentIntoView(this.selectedpage.id); }, }, mounted() { // scrollCurrentIntoView(this.selectedpage.id); this.$root.$emit('redrawLines'); }, updated() { scrollCurrentIntoView(this.selectedpage.id); ItemEventBus.$emit('lineHeightChange', null); this.$root.$emit('redrawLines'); ItemEventBus.$emit('redrawLines'); }, template: `
{{ 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 {};}, }, 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; }, 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(); // 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, }, }); } }, redrawLines(){ // Clean all old lines for(let ix in this.lines){ let lineinfo = this.lines[ix]; if(lineinfo && lineinfo.line){ lineinfo.line.remove(); lineinfo.line = undefined; } } // Create new lines for(let i in this.value.connections.out){ let conn = this.value.connections.out[i]; this.redrawLine(conn); } }, onWindowResize(){ this.redrawLines(); }, onRedrawLines(){ this.redrawLines(); }, removeLine(conn){ if(this.lines[conn.to_id]){ this.lines[conn.to_id].remove(); delete this.lines[conn.to_id]; } }, }, computed: { hasConnectionsOut() { return !(["finish",].includes(this.value.type)); }, hasConnectionsIn() { return !(["start",].includes(this.value.type)); }, hasContext() { return ['start','junction','finish'].includes(this.value.type); } }, created(){ ItemEventBus.$on('redrawLines', this.onRedrawLines); }, mounted(){ // Initialize connection lines when mounting this.redrawLines(); setTimeout(()=>{ this.redrawLines(); },50); // Add resize event listener window.addEventListener('resize',this.onWindowResize); }, beforeDestroy(){ for(let i in this.value.connections.out){ let conn = this.value.connections.out[i]; this.removeLine(conn); } // Remove resize event listener window.removeEventListener('resize',this.onWindowResize); ItemEventBus.$off('redrawLines', this.onRedrawLines); }, beforeUpdate(){ }, updated(){ if(!this.dummy) { this.redrawLines(); } }, template: `
`, }); Vue.component('r-item-invalid', { props: { 'value' :{ type: Object, default: function(){ return null;}, }, }, data() { return { text: strings.invalid, }; }, methods: { }, template: `
{{ text.error }}
`, }); //TAG: Item Course Vue.component('r-item-course', { props: { value :{ type: Object, default(){ return null;}, }, guestmode: { type: Boolean, default(){ return false;} }, teachermode: { type: Boolean, default(){ return false;} }, plan: { type: Object, default(){ return null;} } }, data() { return { text: strings.course, }; }, computed: { startdate(){ return format_date(this.value.course.startdate); }, enddate(){ if(this.value.course.enddate){ return format_date(this.value.course.enddate); } else { return this.text.noenddate; } }, courseprogress() { if (!this.value.course.enrolled) { return 0; } else if(this.value.course.completion) { return (this.value.course.completion.progress / this.value.course.completion.count); } else if(this.value.course.competency) { return (this.value.course.competency.progress / this.value.course.competency.count); } else if(this.value.course.grades) { return (this.gradeprogress(this.value.course.grades) / this.value.course.grades.length); } else { return 0; } }, hasprogressinfo() { if (!this.value.course.enrolled) { return false; } else { return (this.value.course.completion || this.value.course.competency || this.value.course.grades); } }, wwwroot() { return Config.wwwroot; } }, created(){ }, methods: { completion_icon(completion) { switch(completion){ default: // case "incomplete" return "circle-o"; case "pending": return "question-circle"; case "failed": return "times-circle"; case "progress": return "exclamation-circle"; case "completed": return "check-circle"; case "good": return "check-circle"; case "excellent": return "check-circle"; } }, circle_icon(completion) { switch(completion){ default: // case "incomplete" return null; case "failed": return "times"; case "progress": return ""; case "completed": return "check"; case "good": return "check"; case "excellent": return "check"; } }, gradeprogress(grades) { let progress = 0; for (const ix in grades) { const g = grades[ix]; if (["completed","excellent","good"].includes(g.completion)) { progress++; } } return progress; }, }, template: `
{{ 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){ 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: { 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: `