Updated mform helpers. some work on full page support

This commit is contained in:
PMKuipers 2023-11-05 15:49:32 +01:00
parent 93e174967f
commit 702435566d
31 changed files with 807 additions and 364 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

View File

@ -1,3 +1,3 @@
define("local_treestudyplan/studyplan-processor",["exports"],(function(_exports){function ProcessStudyplan(studyplan){var connections={};for(var ip in studyplan.pages){var page=studyplan.pages[ip];for(var il in page.studylines){var line=page.studylines[il];for(var is in line.slots){var slot=line.slots[is];if(void 0!==slot.courses)for(var ic in slot.courses){var itm=slot.courses[ic];for(var idx in itm.connections.in){var conn=itm.connections.in[idx];conn.id in connections?itm.connections[idx]=connections[conn.id]:connections[conn.id]=conn}for(var _idx in itm.connections.out){var _conn=itm.connections.out[_idx];_conn.id in connections?itm.connections[_idx]=connections[_conn.id]:connections[_conn.id]=_conn}}if(void 0!==slot.filters)for(var ix in slot.filters){var _itm=slot.filters[ix];for(var _idx2 in _itm.connections.in){var _conn2=_itm.connections.in[_idx2];_conn2.id in connections?_itm.connections[_idx2]=connections[_conn2.id]:connections[_conn2.id]=_conn2}for(var _idx3 in _itm.connections.out){var _conn3=_itm.connections.out[_idx3];_conn3.id in connections?_itm.connections[_idx3]=connections[_conn3.id]:connections[_conn3.id]=_conn3}}}}}return studyplan}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.ProcessStudyplan=ProcessStudyplan,_exports.ProcessStudyplans=function(studyplans){for(var isx in studyplans){ProcessStudyplan(studyplans[isx])}return studyplans},_exports.objCopy=function(target,source,fields){for(var ix in fields){var field=fields[ix];target[field]=source[field]}},_exports.transportItem=function(target,source,identifier,param){param||(param="value");var item,itemindex;for(var ix in source)if(source[ix][param]==identifier){item=source[ix],itemindex=ix;break}item&&(target.push(item),source.splice(itemindex,1))}})); define("local_treestudyplan/studyplan-processor",["exports"],(function(_exports){function ProcessStudyplan(studyplan){for(var ip in studyplan.pages){ProcessStudyplanPage(studyplan.pages[ip])}return studyplan}function ProcessStudyplanPage(page){var connections={};for(var il in page.studylines){var line=page.studylines[il];for(var is in line.slots){var slot=line.slots[is];if(void 0!==slot.courses)for(var ic in slot.courses){var itm=slot.courses[ic];for(var idx in itm.connections.in){var conn=itm.connections.in[idx];conn.id in connections?itm.connections[idx]=connections[conn.id]:connections[conn.id]=conn}for(var _idx in itm.connections.out){var _conn=itm.connections.out[_idx];_conn.id in connections?itm.connections[_idx]=connections[_conn.id]:connections[_conn.id]=_conn}}if(void 0!==slot.filters)for(var ix in slot.filters){var _itm=slot.filters[ix];for(var _idx2 in _itm.connections.in){var _conn2=_itm.connections.in[_idx2];_conn2.id in connections?_itm.connections[_idx2]=connections[_conn2.id]:connections[_conn2.id]=_conn2}for(var _idx3 in _itm.connections.out){var _conn3=_itm.connections.out[_idx3];_conn3.id in connections?_itm.connections[_idx3]=connections[_conn3.id]:connections[_conn3.id]=_conn3}}}}return page}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.ProcessStudyplan=ProcessStudyplan,_exports.ProcessStudyplanPage=ProcessStudyplanPage,_exports.ProcessStudyplans=function(studyplans){for(var isx in studyplans){ProcessStudyplan(studyplans[isx])}return studyplans},_exports.objCopy=function(target,source,fields){for(var ix in fields){var field=fields[ix];target[field]=source[field]}},_exports.transportItem=function(target,source,identifier,param){param||(param="value");var item,itemindex;for(var ix in source)if(source[ix][param]==identifier){item=source[ix],itemindex=ix;break}item&&(target.push(item),source.splice(itemindex,1))}}));
//# sourceMappingURL=studyplan-processor.min.js.map //# sourceMappingURL=studyplan-processor.min.js.map

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

@ -1,3 +1,3 @@
define("local_treestudyplan/util/formfields",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.text_integer=function(id){var unsigned=arguments.length>1&&void 0!==arguments[1]&&arguments[1],element=document.getElementById(id);element&&element.addEventListener("input",(function(){var val=element.value;if(""!=val)return!(isNaN(val)&&(unsigned||"-"!=val)||unsigned&&val<0)||(element.value=unsigned?val.replace(/[^0-9]/g,""):val.replace(/[^0-9-]/g,"").replace(/(.{1})(-)/g,"$1"),!1)}))}})); define("local_treestudyplan/util/formfields",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.text_integer=function(id){let unsigned=arguments.length>1&&void 0!==arguments[1]&&arguments[1],nonzero=arguments.length>2&&void 0!==arguments[2]&&arguments[2];const element=document.getElementById(id);element&&element.addEventListener("input",(()=>{var val=element.value;if(""!=val)return!(isNaN(val)&&(unsigned||"-"!=val)||unsigned&&val<0||nonzero&&0==val)||(unsigned?(element.value=val.replace(/[^0-9]/g,""),nonzero&&0==val&&(element.value="")):element.value=val.replace(/[^0-9-]/g,"").replace(/(.{1})(-)/g,"$1"),!1)}))}}));
//# sourceMappingURL=formfields.min.js.map //# sourceMappingURL=formfields.min.js.map

View File

@ -1 +1 @@
{"version":3,"file":"formfields.min.js","sources":["../../src/util/formfields.js"],"sourcesContent":["/**\n * convert a text field into an integer only text field\n * @param {string} id The Id of the form field\n * @param {bool} unsigned Allow only unsigned values\n */\nexport function text_integer(id,unsigned=false){\n const element = document.getElementById(id);\n\n if (element) {\n element.addEventListener(\"input\",() => {\n var val = element.value;\n if (val != '') {\n if ((isNaN(val) && !(!unsigned && val == '-')) || (unsigned && val < 0)) {\n // Set input value empty\n if (unsigned) {\n element.value = val.replace(/[^0-9]/g,'');\n } else {\n element.value = val.replace(/[^0-9-]/g,'').replace(/(.{1})(-)/g,'$1');\n }\n return false;\n } else {\n return true;\n }\n }\n });\n }\n}"],"names":["id","unsigned","element","document","getElementById","addEventListener","val","value","isNaN","replace"],"mappings":"oKAK6BA,QAAGC,iEACtBC,QAAUC,SAASC,eAAeJ,IAEpCE,SACAA,QAAQG,iBAAiB,SAAQ,eACzBC,IAAMJ,QAAQK,SACP,IAAPD,YACKE,MAAMF,OAAWL,UAAmB,KAAPK,MAAiBL,UAAYK,IAAM,KAG7DJ,QAAQK,MADRN,SACgBK,IAAIG,QAAQ,UAAU,IAEtBH,IAAIG,QAAQ,WAAW,IAAIA,QAAQ,aAAa,OAE7D"} {"version":3,"file":"formfields.min.js","sources":["../../src/util/formfields.js"],"sourcesContent":["/**\n * convert a text field into an integer only text field\n * @param {string} id The Id of the form field\n * @param {bool} unsigned Allow only unsigned values\n * @param {bool} nonzero Do not allow zero values\n */\nexport function text_integer(id,unsigned=false,nonzero=false){\n const element = document.getElementById(id);\n\n if (element) {\n element.addEventListener(\"input\",() => {\n var val = element.value;\n if (val != '') {\n if ((isNaN(val) && !(!unsigned && val == '-')) || (unsigned && val < 0) || (nonzero && val == 0)) {\n // Set input value empty\n if (unsigned) {\n element.value = val.replace(/[^0-9]/g,'');\n if (nonzero && val == 0) {\n element.value = '';\n }\n } else {\n element.value = val.replace(/[^0-9-]/g,'').replace(/(.{1})(-)/g,'$1');\n }\n return false;\n } else {\n return true;\n }\n }\n });\n }\n}"],"names":["id","unsigned","nonzero","element","document","getElementById","addEventListener","val","value","isNaN","replace"],"mappings":"oKAM6BA,QAAGC,iEAAeC,sEACrCC,QAAUC,SAASC,eAAeL,IAEpCG,SACAA,QAAQG,iBAAiB,SAAQ,SACzBC,IAAMJ,QAAQK,SACP,IAAPD,YACKE,MAAMF,OAAWN,UAAmB,KAAPM,MAAiBN,UAAYM,IAAM,GAAOL,SAAkB,GAAPK,OAE/EN,UACAE,QAAQK,MAAQD,IAAIG,QAAQ,UAAU,IAClCR,SAAkB,GAAPK,MACXJ,QAAQK,MAAQ,KAGpBL,QAAQK,MAAQD,IAAIG,QAAQ,WAAW,IAAIA,QAAQ,aAAa,OAE7D"}

