Implemented backend for studyplan-reports and start of frontend

This commit is contained in:
PMKuipers 2024-02-18 13:47:08 +01:00
parent 0e4cf45a80
commit 9f69b7bc15
25 changed files with 12474 additions and 71 deletions

View File

@ -1,3 +1,3 @@
define("local_treestudyplan/page-studyplan-report",["exports","core/ajax","core/notification","./vue/vue","./util/debugger","./util/string-helper","./studyplan-processor","./util/date-helper","./treestudyplan-components","./modedit-modal","./portal-vue/portal-vue.esm","./bootstrap-vue/bootstrap-vue"],(function(_exports,_ajax,_notification,_vue,_debugger,_stringHelper,_studyplanProcessor,_dateHelper,_treestudyplanComponents,_modeditModal,_portalVue,_bootstrapVue){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=function(studyplanid,period){studyplanid=void 0!==studyplanid&&Number.isInteger(Number(studyplanid))?Number(studyplanid):0;new _vue.default({el:"#root",data:{},async mounted(){},computed:{},methods:{}})},_notification=_interopRequireDefault(_notification),_vue=_interopRequireDefault(_vue),_debugger=_interopRequireDefault(_debugger),_treestudyplanComponents=_interopRequireDefault(_treestudyplanComponents),_modeditModal=_interopRequireDefault(_modeditModal),_portalVue=_interopRequireDefault(_portalVue),_bootstrapVue=_interopRequireDefault(_bootstrapVue),_vue.default.use(_modeditModal.default),_vue.default.use(_portalVue.default),_vue.default.use(_bootstrapVue.default);new _debugger.default("treestudyplanviewer"),(0,_stringHelper.load_strings)({studyplan:{studyplan_select_placeholder:"studyplan_select_placeholder"}})})); define("local_treestudyplan/page-studyplan-report",["exports","core/ajax","core/notification","./vue/vue","./util/debugger","./util/string-helper","./studyplan-report-components","./modedit-modal","./portal-vue/portal-vue.esm","./bootstrap-vue/bootstrap-vue"],(function(_exports,_ajax,_notification,_vue,_debugger,_stringHelper,_studyplanReportComponents,_modeditModal,_portalVue,_bootstrapVue){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=function(studyplanid,pageid,firstperiod,lastperiod){if(void 0===pageid||!Number.isInteger(Number(pageid))||void 0===studyplanid||!Number.isInteger(Number(studyplanid)))return void debug.error("Did Error: studyplan id and page id not provided as integer numbers to script.",studyplanid,pageid,firstperiod,lastperiod);studyplanid=Number(studyplanid),pageid=Number(pageid);new _vue.default({el:"#root",data:{structure:null},async mounted(){},created(){this.loadStructure()},computed:{},methods:{loadStructure(){const self=this;this.structure=null,(0,_ajax.call)([{methodname:"local_treestudyplan_get_report_structure",args:{pageid:pageid}}])[0].then((function(response){self.structure=response})).catch(_notification.default.exception)}}})},_notification=_interopRequireDefault(_notification),_vue=_interopRequireDefault(_vue),_debugger=_interopRequireDefault(_debugger),_studyplanReportComponents=_interopRequireDefault(_studyplanReportComponents),_modeditModal=_interopRequireDefault(_modeditModal),_portalVue=_interopRequireDefault(_portalVue),_bootstrapVue=_interopRequireDefault(_bootstrapVue),_vue.default.use(_studyplanReportComponents.default),_vue.default.use(_modeditModal.default),_vue.default.use(_portalVue.default),_vue.default.use(_bootstrapVue.default);let debug=new _debugger.default("treestudyplanviewer");(0,_stringHelper.load_strings)({studyplan:{studyplan_select_placeholder:"studyplan_select_placeholder"}})}));
//# sourceMappingURL=page-studyplan-report.min.js.map //# sourceMappingURL=page-studyplan-report.min.js.map

View File

