From 9a1b32855fbca8639ea1a13927fd881e11addb2e Mon Sep 17 00:00:00 2001 From: PMKuipers Date: Wed, 15 May 2024 22:57:54 +0200 Subject: [PATCH] Added IDNumber getter and did some polishing --- .../studyplan-editor-components.min.js.map | 2 +- amd/src/studyplan-editor-components.js | 20 +++--- classes/studyplan.php | 67 +++++++++++++++++-- version.php | 2 +- 4 files changed, 75 insertions(+), 16 deletions(-) diff --git a/amd/build/studyplan-editor-components.min.js.map b/amd/build/studyplan-editor-components.min.js.map index d6554b8..0c28627 100644 --- a/amd/build/studyplan-editor-components.min.js.map +++ b/amd/build/studyplan-editor-components.min.js.map @@ -1 +1 @@ -{"version":3,"file":"studyplan-editor-components.min.js","sources":["../src/studyplan-editor-components.js"],"sourcesContent":["/*eslint no-var: \"error\"*/\n/*eslint no-console: \"off\"*/\n/*eslint no-unused-vars: warn */\n/*eslint max-len: [\"error\", { \"code\": 160 }] */\n/*eslint-disable no-trailing-spaces */\n/*eslint-env es6*/\n// Put this file in path/to/plugin/amd/src\n\nimport {SimpleLine} from \"./simpleline/simpleline\";\nimport {call} from 'core/ajax';\nimport notification from 'core/notification';\nimport {get_strings} from 'core/str';\nimport {load_stringkeys, load_strings, strformat } from './util/string-helper';\nimport {format_date, add_days, datespaninfo } from './util/date-helper';\nimport {objCopy,transportItem} from './studyplan-processor';\nimport Debugger from './util/debugger';\nimport Config from 'core/config';\nimport {download,upload} from './downloader';\nimport {ProcessStudyplan, ProcessStudyplanPage} from './studyplan-processor';\n/*import {eventTypes as editSwEventTypes} from 'core/edit_switch';*/\nimport { premiumenabled } from \"./util/premium\";\nimport { settings } from \"./util/settings\";\nimport TSComponents from './treestudyplan-components';\nimport mFormComponents from \"./util/mform-helper\";\nimport pSideBarComponents from \"./util/psidebar-vue\";\n\nimport {Drag, Drop, DropList} from './vue-easy-dnd/vue-easy-dnd.esm';\n\n\n\nconst STUDYPLAN_EDITOR_FIELDS =\n['name','shortname','description','idnumber','context_id', 'aggregation','aggregation_config'];\nconst STUDYPLAN_EDITOR_PAGE_FIELDS = //TODO: Add 'fullname', 'shortname' and 'description' when implementing proper page management\n['context_id', 'periods','startdate','enddate'];\nconst PERIOD_EDITOR_FIELDS =\n['fullname','shortname','startdate','enddate'];\n\nconst LINE_GRAVITY = 1.3;\n\nconst datechanger_globals = {\n default: false,\n defaultchoice: false,\n hidewarn: false,\n};\nexport default {\n STUDYPLAN_EDITOR_FIELDS: STUDYPLAN_EDITOR_FIELDS, // make copy available in plugin\n install(Vue/*,options*/){\n Vue.component('drag',Drag);\n Vue.component('drop',Drop);\n Vue.component('drop-list',DropList);\n Vue.use(TSComponents);\n Vue.use(mFormComponents);\n Vue.use(pSideBarComponents);\n let debug = new Debugger(\"treestudyplan-editor\");\n /************************************\n * *\n * Treestudyplan Editor components *\n * *\n ************************************/\n\n /**\n * Check if element is visible\n * @param {Object} elem The element to check\n * @returns {boolean} True if visible\n */\n function isVisible(elem){\n return !!( elem.offsetWidth || elem.offsetHeight || elem.getClientRects().length );\n }\n\n // Create new eventbus for interaction between item components\n const ItemEventBus = new Vue();\n\n /*\n // Add event listener for the edit mode event so we can react to it, or at the very least ignore it\n document.addEventListener(editSwEventTypes.editModeSet,(e) => {\n e.preventDefault();\n ItemEventBus.$emit('editModeSet', e.detail.editMode);\n });\n */\n\n let string_keys = load_stringkeys({\n conditions: [\n { value: 'ALL', textkey: 'condition_all'},\n { value: 'ANY', textkey: 'condition_any'},\n ],\n });\n\n let strings = load_strings({\n studyplan_text: {\n studyline_editmode: 'studyline_editmode',\n toolbox_toggle: 'toolbox_toggle',\n editmode_modules_hidden:'editmode_modules_hidden',\n studyline_add: 'studyline_add',\n add$core: 'add$core',\n edit$core: 'edit$core',\n studyline_name: 'studyline_name',\n studyline_name_ph: 'studyline_name_ph',\n studyline_shortname: 'studyline_shortname',\n studyline_shortname_ph: 'studyline_shortname_ph',\n studyline_enrollable: 'studyline_enrollable',\n studyline_enrolroles: 'studyline_enrolroles',\n studyline_color: 'studyline_color',\n associations: 'associations',\n associated_cohorts: 'associated_cohorts',\n associated_users: 'associated_users',\n studyline_edit: 'studyline_edit',\n studyplan_name: 'studyplan_name',\n studyplan_name_ph: 'studyplan_name_ph',\n studyplan_shortname: 'studyplan_shortname',\n studyplan_shortname_ph: 'studyplan_shortname_ph',\n studyplan_description: 'studyplan_description',\n studyplan_description_ph: 'studyplan_description_ph',\n studyplan_idnumber: 'studyplan_idnumber',\n studyplan_idnumber_ph: 'studyplan_idnumber_ph',\n studyplan_slots: 'studyplan_slots',\n studyplan_startdate: 'studyplan_startdate',\n studyplan_enddate: 'studyplan_enddate',\n line_enrollable_0: 'line_enrollable:0',\n line_enrollable_1: 'line_enrollable:1',\n line_enrollable_2: 'line_enrollable:2',\n line_enrollable_3: 'line_enrollable:3',\n\n },\n studyplan_advanced: {\n advanced_tools: 'advanced_tools',\n confirm_cancel: 'confirm_cancel',\n confirm_ok: 'confirm_ok',\n success$core: 'success$core',\n error$core: 'failed$core',\n advanced_converted: 'advanced_converted',\n advanced_skipped: 'advanced_skipped',\n advanced_failed: 'advanced_failed',\n advanced_locked: 'advanced_locked',\n advanced_multiple: 'advanced_multiple',\n advanced_error: 'advanced_error',\n advanced_tools_heading: 'advanced_tools_heading',\n advanced_warning_title: 'advanced_warning_title',\n advanced_warning: 'advanced_warning',\n advanced_pick_scale: 'advanced_pick_scale',\n advanced_course_manipulation_title: 'advanced_course_manipulation_title',\n advanced_bulk_course_timing: 'advanced_bulk_course_timing', \n advanced_bulk_course_timing_desc: 'advanced_bulk_course_timing_desc', \n advanced_force_scale_title: 'advanced_force_scale_title',\n advanced_force_scale_desc: 'advanced_force_scale_desc',\n advanced_force_scale_button: 'advanced_force_scale_button',\n advanced_confirm_header: 'advanced_confirm_header',\n advanced_force_scale_confirm: 'advanced_force_scale_confirm',\n advanced_backup_restore: 'advanced_backup_restore',\n advanced_restore: 'advanced_restore',\n advanced_backup: 'advanced_backup',\n advanced_restore_pages: 'advanced_restore_pages',\n advanced_restore_lines: 'advanced_restore_lines',\n advanced_backup_plan: 'advanced_backup_plan',\n advanced_backup_page: 'advanced_backup_page',\n advanced_export: 'advanced_export',\n advanced_export_csv_plan: 'advanced_export_csv_plan',\n advanced_export_csv_page: 'advanced_export_csv_page',\n advanced_import_from_file: 'advanced_import_from_file',\n advanced_purge: \"advanced_purge\",\n advanced_purge_plan: \"advanced_purge_plan\",\n advanced_purge_plan_expl: \"advanced_purge_plan_expl\",\n advanced_purge_page: \"advanced_purge_page\",\n advanced_purge_page_expl: \"advanced_purge_page_expl\",\n advanced_cascade_cohortsync_title: \"advanced_cascade_cohortsync_title\",\n advanced_cascade_cohortsync_desc: \"advanced_cascade_cohortsync_desc\",\n advanced_cascade_cohortsync: \"advanced_cascade_cohortsync\",\n currentpage: \"currentpage\",\n },\n studyplan_edit: {\n studyplan_edit: 'studyplan_edit',\n studyplan_add: 'studyplan_add',\n studyplanpage_add: 'studyplanpage_add',\n studyplanpage_edit: 'studyplanpage_edit',\n info_periodsextended: 'studyplanpage_info_periodsextended',\n warning: 'warning@core',\n },\n period_edit: {\n edit: 'period_edit',\n fullname: 'studyplan_name',\n shortname: 'studyplan_shortname',\n startdate: 'studyplan_startdate',\n enddate: 'studyplan_enddate',\n },\n course_timing: {\n title: 'course_timing_title',\n desc: 'course_timing_desc',\n question: 'course_timing_question',\n warning: 'course_timing_warning',\n timing_ok: 'course_timing_ok',\n timing_off: 'course_timing_off',\n course: 'course@core',\n period: 'period',\n yes: 'yes$core',\n no: 'no$core',\n duration: 'duration',\n years: 'years$core',\n year: 'year$core',\n weeks: 'weeks$core',\n week: 'week$core',\n days: 'days$core',\n day: 'day$core',\n rememberchoice: 'course_timing_rememberchoice',\n hidewarning: 'course_timing_hidewarning',\n periodspan: 'course_period_span',\n periods: 'periods',\n periodspan_desc: 'course_period_span_desc',\n },\n studyplan_associate: {\n associations: 'associations',\n associated_cohorts: 'associated_cohorts',\n associated_users: 'associated_users',\n associated_coaches: 'associated_coaches',\n associate_cohorts: 'associate_cohorts',\n associate_users: 'associate_users',\n associate_coached: 'associate_coaches',\n add_association: 'add_association',\n delete_association: 'delete_association',\n associations_empty: 'associations_empty',\n associations_search: 'associations_search',\n cohorts: 'cohorts',\n users: 'users',\n coaches: 'coaches',\n selected: 'selected',\n name: 'name',\n context: 'context',\n search: 'search',\n },\n item_text: {\n select_conditions: \"select_conditions\",\n item_configuration: \"item_configuration\",\n ok: \"ok@core\",\n delete: \"delete@core\",\n item_delete_message: \"item_delete_message\",\n type_course: \"course@core\",\n type_junction: \"tool-junction\",\n type_start: \"tool-start\",\n type_finish: \"tool-finish\",\n type_badge: \"tool-badge\",\n type_invalid: \"course-invalid\",\n },\n item_course_text: {\n select_conditions: \"select_conditions\",\n select_grades: \"select_grades\",\n coursetiming_past: \"coursetiming_past\",\n coursetiming_present: \"coursetiming_present\",\n coursetiming_future: \"coursetiming_future\",\n grade_include: \"grade_include\",\n grade_require: \"grade_require\",\n ok: \"ok@core\",\n cancel: \"cancel@core\",\n delete: \"delete@core\",\n noenddate: \"noenddate\",\n },\n invalid: {\n error: 'error',\n },\n completion: {\n completion_completed: \"completion_completed\",\n completion_incomplete: \"completion_incomplete\",\n aggregation_all: \"aggregation_all\",\n aggregation_any: \"aggregation_any\",\n aggregation_overall_all: \"aggregation_overall_all\",\n aggregation_overall_any: \"aggregation_overall_any\",\n completion_not_configured: \"completion_not_configured\",\n configure_completion: \"configure_completion\",\n },\n competency: {\n competency_not_configured: \"competency_not_configured\",\n configure_competency: \"configure_competency\",\n when: \"when\",\n required: \"required\",\n points: \"points@core_grades\",\n heading: \"competency_heading\",\n details: \"competency_details\",\n },\n badge: {\n share_badge: \"share_badge\",\n dateissued: \"dateissued\",\n dateexpire: \"dateexpire\",\n badgeinfo: \"badgeinfo\",\n },\n toolbox: {\n toolbox: 'toolbox',\n toolbarRight: 'toolbar-right',\n courses: 'courses',\n flow: 'flow',\n toolJunction: 'tool-junction',\n toolFinish: 'tool-finish',\n toolStart: 'tool-start',\n badges: 'badges',\n relatedbages: 'relatedbages@badges',\n filter: 'filter@core',\n sitebadges: 'sitebadges@core',\n }\n });\n\n /*\n * T-STUDYPLAN-ADVANCED\n */\n Vue.component('t-studyplan-advanced', {\n props: {\n value: {\n type: Object,\n default(){ return null;},\n },\n selectedpage: {\n type: Object,\n default(){ return null;},\n }\n\n },\n data() {\n return {\n force_scales: {\n selected_scale: null,\n result: [],\n },\n text: strings.studyplan_advanced,\n };\n },\n created() {\n },\n mounted() {\n },\n updated() {\n },\n computed: {\n scales(){\n return [{\n id: null,\n disabled: true,\n name: this.text.advanced_pick_scale,\n }].concat(this.value.advanced.force_scales.scales);\n },\n },\n methods: {\n force_scales_start(){\n // set confirmation box\n const self=this;\n this.$bvModal.msgBoxConfirm(this.text.advanced_force_scale_confirm,{\n title: this.text.advanced_force_scale_confirm,\n okVariant: 'danger',\n okTitle: this.text.confirm_ok,\n cancelTitle: this.text.confirm_cancel,\n }).then( value => {\n if(value == true){\n call([{\n methodname: 'local_treestudyplan_force_studyplan_scale',\n args: {\n studyplan_id: this.value.id,\n scale_id: this.force_scales.selected_scale,\n }\n }])[0].then(function(response){\n self.force_scales.result = response;\n }).catch(notification.exception);\n }\n });\n },\n export_page(format){\n const self = this;\n if(format == undefined || ![\"json\",\"csv\"].includes(format)){\n format = \"json\";\n }\n call([{\n methodname: 'local_treestudyplan_export_page',\n args: {\n page_id: this.selectedpage.id,\n format: format,\n },\n }])[0].then(function(response){\n\n download(self.value.shortname+\".page.\"+format,response.content,response.format);\n }).catch(notification.exception);\n },\n export_plan(){\n const self = this;\n call([{\n methodname: 'local_treestudyplan_export_plan',\n args: {\n studyplan_id: this.value.id,\n format: \"json\",\n },\n }])[0].then(function(response){\n download(self.value.shortname+\".plan.json\",response.content,response.format);\n }).catch(notification.exception);\n },\n bulk_course_timing() {\n const self = this;\n call([{\n methodname: 'local_treestudyplan_bulk_course_timing',\n args: {\n page_id: this.selectedpage.id,\n },\n }])[0].then(function(response){\n if(response.success){\n // Reloading the webpage saves trouble reloading the specific page updated.\n location.reload();\n } else {\n this.$bvModal.msgBoxOk(response.msg, {title: \"Could not set bulk course timing\"} );\n debug.error(\"Could not set bulk course timing: \",response.msg);\n }\n }).catch(notification.exception);\n },\n import_studylines(){\n //const self = this;\n upload((filename,content)=>{\n call([{\n methodname: 'local_treestudyplan_import_studylines',\n args: {\n page_id: this.selectedpage.id,\n content: content,\n format: \"application/json\",\n },\n }])[0].then(function(response){\n if(response.success){\n location.reload();\n } else {\n this.$bvModal.msgBoxOk(response.msg, {title: \"Import failed\"} );\n debug.error(\"Import failed: \",response.msg);\n }\n }).catch(notification.exception);\n }, \"application/json\");\n },\n import_pages(){\n //const self = this;\n upload((filename,content)=>{\n call([{\n methodname: 'local_treestudyplan_import_pages',\n args: {\n studyplan_id: this.value.id,\n content: content,\n format: \"application/json\",\n },\n }])[0].then(function(response){\n if(response.success){\n location.reload();\n } else {\n this.$bvModal.msgBoxOk(response.msg, {title: \"Import failed\"} );\n debug.error(\"Import failed: \",response.msg);\n }\n }).catch(notification.exception);\n }, \"application/json\");\n },\n purge_studyplan(){\n call([{\n methodname: 'local_treestudyplan_delete_studyplan',\n args: {\n id: this.value.id,\n force: true,\n },\n }])[0].then(function(response){\n if(response.success){\n location.reload();\n } else {\n this.$bvModal.msgBoxOk(response.msg, {title: \"Could not delete plan \"} );\n debug.error(\"Could not delete plan: \",response.msg);\n }\n }).catch(notification.exception);\n },\n purge_studyplanpage(){\n if (this.selectedpage) {\n call([{\n methodname: 'local_treestudyplan_delete_studyplanpage',\n args: {\n id: this.selectedpage.id,\n force: true,\n },\n }])[0].then(function(response){\n if(response.success){\n location.reload();\n } else {\n this.$bvModal.msgBoxOk(response.msg, {title: \"Could not delete page\"} );\n debug.error(\"Could not delete page: \",response.msg);\n }\n }).catch(notification.exception);\n }\n },\n cascade_cohortsync(){\n const self = this;\n call([{\n methodname: 'local_treestudyplan_cascade_cohortsync',\n args: {\n studyplan_id: this.value.id,\n },\n }])[0].then(function(response){\n self.$bvModal.msgBoxOk(response.success?self.text.success$core:self.text.error$core,\n { title: self.text.advanced_cascade_cohortsync});\n }).catch(notification.exception);\n },\n modal_close(){\n this.force_scales.result = [];\n }\n },\n template:\n `\n \n {{text.advanced_tools}}\n \n \n \n {{ text.advanced_warning}}\n \n \n

{{ text.advanced_cascade_cohortsync_title}}

\n

{{ text.advanced_cascade_cohortsync_desc}}

\n

{{ text.advanced_cascade_cohortsync}}

\n

{{ text.advanced_bulk_course_timing}}

\n

{{ text.advanced_bulk_course_timing_desc}}

\n

{{text.currentpage}} {{selectedpage.fullname}}

\n

{{ text.advanced_bulk_course_timing}}

\n \n
\n \n

{{ text.advanced_backup }}

\n

{{ text.advanced_backup_page }}\n {{text.currentpage}} {{selectedpage.fullname}}

\n

{{ text.advanced_backup_plan }}

\n

{{ text.advanced_restore }}

\n

{{ text.advanced_restore_lines}}

\n

{{ text.advanced_restore_pages }}

\n

{{ text.advanced_export }}

\n

{{ text.advanced_export_csv_page }}\n {{text.currentpage}} {{selectedpage.fullname}}

\n
\n \n

{{text.advanced_purge_page_expl}}

\n

{{text.currentpage}} {{selectedpage.fullname}}

\n

{{ text.advanced_purge_page}}

\n

{{text.advanced_purge_plan_expl}}

\n

{{ text.advanced_purge_plan}}

\n
\n
\n
\n
\n `\n });\n\n\n /*\n * T-STUDYPLAN-EDIT\n */\n Vue.component('t-studyplan-edit', {\n props: {\n 'value' :{\n type: Object,\n default(){ return null;},\n },\n 'mode' :{\n type: String,\n default() { return \"edit\";},\n },\n 'type' :{\n type: String,\n default() { return \"link\";},\n },\n 'variant' : {\n type: String,\n default() { return \"\";},\n },\n 'contextid': {\n type: Number,\n default: 1\n },\n },\n data() {\n return {\n text: strings.studyplan_edit,\n };\n },\n computed: {\n },\n methods: {\n planSaved(updatedplan){\n const self = this;\n debug.info(\"Got new plan data\",updatedplan);\n\n if(self.mode == 'create'){\n // Inform parent of the details of the newly created plan\n self.$emit(\"created\",updatedplan);\n }\n else {\n // determine if the plan moved context...\n const moved_from = self.value.context_id;\n const moved_to = updatedplan.context_id;\n const moved = (moved_from != moved_to);\n\n if(updatedplan.pages[0].periods != self.value.pages[0].periods){\n // If the pages changed, just reload the entire model for the plan\n call([{\n methodname: 'local_treestudyplan_get_studyplan_map',\n args: { id: self.value.id}\n }])[0].then(function(response){\n self.value = ProcessStudyplan(response,true);\n debug.info('studyplan processed');\n self.$emit('input',self.value);\n }).catch(function(error){\n notification.exception(error);\n });\n } else {\n // Copy updated fields and trigger update\n objCopy(self.value,updatedplan,STUDYPLAN_EDITOR_FIELDS);\n self.$emit('input',self.value);\n if(moved){\n self.$emit('moved',self.value, moved_from, moved_to);\n }\n }\n }\n },\n }\n ,\n template:\n `\n \n \n \n `\n });\n\n /*\n * T-STUDYPLAN-EDIT\n */\n Vue.component('t-studyplan-page-edit', {\n props: {\n 'value' :{\n type: Object,\n default(){ return null;},\n },\n 'mode' :{\n type: String,\n default() { return \"edit\";},\n },\n 'type' :{\n type: String,\n default() { return \"link\";},\n },\n 'variant' : {\n type: String,\n default() { return \"\";},\n },\n 'studyplan': {\n type: Object,\n },\n },\n data() {\n return {\n text: strings.studyplan_edit,\n };\n },\n computed: {\n },\n methods: {\n planSaved(updatedpage){\n const self = this;\n\n if(self.mode == 'create'){\n // Inform parent of the details of the newly created plan\n self.$emit(\"created\",updatedpage);\n }\n else {\n const page = ProcessStudyplanPage(updatedpage);\n debug.info('studyplan page processed');\n\n if (self.value.periods < page.periods) {\n this.$bvModal.msgBoxOk(this.text.info_periodsextended, {\n title: this.text.warning,\n okVariant: 'success',\n centered: true\n });\n }\n self.$emit('input',page);\n\n }\n },\n }\n ,\n template:\n `\n \n \n \n `\n });\n\n\n /*\n * T-STUDYPLAN-ASSOCIATE\n */\n Vue.component('t-studyplan-associate', {\n props: ['value',],\n data() {\n return {\n show: false,\n config: {\n userfields: [\n { key: \"selected\",},\n { key: \"firstname\", \"sortable\": true,},\n { key: \"lastname\", \"sortable\": true,},\n ],\n cohortfields:[\n { key: \"selected\",},\n { key: \"name\", \"sortable\": true,},\n { key: \"context\", \"sortable\": true,},\n ]\n },\n association: {\n cohorts: [],\n users: [],\n coaches: []\n },\n loading: {\n cohorts: false,\n users: false,\n coaches: false,\n },\n search: {users: [], cohorts:[], coaches:[]},\n selected: {\n search: {users: [] , cohorts:[], coaches: []},\n associated: {users: [] , cohorts:[], coaches: []}\n },\n text: strings.studyplan_associate,\n };\n },\n created() {\n\n },\n mounted() {\n },\n updated() {\n\n },\n methods: {\n premiumenabled,\n showModal(){\n this.show = true;\n this.loadAssociations();\n },\n cohortOptionModel(c){\n return {\n value: c.id,\n text: c.name + ' (' + c.context.path.join(' / ') + ')',\n };\n },\n userOptionModel(u){\n return {\n value: u.id,\n text: u.firstname + ' ' + u.lastname,\n };\n },\n loadAssociations(){\n const self = this;\n self.loading.cohorts = true;\n self.loading.users = true;\n call([{\n methodname: 'local_treestudyplan_associated_users',\n args: { studyplan_id: self.value.id,}\n }])[0].then(function(response){\n self.association.users = response.map(self.userOptionModel);\n self.loading.users = false;\n }).catch(notification.exception);\n\n call([{\n methodname: 'local_treestudyplan_associated_cohorts',\n args: { studyplan_id: self.value.id,}\n }])[0].then(function(response){\n self.association.cohorts = response.map(self.cohortOptionModel);\n self.loading.cohorts = false;\n }).catch(notification.exception);\n\n if(premiumenabled()) {\n self.loading.coaches = true;\n call([{\n methodname: 'local_treestudyplan_associated_coaches',\n args: { studyplan_id: self.value.id,}\n }])[0].then(function(response){\n self.association.coaches = response.map(self.userOptionModel);\n self.loading.coaches = false;\n }).catch(notification.exception);\n }\n },\n searchCohorts(searchtext){\n const self = this;\n\n if(searchtext.length > 0)\n {\n call([{\n methodname: 'local_treestudyplan_list_cohort',\n args: { like: searchtext, exclude_id: self.value.id}\n }])[0].then(function(response){\n self.search.cohorts = response.map(self.cohortOptionModel);\n }).catch(notification.exception);\n }\n else {\n self.search.cohorts = [];\n }\n },\n cohortAssociate(){\n const self = this;\n let requests = [];\n const associated = self.association.cohorts;\n const search = self.search.cohorts;\n const searchselected = self.selected.search.cohorts;\n for(const i in searchselected){\n const r = searchselected[i];\n requests.push({\n methodname: 'local_treestudyplan_connect_cohort',\n args: {studyplan_id: self.value.id, cohort_id: r},\n fail: notification.exception,\n done: function(response){\n if(response.success){\n transportItem(associated,search,r);\n }\n }\n });\n }\n call(requests);\n },\n cohortDisassociate(){\n const self = this;\n let requests = [];\n const associatedselected = self.selected.associated.cohorts;\n const associated = self.association.cohorts;\n const search = self.search.cohorts;\n for(const i in associatedselected){\n const r = associatedselected[i];\n requests.push({\n methodname: 'local_treestudyplan_disconnect_cohort',\n args: {studyplan_id: self.value.id, cohort_id: r},\n fail: notification.exception,\n done: function(response){\n if(response.success){\n transportItem(search,associated,r);\n }\n }\n });\n }\n call(requests);\n },\n searchUsers(searchtext){\n const self = this;\n if(searchtext.length > 0)\n {\n call([{\n methodname: 'local_treestudyplan_find_user',\n args: { like: searchtext, exclude_id: self.value.id}\n }])[0].then(function(response){\n self.search.users = response.map(self.userOptionModel);\n }).catch(notification.exception);\n }\n else {\n self.search.users = [];\n }\n },\n userAssociate(){\n const self = this;\n let requests = [];\n const associated = self.association.users;\n const search = self.search.users;\n const searchselected = self.selected.search.users;\n for(const i in searchselected){\n const r = searchselected[i];\n\n requests.push({\n methodname: 'local_treestudyplan_connect_user',\n args: {studyplan_id: self.value.id, user_id: r},\n fail: notification.exception,\n done: function(response){\n if(response.success){\n transportItem(associated,search,r);\n }\n }\n });\n }\n call(requests);\n },\n userDisassociate(){\n const self = this;\n let requests = [];\n const associated = self.association.users;\n const associatedselected = self.selected.associated.users;\n const search = self.search.users;\n for(const i in associatedselected){\n const r = associatedselected[i];\n\n requests.push({\n methodname: 'local_treestudyplan_disconnect_user',\n args: {studyplan_id: self.value.id, user_id: r},\n fail: notification.exception,\n done: function(response){\n if(response.success){\n transportItem(search,associated,r);\n }\n }\n });\n }\n call(requests);\n },\n searchCoaches(searchtext){\n if(premiumenabled()){\n const self = this;\n if(searchtext.length > 0)\n {\n call([{\n methodname: 'local_treestudyplan_find_coach',\n args: { like: searchtext, studyplan_id: self.value.id}\n }])[0].then(function(response){\n self.search.coaches = response.map(self.userOptionModel);\n }).catch(notification.exception);\n }\n else {\n self.search.coaches = [];\n }\n }\n },\n coachAssociate(){\n if(premiumenabled()){\n const self = this;\n let requests = [];\n const associated = self.association.coaches;\n const search = self.search.coaches;\n const searchselected = self.selected.search.coaches;\n for(const i in searchselected){\n const r = searchselected[i];\n\n requests.push({\n methodname: 'local_treestudyplan_connect_coach',\n args: {studyplan_id: self.value.id, user_id: r},\n fail: notification.exception,\n done: function(response){\n if(response.success){\n transportItem(associated,search,r);\n }\n }\n });\n }\n call(requests);\n }\n },\n coachDisassociate(){\n if(premiumenabled()){\n const self = this;\n let requests = [];\n const associated = self.association.coaches;\n const associatedselected = self.selected.associated.coaches;\n const search = self.search.coaches;\n for(const i in associatedselected){\n const r = associatedselected[i];\n\n requests.push({\n methodname: 'local_treestudyplan_disconnect_coach',\n args: {studyplan_id: self.value.id, user_id: r},\n fail: notification.exception,\n done: function(response){\n if(response.success){\n transportItem(search,associated,r);\n }\n }\n });\n }\n call(requests);\n }\n },\n }\n ,\n template:\n`\n\n \n \n \n \n \n {{text.associated_cohorts}}\n {{text.associate_cohorts}}\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n  {{text.delete_association}}\n \n \n  {{text.add_association}}\n \n \n \n \n \n \n \n {{text.associated_users}}\n {{text.associate_users}}\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n  {{text.delete_association}}\n \n \n  {{text.add_association}}\n \n \n \n \n \n \n \n {{text.associated_coaches}}\n {{text.associate_coaches}}\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n  {{text.delete_association}}\n \n \n  {{text.add_association}}\n \n \n \n \n \n \n\n`\n });\n\n /*******************\n *\n * Period editor\n *\n *************/\n\n Vue.component('t-period-edit', {\n props: {\n 'value' :{\n type: Object,\n default(){ return null;},\n },\n 'type' :{\n type: String,\n default() { return \"link\";},\n },\n 'variant' : {\n type: String,\n default() { return \"\";},\n },\n 'minstart' : {\n type: String,\n default() { return null;},\n },\n 'maxend' : {\n type: String,\n default() { return null;},\n }\n },\n data() {\n return {\n show: false,\n editdata: {\n fullname: '',\n shortname: '',\n startdate: (new Date()).getFullYear() + '-08-01',\n enddate: ((new Date()).getFullYear()+1) + '-08-01',\n },\n text: strings.period_edit,\n };\n },\n created() {\n },\n mounted() {\n },\n updated() {\n\n },\n computed: {\n },\n methods: {\n editStart(){\n objCopy(this.editdata,this.value,PERIOD_EDITOR_FIELDS);\n this.show = true;\n },\n editFinish(){\n const self = this;\n let args = { 'id': this.value.id };\n\n objCopy(args,this.editdata,PERIOD_EDITOR_FIELDS);\n\n call([{\n methodname: 'local_treestudyplan_edit_period',\n args: args\n }])[0].then(function(response){\n objCopy(self.value,response,PERIOD_EDITOR_FIELDS);\n self.$emit('input',self.value);\n self.$emit('edited',self.value);\n }).catch(notification.exception);\n },\n refresh(){\n const self = this;\n call([{\n methodname: 'local_treestudyplan_get_period',\n args: { 'id': this.value.id },\n }])[0].then(function(response){\n objCopy(self.value,response,PERIOD_EDITOR_FIELDS);\n self.$emit('input',self.value);\n }).catch(notification.exception);\n },\n add_day(date,days) {\n if( days === undefined ){\n days = 1;\n }\n return add_days(date,days);\n },\n sub_day(date,days) {\n if( days === undefined ){\n days = 1;\n }\n return add_days(date,0 - days);\n },\n }\n ,\n template:\n `\n \n \n \n \n \n \n {{ text.fullname}}\n \n \n \n \n \n {{ text.shortname}}\n \n \n \n \n \n {{ text.studyplan_startdate}}\n \n \n \n \n \n {{ text.studyplan_enddate}}\n \n \n \n \n \n \n \n `\n });\n\n // TAG: Start studyplan component\n /*\n * T-STUDYPLAN\n */\n Vue.component('t-studyplan', {\n props: { \n 'value': {\n type: Object,\n }, \n 'coaching': {\n type: Boolean,\n default: false,\n },\n },\n data() {\n return {\n config: {\n userfields: [\n { key: \"selected\",},\n { key: \"firstname\", \"sortable\": true,},\n { key: \"lastname\", \"sortable\": true,},\n ],\n cohortfields:[\n { key: \"selected\",},\n { key: \"name\", \"sortable\": true,},\n { key: \"context\", \"sortable\": true,},\n ]\n },\n create: {\n studyline: {\n name: '',\n shortname: '',\n color: '#DDDDDD',\n enrol: {\n enrollable: 0,\n enrolroles: [],\n }\n },\n page: {\n id: -1,\n name : '',\n shortname : ''\n }\n },\n edit: {\n toolbox_shown: false,\n studyline: {\n editmode: false,\n data: {\n name: '',\n shortname: '',\n color: '#DDDDDD',\n enrol: {\n enrollable: 0,\n enrolroles: [],\n }\n },\n original: {},\n availableroles: [],\n },\n studyplan: {\n data: {\n name: '',\n shortname: '',\n description: '',\n slots : 4,\n startdate: '2020-08-01',\n enddate: '',\n aggregation: '',\n aggregation_config: '',\n aggregation_info: {\n useRequiredGrades: true,\n useItemCondition: false,\n },\n\n },\n original: {},\n }\n },\n text: strings.studyplan_text,\n cache: {\n linelayers: {},\n },\n selectedpageindex: 0,\n emptyline: {\n id: -1,\n name: '',\n shortname: '',\n color: '#FF0000',\n filterslots: [{}],\n courseslots: [{}]\n },\n availableroles: [],\n };\n },\n created() {\n const self = this;\n // Listener for the signal that a new connection was made and needs to be drawn\n // Sent by the incoming item - By convention, outgoing items are responsible for drawing the lines\n ItemEventBus.$on('coursechange', () => {\n self.$emit('pagechanged',this.selectedpage);\n });\n },\n mounted() {\n const self=this;\n if(this.value.pages[0].studylines.length == 0 && !this.coaching){\n // start in editmode if studylines on first page are empty\n this.edit.studyline.editmode = true;\n }\n\n if (!self.coaching) {\n // Retrieve available roles (only needed as manager)\n call([{\n methodname: 'local_treestudyplan_list_roles',\n args: {\n 'studyplan_id': this.value.id,\n }\n }])[0].then(function(response){\n self.availableroles = response;\n }).catch(notification.exception);\n }\n this.$root.$emit('redrawLines');\n this.$emit('pagechanged',this.selectedpage);\n },\n updated() {\n this.$root.$emit('redrawLines');\n ItemEventBus.$emit('redrawLines');\n },\n computed: {\n selectedpage() {\n return this.value.pages[this.selectedpageindex];\n }\n },\n methods: {\n premiumenabled,\n columns(page) {\n return 1+ (page.periods * 2);\n },\n columns_stylerule(page) {\n // Uses css variables, so width for slots and filters can be configured in css\n let s = \"grid-template-columns: var(--studyplan-filter-width)\"; // use css variable here\n for(let i=0; i maxLayer){\n maxLayer = item.layer;\n }\n }\n for(const ix in line.slots[i].filters){\n const item = line.slots[i].filters[ix];\n if(item.layer > maxLayer){\n maxLayer = item.layer;\n }\n }\n\n }\n }\n this.cache.linelayers[line.id] = {\n value: (maxLayer + 1),\n timestamp: (new Date()),\n };\n return maxLayer+1;\n }\n },\n slotsempty(slots) {\n if(Array.isArray(slots)){\n let count = 0;\n for(let i = 0; i < slots.length; i++) {\n if(Array.isArray(slots[i].courses)){\n count += slots[i].courses.length;\n }\n if(Array.isArray(slots[i].filters)){\n count += slots[i].filters.length;\n }\n }\n return (count == 0);\n } else {\n return false;\n }\n },\n movedStudyplan(plan,from,to) {\n this.$emit('moved',plan,from,to); // Throw the event up....\n },\n addStudyLine(page,newlineinfo) {\n call([{\n methodname: 'local_treestudyplan_add_studyline',\n args: {\n 'page_id': page.id,\n 'name': newlineinfo.name,\n 'shortname': newlineinfo.shortname,\n 'color': newlineinfo.color,\n 'sequence': page.studylines.length,\n 'enrollable': newlineinfo.enrol.enrollable,\n 'enrolroles': newlineinfo.enrol.enrolroles\n }\n }])[0].then(function(response){\n page.studylines.push(response);\n newlineinfo.name = '';\n newlineinfo.shortname = '';\n newlineinfo.color = \"#dddddd\";\n newlineinfo.enrol.enrollable = 0;\n newlineinfo.enrol.enrolroles = [];\n }).catch(notification.exception);\n },\n editLineStart(line) {\n const page = this.value.pages[this.selectedpageindex];\n debug.info(\"Starting line edit\", line);\n Object.assign(this.edit.studyline.data,line);\n this.edit.studyline.original = line;\n this.$bvModal.show('modal-edit-studyline-'+page.id);\n },\n editLineFinish() {\n let editedline = this.edit.studyline.data;\n let originalline = this.edit.studyline.original;\n call([{\n methodname: 'local_treestudyplan_edit_studyline',\n args: { 'id': editedline.id,\n 'name': editedline.name,\n 'shortname': editedline.shortname,\n 'color': editedline.color,\n 'enrollable': editedline.enrol.enrollable,\n 'enrolroles': editedline.enrol.enrolroles\n }\n }])[0].then(function(response){\n originalline.name = response.name;\n originalline.shortname = response.shortname;\n originalline.color = response.color;\n originalline.enrol.enrollable = response.enrol.enrollable;\n originalline.enrol.enrolroles = response.enrol.enrolroles;\n }).catch(notification.exception);\n },\n deleteLine(page,line) {\n const self=this;\n get_strings([\n {key: 'studyline_confirm_remove', param: line.name, component: 'local_treestudyplan' },\n {key: 'delete', component: 'core' },\n ]).then(function(s){\n self.$bvModal.msgBoxConfirm(s[0], {\n okTitle: s[1],\n okVariant: 'danger',\n }).then(function(modalresponse){\n if(modalresponse){\n call([{\n methodname: 'local_treestudyplan_delete_studyline',\n args: { 'id': line.id, }\n }])[0].then(function(response){\n if(response.success == true){\n let index = page.studylines.indexOf(line);\n page.studylines.splice(index, 1);\n }\n }).catch(notification.exception);\n }\n });\n });\n },\n reorderLines(event,lines){\n\n // apply reordering\n event.apply(lines);\n // send the new sequence to the server\n let sequence = [];\n for(let idx in lines)\n {\n sequence.push({'id': lines[idx].id,'sequence': idx});\n }\n call([{\n methodname: 'local_treestudyplan_reorder_studylines',\n args: { 'sequence': sequence }\n }])[0].then(function(response){\n }).catch(notification.exception);\n },\n deletePlan(studyplan){\n const self=this;\n get_strings([\n {key: 'studyplan_confirm_remove', param: studyplan.name, component: 'local_treestudyplan' },\n {key: 'delete', component: 'core' },\n ]).then(function(s){\n self.$bvModal.msgBoxConfirm(s[0], {\n okTitle: s[1],\n okVariant: 'danger',\n }).then(function(modalresponse){\n if(modalresponse){\n call([{\n methodname: 'local_treestudyplan_delete_studyplan',\n args: { 'id': studyplan.id, force: true}\n }])[0].then(function(response){\n if(response.success == true){\n self.$root.$emit(\"studyplanRemoved\",studyplan);\n }\n }).catch(notification.exception);\n }\n });\n });\n },\n deleteStudyItem(event){\n //const self = this;\n let item = event.data;\n call([{\n methodname: 'local_treestudyplan_delete_studyitem',\n args: { 'id': item.id, }\n }])[0].then(function(response){\n if(response.success == true){\n event.source.$emit('cut',event);\n }\n }).catch(notification.exception);\n\n },\n showslot(page,line,index, layeridx, type){\n // check if the slot should be hidden because a previous slot has an item with a span\n // so big that it hides this slot\n const forGradable = (type == 'gradable')?true:false;\n const periods = page.periods;\n let show = true;\n for(let i = 0; i < periods; i++){\n if(line.slots[index-i] && line.slots[index-i].courses){\n const list = line.slots[index-i].courses;\n for(const ix in list){ // Really wish that 'for of' would work with the minifier moodle uses\n const item = list[ix];\n if(item.layer == layeridx){\n if(forGradable){\n if(i > 0 && (item.span - i) > 0){\n show = false;\n }\n } else {\n if((item.span - i) > 1){\n show = false;\n }\n }\n }\n }\n }\n }\n\n return show;\n },\n periodEdited(pi) {\n const prev = this.$refs[\"periodeditor-\" + (pi.period - 1)];\n if(prev && prev[0]) {\n prev[0].refresh();\n }\n const next = this.$refs[\"periodeditor-\" + (pi.period + 1)];\n if(next && next[0]) {\n next[0].refresh();\n }\n },\n add_day(date,days) {\n if( days === undefined ){\n days = 1;\n }\n return add_days(date,days);\n },\n sub_day(date,days) {\n if( days === undefined ){\n days = 1;\n }\n return add_days(date,0 - days);\n },\n toolbox_switched(event){\n this.$emit('toggletoolbox',event);\n },\n pagecreated(page) {\n this.value.pages.push(page);\n },\n selectedpageChanged(newTabIndex,prevTabIndex) {\n const page = this.value.pages[newTabIndex];\n this.$emit('pagechanged',page);\n }\n }\n ,\n template:\n `\n
\n \n
\n
\n {{ text.studyline_editmode }}\n {{ text.toolbox_toggle}}\n \n \n
\n
\n \n \n \n \n {{text.associations}}\n \n \n {{text.edit$core}}\n \n \n \n \n
\n
\n \n \n \n \n \n \n
\n \n \n \n
\n
\n \n
\n \n \n \n
\n \n
\n
\n \n \n\n \n \n