View File

@ -1,3 +1,3 @@
define("local_treestudyplan/util/mform-helper",["exports","core/ajax","core/fragment","core/templates","core/notification","./string-helper","./debugger"],(function(_exports,_ajax,_fragment,_templates,_notification,_stringHelper,_debugger){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_notification=_interopRequireDefault(_notification),_debugger=_interopRequireDefault(_debugger);var _default={install:function(Vue){var debug=new _debugger.default("treestudyplan-mform-helper"),strings=(0,_stringHelper.load_strings)({editmod:{save$core:"save$core",cancel$core:"cancel$core"}});Vue.component("mform",{props:{name:{type:String},params:{type:Object},title:{type:String,default:""},variant:{type:String,default:"primary"},type:{type:String,default:"link"}},data:function(){return{content:"",loading:!0,uuid:void 0!==crypto.randomUUID?crypto.randomUUID():"10000000-1000-4000-8000-100000000000".replace(/[018]/g,(function(c){return(c^crypto.getRandomValues(new Uint8Array(1))[0]&15>>c/4).toString(16)})),text:strings}},computed:{},methods:{openForm:function(){this.$refs.editormodal.show()},onShown:function(){var self=this;debug.info('Loading form "'.concat(self.name,'" with params'),self.params),self.loading=!1,(0,_ajax.call)([{methodname:"local_treestudyplan_get_mform",args:{formname:self.name,params:JSON.stringify(self.params)}}])[0].then((function(data){var html=data.html;self.loading=!1;var js=(0,_fragment.processCollectedJavascript)(data.javascript);(0,_templates.replaceNodeContents)(self.$refs.content,html,js)})).catch(_notification.default.exception)},onSave:function(){var self=this,form=this.$refs.content.getElementsByTagName("form")[0];form.dispatchEvent(new Event("save-form-state"));var formdata=new FormData(form),data=new URLSearchParams(formdata).toString();(0,_ajax.call)([{methodname:"local_treestudyplan_submit_mform",args:{formname:self.name,params:JSON.stringify(self.params),formdata:data}}])[0].then((function(response){var updatedplan=JSON.parse(response.data);self.$emit("saved",updatedplan,formdata)})).catch(_notification.default.exception)}},template:'\n <span class=\'mform-container\'>\n <b-button :variant="variant" v-if=\'type == "button"\' @click.prevent=\'openForm\'\n ><slot><i class=\'fa fa-gear\'></i></slot></b-button>\n <a variant="variant" v-else href=\'#\' @click.prevent=\'openForm\'\n ><slot><i class=\'fa fa-gear\'></i></slot></a>\n <b-modal\n ref="editormodal"\n scrollable\n centered\n size="xl"\n id="\'modal-\'+uuid"\n @shown="onShown"\n @ok="onSave"\n :title="title"\n :ok-title="text.save$core"\n ><div :class="\'s-mform-content\'" ref="content"\n ><div class="d-flex justify-content-center mb-3"\n ><b-spinner variant="primary"></b-spinner\n ></div\n ></div\n ></b-modal>\n </span>\n '})}};return _exports.default=_default,_exports.default})); define("local_treestudyplan/util/mform-helper",["exports","core/ajax","core/fragment","core/templates","core/notification","./string-helper","./debugger"],(function(_exports,_ajax,_fragment,_templates,_notification,_stringHelper,_debugger){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_notification=_interopRequireDefault(_notification),_debugger=_interopRequireDefault(_debugger);var _default={install(Vue){let debug=new _debugger.default("treestudyplan-mform-helper"),strings=(0,_stringHelper.load_strings)({editmod:{save$core:"save$core",cancel$core:"cancel$core"}});Vue.component("mform",{props:{name:{type:String},params:{type:Object},title:{type:String,default:""},variant:{type:String,default:"primary"},type:{type:String,default:"link"}},data:()=>({content:"",loading:!0,uuid:void 0!==crypto.randomUUID?crypto.randomUUID():"10000000-1000-4000-8000-100000000000".replace(/[018]/g,(c=>(c^crypto.getRandomValues(new Uint8Array(1))[0]&15>>c/4).toString(16))),text:strings,submitok:!1,observer:null,inputs:[]}),computed:{},methods:{openForm(){this.$refs.editormodal.show()},onShown(){const self=this;debug.info(`Loading form "${self.name}" with params`,self.params),self.loading=!1,(0,_ajax.call)([{methodname:"local_treestudyplan_get_mform",args:{formname:self.name,params:JSON.stringify(self.params)}}])[0].then((data=>{const html=data.html;self.loading=!1;const js=(0,_fragment.processCollectedJavascript)(data.javascript);(0,_templates.replaceNodeContents)(self.$refs.content,html,js),self.initListenChanges()})).catch(_notification.default.exception)},onSave(){const self=this;let form=this.$refs.content.getElementsByTagName("form")[0];form.dispatchEvent(new Event("save-form-state"));const formdata=new FormData(form),data=new URLSearchParams(formdata).toString();this.checkSave()&&(0,_ajax.call)([{methodname:"local_treestudyplan_submit_mform",args:{formname:self.name,params:JSON.stringify(self.params),formdata:data}}])[0].then((response=>{const updatedplan=JSON.parse(response.data);self.$emit("saved",updatedplan,formdata)})).catch(_notification.default.exception)},checkSave(){let canSave=!0;return this.inputs.forEach((el=>{el.classList.contains("is-invalid")&&(canSave=!1)}),this),this.submitok=canSave,canSave},initListenChanges(){const content=this.$refs.content;this.inputs=content.querySelectorAll("input.form-control"),this.checkSave(),this.observer&&this.observer.disconnect(),this.observer=new MutationObserver((mutationList=>{for(const mix in mutationList){const mutation=mutationList[mix];"attributes"===mutation.type&&"class"===mutation.attributeName&&this.checkSave()}})),this.inputs.forEach((el=>{this.observer.observe(el,{attributes:!0})}),this)}},unmount(){this.observer&&this.observer.disconnect()},template:'\n <span class=\'mform-container\'>\n <b-button :variant="variant" v-if=\'type == "button"\' @click.prevent=\'openForm\'\n ><slot><i class=\'fa fa-gear\'></i></slot></b-button>\n <a variant="variant" v-else href=\'#\' @click.prevent=\'openForm\'\n ><slot><i class=\'fa fa-gear\'></i></slot></a>\n <b-modal\n ref="editormodal"\n scrollable\n centered\n size="xl"\n id="\'modal-\'+uuid"\n @shown="onShown"\n @ok="onSave"\n :ok-disabled="!submitok"\n :title="title"\n :ok-title="text.save$core"\n ><div :class="\'s-mform-content\'" ref="content"\n ><div class="d-flex justify-content-center mb-3"\n ><b-spinner variant="primary"></b-spinner\n ></div\n ></div\n ></b-modal>\n </span>\n '})}};return _exports.default=_default,_exports.default}));
//# sourceMappingURL=mform-helper.min.js.map //# sourceMappingURL=mform-helper.min.js.map

File diff suppressed because one or more lines are too long

View File

