Added templating function

This commit is contained in:
PMKuipers 2024-04-19 16:46:30 +02:00
parent 2369610903
commit 82838c57c5
31 changed files with 24337 additions and 93 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,3 +1,3 @@
define("local_treestudyplan/util/date-helper",["exports"],(function(_exports){function format_date(d,short){d instanceof Date||("number"==typeof d&&(d*=1e3),d=new Date(d));let monthformat="short";return!0===short?monthformat="numeric":!1===short&&(monthformat="long"),d.toLocaleDateString(document.documentElement.lang,{year:"numeric",month:monthformat,day:"numeric"})}function studyplanDates(plan){let earliestStart=null,latestEnd=null,openEnded=!1;for(const ix in plan.pages){const page=plan.pages[ix],s=new Date(page.startdate);if(page.enddate||(openEnded=!0),(!earliestStart||s<earliestStart)&&(earliestStart=s),page.enddate){const e=new Date(page.enddate);(!latestEnd||e>latestEnd)&&(latestEnd=e)}}return{start:earliestStart,end:openEnded?null:latestEnd}}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.add_days=function(datestr,days){const date=new Date(datestr);return function(date){const d=new Date(date);let month=""+(d.getMonth()+1),day=""+d.getDate();const year=d.getFullYear();month.length<2&&(month="0"+month);day.length<2&&(day="0"+day);return[year,month,day].join("-")}(new Date(date.getTime()+864e5*days))},_exports.datespaninfo=function(first,last){first instanceof Date||(first=new Date(first));last instanceof Date||(last=new Date(last));first.setHours(0),first.setMinutes(0),first.setSeconds(0),first.setMilliseconds(0),last.setHours(23),last.setMinutes(59),last.setSeconds(59),last.setMilliseconds(999);const dayspan=Math.round((last-first+1)/864e5),years=Math.floor(dayspan/365),ydaysleft=dayspan%365,weeks=Math.floor(ydaysleft/7);return{first:first,last:last,totaldays:dayspan,years:years,weeks:weeks,days:ydaysleft%7,formatted:{first:format_date(first),last:format_date(last)}}},_exports.format_date=format_date,_exports.format_datetime=function(d,short){d instanceof Date||("number"==typeof d&&(d*=1e3),d=new Date(d));let monthformat="short";!0===short?monthformat="numeric":!1===short&&(monthformat="long");return d.toLocaleDateString(document.documentElement.lang,{year:"numeric",month:monthformat,day:"numeric"})+" "+d.toLocaleTimeString(document.documentElement.lang,{timeStyle:"short"})},_exports.studyplanDates=studyplanDates,_exports.studyplanPageTiming=function(page){const now=(new Date).getTime();if(page.timeless)return"present";const start=new Date(page.startdate),end=page.enddate?new Date(page.enddate):null;return start<now?end&&now>end?"past":"present":"future"},_exports.studyplanTiming=function(plan){const now=(new Date).getTime();if(!plan.pages&&0==plan.pages.length||plan.pages[0].timeless)return"present";const dates=studyplanDates(plan);return dates.start<now?dates.end&&now>dates.end?"past":"present":"future"}}));
define("local_treestudyplan/util/date-helper",["exports"],(function(_exports){function format_date(d,short){d instanceof Date||("number"==typeof d&&(d*=1e3),d=new Date(d));let monthformat="short";return!0===short?monthformat="numeric":!1===short&&(monthformat="long"),d.toLocaleDateString(document.documentElement.lang,{year:"numeric",month:monthformat,day:"numeric"})}function studyplanDates(plan){let earliestStart=null,latestEnd=null,openEnded=!1;for(const ix in plan.pages){const page=plan.pages[ix],s=new Date(page.startdate);if(page.enddate||(openEnded=!0),(!earliestStart||s<earliestStart)&&(earliestStart=s),page.enddate){const e=new Date(page.enddate);(!latestEnd||e>latestEnd)&&(latestEnd=e)}}return{start:earliestStart,end:openEnded?null:latestEnd}}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.add_days=function(datestr,days){const date=new Date(datestr);return function(date){const d=new Date(date);let month=""+(d.getMonth()+1),day=""+d.getDate();const year=d.getFullYear();month.length<2&&(month="0"+month);day.length<2&&(day="0"+day);return[year,month,day].join("-")}(new Date(date.getTime()+864e5*days))},_exports.datespaninfo=function(first,last){first instanceof Date||(first=new Date(first));last instanceof Date||(last=new Date(last));first.setHours(0),first.setMinutes(0),first.setSeconds(0),first.setMilliseconds(0),last.setHours(23),last.setMinutes(59),last.setSeconds(59),last.setMilliseconds(999);const dayspan=Math.round((last-first+1)/864e5),years=Math.floor(dayspan/365),ydaysleft=dayspan%365,weeks=Math.floor(ydaysleft/7);return{first:first,last:last,totaldays:dayspan,years:years,weeks:weeks,days:ydaysleft%7,formatted:{first:format_date(first),last:format_date(last)}}},_exports.format_date=format_date,_exports.format_datetime=function(d,short){d instanceof Date||("number"==typeof d&&(d*=1e3),d=new Date(d));let monthformat="short";!0===short?monthformat="numeric":!1===short&&(monthformat="long");return d.toLocaleDateString(document.documentElement.lang,{year:"numeric",month:monthformat,day:"numeric"})+" "+d.toLocaleTimeString(document.documentElement.lang,{timeStyle:"short"})},_exports.studyplanDates=studyplanDates,_exports.studyplanPageTiming=function(page){const now=(new Date).getTime();if(page.timeless)return"present";const start=new Date(page.startdate),end=page.enddate?new Date(page.enddate):null;return start<now?end&&now>end?"past":"present":"future"},_exports.studyplanTiming=function(plan){const now=(new Date).getTime();if(!plan.pages&&0==plan.pages.length||plan.pages[0]&&plan.pages[0].timeless)return"present";const dates=studyplanDates(plan);return dates.start<now?dates.end&&now>dates.end?"past":"present":"future"}}));
//# sourceMappingURL=date-helper.min.js.map

