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 private $linked_userids = null; // cache lookup of linked users (saves queries) private $page_cache = null; public function aggregator(){ 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 shortname(){ return $this->r->shortname; } public function name(){ return $this->r->name; } public function pages(){ // cached version of find_studyplan_children. // (may be premature optimization, since // find_studyplan_children also does some caching) if(empty($this->page_cache)){ $this->page_cache = studyplanpage::find_studyplan_children($this); } return $this->page_cache; } /** * 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 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'), "context_id" => new \external_value(PARAM_INT, 'context_id of studyplan'), "description"=> new \external_value(PARAM_TEXT, 'description 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(), "pages" => new \external_multiple_structure(studyplanpage::simple_structure(),'pages'), ],'Basic studyplan info',$value); } public function simple_model(){ $pages = []; foreach($this->pages() as $p){ $pages[] = $p->simple_model(); } return [ 'id' => $this->r->id, 'name' => $this->r->name, 'shortname' => $this->r->shortname, 'context_id' => $this->context()->id, 'description' => $this->r->description, 'aggregation' => $this->r->aggregation, 'aggregation_config' => $this->aggregator->config_string(), 'aggregation_info' => $this->aggregator->basic_model(), 'pages' => $pages, ]; } 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'), "context_id" => new \external_value(PARAM_INT, 'context_id of studyplan'), "aggregation" => new \external_value(PARAM_TEXT, 'selected aggregator'), "aggregation_config" => new \external_value(PARAM_TEXT, 'config string for aggregator'), "aggregation_info" => aggregator::basic_structure(), "pages" => new \external_multiple_structure(studyplanpage::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, 'context_id' => $this->context()->id, "aggregation" => $this->r->aggregation, "aggregation_config" => $this->aggregator->config_string(), 'aggregation_info' => $this->aggregator->basic_model(), 'pages' => [], ]; foreach($this->pages() as $p) { $model['pages'][] = $p->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,$bare=false){ global $CFG, $DB; $addable = ['name','shortname','description','context_id','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); $plan = self::findById($id); // make sure the new studyplan is immediately cached // Start temporary skräpp code // Add a single page and copy the names.This keeps the data sane until the upgrade to // real page management is done // On import, adding an empty page messes things up for now, so we have an option to skip this.... // TODO: Remove this when proper page management is implemented if(!$bare){ $pageaddable = ['name','shortname','description',]; $pageinfo = ['studyplan_id' => $id]; foreach($pageaddable as $f){ if(array_key_exists($f,$fields)){ if($f == "name"){ $pageinfo["fullname"] = $fields[$f]; } else { $pageinfo[$f] = $fields[$f]; } } } $page = studyplanpage::add($pageinfo); $plan->page_cache = [$page]; } // End temporary skräpp code return $plan; } public function edit($fields){ global $DB; $editable = ['name','shortname','description','context_id','periods','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); // Start temporary skräpp code // TODO: Until proper page editing is implemented, copy data from studyplan to it's first page // This keeps the data sane until the upgrade is done. if(count($this->pages()) == 1){ // update the info to the page as well $page = $this->pages()[0]; $pageeditable = ['name','shortname','description']; $pageinfo = []; foreach($pageeditable as $f){ if(array_key_exists($f,$fields)){ if($f == "name"){ $pageinfo["fullname"] = $fields[$f]; }else { $pageinfo[$f] = $fields[$f]; } } } $page->edit($pageinfo); } // End temporary skräpp code return $this; } public function delete($force=false){ global $DB; if($force){ $children = studyplanpage::find_studyplan_children($this); foreach($children as $c){ $c->delete($force); } } if($DB->count_records('local_treestudyplan_page',['studyplan_id' => $this->id]) > 0){ return success::fail('cannot delete studyplan that still has pages'); } 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 <= 0){ $ids = $DB->get_fieldset_select(self::TABLE,"id",""); } 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,["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; } 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) */ public function find_linked_userids(): array { global $DB; if($this->linked_userids === null){ $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); $this->linked_userids = array_unique($uids); } return $this->linked_userids; } /** 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'), "pages" => new \external_multiple_structure(studyplanpage::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, 'pages' => [], 'aggregation_info' => $this->aggregator->basic_model(), ]; foreach($this->pages() as $p) { $model['pages'][] = $p->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 $newplan =studyplan::add([ 'name' => $name, 'shortname' => $shortname, 'description' => $this->r->description, ]); // next, copy the studylines foreach($this->pages() as $p){ $newchild = $p->duplicate($newplan); } return $newplan; } 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"=>2.0, "studyplan"=>$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, "aggregation" => $this->r->aggregation, "aggregation_config" => json_decode($this->aggregator->config_string()), 'aggregation_info' => $this->aggregator->basic_model(), 'pages' => $this->export_pages_model(), ]; return $model; } public function export_pages_model() { $pages = []; foreach($this->pages() as $p) { $pages[] = $p->export_model(); } return $pages; } public static function import_studyplan($content,$format="application/json",$context_id=1) { if($format != "application/json") { return false;} $content = json_decode($content,true); if($content["type"] == "studyplan" && $content["version"] >= 2.0){ // Make sure the aggregation_config is re-encoded as json text $content["studyplan"]["aggregation_config"] = json_encode($content["studyplan"]["aggregation_config"]); // And make sure the context_id is set to the provided context for import $content["studyplan"]["context_id"] = $context_id; // Create a new plan, based on the given parameters - this is the import studyplan part $plan = self::add($content["studyplan"],true); // Now import each page return $plan->import_pages_model($content["studyplan"]["pages"]); } else { error_log("Invalid format and type: {$content['type']} version {$content['version']}"); return false; } } public function import_pages($content,$format="application/json") { if($format != "application/json") { return false;} $content = json_decode($content,true); if($content["version"] >= 2.0){ if($content["type"] == "studyplanpage"){ // import single page from a studyplanpage (wrapped in array of one page) return $this->import_pages_model([$content["page"]]); } else if($content["type"] == "studyplan"){ // Import all pages from the studyplan return $this->import_pages_model($content["studyplan"]["pages"]); } } else { return false; } } protected function import_pages_model($model) { $this->pages(); // make sure the page cache is initialized, since we will be adding to it. foreach($model as $p){ $p["studyplan_id"] = $this->id(); $page = studyplanpage::add($p); $this->page_cache[] = $page; $page->import_periods_model($p["perioddesc"]); $page->import_studylines_model($p["studylines"]); } return true; } /** * Mark the studyplan as changed regarding courses and associated cohorts */ public function mark_csync_changed(){ global $DB; $DB->update_record(self::TABLE, ['id' => $this->id,"csync_flag" => 1]); $this->r->csync_flag = 1; //manually set it in the cache, if something unexpected happened, an exception has already been thrown anyway } /** * Clear the csync mark */ public function clear_csync_changed(){ global $DB; $DB->update_record(self::TABLE, ['id' => $this->id,"csync_flag" => 0]); $this->r->csync_flag = 0; //manually set it in the cache, if something unexpected happened, an exception has already been thrown anyway } public function has_csync_changed(){ return ($this->r->csync_flag > 0)?true:false; } /** * See if the specified course id is linked in this studyplan */ public function course_linked($courseid){ global $DB; $sql = "SELECT COUNT(i.id) FROM {local_treestudyplan} INNER JOIN {local_treestudyplan_line} l ON p.id = l.studyplan_id INNER JOIN {local_treestudyplan_item} i ON l.id = i.line_id WHERE p.id = :planid AND i.course_id = :courseid"; $count = $DB->get_field_sql($sql,["courseid" => $courseid, "planid" => $this->id]); return ($count > 0)?true:false; } /** * List the course id is linked in this studyplan * Used for cohort enrolment cascading */ public function get_linked_course_ids(){ global $DB; $sql = "SELECT i.course_id FROM {local_treestudyplan} p INNER JOIN {local_treestudyplan_page} pg ON p.id = pg.studyplan_id INNER JOIN {local_treestudyplan_line} l ON pg.id = l.page_id INNER JOIN {local_treestudyplan_item} i ON l.id = i.line_id WHERE p.id = :studyplan_id AND i.type = :itemtype"; $fields = $DB->get_fieldset_sql($sql,["studyplan_id" => $this->id,"itemtype" => studyitem::COURSE]); return $fields; } /** * List the cohort id's associated with this studyplan */ public function get_linked_cohort_ids(){ global $CFG, $DB; $sql = "SELECT DISTINCT j.cohort_id FROM {local_treestudyplan_cohort} j WHERE j.studyplan_id = :studyplan_id"; $fields = $DB->get_fieldset_sql($sql, ['studyplan_id' => $this->id]); return $fields; } /** * List the user id's explicitly associated with this studyplan */ public function get_linked_user_ids(){ global $CFG, $DB; $sql = "SELECT DISTINCT j.user_id FROM {local_treestudyplan_user} j WHERE j.studyplan_id = :studyplan_id"; $fields = $DB->get_fieldset_sql($sql, ['studyplan_id' => $this->id]); return $fields; } /** * See if the specified course id is linked in this studyplan */ public function badge_linked($badgeid){ global $DB; $sql = "SELECT COUNT(i.id) FROM {local_treestudyplan} INNER JOIN {local_treestudyplan_line} l ON p.id = l.studyplan_id INNER JOIN {local_treestudyplan_item} i ON l.id = i.line_id WHERE p.id = :planid AND i.badge_id = :badgeid"; $count = $DB->get_field_sql($sql,["badgeid" => $badgeid, "planid" => $this->id]); return ($count > 0)?true:false; } }