Added icons, finished description editor

This commit is contained in:
PMKuipers 2023-10-23 21:54:09 +02:00
parent 0dda0c6a45
commit c6882b916a
29 changed files with 574 additions and 380 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

3
amd/build/util/formfields.min.js vendored Normal file
View File

@ -0,0 +1,3 @@
define("local_treestudyplan/util/formfields",["exports","./debugger"],(function(_exports,_debugger){var obj;Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.text_integer=function(id){var element=document.getElementById(id);debug.warn("Initializing form element text_integer on ",id,element)};var debug=new(_debugger=(obj=_debugger)&&obj.__esModule?obj:{default:obj}).default("formfields")}));
//# sourceMappingURL=formfields.min.js.map

View File

@ -0,0 +1 @@
{"version":3,"file":"formfields.min.js","sources":["../../src/util/formfields.js"],"sourcesContent":["import Debugger from './debugger';\nconst debug = new Debugger(\"formfields\");\n/**\n * convert a text field into an integer only text field\n * @param {Date|string} id The Id of the form field\n */\nexport function text_integer(id){\n const element = document.getElementById(id);\n debug.warn(\"Initializing form element text_integer on \",id,element);\n}"],"names":["id","element","document","getElementById","debug","warn"],"mappings":"mMAM6BA,QACnBC,QAAUC,SAASC,eAAeH,IACxCI,MAAMC,KAAK,6CAA6CL,GAAGC,cAPzDG,MAAQ,yEAAa"}

3
amd/build/util/int-textfield.min.js vendored Normal file
View File

@ -0,0 +1,3 @@
//# sourceMappingURL=int-textfield.min.js.map

View File

@ -0,0 +1 @@
{"version":3,"file":"int-textfield.min.js","sources":[],"sourcesContent":[],"names":[],"mappings":""}

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:""}},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);debug.info("Replacing content / el",self.$refs.content),debug.info("Replacing content / html",html),debug.info("Replacing content / js",js);var r=(0,_templates.replaceNodeContents)(self.$refs.content,html,js);debug.info("R:",r)})).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(){self.$emit("saved",formdata)})).catch(_notification.default.exception)}},template:'\n <span class=\'mform-container\'><a href=\'#\' @click.prevent="openForm"><slot><i class="fa fa-cog"></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: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}));
//# sourceMappingURL=mform-helper.min.js.map

File diff suppressed because one or more lines are too long

View File

