diff --git a/amd/src/string-helper.js b/amd/src/string-helper.js index 6d4f38a..c2b57f7 100644 --- a/amd/src/string-helper.js +++ b/amd/src/string-helper.js @@ -72,4 +72,48 @@ export function format_date(d,short){ return d.toLocaleDateString(document.documentElement.lang,{ year: 'numeric', month: monthformat, day: 'numeric' }); -} \ No newline at end of file +} + +/** + * Provides standardized information about the period between two dates + * As + * @param {Date|string} first First day of the span + * @param {Date|string} last Last day of the span + * @returns {Object} Object containing formatted start and end dates and span information + */ +export function datespaninfo(first,last){ + if(!(first instanceof Date)){ first = new Date(first);} + if(!(last instanceof Date)){ last = new Date(last);} + + // Make sure the end date is at the very end of the day and the start date at the very beginning + first.setHours(0); + first.setMinutes(0); + first.setSeconds(0); + first.setMilliseconds(0); + last.setHours(23); + last.setMinutes(59); + last.setSeconds(59); + last.setMilliseconds(999); + + const dayspan = Math.round(((last - first)+1)/(24*60*60*1000)); // Add one millisecond to offset the 999 ms + const years = Math.floor(dayspan/365); // Yes, we ignore leap years/leap days + const ydaysleft = dayspan % 365; + + const weeks = Math.floor(ydaysleft/7); + const wdaysleft = ydaysleft % 7; + + return { + first: first, + last: last, + totaldays: dayspan, + years: years, + weeks: weeks, + days: wdaysleft, + formatted: { + first: format_date(first), + last: format_date(last), + } + }; + +} + diff --git a/amd/src/studyplan-editor-components.js b/amd/src/studyplan-editor-components.js index 403e0b4..22a7ee0 100644 --- a/amd/src/studyplan-editor-components.js +++ b/amd/src/studyplan-editor-components.js @@ -10,7 +10,7 @@ import {SimpleLine} from "./simpleline"; import {call} from 'core/ajax'; import notification from 'core/notification'; import {get_strings} from 'core/str'; -import {load_stringkeys, load_strings, format_date} from './string-helper'; +import {load_stringkeys, load_strings, format_date, datespaninfo} from './string-helper'; import {objCopy,transportItem} from './studyplan-processor'; import Debugger from './debugger'; import {download,upload} from './downloader'; @@ -148,6 +148,26 @@ export default { startdate: 'studyplan_startdate', enddate: 'studyplan_enddate', }, + course_timing: { + title: 'course_timing_title', + desc: 'course_timing_desc', + question: 'course_timing_question', + warning: 'course_timing_warning', + course: 'course$core', + period: 'period', + yes: 'yes$core', + no: 'no$core', + duration: 'duration', + years: 'years$core', + year: 'year$core', + weeks: 'weeks$core', + week: 'week$core', + days: 'days$core', + day: 'day$core', + rememberchoice: 'course_timing_rememberchoice', + hidewarning: 'course_timing_hidewarning,' + + }, studyplan_associate: { associations: 'associations', associated_cohorts: 'associated_cohorts', @@ -1526,6 +1546,7 @@ export default { :line="line" :plan="value" :page="page" + :period="page.perioddesc[index-1]" :layer="layeridx-1" :class="'t-studyline ' + ((lineindex%2==0)?' odd ':' even ') + ((lineindex==0 && layeridx==1)?' first ':' ') @@ -1787,7 +1808,14 @@ export default { type: Object, // Studyplan data default(){ return null;}, }, - + page: { + type: Object, // Studyplan data + default(){ return null;}, + }, + period: { + type: Object, // Studyplan data + default(){ return null;}, + }, }, mounted() { const self=this; @@ -1807,6 +1835,9 @@ export default { } }, computed: { + slotkey(){ + return `${this.type}'-'${this.line.id}-${this.slotindex}-${this.layer}`; + }, item(){ for(const ix in this.value){ const itm = this.value[ix]; @@ -1832,10 +1863,18 @@ export default { }, data() { return { + text: strings.course_timing, resizeListener: null, hover: { component:null, type: null, + }, + datechanger: { + coursespan: null, + periodspan: null, + default: false, + defaultchoice: false, + hidewarn: false, } }; }, @@ -1861,15 +1900,18 @@ export default { const self = this; if(self.dragacceptitem().includes(event.type)) { let item = event.data; + // Perform layer update - set this slot and layer here self.relocateStudyItem(item).done(()=>{ item.layer = this.layer; item.slot = this.slotindex; self.value.push(item); - self.$emit("input",self.value); - }); + self.$emit("input",self.value); + self.validate_course_period(); + }); } else if(self.dragacceptcomponent().includes(event.type) ){ + if(event.type == "course"){ call([{ methodname: 'local_treestudyplan_add_studyitem', @@ -1891,7 +1933,8 @@ export default { let item = response; self.relocateStudyItem(item).done(()=>{ self.value.push(item); - self.$emit("input",self.value); + self.$emit("input",self.value); + self.validate_course_period(); }); }).fail(notification.exception); } @@ -1944,7 +1987,58 @@ export default { this.hover.component = null; this.hover.type = null; }, + validate_course_period() { + const self = this; + debug.info("Validating course and period",self.item,self.period); + if(self.item && self.item.type == 'course'){ + self.datechanger.coursespan = datespaninfo(self.item.course.startdate,self.item.course.enddate); + self.datechanger.periodspan = datespaninfo(self.period.startdate,self.period.enddate); + if( self.datechanger.coursespan.first != self.datechanger.periodspan.first + || self.datechanger.coursespan.last != self.datechanger.periodspan.last ){ + debug.info("Course timing does not match period timing"); + if(self.item.course.canupdatecourse){ + if(!self.datechanger.default){ + // Periods do not match, pop up the date change request + this.$bvModal.show("t-course-date-matching-"+this.slotkey); + } else if (self.datechanger.defaultvalue){ + // go for it without asking + self.change_course_period(); + } + } + else { + // user is not able to change course timing - show a warning + if(!self.datechanger.hidewarn){ + this.$bvModal.show("t-course-date-warning-"+this.slotkey); + } + } + } + else { + debug.info("Course timing matches period",self.datechanger); + } + } + }, + change_course_period() { + const self=this; + return call([{ + methodname: 'local_treestudyplan_course_period_timing', + args: { page_id: self.page_id, + period: self.period.period, + course_id: this.item.course.id, + } + }])[0].fail(notification.exception); + }, + format_duration(dsi){ + let s = ""; + if(dsi.years == 1){ s += `1 ${this.text.year}, `;} + else if(dsi.years > 1){ s += `${dsi.years} ${this.text.years}, `;} + if(dsi.weeks == 1){ s += `1 ${this.text.week}, `;} + else if(dsi.weeks > 1){ s += `${dsi.weeks} ${this.text.weeks}, `;} + if(dsi.days == 1){ s += `1 ${this.text.day}, `;} + else if(dsi.days > 1){ s += `${dsi.days} ${this.text.days}, `;} + + return s.toLocaleLowerCase(); + } }, template: `
--{{ hover.type }}--
+ > + + + {{ text.desc }} +
{{ text.question }}
+ + +

{{ text.course }}

+

{{ item.course.fullname }}

+

{{ item.course.shortname }}

+

{{ datechanger.coursespan.formatted.first}} - {{ datechanger.coursespan.formatted.last}}

+

{{ text.duration }}
+ {{ format_duration(datechanger.coursespan)}}

+
+ +

{{ text.period }}

+

{{ period.fullname }}

+

{{ period.shortname }}

+

{{ datechanger.periodspan.formatted.first}} - {{ datechanger.periodspan.formatted.last}}

+

{{ text.duration }}
+ {{ format_duration(datechanger.periodspan)}}

+
+
+ + {{ text.rememberchoice }} + +
+
+ + + {{ text.desc }} +
{{ text.warning }}
+ + +

{{ text.course }}

+

{{ item.course.fullname }}

+

{{ item.course.shortname }}

+

{{ datechanger.coursespan.formatted.first}} - {{ datechanger.coursespan.formatted.last}}

+

{{ text.duration }}
+ {{ format_duration(datechanger.coursespan)}}

+
+ "6"> +

{{ text.period }}

+

{{ period.fullname }}

+

{{ period.shortname }}

+

{{ datechanger.periodspan.formatted.first}} - {{ datechanger.periodspan.formatted.last}}

+

{{ text.duration }}
+ {{ format_duration(datechanger.periodspan)}}

+
+
+ + {{ text.hidewarning }} + +
+
+ `, }); diff --git a/amd/src/treestudyplan-components.js b/amd/src/treestudyplan-components.js index 727bf61..9368454 100644 --- a/amd/src/treestudyplan-components.js +++ b/amd/src/treestudyplan-components.js @@ -175,7 +175,7 @@ export default { >{{ value.fullname }}
{{ startdate }} - {{ enddate }} - +

diff --git a/classes/courseinfo.php b/classes/courseinfo.php index dce3b94..419891e 100644 --- a/classes/courseinfo.php +++ b/classes/courseinfo.php @@ -178,6 +178,7 @@ class courseinfo { "startdate" => new \external_value(PARAM_TEXT, 'Course start date'), "enddate" => new \external_value(PARAM_TEXT, 'Course end date'), "amteacher" => new \external_value(PARAM_BOOL, 'Requesting user is teacher in this course'), + "canupdatecourse" => new \external_value(PARAM_BOOL, "If the current user can update this course"), "canselectgradables" => new \external_value(PARAM_BOOL, 'Requesting user can change selected gradables'), "tag" => new \external_value(PARAM_TEXT, 'Tag'), ], 'referenced course information',$value); @@ -200,6 +201,7 @@ class courseinfo { 'startdate' => date("Y-m-d",$this->course->startdate,), 'enddate' => date("Y-m-d",$this->course->enddate), 'amteacher' => $this->amTeacher(), + 'canupdatecourse' => \has_capability("moodle/course:update",$this->coursecontext), 'canselectgradables' => $this->iCanSelectGradables(), 'tag' => "Editormodel", 'grades' => [], diff --git a/classes/studyplanservice.php b/classes/studyplanservice.php index cb825d5..bfcea46 100644 --- a/classes/studyplanservice.php +++ b/classes/studyplanservice.php @@ -1179,6 +1179,44 @@ class studyplanservice extends \external_api } + /************************ + * * + * edit_period * + * * + ************************/ + + public static function course_period_timing_parameters() + { + return new \external_function_parameters( [ + "page_id" => new \external_value(PARAM_INT, 'Studyplan page id'), + "period" => new \external_value(PARAM_INT, 'Period number within page'), + "course_id"=> new \external_value(PARAM_INT, 'Id of course to adjust dates for'), + ]); + } + + public static function course_period_timing_returns() + { + return success::structure(); + } + public static function course_period_timing($page_id, $period, $course_id){ + $page = studyplanpage::findById($page_id); + $course = \get_course($course_id); + $coursecontext = \context_course::instance($course_id); + // Check for studyplan edit permissions + webservicehelper::require_capabilities(self::CAP_EDIT,$page->studyplan()->context()); + + if(webservicehelper::has_capabilities("moodle/course:update",$coursecontext)){ + //TODO: Actually perform the timing changes, while also updating the module times + // Like what happens on a course "reset" + + + return success::success()->model(); + } else { + // probably should return a nice message + return success::fail("You do not have date change permissions on this course")->model(); + } + + } } \ No newline at end of file diff --git a/lang/en/local_treestudyplan.php b/lang/en/local_treestudyplan.php index b5fb924..959973d 100644 --- a/lang/en/local_treestudyplan.php +++ b/lang/en/local_treestudyplan.php @@ -290,4 +290,12 @@ $string["badgeinfo"] = "Badge details"; $string["badgeissuedstats"] = "Issuing progress"; $string["period_default_fullname"] = 'Period {$a}'; -$string["period_default_shortname"] = 'P{$a}'; \ No newline at end of file +$string["period_default_shortname"] = 'P{$a}'; +$string["course_timing_title"] = 'Course timing does not match period timing'; +$string["course_timing_desc"] = 'The start and end date of the course you are dropping into this period does not match the start and end date of the period.'; +$string["course_timing_question"] = 'Do you want to update the course\'s start and end time to match that of the period?'; +$string["course_timing_warning"] = 'You do not have permission to automatically update this course start and end date. Automatic timing update not available'; +$string["period"] = 'Period'; +$string["duration"] = 'Duration'; +$string["course_timing_rememberchoice"] = 'Remember my choice for future date mismatches'; +$string["course_timing_hidewarning"] = 'Hide this warning next time'; \ No newline at end of file diff --git a/lang/nl/local_treestudyplan.php b/lang/nl/local_treestudyplan.php index a071afe..48c40d0 100644 --- a/lang/nl/local_treestudyplan.php +++ b/lang/nl/local_treestudyplan.php @@ -293,4 +293,12 @@ $string["badgeinfo"] = "Meer details"; $string["badgeissuedstats"] = "Voortgang van uitgifte"; $string["period_default_fullname"] = 'Periode {$a}'; -$string["period_default_shortname"] = 'P{$a}'; \ No newline at end of file +$string["period_default_shortname"] = 'P{$a}'; +$string["course_timing_title"] = 'Cursustiming en periodetiming komen niet overeen'; +$string["course_timing_desc"] = 'De start- en einddatum van de cursus, komen niet overen met die van de periode waarin je hem hebt gedropt.'; +$string["course_timing_question"] = 'Wil je de start- en eindtijd van de cursus aanpassen naar doe van de periode?'; +$string["course_timing_warning"] = 'Je hebt geen rechten om de start- en eindtijd van deze cursus aan te passen. Aanpassen van cursus naar periodetiming is niet beschikbaar.'; +$string["period"] = 'Periode'; +$string["duration"] = 'Duur'; +$string["course_timing_rememberchoice'"] = 'Onthoud mijn keuze voor toekomstige mismatches tussen cursus en periode'; +$string["course_timing_hidewarning"] = 'Hide this warning next time'; \ No newline at end of file