Finalized functionality for period spanning and period matching

This commit is contained in:
PMKuipers 2023-08-06 23:17:36 +02:00
parent 7b36ee3284
commit 5146ebdf0a
15 changed files with 351 additions and 155 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -194,7 +194,6 @@ export function init(contextid,categoryid) {
window.location.search = params.toString(); window.location.search = params.toString();
}, },
onStudyPlanCreated(newstudyplan){ onStudyPlanCreated(newstudyplan){
debug.info("New studyplan:",newstudyplan);
app.studyplans.push(newstudyplan); app.studyplans.push(newstudyplan);
app.selectStudyplan(newstudyplan); app.selectStudyplan(newstudyplan);
@ -213,7 +212,6 @@ export function init(contextid,categoryid) {
methodname: 'local_treestudyplan_get_studyplan_map', methodname: 'local_treestudyplan_get_studyplan_map',
args: { id: studyplan.id} args: { id: studyplan.id}
}])[0].done(function(response){ }])[0].done(function(response){
debug.info(response);
app.activestudyplan = ProcessStudyplan(response,true); app.activestudyplan = ProcessStudyplan(response,true);
debug.info('studyplan processed'); debug.info('studyplan processed');
app.loadingstudyplan = false; app.loadingstudyplan = false;

View file

@ -24,6 +24,12 @@ const PERIOD_EDITOR_FIELDS =
const LINE_GRAVITY = 1.3; const LINE_GRAVITY = 1.3;
const datechanger_globals = {
default: false,
defaultchoice: false,
hidewarn: false,
};
export default { export default {
STUDYPLAN_EDITOR_FIELDS: STUDYPLAN_EDITOR_FIELDS, // make copy available in plugin STUDYPLAN_EDITOR_FIELDS: STUDYPLAN_EDITOR_FIELDS, // make copy available in plugin
install(Vue/*,options*/){ install(Vue/*,options*/){
@ -158,6 +164,8 @@ export default {
desc: 'course_timing_desc', desc: 'course_timing_desc',
question: 'course_timing_question', question: 'course_timing_question',
warning: 'course_timing_warning', warning: 'course_timing_warning',
timing_ok: 'course_timing_ok',
timing_off: 'course_timing_off',
course: 'course$core', course: 'course$core',
period: 'period', period: 'period',
yes: 'yes$core', yes: 'yes$core',
@ -171,6 +179,7 @@ export default {
day: 'day$core', day: 'day$core',
rememberchoice: 'course_timing_rememberchoice', rememberchoice: 'course_timing_rememberchoice',
hidewarning: 'course_timing_hidewarning', hidewarning: 'course_timing_hidewarning',
periodspan: 'course_period_span',
}, },
studyplan_associate: { studyplan_associate: {
associations: 'associations', associations: 'associations',
@ -200,6 +209,7 @@ export default {
coursetiming_future: "coursetiming_future", coursetiming_future: "coursetiming_future",
grade_include: "grade_include", grade_include: "grade_include",
grade_require: "grade_require", grade_require: "grade_require",
ok: "ok$core",
}, },
invalid: { invalid: {
error: 'error', error: 'error',
@ -1262,7 +1272,6 @@ export default {
this.$root.$emit('redrawLines'); this.$root.$emit('redrawLines');
}, },
updated() { updated() {
console.info("UPDATED Studyplan");
this.$root.$emit('redrawLines'); this.$root.$emit('redrawLines');
ItemEventBus.$emit('redrawLines'); ItemEventBus.$emit('redrawLines');
}, },
@ -1334,7 +1343,6 @@ export default {
'sequence': page.studylines.length, 'sequence': page.studylines.length,
} }
}])[0].done(function(response){ }])[0].done(function(response){
debug.info("New studyline:",response);
page.studylines.push(response); page.studylines.push(response);
newlineinfo.name = ''; newlineinfo.name = '';
newlineinfo.shortname = ''; newlineinfo.shortname = '';
@ -1349,7 +1357,6 @@ export default {
editLineFinish() { editLineFinish() {
let editedline = this.edit.studyline.data; let editedline = this.edit.studyline.data;
let originalline = this.edit.studyline.original; let originalline = this.edit.studyline.original;
debug.info('Edit Line',this.edit.studyline);
call([{ call([{
methodname: 'local_treestudyplan_edit_studyline', methodname: 'local_treestudyplan_edit_studyline',
args: { 'id': editedline.id, args: { 'id': editedline.id,
@ -1357,14 +1364,12 @@ export default {
'shortname': editedline.shortname, 'shortname': editedline.shortname,
'color': editedline.color,} 'color': editedline.color,}
}])[0].done(function(response){ }])[0].done(function(response){
debug.info('Edit response:', response);
originalline['name'] = response['name']; originalline['name'] = response['name'];
originalline['shortname'] = response['shortname']; originalline['shortname'] = response['shortname'];
originalline['color'] = response['color']; originalline['color'] = response['color'];
}).fail(notification.exception); }).fail(notification.exception);
}, },
deleteLine(page,line) { deleteLine(page,line) {
debug.info('Delete Line',line);
const self=this; const self=this;
get_strings([ get_strings([
{key: 'studyline_confirm_remove', param: line.name, component: 'local_treestudyplan' }, {key: 'studyline_confirm_remove', param: line.name, component: 'local_treestudyplan' },
@ -1379,7 +1384,6 @@ export default {
methodname: 'local_treestudyplan_delete_studyline', methodname: 'local_treestudyplan_delete_studyline',
args: { 'id': line.id, } args: { 'id': line.id, }
}])[0].done(function(response){ }])[0].done(function(response){
debug.info('Delete response:', response);
if(response.success == true){ if(response.success == true){
let index = page.studylines.indexOf(line); let index = page.studylines.indexOf(line);
page.studylines.splice(index, 1); page.studylines.splice(index, 1);
@ -1390,7 +1394,6 @@ export default {
}); });
}, },
reorderLines(event,lines){ reorderLines(event,lines){
debug.info("Reorder lines",event,lines);
// apply reordering // apply reordering
event.apply(lines); event.apply(lines);
@ -1404,12 +1407,10 @@ export default {
methodname: 'local_treestudyplan_reorder_studylines', methodname: 'local_treestudyplan_reorder_studylines',
args: { 'sequence': sequence } args: { 'sequence': sequence }
}])[0].done(function(response){ }])[0].done(function(response){
debug.info('Reorder response:', response);
}).fail(notification.exception); }).fail(notification.exception);
}, },
deletePlan(studyplan){ deletePlan(studyplan){
const self=this; const self=this;
debug.info('Delete studyplan:', studyplan);
get_strings([ get_strings([
{key: 'studyplan_confirm_remove', param: studyplan.name, component: 'local_treestudyplan' }, {key: 'studyplan_confirm_remove', param: studyplan.name, component: 'local_treestudyplan' },
{key: 'delete', component: 'core' }, {key: 'delete', component: 'core' },
@ -1423,7 +1424,6 @@ export default {
methodname: 'local_treestudyplan_delete_studyplan', methodname: 'local_treestudyplan_delete_studyplan',
args: { 'id': studyplan.id, } args: { 'id': studyplan.id, }
}])[0].done(function(response){ }])[0].done(function(response){
debug.info('Delete response:', response);
if(response.success == true){ if(response.success == true){
self.$root.$emit("studyplanRemoved",studyplan); self.$root.$emit("studyplanRemoved",studyplan);
} }
@ -1433,7 +1433,6 @@ export default {
}); });
}, },
deleteStudyItem(event){ deleteStudyItem(event){
debug.info('Delete studyitem:', event);
//const self = this; //const self = this;
let item = event.data; let item = event.data;
@ -1441,7 +1440,6 @@ export default {
methodname: 'local_treestudyplan_delete_studyitem', methodname: 'local_treestudyplan_delete_studyitem',
args: { 'id': item.id, } args: { 'id': item.id, }
}])[0].done(function(response){ }])[0].done(function(response){
debug.info('Delete response:', response);
if(response.success == true){ if(response.success == true){
event.source.$emit('cut',event); event.source.$emit('cut',event);
} }
@ -1952,10 +1950,10 @@ export default {
// then on the next tick, we inform the back end // then on the next tick, we inform the back end
// Since moving things around has never been unsuccessful, unless you have other problems, // Since moving things around has never been unsuccessful, unless you have other problems,
// it's better to have nice visuals. // it's better to have nice visuals.
this.$nextTick(() => { self.relocateStudyItem(item).done(()=>{
self.relocateStudyItem(item).done(()=>{ if(this.$refs.timingChecker){
self.validate_course_period(); this.$refs.timingChecker.validate_course_period();
}); }
}); });
} }
else if(event.type.component){ else if(event.type.component){
@ -1977,14 +1975,18 @@ export default {
} }
} }
}])[0].done((response) => { }])[0].done((response) => {
console.info('Add item response:', response);
let item = response; let item = response;
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);
// call the validate period function on next tick, // call the validate period function on next tick,
// since it paints the item in the slot first // since it paints the item in the slot first
this.$nextTick(self.validate_course_period); this.$nextTick(() => {
if(this.$refs.timingChecker){
this.$refs.timingChecker.validate_course_period();
}
});
}); });
}).fail(notification.exception); }).fail(notification.exception);
} }
@ -2000,7 +2002,6 @@ export default {
} }
} }
}])[0].done((response) => { }])[0].done((response) => {
console.info('Add item response:', response);
let item = response; let item = response;
self.relocateStudyItem(item).done(()=>{ self.relocateStudyItem(item).done(()=>{
self.value.push(item); self.value.push(item);
@ -2037,58 +2038,6 @@ 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();
},
maxSpan(){ maxSpan(){
// Determine the maximum span for components in this slot // Determine the maximum span for components in this slot
// Used for setting the max in the timing adjustment screen (s) // Used for setting the max in the timing adjustment screen (s)
@ -2100,7 +2049,8 @@ export default {
for(let i = this.slotindex + 1; i <= this.page.periods; i++){ for(let i = this.slotindex + 1; i <= this.page.periods; i++){
if(this.line.slots && this.line.slots[i] && this.line.slots[i].competencies){ if(this.line.slots && this.line.slots[i] && this.line.slots[i].competencies){
const l = this.line.slots[i].competencies; const l = this.line.slots[i].competencies;
if(l[this.layer]) { const f = this.line.slots[i-1].filters; // next filter is in the same slot
if(l[this.layer] || f[this.layer]) {
// slot is busy in this layer. // slot is busy in this layer.
break; break;
} else { } else {
@ -2148,7 +2098,7 @@ export default {
:data="item" :data="item"
:type="makeType(item)" :type="makeType(item)"
@cut="onCut" @cut="onCut"
><t-item v-model="item" :plan="plan"></t-item ><t-item v-model="item" :plan="plan" :page='page' :period='period' :maxspan='maxSpan()'></t-item
></drag ></drag
><drop v-else ><drop v-else
:class="'t-slot-drop '+type + (layer > 0?' secondary':' primary')" :class="'t-slot-drop '+type + (layer > 0?' secondary':' primary')"
@ -2180,8 +2130,171 @@ export default {
:key="hover.type">--{{ hover.type }}--</div :key="hover.type">--{{ hover.type }}--</div
></template ></template
></drop> ></drop>
<t-item-timing-checker hidden
v-if="item && item.course"
ref="timingChecker"
:maxspan="maxSpan()"
:page="page"
:period="period"
v-model="item"
></t-item-timing-checker>
</div>
`,
});
Vue.component('t-item-timing-checker', {
props: {
value: {
type: Object, // t-item model
},
page: {
type: Object, // Studyplan data
},
period: {
type: Object, // Studyplan data
},
maxspan: {
type: Number,
},
hidden: {
type: Boolean,
default: false,
}
},
computed: {
endperiod() {
const endperiodnr = Math.min(this.page.periods,this.period.period + (this.value.span - 1));
return this.page.perioddesc[endperiodnr-1];
},
course_period_matches() {
const self=this;
if(self.value && self.value.type == 'course'){
self.datechanger.coursespan = datespaninfo(self.value.course.startdate,self.value.course.enddate);
self.datechanger.periodspan = datespaninfo(self.period.startdate,self.endperiod.enddate);
if( self.datechanger.coursespan.first.getTime() == self.datechanger.periodspan.first.getTime()
&& self.datechanger.coursespan.last.getTime() == self.datechanger.periodspan.last.getTime() ){
return true;
}
else {
return false;
}
} else {
debug.warn("Timing thing not proper configured",self.value,self.period,self.maxspan);
return false;
}
},
},
data() {
return {
// Create random id to avoid opening the wrong modals
id: Math.floor(Math.random() * Date.now()).toString(16),
text: strings.course_timing,
datechanger: {
coursespan: null,
periodspan: null,
globals: datechanger_globals,
}
};
},
methods: {
validate_course_period(force) {
const self = this;
debug.info("Validating course and period");
if(!(self.course_period_matches)){
debug.info("Course timing does not match period timing");
if(self.value.course.canupdatecourse){
if(!self.hidden || !self.datechanger.globals.default){
// Periods do not match, pop up the date change request
this.$bvModal.show('t-course-timing-matching-'+this.id);
} else if (self.datechanger.globals.defaultvalue){
// go for it without asking
self.change_course_period();
}
}
else {
// user is not able to change course timing - show a warning
if(!self.hidden || !self.datechanger.globals.hidewarn){
this.$bvModal.show('t-course-timing-warning-'+this.id);
}
}
}
else {
debug.info("Course timing matches period",self.datechanger);
}
},
change_course_period() {
const self=this;
// Save the state
if(self.datechanger.globals.default){
self.datechanger.globals.defaultvalue = true;
}
return call([{
methodname: 'local_treestudyplan_course_period_timing',
args: { period_id: self.period.id,
course_id: this.value.course.id,
span: this.value.span,
}
}])[0].fail(notification.exception).done((response) => {
self.value.course.startdate = response.startdate;
self.value.course.enddate = response.enddate;
self.value.course.timing = response.timing;
self.$emit("input",self.value);
});
},
change_span(span) {
const self=this;
return call([{
methodname: 'local_treestudyplan_set_studyitem_span',
args: { id: self.value.id,
span: span
}
}])[0].fail(notification.exception).done((response) => {
self.value.span = response.span;
self.$emit('input',self.value);
self.$nextTick(() => {
self.validate_course_period();
});
} );
},
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();
},
},
// To avoid the span creeping in the dom where it shouldn't, set display to none if it is hidden
// This does not affect the modals, which are rendered outside of this element when needed
template: `
<span :class="'t-course-timing-matcher'" :style="hidden?'display: none ':''">
<span v-if="!hidden">
<i v-if="course_period_matches" class="text-success fa fa-calendar-check-o"
v-b-tooltip.hover.topright :title="text.timing_ok"
></i
><a v-else
href='#' @click="validate_course_period()" class="text-warning"
><i class="fa fa-calendar-times-o" v-b-tooltip.hover.topright :title="text.timing_off"
></i
></a>
<span v-if='value.span > 1 || value.span < maxspan' >
{{ text.periodspan}}
<b-form-select @change="change_span" v-model="value.span">
<b-form-select-option v-for="(n,i) in maxspan" :value='n'
>{{ n }}</b-form-select-option>
</b-form-select>
</span>
</span>
<b-modal <b-modal
:id="'t-course-date-matching-'+this.slotkey" :id="'t-course-timing-matching-'+this.id"
size="lg" size="lg"
:title="text.title" :title="text.title"
@ok="change_course_period" @ok="change_course_period"
@ -2190,85 +2303,97 @@ export default {
:cancel-title="text.no" :cancel-title="text.no"
cancel-variant="primary" cancel-variant="primary"
> >
<b-container v-if="datechanger.coursespan && datechanger.periodspan && item && item.course"> <b-container v-if="datechanger.coursespan && datechanger.periodspan && value && value.course">
<b-row><b-col cols="12">{{ text.desc }}</b-col></b-row> <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="12"><div class="generalbox alert alert-warning">{{ text.question }}</div></b-col></b-row>
<b-row> <b-row>
<b-col cols="6"> <b-col cols="6">
<h3> {{ text.course }} </h3> <h3> {{ text.course }} </h3>
<p><b>{{ item.course.fullname }}</b></p> <p><b>{{ value.course.fullname }}</b></p>
<p><b>{{ item.course.shortname }}</b></p> <p><b>{{ value.course.shortname }}</b></p>
<p>{{ datechanger.coursespan.formatted.first}} - {{ datechanger.coursespan.formatted.last}}</p> <p>{{ datechanger.coursespan.formatted.first}} - {{ datechanger.coursespan.formatted.last}}</p>
<p><b>{{ text.duration }}</b><br> <p><b>{{ text.duration }}</b><br>
{{ format_duration(datechanger.coursespan)}}</p> {{ format_duration(datechanger.coursespan)}}</p>
</b-col> </b-col>
<b-col cols="6"> <b-col cols="6">
<h3> {{ text.period }} </h3> <h3> {{ text.period }} </h3>
<p><b>{{ period.fullname }}</b></p> <p><b>{{ period.fullname }}</b><b v-if="value.span > 1"> - {{ endperiod.fullname }}</b></p>
<p><b>{{ period.shortname }}</b></p> <p><b>{{ period.shortname }}</b><b v-if="value.span > 1"> - {{ endperiod.shortname }}</b></p>
<p>{{ datechanger.periodspan.formatted.first}} - {{ datechanger.periodspan.formatted.last}}</p> <p>{{ datechanger.periodspan.formatted.first}} - {{ datechanger.periodspan.formatted.last}}</p>
<p><b>{{ text.duration }}</b><br> <p><b>{{ text.duration }}</b><br>
{{ format_duration(datechanger.periodspan)}}</p> {{ format_duration(datechanger.periodspan)}}</p>
</b-col> </b-col>
</b-row> </b-row>
<b-row><b-col cols="12"> <b-row v-if='hidden'><b-col cols="12">
<b-form-checkbox type="checkbox" v-model="datechanger.default">{{ text.rememberchoice }}</b-form-checkbox> <b-form-checkbox type="checkbox" v-model="datechanger.globals.default">{{ text.rememberchoice }}</b-form-checkbox>
</b-col></b-row> </b-col></b-row>
</b-container> </b-container>
</b-modal> </b-modal>
<b-modal <b-modal
:id="'t-course-date-warning-'+this.slotkey" :id="'t-course-timing-warning-'+this.id"
size="lg" size="lg"
ok-variant="primary" ok-variant="primary"
:title="text.title" :title="text.title"
:ok-title="text.yes" :ok-title="text.yes"
ok-only ok-only
> >
<b-container v-if="datechanger.coursespan && datechanger.periodspan && item && item.course"> <b-container v-if="datechanger.coursespan && datechanger.periodspan && value && value.course">
<b-row><b-col cols="12">{{ text.desc }}</b-col></b-row> <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="12"><div class="generalbox alert alert-warning">{{ text.warning }}</div></b-col></b-row>
<b-row> <b-row>
<b-col cols="6"> <b-col cols="6">
<h3> {{ text.course }} </h3> <h3> {{ text.course }} </h3>
<p><b>{{ item.course.fullname }}</b></p> <p><b>{{ value.course.fullname }}</b></p>
<p><b>{{ item.course.shortname }}</b></p> <p><b>{{ value.course.shortname }}</b></p>
<p>{{ datechanger.coursespan.formatted.first}} - {{ datechanger.coursespan.formatted.last}}</p> <p>{{ datechanger.coursespan.formatted.first}} - {{ datechanger.coursespan.formatted.last}}</p>
<p><b>{{ text.duration }}</b><br> <p><b>{{ text.duration }}</b><br>
{{ format_duration(datechanger.coursespan)}}</p> {{ format_duration(datechanger.coursespan)}}</p>
</b-col> </b-col>
<b-col cols=>"6"> <b-col cols=>"6">
<h3> {{ text.period }} </h3> <h3> {{ text.period }} </h3>
<p><b>{{ period.fullname }}</b></p> <p><b>{{ period.fullname }}</b><b v-if="value.span > 1"> - {{ endperiod.fullname }}</b></p>
<p><b>{{ period.shortname }}</b></p> <p><b>{{ period.shortname }}</b><b v-if="value.span > 1"> - {{ endperiod.shortname }}</b></p>
<p>{{ datechanger.periodspan.formatted.first}} - {{ datechanger.periodspan.formatted.last}}</p> <p>{{ datechanger.periodspan.formatted.first}} - {{ datechanger.periodspan.formatted.last}}</p>
<p><b>{{ text.duration }}</b><br> <p><b>{{ text.duration }}</b><br>
{{ format_duration(datechanger.periodspan)}}</p> {{ format_duration(datechanger.periodspan)}}</p>
</b-col> </b-col>
</b-row> </b-row>
<b-row><b-col cols="12"> <b-row v-if='hidden'><b-col cols="12">
<b-form-checkbox type="checkbox" v-model="datechanger.hidewarn">{{ text.hidewarning }}</b-form-checkbox> <b-form-checkbox type="checkbox" v-model="datechanger.globals.hidewarn">{{ text.hidewarning }}</b-form-checkbox>
</b-col></b-row> </b-col></b-row>
</b-container> </b-container>
</b-modal> </b-modal>
</div> </span>
`, `,
}); });
Vue.component('t-item', { Vue.component('t-item', {
props: { props: {
'value' :{ value :{
type: Object, type: Object,
default(){ return null;}, default(){ return null;},
}, },
'dummy' :{ dummy :{
type: Boolean, type: Boolean,
default() { return false;}, default() { return false;},
}, },
'plan': { plan: {
type: Object, // Studyplan page type: Object, // Studyplan page
default() { return null;}, default() { return null;},
}, },
page: {
type: Object, // Studyplan page
default() { return null;},
},
period: {
type: Object, // Studyplan page
default() { return null;},
},
maxspan: {
type: Number,
default() { return 0;},
},
}, },
data() { data() {
return { return {
@ -2324,9 +2449,8 @@ export default {
call([{ call([{
methodname: 'local_treestudyplan_connect_studyitems', methodname: 'local_treestudyplan_connect_studyitems',
args: { 'from_id': from_id, 'to_id': to_id } args: { 'from_id': from_id, 'to_id': to_id }
}])[0].done((result)=>{ }])[0].done((response)=>{
console.info("Drop result",result); let conn = {'id': response.id, 'from_id': response.from_id, 'to_id': response.to_id};
let conn = {'id': result.id, 'from_id': result.from_id, 'to_id': result.to_id};
ItemEventBus.$emit("createdConnection",conn); ItemEventBus.$emit("createdConnection",conn);
this.value.connections.in.push(conn); this.value.connections.in.push(conn);
}).fail(notification.exception); }).fail(notification.exception);
@ -2355,12 +2479,11 @@ export default {
}, },
deleteLine(conn){ deleteLine(conn){
const self = this; const self = this;
// console.info("Delete Line",conn);
call([{ call([{
methodname: 'local_treestudyplan_disconnect_studyitems', methodname: 'local_treestudyplan_disconnect_studyitems',
args: { 'from_id': conn.from_id, 'to_id': conn.to_id } args: { 'from_id': conn.from_id, 'to_id': conn.to_id }
}])[0].done((result)=>{ }])[0].done((response)=>{
if(result.success){ if(response.success){
this.removeLine(conn); this.removeLine(conn);
// send disconnect event on message bus, so the connection on the other end can delete it too // send disconnect event on message bus, so the connection on the other end can delete it too
ItemEventBus.$emit("connectionDisconnected",conn); ItemEventBus.$emit("connectionDisconnected",conn);
@ -2397,7 +2520,6 @@ export default {
redrawLines(){ redrawLines(){
for(let i in this.value.connections.out){ for(let i in this.value.connections.out){
let conn = this.value.connections.out[i]; let conn = this.value.connections.out[i];
// console.info('Connection out', conn);
this.redrawLine(conn); this.redrawLine(conn);
} }
}, },
@ -2405,7 +2527,6 @@ export default {
// EVENT LISTENERS // EVENT LISTENERS
onCreatedConnection(conn){ onCreatedConnection(conn){
if(conn.from_id == this.value.id){ if(conn.from_id == this.value.id){
// console.info("incomingConnection",conn);
this.value.connections.out.push(conn); this.value.connections.out.push(conn);
this.redrawLine(conn); this.redrawLine(conn);
} }
@ -2415,7 +2536,6 @@ export default {
for(let i in this.value.connections.in){ for(let i in this.value.connections.in){
let c_in = this.value.connections.in[i]; let c_in = this.value.connections.in[i];
if(conn.id == c_in.id){ if(conn.id == c_in.id){
// console.info("Deleting incoming connection",conn);
self.value.connections.out.splice(i, 1); self.value.connections.out.splice(i, 1);
} }
} }
@ -2513,7 +2633,6 @@ export default {
if(!this.dummy) if(!this.dummy)
{ {
// console.info('Mounted', this);
this.redrawLines(); this.redrawLines();
setTimeout(()=>{ setTimeout(()=>{
ItemEventBus.$emit("rePositioned",this.value.id); ItemEventBus.$emit("rePositioned",this.value.id);
@ -2547,7 +2666,7 @@ export default {
template: ` template: `
<div class="t-item-base" :id="'studyitem-'+value.id"> <div class="t-item-base" :id="'studyitem-'+value.id">
<t-item-course v-model="value" v-if="value.type == 'course'" <t-item-course v-model="value" v-if="value.type == 'course'"
:plan='plan' ></t-item-course> :plan='plan' :page='page' :period='period' :maxspan='maxspan'></t-item-course>
<t-item-junction v-model="value" v-if="value.type == 'junction'" ></t-item-junction> <t-item-junction v-model="value" v-if="value.type == 'junction'" ></t-item-junction>
<t-item-start v-model="value" v-if="value.type == 'start'" ></t-item-start> <t-item-start v-model="value" v-if="value.type == 'start'" ></t-item-start>
<t-item-finish v-model="value" v-if="value.type == 'finish'" ></t-item-finish> <t-item-finish v-model="value" v-if="value.type == 'finish'" ></t-item-finish>
@ -2636,14 +2755,26 @@ export default {
Vue.component('t-item-course', { Vue.component('t-item-course', {
props: { props: {
'value' :{ value:{
type: Object, type: Object,
default(){ return null;}, default(){ return null;},
}, },
'plan' :{ plan:{
type: Object, type: Object,
default(){ return null;}, default(){ return null;},
}, },
page: {
type: Object, // PAge data
default() { return null;}
},
period: {
type: Object, // Period data
default() { return null;}
},
maxspan: {
type: Number,
default() { return 0;}
},
}, },
data() { data() {
return { return {
@ -2788,6 +2919,20 @@ export default {
</div> </div>
</div> </div>
</template> </template>
<template #modal-footer="{ ok }" class='d-flex'>
<div class="flex-fill">
<!-- Configure spans and timing if needed -->
<t-item-timing-checker
:maxspan="maxspan"
:page="page"
:period="period"
v-model="value"
></t-item-timing-checker>
</div>
<b-button class='' variant="primary" @click="ok()">
{{ text.ok }}
</b-button>
</template>
<t-item-course-grades <t-item-course-grades
v-if='!!value.course.grades && value.course.grades.length > 0' v-if='!!value.course.grades && value.course.grades.length > 0'
@ -3181,7 +3326,6 @@ export default {
methodname: 'local_treestudyplan_get_category', methodname: 'local_treestudyplan_get_category',
args: { "id": this.value.id} args: { "id": this.value.id}
}])[0].done(function(response){ }])[0].done(function(response){
debug.info("Course info:",response);
self.$emit('input', response); self.$emit('input', response);
}).fail(notification.exception); }).fail(notification.exception);
} }

View file

@ -124,7 +124,6 @@ export default {
}, },
methods: { methods: {
onHeaderHeightChange(newheight){ onHeaderHeightChange(newheight){
//console.info("Height change event to",newheight);
this.$refs.main.style.height = `${newheight}px`; this.$refs.main.style.height = `${newheight}px`;
} }
}, },

View file

@ -214,7 +214,7 @@ class studyitem {
$info = ['id' => $this->id,]; $info = ['id' => $this->id,];
foreach($editable as $f){ foreach($editable as $f){
if(array_key_exists($f,$fields)){ if(array_key_exists($f,$fields) && isset($fields[$f])){
$info[$f] = $fields[$f]; $info[$f] = $fields[$f];
} }
} }

View file

@ -1180,16 +1180,15 @@ class studyplanservice extends \external_api
} }
/************************ /************************
* * * *
* edit_period * * Change course timing *
* * * *
************************/ ************************/
public static function course_period_timing_parameters() public static function course_period_timing_parameters()
{ {
return new \external_function_parameters( [ return new \external_function_parameters( [
"page_id" => new \external_value(PARAM_INT, 'Studyplan page id'), "period_id" => new \external_value(PARAM_INT, 'Period number within page'),
"period" => new \external_value(PARAM_INT, 'Period number within page'),
"course_id"=> new \external_value(PARAM_INT, 'Id of course to adjust dates for'), "course_id"=> new \external_value(PARAM_INT, 'Id of course to adjust dates for'),
"span"=> new \external_value(PARAM_INT, 'Period span (default 1)',VALUE_DEFAULT), "span"=> new \external_value(PARAM_INT, 'Period span (default 1)',VALUE_DEFAULT),
]); ]);
@ -1197,43 +1196,75 @@ class studyplanservice extends \external_api
public static function course_period_timing_returns() public static function course_period_timing_returns()
{ {
return success::structure(); return courseinfo::editor_structure();
} }
public static function course_period_timing($page_id, $period, $course_id, $span=1){ public static function course_period_timing($period_id, $course_id, $span=1){
$course = \get_course($course_id); $period = period::findById($period_id);
$coursecontext = \context_course::instance($course_id); $periodnr = $period->period();
$page = studyplanpage::findById($page_id); $page = $period->page();
// Check for studyplan edit permissions // Check for studyplan edit permissions
webservicehelper::require_capabilities(self::CAP_EDIT,$page->studyplan()->context()); webservicehelper::require_capabilities(self::CAP_EDIT,$page->studyplan()->context());
$course = \get_course($course_id);
$coursecontext = \context_course::instance($course_id);
// Determine end period number if(webservicehelper::has_capabilities("moodle/course:update",$coursecontext)){
$endperiod = $period + ($span -1);
// Get the proper list of periods for this page
$periods = period::findForPage($page);
if(array_key_exists($period,$periods)){
$pstart = $periods[$period];
$pend = $periods[$endperiod];
if(webservicehelper::has_capabilities("moodle/course:update",$coursecontext)){ // Get the proper list of all the periods for this page
// Actually perform the timing changes, while also updating the module times $periods = period::findForPage($page);
// Like what happens on a course "reset"
$status = reset_course_userdata((object)[
'id' => $course->id,
'reset_start_date' => $pstart->startdate()->getTimestamp(),
'reset_end_date' => $pend->enddate()->getTimestamp(),
'reset_start_date_old' => $course->startdate,
'reset_end_date_old' => $course->enddate,
]); $pstart = $periods[$periodnr];
return success::success()->model(); // Determine end period number - Clip span between 1 and last period
} else { if($span <= 1){
// probably should return a nice message $pend = $pstart;
return success::fail("You do not have date change permissions on this course")->model();
} }
else if ($periodnr + ($span - 1) > $page->periods()){
$pend = $periods[$page->periods()];
}
else {
$pend = $periods[$periodnr + ($span - 1)];
}
// Actually perform the timing changes, while also updating the module times
// Like what happens on a course "reset"
reset_course_userdata((object)[
'id' => $course->id,
'reset_start_date' => $pstart->startdate()->getTimestamp(),
'reset_end_date' => $pend->enddate()->getTimestamp(),
'reset_start_date_old' => $course->startdate,
'reset_end_date_old' => $course->enddate,
]);
return (new courseinfo($course->id))->editor_model();
} else {
// probably should return a nice message
throw new \webservice_access_exception("You do not have date change permissions on this course");
} }
} }
public static function set_studyitem_span_parameters()
{
return new \external_function_parameters( [
"id" => new \external_value(PARAM_INT, 'id of study item'),
"span" => new \external_value(PARAM_INT, 'span of item'),
]);
}
public static function set_studyitem_span_returns()
{
return studyitem::editor_structure();
}
public static function set_studyitem_span($id,$span=null)
{
$o = studyitem::findById($id);
webservicehelper::require_capabilities(self::CAP_EDIT,$o->context());
$config = [ 'span' => $span];
$o->edit($config);
return $o->editor_model();
}
} }

View file

@ -70,7 +70,7 @@ class teachingfinder {
foreach($list as $page_id){ foreach($list as $page_id){
// Retrieve the studyplan id from the page // Retrieve the studyplan id from the page
//TODO: Change this when page management is implemented to return the page instead of the plan //TODO: Change this when page management is implemented to return the page instead of the plan
$planid = $DB->get_field("local_treestudyplan_page","studyplan_id",["page_id" => $page_id]); $planid = $DB->get_field("local_treestudyplan_page","studyplan_id",["id" => $page_id]);
$DB->insert_record(self::TABLE,["teacher_id"=>$userid,"studyplan_id"=>$planid,"update_time"=>$now]); $DB->insert_record(self::TABLE,["teacher_id"=>$userid,"studyplan_id"=>$planid,"update_time"=>$now]);
} }

View file

@ -542,4 +542,22 @@ $functions = [
'ajax' => true, 'ajax' => true,
'loginrequired' => true, 'loginrequired' => true,
], ],
'local_treestudyplan_course_period_timing' => [ //web service function name
'classname' => '\local_treestudyplan\studyplanservice', //class containing the external function
'methodname' => 'course_period_timing', //external function name
'description' => 'Chenge course start and end times to match period', //human readable description of the web service function
'type' => 'read', //database rights of the web service function (read, write)
'capabilities' => 'local/treestudyplan:editstudyplan', // Advises the admin which capabilities are required
'ajax' => true,
'loginrequired' => true,
],
'local_treestudyplan_set_studyitem_span' => [ //web service function name
'classname' => '\local_treestudyplan\studyplanservice', //class containing the external function
'methodname' => 'set_studyitem_span', //external function name
'description' => 'Change the span of a course item', //human readable description of the web service function
'type' => 'read', //database rights of the web service function (read, write)
'capabilities' => 'local/treestudyplan:editstudyplan', // Advises the admin which capabilities are required
'ajax' => true,
'loginrequired' => true,
],
]; ];

View file

@ -292,10 +292,13 @@ $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_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_desc"] = 'The start and end date of the course do not match the start and end date of it\'s period(s) in the studyplan.';
$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_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["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["period"] = 'Period';
$string["duration"] = 'Duration'; $string["duration"] = 'Duration';
$string["course_timing_rememberchoice"] = 'Remember my choice for future date mismatches'; $string["course_timing_rememberchoice"] = 'Remember my choice for future date mismatches';
$string["course_timing_hidewarning"] = 'Hide this warning next time'; $string["course_timing_hidewarning"] = 'Hide this warning next time';
$string["course_timing_ok"] = 'Course timing does matches period timing';
$string["course_timing_off"] = 'Course timing does not match period timing';
$string["course_period_span"] = 'Number of periods';

View file

@ -302,3 +302,6 @@ $string["period"] = 'Periode';
$string["duration"] = 'Duur'; $string["duration"] = 'Duur';
$string["course_timing_rememberchoice'"] = 'Onthoud mijn keuze voor toekomstige mismatches tussen cursus en periode'; $string["course_timing_rememberchoice'"] = 'Onthoud mijn keuze voor toekomstige mismatches tussen cursus en periode';
$string["course_timing_hidewarning"] = 'Hide this warning next time'; $string["course_timing_hidewarning"] = 'Hide this warning next time';
$string["course_timing_ok"] = 'Cursustiming en periodetiming komen overeen';
$string["course_timing_off"] = 'Cursustiming en periodetiming komen niet overeen';
$string["course_period_span"] = 'Aantal perioden';

View file

@ -1,6 +1,6 @@
<?php <?php
$plugin->component = 'local_treestudyplan'; // Recommended since 2.0.2 (MDL-26035). Required since 3.0 (MDL-48494) $plugin->component = 'local_treestudyplan'; // Recommended since 2.0.2 (MDL-26035). Required since 3.0 (MDL-48494)
$plugin->version = 2023080300; // YYYYMMDDHH (year, month, day, iteration) $plugin->version = 2023080500; // YYYYMMDDHH (year, month, day, iteration)
$plugin->requires = 2021051700; // YYYYMMDDHH (This is the release version for Moodle 3.11) $plugin->requires = 2021051700; // YYYYMMDDHH (This is the release version for Moodle 3.11)
$plugin->dependencies = [ $plugin->dependencies = [