libdir.'/externallib.php'); class studyitem { public const COMPETENCY = 'competency'; public const COURSE = 'course'; public const JUNCTION = 'junction'; public const BADGE = 'badge'; public const FINISH = 'finish'; public const START = 'start'; public const INVALID = 'invalid'; public const TABLE = "local_treestudyplan_item"; private static $STUDYITEM_CACHE = []; private $r; // Holds database record private $id; private $courseinfo = null; private $studyline; private $aggregator; public function context(): \context { return $this->studyline->context(); } public function getStudyline(): studyline { return $this->studyline; } public function getConditions() { return $this->r->conditions; } public static function findById($id): self { if(!array_key_exists($id,self::$STUDYITEM_CACHE)){ self::$STUDYITEM_CACHE[$id] = new self($id); } return self::$STUDYITEM_CACHE[$id]; } public function __construct($id) { global $DB; $this->id = $id; $this->r = $DB->get_record(self::TABLE,['id' => $id],"*",MUST_EXIST); $this->studyline = studyline::findById($this->r->line_id); $this->aggregator = $this->getStudyline()->getStudyplan()->getAggregator(); } public function id(){ return $this->id; } public function slot(){ return $this->r->slot; } public function layer(){ return $this->r->layer; } public function type(){ return $this->r->type; } public function courseid(){ return $this->r->course_id; } public static function exists($id){ global $DB; return is_numeric($id) && $DB->record_exists(self::TABLE, array('id' => $id)); } public static function editor_structure($value=VALUE_REQUIRED){ return new \external_single_structure([ "id" => new \external_value(PARAM_INT, 'id of study item'), "type" => new \external_value(PARAM_TEXT, 'shortname of study item'), "conditions"=> new \external_value(PARAM_TEXT, 'conditions for completion'), "slot" => new \external_value(PARAM_INT, 'slot in the study plan'), "layer" => new \external_value(PARAM_INT, 'layer in the slot'), "course" => courseinfo::editor_structure(VALUE_OPTIONAL), "badge" => badgeinfo::editor_structure(VALUE_OPTIONAL), "continuation_id" => new \external_value(PARAM_INT, 'id of continued item'), "connections" => new \external_single_structure([ 'in' => new \external_multiple_structure(studyitemconnection::structure()), 'out' => new \external_multiple_structure(studyitemconnection::structure()), ]), ]); } public function editor_model(){ return $this->generate_model("editor"); } private 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, // Id is needed in export model because of link references 'type' => $this->isValid()?$this->r->type:self::INVALID, 'conditions' => $this->r->conditions, 'slot' => $this->r->slot, 'layer' => $this->r->layer, 'continuation_id' => $this->r->continuation_id, 'connections' => [ "in" => [], "out" => [], ] ]; if($mode == "export"){ // remove slot and layer unset($model["slot"]); unset($model["layer"]); unset($model["continuation_id"]); $model["connections"] = []; // In export mode, connections is just an array of outgoing connections if(!isset($this->r->conditions)){ unset($model["conditions"]); } } // Add course link if available $ci = $this->getcourseinfo(); if(isset($ci)){ if($mode == "export"){ $model['course'] = $ci->shortname(); } else { $model['course'] = $ci->editor_model($this,$this->aggregator->usecorecompletioninfo()); } } // Add badge info if available if(is_numeric($this->r->badge_id) && $DB->record_exists('badge', array('id' => $this->r->badge_id))) { $badge = new \core_badges\badge($this->r->badge_id); $badgeinfo = new badgeinfo($badge); if($mode == "export"){ $model['badge'] = $badgeinfo->name(); } else { $model['badge'] = $badgeinfo->editor_model(); } } if($mode == "export"){ // Also export gradables $gradables = gradeinfo::list_studyitem_gradables($this); if(count($gradables) > 0){ $model["gradables"] = []; foreach($gradables as $g){ $model["gradables"][] = $g->export_model();; } } } // Add incoming and outgoing connection info $conn_out = studyitemconnection::find_outgoing($this->id); if($mode == "export"){ foreach($conn_out as $c) { $model["connections"][] = $c->to_id(); } } else { foreach($conn_out as $c) { $model['connections']['out'][$c->to_id()] = $c->model(); } $conn_in = studyitemconnection::find_incoming($this->id); foreach($conn_in as $c) { $model['connections']['in'][$c->from_id()] = $c->model(); } } return $model; } public static function add($fields,$import=false) { global $DB; $addable = ['line_id','type','conditions','slot','competency_id','course_id','badge_id','continuation_id']; if($import){ $addable[] = "layer";} $info = [ 'layer' => -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 = ['conditions','course_id','continuation_id']; $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 isValid(){ // Check if referenced courses, badges and/or competencies still exist if($this->r->type == static::COURSE){ return courseinfo::exists($this->r->course_id); } else if($this->r->type == static::BADGE){ return badgeinfo::exists($this->r->badge_id); } else { return true; } } public function delete($force=false) { global $DB; // check if this item is referenced in a START item if($force){ // clear continuation id from any references to this item $records = $DB->get_records(self::TABLE,['continuation_id' => $this->id]); foreach($records as $r){ $r->continuation_id = 0; $DB->update_record(self::TABLE,$r); } } if($DB->count_records(self::TABLE,['continuation_id' => $this->id]) > 0){ return success::fail('Cannot remove: item is referenced by another item'); } else { // delete al related connections to this item studyitemconnection::clear($this->id); // delete all grade inclusion references to this item $DB->delete_records("local_treestudyplan_gradeinc",['studyitem_id' => $this->id]); // delete the item itself $DB->delete_records(self::TABLE, ['id' => $this->id]); return success::success(); } } /************************ * * * reorder_studyitems * * * ************************/ public static function reorder($resequence) { global $DB; foreach($resequence as $sq) { $DB->update_record(self::TABLE, [ 'id' => $sq['id'], 'line_id' => $sq['line_id'], 'slot' => $sq['slot'], 'layer' => $sq['layer'], ]); } return success::success(); } public static function find_studyline_children(studyline $line) { global $DB; $list = []; $ids = $DB->get_fieldset_select(self::TABLE,"id","line_id = :line_id ORDER BY layer",['line_id' => $line->id()]); foreach($ids as $id) { $item = self::findById($id,$line); $list[] = $item; } return $list; } private static function link_structure($value=VALUE_REQUIRED){ return new \external_single_structure([ "id" => new \external_value(PARAM_INT, 'id of study item'), "type" => new \external_value(PARAM_TEXT, 'type of study item'), "completion" => completion::structure(), "studyline" => new \external_value(PARAM_TEXT, 'reference label of studyline'), "studyplan" => new \external_value(PARAM_TEXT, 'reference label of studyplan'), ], 'basic info of referenced studyitem', $value); } private function link_model($userid){ global $DB; $line = $DB->get_record(studyline::TABLE,['id' => $this->r->line_id]); $plan = $DB->get_record(studyplan::TABLE,['id' => $line->studyplan_id]); return [ "id" => $this->r->id, "type" => $this->r->type, "completion" => $this->completion($userid), "studyline" => $line->name(), "studyplan" => $plan->name(), ]; } public static function user_structure($value=VALUE_REQUIRED){ return new \external_single_structure([ "id" => new \external_value(PARAM_INT, 'id of study item'), "type" => new \external_value(PARAM_TEXT, 'type of study item'), "completion" => new \external_value(PARAM_TEXT, 'completion state (incomplete|progress|completed|excellent)'), "slot" => new \external_value(PARAM_INT, 'slot in the study plan'), "layer" => new \external_value(PARAM_INT, 'layer in the slot'), "course" => courseinfo::user_structure(VALUE_OPTIONAL), "badge" => badgeinfo::user_structure(VALUE_OPTIONAL), "continuation" => self::link_structure(VALUE_OPTIONAL), "connections" => new \external_single_structure([ 'in' => new \external_multiple_structure(studyitemconnection::structure()), 'out' => new \external_multiple_structure(studyitemconnection::structure()), ]), ],'Study item info',$value); } public function user_model($userid){ global $CFG, $DB; $model = [ 'id' => $this->r->id, 'type' => $this->r->type, 'completion' => completion::label($this->completion($userid)), 'slot' => $this->r->slot, 'layer' => $this->r->layer, 'connections' => [ "in" => [], "out" => [], ] ]; // Add badge info if available if(badgeinfo::exists($this->r->badge_id)) { $badge = new \core_badges\badge($this->r->badge_id); $badgeinfo = new badgeinfo($badge); $model['badge'] = $badgeinfo->user_model($userid); } // Add continuation_info if available if(self::exists($this->r->continuation_id)) { $c_item = self::findById($this->r->continuation_id); $model['continuation'] = $c_item->link_model($userid); } // Add course if available if(courseinfo::exists($this->r->course_id)) { $cinfo = $this->getcourseinfo(); $model['course'] = $cinfo->user_model($userid,$this->aggregator->usecorecompletioninfo()); } // Add incoming and outgoing connection info $conn_out = studyitemconnection::find_outgoing($this->id); foreach($conn_out as $c) { $model['connections']['out'][$c->to_id()] = $c->model(); } $conn_in = studyitemconnection::find_incoming($this->id); foreach($conn_in as $c) { $model['connections']['in'][$c->from_id()] = $c->model(); } return $model; } public function getcourseinfo() { if(empty($this->courseinfo) && courseinfo::exists($this->r->course_id)){ $this->courseinfo = new courseinfo($this->r->course_id, $this); } return $this->courseinfo; } private function completion($userid) { global $DB; if($this->isValid()){ if(strtolower($this->r->type) == 'course'){ // determine competency by competency completion $courseinfo = $this->getcourseinfo(); return $this->aggregator->aggregate_course($courseinfo,$this,$userid); } elseif(strtolower($this->r->type) =='start'){ // Does not need to use aggregator. // Either true, or the completion of the reference if(self::exists($this->r->continuation_id)){ $c_item = self::findById($this->r->continuation_id); return $c_item->completion($userid); } else { return completion::COMPLETED; } } elseif(in_array(strtolower($this->r->type),['junction','finish'])){ // completion of the linked items, according to the rule $in_completed = []; // Retrieve incoming connections $incoming = $DB->get_records(studyitemconnection::TABLE,['to_id' => $this->r->id]); foreach($incoming as $conn){ $item = self::findById($conn->from_id); $in_completed[] = $item->completion($userid); } return $this->aggregator->aggregate_junction($in_completed,$this,$userid); } elseif(strtolower($this->r->type) =='badge'){ global $DB; // badge awarded if(badgeinfo::exists($this->r->badge_id)) { $badge = new \core_badges\badge($this->r->badge_id); if($badge->is_issued($userid)){ if($badge->can_expire()){ // get the issued badges and check if any of them have not expired yet $badges_issued = $DB->get_records("badge_issued",["badge_id" => $this->r->badge_id, "user_id" => $userid]); $notexpired = false; $now = time(); foreach($badges_issued as $bi){ if($bi->dateexpire == null || $bi->dateexpire > $now){ $notexpired = true; break; } } return ($notexpired)?completion::COMPLETED:completion::INCOMPLETE; } else{ return completion::COMPLETED; } } else { return completion::INCOMPLETE; } } else { return completion::INCOMPLETE; } } else { // return incomplete for other types return completion::INCOMPLETE; } } else { // return incomplete for other types return completion::INCOMPLETE; } } public function duplicate($new_line){ global $DB; // clone the database fields $fields = clone $this->r; // set new line id unset($fields->id); $fields->line_id = $new_line->id(); //create new record with the new data $id = $DB->insert_record(self::TABLE, (array)$fields); $new = self::findById($id,$new_line); // copy the grading info if relevant $gradables = gradeinfo::list_studyitem_gradables($this); foreach($gradables as $g){ gradeinfo::include_grade($g->getGradeitem()->id,$new,true); } return $new; } public function export_model(){ return $this->generate_model("export"); } public static function import_item($model){ unset($model["course_id"]); unset($model["competency_id"]); unset($model["badge_id"]); unset($model["continuation_id"]); if(isset($model["course"])){ $model["course_id"] = courseinfo::id_from_shortname(($model["course"])); } if(isset($model["badge"])){ $model["badge_id"] = badgeinfo::id_from_name(($model["badge"])); } $item = self::add($model,true); if(isset($model["course_id"])){ // attempt to import the gradables foreach($model["gradables"] as $gradable){ gradeinfo::import($item,$gradable); } } return $item; } }