Select Git revision
HISinOneSync.php
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
HISinOneSync.php 22.87 KiB
<?php
require_once 'AuthenticatedSoapClient.php';
class HISinOneSync
{
const UNIT_ID_DF = 'add9acb4890daadbcd4b7897e550cb17';
private $unitService;
private $accountService;
private $keyvalueService;
private $logs = '';
public function sync_courses($lvgs, $semester, $update, $update_studip)
{
$this->unitService = new AuthenticatedSoapClient('UnitService');
$this->accountService = new AuthenticatedSoapClient('AccountService');
$this->keyvalueService = new AuthenticatedSoapClient('KeyvalueService');
$personService = new AuthenticatedSoapClient('PersonService');
$planelementService = new AuthenticatedSoapClient('PlanelementService');
$courseInterfaceService = new AuthenticatedSoapClient('CourseInterfaceService');
$curriculumDesignerService = new AuthenticatedSoapClient('CurriculumDesignerService');
$curriculumDesignerRelationService = new AuthenticatedSoapClient('CurriculumDesignerRelationService');
$his_gender = [1 => 'M', 2 => 'W', 3 => 'D'];
$term_info = $this->get_term_info(date('Y-m-d', $semester->beginn));
$unit_type = $this->get_unit_types();
$courses = $this->match_courses($lvgs, $unit_type, $semester);
$current = [];
$updated = [];
$deleted = [];
foreach ($lvgs as $lvg) {
foreach ($lvg->courses as $course) {
$unit_id = $courses[$course->id];
if ($unit_id && $this->course_in_semester($course, $semester)) {
$current[$unit_id] = true;
}
}
// drop relation to reduce memory usage
$lvg->resetRelation('courses');
}
foreach ($lvgs as $lvg) {
$this->log('lvg: %d, "%s"', $lvg->id, $lvg->name);
try {
$lvg_unit = $curriculumDesignerService->readUnit(['unitId' => $lvg->id])->unitAttributes;
$children = $this->unit_get_children($lvg->id);
$orgunits = $this->unit_get_orgunits($lvg->id);
$orgunit_lid = $orgunits[0];
} catch (SoapFault $e) {
$this->log(' error: SOAP fault for unit %s: %s (skipped)', $lvg->id, $e->getMessage());
continue;
}
if ($lvg_unit->kElementtype !== 'VG') {
$this->log(' error: unit %s has invalid type "%s" (skipped)', $lvg->id, $lvg_unit->kElementtype);
continue;
}
foreach ($lvg->courses as $course) {
if (!$this->course_in_semester($course, $semester)) {
continue;
}
$course_name = is_object($course->name) ? $course->name->localized('de_DE') : $course->name;
$course_name = trim(preg_replace('/ \[.*?\]|\[.*?\] ?/', '', $course_name));
$course_name_en = is_object($course->name) ? $course->name->localized('en_GB') : $course->name;
$course_name_en = trim(preg_replace('/ \[.*?\]|\[.*?\] ?/', '', $course_name_en));
$unit_id = $courses[$course->id];
if ($unit_id) {
$unit = $this->readUnit($unit_id);
if ($unit->elementnr != $course->veranstaltungsnummer || $unit->defaulttext != $course_name) {
$this->log(' updating unit %d, "%s: %s"', $unit_id, $unit->elementnr, $unit->defaulttext);
$unitAttributes = [
'elementnr' => $course->veranstaltungsnummer,
'defaulttext' => $course_name,
'longtext' => $course_name
];
if ($update) {
$curriculumDesignerService->updateUnit(['unitId' => $unit_id, 'unitAttributes' => $unitAttributes]);
$unit = $this->readUnit($unit_id, $unitAttributes);
}
}
} else {
$this->log(' [new unit]');
if ($update) {
$unitAttributes = [
'kElementtype' => 'V',
'defaulttext' => $course_name,
'longtext' => $course_name,
'elementnr' => $course->veranstaltungsnummer,
'defaultlanguageValue' => 'de',
'validFrom' => '1900-01-01',
'validTo' => '2100-12-31',
'gradingType' => 'UB',
'extId' => '',
'orgunitLid' => $orgunit_lid
];
$eventAttributes = [
'eventtype' => $lvg_unit->eventAttributes->eventtype,
'frequencyOfOffervalue' => $lvg_unit->eventAttributes->frequencyOfOffervalue
];
$result = $curriculumDesignerService->createNewUnit(['unitAttributes' => $unitAttributes, 'eventAttributes' => $eventAttributes])->result;
if (is_numeric($result)) {
$unit_id = $result;
$unit = $this->readUnit($unit_id);
$courses[$course->id] = $unit_id;
} else {
$this->log(' error: %s', $result);
}
}
}
$this->log(' unit: %d, "%s: %s"', $unit_id, $course->veranstaltungsnummer, $course_name);
if ($unit_id) {
if ($update && $update_studip) {
$entry = DatafieldEntryModel::findByModel($course, self::UNIT_ID_DF)[0];
if ($entry) {
$entry->content = $unit_id;
$entry->store();
}
}
if (!isset($children[$unit_id])) {
$this->log(' adding relation %d -> %d ("%s: %s")', $lvg->id, $unit_id, $course->veranstaltungsnummer, $course_name);
if ($update) {
$curriculumDesignerService->connectParentAndChildUnit(['parentId' => $lvg->id, 'childId' => $unit_id]);
$children[$unit_id] = $unit;
}
}
}
if ($unit_id && !$updated[$unit_id]) {
if ($update) {
$unitAttributes = [
'defaulttext' => $course_name_en ?: $course_name,
'longtext' => $course_name_en ?: $course_name
];
$curriculumDesignerService->translateUnit(['unitId' => $unit_id, 'translationLanguage' => 'en', 'unitAttributes' => $unitAttributes]);
}
$orgunits = $this->unit_get_orgunits($unit_id);
$parents = $this->unitService->readUnitWithParents(['unitId' => $unit_id])->unit;
$lvg_orgunits = [];
foreach ($parents->parents->parentRelation as $parent) {
$lvg_orgunits = array_unique(array_merge($lvg_orgunits, $this->unit_get_orgunits($parent->parentId)));
}
foreach (array_diff($lvg_orgunits, $orgunits) as $orgunit) {
$this->log(' adding orgunit relation with lid %d', $orgunit);
if ($update) {
$curriculumDesignerRelationService->createUnitOrgunitRelation(['unitId' => $unit_id, 'orgunitLid' => $orgunit, 'unitOrgunitRelation' => 'D']);
}
}
foreach (array_diff($orgunits, $lvg_orgunits) as $orgunit) {
$this->log(' removing orgunit relation with lid %d', $orgunit);
if ($update) {
$curriculumDesignerRelationService->deleteUnitOrgunitRelation(['unitId' => $unit_id, 'orgunitLid' => $orgunit, 'unitOrgunitRelation' => 'D']);
}
}
$elements = $courseInterfaceService->readPlanelementsForUnit(['unitId' => $unit_id, 'termyear' => $term_info[0], 'termTypeId' => $term_info[1]])->planelements;
if ($elements->planelement) {
$element = $elements->planelement[0];
$element_id = $element->id;
if ($element->defaulttext != $course_name) {
$this->log(' updating element %d, "%s"', $element_id, $element->defaulttext);
if ($update) {
$planelement = [
'planelementId' => $element_id,
'defaulttext' => $course_name,
'longtext' => $course_name
];
$planelementService->updatePlanelementOfEvent(['planelement' => $planelement]);
}
}
} else {
$this->log(' [new element]');
$element_id = 0;
if ($update) {
$planelement = [
'unitId' => $unit_id,
'termType' => $term_info[2],
'termYear' => $term_info[0],
'defaulttext' => $course_name,
'longtext' => $course_name
];
$element_id = $planelementService->createNewPlanelementOfEvent($planelement)->planelementId;
}
}
if ($element_id) {
if ($update) {
$planelement = [
'planelementId' => $element_id,
'defaulttext' => $course_name_en ?: $course_name,
'longtext' => $course_name_en ?: $course_name,
'language' => 'en'
];
$planelementService->updatePlanelementOfEvent(['planelement' => $planelement]);
}
$persons = $courseInterfaceService->getPersonResponsibleForPlanelement(['planelementId' => $element_id])->personPlanelements;
$persons_matched = [];
$person_ids = [];
foreach ($course->getMembersWithStatus('dozent') as $lecturer) {
if (stripos($lecturer->label, 'nicht prüfungsberechtigt') !== false) {
continue;
}
$username = $lecturer->user->username;
$person_id = $this->find_account($username);
$found = false;
if (!$person_id) {
// createPerson201912() fails if gender is not set
if ($lecturer->user->auth_plugin === 'ldapos' && $lecturer->user->geschlecht) {
$person = [
'firstname' => $lecturer->user->vorname,
'surname' => $lecturer->user->nachname,
'gender' => $his_gender[$lecturer->user->geschlecht]
];
$orgrole = [
'orgunitLid' => $orgunit_lid,
'role' => 'pruefer'
];
$this->log(' [new account for username "%s", orgunit: %d]', $username, $orgunit_lid);
if ($update) {
$person_id = $personService->createPerson201912(['person' => $person, 'orgrole' => $orgrole])->id;
$password = base64_encode(random_bytes(12));
$this->accountService->createNewAccountForPerson(['personId' => $person_id, 'username' => $username, 'password' => $password]);
$this->find_account($username, $person_id);
}
} else {
$this->log(' warning: username "%s" not found', $username);
continue;
}
}
foreach ((array) $persons->personPlanelement as $person) {
if ($person->abstractPersonId == $person_id) {
$persons_matched[$person_id] = true;
$found = true;
}
}
if (!$found) {
$this->log(' adding person %d ("%s") to element %d', $person_id, $username, $element_id);
if ($update) {
$person_ids[] = $person_id;
$planelementService->assignResponsibleInstructor(['personId' => $person_id, 'planelementId' => $element_id]);
}
}
}
foreach ((array) $persons->personPlanelement as $person) {
if ($persons_matched[$person->abstractPersonId]) {
$person_ids[] = $person->abstractPersonId;
} else {
$this->log(' removing person %d from element %d', $person->abstractPersonId, $element_id);
if ($update) {
$planelementService->revokeResponsibleInstructor(['personId' => $person->abstractPersonId, 'planelementId' => $element_id]);
}
}
}
$this->log(' element: %d, "%s" (%s)', $element_id, $course_name, implode(', ', $person_ids));
}
$updated[$unit_id] = true;
}
}
foreach ($children as $child) {
if ($child->elementtypeId === $unit_type['V']) {
$delete = true;
foreach ($lvg->courses as $course) {
$entry = DatafieldEntryModel::findByModel($course, self::UNIT_ID_DF)[0];
$unit_id = $entry->content;
if ($child->id == $unit_id) {
if (!$current[$unit_id] || $this->course_is_recent($course, $semester)) {
$delete = false;
}
}
}
if ($delete) {
$this->log(' removing relation %d -> %d ("%s: %s")', $lvg->id, $child->id, $child->elementnr, $child->defaulttext);
if ($update) {
$curriculumDesignerService->deleteUnitrelation(['parentUnitId' => $lvg->id, 'childUnitId' => $child->id]);
unset($children[$child->id]);
}
}
/*
if (array_search($child->id, $courses) === false && !$deleted[$child->id]) {
$elements = $courseInterfaceService->readPlanelementsForUnit(['unitId' => $child->id, 'termyear' => $term_info[0], 'termTypeId' => $term_info[1]])->planelements;
if ($elements->planelement) {
$element = $elements->planelement[0];
$this->log(' unit: %d, "%s: %s"', $child->id, $child->elementnr, $child->defaulttext);
$this->log(' deleting element %d, "%s"', $element->id, $element->defaulttext);
if ($update) {
$planelementService->deletePlanelementOfEvent(['id' => $element->id]);
}
}
$deleted[$child->id] = true;
}
*/
}
}
// drop relation to reduce memory usage
$lvg->resetRelation('courses');
}
}
private function match_courses($lvgs, $unit_type, $semester)
{
$units = [];
$matched = [];
$priority = [];
$courses = [];
foreach ($lvgs as $lvg) {
try {
$children = $this->unit_get_children($lvg->id);
} catch (SoapFault $e) {
// ignore this group
continue;
}
foreach ($children as $child) {
if ($child->elementtypeId === $unit_type['V']) {
$units[$child->id] = $child;
}
}
}
foreach ($lvgs as $lvg) {
foreach ($lvg->courses as $course) {
if (!$this->course_in_semester($course, $semester)) {
continue;
}
$course_name = is_object($course->name) ? $course->name->localized('de_DE') : $course->name;
$course_name = trim(preg_replace('/ \[.*?\]|\[.*?\] ?/', '', $course_name));
$entry = DatafieldEntryModel::findByModel($course, self::UNIT_ID_DF)[0];
$unit_id = $entry->content;
if ($unit_id) {
$matched[$unit_id] = $course->id;
$priority[$unit_id] = 0;
if (isset($units[$unit_id])) {
$priority[$unit_id] = 4;
continue;
}
}
foreach ($units as $unit) {
$prio = 0;
if ($unit->elementnr === $course->veranstaltungsnummer) {
if (strcasecmp(trim($unit->defaulttext), $course_name) === 0) {
$prio = 3;
} else if (stripos($course_name, $unit->defaulttext) !== false) {
$prio = 1;
} else if (stripos($unit->defaulttext, $course_name) !== false) {
$prio = 1;
}
} else if (strcasecmp(trim($unit->defaulttext), $course_name) === 0) {
$prio = 2;
}
if ($prio && $priority[$unit->id] < $prio) {
$matched[$unit->id] = $course->id;
$priority[$unit->id] = $prio;
}
}
}
$lvg->resetRelation('courses');
}
foreach ($matched as $unit_id => $course_id) {
if (!$courses[$course_id] || $priority[$courses[$course_id]] < $priority[$unit_id]) {
$courses[$course_id] = $unit_id;
}
}
return $courses;
}
private function get_term_info($date)
{
list($year, $month, $day) = explode('-', $date);
if ($month < 4) {
return [$year - 1, 2, 'WiSe'];
} else if ($month < 10) {
return [$year, 1, 'SoSe'];
} else {
return [$year, 2, 'WiSe'];
}
}
private function course_in_semester($course, $semester)
{
return $course->visible == 1 &&
$course->getSemType()['class'] == 1 &&
$course->veranstaltungsnummer !== '' &&
$course->start_time <= $semester->beginn &&
($semester->beginn <= $course->start_time + $course->duration_time || $course->duration_time == -1);
}
private function course_is_recent($course, $semester)
{
$semester = Semester::findByTimestamp($semester->beginn - 1);
return $course->visible == 1 &&
$course->getSemType()['class'] == 1 &&
$course->veranstaltungsnummer !== '' &&
($semester->beginn <= $course->start_time + $course->duration_time || $course->duration_time == -1);
}
private function get_unit_types()
{
$values = $this->keyvalueService->getAll(['valueClass' => 'ElementtypeValue', 'lang' => 'de'])->values;
$result = [];
foreach ($values->value as $value) {
$result[$value->uniquename] = $value->id;
}
return $result;
}
private function readUnit($unit_id, $update = [])
{
static $cache = [];
if (isset($cache[$unit_id])) {
foreach ($update as $key => $value) {
$cache[$unit_id]->$key = $value;
}
return $cache[$unit_id];
}
$unit = $this->unitService->readUnit(['unitId' => $unit_id])->unit;
return $cache[$unit_id] = $unit;
}
private function unit_get_orgunits($unit_id)
{
static $cache = [];
$result = [];
if (isset($cache[$unit_id])) {
return $cache[$unit_id];
}
$orgunits = $this->unitService->findOrgunitsByUnit(['unitId' => $unit_id])->unitOrgunitList;
foreach ((array) $orgunits->unitOrgunitInfo as $info) {
$result[] = $info->orgunitLid;
}
return $cache[$unit_id] = $result;
}
private function unit_get_children($unit_id)
{
static $cache = [];
$result = [];
if (isset($cache[$unit_id])) {
return $cache[$unit_id];
}
$unit = $this->unitService->readUnitWithChildren(['unitId' => $unit_id])->unit;
foreach ((array) $unit->children->childRelation as $child) {
$result[$child->childId] = $this->readUnit($child->childId);
}
return $cache[$unit_id] = $result;
}
private function find_account($username, $person_id = null)
{
static $cache = [];
$result = false;
if (isset($person_id)) {
$cache[$username] = $person_id;
}
if (isset($cache[$username])) {
return $cache[$username];
}
$account = $this->accountService->findAccount(['username' => $username])->findAccountResult;
foreach ((array) $account->accountInfo as $account_info) {
if ($account_info->username === $username) {
$result = $account_info->personId;
}
}
return $cache[$username] = $result;
}
private function log($format, ...$args)
{
$this->logs .= vsprintf($format, $args) . "\n";
}
public function get_logs()
{
return $this->logs;
}
}