File diff suppressed because one or more lines are too long

11912
amd/build/vue/vue.min.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -28,6 +28,9 @@ import {load_strings} from './util/string-helper';
import {ProcessStudyplan} from './studyplan-processor';
import {download,upload} from './downloader';
import {studyplanTiming} from './util/date-helper';
import { premiumenabled, premiumstatus } from "./util/premium";
import mFormComponents from "./util/mform-helper";
Vue.use(mFormComponents);
import PortalVue from './portal-vue/portal-vue.esm';
Vue.use(PortalVue);
@ -50,6 +53,9 @@ import('core_form/changechecker').then((ns) => {
let strings = load_strings({
studyplan: {
studyplan_select_placeholder: 'studyplan_select_placeholder',
advanced_import_from_file: 'advanced_import_from_file',
advanced_create_from_template: 'advanced_create_from_template',
studyplan_add: "studyplan_add",
},
});
@ -99,6 +105,7 @@ export function init(contextid,categoryid,options) {
activepage: null,
loadingstudyplan: false,
studyplans: [],
templatecount: 0,
text: strings.studyplan,
usedcontexts: [],
@ -128,6 +135,7 @@ export function init(contextid,categoryid,options) {
this.initialize();
},
computed: {
premiumenabled,
dropdown_title(){
if(this.activestudyplan && this.activestudyplan.name){
return this.activestudyplan.name;
@ -170,7 +178,12 @@ export function init(contextid,categoryid,options) {
const hash = location.hash.replace('#','');
if(hash){
const id = hash;
app.selectStudyplan(id);
for(const p of app.studyplans) {
if (p.id == id) {
app.selectStudyplan(id);
break;
}
}
}
}).catch(notification.exception);
@ -180,6 +193,16 @@ export function init(contextid,categoryid,options) {
}])[0].then(function(response){
app.usedcontexts = response;
}).catch(notification.exception);
this.refreshTemplateCount();
},
refreshTemplateCount(){
call([{
methodname: 'local_treestudyplan_count_templates',
args: { }
}])[0].then(function(response){
app.templatecount = response;
}).catch(notification.exception);
},
closeStudyplan() {
app.activestudyplan = null;

View File

@ -447,25 +447,23 @@ export default {
}
}
if (self.studyplans.present.length == 1) {
// Directly show the current study plan if it's the only current one
const plan = self.studyplans.present[0];
if (!plan.suspended) {
self.selectStudyplan(plan);
}
} else {
// If there is but a single studyplan, select it anyway, even if it is not current...
if (this.studyplancount == 1) {
if(self.studyplans.future.lengh > 0) {
const plan = self.studyplans.future[0];
if (!plan.suspended) {
self.selectStudyplan(plan);
}
} else {
const plan = self.studyplans.past[0];
if (!plan.suspended) {
self.selectStudyplan(plan);
}
// If there is but a single studyplan, select it anyway, even if it is not current...
if (this.studyplancount == 1) {
if (self.studyplans.present.length > 0) {
// Directly show the current study plan if it's the only current one
const plan = self.studyplans.present[0];
if (!plan.suspended) {
self.selectStudyplan(plan);
}
} else if(self.studyplans.future.lengh > 0) {
const plan = self.studyplans.future[0];
if (!plan.suspended) {
self.selectStudyplan(plan);
}
} else {
const plan = self.studyplans.past[0];
if (!plan.suspended) {
self.selectStudyplan(plan);
}
}
}

View File

@ -980,7 +980,7 @@ export default {
{
call([{
methodname: 'local_treestudyplan_find_coach',
args: { like: searchtext, exclude_id: self.value.id}
args: { like: searchtext, studyplan_id: self.value.id}
}])[0].then(function(response){
self.search.coaches = response.map(self.userOptionModel);
}).catch(notification.exception);

View File

@ -168,7 +168,7 @@ export function studyplanTiming(plan) {
const now = new Date().getTime();
// If timeless or no timing info is available, all plans are present
if (!plan.pages && plan.pages.length == 0 || plan.pages[0].timeless) {
if (!plan.pages && plan.pages.length == 0 || (plan.pages[0] && plan.pages[0].timeless)) {
return 'present';
}

File diff suppressed because one or more lines are too long

View File

@ -140,12 +140,10 @@ class associationservice extends \external_api {
INNER JOIN {local_treestudyplan_page} p ON l.page_id = p.id
WHERE a.userid = :userid AND p.studyplan_id = :studyplanid";
$lastaccess = $DB->get_field_sql($lasql,["userid" => $userid, "studyplanid" => $studyplanid]);
debug::write("Got lastaccess '{$lastaccess}' for user {$userid} in plan {$studyplanid}");
} else {
$lasql = "SELECT MAX(a.timeaccess) FROM {user_lastaccess} a
WHERE a.userid = :userid";
$lastaccess = $DB->get_field_sql($lasql,["userid" => $userid]);
debug::write("Got lastaccess '{$lastaccess}' for user {$userid} in any course");
}
@ -819,8 +817,7 @@ class associationservice extends \external_api {
public static function find_coach_parameters() : \external_function_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),
'context_id' => new \external_value(PARAM_INT, 'context for this request', VALUE_OPTIONAL),
'studyplan_id' => new \external_value(PARAM_INT, 'studyplan id to associate for', VALUE_OPTIONAL),
] );
}
@ -838,12 +835,13 @@ class associationservice extends \external_api {
* @param int $contextid Context to search (default system)
* @return array
*/
public static function find_coach($like, $excludeid = null, $contextid = 1) {
public static function find_coach($like, $studyplan_id) {
global $CFG, $DB;
// Only allow this if the user has the right to edit in this context.
$context = webservicehelper::find_context($contextid);
$studyplan = studyplan::find_by_id($studyplan_id);
$context = $studyplan->context();
webservicehelper::require_capabilities(self::CAP_EDIT, $context);
$pattern = "%{$like}%";
@ -855,7 +853,7 @@ class associationservice extends \external_api {
WHERE u.deleted != 1 AND (firstname LIKE :pattern_fn OR lastname LIKE :pattern_ln OR username LIKE :pattern_un)";
if (isset($excludeid) && is_numeric($excludeid)) {
$sql .= " AND (j.studyplan_id IS NULL OR j.studyplan_id != :exclude_id)";
$params['exclude_id'] = $excludeid;
$params['exclude_id'] = $studyplan;
}
$users = [];

View File

@ -69,7 +69,7 @@ abstract class formbase extends \moodleform {
if($data) {
return $this->process_submitted_data($data);
} else {
throw new \moodle_exception('no_form_data','local_treestudyplan');
throw new \moodle_exception('no_form_data','local_treestudyplan','',null,$data);
}
}

View File

@ -7,6 +7,7 @@ require_once($CFG->dirroot.'/repository/lib.php');
use local_treestudyplan\aggregator;
use local_treestudyplan\studyplan;
use local_treestudyplan\premium;
use local_treestudyplan\contextinfo;
use local_treestudyplan\studyplanservice;
use local_treestudyplan\courseservice;
use local_treestudyplan\form\text_integer;
@ -206,9 +207,10 @@ class studyplan_editform extends formbase {
$field = 'enddate';
$mform->addElement('date_selector',$field,
get_string('studyplan_startdate','local_treestudyplan'),
get_string('studyplan_enddate','local_treestudyplan'),
[]);
$mform->addRule($field, null, 'required', null, 'client');
}
$field = 'periods';
$mform->addElement('text_integer',$field,
@ -225,6 +227,15 @@ class studyplan_editform extends formbase {
get_string('studyplan_suspend_details','local_treestudyplan'),
[],
);
if (premium::enabled()) {
$field = 'template';
$mform->addElement('advcheckbox',$field,
get_string('studyplan_template','local_treestudyplan'),
get_string('studyplan_template_details','local_treestudyplan'),
[],
);
}
}
$aggregators = [];
@ -234,7 +245,8 @@ class studyplan_editform extends formbase {
$aggregators[$a['id']] = $a['name'];
}
}
$mform->addElement('select','aggregation',
$field = 'aggregation';
$mform->addElement('select',$field,
get_string('choose_aggregation_style','local_treestudyplan'),
$aggregators);
@ -303,9 +315,9 @@ class studyplan_editform extends formbase {
}
$aggregation_config = json_encode($ag_cfg);
if($customdata->create) {
if ($customdata->create) {
// Use our own abstraction to update the record, so caches are maintained
// Use our own abstraction to create the record, so caches are maintained
$plan = studyplan::add(['name' => $entry->name,
'shortname' => $entry->shortname,
'idnumber' => $entry->idnumber,
@ -351,6 +363,7 @@ class studyplan_editform extends formbase {
'aggregation' => $entry->aggregation,
'aggregation_config' => $aggregation_config,
'suspended' => $entry->suspended,
'template' => $entry->template,
]);
}

View File

@ -0,0 +1,172 @@
<?php
namespace local_treestudyplan\form;
defined('MOODLE_INTERNAL') || die();
require_once($CFG->dirroot.'/repository/lib.php');
use local_treestudyplan\aggregator;
use local_treestudyplan\studyplan;
use local_treestudyplan\premium;
use local_treestudyplan\contextinfo;
use local_treestudyplan\studyplanservice;
use local_treestudyplan\courseservice;
use local_treestudyplan\form\text_integer;
use moodle_exception;
use stdClass;
/**
* Moodleform class for the studyplan editor. A Moodleform is used here to facilitate a rich editor
* in the studyplan description
*/
class studyplan_fromtemplateform extends formbase {
/**
* Capability required to edit study plans
* @var string
*/
const CAP_EDIT = "local/treestudyplan:editstudyplan";
/**
* Translate parameters into customdata.
*
* @param object $params The parameters for form initialization
* @return array Form data based on parameters
*/
public static function init_customdata(object $params) {
$customdata = new stdClass;
$customdata->context = \context::instance_by_id($params->contextid);
return $customdata;
}
/**
* Validate security access for this form based on the customdata generated by init_customdata
* Return true if validation passes, false or throw an exception if it does not.
*
* @param object $customdata The customdata for this form
* @return bool True if security validation passes.
* @throws \moodle_exception if access denied for a specific reason.
*/
public static function check_security(object $customdata) {
/*webservicehelper::require_capabilities(self::CAP_EDIT,$customdata->context); */
}
/**
* Generate form data from parameters
* Also validate parameters and access permissions here
*
* @param object $customdata The parameters for form initialization
* @return array Form data based on parameters
*/
public function init_formdata(object $customdata) {
global $DB;
/* Use direct database retrieval to avoid our abstractions causing trouble
with existing moodle code assumptions.
The form API does seem needlessly convoluted in it's use, but we need the editor...
*/
$entry = new stdClass;
$entry->context_id = $customdata->context->id;
// Determine the next august 1st for default value purposes.
$august = strtotime("first day of august this year");
if($august < time()) {
$august = strtotime("first day of august next year");
}
$entry->startdate = $august;
$entry->enddate = $august + (364*24*60*60); // Not bothering about leap years here.
$entry->periods = 4;
$entry->template_id = 0;
return $entry;
}
/**
* Set up the form definition
*/
public function definition() {
$mform = $this->_form;
$customdata = (object)$this->_customdata;
// Register integer type
text_integer::Register();
// Define the form
$templatelist = [];
foreach(studyplan::find_template() as $s){
$c = (new contextinfo($s->context()))->model();
$templatelist[$s->id()] = implode(" / ",$c['path']) . " / " . $s->name();
}
$field = 'template_id';
$mform->addElement('autocomplete', $field,
get_string('studyplan_fromtemplate','local_treestudyplan'),
$templatelist);
$mform->addRule($field, null, 'required', null, 'client');
$field = 'name';
$mform->addElement('text',$field,
get_string('studyplan_name','local_treestudyplan'),
[]);
$mform->addRule($field, null, 'required', null, 'client');
$field = 'shortname';
$mform->addElement('text',$field,
get_string('studyplan_shortname','local_treestudyplan'),
[]);
$mform->addRule($field, null, 'required', null, 'client');
$field = 'idnumber';
$mform->addElement('text',$field,
get_string('studyplan_idnumber','local_treestudyplan'),
[]);
$contextlist = [];
foreach(courseservice::list_available_categories('edit') as $c){
$contextlist[$c['context_id']] = implode(" / ",$c['category']['path']);
}
$mform->addElement('autocomplete', 'context_id',
get_string('studyplan_context','local_treestudyplan'),
$contextlist);
$mform->addRule('context_id', null, 'required', null, 'client');
$timeless = \get_config("local_treestudyplan","timelessperiods");
if ( !$timeless) {
// Only add these fields if the studyplans are timed
$field = 'startdate';
$mform->addElement('date_selector',$field,
get_string('studyplan_startdate','local_treestudyplan'),
[]);
$mform->addRule($field, null, 'required', null, 'client');
}
}
/**
* Process the submitted data and perform necessary actions
* @param object $entry The processed form data;
* @return bool false if submission not successful
* @throws \moodle_exception if an error must be given for a specific reason.
*/
protected function process_submitted_data($entry) {
$customdata = (object)$this->_customdata;
// Find template study plan.
$template = studyplan::find_by_id($entry->template_id);
// Copy template plan.
$plan = $template->duplicate($entry->name,$entry->shortname,$entry->context_id,$entry->idnumber,$entry->startdate);
/* Return the simple model of the plan to make sure we can update stuff.
Parse it through the clean_returnvalue function of exernal api (of which studyplanservice is a subclass)
so we return it in a consistent way
*/
return studyplanservice::clean_returnvalue(studyplan::simple_structure(),$plan->simple_model());
}
}

View File

@ -802,14 +802,14 @@ class studyline {
* @param studyplan $newstudyplan Studyplan to copy the line into
* @param array $translation Mapping array of old item ids to new item ids for connection matching
*/
public function duplicate($newstudyplan, &$translation) : self {
public function duplicate($newpage, &$translation) : self {
global $DB;
// Clone the database fields.
$fields = clone $this->r;
// Set new studyplan id.
unset($fields->id);
$fields->studyplan_id = $newstudyplan->id();
$fields->page_id = $newpage->id();
// Create new record with the new data.
$id = $DB->insert_record(self::TABLE, (array)$fields);
$new = self::find_by_id($id);

View File

@ -21,6 +21,9 @@
*/
namespace local_treestudyplan;
use DateInterval;
defined('MOODLE_INTERNAL') || die();
require_once($CFG->libdir.'/externallib.php');
@ -130,6 +133,20 @@ class studyplan {
return boolval($this->r->suspended);
}
/**
* Determine earliest start date of a page
* @return \DateTime|null
*/
public function startdate() {
$date = null;
foreach($this->pages() as $p) {
if (!isset($date) || $p->startdate() < $date) {
$date = $p->startdate();
}
}
return $date;
}
private function icon() {
global $CFG;
@ -419,7 +436,8 @@ class studyplan {
'context_id',
'aggregation',
'aggregation_config',
'suspended'
'suspended',
'template'
];
$info = ['id' => $this->id, ];
foreach ($editable as $f) {
@ -464,7 +482,7 @@ class studyplan {
/**
* Find all studyplans in a given context or the system context
* @param int $contextid Optional contextid to search in. System context used if left empty
* @param int $contextid Optional contextid to search in. ANY context used if left empty
* @return studyplan[]
*/
public static function find_all($contextid = -1) : array {
@ -489,6 +507,57 @@ class studyplan {
return $list;
}
/**
* Find all template studyplans in a given context or the system context
* @param int $contextid Optional contextid to search in. ANY context used if left empty
* @return studyplan[]
*/
public static function find_template($contextid = -1) : array {
global $DB, $USER;
$list = [];
$templatewhere = "template = 1";
if ($contextid <= 0) {
$ids = $DB->get_fieldset_select(self::TABLE, "id", $templatewhere);
} else {
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} AND {$templatewhere}", ["contextid" => $contextid]);
}
foreach ($ids as $id) {
$list[] = self::find_by_id($id);
}
return $list;
}
/**
* Count all template studyplans in a given context or the system context
* @param int $contextid Optional contextid to search in. ANY context used if left empty
* @return int
*/
public static function count_template($contextid = -1) : int {
global $DB, $USER;
$list = [];
$templatewhere = "template = 1";
if ($contextid <= 0) {
return $DB->count_records_select(self::TABLE, $templatewhere);
} else {
if ($contextid == 1) {
$contextid = 1;
$where = "context_id <= :contextid OR context_id IS NULL";
} else {
$where = "context_id = :contextid";
}
return $DB->count_records_select(self::TABLE, "{$where} AND {$templatewhere}", ["contextid" => $contextid]);
}
}
/**
* Find all studyplans in a given context or the system context with a specific short name
* (Used in generating random grades for development)
@ -740,7 +809,7 @@ class studyplan {
*/
public static function duplicate_plan($planid, $name, $shortname) : array {
$ori = self::find_by_id($planid);
$new = $ori->duplicate($name, $shortname);
$new = $ori->duplicate($name, $shortname,$ori->context()->id);
return $new->simple_model();
}
@ -749,23 +818,61 @@ class studyplan {
* @param string $name New fullname of studyplan
* @param string $shortname New shortname of studyplan
*/
public function duplicate($name, $shortname) : self {
public function duplicate($name, $shortname, $contextid, $idnumber=null, $newstartdate = null) : self {
// First duplicate the studyplan structure.
$newplan = self::add([
'name' => $name,
'shortname' => $shortname,
'idnumber' => ($idnumber?$idnumber:$this->r->idnumber),
'context_id' => $contextid,
'description' => $this->r->description,
'descriptionformat' => $this->r->descriptionformat,
'aggregation' => $this->r->aggregation,
'aggregation_config' => $this->r->aggregation_config
]);
],true);
//TODO: Copy any files related to this userid....
// Copy any files related to this studyplan.
$areas = ["icon","studyplan"];
$fs = \get_file_storage();
foreach ($areas as $area) {
$files = $fs->get_area_files(
\context_system::instance()->id,
"local_treestudyplan",
$area,
$this->id()
);
foreach ($files as $file) {
$path = $file->get_filepath();
$filename = $file->get_filename();
if ($filename != ".") {
// Prepare new file info for the target file.
$fileinfo = [
'contextid' => \context_system::instance()->id, // System context.
'component' => 'local_treestudyplan', // Your component name.
'filearea' => $area, // Area name.
'itemid' => $newplan->id(), // Study plan id.
'filepath' => $path, // Original file path.
'filename' => $filename, // Original file name.
];
// Copy existing file into new file.
$fs->create_file_from_storedfile($fileinfo, $file);
debug::write("Copied {$area}::{$path}{$filename} from {$this->id} to {$newplan->id()}");
}
}
}
// Next, copy the studylines.
$timeless = \get_config("local_treestudyplan","timelessperiods");
if (!$timeless && $newstartdate) {
$newstart = new \DateTime(date("Y-m-d",$newstartdate));
$oldstart = $this->startdate();
$timeoffset = $oldstart->diff($newstart);
} else {
$timeoffset = new \DateInterval("P0D");
}
foreach ($this->pages() as $p) {
$newchild = $p->duplicate($newplan);
foreach ($this->pages() as $page) {
$newchild = $page->duplicate($newplan,$timeoffset);
}
return $newplan;

View File

@ -450,7 +450,11 @@ class studyplanpage {
* Duplicate this studyplan page
* @param studyplan $newstudyplan Studyplan to copy the page into
*/
public function duplicate(studyplan $newstudyplan) : self {
public function duplicate(studyplan $newstudyplan, $timeoffset = null) : self {
if ($timeoffset == null) {
$timeoffset = new \DateInterval("P0D");
}
// First duplicate the studyplan structure.
$new = self::add([
'studyplan_id' => $newstudyplan->id(),
@ -458,17 +462,55 @@ class studyplanpage {
'shortname' => $this->r->shortname,
'description' => $this->r->description,
'pages' => $this->r->pages,
'startdate' => $this->r->startdate,
'enddate' => empty($this->r->enddate) ? null : $this->r->enddate,
'startdate' => $this->startdate()->add($timeoffset)->format("Y-m-d"),
'enddate' => empty($this->r->enddate) ? null : ($this->enddate()->add($timeoffset)->format("Y-m-d")),
]);
// Next, copy the studylines.
// Copy any files related to this page.
$areas = ["studyplanpage"];
$fs = \get_file_storage();
foreach ($areas as $area) {
$files = $fs->get_area_files(
\context_system::instance()->id,
"local_treestudyplan",
$area,
$this->id()
);
foreach ($files as $file) {
$path = $file->get_filepath();
$filename = $file->get_filename();
if ($filename != ".") {
// Prepare new file info for the target file.
$fileinfo = [
'contextid' => \context_system::instance()->id, // System context.
'component' => 'local_treestudyplan', // Your component name.
'filearea' => $area, // Area name.
'itemid' => $new->id(), // Study plan id.
'filepath' => $path, // Original file path.
'filename' => $filename, // Original file name.
];
// Copy existing file into new file.
$fs->create_file_from_storedfile($fileinfo, $file);
}
}
}
// Adjust the time offsets of the periods.
foreach ($this->periods() as $p) {
$p->edit([
'startdate' => $p->startdate()->add($timeoffset)->format("Y-m-d"),
'enddate' => $p->enddate()->add($timeoffset)->format("Y-m-d"),
]);
}
// Now , copy the studylines.
$children = studyline::find_page_children($this);
$itemtranslation = [];
$linetranslation = [];
$itemtranslation = []; // Stores ids of original and copied items.
$linetranslation = []; // Stores ids of original and copied lines.
foreach ($children as $c) {
$newchild = $c->duplicate($this, $itemtranslation);
$translation = [];
$newchild = $c->duplicate($new, $translation);
$itemtranslation += $translation; // Fixes behaviour where translation array is reset on each call.
$linetranslation[$c->id()] = $newchild->id();
}
@ -479,7 +521,7 @@ class studyplanpage {
// Copy based on the outgoing connections of each item, to avoid duplicates.
$connections = studyitemconnection::find_outgoing($itemid);
foreach ($connections as $conn) {
studyitemconnection::connect($itemtranslation[$conn->from_id], $itemtranslation[$conn->to_id]);
studyitemconnection::connect($itemtranslation[$conn->from_id()], $itemtranslation[$conn->to_id()]);
}
}
return $new;
@ -788,13 +830,17 @@ class studyplanpage {
// Next, let each study line import the study items.
$itemtranslation = [];
$connections = [];
$itemconnections = [];
foreach ($model as $ix => $linemodel) {
$linemap[$ix]->import_studyitems($linemodel["slots"], $itemtranslation, $connections);
$translation = [];
$connections = [];
$linemap[$ix]->import_studyitems($linemodel["slots"], $translation, $connections);
$itemtranslation += $translation; // Fixes behaviour where translation array is reset on each call.
$itemconnections += $connections;
}
// Finally, create the links between the study items.
foreach ($connections as $from => $dests) {
foreach ($itemconnections as $from => $dests) {
foreach ($dests as $to) {
studyitemconnection::connect($from, $itemtranslation[$to]);
}

View File

@ -2193,4 +2193,35 @@ class studyplanservice extends \external_api {
];
}
/***************************
* *
* count_templates *
* *
***************************/
/**
* Parameter description for webservice function get_teaching_page
*/
public static function count_templates_parameters() : \external_function_parameters {
return new \external_function_parameters( [
] );
}
/**
* Return value description for webservice function get_teaching_page
*/
public static function count_templates_returns() : \external_description {
return new \external_value(PARAM_INT,"True if the requesting user can unenrol students");
}
/**
* Get all or one studyplan the current user is teaching in
* @param int $studyplanid ID of studyplan to retrieve
* @return array
*/
public static function count_templates() {
\external_api::validate_context(\context_system::instance());
return studyplan::count_template();
}
}

View File

@ -54,7 +54,7 @@ class utilityservice extends \external_api {
*/
$modmoodleform = "$CFG->dirroot/local/treestudyplan/classes/form/{$formname}.php";
if (!file_exists($modmoodleform)) {
throw new \moodle_exception('noformfile', 'local_treestudyplan');
throw new \moodle_exception('noformfile', 'local_treestudyplan','',$formname);
}
$mformclassname = "\\local_treestudyplan\\form\\{$formname}";

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" ?>
<XMLDB PATH="local/treestudyplan/db" VERSION="20240309" COMMENT="XMLDB file for Moodle local/treestudyplan"
<XMLDB PATH="local/treestudyplan/db" VERSION="20240415" COMMENT="XMLDB file for Moodle local/treestudyplan"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="../../../lib/xmldb/xmldb.xsd"
>
@ -34,6 +34,7 @@
<FIELD NAME="context_id" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false"/>
<FIELD NAME="csync_flag" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="1" SEQUENCE="false" COMMENT="Flags the studyplan as needing a csync update"/>
<FIELD NAME="suspended" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
<FIELD NAME="template" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>

View File

@ -814,4 +814,14 @@ $functions = [
'capabilities' => '',
'loginrequired' => true,
],
'local_treestudyplan_count_templates' => [ // Web service function name.
'classname' => '\local_treestudyplan\studyplanservice', // Class containing the external function.
'methodname' => 'count_templates', // External function name.
'description' => 'Count number of templates',
'type' => 'read', // Database rights of the web service function (read, write).
'ajax' => true,
'capabilities' => '',
'loginrequired' => true,
],
];

View File

@ -618,5 +618,21 @@ function xmldb_local_treestudyplan_upgrade($oldversion) {
upgrade_plugin_savepoint(true, 2024031000, 'local', 'treestudyplan');
}
if ($oldversion < 2024041501) {
// Define field template to be added to local_treestudyplan.
$table = new xmldb_table('local_treestudyplan');
$field = new xmldb_field('template', XMLDB_TYPE_INTEGER, '1', null, XMLDB_NOTNULL, null, '0', 'suspended');
// Conditionally launch add field template.
if (!$dbman->field_exists($table, $field)) {
$dbman->add_field($table, $field);
}
// Treestudyplan savepoint reached.
upgrade_plugin_savepoint(true, 2024041501, 'local', 'treestudyplan');
}
return true;
}

View File

@ -25,6 +25,7 @@ require_once("../../config.php");
require_once($CFG->libdir.'/weblib.php');
use \local_treestudyplan\contextinfo;
use \local_treestudyplan\studyplan;
use \local_treestudyplan\courseservice;
use \local_treestudyplan\debug;
@ -146,19 +147,28 @@ print $OUTPUT->header();
:key='studyplan.id'
>{{ studyplan.name }}</b-form-select-option>
</b-form-select>&nbsp;
<t-studyplan-edit
@created="onStudyPlanCreated"
v-if='!activestudyplan && !loadingstudyplan'
mode="create"
v-model="create.studyplan"
type="button"
variant="primary"
default-aggregation="<?php print(get_config("local_treestudyplan","aggregation_mode")); ?>"
:contextid='contextid'
><i class='fa fa-plus'></i> <?php t("studyplan_add");?></t-studyplan-edit>
<b-button v-if='!activestudyplan && !loadingstudyplan'
variant='danger' href='#' role='presentation' @click="import_studyplan "
><i class='fa fa-upload'></i> <?php t("advanced_import_from_file");?></b-button>
<template v-if='!activestudyplan && !loadingstudyplan'>
<t-studyplan-edit
@created="onStudyPlanCreated"
mode="create"
v-model="create.studyplan"
type="button"
variant="primary"
default-aggregation="<?php print(get_config("local_treestudyplan","aggregation_mode")); ?>"
:contextid='contextid'
><i class='fa fa-plus'></i> {{ text.studyplan_add }}</t-studyplan-edit>
<b-button
variant='danger' href='#' role='presentation' @click="import_studyplan "
><i class='fa fa-upload'></i> {{ text.advanced_import_from_file }}</b-button
><mform v-if="premiumenabled && templatecount > 0"
name="studyplan_fromtemplateform"
:params="{contextid: contextid }"
@saved="onStudyPlanCreated"
variant="success"
type="button"
:title="text.advanced_import_from_file"
><slot><i class='fa fa-clone'></i> {{ text.advanced_create_from_template }}</slot></mform>
</template>
</div>
<div class='t-studyplan-container'>
@ -183,7 +193,7 @@ print $OUTPUT->header();
>
<template #title>
<span class='s-studyplan-card-title-buttons'>
<t-studyplan-edit v-model="studyplans[planindex]" @moved="movedStudyplan"></t-studyplan-edit>
<t-studyplan-edit v-model="studyplans[planindex]" @moved="movedStudyplan" @input="refreshTemplateCount"></t-studyplan-edit>
<t-studyplan-associate v-model="studyplans[planindex]"></t-studyplan-associate>
</span>
</template>

View File

@ -175,8 +175,12 @@ $string["studyplan_enddate"] = 'End date of plan';
$string["studyplan_noneselected"] = "Pick a study plan to start editing";
$string["studyplan_select"] = "Study plan:";
$string["studyplan_select_placeholder"] = "Select study plan";
$string["studyplan_suspend"] = 'Suspend studyplan';
$string["studyplan_suspend"] = 'Suspend study plan';
$string["studyplan_suspend_details"] = 'Suspend studyplan for all except studyplan managers';
$string["studyplan_fromtemplate"] = 'Based on template';
$string["studyplan_template"] = 'Mark as template';
$string["studyplan_template_details"] = 'Mark this studyplan as a possible template for new studyplans';
$string["studyplan_emptytemplate"] = "Empty study plan";
$string["studyline_add"] = 'Add study line';
$string["studyline_edit"] = 'Edit study line';
@ -387,7 +391,8 @@ $string["advanced_backup_page"] = 'Backup active studyplan page ';
$string["advanced_export"] = 'Export to file';
$string["advanced_export_csv_page"] = 'Export current page as CSV';
$string["advanced_export_csv_plan"] = 'Export studyplan CSV';
$string["advanced_import_from_file"] = "Restore studyplan from file";
$string["advanced_import_from_file"] = "Load from file";
$string["advanced_create_from_template"] = "Create from template";
$string["advanced_purge"] = "Delete";
$string["advanced_purge_plan"] = "Delete entire study plan";

View File

@ -86,7 +86,7 @@ $string["setting_navigation_heading"] = 'Navigatie';
$string["settingdesc_navigation_heading"] = 'Instellingen voor navigatie';
$string["setting_primary_nav_autofill"] = '<i>Aangepast menu items</i> automatisch aanvullen';
$string["settingdesc_primary_nav_autofill"] = 'Om in het primaire navigatiemenu de studieplan links te tonen (vooral in Moodle 4.x), moeten regels worden toegevoegd in <b>Uiterlijk</b>&nbsp;-&nbsp;<b>Thema&nbsp;instellingen</b>&nbsp;<b>Aangepast&nbsp;menu&nbsp;items</b><br>Zet deze functie hier uit als dat niet gewenst is, b.v. als je Moodle (3.11 of lager) thema flat navigation gebruikt.';
$string["setting_defaulticon"] = 'Standaard afbeelding foor studieplan';
$string["setting_defaulticon"] = 'Standaard afbeelding voor studieplan';
$string["settingdesc_defaulticon"] = 'Stel standaard afbeelding in om weer te geven als een studieplan geen eigen afbeelding heeft ingesteld';
$string["settingspage_csync"] = 'Site-groepen en gebruikerskoppelingen doorzetten naar cursussen';
@ -177,7 +177,10 @@ $string["studyplan_select"] = "Studieplan";
$string["studyplan_select_placeholder"] = "Selecteer studieplan";
$string["studyplan_suspend"] = 'Studieplan tijdelijk uitschakelen';
$string["studyplan_suspend_details"] = 'Inkijken van studieplan tijdelijk uitschakelen, behalve voor beheerders.';
$string["studyplan_fromtemplate"] = 'Baseer plan op template';
$string["studyplan_template"] = 'Bruikbaar als template';
$string["studyplan_template_details"] = 'Dit studieplan kan worden gebruikt als template voor nieuwe studieplannen';
$string["studyplan_emptytemplate"] = "Leeg studieplan";
$string["studyline_add"] = 'Nieuwe leerlijn';
$string["studyline_edit"] = 'Leerlijn bewerken';
@ -321,7 +324,7 @@ $string["competency_aggregator_title"] = 'Curuscompetenties';
$string["competency_aggregator_desc"] = 'Gebruik de bij de cursus ingestelde competenties';
$string["setting_bistate_heading"] = 'Standaardwaarden voor Behaald + Vereidte leerdoelen ';
$string["setting_bistate_heading"] = 'Standaardwaarden voor Behaald + Vereiste leerdoelen ';
$string["settingdesc_bistate_heading"] = 'Stel de standaardwaarden in voor deze verzamelmethode';
$string["choose_aggregation_style"] = 'Kies berekening van eindresultaten';
@ -331,7 +334,7 @@ $string["select_scaleitem"] = 'Kies...';
$string["setting_bistate_thresh_excellent"] = 'Drempelwaarde voor uitstekend (%)';
$string["settingdesc_bistate_thresh_excellent"] = 'Minimumpercentage behaalde doelen voor "Uitstekend"';
$string["setting_bistate_thresh_good"] = 'Drempelwaarde voor goed (%)';
$string["settingdesc_bistate_thresh_good"] = 'Minimum ercentage behaalde doelen voor "Goed"';
$string["settingdesc_bistate_thresh_good"] = 'Minimumpercentage behaalde doelen voor "Goed"';
$string["setting_bistate_thresh_completed"] = 'Drempelwaarde voor voltooid (%)';
$string["settingdesc_bistate_thresh_completed"] = 'Minimumpercentage behaalde doelen voor "Voltooid"';
$string["setting_bistate_support_failed"] = 'Onvoldoende ingeschakeld';
@ -391,6 +394,7 @@ $string["advanced_export"] = 'Exporteer naar bestand';
$string["advanced_export_csv_page"] = 'Exporteer actieve tabblad naar CSV';
$string["advanced_export_csv_plan"] = 'Exporteer studeeplan naar CSV';
$string["advanced_import_from_file"] = "Vanuit bestand";
$string["advanced_create_from_template"] = "Nieuw van template";
$string["advanced_purge"] = "Verwijderen";
$string["advanced_purge_plan"] = "Studieplan verwijderen";

View File

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