Loading recovered files from server crash

This commit is contained in:
PMKuipers 2023-05-17 21:19:14 +02:00
commit 1023576b34
81 changed files with 65920 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
/.vs
/node_modules
/amd/build

4
LICENSE Normal file
View File

@ -0,0 +1,4 @@
Copyright (C) 2022 Peter-Martijn Kuipers
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 , USA. Also add information on how to contact you by electronic and paper mail.

2
README.md Normal file
View File

@ -0,0 +1,2 @@
# moodle-local_treestudyplan

33153
amd/src/bootstrap-vue.js vendored Normal file

File diff suppressed because it is too large Load Diff

69
amd/src/cfg-grades.js Normal file
View File

@ -0,0 +1,69 @@
/*eslint no-var: "error" */
/*eslint no-unused-vars: "off" */
/*eslint linebreak-style: "off" */
/*eslint no-trailing-spaces: "off" */
/*eslint-env es6*/
// Put this file in path/to/plugin/amd/src
// You can call it anything you like
import {get_string,get_strings} from 'core/str';
import {call} from 'core/ajax';
import Debugger from './debugger';
import {load_strings} from './string-helper';
let debug = new Debugger("treestudyplan-config-grades");
debug.enable();
/*
let strings = load_strings({
studyplan: {
studyplan_select_placeholder: 'studyplan_select_placeholder',
},
});
*/
/**
* Initialize grade cfg page
*/
export function init() {
{ const
intRx = /\d/,
integerChange = (event) => {
if ( (event.key.length > 1) || intRx.test(event.key)
) {
return;
}
event.preventDefault();
};
for (let input of document.querySelectorAll( 'input[type="number"][step="1"][min="0"]' )){
input.addEventListener("keydown", integerChange);
}
}
{ const
decimal= /^[0-9]*?\.[0-9]*?$/,
intRx = /\d/,
floatChange = (event) => {
if ( (event.key.length > 1) || ( (event.key === ".") && (!event.currentTarget.value.match(decimal)) )
|| intRx.test(event.key)
) {
return;
}
event.preventDefault();
};
for (let input of document.querySelectorAll( 'input[type="number"][min="0"]:not([step])' )){
input.addEventListener("keydown", floatChange);
}
for (let input of document.querySelectorAll( 'input[type="text"].float' )){
input.addEventListener("keydown", floatChange);
}
}
}

29
amd/src/debounce.js Normal file
View File

@ -0,0 +1,29 @@
/*eslint no-var: "error"*/
/*eslint no-console: "off"*/
/*eslint-env es6*/
// Put this file in path/to/plugin/amd/src
// You can call it anything you like
/**
* Limits consecutive function calls.
* @param {function} func The function to wrap.
* @param {int} wait The time limit between function calls.
* @param {bool} immediate perform the actual function call first rather than after the timout passed.
* @returns {function} a new function that wraps the debounce.
*/
function debounce(func, wait, immediate) {
let timeout;
return function() {
let context = this, args = arguments;
let later = function() {
timeout = null;
if (!immediate){ func.apply(context, args); }
};
let callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow){ func.apply(context, args); }
};
}
export {debounce};

51
amd/src/debugger.js Normal file
View File

@ -0,0 +1,51 @@
/*eslint no-var: "error"*/
/*eslint no-console: "off"*/
/*eslint-env es6*/
// Put this file in path/to/plugin/amd/src
// You can call it anything you like
define([], function () {
return (function (handle) {
let output_enabled = false;
return {
write: function debugger_write() {
if (output_enabled) {
let args = Array.prototype.slice.call(arguments);
args.unshift(handle + ": ");
console.info.apply(console, args);
}
},
info: function debugger_info() {
if (output_enabled) {
let args = Array.prototype.slice.call(arguments);
args.unshift(handle + ": ");
console.info.apply(console, args);
}
},
warn: function debugger_warn() {
if (output_enabled) {
let args = Array.prototype.slice.call(arguments);
args.unshift(handle + ": ");
console.warn.apply(console, args);
}
},
error: function debugger_error() {
if (output_enabled) {
let args = Array.prototype.slice.call(arguments);
args.unshift(handle + ": ");
console.error.apply(console, args);
}
},
enable: function debugger_enable() {
output_enabled = true;
},
disable: function debugger_disable() {
output_enabled = false;
}
};
});
});

71
amd/src/downloader.js Normal file
View File

@ -0,0 +1,71 @@
/*eslint no-console: "off"*/
/**
* Save a piece of text to file as if it was downloaded
* @param {string} filename
* @param {string} text
* @param {string} type
*/
export function download(filename, text, type) {
if(undefined == type) { type = "text/plain"; }
var pom = document.createElement('a');
pom.setAttribute('href', 'data:'+type+';charset=utf-8,' + encodeURIComponent(text));
pom.setAttribute('download', filename);
if (document.createEvent) {
var event = document.createEvent('MouseEvents');
event.initEvent('click', true, true);
pom.dispatchEvent(event);
}
else {
pom.click();
}
}
/**
* This callback type is called `requestCallback` and is displayed as a global symbol.
*
* @callback fileOpenedCallback
* @param {File} file File name
* @param {string} content File Contents
*/
/**
* Open a file from disk and read its contents
* @param {fileOpenedCallback} onready Callback to run when file is opened
* @param {Array} accept Array of mime types that the file dialog will accept
*/
export function upload(onready,accept) {
let input = document.createElement('input');
input.type = 'file';
if(Array.isArray(accept)){
if(accept.count > 0){
input.accept = accept.join(", ");
}
} else if (undefined !== accept){
input.accept = accept;
}
input.onchange = () => {
let files = Array.from(input.files);
if(files.length > 0){
let file = files[0];
var reader = new FileReader();
reader.onload = function(e) {
var contents = e.target.result;
if(onready instanceof Function){
onready(file,contents);
}
};
reader.readAsText(file);
}
};
if (document.createEvent) {
var event = document.createEvent('MouseEvents');
event.initEvent('click', true, true);
input.dispatchEvent(event);
}
else {
input.click();
}
}

6
amd/src/leaderline.js Normal file

File diff suppressed because one or more lines are too long

110
amd/src/modedit-modal.js Normal file
View File

@ -0,0 +1,110 @@
/*eslint no-var: "error"*/
/*eslint no-console: "off"*/
/*eslint-disable no-trailing-spaces */
/*eslint-env es6*/
// Put this file in path/to/plugin/amd/src
import {loadFragment} from 'core/fragment';
import {load_strings} from './string-helper';
import {call} from 'core/ajax';
import notification from 'core/notification';
import {replaceNodeContents} from 'core/templates';
//import {markFormSubmitted} from 'core_form/changechecker'; // Moodle 4.00+ only
//import {notifyFormSubmittedByJavascript} from 'core_form/events'; // Moodle 4.00+ only
export default {
install(Vue/*,options*/){
let strings = load_strings({
editmod: {
save$core: "save",
cancel$core: "unknown",
}
});
Vue.component('s-edit-mod', {
props: {
cmid: {
type: Number,
},
coursectxid:{
type: Number,
},
title: {
type: String,
default: "",
},
genericonly: {
type: Boolean,
default: false,
}
},
data() {
return {
content: "",
text: strings.editmod,
};
},
computed: {
},
methods: {
openForm(){
const self = this;
self.$refs["editormodal"].show();
},
onShown(){
const self = this;
let params = {cmid: this.cmid};
console.info("Loading form");
loadFragment('local_treestudyplan', 'mod_edit_form', this.coursectxid, params).then((html,js) =>{
replaceNodeContents(self.$refs["content"], html, js);
}).catch(notification.exception);
},
onSave(){
const self = this;
let form = this.$refs["content"].getElementsByTagName("form")[0];
// markFormSubmitted(form); // Moodle 4.00+ only
// We call this, so other modules can update the form with the latest state.
form.dispatchEvent(new Event("save-form-state"));
// Tell all form fields we are about to submit the form.
// notifyFormSubmittedByJavascript(form); // Moodle 4.00+ only
const formdata = new FormData(form);
const data =new URLSearchParams(formdata).toString();
//const formdata = new FormData(form);
//const data = {};
//formdata.forEach((value, key) => (data[key] = value));
call([{
methodname: 'local_treestudyplan_submit_cm_editform',
args: {cmid: this.cmid, formdata: data}
}])[0].done(()=>{
self.$emit("saved",formdata);
}).fail(notification.exception);
}
},
template: `
<span class='s-edit-mod'><a href='#' @click.prevent="openForm"><slot><i class="fa fa-cog"></i></slot></a>
<b-modal
ref="editormodal"
scrollable
centered
size="xl"
id="'modal-cm-'+cmid"
@shown="onShown"
@ok="onSave"
:title="title"
:ok-title="text.save$core"
><div :class="'s-edit-mod-form '+ (genericonly?'genericonly':'')" ref="content"
><div class="d-flex justify-content-center mb-3"><b-spinner variant="primary"></b-spinner></div
></div
></b-modal>
</span>
`,
});
}
};

264
amd/src/page-edit-plan.js Normal file
View File

@ -0,0 +1,264 @@
/*eslint no-var: "error" */
/*eslint no-unused-vars: "off" */
/*eslint linebreak-style: "off" */
/*eslint no-trailing-spaces: "off" */
/*eslint-env es6*/
// Put this file in path/to/plugin/amd/src
// You can call it anything you like
import {get_string,get_strings} from 'core/str';
import {call} from 'core/ajax';
import notification from 'core/notification';
import Vue from './vue';
import EditorComponents from './studyplan-editor-components';
import TSComponents from './treestudyplan-components';
import ModalComponents from './modedit-modal';
import Debugger from './debugger';
import {load_strings} from './string-helper';
import {ProcessStudyplan, fixLineWrappers} from './studyplan-processor';
import {download,upload} from './downloader';
import PortalVue from './portal-vue';
import BootstrapVue from './bootstrap-vue';
import {Drag, Drop, DropList} from './vue-easy-dnd';
Vue.use(PortalVue);
Vue.use(BootstrapVue);
Vue.use(TSComponents);
Vue.use(EditorComponents);
vue.use(ModalComponents);
Vue.component('drag',Drag);
Vue.component('drop',Drop);
Vue.component('drop-list',DropList);
import vue from './vue';
const debug = new Debugger("treestudyplan");
debug.enable();
let strings = load_strings({
studyplan: {
studyplan_select_placeholder: 'studyplan_select_placeholder',
},
});
/**
* Initialize the Page
* @param {int} contextid The context we should attempt to work in (1:1 related to the category)
* @param {int} categoryid The category we shoud attempt to work in (1:1 related to the context)
*/
export function init(contextid,categoryid) {
// Make sure the id's are numeric and integer
if(undefined === contextid || !Number.isInteger(contextid) || contextid < 1 ){ contextid = 1;}
if(undefined === categoryid || !Number.isInteger(categoryid)){ categoryid = 0;}
const in_systemcontext = (contextid <= 1);
// Add the event listeners for the line wrappers (needed to keep the arrows in their place)
window.addEventListener('resize',fixLineWrappers);
// Setup the initial Vue app for this page
let app = new Vue({
el: '#root',
data: {
create: {
studyplan: {
name: '',
shortname: '',
description: '',
slots : 4,
startdate: '2020-08-01',
enddate: '',
aggregation: 'bistate',
aggregation_config: '',
}
},
toolbox: {
right: true,
},
activestudyplan: null,
loadingstudyplan: false,
studyplans: [],
frameworks: [],
badges: [],
courses: [],
text: strings.studyplan,
usedcontexts: [],
},
created() {
this.$root.$on('redrawLines',()=>{
// Ugly hack, but currently the only way to properly fix scrollablility in the lines
fixLineWrappers();
});
this.$root.$on('studyplanRemoved',(studyplan)=>{
if(app.activestudyplan == studyplan){
app.activestudyplan = null;
}
// remove studyplan from index list
let index = null;
for(let idx in app.studyplans){
if(app.studyplans[idx].id == studyplan.id){
index = idx;
break;
}
}
if(index){
app.studyplans.splice(index, 1);
}
});
},
mounted() {
fixLineWrappers();
call([{
methodname: 'local_treestudyplan_list_studyplans',
args: { context_id: contextid}
}])[0].done(function(response){
const timingval = { future: 0, present: 1, past: 2, };
response.sort((a,b) => {
const timinga = TSComponents.studyplanTiming(a);
const timingb = TSComponents.studyplanTiming(b);
let t = timingval[timinga] - timingval[timingb];
if(t == 0){
// sort by start date if timing is equal
t = new Date(b.startdate).getTime() - new Date(a.startdate).getTime();
if (t == 0) {
// sort by name if timing is equal
t = a.name.localeCompare(b.name);
}
}
return t;
});
app.studyplans = response;
// load studyplan from hash if applicable
const hash = location.hash.replace('#','');
if(hash){
for(let idx in app.studyplans){
if(app.studyplans[idx].id == hash){
app.selectStudyplan(app.studyplans[idx]);
break;
}
}
}
}).fail(notification.exception);
call([{
methodname: 'local_treestudyplan_list_badges',
args: {}
}])[0].done(function(response){
app.badges = response;
}).fail(notification.exception);
call([{
methodname: 'local_treestudyplan_map_categories',
args: {root_id: categoryid}
}])[0].done(function(response){
app.courses = response;
}).fail(notification.exception);
call([{
methodname: 'local_treestudyplan_list_used_categories',
args: { operation: 'edit'}
}])[0].done(function(response){
app.usedcontexts = response;
}).fail(notification.exception);
},
computed: {
dropdown_title(){
if(this.activestudyplan && this.activestudyplan.name){
return this.activestudyplan.name;
}
else{
return this.text.studyplan_select_placeholder;
}
},
contextid(){
return contextid;
}
},
methods: {
closeStudyplan() {
app.activestudyplan = null;
location.hash = '';
},
movedStudyplan(plan,from,to) {
// reload the page in the new context (needed, since a number of links are not reactive in the page)
const params = new URLSearchParams(location.search);
params.delete('categoryid');
params.set("contextid", to);
window.location.search = params.toString();
},
onStudyPlanCreated(newstudyplan){
debug.info("New studyplan:",newstudyplan);
app.studyplans.push(newstudyplan);
app.selectStudyplan(newstudyplan);
},
switchContext(ctx){
const params = new URLSearchParams(location.search);
params.set('categoryid', ctx.id);
window.location.search = params.toString();
},
selectStudyplan(studyplan){
// fetch studyplan
app.loadingstudyplan = true;
app.activestudyplan = null;
call([{
methodname: 'local_treestudyplan_get_studyplan_map',
args: { id: studyplan.id}
}])[0].done(function(response){
debug.info(response);
app.activestudyplan = ProcessStudyplan(response,true);
debug.info('studyplan processed');
app.loadingstudyplan = false;
location.hash = app.activestudyplan.id;
}).fail(function(error){
notification.exception(error);
app.loadingstudyplan = false;
});
},
import_studyplan(){
upload((filename,content)=>{
call([{
methodname: 'local_treestudyplan_import_plan',
args: {
content: content,
format: "application/json",
},
}])[0].done(function(response){
if(response.success){
location.reload();
} else {
debug.error("Import failed: ",response.msg);
}
}).fail(notification.exception);
}, "application/json");
},
export_plan(plan,format){
let self = this;
if(format == undefined || !["json","csv"].includes(format)){
format = "json";
}
call([{
methodname: 'local_treestudyplan_export_plan',
args: {
studyplan_id: plan.id,
format: format
},
}])[0].done(function(response){
download(plan.shortname+".json",response.content,response.format);
}).fail(notification.exception);
},
},
});
}

View File

@ -0,0 +1,51 @@
/*eslint no-var: "error" */
/*eslint no-unused-vars: "off" */
/*eslint linebreak-style: "off" */
/*eslint-env es6*/
// Put this file in path/to/plugin/amd/src
// You can call it anything you like
define(['jquery', 'core/str', 'core/ajax', 'core/modal_factory', 'core/modal_events',
'local_treestudyplan/handlers', 'local_treestudyplan/debugger'],
function ($, str, ajax, ModalFactory, ModalEvents,
handlers, Debugger) {
let debug = new Debugger("treestudyplan");
debug.enable();
let self = {
init: function init() {
$('.path-local-treestudyplan a.m-action-confirm').on('click', function (e) {
e.preventDefault();
let $link = $(e.currentTarget);
let href = $link.attr('data-actionhref');
let text = $link.attr('data-confirmtext');
let oktext = $link.attr('data-confirmbtn');
debug.info("Ok", oktext);
if (undefined == oktext) { oktext = str.get_string('ok'); }
let title = $link.attr('data-confirmtitle');
debug.info("Title", title);
if (undefined == title) { title = str.get_string('confirm'); }
debug.info("Link, href, text", $link, href, text);
ModalFactory.create({
type: ModalFactory.types.SAVE_CANCEL,
title: title,
body: text,
}).then(function (modal) {
modal.setSaveButtonText(oktext);
let root = modal.getRoot();
root.on(ModalEvents.save, function () {
window.location = href;
});
$(modal.modal).css("max-width", "345px");
modal.show();
});
});
},
};
return self;
});

103
amd/src/page-myreport.js Normal file
View File

@ -0,0 +1,103 @@
/*eslint no-var: "error" */
/*eslint no-unused-vars: "off" */
/*eslint linebreak-style: "off" */
/*eslint no-trailing-spaces: "off" */
/*eslint no-console: "off" */
/*eslint-env es6*/
// Put this file in path/to/plugin/amd/src
// You can call it anything you like
import {call} from 'core/ajax';
import notification from 'core/notification';
import Vue from './vue';
import RVComponents from './report-viewer-components';
import TSComponents from './treestudyplan-components';
import Debugger from './debugger';
import {ProcessStudyplans, fixLineWrappers} from './studyplan-processor';
import PortalVue from './portal-vue';
import BootstrapVue from './bootstrap-vue';
Vue.use(RVComponents);
Vue.use(PortalVue);
Vue.use(BootstrapVue);
let debug = new Debugger("treestudyplan-report");
debug.enable();
/**
* Initialize the Page
* @param {string} type Type of page to show
* @param {Object} arg Arguments passed
*/
export function init(type="myreport",arg) {
// Make sure on window resize, the line wrappers are repositioned, to fix scrolling issues
window.addEventListener('resize',fixLineWrappers);
let app = new Vue({
el: '#root',
data: {
"studyplans": [],
},
mounted() {
let call_method;
let call_args;
if(type == "invited"){
call_method = 'local_treestudyplan_get_invited_studyplan';
call_args = {"invitekey": arg};
}
else if(type == "other"){
call_method = 'local_treestudyplan_get_user_studyplans';
call_args = {"userid": arg};
}
else if(type == "teaching"){
call_method = 'local_treestudyplan_get_teaching_studyplans';
call_args = {};
}
else{
call_method = 'local_treestudyplan_get_own_studyplan';
call_args = {};
}
call([{
methodname: call_method,
args: call_args
}])[0].done(function(response){
debug.info("Studyplans:",response);
const timingval = { future: 0, present: 1, past: 2, };
response.sort((a,b) => {
const timinga = TSComponents.studyplanTiming(a);
const timingb = TSComponents.studyplanTiming(b);
let t = timingval[timinga] - timingval[timingb];
if(t == 0){
// sort by start date if timing is equal
t = new Date(b.startdate).getTime() - new Date(a.startdate).getTime();
if (t == 0) {
// sort by name if timing is equal
t = a.name.localeCompare(b.name);
}
}
return t;
});
app.studyplans = ProcessStudyplans(response);
}).fail(notification.exception);
},
created() {
this.$root.$on('redrawLines',()=>{
// Ugly hack, but currently the only way to properly fix scrollablility in the lines
fixLineWrappers();
});
},
updated() {
// Ugly hack, but currently the only way to properly fix scrollablility in the lines
setTimeout(fixLineWrappers, 50);
},
methods: {
},
});
}

216
amd/src/page-view-plan.js Normal file
View File

@ -0,0 +1,216 @@
/*eslint no-var: "error" */
/*eslint no-unused-vars: "off" */
/*eslint linebreak-style: "off" */
/*eslint no-trailing-spaces: "off" */
/*eslint-env es6*/
// Put this file in path/to/plugin/amd/src
// You can call it anything you like
import {call} from 'core/ajax';
import notification from 'core/notification';
import Vue from './vue';
import Debugger from './debugger';
import {load_strings} from './string-helper';
import {ProcessStudyplan, fixLineWrappers} from './studyplan-processor';
import TSComponents from './treestudyplan-components';
Vue.use(TSComponents);
import RVComponents from './report-viewer-components';
Vue.use(RVComponents);
import ModalComponents from './modedit-modal';
Vue.use(ModalComponents);
import PortalVue from './portal-vue';
Vue.use(PortalVue);
import BootstrapVue from './bootstrap-vue';
Vue.use(BootstrapVue);
let debug = new Debugger("treestudyplanviewer");
debug.enable();
let strings = load_strings({
studyplan: {
studyplan_select_placeholder: 'studyplan_select_placeholder',
},
});
/**
* Initialize the Page
* @param {int} contextid The context we should attempt to work in (1:1 related to the category)
* @param {int} categoryid The category we shoud attempt to work in (1:1 related to the context)
*/
export function init(contextid,categoryid) {
// Make sure the id's are numeric and integer
if(undefined === contextid || !Number.isInteger(contextid) || contextid < 1 ){ contextid = 1;}
if(undefined === categoryid || !Number.isInteger(categoryid)){ categoryid = 0;}
const in_systemcontext = (contextid <= 1);
window.addEventListener('resize',fixLineWrappers);
let app = new Vue({
el: '#root',
data: {
displayedstudyplan: null,
activestudyplan: null,
associatedstudents: [],
selectedstudent: null,
studentstudyplan: null,
loadingstudyplan: false,
studyplans: [],
text: strings.studyplan,
toolbox: {
right: true,
},
usedcontexts: [],
},
async created() {
this.$root.$on('redrawLines',()=>{
// Ugly hack, but currently the only way to properly fix scrollablility in the lines
fixLineWrappers();
});
},
updated() {
// Ugly hack, but currently the only way to properly fix scrollablility in the lines
setTimeout(fixLineWrappers, 50);
},
async mounted() {
fixLineWrappers();
call([{
methodname: 'local_treestudyplan_list_studyplans',
args: {context_id: contextid}
}])[0].done(function(response){
const timingval = { present: 0, past: 1, future: 2};
response.sort((a,b) => {
const timinga = TSComponents.studyplanTiming(a);
const timingb = TSComponents.studyplanTiming(b);
const t = timingval[timinga] - timingval[timingb];
if(t == 0){
// sort by name if timing is equal
return a.name.localeCompare(b.name);
}
else {
return t;
}
});
app.studyplans = response;
// load studyplan from hash if applicable
const hash = location.hash.replace('#','');
const parts = hash.split("-");
if(!!parts && parts.length > 0){
for(let idx in app.studyplans){
if(app.studyplans[idx].id == parts[0]){
app.selectStudyplan(app.studyplans[idx],parts[1]);
break;
}
}
}
}).fail(notification.exception);
call([{
methodname: 'local_treestudyplan_list_used_categories',
args: { operation: 'view'}
}])[0].done(function(response){
const contexts = [];
for(const ix in response){
if(response[ix].studyplancount >0){
contexts.push(response[ix]);
}
}
app.usedcontexts = contexts;
}).fail(notification.exception);
},
computed: {
dropdown_title(){
if(this.activestudyplan && this.activestudyplan.name){
return this.activestudyplan.name;
}
else{
return this.text.studyplan_select_placeholder;
}
},
contextid(){
return contextid;
}
},
methods: {
switchContext(ctx){
const params = new URLSearchParams(location.search);
params.set('categoryid', ctx.id);
window.location.search = params.toString();
},
closeStudyplan() {
app.activestudyplan = null;
app.associatedstudents = [];
app.studentstudyplan = [];
app.displayedstudyplan = null;
},
selectStudyplan(studyplan,studentid){
// fetch studyplan
app.loadingstudyplan = true;
app.activestudyplan = null;
app.associatedstudents = [];
app.selectedstudent = null;
app.studentstudyplan = null;
call([{
methodname: 'local_treestudyplan_get_studyplan_map',
args: { id: studyplan.id}
}])[0].done(function(response){
app.activestudyplan = ProcessStudyplan(response,true);
app.displayedstudyplan = app.activestudyplan;
app.loadingstudyplan = false;
location.hash = app.activestudyplan.id;
call([{
methodname: 'local_treestudyplan_all_associated',
args: { studyplan_id: studyplan.id}
}])[0].done(function(response){
app.associatedstudents = response;
if(studentid){
for(const student of app.associatedstudents){
if(student.id == studentid){
app.showStudentView(student);
break;
}
}
}
}).fail(notification.exception);
}).fail(function(error){
notification.exception(error);
app.loadingstudyplan = false;
});
},
showStudentView(student){
app.selectedstudent = student;
app.studentstudyplan = null;
app.loadingstudyplan = true;
call([{
methodname: 'local_treestudyplan_get_user_studyplan',
args: { userid: student.id, studyplanid: app.activestudyplan.id}
}])[0].done(function(response){
app.studentstudyplan = ProcessStudyplan(response,false);
app.displayedstudyplan = app.studentstudyplan;
app.loadingstudyplan = false;
location.hash = app.activestudyplan.id + "-" + student.id;
}).fail(function(error){
notification.exception(error);
app.loadingstudyplan = false;
});
},
showOverview(){
app.selectedstudent = null;
app.studentstudyplan = null;
app.displayedstudyplan = app.activestudyplan;
}
},
});
}

628
amd/src/portal-vue.js Normal file
View File

@ -0,0 +1,628 @@
/* eslint-disable */
/*!
* portal-vue © Thorsten Lünborg, 2019
*
* Version: 2.1.7
*
* LICENCE: MIT
*
* https://github.com/linusborg/portal-vue
*
*/
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('./vue')) :
typeof define === 'function' && define.amd ? define(['exports', './vue'], factory) :
(factory((global.PortalVue = {}),global.Vue));
}(this, (function (exports,Vue) { 'use strict';
Vue = Vue && Vue.hasOwnProperty('default') ? Vue['default'] : Vue;
function _typeof(obj) {
if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") {
_typeof = function (obj) {
return typeof obj;
};
} else {
_typeof = function (obj) {
return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj;
};
}
return _typeof(obj);
}
function _toConsumableArray(arr) {
return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _nonIterableSpread();
}
function _arrayWithoutHoles(arr) {
if (Array.isArray(arr)) {
for (var i = 0, arr2 = new Array(arr.length); i < arr.length; i++) arr2[i] = arr[i];
return arr2;
}
}
function _iterableToArray(iter) {
if (Symbol.iterator in Object(iter) || Object.prototype.toString.call(iter) === "[object Arguments]") return Array.from(iter);
}
function _nonIterableSpread() {
throw new TypeError("Invalid attempt to spread non-iterable instance");
}
var inBrowser = typeof window !== 'undefined';
function freeze(item) {
if (Array.isArray(item) || _typeof(item) === 'object') {
return Object.freeze(item);
}
return item;
}
function combinePassengers(transports) {
var slotProps = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
return transports.reduce(function (passengers, transport) {
var temp = transport.passengers[0];
var newPassengers = typeof temp === 'function' ? temp(slotProps) : transport.passengers;
return passengers.concat(newPassengers);
}, []);
}
function stableSort(array, compareFn) {
return array.map(function (v, idx) {
return [idx, v];
}).sort(function (a, b) {
return compareFn(a[1], b[1]) || a[0] - b[0];
}).map(function (c) {
return c[1];
});
}
function pick(obj, keys) {
return keys.reduce(function (acc, key) {
if (obj.hasOwnProperty(key)) {
acc[key] = obj[key];
}
return acc;
}, {});
}
var transports = {};
var targets = {};
var sources = {};
var Wormhole = Vue.extend({
data: function data() {
return {
transports: transports,
targets: targets,
sources: sources,
trackInstances: inBrowser
};
},
methods: {
open: function open(transport) {
if (!inBrowser) return;
var to = transport.to,
from = transport.from,
passengers = transport.passengers,
_transport$order = transport.order,
order = _transport$order === void 0 ? Infinity : _transport$order;
if (!to || !from || !passengers) return;
var newTransport = {
to: to,
from: from,
passengers: freeze(passengers),
order: order
};
var keys = Object.keys(this.transports);
if (keys.indexOf(to) === -1) {
Vue.set(this.transports, to, []);
}
var currentIndex = this.$_getTransportIndex(newTransport); // Copying the array here so that the PortalTarget change event will actually contain two distinct arrays
var newTransports = this.transports[to].slice(0);
if (currentIndex === -1) {
newTransports.push(newTransport);
} else {
newTransports[currentIndex] = newTransport;
}
this.transports[to] = stableSort(newTransports, function (a, b) {
return a.order - b.order;
});
},
close: function close(transport) {
var force = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
var to = transport.to,
from = transport.from;
if (!to || !from && force === false) return;
if (!this.transports[to]) {
return;
}
if (force) {
this.transports[to] = [];
} else {
var index = this.$_getTransportIndex(transport);
if (index >= 0) {
// Copying the array here so that the PortalTarget change event will actually contain two distinct arrays
var newTransports = this.transports[to].slice(0);
newTransports.splice(index, 1);
this.transports[to] = newTransports;
}
}
},
registerTarget: function registerTarget(target, vm, force) {
if (!inBrowser) return;
if (this.trackInstances && !force && this.targets[target]) {
console.warn("[portal-vue]: Target ".concat(target, " already exists"));
}
this.$set(this.targets, target, Object.freeze([vm]));
},
unregisterTarget: function unregisterTarget(target) {
this.$delete(this.targets, target);
},
registerSource: function registerSource(source, vm, force) {
if (!inBrowser) return;
if (this.trackInstances && !force && this.sources[source]) {
console.warn("[portal-vue]: source ".concat(source, " already exists"));
}
this.$set(this.sources, source, Object.freeze([vm]));
},
unregisterSource: function unregisterSource(source) {
this.$delete(this.sources, source);
},
hasTarget: function hasTarget(to) {
return !!(this.targets[to] && this.targets[to][0]);
},
hasSource: function hasSource(to) {
return !!(this.sources[to] && this.sources[to][0]);
},
hasContentFor: function hasContentFor(to) {
return !!this.transports[to] && !!this.transports[to].length;
},
// Internal
$_getTransportIndex: function $_getTransportIndex(_ref) {
var to = _ref.to,
from = _ref.from;
for (var i in this.transports[to]) {
if (this.transports[to][i].from === from) {
return +i;
}
}
return -1;
}
}
});
var wormhole = new Wormhole(transports);
var _id = 1;
var Portal = Vue.extend({
name: 'portal',
props: {
disabled: {
type: Boolean
},
name: {
type: String,
default: function _default() {
return String(_id++);
}
},
order: {
type: Number,
default: 0
},
slim: {
type: Boolean
},
slotProps: {
type: Object,
default: function _default() {
return {};
}
},
tag: {
type: String,
default: 'DIV'
},
to: {
type: String,
default: function _default() {
return String(Math.round(Math.random() * 10000000));
}
}
},
created: function created() {
var _this = this;
this.$nextTick(function () {
wormhole.registerSource(_this.name, _this);
});
},
mounted: function mounted() {
if (!this.disabled) {
this.sendUpdate();
}
},
updated: function updated() {
if (this.disabled) {
this.clear();
} else {
this.sendUpdate();
}
},
beforeDestroy: function beforeDestroy() {
wormhole.unregisterSource(this.name);
this.clear();
},
watch: {
to: function to(newValue, oldValue) {
oldValue && oldValue !== newValue && this.clear(oldValue);
this.sendUpdate();
}
},
methods: {
clear: function clear(target) {
var closer = {
from: this.name,
to: target || this.to
};
wormhole.close(closer);
},
normalizeSlots: function normalizeSlots() {
return this.$scopedSlots.default ? [this.$scopedSlots.default] : this.$slots.default;
},
normalizeOwnChildren: function normalizeOwnChildren(children) {
return typeof children === 'function' ? children(this.slotProps) : children;
},
sendUpdate: function sendUpdate() {
var slotContent = this.normalizeSlots();
if (slotContent) {
var transport = {
from: this.name,
to: this.to,
passengers: _toConsumableArray(slotContent),
order: this.order
};
wormhole.open(transport);
} else {
this.clear();
}
}
},
render: function render(h) {
var children = this.$slots.default || this.$scopedSlots.default || [];
var Tag = this.tag;
if (children && this.disabled) {
return children.length <= 1 && this.slim ? this.normalizeOwnChildren(children)[0] : h(Tag, [this.normalizeOwnChildren(children)]);
} else {
return this.slim ? h() : h(Tag, {
class: {
'v-portal': true
},
style: {
display: 'none'
},
key: 'v-portal-placeholder'
});
}
}
});
var PortalTarget = Vue.extend({
name: 'portalTarget',
props: {
multiple: {
type: Boolean,
default: false
},
name: {
type: String,
required: true
},
slim: {
type: Boolean,
default: false
},
slotProps: {
type: Object,
default: function _default() {
return {};
}
},
tag: {
type: String,
default: 'div'
},
transition: {
type: [String, Object, Function]
}
},
data: function data() {
return {
transports: wormhole.transports,
firstRender: true
};
},
created: function created() {
var _this = this;
this.$nextTick(function () {
wormhole.registerTarget(_this.name, _this);
});
},
watch: {
ownTransports: function ownTransports() {
this.$emit('change', this.children().length > 0);
},
name: function name(newVal, oldVal) {
/**
* TODO
* This should warn as well ...
*/
wormhole.unregisterTarget(oldVal);
wormhole.registerTarget(newVal, this);
}
},
mounted: function mounted() {
var _this2 = this;
if (this.transition) {
this.$nextTick(function () {
// only when we have a transition, because it causes a re-render
_this2.firstRender = false;
});
}
},
beforeDestroy: function beforeDestroy() {
wormhole.unregisterTarget(this.name);
},
computed: {
ownTransports: function ownTransports() {
var transports = this.transports[this.name] || [];
if (this.multiple) {
return transports;
}
return transports.length === 0 ? [] : [transports[transports.length - 1]];
},
passengers: function passengers() {
return combinePassengers(this.ownTransports, this.slotProps);
}
},
methods: {
// can't be a computed prop because it has to "react" to $slot changes.
children: function children() {
return this.passengers.length !== 0 ? this.passengers : this.$scopedSlots.default ? this.$scopedSlots.default(this.slotProps) : this.$slots.default || [];
},
// can't be a computed prop because it has to "react" to this.children().
noWrapper: function noWrapper() {
var noWrapper = this.slim && !this.transition;
if (noWrapper && this.children().length > 1) {
console.warn('[portal-vue]: PortalTarget with `slim` option received more than one child element.');
}
return noWrapper;
}
},
render: function render(h) {
var noWrapper = this.noWrapper();
var children = this.children();
var Tag = this.transition || this.tag;
return noWrapper ? children[0] : this.slim && !Tag ? h() : h(Tag, {
props: {
// if we have a transition component, pass the tag if it exists
tag: this.transition && this.tag ? this.tag : undefined
},
class: {
'vue-portal-target': true
}
}, children);
}
});
var _id$1 = 0;
var portalProps = ['disabled', 'name', 'order', 'slim', 'slotProps', 'tag', 'to'];
var targetProps = ['multiple', 'transition'];
var MountingPortal = Vue.extend({
name: 'MountingPortal',
inheritAttrs: false,
props: {
append: {
type: [Boolean, String]
},
bail: {
type: Boolean
},
mountTo: {
type: String,
required: true
},
// Portal
disabled: {
type: Boolean
},
// name for the portal
name: {
type: String,
default: function _default() {
return 'mounted_' + String(_id$1++);
}
},
order: {
type: Number,
default: 0
},
slim: {
type: Boolean
},
slotProps: {
type: Object,
default: function _default() {
return {};
}
},
tag: {
type: String,
default: 'DIV'
},
// name for the target
to: {
type: String,
default: function _default() {
return String(Math.round(Math.random() * 10000000));
}
},
// Target
multiple: {
type: Boolean,
default: false
},
targetSlim: {
type: Boolean
},
targetSlotProps: {
type: Object,
default: function _default() {
return {};
}
},
targetTag: {
type: String,
default: 'div'
},
transition: {
type: [String, Object, Function]
}
},
created: function created() {
if (typeof document === 'undefined') return;
var el = document.querySelector(this.mountTo);
if (!el) {
console.error("[portal-vue]: Mount Point '".concat(this.mountTo, "' not found in document"));
return;
}
var props = this.$props; // Target already exists
if (wormhole.targets[props.name]) {
if (props.bail) {
console.warn("[portal-vue]: Target ".concat(props.name, " is already mounted.\n Aborting because 'bail: true' is set"));
} else {
this.portalTarget = wormhole.targets[props.name];
}
return;
}
var append = props.append;
if (append) {
var type = typeof append === 'string' ? append : 'DIV';
var mountEl = document.createElement(type);
el.appendChild(mountEl);
el = mountEl;
} // get props for target from $props
// we have to rename a few of them
var _props = pick(this.$props, targetProps);
_props.slim = this.targetSlim;
_props.tag = this.targetTag;
_props.slotProps = this.targetSlotProps;
_props.name = this.to;
this.portalTarget = new PortalTarget({
el: el,
parent: this.$parent || this,
propsData: _props
});
},
beforeDestroy: function beforeDestroy() {
var target = this.portalTarget;
if (this.append) {
var el = target.$el;
el.parentNode.removeChild(el);
}
target.$destroy();
},
render: function render(h) {
if (!this.portalTarget) {
console.warn("[portal-vue] Target wasn't mounted");
return h();
} // if there's no "manual" scoped slot, so we create a <Portal> ourselves
if (!this.$scopedSlots.manual) {
var props = pick(this.$props, portalProps);
return h(Portal, {
props: props,
attrs: this.$attrs,
on: this.$listeners,
scopedSlots: this.$scopedSlots
}, this.$slots.default);
} // else, we render the scoped slot
var content = this.$scopedSlots.manual({
to: this.to
}); // if user used <template> for the scoped slot
// content will be an array
if (Array.isArray(content)) {
content = content[0];
}
if (!content) return h();
return content;
}
});
function install(Vue$$1) {
var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
Vue$$1.component(options.portalName || 'Portal', Portal);
Vue$$1.component(options.portalTargetName || 'PortalTarget', PortalTarget);
Vue$$1.component(options.MountingPortalName || 'MountingPortal', MountingPortal);
}
if ( // @ts-ignore
typeof window !== 'undefined' && window.Vue && window.Vue === Vue) {
window.Vue.use({
install: install
});
}
var index = {
install: install
};
exports.default = index;
exports.Portal = Portal;
exports.PortalTarget = PortalTarget;
exports.MountingPortal = MountingPortal;
exports.Wormhole = wormhole;
Object.defineProperty(exports, '__esModule', { value: true });
})));

1134
amd/src/reflect-metadata.js Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

56
amd/src/string-helper.js Normal file
View File

@ -0,0 +1,56 @@
import {get_strings} from 'core/str';
/**
* Load the translation of strings from a strings object
* @param {Object} strings The map of strings
* @returns {Object} The map with strings loaded in
*/
export function load_strings(strings){
for(let idx in strings){
let stringkeys = [];
for(const key in strings[idx]){
let parts = key.split("$");
let identifier = parts[0];
let component = (parts.length > 1)?parts[1]:'local_treestudyplan';
stringkeys.push({ key: identifier, component: component});
}
get_strings(stringkeys).then(function(str){
let i = 0;
for(const key in strings[idx]){
strings[idx][key] = str[i];
i++;
}
});
}
return strings;
}
/**
* Load the translation of strings from a strings object based on keys
* Used for loading values for a drop down menu or the like
* @param {Object} string_keys The map of stringkeys
* @returns {Object} The map with strings loaded in
*/
export function load_stringkeys(string_keys){
for(let idx in string_keys){
// Get text strings for condition settings
let stringkeys = [];
for(const i in string_keys[idx]){
const key = string_keys[idx][i].textkey;
let parts = key.split("$");
let identifier = parts[0];
let component = (parts.length > 1)?parts[1]:'local_treestudyplan';
stringkeys.push({ key: identifier, component: component});
}
get_strings(stringkeys).then(function(strings){
for(const i in strings) {
const s = strings[i];
const l = string_keys[idx][i];
l.text = s;
}
});
}
return string_keys;
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,142 @@
/**
* Copy fields from one object to another
* @param {Object} target The target to move to
* @param {Object} source The source to move from
* @param {Array} fields The field names to copy
* @returns {Object} The map with strings loaded in
*/
export function objCopy(target,source,fields){
for(const ix in fields) {
const field = fields[ix];
target[field] = source[field];
}
}
/**
* Transport items from one object to another
* @param {Object} target The target to move to
* @param {Object} source The source to move from
* @param {*} identifier The value used to match the item
* @param {string} param The field name to match on (default: 'value')
*/
export function transportItem(target,source,identifier,param){
if(!param){
param = 'value';
}
// find item
let item;
let itemindex;
for(const ix in source){
if(source[ix][param] == identifier){
item = source[ix];
itemindex = ix;
break;
}
}
if(item){
target.push(item);
source.splice(itemindex,1);
}
}
/**
* Do initial conversion on multiple studyplans
* @param {Array} studyplans The list of studyplans to load
* @returns {Array} List of updated studyplans
*/
export function ProcessStudyplans(studyplans){
// Unify object references to connections between items, so there are no duplicates
for(const isx in studyplans)
{
const studyplan = studyplans[isx];
ProcessStudyplan(studyplan);
}
return studyplans;
}
/**
* Perform initial processing on a downloaded studyplan
* Mainly used to create the proper references between items
* @param {Object} studyplan The studyplan to process
* @returns Processed studyplan
*/
export function ProcessStudyplan(studyplan){
let connections = {};
for(const il in studyplan.studylines) {
const line = studyplan.studylines[il];
for(const is in line.slots ) {
const slot = line.slots[is];
if(slot.competencies !== undefined){
for(const ic in slot.competencies){
const itm = slot.competencies[ic];
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];
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];
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];
if(conn.id in connections){
itm.connections[idx] = connections[conn.id];
} else {
connections[conn.id] = conn;
}
}
}
}
}
}
return studyplan;
}
/**
* Update the line wrapper elements to properly display the lines between items
*/
export function fixLineWrappers(){
let elmLineWrappers = document.getElementsByClassName('l-leaderline-linewrapper');
//debug.info("Line wrappers",elmLineWrappers);
for(let i =0; i < elmLineWrappers.length; i++){
const elm = elmLineWrappers[i];
elm.style.transform = '';
let rectWrapper = elm.getBoundingClientRect();
//debug.info("Line wrapper",elm,rectWrapper,[window.pageXOffset,window.pageYOffset]);
// Move to the origin of coordinates as the document
elm.style.transform = 'translate(' +
((rectWrapper.left + window.pageXOffset ) * -1) + 'px, ' +
((rectWrapper.top + window.pageYOffset ) * -1) + 'px)';
}
}

View File

@ -0,0 +1,107 @@
/*eslint no-var: "error"*/
/*eslint no-console: "off"*/
/*eslint-disable no-trailing-spaces */
/*eslint-env es6*/
// Put this file in path/to/plugin/amd/src
import {load_strings} from './string-helper';
export default {
studyplanTiming(a) {
const now = new Date().getTime();
let timing = 'future';
if(new Date(a.startdate).getTime() < now){
if(a.enddate && now > new Date(a.enddate).getTime()) {
timing = 'past';
} else {
timing = 'present';
}
}
return timing;
},
install(Vue/*,options*/){
let strings = load_strings({
studyplancard: {
open: "open",
noenddate: "noenddate",
}
});
Vue.component('s-studyplan-card', {
props: {
value: {
type: Object,
},
open: {
type: Boolean
}
},
data() {
return {
text: strings.studyplancard
};
},
computed: {
timing(){
const now = new Date().getTime();
const startdate = new Date(this.value.startdate).getTime();
const enddate = new Date(this.value.enddate).getTime();
let timing = 'future';
if(startdate < now){
if(this.value.enddate && now > enddate) {
timing = 'past';
} else {
timing = 'present';
}
}
return timing;
},
startdate(){
const opts = {
year: 'numeric', month: 'short', day: 'numeric'
};
return new Date(this.value.startdate).toLocaleDateString(document.documentElement.lang,opts);
},
enddate(){
if(this.value.enddate){
const opts = {
year: 'numeric', month: 'short', day: 'numeric'
};
return new Date(this.value.enddate).toLocaleDateString(document.documentElement.lang,opts);
}
else {
return this.text.noenddate;
}
}
},
methods: {
onOpenClick(e) {
this.$emit('open',e);
}
},
template: `
<b-card
:class="'s-studyplan-card timing-' + timing"
>
<template #header></template>
<b-card-title>
<a v-if='open' href='#' @click.prevent='onOpenClick($event)'>{{value.name}}</a>
<template v-else>{{value.name}}</template>
<slot name='title'></slot>
</b-card-title>
{{ value.description }}
<slot></slot>
<template #footer>
<span :class="'t-timing-'+timing" v-html="startdate + ' - '+ enddate"></span>
<span class="s-studyplan-card-buttons">
<slot name='footer'></slot>
<b-button style="float:right;" v-if='open' variant='primary'
@click.prevent='onOpenClick($event)'>{{ text.open }}</b-button>
</span>
</template>
</b-card>
`,
});
}
};

View File

@ -0,0 +1,268 @@
/* eslint-disable */
/*eslint no-unused-vars: "off" */
/**
* vue-class-component v7.2.5
* (c) 2015-present Evan You
* @license MIT
*/
import Vue from './vue';
// The rational behind the verbose Reflect-feature check below is the fact that there are polyfills
// which add an implementation for Reflect.defineMetadata but not for Reflect.getOwnMetadataKeys.
// Without this check consumers will encounter hard to track down runtime errors.
function reflectionIsSupported() {
return typeof Reflect !== 'undefined' && Reflect.defineMetadata && Reflect.getOwnMetadataKeys;
}
function copyReflectionMetadata(to, from) {
forwardMetadata(to, from);
Object.getOwnPropertyNames(from.prototype).forEach(key => {
forwardMetadata(to.prototype, from.prototype, key);
});
Object.getOwnPropertyNames(from).forEach(key => {
forwardMetadata(to, from, key);
});
}
function forwardMetadata(to, from, propertyKey) {
var metaKeys = propertyKey ? Reflect.getOwnMetadataKeys(from, propertyKey) : Reflect.getOwnMetadataKeys(from);
metaKeys.forEach(metaKey => {
var metadata = propertyKey ? Reflect.getOwnMetadata(metaKey, from, propertyKey) : Reflect.getOwnMetadata(metaKey, from);
if (propertyKey) {
Reflect.defineMetadata(metaKey, metadata, to, propertyKey);
} else {
Reflect.defineMetadata(metaKey, metadata, to);
}
});
}
var fakeArray = {
__proto__: []
};
var hasProto = fakeArray instanceof Array;
function createDecorator(factory) {
return (target, key, index) => {
var Ctor = typeof target === 'function' ? target : target.constructor;
if (!Ctor.__decorators__) {
Ctor.__decorators__ = [];
}
if (typeof index !== 'number') {
index = undefined;
}
Ctor.__decorators__.push(options => factory(options, key, index));
};
}
function mixins() {
for (var _len = arguments.length, Ctors = new Array(_len), _key = 0; _key < _len; _key++) {
Ctors[_key] = arguments[_key];
}
return Vue.extend({
mixins: Ctors
});
}
function isPrimitive(value) {
var type = typeof value;
return value == null || type !== 'object' && type !== 'function';
}
function warn(message) {
if (typeof console !== 'undefined') {
console.warn('[vue-class-component] ' + message);
}
}
function collectDataFromConstructor(vm, Component) {
// override _init to prevent to init as Vue instance
var originalInit = Component.prototype._init;
Component.prototype._init = function () {
// proxy to actual vm
var keys = Object.getOwnPropertyNames(vm); // 2.2.0 compat (props are no longer exposed as self properties)
if (vm.$options.props) {
for (var key in vm.$options.props) {
if (!vm.hasOwnProperty(key)) {
keys.push(key);
}
}
}
keys.forEach(key => {
Object.defineProperty(this, key, {
get: () => vm[key],
set: value => {
vm[key] = value;
},
configurable: true
});
});
}; // should be acquired class property values
var data = new Component(); // restore original _init to avoid memory leak (#209)
Component.prototype._init = originalInit; // create plain data object
var plainData = {};
Object.keys(data).forEach(key => {
if (data[key] !== undefined) {
plainData[key] = data[key];
}
});
{
if (!(Component.prototype instanceof Vue) && Object.keys(plainData).length > 0) {
warn('Component class must inherit Vue or its descendant class ' + 'when class property is used.');
}
}
return plainData;
}
var $internalHooks = ['data', 'beforeCreate', 'created', 'beforeMount', 'mounted', 'beforeDestroy', 'destroyed', 'beforeUpdate', 'updated', 'activated', 'deactivated', 'render', 'errorCaptured', 'serverPrefetch' // 2.6
];
function componentFactory(Component) {
var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
options.name = options.name || Component._componentTag || Component.name; // prototype props.
var proto = Component.prototype;
Object.getOwnPropertyNames(proto).forEach(function (key) {
if (key === 'constructor') {
return;
} // hooks
if ($internalHooks.indexOf(key) > -1) {
options[key] = proto[key];
return;
}
var descriptor = Object.getOwnPropertyDescriptor(proto, key);
if (descriptor.value !== void 0) {
// methods
if (typeof descriptor.value === 'function') {
(options.methods || (options.methods = {}))[key] = descriptor.value;
} else {
// typescript decorated data
(options.mixins || (options.mixins = [])).push({
data() {
return {
[key]: descriptor.value
};
}
});
}
} else if (descriptor.get || descriptor.set) {
// computed properties
(options.computed || (options.computed = {}))[key] = {
get: descriptor.get,
set: descriptor.set
};
}
});
(options.mixins || (options.mixins = [])).push({
data() {
return collectDataFromConstructor(this, Component);
}
}); // decorate options
var decorators = Component.__decorators__;
if (decorators) {
decorators.forEach(fn => fn(options));
delete Component.__decorators__;
} // find super
var superProto = Object.getPrototypeOf(Component.prototype);
var Super = superProto instanceof Vue ? superProto.constructor : Vue;
var Extended = Super.extend(options);
forwardStaticMembers(Extended, Component, Super);
if (reflectionIsSupported()) {
copyReflectionMetadata(Extended, Component);
}
return Extended;
}
var reservedPropertyNames = [// Unique id
'cid', // Super Vue constructor
'super', // Component options that will be used by the component
'options', 'superOptions', 'extendOptions', 'sealedOptions', // Private assets
'component', 'directive', 'filter'];
var shouldIgnore = {
prototype: true,
arguments: true,
callee: true,
caller: true
};
function forwardStaticMembers(Extended, Original, Super) {
// We have to use getOwnPropertyNames since Babel registers methods as non-enumerable
Object.getOwnPropertyNames(Original).forEach(key => {
// Skip the properties that should not be overwritten
if (shouldIgnore[key]) {
return;
} // Some browsers does not allow reconfigure built-in properties
var extendedDescriptor = Object.getOwnPropertyDescriptor(Extended, key);
if (extendedDescriptor && !extendedDescriptor.configurable) {
return;
}
var descriptor = Object.getOwnPropertyDescriptor(Original, key); // If the user agent does not support `__proto__` or its family (IE <= 10),
// the sub class properties may be inherited properties from the super class in TypeScript.
// We need to exclude such properties to prevent to overwrite
// the component options object which stored on the extended constructor (See #192).
// If the value is a referenced value (object or function),
// we can check equality of them and exclude it if they have the same reference.
// If it is a primitive value, it will be forwarded for safety.
if (!hasProto) {
// Only `cid` is explicitly exluded from property forwarding
// because we cannot detect whether it is a inherited property or not
// on the no `__proto__` environment even though the property is reserved.
if (key === 'cid') {
return;
}
var superDescriptor = Object.getOwnPropertyDescriptor(Super, key);
if (!isPrimitive(descriptor.value) && superDescriptor && superDescriptor.value === descriptor.value) {
return;
}
} // Warn if the users manually declare reserved properties
if ( reservedPropertyNames.indexOf(key) >= 0) {
warn("Static property name '".concat(key, "' declared on class '").concat(Original.name, "' ") + 'conflicts with reserved property name of Vue internal. ' + 'It may cause unexpected behavior of the component. Consider renaming the property.');
}
Object.defineProperty(Extended, key, descriptor);
});
}
function Component(options) {
if (typeof options === 'function') {
return componentFactory(options);
}
return function (Component) {
return componentFactory(Component, options);
};
}
Component.registerHooks = function registerHooks(keys) {
$internalHooks.push(...keys);
};
export { Component, createDecorator, mixins };

1578
amd/src/vue-easy-dnd.js Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,84 @@
/*eslint no-console: "off" */
import {InitializePicker} from './hsluvpicker';
export default {
name: 'hsluv-picker',
props: {
value: {
type: String,
default: "#cccccc",
},
nodisplay: {
type: Boolean,
default: false,
},
displaysize: {
type: String,
default: 300,
},
horizontal: {
type: Boolean,
default: false,
}
},
watch: {
value: function(newVal){
if(newVal != this.currentcolor){
this.$refs.picker.dispatchEvent(new CustomEvent('updatecolor', {detail: newVal}));
this.currentcolor = newVal;
}
}
},
data() {
return {
currentcolor: "",
};
},
methods: {
onColorChange(event) {
this.currentcolor = event.detail;
this.$emit("input",event.detail);
}
},
mounted(){
this.currentcolor=this.value;
InitializePicker(this.$refs.picker, this.value);
},
template: `
<div ref="picker" @colorchange="onColorChange" :class="'vue-hsluv-picker'+(horizontal?' horizontal':'')">
<div v-if="!nodisplay" class="display">
<canvas :height="displaysize" :width="displaysize"></canvas>
</div>
<table class='picker'>
<tr class="control-h">
<td class="cell-input">
<input type="number" min="0" max="360" step="any" class="counter counter-hue" tabindex="0"/>
</td>
<td><div class="range-slider"></div></td>
<td class="picker-label">H</td>
</tr>
<tr class="control-s">
<td class="cell-input">
<input type="number" step="any" min="0" max="100" class="counter counter-saturation"/>
</td>
<td><div class="range-slider"></div></td>
<td class="picker-label">S</td>
</tr>
<tr class="control-l">
<td class="cell-input">
<input type="number" step="any" min="0" max="100" class="counter counter-lightness"/>
</td>
<td><div class="range-slider"></div></td>
<td class="picker-label">L</td>
</tr>
<tr>
<td class="cell-input cell-input-hex">
<input ref="input" class="input-hex" pattern="#?[0-9a-fA-F]{6}"/>
</td>
<td><div class="swatch"></div></td>
<td></td>
</tr>
</table>
</div>
</div>
`,
};

View File

@ -0,0 +1,282 @@
/* eslint-disable */
/*eslint no-unused-vars: "off" */
/** vue-property-decorator verson 9.0.0 MIT LICENSE copyright 2020 kaorun343 */
/// <reference types='reflect-metadata'/>
'use strict';
import Vue from './vue';
import {Component, createDecorator, mixins } from './vue-class-component';
export { Component, Vue, mixins as Mixins };
/** Used for keying reactive provide/inject properties */
var reactiveInjectKey = '__reactiveInject__';
/**
* decorator of an inject
* @param from key
* @return PropertyDecorator
*/
export function Inject(options) {
return createDecorator(function (componentOptions, key) {
if (typeof componentOptions.inject === 'undefined') {
componentOptions.inject = {};
}
if (!Array.isArray(componentOptions.inject)) {
componentOptions.inject[key] = options || key;
}
});
}
/**
* decorator of a reactive inject
* @param from key
* @return PropertyDecorator
*/
export function InjectReactive(options) {
return createDecorator(function (componentOptions, key) {
if (typeof componentOptions.inject === 'undefined') {
componentOptions.inject = {};
}
if (!Array.isArray(componentOptions.inject)) {
var fromKey_1 = !!options ? options.from || options : key;
var defaultVal_1 = (!!options && options.default) || undefined;
if (!componentOptions.computed)
componentOptions.computed = {};
componentOptions.computed[key] = function () {
var obj = this[reactiveInjectKey];
return obj ? obj[fromKey_1] : defaultVal_1;
};
componentOptions.inject[reactiveInjectKey] = reactiveInjectKey;
}
});
}
function produceProvide(original) {
var provide = function () {
var _this = this;
var rv = typeof original === 'function' ? original.call(this) : original;
rv = Object.create(rv || null);
// set reactive services (propagates previous services if necessary)
rv[reactiveInjectKey] = this[reactiveInjectKey] || {};
for (var i in provide.managed) {
rv[provide.managed[i]] = this[i];
}
var _loop_1 = function (i) {
rv[provide.managedReactive[i]] = this_1[i]; // Duplicates the behavior of `@Provide`
Object.defineProperty(rv[reactiveInjectKey], provide.managedReactive[i], {
enumerable: true,
get: function () { return _this[i]; },
});
};
var this_1 = this;
for (var i in provide.managedReactive) {
_loop_1(i);
}
return rv;
};
provide.managed = {};
provide.managedReactive = {};
return provide;
}
function needToProduceProvide(original) {
return (typeof original !== 'function' ||
(!original.managed && !original.managedReactive));
}
/**
* decorator of a provide
* @param key key
* @return PropertyDecorator | void
*/
export function Provide(key) {
return createDecorator(function (componentOptions, k) {
var provide = componentOptions.provide;
if (needToProduceProvide(provide)) {
provide = componentOptions.provide = produceProvide(provide);
}
provide.managed[k] = key || k;
});
}
/**
* decorator of a reactive provide
* @param key key
* @return PropertyDecorator | void
*/
export function ProvideReactive(key) {
return createDecorator(function (componentOptions, k) {
var provide = componentOptions.provide;
// inject parent reactive services (if any)
if (!Array.isArray(componentOptions.inject)) {
componentOptions.inject = componentOptions.inject || {};
componentOptions.inject[reactiveInjectKey] = {
from: reactiveInjectKey,
default: {},
};
}
if (needToProduceProvide(provide)) {
provide = componentOptions.provide = produceProvide(provide);
}
provide.managedReactive[k] = key || k;
});
}
/** @see {@link https://github.com/vuejs/vue-class-component/blob/master/src/reflect.ts} */
var reflectMetadataIsSupported = typeof Reflect !== 'undefined' && typeof Reflect.getMetadata !== 'undefined';
function applyMetadata(options, target, key) {
if (reflectMetadataIsSupported) {
if (!Array.isArray(options) &&
typeof options !== 'function' &&
typeof options.type === 'undefined') {
var type = Reflect.getMetadata('design:type', target, key);
if (type !== Object) {
options.type = type;
}
}
}
}
/**
* decorator of model
* @param event event name
* @param options options
* @return PropertyDecorator
*/
export function Model(event, options) {
if (options === void 0) { options = {}; }
return function (target, key) {
applyMetadata(options, target, key);
createDecorator(function (componentOptions, k) {
;
(componentOptions.props || (componentOptions.props = {}))[k] = options;
componentOptions.model = { prop: k, event: event || k };
})(target, key);
};
}
/**
* decorator of a prop
* @param options the options for the prop
* @return PropertyDecorator | void
*/
export function Prop(options) {
if (options === void 0) { options = {}; }
return function (target, key) {
applyMetadata(options, target, key);
createDecorator(function (componentOptions, k) {
;
(componentOptions.props || (componentOptions.props = {}))[k] = options;
})(target, key);
};
}
/**
* decorator of a synced prop
* @param propName the name to interface with from outside, must be different from decorated property
* @param options the options for the synced prop
* @return PropertyDecorator | void
*/
export function PropSync(propName, options) {
if (options === void 0) { options = {}; }
// @ts-ignore
return function (target, key) {
applyMetadata(options, target, key);
createDecorator(function (componentOptions, k) {
;
(componentOptions.props || (componentOptions.props = {}))[propName] = options;
(componentOptions.computed || (componentOptions.computed = {}))[k] = {
get: function () {
return this[propName];
},
set: function (value) {
// @ts-ignore
this.$emit("update:" + propName, value);
},
};
})(target, key);
};
}
/**
* decorator of a watch function
* @param path the path or the expression to observe
* @param WatchOption
* @return MethodDecorator
*/
export function Watch(path, options) {
if (options === void 0) { options = {}; }
var _a = options.deep, deep = _a === void 0 ? false : _a, _b = options.immediate, immediate = _b === void 0 ? false : _b;
return createDecorator(function (componentOptions, handler) {
if (typeof componentOptions.watch !== 'object') {
componentOptions.watch = Object.create(null);
}
var watch = componentOptions.watch;
if (typeof watch[path] === 'object' && !Array.isArray(watch[path])) {
watch[path] = [watch[path]];
}
else if (typeof watch[path] === 'undefined') {
watch[path] = [];
}
watch[path].push({ handler: handler, deep: deep, immediate: immediate });
});
}
// Code copied from Vue/src/shared/util.js
var hyphenateRE = /\B([A-Z])/g;
var hyphenate = function (str) { return str.replace(hyphenateRE, '-$1').toLowerCase(); };
/**
* decorator of an event-emitter function
* @param event The name of the event
* @return MethodDecorator
*/
export function Emit(event) {
return function (_target, propertyKey, descriptor) {
var key = hyphenate(propertyKey);
var original = descriptor.value;
descriptor.value = function emitter() {
var _this = this;
var args = [];
for (var _i = 0; _i < arguments.length; _i++) {
args[_i] = arguments[_i];
}
var emit = function (returnValue) {
var emitName = event || key;
if (returnValue === undefined) {
if (args.length === 0) {
_this.$emit(emitName);
}
else if (args.length === 1) {
_this.$emit(emitName, args[0]);
}
else {
_this.$emit.apply(_this, [emitName].concat(args));
}
}
else {
if (args.length === 0) {
_this.$emit(emitName, returnValue);
}
else if (args.length === 1) {
_this.$emit(emitName, returnValue, args[0]);
}
else {
_this.$emit.apply(_this, [emitName, returnValue].concat(args));
}
}
};
var returnValue = original.apply(this, args);
if (isPromise(returnValue)) {
returnValue.then(emit);
}
else {
emit(returnValue);
}
return returnValue;
};
};
}
/**
* decorator of a ref prop
* @param refKey the ref key defined in template
*/
export function Ref(refKey) {
return createDecorator(function (options, key) {
options.computed = options.computed || {};
options.computed[key] = {
cache: false,
get: function () {
return this.$refs[refKey || key];
},
};
});
}
function isPromise(obj) {
return obj instanceof Promise || (obj && typeof obj.then === 'function');
}

11967
amd/src/vue.js Normal file

File diff suppressed because it is too large Load Diff

232
cfg_grades.php Normal file
View File

@ -0,0 +1,232 @@
<?php
require_once("../../config.php");
require_once($CFG->libdir.'/adminlib.php');
admin_externalpage_setup("local_treestudyplan_gradeconfig");
$systemcontext = context_system::instance();
// Check if user has capability to manage this
require_capability('local/treestudyplan:configure', $systemcontext);
$PAGE->requires->js_call_amd('local_treestudyplan/cfg-grades', 'init');
const GRADECFG_TABLE = "local_treestudyplan_gradecfg";
$scales = \grade_scale::fetch_all_global();
$mappings = $DB->get_records(GRADECFG_TABLE);
$scale_cfgs = [];
$grade_cfgs = [];
foreach($mappings as $cfg){
if(!empty($cfg->scale_id)){
$scale_cfgs[$cfg->scale_id] = $cfg;
}
elseif(!empty($cfg->grade_points)){
$grade_cfgs[$cfg->grade_points] = $cfg;
}
}
print $OUTPUT->header();
if($_POST["action"] == "update"){
// First loop through the scales to see which need to be updated
foreach($scales as $scale)
{
if(array_key_exists($scale->id,$scale_cfgs)){
$scalecfg = $scale_cfgs[$scale->id];
$needupdate = false;
foreach(["min_progress", "min_completed"] as $handle) {
$key = "s_{$scale->id}_{$handle}";
if(array_key_exists($key,$_POST) && is_numeric($_POST[$key])){
$value = intval($_POST[$key]);
if($value != $scalecfg->$handle){
$scalecfg->$handle = $value;
$needupdate = true;
}
}
}
if(needupdate){
$DB->update_record(GRADECFG_TABLE,$scalecfg);
}
}
else {
$scalecfg = (object)[ "scale_id" => $scale->id,];
$requireinsert = false;
foreach(["min_progress", "min_completed"] as $handle) {
$key = "s_{$scale->id}_{$handle}";
if(array_key_exists($key,$_POST) && is_numeric($_POST[$key])){
$scalecfg->$handle = intval($_POST[$key]);
$requireinsert = true;
}
}
if($requireinsert){
// Insert into database and add to the list of scale configs
$id = $DB->insert_record(GRADECFG_TABLE,$scalecfg);
$scalecfg = $DB->get_record(GRADECFG_TABLE,['id' => $id]);
$scale_cfgs[$id] = $scalecfg;
}
}
}
// Now, loop through the gradepoints to parse
$deletelist = [];
foreach($grade_cfgs as $gradecfg){
$deletekey = "g_{$gradecfg->grade_points}_delete";
if(array_key_exists($deletekey,$_POST) && boolval($_POST[$deletekey]) === true){
$DB->delete_records(GRADECFG_TABLE,["id" => $gradecfg->id]);
$deletelist[] = $gradecfg;
}
else
{
foreach(["min_progress", "min_completed"] as $handle) {
$key = "g_{$gradecfg->grade_points}_{$handle}";
if(array_key_exists($key,$_POST) && is_numeric($_POST[$key])){
$gradecfg->$handle = floatval($_POST[$key]);
}
}
$DB->update_record(GRADECFG_TABLE,$gradecfg);
// reload to ensure proper rounding is done
$grade_cfgs[$gradecfg->grade_points] = $DB->get_record(GRADECFG_TABLE,['id' => $gradecfg->id]);
}
}
foreach($deletelist as $gradeconfig){
unset($grade_cfgs[$gradecfg->grade_points]);
}
unset($deletelist);
// And add an optionally existing new gradepoint setting
if(array_key_exists("g_new_gradepoints",$_POST) && !empty($_POST["g_new_gradepoints"]) && is_numeric($_POST["g_new_gradepoints"]) ){
$gp = intval($_POST["g_new_gradepoints"]);
if(!array_key_exists($gp,$grade_cfgs)){
$gradecfg = (object)[ "grade_points" => $gp];
$requireinsert = false;
foreach(["min_progress", "min_completed"] as $handle) {
$key = "g_new_{$handle}";
if(array_key_exists($key,$_POST) && is_numeric($_POST[$key])){
$gradecfg->$handle = floatval($_POST[$key]);
$requireinsert = true;
}
}
if($requireinsert){
// Insert into database and add to the list of grade configs
$id = $DB->insert_record(GRADECFG_TABLE,$gradecfg);
// reload to ensure proper rounding is done
$gradecfg = $DB->get_record(GRADECFG_TABLE,['id' => $id]);
$grade_cfgs[$id] = $gradecfg;
}
}
}
}
//process all available scales and load the current configuration for it.
$data = [];
foreach($scales as $scale)
{
$scale->load_items();
$scalecfg = null;
if(array_key_exists($scale->id,$scale_cfgs)){
$scalecfg = $scale_cfgs[$scale->id];
}
$attrs_c = ['value' => '','disabled' => 'disabled', ];
$attrs_p = ['value' => '','disabled' => 'disabled', ];
if(!isset($scalecfg) || $scalecfg->min_completed == ""){
$attrs_c["selected"] = "selected";
}
if(!isset($scalecfg) || $scalecfg->min_progress == ""){
$attrs_p["selected"] = "selected";
}
$options_completed = html_writer::tag("option",get_string('select_scaleitem','local_treestudyplan'),$attrs_c);
$options_progress = html_writer::tag("option",get_string('select_scaleitem','local_treestudyplan'),$attrs_p);
$key = 1; // Start counting by one, as used in sum aggregations
foreach($scale->scale_items as $value){
$attrs_c = ["value" => $key];
$attrs_p = ["value" => $key];
if(isset($scalecfg)){
if(intval($scalecfg->min_completed) == $key){
$attrs_c["selected"] = "selected";
}
if(intval($scalecfg->min_progress) == $key){
$attrs_p["selected"] = "selected";
}
}
$options_progress .= html_writer::tag("option",$value,$attrs_p);
$options_completed .= html_writer::tag("option",$value,$attrs_c);
$key++;
}
$row = [];
$row[] = $scale->name;
$row[] = html_writer::tag("select", $options_progress, ['name' => "s_{$scale->id}_min_progress",'autocomplete' => 'off']) ;
$row[] = html_writer::tag("select", $options_completed, ['name' => "s_{$scale->id}_min_completed",'autocomplete' => 'off']) ;
$data[] = $row;
}
print html_writer::start_tag("form",["method" => "post",]);
print html_writer::tag("input", null, ['name' => "action", 'value' => 'update', 'type' => 'hidden']);
$table = new html_table();
$table->id = "";
$table->attributes['class'] = 'generaltable m-roomtable';
$table->tablealign = 'center';
$table->summary = '';//get_string('uploadtimetable_preview', 'local_chronotable');
$table->head = [];
$table->data = $data;
$table->head[] = get_string('scale');
$table->head[] = get_string('min_progress', 'local_treestudyplan');
$table->head[] = get_string('min_completed', 'local_treestudyplan');
print $OUTPUT->heading(get_string('cfg_grades_desc_head', 'local_treestudyplan'));
print html_writer::tag('p', get_string('cfg_grades_desc', 'local_treestudyplan'));
print $OUTPUT->heading(get_string('cfg_grades_scales', 'local_treestudyplan'));
print html_writer::tag('div', html_writer::table($table), ['class'=>'flexible-wrap']);
$data = [];
foreach($grade_cfgs as $g){
$row = [];
$row[] = $g->grade_points;
$row[] = html_writer::tag("input", null, ['name' => "g_{$g->grade_points}_min_progress", 'value' => "{$g->min_progress}", 'type' => 'text', "class" => "float", 'autocomplete' => 'off']) ;
$row[] = html_writer::tag("input", null, ['name' => "g_{$g->grade_points}_min_completed", 'value' => "{$g->min_completed}", 'type' => 'text', "class" => "float", 'autocomplete' => 'off']) ;
$row[] = html_writer::tag("input", null, ['name' => "g_{$g->grade_points}_delete", 'type' => 'checkbox', ]) ;
$data[] = $row;
}
$row = [];
$row[] = html_writer::tag("input", null, ['name' => "g_new_gradepoints", 'value' => '', 'type' => 'number', 'min' => '0', 'pattern' => '/d+', 'step' => '1', 'autocomplete' => 'off']);
$row[] = html_writer::tag("input", null, ['name' => "g_new_min_progress", 'value' => '', 'type' => 'text', "class" => "float", 'autocomplete' => 'off']) ;
$row[] = html_writer::tag("input", null, ['name' => "g_new_min_completed", 'value' => '', 'type' => 'text',"class" => "float", 'autocomplete' => 'off']) ;
$data[] = $row;
$table = new html_table();
$table->id = "";
$table->attributes['class'] = 'generaltable m-roomtable';
$table->tablealign = 'center';
$table->summary = '';//get_string('uploadtimetable_preview', 'local_chronotable');
$table->head = [];
$table->data = $data;
$table->head[] = get_string('grade_points', 'local_treestudyplan');
$table->head[] = get_string('min_progress', 'local_treestudyplan');
$table->head[] = get_string('min_completed', 'local_treestudyplan');
$table->head[] = get_string('delete',);
print $OUTPUT->heading(get_string('cfg_grades_grades', 'local_treestudyplan'));
print html_writer::tag('div', html_writer::table($table), ['class'=>'flexible-wrap']);
print html_writer::tag("input", null, ['value' => get_string("save"), 'type' => 'submit', "class" => "btn btn-primary"]);
print html_writer::end_tag("form");
print $OUTPUT->footer();

120
classes/aggregator.php Normal file
View File

@ -0,0 +1,120 @@
<?php
namespace local_treestudyplan;
require_once($CFG->libdir.'/externallib.php');
abstract class aggregator {
private const FALLBACK = "bistate";
private static $mod_supported = [];
public static function supported($mod){
if(!array_key_exists($mod,self::$mod_supported)){
self::$mod_supported[$mod] = class_exists(self::aggregator_name($mod));
}
return self::$mod_supported[$mod];
}
private static function aggregator_name($mod){
return "\local_treestudyplan\\local\\aggregators\\{$mod}_aggregator";
}
public static function list(){
// static list, since we'd need to implement a lot of static data for new aggregation methods anyway
// and this is faster than any dynamic method.
return [
"core", # use moodle core completion
"bistate",
"tristate", # deprecated
];
}
public static function create($mod,$configstr){
if(self::supported($mod)){
$ag_class = self::aggregator_name($mod);
return new $ag_class($configstr);
} else {
throw new \InvalidArgumentException("Cannot find aggregator '{$mod}'");
}
}
public static function createOrDefault($mod,$configstr){
try {
return self::create($mod,$configstr);
}
catch(\ValueError $x){
return self::create(self::FALLBACK,"");
}
}
private function __construct($configstr) {
$this->initialize($configstr);
}
protected function initialize($configstr) {}
public abstract function needSelectGradables();
public abstract function isDeprecated();
public abstract function aggregate_course(courseinfo $courseinfo, studyitem $studyitem, $userid);
public abstract function aggregate_junction(array $completion, studyitem $studyitem, $userid);
public abstract function grade_completion(gradeinfo $gradeinfo, $userid);
// Aggregation method makes use of "required grades" in a course/module
public abstract function useRequiredGrades();
// Aggregation method makes use of
public abstract function useItemConditions();
// Whether the aggregation method uses core_completion, or treestudyplan custom completion
public function usecorecompletioninfo(){
return false;
}
// Parameter editing functions - override in child class to implement parameter config for aggregation
// Return the current configuration string
public function config_string() {
return "";
}
public static function basic_structure($value=VALUE_REQUIRED){
return new \external_single_structure([
"useRequiredGrades" => new \external_value(PARAM_BOOL, 'id of studyplan'),
"useItemConditions" => new \external_value(PARAM_BOOL, 'name of studyplan'),
],"Aggregator requirements",$value);
}
public function basic_model(){
return [
"useRequiredGrades" => $this->useRequiredGrades(),
"useItemConditions" => $this->useItemConditions(),
];
}
public static function list_structure($value=VALUE_REQUIRED){
return new \external_multiple_structure(new \external_single_structure([
"id" => new \external_value(PARAM_TEXT, 'id of aggregator'),
"name" => new \external_value(PARAM_TEXT, 'name of agregator'),
"deprecated" => new \external_value(PARAM_BOOL, 'if method is deprecated'),
"defaultconfig" => new \external_value(PARAM_TEXT, 'default config of agregator'),
],"Available aggregators",$value));
}
public static function list_model(){
$list = [];
foreach(self::list() as $agid){
$a = self::create($agid,""); // create new one with empty config string
$list[] = [
'id' => $agid,
'name' => get_string("{$agid}_aggregator_title","local_treestudyplan"),
'deprecated' => $a->isDeprecated(),
'defaultconfig' => $a->config_string(),
];
}
return $list;
}
}

View File

@ -0,0 +1,408 @@
<?php
namespace local_treestudyplan;
require_once($CFG->libdir.'/externallib.php');
class associationservice extends \external_api
{
public static function user_structure(){
return new \external_single_structure([
"id" => new \external_value(PARAM_INT, 'user id'),
"username" => new \external_value(PARAM_TEXT, 'username'),
"firstname" => new \external_value(PARAM_TEXT, 'first name'),
"lastname" => new \external_value(PARAM_TEXT, 'last name'),
"idnumber" => new \external_value(PARAM_TEXT, 'id number'),
"email" => new \external_value(PARAM_TEXT, 'email address'),
]);
}
public static function make_user_model($r){
return [
"id" => $r->id,
"username" => $r->username,
"firstname" => $r->firstname,
"lastname" => $r->lastname,
"idnumber" => $r->idnumber,
"email" => $r->email,
];
}
public static function cohort_structure(){
return new \external_single_structure([
"id" => new \external_value(PARAM_INT, 'cohort id'),
"name" => new \external_value(PARAM_TEXT, 'name'),
"idnumber" => new \external_value(PARAM_TEXT, 'id number'),
"description" => new \external_value(PARAM_TEXT, 'description'),
"visible" => new \external_value(PARAM_BOOL, 'is visible'),
"context" => new \external_single_structure([
"name" => new \external_value(PARAM_TEXT, 'context name'),
"shortname" => new \external_value(PARAM_TEXT, 'context short name'),
"path" => new \external_multiple_structure( new \external_value(PARAM_TEXT)),
"shortpath" => new \external_multiple_structure( new \external_value(PARAM_TEXT)),
], 'context information', VALUE_OPTIONAL),
]);
}
public static function make_cohort_model($r){
global $DB;
$ctx = \context::instance_by_id($r->contextid);
$ctxPath = array_reverse($ctx->get_parent_context_ids(true));
if(count($ctxPath) > 1 && $ctxPath[0] == 1) {
array_shift($ctxPath);
}
$result = [
"id" => $r->id,
"name" => $r->name,
"idnumber" => $r->idnumber,
"description" => $r->description,
"visible" => $r->visible,
"context" => [
"name" => $ctx->get_context_name(false,false),
"shortname" => $ctx->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),
]
];
return $result;
}
public static function list_cohort_parameters()
{
return new \external_function_parameters( [
'like' => new \external_value(PARAM_TEXT, 'search text', VALUE_OPTIONAL),
'exclude_id' => new \external_value(PARAM_INT, 'exclude members of this studyplan', VALUE_OPTIONAL),
] );
}
public static function list_cohort_returns()
{
return new \external_multiple_structure(self::cohort_structure());
}
// Actual functions
public static function list_cohort($like='',$exclude_id=null)
{
global $CFG, $DB;
$pattern = "%{$like}%";
$params = ["pattern_nm" => $pattern,"pattern_id" => $pattern,];
$sql = "SELECT c.* from {cohort} c LEFT JOIN {local_treestudyplan_cohort} j ON c.id = j.cohort_id";
$sql .= " WHERE c.visible = 1 AND(name LIKE :pattern_nm OR idnumber LIKE :pattern_id)";
if(isset($exclude_id) && is_numeric($exclude_id)){
$sql .= " AND (j.studyplan_id IS NULL OR j.studyplan_id != :exclude_id)";
$params['exclude_id'] = $exclude_id;
}
$cohorts = [];
$rs = $DB->get_recordset_sql($sql,$params);
foreach ($rs as $r) {
$cohorts[] = static::make_cohort_model($r);
}
$rs->close();
return $cohorts;
}
public static function find_user_parameters()
{
return new \external_function_parameters( [
'like' => new \external_value(PARAM_TEXT, 'search text'),
'exclude_id' => new \external_value(PARAM_INT, 'exclude members of this studyplan', VALUE_OPTIONAL),
] );
}
public static function find_user_returns()
{
return new \external_multiple_structure(self::user_structure());
}
// Actual functions
public static function find_user($like,$exclude_id=null)
{
global $CFG, $DB;
$pattern = "%{$like}%";
$params = ["pattern_fn" => $pattern,
"pattern_ln" => $pattern,
"pattern_un" => $pattern,
];
$sql = "SELECT u.* from {user} u LEFT JOIN {local_treestudyplan_user} j ON u.id = j.user_id";
$sql .= " WHERE u.deleted != 1 AND (firstname LIKE :pattern_fn OR lastname LIKE :pattern_ln OR username LIKE :pattern_un)";
if(isset($exclude_id) && is_numeric($exclude_id)){
$sql .= " AND (j.studyplan_id IS NULL OR j.studyplan_id != :exclude_id)";
$params['exclude_id'] = $exclude_id;
}
$users = [];
$rs = $DB->get_recordset_sql($sql, $params);
foreach ($rs as $r) {
$users[] = static::make_user_model($r);
}
$rs->close();
self::sortusermodels($users);
return $users;
}
public static function connect_cohort_parameters()
{
return new \external_function_parameters( [
"studyplan_id" => new \external_value(PARAM_INT, 'id of studyplan', VALUE_OPTIONAL),
"cohort_id" => new \external_value(PARAM_INT, 'id of cohort to link', VALUE_OPTIONAL),
] );
}
public static function connect_cohort_returns()
{
return new \external_single_structure([
"success" => new \external_value(PARAM_BOOL, 'operation completed succesfully'),
"msg" => new \external_value(PARAM_TEXT, 'message'),
]);
}
// Actual functions
public static function connect_cohort($studyplan_id,$cohort_id)
{
global $CFG, $DB;
if(!$DB->record_exists('local_treestudyplan_cohort', ['studyplan_id' => $studyplan_id, 'cohort_id' => $cohort_id]))
{
$id = $DB->insert_record('local_treestudyplan_cohort', [
'studyplan_id' => $studyplan_id,
'cohort_id' => $cohort_id,
]);
return ['success' => true, 'msg'=>'Cohort connected'];
} else {
return ['success' => true, 'msg'=>'Cohort already connected'];
}
}
public static function disconnect_cohort_parameters()
{
return new \external_function_parameters( [
"studyplan_id" => new \external_value(PARAM_INT, 'id of studyplan', VALUE_OPTIONAL),
"cohort_id" => new \external_value(PARAM_INT, 'id of cohort to link', VALUE_OPTIONAL),
] );
}
public static function disconnect_cohort_returns()
{
return new \external_single_structure([
"success" => new \external_value(PARAM_BOOL, 'operation completed succesfully'),
"msg" => new \external_value(PARAM_TEXT, 'message'),
]);
}
// Actual functions
public static function disconnect_cohort($studyplan_id,$cohort_id)
{
global $CFG, $DB;
if($DB->record_exists('local_treestudyplan_cohort', ['studyplan_id' => $studyplan_id, 'cohort_id' => $cohort_id]))
{
$DB->delete_records('local_treestudyplan_cohort', [
'studyplan_id' => $studyplan_id,
'cohort_id' => $cohort_id,
]);
return ['success' => true, 'msg'=>'Cohort Disconnected'];
} else {
return ['success' => true, 'msg'=>'Connection does not exist'];
}
}
public static function connect_user_parameters()
{
return new \external_function_parameters( [
"studyplan_id" => new \external_value(PARAM_INT, 'id of studyplan', VALUE_OPTIONAL),
"user_id" => new \external_value(PARAM_INT, 'id of user to link', VALUE_OPTIONAL),
] );
}
public static function connect_user_returns()
{
return new \external_single_structure([
"success" => new \external_value(PARAM_BOOL, 'operation completed succesfully'),
"msg" => new \external_value(PARAM_TEXT, 'message'),
]);
}
// Actual functions
public static function connect_user($studyplan_id,$user_id)
{
global $CFG, $DB;
if(!$DB->record_exists('local_treestudyplan_user', ['studyplan_id' => $studyplan_id, 'user_id' => $user_id]))
{
$id = $DB->insert_record('local_treestudyplan_user', [
'studyplan_id' => $studyplan_id,
'user_id' => $user_id,
]);
return ['success' => true, 'msg'=>'Cohort connected'];
} else {
return ['success' => true, 'msg'=>'Cohort already connected'];
}
}
public static function disconnect_user_parameters()
{
return new \external_function_parameters( [
"studyplan_id" => new \external_value(PARAM_INT, 'id of studyplan', VALUE_OPTIONAL),
"user_id" => new \external_value(PARAM_INT, 'id of user to link', VALUE_OPTIONAL),
] );
}
public static function disconnect_user_returns()
{
return new \external_single_structure([
"success" => new \external_value(PARAM_BOOL, 'operation completed succesfully'),
"msg" => new \external_value(PARAM_TEXT, 'message'),
]);
}
// Actual functions
public static function disconnect_user($studyplan_id,$user_id)
{
global $CFG, $DB;
if($DB->record_exists('local_treestudyplan_user', ['studyplan_id' => $studyplan_id, 'user_id' => $user_id]))
{
$DB->delete_records('local_treestudyplan_user', [
'studyplan_id' => $studyplan_id,
'user_id' => $user_id,
]);
return ['success' => true, 'msg'=>'USer Disconnected'];
} else {
return ['success' => true, 'msg'=>'Connection does not exist'];
}
}
public static function associated_users_parameters()
{
return new \external_function_parameters( [
"studyplan_id" => new \external_value(PARAM_INT, 'id of studyplan', VALUE_OPTIONAL),
] );
}
public static function associated_users_returns()
{
return new \external_multiple_structure(self::user_structure());
}
// Actual functions
public static function associated_users($studyplan_id)
{
global $CFG, $DB;
$sql = "SELECT DISTINCT u.* FROM {user} u INNER JOIN {local_treestudyplan_user} j ON j.user_id = u.id";
$sql .= " WHERE j.studyplan_id = :studyplan_id";
$rs = $DB->get_recordset_sql($sql, ['studyplan_id' => $studyplan_id]);
$users = [];
foreach($rs as $u)
{
$users[] = self::make_user_model($u);
}
$rs->close();
self::sortusermodels($users);
return $users;
}
public static function associated_cohorts_parameters()
{
return new \external_function_parameters( [
"studyplan_id" => new \external_value(PARAM_INT, 'id of studyplan', VALUE_OPTIONAL),
] );
}
public static function associated_cohorts_returns()
{
return new \external_multiple_structure(self::cohort_structure());
}
// Actual functions
public static function associated_cohorts($studyplan_id)
{
global $CFG, $DB;
$sql = "SELECT DISTINCT c.* FROM {cohort} c INNER JOIN {local_treestudyplan_cohort} j ON j.cohort_id = c.id";
$sql .= " WHERE j.studyplan_id = :studyplan_id";
$rs = $DB->get_recordset_sql($sql, ['studyplan_id' => $studyplan_id]);
$cohorts = [];
foreach($rs as $c)
{
$cohorts[] = self::make_cohort_model($c);
}
$rs->close();
return $cohorts;
}
public static function all_associated_parameters()
{
return new \external_function_parameters( [
"studyplan_id" => new \external_value(PARAM_INT, 'id of studyplan', VALUE_OPTIONAL),
] );
}
public static function all_associated_returns()
{
return new \external_multiple_structure(self::user_structure());
}
// Actual functions
public static function all_associated($studyplan_id)
{
global $CFG, $DB;
$users = [];
// SQL JOIN script selecting all users that have a cohort linked to this studyplan
// or are directly linked
$sql = "SELECT DISTINCT u.id, u.username, u.firstname, u.lastname, u.idnumber, u.email
FROM {user} u
LEFT JOIN {cohort_members} cm ON u.id = cm.userid
LEFT JOIN {local_treestudyplan_cohort} tc ON cm.cohortid = tc.cohort_id
LEFT JOIN {local_treestudyplan_user} tu ON u.id = tu.user_id
WHERE tc.studyplan_id = {$studyplan_id}
OR tu.studyplan_id = {$studyplan_id}
ORDER BY u.lastname, u.firstname";
$rs = $DB->get_recordset_sql($sql);
foreach($rs as $u)
{
$users[] = self::make_user_model($u);
}
$rs->close();
self::sortusermodels($users);
return $users;
}
public static function sortusermodels(&$list){
return usort($list,function($a,$b){
$m= [];
if(preg_match("/.*?([A-Z].*)/",$a['lastname'],$m)){
$sort_ln_a = $m[1];
} else {
$sort_ln_a = $a['lastname'];
}
if(preg_match("/.*?([A-Z].*)/",$b['lastname'],$m)){
$sort_ln_b = $m[1];
} else {
$sort_ln_b = $b['lastname'];
}
$cmp= $sort_ln_a <=> $sort_ln_b;
return ($cmp != 0)?$cmp:$a['firstname'] <=> $b['firstname'];
});
}
}

107
classes/badgeinfo.php Normal file
View File

@ -0,0 +1,107 @@
<?php
namespace local_treestudyplan;
require_once($CFG->libdir.'/externallib.php');
class badgeinfo {
private $badge; // Holds database record
private const STATUSINFO = [
BADGE_STATUS_INACTIVE => 'inactive',
BADGE_STATUS_ACTIVE => 'active',
BADGE_STATUS_INACTIVE_LOCKED => 'inactive',
BADGE_STATUS_ACTIVE_LOCKED => 'active',
BADGE_STATUS_ARCHIVED => 'archived',
];
private const LOCKEDINFO = [
BADGE_STATUS_INACTIVE => 0,
BADGE_STATUS_ACTIVE => 0,
BADGE_STATUS_INACTIVE_LOCKED => 1,
BADGE_STATUS_ACTIVE_LOCKED => 1,
BADGE_STATUS_ARCHIVED => 1, // We don't want to edit archived badges anyway....
];
public function __construct(\core_badges\badge $badge) {
global $DB;
$this->badge = $badge;
}
public function name(){
return $this->badge->name;
}
public static function id_from_name($name){
global $DB;
return $DB->get_field("badge", "id", ['name' => $name]);
}
public static function exists($id){
global $DB;
return is_numeric($id) && $DB->record_exists('badge', array('id' => $id));
}
public static function editor_structure($value=VALUE_REQUIRED){
return new \external_single_structure([
"id" => new \external_value(PARAM_INT, 'id of badge'),
"name" => new \external_value(PARAM_TEXT, 'badge name'),
"status" => new \external_value(PARAM_TEXT, 'badge status'),
"locked" => new \external_value(PARAM_TEXT, 'badge lock status'),
"description"=> new \external_value(PARAM_TEXT, 'badge description'),
"imageurl" => new \external_value(PARAM_TEXT, 'url of badge image'),
],"Badge info",$value);
}
public function editor_model()
{
$context = ($this->badge->type == BADGE_TYPE_SITE) ? \context_system::instance() : \context_course::instance($this->badge->courseid);
// If the user is viewing another user's badge and doesn't have the right capability return only part of the data.
$model = [
'id' => $this->badge->id,
'name' => $this->badge->name,
'status' => self::STATUSINFO[$this->badge->status],
'locked' => self::LOCKEDINFO[$this->badge->status],
'description' => $this->badge->description,
'imageurl' => \moodle_url::make_pluginfile_url($context->id, 'badges', 'badgeimage', $this->badge->id, '/','f1')->out(false),
];
return $model;
}
public static function user_structure($value=VALUE_REQUIRED)
{
return new \external_single_structure([
"id" => new \external_value(PARAM_INT, 'id of badge'),
"name" => new \external_value(PARAM_TEXT, 'badge name'),
"description"=> new \external_value(PARAM_TEXT, 'badge description'),
"imageurl" => new \external_value(PARAM_TEXT, 'url of badge image'),
"issued" => new \external_value(PARAM_BOOL, 'badge is issued'),
"uniquehash" => new \external_value(PARAM_TEXT, 'badge issue hash', VALUE_OPTIONAL),
"issuedlink" => new \external_value(PARAM_TEXT, 'badge issue information link', VALUE_OPTIONAL),
],"Badge info",$value);
}
public function user_model($userid)
{
global $DB;
$context = ($this->badge->type == BADGE_TYPE_SITE) ? \context_system::instance() : \context_course::instance($this->badge->courseid);
$issued = $this->badge->is_issued($userid);
// If the user is viewing another user's badge and doesn't have the right capability return only part of the data.
$badge = [
'id' => $this->badge->id,
'name' => $this->badge->name,
'description' => $this->badge->description,
'imageurl' => \moodle_url::make_pluginfile_url($context->id, 'badges', 'badgeimage', $this->badge->id, '/','f1')->out(false),
'issued' => $issued,
];
if($issued) {
$issueinfo = $DB->get_record('badge_issued', array('badgeid' => $this->badge->id, 'userid' => $userid));
$badge['uniquehash'] = $issueinfo->uniquehash;
$badge['issuedlink'] = new \moodle_url('/badges/badge.php', array('hash' => $issueinfo->uniquehash));
}
return $badge;
}
}

56
classes/completion.php Normal file
View File

@ -0,0 +1,56 @@
<?php
namespace local_treestudyplan;
require_once($CFG->libdir.'/externallib.php');
class completion {
public const FAILED = -1;
public const INCOMPLETE = 0;
public const PENDING = 1;
public const PROGRESS = 2;
public const COMPLETED = 3;
public const GOOD = 4;
public const EXCELLENT = 5;
private const LABELS = [
self::FAILED => 'failed',
self::INCOMPLETE => 'incomplete',
self::PENDING => 'pending',
self::PROGRESS => 'progress',
self::COMPLETED => 'completed',
self::GOOD => 'good',
self::EXCELLENT => 'excellent',
];
public static function label($completion) {
if(array_key_exists($completion,self::LABELS)){
return self::LABELS[$completion];
}
else
{
return self::LABELS[self::INCOMPLETE];
}
}
public static function structure($value=VALUE_REQUIRED){
return new \external_value(PARAM_TEXT, 'completion state (failed|incomplete|pending|progress|completed|good|excellent)',$value);
}
public static function count_states(array $states){
// initialize result array
$statecount = [];
foreach(array_keys(self::LABELS) as $key) {
$statecount[$key] = 0;
}
// process all states in array and increment relevant counter for each one
foreach($states as $c){
if(array_key_exists($c,$statecount)){
$statecount[$c] += 1;
}
}
return $statecount;
}
}

49
classes/contextinfo.php Normal file
View File

@ -0,0 +1,49 @@
<?php
namespace local_treestudyplan;
class contextinfo {
public $context;
public function __construct($context){
$this->context = $context;
}
public static function structure($value=VALUE_REQUIRED){
return new \external_single_structure([
"name" => new \external_value(PARAM_TEXT, 'context name'),
"shortname" => new \external_value(PARAM_TEXT, 'context short name'),
"path" => new \external_multiple_structure( new \external_value(PARAM_TEXT)),
"shortpath" => new \external_multiple_structure( new \external_value(PARAM_TEXT)),
], 'context information', $value);
}
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),
];
}
public static function by_id($contextid): self {
return new self(self::context_by_id($contextid));
}
public static function context_by_id($contextid): \context {
if($contextid <= 1){
$contextid = 1;
}
return \context::instance_by_id($contextid);
}
}

View File

@ -0,0 +1,560 @@
<?php
namespace local_treestudyplan;
require_once($CFG->libdir.'/externallib.php');
require_once($CFG->libdir.'/gradelib.php');
require_once($CFG->dirroot.'/course/lib.php');
use core_course\local\repository\caching_content_item_readonly_repository;
use core_course\local\repository\content_item_readonly_repository;
use \grade_item;
use \grade_scale;
use \grade_outcome;
class corecompletioninfo {
private $course;
private $completion;
private $modinfo;
private static $COMPLETION_HANDLES = null;
public function id(){
return $this->course->id;
}
public function __construct($course){
global $DB;
$this->course = $course;
$this->completion = new \completion_info($this->course);
$this->modinfo = get_fast_modinfo($this->course);
}
static public function completiontypes(){
global $COMPLETION_CRITERIA_TYPES;
// Just return the keys of the global array COMPLETION_CRITERIA_TYPES, so we don't have to manually
// add any completion types....
return \array_keys($COMPLETION_CRITERIA_TYPES);
}
/**
* Translate a numeric completion constant to a text string
* @param $completion The completion code as defined in completionlib.php to translate to a text handle
*/
static public function completion_handle($completion){
if(empty(self::$COMPLETION_HANDLES)){
// Cache the translation table, to avoid overhead
self::$COMPLETION_HANDLES = [
COMPLETION_INCOMPLETE => "incomplete",
COMPLETION_COMPLETE => "complete",
COMPLETION_COMPLETE_PASS => "complete-pass",
COMPLETION_COMPLETE_FAIL => "complete-fail",
COMPLETION_COMPLETE_FAIL_HIDDEN => "complete-fail"]; // the front end won't differentiate between hidden or not
}
return self::$COMPLETION_HANDLES[$completion] ?? "undefined";
}
public static function completion_item_editor_structure($value=VALUE_REQUIRED){
return new \external_single_structure([
"title" => new \external_value(PARAM_TEXT,'name of subitem',VALUE_OPTIONAL),
"link" => new \external_value(PARAM_TEXT, 'optional link to more details',VALUE_OPTIONAL),
// ADD BELOW IF NEEDED - try using name, description and link fields first
/*
"required_grade" => new \external_value(PARAM_TEXT, 'required_grade',VALUE_OPTIONAL),
"course_link" => course_info::simple_structure(VALUE_OPTIONAL),
*/
], 'completion type',$value);
}
public static function completion_type_editor_structure($value=VALUE_REQUIRED){
return new \external_single_structure([
"items" => new \external_multiple_structure(self::completion_item_editor_structure(),'subitems',VALUE_OPTIONAL),
"title" => new \external_value(PARAM_TEXT,'optional title',VALUE_OPTIONAL),
"desc" => new \external_value(PARAM_TEXT, 'optional description',VALUE_OPTIONAL),
"type" => new \external_value(PARAM_TEXT, 'completion type name'),
"aggregation" => new \external_value(PARAM_TEXT, 'completion aggregation for this type ["all","any"]'),
], 'completion type',$value);
}
public static function editor_structure($value=VALUE_REQUIRED){
return new \external_single_structure([
"conditions" => new \external_multiple_structure(self::completion_type_editor_structure(),'completion conditions'),
"aggregation" => new \external_value(PARAM_TEXT, 'completion aggregation ["all","any"]'),
"enabled" => new \external_value(PARAM_BOOL,"whether completion is enabled here"),
], 'course completion info',$value);
}
public static function completion_item_user_structure($value=VALUE_REQUIRED){
return new \external_single_structure([
"id" => new \external_value(PARAM_INT,'id of subitem',VALUE_OPTIONAL),
"title" => new \external_value(PARAM_TEXT,'name of subitem',VALUE_OPTIONAL),
"details" => new \external_single_structure([
"type" => new \external_value(PARAM_RAW, 'type',VALUE_OPTIONAL),
"criteria" => new \external_value(PARAM_RAW, 'criteria',VALUE_OPTIONAL),
"requirement" => new \external_value(PARAM_RAW, 'requirement',VALUE_OPTIONAL),
"status" => new \external_value(PARAM_RAW, 'status',VALUE_OPTIONAL),
]),
"link" => new \external_value(PARAM_TEXT, 'optional link to more details',VALUE_OPTIONAL),
// ADD BELOW IF NEEDED - try using name, description and link fields first
/*
"required_grade" => new \external_value(PARAM_TEXT, 'required_grade',VALUE_OPTIONAL),
"course_link" => course_info::simple_structure(VALUE_OPTIONAL),
*/
"completed" => new \external_value(PARAM_BOOL, 'simple completed or not'),
"status" => new \external_value(PARAM_TEXT, 'extended completion status ["incomplete","progress","complete", "complete-pass","complete-fail"]'),
"pending" => new \external_value(PARAM_BOOL, 'optional pending state, for submitted but not yet reviewed activities',VALUE_OPTIONAL),
"grade" => new \external_value(PARAM_TEXT, 'optional grade result for this subitem',VALUE_OPTIONAL),
"feedback" => new \external_value(PARAM_RAW, 'optional feedback for this subitem ',VALUE_OPTIONAL),
], 'completion type',$value);
}
public static function completion_type_user_structure($value=VALUE_REQUIRED){
return new \external_single_structure([
"items" => new \external_multiple_structure(self::completion_item_user_structure(),'subitems',VALUE_OPTIONAL),
"title" => new \external_value(PARAM_TEXT,'optional title',VALUE_OPTIONAL),
"desc" => new \external_value(PARAM_TEXT, 'optional description',VALUE_OPTIONAL),
"type" => new \external_value(PARAM_TEXT, 'completion type name'),
"aggregation" => new \external_value(PARAM_TEXT, 'completion aggregation for this type ["all","any"]'),
"completed" => new \external_value(PARAM_BOOL, 'current completion value for this type'),
"status" => new \external_value(PARAM_TEXT, 'extended completion status ["incomplete","progress","complete", "complete-pass","complete-fail"]')
], 'completion type',$value);
}
public static function user_structure($value=VALUE_REQUIRED){
return new \external_single_structure([
"progress" => new \external_value(PARAM_INT, 'completed sub-conditions'),
"enabled" => new \external_value(PARAM_BOOL,"whether completion is enabled here"),
"tracked" => new \external_value(PARAM_BOOL,"whether completion is tracked for the user",VALUE_OPTIONAL),
"count" => new \external_value(PARAM_INT, 'total number of sub-conditions'),
"conditions" => new \external_multiple_structure(self::completion_type_user_structure(),'completion conditions'),
"completed" => new \external_value(PARAM_BOOL, 'current completion value'),
"aggregation" => new \external_value(PARAM_TEXT, 'completion aggregation ["all","any"]'),
"pending" => new \external_value(PARAM_BOOL,"true if the user has any assignments pending grading",VALUE_OPTIONAL),
], 'course completion info',$value);
}
private static function aggregation_handle($method){
return ($method==COMPLETION_AGGREGATION_ALL)?"all":"any";
}
public function editor_model() {
global $DB, $COMPLETION_CRITERIA_TYPES;
$conditions = [];
$aggregation = "all"; // default
$info = [
"conditions" => $conditions,
"aggregation" => self::aggregation_handle($this->completion->get_aggregation_method()),
"enabled" => $this->completion->is_enabled()
];
// Check if completion tracking is enabled for this course - otherwise, revert to defaults
if($this->completion->is_enabled())
{
$aggregation = $this->completion->get_aggregation_method();
// Loop through all condition types to see if they are applicable
foreach(self::completiontypes() as $type){
$criterias = $this->completion->get_criteria($type); // Returns array of relevant criteria items
if(count($criterias) > 0 ) // Only take it into account if the criteria count is > 0
{
$cinfo = [
"type" => $COMPLETION_CRITERIA_TYPES[$type],
"aggregation" => self::aggregation_handle($this->completion->get_aggregation_method($type)),
"title" => reset($criterias)->get_type_title(),
"items" => [],
];
foreach($criterias as $criteria){
$iinfo = [
"title" => $criteria->get_title_detailed(),
];
//TODO: MAKE SURE THIS DATA IS FILLED
if($type == COMPLETION_CRITERIA_TYPE_ACTIVITY){
// If it's an activity completion, add the relevant activity
//$cm = $this->modinfo->get_cm($criterias->moduleinstance);
// retrieve data for this object
//$data = $completion->get_data($cm, false, $userid);
}
else if ($type == COMPLETION_CRITERIA_TYPE_COURSE){
// If it's a (sub) course dependency, add the course as a link
}
else if ($type == COMPLETION_CRITERIA_TYPE_ROLE){
// If it needs approval by a role, it probably already is in the title
}
// only add the items list if we actually have items...
$cinfo["items"][] = $iinfo;
}
$info['conditions'][] = $cinfo;
}
}
}
return $info;
}
private function aggregate_completions($typeaggregation,$completions){
$completed = 0;
$count = count($completions);
foreach($completions as $c){
if($c->is_complete()){
$completed++;
}
}
if($typeaggregation == COMPLETION_AGGREGATION_ALL){
return $completed >= $count;
}
else { // COMPLETION_AGGREGATION_ANY
return $completed > 1;
}
}
public function user_model($userid) {
global $DB, $COMPLETION_CRITERIA_TYPES;
$progress = $this->get_advanced_progress_percentage($userid);
$info = [
'progress' => $progress->completed,
"count" => $progress->count,
"conditions" => [],
"completed" => $this->completion->is_course_complete($userid),
"aggregation" => self::aggregation_handle($this->completion->get_aggregation_method()),
"enabled" => $this->completion->is_enabled(),
"tracked" => $this->completion->is_tracked_user($userid),
];
// Check if completion tracking is enabled for this course - otherwise, revert to defaults
if($this->completion->is_enabled() && $this->completion->is_tracked_user($userid))
{
$anypending = false;
// Loop through all conditions to see if they are applicable
foreach(self::completiontypes() as $type){
// Get the main completion for this type
$completions = $this->completion->get_completions($userid,$type);
if(count($completions) > 0){
$typeaggregation = $this->completion->get_aggregation_method($type);
$completed = $this->aggregate_completions($typeaggregation,$completions);
$cinfo = [
"type" => $COMPLETION_CRITERIA_TYPES[$type],
"aggregation" => self::aggregation_handle($typeaggregation),
"completed" => $completed,
"status" => $completed?"completed":"incomplete",
"title" => reset($completions)->get_criteria()->get_type_title(),
"items" => [],
];
foreach($completions as $completion){
$criteria = $completion->get_criteria();
$iinfo = [
"id" => $criteria->id,
"title" => $criteria->get_title_detailed(),
"details" => $criteria->get_details($completion),
"completed" => $completion->is_complete(), // Make sure to override for activi
"status" => self::completion_handle($completion->is_complete()?COMPLETION_COMPLETE:COMPLETION_INCOMPLETE),
];
if($type == COMPLETION_CRITERIA_TYPE_ACTIVITY){
$cm = $this->modinfo->get_cm($criteria->moduleinstance);
// If it's an activity completion, add all the relevant activities as sub-items
$completion_status = $this->completion->get_grade_completion($cm,$userid);
$iinfo['status'] = self::completion_handle($completion_status);
// Re-evaluate the completed value, to make sure COMPLETE_FAIL doesn't creep in as completed
$iinfo['completed'] = in_array($completion_status,[COMPLETION_COMPLETE, COMPLETION_COMPLETE_PASS]);
// Determine the grade (retrieve from grade item, not from completion)
$grade = $this->get_grade($cm,$userid);
$iinfo['grade'] = $grade->grade;
$iinfo['feedback'] = $grade->feedback;
$iinfo['pending'] = $grade->pending;
$anypending = $anypending || $grade->pending;
// Overwrite the status with progress if something has been graded, or is pending
if($completion_status != COMPLETION_INCOMPLETE || $anypending){
if($cinfo["status"] == "incomplete"){
$cinfo["status"] = "progress";
}
}
}
else if ($type == COMPLETION_CRITERIA_TYPE_GRADE){
// Make sure we provide the current course grade
$iinfo['grade'] = $this->get_course_grade($userid);
if($iinfo["grade"] > 0){
$iinfo["status"] = $completion->is_complete()?"complete-pass":"complete-fail";
if ($cinfo["status"] == "incomplete"){
$cinfo["status"] = "progress";
}
}
}
// finally add the item to the items list
$cinfo["items"][] = $iinfo;
}
$info['conditions'][] = $cinfo;
$info['pending'] = $anypending;
}
}
}
return $info;
}
/**
* Get the grade for a certain course module
* @return stdClass|null object containing 'grade' and optional 'feedback' attribute
*/
private function get_grade($cm,$userid){
// TODO: Display grade in the way described in the course setup (with letters if needed)
$gi= grade_item::fetch(['itemtype' => 'mod',
'itemmodule' => $cm->modname,
'iteminstance' => $cm->instance,
'courseid' => $this->course->id]); // Make sure we only get results relevant to this course
if($gi)
{
// Only the following types of grade yield a result
if(($gi->gradetype == GRADE_TYPE_VALUE || $gi->gradetype == GRADE_TYPE_SCALE))
{
$scale = $gi->load_scale();
$grade = $gi->get_final($userid); // Get the grade for the specified user
$result = new \stdClass;
// Check if the final grade is available and numeric (safety check)
if(!empty($grade) && !empty($grade->finalgrade) && is_numeric($grade->finalgrade)){
// convert scale grades to corresponding scale name
if(isset($scale)){
// get scale value
$result->grade = $scale->get_nearest_item($grade->finalgrade);
}
else
{
// round final grade to 1 decimal point
$result->grade = round($grade->finalgrade,1);
}
$result->feedback = trim($grade->feedback);
$result->pending = (new gradingscanner($gi))->pending($userid);
}
else {
$result->grade = "-"; // Activity is gradable, but user did not receive a grade yet
$result->feedback = null;
$result->pending = false;
}
return $result;
}
}
return null; // Activity cannot be graded (Shouldn't be happening, but still....)
}
/**
* Get the grade for a certain course module
* @return stdClass|null object containing 'grade' and optional 'feedback' attribute
*/
private function get_course_grade($userid){
// TODO: Display grade in the way described in the course setup (with letters if needed)
$gi= grade_item::fetch(['itemtype' => 'course',
'iteminstance' => $this->course->id,
'courseid' => $this->course->id]);
if($gi)
{
// Only the following types of grade yield a result
if(($gi->gradetype == GRADE_TYPE_VALUE || $gi->gradetype == GRADE_TYPE_SCALE))
{
$scale = $gi->load_scale();
$grade = $gi->get_final($userid); // Get the grade for the specified user
// Check if the final grade is available and numeric (safety check)
if(!empty($grade) && !empty($grade->finalgrade) && is_numeric($grade->finalgrade)){
// convert scale grades to corresponding scale name
if(isset($scale)){
// get scale value
return $scale->get_nearest_item($grade->finalgrade);
}
else
{
// round final grade to 1 decimal point
return round($grade->finalgrade,1);
}
}
else {
return "-"; // User did not receive a grade yet for this course
}
}
}
return null; // Course cannot be graded (Shouldn't be happening, but still....)
}
/**
* Returns the percentage completed by a certain user, returns null if no completion data is available.
*
* @param int $userid The id of the user, 0 for the current user
* @return null|float The percentage, or null if completion is not supported in the course,
* or there are no activities that support completion.
*/
function get_progress_percentage($userid){
// First, let's make sure completion is enabled.
if (!$this->completion->is_enabled()) {
return null;
}
if (!$this->completion->is_tracked_user($userid)) {
return null;
}
$completions = $this->completion->get_completions($userid);
$count = count($completions);
$completed = 0;
// Before we check how many modules have been completed see if the course has completed.
if ($this->completion->is_course_complete($userid)) {
$completed = $count;
}
else {
// count all completions, but treat
foreach($completions as $completion){
$crit = $completion->get_criteria();
if($crit->criteriatype == COMPLETION_CRITERIA_TYPE_ACTIVITY) {
// get the cm data object
$cm = $this->modinfo->get_cm($crit->moduleinstance);
// retrieve data for this object
$data = $this->completion->get_data($cm, false, $userid);
// Count complete, but failed as incomplete too...
if (($data->completionstate == COMPLETION_INCOMPLETE) || ($data->completionstate == COMPLETION_COMPLETE_FAIL)) {
$completed += 0;
} else {
$completed += 1;
}
}
else {
if($completion->is_complete()){
$completed += 1;
}
}
}
}
$result = new \stdClass;
$result->count = $count;
$result->completed = $completed;
$result->percentage = ($completed / $count) * 100;
return $result;
}
/**
* Returns the percentage completed by a certain user, returns null if no completion data is available.
*
* @param int $userid The id of the user, 0 for the current user
* @return null|float The percentage, or null if completion is not supported in the course,
* or there are no activities that support completion.
*/
function get_advanced_progress_percentage($userid){
// First, let's make sure completion is enabled.
if (!$this->completion->is_enabled()) {
return null;
}
if (!$this->completion->is_tracked_user($userid)) {
return null;
}
$completions = $this->completion->get_completions($userid);
$aggregation = $this->completion->get_aggregation_method();
$critcount = [];
// Before we check how many modules have been completed see if the course has completed.
if ($this->completion->is_course_complete($userid)) {
$completed = $count;
}
else {
// count all completions, but treat
foreach($completions as $completion){
$crit = $completion->get_criteria();
// Make a new object for the type if it's not already there
$type = $crit->criteriatype;
if(!array_key_exists($type,$critcount)){
$critcount[$type] = new \stdClass;
$critcount[$type]->count = 0;
$critcount[$type]->completed = 0;
$critcount[$type]->aggregation = $this->completion->get_aggregation_method($type);
}
// Get a reference to the counter object for this type
$typecount =& $critcount[$type];
$typecount->count += 1;
if($crit->criteriatype == COMPLETION_CRITERIA_TYPE_ACTIVITY) {
// get the cm data object
$cm = $this->modinfo->get_cm($crit->moduleinstance);
// retrieve data for this object
$data = $this->completion->get_data($cm, false, $userid);
// Count complete, but failed as incomplete too...
if (($data->completionstate == COMPLETION_INCOMPLETE) || ($data->completionstate == COMPLETION_COMPLETE_FAIL)) {
$typecount->completed += 0;
} else {
$typecount->completed += 1;
}
}
else {
if($completion->is_complete()){
$typecount->completed += 1;
}
}
}
}
// Now that we have all completions sorted by type, we can be smart about how to do the count
$count = 0;
$completed = 0;
$completion_percentage = 0;
foreach($critcount as $c){
// Take only types that are actually present into account
if($c->count > 0){
// If the aggregation for the type is ANY, reduce the count to 1 for this type
// And adjust the progress accordingly (check if any have been completed or not)
if($c->aggregation == COMPLETION_AGGREGATION_ALL){
$ct = $c->count;
$cmpl = $c->completed;
}
else {
$ct = 1;
$cmpl = ($c->completed > 0)?1:0;
}
// if ANY completion for the types, count only the criteria type with the highest completion percentage -
// Overwrite data if current type is more complete
if($aggregation == COMPLETION_AGGREGATION_ANY) {
$pct = $cmpl/$ct;
if($pct > $completion_percentage){
$count = $ct;
$completed = $cmpl;
$completion_percentage = $pct;
}
}
// if ALL completion for the types, add the count for this type to that of the others
else {
$count += $ct;
$completed += $cmpl;
// Don't really care about recalculating completion percentage every round in this case
}
}
}
$result = new \stdClass;
$result->count = $count;
$result->completed = $completed;
$result->percentage = ($count > 0)?(($completed / $count) * 100):0;
return $result;
}
}

274
classes/courseinfo.php Normal file
View File

@ -0,0 +1,274 @@
<?php
namespace local_treestudyplan;
require_once($CFG->libdir.'/externallib.php');
require_once($CFG->libdir.'/gradelib.php');
require_once($CFG->dirroot.'/course/lib.php');
use core_course\local\repository\caching_content_item_readonly_repository;
use core_course\local\repository\content_item_readonly_repository;
use \grade_item;
use \grade_scale;
use \grade_outcome;
class courseinfo {
const TABLE = 'course';
private $course;
private $context;
private $studyitem;
private static $contentitems = null;
public function id(){
return $this->course->id;
}
public function shortname(){
return $this->course->shortname;
}
public function course(){
return $this->course; // php arrays are assigned by copy
}
public function course_context(){
return $this->coursecontext;
}
public function category_context(){
return $this->context;
}
protected function get_contentitems() {
global $PAGE;
if(empty(static::$contentitems)){
$PAGE->set_context(\context_system::instance());
static::$contentitems = (new content_item_readonly_repository())->find_all();
}
return static::$contentitems;
}
protected function amTeacher(){
global $USER;
return is_enrolled($this->coursecontext, $USER, 'mod/assign:grade');
}
protected function iCanSelectGradables($userid=-1){
global $USER, $DB;
if($userid <= 0){
$usr = $USER;
}
else
{
$usr = $DB->get_record('user', ['id' => $userid, 'deleted' => 0]);
}
return($usr && is_enrolled($this->coursecontext, $usr, 'local/treestudyplan:selectowngradables'));
}
public static function get_contentitem($name) {
$contentitems = static::get_contentitems();
for($i = 0; $i < count($contentitems); $i++){
if($contentitems[$i]->get_name() == $name){
return $contentitems[$i];
}
}
return null;
}
public function __construct($id,studyitem $studyitem = null){
global $DB;
$this->studyitem = $studyitem;
$this->course = \get_course($id);
$this->context = \context_coursecat::instance($this->course->category);
$this->coursecontext = \context_course::instance($this->course->id);
}
public static function exists($id){
global $DB;
return is_numeric($id) && $DB->record_exists(self::TABLE, ['id' => $id]);
}
public static function id_from_shortname($shortname){
global $DB;
return $DB->get_field(self::TABLE, "id", ['shortname' => $shortname]);
}
public static function coursetiming($course){
$now = time();
if($now > $course->startdate)
{
if($course->enddate > 0 && $now > $course->enddate)
{
return "past";
}
else {
return "present";
}
}
else{
return "future";
}
}
public function timing(){
return self::coursetiming($this->course);
}
public function displayname(){
$displayfield = get_config("local_treestudyplan","display_field");
if ($displayfield == "idnumber") {
$idnumber = trim(preg_replace("/\s+/u", " ",$this->course->idnumber));
if(strlen($idnumber) > 0){
return $this->course->idnumber;
}
} else if(strpos( $displayfield ,"customfield_") === 0) {
$fieldname = substr($displayfield,strlen("customfield_"));
$handler = \core_customfield\handler::get_handler('core_course', 'course');
$datas = $handler->get_instance_data($this->course->id);
foreach($datas as $data){
if($data->get_field()->get('shortname') == $fieldname){
$value = trim(preg_replace("/\s+/u", " ",$data->get_value()));
if(strlen($value) > 0){
return $value;
}
}
}
}
// Fallback to shortname when the specified display field fails, since shortname is never empty
return $this->course->shortname;
}
public static function simple_structure($value=VALUE_REQUIRED){
return new \external_single_structure([
"id" => new \external_value(PARAM_INT, 'linked course id'),
"fullname" => new \external_value(PARAM_TEXT, 'linked course name'),
"shortname" => new \external_value(PARAM_TEXT, 'linked course shortname'),
"displayname" => new \external_value(PARAM_TEXT, 'linked course displayname'),
"context" => contextinfo::structure(VALUE_OPTIONAL),
], 'referenced course information',$value);
}
public function simple_model() {
$contextinfo = new contextinfo($this->context);
$info = [
'id' => $this->course->id,
'fullname' => $this->course->fullname,
'shortname' => $this->course->shortname,
'displayname' => $this->displayname(),
'context' => $contextinfo->model()
];
return $info;
}
public static function editor_structure($value=VALUE_REQUIRED){
return new \external_single_structure([
"id" => new \external_value(PARAM_INT, 'linked course id'),
"fullname" => new \external_value(PARAM_TEXT, 'linked course name'),
"shortname" => new \external_value(PARAM_TEXT, 'linked course shortname'),
"displayname" => new \external_value(PARAM_TEXT, 'linked course displayname'),
"context" => contextinfo::structure(VALUE_OPTIONAL),
"ctxid" => new \external_value(PARAM_INT, 'course context id name'),
"grades" => new \external_multiple_structure(gradeinfo::editor_structure(),'grade list (legacy list)',VALUE_OPTIONAL),
"completion" => corecompletioninfo::editor_structure(VALUE_OPTIONAL),
"timing" => new \external_value(PARAM_TEXT, '(past|present|future)'),
"startdate" => new \external_value(PARAM_TEXT, 'Course start date'),
"enddate" => new \external_value(PARAM_TEXT, 'Course end date'),
"amteacher" => new \external_value(PARAM_BOOL, 'Requesting user is teacher in this course'),
"canselectgradables" => new \external_value(PARAM_BOOL, 'Requesting user can change selected gradables'),
"tag" => new \external_value(PARAM_TEXT, 'Tag'),
], 'referenced course information',$value);
}
public function editor_model(studyitem $studyitem=null, $usecorecompletioninfo=false) {
global $DB;
$contextinfo = new contextinfo($this->context);
$timing = $this->timing();
$info = [
'id' => $this->course->id,
'fullname' => $this->course->fullname,
'shortname' => $this->course->shortname,
'displayname' => $this->displayname(),
'context' => $contextinfo->model(),
'ctxid' => $this->coursecontext->id,
'timing' => $timing,
'startdate' => userdate($this->course->startdate,"%e %b %G"),
'enddate' => userdate($this->course->enddate, "%e %b %G"),
'amteacher' => $this->amTeacher(),
'canselectgradables' => $this->iCanSelectGradables(),
'tag' => "Editormodel",
'grades' => [],
];
if(!$usecorecompletioninfo){
$gradables = gradeinfo::list_course_gradables($this->course,$studyitem);
foreach($gradables as $gradable) {
$info['grades'][] = $gradable->editor_model();
}
}
else {
$cc = new corecompletioninfo($this->course);
$info['completion'] = $cc->editor_model();
}
return $info;
}
public static function user_structure($value=VALUE_REQUIRED){
return new \external_single_structure([
"id" => new \external_value(PARAM_INT, 'linked course id'),
"fullname" => new \external_value(PARAM_TEXT, 'linked course name'),
"shortname" => new \external_value(PARAM_TEXT, 'linked course shortname'),
"displayname" => new \external_value(PARAM_TEXT, 'linked course displayname'),
"context" => contextinfo::structure(VALUE_OPTIONAL),
"ctxid" => new \external_value(PARAM_INT, 'course context id name'),
"grades" => new \external_multiple_structure(gradeinfo::user_structure(),'grade list (legacy list)',VALUE_OPTIONAL),
"completion" => corecompletioninfo::user_structure(VALUE_OPTIONAL),
"timing" => new \external_value(PARAM_TEXT, '(past|present|future)'),
"startdate" => new \external_value(PARAM_TEXT, 'Course start date'),
"enddate" => new \external_value(PARAM_TEXT, 'Course end date'),
], 'course information',$value);
}
public function user_model($userid,$usecorecompletioninfo=false) {
global $DB;
$contextinfo = new contextinfo($this->context);
$timing = $this->timing();
$info = [
'id' => $this->course->id,
'fullname' => $this->course->fullname,
'shortname' => $this->course->shortname,
'displayname' => $this->displayname(),
'context' => $contextinfo->model(),
'ctxid' => $this->coursecontext->id,
'timing' => $timing,
'startdate' => userdate($this->course->startdate,"%e %b %G"),
'enddate' => userdate($this->course->enddate, "%e %b %G"),
'grades' => [],
];
if(!$usecorecompletioninfo){
$gradables = gradeinfo::list_studyitem_gradables($this->studyitem);
foreach($gradables as $gi) {
$info['grades'][] = $gi->user_model($userid);
}
}
else {
$cc = new corecompletioninfo($this->course);
$info['completion'] = $cc->user_model($userid);
}
return $info;
}
}

View File

@ -0,0 +1,36 @@
<?php
namespace local_treestudyplan;
require_once($CFG->libdir.'/externallib.php');
require_once($CFG->libdir.'/modinfolib.php');
require_once($CFG->dirroot.'/course/lib.php');
use core_course\local\repository\caching_content_item_readonly_repository;
use core_course\local\repository\content_item_readonly_repository;
use \grade_item;
class coursemoduleinfo {
private $id;
private $cm;
private $cm_info;
private $db_record;
public function __construct($id){
global $DB;
// Determine the icon for the associated activity
$this->id = $id;
$this->cm = $DB->get_record("course_modules",["id" => $id]);
$this->cm_info = \cm_info::create($this->cm);
// $this->db_record = $DB->get_record($this->cm_info->modname,["id" => $this->cm_info->instance]);
}
public function getTitle(){
return $this->cm_info->name;
}
public function setTitle($value){
$this->cm_info->set_name($value);
// TODO: Actually save this after setting the cminfo
}
}

279
classes/courseservice.php Normal file
View File

@ -0,0 +1,279 @@
<?php
namespace local_treestudyplan;
require_once($CFG->libdir.'/externallib.php');
use \local_treestudyplan\courseinfo;
use \local_treestudyplan\local\helpers\webservicehelper;
class courseservice extends \external_api
{
/************************
* *
* list_courses *
* *
************************/
public static function map_categories_parameters()
{
return new \external_function_parameters( [
"root_id" => new \external_value(PARAM_INT, 'root category to use as base', VALUE_DEFAULT),
] );
}
public static function map_categories_returns()
{
return new \external_multiple_structure(static::map_category_structure(false));
}
protected static function map_category_structure($lazy=false,$value=VALUE_REQUIRED)
{
$s = [
"id" => new \external_value(PARAM_INT, 'course category id'),
"context_id" => new \external_value(PARAM_INT, 'course category context id'),
"category" => contextinfo::structure(VALUE_OPTIONAL),
"haschildren" => new \external_value(PARAM_BOOL, 'True if the category has child categories'),
"hascourses" => new \external_value(PARAM_BOOL, 'True if the category contains courses'),
"studyplancount" => new \external_value(PARAM_INT, 'number of linked studyplans',VALUE_OPTIONAL),
];
if(!$lazy > 0) {
$s["courses"] = new \external_multiple_structure( courseinfo::editor_structure() );
$s["children"] = new \external_multiple_structure( static::map_category_structure(true));
}
return new \external_single_structure($s,"CourseCat info",$value);
}
public static function map_categories($root_id = 0){
global $CFG, $DB;
$root = \core_course_category::get($root_id);
$context = $root->get_context();
// Make sure the user has access to the context for editing purposes
webservicehelper::require_capabilities("local/treestudyplan:editstudyplan",$context);
// Determine top categories from provided context
if($root->id == 0){
// on the system level, determine the user's topmost allowed catecories
$user_top = \core_course_category::user_top();
if($user_top->id == 0){ // top category..
$children = $root->get_children(); // returns a list of çore_course_category, let it overwrite $children
} else {
$children = [$user_top];
}
} else if ($root->is_uservisible()){
$children = [$root];
}
foreach($children as $cat){
$list[] = static::map_category($cat,false);
}
return $list;
}
public static function get_category_parameters()
{
return new \external_function_parameters( [
"id" => new \external_value(PARAM_INT, 'id of category'),
] );
}
public static function get_category_returns()
{
return static::map_category_structure(false);
}
public static function get_category($id){
$cat = \core_course_category::get($id);
return static::map_category($cat);
}
protected static function map_category(\core_course_category $cat,$lazy=false){
global $DB;
$catcontext = $cat->get_context();
$ctx_info = new contextinfo($catcontext);
$children = $cat->get_children(); // only shows children visible to the current user
$courses = $cat->get_courses();
$model = [
"id" => $cat->id,
"context_id" => $catcontext->id,
"category" => $ctx_info->model(),
"haschildren" => !empty($children),
"hascourses" => !empty($courses),
];
if(!$lazy)
{
$model["courses"] = [];
foreach($courses as $course){
$courseinfo = new courseinfo($course->id);
$model["courses"][] = $courseinfo->editor_model();
}
$model["children"] = [];
foreach($children as $child){
$model["children"][] = static::map_category($child,true);
}
}
return $model;
}
public static function list_accessible_categories_parameters()
{
return new \external_function_parameters( [
"operation" => new \external_value(PARAM_TEXT, 'type of operation ["view"|"edit"]',VALUE_DEFAULT),]
);
}
public static function list_accessible_categories_returns()
{
return new \external_multiple_structure(static::map_category_structure(true));
}
public static function list_accessible_categories($operation="edit")
{
if($operation == "edit"){
$capability = "local/treestudyplan:editstudyplan";
} else { // $operation == "view" || default
$capability = "local/treestudyplan:viewuserreports";
}
$cats = static::categories_by_capability($capability);
$list = [];
/* @var $cat \core_course_category */
foreach($cats as $cat){
$list[] = static::map_category($cat,true);
}
return $list;
}
public static function categories_by_capability($capability,\core_course_category $parent=null){
// List the categories in which the user has a specific capability
$list = [];
// initialize parent if needed
if($parent == null){
$parent = \core_course_category::user_top();
if(has_capability($capability,$parent->get_context())){
$list[] = $parent;
}
}
$children = $parent->get_children();
foreach($children as $child){
// Check if we should add this category
if(has_capability($capability,$child->get_context())){
$list[] = $child;
// For optimization purposes, we include all its children now, since they will have inherited the permission
// #PREMATURE_OPTIMIZATION ???
$list = array_merge($list,self::recursive_child_categories($child));
} else {
if($child->get_children_count() > 0){
$list = array_merge($list,self::categories_by_capability($capability,$child));
}
}
}
return $list;
}
protected static function recursive_child_categories(\core_course_category $parent){
$list = [];
$children = $parent->get_children();
foreach($children as $child){
$list[] = $child;
if($child->get_children_count() > 0){
$list = array_merge($list,self::recursive_child_categories($child));
}
}
return $list;
}
public static function list_used_categories_parameters()
{
return new \external_function_parameters( [
"operation" => new \external_value(PARAM_TEXT, 'type of operation ["view"|"edit"]',VALUE_DEFAULT),
]);
}
public static function list_used_categories_returns()
{
return new \external_multiple_structure(static::map_category_structure(true));
}
public static function list_used_categories($operation='edit')
{
global $DB;
if($operation == "edit"){
$capability = "local/treestudyplan:editstudyplan";
} else { // $operation == "view" || default
$capability = "local/treestudyplan:viewuserreports";
}
$context_ids = [];
$rs = $DB->get_recordset_sql("SELECT DISTINCT context_id, COUNT(*) as num FROM {local_treestudyplan}
GROUP BY context_id");
foreach($rs as $r){
$context_ids[$r->context_id] = $r->num;
}
$rs->close();
// Now filter the categories that the user has acces to by the used context id's
// (That should filter out irrelevant stuff)
$cats = static::categories_by_capability($capability);
$list = [];
foreach($cats as $cat){
$count = 0;
$ctxid = $cat->get_context()->id;
if(array_key_exists($ctxid,$context_ids)){
$count = $context_ids[$ctxid];
}
$o = static::map_category($cat,true);
$o["studyplancount"] = $count;
$list[] = $o;
}
return $list;
}
public static function list_accessible_categories_with_usage($operation='edit'){
global $DB;
if($operation == "edit"){
$capability = "local/treestudyplan:editstudyplan";
} else { // $operation == "view" || default
$capability = "local/treestudyplan:viewuserreports";
}
// retrieve context ids used
$context_ids = [];
$rs = $DB->get_recordset_sql("SELECT DISTINCT context_id, COUNT(*) as num FROM {local_treestudyplan}
GROUP BY context_id");
foreach($rs as $r){
$context_ids[$r->context_id] = $r->num;
}
$rs->close();
// Now filter the categories that the user has acces to by the used context id's
// (That should filter out irrelevant stuff)
$cats = static::categories_by_capability($capability);
$list = [];
foreach($cats as $cat){
$count = 0;
$ctxid = $cat->get_context()->id;
if(array_key_exists($ctxid,$context_ids)){
$count = $context_ids[$ctxid];
}
$o = new \stdClass();
$o->cat = $cat;
$o->count = $count;
$list[] = $o;
}
return $list;
}
}

30
classes/debug.php Normal file
View File

@ -0,0 +1,30 @@
<?php
namespace local_treestudyplan;
class debug {
public static function dump($tag,$object)
{
global $CFG;
// assume debug environment if cachejs is false
if(isset($CFG->cachejs) && $CFG->cachejs == false){
$f = fopen("/tmp/log.txt","a");
fwrite($f,$tag . ":\n".print_r($object,true)."\n");
fclose($f);
}
}
public static function msg($tag,$str)
{
global $CFG;
// assume debug environment if cachejs is false
if(isset($CFG->cachejs) && $CFG->cachejs == false){
$f = fopen("/tmp/log.txt","a");
fwrite($f,$tag . ":\n".$str."\n");
fclose($f);
}
}
}

394
classes/gradeinfo.php Normal file
View File

@ -0,0 +1,394 @@
<?php
namespace local_treestudyplan;
require_once($CFG->libdir.'/externallib.php');
require_once($CFG->libdir.'/gradelib.php');
require_once($CFG->dirroot.'/course/lib.php');
use core_course\local\repository\caching_content_item_readonly_repository;
use core_course\local\repository\content_item_readonly_repository;
use \grade_item;
use \grade_scale;
use \grade_outcome;
use \core_plugin_manager;
class gradeinfo {
private $studyitem = null;
private $id;
private $gradeitem;
private $icon;
private $link;
private $gradinglink;
private $scale;
private $outcome;
private $hidden = false;
private $name;
private $typename;
private $section;
private $sectionorder;
private $cmid;
private $coursesort;
private static $contentitems = null;
private $gradingscanner;
private static $sections = [];
protected static function getSectionSequence($sectionid){
global $DB;
if(!array_key_exists($sectionid,self::$sections)){
self::$sections[$sectionid] = explode(",",$DB->get_field("course_sections","sequence",["id"=>$sectionid]));
}
return self::$sections[$sectionid];
}
public function getGradeitem(){
return $this->gradeitem;
}
public function getGradingscanner(){
return $this->gradingscanner;
}
public function getScale(){
return $this->scale;
}
protected static function get_contentitems() {
global $PAGE;
if(empty(static::$contentitems)){
$PAGE->set_context(\context_system::instance());
static::$contentitems = (new content_item_readonly_repository())->find_all();
}
return static::$contentitems;
}
public static function get_contentitem($name) {
$contentitems = static::get_contentitems();
for($i = 0; $i < count($contentitems); $i++){
if($contentitems[$i]->get_name() == $name){
return $contentitems[$i];
}
}
return null;
}
public static function getCourseContextById($id){
$gi = grade_item::fetch(["id" => $id]);
if(!$gi || course_module_instance_pending_deletion($gi->courseid, $gi->itemmodule, $gi->iteminstance))
{
throw new \InvalidArgumentException ("Grade {$id} not found in database". print_r($gi,true));
}
return \context_course::instance($gi->courseid);;
}
public function __construct($id,studyitem $studyitem = null){
global $DB;
$this->studyitem = $studyitem;
$gi = grade_item::fetch(["id" => $id]);
if(!$gi || course_module_instance_pending_deletion($gi->courseid, $gi->itemmodule, $gi->iteminstance))
{
throw new \InvalidArgumentException ("Grade {$id} not found in database". print_r($gi,true));
}
$this->id = $id;
$this->gradeitem = $gi;
// Determine the icon for the associated activity
$contentitem = static::get_contentitem($gi->itemmodule);
$this->icon = empty($contentitem)?"":$contentitem->get_icon();
// Determine a link to the associated activity
if($gi->itemtype != "mod" || empty($gi->itemmodule) || empty($gi->iteminstance)){
$this->link = "";
$this->cmid = 0;
$this->section = 0;
$this->sectionorder = 0;
}
else {
list($c,$cminfo) = get_course_and_cm_from_instance($gi->iteminstance,$gi->itemmodule);
$this->cmid = $cminfo->id;
// sort by position in course
//
$this->section = $cminfo->sectionnum;
$ssequence = self::getSectionSequence($cminfo->section);
$this->sectionorder = array_search($cminfo->id,$ssequence);
$this->link = "/mod/{$gi->itemmodule}/view.php?id={$cminfo->id}";
if($gi->itemmodule == 'quiz'){
$this->gradinglink = "/mod/{$gi->itemmodule}/report.php?id={$cminfo->id}&mode=grading";
}
else if($gi->itemmodule == "assign") {
$this->gradinglink = $this->link ."&action=grading";
}
else {
$this->gradinglink = $this->link;
}
}
$this->scale = $gi->load_scale();
$this->outcome = $gi->load_outcome();
$this->hidden = ($gi->hidden || (!empty($outcome) && $outcome->hidden))?true:false;
$this->name = empty($outcome)?$gi->itemname:$outcome->name;
$this->typename = empty($contentitem)?$gi->itemmodule:$contentitem->get_title()->get_value();
$this->gradingscanner = new gradingscanner($gi);
$this->coursesort = $this->section * 1000 + $this->sectionorder;
}
public function is_selected(){
global $DB;
if($this->studyitem){
// Check if selected for this studyitem
$r = $DB->get_record('local_treestudyplan_gradeinc',['studyitem_id' => $this->studyitem->id(), 'grade_item_id'=> $this->gradeitem->id]);
if($r && $r->include) {
return(true);
}
}
return(false);
}
public function is_required(){
global $DB;
if($this->studyitem){
// Check if selected for this studyitem
$r = $DB->get_record('local_treestudyplan_gradeinc',['studyitem_id' => $this->studyitem->id(), 'grade_item_id'=> $this->gradeitem->id]);
if($r && $r->include && $r->required) {
return(true);
}
}
return(false);
}
public static function editor_structure($value=VALUE_REQUIRED){
return new \external_single_structure([
"id" => new \external_value(PARAM_INT, 'grade_item id'),
"cmid" => new \external_value(PARAM_INT, 'course module id'),
"name" => new \external_value(PARAM_TEXT, 'grade item name'),
"typename" => new \external_value(PARAM_TEXT, 'grade item type name'),
"outcome" => new \external_value(PARAM_BOOL, 'is outcome'),
"selected" => new \external_value(PARAM_BOOL, 'is selected for current studyitem'),
"icon" => new \external_value(PARAM_RAW, 'html for icon of related activity'),
"link" => new \external_value(PARAM_TEXT, 'link to related activity'),
"gradinglink" => new \external_value(PARAM_TEXT, 'link to related activity'),
"grading" => gradingscanner::structure(),
"required" => new \external_value(PARAM_BOOL, 'is required for current studyitem'),
], 'referenced course information',$value);
}
public function editor_model(studyitem $studyitem=null) {
$model = [
"id" => $this->id,
"cmid" => $this->cmid,
"name" => $this->name,
"typename" => $this->typename,
"outcome" => isset($this->outcome),
"selected" => $this->is_selected(),
"icon" => $this->icon,
"link" => $this->link,
"gradinglink" => $this->gradinglink,
"required" => $this->is_required(),
];
if($this->is_selected() && has_capability('local/treestudyplan:viewuserreports',\context_system::instance())
&& $this->gradingscanner->is_available()){
$model['grading'] = $this->gradingscanner->model();
}
return $model;
}
public static function user_structure($value=VALUE_REQUIRED){
return new \external_single_structure([
"id" => new \external_value(PARAM_INT, 'grade_item id'),
"cmid" => new \external_value(PARAM_INT, 'course module id'),
"name" => new \external_value(PARAM_TEXT, 'grade item name'),
"typename" => new \external_value(PARAM_TEXT, 'grade item type name'),
"grade" => new \external_value(PARAM_TEXT, 'is outcome'),
"gradetype" => new \external_value(PARAM_TEXT, 'grade type (completion|grade)'),
"feedback" => new \external_value(PARAM_RAW, 'html for feedback'),
"completion" => new \external_value(PARAM_TEXT, 'completion state (incomplete|progress|completed|excellent)'),
"icon" => new \external_value(PARAM_RAW, 'html for icon of related activity'),
"link" => new \external_value(PARAM_TEXT, 'link to related activity'),
"pendingsubmission" => new \external_value(PARAM_BOOL, 'is selected for current studyitem',VALUE_OPTIONAL),
"required" => new \external_value(PARAM_BOOL, 'is required for current studyitem'),
"selected" => new \external_value(PARAM_BOOL, 'is selected for current studyitem'),
], 'referenced course information',$value);
}
public function user_model($userid) {
global $DB;
$grade = $this->gradeitem->get_final($userid);
// convert scale grades to corresponding scale name
if(!empty($grade)){
if(!is_numeric($grade->finalgrade) && empty($grade->finalgrade)){
$finalgrade = "-";
}
else if(isset($this->scale)){
$finalgrade = $this->scale->get_nearest_item($grade->finalgrade);
}
else
{
$finalgrade = round($grade->finalgrade,1);
}
}
else
{
$finalgrade = "-";
}
// retrieve the aggregator and determine completion
if(!isset($this->studyitem)){
throw new \UnexpectedValueException("Study item not set (null) for gradeinfo in report mode");
}
$aggregator = $this->studyitem->getStudyline()->getStudyplan()->getAggregator();
$completion = $aggregator->grade_completion($this,$userid);
$model = [
"id" => $this->id,
"cmid" => $this->cmid,
"name" => $this->name,
"typename" => $this->typename,
"grade" => $finalgrade,
"gradetype" => isset($this->scale)?"completion":"grade",
"feedback" => empty($grade)?null:$grade->feedback,
"completion" => completion::label($completion),
"icon" => $this->icon,
"link" => $this->link,
"pendingsubmission" => $this->gradingscanner->pending($userid),
"required" => $this->is_required(),
"selected" => $this->is_selected(),
];
return $model;
}
public function export_model(){
return [
"name" => $this->name,
"type" => $this->gradeitem->itemmodule,
"selected" => $this->is_selected(),
"required" => $this->is_required(),
];
}
public static function import(studyitem $item,array $model){
if($item->type() == studyitem::COURSE){
$course_id = $item->courseid();
$gradeitems= grade_item::fetch_all(['itemtype' => 'mod', 'courseid' => $course_id]);
foreach($gradeitems as $gi){
$gi_name = empty($outcome)?$gi->itemname:$outcome->name;
$gi_type = $gi->itemmodule;
if($gi_name == $model["name"] && $gi_type == $model["type"]){
// we have a match
if(!isset($model["selected"])){ $model["selected"] = true;}
if(!isset($model["required"])){ $model["required"] = false;}
if($model["selected"] || $model["required"]){
static::include_grade($gi->id,$item->id(),$model["selected"], $model["required"]);
}
}
}
}
}
public static function list_course_gradables($course,studyitem $studyitem=null) {
$list = [];
if(method_exists("\course_modinfo","get_array_of_activities")){
$activities = \course_modinfo::get_array_of_activities($course);
} else {
// Deprecated in Moodle 4.0+, but not yet available in Moodle 3.11
$activities = get_array_of_activities($course->id);
}
foreach($activities as $act)
{
if($act->visible)
{
$gradeitems= grade_item::fetch_all(['itemtype' => 'mod', 'itemmodule' => $act->mod, 'iteminstance' => $act->id, 'courseid' => $course->id]);
if(!empty($gradeitems))
{
foreach($gradeitems as $gi){
if(($gi->gradetype == GRADE_TYPE_VALUE || $gi->gradetype == GRADE_TYPE_SCALE))
{
try {
$gradable = new static($gi->id,$studyitem);
$list[] = $gradable;
}
catch(\InvalidArgumentException $x){}
}
}
}
}
}
usort($list, function($a,$b){
$course = $a->coursesort <=> $b->coursesort;
return ($course != 0)?$course:$a->gradeitem->sortorder <=> $b->gradeitem->sortorder;
});
return $list;
}
public static function list_studyitem_gradables(studyitem $studyitem)
{
global $DB;
$table = 'local_treestudyplan_gradeinc';
$list = [];
$records = $DB->get_records($table,['studyitem_id' => $studyitem->id()]);
foreach($records as $r){
if(isset($r->grade_item_id)){
try {
if($r->include || $r->required){
$list[] = new static($r->grade_item_id,$studyitem);
}
}
catch(\InvalidArgumentException $x){
// on InvalidArgumentException, the grade_item id can no longer be found
// Remove the link to avoid database record hogging
$DB->delete_records($table, ['id' => $r->id]);
}
}
}
usort($list, function($a,$b){
$course = $a->coursesort <=> $b->coursesort;
return ($course != 0)?$course:$a->gradeitem->sortorder <=> $b->gradeitem->sortorder;
});
return $list;
}
public static function include_grade(int $grade_id,int $item_id,bool $include,bool $required=false) {
global $DB;
$table = 'local_treestudyplan_gradeinc';
if($include){
// make sure a record exits
$r = $DB->get_record($table,['studyitem_id' => $item_id, 'grade_item_id' => $grade_id]);
if($r){
$r->include = 1;
$r->required = boolval($required)?1:0;
$id = $DB->update_record($table, $r);
} else {
$DB->insert_record($table, [
'studyitem_id' => $item_id,
'grade_item_id' => $grade_id,
'include' => 1,
'required' =>boolval($required)?1:0]
);
}
} else {
// remove if it should not be included
$r = $DB->get_record($table,['studyitem_id' => $item_id, 'grade_item_id' => $grade_id]);
if($r){
$DB->delete_records($table, ['id' => $r->id]);
}
}
return success::success();
}
}

100
classes/gradingscanner.php Normal file
View File

@ -0,0 +1,100 @@
<?php
namespace local_treestudyplan;
require_once($CFG->libdir.'/externallib.php');
use \grade_item;
// $gi->courseid,
// $gi->itemmodule,
// $gi->iteminstance
class gradingscanner
{
private static $mod_supported = [];
private static $course_students = [];
private $scanner = null;
private $gi = null;
private $pending_cache = [];
public static function supported($mod){
if(!array_key_exists($mod,self::$mod_supported)){
self::$mod_supported[$mod] = class_exists("\local_treestudyplan\\local\\ungradedscanners\\{$mod}_scanner");
}
return self::$mod_supported[$mod];
}
public static function get_course_students($courseid){
global $CFG;
if(!array_key_exists($courseid,self::$course_students)){
$students = [];
$context = \context_course::instance($courseid);
foreach (explode(',', $CFG->gradebookroles) as $roleid) {
$roleid = trim($roleid);
$students = array_keys(get_role_users($roleid, $context, false, 'u.id', 'u.id ASC'));
}
self::$course_students[$courseid] = $students;
}
return self::$course_students[$courseid];
}
public function __construct(grade_item $gi){
$this->gi = $gi;
if(self::supported($gi->itemmodule)) {
$scannerclass = "\local_treestudyplan\\local\ungradedscanners\\{$gi->itemmodule}_scanner";
$this->scanner = new $scannerclass($gi);
}
}
public function is_available(){
return $this->scanner !== null;
}
public function count_students(){
return count(self::get_course_students($this->gi->courseid));
}
public function count_ungraded(){
if($this->scanner === null) {
return -1;
}
return $this->scanner->count_ungraded(self::get_course_students($this->gi->courseid));
}
public function count_graded(){
if($this->scanner === null) {
return -1;
}
return $this->scanner->count_graded(self::get_course_students($this->gi->courseid));
}
public function pending($userid){
if(!array_key_exists($userid, $this->pending_cache)){
if($this->scanner === null) {
$this->pending_cache[$userid] = false;
}
else {
$this->pending_cache[$userid] = $this->scanner->has_ungraded_submission($userid);;
}
}
return $this->pending_cache[$userid];
}
public static function structure($value=VALUE_OPTIONAL){
return new \external_single_structure([
"ungraded" => new \external_value(PARAM_INT, 'number of ungraded submissions'),
"graded" => new \external_value(PARAM_INT, 'number of graded students'),
"students" => new \external_value(PARAM_INT, 'number of students that should submit'),
],"details about gradable submissions",$value);
}
public function model(){
return [
'ungraded' => $this->count_ungraded(),
'graded' => $this->count_graded(),
'students' => $this->count_students(),
];
}
}

View File

@ -0,0 +1,294 @@
<?php
namespace local_treestudyplan\local\aggregators;
use \local_treestudyplan\courseinfo;
use \local_treestudyplan\gradeinfo;
use \local_treestudyplan\studyitem;
use \local_treestudyplan\completion;
use \local_treestudyplan\debug;
class bistate_aggregator extends \local_treestudyplan\aggregator {
public const DEPRECATED = false;
private const DEFAULT_CONDITION = "50";
private $thresh_excellent = 1.0; // Minimum fraction that must be completed to aggregate as excellent (usually 1.0)
private $thresh_good = 0.8; // Minimum fraction that must be completed to aggregate as good
private $thresh_completed = 0.66; // Minimum fraction that must be completed to aggregate as completed
private $use_failed = True; // Support failed completion yes/no
private $thresh_progress = 0.33; // Minimum fraction that must be failed to aggregate as failed instead of progress
private $accept_pending_as_submitted = False; // Also count ungraded but submitted
public function __construct($configstr) {
// allow public constructor for testing purposes
$this->initialize($configstr);
}
protected function initialize($configstr) {
// First initialize with the defaults
foreach(["thresh_excellent", "thresh_good", "thresh_completed","thresh_progress",] as $key){
$val = intval(get_config('local_treestudyplan', "bistate_{$key}"));
if($val >= 0 && $val <= 100)
{
$this->$key = floatval($val)/100;
}
}
foreach(["use_failed", "accept_pending_as_submitted"] as $key){
$this->$key = boolval(get_config('local_treestudyplan', "bistate_{$key}"));
}
// Next, decode json
$config = \json_decode($configstr,true);
if(is_array($config)){
// copy all valid config settings to this item
foreach(["thresh_excellent", "thresh_good", "thresh_completed","thresh_progress",] as $key){
if(array_key_exists($key,$config)){
$val = $config[$key];
if($val >= 0 && $val <= 100)
{
$this->$key = floatval($val)/100;
}
}
}
foreach(["use_failed", "accept_pending_as_submitted"] as $key){
if(array_key_exists($key,$config)){
$this->$key = boolval($config[$key]);
}
}
} else {
debug::msg("ARRAY is NOT CONFIG","");
}
}
// Return active configuration model
public function config_string() {
return json_encode([
"thresh_excellent" => 100*$this->thresh_excellent,
"thresh_good" => 100*$this->thresh_good,
"thresh_completed" => 100*$this->thresh_completed,
"thresh_progress" => 100*$this->thresh_progress,
"use_failed" => $this->use_failed,
"accept_pending_as_submitted" => $this->accept_pending_as_submitted,
]);
}
public function needSelectGradables(){ return True;}
public function isDeprecated() { return self::DEPRECATED;}
public function useRequiredGrades() { return True;}
public function useItemConditions() { return False;}
public function aggregate_binary_goals(array $completions, array $required = []){
// function is public to allow access for the testing code
// return te following conditions
// Possible states:
// - completion::EXCELLENT - At least $thresh_excellent fraction of goals are complete and all required goals are met
// - completion::GOOD - At least $thresh_good fraction of goals are complete and all required goals are met
// - completion::COMPLETED - At least $thresh_complete fraction of goals are completed and all required goals are met
// - completion::FAILED - At least $thresh_progress fraction of goals is not failed
// - completion::INCOMPLETE - No goals have been started
// - completion::PROGRESS - All other states
$total = count($completions);
$completed = 0;
$progress = 0;
$failed = 0;
$started = 0;
$total_required = 0;
$required_met = 0;
$MIN_PROGRESS = ($this->accept_pending_as_submitted)?completion::PENDING:completion::PROGRESS;
foreach($completions as $index => $c) {
$completed += ($c >= completion::COMPLETED)?1:0;
$progress += ($c >= $MIN_PROGRESS)?1:0;
$failed += ($c <= completion::FAILED)?1:0;
}
$started = $progress + $failed;
$allrequiredmet = ($required_met >= $total_required);
$fraction_completed = ($total >0)?(floatval($completed)/floatval($total)):0.0;
$fraction_progress = ($total >0)?(floatval($progress)/floatval($total)):0.0;
$fraction_failed = ($total >0)?(floatval($failed)/floatval($total)):0.0;
$fraction_started = ($total >0)?(floatval($started)/floatval($total)):0.0;
if($total == 0){
return completion::INCOMPLETE;
}
if($fraction_completed >= $this->thresh_excellent && $allrequiredmet){
return completion::EXCELLENT;
}
else if($fraction_completed >= $this->thresh_good && $allrequiredmet){
return completion::GOOD;
}
else if($fraction_completed >= $this->thresh_completed && $allrequiredmet){
return completion::COMPLETED;
}
else if($started == 0){
return completion::INCOMPLETE;
}
else if($this->use_failed && ($fraction_failed >= $this->thresh_progress)){
return completion::FAILED;
}
else {
return completion::PROGRESS;
}
}
public function aggregate_course(courseinfo $courseinfo, studyitem $studyitem, $userid){
// Note: studyitem condition config is not used in this aggregator.
// loop through all associated gradables and count the totals, completed, etc..
$completions = [];
$required = [];
foreach(gradeinfo::list_studyitem_gradables($studyitem) as $gi){
$completions[] = $this->grade_completion($gi,$userid);
if($gi->is_required()){
// if it's a required grade
// also add it's index in the completion list to the list of required grades
$required[] = count($completions) - 1;
}
}
// Combine the aquired completions into one
return self::aggregate_binary_goals($completions,$required);
}
public function aggregate_junction(array $completion, studyitem $studyitem = null, $userid = 0){
// Aggregate multiple incoming states into one junction or finish.
// Possible states:
// - completion::EXCELLENT - All incoming states are excellent
// - completion::GOOD - All incoming states are at least good
// - completion::COMPLETED - All incoming states are at least completed
// - completion::FAILED - All incoming states are failed
// - completion::INCOMPLETE - All incoming states are incomplete
// - completion::PROGRESS - All other states
// First count all states
$statecount = completion::count_states($completion);
$total = count($completion);
if( $total == $statecount[completion::EXCELLENT]){
return completion::EXCELLENT;
}
else if ( $total == $statecount[completion::EXCELLENT] + $statecount[completion::GOOD]){
return completion::GOOD;
}
else if ( $total == $statecount[completion::EXCELLENT] + $statecount[completion::GOOD] + $statecount[completion::COMPLETED]){
return completion::COMPLETED;
}
else if( $statecount[completion::FAILED]){
return completion::FAILED;
}
else if( $total == $statecount[completion::INCOMPLETE]){
return completion::INCOMPLETE;
}
else {
return completion::PROGRESS;
}
}
public function grade_completion(gradeinfo $gradeinfo, $userid) {
global $DB;
$table = "local_treestudyplan_gradecfg";
$gradeitem = $gradeinfo->getGradeitem();
$grade = $gradeitem->get_final($userid);
if(empty($grade)){
return completion::INCOMPLETE;
}
else if($grade->finalgrade === NULL)
{
// on assignments, grade NULL means a submission has not yet been graded,
// but on quizes this can also mean a quiz might have been started
// Therefor, we treat a NULL result as a reason to check the relevant gradingscanner for presence of pending items
// Since we want old results to be visible until a pending item was graded, we only use this state here.
// Pending items are otherwise expressly indicated by the "pendingsubmission" field in the user model
if($gradeinfo->getGradingscanner()->pending($userid)){
return completion::PENDING;
} else {
return completion::INCOMPLETE;
}
}
else {
// first determine if we have a grade_config for this scale or this maximum grade
$finalgrade = $grade->finalgrade;
$scale = $gradeinfo->getScale();
if( isset($scale)){
$gradecfg = $DB->get_record($table,["scale_id"=>$scale->id]);
}
else if($gradeitem->grademin == 0)
{
$gradecfg = $DB->get_record($table,["grade_points"=>$gradeitem->grademax]);
}
else
{
$gradecfg = null;
}
// for point grades, a provided grade pass overrides the defaults in the gradeconfig
// for scales, the configuration in the gradeconfig is leading
if($gradecfg && (isset($scale) || $gradeitem->gradepass == 0))
{
// if so, we need to know if the grade is
if($finalgrade >= $gradecfg->min_completed){
// return completed if completed
return completion::COMPLETED;
}
else if($this->use_failed && $finalgrade < $gradecfg->min_progress)
{
// return failed if failed is enabled and the grade is less than the minimum grade for progress
return completion::FAILED;
}
else {
return completion::PROGRESS;
}
}
else if($gradeitem->gradepass > 0)
{
$range = floatval($gradeitem->grademax - $gradeitem->grademin);
// if no gradeconfig and gradepass is set, use that one to determine config.
if($finalgrade >= $gradeitem->gradepass){
return completion::COMPLETED;
}
else if($this->use_failed && $gradeitem->gradepass >= 3 && $range >= 3 && $finalgrade == 1)
{
// return failed if failed is enabled and the grade is 1, while there are at leas 3 states.
return completion::FAILED;
}
else {
return completion::PROGRESS;
}
}
else {
// Blind assumptions if nothing is provided
// over 55% of range is completed
// if range >= 3 and failed is enabled, assume that this means failed
$g = floatval($finalgrade - $gradeitem->grademin);
$range = floatval($gradeitem->grademax - $gradeitem->grademin);
$score = $g / $range;
if($score > 0.55){
return completion::COMPLETED;
}
else if($this->use_failed && $range >= 3 && $finalgrade == 1)
{
// return failed if failed is enabled and the grade is 1, while there are at leas 3 states.
return completion::FAILED;
}
else {
return completion::PROGRESS;
}
}
}
}
}

View File

@ -0,0 +1,245 @@
<?php
namespace local_treestudyplan\local\aggregators;
use \local_treestudyplan\courseinfo;
use \local_treestudyplan\corecompletioninfo;
use \local_treestudyplan\gradeinfo;
use \local_treestudyplan\studyitem;
use \local_treestudyplan\completion;
use \local_treestudyplan\debug;
class core_aggregator extends \local_treestudyplan\aggregator {
public const DEPRECATED = false;
private $accept_pending_as_submitted = False; // Also count ungraded but submitted
public function __construct($configstr) {
// allow public constructor for testing purposes
$this->initialize($configstr);
}
protected function initialize($configstr) {
// First initialize with the defaults
foreach(["accept_pending_as_submitted"] as $key){
$this->$key = boolval(get_config('local_treestudyplan', "bistate_{$key}"));
}
// Next, decode json
$config = \json_decode($configstr,true);
if(is_array($config)){
// copy all valid config settings to this item
foreach(["accept_pending_as_submitted"] as $key){
if(array_key_exists($key,$config)){
$this->$key = boolval($config[$key]);
}
}
} else {
debug::msg("ARRAY is NOT CONFIG","");
}
}
// Return active configuration model
public function config_string() {
return json_encode([
"accept_pending_as_submitted" => $this->accept_pending_as_submitted,
]);
}
public function needSelectGradables(){ return False;}
public function isDeprecated() { return self::DEPRECATED;}
public function useRequiredGrades() { return True;}
public function useItemConditions() { return False;}
public function usecorecompletioninfo() { return True; }
/**
* Return course completion information based on the core completion infromation
* Possible states:
* completion::EXCELLENT - Completed with excellent results
* completion::GOOD - Completed with good results
* completion::COMPLETED - Completed
* completion::PROGRESS - Started, but not completed yey
* completion::FAILED - Failed
* completion::INCOMPLETE - Not yet started
*
* @param mixed $courseinfo
* @param mixed $studyitem
* @param mixed $userid
* @return void
*/
public function aggregate_course(courseinfo $courseinfo, studyitem $studyitem, $userid){
// Retrieve the core completion info from the core
$course = $courseinfo->course();
$completion = new \completion_info($course);
if ($completion->is_enabled() && $completion->is_tracked_user($userid)){
if($completion->is_course_complete($userid)){
// Now, the trick is to determine what constitutes excellent and good completion....
// TODO: Determine excellent and maybe good completion
// Option: Use course end grade to determine that...
// Probably needs a config value in the aggregator....
return completion::COMPLETED;
} else {
// Check if the course is over or not, if it is over, display failed
// Else, return PROGRESS
// Retrieve timing through courseinfo
$timing = courseinfo::coursetiming($course);
// Not met and time is passed, means FAILED
if($timing == "past"){
return completion::FAILED;
}
else {
// Check if any of the requirements are being met?
$completions = $completion->get_completions($userid);
foreach($completions as $c){
if($c->is_complete()){
// If so, return progress
return completion::PROGRESS;
}
}
return completion::INCOMPLETE;
}
}
}
else{
return completion::INCOMPLETE;
}
}
public function aggregate_junction(array $completion, studyitem $studyitem = null, $userid = 0){
// Aggregate multiple incoming states into one junction or finish.
// Possible states:
// - completion::EXCELLENT - All incoming states are excellent
// - completion::GOOD - All incoming states are at least good
// - completion::COMPLETED - All incoming states are at least completed
// - completion::FAILED - All incoming states are failed
// - completion::INCOMPLETE - All incoming states are incomplete
// - completion::PROGRESS - All other states
// First count all states
$statecount = completion::count_states($completion);
$total = count($completion);
if( $total == $statecount[completion::EXCELLENT]){
return completion::EXCELLENT;
}
else if ( $total == $statecount[completion::EXCELLENT] + $statecount[completion::GOOD]){
return completion::GOOD;
}
else if ( $total == $statecount[completion::EXCELLENT] + $statecount[completion::GOOD] + $statecount[completion::COMPLETED]){
return completion::COMPLETED;
}
else if( $statecount[completion::FAILED]){
return completion::FAILED;
}
else if( $total == $statecount[completion::INCOMPLETE]){
return completion::INCOMPLETE;
}
else {
return completion::PROGRESS;
}
}
public function grade_completion(gradeinfo $gradeinfo, $userid) {
global $DB;
$table = "local_treestudyplan_gradecfg";
$gradeitem = $gradeinfo->getGradeitem();
$grade = $gradeitem->get_final($userid);
if(empty($grade)){
return completion::INCOMPLETE;
}
else if($grade->finalgrade === NULL)
{
// on assignments, grade NULL means a submission has not yet been graded,
// but on quizes this can also mean a quiz might have been started
// Therefor, we treat a NULL result as a reason to check the relevant gradingscanner for presence of pending items
// Since we want old results to be visible until a pending item was graded, we only use this state here.
// Pending items are otherwise expressly indicated by the "pendingsubmission" field in the user model
if($gradeinfo->getGradingscanner()->pending($userid)){
return completion::PENDING;
} else {
return completion::INCOMPLETE;
}
}
else {
// first determine if we have a grade_config for this scale or this maximum grade
$finalgrade = $grade->finalgrade;
$scale = $gradeinfo->getScale();
if( isset($scale)){
$gradecfg = $DB->get_record($table,["scale_id"=>$scale->id]);
}
else if($gradeitem->grademin == 0)
{
$gradecfg = $DB->get_record($table,["grade_points"=>$gradeitem->grademax]);
}
else
{
$gradecfg = null;
}
// for point grades, a provided grade pass overrides the defaults in the gradeconfig
// for scales, the configuration in the gradeconfig is leading
if($gradecfg && (isset($scale) || $gradeitem->gradepass == 0))
{
// if so, we need to know if the grade is
if($finalgrade >= $gradecfg->min_completed){
// return completed if completed
return completion::COMPLETED;
}
else if($this->use_failed && $finalgrade < $gradecfg->min_progress)
{
// return failed if failed is enabled and the grade is less than the minimum grade for progress
return completion::FAILED;
}
else {
return completion::PROGRESS;
}
}
else if($gradeitem->gradepass > 0)
{
$range = floatval($gradeitem->grademax - $gradeitem->grademin);
// if no gradeconfig and gradepass is set, use that one to determine config.
if($finalgrade >= $gradeitem->gradepass){
return completion::COMPLETED;
}
else if($this->use_failed && $gradeitem->gradepass >= 3 && $range >= 3 && $finalgrade == 1)
{
// return failed if failed is enabled and the grade is 1, while there are at leas 3 states.
return completion::FAILED;
}
else {
return completion::PROGRESS;
}
}
else {
// Blind assumptions if nothing is provided
// over 55% of range is completed
// if range >= 3 and failed is enabled, assume that this means failed
$g = floatval($finalgrade - $gradeitem->grademin);
$range = floatval($gradeitem->grademax - $gradeitem->grademin);
$score = $g / $range;
if($score > 0.55){
return completion::COMPLETED;
}
else if($this->use_failed && $range >= 3 && $finalgrade == 1)
{
// return failed if failed is enabled and the grade is 1, while there are at leas 3 states.
return completion::FAILED;
}
else {
return completion::PROGRESS;
}
}
}
}
}

View File

@ -0,0 +1,156 @@
<?php
namespace local_treestudyplan\local\aggregators;
use \local_treestudyplan\courseinfo;
use \local_treestudyplan\gradeinfo;
use \local_treestudyplan\studyitem;
use \local_treestudyplan\completion;
class tristate_aggregator extends \local_treestudyplan\aggregator {
public const DEPRECATED = true;
private const DEFAULT_CONDITION = "50";
public function needSelectGradables(){ return True;}
public function isDeprecated() { return self::DEPRECATED;}
public function useRequiredGrades() { return False;}
public function useItemConditions() { return True;}
protected function aggregate_completion(array $a, $condition = "50") {
if(in_array($condition, ['ALL','67','50','ANY'])){
// condition is one of the valid conditions
$c_completed = 0;
$c_excellent = 0;
$c_progress = 0;
$c_pending = 0;
$count = sizeof($a);
if($count > 0)
{
foreach($a as $c) {
$c_progress += ($c>=completion::PROGRESS)?1:0;
$c_completed += ($c>=completion::COMPLETED)?1:0;
$c_excellent += ($c>=completion::EXCELLENT)?1:0;
$c_pending += ($c>=completion::PENDING)?1:0;
}
$required = [
'ALL' => 1.00 * $count,
'67' => 0.67 * $count,
'50' => 0.50 * $count,
'ANY' => 1,
][$condition];
if($c_excellent >= $required) {
return completion::EXCELLENT;
} else if ($c_completed >= $required) {
return completion::COMPLETED;
} else {
// Return PROGRESS if one or more completions are COMPLETED or EXCELLENT, but the aggregation margin is not met
// state PROGRESS will not carry on if aggregations are chained
if($c_progress > 0){
return completion::PROGRESS;
}
else if($c_pending > 0){
return completion::PENDING;
}
else {
return completion::INCOMPLETE;
}
}
}
else {
return completion::INCOMPLETE;
}
}
else
{
// indeterminable, return null
return null;
}
}
public function aggregate_course(courseinfo $courseinfo, studyitem $studyitem, $userid){
$condition = $studyitem->getConditions();
if(empty($condition)){
$condition = self::DEFAULT_CONDITION;
}
$list = [];
foreach(gradeinfo::list_studyitem_gradables($studyitem) as $gi){
$list[] = $this->grade_completion($gi,$userid);
}
$completion = self::aggregate_completion($list,$condition);
return $completion;
}
public function aggregate_junction(array $completion, studyitem $studyitem, $userid){
$completed = self::aggregate_completion($completion,$studyitem->getConditions());
// if null result (conditions are unknown/null) - default to ALL
return isset($completed)?$completed:(self::aggregate_completion($completion,'ALL'));
}
public function grade_completion(gradeinfo $gradeinfo, $userid) {
global $DB;
$table = "local_treestudyplan_gradecfg";
$gradeitem = $gradeinfo->getGradeitem();
$grade = $gradeitem->get_final($userid);
if(empty($grade)){
return completion::INCOMPLETE;
}
else if($grade->finalgrade === NULL)
{
// on assignments, grade NULL means a submission has not yet been graded,
// but on quizes this can also mean a quiz might have been started
// Therefor, we treat a NULL result as a reason to check the relevant gradingscanner for presence of pending items
// Since we want old results to be visible until a pending item was graded, we only use this state here.
// Pending items are otherwise expressly indicated by the "pendingsubmission" field in the user model
if($gradeinfo->getGradingscanner()->pending($userid)){
return completion::PENDING;
} else {
return completion::INCOMPLETE;
}
}
else {
$finalgrade = $grade->finalgrade;
$scale = $gradeinfo->getScale();
if($gradeitem->gradepass > 0)
{
// Base completion off of gradepass (if set)
if($gradeitem->grademax > $gradeitem->gradepass && $finalgrade >= $gradeitem->grademax){
// If gradepass is configured
return completion::EXCELLENT;
}
else if($finalgrade >= $gradeitem->gradepass){
return completion::COMPLETED;
}
else {
return completion::PROGRESS;
}
}
else {
// Blind assumptions:
// over 55% of range is completed
// over 85% of range is excellent
$g = floatval($finalgrade - $gradeitem->grademin);
$range = floatval($gradeitem->grademax - $gradeitem->grademin);
$score = $g / $range;
if($score > 0.85){
return completion::EXCELLENT;
}
else if($score > 0.55){
return completion::COMPLETED;
}
else {
return completion::PROGRESS;
}
}
}
}
}

View File

@ -0,0 +1,280 @@
<?php
namespace local_treestudyplan\local;
use Exception;
class gradegenerator {
private $table = [];
private static $loremipsum = [
"Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
"Etiam scelerisque ligula porttitor velit sollicitudin blandit.",
"Praesent laoreet nisi id lacus laoreet volutpat.",
"Donec rutrum tortor tempus lectus malesuada, ut pretium eros vehicula.",
"Phasellus vulputate tortor vehicula mauris porta ultricies.",
"Ut et lacus sit amet nisl facilisis elementum.",
"Vestibulum ut mauris ac justo tincidunt hendrerit.",
"Fusce congue nulla quis elit facilisis malesuada.",
"Aenean ornare eros placerat ipsum fringilla, sed imperdiet felis imperdiet.",
"Ut malesuada risus ultricies arcu dapibus, quis lobortis eros maximus.",
"Nam ullamcorper dolor ac est tristique, vel blandit tortor tristique.",
"Quisque quis lorem vitae leo lobortis posuere.",
"Nulla ac enim consectetur, rhoncus eros sed, malesuada enim.",
"Vestibulum lobortis lacus ac dolor pulvinar, gravida tincidunt dolor bibendum.",
"Maecenas fringilla urna eget sem bibendum, non lacinia lorem tempus.",
"Nullam quis metus sagittis, pharetra orci eget, ultrices nunc.",
"Morbi et ante at ipsum sodales porta.",
"Morbi vel neque in urna vestibulum vestibulum eu quis lectus.",
"Nam consequat dolor at enim vestibulum, ac gravida nisl consequat.",
"Phasellus ac libero vestibulum, vulputate tellus at, viverra dui.",
"Vivamus venenatis magna a nunc cursus, eget laoreet velit malesuada.",
"Cras fermentum velit vitae tellus sodales, vulputate semper purus porta.",
"Cras ultricies orci in est elementum, at laoreet erat tempus.",
"In non magna et lorem sagittis sollicitudin sit amet et est.",
"Etiam vitae augue ac turpis volutpat iaculis a vitae enim.",
"Integer pharetra quam ac tortor porta dignissim.",
"Pellentesque ullamcorper neque vitae ligula rhoncus accumsan.",
"Nullam in lectus sit amet est faucibus elementum vitae vel risus.",
"Aenean vehicula libero ut convallis blandit.",
"Aenean id mi facilisis, tristique enim vel, egestas lorem.",
"Mauris suscipit dui eget neque gravida, vel pellentesque leo gravida.",
"Quisque quis elit at velit maximus viverra ultricies in nisi.",
"Vivamus et orci nec magna hendrerit egestas sed quis arcu.",
"Suspendisse semper tortor sed justo iaculis volutpat.",
"Praesent interdum dolor nec ultricies imperdiet.",
"Vivamus tristique justo quis tellus commodo, at faucibus justo auctor.",
"Praesent pharetra tellus vel nunc mattis pharetra.",
"Cras a dui quis arcu rutrum ullamcorper sit amet et sem.",
"Aenean porttitor risus ac enim tempor posuere.",
"Mauris bibendum augue ac vehicula mattis.",
"Vestibulum nec justo vehicula, euismod enim sed, convallis magna.",
"Praesent ultrices elit vitae velit dignissim dignissim.",
"Curabitur vehicula velit vitae tortor consequat consectetur sit amet at leo.",
"Sed lobortis neque a magna facilisis aliquam.",
"Phasellus a libero in sem aliquam varius.",
"Mauris tincidunt ligula a risus efficitur euismod.",
"Sed pharetra diam ac neque tempus convallis.",
"Donec at ipsum elementum ex hendrerit laoreet mollis non elit.",
"Praesent eu arcu sollicitudin, fermentum tellus at, blandit dolor.",
"Curabitur in lectus consequat, bibendum ligula vitae, semper lacus.",
"Aenean eu risus non sem pretium dictum.",
"Praesent nec risus vestibulum quam venenatis tempor.",
"Nullam rhoncus ex a quam egestas, eu auctor enim lobortis.",
"Nam luctus ante id lacus scelerisque, quis blandit ante elementum.",
];
private function generatedfeedback(){
if(file_exists("/usr/games/fortune")){
// get a fortune if it is available
return shell_exec("/usr/games/fortune -n 160 -e disclaimer literature science pratchett wisdom education");
} else {
// get a random loremipsum string
return self::$loremipsum[rand(0,count(self::$loremipsum)-1)];
}
}
public function __construct(){
}
public function addstudent(string $student){
if(!array_key_exists($student,$this->table)){
$this->table[$student] = [
"intelligence" => rand(70,100),
"endurance" => rand(70,100),
"skills" => [],
];
}
}
public function addskill(string $student, string $skill){
$this->addstudent($student);
if(!array_key_exists($skill,$this->table[$student]["skills"])){
$int = $this->table[$student]["intelligence"];
$end = $this->table[$student]["endurance"];
$this->table[$student]["skills"][$skill] = [
"intelligence" => min(100, $int + rand(-30,30)),
"endurance" => min(100, $end + rand(-10,10)),
];
}
}
// Below is mostly just for reference in the json file
public function addUserNameInfo(string $student, $firstname,$lastname){
$this->addstudent($student);
$this->table[$student]["firstname"] = $firstname;
$this->table[$student]["lastname"] = $lastname;
}
public function generateraw($student,$skill, $count )
{
$this->addskill($student,$skill);
$int = $this->table[$student]["skills"][$skill]["intelligence"];
$end = $this->table[$student]["skills"][$skill]["endurance"];
$results = [];
$gaveup = false;
for($i=0; $i < $count; $i++){
$r = new \stdClass;
if($gaveup) {
$r->done = !$gaveup;
} else {
$r->done = (rand(0, $end) > 20); // Determine if the assignment was done
}
if($r->done){
$score = rand(0,$int) ;
$r->result = ($score > 20); // determine if the assignment was successful
if(!$r->result){
$r->failed = !($score > 10);
}
} else {
$r->result = false; // make sure a result property is always there
$r->failed = true;
}
// Aways generate a little feedback
$r->fb = $this->generatedfeedback();
$results[] = $r;
if(!$gaveup && $i >= 3) {
// There is a slight chance the students with low endurance for this course will stop with this course's work entirely
$gaveup = (rand(0,$end) < 15);
}
}
return $results;
}
public function generate($student,$skill, array $gradeinfos ){
global $DB;
$table ="local_treestudyplan_gradecfg";
$rlist = [];
$gen = $this->generateraw($student,$skill, count($gradeinfos));
for($i=0; $i < count($gradeinfos); $i++){
$g = $gradeinfos[$i];
$gi = $g->getGradeitem();
$gr = $gen[$i];
// First get the configured interpretation for this scale or grade
$scale = $gi->load_scale();
if( isset($scale)){
$gradecfg = $DB->get_record($table,["scale_id"=>$scale->id]);
}
else if($gi->grademin == 0)
{
$gradecfg = $DB->get_record($table,["grade_points"=>$gi->grademax]);
}
else
{
$gradecfg = null;
}
// next generate the grade
if($gradecfg)
{
if(!$gr->done){
// INCOMPLETE
// fair chance of teacher forgetting to set incomplete to "no evidence"
$grade = 0;// $grade = (rand(0,100) > 15)?max(1, $gradecfg->min_progress-1):"0";
$r = (object)["gi" => $g, "grade" => $grade, "fb" =>"" ];
}
else if(!$gr->result){
$grade = rand($gradecfg->min_progress, $gradecfg->min_completed -1 );
$r = (object)["gi" => $g, "grade" => $grade, "fb" =>$gr->fb ];
}
else{
// COMPLETED
$r = (object)["gi" => $g, "grade" => rand( $gradecfg->min_completed, $gi->grademax ), "fb" =>$gr->fb ];
}
$r->gradetext = $r->grade;
if( isset($scale)){
$scaleitems = $scale->load_items();
if($r->grade > 0){
$r->gradetext = trim($scale->get_nearest_item($r->grade));
} else {
$r->gradetext = "-";
}
}
}
else if($gi->gradepass > 0)
{
if(!$gr->done){
// INCOMPLETe or FAILED
$grade = rand(0, $gi->gradepass/2);
$r = (object)["gi" => $g, "grade" => $grade, "fb" =>($grade > 0)?$gr->fb:"" ];
}
else if(!$gr->result){
//PROGRESS
$r = (object)["gi" => $g, "grade" => rand( round($gi->gradepass/2), $gi->gradepass -1 ), "fb" =>$gr->fb ];
}
else{
// COMPLETED
$r = (object)["gi" => $g, "grade" => rand( $gi->gradepass, $gi->grademax ), "fb" =>$gr->fb ];
}
$r->gradetext = $r->grade;
}
else {
// Blind assumptions if nothing is provided
// over 55% of range is completed
// under 35% is not done
$range = floatval($gi->grademax - $gi->grademin);
if(!$gr->done){
// INCOMPLETe or FAILED
$grade = rand(0, round($range * 0.35) - 1);
$r = (object)["gi" => $g, "grade" => $gi->grademin+$grade, "fb" =>($grade > 0)?$gr->fb:"" ];
}
else if(!$gr->result){
//PROGRESS
$r = (object)["gi" => $g, "grade" => $gi->grademin+rand(round($range * 0.35),round($range * 0.55) - 1 ), "fb" =>$gr->fb ];
}
else{
// COMPLETED
$r = (object)["gi" => $g, "grade" => $gi->grademin+rand(round($range * 0.55) , $range ), "fb" =>$gr->fb ];
}
$r->gradetext = $r->grade;
}
$rlist[] = $r;
}
return $rlist;
}
public function getstats($student){
return $this->table[$student];
}
public function serialize(): ?string{
return json_encode([
"table" => $this->table],JSON_PRETTY_PRINT);
}
public function unserialize(string $data): void {
$o = json_decode($data,true);
$this->table = $o["table"];
}
public function toFile(string $filename){
file_put_contents($filename,$this->serialize());
}
public function fromFile(string $filename){
if(file_exists($filename)){
try{
$json = file_get_contents($filename);
$this->unserialize($json);
} catch(Exception $x){
cli_problem("ERROR loading from file");
throw $x; // Throw X up again to show the output
}
}
}
}

View File

@ -0,0 +1,90 @@
<?php
namespace local_treestudyplan\local\helpers;
class webservicehelper {
/** @var \context_system */
private static $systemcontext = null;
private static $validated_contexts = [];
/**
* Test for capability in the given context for the current user and throw a \webservice_access_exception if not
* Note: The context is not validate
* @param array|string $capability One or more capabilities to be tested OR wise (if one capability is given, the function passes)
* @param \context $context The context in which to check for the capability.
* @throws \webservice_access_exception If none of the capabilities provided are given to the current user
*/
public static function has_capabilities($capability,$context){
if($context == null){
$context = \context_system::instance();
}
if(is_array($capability)){
foreach($capability as $cap){
if(has_capability($cap,$context)){
return true;
}
}
}
elseif(has_capability($capability,$context)){
return true;
}
}
/**
* Test for capability in the given context for the current user and throw a \webservice_access_exception if not
* @param array|string $capability One or more capabilities to be tested OR wise (if one capability is given, the function passes)
* @param \context $context The context in which to check for the capability. Leave empty to use the system context.
* @param bool $validate Validate the context before checking capabilities
* @throws \webservice_access_exception If none of the capabilities provided are given to the current user
*/
public static function require_capabilities($capability,$context=null,$validate=true){
if($validate) {
\external_api::validate_context($context);
}
if(! static::has_capabilities($capability,$context)){
throw new \webservice_access_exception("The capability {$capability} is required on this context ({$context->get_context_name()})");
}
}
/**
* Find and validate a given context by id
* @param int $contextid The id of the context
* @return \context The found context by id
* @throws \InvalidArgumentException When the context is not found
*/
public static function find_context($contextid): \context{
if(isset($contextid) && is_int($contextid) && $contextid > 0){
if(!in_array($contextid,self::$validated_contexts)){ // Cache the context and make sure it is only validated once...
try{
$context = \context::instance_by_id($contextid);
}
catch(\dml_missing_record_exception $x){
throw new \InvalidArgumentException("Context {$contextid} not available"); // Just throw it up again. catch is included here to make sure we know it throws this exception
}
// Validate the found context
\external_api::validate_context($context);
self::$validated_contexts[$contextid] = $context;
}
return self::$validated_contexts[$contextid];
}
else{
return static::system_context(); // This function ensures the system context is validated just once this call
}
}
/**
* Return the validated system context (validation happens only once for this call)
* @return \context_system The system context, validated to use as this context
*/
public static function system_context(): \context_system {
if(!isset(static::$systemcontext)){
static::$systemcontext = \context_system::instance();
\external_api::validate_context(static::$systemcontext);
}
return static::$systemcontext;
}
}

View File

@ -0,0 +1,66 @@
<?php
namespace local_treestudyplan\local\ungradedscanners;
class assign_scanner extends scanner_base {
protected function get_ungraded_submissions(){
global $DB;
//SELECT asgn_sub.id as submissionid, a.id as instanceid, asgn_sub.userid as userid, asgn_sub.timemodified as timesubmitted, asgn_sub.attemptnumber , a.maxattempts
$sql = "SELECT DISTINCT asgn_sub.userid
FROM {assign_submission} asgn_sub
JOIN {assign} a ON a.id = asgn_sub.assignment
LEFT JOIN {assign_grades} ag ON ag.assignment = asgn_sub.assignment AND ag.userid = asgn_sub.userid AND
asgn_sub.attemptnumber = ag.attemptnumber
WHERE a.id = {$this->gi->iteminstance}
AND asgn_sub.status = 'submitted'
AND asgn_sub.userid > 0
AND a.grade <> 0 AND (ag.id IS NULL OR asgn_sub.timemodified >= ag.timemodified)
";
return $DB->get_fieldset_sql($sql);
}
protected function get_graded_users(){
global $DB;
$sql = "SELECT DISTINCT g.userid
FROM {grade_grades} g
LEFT JOIN {grade_items} gi on g.itemid = gi.id
WHERE gi.itemmodule = 'assign' AND gi.iteminstance = {$this->gi->iteminstance}
AND g.finalgrade IS NOT NULL"; // MAy turn out to be needed, dunno
return $DB->get_fieldset_sql($sql);
}
public function count_ungraded($course_userids=[]){
$ungraded = $this->get_ungraded_submissions();
if(count($course_userids) > 0){
$ungraded = array_intersect($ungraded,$course_userids);
}
return count($ungraded);
}
public function count_graded($course_userids=[]){
$ungraded = $this->get_ungraded_submissions();
$graded = $this->get_graded_users();
if(count($course_userids) > 0){
$ungraded = array_intersect($ungraded,$course_userids);
$graded = array_intersect($graded,$course_userids);
}
// determine how many id's have a grade, but also an ungraded submission
$dual = array_intersect($ungraded,$graded);
// subtract those from the graded count
return count($graded) - count($dual);
}
public function has_ungraded_submission($userid)
{
$ungraded = $this->get_ungraded_submissions();
return in_array($userid,$ungraded);
}
}

View File

@ -0,0 +1,67 @@
<?php
namespace local_treestudyplan\local\ungradedscanners;
require_once($CFG->dirroot.'/question/engine/states.php'); // for reading question state
class quiz_scanner extends scanner_base {
protected function get_ungraded_submissions(){
// count all users who have one or more questions that still need grading
global $DB;
// First find all question attempts that need grading
$sql = "SELECT qza.id as submissionid, qza.userid as userid, qas.questionattemptid as attempt_id, qas.sequencenumber as sequencenumber
FROM {question_attempt_steps} qas
JOIN {question_attempts} qna ON qas.questionattemptid = qna.id
JOIN {quiz_attempts} qza ON qna.questionusageid = qza.uniqueid
WHERE qas.state = 'needsgrading' AND qza.quiz = {$this->gi->iteminstance}";
$rs = $DB->get_recordset_sql($sql);
$submissions = [];
foreach($rs as $r){
// Now, check if
$maxstate_sql = "SELECT MAX(qas.sequencenumber) FROM {question_attempt_steps} qas WHERE qas.questionattemptid = {$r->attempt_id}";
$max = $DB->get_field_sql($maxstate_sql);
if($r->sequencenumber == $max){
$submissions[$r->userid] = true; // set array index based on user id, to avoid checking if value is in array
}
}
$rs->close();
return array_keys($submissions);
}
public function count_ungraded($course_userids=[]){
$ungraded = $this->get_ungraded_submissions();
if(count($course_userids) > 0){
$ungraded = array_intersect($ungraded,$course_userids);
}
return count($ungraded);
}
public function count_graded($course_userids=[]){
// count all users who submitted one or more finished tests.
global $DB;
$sql = "SELECT DISTINCT g.userid
FROM {grade_grades} g
LEFT JOIN {grade_items} gi on g.itemid = gi.id
WHERE gi.itemmodule = 'quiz' AND gi.iteminstance = {$this->gi->iteminstance}
AND g.finalgrade IS NOT NULL"; // MAy turn out to be needed, dunno
$graded = $DB->get_fieldset_sql($sql);
if(count($course_userids) > 0){
$graded = array_intersect($graded,$course_userids);
}
return count($graded);
}
public function has_ungraded_submission($userid)
{
$ungraded = $this->get_ungraded_submissions();
return in_array($userid,$ungraded);
}
}

View File

@ -0,0 +1,19 @@
<?php
namespace local_treestudyplan\local\ungradedscanners;
use \grade_item;
abstract class scanner_base {
protected $gi;
public function __construct(grade_item $gi){
$this->gi = $gi;
}
public abstract function count_ungraded($course_userids=[]);
public abstract function count_graded($course_userids=[]);
public abstract function has_ungraded_submission($userid);
}

View File

@ -0,0 +1,91 @@
<?php
require_once("$CFG->libdir/formslib.php");
require_once("$CFG->dirroot/local/treestudyplan/lib.php");
class reportinvite_form extends moodleform {
//Add elements to form
const GOALS_EDITOR_OPTIONS = array('trusttext'=>true, 'subdirs'=>true, 'maxfiles'=>0,'maxbytes'=>5*1024*1025);
public function definition() {
global $CFG;
// 'code', 'revision', 'description', 'goals', 'complexity', 'points', 'studyhours'
$mform = $this->_form; // Don't forget the underscore!
$mform->addElement('hidden', 'add', 0);
$mform->setType('add', PARAM_ALPHANUM);
$mform->addElement('hidden', 'update', 0);
$mform->setType('update', PARAM_INT);
// $mform->addElement('static', 'desc_new', get_string('invite_desc_new','local_treestudyplan')); // Add elements to your form
// $mform->addElement('static', 'desc_edit', get_string('invite_desc_edit','local_treestudyplan')); // Add elements to your form
$mform->addElement('text', 'name', get_string('invite_name','local_treestudyplan'), array('size' => 50)); // Add elements to your form
$mform->setType('name', PARAM_NOTAGS); //Set type of element
$mform->setDefault('name', ''); //Default value
$mform->addRule('name', get_string('required'), 'required', null, 'client');
$mform->addElement('text', 'email', get_string('invite_email','local_treestudyplan'), array('size' => 20)); // Add elements to your form
$mform->setType('email', PARAM_NOTAGS); //Set type of element
$mform->setDefault('email', ''); //Default value
$mform->addRule('email', get_string('required'), 'required', null, 'client');
$mform->addRule('email', get_string('email'), 'email', null, 'client');
$mform->addElement('static', get_string('invite_email','local_treestudyplan') ); // Add elements to your form
$this->add_action_buttons();
}
//Custom validation should be added here
function validation($data, $files) {
return array();
}
function set_data($data) {
parent::set_data($data);
}
function get_data()
{
global $DB,$USER;
$data = parent::get_data();
if($data != NULL)
{
if(empty($data->user_id))
{
$data->user_id = $USER->id;
}
if(empty($data->update))
{
$date = new DateTime("now", core_date::get_user_timezone_object());
$date->setTime(0, 0, 0);
$data->date = $date->getTimeStamp();
}
if(empty($data->update))
{
//create a new random key for the invite
do {
$length = 20;
$characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
$charactersLength = strlen($characters);
$randomkey = '';
for ($i = 0; $i < $length; $i++) {
$randomkey .= $characters[rand(0, $charactersLength - 1)];
}
// Double check that the key is unique before inserting
} while($DB->record_exists_select("local_treestudyplan_invit", $DB->sql_compare_text("invitekey"). " = " . $DB->sql_compare_text(":invitekey"), ['invitekey' => $randomkey]));
$data->invitekey = $randomkey;
}
}
return $data;
}
}

View File

@ -0,0 +1,274 @@
<?php
namespace local_treestudyplan;
require_once($CFG->libdir.'/externallib.php');
use \local_treestudyplan\local\helpers\webservicehelper;
require_once($CFG->libdir.'/badgeslib.php');
class studentstudyplanservice extends \external_api
{
const CAP_VIEWOTHER = "local/treestudyplan:viewuserreports";
/************************
* *
* list_user_studyplans *
* *
************************/
public static function list_user_studyplans_parameters()
{
return new \external_function_parameters([
"userid" => new \external_value(PARAM_INT, 'id of student', VALUE_DEFAULT),
]);
}
public static function list_user_studyplans_returns()
{
return new \external_multiple_structure(
studyplan::simple_structure()
);
}
private static function list_user_studyplans($userid){
global $CFG, $DB;
$list = [];
$studyplans = studyplan::find_for_user($userid);
foreach($studyplans as $studyplan)
{
// only include studyplans in the context the user has permissions for
if(webservicehelper::has_capabilities(self::CAP_VIEWOTHER,$studyplan->context(),false)){
$list[] =$studyplan->simple_model();
}
}
return $list;
}
/************************
* *
* get_user_studyplans *
* *
************************/
public static function get_user_studyplans_parameters()
{
return new \external_function_parameters( [
"userid" => new \external_value(PARAM_INT, 'id of user'),
] );
}
public static function get_user_studyplans_returns()
{
return new \external_multiple_structure(
studyplan::user_structure()
);
}
public static function get_user_studyplans($userid)
{
global $CFG, $DB;
$studyplans = studyplan::find_for_user($userid);
$map = [];
foreach($studyplans as $studyplan)
{
// only include studyplans in the context the user has permissions for
if(webservicehelper::has_capabilities(self::CAP_VIEWOTHER,$studyplan->context(),false)){
$map[] = $studyplan->user_model($userid);
}
}
return $map;
}
/************************
* *
* get_user_studyplan *
* *
************************/
public static function get_user_studyplan_parameters()
{
return new \external_function_parameters( [
"userid" => new \external_value(PARAM_INT, 'id of user'),
"studyplanid" => new \external_value(PARAM_INT, 'id of specific studyplan to provide'),
] );
}
public static function get_user_studyplan_returns()
{
return studyplan::user_structure();
}
public static function get_user_studyplan($userid,$studyplanid)
{
global $CFG, $DB;
$studyplan = studyplan::findById($studyplanid);
webservicehelper::require_capabilities(self::CAP_VIEWOTHER,$studyplan->context());
if($studyplan->has_linked_user($userid)){
return $studyplan->user_model($userid);
}
else {
return null;
}
}
/****************************
* *
* get_invited_studyplan *
* *
****************************/
public static function get_invited_studyplan_parameters()
{
return new \external_function_parameters( [
"invitekey" => new \external_value(PARAM_RAW, 'invite key'),
] );
}
public static function get_invited_studyplan_returns()
{
return new \external_multiple_structure(
studyplan::user_structure()
);
}
public static function get_invited_studyplan($invitekey)
{
global $CFG, $DB;
$invite = $DB->get_record_select("local_treestudyplan_invit", $DB->sql_compare_text("invitekey"). " = " . $DB->sql_compare_text(":invitekey"), ['invitekey' => $invitekey]);
if(empty($invite)){
return [];
}
$userid = $invite->user_id;
$map = [];
$studyplans = studyplan::find_for_user($userid);
foreach($studyplans as $studyplan)
{
$map[] = $studyplan->user_model($userid);
}
return $map;
}
/************************
* *
* list_own_studyplans *
* *
************************/
public static function list_own_studyplans_parameters()
{
return new \external_function_parameters([]);
}
public static function list_own_studyplans_returns()
{
return new \external_multiple_structure(
studyplan::simple_structure()
);
}
private static function list_own_studyplans(){
global $CFG, $DB, $USER;
$userid = $USER->id;
$list = [];
$studyplans = studyplan::find_for_user($userid);
foreach($studyplans as $studyplan)
{
$list[] =$studyplan->simple_model();
}
return $list;
}
/************************
* *
* get_own_studyplan *
* *
************************/
public static function get_own_studyplan_parameters()
{
return new \external_function_parameters( [
"id" => new \external_value(PARAM_INT, 'id of specific studyplan to provide', VALUE_DEFAULT),
] );
}
public static function get_own_studyplan_returns()
{
return new \external_multiple_structure(
studyplan::user_structure()
);
}
public static function get_own_studyplan($id=null)
{
global $CFG, $DB, $USER;
$userid = $USER->id;
$studyplans = studyplan::find_for_user($userid);
if(isset($id) && $id > 0){
if(isset($studyplans[$id])){
$studyplan = $studyplans[$id];
return [$studyplan->user_model($userid)];
} else {
return [];
}
}
else {
$map = [];
foreach($studyplans as $studyplan){
$map[] = $studyplan->user_model($userid);
}
return $map;
}
}
/***************************
* *
* get_teaching_studyplans *
* *
***************************/
public static function get_teaching_studyplans_parameters()
{
return new \external_function_parameters( [
"id" => new \external_value(PARAM_INT, 'id of specific studyplan to provide', VALUE_DEFAULT),
] );
}
public static function get_teaching_studyplans_returns()
{
return new \external_multiple_structure(
studyplan::editor_structure()
);
}
public static function get_teaching_studyplans($id=null)
{
global $CFG, $DB, $USER;
$userid = $USER->id;
$studyplans = studyplan::find_teaching($userid);
if(isset($id) && $id > 0){
if(isset($studyplans[$id])){
$studyplan = $studyplans[$id];
return [$studyplan->editor_model($userid)];
} else {
return [];
}
}
else {
$map = [];
foreach($studyplans as $studyplan){
$map[] = $studyplan->editor_model($userid);
}
return $map;
}
}
}

516
classes/studyitem.php Normal file
View File

@ -0,0 +1,516 @@
<?php
namespace local_treestudyplan;
require_once($CFG->libdir.'/externallib.php');
class studyitem {
public const COMPETENCY = 'competency';
public const COURSE = 'course';
public const JUNCTION = 'junction';
public const BADGE = 'badge';
public const FINISH = 'finish';
public const START = 'start';
public const INVALID = 'invalid';
public const TABLE = "local_treestudyplan_item";
private static $STUDYITEM_CACHE = [];
private $r; // Holds database record
private $id;
private $courseinfo = null;
private $studyline;
private $aggregator;
public function context(): \context {
return $this->studyline->context();
}
public function getStudyline(): studyline {
return $this->studyline;
}
public function getConditions() {
return $this->r->conditions;
}
public static function findById($id): self {
if(!array_key_exists($id,self::$STUDYITEM_CACHE)){
self::$STUDYITEM_CACHE[$id] = new self($id);
}
return self::$STUDYITEM_CACHE[$id];
}
public function __construct($id) {
global $DB;
$this->id = $id;
$this->r = $DB->get_record(self::TABLE,['id' => $id],"*",MUST_EXIST);
$this->studyline = studyline::findById($this->r->line_id);
$this->aggregator = $this->getStudyline()->getStudyplan()->getAggregator();
}
public function id(){
return $this->id;
}
public function slot(){
return $this->r->slot;
}
public function layer(){
return $this->r->layer;
}
public function type(){
return $this->r->type;
}
public function courseid(){
return $this->r->course_id;
}
public static function exists($id){
global $DB;
return is_numeric($id) && $DB->record_exists(self::TABLE, array('id' => $id));
}
public static function editor_structure($value=VALUE_REQUIRED){
return new \external_single_structure([
"id" => new \external_value(PARAM_INT, 'id of study item'),
"type" => new \external_value(PARAM_TEXT, 'shortname of study item'),
"conditions"=> new \external_value(PARAM_TEXT, 'conditions for completion'),
"slot" => new \external_value(PARAM_INT, 'slot in the study plan'),
"layer" => new \external_value(PARAM_INT, 'layer in the slot'),
"course" => courseinfo::editor_structure(VALUE_OPTIONAL),
"badge" => badgeinfo::editor_structure(VALUE_OPTIONAL),
"continuation_id" => new \external_value(PARAM_INT, 'id of continued item'),
"connections" => new \external_single_structure([
'in' => new \external_multiple_structure(studyitemconnection::structure()),
'out' => new \external_multiple_structure(studyitemconnection::structure()),
]),
]);
}
public function editor_model(){
return $this->generate_model("editor");
}
private function generate_model($mode){
// Mode parameter is used to geep this function for both editor model and export model
// (Export model results in fewer parameters on children, but is otherwise basically the same as this function)
global $DB;
$model = [
'id' => $this->r->id, // Id is needed in export model because of link references
'type' => $this->isValid()?$this->r->type:self::INVALID,
'conditions' => $this->r->conditions,
'slot' => $this->r->slot,
'layer' => $this->r->layer,
'continuation_id' => $this->r->continuation_id,
'connections' => [
"in" => [],
"out" => [],
]
];
if($mode == "export"){
// remove slot and layer
unset($model["slot"]);
unset($model["layer"]);
unset($model["continuation_id"]);
$model["connections"] = []; // In export mode, connections is just an array of outgoing connections
if(!isset($this->r->conditions)){
unset($model["conditions"]);
}
}
// Add course link if available
$ci = $this->getcourseinfo();
if(isset($ci)){
if($mode == "export"){
$model['course'] = $ci->shortname();
} else {
$model['course'] = $ci->editor_model($this,$this->aggregator->usecorecompletioninfo());
}
}
// Add badge info if available
if(is_numeric($this->r->badge_id) && $DB->record_exists('badge', array('id' => $this->r->badge_id)))
{
$badge = new \core_badges\badge($this->r->badge_id);
$badgeinfo = new badgeinfo($badge);
if($mode == "export"){
$model['badge'] = $badgeinfo->name();
} else {
$model['badge'] = $badgeinfo->editor_model();
}
}
if($mode == "export"){
// Also export gradables
$gradables = gradeinfo::list_studyitem_gradables($this);
if(count($gradables) > 0){
$model["gradables"] = [];
foreach($gradables as $g){
$model["gradables"][] = $g->export_model();;
}
}
}
// Add incoming and outgoing connection info
$conn_out = studyitemconnection::find_outgoing($this->id);
if($mode == "export"){
foreach($conn_out as $c) {
$model["connections"][] = $c->to_id();
}
}
else {
foreach($conn_out as $c) {
$model['connections']['out'][$c->to_id()] = $c->model();
}
$conn_in = studyitemconnection::find_incoming($this->id);
foreach($conn_in as $c) {
$model['connections']['in'][$c->from_id()] = $c->model();
}
}
return $model;
}
public static function add($fields,$import=false)
{
global $DB;
$addable = ['line_id','type','conditions','slot','competency_id','course_id','badge_id','continuation_id'];
if($import){ $addable[] = "layer";}
$info = [ 'layer' => -1, ];
foreach($addable as $f){
if(array_key_exists($f,$fields)){
$info[$f] = $fields[$f];
}
}
$id = $DB->insert_record(self::TABLE, $info);
return self::findById($id);
}
public function edit($fields)
{
global $DB;
$editable = ['conditions','course_id','continuation_id'];
$info = ['id' => $this->id,];
foreach($editable as $f){
if(array_key_exists($f,$fields)){
$info[$f] = $fields[$f];
}
}
$DB->update_record(self::TABLE, $info);
//reload record after edit
$this->r = $DB->get_record(self::TABLE,['id' => $this->id],"*",MUST_EXIST);
return $this;
}
public function isValid(){
// Check if referenced courses, badges and/or competencies still exist
if($this->r->type == static::COURSE){
return courseinfo::exists($this->r->course_id);
}
else if($this->r->type == static::BADGE){
return badgeinfo::exists($this->r->badge_id);
}
else {
return true;
}
}
public function delete($force=false)
{
global $DB;
// check if this item is referenced in a START item
if($force){
// clear continuation id from any references to this item
$records = $DB->get_records(self::TABLE,['continuation_id' => $this->id]);
foreach($records as $r){
$r->continuation_id = 0;
$DB->update_record(self::TABLE,$r);
}
}
if($DB->count_records(self::TABLE,['continuation_id' => $this->id]) > 0){
return success::fail('Cannot remove: item is referenced by another item');
}
else
{
// delete al related connections to this item
studyitemconnection::clear($this->id);
// delete all grade inclusion references to this item
$DB->delete_records("local_treestudyplan_gradeinc",['studyitem_id' => $this->id]);
// delete the item itself
$DB->delete_records(self::TABLE, ['id' => $this->id]);
return success::success();
}
}
/************************
* *
* reorder_studyitems *
* *
************************/
public static function reorder($resequence)
{
global $DB;
foreach($resequence as $sq)
{
$DB->update_record(self::TABLE, [
'id' => $sq['id'],
'line_id' => $sq['line_id'],
'slot' => $sq['slot'],
'layer' => $sq['layer'],
]);
}
return success::success();
}
public static function find_studyline_children(studyline $line)
{
global $DB;
$list = [];
$ids = $DB->get_fieldset_select(self::TABLE,"id","line_id = :line_id ORDER BY layer",['line_id' => $line->id()]);
foreach($ids as $id) {
$item = self::findById($id,$line);
$list[] = $item;
}
return $list;
}
private static function link_structure($value=VALUE_REQUIRED){
return new \external_single_structure([
"id" => new \external_value(PARAM_INT, 'id of study item'),
"type" => new \external_value(PARAM_TEXT, 'type of study item'),
"completion" => completion::structure(),
"studyline" => new \external_value(PARAM_TEXT, 'reference label of studyline'),
"studyplan" => new \external_value(PARAM_TEXT, 'reference label of studyplan'),
], 'basic info of referenced studyitem', $value);
}
private function link_model($userid){
global $DB;
$line = $DB->get_record(studyline::TABLE,['id' => $this->r->line_id]);
$plan = $DB->get_record(studyplan::TABLE,['id' => $line->studyplan_id]);
return [
"id" => $this->r->id,
"type" => $this->r->type,
"completion" => $this->completion($userid),
"studyline" => $line->name(),
"studyplan" => $plan->name(),
];
}
public static function user_structure($value=VALUE_REQUIRED){
return new \external_single_structure([
"id" => new \external_value(PARAM_INT, 'id of study item'),
"type" => new \external_value(PARAM_TEXT, 'type of study item'),
"completion" => new \external_value(PARAM_TEXT, 'completion state (incomplete|progress|completed|excellent)'),
"slot" => new \external_value(PARAM_INT, 'slot in the study plan'),
"layer" => new \external_value(PARAM_INT, 'layer in the slot'),
"course" => courseinfo::user_structure(VALUE_OPTIONAL),
"badge" => badgeinfo::user_structure(VALUE_OPTIONAL),
"continuation" => self::link_structure(VALUE_OPTIONAL),
"connections" => new \external_single_structure([
'in' => new \external_multiple_structure(studyitemconnection::structure()),
'out' => new \external_multiple_structure(studyitemconnection::structure()),
]),
],'Study item info',$value);
}
public function user_model($userid){
global $CFG, $DB;
$model = [
'id' => $this->r->id,
'type' => $this->r->type,
'completion' => completion::label($this->completion($userid)),
'slot' => $this->r->slot,
'layer' => $this->r->layer,
'connections' => [
"in" => [],
"out" => [],
]
];
// Add badge info if available
if(badgeinfo::exists($this->r->badge_id))
{
$badge = new \core_badges\badge($this->r->badge_id);
$badgeinfo = new badgeinfo($badge);
$model['badge'] = $badgeinfo->user_model($userid);
}
// Add continuation_info if available
if(self::exists($this->r->continuation_id))
{
$c_item = self::findById($this->r->continuation_id);
$model['continuation'] = $c_item->link_model($userid);
}
// Add course if available
if(courseinfo::exists($this->r->course_id))
{
$cinfo = $this->getcourseinfo();
$model['course'] = $cinfo->user_model($userid,$this->aggregator->usecorecompletioninfo());
}
// Add incoming and outgoing connection info
$conn_out = studyitemconnection::find_outgoing($this->id);
foreach($conn_out as $c) {
$model['connections']['out'][$c->to_id()] = $c->model();
}
$conn_in = studyitemconnection::find_incoming($this->id);
foreach($conn_in as $c) {
$model['connections']['in'][$c->from_id()] = $c->model();
}
return $model;
}
public function getcourseinfo()
{
if(empty($this->courseinfo) && courseinfo::exists($this->r->course_id)){
$this->courseinfo = new courseinfo($this->r->course_id, $this);
}
return $this->courseinfo;
}
private function completion($userid) {
global $DB;
if($this->isValid()){
if(strtolower($this->r->type) == 'course'){
// determine competency by competency completion
$courseinfo = $this->getcourseinfo();
return $this->aggregator->aggregate_course($courseinfo,$this,$userid);
}
elseif(strtolower($this->r->type) =='start'){
// Does not need to use aggregator.
// Either true, or the completion of the reference
if(self::exists($this->r->continuation_id)){
$c_item = self::findById($this->r->continuation_id);
return $c_item->completion($userid);
} else {
return completion::COMPLETED;
}
}
elseif(in_array(strtolower($this->r->type),['junction','finish'])){
// completion of the linked items, according to the rule
$in_completed = [];
// Retrieve incoming connections
$incoming = $DB->get_records(studyitemconnection::TABLE,['to_id' => $this->r->id]);
foreach($incoming as $conn){
$item = self::findById($conn->from_id);
$in_completed[] = $item->completion($userid);
}
return $this->aggregator->aggregate_junction($in_completed,$this,$userid);
}
elseif(strtolower($this->r->type) =='badge'){
global $DB;
// badge awarded
if(badgeinfo::exists($this->r->badge_id))
{
$badge = new \core_badges\badge($this->r->badge_id);
if($badge->is_issued($userid)){
if($badge->can_expire()){
// get the issued badges and check if any of them have not expired yet
$badges_issued = $DB->get_records("badge_issued",["badge_id" => $this->r->badge_id, "user_id" => $userid]);
$notexpired = false;
$now = time();
foreach($badges_issued as $bi){
if($bi->dateexpire == null || $bi->dateexpire > $now){
$notexpired = true;
break;
}
}
return ($notexpired)?completion::COMPLETED:completion::INCOMPLETE;
}
else{
return completion::COMPLETED;
}
} else {
return completion::INCOMPLETE;
}
} else {
return completion::INCOMPLETE;
}
}
else {
// return incomplete for other types
return completion::INCOMPLETE;
}
}
else {
// return incomplete for other types
return completion::INCOMPLETE;
}
}
public function duplicate($new_line){
global $DB;
// clone the database fields
$fields = clone $this->r;
// set new line id
unset($fields->id);
$fields->line_id = $new_line->id();
//create new record with the new data
$id = $DB->insert_record(self::TABLE, (array)$fields);
$new = self::findById($id,$new_line);
// copy the grading info if relevant
$gradables = gradeinfo::list_studyitem_gradables($this);
foreach($gradables as $g){
gradeinfo::include_grade($g->getGradeitem()->id,$new,true);
}
return $new;
}
public function export_model(){
return $this->generate_model("export");
}
public static function import_item($model){
unset($model["course_id"]);
unset($model["competency_id"]);
unset($model["badge_id"]);
unset($model["continuation_id"]);
if(isset($model["course"])){
$model["course_id"] = courseinfo::id_from_shortname(($model["course"]));
}
if(isset($model["badge"])){
$model["badge_id"] = badgeinfo::id_from_name(($model["badge"]));
}
$item = self::add($model,true);
if(isset($model["course_id"])){
// attempt to import the gradables
foreach($model["gradables"] as $gradable){
gradeinfo::import($item,$gradable);
}
}
return $item;
}
}

View File

@ -0,0 +1,110 @@
<?php
namespace local_treestudyplan;
require_once($CFG->libdir.'/externallib.php');
class studyitemconnection {
const TABLE = "local_treestudyplan_connect";
private $r;
private $id;
protected function __construct($r){
$this->r = $r;
$this->id = $r->id;
}
public static function structure($value=VALUE_REQUIRED){
return new \external_single_structure([
'id' => new \external_value(PARAM_INT, 'id of connection'),
'from_id' => new \external_value(PARAM_INT, 'id of start item'),
'to_id' => new \external_value(PARAM_INT, 'id of end item'),
],'',$value);
}
public function model(){
return ['id' => $this->r->id, 'from_id' => $this->r->from_id, 'to_id' => $this->r->to_id];
}
public function from_item(){
return studyitem::findById($this->r->from_id);
}
public function to_item(){
return studyitem::findById($this->r->to_id);
}
public function from_id(){
return $this->r->from_id;
}
public function to_id(){
return $this->r->to_id;
}
public static function find_outgoing($item_id){
global $DB;
$list = [];
$conn_out = $DB->get_records(self::TABLE,['from_id' => $item_id]);
foreach($conn_out as $c) {
$list[] = new self($c);
}
return $list;
}
public static function find_incoming($item_id){
global $DB;
$list = [];
$conn_in = $DB->get_records(self::TABLE,['to_id' => $item_id]);
foreach($conn_in as $c) {
$list[] = new self($c);
}
return $list;
}
public static function connect($from_id,$to_id)
{
global $DB;
//check if link already exists
if(!$DB->record_exists(self::TABLE, ['from_id' => $from_id, 'to_id' => $to_id]))
{
$id = $DB->insert_record(self::TABLE, [
'from_id' => $from_id,
'to_id' => $to_id,
]);
return new self($DB->get_record(self::TABLE,['id' => $id]));
} else {
return new self($DB->get_record(self::TABLE,['from_id' => $from_id, 'to_id' => $to_id]));
}
}
public static function disconnect($from_id,$to_id)
{
global $DB;
if($DB->record_exists(self::TABLE, ['from_id' => $from_id, 'to_id' => $to_id]))
{
$DB->delete_records(self::TABLE, [
'from_id' => $from_id,
'to_id' => $to_id,
]);
return success::success('Items Disconnected');
} else {
return success::success('Connection does not exist');
}
}
public static function clear($id) {
global $DB;
$DB->delete_records(self::TABLE, ['from_id' => $id]);
$DB->delete_records(self::TABLE, ['to_id' => $id]);
}
}

369
classes/studyline.php Normal file
View File

@ -0,0 +1,369 @@
<?php
namespace local_treestudyplan;
require_once($CFG->libdir.'/externallib.php');
class studyline {
public const SLOTSET_COMPETENCY = 'competencies';
public const SLOTSET_FILTER = 'filters';
public const COMPETENCY_TYPES = [
studyitem::COMPETENCY,
studyitem::COURSE,
];
public const FILTER_TYPES = [
studyitem::JUNCTION,
studyitem::BADGE,
studyitem::FINISH,
studyitem::START,
];
public const FILTER0_TYPES = [
studyitem::START,
];
public const TABLE = "local_treestudyplan_line";
private static $STUDYLINE_CACHE = [];
private $r; // Holds database record
private $id;
private $studyplan;
public function context(): \context {
return $this->studyplan->context();
}
public function getStudyplan() : studyplan {
return $this->studyplan;
}
public static function findById($id): self {
if(!array_key_exists($id,self::$STUDYLINE_CACHE)){
self::$STUDYLINE_CACHE[$id] = new self($id);
}
return self::$STUDYLINE_CACHE[$id];
}
private function __construct($id) {
global $DB;
$this->id = $id;
$this->r = $DB->get_record(self::TABLE,['id' => $id]);
$this->studyplan = studyplan::findById($this->r->studyplan_id);
}
public function id(){
return $this->id;
}
public function name(){
return $this->r->name;
}
public function shortname(){
return $this->r->shortname;
}
public static function editor_structure($value=VALUE_REQUIRED){
return new \external_single_structure([
"id" => new \external_value(PARAM_INT, 'id of studyline'),
"name" => new \external_value(PARAM_TEXT, 'shortname of studyline'),
"shortname"=> new \external_value(PARAM_TEXT, 'idnumber of studyline'),
"color"=> new \external_value(PARAM_TEXT, 'description of studyline'),
"sequence" => new \external_value(PARAM_INT, 'order of studyline'),
"slots" => new \external_multiple_structure(
new \external_single_structure([
self::SLOTSET_COMPETENCY => new \external_multiple_structure(studyitem::editor_structure(),'competency items',VALUE_OPTIONAL),
self::SLOTSET_FILTER => new \external_multiple_structure(studyitem::editor_structure(),'filter items'),
])
)
]);
}
public function editor_model(){
return $this->generate_model("editor");
}
protected function generate_model($mode){
// Mode parameter is used to geep this function for both editor model and export model
// (Export model results in fewer parameters on children, but is otherwise basically the same as this function)
global $DB;
$model = [
'id' => $this->r->id,
'name' => $this->r->name,
'shortname' => $this->r->shortname,
'color' => $this->r->color,
'sequence' => $this->r->sequence,
'slots' => [],
];
if($mode == "export"){
// Id and sequence are not used in export model
unset($model["id"]);
unset($model["sequence"]);
}
// Get the number of slots
// As a safety data integrity measure, if there are any items in a higher slot than currently allowed,
// make sure there are enought slots to account for them
// Alternatively, we could ensure that on reduction of slots, the items that no longer have a slot will be removed.
$max_slot = $DB->get_field_select(studyitem::TABLE,"MAX(slot)","line_id = :lineid",['lineid' => $this->id]);
$num_slots = max($this->studyplan->slots(),$max_slot +1);
// Create the required amount of slots
for($i=0; $i < $num_slots+1; $i++){
if($mode == "export") {
// Export mode does not separate between filter or competency type, since that is determined automatically
$slots = [];
} else {
if($i > 0) {
$slots = [self::SLOTSET_COMPETENCY => [], self::SLOTSET_FILTER => []];
} else {
$slots = [self::SLOTSET_FILTER => []];
}
}
$model['slots'][$i] = $slots;
}
$children = studyitem::find_studyline_children($this);
foreach($children as $c)
{
if($mode == "export") {
$model['slots'][$c->slot()][] = $c->export_model();
} else {
$slotset = null;
if($c->slot() > 0) {
if(in_array($c->type(),self::COMPETENCY_TYPES)) {
$slotset = self::SLOTSET_COMPETENCY;
} else if(in_array($c->type(),self::FILTER_TYPES)) {
$slotset = self::SLOTSET_FILTER;
}
}
else if(in_array($c->type(),self::FILTER0_TYPES)) {
$slotset = self::SLOTSET_FILTER;
}
if(isset($slotset)) {
$model['slots'][$c->slot()][$slotset][] = $c->editor_model();
}
}
}
return $model;
}
public static function add($fields){
global $DB;
if(!isset($fields['studyplan_id'])){
throw new \InvalidArgumentException("parameter 'studyplan_id' missing");
}
$studyplan_id = $fields['studyplan_id'];
$sqmax = $DB->get_field_select(self::TABLE,"MAX(sequence)","studyplan_id = :studyplan_id",['studyplan_id' => $studyplan_id]);
$addable = ['studyplan_id','name','shortname','color'];
$info = ['sequence' => $sqmax+1];
foreach($addable as $f){
if(array_key_exists($f,$fields)){
$info[$f] = $fields[$f];
}
}
$id = $DB->insert_record(self::TABLE, $info);
return self::findById($id);
}
public function edit($fields){
global $DB;
$editable = ['name','shortname','color'];
$info = ['id' => $this->id,];
foreach($editable as $f){
if(array_key_exists($f,$fields)){
$info[$f] = $fields[$f];
}
}
$DB->update_record(self::TABLE, $info);
//reload record after edit
$this->r = $DB->get_record(self::TABLE,['id' => $this->id],"*",MUST_EXIST);
return $this;
}
public function delete($force = false){
global $DB;
if($force){
$children = studyitem::find_studyline_children($this);
foreach($children as $c){
$c->delete($force);
}
}
// check if this item has study items in it
if($DB->count_records(studyitem::TABLE,['line_id' => $this->id]) > 0){
return success::fail('cannot delete studyline with items');
}
else
{
$DB->delete_records(self::TABLE, ['id' => $this->id]);
return success::success();
}
}
public static function reorder($resequence)
{
global $DB;
foreach($resequence as $sq)
{
$DB->update_record(self::TABLE, [
'id' => $sq['id'],
'sequence' => $sq['sequence'],
]);
}
return success::success();
}
public static function find_studyplan_children(studyplan $plan)
{
global $DB;
$list = [];
$ids = $DB->get_fieldset_select(self::TABLE,"id","studyplan_id = :plan_id ORDER BY sequence",['plan_id' => $plan->id()]);
foreach($ids as $id) {
$list[] = self::findById($id);
}
return $list;
}
public static function user_structure($value=VALUE_REQUIRED){
return new \external_single_structure([
"id" => new \external_value(PARAM_INT, 'id of studyline'),
"name" => new \external_value(PARAM_TEXT, 'shortname of studyline'),
"shortname"=> new \external_value(PARAM_TEXT, 'idnumber of studyline'),
"color"=> new \external_value(PARAM_TEXT, 'description of studyline'),
"sequence" => new \external_value(PARAM_INT, 'order of studyline'),
"slots" => new \external_multiple_structure(
new \external_single_structure([
self::SLOTSET_COMPETENCY => new \external_multiple_structure(studyitem::user_structure(),'competency items',VALUE_OPTIONAL),
self::SLOTSET_FILTER => new \external_multiple_structure(studyitem::user_structure(),'filter items'),
])
)
],'Studyline with user info',$value);
}
public function user_model($userid){
// TODO: Integrate this function into generate_model() for ease of maintenance
global $DB;
$model = [
'id' => $this->r->id,
'name' => $this->r->name,
'shortname' => $this->r->shortname,
'color' => $this->r->color,
'sequence' => $this->r->sequence,
'slots' => [],
];
// Get the number of slots
// As a safety data integrity measure, if there are any items in a higher slot than currently allowed,
// make sure there are enought slots to account for them
// Alternatively, we could ensure that on reduction of slots, the items that no longer have a slot will be removed.
$max_slot = $DB->get_field_select(studyitem::TABLE,"MAX(slot)","line_id = :lineid",['lineid' => $this->id]);
$num_slots = max($this->studyplan->slots(),$max_slot +1);
// Create the required amount of slots
for($i=0; $i < $num_slots+1; $i++){
if($i > 0) {
$slots = [self::SLOTSET_COMPETENCY => [], self::SLOTSET_FILTER => []];
} else {
$slots = [self::SLOTSET_FILTER => []];
}
$model['slots'][$i] = $slots;
}
$children = studyitem::find_studyline_children($this);
foreach($children as $c)
{
if($c->isValid()){
$slotset = null;
if($c->slot() > 0) {
if(in_array($c->type(),self::COMPETENCY_TYPES)) {
$slotset = self::SLOTSET_COMPETENCY;
} else if(in_array($c->type(),self::FILTER_TYPES)) {
$slotset = self::SLOTSET_FILTER;
}
}
else if(in_array($c->type(),self::FILTER0_TYPES)) {
$slotset = self::SLOTSET_FILTER;
}
if(isset($slotset)) {
$model['slots'][$c->slot()][$slotset][] = $c->user_model($userid);
}
}
}
return $model;
}
public function duplicate($new_studyplan,&$translation){
global $DB;
// clone the database fields
$fields = clone $this->r;
// set new studyplan id
unset($fields->id);
$fields->studyplan_id = $new_studyplan->id();
// create new record with the new data
$id = $DB->insert_record(self::TABLE, (array)$fields);
$new = self::findById($id);
// Next copy all the study items for this studyline
// and record the original and copy id's in the $translation array
// so the calling function can connect the new studyitems as required
$children = studyitem::find_studyline_children($this);
$translation = [];
foreach($children as $c)
{
$newchild = $c->duplicate($this);
$translation[$c->id()] = $newchild->id();
}
return $new;
}
public function export_model()
{
return $this->generate_model("export");
}
public function import_studyitems($model,&$itemtranslation,&$connections){
global $DB;
foreach($model as $slot=>$slotmodel)
{
$courselayer = 0;
$filterlayer = 0;
foreach($slotmodel as $itemmodel)
{
if($itemmodel["type"] == "course" || $itemmodel["type"] == "competency"){
$itemmodel["layer"] = $courselayer;
$courselayer++;
}else {
$itemmodel["layer"] = $filterlayer;
$filterlayer++;
}
$itemmodel["slot"] = $slot;
$itemmodel["line_id"] = $this->id();
$item = studyitem::import_item($itemmodel);
if(!empty($item)){
$itemtranslation[$itemmodel["id"]] = $item->id();
if(count($itemmodel["connections"]) > 0){
if(! isset($connections[$item->id()]) || ! is_array($connections[$item->id()])){
$connections[$item->id()] = [];
}
foreach($itemmodel["connections"] as $to_id){
$connections[$item->id()][] = $to_id;
}
}
}
}
}
}
}

718
classes/studyplan.php Normal file
View File

@ -0,0 +1,718 @@
<?php
namespace local_treestudyplan;
require_once($CFG->libdir.'/externallib.php');
class studyplan {
const TABLE = "local_treestudyplan";
private static $STUDYPLAN_CACHE = [];
private $r; // Holds database record
private $id;
private $aggregator;
private $context = null; // Hold context object once retrieved
public function getAggregator(){
return $this->aggregator;
}
// Cache constructors to avoid multiple creation events in one session.
public static function findById($id): self {
if(!array_key_exists($id,self::$STUDYPLAN_CACHE)){
self::$STUDYPLAN_CACHE[$id] = new self($id);
}
return self::$STUDYPLAN_CACHE[$id];
}
private function __construct($id) {
global $DB;
$this->id = $id;
$this->r = $DB->get_record(self::TABLE,['id' => $id]);
$this->aggregator = aggregator::createOrDefault($this->r->aggregation, $this->r->aggregation_config);
}
public function id(){
return $this->id;
}
public function slots(){
return $this->r->slots;
}
public function name(){
return $this->r->name;
}
public function startdate(){
return new \DateTime($this->r->startdate);
}
/**
* Return the context this studyplan is associated to
*/
public function context(): \context{
if(!isset($this->context)){
try{
$this->context = contextinfo::by_id($this->r->context_id)->context;
}
catch(\dml_missing_record_exception $x){
throw new \InvalidArgumentException("Context {$this->r->context_id} not available"); // Just throw it up again. catch is included here to make sure we know it throws this exception
}
}
return $this->context;
}
public function enddate(){
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"));
}
}
public static function simple_structure($value=VALUE_REQUIRED){
return new \external_single_structure([
"id" => new \external_value(PARAM_INT, 'id of studyplan'),
"name" => new \external_value(PARAM_TEXT, 'name of studyplan'),
"shortname"=> new \external_value(PARAM_TEXT, 'shortname of studyplan'),
"slots" => new \external_value(PARAM_INT, 'number of slots in studyplan'),
"context_id" => new \external_value(PARAM_INT, 'context_id of studyplan'),
"description"=> new \external_value(PARAM_TEXT, 'description of studyplan'),
"startdate" => new \external_value(PARAM_TEXT, 'start date of studyplan'),
"enddate" => new \external_value(PARAM_TEXT, 'end date of studyplan'),
"aggregation" => new \external_value(PARAM_TEXT, 'selected aggregator'),
"aggregation_config" => new \external_value(PARAM_TEXT, 'config string for aggregator'),
"aggregation_info" => aggregator::basic_structure(),
],'Basic studyplan info',$value);
}
public function simple_model(){
return [
'id' => $this->r->id,
'name' => $this->r->name,
'shortname' => $this->r->shortname,
'slots' => $this->r->slots,
'context_id' => $this->context()->id,
'description' => $this->r->description,
'startdate' => $this->r->startdate,
'enddate' => $this->r->enddate,
"aggregation" => $this->r->aggregation,
"aggregation_config" => $this->aggregator->config_string(),
'aggregation_info' => $this->aggregator->basic_model(),
];
}
public static function editor_structure($value=VALUE_REQUIRED){
return new \external_single_structure([
"id" => new \external_value(PARAM_INT, 'id of studyplan'),
"name" => new \external_value(PARAM_TEXT, 'name of studyplan'),
"shortname"=> new \external_value(PARAM_TEXT, 'shortname of studyplan'),
"description"=> new \external_value(PARAM_TEXT, 'description of studyplan'),
"slots" => new \external_value(PARAM_INT, 'number of slots in studyplan'),
"context_id" => new \external_value(PARAM_INT, 'context_id of studyplan'),
"startdate" => new \external_value(PARAM_TEXT, 'start date of studyplan'),
"enddate" => new \external_value(PARAM_TEXT, 'end date of studyplan'),
"aggregation" => new \external_value(PARAM_TEXT, 'selected aggregator'),
"aggregation_config" => new \external_value(PARAM_TEXT, 'config string for aggregator'),
"aggregation_info" => aggregator::basic_structure(),
/*"association" => new \external_single_structure([
'cohorts' => new \external_multiple_structure(associationservice::cohort_structure()),
'users' => new \external_multiple_structure(associationservice::user_structure()),
]),*/
"studylines" => new \external_multiple_structure(studyline::editor_structure()),
"advanced" => new \external_single_structure([
"force_scales" => new \external_single_structure([
"scales" => new \external_multiple_structure(new \external_single_structure([
"id" => new \external_value(PARAM_INT, 'id of scale'),
"name" => new \external_value(PARAM_TEXT, 'name of scale'),
])),
],"Scale forcing on stuff", VALUE_OPTIONAL),
],"Advanced features available", VALUE_OPTIONAL),
],'Studyplan full structure',$value);
}
public function editor_model(){
global $DB;
$model = [
'id' => $this->r->id,
'name' => $this->r->name,
'shortname' => $this->r->shortname,
'description' => $this->r->description,
'slots' => $this->r->slots,
'context_id' => $this->context()->id,
'startdate' => $this->r->startdate,
'enddate' => $this->r->enddate,
"aggregation" => $this->r->aggregation,
"aggregation_config" => $this->aggregator->config_string(),
'aggregation_info' => $this->aggregator->basic_model(),
/*'association' => [
'cohorts' => associationservice::associated_cohorts($this->r->id),
'users' => associationservice::associated_users($this->r->id),
],*/
'studylines' => [],
];
$children = studyline::find_studyplan_children($this);
foreach($children as $c)
{
$model['studylines'][] = $c->editor_model();
}
if(has_capability('local/treestudyplan:forcescales', \context_system::instance())){
if(!array_key_exists('advanced',$model)){
// Create advanced node if it does not exist
$model['advanced'] = [];
}
// get a list of available scales
$scales = array_map( function($scale){
return [ "id" => $scale->id, "name" => $scale->name,];
}, \grade_scale::fetch_all(array('courseid'=>0)) ) ;
$model['advanced']['force_scales'] = [
'scales' => $scales,
];
}
return $model;
}
public static function add($fields){
global $CFG, $DB;
$addable = ['name','shortname','description','context_id','slots','startdate','enddate','aggregation','aggregation_config'];
$info = ['enddate' => null ];
foreach($addable as $f){
if(array_key_exists($f,$fields)){
$info[$f] = $fields[$f];
}
}
$id = $DB->insert_record(self::TABLE, $info);
return self::findById($id); // make sure the new studyplan is immediately cached
}
public function edit($fields){
global $DB;
$editable = ['name','shortname','description','context_id','slots','startdate','enddate','aggregation','aggregation_config'];
$info = ['id' => $this->id,];
foreach($editable as $f){
if(array_key_exists($f,$fields)){
$info[$f] = $fields[$f];
}
}
$DB->update_record(self::TABLE, $info);
//reload record after edit
$this->r = $DB->get_record(self::TABLE,['id' => $this->id],"*",MUST_EXIST);
//reload the context...
$this->context = null;
$this->context();
// reload aggregator
$this->aggregator = aggregator::createOrDefault($this->r->aggregation, $this->r->aggregation_config);
return $this;
}
public function delete($force=false){
global $DB;
if($force){
$children = studyline::find_studyplan_children($this);
foreach($children as $c){
$c->delete($force);
}
}
if($DB->count_records('local_treestudyplan_line',['studyplan_id' => $this->id]) > 0){
return success::fail('cannot delete studyplan that still has studylines');
}
else
{
$DB->delete_records('local_treestudyplan', ['id' => $this->id]);
return success::success();
}
}
public static function find_all($contextid=1){
global $DB, $USER;
$list = [];
if($contextid <= 1){
$contextid = 1;
$where = "context_id <= :contextid OR context_id IS NULL";
} else {
$where = "context_id = :contextid";
}
$ids = $DB->get_fieldset_select(self::TABLE,"id",$where,["contextid" => $contextid]);
foreach($ids as $id)
{
$list[] = studyplan::findById($id);
}
return $list;
}
public static function find_by_shortname($shortname, $contextid = 0): array{
global $DB;
$list = [];
$where = "shortname = :shortname AND context_id = :contextid";
if($contextid == 0){
$where .= "OR context_id IS NULL";
}
$ids = $DB->get_fieldset_select(self::TABLE,"id",$where,["shortname"=>$shortname, "contextid" => $contextid]);
foreach($ids as $id)
{
$list[] = studyplan::findById($id);
}
return $list;
}
public static function find_for_user($userid)
{
global $DB;
$sql = "SELECT s.id FROM {local_treestudyplan} s
INNER JOIN {local_treestudyplan_cohort} j ON j.studyplan_id = s.id
INNER JOIN {cohort_members} cm ON j.cohort_id = cm.cohortid
WHERE cm.userid = :userid";
$cohort_plan_ids = $DB->get_fieldset_sql($sql, ['userid' => $userid]);
$sql = "SELECT s.id FROM {local_treestudyplan} s
INNER JOIN {local_treestudyplan_user} j ON j.studyplan_id = s.id
WHERE j.user_id = :userid";
$user_plan_ids = $DB->get_fieldset_sql($sql, ['userid' => $userid]);
$plans = [];
foreach($cohort_plan_ids as $id) {
$plans[$id] = self::findById($id);
}
foreach($user_plan_ids as $id) {
if(!array_key_exists($id,$plans)){
$plans[$id] = self::findById($id);
}
}
return $plans;
}
/**
* Find The active studyplans where the specified user is a teacher
* (Has the mod/assign::grade capability in one of the linked courses)
* TODO: OPTIMIZE THIS CHECK!!!
*/
public static function find_teaching($userid){
global $DB;
$list = [];
// First find all active study plans
$sql = "SELECT s.id FROM {local_treestudyplan} s
WHERE startdate <= NOW() and enddate >= NOW()";
$plan_ids = $DB->get_fieldset_sql($sql, []);
foreach($plan_ids as $planid) {
$sql = "SELECT i.course_id FROM mdl_local_treestudyplan_item i
INNER JOIN mdl_local_treestudyplan_line l ON i.line_id = l.id
WHERE l.studyplan_id = :plan_id AND i.course_id IS NOT NULL";
$course_ids = $DB->get_fieldset_sql($sql, ["plan_id" => $planid]);
$linked = false;
foreach($course_ids as $cid){
$coursecontext = \context_course::instance($cid);
if (is_enrolled($coursecontext, $userid, 'mod/assign:grade')){
$linked = true;
break; // No need to search further
}
}
if($linked)
{
$list[$planid] = self::findById($planid);
}
}
return $list;
}
static public function exist_for_user($userid)
{
global $DB;
$count = 0;
$sql = "SELECT s.* FROM {local_treestudyplan} s
INNER JOIN {local_treestudyplan_cohort} j ON j.studyplan_id = s.id
INNER JOIN {cohort_members} cm ON j.cohort_id = cm.cohortid
WHERE cm.userid = :userid";
$count += $DB->count_records_sql($sql, ['userid' => $userid]);
$sql = "SELECT s.* FROM {local_treestudyplan} s
INNER JOIN {local_treestudyplan_user} j ON j.studyplan_id = s.id
WHERE j.user_id = :userid";
$count += $DB->count_records_sql($sql, ['userid' => $userid]);
return ($count > 0);
}
/**
* Retrieve the users linked to this studyplan.
* @return array of User objects
*/
public function find_linked_users(){
global $DB;
$users = [];
$uids = $this->find_linked_userids();
foreach($uids as $uid){
$users[] = $DB->get_record("user",["id"=>$uid]);
}
return $users;
}
/**
* Retrieve the user id's of the users linked to this studyplan.
* @return array of int (User Id)
*/
private function find_linked_userids(): array {
global $DB;
$uids = [];
// First get directly linked userids
$sql = "SELECT j.user_id FROM {local_treestudyplan_user} j
WHERE j.studyplan_id = :planid";
$ulist = $DB->get_fieldset_sql($sql, ['planid' => $this->id]);
$uids = array_merge($uids,$ulist);
foreach($ulist as $uid){
$users[] = $DB->get_record("user",["id"=>$uid]);
}
// Next het users linked though cohort
$sql = "SELECT cm.userid FROM {local_treestudyplan_cohort} j
INNER JOIN {cohort_members} cm ON j.cohort_id = cm.cohortid
WHERE j.studyplan_id = :planid";
$ulist = $DB->get_fieldset_sql($sql, ['planid' => $this->id]);
$uids = array_merge($uids,$ulist);
return array_unique($uids);
}
/** Check if this studyplan is linked to a particular user
* @param bool|stdClass $user The userid or user record of the user
*/
public function has_linked_user($user){
if(is_int($user)){
$userid = $user;
} else {
$userid = $user->id;
}
$uids = $this->find_linked_userids();
if(in_array($userid,$uids)){
return true;
} else {
return false;
}
}
public static function user_structure($value=VALUE_REQUIRED){
return new \external_single_structure([
"id" => new \external_value(PARAM_INT, 'id of studyplan'),
"name" => new \external_value(PARAM_TEXT, 'name of studyplan'),
"shortname"=> new \external_value(PARAM_TEXT, 'shortname of studyplan'),
"description"=> new \external_value(PARAM_TEXT, 'description of studyplan'),
"slots" => new \external_value(PARAM_INT, 'number of slots in studyplan'),
"startdate" => new \external_value(PARAM_TEXT, 'start date of studyplan'),
"enddate" => new \external_value(PARAM_TEXT, 'end date of studyplan'),
"studylines" => new \external_multiple_structure(studyline::user_structure()),
"aggregation_info" => aggregator::basic_structure(),
],'Studyplan with user info',$value);
}
public function user_model($userid){
$model = [
'id' => $this->r->id,
'name' => $this->r->name,
'shortname' => $this->r->shortname,
'description' => $this->r->description,
'slots' => $this->r->slots,
'startdate' => $this->r->startdate,
'enddate' => $this->r->enddate,
'studylines' => [],
'aggregation_info' => $this->aggregator->basic_model(),
];
$children = studyline::find_studyplan_children($this);
foreach($children as $c)
{
$model['studylines'][] = $c->user_model($userid);
}
return $model;
}
public static function duplicate_plan($plan_id,$name,$shortname)
{
$ori = self::findById($plan_id);
$new = $ori->duplicate($name,$shortname);
return $new->simple_model();
}
public function duplicate($name,$shortname)
{
// First duplicate the studyplan structure
$new =studyplan::add([
'name' => $name,
'shortname' => $shortname,
'description' => $this->r->description,
'slots' => $this->r->slots,
'startdate' => $this->r->startdate,
'enddate' => empty($this->r->enddate)?null:$this->r->enddate,
]);
// next, copy the studylines
$children = studyline::find_studyplan_children($this);
$itemtranslation = [];
$linetranslation = [];
foreach($children as $c){
$newchild = $c->duplicate($this,$itemtranslation);
$linetranslation[$c->id()] = $newchild->id();
}
// now the itemtranslation array contains all of the old child id's as keys and all of the related new ids as values
// (feature of the studyline::duplicate function)
// use this to recreate the lines in the new plan
foreach(array_keys($itemtranslation) as $item_id){
// copy based on the outgoing connections of each item, to avoid duplicates
$connections = studyitemconnection::find_outgoing($item_id);
foreach($connections as $conn){
studyitemconnection::connect($itemtranslation[$conn->from_id],$itemtranslation[$conn->to_id]);
}
}
return $new;
}
public static function export_structure()
{
return new \external_single_structure([
"format" => new \external_value(PARAM_TEXT, 'format of studyplan export'),
"content"=> new \external_value(PARAM_TEXT, 'exported studyplan content'),
],'Exported studyplan');
}
public function export_plan()
{
$model = $this->export_model();
$json = json_encode([
"type"=>"studyplan",
"version"=>1.0,
"studyplan"=>$model
],\JSON_PRETTY_PRINT);
return [ "format" => "application/json", "content" => $json];
}
public function export_plan_csv()
{
$model = $this->editor_model();
$slots = intval($model["slots"]);
// First line
$csv = "\"Studyline[{$slots}]\"";
for($i = 1; $i <= $slots; $i++){
$csv .= ",\"P{$i}\"";
}
$csv .= "\r\n";
// next, make one line per studyline
foreach($model["studylines"] as $line){
// determine how many fields are simultaneous in the line at maximum
$maxlines = 1;
for($i = 1; $i <= $slots; $i++){
if(count($line["slots"]) > $i){
$ct = 0;
foreach($line["slots"][$i][studyline::SLOTSET_COMPETENCY] as $itm){
if($itm["type"] == "course"){
$ct += 1;
}
}
if($ct > $maxlines){
$maxlines = $ct;
}
}
}
for($lct = 0; $lct < $maxlines; $lct++){
$csv .= "\"{$line["name"]}\"";
for($i = 1; $i <= $slots; $i++){
$filled = false;
if(count($line["slots"]) > $i){
$ct = 0;
foreach($line["slots"][$i][studyline::SLOTSET_COMPETENCY] as $itm){
if($itm["type"] == "course"){
if($ct == $lct){
$csv .= ",\"";
$csv .= $itm["course"]["fullname"];
$csv .= "\r\n";
$first = true;
foreach($itm["course"]["grades"] as $g){
if($g["selected"]){
if($first){
$first = false;
}
else{
$csv .= "\r\n";
}
$csv .= "- ".str_replace('"', '\'', $g["name"]);
}
}
$csv .= "\"";
$filled = true;
break;
}
$ct++;
}
}
}
if(!$filled) {
$csv .= ",\"\"";
}
}
$csv .= "\r\n";
}
}
return [ "format" => "text/csv", "content" => $csv];
}
public function export_studylines(){
$model = $this->export_studylines_model();
$json = json_encode([
"type"=>"studylines",
"version"=>1.0,
"studylines"=>$model,
],\JSON_PRETTY_PRINT);
return [ "format" => "application/json", "content" => $json];
}
public function export_model()
{
$model = [
'name' => $this->r->name,
'shortname' => $this->r->shortname,
'description' => $this->r->description,
'slots' => $this->r->slots,
'startdate' => $this->r->startdate,
'enddate' => $this->r->enddate,
"aggregation" => $this->r->aggregation,
"aggregation_config" => json_decode($this->aggregator->config_string()),
'aggregation_info' => $this->aggregator->basic_model(),
'studylines' => $this->export_studylines_model(),
];
return $model;
}
public function export_studylines_model()
{
$children = studyline::find_studyplan_children($this);
$lines = [];
foreach($children as $c)
{
$lines[] = $c->export_model();
}
return $lines;
}
public static function import_studyplan($content,$format="application/json")
{
if($format != "application/json") { return false;}
$content = json_decode($content,true);
if($content["type"] == "studyplan" && $content["version"] >= 1.0){
// Make sure the aggregation_config is re-encoded as json text
$content["studyplan"]["aggregation_config"] = json_encode($content["studyplan"]["aggregation_config"]);
$plan = self::add($content["studyplan"]);
return $plan->import_studylines_model($content["studyplan"]["studylines"]);
}
else {
error_log("Invalid format and type: {$content['type']} version {$content['version']}");
return false;
}
}
public function import_studylines($content,$format="application/json")
{
if($format != "application/json") { return false;}
$content = json_decode($content,true);
if($content["type"] == "studylines" && $content["version"] >= 1.0){
return $this->import_studylines_model($content["studylines"]);
}
else if($content["type"] == "studyplan" && $content["version"] >= 1.0){
return $this->import_studylines_model($content["studyplan"]["studylines"]);
}
else {
return false;
}
}
protected function find_studyline_by_shortname($shortname){
$children = studyline::find_studyplan_children($this);
foreach($children as $l){
if($shortname == $l->shortname()){
return $l;
}
}
return null;
}
protected function import_studylines_model($model)
{
// First attempt to map each studyline model to an existing or new line
$line_map = [];
foreach($model as $ix => $linemodel){
$line = $this->find_studyline_by_shortname($linemodel["shortname"]);
if(empty($line)){
$linemodel["studyplan_id"] = $this->id;
$line = studyline::add($linemodel);
} else {
//$line->edit($linemodel); // Update the line with the settings from the imported file
}
$line_map[$ix] = $line;
}
// next, let each study line import the study items
$itemtranslation = [];
$connections = [];
foreach($model as $ix => $linemodel){
$line_map[$ix]->import_studyitems($linemodel["slots"],$itemtranslation,$connections);
}
// Finally, create the links between the study items
foreach($connections as $from => $dests){
foreach($dests as $to){
studyitemconnection::connect($from,$itemtranslation[$to]);
}
}
return true;
}
}

1167
classes/studyplanservice.php Normal file

File diff suppressed because it is too large Load Diff

45
classes/success.php Normal file
View File

@ -0,0 +1,45 @@
<?php
namespace local_treestudyplan;
class success {
private $success;
private $msg;
public static function success($msg=""){
return new self(true,$msg);
}
public static function fail($msg=""){
return new self(false,$msg);
}
public function __construct($success,$msg){
$this->success = ($success)?true:false;
$this->msg = $msg;
}
public static function structure()
{
return new \external_single_structure([
"success" => new \external_value(PARAM_BOOL, 'operation completed succesfully'),
"msg" => new \external_value(PARAM_TEXT, 'message'),
]);
}
public function model() {
return ["success" => $this->success, "msg"=> $this->msg];
}
public function successful(){
return $this->success;
}
public function msg(){
return $this->msg;
}
}

137
classes/task/deepcopy.php Normal file
View File

@ -0,0 +1,137 @@
<?php
namespace local_treestudyplan\task;
require_once($CFG->dirroot.'/course/externallib.php');
class deepcopy extends \core\task\adhoc_task {
public const TABLE = "local_treestudyplan_item";
private $id;
public function __construct(){
}
public static function create($studyplan_id,$params){
global $USER, $DB;
$recordid = $DB->insert_record(self::TABLE, [
'studyplan_id' => $studyplan_id,
'status' => 'pending',
'completion' => 0,
'parameters' => \json_encode($params),
]);
$task = new self();
$task->set_custom_data((object)[ // cast to object to avoid confusion below (data encoded as JSON which always returns object on dictionary array)
'recordid' => $recordid,
]);
$task->set_userid($USER->id);
return $task;
}
/**
* Return the task's name as shown in admin screens.
*
* @return string
*/
public function get_name() {
return get_string('task_process_request', 'report_downloadall');
}
protected function setStatus($status,$completion){
global $DB;
if(!empty($this->id)){
$DB->update_record(self::TABLE, ['id' => $this->id,"status"=>$status,"completion" => $completion]);
}
}
public static function findStatus($studyplan_id){
global $DB;
$records = $DB->get_records(self::TABLE,['studyplan_id' => $studyplan_id]);
$list = [];
return $list;
}
/**
* Execute the task.
*/
public function execute() {
global $DB;
$data = $this->get_custom_data(); // returns array data as object, because JSON
$recordid = $data->recordid;
$this->id = $recordid;
$record = $DB->get_record(self::TABLe,['id' => $recordid]);
$params = \json_decode();
$this->setStatus('copying',0);
// gather the study items that need to be converted
$studyitems = [];
$studyplan = studyplan::findById($record->studyplan_id);
foreach(studyline::find_studyplan_children($studyplan) as $line){
foreach(studyitem::find_studyline_children($line) as $item){
if($item->type() == studyitem::COURSE && $item->isValid()){
// queue this studyitem and related study line (for extra info)
$studyitems[] = [$item,$line];
}
}
}
// Make sure we can calculate progress
$count = count($studyitems);
for($i=0;$i<$count;$i++){
$progress = 100.0 * (floatval($i) / floatval($count));
$this->setStatus('copying',$progress);
[$old, $line] = $studyitems[$i];
$old_ci = $old->getcourseinfo();
$old_course = $old_ci->course();
// perform a copy of the course
/**
* Duplicate a course
*
* @param int $courseid
* @param string $fullname Duplicated course fullname
* @param string $shortname Duplicated course shortname
* @param int $categoryid Duplicated course parent category id
* @param int $visible Duplicated course availability
* @param array $options List of backup options
* @return array New course info
* @since Moodle 2.3
*/
// public static function duplicate_course($courseid, $fullname, $shortname, $categoryid, $visible = 1, $options = array());
if(!array_key_exists($old_course->id))
{
$new_fullname = $old_course->fullname;
$new_shortname = $old_course->shortname;
$new_categoryid = $old_course->category; // We likely don't want this in the same cetegory, but in a new one for the next school year
// TODO: select new category, and optionally include
// TODO: generate new shortname and verify it is unique
[$new_course_id,] = \core_course_external::duplicate_course($old_course->id,$new_fullname,$new_shortname,$new_categoryid,true,[
'enrolments' => 0,
]);
$duplicationmap[$old_course->id] = $new_course_id;
}
else {
$new_course_id = $duplicationmap[$old_course->id];
}
// retrieve the course
$new_course = \get_course($new_course_id);
// now, update the timing of the course, based on the provided parameters
// TODO: Make sure that the timing of courses included in multiple slots, is updated to match the start and end of alle those slots.
}
$this->setStatus('done',$progress);
}
}

78
cli/prime_students.php Normal file
View File

@ -0,0 +1,78 @@
<?php
namespace local_treestudyplan;
use \local_treestudyplan\local\gradegenerator;
define('CLI_SCRIPT', true);
require(__DIR__ . '/../../../config.php');
require_once($CFG->libdir . '/clilib.php');
$usage = "Prime the generator with random stats for all relevant students
Usage:
# php prime_students.php --studyplan=<shortname> [--file|-f=userfile]
# php prime_students.php --all|-a [--file|-f=userfile]
# php prime_students.php --help|-h
Options:
-h --help Print this help.
--file=<value> Specify file name to store generated user specs in.
";
list($options, $unrecognised) = cli_get_params([
'help' => false,
'studyplan' => null,
'all' => false,
'file' => "/tmp/generategrades.json"
], [
'h' => 'help',
's' => 'studyplan',
'a' => 'all',
'f' => 'file',
]);
if ($unrecognised) {
$unrecognised = implode(PHP_EOL . ' ', $unrecognised);
cli_error(get_string('cliunknowoption', 'core_admin', $unrecognised));
}
if ($options['help']) {
cli_writeln($usage);
exit(2);
}
//cli_writeln(print_r($options,true));
if (empty($options['studyplan']) && empty($options["all"])) {
cli_error('Missing mandatory argument studyplan.', 2);
}
if(!empty($options["all"])){
$plans = studyplan::find_all();
} else {
$plans = studyplan::find_by_shortname($options["studyplan"]);
}
$generator = new gradegenerator();
$generator->fromFile($options["file"]);
cli_writeln(count($plans)." studyplans found:");
foreach($plans as $plan){
cli_heading($plan->name());
$users = $plan->find_linked_users();
foreach($users as $u){
$generator->addstudent($u->username);
$generator->addUserNameInfo($u->username,$u->firstname,$u->lastname);
cli_writeln(" - {$u->firstname} {$u->lastname} / {$u->username}");
}
$lines = studyline::find_studyplan_children($plan);
foreach($lines as $line){
cli_writeln(" ** {$line->name()} **");
$items = studyitem::find_studyline_children($line);
foreach($users as $u){
$generator->addskill($u->username,$line->shortname());
}
}
}
$generator->toFile($options["file"]);

167
cli/randomize_grades.php Normal file
View File

@ -0,0 +1,167 @@
<?php
namespace local_treestudyplan;
use \local_treestudyplan\local\gradegenerator;
define('CLI_SCRIPT', true);
require(__DIR__ . '/../../../config.php');
require_once($CFG->libdir . '/clilib.php');
require_once($CFG->dirroot. '/mod/assign/locallib.php');
$usage = "Fill all the gradables in a study plan with random values
Usage:
# php randomize_grades.php --studyplan=<shortname> [--file|-f=userfile]
# php randomize_grades.php --all|-a [--file|-f=userfile]
# php randomize_grades.php --help|-h
Options:
-h --help Print this help.
-f --file=<value> Specify file name to store generated user specs in
-d --dryrun Do not store grades
";
list($options, $unrecognised) = cli_get_params([
'help' => false,
'dryrun' => false,
'studyplan' => null,
'all' => false,
'file' => "/tmp/generategrades.json"
], [
'h' => 'help',
's' => 'studyplan',
'a' => 'all',
'f' => 'file',
'd' => 'dryrun'
]);
if ($unrecognised) {
$unrecognised = implode(PHP_EOL . ' ', $unrecognised);
cli_error(get_string('cliunknowoption', 'core_admin', $unrecognised));
}
if ($options['help']) {
cli_writeln($usage);
exit(2);
}
/////////////////////////////////
$user = get_admin();
if (!$user) {
cli_error("Unable to find admin user in DB.");
}
$auth = empty($user->auth) ? 'manual' : $user->auth;
if ($auth == 'nologin' or !is_enabled_auth($auth)) {
cli_error(sprintf("User authentication is either 'nologin' or disabled. Check Moodle authentication method for '%s'",
$user->username));
}
$authplugin = get_auth_plugin($auth);
$authplugin->sync_roles($user);
login_attempt_valid($user);
complete_user_login($user);
////////////////////////////////
if (empty($options['studyplan']) && empty($options["all"])) {
cli_error('Missing mandatory argument studyplan.', 2);
}
if(!empty($options["all"])){
$plans = studyplan::find_all();
} else {
$plans = studyplan::find_by_shortname($options["studyplan"]);
}
$generator = new gradegenerator();
$generator->fromFile($options["file"]);
$assignments = [];
cli_writeln(count($plans)." studyplans found:");
foreach($plans as $plan){
cli_heading($plan->name());
$users = $plan->find_linked_users();
$lines = studyline::find_studyplan_children($plan);
foreach($lines as $line){
cli_writeln(" ** {$line->name()} **");
$items = studyitem::find_studyline_children($line);
foreach($items as $item){
if($item->type() == studyitem::COURSE) { // only handle courses for now
$courseinfo = $item->getcourseinfo();
cli_writeln(" # {$courseinfo->shortname()}");
if($courseinfo->course()->startdate <= time()){
foreach($users as $u){
cli_writeln(" -> {$u->firstname} {$u->lastname} <-");
$gradables = gradeinfo::list_studyitem_gradables($item);
$gen = $generator->generate($u->username,$line->shortname(),$gradables);
foreach($gen as $gg){
$g = $gg->gi;
$gi = $g->getGradeitem();
$name = $gi->itemname;
$grade = $gg->gradetext;
cli_write (" - {$name} = {$grade}");
// Check if the item is alreaady graded for this user
$existing = $count = $DB->count_records_select('grade_grades','itemid = :gradeitemid AND finalgrade IS NOT NULL and userid = :userid',
['gradeitemid' => $gi->id, 'userid' => $u->id]);
if(!$existing){
if($gg->grade > 0){
if($gi->itemmodule == "assign"){
// If it is an assignment, submit though that interface
list($c,$cminfo) = get_course_and_cm_from_instance($gi->iteminstance,$gi->itemmodule);
$cm_ctx = \context_module::instance($cminfo->id);
$a = new \assign($cm_ctx,$cminfo,$c);
$ug = $a->get_user_grade($u->id,true);
$ug->grade = grade_floatval($gg->grade);
$ug->grader = $USER->id;
$ug->feedbacktext = nl2br( htmlspecialchars($gg->fb));
$ug->feedbackformat = FORMAT_HTML;
//print_r($ug);
if(!$options["dryrun"]){
$a->update_grade($ug);
grade_regrade_final_grades($c->id,$u->id,$gi);
cli_writeln(" ... Stored");
} else {
cli_writeln(" ... (Dry Run)");
}
} else {
// Otherwise, set the grade through the manual grading override
cli_writeln(" ... Cannot store");
}
} else {
cli_writeln(" ... No grade");
}
} else {
cli_writeln(" ... Already graded");
}
}
}
}
else
{
cli_writeln(" Skipping since it has not started yet");
}
}
}
}
}
$generator->toFile($options["file"]);

4
css/bootstrap-vue.min.css vendored Normal file

File diff suppressed because one or more lines are too long

955
css/devstyles.css Normal file
View File

@ -0,0 +1,955 @@
/* stylelint-disable length-zero-no-unit, color-hex-case, color-hex-length, no-eol-whitespace, unit-blacklist, block-no-empty */
.t-toolbox-preface {
margin: 10px;
}
.t-studyplan-container {
margin-top: 16px;
min-height: 500px;
}
ul.dropdown-menu.show {
background-color: white;
}
.t-studyline {
width: min-content;
display: grid;
grid-auto-flow: column;
/*border-bottom-style: solid;*/
border-right-style: solid;
border-color: #cccccc;
border-width: 1px;
}
.t-studyline:first-child {
border-top-style: solid;
}
.t-studyline .controlbox {
border-left-style: solid;
border-color: #cccccc;
border-width: 1px;
white-space: nowrap;
}
.t-studyline .control {
display: inline-block;
width: 24px;
text-align: center;
padding-top: 5px;
}
.t-studyline-editmode-content {
min-width: 450px;
max-width: 700px;
display: flex;
flex-direction: row;
justify-content: center;
}
.t-studyplan-controlbox {
height: 30px;
}
.t-studyplan-controlbox .control {
float: right;
margin-left: 10px;
margin-right: 5px;
}
.t-studyline-drag {
display: inline;
}
.t-studyline-add {
margin-top: 0.5em;
margin-bottom: 1em;
}
.t-studyline-title {
padding-top: 5px;
padding-left: 10px;
width: 150px;
white-space: nowrap;
border-color: rgba(0, 0, 0, 0.125);
border-width: 1px;
border-left-style: solid;
border-right-style: solid;
display: flex;
flex-direction: column;
justify-content: center;
}
.t-studyline-title abbr {
display: inline-block;
vertical-align: middle;
font-weight: bold;
font-style: italic;
}
svg.empty-slot circle {
fill: transparent;
stroke: #ccc;
stroke-width: 4px;
stroke-opacity: 0.5;
stroke-dasharray: 4 4;
}
ul.t-item-module-children,
ul.t-coursecat-list li,
ul.t-course-list li {
list-style: none;
padding-left: 0;
}
li.t-item-course-gradeinfo {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
span.t-item-course-chk-lbl {
font-size: 0.7em;
display: inline-block;
width: 4em;
}
li.t-item-course-gradeinfo img {
vertical-align: top;
top: 3px;
position: relative;
max-width: 24px;
max-height: 24px;
}
i.t-coursecat-list-item {
color: blue;
}
i.t-course-list-item {
color: green;
}
ul.t-competency-list li {
list-style: none;
}
.t-competency-heading .draggable-competency {
display: inline-block;
}
.t-competency-heading .disabled-competency {
text-decoration: line-through;
}
.t-competency-heading .competency-info {
color: #aaa;
}
.t-competency-heading i.t-goal {
color: #aaa;
}
.t-competency-heading i.t-module {
color: green;
}
.t-competency-heading i.t-domain {
color: blueviolet;
}
.t-framework-heading i {
color: blue;
}
.t-framework-heading {
font-weight: bold;
}
.collapsed > .when-open,
.not-collapsed > .when-closed {
display: none;
}
.t-studyline-slot {
width: 130px;
}
.t-studyline-slot.t-studyline-slot-0 {
width: 75px;
}
.t-studyline-slot.t-studyline-slot-0 .t-slot-droplist.filter .t-slot-item {
margin-left: 10px;
}
.t-slot-droplist {
min-height: 32px;
height: 100%;
min-width: 50px;
display: flex;
flex-direction: column;
align-content: center;
justify-content: center;
}
.t-slot-droplist.competency {
min-width: 100px;
}
.t-slot-droplist.filter {
min-width: 50px;
}
.t-drop-slot:not(.drop-allowed) {
display: none;
}
.t-drop-slot.drop-allowed i {
color: #ccc;
}
.t-drop-slot.drop-allowed.drop-in i {
color: #333;
}
.t-slot-item.feedback {
border: 2px dashed black;
border-radius: 3px;
}
.t-slot-item.drag-mode-copy {
color: green;
font-weight: bold;
}
.t-slot-item.drag-mode-cut {
color: red;
font-weight: bold;
}
.t-slot-item.drag-mode-reordering {
color: blueviolet;
font-weight: bold;
}
.t-item-deletebox {
display: inline-block;
width: 100px;
text-align: center;
visibility: hidden;
}
.t-item-deletebox.drop-allowed {
visibility: visible;
border-width: 1px;
border-style: dashed;
color: #f77;
}
.t-item-deletebox.drop-in {
visibility: visible;
border-style: solid;
background-color: #FFCCCC;
color: #a00;
}
.modal-dialog .modal-content {
background: white;
}
.modal-dialog.modal-lg {
max-width: 800px;
}
.modal-dialog.modal-sm {
max-width: 300px;
}
.gradable .t-slot-item {
width: 100%;
}
.t-slot-item {
margin-top: 5px;
margin-bottom: 5px;
margin-left: auto;
margin-right: auto;
}
.t-item-base {
position: relative;
}
.t-item-connector-start {
position: absolute;
top: calc(50% - 5px);
right: -1px;
line-height: 0px;
}
.t-item-connector-start svg rect {
cursor: crosshair;
stroke-width: 1px;
stroke: #3c3;
fill: #3c3;
}
.t-item-connector-start.deleteMode svg rect {
stroke: #f70;
fill: #f70;
}
.t-item-connector-end {
position: absolute;
top: 50%;
transform: translate(0, -50%);
left: -1px;
line-height: 0px;
}
.t-item-connector-end svg rect {
stroke-width: 1px;
stroke: #f00;
fill: #f00;
}
.sw-studyline-editmode {
display: inline-block;
}
.t-item-base .deletebox {
position: absolute;
top: 50%;
transform: translate(0, -50%);
right: 5px;
border-radius: 5px;
padding: 3px;
background-color: #fff7;
cursor: default;
border-color: #ccc;
border-width: 1px;
border-style: solid;
z-index: 20;
}
.t-item-base .deletebox a {
display: block;
margin: 3px;
}
.t-item-base .t-item-contextview {
position: absolute;
left: 50%;
transform: translate(-50%, 100%);
bottom: 0px;
z-index: 25;
}
.t-item-contextview .close-button {
float: right;
}
ul.t-toolbox li {
list-style: none;
}
.t-item-filter {
display: inline-block;
width: 1em;
height: 1em;
padding: 0px;
margin: -0.14em;
text-align: left;
font-size: 2em;
}
.t-toolbox .t-item-filter {
font-size: 1em;
}
.t-item-junction i {
color: #eebb00;
}
.t-item-finish i {
color: #009900;
}
.t-item-start i {
color: #009900;
}
.t-item-badge i {
color: #ddaa00;
}
.t-slot-droplist.type-allowed {
border-color: green;
border-style: dashed;
border-width: 1px;
}
.t-slot-droplist.type-allowed.drop-forbidden {
border-color: red;
}
.t-slot-droplist.filter .t-item-base {
display: inline-block;
margin-top: 5px;
margin-bottom: 5px;
margin-left: auto;
margin-right: auto;
line-height: 1px;
}
a.t-item-config {
position: absolute;
top: -5px;
right: -5px;
}
a.t-item-course-config {
font-size: 16pt;
vertical-align: middle;
float: right;
margin-right: 2px;
margin-top: -5px;
}
.t-item-connector-end {
visibility: hidden;
}
.t-item-connector-end.type-allowed.drop-allowed {
visibility: visible;
}
.t-badges li {
list-style: none;
}
.t-badges .t-badge-drag {
display: inline;
}
.t-badges img {
width: 32px;
height: 32px;
}
.t-item-badge {
width: 50px;
height: 50px;
position: relative;
margin-top: 3px;
}
.t-item-badge img.badge-image {
width: 32px;
height: 32px;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.t-item-badge svg.t-badge-backdrop {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.t-item-badge svg.t-badge-backdrop circle {
stroke: black;
stroke-width: 2px;
fill: #ccc;
}
.l-leaderline-linewrapper {
width: 0;
height: 0;
position: relative; /* Origin of coordinates for lines, and scrolled content (i.e. not `absolute`) */
}
/******************************************************************************/
.r-report-tabs .list-group-item-action {
width: inherit;
}
.r-studyplan-content {
overflow-y: visible;
width: min-content;
}
.r-studyplan-tab,
.t-studyplan-tab {
width: auto;
overflow-x: auto;
}
.r-studyline {
width: min-content;
display: grid;
grid-auto-flow: column;
/*border-bottom-style: solid;*/
border-right-style: solid;
border-color: #cccccc;
border-width: 1px;
}
.t-studyline.even,
.r-studyline.odd {
background-color: #f0f0f0;
}
.t-studyline.first,
.r-studyline.first {
border-top-style: solid;
}
.t-studyline.last,
.r-studyline.last {
border-bottom-style: solid;
}
.t-studyline-handle,
.r-studyline-handle {
width: 10px;
height: 100%;
border-left-style: solid;
border-width: 1px;
border-color: rgba(0, 0, 0, 0.125);
}
.r-studyline-title {
padding-top: 5px;
padding-left: 10px;
width: 130px;
flex-shrink: 0;
white-space: nowrap;
border-color: rgba(0, 0, 0, 0.125);
border-width: 1px;
border-left-style: solid;
border-right-style: solid;
display: flex;
flex-direction: column;
justify-content: center;
}
.r-studyline-title abbr {
display: inline-block;
vertical-align: middle;
font-weight: bold;
font-style: italic;
}
.r-studyline-slot {
width: 130px;
min-height: 32px;
min-width: 50px;
display: flex;
flex-shrink: 0;
flex-direction: column;
align-content: center;
justify-content: center;
}
.r-studyline-slot.r-studyline-slot-0 {
width: 75px;
}
.r-studyline-slot.r-studyline-slot-0 .r-item-base {
margin-left: 10px;
}
.r-studyline-slot.competency {
min-width: 100px;
}
.r-studyline-slot.filter {
min-width: 50px;
}
.r-item-base {
margin-top: 5px;
margin-bottom: 5px;
margin-left: auto;
margin-right: auto;
position: relative;
}
.competency .r-item-base {
width: 100%;
}
.t-item-invalid .card-body,
.r-item-invalid .card-body,
.t-item-competency .card-body,
.t-item-course .card-body,
.r-item-competency .card-body {
padding: 3px;
padding-left: 7px;
padding-right: 7px;
}
.r-item-invalid .card-body,
.t-item-invalid .card-body {
color: darkred;
}
.r-item-filter {
display: inline-block;
padding: 0px;
margin: -0.28em;
text-align: left;
font-size: 2em;
}
.r-item-start i {
color: #009900;
}
.r-item-badge i {
color: #ddaa00;
}
.r-badges li {
list-style: none;
}
.r-badges .r-badge-drag {
display: inline;
}
.r-badges img {
width: 32px;
height: 32px;
}
.r-item-badge {
width: 50px;
height: 50px;
position: relative;
margin-top: 3px;
}
.r-item-badge img.badge-image {
width: 32px;
height: 32px;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.r-item-badge svg.r-badge-backdrop {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.r-item-badge svg.r-badge-backdrop circle {
stroke: black;
stroke-width: 2px;
fill: #ccc;
}
.r-studyplan-line-wrapper {
width: 0;
height: 0;
position: relative; /* Origin of coordinates for lines, and scrolled content (i.e. not `absolute`) */
}
.r-item-module-children {
list-style: none;
}
.r-item-start.completion-incomplete i,
i.r-completion-incomplete {
color: #7f7f7f;
}
i.r-completion-progress {
color: rgb(139, 107, 0);
}
i.r-completion-completed, i.r-completion-complete-pass {
color: rgb(0, 126, 0);
}
i.r-completion-good {
color: #398;
}
i.r-completion-excellent, i.r-completion-complete {
color: rgb(0, 103, 255);
}
i.r-completion-pending {
color: #7f7f7f;
}
i.r-completion-failed, i.r-completion-complete-fail {
color: #933;
}
th.r-aggregation-all {
color:rgb(0, 32, 80);
}
th.r-aggregation-any {
color:rgb(0, 46, 0);
}
tr.r-completion-category-header {
border-top-style: solid;
border-top-width: 1px;
border-color:rgb(127, 127, 127);
}
.r-course-grading {
font-size: 16pt;
margin-right: 2px;
vertical-align: bottom;
}
.r-course-graded,
.r-course-result {
font-size: 16pt;
vertical-align: middle;
float: right;
margin-right: 2px;
margin-top: 2px;
}
.r-completion-detail-header {
font-size: 20pt;
}
.r-item-finish.completion-incomplete,
.r-item-junction.completion-incomplete {
color: rgb(127, 127, 127);
}
.r-item-finish.completion-progress,
.r-item-junction.completion-progress {
color: rgb(139, 107, 0);
}
.r-item-finish.completion-completed,
.r-item-junction.completion-completed {
color: rgb(0, 126, 0);
}
.r-item-finish.completion-good,
.r-item-junction.completion-good {
color: #398;
}
.r-item-finish.completion-excellent,
.r-item-junction.completion-excellent {
color: rgb(0, 103, 255);
}
.r-item-finish.completion-failed,
.r-item-junction.completion-failed {
color: #933;
}
.r-activity-icon {
position: relative;
top: -2px;
}
table.r-item-course-grade-details td {
padding-right: 10px;
}
.r-course-detail-header-right {
text-align: end;
}
.r-timing-invalid,
.t-timing-invalid {
color: darkred;
}
.t-timing-past,
.r-timing-past {
color: darkgreen;
}
.t-timing-present,
.r-timing-present {
color: darkblue;
}
.t-timing-future,
.r-timing-future {
color: grey;
}
.t-timing-indicator,
.r-timing-indicator {
border-color: rgba(0, 0, 0, 0.125);
width: 7px;
display: inline-block;
height: 100%;
border-width: 1px;
border-top-left-radius: 3.5px;
border-bottom-left-radius: 3.5px;
}
.t-timing-indicator.timing-invalid,
.r-timing-indicator.timing-invalid {
background-color: #a33;
}
.t-timing-indicator.timing-past,
.r-timing-indicator.timing-past {
background-color: #3a3;
}
.t-timing-indicator.timing-present,
.r-timing-indicator.timing-present {
background-color: #33f;
}
.t-timing-indicator.timing-future,
.r-timing-indicator.timing-future {
background-color: #777;
}
.r-course-am-teacher {
box-shadow: 0 0 3px 3px rgba(255, 224, 0, 0.5);
}
.r-graded-unknown {
color: rgb(139, 107, 0);
}
.r-graded-unsubmitted {
color: #777;
}
.r-graded-ungraded {
color: #a33;
}
.r-graded-allgraded {
color: #35f;
}
.r-graded-graded {
color: #3a3;
}
.r-graded-nogrades {
color: #ddd;
}
.r-grading-bar {
display: inline-block;
white-space: nowrap;
height: min-content;
}
.r-grading-bar-segment {
border-color: #aaa;
border-width: 1px;
display: inline-block;
border-bottom-style: solid;
border-top-style: solid;
}
.r-grading-bar-segment:first-child {
border-bottom-left-radius: 3px;
border-top-left-radius: 3px;
border-left-style: solid;
}
.r-grading-bar-segment:last-child {
border-bottom-right-radius: 3px;
border-top-right-radius: 3px;
border-right-style: solid;
}
.r-grading-bar-unsubmitted {
background-color: #ddd;
}
.r-grading-bar-graded {
background-color: #3a3;
}
.r-grading-bar-ungraded {
background-color: #a33;
}
.card.s-studyplan-card {
min-width: 300px;
max-width: 500px;
margin-bottom: 1em;
}
.card.s-studyplan-card.timing-past .card-header {
background-color: #3a3;
}
.card.s-studyplan-card.timing-present .card-header {
background-color: #33f;
}
.card.s-studyplan-card.timing-future .card-header {
background-color: #777;
}
.s-studyplan-card-title-buttons {
font-size: 12pt;
float: right;
}
.s-studyplan-card-title-buttons > * {
margin-left: 0.2em;
margin-right: 0.3em;
}
.s-studyplan-card-buttons {
float: right;
display: flex;
align-items: center;
justify-content: right;
}
.s-studyplan-card-buttons > * {
margin-left: 1em;
}
.s-studyplan-associate-window .custom-select {
width: 100%;
max-width: 100%;
}
.s-required {
color: #a33;
}
.s-required.completed,
.s-required.good,
.s-required.excellent,
.s-required.allgraded {
color: rgb(0, 126, 0);
}
.s-required.neutral {
color: #aaa;
}
.m-buttonbar {
display: flex;
align-items: center;
justify-content: left;
}
.m-buttonbar a,
.m-buttonbar span,
.m-buttonbar i {
vertical-align: middle;
display: inline;
}
.m-buttonbar a {
margin-right: 1em;
}
.s-edit-mod-form [data-fieldtype=submit] { display: none ! important; }
.s-edit-mod-form.genericonly form > fieldset:not(#id_general) { display: none ! important; }

91
css/vue-hsluv-picker.css Normal file
View File

@ -0,0 +1,91 @@
.vue-hsluv-picker {
min-width: 300px;
display: flex;
flex-direction: column;
}
.vue-hsluv-picker.horizontal {
flex-direction: row;
}
.vue-hsluv-picker.horizontal .display {
margin-top: 10px;
}
.vue-hsluv-picker td.cell-input {
width: 90px;
padding-right: 20px;
}
.vue-hsluv-picker td.cell-input input {
margin: 0;
height: 22px;
background: transparent;
outline: none;
border: 1px solid #333;
border-radius: 0;
text-align: right;
width: 100%;
padding: 0;
}
.vue-hsluv-picker td.cell-input.cell-input-hex input {
font-family: monospace;
border-color: #555;
}
.vue-hsluv-picker table {
margin-top: 20px;
width: 100%;
}
.vue-hsluv-picker table td {
padding: 5px 5px;
vertical-align: top;
border: none;
}
.vue-hsluv-picker table td.picker-label {
color: #eee;
width: 30px;
line-height: 22px;
}
.vue-hsluv-picker table .swatch {
height: 50px;
border-color: #555;
border-style: solid;
border-width: 1px;
}
.vue-hsluv-picker .explanation-text {
margin-bottom: 60px;
margin-top: 100px;
}
.vue-hsluv-picker .range-slider {
height: 22px;
display: block;
position: relative;
border-color: #555;
border-style: solid;
border-width: 1px;
}
.vue-hsluv-picker .range-slider-handle {
display: inline-block;
position: absolute;
width: 6px;
left: -5px;
top: -2px;
height: calc(100% + 4px);
cursor: default;
border: 2px solid #333;
touch-action: pan-y;
-ms-touch-action: pan-y;
background-color: #fff;
}
.vue-hsluv-picker circle.outercircle {
stroke: black;
}

51
db/access.php Normal file
View File

@ -0,0 +1,51 @@
<?php
$capabilities = [
'local/treestudyplan:editstudyplan' => [
'riskbitmask' => RISK_DATALOSS ,
'captype' => 'write',
'contextlevel' => CONTEXT_SYSTEM,
'archetypes' => array(
'manager' => CAP_ALLOW
),
],
'local/treestudyplan:forcescales' => [
'riskbitmask' => RISK_DATALOSS ,
'captype' => 'write',
'contextlevel' => CONTEXT_SYSTEM,
'archetypes' => array(
'manager' => CAP_ALLOW
),
],
'local/treestudyplan:selectowngradables' => [
'riskbitmask' => RISK_DATALOSS ,
'captype' => 'write',
'contextlevel' => CONTEXT_SYSTEM,
'archetypes' => array(
'teacher' => CAP_ALLOW
),
],
'local/treestudyplan:configure' => [
'riskbitmask' => RISK_DATALOSS ,
'captype' => 'write',
'contextlevel' => CONTEXT_SYSTEM,
'archetypes' => array(
'manager' => CAP_ALLOW
),
],
'local/treestudyplan:viewuserreports' => [
'riskbitmask' => RISK_PERSONAL ,
'captype' => 'write',
'contextlevel' => CONTEXT_SYSTEM,
'archetypes' => array(
'manager' => CAP_ALLOW
),
],
];

139
db/install.xml Normal file
View File

@ -0,0 +1,139 @@
<?xml version="1.0" encoding="UTF-8" ?>
<XMLDB PATH="local/treestudyplan/db" VERSION="20210827" COMMENT="XMLDB file for Moodle local/treestudyplan"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="../../../lib/xmldb/xmldb.xsd"
>
<TABLES>
<TABLE NAME="local_treestudyplan_invit" COMMENT="Invitations to view report card">
<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
<FIELD NAME="user_id" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false" COMMENT="ID of user to view report of"/>
<FIELD NAME="name" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="Name or description of invite recipient"/>
<FIELD NAME="email" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="email address the invite was sent to"/>
<FIELD NAME="invitekey" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="Invitation key for this invite"/>
<FIELD NAME="date" TYPE="int" LENGTH="20" NOTNULL="false" SEQUENCE="false" COMMENT="Date the invite was created"/>
<FIELD NAME="allow_details" TYPE="int" LENGTH="2" NOTNULL="false" SEQUENCE="false"/>
<FIELD NAME="allow_calendar" TYPE="int" LENGTH="2" NOTNULL="false" SEQUENCE="false"/>
<FIELD NAME="allow_badges" TYPE="int" LENGTH="2" NOTNULL="false" SEQUENCE="false"/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
<KEY NAME="user_id-id" TYPE="foreign" FIELDS="user_id" REFTABLE="user" REFFIELDS="id"/>
</KEYS>
</TABLE>
<TABLE NAME="local_treestudyplan" COMMENT="Table">
<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="18" NOTNULL="true" SEQUENCE="true"/>
<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="slots" TYPE="int" LENGTH="9" NOTNULL="true" DEFAULT="4" SEQUENCE="false"/>
<FIELD NAME="startdate" TYPE="text" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="enddate" 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"/>
<FIELD NAME="context_id" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false"/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
</KEYS>
</TABLE>
<TABLE NAME="local_treestudyplan_user" COMMENT="Table">
<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="18" NOTNULL="true" SEQUENCE="true"/>
<FIELD NAME="user_id" TYPE="int" LENGTH="18" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="studyplan_id" TYPE="int" LENGTH="18" NOTNULL="true" SEQUENCE="false"/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
<KEY NAME="user_id-id" TYPE="foreign" FIELDS="user_id" REFTABLE="user" REFFIELDS="id"/>
<KEY NAME="studyplan_id-id" TYPE="foreign" FIELDS="studyplan_id" REFTABLE="local_treestudyplan" REFFIELDS="id"/>
</KEYS>
</TABLE>
<TABLE NAME="local_treestudyplan_cohort" COMMENT="Table">
<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="18" NOTNULL="true" SEQUENCE="true"/>
<FIELD NAME="studyplan_id" TYPE="int" LENGTH="18" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="cohort_id" TYPE="int" LENGTH="18" NOTNULL="true" SEQUENCE="false"/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
</KEYS>
</TABLE>
<TABLE NAME="local_treestudyplan_line" COMMENT="Table">
<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="18" NOTNULL="true" SEQUENCE="true"/>
<FIELD NAME="studyplan_id" TYPE="int" LENGTH="18" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="name" TYPE="text" NOTNULL="false" SEQUENCE="false"/>
<FIELD NAME="shortname" TYPE="text" NOTNULL="false" SEQUENCE="false"/>
<FIELD NAME="color" TYPE="char" LENGTH="12" NOTNULL="false" SEQUENCE="false"/>
<FIELD NAME="sequence" TYPE="int" LENGTH="18" NOTNULL="false" SEQUENCE="false"/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
<KEY NAME="studyplan_id-id" TYPE="foreign" FIELDS="studyplan_id" REFTABLE="local_treestudyplan" REFFIELDS="id"/>
</KEYS>
</TABLE>
<TABLE NAME="local_treestudyplan_item" COMMENT="Table">
<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="18" NOTNULL="true" SEQUENCE="true"/>
<FIELD NAME="line_id" TYPE="int" LENGTH="18" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="type" TYPE="text" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="conditions" TYPE="text" NOTNULL="false" SEQUENCE="false"/>
<FIELD NAME="slot" TYPE="int" LENGTH="9" NOTNULL="false" SEQUENCE="false"/>
<FIELD NAME="layer" TYPE="int" LENGTH="9" NOTNULL="false" SEQUENCE="false"/>
<FIELD NAME="competency_id" TYPE="int" LENGTH="18" NOTNULL="false" SEQUENCE="false"/>
<FIELD NAME="badge_id" TYPE="int" LENGTH="18" NOTNULL="false" SEQUENCE="false"/>
<FIELD NAME="course_id" TYPE="int" LENGTH="18" NOTNULL="false" SEQUENCE="false"/>
<FIELD NAME="continuation_id" TYPE="int" LENGTH="18" NOTNULL="false" SEQUENCE="false"/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
<KEY NAME="competency_id-id" TYPE="foreign" FIELDS="competency_id" REFTABLE="competency" REFFIELDS="id"/>
<KEY NAME="badge_id-id" TYPE="foreign" FIELDS="badge_id" REFTABLE="badge" REFFIELDS="id"/>
<KEY NAME="course_id-id" TYPE="foreign" FIELDS="course_id" REFTABLE="course" REFFIELDS="id"/>
<KEY NAME="line_id-id" TYPE="foreign" FIELDS="line_id" REFTABLE="local_treestudyplan_line" REFFIELDS="id"/>
<KEY NAME="continuation_id-id" TYPE="foreign" FIELDS="continuation_id" REFTABLE="local_treestudyplan_item" REFFIELDS="id"/>
</KEYS>
</TABLE>
<TABLE NAME="local_treestudyplan_connect" COMMENT="Table">
<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="18" NOTNULL="true" SEQUENCE="true"/>
<FIELD NAME="from_id" TYPE="int" LENGTH="18" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="to_id" TYPE="int" LENGTH="18" NOTNULL="true" SEQUENCE="false"/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
<KEY NAME="from_id-id" TYPE="foreign" FIELDS="from_id" REFTABLE="local_treestudyplan_item" REFFIELDS="id"/>
<KEY NAME="to_id-id" TYPE="foreign" FIELDS="to_id" REFTABLE="local_treestudyplan_item" REFFIELDS="id"/>
</KEYS>
</TABLE>
<TABLE NAME="local_treestudyplan_gradeinc" COMMENT="Information about whether or not to include grade_items as goals">
<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="18" NOTNULL="true" SEQUENCE="true"/>
<FIELD NAME="grade_item_id" TYPE="int" LENGTH="20" NOTNULL="false" SEQUENCE="false"/>
<FIELD NAME="include" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
<FIELD NAME="studyitem_id" TYPE="int" LENGTH="20" NOTNULL="false" SEQUENCE="false"/>
<FIELD NAME="required" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
<KEY NAME="grade_item_id-id" TYPE="foreign" FIELDS="grade_item_id" REFTABLE="grade_item" REFFIELDS="id"/>
<KEY NAME="studyitem_id-id" TYPE="foreign" FIELDS="studyitem_id" REFTABLE="local_treestudyplan_item" REFFIELDS="id"/>
</KEYS>
</TABLE>
<TABLE NAME="local_treestudyplan_gradecfg" COMMENT="Stores grade configuration for scales et al.">
<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
<FIELD NAME="scale_id" TYPE="int" LENGTH="20" NOTNULL="false" SEQUENCE="false"/>
<FIELD NAME="grade_points" TYPE="int" LENGTH="20" NOTNULL="false" SEQUENCE="false"/>
<FIELD NAME="min_completed" TYPE="number" LENGTH="20" NOTNULL="false" SEQUENCE="false" DECIMALS="1"/>
<FIELD NAME="min_progress" TYPE="number" LENGTH="20" NOTNULL="false" SEQUENCE="false" DECIMALS="1"/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
<KEY NAME="scale_id-id" TYPE="foreign" FIELDS="scale_id" REFTABLE="scale" REFFIELDS="id"/>
</KEYS>
</TABLE>
</TABLES>
</XMLDB>

18
db/itemsetup.txt Normal file
View File

@ -0,0 +1,18 @@
SLOT INDEX 0 SLOT INDEX [1,slots]
--------------- --------------------------------------
[FILTER SLOT] [ [COMPETENCY SLOT] [FILTER SLOT ] ]
Types allowed Types allowed Types allowed
- START - COMPETENCY - JUNCTION
- BADGE
- FINISH
- CONTINUATION
Type description
- START : Copies the state of an ITEM (FINISH ITEM) in another studyplan. It is used to allow continuation across studyplans
- COMPETENCY :
- JUNCTION :
- BADGE :
- FINISH : Denotes the final outcome of a studyline in a studyplan
CONDITIONS:

517
db/services.php Normal file
View File

@ -0,0 +1,517 @@
<?php
$services = [
"Competency listing" => [
'functions' => [
'local_treestudyplan_get_studyplan_map',
'local_treestudyplan_get_studyline_map',
'local_treestudyplan_add_studyplan',
'local_treestudyplan_add_studyline',
'local_treestudyplan_edit_studyplan',
'local_treestudyplan_edit_studyline',
'local_treestudyplan_delete_studyplan',
'local_treestudyplan_delete_studyline',
'local_treestudyplan_reorder_studylines',
'local_treestudyplan_get_studyitem',
'local_treestudyplan_add_studyitem',
'local_treestudyplan_edit_studyitem',
'local_treestudyplan_reorder_studyitems',
'local_treestudyplan_delete_studyitem',
'local_treestudyplan_connect_studyitems',
'local_treestudyplan_disconnect_studyitems',
'local_treestudyplan_get_user_studyplan_map',
'local_treestudyplan_map_courses',
'local_treestudyplan_export_studyplan',
'local_treestudyplan_import_studyplan',
],
'requiredcapability' => 'local/treestudyplan:configure',
'shortname'=> 'local_treestudyplan_cohorts',
'restrictedusers' => 0,
'enabled' => 0,
'ajax' => true,
],
];
$functions = [
/***************************
* Studyplan functions
***************************/
'local_treestudyplan_list_studyplans' => [ //web service function name
'classname' => '\local_treestudyplan\studyplanservice', //class containing the external function
'methodname' => 'list_studyplans', //external function name
'description' => 'List available studyplans', //human readable description of the web service function
'type' => 'read', //database rights of the web service function (read, write)
'ajax' => true,
'capabilities' => 'local/treestudyplan:editstudyplan, local/treestudyplan:viewuserreports', // Advises the admin which capabilities are required
'loginrequired' => true,
],
'local_treestudyplan_get_studyplan_map' => [ //web service function name
'classname' => '\local_treestudyplan\studyplanservice', //class containing the external function
'methodname' => 'get_studyplan_map', //external function name
'description' => 'Retrieve studyplan map', //human readable description of the web service function
'type' => 'read', //database rights of the web service function (read, write)
'ajax' => true,
'capabilities' => 'local/treestudyplan:editstudyplan, local/treestudyplan:viewuserreports', // Advises the admin which capabilities are required
'loginrequired' => true,
],
'local_treestudyplan_get_studyline_map' => [ //web service function name
'classname' => '\local_treestudyplan\studyplanservice', //class containing the external function
'methodname' => 'get_studyline_map', //external function name
'description' => 'Retrieve studyline map', //human readable description of the web service function
'type' => 'read', //database rights of the web service function (read, write)
'ajax' => true,
'capabilities' => 'local/treestudyplan:editstudyplan', // Advises the admin which capabilities are required
'loginrequired' => true,
],
'local_treestudyplan_add_studyplan' => [ //web service function name
'classname' => '\local_treestudyplan\studyplanservice', //class containing the external function
'methodname' => 'add_studyplan', //external function name
'description' => 'Add studyplan', //human readable description of the web service function
'type' => 'write', //database rights of the web service function (read, write)
'ajax' => true,
'capabilities' => 'local/treestudyplan:editstudyplan', // Advises the admin which capabilities are required
'loginrequired' => true,
],
'local_treestudyplan_add_studyline' => [ //web service function name
'classname' => '\local_treestudyplan\studyplanservice', //class containing the external function
'methodname' => 'add_studyline', //external function name
'description' => 'Add studyline', //human readable description of the web service function
'type' => 'write', //database rights of the web service function (read, write)
'ajax' => true,
'capabilities' => 'local/treestudyplan:editstudyplan', // Advises the admin which capabilities are required
'loginrequired' => true,
],
'local_treestudyplan_edit_studyplan' => [ //web service function name
'classname' => '\local_treestudyplan\studyplanservice', //class containing the external function
'methodname' => 'edit_studyplan', //external function name
'description' => 'Edit studyplan', //human readable description of the web service function
'type' => 'write', //database rights of the web service function (read, write)
'ajax' => true,
'capabilities' => 'local/treestudyplan:editstudyplan', // Advises the admin which capabilities are required
'loginrequired' => true,
],
'local_treestudyplan_edit_studyline' => [ //web service function name
'classname' => '\local_treestudyplan\studyplanservice', //class containing the external function
'methodname' => 'edit_studyline', //external function name
'description' => 'Edit studyline', //human readable description of the web service function
'type' => 'write', //database rights of the web service function (read, write)
'ajax' => true,
'capabilities' => 'local/treestudyplan:editstudyplan', // Advises the admin which capabilities are required
'loginrequired' => true,
],
'local_treestudyplan_delete_studyplan' => [ //web service function name
'classname' => '\local_treestudyplan\studyplanservice', //class containing the external function
'methodname' => 'delete_studyplan', //external function name
'description' => 'Delete studyplan', //human readable description of the web service function
'type' => 'write', //database rights of the web service function (read, write)
'ajax' => true,
'capabilities' => 'local/treestudyplan:editstudyplan', // Advises the admin which capabilities are required
'loginrequired' => true,
],
'local_treestudyplan_delete_studyline' => [ //web service function name
'classname' => '\local_treestudyplan\studyplanservice', //class containing the external function
'methodname' => 'delete_studyline', //external function name
'description' => 'Delete studyline', //human readable description of the web service function
'type' => 'write', //database rights of the web service function (read, write)
'ajax' => true,
'capabilities' => 'local/treestudyplan:editstudyplan', // Advises the admin which capabilities are required
'loginrequired' => true,
],
'local_treestudyplan_reorder_studylines' => [ //web service function name
'classname' => '\local_treestudyplan\studyplanservice', //class containing the external function
'methodname' => 'reorder_studylines', //external function name
'description' => 'Reorder studylines', //human readable description of the web service function
'type' => 'write', //database rights of the web service function (read, write)
'ajax' => true,
'capabilities' => 'local/treestudyplan:editstudyplan', // Advises the admin which capabilities are required
'loginrequired' => true,
],
'local_treestudyplan_get_studyitem' => [ //web service function name
'classname' => '\local_treestudyplan\studyplanservice', //class containing the external function
'methodname' => 'get_studyitem', //external function name
'description' => 'Retrieve study item', //human readable description of the web service function
'type' => 'read', //database rights of the web service function (read, write)
'ajax' => true,
'capabilities' => 'local/treestudyplan:editstudyplan', // Advises the admin which capabilities are required
'loginrequired' => true,
],
'local_treestudyplan_add_studyitem' => [ //web service function name
'classname' => '\local_treestudyplan\studyplanservice', //class containing the external function
'methodname' => 'add_studyitem', //external function name
'description' => 'Add study item', //human readable description of the web service function
'type' => 'write', //database rights of the web service function (read, write)
'ajax' => true,
'capabilities' => 'local/treestudyplan:editstudyplan', // Advises the admin which capabilities are required
'loginrequired' => true,
],
'local_treestudyplan_edit_studyitem' => [ //web service function name
'classname' => '\local_treestudyplan\studyplanservice', //class containing the external function
'methodname' => 'edit_studyitem', //external function name
'description' => 'Edit study item', //human readable description of the web service function
'type' => 'write', //database rights of the web service function (read, write)
'ajax' => true,
'capabilities' => 'local/treestudyplan:editstudyplan', // Advises the admin which capabilities are required
'loginrequired' => true,
],
'local_treestudyplan_reorder_studyitems' => [ //web service function name
'classname' => '\local_treestudyplan\studyplanservice', //class containing the external function
'methodname' => 'reorder_studyitems', //external function name
'description' => 'Reorder study items', //human readable description of the web service function
'type' => 'write', //database rights of the web service function (read, write)
'ajax' => true,
'capabilities' => 'local/treestudyplan:editstudyplan', // Advises the admin which capabilities are required
'loginrequired' => true,
],
'local_treestudyplan_delete_studyitem' => [ //web service function name
'classname' => '\local_treestudyplan\studyplanservice', //class containing the external function
'methodname' => 'delete_studyitem', //external function name
'description' => 'Delete study item', //human readable description of the web service function
'type' => 'write', //database rights of the web service function (read, write)
'ajax' => true,
'capabilities' => 'local/treestudyplan:editstudyplan', // Advises the admin which capabilities are required
'loginrequired' => true,
],
'local_treestudyplan_connect_studyitems' => [ //web service function name
'classname' => '\local_treestudyplan\studyplanservice', //class containing the external function
'methodname' => 'connect_studyitems', //external function name
'description' => 'Connect study items', //human readable description of the web service function
'type' => 'write', //database rights of the web service function (read, write)
'ajax' => true,
'capabilities' => 'local/treestudyplan:editstudyplan', // Advises the admin which capabilities are required
'loginrequired' => true,
],
'local_treestudyplan_disconnect_studyitems' => [ //web service function name
'classname' => '\local_treestudyplan\studyplanservice', //class containing the external function
'methodname' => 'disconnect_studyitems', //external function name
'description' => 'Disconnect study items', //human readable description of the web service function
'type' => 'write', //database rights of the web service function (read, write)
'ajax' => true,
'capabilities' => 'local/treestudyplan:editstudyplan', // Advises the admin which capabilities are required
'loginrequired' => true,
],
/***************************
* Badge functions
***************************/
'local_treestudyplan_list_badges' => [ //web service function name
'classname' => '\local_treestudyplan\studyplanservice', //class containing the external function
'methodname' => 'list_badges', //external function name
'description' => 'List availabel site badges', //human readable description of the web service function
'type' => 'read', //database rights of the web service function (read, write)
'ajax' => true,
'capabilities' => 'local/treestudyplan:editstudyplan', // Advises the admin which capabilities are required
'loginrequired' => true,
],
/***************************
* Association functions
***************************/
'local_treestudyplan_list_cohort' => [ //web service function name
'classname' => '\local_treestudyplan\associationservice', //class containing the external function
'methodname' => 'list_cohort', //external function name
'description' => 'List available cohorts', //human readable description of the web service function
'type' => 'read', //database rights of the web service function (read, write)
'ajax' => true,
'capabilities' => 'local/treestudyplan:editstudyplan', // Advises the admin which capabilities are required
'loginrequired' => true,
],
'local_treestudyplan_find_user' => [ //web service function name
'classname' => '\local_treestudyplan\associationservice', //class containing the external function
'methodname' => 'find_user', //external function name
'description' => 'Find user', //human readable description of the web service function
'type' => 'read', //database rights of the web service function (read, write)
'ajax' => true,
'capabilities' => 'local/treestudyplan:editstudyplan', // Advises the admin which capabilities are required
'loginrequired' => true,
],
'local_treestudyplan_connect_cohort' => [ //web service function name
'classname' => '\local_treestudyplan\associationservice', //class containing the external function
'methodname' => 'connect_cohort', //external function name
'description' => 'Connect cohort to studyplan', //human readable description of the web service function
'type' => 'read', //database rights of the web service function (read, write)
'ajax' => true,
'capabilities' => 'local/treestudyplan:editstudyplan', // Advises the admin which capabilities are required
'loginrequired' => true,
],
'local_treestudyplan_disconnect_cohort' => [ //web service function name
'classname' => '\local_treestudyplan\associationservice', //class containing the external function
'methodname' => 'disconnect_cohort', //external function name
'description' => 'Disconnect cohort from study plan', //human readable description of the web service function
'type' => 'read', //database rights of the web service function (read, write)
'ajax' => true,
'capabilities' => 'local/treestudyplan:editstudyplan', // Advises the admin which capabilities are required
'loginrequired' => true,
],
'local_treestudyplan_connect_user' => [ //web service function name
'classname' => '\local_treestudyplan\associationservice', //class containing the external function
'methodname' => 'connect_user', //external function name
'description' => 'Connect user to study plan', //human readable description of the web service function
'type' => 'read', //database rights of the web service function (read, write)
'ajax' => true,
'capabilities' => 'local/treestudyplan:editstudyplan', // Advises the admin which capabilities are required
'loginrequired' => true,
],
'local_treestudyplan_disconnect_user' => [ //web service function name
'classname' => '\local_treestudyplan\associationservice', //class containing the external function
'methodname' => 'disconnect_user', //external function name
'description' => 'Disconnect user from studyplan', //human readable description of the web service function
'type' => 'read', //database rights of the web service function (read, write)
'ajax' => true,
'capabilities' => 'local/treestudyplan:editstudyplan', // Advises the admin which capabilities are required
'loginrequired' => true,
],
'local_treestudyplan_associated_users' => [ //web service function name
'classname' => '\local_treestudyplan\associationservice', //class containing the external function
'methodname' => 'associated_users', //external function name
'description' => 'List users associated with a studyplan', //human readable description of the web service function
'type' => 'read', //database rights of the web service function (read, write)
'ajax' => true,
'capabilities' => 'local/treestudyplan:editstudyplan', // Advises the admin which capabilities are required
'loginrequired' => true,
],
'local_treestudyplan_associated_cohorts' => [ //web service function name
'classname' => '\local_treestudyplan\associationservice', //class containing the external function
'methodname' => 'associated_cohorts', //external function name
'description' => 'List cohorts associated with a studyplan', //human readable description of the web service function
'type' => 'read', //database rights of the web service function (read, write)
'ajax' => true,
'capabilities' => 'local/treestudyplan:editstudyplan', // Advises the admin which capabilities are required
'loginrequired' => true,
],
'local_treestudyplan_list_user_studyplans' => [ //web service function name
'classname' => '\local_treestudyplan\studentstudyplanservice', //class containing the external function
'methodname' => 'list_user_studyplans', //external function name
'description' => 'List user studyplans', //human readable description of the web service function
'type' => 'read', //database rights of the web service function (read, write)
'ajax' => true,
'capabilities' => 'local/treestudyplan:viewuserreports', // Advises the admin which capabilities are required
'loginrequired' => true,
],
'local_treestudyplan_get_user_studyplans' => [ //web service function name
'classname' => '\local_treestudyplan\studentstudyplanservice', //class containing the external function
'methodname' => 'get_user_studyplans', //external function name
'description' => 'Retrieve user studyplan', //human readable description of the web service function
'type' => 'read', //database rights of the web service function (read, write)
'ajax' => true,
'capabilities' => 'local/treestudyplan:viewuserreports', // Advises the admin which capabilities are required
'loginrequired' => true,
],
'local_treestudyplan_get_user_studyplan' => [ //web service function name
'classname' => '\local_treestudyplan\studentstudyplanservice', //class containing the external function
'methodname' => 'get_user_studyplan', //external function name
'description' => 'Retrieve user studyplan', //human readable description of the web service function
'type' => 'read', //database rights of the web service function (read, write)
'ajax' => true,
'capabilities' => 'local/treestudyplan:viewuserreports', // Advises the admin which capabilities are required
'loginrequired' => true,
],
'local_treestudyplan_get_invited_studyplan' => [ //web service function name
'classname' => '\local_treestudyplan\studentstudyplanservice', //class containing the external function
'methodname' => 'get_invited_studyplan', //external function name
'description' => 'Retrieve user studyplan based on invite', //human readable description of the web service function
'type' => 'read', //database rights of the web service function (read, write)
'ajax' => true,
'capabilities' => '', // Advises the admin which capabilities are required
'loginrequired' => false,
],
'local_treestudyplan_list_own_studyplans' => [ //web service function name
'classname' => '\local_treestudyplan\studentstudyplanservice', //class containing the external function
'methodname' => 'list_own_studyplans', //external function name
'description' => 'List own studyplans', //human readable description of the web service function
'type' => 'read', //database rights of the web service function (read, write)
'ajax' => true,
'capabilities' => '', // Advises the admin which capabilities are required
'loginrequired' => true,
],
'local_treestudyplan_get_own_studyplan' => [ //web service function name
'classname' => '\local_treestudyplan\studentstudyplanservice', //class containing the external function
'methodname' => 'get_own_studyplan', //external function name
'description' => 'Retrieve own studyplan', //human readable description of the web service function
'type' => 'read', //database rights of the web service function (read, write)
'ajax' => true,
'capabilities' => '', // Advises the admin which capabilities are required
'loginrequired' => true,
],
'local_treestudyplan_map_categories' => [ //web service function name
'classname' => '\local_treestudyplan\courseservice', //class containing the external function
'methodname' => 'map_categories', //external function name
'description' => 'List available root categories', //human readable description of the web service function
'type' => 'read', //database rights of the web service function (read, write)
'ajax' => true,
'capabilities' => 'local/treestudyplan:editstudyplan', // Advises the admin which capabilities are required
'loginrequired' => true,
],
'local_treestudyplan_get_category' => [ //web service function name
'classname' => '\local_treestudyplan\courseservice', //class containing the external function
'methodname' => 'get_category', //external function name
'description' => 'Get details for specified category', //human readable description of the web service function
'type' => 'read', //database rights of the web service function (read, write)
'ajax' => true,
'capabilities' => 'local/treestudyplan:editstudyplan', // Advises the admin which capabilities are required
'loginrequired' => true,
],
'local_treestudyplan_include_grade' => [ //web service function name
'classname' => '\local_treestudyplan\studyplanservice', //class containing the external function
'methodname' => 'include_grade', //external function name
'description' => 'Include gradable in result', //human readable description of the web service function
'type' => 'read', //database rights of the web service function (read, write)
'ajax' => true,
'capabilities' => 'local/treestudyplan:editstudyplan, local/treestudyplan:selectowngradables', // Advises the admin which capabilities are required
'loginrequired' => true,
],
'local_treestudyplan_all_associated' => [ //web service function name
'classname' => '\local_treestudyplan\associationservice', //class containing the external function
'methodname' => 'all_associated', //external function name
'description' => 'List associated users', //human readable description of the web service function
'type' => 'read', //database rights of the web service function (read, write)
'ajax' => true,
'capabilities' => 'local/treestudyplan:viewuserreports', // Advises the admin which capabilities are required
'loginrequired' => true,
],
'local_treestudyplan_list_aggregators' => [ //web service function name
'classname' => '\local_treestudyplan\studyplanservice', //class containing the external function
'methodname' => 'list_aggregators', //external function name
'description' => 'List available aggregators', //human readable description of the web service function
'type' => 'read', //database rights of the web service function (read, write)
'ajax' => true,
'capabilities' => 'local/treestudyplan:editstudyplan', // Advises the admin which capabilities are required
'loginrequired' => true,
],
'local_treestudyplan_disable_autoenddate' => [ //web service function name
'classname' => '\local_treestudyplan\studyplanservice', //class containing the external function
'methodname' => 'disable_autoenddate', //external function name
'description' => 'Disable automatic end dates on courses in the study plan', //human readable description of the web service function
'type' => 'write', //database rights of the web service function (read, write)
'ajax' => true,
'capabilities' => 'local/treestudyplan:forcescales', // Advises the admin which capabilities are required
'loginrequired' => true,
],
'local_treestudyplan_force_studyplan_scale' => [ //web service function name
'classname' => '\local_treestudyplan\studyplanservice', //class containing the external function
'methodname' => 'force_studyplan_scale', //external function name
'description' => 'Change all associated gradables to the chosen scale if possible', //human readable description of the web service function
'type' => 'write', //database rights of the web service function (read, write)
'ajax' => true,
'capabilities' => 'local/treestudyplan:forcescales', // Advises the admin which capabilities are required
'loginrequired' => true,
],
'local_treestudyplan_list_scales' => [ //web service function name
'classname' => '\local_treestudyplan\studyplanservice', //class containing the external function
'methodname' => 'list_scales', //external function name
'description' => 'List system scales', //human readable description of the web service function
'type' => 'write', //database rights of the web service function (read, write)
'ajax' => true,
'capabilities' => 'local/treestudyplan:forcescales', // Advises the admin which capabilities are required
'loginrequired' => true,
],
'local_treestudyplan_duplicate_plan' => [ //web service function name
'classname' => '\local_treestudyplan\studyplanservice', //class containing the external function
'methodname' => 'duplicate_plan', //external function name
'description' => 'Copy studyplan', //human readable description of the web service function
'type' => 'write', //database rights of the web service function (read, write)
'ajax' => true,
'capabilities' => 'local/treestudyplan:editstudyplan', // Advises the admin which capabilities are required
'loginrequired' => true,
],
'local_treestudyplan_export_plan' => [ //web service function name
'classname' => '\local_treestudyplan\studyplanservice', //class containing the external function
'methodname' => 'export_plan', //external function name
'description' => 'Export study plan', //human readable description of the web service function
'type' => 'read', //database rights of the web service function (read, write)
'ajax' => true,
'capabilities' => 'local/treestudyplan:editstudyplan', // Advises the admin which capabilities are required
'loginrequired' => true,
],
'local_treestudyplan_export_studylines' => [ //web service function name
'classname' => '\local_treestudyplan\studyplanservice', //class containing the external function
'methodname' => 'export_studylines', //external function name
'description' => 'Export study plan', //human readable description of the web service function
'type' => 'read', //database rights of the web service function (read, write)
'ajax' => true,
'capabilities' => 'local/treestudyplan:editstudyplan', // Advises the admin which capabilities are required
'loginrequired' => true,
],
'local_treestudyplan_import_plan' => [ //web service function name
'classname' => '\local_treestudyplan\studyplanservice', //class containing the external function
'methodname' => 'import_plan', //external function name
'description' => 'Import study plan', //human readable description of the web service function
'type' => 'write', //database rights of the web service function (read, write)
'ajax' => true,
'capabilities' => 'local/treestudyplan:editstudyplan', // Advises the admin which capabilities are required
'loginrequired' => true,
],
'local_treestudyplan_import_studylines' => [ //web service function name
'classname' => '\local_treestudyplan\studyplanservice', //class containing the external function
'methodname' => 'import_studylines', //external function name
'description' => 'Import study plan', //human readable description of the web service function
'type' => 'write', //database rights of the web service function (read, write)
'ajax' => true,
'capabilities' => 'local/treestudyplan:editstudyplan', // Advises the admin which capabilities are required
'loginrequired' => true,
],
'local_treestudyplan_read_course_displayname' => [ //web service function name
'classname' => '\local_treestudyplan\studyplanservice', //class containing the external function
'methodname' => 'read_course_displayname', //external function name
'description' => 'Read title and desc for course module', //human readable description of the web service function
'type' => 'read', //database rights of the web service function (read, write)
'ajax' => true,
'capabilities' => 'local/treestudyplan:editstudyplan', // Advises the admin which capabilities are required
'loginrequired' => true,
],
'local_treestudyplan_write_course_displayname' => [ //web service function name
'classname' => '\local_treestudyplan\studyplanservice', //class containing the external function
'methodname' => 'write_course_displayname', //external function name
'description' => 'Write title and desc for course module', //human readable description of the web service function
'type' => 'read', //database rights of the web service function (read, write)
'ajax' => true,
'capabilities' => 'local/treestudyplan:editstudyplan', // Advises the admin which capabilities are required
'loginrequired' => true,
],
'local_treestudyplan_submit_cm_editform' => [ //web service function name
'classname' => '\local_treestudyplan\studyplanservice', //class containing the external function
'methodname' => 'submit_cm_editform', //external function name
'description' => 'Submit course module edit form', //human readable description of the web service function
'type' => 'write', //database rights of the web service function (read, write)
'ajax' => true,
'capabilities' => 'local/treestudyplan:editstudyplan', // Advises the admin which capabilities are required
'loginrequired' => true,
],
'local_treestudyplan_get_teaching_studyplans' => [ //web service function name
'classname' => '\local_treestudyplan\studentstudyplanservice', //class containing the external function
'methodname' => 'get_teaching_studyplans', //external function name
'description' => 'Get the studyplans I currently teach in', //human readable description of the web service function
'type' => 'read', //database rights of the web service function (read, write)
'ajax' => true,
'capabilities' => 'local/treestudyplan:viewuserreports', // Advises the admin which capabilities are required
'loginrequired' => true,
],
'local_treestudyplan_list_accessible_categories' => [ //web service function name
'classname' => '\local_treestudyplan\courseservice', //class containing the external function
'methodname' => 'list_accessible_categories', //external function name
'description' => 'Get categories accessible to the current user', //human readable description of the web service function
'type' => 'read', //database rights of the web service function (read, write)
'ajax' => true,
'loginrequired' => true,
],
'local_treestudyplan_list_used_categories' => [ //web service function name
'classname' => '\local_treestudyplan\courseservice', //class containing the external function
'methodname' => 'list_used_categories', //external function name
'description' => 'Get categories hosting a studyplan', //human readable description of the web service function
'type' => 'read', //database rights of the web service function (read, write)
'capabilities' => 'local/treestudyplan:editstudyplan', // Advises the admin which capabilities are required
'ajax' => true,
'loginrequired' => true,
],];

189
db/upgrade.php Normal file
View File

@ -0,0 +1,189 @@
<?php
function xmldb_local_treestudyplan_upgrade($oldversion) {
global $DB;
$dbman = $DB->get_manager();
if ($oldversion < 2020112900) {
// Define table local_treestudyplan_gradeinc to be created.
$table = new xmldb_table('local_treestudyplan_gradeinc');
// Adding fields to table local_treestudyplan_gradeinc.
$table->add_field('id', XMLDB_TYPE_INTEGER, '18', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
$table->add_field('grade_item_id', XMLDB_TYPE_INTEGER, '20', null, null, null, null);
$table->add_field('include', XMLDB_TYPE_INTEGER, '1', null, XMLDB_NOTNULL, null, '0');
// Adding keys to table local_treestudyplan_gradeinc.
$table->add_key('primary', XMLDB_KEY_PRIMARY, ['id']);
$table->add_key('grade_item_id-id', XMLDB_KEY_FOREIGN, ['grade_item_id'], 'grade_item', ['id']);
// Conditionally launch create table for local_treestudyplan_gradeinc.
if (!$dbman->table_exists($table)) {
$dbman->create_table($table);
}
// Treestudyplan savepoint reached.
upgrade_plugin_savepoint(true, 2020112900, 'local', 'treestudyplan');
}
if ($oldversion < 2020120301) {
// Define field studyitem_id to be added to local_treestudyplan_gradeinc.
$table = new xmldb_table('local_treestudyplan_gradeinc');
$field = new xmldb_field('studyitem_id', XMLDB_TYPE_INTEGER, '20', null, null, null, null, 'include');
// Conditionally launch add field studyitem_id.
if (!$dbman->field_exists($table, $field)) {
$dbman->add_field($table, $field);
}
$key = new xmldb_key('studyitem_id-id', XMLDB_KEY_FOREIGN, ['studyitem_id'], 'local_treestudyplan_item', ['id']);
// Launch add key studyitem_id-id.
$dbman->add_key($table, $key);
// Treestudyplan savepoint reached.
upgrade_plugin_savepoint(true, 2020120301, 'local', 'treestudyplan');
}
if ($oldversion < 2020120500) {
// Define table local_treestudyplan_gradecfg to be created.
$table = new xmldb_table('local_treestudyplan_gradecfg');
// Adding fields to table local_treestudyplan_gradecfg.
$table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
$table->add_field('scale_id', XMLDB_TYPE_INTEGER, '20', null, null, null, null);
$table->add_field('grade_points', XMLDB_TYPE_INTEGER, '20', null, null, null, null);
$table->add_field('min_completed', XMLDB_TYPE_INTEGER, '20', null, null, null, null);
$table->add_field('min_excellent', XMLDB_TYPE_INTEGER, '20', null, null, null, null);
// Adding keys to table local_treestudyplan_gradecfg.
$table->add_key('primary', XMLDB_KEY_PRIMARY, ['id']);
$table->add_key('scale_id-id', XMLDB_KEY_FOREIGN, ['scale_id'], 'scale', ['id']);
// Conditionally launch create table for local_treestudyplan_gradecfg.
if (!$dbman->table_exists($table)) {
$dbman->create_table($table);
}
// Treestudyplan savepoint reached.
upgrade_plugin_savepoint(true, 2020120500, 'local', 'treestudyplan');
}
if ($oldversion < 2021082300) {
// Define field aggregation to be added to local_treestudyplan.
$table = new xmldb_table('local_treestudyplan');
$field = new xmldb_field('aggregation', XMLDB_TYPE_CHAR, '30', null, XMLDB_NOTNULL, null, 'tristate', 'enddate');
// Conditionally launch add field aggregation.
if (!$dbman->field_exists($table, $field)) {
$dbman->add_field($table, $field);
}
// Define field aggregation_config to be added to local_treestudyplan.
$field = new xmldb_field('aggregation_config', XMLDB_TYPE_TEXT, null, null, null, null, null, 'aggregation');
// Conditionally launch add field aggregation_config.
if (!$dbman->field_exists($table, $field)) {
$dbman->add_field($table, $field);
}
// Treestudyplan savepoint reached.
upgrade_plugin_savepoint(true, 2021082300, 'local', 'treestudyplan');
}
if ($oldversion < 2021082600) {
// Define field min_progress to be dropped from local_treestudyplan_gradecfg.
$table = new xmldb_table('local_treestudyplan_gradecfg');
$field = new xmldb_field('min_excellent');
// Conditionally launch drop field min_progress.
if ($dbman->field_exists($table, $field)) {
$dbman->drop_field($table, $field);
}
$field = new xmldb_field('min_progress', XMLDB_TYPE_INTEGER, '20', null, null, null, null, 'min_completed');
// Conditionally launch add field min_progress.
if (!$dbman->field_exists($table, $field)) {
$dbman->add_field($table, $field);
}
// Treestudyplan savepoint reached.
upgrade_plugin_savepoint(true, 2021082600, 'local', 'treestudyplan');
}
if ($oldversion < 2021082601) {
$table = new xmldb_table('local_treestudyplan_gradecfg');
// Changing type of field min_completed on table local_treestudyplan_gradecfg to number.
$field = new xmldb_field('min_completed', XMLDB_TYPE_NUMBER, '20', null, null, null, null, 'grade_points');
// Launch change of type for field min_completed.
$dbman->change_field_type($table, $field);
// Changing type of field min_progress on table local_treestudyplan_gradecfg to number.
$field = new xmldb_field('min_progress', XMLDB_TYPE_NUMBER, '20', null, null, null, null, 'min_completed');
// Launch change of type for field min_progress.
$dbman->change_field_type($table, $field);
// Treestudyplan savepoint reached.
upgrade_plugin_savepoint(true, 2021082601, 'local', 'treestudyplan');
}
if ($oldversion < 2021082700) {
$table = new xmldb_table('local_treestudyplan_gradecfg');
// Changing precision of field min_completed on table local_treestudyplan_gradecfg to (20, 2).
$field = new xmldb_field('min_completed', XMLDB_TYPE_NUMBER, '20, 1', null, null, null, null, 'grade_points');
// Launch change of precision for field min_completed.
$dbman->change_field_precision($table, $field);
// Changing precision of field min_progress on table local_treestudyplan_gradecfg to (20, 2).
$field = new xmldb_field('min_progress', XMLDB_TYPE_NUMBER, '20, 1', null, null, null, null, 'min_completed');
// Launch change of precision for field min_progress.
$dbman->change_field_precision($table, $field);
// Treestudyplan savepoint reached.
upgrade_plugin_savepoint(true, 2021082700, 'local', 'treestudyplan');
}
if ($oldversion < 2021082701) {
// Define field required to be added to local_treestudyplan_gradeinc.
$table = new xmldb_table('local_treestudyplan_gradeinc');
$field = new xmldb_field('required', XMLDB_TYPE_INTEGER, '1', null, XMLDB_NOTNULL, null, '0', 'studyitem_id');
// Conditionally launch add field required.
if (!$dbman->field_exists($table, $field)) {
$dbman->add_field($table, $field);
}
// Treestudyplan savepoint reached.
upgrade_plugin_savepoint(true, 2021082701, 'local', 'treestudyplan');
}
if ($oldversion < 2023051700) {
// Define field context_id to be added to local_treestudyplan.
$table = new xmldb_table('local_treestudyplan');
$field = new xmldb_field('context_id', XMLDB_TYPE_INTEGER, '10', null, null, null, null, 'aggregation_config');
// Conditionally launch add field context_id.
if (!$dbman->field_exists($table, $field)) {
$dbman->add_field($table, $field);
}
// Treestudyplan savepoint reached.
upgrade_plugin_savepoint(true, 2023051700, 'local', 'treestudyplan');
}
return true;
}

164
edit-invite.php Normal file
View File

@ -0,0 +1,164 @@
<?php
if(isset($_SERVER['SCRIPT_FILENAME']))
{
// If SCRIPT_FILENAME is set, use that so the symlinked directories the developmen environment uses are handled correctly
$root = dirname(dirname(dirname($_SERVER['SCRIPT_FILENAME'])));
error_log("Using {$root}/config.php");
require_once($root."/config.php");
}
else
{
// If not, assume the cwd is not symlinked and proceed as we are used to
require_once("../../config.php");
}
require_once("./lib.php");
require_once($CFG->libdir.'/weblib.php');
require_once($CFG->dirroot.'/local/treestudyplan/classes/reportinvite_form.php');
$add = optional_param('add', '', PARAM_ALPHANUM); // module name
$update = optional_param('update', 0, PARAM_INT);
$resend = optional_param('resend', 0, PARAM_INT);
$delete = optional_param('delete', 0, PARAM_INT);
$systemcontext = context_system::instance();
$PAGE->set_url("/local/treestudyplan/edit-invite.php");
$PAGE->set_pagelayout('base');
$PAGE->set_context($systemcontext);
if($update > 0)
{
$PAGE->set_title(get_string('invite_desc_edit', 'local_treestudyplan'));
$PAGE->set_heading(get_string('invite_desc_edit', 'local_treestudyplan'));
}
else
{
$PAGE->set_title(get_string('invite_desc_new', 'local_treestudyplan'));
$PAGE->set_heading(get_string('invite_desc_new', 'local_treestudyplan'));
}
// Check if user has capability to manage study plan units
require_login();
print $OUTPUT->header();
if(!empty($add))
{
$data = array(
'add' => 1,
);
}
else if(!empty($update))
{
$data = $DB->get_record("local_treestudyplan_invit", array('id' => $update));
$data->update = $update;
if(empty($data) || $data->user_id != $USER->id)
{
print_error('invalidaction');
exit;
}
}
else if(!empty($resend))
{
$data = $DB->get_record("local_treestudyplan_invit", array('id' => $resend));
$data->resend = $resend;
if(empty($data) || $data->user_id != $USER->id)
{
print_error('invalidaction');
exit;
}
// Do some resending of an invitation
local_treestudyplan_send_invite($data->id);
redirect("$CFG->wwwroot/local/treestudyplan/invitations.php?sent={$resend}");
print $OUTPUT->footer();
exit;
}
else if(!empty($delete))
{
$data = $DB->get_record("local_treestudyplan_invit", array('id' => $delete));
$data->delete = $delete;
if(empty($data) || $data->user_id != $USER->id)
{
print_error('invalidaction');
exit;
}
$DB->delete_records('local_treestudyplan_invit', ['id' => $data->delete]);
redirect("$CFG->wwwroot/local/treestudyplan/invitations.php");
print $OUTPUT->footer();
exit;
}
else {
print_error('invalidaction');
}
$mform = new reportinvite_form();
$mform->set_data($data);
if($mform->is_cancelled())
{
redirect("$CFG->wwwroot/local/treestudyplan/invitations.php");
}
else if ($data = $mform->get_data())
{
if(!empty($data->update))
{
$id = $data->update;
$data->id = $id;
$DB->update_record('local_treestudyplan_invit', $data);
redirect("$CFG->wwwroot/local/treestudyplan/invitations.php");
}
else if(!empty($data->add))
{
$id = $DB->insert_record("local_treestudyplan_invit",$data, true);
// Send invitaion mail
local_treestudyplan_send_invite($id);
redirect("$CFG->wwwroot/local/treestudyplan/invitations.php?sent={$id}");
}
else if(!empty($data->resend))
{
}
else if(!empty($data->delete))
{
}
else
{
print_error("invaliddata");
}
exit;
}
else
{
$data = null;
if($unitid > 0)
{
}
$mform->display();
}
print $OUTPUT->footer();

190
edit-plan.php Normal file
View File

@ -0,0 +1,190 @@
<?php
require_once("../../config.php");
require_once($CFG->libdir.'/weblib.php');
use \local_treestudyplan\courseservice;
$systemcontext = context_system::instance();
$PAGE->set_url("/local/treestudyplan/edit-plan.php",array());
require_login();
// Figure out the context (category or system, based on either category or context parameter)
$categoryid = optional_param('categoryid', 0, PARAM_INT); // Category id
$contextid = optional_param('contextid', 0, PARAM_INT); // Context id
if($categoryid > 0){
$studyplancontext = context_coursecat::instance($categoryid);
}
elseif($contextid > 0)
{
$studyplancontext = context::instance_by_id($contextid);
if(in_array($studyplancontext->contextlevel,[CONTEXT_SYSTEM,CONTEXT_COURSECAT]))
{
$categoryid = $studyplancontext->instanceid;
}
else
{
$studyplancontext = $systemcontext;
}
}
else
{
$categoryid = 0;
$studyplancontext = $systemcontext;
}
require_capability('local/treestudyplan:editstudyplan',$studyplancontext);
$contextname = $studyplancontext->get_context_name(false,false);
$PAGE->set_pagelayout('coursecategory');
$PAGE->set_context($studyplancontext);
$PAGE->set_title(get_string('cfg_plans','local_treestudyplan')." - ".$contextname);
$PAGE->set_heading($contextname);
if($studyplancontext->id > 1){
navigation_node::override_active_url(new moodle_url('/course/index.php', ['categoryid' => $categoryid ]));
$PAGE->navbar->add(get_string('cfg_plans','local_treestudyplan'));
}
// Load javascripts and specific css
$PAGE->requires->css(new moodle_url($CFG->wwwroot.'/local/treestudyplan/css/bootstrap-vue.min.css'));
$PAGE->requires->css(new moodle_url($CFG->wwwroot.'/local/treestudyplan/css/devstyles.css'));
$PAGE->requires->css(new moodle_url($CFG->wwwroot.'/local/treestudyplan/css/vue-hsluv-picker.css'));
$PAGE->requires->js_call_amd('local_treestudyplan/page-edit-plan', 'init', [$studyplancontext->id,$categoryid]);
$catlist = courseservice::list_accessible_categories_with_usage("edit");
//Local translate function
function t($str, $param=null, $plugin='local_treestudyplan'){
print get_string($str,$plugin,$param);
}
print $OUTPUT->header();
?>
<div id='root'>
<div class='vue-loader' v-show='false'>
<div class="spinner-border text-primary" role="status">
<span class="sr-only">Loading...</span>
</div>
</div>
<div v-cloak>
<div v-if='!activestudyplan && usedcontexts' class='ml-3 mb-3'>
<b-form-select text='<?php print($contextname);?>' :value="contextid">
<b-form-select-option v-for='ctx in usedcontexts' :key='ctx.id' :value="ctx.context_id" @click='switchContext(ctx)'
:active="ctx.context_id == contextid" :class="(ctx.studyplancount > 0)?'font-weight-bold':''"
><span v-for="(p,i) in ctx.category.path"><span v-if="i>0"> / </span>{{ p }}</span> <span>({{ ctx.studyplancount }})</b-form-select-option>
</b-form-select>
</div>
<h3 v-else><?php print $contextname; ?></h3>
<div class="m-buttonbar" style="margin-bottom: 1em;">
<a href='#' v-if='activestudyplan' @click.prevent='closeStudyplan'><i style='font-size: 150%;' class='fa fa-chevron-left'></i> <?php t('back');?></a>
<span v-if='activestudyplan'><?php t("studyplan_select"); ?></span>&nbsp;
<b-dropdown v-if='activestudyplan' lazy :text='dropdown_title'>
<b-dropdown-item-button v-for='(studyplan,planindex) in studyplans' :key='studyplan.id' @click='selectStudyplan(studyplan)'>{{ studyplan.name }}</b-dropdown-item>
</b-dropdown>&nbsp;
<t-studyplan-edit
@creating=""
@created="onStudyPlanCreated"
v-if='!activestudyplan'
mode="create"
v-model="create.studyplan"
type="button"
variant="primary"
><i class='fa fa-plus'></i> <?php t("studyplan_add");?></t-studyplan-edit>
<b-button v-if='!activestudyplan' variant='danger' href='#' role='presentation' @click="import_studyplan "><i class='fa fa-upload'></i> <?php t("advanced_import_from_file");?></b-button>
<b-button v-if='activestudyplan' variant='primary' v-b-toggle.toolbox-sidebar><?php t('opentoolbox') ?></b-button>
</div>
<div class='t-studyplan-container'>
<t-studyplan v-if='activestudyplan' v-model='activestudyplan' @moved="movedStudyplan"></t-studyplan>
<div v-else-if='loadingstudyplan' class="spinner-border text-primary" role="status">
<span class="sr-only">Loading...</span>
</div>
<div v-else class='t-studyplan-notselected'>
<p><?php t("studyplan_noneselected"); ?></p>
<b-card-group deck>
<s-studyplan-card
v-for='(studyplan,planindex) in studyplans'
:key='studyplan.id'
v-model='studyplans[planindex]'
open
@open='selectStudyplan(studyplan)'
>
<template #title>
<span class='s-studyplan-card-title-buttons'>
<a href='#' @click.prevent="export_plan(studyplan)" ><i class='fa fa-download'></i></a>
<t-studyplan-edit v-model="studyplans[planindex]"></t-studyplan-edit>
<t-studyplan-associate v-model="studyplans[planindex]"></t-studyplan-associate>
</span>
</template>
</s-studyplan-card>
</b-card-group>
</div>
</div>
<b-sidebar
id="toolbox-sidebar"
:right='toolbox.right'
shadow
title='<?php t("toolbox")?>'
>
<div class='t-toolbox-preface'>
<b-form-checkbox v-model="toolbox.right" switch><?php t("toolbar-right");?></b-form-checkbox>
</div>
<b-tabs content-class='mt-3'>
<b-tab title="<?php t('courses')?>">
<t-coursecat-list v-model="courses"></t-coursecat-list>
</b-tab>
<b-tab title="<?php t('toolbox')?>">
<ul class="t-toolbox">
<li><drag
type="filter"
:data="{type: 'junction'}"
@cut=""
><t-item-junction></t-item-junction><?php t("tool-junction") ?>
<template v-slot:drag-image="{data}"><t-item-junction></t-item-junction></template>
</drag></li>
<li><drag
type="filter"
:data="{type: 'finish'}"
@cut=""
><t-item-finish></t-item-finish><?php t("tool-finish") ?>
<template v-slot:drag-image="{data}"><t-item-finish></t-item-finish></template>
</drag></li>
<li><drag
type="filter"
:data="{type: 'start'}"
@cut=""
><t-item-start></t-item-start><?php t("tool-start") ?>
<template v-slot:drag-image="{data}"><t-item-start></t-item-start></template>
</drag></li>
</ul>
</b-tab>
<b-tab title="<?php t('badges')?>">
<ul class="t-badges">
<li v-for="b in badges"><img :src="b.imageurl" :alt="b.name"><drag
class="t-badge-drag"
type="filter"
:data="{type: 'badge', badge: b}"
@cut=""
>{{b.name}}
<template v-slot:drag-image="{data}"><img :src="b.imageurl" :alt="b.name"></template>
</drag></li>
</ul>
</b-tab>
</b-tabs>
</b-sidebar>
</div>
</div>
<?php
print $OUTPUT->footer();

103
invitations.php Normal file
View File

@ -0,0 +1,103 @@
<?php
if(isset($_SERVER['SCRIPT_FILENAME']))
{
// If SCRIPT_FILENAME is set, use that so the symlinked directories the developmen environment uses are handled correctly
$root = dirname(dirname(dirname($_SERVER['SCRIPT_FILENAME'])));
error_log("Using {$root}/config.php");
require_once($root."/config.php");
}
else
{
// If not, assume the cwd is not symlinked and proceed as we are used to
require_once("../../config.php");
}
require_once($CFG->libdir.'/weblib.php');
require_once($CFG->dirroot.'/grade/querylib.php');
use local_treestudyplan;
$INVITED_URL = "/local/treestudyplan/invited.php";
//admin_externalpage_setup('major');
$systemcontext = context_system::instance();
$PAGE->set_url("/local/treestudyplan/invitations.php",array());
require_login();
$PAGE->set_pagelayout('base');
$PAGE->set_context($systemcontext);
$PAGE->set_title(get_string('manage_invites', 'local_treestudyplan'));
$PAGE->set_heading(get_string('manage_invites', 'local_treestudyplan'));
// Load javascripts
$PAGE->requires->js_call_amd('local_treestudyplan/page-invitemanager', 'init');
$PAGE->requires->js_call_amd('local_treestudyplan/buttonlinks', 'init');
// retrieve list of courses that the student is enrolled in
$sent = optional_param('sent','',PARAM_INT);
if(!empty($sent))
{
$invite = $DB->get_record('local_treestudyplan_invit', array('id' => $sent));
\core\notification::success(get_string('invite_resent_msg','local_treestudyplan',$invite));
};
print $OUTPUT->header();
print "<p>".get_string('invite_description', 'local_treestudyplan')."</p>";
$invites = $DB->get_records('local_treestudyplan_invit', array('user_id' => $USER->id));
print "<h3>".get_string('invite_tablecaption','local_treestudyplan')."</h3>";
print "<table class='m-manage_invites'>";
print "<thead>";
print "<th>".get_string('invite_name','local_treestudyplan')."</th>";
print "<th>".get_string('invite_email','local_treestudyplan')."</th>";
print "<th>".get_string('invite_date','local_treestudyplan')."</th>";
print "<th>&nbsp;</th>";
print "</thead>";
print "<tbody>";
if(count($invites) > 0)
{
foreach($invites as $invite)
{
$testlink = $INVITED_URL."?key={$invite->invitekey}";
print "<tr data-id='{$invite->id}'>";
print "<td data-field='name'>{$invite->name}</td>";
print "<td data-field='email'>{$invite->email}</td>";
print "<td data-field='date'>".userdate($invite->date, "%x")."</td>";
print "<td data-field='control'>";
print "<a class='m-action-view ' href='{$testlink}' title='".get_string('invite_tooltip_testlink','local_treestudyplan')."'><i class='fa fa-eye'></i></a>";
print "<a class='m-action-resend m-action-confirm'";
print " data-confirmtext='".get_string('invite_confirm_resend','local_treestudyplan',$invite->name)."'";
print " data-confirmbtn='".get_string('send','local_treestudyplan')."'";
print " href='#' data-actionhref='edit-invite.php?resend={$invite->id}' title='".get_string('invite_tooltip_resend','local_treestudyplan')."'";
print " ><i class='fa fa-envelope'></i></a>";
print "<a href='edit-invite.php?update={$invite->id}'><i class='fa fa-pencil' title='".get_string('invite_tooltip_edit','local_treestudyplan')."'></i></a>";
print "<a class='m-action-delete m-action-confirm'";
print " data-confirmtext='".get_string('invite_confirm_delete','local_treestudyplan', $invite->name)."'";
print " data-confirmbtn='".get_string('delete')."'";
print " href='#' data-actionhref='edit-invite.php?delete={$invite->id}' title='".get_string('invite_tooltip_delete','local_treestudyplan')."'";
print " ><i class='fa fa-trash'></i></a>";
print "</td>";
}
}
else
{
print "<tr><td colspan='6'>".get_string('invite_table_empty','local_treestudyplan')."</td></tr>";
}
print "</tbody></table>";
print "<a class='btn btn-info' href='/local/treestudyplan/edit-invite.php?add=true' class='btn btn-primary' id='add_invite'><i class='fa fa-plus'></i> ".get_string('invite_button_new', 'local_treestudyplan')."</a>";
print $OUTPUT->footer();

66
invited.php Normal file
View File

@ -0,0 +1,66 @@
<?php
// If not, assume the cwd is not symlinked and proceed as we are used to
require_once("../../config.php");
//Local translate function
function t($str, $param=null, $plugin='local_treestudyplan'){
print get_string($str,$plugin,$param);
}
$systemcontext = context_system::instance();
$PAGE->set_pagelayout('base');
$PAGE->set_context($systemcontext);
// See if we can get a valid user for this invited
$invitekey = optional_param('key', '', PARAM_ALPHANUM); // module name
$PAGE->set_url("/local/treestudyplan/invited.php",array('key' => $invitekey));
$invite = $DB->get_record_select("local_treestudyplan_invit", $DB->sql_compare_text("invitekey"). " = " . $DB->sql_compare_text(":invitekey"), ['invitekey' => $invitekey]);
if(empty($invite))
{
$PAGE->set_title(get_string('invalid_invitekey_title', 'local_treestudyplan'));
$PAGE->set_heading(get_string('invalid_invitekey_title', 'local_treestudyplan'));
print $OUTPUT->header();
// render page for skill level 0 (global)
print "<div class='box errorbox alert alert-danger'>";
print get_string('invalid_invitekey_error','local_treestudyplan');
print "</div>";
print $OUTPUT->footer();
exit;
}
else
{
// Load javascripts and specific css
$PAGE->requires->css(new moodle_url($CFG->wwwroot.'/local/treestudyplan/css/bootstrap-vue.min.css'));
$PAGE->requires->css(new moodle_url($CFG->wwwroot.'/local/treestudyplan/css/devstyles.css'));
$PAGE->requires->js_call_amd('local_treestudyplan/page-myreport', 'init',['invited',$invitekey]);
$student = $DB->get_record('user', array('id' => $invite->user_id));
$PAGE->set_title(get_string('report_invited', 'local_treestudyplan', "{$student->firstname} {$student->lastname}" ));
$PAGE->set_heading(get_string('report_invited', 'local_treestudyplan', "{$student->firstname} {$student->lastname}"));
print $OUTPUT->header();
?>
<div id='root'>
<div class='vue-loader' v-show='false'>
<div class="spinner-border text-primary" role="status">
<span class="sr-only">Loading...</span>
</div>
</div>
<div v-cloak>
<r-report v-model="studyplans" :guestmode="true"></r-report>
</div>
</div>
<?php
print $OUTPUT->footer();
}

View File

@ -0,0 +1,246 @@
<?php
$string['pluginname'] = 'Studyplans';
$string['treestudyplan:editstudyplan'] = "Manage studyplans";
$string['treestudyplan:configure'] = "Configure studyplans";
$string['treestudyplan:viewuserreports'] = "View study plan of others";
$string['report'] = 'Progress report';
$string['report_invited'] = 'Progress report for {$a}';
$string['report_index'] = 'View student progress reports';
$string['manage_invites'] = 'Share studyplan access';
$string['share_report'] = 'Share';
$string['invalid_invitekey_title'] = "Invite key required";
$string['invalid_invitekey_error'] = "You need an invitation to view data via this portal";
$string['invite_description'] = "You can invite your parents or guardians to view your results. Those who receive your sharing link can view your grades until you revoke the invite";
$string['invite_tablecaption'] = "Active invites";
$string['invite_table_empty'] = "No active invites";
$string['invite_tooltip_resend'] = "Resend invite";
$string['invite_tooltip_edit'] = "Edit invite";
$string['invite_tooltip_delete'] = "Delete invite";
$string['invite_tooltip_testlink'] = "See how your results are shown";
$string['invite_button_new'] = "New invite";
$string['invite_confirm_resend'] = 'Are your sure you want to send the invite link to {$a} again?';
$string['invite_confirm_delete'] = 'Are your sure tou want to delete/revoke the invite for {$a}?';
$string['invite_desc_new'] = "Create a new invitation";
$string['invite_desc_edit'] = "Edit an existing invitation";
$string['invite_name'] = "Name";
$string['invite_email'] = "Email";
$string['invite_date'] = "Date";
$string['invite_resent_msg'] = 'The invitation for {$a->name}<{$a->email}> has been sent';
$string['invite_mail_subject'] = 'Shared grade card of {$a->sender}';
$string['invite_mail_text'] = '
<p>Dear {$a->invitee},</p>
<p>I\'d like to invite you to view my study plan and progess.</p>
<p>The link below gives you access at any time to view the most recent results. Feel free to bookmark this link in your browser.</p>
<p>Click the link below to view the study plan:<br>
<a href="{$a->link}">{$a->link}</a></p>
<p>Kind regards,<br>
{$a->sender}</p>
';
$string['view_plan'] = 'View studyplans';
$string['edit_plan'] = 'Edit studyplan';
$string['settingspage'] = 'Tree studyplan settings';
$string['setting_taxonomy_category'] = 'Competency taxonomy for categories';
$string['settingdesc_taxonomy_category'] = 'Indicate the taxonomy used to indicate a competency as a category';
$string['setting_taxonomy_module'] = 'Competency taxonomy for modules';
$string['settingdesc_taxonomy_module'] = 'Indicate the taxonomy used to indicate a competency as a module';
$string['setting_taxonomy_goal'] = 'Competency taxonomy for learning goals';
$string['settingdesc_taxonomy_goal'] = 'Indicate the taxonomy used to indicate a competency as learning goal';
$string['setting_display_heading'] = 'Display';
$string['settingdesc_display_heading'] = 'Study plan display settings';
$string['setting_display_field'] = 'Course display name';
$string['settingdesc_display_field'] = 'Select the field to use for the display name of a course in the studyplan';
$string['studyplan_add'] = 'Add study plan';
$string['studyplan_edit'] = 'Edit study plan';
$string['studyplan_remove'] = 'Remove study plan';
$string['studyplan_confirm_remove'] = 'Are you sure you want to remove studyplan {$a}?';
$string['studyplan_name'] = 'Full name';
$string['studyplan_name_ph'] = '';
$string['studyplan_shortname'] = 'Code';
$string['studyplan_shortname_ph'] = '';
$string['studyplan_description'] = 'Description';
$string['studyplan_description_ph'] = '';
$string['studyplan_slots'] = 'Number of slots in plan';
$string['studyplan_startdate'] = 'Start date of plan';
$string['studyplan_enddate'] = 'End date of plan';
$string['studyplan_noneselected'] = "Pick a studyplan to start editing";
$string['studyplan_select'] = "Study plan:";
$string['studyplan_select_placeholder'] = "Select studyplan";
$string['studyline_add'] = 'Add study line';
$string['studyline_edit'] = 'Edit study line';
$string['studyline_editmode'] = 'Edit study lines';
$string['studyline_remove'] = 'Remove study line';
$string['studyline_confirm_remove'] = 'Are you sure you want to remove studyline {$a}?';
$string['studyline_name'] = 'Full name';
$string['studyline_name_ph'] = '';
$string['studyline_shortname'] = 'Code';
$string['studyline_shortname_ph'] = '';
$string['studyline_color'] = 'Background color';
$string['studyitem_confirm_remove'] = 'Are you sure you want to remove module {$a}?';
$string['editmode_modules_hidden'] = 'Modules hidden in edit mode';
$string['toolbox'] = 'Toolbox';
$string['opentoolbox'] = 'Open toolbox';
$string['toolbar-right'] = 'Show toolbox at the right';
$string['tool-junction'] = 'Junction';
$string['tool-start'] = 'Start/continue';
$string['tool-finish'] = 'Finish';
$string['tool-badge'] = 'Badge';
$string['badges'] = 'Badges';
$string['item_configuration'] = "Configure study item";
$string['select_conditions'] = 'Completion condition';
$string['condition_default'] = 'Default for this item';
$string['condition_all'] = 'All entries need to be completed';
$string['condition_67'] = '2/3 of entries need to be completed';
$string['condition_50'] = 'Half of entries need to be completed';
$string['condition_any'] = 'One or more entries need to be completed';
$string['courses'] = 'Courses';
$string['select_grades'] = 'Grades included in report';
$string['completion_failed'] = "Failed";
$string['completion_incomplete'] = "Not started";
$string['completion_pending'] = "Pending review";
$string['completion_progress'] = "In progress";
$string['completion_completed'] = "Completed";
$string['completion_good'] = "Good";
$string['completion_excellent'] = "Excellent";
$string['cfg_grades'] = 'Configure grade & scale interpretation';
$string['cfg_plans'] = 'Manage study plans';
$string['cfg_grades_desc_head'] = 'Configure how reuslts for goals are presented.';
$string['cfg_grades_desc'] = 'If failed results are supported, the threshold for progression is relevant. All results below this threshold will be considered failed. Empty results or 0 will always be read as not started. <br> The completion threshold always takes preference if it is equal to or lower than th progression threshold.';
$string['cfg_grades_grades'] = 'Configure scale grade interpretation';
$string['cfg_grades_scales'] = 'Configure point grade interpretation';
$string['min_progress'] = 'Progression threshold';
$string['min_completed'] = 'Completion threshold';
$string['grade_points'] = 'Maximum grade points';
$string['view_feedback'] = 'View feedback';
$string["coursetiming_past"] = "Past course";
$string["coursetiming_present"] = "Active course";
$string["coursetiming_future"] = "Upcoming course";
$string["link_myreport"] = "My study plan";
$string["link_viewplan"] = "Study plans";
$string["nav_invited"] = "View study plan by invitation";
$string['associations'] = 'Associations';
$string['associated_cohorts'] = 'Linked cohorts';
$string['associated_users'] = 'Linked users';
$string['associate_cohorts'] = 'Search cohorts to add';
$string['associate_users'] = 'Search users to add';
$string['add_association'] = 'Add';
$string['delete_association'] = 'Delete';
$string['associations_empty'] = 'No active associations';
$string['associations_search'] = 'Search';
$string['users'] = 'Users';
$string['cohorts'] = 'Cohorts';
$string['selected'] = 'Select';
$string['name'] = 'Name';
$string['context'] = 'Category';
$string['error'] = "Error";
$string['ungraded'] = 'Needs grading';
$string['graded'] = 'Graded';
$string['allgraded'] = 'All graded';
$string['unsubmitted'] = 'No submission';
$string['nogrades'] = 'No grades';
$string['unknown'] = 'Grading status unknown';
$string['selectstudent_btn'] = "View student plans";
$string['selectstudent'] = "Choose student";
$string['selectstudent_details'] = "Pick a student from the list below to see their progress in this studyplan";
$string['showoverview'] = "Teacher view";
$string['open'] = "Open";
$string['noenddate'] = "Ongoing";
$string['back'] = "Back";
$string['send'] = "Send";
$string['setting_aggregation_heading'] = "Goal aggregation settings";
$string['settingdesc_aggregation_heading'] = "Configure how completing goals is processed into completing a module";
$string['setting_aggregation_mode'] = "Default aggregation style";
$string['settingdesc_aggregation_mode'] = "Choose a default aggregation style for new study plans";
$string['tristate_aggregator_title'] = 'Manual: Progress/Completed/Excellent Classic';
$string['tristate_aggregator_desc'] = 'Goals are graded progress, completed and excellent. Modules can be configured to require a specific amount of completed goals';
$string['bistate_aggregator_title'] = 'Manual: Completed + Required goals';
$string['bistate_aggregator_desc'] = 'Goals are completed or not (e.g. not started, in progress or failed). Required goals for completion are supported. Studyplans configure how many attained goals result in a completed, good or excellent module score';
$string['core_aggregator_title'] = 'Moodle course completion';
$string['core_aggregator_desc'] = 'Use Moodle core completion';
$string['setting_bistate_heading'] = 'Defaults for Completed + Required goalsn';
$string['settingdesc_bistate_heading'] = 'Set the defaults for this aggregation method';
$string['choose_aggregation_style'] = 'Choose aggregation style';
$string['select_scaleitem'] = 'Choose...';
$string['setting_bistate_thresh_excellent'] = 'Threshold for excellent';
$string['settingdesc_bistate_thresh_excellent'] = 'Minimum percentage of goals completed for result "Excellent"';
$string['setting_bistate_thresh_good'] = 'Threshold for good';
$string['settingdesc_bistate_thresh_good'] = 'Minimum percentage of goals completed for result "Good"';
$string['setting_bistate_thresh_completed'] = 'Threshold for completed';
$string['settingdesc_bistate_thresh_completed'] = 'Minimum percentage of goals completed for result "Completed"';
$string['setting_bistate_support_failed'] = 'Support "Failed" result';
$string['settingdesc_bistate_support_failed'] = 'Whether the result "Failed" is supported or not';
$string['setting_bistate_thresh_progress'] = 'Threshold for progress';
$string['settingdesc_bistate_thresh_progress'] = 'Minimum percentage of goals that should not be failed in order to qualify for progress resullt. <br><strong>Only relevant if "Failed" results are supported</strong>';
$string['setting_bistate_accept_pending_submitted'] = 'Accept submitted but ungraded result as "progress"';
$string['settingdesc_bistate_accept_pending_submitted'] = 'If enabled, submitted but ungraded goals will still count toward progress. If disabled, only graded goals will count';
$string['grade_include'] = "Include";
$string['grade_require'] = "Require";
$string['required_goal'] = "Required goal";
$string['advanced_tools'] = 'Advanced';
$string['confirm_cancel'] = 'Cancel';
$string['confirm_ok'] = 'Confirm';
$string['advanced_converted'] = 'converted';
$string['advanced_skipped'] = 'skipped';
$string['advanced_failed'] = 'failed';
$string['advanced_locked'] = 'locked';
$string['advanced_multiple'] = 'multiple';
$string['advanced_error'] = 'error';
$string['advanced_tools_heading'] = 'Advanced tools';
$string['advanced_warning_title'] = 'Warning';
$string['advanced_warning'] = 'These advanced tools should be used with the utmost care. Make sure you know what you are doing when using them';
$string['advanced_pick_scale'] = 'Pick scale';
$string['advanced_force_scale_title'] = 'Force scales';
$string['advanced_force_scale_desc'] = 'Change all gradable goals associated with this studyplan to scale grading with the selected scale. This will only affect gradables that do not already have grades.';
$string['advanced_force_scale_button'] = 'Apply';
$string['advanced_confirm_header'] = 'Are you sure';
$string['advanced_force_scale_confirm'] = 'Are you sure you want to set all associated gradables to this scale?';
$string["advanced_import_export"] = 'Backup and restore';
$string["advanced_import"] = 'Restore studylines from backup';
$string["advanced_export"] = 'Backup studyplan';
$string["advanced_export_csv"] = 'Export as CSV';
$string["advanced_import_from_file"] = "Open from file";
$string["advanced_purge"] = "Delete";
$string["advanced_purge_expl"] = "Delete the entire studyplan. This is not recoverable";
$string["advanced_course_manipulation_title"] = 'Course manipulation';
$string["advanced_disable_autoenddate_title"] = 'Disable automatic end date';
$string["advanced_disable_autoenddate_desc"] = 'Disable the default on automatic end date function weekly topics have set in all courses in the studyplan';
$string["advanced_disable_autoenddate_button"] = 'Disable';
$string["myreport_teachermode"] = 'Studyplans I am teaching';
$string["aggregation_overall_all"] = "Complete all of the categories";
$string["aggregation_overall_any"] = "Complete one or more of the categories";
$string["aggregation_all"] = "Complete all";
$string["aggregation_any"] = "Complete one or more";

View File

@ -0,0 +1,249 @@
<?php
$string['pluginname'] = 'Studieplannen';
$string['treestudyplan:editstudyplan'] = "Studieplanen beheren";
$string['treestudyplan:configure'] = "Studieplannen configureren";
$string['treestudyplan:viewuserreports'] = "Studieplannen van anderen bekijken";
$string['report'] = 'Voortgangsrapport';
$string['report_invited'] = 'Voortgang van {$a}';
$string['report_index'] = 'Studieplannen van studenten inzien';
$string['manage_invites'] = 'Studieplan delen';
$string['share_report'] = 'Delen';
$string['invalid_invitekey_title'] = "Uitnodiging vereist";
$string['invalid_invitekey_error'] = "Je moet een uitnodiging hebben om via deze portal informatie te bekijken";
$string['invite_description'] = "Je kunt je ouders/verzorgers een uitnodiging sturen waarmee je live inzage in jouw studieplan en voortgang met ze deelt. Degene die de uitnodiging krijgt kan jouw studieplan inzien totdat je de uitnodiging weer intrekt.";
$string['invite_tablecaption'] = "Actieve uitnodigingen";
$string['invite_table_empty'] = "Geen openstaande uitnodigingen";
$string['invite_tooltip_resend'] = "Opnieuw vesturen";
$string['invite_tooltip_edit'] = "Bewerken";
$string['invite_tooltip_delete'] = "Vewijderen";
$string['invite_tooltip_testlink'] = "Bekijk wat de anderen zien";
$string['invite_button_new'] = "Nieuwe uitnodiging";
$string['invite_confirm_resend'] = 'Weet je zeker dat je de uitnodiging opnieuw wil verzenden naar {$a}?';
$string['invite_confirm_delete'] = 'Weet je zeker dat je de uitnodiging voor {$a} wilt intrekken/verwijderen?';
$string['invite_desc_new'] = "Nieuwe uitnodiging maken";
$string['invite_desc_edit'] = "Uitnodiging bewerken";
$string['invite_name'] = "Naam";
$string['invite_email'] = "Email";
$string['invite_date'] = "Datum";
$string['invite_resent_msg'] = 'De uitnodiging naar {$a->name}<{$a->email}> is verzonden';
$string['invite_mail_subject'] = 'Gedeeld rapport van {$a->sender}';
$string['invite_mail_text'] = '
<p>Beste {$a->invitee},</p>
<p>Bij deze wil ik je graag uitnodigen om mijn studieplan en studievoortgang te bekijken.</p>
<p>Via de link hieronder kun je op elk moment het meest recente resultatenoverzicht bekijken. Je kunt deze link ook bewaren als bookmark in je browser.</p>
<p>Klik op de volgende link om het studieplan te bekijken:<br>
<a href="{$a->link}">{$a->link}</a></p>
<p>Met vriendelijke groet,<br>
{$a->sender}</p>
';
$string['view_plan'] = 'Studieplannen bekijken';
$string['edit_plan'] = 'Studieplan bewerken';
$string['settingspage'] = 'Tree studieplan instellingen';
$string['setting_taxonomy_category'] = 'Competentietaxonomie voor categorieën';
$string['settingdesc_taxonomy_category'] = 'Geef aan bij welke taxonomie een competentie categorie module moet worden gezien';
$string['setting_taxonomy_module'] = 'Competentietaxonomie voor modules';
$string['settingdesc_taxonomy_module'] = 'Geef aan bij welke taxonomie een competentie als module moet worden gezien';
$string['setting_taxonomy_goal'] = 'Competentietaxonomie voor leerdoelen';
$string['settingdesc_taxonomy_goal'] = 'Geef aan bij welke taxonomie een competentie als leerdoel moet worden gezien';
$string['setting_display_heading'] = 'Weergave';
$string['settingdesc_display_heading'] = 'Configuratie voor de weergave van de studieplannen';
$string['setting_display_field'] = 'Weergavenaam cursus';
$string['settingdesc_display_field'] = 'Kies welk veld gebruikt moet worden als weergavenaam van een cursus';
$string['studyplan_add'] = 'Nieuw studieplan';
$string['studyplan_edit'] = 'Studieplan bewerken';
$string['studyplan_remove'] = 'Studieplan verwijderen';
$string['studyplan_confirm_remove'] = 'Weet je zeker dat je studieplan {$a} wilt verwijderen?';
$string['studyplan_name'] = 'Naam';
$string['studyplan_name_ph'] = '';
$string['studyplan_shortname'] = 'Code';
$string['studyplan_shortname_ph'] = '';
$string['studyplan_description'] = 'Beschrijving';
$string['studyplan_description_ph'] = '';
$string['studyplan_slots'] = 'Aantal periodes in studieplan';
$string['studyplan_startdate'] = 'Startdatum';
$string['studyplan_enddate'] = 'Einddatum';
$string['studyplan_noneselected'] = "Kies een studieplan uit de lijst";
$string['studyplan_select'] = "Studieplan";
$string['studyplan_select_placeholder'] = "Selecteer studieplan";
$string['studyline_add'] = 'Nieuwe leerlijn';
$string['studyline_edit'] = 'Leerlijn bewerken';
$string['studyline_editmode'] = 'Leerlijnen bewerken';
$string['studyline_remove'] = 'Leerlijn verwijderen';
$string['studyline_confirm_remove'] = 'Weet je zeker dat je leerlijn {$a} wilt verwijderen?';
$string['studyline_name'] = 'Naam';
$string['studyline_name_ph'] = '';
$string['studyline_shortname'] = 'Code';
$string['studyline_shortname_ph'] = '';
$string['studyline_color'] = 'Achtergrondkleur';
$string['studyitem_confirm_remove'] = 'Weet je zeker dat je module {$a} wilt verwijderen?';
$string['editmode_modules_hidden'] = 'Modules verborgen tijdens bewerken';
$string['toolbox'] = 'Toolbox';
$string['opentoolbox'] = 'Toolbox openen';
$string['toolbar-right'] = 'Toolbox rechts tonen';
$string['tool-junction'] = 'Kruispunt';
$string['tool-start'] = 'Start/Vervolgpunt';
$string['tool-finish'] = 'Eindpunt';
$string['tool-badge'] = 'Badge';
$string['badges'] = 'Badges';
$string['item_configuration'] = "Module configureren";
$string['select_conditions'] = 'Voorwaarde voor afronding';
$string['condition_default'] = 'Standaard';
$string['condition_all'] = 'Alle onderdelen moeten afgerond zijn';
$string['condition_67'] = '2/3 van de onderdelen moeten afgerond zijn';
$string['condition_50'] = 'De helft van de onderdelen moeten afgerond zijn';
$string['condition_any'] = 'Minimaal één onderdeel moet afgerond zijn';
$string['courses'] = 'Cursussen';
$string['select_grades'] = 'Resultaten die meetellen';
$string['completion_failed'] = "Onvoldoende";
$string['completion_incomplete'] = "Niet gestart";
$string['completion_pending'] = "Wacht op beoordelen";
$string['completion_progress'] = "In ontwikkeling";
$string['completion_completed'] = "Voltooid";
$string['completion_good'] = "Goed";
$string['completion_excellent'] = "Uitstekend";
$string['cfg_grades'] = 'Configureer betekenis van beoordelingen en schalen';
$string['cfg_plans'] = 'Studieplannen beheren';
$string['cfg_grades_desc_head'] = 'Stel hier in op welke manier resultaten voor doelen worden geinterpreteerd.';
$string['cfg_grades_desc'] = 'Bij gebruik van onvoldoende beoordelingen is de drempelwaarde voor ontwikkeling relevant. Bij waarden onder deze drempel, wordt het resultaat weergegeven als onvoldoende. Een lege beoordeling wordt altijd gezien als niet gestart. <br> Als de drempel voor voltooing kleiner dan of gelijk is aan die voor ontwikkeling, dan krijgt die voor voltooing voorrang';
$string['cfg_grades_grades'] = 'Instellen betekenis resultaatschalen';
$string['cfg_grades_scales'] = 'Instellen betekenis puntenbeoordeling';
$string['min_progress'] = 'Drempelwaarde voor ontwikkeling';
$string['min_completed'] = 'Drempelwaarde voor voltooid';
$string['grade_points'] = 'Maximum aantal points';
$string['view_feedback'] = 'Feedback bekijken';
$string["coursetiming_past"] = "Eerdere cursus";
$string["coursetiming_present"] = "Actieve curus";
$string["coursetiming_future"] = "Toekomstige cursus";
$string["link_myreport"] = "Mijn studieplan";
$string["link_viewplan"] = "Studieplannen";
$string["nav_invited"] = "Studieplan op uitnodiging bekijken";
$string['associations'] = 'Koppelingen';
$string['associated_cohorts'] = 'Gekoppelde site-groepen';
$string['associated_users'] = 'Gekoppelde gebruikers';
$string['associate_cohorts'] = 'Zoek om te koppelen';
$string['associate_users'] = 'Zoek om te koppelen';
$string['add_association'] = 'Toevoegen';
$string['delete_association'] = 'Verwijderen';
$string['associations_empty'] = 'Geen koppelingen';
$string['associations_search'] = 'Zoeken';
$string['users'] = 'Gebruikers';
$string['cohorts'] = 'Site-groepen';
$string['selected'] = 'Kies';
$string['name'] = 'Naam';
$string['context'] = 'Categorie';
$string['error'] = "Fout";
$string['ungraded'] = 'Nog beoordelen';
$string['graded'] = 'Beoordeeld';
$string['allgraded'] = 'Alles beoordeeld';
$string['unsubmitted'] = 'Geen inzendingen';
$string['nogrades'] = 'Geen items';
$string['unknown'] = 'Beoordelingen onbekend';
$string['selectstudent_btn'] = "Bekijk studentenvoortgang";
$string['selectstudent'] = "Kies een student";
$string['selectstudent_details'] = "Kies een student uit de lijst om zijn/haar voortgang in dit studieplan te bekijken";
$string['showoverview'] = "Docentenweergave";
$string['open'] = "Openen";
$string['noenddate'] = "&infin;";
$string['back'] = "Terug";
$string['send'] = "Verzenden";
$string['setting_aggregation_heading'] = "Verzamelinstellingen voor leerdoelen";
$string['settingdesc_aggregation_heading'] = "Stel hier in hoe het behalen van leerdoelen wordt verwerkt in behalen van modules";
$string['setting_aggregation_mode'] = "Standaard verwerkingsmethode";
$string['settingdesc_aggregation_mode'] = "Kies de standaard verwerkingsmethode voor nieuwe studieplannen";
$string['tristate_aggregator_title'] = 'Handmatig: Voortgang/Behaald/Goed Stijl';
$string['tristate_aggregator_desc'] = 'Leerdoelen kunnen reviseren, voltooid of uitstekend beoordeeld worden. Per modules kan worden ingesteld wel deel (standaard 50%) nodig is voor behalen van module';
$string['bistate_aggregator_title'] = 'Handimatig: Behaald + Vereiste leerdoelen';
$string['bistate_aggregator_desc'] = 'Doelen zijn behaald of niet (o.a. niet gestart, bezig en niet behaald). Vereiste leerdoelen zijn mogelijk. Per studieplan wordt ingesteld bij hoeveel doelen het resultaat voltooid, goed of uitstekend is.';
$string['core_aggregator_title'] = 'Moodle cursusvoltooiing';
$string['core_aggregator_desc'] = 'Gebruik de ingesteld cursusvoltooiing';
$string['setting_bistate_heading'] = 'Standaardwaarden voor Behaald + Vereidte leerdoelen ';
$string['settingdesc_bistate_heading'] = 'Stel de standaardwaarden in voor deze verzamelmethode';
$string['choose_aggregation_style'] = 'Kies berkening van eindresultaten';
$string['select_scaleitem'] = 'Kies...';
$string['setting_bistate_thresh_excellent'] = 'Drempelwaarde voor uitstekend (%)';
$string['settingdesc_bistate_thresh_excellent'] = 'Minimumpercentage of goals completed for result "Excellent"';
$string['setting_bistate_thresh_good'] = 'Drempelwaarde voor goed (%)';
$string['settingdesc_bistate_thresh_good'] = 'Minimum percentage of goals completed for result "Good"';
$string['setting_bistate_thresh_completed'] = 'Drempelwaarde voor voltooid (%)';
$string['settingdesc_bistate_thresh_completed'] = 'Minimumpercentage of goals completed for result "Completed"';
$string['setting_bistate_support_failed'] = 'Onvoldoende ingeschakeld';
$string['settingdesc_bistate_support_failed'] = 'Vink aan om "Onvoldoende" weer te kunnen geven als resultaat voor een module';
$string['setting_bistate_thresh_progress'] = 'Drempelwaarde voor "in ontwikkeling" (%)';
$string['settingdesc_bistate_thresh_progress'] = 'Minimumpercentage van doelen die niet onvoldoende mogen zijn voor resultaat "in ontwikkeling".<br><strong>Alleen van toepassing als Onvoldoende resultaten zijn ingeschakeld</strong>';
$string['setting_bistate_accept_pending_submitted'] = 'Accepteer nog niet beoordeelde doelen als "in ontwikkeling"';
$string['settingdesc_bistate_accept_pending_submitted'] = 'Neem leerdoelen waarbij bewijs is ingeleverd, maar wat nog niet is beoordeeld mee als "in ontwikkeling", zolang er nog geen beoordeling is';
$string['grade_include'] = "Doel";
$string['grade_require'] = "Verplicht";
$string['required_goal'] = "Verplicht leerdoel";
$string['advanced_tools'] = 'Expert';
$string['confirm_cancel'] = 'Annuleren';
$string['confirm_ok'] = 'Doorgaan';
$string['advanced_converted'] = 'verwerkt';
$string['advanced_skipped'] = 'overgeslagen';
$string['advanced_failed'] = 'mislukt';
$string['advanced_locked'] = 'locked';
$string['advanced_multiple'] = 'meerdere';
$string['advanced_error'] = 'fout';
$string['advanced_tools_heading'] = 'Expert tools';
$string['advanced_warning_title'] = 'Waarschuwing';
$string['advanced_warning'] = 'De expert tools zijn krachtige hulpmiddelen die met beleid moeten worden gebruikt. Gebruik deze alleen als je zeker weet hoe je deze moet gebruiken.';
$string['advanced_pick_scale'] = 'Kies resultaatschaal';
$string['advanced_force_scale_title'] = 'Schaal overschrijven';
$string['advanced_force_scale_desc'] = 'Stel bij alle aan dit studieplan als leerdoel gekoppelde activiteiten de beoordeling in op de geselecteerde resultaatschaal. Dit werkt alleen als er nog geen resultaten zijn toegekend.';
$string['advanced_force_scale_button'] = 'Toepassen';
$string['advanced_confirm_header'] = 'Weet je het zeker?';
$string['advanced_force_scale_confirm'] = 'Weet je zeker dat je de beoordeling van alle gekoppelde activiteiten wilt omzetten?';
$string["advanced_import_export"] = 'Backup and terugzetten';
$string["advanced_import"] = 'Studieplan invullan vanuit backup';
$string["advanced_export"] = 'Backup downloaden';
$string["advanced_export_csv"] = 'Export als CSV';
$string["advanced_import_from_file"] = "Vanuit bestand";
$string["advanced_purge"] = "Verwijderen";
$string["advanced_purge_expl"] = "Dit hele studieplan permanent verwijderen.";
$string["advanced_course_manipulation_title"] = 'Cursussen aanpassen';
$string["advanced_disable_autoenddate_title"] = 'Automatische einddatum uitschakelen';
$string["advanced_disable_autoenddate_desc"] = 'Schakel de optie automatische einddatum uit in alle cursussen in dit studieplan';
$string["advanced_disable_autoenddate_button"] = 'Uitschakelen';
$string["myreport_teachermode"] = 'Studieplannen waar ik les aan geef';
$string["aggregation_overall_all"] = "Behaal alle categorieë";
$string["aggregation_overall_any"] = "Behaal één of meer categorieën";
$string["aggregation_all"] = "Alles behalen";
$string["aggregation_any"] = "Eén of meer behalen";

280
lib.php Normal file
View File

@ -0,0 +1,280 @@
<?php
require_once($CFG->dirroot.'/course/modlib.php');
use \local_treestudyplan\studyplan;
defined('MOODLE_INTERNAL') || die();
function local_treestudyplan_unit_get_editor_options($context) {
global $CFG;
return array('subdirs'=>1, 'maxbytes'=>$CFG->maxbytes, 'maxfiles'=>-1, 'changeformat'=>1, 'context'=>$context, 'noclean'=>1, 'trusttext'=>0);
}
function local_treestudyplan_extend_navigation(global_navigation $navigation) {
global $CFG, $PAGE, $COURSE, $USER;
$systemcontext = context_system::instance();
if($USER->id > 1) // Don't show if user is not logged in (id == 0) or is guest user (id == 1)
{
$userstudyplans = studyplan::find_for_user($USER->id);
if(!empty($userstudyplans))
{
// create studyplan node
$node = navigation_node::create(
get_string("link_myreport","local_treestudyplan"),
new moodle_url($CFG->wwwroot . "/local/treestudyplan/myreport.php", array()),
global_navigation::TYPE_USER ,
null,
"local_treestudyplan_myreport",
new pix_icon("myreport", '', 'local_treestudyplan')
);
$node->showinflatnavigation = true;
// create invitenode node
$invitenode = navigation_node::create(
get_string("manage_invites","local_treestudyplan"),
new moodle_url($CFG->wwwroot . "/local/treestudyplan/invitations.php", array()),
global_navigation::TYPE_USER ,
null,
"local_treestudyplan_invitemgmt",
new pix_icon("invitemgmt", '', 'local_treestudyplan')
);
$invitenode->showinflatnavigation = false;
$node->add_node($invitenode);
$navigation->add_node($node,'mycourses');
}
if(has_capability('local/treestudyplan:viewuserreports',context_system::instance()))
{
$node = navigation_node::create(
get_string("link_viewplan","local_treestudyplan"),
new moodle_url($CFG->wwwroot . "/local/treestudyplan/view-plan.php", array()),
global_navigation::TYPE_USER ,
null,
"local_treestudyplan_viewplan",
new pix_icon("viewplans", '', 'local_treestudyplan')
);
$node->showinflatnavigation = true;
$navigation->add_node($node,'mycourses');
}
if(has_capability('local/treestudyplan:editstudyplan',context_system::instance()))
{
$node = navigation_node::create(
get_string("cfg_plans","local_treestudyplan"),
new moodle_url($CFG->wwwroot . "/local/treestudyplan/edit-plan.php", array()),
global_navigation::TYPE_USER ,
null,
"local_treestudyplan_editplan",
new pix_icon("viewplans", '', 'local_treestudyplan')
);
$navigation->add_node($node,'mycourses');
}
}
// create invitenode node
$invitenode = navigation_node::create(
get_string("nav_invited","local_treestudyplan"),
new moodle_url($CFG->wwwroot . "/local/treestudyplan/invited.php", array()),
global_navigation::TYPE_USER ,
null,
"local_treestudyplan_invitemgmt",
new pix_icon("nav_invited", '', 'local_treestudyplan')
);
$invitenode->showinflatnavigation = false;
$navigation->add_node($invitenode,'mycourses');
// Add navigation node to course category pages
$categoryid = optional_param('categoryid', 0, PARAM_INT); // Category id
}
function local_treestudyplan_extend_navigation_category_settings($navigation, context_coursecat $coursecategorycontext) {
global $CFG, $PAGE;
$categoryid = $coursecategorycontext->instanceid;
if(has_capability('local/treestudyplan:editstudyplan',$coursecategorycontext)){
$node = $navigation->add(
get_string('treestudyplan:editstudyplan',"local_treestudyplan"),
new moodle_url($CFG->wwwroot . "/local/treestudyplan/edit-plan.php", ["categoryid"=>$categoryid]),
global_navigation::TYPE_CATEGORY,
null,
"local_treestudyplan_editplan",
new pix_icon("editplans", '', 'local_treestudyplan')
);
//$node->make_active();
}
if(has_capability('local/treestudyplan:viewuserreports',$coursecategorycontext)){
$node = $navigation->add(
get_string('link_viewplan',"local_treestudyplan"),
new moodle_url($CFG->wwwroot . "/local/treestudyplan/view-plan.php", ["categoryid"=>$categoryid]),
global_navigation::TYPE_CATEGORY,
null,
"local_treestudyplan_viewplan",
new pix_icon("viewplans", '', 'local_treestudyplan')
);
//$node->make_active();
}
}
function local_treestudyplan_get_fontawesome_icon_map() {
// Create the icon map with the icons which are used in any case.
$iconmapping = [
'local_treestudyplan:myreport' => 'fa-vcard',
'local_treestudyplan:editplans' => 'fa-share-alt',
'local_treestudyplan:viewplans' => 'fa-share-alt',
];
return $iconmapping;
}
/**
* Helper function to reset the icon system used as updatecallback function when saving some of the plugin's settings.
*/
function local_treestudyplan_reset_fontawesome_icon_map() {
// Reset the icon system cache.
// There is the function \core\output\icon_system::reset_caches() which does seem to be only usable in unit tests.
// Thus, we clear the icon system cache brutally.
$cache = \cache::make('core', 'fontawesomeiconmapping');
$cache->delete('mapping');
// And rebuild it brutally.
$instance = \core\output\icon_system::instance(\core\output\icon_system::FONTAWESOME);
$instance->get_icon_name_map();
}
function local_treestudyplan_send_invite($inviteid)
{
global $DB,$USER,$CFG;
$invite = $DB->get_record("local_treestudyplan_invit", array('id' => $inviteid));
$noreply = 'noreply@' . get_host_from_url($CFG->wwwroot);
$mailer = get_mailer();
$mailer->setFrom($noreply,"{$USER->firstname} {$USER->lastname}");
$mailer->addAddress($invite->email,$invite->name);
$mailer->addReplyTo($USER->email,"{$USER->firstname} {$USER->lastname}");
$invitehref = $CFG->wwwroot."/local/treestudyplan/invited.php?key={$invite->invitekey}";
$data = [ 'permissions'=> '',
'invitee' => $invite->name,
'sender' => "{$USER->firstname} {$USER->lastname}",
'link' => $invitehref];
if($invite->allow_details || $invite->allow_calendar || $invite->allow_badges)
{
$data['permissions'] = get_string('invite_mail_permissions','local_treestudyplan');
$data['permissions'] .= "<ul>\n";
if($invite->allow_details )
{
$data['permissions'] .= "<li>".get_string('invite_allow_details','local_treestudyplan')."</li>\n";
}
if($invite->allow_calendar)
{
$data['permissions'] .= "<li>".get_string('invite_allow_calendar','local_treestudyplan')."</li>\n";
}
if($invite->allow_badges)
{
$data['permissions'] .= "<li>".get_string('invite_allow_badges','local_treestudyplan')."</li>\n";
}
$data['permissions'] .= "</ul></p>\n";
}
$body = get_string('invite_mail_text','local_treestudyplan',$data);
$subject = get_string('invite_mail_subject','local_treestudyplan', $data);
$html = "
<!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'>
<html xmlns='http://www.w3.org/1999/xhtml'>
<head>
<meta http-equiv='Content-Type' content='text/html; charset=UTF-8' />
<title>{$subject}</title>
<meta name='viewport' content='width=device-width, initial-scale=1.0'/>
</head>
<body>
{$body}
</body>
</html>";
$mailer->isHTML(true);
$mailer->Subject = $subject;
$mailer->Body = $html;
$mailer->AltBody = strip_tags($body);
$mailer->send();
}
function local_treestudyplan_find_cohortmembers($cohortid)
{
global $DB;
// By default wherecondition retrieves all users except the deleted, not confirmed and guest.
$params['cohortid'] = $cohortid;
$sql = "SELECT * FROM {user} u
JOIN {cohort_members} cm ON (cm.userid = u.id AND cm.cohortid = :cohortid)
WHERE u.suspended = 0 AND u.id > 1
ORDER BY u.lastname
";
$availableusers = $DB->get_records_sql($sql,$params);
return $availableusers;
}
function local_treestudyplan_get_cohort_path($cohort)
{
$cohortcontext = context::instance_by_id($cohort->contextid);
if($cohortcontext && $cohortcontext->id != SYSCONTEXTID)
{
$ctxpath = array_map(
function($ctx){ return $ctx->get_context_name(false);},
$cohortcontext->get_parent_contexts(true)
);
array_pop($ctxpath); // pop system context off the list
$ctxpath = array_reverse($ctxpath);
$ctxpath[] = $cohort->name;
return implode(" / ",$ctxpath);
}
else
{
return $cohort->name;
}
}
function local_treestudyplan_output_fragment_mod_edit_form($args){
global $CFG;
global $DB;
$args = (object)$args;
$context = $args->context;
if(empty($args->cmid)){
return "RANDOM!";
}
// Check the course module exists.
$cm = \get_coursemodule_from_id('', $args->cmid, 0, false, MUST_EXIST);
// Check the course exists.
$course = \get_course($cm->course);
// require_login
require_login($course, false, $cm); // needed to setup proper $COURSE
list($cm, $context, $module, $data, $cw) = \get_moduleinfo_data($cm, $course);
$modmoodleform = "$CFG->dirroot/mod/$module->name/mod_form.php";
if (file_exists($modmoodleform)) {
require_once($modmoodleform);
} else {
print_error('noformdesc');
}
$mformclassname = 'mod_'.$module->name.'_mod_form';
$mform = new $mformclassname($data, $cw->section, $cm, $course);
$mform->set_data($data);
return $mform->render();
}

46
myreport-embed.php Normal file
View File

@ -0,0 +1,46 @@
<?php
require_once("../../config.php");
require_once($CFG->libdir.'/weblib.php');
use local_treestudyplan;
$systemcontext = context_system::instance();
$PAGE->set_url("/local/treestudyplan/myreport.php",array());
require_login();
$PAGE->set_pagelayout('embedded');
$PAGE->set_context($systemcontext);
$PAGE->set_title(get_string('report_invited','local_treestudyplan',"{$USER->firstname} {$USER->lastname}"));
$PAGE->set_heading(get_string('report_invited','local_treestudyplan',"{$USER->firstname} {$USER->lastname}"));
// Load javascripts and specific css
$PAGE->requires->css(new moodle_url($CFG->wwwroot.'/local/treestudyplan/css/bootstrap-vue.min.css'));
$PAGE->requires->css(new moodle_url($CFG->wwwroot.'/local/treestudyplan/css/devstyles.css'));
$PAGE->requires->js_call_amd('local_treestudyplan/page-myreport', 'init');
//Local translate function
function t($str, $param=null, $plugin='local_treestudyplan'){
print get_string($str,$plugin,$param);
}
print $OUTPUT->header();
?>
<div class="m-buttonbar" style="margin-bottom: 1em; text-align: right;">
<a class="btn btn-primary" href="invitations.php" id="manage_invites"><i class="fa fa-share"></i> <?php t('manage_invites'); ?></a>
</div>
<div id='root'>
<div class='vue-loader' v-show='false'>
<div class="spinner-border text-primary" role="status">
<span class="sr-only">Loading...</span>
</div>
</div>
<div v-cloak>
<r-report v-model="studyplans"></r-report>
</div>
</div>
<?php
print $OUTPUT->footer();

57
myreport.php Normal file
View File

@ -0,0 +1,57 @@
<?php
require_once("../../config.php");
require_once($CFG->libdir.'/weblib.php');
use local_treestudyplan;
$systemcontext = context_system::instance();
$PAGE->set_url("/local/treestudyplan/myreport.php",array());
require_login();
$PAGE->set_pagelayout('base');
$PAGE->set_context($systemcontext);
$teachermode = has_capability("local/treestudyplan:viewuserreports",$systemcontext);
if($teachermode){
$PAGE->set_title(get_string('myreport_teachermode','local_treestudyplan'));
$PAGE->set_heading(get_string('myreport_teachermode','local_treestudyplan'));
} else {
$PAGE->set_title(get_string('report_invited','local_treestudyplan',"{$USER->firstname} {$USER->lastname}"));
$PAGE->set_heading(get_string('report_invited','local_treestudyplan',"{$USER->firstname} {$USER->lastname}"));
}
// Load javascripts and specific css
$PAGE->requires->css(new moodle_url($CFG->wwwroot.'/local/treestudyplan/css/bootstrap-vue.min.css'));
$PAGE->requires->css(new moodle_url($CFG->wwwroot.'/local/treestudyplan/css/devstyles.css'));
$PAGE->requires->js_call_amd('local_treestudyplan/page-myreport', 'init',[$teachermode?'teaching':'myreport']);
//Local translate function
function t($str, $param=null, $plugin='local_treestudyplan'){
print get_string($str,$plugin,$param);
}
print $OUTPUT->header();
?>
<div class="m-buttonbar" style="margin-bottom: 1em; text-align: right;">
<?php if(!$teachermode){ ?>
<a class="btn btn-primary" href="invitations.php" id="manage_invites"><i class="fa fa-share"></i> <?php t('manage_invites'); ?></a>
<?php } ?>
</div>
<div id='root'>
<div class='vue-loader' v-show='false'>
<div class="spinner-border text-primary" role="status">
<span class="sr-only">Loading...</span>
</div>
</div>
<div v-cloak>
<r-report v-model="studyplans" <?php print ($teachermode?"teachermode":""); ?> ></r-report>
</div>
</div>
<?php
print $OUTPUT->footer();

168
reports.php Normal file
View File

@ -0,0 +1,168 @@
<?php
// If not, assume the cwd is not symlinked and proceed as we are used to
require_once("../../config.php");
require_once($CFG->libdir.'/weblib.php');
require_once($CFG->dirroot.'/grade/querylib.php');
require_once($CFG->dirroot.'/cohort/lib.php');
require_once($CFG->dirroot.'/cohort/locallib.php');
require_once("lib.php");
use local_treestudyplan;
//admin_externalpage_setup('major');
$systemcontext = context_system::instance();
require_login();
require_capability('local/treestudyplan:viewothercards',$systemcontext);
$PAGE->set_pagelayout('base');
$PAGE->set_context($systemcontext);
$PAGE->set_title(get_string('report_index','local_treestudyplan'));
$PAGE->set_heading(get_string('report_index','local_treestudyplan'));
// Load javascripts
//$PAGE->requires->js_call_amd('local_treestudyplan/fixlinks', 'init', ['div.tab-content']);
$PAGE->requires->js_call_amd('block_gradelevel/renderbadge', 'init');
$PAGE->requires->js_call_amd('local_treestudyplan/report', 'init');
//$PAGE->requires->css(new moodle_url($CFG->wwwroot.'/local/treestudyplan/css/eqstyles.css'));
$PAGE->requires->css(new moodle_url($CFG->wwwroot.'/local/treestudyplan/css/bootstrap-toggle.min.css'));
$userid = null;
$studentid = optional_param('studentid', '', PARAM_INT);
$cohortid = optional_param('cohortid', '', PARAM_INT);
if(!empty($studentid)){
$PAGE->set_url("/local/treestudyplan/reports.php",array('studentid' => $studentid));
$student = $DB->get_record("user",['id' => $studentid]);
$userlist = [];
$nextstudent = null;
$prevstudent = null;
if(!empty($cohortid))
{
$cohort = $DB->get_record("cohort",['id' => $cohortid]);
$userlist = array_values(local_treestudyplan_find_cohortmembers($cohortid));
for($i = 0; $i < count($userlist); $i++)
{
if($userlist[$i]->userid == $studentid)
{
if($i > 0)
{
$prevstudent = (object)['id' => $userlist[$i - 1]->userid, 'name' => "{$userlist[$i - 1]->firstname} {$userlist[$i - 1]->lastname}"];
}
if($i < count($userlist) - 1)
{
$nextstudent = (object)['id' => $userlist[$i + 1]->userid, 'name' => "{$userlist[$i + 1]->firstname} {$userlist[$i + 1]->lastname}"];
}
break;
}
}
$cohortpath = local_treestudyplan_get_cohort_path($cohort);
$PAGE->set_title(get_string('report_invited','local_treestudyplan',"{$cohortpath}: {$student->firstname} {$student->lastname}"));
$PAGE->set_heading(get_string('report_invited','local_treestudyplan',"{$cohortpath}: {$student->firstname} {$student->lastname}"));
}
else
{
$PAGE->set_title(get_string('report_invited','local_treestudyplan',"$cohort->{$student->firstname} {$student->lastname}"));
$PAGE->set_heading(get_string('report_invited','local_treestudyplan',"{$student->firstname} {$student->lastname}"));
}
$gradewriter = new local_treestudyplan_cardwriter($studentid,true);
$badgewriter = new local_treestudyplan_badgewriter($studentid);
$calendarwriter = new local_treestudyplan_calendarwriter($studentid);
print $OUTPUT->header();
print "<div class='m-buttonbar' style='margin-bottom: 1.5em; height: 1em;'>";
if(isset($prevstudent))
{
print "<a style='float:left;' href='/local/treestudyplan/reports.php?studentid={$prevstudent->id}&cohortid={$cohortid}'><i class='fa fa-arrow-left'></i> {$prevstudent->name} </a>";
}
if(isset($nextstudent))
{
print "<a style='float:right;' href='/local/treestudyplan/reports.php?studentid={$nextstudent->id}&cohortid={$cohortid}'>{$nextstudent->name} <i class='fa fa-arrow-right'></i></a>";
}
print "</div>";
print "<ul class='nav nav-tabs' role='tablist'>";
print "<li class='nav-item'><a class='nav-link active' href='#link-report' data-toggle='tab' role='tab'>".get_string('nav_report','local_treestudyplan')."</a></li>";
print "<li class='nav-item'><a class='nav-link ' href='#link-badges' data-toggle='tab' role='tab'>".get_string('nav_badges','local_treestudyplan')."</a></li>";
print "<li class='nav-item'><a class='nav-link ' href='#link-calendar' data-toggle='tab' role='tab'>".get_string('nav_calendar','local_treestudyplan')."</a></li>";
print "</ul>";
print "<div class='tab-content mt-3'>";
print "<div class='tab-pane active' id='link-report' data-toggle='tab' role='tab'>";
print $gradewriter->render(true,false);
print "</div>";
print "<div class='tab-pane ' id='link-badges' data-toggle='tab' role='tab'>";
print $badgewriter->render();
print "</div>";
print "<div class='tab-pane' id='link-calendar' data-toggle='tab' role='tab'>";
print $calendarwriter->render();
print "</div>";
print "</div>";
print $OUTPUT->footer();
}
else {
$PAGE->set_url("/local/treestudyplan/reports.php",array());
// show student picker
$cohortlist = $DB->get_records("cohort");
$cohorts = [];
foreach($cohortlist as $c)
{
if($c->visible)
{
$cohortcontext = context::instance_by_id($c->contextid);
if($cohortcontext) // TODO: add check if user has rights in this context
{
$users = local_treestudyplan_find_cohortmembers($c->id);
$cohorts[$c->id] = (object)[
'id' => $c->id,
'cohort' => $c,
'name' => $c->name,
'path' => local_treestudyplan_get_cohort_path($c),
'users' => $users,
];
}
}
}
print $OUTPUT->header();
usort($cohorts,function($a,$b){
return $a->path <=> $b->path;
});
$regex = get_config('local_treestudyplan', 'cohortidregex');
foreach($cohorts as $c)
{
$m = [];
if(preg_match("/".$regex."/",$c->cohort->idnumber,$m) && $c->cohort->visible)
{
print "<legend class='collapse-header'><a data-toggle='collapse' class='collapsed' href='#cohort-{$c->id}' role='button'>{$c->path}</a></legend>";
print "<div id='cohort-{$c->id}' class='collapse'>";
print "<div class='card card-body'><ul class='gradecardlist'>";
foreach($c->users as $u)
{
print "<li class='gradestudent'>";
print "<a href='/local/treestudyplan/reports.php?studentid={$u->userid}&cohortid={$c->id}'>{$u->firstname} {$u->lastname}</a>";
print "</li>";
}
print "</ul>";
print "</div></div>";
}
}
print "</div>";
print $OUTPUT->footer();
}

145
settings.php Normal file
View File

@ -0,0 +1,145 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Local plugin "Boost navigation fumbling" - Settings
*
* @package local_chronotable
* @copyright 2017 Alexander Bias, Ulm University <alexander.bias@uni-ulm.de>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
use local_treestudyplan\aggregator;
if ($hassiteconfig) {
$taxonomies = core_competency\competency_framework::get_taxonomies_list();
// Create admin settings category.
$ADMIN->add('courses', new admin_category('local_treestudyplan',
get_string('pluginname', 'local_treestudyplan', null, true)));
// Settings page: Root nodes.
$page = new admin_settingpage('local_treestudyplan_settings',
get_string('settingspage', 'local_treestudyplan', null, true));
// GOAL AGGREGATION SETTINGS
$page->add(new admin_setting_heading('local_treestudyplan/aggregation_heading',
get_string('setting_aggregation_heading', 'local_treestudyplan'),
get_string('settingdesc_aggregation_heading', 'local_treestudyplan')
));
$aggregators = [];
foreach(aggregator::list() as $a){
$aggregators[$a] = get_string("{$a}_aggregator_title",'local_treestudyplan', null, true);
}
$page->add(new admin_setting_configselect('local_treestudyplan/aggregation_mode',
get_string('setting_aggregation_mode', 'local_treestudyplan'),
get_string('settingdesc_aggregation_mode', 'local_treestudyplan'),
"bistate",
$aggregators
));
// DISPLAY COURSE INFO SETTINGS
$page->add(new admin_setting_heading('local_treestudyplan/display_heading',
get_string('setting_display_heading', 'local_treestudyplan'),
get_string('settingdesc_display_heading', 'local_treestudyplan')
));
$displayfields = ["shortname" => get_string("shortname"), "idnumber" => get_string("idnumber")];
$handler = \core_customfield\handler::get_handler('core_course', 'course');
foreach($handler->get_categories_with_fields() as $cat){
$catname = $cat->get_formatted_name();
foreach($cat->get_fields() as $field) {
$fieldname = $field->get_formatted_name();
$fieldid = $field->get("shortname");
$displayfields["customfield_".$fieldid] = $catname.": ".$fieldname;
}
}
$page->add(new admin_setting_configselect('local_treestudyplan/display_field',
get_string('setting_display_field', 'local_treestudyplan'),
get_string('settingdesc_display_field', 'local_treestudyplan'),
"shortname",
$displayfields
));
// BISTATE AGGREGATON DEFAULTS
$page->add(new admin_setting_heading('local_treestudyplan/bistate_aggregation_heading',
get_string('setting_bistate_heading', 'local_treestudyplan'),
get_string('settingdesc_bistate_heading', 'local_treestudyplan')
));
$page->add(new admin_setting_configtext('local_treestudyplan/bistate_thresh_excellent',
get_string('setting_bistate_thresh_excellent', 'local_treestudyplan'),
get_string('settingdesc_bistate_thresh_excellent', 'local_treestudyplan'),
"100",
PARAM_INT
));
$page->add(new admin_setting_configtext('local_treestudyplan/bistate_thresh_good',
get_string('setting_bistate_thresh_good', 'local_treestudyplan'),
get_string('settingdesc_bistate_thresh_good', 'local_treestudyplan'),
"80",
PARAM_INT
));
$page->add(new admin_setting_configtext('local_treestudyplan/bistate_thresh_completed',
get_string('setting_bistate_thresh_completed', 'local_treestudyplan'),
get_string('settingdesc_bistate_thresh_completed', 'local_treestudyplan'),
"66",
PARAM_INT
));
$page->add(new admin_setting_configcheckbox('local_treestudyplan/bistate_support_failed',
get_string('setting_bistate_support_failed', 'local_treestudyplan'),
get_string('settingdesc_bistate_support_failed', 'local_treestudyplan'),
True,
));
$page->add(new admin_setting_configtext('local_treestudyplan/bistate_thresh_progress',
get_string('setting_bistate_thresh_progress', 'local_treestudyplan'),
get_string('settingdesc_bistate_thresh_progress', 'local_treestudyplan'),
"33",
PARAM_INT
));
$page->add(new admin_setting_configcheckbox('local_treestudyplan/bistate_accept_pending_submitted',
get_string('setting_bistate_accept_pending_submitted', 'local_treestudyplan'),
get_string('settingdesc_bistate_accept_pending_submitted', 'local_treestudyplan'),
False,
));
// Add settings page to the admin settings category.
$ADMIN->add('local_treestudyplan', $page);
// Cohort config page
$ADMIN->add('local_treestudyplan', new admin_externalpage(
'local_treestudyplan_editplans',
get_string('cfg_plans', 'local_treestudyplan', null, true),
$CFG->wwwroot . '/local/treestudyplan/edit-plan.php'));
// Cohort config page
$ADMIN->add('local_treestudyplan', new admin_externalpage(
'local_treestudyplan_gradeconfig',
get_string('cfg_grades', 'local_treestudyplan', null, true),
$CFG->wwwroot . '/local/treestudyplan/cfg_grades.php'));
}

73
styles.css Normal file
View File

@ -0,0 +1,73 @@
/* stylelint-disable length-zero-no-unit, color-hex-case, color-hex-length*/
.path-local-treestudyplan div.tab-pane:target {
margin-top: 0px;
}
.path-local-treestudyplan [v-cloak] {
visibility: hidden;
}
.path-local-treestudyplan .vue-loader {
width: 32px;
margin: auto;
}
/*******************
*
* Invite manager
*
********************/
.path-local-treestudyplan table.m-manage_invites {
margin-bottom: 2em;
width: 80%;
}
.path-local-treestudyplan table.m-manage_invites thead tr {
background-color: #009EE2;
color: white;
}
.path-local-treestudyplan table.m-manage_invites tbody tr:nth-child(even) {
background-color: #D4EDFC;
}
.path-local-treestudyplan table.m-manage_invites tbody tr:nth-child(odd) {
background-color: white;
}
.path-local-treestudyplan table.m-manage_invites tbody td {
padding: 5px;
}
.path-local-treestudyplan table.m-manage_invites tbody td[data-field=name] {
width: 20em;
}
.path-local-treestudyplan table.m-manage_invites tbody td[data-field=email] {
width: 10em;
}
.path-local-treestudyplan table.m-manage_invites tbody td[data-field=date] {
width: 10em;
}
.path-local-treestudyplan table.m-manage_invites tbody td[data-field=allow_details],
.path-local-treestudyplan table.m-manage_invites tbody td[data-field=allow_calendar],
.path-local-treestudyplan table.m-manage_invites tbody td[data-field=allow_badges] {
width: 5em;
padding-left: 1em;
}
.path-local-treestudyplan table.m-manage_invites tbody td[data-field=control] {
width: 150px;
}
.path-local-treestudyplan table.m-manage_invites tbody td[data-field=control] a {
margin-left: 7px;
margin-right: 7px;
}
.path-admin-local-treestudyplan table.m-cohortgrouptable,
.path-admin-local-treestudyplan table.m-roomtable {
width: auto;
}

View File

@ -0,0 +1,182 @@
<?php
namespace local_treestudyplan;
use \local_treestudyplan\local\aggregators\bistate_aggregator;
class bistate_aggregator_test extends \basic_testcase {
private function goalaggregation_test($configstr, $outcome, $completions,$required){
$ag = new bistate_aggregator($configstr);
// test aggregation with the required data
$result = $ag->aggregate_binary_goals($completions,$required);
// assert if the test is succesful
$this->assertEquals(completion::label($outcome),completion::label($result));
}
private function junctionaggregation_test($configstr, $outcome, $completions){
$ag = new bistate_aggregator($configstr);
// test aggregation with the minimum required data
$result = $ag->aggregate_junction($completions);
// assert if the test is succesful
$this->assertEquals(completion::label($outcome),completion::label($result));
}
public function test_goalaggregation_0() {
$this->goalaggregation_test(
'{"thresh_excellent":100,"thresh_good":85,"thresh_completed":66,"thresh_progress":25,"use_failed":true,"accept_pending_as_submitted":true}',
completion::INCOMPLETE,
[
],
[],
);
}
public function test_goalaggregation_1() {
$this->goalaggregation_test(
'{"thresh_excellent":100,"thresh_good":85,"thresh_completed":66,"thresh_progress":25,"use_failed":true,"accept_pending_as_submitted":true}',
completion::FAILED,
[ // completions
completion::FAILED,
completion::FAILED,
completion::FAILED,
completion::FAILED,
completion::FAILED,
],
[] // required
);
}
public function test_goalaggregation_2() {
$this->goalaggregation_test(
'{"thresh_excellent":100,"thresh_good":85,"thresh_completed":66,"thresh_progress":25,"use_failed":true,"accept_pending_as_submitted":true}',
completion::EXCELLENT,
[
completion::COMPLETED,
completion::COMPLETED,
completion::COMPLETED,
completion::COMPLETED,
completion::COMPLETED,
],
[],
);
}
public function test_goalaggregation_3() {
$this->goalaggregation_test(
'{"thresh_excellent":100,"thresh_good":85,"thresh_completed":66,"thresh_progress":25,"use_failed":true,"accept_pending_as_submitted":true}',
completion::INCOMPLETE,
[
completion::INCOMPLETE,
completion::INCOMPLETE,
completion::INCOMPLETE,
completion::INCOMPLETE,
completion::INCOMPLETE,
],
[],
);
}
public function test_goalaggregation_4() {
$this->goalaggregation_test(
'{"thresh_excellent":100,"thresh_good":85,"thresh_completed":66,"thresh_progress":25,"use_failed":true,"accept_pending_as_submitted":true}',
completion::PROGRESS,
[
completion::COMPLETED,
completion::COMPLETED,
completion::COMPLETED,
completion::PROGRESS,
completion::PROGRESS,
],
[],
);
}
public function test_goalaggregation_5() {
$this->goalaggregation_test(
'{"thresh_excellent":100,"thresh_good":85,"thresh_completed":66,"thresh_progress":25,"use_failed":true,"accept_pending_as_submitted":true}',
completion::COMPLETED,
[
completion::COMPLETED,
completion::COMPLETED,
completion::COMPLETED,
completion::COMPLETED,
completion::PROGRESS,
],
[],
);
}
public function test_goalaggregation_6() {
$this->goalaggregation_test(
'{"thresh_excellent":100,"thresh_good":85,"thresh_completed":66,"thresh_progress":25,"use_failed":true,"accept_pending_as_submitted":true}',
completion::PROGRESS,
[
completion::PROGRESS,
completion::PROGRESS,
completion::PROGRESS,
completion::PROGRESS,
completion::INCOMPLETE,
completion::INCOMPLETE,
],
[],
);
}
public function test_junctionaggregation_0() {
$this->junctionaggregation_test(
'{"thresh_excellent":100,"thresh_good":85,"thresh_completed":66,"thresh_progress":25,"use_failed":true,"accept_pending_as_submitted":true}',
completion::FAILED,
[
completion::FAILED,
completion::FAILED,
completion::FAILED,
completion::FAILED,
],
);
}
public function test_junctionaggregation_1() {
$this->junctionaggregation_test(
'{"thresh_excellent":100,"thresh_good":85,"thresh_completed":66,"thresh_progress":25,"use_failed":true,"accept_pending_as_submitted":true}',
completion::INCOMPLETE,
[
completion::INCOMPLETE,
completion::INCOMPLETE,
completion::INCOMPLETE,
completion::INCOMPLETE,
],
);
}
public function test_junctionaggregation_2() {
$this->junctionaggregation_test(
'{"thresh_excellent":100,"thresh_good":85,"thresh_completed":66,"thresh_progress":25,"use_failed":true,"accept_pending_as_submitted":true}',
completion::FAILED,
[
completion::FAILED,
completion::INCOMPLETE,
completion::INCOMPLETE,
completion::INCOMPLETE,
],
);
}
public function test_junctionaggregation_3() {
$this->junctionaggregation_test(
'{"thresh_excellent":100,"thresh_good":85,"thresh_completed":66,"thresh_progress":25,"use_failed":true,"accept_pending_as_submitted":true}',
completion::PROGRESS,
[
completion::PROGRESS,
completion::INCOMPLETE,
completion::INCOMPLETE,
completion::INCOMPLETE,
],
);
}
}

8
version.php Normal file
View File

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

134
view-plan.php Normal file
View File

@ -0,0 +1,134 @@
<?php
require_once("../../config.php");
require_once($CFG->libdir.'/weblib.php');
$systemcontext = context_system::instance();
$PAGE->set_url("/local/treestudyplan/view-plan.php",array());
require_login();
// Figure out the context (category or system, based on either category or context parameter)
$categoryid = optional_param('categoryid', 0, PARAM_INT); // Category id
$contextid = optional_param('contextid', 0, PARAM_INT); // Context id
if($categoryid > 0){
$studyplancontext = context_coursecat::instance($categoryid);
}
elseif($contextid > 0)
{
$studyplancontext = context::instance_by_id($contextid);
if(in_array($studyplancontext->contextlevel,[CONTEXT_SYSTEM,CONTEXT_COURSECAT]))
{
$categoryid = $studyplancontext->instanceid;
}
else
{
$studyplancontext = $systemcontext;
}
}
else
{
$categoryid = 0;
$studyplancontext = $systemcontext;
}
require_capability('local/treestudyplan:viewuserreports',$studyplancontext);
$contextname = $studyplancontext->get_context_name(false,false);
$PAGE->set_pagelayout('base');
$PAGE->set_context($studyplancontext);
$PAGE->set_title(get_string('view_plan','local_treestudyplan')." - ".$contextname);
$PAGE->set_heading(get_string('view_plan','local_treestudyplan')." - ".$contextname);
if($studyplancontext->id > 1){
navigation_node::override_active_url(new moodle_url('/course/index.php', ['categoryid' => $categoryid ]));
$PAGE->navbar->add(get_string('view_plan','local_treestudyplan'));
}
// Load javascripts and specific css
$PAGE->requires->css(new moodle_url($CFG->wwwroot.'/local/treestudyplan/css/bootstrap-vue.min.css'));
$PAGE->requires->css(new moodle_url($CFG->wwwroot.'/local/treestudyplan/css/devstyles.css'));
$PAGE->requires->css(new moodle_url($CFG->wwwroot.'/local/treestudyplan/css/vue-hsluv-picker.css'));
$PAGE->requires->js_call_amd('local_treestudyplan/page-view-plan', 'init',[$studyplancontext->id,$categoryid]);
//Local translate function
function t($str, $param=null, $plugin='local_treestudyplan'){
print get_string($str,$plugin,$param);
}
print $OUTPUT->header();
?>
<div id='root'>
<div class='vue-loader' v-show='false'>
<div class="spinner-border text-primary" role="status">
<span class="sr-only">Loading...</span>
</div>
</div>
<div v-cloak>
<div v-if='!activestudyplan && usedcontexts' class='ml-3 mb-3'>
<b-form-select text='<?php print($contextname);?>' :value="contextid">
<b-form-select-option v-for='ctx in usedcontexts' :key='ctx.id' :value="ctx.context_id" @click='switchContext(ctx)'
:active="ctx.context_id == contextid" :class="(ctx.studyplancount > 0)?'font-weight-bold':''"
><span v-for="(p,i) in ctx.category.path"><span v-if="i>0"> / </span>{{ p }}</span> <span>({{ ctx.studyplancount }})</b-form-select-option>
</b-form-select>
</div>
<h3 v-else><?php print $contextname; ?></h3>
<div class="m-buttonbar" style="margin-bottom: 1em;">
<a href='#' v-if='displayedstudyplan' @click.prevent='closeStudyplan'><i style='font-size: 150%;' class='fa fa-chevron-left'></i> <?php t('back');?></a>
<span v-if='displayedstudyplan'><?php t("studyplan_select"); ?></span>&nbsp;
<b-dropdown v-if='displayedstudyplan'lazy :text='dropdown_title'>
<b-dropdown-item-button v-for='(studyplan,planindex) in studyplans' :key='studyplan.id' @click='selectStudyplan(studyplan)'>{{ studyplan.name }}</b-dropdown-item>
</b-dropdown>&nbsp;
<b-button variant='primary' v-if='associatedstudents && associatedstudents.length > 0' v-b-toggle.toolbox-sidebar><?php t('selectstudent_btn') ?></b-button>
</div>
<div class='t-studyplan-container'>
<h2 v-if='displayedstudyplan&& selectedstudent'>{{selectedstudent.firstname}} {{selectedstudent.lastname}} - {{displayedstudyplan.name}}</h2>
<h2 v-else-if='displayedstudyplan'><?php t("showoverview"); ?> - {{displayedstudyplan.name}}</h2>
<r-studyplan v-if='!loadingstudyplan && displayedstudyplan' v-model='displayedstudyplan' :teachermode='!selectedstudent'></r-studyplan>
<div v-else-if='loadingstudyplan' class="spinner-border text-primary" role="status">
<span class="sr-only">Loading...</span>
</div>
<div v-else class='t-studyplan-notselected'>
<p><?php t("studyplan_noneselected"); ?></p>
<b-card-group deck>
<s-studyplan-card
v-for='(studyplan,planindex) in studyplans'
:key='studyplan.id'
v-model='studyplans[planindex]'
open
@open='selectStudyplan(studyplan)'
></s-studyplan-card>
</b-card-group>
</div>
</div>
<b-sidebar
id="toolbox-sidebar"
right
shadow
title='<?php t("selectstudent")?>'
>
<div class='m-2'><?php t("selectstudent_details")?></div>
<b-list-group v-if="associatedstudents">
<b-list-group-item :active="! selectedstudent"
button
variant="primary"
@click='showOverview()'
>
<?php t("showoverview"); ?>
</b-list-group-item>
<b-list-group-item v-for="student in associatedstudents" :key="student.id"
:active="selectedstudent && student.id == selectedstudent.id"
button
@click='showStudentView(student)'
>
{{student.firstname}} {{student.lastname}}
</b-list-group-item>
</b-list-group>
</b-sidebar>
</div>
</div>
<?php
print $OUTPUT->footer();