Groundwork for pages and periods in backend

This commit is contained in:
PMKuipers 2023-07-23 16:25:08 +02:00
parent ff321e21c5
commit 3687461585
10 changed files with 720 additions and 297 deletions

View File

@ -195,7 +195,7 @@ class gradeinfo {
"required" => $this->is_required(),
];
// Unfortunately, lazy loading of the completion data is off, since we need the data to show study item completion...
if($studyitem !== null && $this->is_selected() && has_capability('local/treestudyplan:viewuserreports',$studyitem->getStudyline()->getStudyplan()->context())
if($studyitem !== null && $this->is_selected() && has_capability('local/treestudyplan:viewuserreports',$studyitem->getStudyline()->studyplan()->context())
&& $this->gradingscanner->is_available()){
$model['grading'] = $this->gradingscanner->model();
}
@ -246,7 +246,7 @@ class gradeinfo {
if(!isset($this->studyitem)){
throw new \UnexpectedValueException("Study item not set (null) for gradeinfo in report mode");
}
$aggregator = $this->studyitem->getStudyline()->getStudyplan()->getAggregator();
$aggregator = $this->studyitem->studyline()->studyplan()->aggregator();
$completion = $aggregator->grade_completion($this,$userid);
$model = [

View File

@ -72,7 +72,7 @@ class tristate_aggregator extends \local_treestudyplan\aggregator {
}
public function aggregate_course(courseinfo $courseinfo, studyitem $studyitem, $userid){
$condition = $studyitem->getConditions();
$condition = $studyitem->conditions();
if(empty($condition)){
$condition = self::DEFAULT_CONDITION;
}
@ -85,7 +85,7 @@ class tristate_aggregator extends \local_treestudyplan\aggregator {
}
public function aggregate_junction(array $completion, studyitem $studyitem, $userid){
$completed = self::aggregate_completion($completion,$studyitem->getConditions());
$completed = self::aggregate_completion($completion,$studyitem->conditions());
// if null result (conditions are unknown/null) - default to ALL
return isset($completed)?$completed:(self::aggregate_completion($completion,'ALL'));
}

View File

@ -27,11 +27,11 @@ class studyitem {
return $this->studyline->context();
}
public function getStudyline(): studyline {
public function studyline(): studyline {
return $this->studyline;
}
public function getConditions() {
public function conditions() {
return $this->r->conditions;
}
@ -49,7 +49,7 @@ class studyitem {
$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();
$this->aggregator = $this->studyline()->studyplan()->aggregator();
}
public function id(){
@ -147,7 +147,7 @@ class studyitem {
} 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();
$studentids = $this->studyline()->studyplan()->find_linked_userids();
$model['badge'] = $badgeinfo->editor_model($studentids);
}
}
@ -200,7 +200,7 @@ class studyitem {
$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();
$item->studyline()->studyplan()->mark_csync_changed();
}
return $item;
}

View File

@ -26,13 +26,14 @@ class studyline {
private $r; // Holds database record
private $id;
private $page;
private $studyplan;
public function context(): \context {
return $this->studyplan->context();
}
public function getStudyplan() : studyplan {
public function studyplan() : studyplan {
return $this->studyplan;
}
@ -47,7 +48,8 @@ class studyline {
global $DB;
$this->id = $id;
$this->r = $DB->get_record(self::TABLE,['id' => $id]);
$this->studyplan = studyplan::findById($this->r->studyplan_id);
$this->page = studyplanpage::findById($this->r->page_id);
$this->studyplan = $this->page->studyplan();
}
public function id(){
@ -105,7 +107,7 @@ class studyline {
// 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);
$num_slots = max($this->page->periods(),$max_slot +1);
// Create the required amount of slots
for($i=0; $i < $num_slots+1; $i++){
@ -150,13 +152,13 @@ class studyline {
public static function add($fields){
global $DB;
if(!isset($fields['studyplan_id'])){
throw new \InvalidArgumentException("parameter 'studyplan_id' missing");
if(!isset($fields['page_id'])){
throw new \InvalidArgumentException("parameter 'page_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'];
$page_id = $fields['page_id'];
$sqmax = $DB->get_field_select(self::TABLE,"MAX(sequence)","page_id = :page_id",['page_id' => $page_id]);
$addable = ['page_id','name','shortname','color'];
$info = ['sequence' => $sqmax+1];
foreach($addable as $f){
if(array_key_exists($f,$fields)){
@ -217,11 +219,12 @@ class studyline {
return success::success();
}
public static function find_studyplan_children(studyplan $plan)
public static function find_page_children(studyplanpage $page)
{
global $DB;
$list = [];
$ids = $DB->get_fieldset_select(self::TABLE,"id","studyplan_id = :plan_id ORDER BY sequence",['plan_id' => $plan->id()]);
$ids = $DB->get_fieldset_select(self::TABLE,"id","page_id = :page_id ORDER BY sequence",
['page_id' => $page->id()]);
foreach($ids as $id) {
$list[] = self::findById($id);
}
@ -263,7 +266,7 @@ class studyline {
// 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);
$num_slots = max($this->page->periods(),$max_slot +1);
// Create the required amount of slots
for($i=0; $i < $num_slots+1; $i++){
@ -339,7 +342,7 @@ class studyline {
$filterlayer = 0;
foreach($slotmodel as $itemmodel)
{
if($itemmodel["type"] == "course" || $itemmodel["type"] == "competency"){
if($itemmodel["type"] == "course"){
$itemmodel["layer"] = $courselayer;
$courselayer++;
}else {

View File

@ -15,8 +15,9 @@ class studyplan {
private $aggregator;
private $context = null; // Hold context object once retrieved
private $linked_userids = null; // cache lookup of linked users (saves queries)
private $page_cache = null;
public function getAggregator(){
public function aggregator(){
return $this->aggregator;
}
@ -44,10 +45,6 @@ class studyplan {
return $this->r->shortname;
}
public function slots(){
return $this->r->slots;
}
public function name(){
return $this->r->name;
}
@ -56,6 +53,16 @@ class studyplan {
return new \DateTime($this->r->startdate);
}
public function pages(){
// cached version of find_studyplan_children.
// (may be premature optimization, since
// find_studyplan_children also does some caching)
if(empty($this->page_cache)){
$this->page_cache = studyplanpage::find_studyplan_children($this);
}
return $this->page_cache;
}
/**
* Return the context this studyplan is associated to
*/
@ -71,27 +78,13 @@ class studyplan {
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(),
@ -103,11 +96,8 @@ class studyplan {
'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(),
@ -120,18 +110,11 @@ class 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()),
"pages" => new \external_multiple_structure(studyplanpage::editor_structure()),
"advanced" => new \external_single_structure([
"force_scales" => new \external_single_structure([
"scales" => new \external_multiple_structure(new \external_single_structure([
@ -151,24 +134,16 @@ class studyplan {
'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' => [],
'aggregation_info' => $this->aggregator->basic_model(),
'pages' => [],
];
$children = studyline::find_studyplan_children($this);
foreach($children as $c)
foreach($this->pages() as $p)
{
$model['studylines'][] = $c->editor_model();
$model['pages'][] = $p->editor_model();
}
if(has_capability('local/treestudyplan:forcescales', \context_system::instance())){
@ -231,14 +206,14 @@ class studyplan {
global $DB;
if($force){
$children = studyline::find_studyplan_children($this);
$children = studyplanpage::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');
if($DB->count_records('local_treestudyplan_page',['studyplan_id' => $this->id]) > 0){
return success::fail('cannot delete studyplan that still has pages');
}
else
{
@ -407,10 +382,7 @@ class 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()),
"pages" => new \external_multiple_structure(studyplanpage::user_structure()),
"aggregation_info" => aggregator::basic_structure(),
],'Studyplan with user info',$value);
@ -423,17 +395,13 @@ class studyplan {
'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' => [],
'pages' => [],
'aggregation_info' => $this->aggregator->basic_model(),
];
$children = studyline::find_studyplan_children($this);
foreach($children as $c)
foreach($this->pages() as $p)
{
$model['studylines'][] = $c->user_model($userid);
$model['pages'][] = $p->user_model($userid);
}
return $model;
}
@ -459,24 +427,10 @@ class studyplan {
// 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();
foreach($this->pages() as $p){
$newchild = $p->duplicate($this);
}
// 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;
}
@ -493,95 +447,12 @@ class studyplan {
$model = $this->export_model();
$json = json_encode([
"type"=>"studyplan",
"version"=>1.0,
"version"=>2.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 = [
@ -594,20 +465,19 @@ class studyplan {
"aggregation" => $this->r->aggregation,
"aggregation_config" => json_decode($this->aggregator->config_string()),
'aggregation_info' => $this->aggregator->basic_model(),
'studylines' => $this->export_studylines_model(),
'pages' => $this->export_pages_model(),
];
return $model;
}
public function export_studylines_model()
public function export_pages_model()
{
$children = studyline::find_studyplan_children($this);
$lines = [];
foreach($children as $c)
$pages = [];
foreach($this->pages() as $p)
{
$lines[] = $c->export_model();
$pages[] = $p->export_model();
}
return $lines;
return $pages;
}
public static function import_studyplan($content,$format="application/json",$context_id=1)
@ -615,15 +485,18 @@ class studyplan {
if($format != "application/json") { return false;}
$content = json_decode($content,true);
if($content["type"] == "studyplan" && $content["version"] >= 1.0){
if($content["type"] == "studyplan" && $content["version"] >= 2.0){
// Make sure the aggregation_config is re-encoded as json text
$content["studyplan"]["aggregation_config"] = json_encode($content["studyplan"]["aggregation_config"]);
// And make sure the context_id is set to the
// And make sure the context_id is set to the provided context for import
$content["studyplan"]["context_id"] = $context_id;
// Create a new plan, based on the given parameters - this is the import studyplan part
$plan = self::add($content["studyplan"]);
return $plan->import_studylines_model($content["studyplan"]["studylines"]);
// Now import each page
return $plan->import_pages_model($content["studyplan"]["pages"]);
}
else {
@ -632,58 +505,33 @@ class studyplan {
}
}
public function import_studylines($content,$format="application/json")
public function import_pages($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"]);
if($content["version"] >= 2.0){
if($content["type"] == "studyplanpage"){
// import single page from a studyplanpage (wrapped in array of one page)
return $this->import_pages_model([$content["page"]]);
}
else if($content["type"] == "studyplan"){
// Import all pages from the studyplan
return $this->import_pages_model($content["studyplan"]["pages"]);
}
}
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)
protected function import_pages_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]);
}
$this->pages(); // make sure the page cache is initialized, since we will be adding to it.
foreach($model as $p){
$p["studyplan_id"] = $this->id();
$page = studyplanpage::add($p);
$this->page_cache[] = $page;
$page->import_studylines_model($p["studylines"]);
}
return true;
}

544
classes/studyplanpage.php Normal file
View File

@ -0,0 +1,544 @@
<?php
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'),
],'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,
];
}
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 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()),
],'Studyplan page 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,
'periods' => $this->r->periods,
'startdate' => $this->r->startdate,
'enddate' => $this->r->enddate,
'studylines' => [],
];
$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];
}
}
$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];
}
}
$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',['studyplan_id' => $this->id]) > 0){
return success::fail('cannot delete studyplan that still has studylines');
}
else
{
$DB->delete_records('local_treestudyplan_page', ['id' => $this->id]);
return success::success();
}
}
public static function find_all($contextid=-1){
global $DB, $USER;
$list = [];
if($contextid <= 0){
$ids = $DB->get_fieldset_select(self::TABLE,"id","");
}
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]);
}
foreach($ids as $id)
{
$list[] = self::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[] = self::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;
}
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);
}
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()),
],'Studyplan page with user info',$value);
}
public function user_model($userid){
$model = [
'id' => $this->r->id,
'fullname' => $this->r->name,
'shortname' => $this->r->shortname,
'description' => $this->r->description,
'periods' => $this->r->periods,
'startdate' => $this->r->startdate,
'enddate' => $this->r->enddate,
'studylines' => [],
];
$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($page_id,$name,$shortname)
{
$ori = self::findById($page_id);
$new = $ori->duplicate($name,$shortname);
return $new->simple_model();
}
public function duplicate($name,$shortname)
{
// First duplicate the studyplan structure
$new = studyplanpage::add([
'fullname' => $name,
'shortname' => $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 $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_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()
{
//TODO: Period shortnames instead of just P1, P2, P3 etc
$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 = [
'fullname' => $this->r->name,
'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(),
];
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_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 {
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_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;
}
}

View File

@ -65,12 +65,14 @@ foreach($plans as $plan){
cli_writeln(" - {$u->firstname} {$u->lastname} / {$u->username}");
}
$lines = studyline::find_studyplan_children($plan);
foreach($lines as $line){
cli_writeln(" ** {$line->name()} **");
$items = studyitem::find_studyline_children($line);
foreach($users as $u){
$generator->addskill($u->username,$line->shortname());
foreach($plan->pages() as $page){
$lines = studyline::find_page_children($page);
foreach($lines as $line){
cli_writeln(" ** {$line->name()} **");
$items = studyitem::find_studyline_children($line);
foreach($users as $u){
$generator->addskill($u->username,$line->shortname());
}
}
}
}

View File

@ -89,74 +89,76 @@ foreach($plans as $plan){
cli_heading($plan->name());
$users = $plan->find_linked_users();
$lines = studyline::find_studyplan_children($plan);
foreach($lines as $line){
cli_writeln(" ** {$line->name()} **");
$items = studyitem::find_studyline_children($line);
foreach($items as $item){
if($item->type() == studyitem::COURSE) { // only handle courses for now
$courseinfo = $item->getcourseinfo();
cli_writeln(" # {$courseinfo->shortname()}");
foreach($plan->pages() as $page){
$lines = studyline::find_page_children($page);
foreach($lines as $line){
cli_writeln(" ** {$line->name()} **");
$items = studyitem::find_studyline_children($line);
foreach($items as $item){
if($item->type() == studyitem::COURSE) { // only handle courses for now
$courseinfo = $item->getcourseinfo();
cli_writeln(" # {$courseinfo->shortname()}");
if($courseinfo->course()->startdate <= time()){
if($courseinfo->course()->startdate <= time()){
foreach($users as $u){
cli_writeln(" -> {$u->firstname} {$u->lastname} <-");
$gradables = gradeinfo::list_studyitem_gradables($item);
$gen = $generator->generate($u->username,$line->shortname(),$gradables);
foreach($gen as $gg){
$g = $gg->gi;
$gi = $g->getGradeitem();
foreach($users as $u){
cli_writeln(" -> {$u->firstname} {$u->lastname} <-");
$gradables = gradeinfo::list_studyitem_gradables($item);
$gen = $generator->generate($u->username,$line->shortname(),$gradables);
foreach($gen as $gg){
$g = $gg->gi;
$gi = $g->getGradeitem();
$name = $gi->itemname;
$grade = $gg->gradetext;
cli_write (" - {$name} = {$grade}");
// Check if the item is alreaady graded for this user
$existing = $count = $DB->count_records_select('grade_grades','itemid = :gradeitemid AND finalgrade IS NOT NULL and userid = :userid',
['gradeitemid' => $gi->id, 'userid' => $u->id]);
$name = $gi->itemname;
$grade = $gg->gradetext;
cli_write (" - {$name} = {$grade}");
// Check if the item is alreaady graded for this user
$existing = $count = $DB->count_records_select('grade_grades','itemid = :gradeitemid AND finalgrade IS NOT NULL and userid = :userid',
['gradeitemid' => $gi->id, 'userid' => $u->id]);
if(!$existing){
if($gg->grade > 0){
if($gi->itemmodule == "assign"){
// If it is an assignment, submit though that interface
list($c,$cminfo) = get_course_and_cm_from_instance($gi->iteminstance,$gi->itemmodule);
$cm_ctx = \context_module::instance($cminfo->id);
$a = new \assign($cm_ctx,$cminfo,$c);
if(!$existing){
if($gg->grade > 0){
if($gi->itemmodule == "assign"){
// If it is an assignment, submit though that interface
list($c,$cminfo) = get_course_and_cm_from_instance($gi->iteminstance,$gi->itemmodule);
$cm_ctx = \context_module::instance($cminfo->id);
$a = new \assign($cm_ctx,$cminfo,$c);
$ug = $a->get_user_grade($u->id,true);
$ug->grade = grade_floatval($gg->grade);
$ug->grader = $USER->id;
$ug->feedbacktext = nl2br( htmlspecialchars($gg->fb));
$ug->feedbackformat = FORMAT_HTML;
$ug = $a->get_user_grade($u->id,true);
$ug->grade = grade_floatval($gg->grade);
$ug->grader = $USER->id;
$ug->feedbacktext = nl2br( htmlspecialchars($gg->fb));
$ug->feedbackformat = FORMAT_HTML;
//print_r($ug);
if(!$options["dryrun"]){
$a->update_grade($ug);
grade_regrade_final_grades($c->id,$u->id,$gi);
cli_writeln(" ... Stored");
} else {
cli_writeln(" ... (Dry Run)");
}
//print_r($ug);
if(!$options["dryrun"]){
$a->update_grade($ug);
grade_regrade_final_grades($c->id,$u->id,$gi);
cli_writeln(" ... Stored");
} else {
cli_writeln(" ... (Dry Run)");
// Otherwise, set the grade through the manual grading override
cli_writeln(" ... Cannot store");
}
} else {
// Otherwise, set the grade through the manual grading override
cli_writeln(" ... Cannot store");
cli_writeln(" ... No grade");
}
} else {
cli_writeln(" ... No grade");
cli_writeln(" ... Already graded");
}
} else {
cli_writeln(" ... Already graded");
}
}
}
}
else
{
cli_writeln(" Skipping since it has not started yet");
else
{
cli_writeln(" Skipping since it has not started yet");
}
}
}
}

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" ?>
<XMLDB PATH="local/treestudyplan/db" VERSION="20230719" COMMENT="XMLDB file for Moodle local/treestudyplan"
<XMLDB PATH="local/treestudyplan/db" VERSION="20230720" COMMENT="XMLDB file for Moodle local/treestudyplan"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="../../../lib/xmldb/xmldb.xsd"
>
@ -27,9 +27,6 @@
<FIELD NAME="name" TYPE="text" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="shortname" TYPE="text" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="description" TYPE="text" NOTNULL="false" SEQUENCE="false"/>
<FIELD NAME="slots" TYPE="int" LENGTH="9" NOTNULL="true" DEFAULT="4" SEQUENCE="false"/>
<FIELD NAME="startdate" TYPE="text" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="enddate" TYPE="text" NOTNULL="false" SEQUENCE="false"/>
<FIELD NAME="aggregation" TYPE="char" LENGTH="30" NOTNULL="true" DEFAULT="bistate" SEQUENCE="false"/>
<FIELD NAME="aggregation_config" TYPE="text" NOTNULL="false" SEQUENCE="false"/>
<FIELD NAME="context_id" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false"/>

View File

@ -381,14 +381,41 @@ function xmldb_local_treestudyplan_upgrade($oldversion) {
$recordset->close();
}
// Treestudyplan savepoint reached.
upgrade_plugin_savepoint(true, 2023071900, 'local', 'treestudyplan');
}
if ($oldversion < 2023072000) {
// Define field slots to be dropped from local_treestudyplan.
$table = new xmldb_table('local_treestudyplan');
$field = new xmldb_field('slots');
// Conditionally launch drop field slots.
if ($dbman->field_exists($table, $field)) {
$dbman->drop_field($table, $field);
}
// Define field startdate to be dropped from local_treestudyplan.
$table = new xmldb_table('local_treestudyplan');
$field = new xmldb_field('startdate');
// Conditionally launch drop field startdate.
if ($dbman->field_exists($table, $field)) {
$dbman->drop_field($table, $field);
}
// Define field enddate to be dropped from local_treestudyplan.
$table = new xmldb_table('local_treestudyplan');
$field = new xmldb_field('enddate');
// Conditionally launch drop field enddate.
if ($dbman->field_exists($table, $field)) {
$dbman->drop_field($table, $field);
}
// Treestudyplan savepoint reached.
upgrade_plugin_savepoint(true, 2023072000, 'local', 'treestudyplan');
}