2023-05-17 21:19:14 +02:00
< ? php
namespace local_treestudyplan ;
require_once ( $CFG -> libdir . '/externallib.php' );
class studyplan {
const TABLE = " local_treestudyplan " ;
private static $STUDYPLAN_CACHE = [];
private $r ; // Holds database record
private $id ;
private $aggregator ;
private $context = null ; // Hold context object once retrieved
public function getAggregator (){
return $this -> aggregator ;
}
// Cache constructors to avoid multiple creation events in one session.
public static function findById ( $id ) : self {
if ( ! array_key_exists ( $id , self :: $STUDYPLAN_CACHE )){
self :: $STUDYPLAN_CACHE [ $id ] = new self ( $id );
}
return self :: $STUDYPLAN_CACHE [ $id ];
}
private function __construct ( $id ) {
global $DB ;
$this -> id = $id ;
$this -> r = $DB -> get_record ( self :: TABLE ,[ 'id' => $id ]);
$this -> aggregator = aggregator :: createOrDefault ( $this -> r -> aggregation , $this -> r -> aggregation_config );
}
public function id (){
return $this -> id ;
}
public function slots (){
return $this -> r -> slots ;
}
public function name (){
return $this -> r -> name ;
}
public function startdate (){
return new \DateTime ( $this -> r -> startdate );
}
/**
* Return the context this studyplan is associated to
*/
public function context () : \context {
if ( ! isset ( $this -> context )){
try {
$this -> context = contextinfo :: by_id ( $this -> r -> context_id ) -> context ;
}
catch ( \dml_missing_record_exception $x ){
throw new \InvalidArgumentException ( " Context { $this -> r -> context_id } not available " ); // Just throw it up again. catch is included here to make sure we know it throws this exception
}
}
return $this -> context ;
}
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' ),
" name " => new \external_value ( PARAM_TEXT , 'name of studyplan' ),
" shortname " => new \external_value ( PARAM_TEXT , 'shortname of studyplan' ),
" slots " => new \external_value ( PARAM_INT , 'number of slots in studyplan' ),
" context_id " => new \external_value ( PARAM_INT , 'context_id of studyplan' ),
" description " => new \external_value ( PARAM_TEXT , 'description of studyplan' ),
" startdate " => new \external_value ( PARAM_TEXT , 'start date of studyplan' ),
" enddate " => new \external_value ( PARAM_TEXT , 'end date of studyplan' ),
" aggregation " => new \external_value ( PARAM_TEXT , 'selected aggregator' ),
" aggregation_config " => new \external_value ( PARAM_TEXT , 'config string for aggregator' ),
" aggregation_info " => aggregator :: basic_structure (),
], 'Basic studyplan info' , $value );
}
public function simple_model (){
return [
'id' => $this -> r -> id ,
'name' => $this -> r -> name ,
'shortname' => $this -> r -> shortname ,
'slots' => $this -> r -> slots ,
'context_id' => $this -> context () -> id ,
'description' => $this -> r -> description ,
'startdate' => $this -> r -> startdate ,
'enddate' => $this -> r -> enddate ,
" aggregation " => $this -> r -> aggregation ,
" aggregation_config " => $this -> aggregator -> config_string (),
'aggregation_info' => $this -> aggregator -> basic_model (),
];
}
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' ),
" shortname " => new \external_value ( PARAM_TEXT , 'shortname of studyplan' ),
" description " => new \external_value ( PARAM_TEXT , 'description of studyplan' ),
" slots " => new \external_value ( PARAM_INT , 'number of slots in studyplan' ),
" context_id " => new \external_value ( PARAM_INT , 'context_id of studyplan' ),
" startdate " => new \external_value ( PARAM_TEXT , 'start date of studyplan' ),
" enddate " => new \external_value ( PARAM_TEXT , 'end date of studyplan' ),
" aggregation " => new \external_value ( PARAM_TEXT , 'selected aggregator' ),
" aggregation_config " => new \external_value ( PARAM_TEXT , 'config string for aggregator' ),
" aggregation_info " => aggregator :: basic_structure (),
/* " association " => new \external_single_structure ([
'cohorts' => new \external_multiple_structure ( associationservice :: cohort_structure ()),
'users' => new \external_multiple_structure ( associationservice :: user_structure ()),
]), */
" studylines " => new \external_multiple_structure ( studyline :: editor_structure ()),
" advanced " => new \external_single_structure ([
" force_scales " => new \external_single_structure ([
" scales " => new \external_multiple_structure ( new \external_single_structure ([
" id " => new \external_value ( PARAM_INT , 'id of scale' ),
" name " => new \external_value ( PARAM_TEXT , 'name of scale' ),
])),
], " Scale forcing on stuff " , VALUE_OPTIONAL ),
], " Advanced features available " , VALUE_OPTIONAL ),
], 'Studyplan 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 ,
'slots' => $this -> r -> slots ,
'context_id' => $this -> context () -> id ,
'startdate' => $this -> r -> startdate ,
'enddate' => $this -> r -> enddate ,
" aggregation " => $this -> r -> aggregation ,
" aggregation_config " => $this -> aggregator -> config_string (),
'aggregation_info' => $this -> aggregator -> basic_model (),
/* 'association' => [
'cohorts' => associationservice :: associated_cohorts ( $this -> r -> id ),
'users' => associationservice :: associated_users ( $this -> r -> id ),
], */
'studylines' => [],
];
$children = studyline :: find_studyplan_children ( $this );
foreach ( $children as $c )
{
$model [ 'studylines' ][] = $c -> editor_model ();
}
if ( has_capability ( 'local/treestudyplan:forcescales' , \context_system :: instance ())){
if ( ! array_key_exists ( 'advanced' , $model )){
// Create advanced node if it does not exist
$model [ 'advanced' ] = [];
}
// get a list of available scales
$scales = array_map ( function ( $scale ){
return [ " id " => $scale -> id , " name " => $scale -> name ,];
}, \grade_scale :: fetch_all ( array ( 'courseid' => 0 )) ) ;
$model [ 'advanced' ][ 'force_scales' ] = [
'scales' => $scales ,
];
}
return $model ;
}
public static function add ( $fields ){
global $CFG , $DB ;
$addable = [ 'name' , 'shortname' , 'description' , 'context_id' , 'slots' , 'startdate' , 'enddate' , 'aggregation' , 'aggregation_config' ];
$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 studyplan is immediately cached
}
public function edit ( $fields ){
global $DB ;
$editable = [ 'name' , 'shortname' , 'description' , 'context_id' , 'slots' , 'startdate' , 'enddate' , 'aggregation' , 'aggregation_config' ];
$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 );
//reload the context...
$this -> context = null ;
$this -> context ();
// reload aggregator
$this -> aggregator = aggregator :: createOrDefault ( $this -> r -> aggregation , $this -> r -> aggregation_config );
return $this ;
}
public function delete ( $force = false ){
global $DB ;
if ( $force ){
$children = studyline :: find_studyplan_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' , [ 'id' => $this -> id ]);
return success :: success ();
}
}
2023-05-19 16:45:15 +02:00
public static function find_all ( $contextid =- 1 ){
2023-05-17 21:19:14 +02:00
global $DB , $USER ;
$list = [];
2023-05-19 16:45:15 +02:00
if ( $contextid <= 0 ){
$ids = $DB -> get_fieldset_select ( self :: TABLE , " id " , " " );
2023-05-17 21:19:14 +02:00
}
2023-05-19 16:45:15 +02:00
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 ]);
}
2023-05-17 21:19:14 +02:00
foreach ( $ids as $id )
{
$list [] = studyplan :: 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 [] = studyplan :: 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 ;
}
/**
* Find The active studyplans where the specified user is a teacher
* ( Has the mod / assign :: grade capability in one of the linked courses )
* TODO : OPTIMIZE THIS CHECK !!!
*/
public static function find_teaching ( $userid ){
global $DB ;
$list = [];
// First find all active study plans
$sql = " SELECT s.id FROM { local_treestudyplan} s
WHERE startdate <= NOW () and enddate >= NOW () " ;
$plan_ids = $DB -> get_fieldset_sql ( $sql , []);
foreach ( $plan_ids as $planid ) {
$sql = " SELECT i.course_id FROM mdl_local_treestudyplan_item i
INNER JOIN mdl_local_treestudyplan_line l ON i . line_id = l . id
WHERE l . studyplan_id = : plan_id AND i . course_id IS NOT NULL " ;
$course_ids = $DB -> get_fieldset_sql ( $sql , [ " plan_id " => $planid ]);
$linked = false ;
foreach ( $course_ids as $cid ){
$coursecontext = \context_course :: instance ( $cid );
if ( is_enrolled ( $coursecontext , $userid , 'mod/assign:grade' )){
$linked = true ;
break ; // No need to search further
}
}
if ( $linked )
{
$list [ $planid ] = self :: findById ( $planid );
}
}
return $list ;
}
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 );
}
/**
* Retrieve the users linked to this studyplan .
* @ return array of User objects
*/
public function find_linked_users (){
global $DB ;
$users = [];
$uids = $this -> find_linked_userids ();
foreach ( $uids as $uid ){
$users [] = $DB -> get_record ( " user " ,[ " id " => $uid ]);
}
return $users ;
}
/**
* Retrieve the user id ' s of the users linked to this studyplan .
* @ return array of int ( User Id )
*/
private function find_linked_userids () : array {
global $DB ;
$uids = [];
// First get directly linked userids
$sql = " SELECT j.user_id FROM { local_treestudyplan_user} j
WHERE j . studyplan_id = : planid " ;
$ulist = $DB -> get_fieldset_sql ( $sql , [ 'planid' => $this -> id ]);
$uids = array_merge ( $uids , $ulist );
foreach ( $ulist as $uid ){
$users [] = $DB -> get_record ( " user " ,[ " id " => $uid ]);
}
// Next het users linked though cohort
$sql = " SELECT cm.userid FROM { local_treestudyplan_cohort} j
INNER JOIN { cohort_members } cm ON j . cohort_id = cm . cohortid
WHERE j . studyplan_id = : planid " ;
$ulist = $DB -> get_fieldset_sql ( $sql , [ 'planid' => $this -> id ]);
$uids = array_merge ( $uids , $ulist );
return array_unique ( $uids );
}
/** Check if this studyplan is linked to a particular user
* @ param bool | stdClass $user The userid or user record of the user
*/
public function has_linked_user ( $user ){
if ( is_int ( $user )){
$userid = $user ;
} else {
$userid = $user -> id ;
}
$uids = $this -> find_linked_userids ();
if ( in_array ( $userid , $uids )){
return true ;
} else {
return false ;
}
}
public static function user_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' ),
" shortname " => new \external_value ( PARAM_TEXT , 'shortname of studyplan' ),
" description " => new \external_value ( PARAM_TEXT , 'description of studyplan' ),
" slots " => new \external_value ( PARAM_INT , 'number of slots in studyplan' ),
" startdate " => new \external_value ( PARAM_TEXT , 'start date of studyplan' ),
" enddate " => new \external_value ( PARAM_TEXT , 'end date of studyplan' ),
" studylines " => new \external_multiple_structure ( studyline :: user_structure ()),
" aggregation_info " => aggregator :: basic_structure (),
], 'Studyplan with user info' , $value );
}
public function user_model ( $userid ){
$model = [
'id' => $this -> r -> id ,
'name' => $this -> r -> name ,
'shortname' => $this -> r -> shortname ,
'description' => $this -> r -> description ,
'slots' => $this -> r -> slots ,
'startdate' => $this -> r -> startdate ,
'enddate' => $this -> r -> enddate ,
'studylines' => [],
'aggregation_info' => $this -> aggregator -> basic_model (),
];
$children = studyline :: find_studyplan_children ( $this );
foreach ( $children as $c )
{
$model [ 'studylines' ][] = $c -> user_model ( $userid );
}
return $model ;
}
public static function duplicate_plan ( $plan_id , $name , $shortname )
{
$ori = self :: findById ( $plan_id );
$new = $ori -> duplicate ( $name , $shortname );
return $new -> simple_model ();
}
public function duplicate ( $name , $shortname )
{
// First duplicate the studyplan structure
$new = studyplan :: add ([
'name' => $name ,
'shortname' => $shortname ,
'description' => $this -> r -> description ,
'slots' => $this -> r -> slots ,
'startdate' => $this -> r -> startdate ,
'enddate' => empty ( $this -> r -> enddate ) ? null : $this -> r -> enddate ,
]);
// next, copy the studylines
$children = studyline :: find_studyplan_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_plan ()
{
$model = $this -> export_model ();
$json = json_encode ([
" type " => " studyplan " ,
" version " => 1.0 ,
" studyplan " => $model
], \JSON_PRETTY_PRINT );
return [ " format " => " application/json " , " content " => $json ];
}
public function export_plan_csv ()
{
$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 = [
'name' => $this -> r -> name ,
'shortname' => $this -> r -> shortname ,
'description' => $this -> r -> description ,
'slots' => $this -> r -> slots ,
'startdate' => $this -> r -> startdate ,
'enddate' => $this -> r -> enddate ,
" aggregation " => $this -> r -> aggregation ,
" aggregation_config " => json_decode ( $this -> aggregator -> config_string ()),
'aggregation_info' => $this -> aggregator -> basic_model (),
'studylines' => $this -> export_studylines_model (),
];
return $model ;
}
public function export_studylines_model ()
{
$children = studyline :: find_studyplan_children ( $this );
$lines = [];
foreach ( $children as $c )
{
$lines [] = $c -> export_model ();
}
return $lines ;
}
2023-05-19 16:45:15 +02:00
public static function import_studyplan ( $content , $format = " application/json " , $context_id = 1 )
2023-05-17 21:19:14 +02:00
{
if ( $format != " application/json " ) { return false ;}
$content = json_decode ( $content , true );
if ( $content [ " type " ] == " studyplan " && $content [ " version " ] >= 1.0 ){
// Make sure the aggregation_config is re-encoded as json text
$content [ " studyplan " ][ " aggregation_config " ] = json_encode ( $content [ " studyplan " ][ " aggregation_config " ]);
2023-05-19 16:45:15 +02:00
// And make sure the context_id is set to the
$content [ " studyplan " ][ " context_id " ] = $context_id ;
2023-05-17 21:19:14 +02:00
$plan = self :: add ( $content [ " studyplan " ]);
return $plan -> import_studylines_model ( $content [ " studyplan " ][ " studylines " ]);
}
else {
error_log ( " Invalid format and type: { $content [ 'type' ] } version { $content [ 'version' ] } " );
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 " ] >= 1.0 ){
return $this -> import_studylines_model ( $content [ " studylines " ]);
}
else if ( $content [ " type " ] == " studyplan " && $content [ " version " ] >= 1.0 ){
return $this -> import_studylines_model ( $content [ " studyplan " ][ " studylines " ]);
}
else {
return false ;
}
}
protected function find_studyline_by_shortname ( $shortname ){
$children = studyline :: find_studyplan_children ( $this );
foreach ( $children as $l ){
if ( $shortname == $l -> shortname ()){
return $l ;
}
}
return null ;
}
protected 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 ;
}
}