moodle_local_treestudyplan/classes/studyitem.php
2023-06-27 07:33:27 +02:00

524 lines
No EOL
18 KiB
PHP

<?php
namespace local_treestudyplan;
require_once($CFG->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 {
// Also supply a list of linked users, so the badgeinfo can give stats on
// the amount issued, related to this studyplan
$studentids = $this->getStudyline()->getStudyplan()->find_linked_userids();
$model['badge'] = $badgeinfo->editor_model($studentids);
}
}
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);
$item = self::findById($id);
if($item->type() == self::COURSE){
// Signal the studyplan that a course has been added so it can be marked for csync cascading
$item->getStudyline()->getStudyplan()->mark_csync_changed();
}
return $item;
}
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;
}
}