@ -1 +1 @@
{"version":3,"file":"page-studyplan-report.min.js","sources":["../src/page-studyplan-report.js"],"sourcesContent":["/*eslint no-var: \"error\" */\n/*eslint no-unused-vars: \"off\" */\n/*eslint linebreak-style: \"off\" */\n/*eslint no-trailing-spaces: \"off\" */\n/*eslint-env es6*/\n\nimport {call} from 'core/ajax';\nimport notification from 'core/notification';\n\nimport Vue from './vue/vue';\n\nimport Debugger from './util/debugger';\nimport {load_strings} from './util/string-helper';\nimport {ProcessStudyplan} from './studyplan-processor';\nimport {studyplanTiming} from './util/date-helper';\n\nimport TSComponents from './treestudyplan-components';\nimport ModalComponents from './modedit-modal';\nVue.use(ModalComponents);\n\nimport PortalVue from './portal-vue/portal-vue.esm';\nVue.use(PortalVue);\nimport BootstrapVue from './bootstrap-vue/bootstrap-vue';\nVue.use(BootstrapVue);\n\n\nlet debug = new Debugger(\"treestudyplanviewer\");\n\nlet strings = load_strings({\n studyplan: {\n studyplan_select_placeholder: 'studyplan_select_placeholder',\n },\n});\n\n/**\n * Initialize the Page\n * @param {Number} studyplanid The id of the studyplan we need to view \n * @param {Number} period The id of the studyplan we need to view \n */\nexport function init(studyplanid,period) {\n // Make sure the id's are numeric and integer\n if (undefined === studyplanid || !Number.isInteger(Number(studyplanid)) ){ \n studyplanid = 0;\n } else {\n studyplanid = Number(studyplanid);\n } // ensure a numeric value instead of string.\n\n const app = new Vue({\n el: '#root',\n data: {\n\n },\n async mounted() {\n \n },\n computed: {\n \n },\n methods: {\n \n },\n });\n}\n"],"names":["studyplanid","period","undefined","Number","isInteger","Vue","el","data","computed","methods","use","ModalComponents","PortalVue","BootstrapVue","Debugger","studyplan","studyplan_select_placeholder"],"mappings":"onBAuCqBA,YAAYC,QAKzBD,iBAHAE,IAAcF,aAAgBG,OAAOC,UAAUD,OAAOH,cAGxCG,OAAOH,aAFP,EAKN,IAAIK,aAAI,CAChBC,GAAI,QACJC,KAAM,qBAMNC,SAAU,GAGVC,QAAS,qXAxCbC,IAAIC,oCAGJD,IAAIE,iCAEJF,IAAIG,uBAGI,IAAIC,kBAAS,wBAEX,8BAAa,CACvBC,UAAW,CACPC,6BAA8B"} {"version":3,"file":"page-studyplan-report.min.js","sources":["../src/page-studyplan-report.js"],"sourcesContent":["/*eslint no-var: \"error\" */\n/*eslint no-unused-vars: \"off\" */\n/*eslint linebreak-style: \"off\" */\n/*eslint no-trailing-spaces: \"off\" */\n/*eslint-env es6*/\n\nimport {call} from 'core/ajax';\nimport notification from 'core/notification';\n\nimport Vue from './vue/vue';\n\nimport Debugger from './util/debugger';\nimport {load_strings} from './util/string-helper';\n\nimport SRComponents from './studyplan-report-components';\nVue.use(SRComponents);\nimport ModalComponents from './modedit-modal';\nVue.use(ModalComponents);\n\nimport PortalVue from './portal-vue/portal-vue.esm';\nVue.use(PortalVue);\nimport BootstrapVue from './bootstrap-vue/bootstrap-vue';\nVue.use(BootstrapVue);\n\n\nlet debug = new Debugger(\"treestudyplanviewer\");\n\nlet strings = load_strings({\n studyplan: {\n studyplan_select_placeholder: 'studyplan_select_placeholder',\n },\n});\n\n/**\n * Initialize the Page\n * @param {Number} studyplanid The id of the studyplan we need to view \n * @param {Number} pageid The id of the studyplan page we need to view \n * @param {Number} firstperiod The number of the first period to view\n * @param {Number} lastperiod The number of the last period to view \n */\nexport function init(studyplanid, pageid, firstperiod, lastperiod) {\n if (undefined === pageid || !Number.isInteger(Number(pageid)) ||\n undefined === studyplanid || !Number.isInteger(Number(studyplanid))) {\n debug.error(\"Did Error: studyplan id and page id not provided as integer numbers to script.\",\n studyplanid, pageid, firstperiod, lastperiod);\n return; // Do not continue if plan and page are not proper integers\n }\n // Ensure a numeric value instead of string.\n studyplanid = Number(studyplanid);\n pageid = Number(pageid);\n\n // Startup app.\n const app = new Vue({\n el: '#root',\n data: {\n structure: null,\n },\n async mounted() {\n \n },\n created() {\n this.loadStructure();\n },\n computed: {\n \n },\n methods: {\n loadStructure() {\n const self = this;\n this.structure = null; // Starts loading icon. Hides old data.\n call([{\n methodname: 'local_treestudyplan_get_report_structure',\n args: { pageid: pageid}\n }])[0].then(function(response){\n self.structure = response;\n }).catch(notification.exception);\n }\n },\n });\n}\n"],"names":["studyplanid","pageid","firstperiod","lastperiod","undefined","Number","isInteger","debug","error","Vue","el","data","structure","created","loadStructure","computed","methods","self","this","methodname","args","then","response","catch","notification","exception","use","SRComponents","ModalComponents","PortalVue","BootstrapVue","Debugger","studyplan","studyplan_select_placeholder"],"mappings":"4iBAwCqBA,YAAaC,OAAQC,YAAaC,oBAC/CC,IAAcH,SAAWI,OAAOC,UAAUD,OAAOJ,eACjDG,IAAcJ,cAAgBK,OAAOC,UAAUD,OAAOL,0BACtDO,MAAMC,MAAM,iFACRR,YAAaC,OAAQC,YAAaC,YAI1CH,YAAcK,OAAOL,aACrBC,OAASI,OAAOJ,QAGJ,IAAIQ,aAAI,CAChBC,GAAI,QACJC,KAAM,CACFC,UAAW,wBAKfC,eACSC,iBAETC,SAAU,GAGVC,QAAS,CACLF,sBACUG,KAAOC,UACRN,UAAY,oBACZ,CAAC,CACFO,WAAY,2CACZC,KAAM,CAAEnB,OAAQA,WAChB,GAAGoB,MAAK,SAASC,UACjBL,KAAKL,UAAYU,YAClBC,MAAMC,sBAAaC,mYA5DlCC,IAAIC,iDAEJD,IAAIE,oCAGJF,IAAIG,iCAEJH,IAAII,2BAGJvB,MAAQ,IAAIwB,kBAAS,wBAEX,8BAAa,CACvBC,UAAW,CACPC,6BAA8B"}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,3 @@
define("local_treestudyplan/studyplan-report-components",["exports","./simpleline/simpleline","core/str","./util/string-helper","./util/date-helper","core/ajax","core/notification","./util/svgarc","./util/debugger","core/config","./studyplan-processor","./treestudyplan-components","core/edit_switch"],(function(_exports,_simpleline,_str,_stringHelper,_dateHelper,_ajax,_notification,_svgarc,_debugger,_config,_studyplanProcessor,_treestudyplanComponents,_edit_switch){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_notification=_interopRequireDefault(_notification),_debugger=_interopRequireDefault(_debugger),_config=_interopRequireDefault(_config),_treestudyplanComponents=_interopRequireDefault(_treestudyplanComponents);Math.PI;var _default={install(Vue){Vue.use(_treestudyplanComponents.default);new _debugger.default("treestudyplan-viewer"),(0,_stringHelper.load_strings)({report:{loading:"loadinghelp@core",studyplan_past:"studyplan_past",studyplan_present:"studyplan_present",studyplan_future:"studyplan_future",back:"back"},invalid:{error:"error"},grading:{ungraded:"ungraded",graded:"graded",allgraded:"allgraded",unsubmitted:"unsubmitted",nogrades:"nogrades",unknown:"unknown"},completion:{completed:"completion_completed",incomplete:"completion_incomplete",completed_pass:"completion_passed",completed_fail:"completion_failed",ungraded:"ungraded",aggregation_all:"aggregation_all",aggregation_any:"aggregation_any",aggregation_one:"aggregation_one",aggregation_overall_all:"aggregation_overall_all",aggregation_overall_any:"aggregation_overall_any",aggregation_overall_one:"aggregation_overall_one",completion_not_configured:"completion_not_configured",configure_completion:"configure_completion",view_completion_report:"view_completion_report",completion_incomplete:"completion_incomplete",completion_failed:"completion_failed",completion_pending:"completion_pending",completion_progress:"completion_progress",completion_completed:"completion_completed",completion_good:"completion_good",completion_excellent:"completion_excellent",view_feedback:"view_feedback",coursetiming_past:"coursetiming_past",coursetiming_present:"coursetiming_present",coursetiming_future:"coursetiming_future",required_goal:"required_goal",student_not_tracked:"student_not_tracked",completion_not_enabled:"completion_not_enabled"},badge:{share_badge:"share_badge",dateissued:"dateissued",dateexpire:"dateexpire",badgeinfo:"badgeinfo",badgeissuedstats:"badgeissuedstats",completion_incomplete:"completion_incomplete_badge",completion_completed:"completion_completed_badge",completioninfo:"completioninfo",badgedisabled:"badgedisabled"},course:{completion_incomplete:"completion_incomplete",completion_failed:"completion_failed",completion_pending:"completion_pending",completion_progress:"completion_progress",completion_completed:"completion_completed",completion_good:"completion_good",completion_excellent:"completion_excellent",view_feedback:"view_feedback",coursetiming_past:"coursetiming_past",coursetiming_present:"coursetiming_present",coursetiming_future:"coursetiming_future",required_goal:"required_goal",student_not_tracked:"student_not_tracked",not_enrolled:"not_enrolled"},competency:{competency_not_configured:"competency_not_configured",configure_competency:"configure_competency",when:"when",required:"required",points:"points@core_grades",heading:"competency_heading",details:"competency_details",results:"results",unrated:"unrated",progress:"completion_progress",view_feedback:"view_feedback"},pageinfo:{edit:"period_edit",fullname:"studyplan_name",shortname:"studyplan_shortname",startdate:"studyplan_startdate",enddate:"studyplan_enddate",description:"studyplan_description",duration:"studyplan_duration",details:"studyplan_details"}});Vue.component("q-studyplanreport",{props:{structure:{type:Object}},data:()=>({students:[],studentresults:{},sorting:{name:"asc"}}),created(){this.loadStudents()},computed:{},methods:{loadStudents(){const self=this;(0,_ajax.call)([{methodname:"local_treestudyplan_all_associated_grouped",args:{studyplan_id:this.structure.studyplan.id}}])[0].then((function(response){self.students=response;for(const group of self.students)for(const student of group.users)self.studentresults[student.id]={loading:!0,results:[]},(0,_ajax.call)([{methodname:"local_treestudyplan_get_report_data",args:{pageid:self.structure.page.id,userid:student.id,firstperiod:self.structure.firstperiod,lastperiod:self.structure.lastperiod}}])[0].then((function(response){self.studentresults[student.id].loading=!1,self.studentresults[student.id].results=response})).catch(_notification.default.exception)})).catch(_notification.default.exception)}},mounted(){},updated(){},template:"\n <div>\n \n </div>\n "})}};return _exports.default=_default,_exports.default}));
//# sourceMappingURL=studyplan-report-components.min.js.map

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -11,10 +11,9 @@ import Vue from './vue/vue';
import Debugger from './util/debugger'; import Debugger from './util/debugger';
import {load_strings} from './util/string-helper'; import {load_strings} from './util/string-helper';
import {ProcessStudyplan} from './studyplan-processor';
import {studyplanTiming} from './util/date-helper';
import TSComponents from './treestudyplan-components'; import SRComponents from './studyplan-report-components';
Vue.use(SRComponents);
import ModalComponents from './modedit-modal'; import ModalComponents from './modedit-modal';
Vue.use(ModalComponents); Vue.use(ModalComponents);
@ -35,29 +34,47 @@ let strings = load_strings({
/** /**
* Initialize the Page * Initialize the Page
* @param {Number} studyplanid The id of the studyplan we need to view * @param {Number} studyplanid The id of the studyplan we need to view
* @param {Number} period The id of the studyplan we need to view * @param {Number} pageid The id of the studyplan page we need to view
* @param {Number} firstperiod The number of the first period to view
* @param {Number} lastperiod The number of the last period to view
*/ */
export function init(studyplanid,period) { export function init(studyplanid, pageid, firstperiod, lastperiod) {
// Make sure the id's are numeric and integer if (undefined === pageid || !Number.isInteger(Number(pageid)) ||
if (undefined === studyplanid || !Number.isInteger(Number(studyplanid)) ){ undefined === studyplanid || !Number.isInteger(Number(studyplanid))) {
studyplanid = 0; debug.error("Did Error: studyplan id and page id not provided as integer numbers to script.",
} else { studyplanid, pageid, firstperiod, lastperiod);
studyplanid = Number(studyplanid); return; // Do not continue if plan and page are not proper integers
} // ensure a numeric value instead of string. }
// Ensure a numeric value instead of string.
studyplanid = Number(studyplanid);
pageid = Number(pageid);
// Startup app.
const app = new Vue({ const app = new Vue({
el: '#root', el: '#root',
data: { data: {
structure: null,
}, },
async mounted() { async mounted() {
},
created() {
this.loadStructure();
}, },
computed: { computed: {
}, },
methods: { methods: {
loadStructure() {
const self = this;
this.structure = null; // Starts loading icon. Hides old data.
call([{
methodname: 'local_treestudyplan_get_report_structure',
args: { pageid: pageid}
}])[0].then(function(response){
self.structure = response;
}).catch(notification.exception);
}
}, },
}); });
} }

View File

@ -165,10 +165,12 @@ export function init(contextid,categoryid) {
}])[0].then(function(response){ }])[0].then(function(response){
app.associatedstudents = response; app.associatedstudents = response;
if(studentid){ if(studentid){
for(const student of app.associatedstudents){ for(const group of app.associatedstudents) {
if(student.id == studentid){ for(const student of group.users){
app.showStudentView(student); if(student.id == studentid){
break; app.showStudentView(student);
break;
}
} }
} }
} }

View File

@ -0,0 +1,207 @@
/*eslint no-var: "error"*/
/*eslint no-console: "off"*/
/*eslint no-unused-vars: warn */
/*eslint max-len: ["error", { "code": 160 }] */
/*eslint-disable no-trailing-spaces */
/*eslint-env es6*/
// Put this file in path/to/plugin/amd/src
import {SimpleLine} from './simpleline/simpleline';
import {get_strings} from 'core/str';
import {load_strings} from './util/string-helper';
import {format_date,studyplanPageTiming,studyplanTiming} from './util/date-helper';
import {call} from 'core/ajax';
import notification from 'core/notification';
import {svgarcpath} from './util/svgarc';
import Debugger from './util/debugger';
import Config from 'core/config';
import {ProcessStudyplan, ProcessStudyplanPage, objCopy} from './studyplan-processor';
import TSComponents from './treestudyplan-components';
import {eventTypes as editSwEventTypes} from 'core/edit_switch';
// Make π available as a constant
const π = Math.PI;
// Gravity value for arrow lines - determines how much a line is pulled in the direction of the start/end before changing direction
const LINE_GRAVITY = 1.3;
export default {
install(Vue/*,options*/){
Vue.use(TSComponents);
let debug = new Debugger("treestudyplan-viewer");
let strings = load_strings({
report: {
loading: "loadinghelp@core",
studyplan_past: "studyplan_past",
studyplan_present: "studyplan_present",
studyplan_future: "studyplan_future",
back: "back",
},
invalid: {
error: 'error',
},
grading: {
ungraded: "ungraded",
graded: "graded",
allgraded: "allgraded",
unsubmitted: "unsubmitted",
nogrades: "nogrades",
unknown: "unknown",
},
completion: {
completed: "completion_completed",
incomplete: "completion_incomplete",
completed_pass: "completion_passed",
completed_fail: "completion_failed",
ungraded: "ungraded",
aggregation_all: "aggregation_all",
aggregation_any: "aggregation_any",
aggregation_one: "aggregation_one",
aggregation_overall_all: "aggregation_overall_all",
aggregation_overall_any: "aggregation_overall_any",
aggregation_overall_one: "aggregation_overall_one",
completion_not_configured: "completion_not_configured",
configure_completion: "configure_completion",
view_completion_report: "view_completion_report",
completion_incomplete: "completion_incomplete",
completion_failed: "completion_failed",
completion_pending: "completion_pending",
completion_progress: "completion_progress",
completion_completed: "completion_completed",
completion_good: "completion_good",
completion_excellent: "completion_excellent",
view_feedback: "view_feedback",
coursetiming_past: "coursetiming_past",
coursetiming_present: "coursetiming_present",
coursetiming_future: "coursetiming_future",
required_goal: "required_goal",
student_not_tracked: "student_not_tracked",
completion_not_enabled: "completion_not_enabled",
},
badge: {
share_badge: "share_badge",
dateissued: "dateissued",
dateexpire: "dateexpire",
badgeinfo: "badgeinfo",
badgeissuedstats: "badgeissuedstats",
completion_incomplete: "completion_incomplete_badge",
completion_completed: "completion_completed_badge",
completioninfo: "completioninfo",
badgedisabled: "badgedisabled"
},
course: {
completion_incomplete: "completion_incomplete",
completion_failed: "completion_failed",
completion_pending: "completion_pending",
completion_progress: "completion_progress",
completion_completed: "completion_completed",
completion_good: "completion_good",
completion_excellent: "completion_excellent",
view_feedback: "view_feedback",
coursetiming_past: "coursetiming_past",
coursetiming_present: "coursetiming_present",
coursetiming_future: "coursetiming_future",
required_goal: "required_goal",
student_not_tracked: "student_not_tracked",
not_enrolled: "not_enrolled",
},
competency: {
competency_not_configured: "competency_not_configured",
configure_competency: "configure_competency",
when: "when",
required: "required",
points: "points@core_grades",
heading: "competency_heading",
details: "competency_details",
results: "results",
unrated: "unrated",
progress: "completion_progress",
view_feedback: "view_feedback",
},
pageinfo: {
edit: 'period_edit',
fullname: 'studyplan_name',
shortname: 'studyplan_shortname',
startdate: 'studyplan_startdate',
enddate: 'studyplan_enddate',
description: 'studyplan_description',
duration: 'studyplan_duration',
details: 'studyplan_details',
}
});
/************************************
* *
* Treestudyplan Viewer components *
* *
************************************/
Vue.component('q-studyplanreport', {
props: {
structure: {
type: Object,
},
},
data() {
return {
students: [],
studentresults: {},
sorting: {
name: "asc",
}
};
},
created() {
this.loadStudents();
},
computed: {
},
methods: {
loadStudents() {
const self = this;
call([{
methodname: 'local_treestudyplan_all_associated_grouped',
args: { studyplan_id: this.structure.studyplan.id}
}])[0].then(function(response){
self.students = response;
for(const group of self.students) {
for(const student of group.users){
self.studentresults[student.id] = {
loading: true,
results: [],
};
call([{
methodname: 'local_treestudyplan_get_report_data',
args: { pageid: self.structure.page.id,
userid: student.id,
firstperiod: self.structure.firstperiod,
lastperiod: self.structure.lastperiod,
}
}])[0].then(function(response){
self.studentresults[student.id].loading = false;
self.studentresults[student.id].results = response;
}).catch(notification.exception);
}
}
}).catch(notification.exception);
}
},
mounted() {
},
updated() {
},
template: `
<div>
</div>
`,
});
},
};

File diff suppressed because one or more lines are too long

View File

@ -100,6 +100,49 @@ class badgeinfo {
return is_numeric($id) && $DB->record_exists('badge', array('id' => $id)); return is_numeric($id) && $DB->record_exists('badge', array('id' => $id));
} }
/**
* Webservice structure for editor info
* @param int $value Webservice requirement constant
*/
public static function simple_structure($value = VALUE_REQUIRED) : \external_description {
return new \external_single_structure([
"id" => new \external_value(PARAM_INT, 'id of badge'),
"infolink" => new \external_value(PARAM_RAW, 'badge issue information link', VALUE_OPTIONAL),
"name" => new \external_value(PARAM_RAW, 'badge name'),
"status" => new \external_value(PARAM_TEXT, 'badge status'),
"locked" => new \external_value(PARAM_TEXT, 'badge lock status'),
"description" => new \external_value(PARAM_RAW, 'badge description'),
"imageurl" => new \external_value(PARAM_RAW, 'url of badge image'),
"active" => new \external_value(PARAM_BOOL, 'badge is available'),
], "Badge basic info", $value);
}
/**
* Webservice model for editor info
* @param int[] $studentlist List of user id's to use for checking issueing progress within a study plan
* @return array Webservice data model
*/
public function simple_model() {
if ($this->badge->type == BADGE_TYPE_SITE) {
$context = \context_system::instance();
} else {
$context = \context_course::instance($this->badge->courseid);
}
$model = [
'id' => $this->badge->id,
'infolink' => (new \moodle_url('/badges/overview.php', ['id' => $this->badge->id]))->out(false),
'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),
"active" => $this->badge->is_active(),
];
return $model;
}
/** /**
* Webservice structure for editor info * Webservice structure for editor info
* @param int $value Webservice requirement constant * @param int $value Webservice requirement constant

View File

@ -36,6 +36,10 @@ use stdClass;
*/ */
class premium extends \external_api { class premium extends \external_api {
// Toggle the variable below to enable support for premium stuff.
// If set to false, all premium features will be enabled and no premium settings panel will be visible.
private static $premium_supported = false;
private static $premiumcrt = "-----BEGIN CERTIFICATE----- private static $premiumcrt = "-----BEGIN CERTIFICATE-----
MIIDSzCCAjMCFFlyhmKf1fN7U5lQL/dtlsyP24AQMA0GCSqGSIb3DQEBCwUAMGEx MIIDSzCCAjMCFFlyhmKf1fN7U5lQL/dtlsyP24AQMA0GCSqGSIb3DQEBCwUAMGEx
CzAJBgNVBAYTAk5MMRYwFAYDVQQIDA1Ob29yZC1Ib2xsYW5kMRowGAYDVQQKDBFN CzAJBgNVBAYTAk5MMRYwFAYDVQQIDA1Ob29yZC1Ib2xsYW5kMRowGAYDVQQKDBFN
@ -59,6 +63,11 @@ Klc5I28bGbvxIV5pnL6ZSjHEDp2WreM8HB0XFJwU+Q==
private static $cachedpremiumstatus = null; private static $cachedpremiumstatus = null;
public static function supported() {
return self::$premium_supported;
}
private static function decrypt($encrypted) { private static function decrypt($encrypted) {
// Get the public key. // Get the public key.
$key = \openssl_get_publickey(self::$premiumcrt); $key = \openssl_get_publickey(self::$premiumcrt);
@ -130,8 +139,12 @@ Klc5I28bGbvxIV5pnL6ZSjHEDp2WreM8HB0XFJwU+Q==
* @return bool * @return bool
*/ */
public static function enabled() { public static function enabled() {
$status = self::premiumStatus(); if (self::$premium_supported) {
return $status->enabled; $status = self::premiumStatus();
return $status->enabled;
} else {
return true;
}
} }
/** /**

View File

@ -24,6 +24,9 @@ namespace local_treestudyplan;
defined('MOODLE_INTERNAL') || die(); defined('MOODLE_INTERNAL') || die();
use local_treestudyplan\local\helpers\webservicehelper; use local_treestudyplan\local\helpers\webservicehelper;
use local_treestudyplan\period;
use local_treestudyplan\studyplan;
use local_treestudyplan\studyplanpage;
require_once($CFG->libdir.'/externallib.php'); require_once($CFG->libdir.'/externallib.php');
@ -42,7 +45,7 @@ class reportservice extends \external_api {
return new \external_function_parameters([ return new \external_function_parameters([
"pageid" => new \external_value(PARAM_INT, 'id of studyplan page'), "pageid" => new \external_value(PARAM_INT, 'id of studyplan page'),
"firstperiod" => new \external_value(PARAM_INT, 'first period to include in report',VALUE_DEFAULT), "firstperiod" => new \external_value(PARAM_INT, 'first period to include in report',VALUE_DEFAULT),
"lastperiod" => new \external_value(PARAM_INT, 'last perioe to include in report',VALUE_DEFAULT), "lastperiod" => new \external_value(PARAM_INT, 'last period to include in report',VALUE_DEFAULT),
]); ]);
} }
@ -51,6 +54,17 @@ class reportservice extends \external_api {
*/ */
public static function get_report_structure_returns() : \external_description { public static function get_report_structure_returns() : \external_description {
return new \external_single_structure([ return new \external_single_structure([
"periods" => new \external_multiple_structure(new \external_single_structure([
"period" => period::structure(),
"lines" => new \external_multiple_structure(new \external_single_structure([
"line" => studyline::simple_structure(),
"items" => new \external_multiple_structure(studyitem::editor_structure(), "Information per studyitem"),
])),
])),
"firstperiod" => new \external_value(PARAM_INT, 'first period included in report'),
"lastperiod" => new \external_value(PARAM_INT, 'last period included in report'),
"studyplan" => studyplan::simple_structure(),
"page" => studyplanpage::simple_structure(),
]); ]);
} }
@ -62,7 +76,57 @@ class reportservice extends \external_api {
* @return object * @return object
*/ */
public static function get_report_structure($pageid,$firstperiod=null,$lastperiod=null) { public static function get_report_structure($pageid,$firstperiod=null,$lastperiod=null) {
$page = studyplanpage::find_by_id($pageid);
webservicehelper::require_capabilities(self::CAP_VIEW,$page->studyplan()->context());
$plan = $page->studyplan();
if (empty($firstperiod)) {
$firstperiod = 1;
}
if (empty($lastperiod)) {
// Index for periods starts at 1, so the period count is same as length
$lastperiod = $page->periods();
}
$model = [
"studyplan" => $plan->simple_model(),
"page" => $page->simple_model(),
"firstperiod" => $firstperiod,
"lastperiod" => $lastperiod,
"periods" => [],
];
// Find all relevant parts in this order
for($i = $firstperiod; $i <= $lastperiod; $i++) {
$period = period::find($page, $i);
$pmodel = [
'period' => $period->model(),
'lines' => [],
];
$lines = studyline::find_page_children($page);
foreach ($lines as $line) {
$lmodel = [
"line" => $line->simple_model(),
"items" => [],
];
$items = studyitem::search_studyline_children($line,$i,studyitem::COURSE);
if (count($items) > 0) {
foreach ($items as $item) {
$lmodel["items"][] = $item->editor_model();
}
// Only include the line if it has actual children - saves us from muddying up the report.
$pmodel['lines'][] = $lmodel;
}
}
$model["periods"][] = $pmodel;
}
return $model;
} }
public static function get_report_data_parameters() : \external_function_parameters { public static function get_report_data_parameters() : \external_function_parameters {
@ -79,22 +143,48 @@ class reportservice extends \external_api {
* Return value description for webservice function list_user_studyplans * Return value description for webservice function list_user_studyplans
*/ */
public static function get_report_data_returns() : \external_description { public static function get_report_data_returns() : \external_description {
return new \external_single_structure([ return new \external_multiple_structure(studyitem::user_structure(), "Information per studyitem");
]);
} }
/**
* Get studyplan page structure for a report
* @param int $pageid ID of user to check specific info for
* @param int $userid ID of user to check specific info for
* @param int|null $firstperiod First period to include in report
* @param int|null $lastperiod Last period to include in report
* @return object
*/
public static function get_report_data($pageid,$userid,$firstperiod=null,$lastperiod=null) { public static function get_report_data($pageid,$userid,$firstperiod=null,$lastperiod=null) {
$page = studyplanpage::find_by_id($pageid);
webservicehelper::require_capabilities(self::CAP_VIEW,$page->studyplan()->context());
$plan = $page->studyplan();
if (!$plan->has_linked_user($userid)) {
throw new \moodle_exception("error:usernotassociated","local_treestudyplan");
}
if (empty($firstperiod)) {
$firstperiod = 1;
}
if (empty($lastperiod)) {
// Index for periods starts at 1, so the period count is same as length
$lastperiod = $page->periods();
}
$model = [];
//TODO: Build report structure
for($i = $firstperiod; $i <= $lastperiod; $i++) {
$lines = studyline::find_page_children($page);
foreach ($lines as $line) {
$items = studyitem::search_studyline_children($line,$i,studyitem::COURSE);
if (count($items) > 0) {
foreach ($items as $item) {
$model[] = $item->user_model($userid);
}
}
}
}
return $model;
} }
public static function get_report_details_parameters() : \external_function_parameters { public static function get_report_details_parameters() : \external_function_parameters {
return new \external_function_parameters([ return new \external_function_parameters([
"itemid" => new \external_value(PARAM_INT, 'id of studyitem'), "itemid" => new \external_value(PARAM_INT, 'id of studyitem'),
@ -106,8 +196,7 @@ class reportservice extends \external_api {
* Return value description for webservice function list_user_studyplans * Return value description for webservice function list_user_studyplans
*/ */
public static function get_report_details_returns() : \external_description { public static function get_report_details_returns() : \external_description {
return new \external_single_structure([ return studyitem::user_structure();
]);
} }
/** /**
@ -117,7 +206,13 @@ class reportservice extends \external_api {
* @return array * @return array
*/ */
public static function get_report_details($itemid,$userid) { public static function get_report_details($itemid,$userid) {
$item = studyitem::find_by_id($itemid);
$plan = $item->studyline()->studyplan();
webservicehelper::require_capabilities(self::CAP_VIEW,$plan->context());
if (!$plan->has_linked_user($userid)) {
throw new \moodle_exception("error:usernotassociated","local_treestudyplan");
}
return $item->user_model($userid);
} }

View File

@ -166,6 +166,47 @@ class studyitem {
return is_numeric($id) && $DB->record_exists(self::TABLE, array('id' => $id)); return is_numeric($id) && $DB->record_exists(self::TABLE, array('id' => $id));
} }
/**
* Webservice structure for simple info
* @param int $value Webservice requirement constant
*/
public static function simple_structure($value = VALUE_REQUIRED) : \external_description {
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'),
"slot" => new \external_value(PARAM_INT, 'slot in the study plan'),
"layer" => new \external_value(PARAM_INT, 'layer in the slot'),
"span" => new \external_value(PARAM_INT, 'how many periods the item spans'),
"course" => courseinfo::simple_structure(VALUE_OPTIONAL),
"badge" => badgeinfo::simple_structure(VALUE_OPTIONAL),
],"",$value);
}
/**
* Webservice model for editor info
* @return array Webservice data model
*/
public function simple_model() {
global $DB;
$model = [
'id' => $this->r->id, // Id is needed in export model because of link references.
'type' => $this->valid() ? $this->r->type : self::INVALID,
'slot' => $this->r->slot,
'layer' => $this->r->layer,
'span' => $this->r->span,
];
$ci = $this->getcourseinfo();
if (isset($ci)) {
$model['course'] = $ci->simple_model();
}
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);
$model['badge'] = $badgeinfo->simple_model();
}
return $model;
}
/** /**
* Webservice structure for editor info * Webservice structure for editor info
* @param int $value Webservice requirement constant * @param int $value Webservice requirement constant
@ -177,7 +218,7 @@ class studyitem {
"conditions" => new \external_value(PARAM_TEXT, 'conditions for completion'), "conditions" => new \external_value(PARAM_TEXT, 'conditions for completion'),
"slot" => new \external_value(PARAM_INT, 'slot in the study plan'), "slot" => new \external_value(PARAM_INT, 'slot in the study plan'),
"layer" => new \external_value(PARAM_INT, 'layer in the slot'), "layer" => new \external_value(PARAM_INT, 'layer in the slot'),
"span" => new \external_value(PARAM_INT, 'how many periods the item spans'), "span" => new \external_value(PARAM_INT, 'how many periods the item spans'),
"course" => courseinfo::editor_structure(VALUE_OPTIONAL), "course" => courseinfo::editor_structure(VALUE_OPTIONAL),
"badge" => badgeinfo::editor_structure(VALUE_OPTIONAL), "badge" => badgeinfo::editor_structure(VALUE_OPTIONAL),
"continuation_id" => new \external_value(PARAM_INT, 'id of continued item'), "continuation_id" => new \external_value(PARAM_INT, 'id of continued item'),
@ -185,8 +226,7 @@ class studyitem {
'in' => new \external_multiple_structure(studyitemconnection::structure()), 'in' => new \external_multiple_structure(studyitemconnection::structure()),
'out' => new \external_multiple_structure(studyitemconnection::structure()), 'out' => new \external_multiple_structure(studyitemconnection::structure()),
]), ]),
]); ],"",$value);
} }
/** /**
@ -413,6 +453,30 @@ class studyitem {
return $list; return $list;
} }
/**
* List all studyitems of a given type in a specific period in this line
* @param studyline $line The studyline to search for
* @param int $slot The slot to search in
* @param int $type The type of items to include
* @return studyitem[]
*/
public static function search_studyline_children(studyline $line,$slot,$type) : array {
global $DB;
$list = [];
$ids = $DB->get_fieldset_select(
self::TABLE, "id",
"line_id = :line_id AND type = :type AND ( slot <= :slota AND ( slot + (span-1) >= :slotb ) ) ORDER BY layer",
['line_id' => $line->id(), 'slota' => $slot, 'slotb' => $slot,'type' => $type]
);
foreach ($ids as $id) {
$item = self::find_by_id($id, $line);
$list[] = $item;
}
return $list;
}
/** /**
* Webservice structure for linking between plans * Webservice structure for linking between plans
* @param int $value Webservice requirement constant * @param int $value Webservice requirement constant

View File

@ -171,7 +171,35 @@ class studyline {
studyitem::editor_structure(), 'filter items'), studyitem::editor_structure(), 'filter items'),
]) ])
) )
]); ],"Study line editor structure",$value);
}
/**
* Webservice structure for simple info
* @param int $value Webservice requirement constant
*/
public static function simple_structure($value = VALUE_REQUIRED) : \external_description {
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'),
],"Study line simple structure",$value);
}
/**
* Webservice model for simple info
* @return array Webservice data model
*/
public function simple_model() : array {
return [
'id' => $this->r->id,
'name' => $this->r->name,
'shortname' => $this->r->shortname,
'color' => $this->r->color,
'sequence' => $this->r->sequence,
];
} }
/** /**

View File

@ -1575,6 +1575,10 @@ body.path-local-treestudyplan .editmode-switch-form > * {
color: var(--danger); color: var(--danger);
} }
.path-local-treestudyplan-studyplan-report {
font: inherit;
}
.path-local-treestudyplan .b-modal-justify-footer-between .modal-footer, .path-local-treestudyplan .b-modal-justify-footer-between .modal-footer,
.features-treestudyplan .b-modal-justify-footer-between .modal-footer { .features-treestudyplan .b-modal-justify-footer-between .modal-footer {
justify-content: space-between; justify-content: space-between;

View File

@ -434,6 +434,7 @@ $string["error:cannotviewcategory"] = 'Error: You do not have access to view thi
$string["error:nostudyplanviewaccess"] = 'Error: You do not have access to view study plans in this category or context: {$a}'; $string["error:nostudyplanviewaccess"] = 'Error: You do not have access to view study plans in this category or context: {$a}';
$string["error:nostudyplaneditaccess"] = 'Error: You do not have access to manage study plans in this category or context: {$a}'; $string["error:nostudyplaneditaccess"] = 'Error: You do not have access to manage study plans in this category or context: {$a}';
$string["error:nocategoriesvisible"] = 'Error: You have no viewing permissions in any category. Therefore the course list remains empty.'; $string["error:nocategoriesvisible"] = 'Error: You have no viewing permissions in any category. Therefore the course list remains empty.';
$string["error:usernotassociated"] = 'Error: Requested student is not associated with this studyplan';
$string["premium:never"] = 'never'; $string["premium:never"] = 'never';
$string["premium:anywhere"] = 'for use anywhere'; $string["premium:anywhere"] = 'for use anywhere';

View File

@ -434,6 +434,7 @@ $string["error:cannotviewcategory"] = 'Fout: Je hebt geen rechten om deze catego
$string["error:nostudyplanviewaccess"] = 'Fout: Je hebt geen rechten om studieplannen in deze categorie of context te bekijken: {$a}'; $string["error:nostudyplanviewaccess"] = 'Fout: Je hebt geen rechten om studieplannen in deze categorie of context te bekijken: {$a}';
$string["error:nostudyplaneditaccess"] = 'Fout: Je hebt geen rechten om studieplannen in deze categorie of context te beheren: {$a}'; $string["error:nostudyplaneditaccess"] = 'Fout: Je hebt geen rechten om studieplannen in deze categorie of context te beheren: {$a}';
$string["error:nocategoriesvisible"] = 'Fout: Je kunt geen cursussen in een categorie bekijken. Daarom blijft de cursuslijst leeg'; $string["error:nocategoriesvisible"] = 'Fout: Je kunt geen cursussen in een categorie bekijken. Daarom blijft de cursuslijst leeg';
$string["error:usernotassociated"] = 'Fout: De gevraagde student is niet gekoppeld aan dit studieplan';
$string["premium:never"] = 'nooit'; $string["premium:never"] = 'nooit';
$string["premium:anywhere"] = 'voor gebruik overal'; $string["premium:anywhere"] = 'voor gebruik overal';

View File

@ -0,0 +1,5 @@
.path-local-treestudyplan-studyplan-report {
font: inherit;
}

View File

@ -3,4 +3,5 @@
@import "invitemanager.scss"; @import "invitemanager.scss";
@import "studyplan.scss"; @import "studyplan.scss";
@import "studyplancard.scss"; @import "studyplancard.scss";
@import "studyplan-report.scss";
@import "bootstraptweaking.scss"; @import "bootstraptweaking.scss";

View File

@ -333,33 +333,34 @@ if ($hassiteconfig) {
/************************************** /**************************************
* *
* Settings page: Cohort sync * Settings page: Premium registration
* *
*************************************/ *************************************/
if (\local_treestudyplan\premium::supported()) {
$pagepremium = new admin_settingpage('local_treestudyplan_settings_premium',
get_string('settingspage_premium', 'local_treestudyplan', null, true));
$pagepremium = new admin_settingpage('local_treestudyplan_settings_premium', // Description heading.
get_string('settingspage_premium', 'local_treestudyplan', null, true)); $pagepremium->add(new admin_setting_heading('local_treestudyplan/premium_heading',
get_string('setting_premium_heading', 'local_treestudyplan'),
get_string('settingdesc_premium_heading', 'local_treestudyplan')
));
// Description heading. // Description heading.
$pagepremium->add(new admin_setting_heading('local_treestudyplan/premium_heading', $pagepremium->add(new admin_setting_description('local_treestudyplan/premium_status',
get_string('setting_premium_heading', 'local_treestudyplan'), get_string('setting_premium_status', 'local_treestudyplan'),
get_string('settingdesc_premium_heading', 'local_treestudyplan') premium::statusdescription() . "<br>&nbsp;<br>" // Add empty row at end.
)); ));
// Description heading. $pagepremium->add(new admin_setting_configtextarea('local_treestudyplan/premium_key',
$pagepremium->add(new admin_setting_description('local_treestudyplan/premium_status', get_string('setting_premium_key', 'local_treestudyplan'),
get_string('setting_premium_status', 'local_treestudyplan'), get_string('settingdesc_premium_key', 'local_treestudyplan'),
premium::statusdescription() . "<br>&nbsp;<br>" // Add empty row at end. "",
)); PARAM_RAW
));
$pagepremium->add(new admin_setting_configtextarea('local_treestudyplan/premium_key', // Add settings page2 to the admin settings category.
get_string('setting_premium_key', 'local_treestudyplan'), $ADMIN->add('local_treestudyplan', $pagepremium);
get_string('settingdesc_premium_key', 'local_treestudyplan'), }
"",
PARAM_RAW
));
// Add settings page2 to the admin settings category.
$ADMIN->add('local_treestudyplan', $pagepremium);
} }

View File

@ -26,6 +26,7 @@ use local_treestudyplan\contextinfo;
use \local_treestudyplan\courseservice; use \local_treestudyplan\courseservice;
use \local_treestudyplan\studyplan; use \local_treestudyplan\studyplan;
use \local_treestudyplan\studyplanpage; use \local_treestudyplan\studyplanpage;
use \local_treestudyplan\premium;
require_once($CFG->libdir.'/weblib.php'); require_once($CFG->libdir.'/weblib.php');
@ -33,11 +34,11 @@ $PAGE->set_url("/local/treestudyplan/studyplan-report.php", array());
require_login(); require_login();
// Figure out the context (category or system, based on either category or context parameter). // Figure out the context (category or system, based on either category or context parameter).
$pageid = required_param('page', 0, PARAM_INT); // Category id. $pageid = required_param('page', PARAM_INT); // Category id.
$page = studyplanpage::find_by_id($pageid); $page = studyplanpage::find_by_id($pageid);
$studyplan = $page->studyplan(); $studyplan = $page->studyplan();
$context = $studyplan->context(); $context = $studyplan->context();
$contexname = (new contextinfo($studyplancontext))->pathstr(); $contexname = (new contextinfo($context))->pathstr();
$firstperiod = optional_param('firstperiod', 0, PARAM_INT); // First period to show $firstperiod = optional_param('firstperiod', 0, PARAM_INT); // First period to show
$lastperiod = optional_param('lastperiod', 0, PARAM_INT); // Last periode to show $lastperiod = optional_param('lastperiod', 0, PARAM_INT); // Last periode to show
@ -49,7 +50,11 @@ $PAGE->set_pagelayout('base');
$PAGE->set_title(get_string('studyplan_report', 'local_treestudyplan')." - ".$contextname); $PAGE->set_title(get_string('studyplan_report', 'local_treestudyplan')." - ".$contextname);
$PAGE->set_heading(get_string('studyplan_report', 'local_treestudyplan')." - ".$contextname); $PAGE->set_heading(get_string('studyplan_report', 'local_treestudyplan')." - ".$contextname);
if(!has_capability('local/treestudyplan:viewuserreports', $studyplancontext)) { if (!premium::enabled()) {
throw new \moodle_exception("error:nopremiumaccess","local_treestudyplan","",premium::statusdescription());
}
if(!has_capability('local/treestudyplan:viewuserreports', $context)) {
throw new \moodle_exception("error:nostudyplanviewaccess","local_treestudyplan","",$contextname); throw new \moodle_exception("error:nostudyplanviewaccess","local_treestudyplan","",$contextname);
} }
@ -82,7 +87,12 @@ print $OUTPUT->header();
</div> </div>
</div> </div>
<div v-cloak> <div v-cloak>
<div class='vue-loader' v-if="!structure">
<div class="spinner-border text-primary" role="status">
<span class="sr-only">Loading...</span>
</div>
</div>
<q-studyplanreport v-else :structure="structure" ></s-studyplanreport>
</div> </div>
</div> </div>
<?php <?php

View File

@ -1575,6 +1575,10 @@ body.path-local-treestudyplan .editmode-switch-form > * {
color: var(--danger); color: var(--danger);
} }
.path-local-treestudyplan-studyplan-report {
font: inherit;
}
.path-local-treestudyplan .b-modal-justify-footer-between .modal-footer, .path-local-treestudyplan .b-modal-justify-footer-between .modal-footer,
.features-treestudyplan .b-modal-justify-footer-between .modal-footer { .features-treestudyplan .b-modal-justify-footer-between .modal-footer {
justify-content: space-between; justify-content: space-between;