libdir.'/externallib.php'); class studyplanpage { const TABLE = "local_treestudyplan_page"; private static $CACHE = []; private $r; // Holds database record private $id; private $studyplan; public function aggregator(){ return $this->studyplan->aggregator(); } // Cache constructors to avoid multiple creation events in one session. public static function findById($id): self { if(!array_key_exists($id,self::$CACHE)){ self::$CACHE[$id] = new self($id); } return self::$CACHE[$id]; } private function __construct($id) { global $DB; $this->id = $id; $this->r = $DB->get_record(self::TABLE,['id' => $id]); $this->studyplan = studyplan::findById($this->r->studyplan_id); } public function id(){ return $this->id; } public function studyplan() : studyplan { return $this->studyplan; } public function shortname(){ return $this->r->shortname; } public function periods(){ return $this->r->periods; } public function fullname(){ return $this->r->fullname; } public function startdate(){ return new \DateTime($this->r->startdate); } public function enddate(){ if($this->r->enddate && strlen($this->r->enddate) > 0){ return new \DateTime($this->r->enddate); } else{ // return a date 100 years into the future return (new \DateTime($this->r->startdate))->add(new \DateInterval("P100Y")); } } public static function simple_structure($value=VALUE_REQUIRED){ return new \external_single_structure([ "id" => new \external_value(PARAM_INT, 'id of studyplan page'), "fullname" => new \external_value(PARAM_TEXT, 'name of studyplan page'), "shortname"=> new \external_value(PARAM_TEXT, 'shortname of studyplan page'), "periods" => new \external_value(PARAM_INT, 'number of periods in studyplan page'), "description"=> new \external_value(PARAM_TEXT, 'description of studyplan page'), "startdate" => new \external_value(PARAM_TEXT, 'start date of studyplan'), "enddate" => new \external_value(PARAM_TEXT, 'end date of studyplan'), ],'Studyplan page basic info',$value); } public function simple_model(){ return [ 'id' => $this->r->id, 'fullname' => $this->r->fullname, 'shortname' => $this->r->shortname, 'periods' => $this->r->periods, 'description' => $this->r->description, 'startdate' => $this->r->startdate, 'enddate' => $this->r->enddate, ]; } 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 page'), "shortname"=> new \external_value(PARAM_TEXT, 'shortname of studyplan page'), "description"=> new \external_value(PARAM_TEXT, 'description of studyplan page'), "periods" => new \external_value(PARAM_INT, 'number of periods in studyplan page'), "startdate" => new \external_value(PARAM_TEXT, 'start date of studyplan page'), "enddate" => new \external_value(PARAM_TEXT, 'end date of studyplan page'), "studylines" => new \external_multiple_structure(studyline::editor_structure()), ],'Studyplan page 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, 'periods' => $this->r->periods, 'startdate' => $this->r->startdate, 'enddate' => $this->r->enddate, 'studylines' => [], ]; $children = studyline::find_page_children($this); foreach($children as $c) { $model['studylines'][] = $c->editor_model(); } return $model; } public static function add($fields){ global $CFG, $DB; if(!isset($fields['studyplan_id'])){ throw new \InvalidArgumentException("parameter 'studyplan_id' missing"); } $addable = ['studyplan_id','fullname','shortname','description','periods','startdate','enddate']; $info = ['enddate' => null ]; foreach($addable as $f){ if(array_key_exists($f,$fields)){ $info[$f] = $fields[$f]; } } $id = $DB->insert_record(self::TABLE, $info); return self::findById($id); // make sure the new page is immediately cached } public function edit($fields){ global $DB; $editable = ['fullname','shortname','description','periods','startdate','enddate']; $info = ['id' => $this->id,]; foreach($editable as $f){ if(array_key_exists($f,$fields)){ $info[$f] = $fields[$f]; } } $DB->update_record(self::TABLE, $info); //reload record after edit $this->r = $DB->get_record(self::TABLE,['id' => $this->id],"*",MUST_EXIST); return $this; } public function delete($force=false){ global $DB; if($force){ $children = studyline::find_page_children($this); foreach($children as $c){ $c->delete($force); } } if($DB->count_records('local_treestudyplan_line',['studyplan_id' => $this->id]) > 0){ return success::fail('cannot delete studyplan that still has studylines'); } else { $DB->delete_records('local_treestudyplan_page', ['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[] = self::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[] = self::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); } public static function user_structure($value=VALUE_REQUIRED){ return new \external_single_structure([ "id" => new \external_value(PARAM_INT, 'id of studyplan page'), "fullname" => new \external_value(PARAM_TEXT, 'name of studyplan page'), "shortname"=> new \external_value(PARAM_TEXT, 'shortname of studyplan page'), "description"=> new \external_value(PARAM_TEXT, 'description of studyplan page'), "periods" => new \external_value(PARAM_INT, 'number of slots in studyplan page'), "startdate" => new \external_value(PARAM_TEXT, 'start date of studyplan page'), "enddate" => new \external_value(PARAM_TEXT, 'end date of studyplan page'), "studylines" => new \external_multiple_structure(studyline::user_structure()), ],'Studyplan page with user info',$value); } public function user_model($userid){ $model = [ 'id' => $this->r->id, 'fullname' => $this->r->name, 'shortname' => $this->r->shortname, 'description' => $this->r->description, 'periods' => $this->r->periods, 'startdate' => $this->r->startdate, 'enddate' => $this->r->enddate, 'studylines' => [], ]; $children = studyline::find_page_children($this); foreach($children as $c) { $model['studylines'][] = $c->user_model($userid); } return $model; } public static function find_studyplan_children(studyplan $plan) { global $DB; $list = []; $ids = $DB->get_fieldset_select(self::TABLE,"id","studyplan_id = :plan_id ORDER BY startdate", ['plan_id' => $plan->id()]); foreach($ids as $id) { $list[] = self::findById($id); } return $list; } public static function duplicate_page($page_id,$name,$shortname) { $ori = self::findById($page_id); $new = $ori->duplicate($name,$shortname); return $new->simple_model(); } public function duplicate($name,$shortname) { // First duplicate the studyplan structure $new = studyplanpage::add([ 'fullname' => $name, 'shortname' => $shortname, 'description' => $this->r->description, 'pages' => $this->r->pages, 'startdate' => $this->r->startdate, 'enddate' => empty($this->r->enddate)?null:$this->r->enddate, ]); // next, copy the studylines $children = studyline::find_page_children($this); $itemtranslation = []; $linetranslation = []; foreach($children as $c){ $newchild = $c->duplicate($this,$itemtranslation); $linetranslation[$c->id()] = $newchild->id(); } // now the itemtranslation array contains all of the old child id's as keys and all of the related new ids as values // (feature of the studyline::duplicate function) // use this to recreate the lines in the new plan foreach(array_keys($itemtranslation) as $item_id){ // copy based on the outgoing connections of each item, to avoid duplicates $connections = studyitemconnection::find_outgoing($item_id); foreach($connections as $conn){ studyitemconnection::connect($itemtranslation[$conn->from_id],$itemtranslation[$conn->to_id]); } } return $new; } public static function export_structure() { return new \external_single_structure([ "format" => new \external_value(PARAM_TEXT, 'format of studyplan export'), "content"=> new \external_value(PARAM_TEXT, 'exported studyplan content'), ],'Exported studyplan'); } public function export_page() { $model = $this->export_model(); $json = json_encode([ "type"=>"studyplanpage", "version"=>2.0, "page"=>$model ],\JSON_PRETTY_PRINT); return [ "format" => "application/json", "content" => $json]; } public function export_page_csv() { //TODO: Period shortnames instead of just P1, P2, P3 etc $model = $this->editor_model(); $slots = intval($model["slots"]); // First line $csv = "\"Studyline[{$slots}]\""; for($i = 1; $i <= $slots; $i++){ $csv .= ",\"P{$i}\""; } $csv .= "\r\n"; // next, make one line per studyline foreach($model["studylines"] as $line){ // determine how many fields are simultaneous in the line at maximum $maxlines = 1; for($i = 1; $i <= $slots; $i++){ if(count($line["slots"]) > $i){ $ct = 0; foreach($line["slots"][$i][studyline::SLOTSET_COMPETENCY] as $itm){ if($itm["type"] == "course"){ $ct += 1; } } if($ct > $maxlines){ $maxlines = $ct; } } } for($lct = 0; $lct < $maxlines; $lct++){ $csv .= "\"{$line["name"]}\""; for($i = 1; $i <= $slots; $i++){ $filled = false; if(count($line["slots"]) > $i){ $ct = 0; foreach($line["slots"][$i][studyline::SLOTSET_COMPETENCY] as $itm){ if($itm["type"] == "course"){ if($ct == $lct){ $csv .= ",\""; $csv .= $itm["course"]["fullname"]; $csv .= "\r\n"; $first = true; foreach($itm["course"]["grades"] as $g){ if($g["selected"]){ if($first){ $first = false; } else{ $csv .= "\r\n"; } $csv .= "- ".str_replace('"', '\'', $g["name"]); } } $csv .= "\""; $filled = true; break; } $ct++; } } } if(!$filled) { $csv .= ",\"\""; } } $csv .= "\r\n"; } } return [ "format" => "text/csv", "content" => $csv]; } public function export_studylines(){ $model = $this->export_studylines_model(); $json = json_encode([ "type"=>"studylines", "version"=>1.0, "studylines"=>$model, ],\JSON_PRETTY_PRINT); return [ "format" => "application/json", "content" => $json]; } public function export_model() { $model = [ 'fullname' => $this->r->name, 'shortname' => $this->r->shortname, 'description' => $this->r->description, 'periods' => $this->r->periods, 'startdate' => $this->r->startdate, 'enddate' => $this->r->enddate, 'studylines' => $this->export_studylines_model(), ]; return $model; } public function export_studylines_model() { $children = studyline::find_page_children($this); $lines = []; foreach($children as $c) { $lines[] = $c->export_model(); } return $lines; } public function import_studylines($content,$format="application/json") { if($format != "application/json") { return false;} $content = json_decode($content,true); if($content["type"] == "studylines" && $content["version"] >= 2.0){ return $this->import_studylines_model($content["studylines"]); } else if($content["type"] == "studyplanpage" && $content["version"] >= 2.0){ return $this->import_studylines_model($content["page"]["studylines"]); } else { return false; } } protected function find_studyline_by_shortname($shortname){ $children = studyline::find_page_children($this); foreach($children as $l){ if($shortname == $l->shortname()){ return $l; } } return null; } public function import_studylines_model($model) { // First attempt to map each studyline model to an existing or new line $line_map = []; foreach($model as $ix => $linemodel){ $line = $this->find_studyline_by_shortname($linemodel["shortname"]); if(empty($line)){ $linemodel["studyplan_id"] = $this->id; $line = studyline::add($linemodel); } else { //$line->edit($linemodel); // Update the line with the settings from the imported file } $line_map[$ix] = $line; } // next, let each study line import the study items $itemtranslation = []; $connections = []; foreach($model as $ix => $linemodel){ $line_map[$ix]->import_studyitems($linemodel["slots"],$itemtranslation,$connections); } // Finally, create the links between the study items foreach($connections as $from => $dests){ foreach($dests as $to){ studyitemconnection::connect($from,$itemtranslation[$to]); } } return true; } }