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

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

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

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',
args: { id: studyplan.id}
}])[0].done(function(response){
app.activestudyplan = ProcessStudyplan(response,true);
app.activestudyplan = ProcessStudyplan(response);
debug.info('studyplan processed');
app.loadingstudyplan = false;
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 Debugger from './util/debugger';
import {download,upload} from './downloader';
import {ProcessStudyplan} from './studyplan-processor';
import {ProcessStudyplan, ProcessStudyplanPage} from './studyplan-processor';
import TSComponents from './treestudyplan-components';
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
*/
@ -1032,6 +1097,7 @@ export default {
`
});
// TAG: Start studyplan component
/*
* T-STUDYPLAN
*/
@ -1057,6 +1123,11 @@ export default {
'shortname': '',
'color': '#DDDDDD',
},
page: {
'id': -1,
'name' : '',
'shortname' : ''
}
},
edit: {
toolbox_shown: false,
@ -1091,14 +1162,14 @@ export default {
text: strings.studyplan_text,
cache: {
linelayers: {},
}
},
};
},
created() {
},
mounted() {
if(this.page.studylines.length == 0){
if(this.value.pages[0].studylines.length == 0){
// start in editmode if studylines are empty
this.edit.studyline.editmode = true;
}
@ -1109,23 +1180,20 @@ export default {
ItemEventBus.$emit('redrawLines');
},
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
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)";
}
return s+";";
},
page(){
// FIXME: Temporary hack until real page management is implemented
return this.value.pages[0];
}
},
methods: {
trashbin_accepts(type){
if(type.item){
return true;
@ -1133,7 +1201,7 @@ export default {
return false;
}
},
countLineLayers(line){
countLineLayers(line,page){
// 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.....
if( this.cache.linelayers[line.id]
@ -1144,7 +1212,7 @@ export default {
else
{
let maxLayer = -1;
for(let i = 0; i <= this.page.periods; i++){
for(let i = 0; i <= page.periods; i++){
if(line.slots[i]){
const slot = line.slots[i];
// Determine the amount of used layers in a studyline slit
@ -1303,11 +1371,11 @@ export default {
}).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
// so big that it hides this slot
const forGradable = (type == 'gradable')?true:false;
const periods = this.page.periods;
const periods = page.periods;
let show = true;
for(let i = 0; i < periods; i++){
if(line.slots[index-i] && line.slots[index-i].courses){
@ -1355,6 +1423,9 @@ export default {
},
toolbox_switched(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>
</span>
<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>
</span>
</div>
</div>
<div class='t-studyplan-content-edit' v-if="edit.studyline.editmode">
<drop-list
:items="page.studylines"
class="t-slot-droplist"
:accepts-type="'studyline-'+page.id"
xreorder="$event.apply(page.studylines)"
@reorder="reorderLines($event,page.studylines)"
mode="copy"
row
>
<template v-slot:item="{item}">
<drag
:key="item.id"
class='t-studyline-drag'
:data="item"
:type="'studyline-'+page.id"
>
<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>
<b-tabs content-class="mt-1">
<!-- New Tab Button (Using tabs-end slot) -->
<template #tabs-end>
<t-studyplan-page-edit
:studyplan="value"
v-model="create.page"
type="link"
mode="create"
@created="pagecreated"
><i class='fa fa-plus'></i></t-studyplan-page-edit>
</template>
<b-tab
v-for="(page,pageindex) in value.pages"
:key="page.id"
>
<template #title>
{{page.shortname}}
<t-studyplan-page-edit
v-model="value.pages[pageindex]"
:studyplan="value"
type="link"
></t-studyplan-page-edit>
</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></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)+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">
<!-- 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 ':' ')"
<div class='t-studyplan-content-edit' v-if="edit.studyline.editmode">
<drop-list
:items="page.studylines"
class="t-slot-droplist"
:accepts-type="'studyline-'+page.id"
xreorder="$event.apply(page.studylines)"
@reorder="reorderLines($event,page.studylines)"
mode="copy"
row
>
<template v-slot:item="{item}">
<drag
:key="item.id"
class='t-studyline-drag'
:data="item"
:type="'studyline-'+page.id"
>
</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>
</div>
</div>
<div v-if="edit.studyline.editmode" class='t-studyline-add'>
<a href="#" v-b-modal="'modal-add-studyline-'+page.id" @click="false;"
><i class='fa fa-plus'></i>{{ text.studyline_add }}</a>
</div>
<b-modal
:id="'modal-add-studyline-'+page.id"
size="lg"
:ok-title="text.add$core"
ok-variant="primary"
:title="text.studyline_add"
@ok="addStudyLine(page,create.studyline)"
:ok-disabled="Math.min(create.studyline.name.length,create.studyline.shortname.length) == 0"
>
<b-container>
<b-row>
<b-col cols="3">{{text.studyline_name}}</b-col>
<b-col>
<b-form-input v-model="create.studyline.name" :placeholder="text.studyline_name_ph"></b-form-input>
</b-col>
</b-row>
<b-row>
<b-col cols="3">{{text.studyline_shortname}}</b-col>
<b-col>
<b-form-input
v-model="create.studyline.shortname"
:placeholder="text.studyline_shortname_ph"></b-form-input>
</b-col>
</b-row>
<b-row>
<b-col cols="3">{{text.studyline_color}}</b-col>
<b-col>
<input type="color" v-model="create.studyline.color" />
<!-- hsluv-picker v-model="create.studyline.color" horizontal displaysize="175" ></hsluv-picker -->
</b-col>
</b-row>
</b-container>
</b-modal>
<b-modal
:id="'modal-edit-studyline-'+value.id"
size="lg"
ok-variant="primary"
:title="text.studyline_edit"
@ok="editLineFinish()"
:ok-disabled="Math.min(edit.studyline.data.name.length,edit.studyline.data.shortname.length) == 0"
>
<b-container>
<b-row>
<b-col cols="3">{{ text.studyline_name}}</b-col>
<b-col>
<b-form-input
v-model="edit.studyline.data.name"
:placeholder="text.studyline_name_ph"></b-form-input>
</b-col>
</b-row>
<b-row>
<b-col cols="3">{{ text.studyline_shortname}}</b-col>
<b-col>
<b-form-input
v-model="edit.studyline.data.shortname"
:placeholder="text.studyline_shortname_ph"></b-form-input>
</b-col>
</b-row>
<b-row>
<b-col cols="3">{{ text.studyline_color}}</b-col>
<b-col>
<input type="color" v-model="edit.studyline.data.color" />
</b-col>
</b-row>
</b-container>
</b-modal>
></div>
</div>
</div>
<div v-if="edit.studyline.editmode" class='t-studyline-add'>
<a href="#" v-b-modal="'modal-add-studyline-'+page.id" @click="false;"
><i class='fa fa-plus'></i>{{ text.studyline_add }}</a>
</div>
<b-modal
:id="'modal-add-studyline-'+page.id"
size="lg"
:ok-title="text.add$core"
ok-variant="primary"
:title="text.studyline_add"
@ok="addStudyLine(page,create.studyline)"
:ok-disabled="Math.min(create.studyline.name.length,create.studyline.shortname.length) == 0"
>
<b-container>
<b-row>
<b-col cols="3">{{text.studyline_name}}</b-col>
<b-col>
<b-form-input v-model="create.studyline.name" :placeholder="text.studyline_name_ph"></b-form-input>
</b-col>
</b-row>
<b-row>
<b-col cols="3">{{text.studyline_shortname}}</b-col>
<b-col>
<b-form-input
v-model="create.studyline.shortname"
:placeholder="text.studyline_shortname_ph"></b-form-input>
</b-col>
</b-row>
<b-row>
<b-col cols="3">{{text.studyline_color}}</b-col>
<b-col>
<input type="color" v-model="create.studyline.color" />
<!-- hsluv-picker v-model="create.studyline.color" horizontal displaysize="175" ></hsluv-picker -->
</b-col>
</b-row>
</b-container>
</b-modal>
<b-modal
:id="'modal-edit-studyline-'+page.id"
size="lg"
ok-variant="primary"
:title="text.studyline_edit"
@ok="editLineFinish()"
:ok-disabled="Math.min(edit.studyline.data.name.length,edit.studyline.data.shortname.length) == 0"
>
<b-container>
<b-row>
<b-col cols="3">{{ text.studyline_name}}</b-col>
<b-col>
<b-form-input
v-model="edit.studyline.data.name"
:placeholder="text.studyline_name_ph"></b-form-input>
</b-col>
</b-row>
<b-row>
<b-col cols="3">{{ text.studyline_shortname}}</b-col>
<b-col>
<b-form-input
v-model="edit.studyline.data.shortname"
:placeholder="text.studyline_shortname_ph"></b-form-input>
</b-col>
</b-row>
<b-row>
<b-col cols="3">{{ text.studyline_color}}</b-col>
<b-col>
<input type="color" v-model="edit.studyline.data.color" />
</b-col>
</b-row>
</b-container>
</b-modal>
</b-tab>
</b-tabs>
</div>
`
});

View File

@ -63,66 +63,77 @@ export function ProcessStudyplans(studyplans){
* @returns Processed studyplan
*/
export function ProcessStudyplan(studyplan){
let connections = {};
for(const ip in studyplan.pages){
const page = studyplan.pages[ip];
for(const il in page.studylines) {
const line = page.studylines[il];
ProcessStudyplanPage(page);
}
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 ic in slot.courses){
const itm = slot.courses[ic];
for(const is in line.slots ) {
const slot = line.slots[is];
for(const idx in itm.connections.in) {
const conn = itm.connections.in[idx];
if(slot.courses !== undefined){
for(const ic in slot.courses){
const itm = slot.courses[ic];
if(conn.id in connections){
itm.connections[idx] = connections[conn.id];
} else {
connections[conn.id] = conn;
}
for(const idx in itm.connections.in) {
const conn = itm.connections.in[idx];
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){
itm.connections[idx] = connections[conn.id];
} else {
connections[conn.id] = conn;
}
if(conn.id in connections){
itm.connections[idx] = connections[conn.id];
} else {
connections[conn.id] = conn;
}
}
}
}
if(slot.filters !== undefined){
for(const ix in slot.filters){
const itm = slot.filters[ix];
if(slot.filters !== undefined){
for(const ix in slot.filters){
const itm = slot.filters[ix];
for(const idx in itm.connections.in) {
const conn = itm.connections.in[idx];
for(const idx in itm.connections.in) {
const conn = itm.connections.in[idx];
if(conn.id in connections){
itm.connections[idx] = connections[conn.id];
} 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){
itm.connections[idx] = connections[conn.id];
} else {
connections[conn.id] = conn;
}
if(conn.id in connections){
itm.connections[idx] = connections[conn.id];
} else {
connections[conn.id] = conn;
}
}
}
}
}
}
return studyplan;
return page;
}

View File

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

View File

@ -2,18 +2,22 @@
* convert a text field into an integer only text field
* @param {string} id The Id of the form field
* @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);
if (element) {
element.addEventListener("input",() => {
var val = element.value;
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
if (unsigned) {
element.value = val.replace(/[^0-9]/g,'');
if (nonzero && val == 0) {
element.value = '';
}
} else {
element.value = val.replace(/[^0-9-]/g,'').replace(/(.{1})(-)/g,'$1');
}

View File

@ -65,16 +65,19 @@ export default {
loading: true,
uuid: create_uuid(),
text: strings,
submitok: false,
observer: null,
inputs: [],
};
},
computed: {
},
methods: {
openForm(){
openForm() {
const self = this;
self.$refs["editormodal"].show();
},
onShown(){
onShown() {
const self = this;
debug.info(`Loading form "${self.name}" with params`,self.params);
self.loading = false;
@ -87,11 +90,11 @@ export default {
// Process the collected javascript;
const js = processCollectedJavascript(data.javascript);
replaceNodeContents(self.$refs["content"], html, js);
self.initListenChanges();
}).catch(notification.exception);
},
onSave(){
onSave() {
const self = this;
let form = this.$refs["content"].getElementsByTagName("form")[0];
@ -104,13 +107,59 @@ export default {
const formdata = new FormData(form);
const data = new URLSearchParams(formdata).toString();
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.exception);
if(this.checkSave()){
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.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: `
@ -127,6 +176,7 @@ export default {
id="'modal-'+uuid"
@shown="onShown"
@ok="onSave"
:ok-disabled="!submitok"
:title="title"
:ok-title="text.save$core"
><div :class="'s-mform-content'" ref="content"

View File

@ -36,6 +36,10 @@ class contextinfo {
*/
public function __construct(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() {
$ctxpath = array_reverse($this->context->get_parent_context_ids(true));
if (count($ctxpath) > 1 && $ctxpath[0] == 1) {
array_shift($ctxpath);
}
return [
"name" => $this->context->get_context_name(false, false),
"shortname" => $this->context->get_context_name(false, true),
"path" => array_map(function($c) {
return \context::instance_by_id($c)->get_context_name(false, false);
}, $ctxpath),
"shortpath" => array_map(function($c) {
return \context::instance_by_id($c)->get_context_name(false, true);
}, $ctxpath),
"path" => $this->path(false),
"shortpath" => $this->path(true),
];
}
/**
* 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
* @param int $contextid Context id

View File

@ -4,6 +4,7 @@ namespace local_treestudyplan\form;
use local_treestudyplan\aggregator;
use local_treestudyplan\studyplan;
use local_treestudyplan\studyplanservice;
use local_treestudyplan\courseservice;
use local_treestudyplan\form\text_integer;
use local_treestudyplan\local\helpers\webservicehelper;
@ -189,24 +190,28 @@ class studyplan_editform extends formbase {
$customdata->fileoptions
);
$field = 'startdate';
$mform->addElement('date_selector',$field,
get_string('studyplan_startdate','local_treestudyplan'),
[]);
$mform->addRule($field, null, 'required', null, 'client');
if ($customdata->create) {
// Only add these fields if a new studyplan is created, to easily initialize the first page
$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 = '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]);
$mform->setType($field, PARAM_INT);
$mform->addRule($field, null, 'required', null, 'client');
$field = 'periods';
$mform->addElement('text_integer',$field,
get_string('studyplan_slots','local_treestudyplan'),
["unsigned" => true]);
$mform->setType($field, PARAM_INT);
$mform->addRule($field, null, 'required', null, 'client');
}
$aggregators = [];
foreach(aggregator::list_model() as $a){
@ -330,9 +335,6 @@ class studyplan_editform extends formbase {
'descriptionformat' => $entry->descriptionformat,
'aggregation' => $entry->aggregation,
'aggregation_config' => $aggregation_config,
'startdate' => date("Y-m-d",$entry->startdate),
'enddate' => date("Y-m-d",$entry->enddate),
'periods' => $entry->periods,
]);
}
@ -349,7 +351,11 @@ class studyplan_editform extends formbase {
$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();
/*
$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']);
$nonzero = (isset($this->_attributes['nonzero']) && $this->_attributes['nonzero']);
$context = array(
'element' => $elementcontext,
'label' => $label,
'unsigned' => ($unsigned)?true:false ,
'nonzero' => ($nonzero)?true:false ,
'required' => $required,
'advanced' => $advanced,
'helpbutton' => $helpbutton,

View File

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

View File

@ -89,7 +89,7 @@ class studyplan {
private function __construct($id) {
global $DB;
$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);
}
@ -176,11 +176,11 @@ class studyplan {
}
/**
* Return the studyplan pages associated with this plan
* @param bool $refresh Set to true to force a refresh of the pages
* @return studyplanpage[]
*/
public function pages() : array {
if (empty($this->pagecache)) {
public function pages($refresh=false) : array {
if ( ((bool)$refresh) || empty($this->pagecache)) {
$this->pagecache = studyplanpage::find_studyplan_children($this);
}
return $this->pagecache;
@ -347,11 +347,9 @@ class studyplan {
$id = $DB->insert_record(self::TABLE, $info);
$plan = self::find_by_id($id); // Make sure the new studyplan is immediately cached.
// Start temporary skräpp code.
// Add a single page and copy the names.This keeps the data sane until the upgrade to .
// Real page management is done.
// 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.
// Add a single page and initialize it with placeholder data
// This makes it easier to create a new study plan
// On import, adding an empty page messes things up , so we have an option to skip this....
if (!$bare) {
$pageaddable = ['name', 'shortname', 'description', 'periods', 'startdate', 'enddate'];
@ -398,27 +396,6 @@ class studyplan {
// Reload aggregator.
$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;
}

View File

@ -126,14 +126,18 @@ class studyplanpage {
/**
* End date
* @return \DateTime
* @return \DateTime|null
*/
public function enddate() {
public function enddate($farahead = true) {
if ($this->r->enddate && strlen($this->r->enddate) > 0) {
return new \DateTime($this->r->enddate);
} else {
// 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
*/
public static function add($fields) : self {
global $CFG, $DB;
global $DB;
if (!isset($fields['studyplan_id'])) {
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'];
@ -241,6 +248,8 @@ class studyplanpage {
}
$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.
}

View File

@ -1298,6 +1298,12 @@
.features-treestudyplan .border-grey {
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,
.features-treestudyplan .card.s-studyplan-card {

View File

@ -24,6 +24,7 @@ require_once("../../config.php");
require_once($CFG->libdir.'/weblib.php');
use local_treestudyplan\contextinfo;
use \local_treestudyplan\courseservice;
$systemcontext = context_system::instance();
@ -61,10 +62,11 @@ if ($categoryid > 0) {
}
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_context($studyplancontext);
$PAGE->set_pagelayout('base');
//$PAGE->set_context($studyplancontext);
$PAGE->set_title(get_string('cfg_plans', 'local_treestudyplan')." - ".$contextname);
$PAGE->set_heading($contextname);

View File

@ -1112,4 +1112,10 @@
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 {
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,
.features-treestudyplan .card.s-studyplan-card {

View File

@ -17,6 +17,6 @@
{{/ core_form/element-template }}
{{#js}}
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}}

View File

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