@ -135,31 +135,7 @@ export default {
},
studyplan_edit: {
studyplan_edit: 'studyplan_edit',
studyplan_name: 'studyplan_name',
studyplan_name_ph: 'studyplan_name_ph',
studyplan_shortname: 'studyplan_shortname',
studyplan_shortname_ph: 'studyplan_shortname_ph',
studyplan_description: 'studyplan_description',
studyplan_description_ph: 'studyplan_description_ph',
studyplan_idnumber: 'studyplan_idnumber',
studyplan_idnumber_ph: 'studyplan_idnumber_ph',
studyplan_context: 'studyplan_context',
studyplan_slots: 'studyplan_slots',
studyplan_startdate: 'studyplan_startdate',
studyplan_enddate: 'studyplan_enddate',
choose_aggregation_style: 'choose_aggregation_style',
setting_bistate_thresh_excellent: 'setting_bistate_thresh_excellent',
settingdesc_bistate_thresh_excellent: 'settingdesc_bistate_thresh_excellent',
setting_bistate_thresh_good: 'setting_bistate_thresh_good',
settingdesc_bistate_thresh_good: 'settingdesc_bistate_thresh_good',
setting_bistate_thresh_completed: 'setting_bistate_thresh_completed',
settingdesc_bistate_thresh_completed: 'settingdesc_bistate_thresh_completed',
setting_bistate_support_failed: 'setting_bistate_support_failed',
settingdesc_bistate_support_failed: 'settingdesc_bistate_support_failed',
setting_bistate_thresh_progress: 'setting_bistate_thresh_progress',
settingdesc_bistate_thresh_progress: 'settingdesc_bistate_thresh_progress',
setting_bistate_accept_pending_submitted: 'setting_bistate_accept_pending_submitted',
settingdesc_bistate_accept_pending_submitted: 'settingdesc_bistate_accept_pending_submitted',
studyplan_add: 'studyplan_add',
},
period_edit: {
edit: 'period_edit',
@ -498,7 +474,8 @@ export default {
variant="danger"
@click="purge_studyline"
>{{ text.advanced_purge}}</b-button></p>
</b-tab> </b-tabs>
</b-tab>
</b-tabs>
</b-card>
</b-modal>
</span>
@ -531,147 +508,31 @@ export default {
type: Number,
default: 1
},
'defaultAggregation': {
type: String,
default: "core",
}
},
data() {
return {
show: false,
config: {
userfields: [
{ key: "selected",},
{ key: "firstname", "sortable": true,},
{ key: "lastname", "sortable": true,},
],
cohortfields:[
{ key: "selected",},
{ key: "name", "sortable": true,},
{ key: "context", "sortable": true,},
]
},
editdata: {
name: '',
shortname: '',
description: '',
idnumber: '',
context_id: this.contextid,
periods : 4,
startdate: (new Date()).getFullYear() + '-08-01',
enddate: ((new Date()).getFullYear()+1) + '-08-01',
aggregation: this.defaultAggregation,
aggregation_config: '',
},
aggregation_parsed: {
},
aggregators: [],
categories: [ { context_id: 1, category: { path: "System"}}], // overwritten during load...
text: strings.studyplan_edit,
};
},
created() {
// retrieve aggregator info
const self = this;
call([{
methodname: 'local_treestudyplan_list_aggregators',
args: [],
}])[0].done(function(response){
self.aggregators = response;
for(const ix in self.aggregators){
const ag = self.aggregators[ix];
try{
if(ag.defaultconfig && ag.defaultconfig.length > 0){
self.aggregation_parsed[ag.id] = JSON.parse(ag.defaultconfig);
}
}
catch(e){
debug.warn(e);
}
}
}).fail(notification.exception);
call([{
methodname: 'local_treestudyplan_list_accessible_categories',
args: {operation: "edit",}
}])[0].done(function(response){
for(const ix in response){
const cat = response[ix];
cat.category.pathname = cat.category.path.join(" / ");
}
self.categories = response;
}).fail(notification.exception);
},
mounted() {
},
updated() {
},
computed: {
},
methods: {
editPlanStart(){
if(this.mode != 'create'){
objCopy(this.editdata,this.value.pages[0],STUDYPLAN_EDITOR_PAGE_FIELDS);
objCopy(this.editdata,this.value,STUDYPLAN_EDITOR_FIELDS);
}
// decode the aggregation config data that is stored
if(this.editdata.aggregation_config && this.editdata.aggregation_config.length > 0){
try{
this.aggregation_parsed[this.editdata.aggregation] = JSON.parse(this.editdata.aggregation_config);
}
catch(e){
debug.warn(e);
}
}
this.show = true;
},
editPlanFinish(){
planSaved(updatedplan){
const self = this;
let args = { };
let method = 'local_treestudyplan_edit_studyplan';
if(this.mode == 'create'){
method = 'local_treestudyplan_add_studyplan';
} else {
args['id'] = this.value.id;
}
debug.info("Got new plan data",updatedplan);
// store the configuration for this aggregation type if it is relevant
if(this.aggregation_parsed[this.editdata.aggregation]){
this.editdata.aggregation_config = JSON.stringify(this.aggregation_parsed[this.editdata.aggregation]);
}
objCopy(args,this.editdata,STUDYPLAN_EDITOR_FIELDS);
objCopy(args,this.editdata,STUDYPLAN_EDITOR_PAGE_FIELDS);
call([{
methodname: method,
args: args
}])[0].done(function(response){
if(self.mode == 'create'){
self.$emit("created", response);
// And reset the edit fields to default
self.editdata = {
name: '',
shortname: '',
description: '',
context_id: 1,
periods : 4,
startdate: (new Date()).getFullYear() + '-08-01',
enddate: ((new Date()).getFullYear()+1) + '-08-01',
aggregation: 'bistate',
aggregation_config: '',
};
// Inform parent of the details of the newly created plan
self.$emit("created",updatedplan);
}
else {
// determine if the plan moved context...
const moved_from = self.value.context_id;
const moved_to = response.context_id;
const moved_to = updatedplan.context_id;
const moved = (moved_from != moved_to);
if(response.pages[0].periods != self.value.pages[0].periods){
// reload the entire model
if(updatedplan.pages[0].periods != self.value.pages[0].periods){
// If the pages changed, just reload the entire model for the plan
call([{
methodname: 'local_treestudyplan_get_studyplan_map',
args: { id: self.value.id}
@ -683,147 +544,28 @@ export default {
notification.exception(error);
});
} else {
objCopy(self.value,response,STUDYPLAN_EDITOR_FIELDS);
// Copy updated fields and trigger update
objCopy(self.value,updatedplan,STUDYPLAN_EDITOR_FIELDS);
self.$emit('input',self.value);
if(moved){
self.$emit('moved',self.value,moved_from, moved_to);
self.$emit('moved',self.value, moved_from, moved_to);
}
}
}
}).fail(notification.exception);
},
numberFilter(value){
return value;
},
}
,
template:
`
<span class='s-studyplan-edit'>
<b-button :variant="variant" v-if='type == "button"' @click.prevent='editPlanStart()'
><slot><i class='fa fa-gear'></i></slot></b-button>
<a variant="variant" v-else href='#' @click.prevent='editPlanStart()'
><slot><i class='fa fa-gear'></i></slot></a>
<b-modal
v-model="show"
size="lg"
ok-variant="primary"
:title="text.studyplan_edit"
@ok="editPlanFinish()"
:ok-disabled="Math.min(editdata.name.length,editdata.shortname.length) == 0"
>
<b-container>
<b-row>
<b-col cols="4">{{ text.studyplan_name}}</b-col>
<b-col cols="8">
<b-form-input v-model="editdata.name"
:state='editdata.name.length>0'
:placeholder="text.studyplan_name_ph"></b-form-input>
</b-col>
</b-row>
<b-row>
<b-col cols="4">{{ text.studyplan_shortname}}</b-col>
<b-col cols="8">
<b-form-input v-model="editdata.shortname"
:state='editdata.shortname.length>0'
:placeholder="text.studyplan_shortname_ph"></b-form-input>
</b-col>
</b-row>
<b-row>
<b-col cols="4">{{ text.studyplan_idnumber}}</b-col>
<b-col cols="8">
<b-form-input v-model="editdata.idnumber"
:placeholder="text.studyplan_idnumber_ph"></b-form-input>
</b-col>
</b-row> <b-row>
<b-col cols="4">{{ text.studyplan_description}}</b-col>
<b-col cols="8">
<mform name="studyplan_editform" :params="{studyplan_id: value.id}"></mform>
</b-col>
</b-row>
<b-row>
<b-col cols="4">{{ text.studyplan_context}}</b-col>
<b-col cols="8">
<b-form-select v-model="editdata.context_id"
:options="categories" text-field="category.pathname" value-field="context_id"
></b-form-select>
</b-col>
</b-row>
<b-row>
<b-col cols="4">{{ text.studyplan_slots}}</b-col>
<b-col cols="8">
<b-form-input
min="1"
type="number"
no-wheel
v-model="editdata.periods"
></b-form-input>
</b-col>
</b-row>
<b-row>
<b-col cols="4">{{ text.studyplan_startdate}}</b-col>
<b-col cols="8">
<b-form-datepicker start-weekday="1" v-model="editdata.startdate"></b-form-datepicker>
</b-col>
</b-row>
<b-row>
<b-col cols="4">{{ text.studyplan_enddate}}</b-col>
<b-col cols="8">
<b-form-datepicker start-weekday="1" v-model="editdata.enddate" ></b-form-datepicker>
</b-col>
</b-row>
<b-row>
<b-col cols="4">{{ text.choose_aggregation_style}}</b-col>
<b-col cols="8">
<b-form-select v-model="editdata.aggregation"
:options="aggregators" text-field="name" value-field="id"
></b-form-select>
</b-col>
</b-row>
<template v-if="aggregation_parsed.bistate && editdata.aggregation == 'bistate'">
<b-row >
<b-col cols="4">{{ text.setting_bistate_thresh_excellent}}</b-col>
<b-col cols="8">
<b-form-input v-model="aggregation_parsed.bistate.thresh_excellent"
type="number" number :formatter="numberFilter"
></b-form-input>
</b-col>
</b-row>
<b-row >
<b-col cols="4">{{ text.setting_bistate_thresh_good}}</b-col>
<b-col cols="8">
<b-form-input v-model="aggregation_parsed.bistate.thresh_good"
type="number" number :formatter="numberFilter"
></b-form-input>
</b-col>
</b-row>
<b-row >
<b-col cols="4">{{ text.setting_bistate_thresh_completed}}</b-col>
<b-col cols="8">
<b-form-input v-model="aggregation_parsed.bistate.thresh_completed"
type="number" number :formatter="numberFilter"
></b-form-input>
</b-col>
</b-row>
<b-row><b-col cols="*">&nbsp;</b-col></b-row>
<b-row>
<b-col cols="7">{{ text.setting_bistate_support_failed}}</b-col>
<b-col cols="3">
<b-form-checkbox v-model="aggregation_parsed.bistate.use_failed"
></b-form-checkbox>
</b-col>
</b-row>
<b-row><b-col cols="*">&nbsp;</b-col></b-row>
<b-row >
<b-col cols="7">{{ text.setting_bistate_accept_pending_submitted}}</b-col>
<b-col cols="3">
<b-form-checkbox v-model="aggregation_parsed.bistate.accept_pending_as_submitted"
></b-form-checkbox>
</b-col>
</b-row>
</template>
</b-container>
</b-modal>
<mform
name="studyplan_editform"
:params="{studyplan_id: value.id, mode: mode, contextid: contextid }"
@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>
`
});

View File

@ -26,7 +26,8 @@ export default {
studyplancard: {
open: "open",
noenddate: "noenddate",
idnumber: "studyplan_idnumber"
idnumber: "studyplan_idnumber",
description: "studyplan_description"
}
});
// Create new eventbus for interaction between item components
@ -88,13 +89,27 @@ export default {
<template v-else>{{value.name}}</template>
<slot name='title'></slot>
</b-card-title>
<div class='s-studyplan-card-icon'><img :src='value.icon' style="width: 64px; height: 64px;"></div>
<div class='s-studyplan-card-idnumber' v-if='value.idnumber'><i>{{ text.idnumber}}:</i> {{ value.idnumber }}</div>
<div class='s-studyplan-card-description' v-if='value.description'>{{ value.description }}</div>
<slot></slot>
<template #footer>
<span :class="'t-timing-'+timing" v-html="startdate + ' - '+ enddate"></span>
<span class="s-studyplan-card-buttons">
<slot name='footer'></slot>
<template v-if='value.description'>
<b-button variant="primary" v-b-modal="'modal-description-'+value.id"
><i class='fa fa-info-circle'></i> {{ text.description }}</b-button>
<b-modal
:title="value.name"
scrollable
centered
ok-only
size="xl"
:id="'modal-description-'+value.id"
><span v-html="value.description"></span>
</b-modal>
</template>
<b-button style="float:right;" v-if='open' variant='primary'
@click.prevent='onOpenClick($event)'>{{ text.open }}</b-button>
</span>

View File

@ -0,0 +1,10 @@
import Debugger from './debugger';
const debug = new Debugger("formfields");
/**
* convert a text field into an integer only text field
* @param {Date|string} id The Id of the form field
*/
export function text_integer(id){
const element = document.getElementById(id);
debug.warn("Initializing form element text_integer on ",id,element);
}

View File

@ -50,6 +50,14 @@ export default {
type: String,
default: "",
},
variant: {
type: String,
default: "primary",
},
type: {
type: String,
default: "link",
}
},
data() {
return {
@ -78,12 +86,7 @@ export default {
self.loading = false;
// Process the collected javascript;
const js = processCollectedJavascript(data.javascript);
debug.info("Replacing content / el",self.$refs["content"]);
debug.info("Replacing content / html", html);
debug.info("Replacing content / js",js);
const r = replaceNodeContents(self.$refs["content"], html, js);
debug.info("R:",r);
replaceNodeContents(self.$refs["content"], html, js);
}).catch(notification.exception);
@ -104,14 +107,18 @@ export default {
call([{
methodname: 'local_treestudyplan_submit_mform',
args: {formname: self.name, params: JSON.stringify(self.params), formdata: data}
}])[0].then(()=>{
self.$emit("saved",formdata);
}])[0].then((response)=>{
const updatedplan = JSON.parse(response.data);
self.$emit("saved",updatedplan,formdata);
}).catch(notification.exception);
}
},
template: `
<span class='mform-container'><a href='#' @click.prevent="openForm"><slot><i class="fa fa-cog"></i></slot></a>
<span class='mform-container'>
<b-button :variant="variant" v-if='type == "button"' @click.prevent='openForm'
><slot><i class='fa fa-gear'></i></slot></b-button>
<a variant="variant" v-else href='#' @click.prevent='openForm'
><slot><i class='fa fa-gear'></i></slot></a>
<b-modal
ref="editormodal"
scrollable

View File

@ -21,6 +21,8 @@
*/
namespace local_treestudyplan;
use moodle_exception;
use \ValueError;
defined('MOODLE_INTERNAL') || die();
@ -71,7 +73,6 @@ abstract class aggregator {
];
}
/**
* Create a new aggregatior object based on the specified method
* @param mixed $method Aggregation method
@ -83,7 +84,7 @@ abstract class aggregator {
$agclass = self::aggregator_name($method);
return new $agclass($configstr);
} else {
throw new \ValueError("Cannot find aggregator '{$method}'");
throw new moodle_exception("Cannot find aggregator '{$method}'");
}
}

View File

@ -0,0 +1,40 @@
<?php
namespace local_treestudyplan\local\form_elements;
use MoodleQuickForm_text;
use MoodleQuickForm;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->libdir . "/form/text.php");
class text_integer extends MoodleQuickForm_text {
public function toHtml()
{
global $PAGE;
// Add number type attribute
$this->_attributes['type'] = 'number';
$html = parent::toHtml();
// Add javascript call to handle stuff
$PAGE->requires->js_call_amd('local_treestudyplan/util/formfields', 'text_integer', ['id' => $this->getAttribute('id')]);
return $html;
}
public static function Register() {
global $CFG;
MoodleQuickForm::registerElementType(
// The custom element is named `course_competency_rule`.
// This is the element name used in the `addElement()` function.
'text_integer',
// This is where it's definition is defined.
// This does not currently support class auto-loading.
"$CFG->dirroot/local/treestudyplan/classes/local/form_elements/text_integer.php",
// The class name of the element.
'local_treestudyplan\local\form_elements\text_integer'
);
}
}

View File

@ -53,14 +53,14 @@ abstract class formbase extends \moodleform {
/**
* Process the submission and perform necessary actions
* @param object $entry The processed form data
* @return bool True if submission successful
* @return object|array Data to pass to receiver if submission successful
* @throws \moodle_exception if an error must be given for a specific reason.
*/
abstract protected function process_submitted_data(object $entry);
/**
* Process the submission and perform necessary actions
* @return bool True if submission successful
* @return object|array Data to pass to receiver if submission successful
* @throws \moodle_exception if an error must be given for a specific reason.
*/
public function process_submission() {
@ -69,7 +69,7 @@ abstract class formbase extends \moodleform {
if($data) {
return $this->process_submitted_data($data);
} else {
return false;
throw new \moodle_exception('no_form_data','local_treestudyplan');
}
}

View File

@ -1,7 +1,11 @@
<?php
namespace local_treestudyplan\local\forms;
use local_treestudyplan\aggregator;
use local_treestudyplan\studyplan;
use local_treestudyplan\courseservice;
use local_treestudyplan\local\form_elements\text_integer;
use local_treestudyplan\local\helpers\webservicehelper;
use moodle_exception;
use stdClass;
@ -25,14 +29,30 @@ class studyplan_editform extends formbase {
*/
public static function init_customdata(object $params) {
$customdata = new stdClass;
$customdata->create = $params->mode=='create'?true:false;
if($customdata->create){
$customdata->context = \context::instance_by_id($params->contextid);
} else {
$customdata->plan = studyplan::find_by_id($params->studyplan_id);
$customdata->context = $customdata->plan->context();
$customdata->simplemodel = $customdata->plan->simple_model();
}
$customdata->editoroptions = [
'trusttext' => true,
'subdirs' => true,
'maxfiles' => 20,
'maxbytes' => 20*1024*1024,
'context' => \context_system::instance(),
'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;
}
@ -62,16 +82,65 @@ class studyplan_editform extends formbase {
with existing moodle code assumptions.
The form API does seem needlessly convoluted in it's use, but we need the editor...
*/
if($customdata->create) {
$entry = new stdClass;
$entry->context_id = $customdata->context->id;
$entry->aggregation = get_config("local_treestudyplan","aggregation_mode");
$ag_cfg = json_decode(aggregator::create($entry->aggregation, "")->config_string(),true);
// Determine the next august 1st for default value purposes.
$august = strtotime("first day of august this year");
if($august < time()) {
$august = strtotime("first day of august next year");
}
$entry->startdate = $august;
$entry->enddate = $august + (364*24*60*60); // Not bothering about leap years here.
$entry->periods = 4;
} else {
$entry = $DB->get_record(studyplan::TABLE, ['id' => $customdata->plan->id()]);
$entry->startdate = strtotime($customdata->simplemodel['pages'][0]['startdate']);
$entry->enddate = strtotime($customdata->simplemodel['pages'][0]['enddate']);
$entry->periods = $customdata->simplemodel['pages'][0]['periods'];
$ag_cfg = json_decode($customdata->plan->aggregator()->config_string(),true);
}
// Prepare the editor
$entry = file_prepare_standard_editor( $entry,
'description',
$customdata->editoroptions,
\context_system::instance(),
'local_treestudyplan',
'studyplan',
$customdata->plan->id());
($customdata->create)?null:$customdata->plan->id()
);
// Prepare file area for the icon
// Get an unused draft itemid which will be used for this form.
$draftitemid = file_get_submitted_draft_itemid('icon');
file_prepare_draft_area(
// The $draftitemid is the target location.
$draftitemid,
// The combination of contextid / component / filearea / itemid
// form the virtual bucket that files are currently stored in
// and will be copied from.
\context_system::instance()->id,
'local_treestudyplan',
'icon',
($customdata->create)?null:$customdata->plan->id(),
$customdata->fileoptions
);
$entry->icon = $draftitemid;
// Add aggregation configs to entry.
foreach ($ag_cfg as $key => $val) {
$entrykey = $entry->aggregation."_".$key;
$entry->$entrykey = $val;
}
return $entry;
}
/**
@ -80,7 +149,117 @@ class studyplan_editform extends formbase {
public function definition() {
$mform = $this->_form;
$customdata = (object)$this->_customdata;
// Register integer type
text_integer::Register();
// Define the form
$field = 'name';
$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 = 'idnumber';
$mform->addElement('text',$field,
get_string('studyplan_idnumber','local_treestudyplan'),
[]);
$contextlist = [];
foreach(courseservice::list_accessible_categories() as $c){
$contextlist[$c['context_id']] = implode(" / ",$c['category']['path']);
}
$mform->addElement('autocomplete', 'context_id',
get_string('studyplan_context','local_treestudyplan'),
$contextlist);
$mform->addRule('context_id', null, 'required', null, 'client');
$mform->addElement(
'filemanager',
'icon',
get_string('studyplan_icon', 'local_treestudyplan'),
null,
$customdata->fileoptions
);
$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'),
[]);
$mform->setType($field, PARAM_INT);
$mform->addRule($field, null, 'required', null, 'client');
$aggregators = [];
foreach(aggregator::list_model() as $a){
// Add method only if not deprecated or currently used
if ( $customdata->simplemodel['aggregation'] == $a['id'] || !($a['deprecated']) ) {
$aggregators[$a['id']] = $a['name'];
}
}
$mform->addElement('select','aggregation',
get_string('choose_aggregation_style','local_treestudyplan'),
$aggregators);
/* Start Bistate aggregation specific items */
$field = 'bistate_thresh_excellent';
$mform->addElement('text_integer',$field,
get_string('setting_bistate_thresh_excellent','local_treestudyplan'),
[],
);
$mform->setType($field, PARAM_INT);
$mform->hideIf($field, "aggregation", "neq", "bistate");
$field = 'bistate_thresh_good';
$mform->addElement('text_integer',$field,
get_string('setting_bistate_thresh_good','local_treestudyplan'),
[],
);
$mform->setType($field, PARAM_INT);
$mform->hideIf($field, "aggregation", "neq", "bistate");
$field = 'bistate_thresh_completed';
$mform->addElement('text_integer',$field,
get_string('setting_bistate_thresh_completed','local_treestudyplan'),
[],
);
$mform->setType($field, PARAM_INT);
$mform->hideIf($field, "aggregation", "neq", "bistate");
$field = 'bistate_use_failed';
$mform->addElement('checkbox',$field,
get_string('setting_bistate_support_failed','local_treestudyplan'),
[],
);
$mform->hideIf($field, "aggregation", "neq", "bistate");
$field = 'bistate_accept_pending_as_submitted';
$mform->addElement('checkbox',$field,
get_string('setting_bistate_accept_pending_submitted','local_treestudyplan'),
[],
);
$mform->hideIf($field, "aggregation", "neq", "bistate");
/* End Bistate aggregation specific items */
$mform->addElement('editor', 'description_editor',
get_string('studyplan_description', 'local_treestudyplan'),
null,
@ -96,6 +275,28 @@ class studyplan_editform extends formbase {
*/
protected function process_submitted_data($entry) {
$customdata = (object)$this->_customdata;
// Add aggregation configs to entry.
$ag_cfg = json_decode(aggregator::create($entry->aggregation, "")->config_string(),true); // Retrieve default config string from selected aggregation method
foreach ($ag_cfg as $key => $val) {
$entrykey = $entry->aggregation."_".$key;
$ag_cfg[$key] = $entry->$entrykey;
}
$aggregation_config = json_encode($ag_cfg);
if($customdata->create) {
// Use our own abstraction to update the record, so caches are maintained
$plan = studyplan::add(['name' => $entry->name,
'shortname' => $entry->shortname,
'idnumber' => $entry->idnumber,
'context_id' => $customdata->contextid,
'aggregation' => $entry->aggregation,
'aggregation_config' => $aggregation_config,
'startdate' => date("Y-m-d",$entry->startdate),
'enddate' => date("Y-m-d",$entry->enddate),
'periods' => $entry->periods,
]);
// Process the provided files in the description editor
$entry = file_postupdate_standard_editor($entry,
'description',
@ -103,18 +304,52 @@ class studyplan_editform extends formbase {
\context_system::instance(),
'local_treestudyplan',
'studyplan',
$customdata->plan->id());
$plan->id());
// Update the description
$plan->edit([
'description' => $entry->description,
'descriptionformat' => $entry->descriptionformat,
]);
} else {
$plan = $customdata->plan;
$f = fopen("/tmp/debug","a+");
fputs($f,print_r($entry,true));
fclose($f);
// Process the provided files in the description editor
$entry = file_postupdate_standard_editor($entry,
'description',
$customdata->editoroptions,
\context_system::instance(),
'local_treestudyplan',
'studyplan',
$plan->id());
// Use our own abstraction to update the record, so caches are maintained
$customdata->plan->edit(['description' => $entry->description,
'descriptionformat' => $entry->descriptionformat]);
$plan->edit(['name' => $entry->name,
'shortname' => $entry->shortname,
'idnumber' => $entry->idnumber,
'description' => $entry->description,
'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,
]);
}
// Now save the icon file in correct part of the File API.
file_save_draft_area_files(
// The $entry->icon property contains the itemid of the draft file area.
$entry->icon,
// The combination of contextid / component / filearea / itemid
// form the virtual bucket that file are stored in.
\context_system::instance()->id,
'local_treestudyplan',
'icon',
$plan->id(),
$customdata->fileoptions
);
return true;
return $plan->simple_model(); // Return the simple model of the plan to make sure we can update stuff
}

View File

@ -168,9 +168,9 @@ class studyitemconnection {
'to_id' => $toid,
]);
return success::success('Items Disconnected');
return success::success(['msg'=>'Items Disconnected']);
} else {
return success::success('Connection does not exist');
return success::success(['msg'=>'Connection does not exist']);
}
}

View File

@ -118,6 +118,62 @@ class studyplan {
return $this->r->name;
}
private function icon() {
global $CFG;
$fs = \get_file_storage();
// Returns an array of `stored_file` instances.
$files = $fs->get_area_files(\context_system::instance()->id, 'local_treestudyplan', 'icon', $this->id);
if (count($files) > 0 ){
$file = array_shift($files);
if ($file->get_filename() == ".") {
// Get next file if the first is the directory itself.
$file = array_shift($files);
}
$url = \moodle_url::make_pluginfile_url(
$file->get_contextid(),
$file->get_component(),
$file->get_filearea(),
$file->get_itemid(),
$file->get_filepath(),
$file->get_filename(),
false // Do not force download of the file.
);
} else {
// Try the configured default in settings.
$defaulticon = get_config('local_treestudyplan', 'defaulticon');
if (empty($defaulticon)) {
// Fall back to the standard (ugly) default image.
$url = new \moodle_url($CFG->wwwroot . "/local/treestudyplan/pix/default_icon.png");
} else {
$url = \moodle_url::make_pluginfile_url(\context_system::instance()->id, 'local_treestudyplan', 'defaulticon', 0, "/",
$defaulticon);
}
}
return $url->out();
}
/**
* Return description with all file references resolved
*/
public function description() {
$text = file_rewrite_pluginfile_urls(
// The content of the text stored in the database.
$this->r->description,
// The pluginfile URL which will serve the request.
'pluginfile.php',
// The combination of contextid / component / filearea / itemid
// form the virtual bucket that file are stored in.
\context_system::instance()->id, // System instance is always used for this
'local_treestudyplan',
'studyplan',
$this->id
);
return $text;
}
/**
* Return the studyplan pages associated with this plan
* @return studyplanpage[]
@ -160,6 +216,7 @@ class studyplan {
"context_id" => new \external_value(PARAM_INT, 'context_id of studyplan'),
"description" => new \external_value(PARAM_RAW, 'description of studyplan'),
"descriptionformat" => new \external_value(PARAM_INT, 'description format'),
"icon" => new \external_value(PARAM_RAW,'icon for this plan'),
"aggregation" => new \external_value(PARAM_TEXT, 'selected aggregator'),
"aggregation_config" => new \external_value(PARAM_TEXT, 'config string for aggregator'),
"aggregation_info" => aggregator::basic_structure(),
@ -183,8 +240,9 @@ class studyplan {
'shortname' => $this->r->shortname,
'idnumber' => $this->r->idnumber,
'context_id' => $this->context()->id,
'description' => $this->r->description,
'description' => $this->description(),
'descriptionformat' => $this->r->descriptionformat,
'icon' => $this->icon(),
'aggregation' => $this->r->aggregation,
'aggregation_config' => $this->aggregator->config_string(),
'aggregation_info' => $this->aggregator->basic_model(),
@ -204,6 +262,7 @@ class studyplan {
"idnumber" => new \external_value(PARAM_TEXT, 'idnumber of curriculum'),
"description" => new \external_value(PARAM_RAW, 'description of studyplan'),
"descriptionformat" => new \external_value(PARAM_INT, 'description format'),
"icon" => new \external_value(PARAM_RAW,'icon for this plan'),
"context_id" => new \external_value(PARAM_INT, 'context_id of studyplan'),
"aggregation" => new \external_value(PARAM_TEXT, 'selected aggregator'),
"aggregation_config" => new \external_value(PARAM_TEXT, 'config string for aggregator'),
@ -232,8 +291,9 @@ class studyplan {
'name' => $this->r->name,
'shortname' => $this->r->shortname,
'idnumber' => $this->r->idnumber,
'description' => $this->r->description,
'description' => $this->description(),
'descriptionformat' => $this->r->descriptionformat,
'icon' => $this->icon(),
'context_id' => $this->context()->id,
"aggregation" => $this->r->aggregation,
"aggregation_config" => $this->aggregator->config_string(),
@ -562,6 +622,7 @@ class studyplan {
"shortname" => new \external_value(PARAM_TEXT, 'shortname of studyplan'),
"description" => new \external_value(PARAM_RAW, 'description of studyplan'),
"descriptionformat" => new \external_value(PARAM_INT, 'description format'),
"icon" => new \external_value(PARAM_RAW,'icon for this plan'),
"idnumber" => new \external_value(PARAM_TEXT, 'idnumber of curriculum'),
"pages" => new \external_multiple_structure(studyplanpage::user_structure()),
"aggregation_info" => aggregator::basic_structure(),
@ -580,8 +641,9 @@ class studyplan {
'id' => $this->r->id,
'name' => $this->r->name,
'shortname' => $this->r->shortname,
'description' => $this->r->description,
'description' => $this->description(),
'descriptionformat' => $this->r->descriptionformat,
'icon' => $this->icon(),
'idnumber' => $this->r->idnumber,
'pages' => [],
'aggregation_info' => $this->aggregator->basic_model(),
@ -618,8 +680,13 @@ class studyplan {
'name' => $name,
'shortname' => $shortname,
'description' => $this->r->description,
'descriptionformat' => $this->r->descriptionformat,
'aggregation' => $this->r->aggregation,
'aggregation_config' => $this->r->aggregation_config
]);
//TODO: Copy any files related to this userid....
// Next, copy the studylines.
foreach ($this->pages() as $p) {

View File

@ -41,27 +41,31 @@ class success {
/**
* Create new successful result with optional message
* @param string $msg Message to add to result
* @param array|object $data Custom data to pass to receiver
*/
public static function success($msg = "") : self {
return new self(true, $msg);
public static function success($data=[]) : self {
return new self(true, 'success', $data);
}
/**
* Create new failed result with optional message
* @param string $msg Message to add to result
* @param array|object $data Custom data to pass to receiver
*/
public static function fail($msg = "") : self {
return new self(false, $msg);
public static function fail($msg = "", $data=[]) : self {
return new self(false, $msg, $data);
}
/**
* Create new succes result
* @param bool $success Whether result is succesful or not
* @param string $msg Message to add to result
* @param array|object $data Custom data to pass to receiver
*/
public function __construct($success, $msg) {
public function __construct($success, $msg, $data=[]) {
$this->success = ($success) ? true : false;
$this->msg = $msg;
$this->data = json_encode($data);
}
/**
@ -71,6 +75,7 @@ class success {
return new \external_single_structure([
"success" => new \external_value(PARAM_BOOL, 'operation completed succesfully'),
"msg" => new \external_value(PARAM_TEXT, 'message'),
"data" => new \external_value(PARAM_RAW, 'message'),
]);
}
@ -80,7 +85,7 @@ class success {
* @return array Webservice value
*/
public function model() {
return ["success" => $this->success, "msg" => $this->msg];
return ["success" => $this->success, "msg" => $this->msg, "data" => $this->data];
}
/**
@ -101,4 +106,13 @@ class success {
return $this->msg;
}
/**
* Get data
*
* @return string Message
*/
public function data() {
return json_decode($this->data);
}
}

View File

@ -157,11 +157,8 @@ class utilityservice extends \external_api {
// Load the form, provide submitted form data and perform security checks
$mform = self::load_mform($formname, $params, $ajaxformdata);
if ($mform->process_submission() !== false) {
return success::success()->model();
} else {
return success::fail("Error in submission data")->model();
}
$return = $mform->process_submission();
return success::success($return)->model();
}

View File

@ -27,7 +27,7 @@
<FIELD NAME="name" TYPE="text" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="shortname" TYPE="text" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="description" TYPE="text" NOTNULL="false" SEQUENCE="false"/>
<FIELD NAME="descriptionformat" TYPE="int" LENGTH="4" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
<FIELD NAME="descriptionformat" TYPE="int" LENGTH="4" NOTNULL="true" DEFAULT="1" SEQUENCE="false"/>
<FIELD NAME="idnumber" TYPE="text" NOTNULL="false" SEQUENCE="false"/>
<FIELD NAME="aggregation" TYPE="char" LENGTH="30" NOTNULL="true" DEFAULT="bistate" SEQUENCE="false"/>
<FIELD NAME="aggregation_config" TYPE="text" NOTNULL="false" SEQUENCE="false"/>
@ -170,6 +170,7 @@
<FIELD NAME="fullname" TYPE="text" NOTNULL="false" SEQUENCE="false"/>
<FIELD NAME="shortname" TYPE="text" NOTNULL="false" SEQUENCE="false"/>
<FIELD NAME="description" TYPE="text" NOTNULL="false" SEQUENCE="false"/>
<FIELD NAME="descriptionformat" TYPE="int" LENGTH="4" NOTNULL="true" DEFAULT="1" SEQUENCE="false"/>
<FIELD NAME="startdate" TYPE="text" NOTNULL="false" SEQUENCE="false"/>
<FIELD NAME="enddate" TYPE="text" NOTNULL="false" SEQUENCE="false"/>
</FIELDS>

View File

@ -497,7 +497,7 @@ function xmldb_local_treestudyplan_upgrade($oldversion) {
// Define field descriptionformat to be added to local_treestudyplan.
$table = new xmldb_table('local_treestudyplan');
$field = new xmldb_field('descriptionformat', XMLDB_TYPE_INTEGER, '4', null, XMLDB_NOTNULL, null, '0', 'description');
$field = new xmldb_field('descriptionformat', XMLDB_TYPE_INTEGER, '4', null, XMLDB_NOTNULL, null, '1', 'description');
// Conditionally launch add field descriptionformat.
if (!$dbman->field_exists($table, $field)) {
@ -507,6 +507,19 @@ function xmldb_local_treestudyplan_upgrade($oldversion) {
// Treestudyplan savepoint reached.
upgrade_plugin_savepoint(true, 2023101900, 'local', 'treestudyplan');
}
if ($oldversion < 2023102200) {
// Define field descriptionformat to be added to local_treestudyplan.
$table = new xmldb_table('local_treestudyplan_page');
$field = new xmldb_field('descriptionformat', XMLDB_TYPE_INTEGER, '4', null, XMLDB_NOTNULL, null, '1', 'description');
// Conditionally launch add field descriptionformat.
if (!$dbman->field_exists($table, $field)) {
$dbman->add_field($table, $field);
}
// Treestudyplan savepoint reached.
upgrade_plugin_savepoint(true, 2023102200, 'local', 'treestudyplan');
}
return true;
}

View File

@ -128,7 +128,6 @@ print $OUTPUT->header();
@click='selectStudyplan(studyplan)'>{{ studyplan.name }}</b-form-select-option>
</b-form-select>&nbsp;
<t-studyplan-edit
@creating=""
@created="onStudyPlanCreated"
v-if='!activestudyplan && !loadingstudyplan'
mode="create"

57
lib.php
View File

@ -404,23 +404,37 @@ function local_treestudyplan_pluginfile(
): bool {
global $DB,$USER;
$fp = fopen("/tmp/debug","a+");
fputs($fp, print_r([
'context' => $context,
'filearea' => $filearea,
'args' => $args,
'forcedownload' => $forcedownload,
'options' => $options,
],true)."\n\n");
fclose($fp);
$studyplan_filecaps = ["local/treestudyplan:editstudyplan","local/treestudyplan:viewuserreports"];
// Check the contextlevel is as expected - the studyplan plugin only uses system context for storing files.
// This avoids headaches when moving studyplans, while the security impact is minimal...
// This avoids headaches when moving studyplans between contexts, while the security impact is minimal...
if ($context->contextlevel != CONTEXT_SYSTEM) {
return false;
}
// Make sure the filearea is one of those used by the plugin.
if ($filearea == "studyplan") {
if (in_array($filearea,["studyplan","icon"])) {
// The args is an array containing [itemid, path].
// Fetch the itemid from the path.
$itemid = array_shift($args);
$plan = studyplan::find_by_id($itemid);
$planctx = $plan->context();
// Check if the current user has access to this studyplan
if (webservicehelper::has_capabilities(['',''],$planctx) || $plan->has_linked_user($USER)) {
// Extract the filename / filepath from the $args array.
if ( webservicehelper::has_capabilities($studyplan_filecaps,$planctx) || $plan->has_linked_user($USER)) {
// Extract the filename / filepath from the $args array
$filename = array_pop($args); // The last item in the $args array.
if (empty($args)) {
// $args is empty => the path is '/'.
@ -432,12 +446,39 @@ function local_treestudyplan_pluginfile(
// Retrieve the file from the Files API.
$fs = get_file_storage();
$file = $fs->get_file(\context_system::instance(), 'local_treestudyplan', $filearea, $itemid, $filepath, $filename);
$file = $fs->get_file(\context_system::instance()->id, 'local_treestudyplan', $filearea, $itemid, $filepath, $filename);
if (!$file) {
// The file does not exist.
return false;
}
// We can now send the file back to the browser - in this case with a cache lifetime of 1 day and no filtering.
send_stored_file($file, 24*60*60, 0, $forcedownload, $options);
} else {
return false;
}
} else if (in_array($filearea,['defaulticon'])) {
// The args is an array containing [itemid, path].
// Fetch the itemid from the path.
$itemid = array_shift($args);
// Extract the filename / filepath from the $args array
$filename = array_pop($args); // The last item in the $args array.
if (empty($args)) {
// $args is empty => the path is '/'.
$filepath = '/';
} else {
// $args contains the remaining elements of the filepath.
$filepath = '/' . implode('/', $args) . '/';
}
// Retrieve the file from the Files API.
$fs = get_file_storage();
$file = $fs->get_file(\context_system::instance()->id, 'local_treestudyplan', $filearea, $itemid, $filepath, $filename);
if (!$file) {
// The file does not exist.
return false;
}
// We can now send the file back to the browser - in this case with a cache lifetime of 1 day and no filtering.
send_stored_file($file, 24*60*60, 0, $forcedownload, $options);
} else {
@ -446,10 +487,4 @@ function local_treestudyplan_pluginfile(
} else {
return false;
}
}

BIN
pix/default_icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

View File

@ -54,6 +54,16 @@ if ($hassiteconfig) {
true,
));
// Default image for study plans
$page->add(new admin_setting_configstoredfile('local_treestudyplan/defaulticon',
get_string('setting_defaulticon', 'local_treestudyplan'),
get_string('setting_defaulticon_desc', 'local_treestudyplan'),
'defaulticon', 0,
[
'maxfiles' => 1,
'accepted_types' => ['.jpg', '.png']]
));
// OUTCOME AGGREGATION SETTINGS.
$page->add(new admin_setting_heading('local_treestudyplan/aggregation_heading',
get_string('setting_aggregation_heading', 'local_treestudyplan'),

View File

@ -22,7 +22,7 @@
defined('MOODLE_INTERNAL') || die();
$plugin->component = 'local_treestudyplan'; // Recommended since 2.0.2 (MDL-26035). Required since 3.0 (MDL-48494).
$plugin->version = 2023102100; // YYYYMMDDHH (year, month, day, iteration).
$plugin->version = 2023102300; // YYYYMMDDHH (year, month, day, iteration).
$plugin->requires = 2021051700; // YYYYMMDDHH (This is the release version for Moodle 3.11).
$plugin->release = "1.1.0-b";