@ -228,7 +228,7 @@ export function init(contextid,categoryid,options) {
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){
app.activestudyplan = ProcessStudyplan(response,true); app.activestudyplan = ProcessStudyplan(response);
debug.info('studyplan processed'); debug.info('studyplan processed');
app.loadingstudyplan = false; app.loadingstudyplan = false;
window.location.hash = app.activestudyplan.id; window.location.hash = app.activestudyplan.id;

View File

@ -15,7 +15,7 @@ import {format_date, add_days, datespaninfo } from './util/date-helper';
import {objCopy,transportItem} from './studyplan-processor'; import {objCopy,transportItem} from './studyplan-processor';
import Debugger from './util/debugger'; import Debugger from './util/debugger';
import {download,upload} from './downloader'; import {download,upload} from './downloader';
import {ProcessStudyplan} from './studyplan-processor'; import {ProcessStudyplan, ProcessStudyplanPage} from './studyplan-processor';
import TSComponents from './treestudyplan-components'; import TSComponents from './treestudyplan-components';
import mFormComponents from "./util/mform-helper"; import mFormComponents from "./util/mform-helper";
@ -570,6 +570,71 @@ export default {
` `
}); });
/*
* T-STUDYPLAN-EDIT
*/
Vue.component('t-studyplan-page-edit', {
props: {
'value' :{
type: Object,
default(){ return null;},
},
'mode' :{
type: String,
default() { return "edit";},
},
'type' :{
type: String,
default() { return "link";},
},
'variant' : {
type: String,
default() { return "";},
},
'studyplan': {
type: Object,
},
},
data() {
return {
text: strings.studyplan_edit,
};
},
computed: {
},
methods: {
planSaved(updatedpage){
const self = this;
debug.info("Got new page data",updatedpage);
if(self.mode == 'create'){
// Inform parent of the details of the newly created plan
self.$emit("created",updatedpage);
}
else {
const page = ProcessStudyplanPage(updatedpage);
debug.info('studyplan page processed');
self.$emit('input',page);
}
},
}
,
template:
`
<span class='s-studyplan-page-edit'>
<mform
name="studyplanpage_editform"
:params="{page_id: value.id, studyplan_id: studyplan.id, mode: mode }"
@saved="planSaved"
:variant="variant"
:type="type"
:title="(mode == 'create')?text.studyplan_add:text.studyplan_edit"
><slot><i class='fa fa-gear'></i></slot></mform>
</span>
`
});
/* /*
* T-STUDYPLAN-ASSOCIATE * T-STUDYPLAN-ASSOCIATE
*/ */
@ -1032,6 +1097,7 @@ export default {
` `
}); });
// TAG: Start studyplan component
/* /*
* T-STUDYPLAN * T-STUDYPLAN
*/ */
@ -1057,6 +1123,11 @@ export default {
'shortname': '', 'shortname': '',
'color': '#DDDDDD', 'color': '#DDDDDD',
}, },
page: {
'id': -1,
'name' : '',
'shortname' : ''
}
}, },
edit: { edit: {
toolbox_shown: false, toolbox_shown: false,
@ -1091,14 +1162,14 @@ export default {
text: strings.studyplan_text, text: strings.studyplan_text,
cache: { cache: {
linelayers: {}, linelayers: {},
} },
}; };
}, },
created() { created() {
}, },
mounted() { mounted() {
if(this.page.studylines.length == 0){ if(this.value.pages[0].studylines.length == 0){
// start in editmode if studylines are empty // start in editmode if studylines are empty
this.edit.studyline.editmode = true; this.edit.studyline.editmode = true;
} }
@ -1109,23 +1180,20 @@ export default {
ItemEventBus.$emit('redrawLines'); ItemEventBus.$emit('redrawLines');
}, },
computed: { computed: {
columns() {
return 1+ (this.page.periods * 2); },
methods: {
columns(page) {
return 1+ (page.periods * 2);
}, },
columns_stylerule() { columns_stylerule(page) {
// Uses css variables, so width for slots and filters can be configured in css // 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 let s = "grid-template-columns: var(--studyplan-filter-width)"; // use css variable here
for(let i=0; i<this.page.periods;i++){ for(let i=0; i<page.periods;i++){
s+= " var(--studyplan-course-width) var(--studyplan-filter-width)"; s+= " var(--studyplan-course-width) var(--studyplan-filter-width)";
} }
return s+";"; return s+";";
}, },
page(){
// FIXME: Temporary hack until real page management is implemented
return this.value.pages[0];
}
},
methods: {
trashbin_accepts(type){ trashbin_accepts(type){
if(type.item){ if(type.item){
return true; return true;
@ -1133,7 +1201,7 @@ export default {
return false; return false;
} }
}, },
countLineLayers(line){ countLineLayers(line,page){
// For some optimization, we cache the value of this calculation for about a second // For some optimization, we cache the value of this calculation for about a second
// Would be a lot nicer if we could use a computed property for this..... // Would be a lot nicer if we could use a computed property for this.....
if( this.cache.linelayers[line.id] if( this.cache.linelayers[line.id]
@ -1144,7 +1212,7 @@ export default {
else else
{ {
let maxLayer = -1; let maxLayer = -1;
for(let i = 0; i <= this.page.periods; i++){ for(let i = 0; i <= page.periods; i++){
if(line.slots[i]){ if(line.slots[i]){
const slot = line.slots[i]; const slot = line.slots[i];
// Determine the amount of used layers in a studyline slit // Determine the amount of used layers in a studyline slit
@ -1303,11 +1371,11 @@ export default {
}).fail(notification.exception); }).fail(notification.exception);
}, },
showslot(line,index, layeridx, type){ showslot(page,line,index, layeridx, type){
// check if the slot should be hidden because a previous slot has an item with a span // check if the slot should be hidden because a previous slot has an item with a span
// so big that it hides this slot // so big that it hides this slot
const forGradable = (type == 'gradable')?true:false; const forGradable = (type == 'gradable')?true:false;
const periods = this.page.periods; const periods = page.periods;
let show = true; let show = true;
for(let i = 0; i < periods; i++){ for(let i = 0; i < periods; i++){
if(line.slots[index-i] && line.slots[index-i].courses){ if(line.slots[index-i] && line.slots[index-i].courses){
@ -1355,6 +1423,9 @@ export default {
}, },
toolbox_switched(event){ toolbox_switched(event){
this.$emit('toggletoolbox',event); this.$emit('toggletoolbox',event);
},
pagecreated(page) {
this.value.pages.push(page);
} }
} }
, ,
@ -1390,193 +1461,219 @@ export default {
><i class='fa fa-gear'></i> {{text.edit$core}}</t-studyplan-edit> ><i class='fa fa-gear'></i> {{text.edit$core}}</t-studyplan-edit>
</span> </span>
<span class='control deletable'> <span class='control deletable'>
<a v-if='page.studylines.length == 0' href='#' @click='deletePlan(value)' <a v-if='value.pages.length == 0' href='#' @click='deletePlan(value)'
><i class='text-danger fa fa-trash'></i></a> ><i class='text-danger fa fa-trash'></i></a>
</span> </span>
</div> </div>
</div> </div>
<div class='t-studyplan-content-edit' v-if="edit.studyline.editmode"> <b-tabs content-class="mt-1">
<drop-list <!-- New Tab Button (Using tabs-end slot) -->
:items="page.studylines" <template #tabs-end>
class="t-slot-droplist" <t-studyplan-page-edit
:accepts-type="'studyline-'+page.id" :studyplan="value"
xreorder="$event.apply(page.studylines)" v-model="create.page"
@reorder="reorderLines($event,page.studylines)" type="link"
mode="copy" mode="create"
row @created="pagecreated"
> ><i class='fa fa-plus'></i></t-studyplan-page-edit>
<template v-slot:item="{item}"> </template>
<drag <b-tab
:key="item.id" v-for="(page,pageindex) in value.pages"
class='t-studyline-drag' :key="page.id"
:data="item" >
:type="'studyline-'+page.id" <template #title>
> {{page.shortname}}
<template v-slot:drag-image> <t-studyplan-page-edit
<i class="fa fa-arrows text-primary"></i> v-model="value.pages[pageindex]"
</template> :studyplan="value"
<t-studyline-edit type="link"
v-model="item" ></t-studyplan-page-edit>
@edit='editLineStart(item)'
@delete='deleteLine(page,item)'
>
<div v-if="!slotsempty(item.slots)"> {{ text.editmode_modules_hidden}} </div>
</t-studyline-edit>
</drag>
</template> </template>
</drop-list> <div class='t-studyplan-content-edit' v-if="edit.studyline.editmode">
</div> <drop-list
<div class='t-studyplan-content' v-else> :items="page.studylines"
class="t-slot-droplist"
<!-- Now paint the headings column --> :accepts-type="'studyline-'+page.id"
<div class='t-studyplan-headings'> xreorder="$event.apply(page.studylines)"
<s-studyline-header-heading></s-studyline-header-heading> @reorder="reorderLines($event,page.studylines)"
<t-studyline-heading v-for="(line,lineindex) in page.studylines" mode="copy"
:key="line.id" row
@resize="headingresized(lineindex,$event)" >
v-model="page.studylines[lineindex]" <template v-slot:item="{item}">
:layers='countLineLayers(line)+1' <drag
:class=" 't-studyline' + ((lineindex%2==0)?' odd ' :' even ' ) :key="item.id"
+ ((lineindex==0)?' first ':' ') class='t-studyline-drag'
+ ((lineindex==page.studylines.length-1)?' last ':' ')" :data="item"
></t-studyline-heading> :type="'studyline-'+page.id"
</div>
<!-- Next, paint all the cells in the scrollable -->
<div class="t-studyplan-scrollable" >
<div class="t-studyplan-timeline" :style="columns_stylerule">
<!-- add period information -->
<template v-for="(n,index) in (page.periods+1)">
<s-studyline-header-period
v-if="index > 0"
v-model="page.perioddesc[index-1]"
><t-period-edit
:ref="'periodeditor-'+index"
@edited="periodEdited"
v-model="page.perioddesc[index-1]"
:minstart="(index > 1) ? add_day(page.perioddesc[index-2].startdate,2) : null"
:maxend="(index < page.periods) ? sub_day(page.perioddesc[index].enddate,2) : null"
></t-period-edit
></s-studyline-header-period>
<div class="s-studyline-header-filter"></div>
</template>
<!-- Line by line add the items -->
<!-- The grid layout handles putting it in rows and columns -->
<template v-for="(line,lineindex) in page.studylines"
><template v-for="(layernr,layeridx) in countLineLayers(line)+1"
><template v-for="(n,index) in (page.periods+1)"
>
<t-studyline-slot
v-if="index > 0 && showslot(line, index, layeridx, 'gradable')"
type='gradable'
v-model="line.slots[index].courses"
:key="'c-'+lineindex+'-'+index+'-'+layernr"
:slotindex="index"
:line="line"
:plan="value"
:page="page"
:period="page.perioddesc[index-1]"
:layer="layeridx"
:class="'t-studyline ' + ((lineindex%2==0)?' odd ':' even ')
+ ((lineindex==0 && layernr==1)?' first ':' ')
+ ((lineindex==page.studylines.length-1)?' last ':' ')
+ ((layernr == countLineLayers(line))?' lastlyr ':' ')
+ ((layernr == countLineLayers(line)+1)?' newlyr ':' ')"
></t-studyline-slot
><t-studyline-slot
type='filter'
v-if="showslot(line, index, layeridx, 'filter')"
v-model="line.slots[index].filters"
:key="'f-'+lineindex+'-'+index+'-'+layernr"
:slotindex="index"
:line="line"
:plan="value"
:page="page"
:layer="layeridx"
:class="'t-studyline ' + ((lineindex%2==0)?' odd ':' even ')
+ ((lineindex==0 && layernr==1)?' first ':'')
+ ((lineindex==page.studylines.length-1)?' last ':' ')
+ ((index==page.periods)?' rightmost':'')
+ ((layernr == countLineLayers(line))?' lastlyr ':' ')
+ ((layernr == countLineLayers(line)+1)?' newlyr ':' ')"
> >
</t-studyline-slot <template v-slot:drag-image>
<i class="fa fa-arrows text-primary"></i>
</template>
<t-studyline-edit
v-model="item"
@edit='editLineStart(item)'
@delete='deleteLine(page,item)'
>
<div v-if="!slotsempty(item.slots)"> {{ text.editmode_modules_hidden}} </div>
</t-studyline-edit>
</drag>
</template>
</drop-list>
</div>
<div class='t-studyplan-content' v-else>
<!-- Now paint the headings column -->
<div class='t-studyplan-headings'>
<s-studyline-header-heading :identifier='Number(page.id)'></s-studyline-header-heading>
<t-studyline-heading v-for="(line,lineindex) in page.studylines"
:key="line.id"
@resize="headingresized(lineindex,$event)"
v-model="page.studylines[lineindex]"
:layers='countLineLayers(line,page)+1'
:class=" 't-studyline' + ((lineindex%2==0)?' odd ' :' even ' )
+ ((lineindex==0)?' first ':' ')
+ ((lineindex==page.studylines.length-1)?' last ':' ')"
></t-studyline-heading>
</div>
<!-- Next, paint all the cells in the scrollable -->
<div class="t-studyplan-scrollable" >
<div class="t-studyplan-timeline" :style="columns_stylerule(page)">
<!-- add period information -->
<template v-for="(n,index) in (page.periods+1)">
<s-studyline-header-period
:identifier='Number(page.id)'
v-if="index > 0"
v-model="page.perioddesc[index-1]"
><t-period-edit
:ref="'periodeditor-'+index"
@edited="periodEdited"
v-model="page.perioddesc[index-1]"
:minstart="(index > 1) ? add_day(page.perioddesc[index-2].startdate,2) : null"
:maxend="(index < page.periods) ? sub_day(page.perioddesc[index].enddate,2) : null"
></t-period-edit
></s-studyline-header-period>
<div class="s-studyline-header-filter"></div>
</template>
<!-- Line by line add the items -->
<!-- The grid layout handles putting it in rows and columns -->
<template v-for="(line,lineindex) in page.studylines"
><template v-for="(layernr,layeridx) in countLineLayers(line,page)+1"
><template v-for="(n,index) in (page.periods+1)"
>
<t-studyline-slot
v-if="index > 0 && showslot(page,line, index, layeridx, 'gradable')"
type='gradable'
v-model="line.slots[index].courses"
:key="'c-'+lineindex+'-'+index+'-'+layernr"
:slotindex="index"
:line="line"
:plan="value"
:page="page"
:period="page.perioddesc[index-1]"
:layer="layeridx"
:class="'t-studyline ' + ((lineindex%2==0)?' odd ':' even ')
+ ((lineindex==0 && layernr==1)?' first ':' ')
+ ((lineindex==page.studylines.length-1)?' last ':' ')
+ ((layernr == countLineLayers(line,page))?' lastlyr ':' ')
+ ((layernr == countLineLayers(line,page)+1)?' newlyr ':' ')"
></t-studyline-slot
><t-studyline-slot
type='filter'
v-if="showslot(page,line, index, layeridx, 'filter')"
v-model="line.slots[index].filters"
:key="'f-'+lineindex+'-'+index+'-'+layernr"
:slotindex="index"
:line="line"
:plan="value"
:page="page"
:layer="layeridx"
:class="'t-studyline ' + ((lineindex%2==0)?' odd ':' even ')
+ ((lineindex==0 && layernr==1)?' first ':'')
+ ((lineindex==page.studylines.length-1)?' last ':' ')
+ ((index==page.periods)?' rightmost':'')
+ ((layernr == countLineLayers(line,page))?' lastlyr ':' ')
+ ((layernr == countLineLayers(line,page)+1)?' newlyr ':' ')"
>
</t-studyline-slot
></template
></template
></template ></template
></template ></div>
></template </div>
></div> </div>
</div> <div v-if="edit.studyline.editmode" class='t-studyline-add'>
</div> <a href="#" v-b-modal="'modal-add-studyline-'+page.id" @click="false;"
<div v-if="edit.studyline.editmode" class='t-studyline-add'> ><i class='fa fa-plus'></i>{{ text.studyline_add }}</a>
<a href="#" v-b-modal="'modal-add-studyline-'+page.id" @click="false;" </div>
><i class='fa fa-plus'></i>{{ text.studyline_add }}</a> <b-modal
</div> :id="'modal-add-studyline-'+page.id"
<b-modal size="lg"
:id="'modal-add-studyline-'+page.id" :ok-title="text.add$core"
size="lg" ok-variant="primary"
:ok-title="text.add$core" :title="text.studyline_add"
ok-variant="primary" @ok="addStudyLine(page,create.studyline)"
:title="text.studyline_add" :ok-disabled="Math.min(create.studyline.name.length,create.studyline.shortname.length) == 0"
@ok="addStudyLine(page,create.studyline)" >
:ok-disabled="Math.min(create.studyline.name.length,create.studyline.shortname.length) == 0" <b-container>
> <b-row>
<b-container> <b-col cols="3">{{text.studyline_name}}</b-col>
<b-row> <b-col>
<b-col cols="3">{{text.studyline_name}}</b-col> <b-form-input v-model="create.studyline.name" :placeholder="text.studyline_name_ph"></b-form-input>
<b-col> </b-col>
<b-form-input v-model="create.studyline.name" :placeholder="text.studyline_name_ph"></b-form-input> </b-row>
</b-col> <b-row>
</b-row> <b-col cols="3">{{text.studyline_shortname}}</b-col>
<b-row> <b-col>
<b-col cols="3">{{text.studyline_shortname}}</b-col> <b-form-input
<b-col> v-model="create.studyline.shortname"
<b-form-input :placeholder="text.studyline_shortname_ph"></b-form-input>
v-model="create.studyline.shortname" </b-col>
:placeholder="text.studyline_shortname_ph"></b-form-input> </b-row>
</b-col> <b-row>
</b-row> <b-col cols="3">{{text.studyline_color}}</b-col>
<b-row> <b-col>
<b-col cols="3">{{text.studyline_color}}</b-col> <input type="color" v-model="create.studyline.color" />
<b-col> <!-- hsluv-picker v-model="create.studyline.color" horizontal displaysize="175" ></hsluv-picker -->
<input type="color" v-model="create.studyline.color" /> </b-col>
<!-- hsluv-picker v-model="create.studyline.color" horizontal displaysize="175" ></hsluv-picker --> </b-row>
</b-col> </b-container>
</b-row> </b-modal>
</b-container> <b-modal
</b-modal> :id="'modal-edit-studyline-'+page.id"
<b-modal size="lg"
:id="'modal-edit-studyline-'+value.id" ok-variant="primary"
size="lg" :title="text.studyline_edit"
ok-variant="primary" @ok="editLineFinish()"
:title="text.studyline_edit" :ok-disabled="Math.min(edit.studyline.data.name.length,edit.studyline.data.shortname.length) == 0"
@ok="editLineFinish()" >
:ok-disabled="Math.min(edit.studyline.data.name.length,edit.studyline.data.shortname.length) == 0" <b-container>
> <b-row>
<b-container> <b-col cols="3">{{ text.studyline_name}}</b-col>
<b-row> <b-col>
<b-col cols="3">{{ text.studyline_name}}</b-col> <b-form-input
<b-col> v-model="edit.studyline.data.name"
<b-form-input :placeholder="text.studyline_name_ph"></b-form-input>
v-model="edit.studyline.data.name" </b-col>
:placeholder="text.studyline_name_ph"></b-form-input> </b-row>
</b-col> <b-row>
</b-row> <b-col cols="3">{{ text.studyline_shortname}}</b-col>
<b-row> <b-col>
<b-col cols="3">{{ text.studyline_shortname}}</b-col> <b-form-input
<b-col> v-model="edit.studyline.data.shortname"
<b-form-input :placeholder="text.studyline_shortname_ph"></b-form-input>
v-model="edit.studyline.data.shortname" </b-col>
:placeholder="text.studyline_shortname_ph"></b-form-input> </b-row>
</b-col> <b-row>
</b-row> <b-col cols="3">{{ text.studyline_color}}</b-col>
<b-row> <b-col>
<b-col cols="3">{{ text.studyline_color}}</b-col> <input type="color" v-model="edit.studyline.data.color" />
<b-col> </b-col>
<input type="color" v-model="edit.studyline.data.color" /> </b-row>
</b-col> </b-container>
</b-row> </b-modal>
</b-container> </b-tab>
</b-modal> </b-tabs>
</div> </div>
` `
}); });

View File

@ -63,66 +63,77 @@ export function ProcessStudyplans(studyplans){
* @returns Processed studyplan * @returns Processed studyplan
*/ */
export function ProcessStudyplan(studyplan){ export function ProcessStudyplan(studyplan){
let connections = {};
for(const ip in studyplan.pages){ for(const ip in studyplan.pages){
const page = studyplan.pages[ip]; const page = studyplan.pages[ip];
for(const il in page.studylines) { ProcessStudyplanPage(page);
const line = page.studylines[il]; }
return studyplan;
}
for(const is in line.slots ) { /**
const slot = line.slots[is]; * Perform initial processing on a downloaded studyplan'page
* Mainly used to create the proper references between items
* @param {Object} page The studyplan page to process
* @returns Processed studyplan
*/
export function ProcessStudyplanPage(page){
let connections = {};
for(const il in page.studylines) {
const line = page.studylines[il];
if(slot.courses !== undefined){ for(const is in line.slots ) {
for(const ic in slot.courses){ const slot = line.slots[is];
const itm = slot.courses[ic];
for(const idx in itm.connections.in) { if(slot.courses !== undefined){
const conn = itm.connections.in[idx]; for(const ic in slot.courses){
const itm = slot.courses[ic];
if(conn.id in connections){ for(const idx in itm.connections.in) {
itm.connections[idx] = connections[conn.id]; const conn = itm.connections.in[idx];
} else {
connections[conn.id] = conn; if(conn.id in connections){
} itm.connections[idx] = connections[conn.id];
} else {
connections[conn.id] = conn;
} }
for(const idx in itm.connections.out) { }
const conn = itm.connections.out[idx]; for(const idx in itm.connections.out) {
const conn = itm.connections.out[idx];
if(conn.id in connections){ if(conn.id in connections){
itm.connections[idx] = connections[conn.id]; itm.connections[idx] = connections[conn.id];
} else { } else {
connections[conn.id] = conn; connections[conn.id] = conn;
}
} }
} }
} }
}
if(slot.filters !== undefined){ if(slot.filters !== undefined){
for(const ix in slot.filters){ for(const ix in slot.filters){
const itm = slot.filters[ix]; const itm = slot.filters[ix];
for(const idx in itm.connections.in) { for(const idx in itm.connections.in) {
const conn = itm.connections.in[idx]; const conn = itm.connections.in[idx];
if(conn.id in connections){ if(conn.id in connections){
itm.connections[idx] = connections[conn.id]; itm.connections[idx] = connections[conn.id];
} else { } else {
connections[conn.id] = conn; connections[conn.id] = conn;
}
} }
for(const idx in itm.connections.out) { }
const conn = itm.connections.out[idx]; for(const idx in itm.connections.out) {
const conn = itm.connections.out[idx];
if(conn.id in connections){ if(conn.id in connections){
itm.connections[idx] = connections[conn.id]; itm.connections[idx] = connections[conn.id];
} else { } else {
connections[conn.id] = conn; connections[conn.id] = conn;
}
} }
} }
} }
} }
} }
} }
return studyplan; return page;
} }

View File

@ -170,7 +170,10 @@ export default {
*/ */
Vue.component('s-studyline-header-heading', { Vue.component('s-studyline-header-heading', {
props: { props: {
identifier: {
type: Number, // Page reference.
default() { return 0;}
}
}, },
data() { data() {
return { return {
@ -186,14 +189,16 @@ export default {
}, },
methods: { methods: {
onHeaderHeightChange(newheight){ onHeaderHeightChange(newheight,identifier){
if(this.$refs.main){ if (this.identifier == identifier) {
this.$refs.main.style.height = `${newheight}px`; if(this.$refs.main){
this.$refs.main.style.height = `${newheight}px`;
}
} }
} }
}, },
template: ` template: `
<div class="s-studyline-header-heading" ref="main"></div> <div class="s-studyline-header-heading" ref="main" :data-id="identifier"></div>
`, `,
}); });
@ -202,6 +207,10 @@ export default {
value: { value: {
type: Object, // dict with layer as index type: Object, // dict with layer as index
}, },
identifier: {
type: Number, // Page reference.
default() { return 0;}
}
}, },
mounted() { mounted() {
const self=this; const self=this;
@ -209,7 +218,7 @@ export default {
self.resizeListener = new ResizeObserver(() => { self.resizeListener = new ResizeObserver(() => {
if(self.$refs.main){ if(self.$refs.main){
const size = self.$refs.main.getBoundingClientRect(); const size = self.$refs.main.getBoundingClientRect();
ItemEventBus.$emit('headerHeightChange', size.height); ItemEventBus.$emit('headerHeightChange', size.height, self.identifier);
} }
}).observe(self.$refs.main); }).observe(self.$refs.main);
} }
@ -244,7 +253,7 @@ export default {
}; };
}, },
template: ` template: `
<div :class="'s-studyline-header-period ' + (current?'current ':' ')" ref="main" <div :class="'s-studyline-header-period ' + (current?'current ':' ')" ref="main" :data-id="identifier"
><p><abbr :id="'s-period-'+value.id" :title="value.fullname">{{ value.shortname }}</abbr> ><p><abbr :id="'s-period-'+value.id" :title="value.fullname">{{ value.shortname }}</abbr>
<b-tooltip <b-tooltip
:target="'s-period-'+value.id" triggers="hover" :target="'s-period-'+value.id" triggers="hover"

View File

@ -2,18 +2,22 @@
* convert a text field into an integer only text field * convert a text field into an integer only text field
* @param {string} id The Id of the form field * @param {string} id The Id of the form field
* @param {bool} unsigned Allow only unsigned values * @param {bool} unsigned Allow only unsigned values
* @param {bool} nonzero Do not allow zero values
*/ */
export function text_integer(id,unsigned=false){ export function text_integer(id,unsigned=false,nonzero=false){
const element = document.getElementById(id); const element = document.getElementById(id);
if (element) { if (element) {
element.addEventListener("input",() => { element.addEventListener("input",() => {
var val = element.value; var val = element.value;
if (val != '') { if (val != '') {
if ((isNaN(val) && !(!unsigned && val == '-')) || (unsigned && val < 0)) { if ((isNaN(val) && !(!unsigned && val == '-')) || (unsigned && val < 0) || (nonzero && val == 0)) {
// Set input value empty // Set input value empty
if (unsigned) { if (unsigned) {
element.value = val.replace(/[^0-9]/g,''); element.value = val.replace(/[^0-9]/g,'');
if (nonzero && val == 0) {
element.value = '';
}
} else { } else {
element.value = val.replace(/[^0-9-]/g,'').replace(/(.{1})(-)/g,'$1'); element.value = val.replace(/[^0-9-]/g,'').replace(/(.{1})(-)/g,'$1');
} }

View File

@ -65,16 +65,19 @@ export default {
loading: true, loading: true,
uuid: create_uuid(), uuid: create_uuid(),
text: strings, text: strings,
submitok: false,
observer: null,
inputs: [],
}; };
}, },
computed: { computed: {
}, },
methods: { methods: {
openForm(){ openForm() {
const self = this; const self = this;
self.$refs["editormodal"].show(); self.$refs["editormodal"].show();
}, },
onShown(){ onShown() {
const self = this; const self = this;
debug.info(`Loading form "${self.name}" with params`,self.params); debug.info(`Loading form "${self.name}" with params`,self.params);
self.loading = false; self.loading = false;
@ -87,11 +90,11 @@ export default {
// Process the collected javascript; // Process the collected javascript;
const js = processCollectedJavascript(data.javascript); const js = processCollectedJavascript(data.javascript);
replaceNodeContents(self.$refs["content"], html, js); replaceNodeContents(self.$refs["content"], html, js);
self.initListenChanges();
}).catch(notification.exception); }).catch(notification.exception);
}, },
onSave(){ onSave() {
const self = this; const self = this;
let form = this.$refs["content"].getElementsByTagName("form")[0]; let form = this.$refs["content"].getElementsByTagName("form")[0];
@ -104,13 +107,59 @@ export default {
const formdata = new FormData(form); const formdata = new FormData(form);
const data = new URLSearchParams(formdata).toString(); const data = new URLSearchParams(formdata).toString();
call([{ if(this.checkSave()){
methodname: 'local_treestudyplan_submit_mform', call([{
args: {formname: self.name, params: JSON.stringify(self.params), formdata: data} methodname: 'local_treestudyplan_submit_mform',
}])[0].then((response)=>{ args: {formname: self.name, params: JSON.stringify(self.params), formdata: data}
const updatedplan = JSON.parse(response.data); }])[0].then((response)=>{
self.$emit("saved",updatedplan,formdata); const updatedplan = JSON.parse(response.data);
}).catch(notification.exception); self.$emit("saved",updatedplan,formdata);
}).catch(notification.exception);
}
/* No error if we cannot save, since it would just be to handle the edge case
where someone clicks on the save button before
an invalid input got a chance to update. */
},
checkSave() {
let canSave = true;
this.inputs.forEach(el => {
if (el.classList.contains("is-invalid")) {
canSave = false;
}
},this);
this.submitok = canSave;
return canSave;
},
initListenChanges() {
const content = this.$refs["content"];
this.inputs = content.querySelectorAll("input.form-control");
// Check if save needs to be blocked immediately.
this.checkSave();
// Disconnect any existing observer.
if(this.observer) {
this.observer.disconnect();
}
// Initialize new observer and callback.
this.observer = new MutationObserver((mutationList) => {
for(const mix in mutationList){
const mutation = mutationList[mix];
if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
this.checkSave();
}
}
});
// Connect the observer to the form inputs.
this.inputs.forEach(el => {
this.observer.observe(el,{ attributes: true });
},this);
},
},
unmount() {
if(this.observer) {
this.observer.disconnect();
} }
}, },
template: ` template: `
@ -127,6 +176,7 @@ export default {
id="'modal-'+uuid" id="'modal-'+uuid"
@shown="onShown" @shown="onShown"
@ok="onSave" @ok="onSave"
:ok-disabled="!submitok"
:title="title" :title="title"
:ok-title="text.save$core" :ok-title="text.save$core"
><div :class="'s-mform-content'" ref="content" ><div :class="'s-mform-content'" ref="content"

View File

@ -36,6 +36,10 @@ class contextinfo {
*/ */
public function __construct(context $context) { public function __construct(context $context) {
$this->context = $context; $this->context = $context;
$this->ctxpath = array_reverse($this->context->get_parent_context_ids(true));
if (count($this->ctxpath) > 1 && $this->ctxpath[0] == 1) {
array_shift($this->ctxpath);
}
} }
/** /**
@ -57,22 +61,42 @@ class contextinfo {
*/ */
public function model() { public function model() {
$ctxpath = array_reverse($this->context->get_parent_context_ids(true));
if (count($ctxpath) > 1 && $ctxpath[0] == 1) {
array_shift($ctxpath);
}
return [ return [
"name" => $this->context->get_context_name(false, false), "name" => $this->context->get_context_name(false, false),
"shortname" => $this->context->get_context_name(false, true), "shortname" => $this->context->get_context_name(false, true),
"path" => array_map(function($c) { "path" => $this->path(false),
return \context::instance_by_id($c)->get_context_name(false, false); "shortpath" => $this->path(true),
}, $ctxpath),
"shortpath" => array_map(function($c) {
return \context::instance_by_id($c)->get_context_name(false, true);
}, $ctxpath),
]; ];
} }
/**
* Return context path names
* @param $short Use short names of contexts in path
* @return array of context path names
*/
public function path($short=false){
if ($short) {
return array_map(function($c) {
return \context::instance_by_id($c)->get_context_name(false, true);
}, $this->ctxpath);
} else {
return array_map(function($c) {
return \context::instance_by_id($c)->get_context_name(false, false);
}, $this->ctxpath);
}
}
/**
* Return full context path name
* @param $short Use short names of contexts in path
* @return string Concatenated string of paths
*/
public function pathstr($short=false){
return implode(" / ", $this->path($short));
}
/** /**
* Make new Contextinfo for context id * Make new Contextinfo for context id
* @param int $contextid Context id * @param int $contextid Context id

View File

@ -4,6 +4,7 @@ namespace local_treestudyplan\form;
use local_treestudyplan\aggregator; use local_treestudyplan\aggregator;
use local_treestudyplan\studyplan; use local_treestudyplan\studyplan;
use local_treestudyplan\studyplanservice;
use local_treestudyplan\courseservice; use local_treestudyplan\courseservice;
use local_treestudyplan\form\text_integer; use local_treestudyplan\form\text_integer;
use local_treestudyplan\local\helpers\webservicehelper; use local_treestudyplan\local\helpers\webservicehelper;
@ -189,24 +190,28 @@ class studyplan_editform extends formbase {
$customdata->fileoptions $customdata->fileoptions
); );
$field = 'startdate'; if ($customdata->create) {
$mform->addElement('date_selector',$field, // Only add these fields if a new studyplan is created, to easily initialize the first page
get_string('studyplan_startdate','local_treestudyplan'), $field = 'startdate';
[]); $mform->addElement('date_selector',$field,
$mform->addRule($field, null, 'required', null, 'client'); get_string('studyplan_startdate','local_treestudyplan'),
[]);
$mform->addRule($field, null, 'required', null, 'client');
$field = 'enddate'; $field = 'enddate';
$mform->addElement('date_selector',$field, $mform->addElement('date_selector',$field,
get_string('studyplan_startdate','local_treestudyplan'), get_string('studyplan_startdate','local_treestudyplan'),
[]); []);
$mform->addRule($field, null, 'required', null, 'client'); $mform->addRule($field, null, 'required', null, 'client');
$field = 'periods'; $field = 'periods';
$mform->addElement('text_integer',$field, $mform->addElement('text_integer',$field,
get_string('studyplan_slots','local_treestudyplan'), get_string('studyplan_slots','local_treestudyplan'),
["unsigned" => true]); ["unsigned" => true]);
$mform->setType($field, PARAM_INT); $mform->setType($field, PARAM_INT);
$mform->addRule($field, null, 'required', null, 'client'); $mform->addRule($field, null, 'required', null, 'client');
}
$aggregators = []; $aggregators = [];
foreach(aggregator::list_model() as $a){ foreach(aggregator::list_model() as $a){
@ -330,9 +335,6 @@ class studyplan_editform extends formbase {
'descriptionformat' => $entry->descriptionformat, 'descriptionformat' => $entry->descriptionformat,
'aggregation' => $entry->aggregation, 'aggregation' => $entry->aggregation,
'aggregation_config' => $aggregation_config, 'aggregation_config' => $aggregation_config,
'startdate' => date("Y-m-d",$entry->startdate),
'enddate' => date("Y-m-d",$entry->enddate),
'periods' => $entry->periods,
]); ]);
} }
@ -348,8 +350,12 @@ class studyplan_editform extends formbase {
$plan->id(), $plan->id(),
$customdata->fileoptions $customdata->fileoptions
); );
return $plan->simple_model(); // Return the simple model of the plan to make sure we can update stuff /* Return the simple model of the plan to make sure we can update stuff.
Parse it through the clean_returnvalue function of exernal api (of which studyplanservice is a subclass)
so we return it in a consistent way
*/
return studyplanservice::clean_returnvalue(studyplan::simple_structure(),$plan->simple_model());
} }

View File

@ -0,0 +1,251 @@
<?php
namespace local_treestudyplan\form;
use local_treestudyplan\aggregator;
use local_treestudyplan\studyplan;
use local_treestudyplan\studyplanpage;
use local_treestudyplan\courseservice;
use local_treestudyplan\form\text_integer;
use local_treestudyplan\local\helpers\webservicehelper;
use local_treestudyplan\studyplanservice;
use moodle_exception;
use stdClass;
/**
* Moodleform class for the studyplan editor. A Moodleform is used here to facilitate a rich editor
* in the studyplan description
*/
class studyplanpage_editform extends formbase {
/**
* Capability required to edit study plans
* @var string
*/
const CAP_EDIT = "local/treestudyplan:editstudyplan";
/**
* Translate parameters into customdata.
*
* @param object $params The parameters for form initialization
* @return array Form data based on parameters
*/
public static function init_customdata(object $params) {
$customdata = new stdClass;
$customdata->create = $params->mode=='create'?true:false;
if($customdata->create){
$customdata->plan = studyplan::find_by_id($params->studyplan_id);
} else {
$customdata->page = studyplanpage::find_by_id($params->page_id);
$customdata->plan = $customdata->page->studyplan();
$customdata->simplemodel = $customdata->page->simple_model();
}
$customdata->context = $customdata->plan->context();
$customdata->editoroptions = [
'trusttext' => true,
'subdirs' => true,
'maxfiles' => 20,
'maxbytes' => 20*1024*1024,
'context' => \context_system::instance(), // Keep the files in system context
];
$customdata->fileoptions = [
'subdirs' => 0,
'maxbytes' => 10*1024*1024, // Max 10MiB should be sufficient for a picture.
'areamaxbytes' => 10485760,
'maxfiles' => 1, // Just one file
'accepted_types' => ['.jpg', '.png'],
'return_types' => FILE_INTERNAL | FILE_EXTERNAL,
];
return $customdata;
}
/**
* Validate security access for this form based on the customdata generated by init_customdata
* Return true if validation passes, false or throw an exception if it does not.
*
* @param object $customdata The customdata for this form
* @return bool True if security validation passes.
* @throws \moodle_exception if access denied for a specific reason.
*/
public static function check_security(object $customdata) {
/*webservicehelper::require_capabilities(self::CAP_EDIT,$customdata->context); */
}
/**
* Generate form data from parameters
* Also validate parameters and access permissions here
*
* @param object $customdata The parameters for form initialization
* @return array Form data based on parameters
*/
public function init_formdata(object $customdata) {
global $DB;
/* Use direct database retrieval to avoid our abstractions causing trouble
with existing moodle code assumptions.
The form API does seem needlessly convoluted in it's use, but we need the editor...
*/
if($customdata->create) {
$plan = $customdata->plan;
$entry = new stdClass;
$entry->studyplan_id = $plan->id();
// By default, make the start date of a new page, continue 1 day after the last page's start date;
$otherpages = $plan->pages();
if(count($otherpages) > 0){
$lastpage = $otherpages[count($otherpages) -1];
// Propose 1 year after the last page's start date, if no end date is set.
if ($lastpage->enddate(false) == null) {
$entry->startdate = $lastpage->startdate()->add(new \DateInterval("P1Y"))->format("U");
} else {
// Otherwise, propose 1 day after the last page's end date
$entry->startdate = $lastpage->enddate()->add(new \DateInterval("P1D"))->format("U");
}
} else {
// Determine the next august 1st for default value purposes. Only if no other page is available
$august = strtotime("first day of august this year");
if($august < time()) {
$august = strtotime("first day of august next year");
}
$entry->startdate = $august;
}
$entry->enddate = $entry->startdate + (364*24*60*60); // Not bothering about leap years here.
$entry->periods = 4;
} else {
$entry = $DB->get_record(studyplanpage::TABLE, ['id' => $customdata->page->id()]);
$entry->startdate = strtotime($entry->startdate);
$entry->enddate = strtotime($entry->enddate);
}
// Prepare the editor
$entry = file_prepare_standard_editor( $entry,
'description',
$customdata->editoroptions,
\context_system::instance(),
'local_treestudyplan',
'studyplanpage',
($customdata->create)?null:$customdata->page->id()
);
return $entry;
}
/**
* Set up the form definition
*/
public function definition() {
$mform = $this->_form;
$customdata = (object)$this->_customdata;
// Register integer type
text_integer::Register();
// Define the form
$field = 'fullname';
$mform->addElement('text',$field,
get_string('studyplan_name','local_treestudyplan'),
[]);
$mform->addRule($field, null, 'required', null, 'client');
$field = 'shortname';
$mform->addElement('text',$field,
get_string('studyplan_shortname','local_treestudyplan'),
[]);
$mform->addRule($field, null, 'required', null, 'client');
$field = 'startdate';
$mform->addElement('date_selector',$field,
get_string('studyplan_startdate','local_treestudyplan'),
[]);
$mform->addRule($field, null, 'required', null, 'client');
$field = 'enddate';
$mform->addElement('date_selector',$field,
get_string('studyplan_startdate','local_treestudyplan'),
[]);
$mform->addRule($field, null, 'required', null, 'client');
$field = 'periods';
$mform->addElement('text_integer',$field,
get_string('studyplan_slots','local_treestudyplan'),
["unsigned" => true, "nonzero" => true]);
$mform->setType($field, PARAM_INT);
$mform->addRule($field, null, 'required', null, 'client');
$field = 'description_editor';
$mform->addElement('editor', $field,
get_string('studyplan_description', 'local_treestudyplan'),
null,
$customdata->editoroptions);
$mform->setType($field, PARAM_RAW);
}
/**
* Process the submitted data and perform necessary actions
* @param object $entry The processed form data;
* @return bool false if submission not successful
* @throws \moodle_exception if an error must be given for a specific reason.
*/
protected function process_submitted_data($entry) {
$customdata = (object)$this->_customdata;
if($customdata->create) {
// Use our own abstraction to update the record, so caches are maintained
$page = studyplanpage::add(['fullname' => $entry->fullname,
'shortname' => $entry->shortname,
'startdate' => date("Y-m-d",$entry->startdate),
'enddate' => date("Y-m-d",$entry->enddate),
'periods' => $entry->periods,
'studyplan_id' => $customdata->plan->id(),
]);
// Process the provided files in the description editor
$entry = file_postupdate_standard_editor($entry,
'description',
$customdata->editoroptions,
\context_system::instance(),
'local_treestudyplan',
'studyplan',
$page->id());
// Update the description
$page->edit([
'description' => $entry->description,
'descriptionformat' => $entry->descriptionformat,
]);
} else {
$page = $customdata->page;
// Process the provided files in the description editor
$entry = file_postupdate_standard_editor($entry,
'description',
$customdata->editoroptions,
\context_system::instance(),
'local_treestudyplan',
'studyplanpage',
$page->id());
// Use our own abstraction to update the record, so caches are maintained
$page->edit(['fullname' => $entry->fullname,
'shortname' => $entry->shortname,
'description' => $entry->description,
'descriptionformat' => $entry->descriptionformat,
'startdate' => date("Y-m-d",$entry->startdate),
'enddate' => date("Y-m-d",$entry->enddate),
'periods' => $entry->periods,
]);
}
/* Return the simple model of the page to make sure we can update stuff.
Parse it through the clean_returnvalue function of exernal api (of which studyplanservice is a subclass)
so we return it in a consistent way
*/
return studyplanservice::clean_returnvalue(studyplanpage::editor_structure(),$page->editor_model());
}
}

View File

@ -32,23 +32,14 @@ class text_integer extends MoodleQuickForm_text {
} }
$label = $this->getLabel(); $label = $this->getLabel();
/*
$text = '';
if (method_exists($this, 'getText')) {
// There currently exists code that adds a form element with an empty label.
// If this is the case then set the label to the description.
if (empty($label)) {
$label = $this->getText();
} else {
$text = $this->getText();
}
}*/
$unsigned = (isset($this->_attributes['unsigned']) && $this->_attributes['unsigned']); $unsigned = (isset($this->_attributes['unsigned']) && $this->_attributes['unsigned']);
$nonzero = (isset($this->_attributes['nonzero']) && $this->_attributes['nonzero']);
$context = array( $context = array(
'element' => $elementcontext, 'element' => $elementcontext,
'label' => $label, 'label' => $label,
'unsigned' => ($unsigned)?true:false , 'unsigned' => ($unsigned)?true:false ,
'nonzero' => ($nonzero)?true:false ,
'required' => $required, 'required' => $required,
'advanced' => $advanced, 'advanced' => $advanced,
'helpbutton' => $helpbutton, 'helpbutton' => $helpbutton,

View File

@ -137,16 +137,13 @@ class period {
* @return period[] * @return period[]
*/ */
public static function find_for_page(studyplanpage $page): array { public static function find_for_page(studyplanpage $page): array {
if (!array_key_exists($page->id(), self::$pagecache)) { $periods = [];
$periods = []; // Find and add the periods to an array with the period sequence as a key.
// Find and add the periods to an array with the period sequence as a key. for ($i = 1; $i <= $page->periods(); $i++) {
for ($i = 1; $i <= $page->periods(); $i++) { $period = self::find($page, $i);
$period = self::find($page, $i); $periods[$i] = $period;
$periods[$i] = $period;
}
self::$pagecache[$page->id()] = $periods;
} }
return self::$pagecache[$page->id()]; return $periods;
} }
/** /**
@ -286,7 +283,6 @@ class period {
} }
} }
$id = $DB->insert_record(self::TABLE, $info); $id = $DB->insert_record(self::TABLE, $info);
unset(self::$pagecache[$fields['page_id']]); // Invalidate the cache for this page.
return self::find_by_id($id); // Make sure the new page is immediately cached. return self::find_by_id($id); // Make sure the new page is immediately cached.
} }
@ -330,7 +326,6 @@ class period {
} }
} }
unset(self::$pagecache[$this->r->page_id]); // Invalidate the cache for this page.
return $this; return $this;
} }
@ -340,7 +335,6 @@ class period {
public function delete() : success { public function delete() : success {
global $DB; global $DB;
$DB->delete_records(self::TABLE, ['id' => $this->id]); $DB->delete_records(self::TABLE, ['id' => $this->id]);
unset(self::$pagecache[$this->r->page_id]); // Invalidate the cache for this page.
return success::success(); return success::success();
} }
@ -359,7 +353,7 @@ class period {
*/ */
public static function page_model(studyplanpage $page) : array { public static function page_model(studyplanpage $page) : array {
$model = []; $model = [];
foreach (self::find_for_page($page) as $p) { foreach (self::find_for_page($page,true) as $p) {
$model[] = $p->model(); $model[] = $p->model();
} }
return $model; return $model;

View File

@ -89,7 +89,7 @@ class studyplan {
private function __construct($id) { private function __construct($id) {
global $DB; global $DB;
$this->id = $id; $this->id = $id;
$this->r = $DB->get_record(self::TABLE, ['id' => $id]); $this->r = $DB->get_record(self::TABLE, ['id' => $id],"*",MUST_EXIST);
$this->aggregator = aggregator::create_or_default($this->r->aggregation, $this->r->aggregation_config); $this->aggregator = aggregator::create_or_default($this->r->aggregation, $this->r->aggregation_config);
} }
@ -176,11 +176,11 @@ class studyplan {
} }
/** /**
* Return the studyplan pages associated with this plan * Return the studyplan pages associated with this plan
* @param bool $refresh Set to true to force a refresh of the pages
* @return studyplanpage[] * @return studyplanpage[]
*/ */
public function pages() : array { public function pages($refresh=false) : array {
if ( ((bool)$refresh) || empty($this->pagecache)) {
if (empty($this->pagecache)) {
$this->pagecache = studyplanpage::find_studyplan_children($this); $this->pagecache = studyplanpage::find_studyplan_children($this);
} }
return $this->pagecache; return $this->pagecache;
@ -347,11 +347,9 @@ class studyplan {
$id = $DB->insert_record(self::TABLE, $info); $id = $DB->insert_record(self::TABLE, $info);
$plan = self::find_by_id($id); // Make sure the new studyplan is immediately cached. $plan = self::find_by_id($id); // Make sure the new studyplan is immediately cached.
// Start temporary skräpp code. // Add a single page and initialize it with placeholder data
// Add a single page and copy the names.This keeps the data sane until the upgrade to . // This makes it easier to create a new study plan
// Real page management is done. // On import, adding an empty page messes things up , so we have an option to skip this....
// On import, adding an empty page messes things up for now, so we have an option to skip this....
// TODO: Remove this when proper page management is implemented.
if (!$bare) { if (!$bare) {
$pageaddable = ['name', 'shortname', 'description', 'periods', 'startdate', 'enddate']; $pageaddable = ['name', 'shortname', 'description', 'periods', 'startdate', 'enddate'];
@ -398,27 +396,6 @@ class studyplan {
// Reload aggregator. // Reload aggregator.
$this->aggregator = aggregator::create_or_default($this->r->aggregation, $this->r->aggregation_config); $this->aggregator = aggregator::create_or_default($this->r->aggregation, $this->r->aggregation_config);
// Start temporary skräpp code.
// TODO: Until proper page editing is implemented, copy data from studyplan to it's first page.
// This keeps the data sane until the upgrade is done.
if (count($this->pages()) == 1) {
// Update the info to the page as well.
$page = $this->pages()[0];
$pageeditable = ['name', 'shortname', 'description', 'periods', 'startdate', 'enddate'];
$pageinfo = [];
foreach ($pageeditable as $f) {
if (array_key_exists($f, $fields)) {
if ($f == "name") {
$pageinfo["fullname"] = $fields[$f];
} else {
$pageinfo[$f] = $fields[$f];
}
}
}
$page->edit($pageinfo);
}
// End temporary skräpp code.
return $this; return $this;
} }

View File

@ -126,14 +126,18 @@ class studyplanpage {
/** /**
* End date * End date
* @return \DateTime * @return \DateTime|null
*/ */
public function enddate() { public function enddate($farahead = true) {
if ($this->r->enddate && strlen($this->r->enddate) > 0) { if ($this->r->enddate && strlen($this->r->enddate) > 0) {
return new \DateTime($this->r->enddate); return new \DateTime($this->r->enddate);
} else { } else {
// Return a date 100 years into the future. // Return a date 100 years into the future.
return (new \DateTime($this->r->startdate))->add(new \DateInterval("P100Y")); if ($farahead) {
return (new \DateTime($this->r->startdate))->add(new \DateInterval("P100Y"));
} else {
return null;
}
} }
} }
@ -221,10 +225,13 @@ class studyplanpage {
* @param mixed $fields Parameter for new study plan page * @param mixed $fields Parameter for new study plan page
*/ */
public static function add($fields) : self { public static function add($fields) : self {
global $CFG, $DB; global $DB;
if (!isset($fields['studyplan_id'])) { if (!isset($fields['studyplan_id'])) {
throw new \InvalidArgumentException("parameter 'studyplan_id' missing"); throw new \InvalidArgumentException("parameter 'studyplan_id' missing");
} else {
$plan = studyplan::find_by_id($fields['studyplan_id']);
} }
$addable = ['studyplan_id', 'fullname', 'shortname', 'description', 'periods', 'startdate', 'enddate']; $addable = ['studyplan_id', 'fullname', 'shortname', 'description', 'periods', 'startdate', 'enddate'];
@ -241,6 +248,8 @@ class studyplanpage {
} }
$id = $DB->insert_record(self::TABLE, $info); $id = $DB->insert_record(self::TABLE, $info);
// Refresh the page cache for the studyplan
$plan->pages(true);
return self::find_by_id($id); // Make sure the new page is immediately cached. return self::find_by_id($id); // Make sure the new page is immediately cached.
} }

View File

@ -1298,6 +1298,12 @@
.features-treestudyplan .border-grey { .features-treestudyplan .border-grey {
border-color: #aaa; border-color: #aaa;
} }
.path-local-treestudyplan .s-studyplan-page-edit,
.features-treestudyplan .s-studyplan-page-edit {
margin-top: auto;
margin-bottom: auto;
margin-left: 0.5em;
}
.path-local-treestudyplan .card.s-studyplan-card, .path-local-treestudyplan .card.s-studyplan-card,
.features-treestudyplan .card.s-studyplan-card { .features-treestudyplan .card.s-studyplan-card {

View File

@ -24,6 +24,7 @@ require_once("../../config.php");
require_once($CFG->libdir.'/weblib.php'); require_once($CFG->libdir.'/weblib.php');
use local_treestudyplan\contextinfo;
use \local_treestudyplan\courseservice; use \local_treestudyplan\courseservice;
$systemcontext = context_system::instance(); $systemcontext = context_system::instance();
@ -61,10 +62,11 @@ if ($categoryid > 0) {
} }
require_capability('local/treestudyplan:editstudyplan', $studyplancontext); require_capability('local/treestudyplan:editstudyplan', $studyplancontext);
$contextname = $studyplancontext->get_context_name(false, false); $ci = new contextinfo($studyplancontext);
$contextname = $ci->pathstr();
$PAGE->set_pagelayout('coursecategory'); $PAGE->set_pagelayout('base');
$PAGE->set_context($studyplancontext); //$PAGE->set_context($studyplancontext);
$PAGE->set_title(get_string('cfg_plans', 'local_treestudyplan')." - ".$contextname); $PAGE->set_title(get_string('cfg_plans', 'local_treestudyplan')." - ".$contextname);
$PAGE->set_heading($contextname); $PAGE->set_heading($contextname);

View File

@ -1112,4 +1112,10 @@
border-color: #aaa; border-color: #aaa;
} }
.s-studyplan-page-edit {
margin-top: auto;
margin-bottom: auto;
margin-left: 0.5em;
}
} }

View File

@ -1298,6 +1298,12 @@
.features-treestudyplan .border-grey { .features-treestudyplan .border-grey {
border-color: #aaa; border-color: #aaa;
} }
.path-local-treestudyplan .s-studyplan-page-edit,
.features-treestudyplan .s-studyplan-page-edit {
margin-top: auto;
margin-bottom: auto;
margin-left: 0.5em;
}
.path-local-treestudyplan .card.s-studyplan-card, .path-local-treestudyplan .card.s-studyplan-card,
.features-treestudyplan .card.s-studyplan-card { .features-treestudyplan .card.s-studyplan-card {

View File

@ -17,6 +17,6 @@
{{/ core_form/element-template }} {{/ core_form/element-template }}
{{#js}} {{#js}}
require(['local_treestudyplan/util/formfields'], function(formfields) { require(['local_treestudyplan/util/formfields'], function(formfields) {
formfields.text_integer({{#quote}}{{element.id}}{{/quote}},{{unsigned}}); formfields.text_integer({{#quote}}{{element.id}}{{/quote}},{{unsigned}},{{nonzero}});
}); });
{{/js}} {{/js}}

View File

@ -64,7 +64,7 @@ require_capability('local/treestudyplan:viewuserreports', $studyplancontext);
$contextname = $studyplancontext->get_context_name(false, false); $contextname = $studyplancontext->get_context_name(false, false);
$PAGE->set_pagelayout('base'); $PAGE->set_pagelayout('base');
$PAGE->set_context($studyplancontext); //$PAGE->set_context($studyplancontext);
$PAGE->set_title(get_string('view_plan', 'local_treestudyplan')." - ".$contextname); $PAGE->set_title(get_string('view_plan', 'local_treestudyplan')." - ".$contextname);
$PAGE->set_heading(get_string('view_plan', 'local_treestudyplan')." - ".$contextname); $PAGE->set_heading(get_string('view_plan', 'local_treestudyplan')." - ".$contextname);