libdir.'/externallib.php'); class studyline { public const SLOTSET_COMPETENCY = 'competencies'; public const SLOTSET_FILTER = 'filters'; public const COMPETENCY_TYPES = [ studyitem::COMPETENCY, studyitem::COURSE, ]; public const FILTER_TYPES = [ studyitem::JUNCTION, studyitem::BADGE, studyitem::FINISH, studyitem::START, ]; public const FILTER0_TYPES = [ studyitem::START, ]; public const TABLE = "local_treestudyplan_line"; private static $STUDYLINE_CACHE = []; private $r; // Holds database record private $id; private $studyplan; public function context(): \context { return $this->studyplan->context(); } public function getStudyplan() : studyplan { return $this->studyplan; } public static function findById($id): self { if(!array_key_exists($id,self::$STUDYLINE_CACHE)){ self::$STUDYLINE_CACHE[$id] = new self($id); } return self::$STUDYLINE_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 name(){ return $this->r->name; } public function shortname(){ return $this->r->shortname; } public static function editor_structure($value=VALUE_REQUIRED){ 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'), "slots" => new \external_multiple_structure( new \external_single_structure([ self::SLOTSET_COMPETENCY => new \external_multiple_structure(studyitem::editor_structure(),'competency items',VALUE_OPTIONAL), self::SLOTSET_FILTER => new \external_multiple_structure(studyitem::editor_structure(),'filter items'), ]) ) ]); } public function editor_model(){ return $this->generate_model("editor"); } protected function generate_model($mode){ // Mode parameter is used to geep this function for both editor model and export model // (Export model results in fewer parameters on children, but is otherwise basically the same as this function) global $DB; $model = [ 'id' => $this->r->id, 'name' => $this->r->name, 'shortname' => $this->r->shortname, 'color' => $this->r->color, 'sequence' => $this->r->sequence, 'slots' => [], ]; if($mode == "export"){ // Id and sequence are not used in export model unset($model["id"]); unset($model["sequence"]); } // Get the number of slots // As a safety data integrity measure, if there are any items in a higher slot than currently allowed, // make sure there are enought slots to account for them // Alternatively, we could ensure that on reduction of slots, the items that no longer have a slot will be removed. $max_slot = $DB->get_field_select(studyitem::TABLE,"MAX(slot)","line_id = :lineid",['lineid' => $this->id]); $num_slots = max($this->studyplan->slots(),$max_slot +1); // Create the required amount of slots for($i=0; $i < $num_slots+1; $i++){ if($mode == "export") { // Export mode does not separate between filter or competency type, since that is determined automatically $slots = []; } else { if($i > 0) { $slots = [self::SLOTSET_COMPETENCY => [], self::SLOTSET_FILTER => []]; } else { $slots = [self::SLOTSET_FILTER => []]; } } $model['slots'][$i] = $slots; } $children = studyitem::find_studyline_children($this); foreach($children as $c) { if($mode == "export") { $model['slots'][$c->slot()][] = $c->export_model(); } else { $slotset = null; if($c->slot() > 0) { if(in_array($c->type(),self::COMPETENCY_TYPES)) { $slotset = self::SLOTSET_COMPETENCY; } else if(in_array($c->type(),self::FILTER_TYPES)) { $slotset = self::SLOTSET_FILTER; } } else if(in_array($c->type(),self::FILTER0_TYPES)) { $slotset = self::SLOTSET_FILTER; } if(isset($slotset)) { $model['slots'][$c->slot()][$slotset][] = $c->editor_model(); } } } return $model; } public static function add($fields){ global $DB; if(!isset($fields['studyplan_id'])){ throw new \InvalidArgumentException("parameter 'studyplan_id' missing"); } $studyplan_id = $fields['studyplan_id']; $sqmax = $DB->get_field_select(self::TABLE,"MAX(sequence)","studyplan_id = :studyplan_id",['studyplan_id' => $studyplan_id]); $addable = ['studyplan_id','name','shortname','color']; $info = ['sequence' => $sqmax+1]; 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); } public function edit($fields){ global $DB; $editable = ['name','shortname','color']; $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 = studyitem::find_studyline_children($this); foreach($children as $c){ $c->delete($force); } } // check if this item has study items in it if($DB->count_records(studyitem::TABLE,['line_id' => $this->id]) > 0){ return success::fail('cannot delete studyline with items'); } else { $DB->delete_records(self::TABLE, ['id' => $this->id]); return success::success(); } } public static function reorder($resequence) { global $DB; foreach($resequence as $sq) { $DB->update_record(self::TABLE, [ 'id' => $sq['id'], 'sequence' => $sq['sequence'], ]); } return success::success(); } 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 sequence",['plan_id' => $plan->id()]); foreach($ids as $id) { $list[] = self::findById($id); } return $list; } public static function user_structure($value=VALUE_REQUIRED){ 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'), "slots" => new \external_multiple_structure( new \external_single_structure([ self::SLOTSET_COMPETENCY => new \external_multiple_structure(studyitem::user_structure(),'competency items',VALUE_OPTIONAL), self::SLOTSET_FILTER => new \external_multiple_structure(studyitem::user_structure(),'filter items'), ]) ) ],'Studyline with user info',$value); } public function user_model($userid){ // TODO: Integrate this function into generate_model() for ease of maintenance global $DB; $model = [ 'id' => $this->r->id, 'name' => $this->r->name, 'shortname' => $this->r->shortname, 'color' => $this->r->color, 'sequence' => $this->r->sequence, 'slots' => [], ]; // Get the number of slots // As a safety data integrity measure, if there are any items in a higher slot than currently allowed, // make sure there are enought slots to account for them // Alternatively, we could ensure that on reduction of slots, the items that no longer have a slot will be removed. $max_slot = $DB->get_field_select(studyitem::TABLE,"MAX(slot)","line_id = :lineid",['lineid' => $this->id]); $num_slots = max($this->studyplan->slots(),$max_slot +1); // Create the required amount of slots for($i=0; $i < $num_slots+1; $i++){ if($i > 0) { $slots = [self::SLOTSET_COMPETENCY => [], self::SLOTSET_FILTER => []]; } else { $slots = [self::SLOTSET_FILTER => []]; } $model['slots'][$i] = $slots; } $children = studyitem::find_studyline_children($this); foreach($children as $c) { if($c->isValid()){ $slotset = null; if($c->slot() > 0) { if(in_array($c->type(),self::COMPETENCY_TYPES)) { $slotset = self::SLOTSET_COMPETENCY; } else if(in_array($c->type(),self::FILTER_TYPES)) { $slotset = self::SLOTSET_FILTER; } } else if(in_array($c->type(),self::FILTER0_TYPES)) { $slotset = self::SLOTSET_FILTER; } if(isset($slotset)) { $model['slots'][$c->slot()][$slotset][] = $c->user_model($userid); } } } return $model; } public function duplicate($new_studyplan,&$translation){ global $DB; // clone the database fields $fields = clone $this->r; // set new studyplan id unset($fields->id); $fields->studyplan_id = $new_studyplan->id(); // create new record with the new data $id = $DB->insert_record(self::TABLE, (array)$fields); $new = self::findById($id); // Next copy all the study items for this studyline // and record the original and copy id's in the $translation array // so the calling function can connect the new studyitems as required $children = studyitem::find_studyline_children($this); $translation = []; foreach($children as $c) { $newchild = $c->duplicate($this); $translation[$c->id()] = $newchild->id(); } return $new; } public function export_model() { return $this->generate_model("export"); } public function import_studyitems($model,&$itemtranslation,&$connections){ global $DB; foreach($model as $slot=>$slotmodel) { $courselayer = 0; $filterlayer = 0; foreach($slotmodel as $itemmodel) { if($itemmodel["type"] == "course" || $itemmodel["type"] == "competency"){ $itemmodel["layer"] = $courselayer; $courselayer++; }else { $itemmodel["layer"] = $filterlayer; $filterlayer++; } $itemmodel["slot"] = $slot; $itemmodel["line_id"] = $this->id(); $item = studyitem::import_item($itemmodel); if(!empty($item)){ $itemtranslation[$itemmodel["id"]] = $item->id(); if(count($itemmodel["connections"]) > 0){ if(! isset($connections[$item->id()]) || ! is_array($connections[$item->id()])){ $connections[$item->id()] = []; } foreach($itemmodel["connections"] as $to_id){ $connections[$item->id()][] = $to_id; } } } } } } }