Course update permission check and warnings

This commit is contained in:
PMKuipers 2023-07-30 04:08:57 +02:00
parent 795473a580
commit 0d793fce8f
7 changed files with 274 additions and 11 deletions

View file

@ -73,3 +73,47 @@ export function format_date(d,short){
year: 'numeric', month: monthformat, day: 'numeric' year: 'numeric', month: monthformat, day: 'numeric'
}); });
} }
/**
* 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),
}
};
}

View file

@ -10,7 +10,7 @@ import {SimpleLine} from "./simpleline";
import {call} from 'core/ajax'; import {call} from 'core/ajax';
import notification from 'core/notification'; import notification from 'core/notification';
import {get_strings} from 'core/str'; 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 {objCopy,transportItem} from './studyplan-processor';
import Debugger from './debugger'; import Debugger from './debugger';
import {download,upload} from './downloader'; import {download,upload} from './downloader';
@ -148,6 +148,26 @@ export default {
startdate: 'studyplan_startdate', startdate: 'studyplan_startdate',
enddate: 'studyplan_enddate', 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: { studyplan_associate: {
associations: 'associations', associations: 'associations',
associated_cohorts: 'associated_cohorts', associated_cohorts: 'associated_cohorts',
@ -1526,6 +1546,7 @@ export default {
:line="line" :line="line"
:plan="value" :plan="value"
:page="page" :page="page"
:period="page.perioddesc[index-1]"
:layer="layeridx-1" :layer="layeridx-1"
:class="'t-studyline ' + ((lineindex%2==0)?' odd ':' even ') :class="'t-studyline ' + ((lineindex%2==0)?' odd ':' even ')
+ ((lineindex==0 && layeridx==1)?' first ':' ') + ((lineindex==0 && layeridx==1)?' first ':' ')
@ -1787,7 +1808,14 @@ export default {
type: Object, // Studyplan data type: Object, // Studyplan data
default(){ return null;}, default(){ return null;},
}, },
page: {
type: Object, // Studyplan data
default(){ return null;},
},
period: {
type: Object, // Studyplan data
default(){ return null;},
},
}, },
mounted() { mounted() {
const self=this; const self=this;
@ -1807,6 +1835,9 @@ export default {
} }
}, },
computed: { computed: {
slotkey(){
return `${this.type}'-'${this.line.id}-${this.slotindex}-${this.layer}`;
},
item(){ item(){
for(const ix in this.value){ for(const ix in this.value){
const itm = this.value[ix]; const itm = this.value[ix];
@ -1832,10 +1863,18 @@ export default {
}, },
data() { data() {
return { return {
text: strings.course_timing,
resizeListener: null, resizeListener: null,
hover: { hover: {
component:null, component:null,
type: null, type: null,
},
datechanger: {
coursespan: null,
periodspan: null,
default: false,
defaultchoice: false,
hidewarn: false,
} }
}; };
}, },
@ -1861,15 +1900,18 @@ export default {
const self = this; const self = this;
if(self.dragacceptitem().includes(event.type)) { if(self.dragacceptitem().includes(event.type)) {
let item = event.data; let item = event.data;
// Perform layer update - set this slot and layer here // Perform layer update - set this slot and layer here
self.relocateStudyItem(item).done(()=>{ self.relocateStudyItem(item).done(()=>{
item.layer = this.layer; item.layer = this.layer;
item.slot = this.slotindex; item.slot = this.slotindex;
self.value.push(item); 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) ){ else if(self.dragacceptcomponent().includes(event.type) ){
if(event.type == "course"){ if(event.type == "course"){
call([{ call([{
methodname: 'local_treestudyplan_add_studyitem', methodname: 'local_treestudyplan_add_studyitem',
@ -1892,6 +1934,7 @@ export default {
self.relocateStudyItem(item).done(()=>{ self.relocateStudyItem(item).done(()=>{
self.value.push(item); self.value.push(item);
self.$emit("input",self.value); self.$emit("input",self.value);
self.validate_course_period();
}); });
}).fail(notification.exception); }).fail(notification.exception);
} }
@ -1944,7 +1987,58 @@ export default {
this.hover.component = null; this.hover.component = null;
this.hover.type = 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: ` template: `
<div :class="'t-studyline-slot '+type + ' t-studyline-slot-'+slotindex + ' ' + ((slotindex==0)?' t-studyline-firstcolumn ':' ')" <div :class="'t-studyline-slot '+type + ' t-studyline-slot-'+slotindex + ' ' + ((slotindex==0)?' t-studyline-firstcolumn ':' ')"
@ -1987,8 +2081,77 @@ export default {
class="t-slot-item feedback" class="t-slot-item feedback"
:key="hover.type">--{{ hover.type }}--</div :key="hover.type">--{{ hover.type }}--</div
></template ></template
></drop ></drop>
></div> <b-modal
:id="'t-course-date-matching-'+this.slotkey"
size="lg"
:title="text.title"
@ok="change_course_period"
:ok-title="text.yes"
cancel-disabled
>
<b-container v-if="datechanger.coursespan && datechanger.periodspan && item && item.course">
<b-row><b-col cols="12">{{ text.desc }}</b-col></b-row>
<b-row><b-col cols="12"><div class="generalbox alert alert-warning">{{ text.question }}</div></b-col></b-row>
<b-row>
<b-col cols="6">
<h3> {{ text.course }} </h3>
<p><b>{{ item.course.fullname }}</b></p>
<p><b>{{ item.course.shortname }}</b></p>
<p>{{ datechanger.coursespan.formatted.first}} - {{ datechanger.coursespan.formatted.last}}</p>
<p><b>{{ text.duration }}</b><br>
{{ format_duration(datechanger.coursespan)}}</p>
</b-col>
<b-col cols="6">
<h3> {{ text.period }} </h3>
<p><b>{{ period.fullname }}</b></p>
<p><b>{{ period.shortname }}</b></p>
<p>{{ datechanger.periodspan.formatted.first}} - {{ datechanger.periodspan.formatted.last}}</p>
<p><b>{{ text.duration }}</b><br>
{{ format_duration(datechanger.periodspan)}}</p>
</b-col>
</b-row>
<b-row><b-col cols="12">
<b-form-checkbox type="checkbox" v-model="datechanger.default">{{ text.rememberchoice }}</b-form-checkbox>
</b-col></b-row>
</b-container>
</b-modal>
<b-modal
:id="'t-course-date-warning-'+this.slotkey"
size="lg"
ok-variant="danger"
:title="text.title"
:ok-title="text.yes"
:cancel-title="text.no"
cancel-variant="primary"
>
<b-container v-if="datechanger.coursespan && datechanger.periodspan && item && item.course">
<b-row><b-col cols="12">{{ text.desc }}</b-col></b-row>
<b-row><b-col cols="12"><div class="generalbox alert alert-warning">{{ text.warning }}</div></b-col></b-row>
<b-row>
<b-col cols="6">
<h3> {{ text.course }} </h3>
<p><b>{{ item.course.fullname }}</b></p>
<p><b>{{ item.course.shortname }}</b></p>
<p>{{ datechanger.coursespan.formatted.first}} - {{ datechanger.coursespan.formatted.last}}</p>
<p><b>{{ text.duration }}</b><br>
{{ format_duration(datechanger.coursespan)}}</p>
</b-col>
<b-col cols=>"6">
<h3> {{ text.period }} </h3>
<p><b>{{ period.fullname }}</b></p>
<p><b>{{ period.shortname }}</b></p>
<p>{{ datechanger.periodspan.formatted.first}} - {{ datechanger.periodspan.formatted.last}}</p>
<p><b>{{ text.duration }}</b><br>
{{ format_duration(datechanger.periodspan)}}</p>
</b-col>
</b-row>
<b-row><b-col cols="12">
<b-form-checkbox type="checkbox" v-model="datechanger.hidewarn">{{ text.hidewarning }}</b-form-checkbox>
</b-col></b-row>
</b-container>
</b-modal>
</div>
`, `,
}); });

View file

@ -175,7 +175,7 @@ export default {
>{{ value.fullname }}<br> >{{ value.fullname }}<br>
<span class="s-studyline-header-period-datespan"> <span class="s-studyline-header-period-datespan">
<span class="date">{{ startdate }}</span> - <span class="date">{{ enddate }}</span> <span class="date">{{ startdate }}</span> - <span class="date">{{ enddate }}</span>
<span> </span>
</b-tooltip> </b-tooltip>
<slot></slot <slot></slot
><p class="s-studyline-header-period-datespan small"> ><p class="s-studyline-header-period-datespan small">

View file

@ -178,6 +178,7 @@ class courseinfo {
"startdate" => new \external_value(PARAM_TEXT, 'Course start date'), "startdate" => new \external_value(PARAM_TEXT, 'Course start date'),
"enddate" => new \external_value(PARAM_TEXT, 'Course end date'), "enddate" => new \external_value(PARAM_TEXT, 'Course end date'),
"amteacher" => new \external_value(PARAM_BOOL, 'Requesting user is teacher in this course'), "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'), "canselectgradables" => new \external_value(PARAM_BOOL, 'Requesting user can change selected gradables'),
"tag" => new \external_value(PARAM_TEXT, 'Tag'), "tag" => new \external_value(PARAM_TEXT, 'Tag'),
], 'referenced course information',$value); ], 'referenced course information',$value);
@ -200,6 +201,7 @@ class courseinfo {
'startdate' => date("Y-m-d",$this->course->startdate,), 'startdate' => date("Y-m-d",$this->course->startdate,),
'enddate' => date("Y-m-d",$this->course->enddate), 'enddate' => date("Y-m-d",$this->course->enddate),
'amteacher' => $this->amTeacher(), 'amteacher' => $this->amTeacher(),
'canupdatecourse' => \has_capability("moodle/course:update",$this->coursecontext),
'canselectgradables' => $this->iCanSelectGradables(), 'canselectgradables' => $this->iCanSelectGradables(),
'tag' => "Editormodel", 'tag' => "Editormodel",
'grades' => [], 'grades' => [],

View file

@ -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();
}
}
} }

View file

@ -291,3 +291,11 @@ $string["badgeissuedstats"] = "Issuing progress";
$string["period_default_fullname"] = 'Period {$a}'; $string["period_default_fullname"] = 'Period {$a}';
$string["period_default_shortname"] = 'P{$a}'; $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';

View file

@ -294,3 +294,11 @@ $string["badgeissuedstats"] = "Voortgang van uitgifte";
$string["period_default_fullname"] = 'Periode {$a}'; $string["period_default_fullname"] = 'Periode {$a}';
$string["period_default_shortname"] = 'P{$a}'; $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';