. /** * * @package local_treestudyplan * @copyright 2023 P.M. Kuipers * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace local_treestudyplan; require_once($CFG->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'), "perioddesc" => period::page_structure(), ], '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, "perioddesc" => period::page_model($this), ]; } public static function editor_structure($value=VALUE_REQUIRED) { return new \external_single_structure([ "id" => new \external_value(PARAM_INT, 'id of studyplan'), "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 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()), "perioddesc" => period::page_structure(), ], 'Studyplan page full structure', $value); } public function editor_model() { global $DB; $model = [ 'id' => $this->r->id, 'fullname' => $this->r->fullname, 'shortname' => $this->r->shortname, 'description' => $this->r->description, 'periods' => $this->r->periods, 'startdate' => $this->r->startdate, 'enddate' => $this->r->enddate, 'studylines' => [], "perioddesc" => period::page_model($this), ]; $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]; } } if (!isset($addable['periods'])) { $addable['periods'] = 4; } else if ($addable['periods'] < 1) { $addable['periods'] = 1; } $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]; } } if (isset($info['periods']) && $info['periods'] < 1) { $info['periods'] = 1; } $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', ['page_id' => $this->id]) > 0) { return success::fail('cannot delete studyplan page that still has studylines'); } else { $DB->delete_records(self::TABLE, ['id' => $this->id]); return success::success(); } } 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()), "perioddesc" => period::page_structure(), ], 'Studyplan page with user info', $value); } public function user_model($userid) { $model = [ 'id' => $this->r->id, 'fullname' => $this->r->fullname, 'shortname' => $this->r->shortname, 'description' => $this->r->description, 'periods' => $this->r->periods, 'startdate' => $this->r->startdate, 'enddate' => $this->r->enddate, 'studylines' => [], "perioddesc" => period::page_model($this), ]; $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($pageid, $name, $shortname) { $ori = self::findById($pageid); $new = $ori->duplicate($name, $shortname); return $new->simple_model(); } public function duplicate($newstudyplan) { // First duplicate the studyplan structure. $new = studyplanpage::add([ 'studyplan_id' => $newstudyplan->id(), 'fullname' => $this->r->fullname, '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, ]); // 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 $itemid) { // 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]); } } 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() { $plist = period::findForPage($this); $model = $this->editor_model(); $periods = intval($model["periods"]); // First line. $csv = "\"\""; for($i = 1; $i <= $periods; $i++) { $name = $plist[$i]->shortname(); $csv .= ", \"{$name}\""; } $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 <= $periods; $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 <= $periods; $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"=>2.0, "studylines"=>$model, ], \JSON_PRETTY_PRINT); return [ "format" => "application/json", "content" => $json]; } public function export_periods() { $model = period::page_model($this); $json = json_encode([ "type"=>"periods", "version"=>2.0, "perioddesc"=>$model, ], \JSON_PRETTY_PRINT); return [ "format" => "application/json", "content" => $json]; } public function export_model() { $model = [ 'fullname' => $this->r->fullname, '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(), 'perioddesc' => period::page_model($this), ]; 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_periods($content, $format="application/json") { if ($format != "application/json") { return false;} $content = json_decode($content, true); if ($content["type"] == "periods" && $content["version"] >= 2.0) { return $this->import_periods_model($content["perioddesc"]); } else if ($content["type"] == "studyplanpage" && $content["version"] >= 2.0) { return $this->import_periods_model($content["page"]["perioddesc"]); } else { return false; } } public function import_studylines($content, $format="application/json") { if ($format != "application/json") { return false;} $content = json_decode($content, true); if ($content["type"] == "studylines" && $content["version"] >= 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 if ($content["type"] == "studyplan" && $content["version"] >= 2.0) { return $this->import_studylines_model($content["studyplan"]["pages"][0]["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_periods_model($model) { $periods = period::findForPage($this); foreach ($model as $pmodel) { $pi = $pmodel["period"]; if (array_key_exists($pi, $periods)) { $periods[$pi]->edit($pmodel); } } } public function import_studylines_model($model) { // First attempt to map each studyline model to an existing or new line. $linemap = []; foreach ($model as $ix => $linemodel) { $line = $this->find_studyline_by_shortname($linemodel["shortname"]); if (empty($line)) { $linemodel["page_id"] = $this->id; $line = studyline::add($linemodel); } else { //$line->edit($linemodel); // Update the line with the settings from the imported file. } $linemap[$ix] = $line; } // next, let each study line import the study items. $itemtranslation = []; $connections = []; foreach ($model as $ix => $linemodel) { $linemap[$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; } }