. */ if (! defined('BASEPATH')) exit('No direct script access allowed'); use CI3_Events as Events; class Noten extends FHCAPI_Controller { /** tbl_note by PK, read once per request. @see aktiveNoten() */ private $aktiveNotenCache = null; /** The teachers of one Lehreinheit. They do not change during one request. */ private $lehrendeCache = array(); /** The Lehreinheiten of one student in one course. A bulk path asks for them several times. */ private $lehreinheitCache = array(); /** Transcript grades of one student in one course. This tool never writes them. */ private $zeugnisnoteCache = array(); public function __construct() { $permissions = self::berechtigungenAusMatrix(); parent::__construct([ 'getStudentenNoten' => $permissions, 'getNoten' => $permissions, 'saveStudentenNoten' => $permissions, 'getNotenvorschlagStudent' => $permissions, 'saveNotenvorschlag' => $permissions, 'saveStudentPruefung' => $permissions, 'createPruefungen' => $permissions, 'saveNotenvorschlagBulk' => $permissions, 'savePruefungenBulk' => $permissions, 'getCisConfig' => $permissions, 'getNoteByPunkte' => $permissions, 'getBenotungstoolContext' => $permissions, 'getLehreinheitenFuerLv' => $permissions, 'getLehrendeFuerLehreinheit' => $permissions, 'getLvForStudiengang' => $permissions ]); $this->load->library('AuthLib', null, 'AuthLib'); $this->load->library('PhrasesLib'); $this->load->library('PruefungsverlaufLib', null, 'VerlaufLib'); // Loads LogLib with different debug trace levels to get data of the job that extends this class // It also specify parameters to set database fields $this->load->library('LogLib', array( 'classIndex' => 5, 'functionIndex' => 5, 'lineIndex' => 4, 'dbLogType' => 'API', // required 'dbExecuteUser' => 'RESTful API', 'requestId' => 'API', 'requestDataFormatter' => function ($data) { return json_encode($data); } ), 'logLib'); // Loads phrases system $this->loadPhrases([ 'global', 'person', 'benotungstool', 'lehre', 'ui', 'password' ]); $this->load->model('education/LePruefung_model', 'LePruefungModel'); $this->load->model('education/Lvgesamtnote_model', 'LvgesamtnoteModel'); $this->load->model('education/Lehrveranstaltung_model', 'LehrveranstaltungModel'); $this->load->model('education/Notenschluesselaufteilung_model', 'NotenschluesselaufteilungModel'); $this->load->model('education/Note_model', 'NoteModel'); $this->load->model('person/Person_model', 'PersonModel'); $this->load->model('organisation/Studienplan_model', 'StudienplanModel'); $this->load->model('crm/Student_model', 'StudentModel'); $this->load->model('codex/Mobilitaet_model', 'MobilitaetModel'); $this->load->model('organisation/Erhalter_model', 'ErhalterModel'); $this->load->model('organisation/Studiengang_model', 'StudiengangModel'); $this->load->config('noten'); $this->load->helper('hlp_sancho_helper'); } /** * POST 'uids', 'datum', optional 'note'/'punkte'. One exam for several students; without a * grade it writes "Noch nicht eingetragen". */ public function createPruefungen() { // role first: a caller without the action fails on the right, not on missing parameters $this->assertAktion('pruefung'); $payload = $this->getPostJSON(); if(!property_exists($payload, 'uids') || !property_exists($payload, 'datum') || !property_exists($payload, 'lva_id') || !property_exists($payload, 'sem_kurzbz')) { $this->terminateWithError($this->p->t('global', 'missingParameters'), 'general'); } $uids = $payload->uids; $datum = $payload->datum; $lva_id = $payload->lva_id; $stsem = $payload->sem_kurzbz; // every entry must name a student; the Lehreinheit is optional, the server looks it up if(!is_array($uids) || count($uids) === 0) { $this->terminateWithError($this->p->t('global', 'wrongParameters'), 'general'); } foreach($uids as $student) { if(!is_object($student) || !property_exists($student, 'uid') || isEmptyString($student->uid)) { $this->terminateWithError($this->p->t('global', 'wrongParameters'), 'general'); } } $this->assertLvAccess($lva_id, $stsem); // examination rules: no entry after the grade entry deadline $this->enforceNoteneintragungsfrist($stsem); $ret = []; $note = property_exists($payload, 'note') ? $payload->note : null; $punkte = property_exists($payload, 'punkte') ? $payload->punkte : null; // the points win: the grade comes from the grading scale; points without a grade refuse the entry if(CIS_GESAMTNOTE_PUNKTE && $punkte !== null && $punkte !== '') { $note = $this->noteAusPunkten($punkte, $lva_id, $stsem, implode(', ', array_column($uids, 'uid'))); if(is_string($note)) $this->terminateWithError($note, 'general'); } // without a selection the exam has no grade if($note === null || $note === '') { // config names the grade, the lib resolves it in tbl_note $note = $this->VerlaufLib->getNoteNichtEingetragen(); $punkte = null; } // the dialog sends the teacher when all selected students share one Lehreinheit $mitarbeiter_uid = property_exists($payload, 'mitarbeiter_uid') ? $payload->mitarbeiter_uid : null; // the same core as the dialog; each row gets its own error message foreach ($uids as $student) { $lehreinheit_id = property_exists($student, 'lehreinheit_id') ? $student->lehreinheit_id : null; $ret[$student->uid] = $this->savePruefungFuerStudent( null, $student->uid, $lva_id, $stsem, $lehreinheit_id, $note, $punkte, $datum, $mitarbeiter_uid ); } $this->logLib->logInfoDB(array('createPruefungen',$ret, getAuthUID(), getAuthPersonId())); $this->terminateWithSuccess($ret); } /** * GET, optional 'sem_kurzbz'. Role-determining entry point: a teacher gets their own courses, * an Assistenz gets the degree programmes they may pick from. */ public function getBenotungstoolContext() { $sem_kurzbz = $this->input->get("sem_kurzbz", TRUE); $lv_id = $this->input->get("lv_id", TRUE); // optional: deep-link target, used to preselect $this->load->library('PermissionLib'); // teachers keep the classic assigned-LV flow; the Studiengang flow is only for Assistenz. // Role determination mirrors assertLvAccess, which scopes each role's actual data // access. A teacher sees the own courses, an assistant sees the entitled degree programmes. $isLektor = $this->permissionlib->isBerechtigt('lehre/benotungstool'); $entitledStgs = $this->permissionlib->getSTG_isEntitledFor('lehre/benotungstool_assistenz'); $isAssistenz = !$isLektor && is_array($entitledStgs) && count($entitledStgs) > 0; $studiengaenge = array(); $lehrveranstaltungen = array(); $preselectStudiengang_kz = null; if (isset($sem_kurzbz) && !isEmptyString($sem_kurzbz)) { if ($isAssistenz) { $result = $this->StudiengangModel->getByStgs($entitledStgs, $sem_kurzbz); if (!isError($result)) $studiengaenge = getData($result) ?? array(); // deep-link: resolve the Studiengang of the requested LV so the frontend can preselect // the Studiengang dropdown (and then its LV) - only if the Assistenz is entitled for it // only digits are a course id if (isset($lv_id) && ctype_digit((string) $lv_id)) { $res = $this->LehrveranstaltungModel->load($lv_id); if (!isError($res) && hasData($res)) { $stg = getData($res)[0]->studiengang_kz; if (in_array($stg, $entitledStgs)) $preselectStudiengang_kz = $stg; } } } else { $result = $this->LehrveranstaltungModel->getLvForLektorInSemester($sem_kurzbz, getAuthUID()); if (!isError($result)) $lehrveranstaltungen = getData($result) ?? array(); } } $this->terminateWithSuccess(array( 'isAssistenz' => $isAssistenz, 'studiengaenge' => $studiengaenge, 'lehrveranstaltungen' => $lehrveranstaltungen, 'preselectStudiengang_kz' => $preselectStudiengang_kz )); } public function getCisConfig() { // The configuration names the special grades, tbl_note gives their keys. The client compares // keys, therefore the answer carries the resolved keys, never a Bezeichnung. $special = $this->VerlaufLib->getSpecialNotes(); $NOTEN_OHNE_ANTRITT = $special['ohneAntritt']; $NOTEN_OCCURANCE_LIMIT_MAP = $special['limitMap']; $NOTE_ENTSCHULDIGT = $special['entschuldigt']; $this->terminateWithSuccess( array( // show the points during the grade entry 'CIS_GESAMTNOTE_PUNKTE' => CIS_GESAMTNOTE_PUNKTE, // basically on/of toggle for the points/grade col and the arrow button 'CIS_GESAMTNOTE_UEBERSCHREIBEN' => CIS_GESAMTNOTE_UEBERSCHREIBEN, // only relevant in punkte calculation in backend // 'CIS_GESAMTNOTE_GEWICHTUNG' => CIS_GESAMTNOTE_GEWICHTUNG, // The maximum number of attempts that count in this tool. The server derives it from the // configuration or from the old TERMIN2/TERMIN3 flags. The client does not calculate it. 'CIS_GESAMTNOTE_MAX_ANTRITTE' => $this->VerlaufLib->getMaxAntritte(), // attempt from which the exam is kommissionell, or null; 'letzter' already resolved 'CIS_GESAMTNOTE_KOMMISSIONELL_AB_ANTRITT' => $this->VerlaufLib->getKommissionellAbAntritt(), // legacy type the kommissionell role writes; Stv reads that column 'PRUEFUNG_TYP_KOMMISSIONELL' => $this->config->item('PRUEFUNG_TYP_KOMMISSIONELL'), // the default column layout ('antritt' or 'datum'); the user can change it in the tool 'CIS_GESAMTNOTE_PRUEFUNGSSPALTEN' => $this->config->item('CIS_GESAMTNOTE_PRUEFUNGSSPALTEN'), // may this tool CREATE the kommissionelle Prüfung (application/config/noten.php)? // It always shows one that exists. 'CIS_GESAMTNOTE_ALLOW_CREATE_KOMMPRUEF' => $this->VerlaufLib->darfKommPruefAnlegen(), //technically exists but is never used, could be LE pendant to next flag // 'CIS_GESAMTNOTE_PRUEFUNG_MOODLE_NOTE' => CIS_GESAMTNOTE_PRUEFUNG_MOODLE_NOTE, // basically a toggle for "use teilnoten" and the source is always moodle // setting this to false breaks legacy tool and if that was fixed it wouldnt render any table at all // anyway so not sure why this even is a config at all. placebo at best // toggles availability of the teilnoten column... existas but do we really need this? 'CIS_GESAMTNOTE_PRUEFUNG_MOODLE_LE_NOTE' => CIS_GESAMTNOTE_PRUEFUNG_MOODLE_LE_NOTE, // availability of the two import flows (application/config/noten.php); when both are // true they are shown separately 'CIS_GESAMTNOTE_PRUEFUNGSIMPORT' => $this->config->item('CIS_GESAMTNOTE_PRUEFUNGSIMPORT'), 'CIS_GESAMTNOTE_NOTENIMPORT' => $this->config->item('CIS_GESAMTNOTE_NOTENIMPORT'), // does an imported row accept the shorthand from tbl_note.anmerkung as the grade? 'CIS_GESAMTNOTE_IMPORT_NOTENKUERZEL' => (bool) $this->config->item('CIS_GESAMTNOTE_IMPORT_NOTENKUERZEL'), // shape of an imported row and the badge in the exam cell 'CIS_GESAMTNOTE_IMPORT_SPALTEN_NOTEN' => $this->config->item('CIS_GESAMTNOTE_IMPORT_SPALTEN_NOTEN'), 'CIS_GESAMTNOTE_IMPORT_SPALTEN_PRUEFUNG' => $this->config->item('CIS_GESAMTNOTE_IMPORT_SPALTEN_PRUEFUNG'), 'CIS_GESAMTNOTE_IMPORT_DATUMSFORMAT' => $this->config->item('CIS_GESAMTNOTE_IMPORT_DATUMSFORMAT'), 'CIS_GESAMTNOTE_ANTRITT_ZEICHEN' => $this->config->item('CIS_GESAMTNOTE_ANTRITT_ZEICHEN'), // weighting of the partial grades; server applies, client only shows 'CIS_GESAMTNOTE_GEWICHTUNG' => defined('CIS_GESAMTNOTE_GEWICHTUNG') && CIS_GESAMTNOTE_GEWICHTUNG, // send a mail when approving grades 'CIS_GESAMTNOTE_FREIGABEMAIL_NOTE' => CIS_GESAMTNOTE_FREIGABEMAIL_NOTE, // actions this caller may perform; the client only hides buttons, the server decides 'CIS_GESAMTNOTE_AKTIONEN' => $this->erlaubteAktionen(), // release: password required, and final? 'CIS_GESAMTNOTE_FREIGABE_PASSWORT' => $this->config->item('CIS_GESAMTNOTE_FREIGABE_PASSWORT') !== false, 'CIS_GESAMTNOTE_FREIGABE_FINAL' => (bool) $this->config->item('CIS_GESAMTNOTE_FREIGABE_FINAL'), // release mail on or off; the test suite releases only without it 'CIS_GESAMTNOTE_FREIGABEMAIL' => $this->config->item('CIS_GESAMTNOTE_FREIGABEMAIL') !== false, // takeover and release write attempt 1 as an exam row 'CIS_GESAMTNOTE_ERSTANTRITT_BEI_UEBERNAHME' => (bool) $this->config->item('CIS_GESAMTNOTE_ERSTANTRITT_BEI_UEBERNAHME'), 'NOTEN_OHNE_ANTRITT' => $NOTEN_OHNE_ANTRITT, 'NOTEN_OCCURANCE_LIMIT_MAP' => $NOTEN_OCCURANCE_LIMIT_MAP, // pk of the 'entschuldigt' note; used to preserve excused Termine on new pruefung creation 'NOTE_ENTSCHULDIGT' => $NOTE_ENTSCHULDIGT, // Noteneintragungsfrist window (enforced server-side; also surfaced so the UI can hint at it) 'CIS_GESAMTNOTE_NOTENEINTRAGUNGSFRIST' => $this->config->item('CIS_GESAMTNOTE_NOTENEINTRAGUNGSFRIST'), // the two deadlines, fallback already resolved 'CIS_GESAMTNOTE_FRIST_EINGABE' => $this->fristAktiv('CIS_GESAMTNOTE_FRIST_EINGABE'), 'CIS_GESAMTNOTE_FRIST_PRUEFUNGSDATUM' => $this->fristAktiv('CIS_GESAMTNOTE_FRIST_PRUEFUNGSDATUM'), // does THIS user carry an entry-deadline exception? 'CIS_GESAMTNOTE_FRIST_AUSNAHME_GILT' => $this->darfFristUeberschreiten(), // exam date guards; server enforces, client only hides buttons 'CIS_GESAMTNOTE_TERMIN_GLEICHER_TAG' => (bool) $this->config->item('CIS_GESAMTNOTE_TERMIN_GLEICHER_TAG'), 'CIS_GESAMTNOTE_NOTE_SPERRE_BEI_SPAETEREM_TERMIN' => $this->config->item('CIS_GESAMTNOTE_NOTE_SPERRE_BEI_SPAETEREM_TERMIN') === null ? true : (bool) $this->config->item('CIS_GESAMTNOTE_NOTE_SPERRE_BEI_SPAETEREM_TERMIN'), 'CIS_GESAMTNOTE_DATUM_ZUKUNFT' => (bool) $this->config->item('CIS_GESAMTNOTE_DATUM_ZUKUNFT'), 'CIS_GESAMTNOTE_ANTRITT_MIN_ABSTAND_TAGE' => $this->config->item('CIS_GESAMTNOTE_ANTRITT_MIN_ABSTAND_TAGE'), 'CIS_GESAMTNOTE_ANTRITT_MAX_ABSTAND_TAGE' => $this->config->item('CIS_GESAMTNOTE_ANTRITT_MAX_ABSTAND_TAGE'), // when a grade closes the chain; resolved keys, never a Bezeichnung 'NOTEN_ABSCHLIESSEND' => $special['abschliessend'], 'CIS_GESAMTNOTE_NOTENVERBESSERUNG' => $this->VerlaufLib->darfVerbessern(), 'CIS_GESAMTNOTE_VERBESSERUNG_BESSERE_GEWINNT' => (bool) $this->config->item('CIS_GESAMTNOTE_VERBESSERUNG_BESSERE_GEWINNT'), // the remaining rule keys, as the server applies them; the test suite reads a rule only from here 'NOTEN_ANRECHNUNG' => $special['anrechnung'], 'NOTEN_RANGFOLGE' => $special['rangfolge'], 'NOTE_NICHT_EINGETRAGEN' => $special['nichtEingetragen'], 'PRUEFUNG_KOMMISSIONELL_TYPEN' => $this->VerlaufLib->getKommissionellTypen(), 'PRUEFUNG_TYPEN_OHNE_ANTRITT' => $this->VerlaufLib->getTypenOhneAntritt(), 'PRUEFUNG_TYP_JE_ANTRITT' => $this->VerlaufLib->getTypJeAntritt(), 'NOTENEINTRAGUNGSFRIST_SS' => $this->fristMonatTag('SS'), 'NOTENEINTRAGUNGSFRIST_WS' => $this->fristMonatTag('WS'), 'CIS_GESAMTNOTE_FRIST_AUSNAHME' => (array) $this->config->item('CIS_GESAMTNOTE_FRIST_AUSNAHME'), 'CIS_GESAMTNOTE_ROLLENMATRIX' => (array) $this->config->item('CIS_GESAMTNOTE_ROLLENMATRIX'), 'CIS_GESAMTNOTE_LEKTOR_NUR_EIGENE_LV' => $this->config->item('CIS_GESAMTNOTE_LEKTOR_NUR_EIGENE_LV') !== false, 'CIS_GESAMTNOTE_FREIGABEMAIL_EMPFAENGER' => $this->freigabeEmpfaengerEintraege(), 'CIS_GESAMTNOTE_FREIGABEMAIL_VORLAGE' => $this->freigabeVorlage(), 'CIS_GESAMTNOTE_PRUEFUNG_HEBT_FREIGABE_AUF' => $this->config->item('CIS_GESAMTNOTE_PRUEFUNG_HEBT_FREIGABE_AUF') !== false, 'CIS_GESAMTNOTE_VORSCHLAG_RUNDUNG' => $this->rundungsModus(), 'CIS_GESAMTNOTE_VORSCHLAG_PUNKTE_STELLEN' => $this->punkteNachkommastellen(), 'CIS_GESAMTNOTE_VORSCHLAG_NUR_LEHRENOTEN' => $this->config->item('CIS_GESAMTNOTE_VORSCHLAG_NUR_LEHRENOTEN') !== false, 'CIS_GESAMTNOTE_VORSCHLAG_NACH_WIEDERHOLUNG' => $this->config->item('CIS_GESAMTNOTE_VORSCHLAG_NACH_WIEDERHOLUNG') === true, 'CIS_GESAMTNOTE_IMPORT_ABBRUCH' => $this->importBrichtAb(), 'NOTEN_SORTIERUNG' => $this->config->item('NOTEN_SORTIERUNG') === 'bezeichnung' ? 'bezeichnung' : 'skala' ) ); } /** GET 'lv_id', 'sem_kurzbz'. All Lehreinheiten of the course, for the filter and the grade lists. */ public function getLehreinheitenFuerLv() { $lv_id = $this->input->get('lv_id'); $sem_kurzbz = $this->input->get('sem_kurzbz'); $this->assertLvAccess($lv_id, $sem_kurzbz); $this->load->model('education/Lehreinheit_model', 'LehreinheitModel'); $result = $this->LehreinheitModel->getLehreinheitenForLv($lv_id, $sem_kurzbz); $this->terminateWithSuccess($this->getDataOrTerminateWithError($result)); } /** * GET 'lehreinheit_id', 'lv_id', 'sem_kurzbz'. Teachers of one Lehreinheit; the dialog offers a * choice when there is more than one. */ public function getLehrendeFuerLehreinheit() { $lehreinheit_id = $this->input->get('lehreinheit_id'); $lv_id = $this->input->get('lv_id'); $sem_kurzbz = $this->input->get('sem_kurzbz'); if(!ctype_digit((string) $lehreinheit_id) || !$lv_id || !$sem_kurzbz) { $this->terminateWithError($this->p->t('global', 'missingParameters'), 'general'); } $this->assertLvAccess($lv_id, $sem_kurzbz); // the Lehreinheit must belong to this course $this->load->model('education/Lehreinheit_model', 'LehreinheitModel'); $le = $this->LehreinheitModel->loadWhere(array( 'lehreinheit_id' => $lehreinheit_id, 'lehrveranstaltung_id' => $lv_id, 'studiensemester_kurzbz' => $sem_kurzbz )); if(!hasData($le)) $this->terminateWithError($this->p->t('global', 'wrongParameters'), 'general'); $lehrende = array(); foreach($this->lehrendeDerLehreinheit($lehreinheit_id) as $l) { $lehrende[] = array( 'mitarbeiter_uid' => $l->mitarbeiter_uid, 'vorname' => $l->vorname, 'nachname' => $l->nachname ); } $this->terminateWithSuccess($lehrende); } /** * GET 'studiengang_kz', 'sem_kurzbz'. Courses of one Studiengang for the Assistenz flow; only * programmes the caller is entitled for. */ public function getLvForStudiengang() { $studiengang_kz = $this->input->get("studiengang_kz", TRUE); $sem_kurzbz = $this->input->get("sem_kurzbz", TRUE); if (!isset($studiengang_kz) || isEmptyString($studiengang_kz) || !isset($sem_kurzbz) || isEmptyString($sem_kurzbz)) { $this->terminateWithError($this->p->t('global', 'wrongParameters'), 'general'); } $this->load->library('PermissionLib'); $entitledStgs = $this->permissionlib->getSTG_isEntitledFor('lehre/benotungstool_assistenz'); $isAdmin = $this->permissionlib->isBerechtigt('admin'); if (!$isAdmin && (!is_array($entitledStgs) || !in_array($studiengang_kz, $entitledStgs))) { $this->terminateWithError($this->p->t('ui', 'keineBerechtigung'), 'general'); } $result = $this->LehrveranstaltungModel->getLvForStudiengangInSemester($sem_kurzbz, $studiengang_kz); $data = $this->getDataOrTerminateWithError($result); $this->terminateWithSuccess($data); } public function getNoteByPunkte() { $result = $this->getPostJSON(); if(!property_exists($result, 'punkte') || !property_exists($result, 'lv_id') || !property_exists($result, 'sem_kurzbz')) { $this->terminateWithError($this->p->t('global', 'missingParameters'), 'general'); } $punkte = $result->punkte; $lv_id = $result->lv_id; $sem_kurzbz = $result->sem_kurzbz; // the grading scale belongs to the course, therefore the same scope as every other endpoint $this->assertLvAccess($lv_id, $sem_kurzbz); $result = $this->NotenschluesselaufteilungModel->getNote($punkte, $lv_id, $sem_kurzbz); $data = $this->getDataOrTerminateWithError($result); $this->terminateWithSuccess($data); } /** * GET METHOD * returns List of all available & active NotenOptions */ public function getNoten() { $this->load->model('education/Note_model', 'NoteModel'); // the controller holds the configuration; the model only builds the order $result = $this->NoteModel->getAllActive($this->config->item('NOTEN_SORTIERUNG')); $noten = $this->getDataOrTerminateWithError($result); $this->terminateWithSuccess($noten); } /** * GET METHOD * should return Notenvorschlag for single Students, not used anywhere but required as per * https://openproject.technikum-wien.at/projects/fh-complete/work_packages/60873/activity */ public function getNotenvorschlagStudent() { $uid = $this->input->get("uid",TRUE); // if uid is missing or empty, fall back to getAuthUID() if ($uid === NULL || trim((string)$uid) === '') { $uid = getAuthUID(); } $sem_kurzbz = $this->input->get("sem_kurzbz",TRUE); $lv_id = $this->input->get("lv_id",TRUE); if ($uid === NULL || trim((string)$uid) === '' || $sem_kurzbz === NULL || trim((string)$sem_kurzbz) === '' || $lv_id === NULL || trim((string)$lv_id) === '') { $this->terminateWithError($this->p->t('global', 'missingParameters'), 'general'); } $this->assertLvAccess($lv_id, $sem_kurzbz); $result = $this->LvgesamtnoteModel->getLvGesamtNoteVorschlag($lv_id, $uid, $sem_kurzbz); $data = $this->getDataOrTerminateWithError($result); // TODO: moodle teilnote but it seems they only work for a whole course? // get anw% of student by prestudent_id // $anwresult = $this->getAnwesenheiten($prestudent_ids, $lv_id, $sem_kurzbz); $this->terminateWithSuccess($data); } /** * GET 'lv_id', 'sem_kurzbz'. Students of the course with their grades, the Teilnoten from * getExternalGrades, the averaged Notenvorschlag and every exam of the semester. */ public function getStudentenNoten() { $lv_id = $this->input->get("lv_id",TRUE); $sem_kurzbz = $this->input->get("sem_kurzbz",TRUE); if (!isset($lv_id) || isEmptyString($lv_id) || !isset($sem_kurzbz) || isEmptyString($sem_kurzbz)) $this->terminateWithError($this->p->t('global', 'wrongParameters'), 'general'); $this->assertLvAccess($lv_id, $sem_kurzbz); // get studenten for lva & sem with zeugnisnote if available $studenten = $this->LehrveranstaltungModel->getStudentsByLv($sem_kurzbz, $lv_id); $studentenData = $this->getDataOrTerminateWithError($studenten); if(count($studentenData) == 0) { $this->terminateWithError($this->p->t('benotungstool', 'c4keineStudentenGefunden')); } $func = function ($value) { return $value->uid; }; $grades = array(); $student_uids = array_map($func, $studentenData); $funcpre = function ($value) { return $value->prestudent_id; }; $prestudent_ids = array_map($funcpre, $studentenData); if(count($student_uids) > 0) { $mobres = $this->MobilitaetModel->getMobilityZusatzForUids($student_uids); $mobData = $this->getDataOrTerminateWithError($mobres); $result = $this->ErhalterModel->load(); $erhalter = getData($result)[0]; $erhalter_kz = '9' . sprintf("%03s", $erhalter->erhalter_kz); foreach($mobData as $mob) { $grades[$mob->uid]['mobility_zusatz'] = $this->MobilitaetModel->formatZusatz($mob, $erhalter_kz); } } // All course grades of the course in one query, without the release filter: getLvGesamtNoten() // gives released grades only. The history below needs the same rows. $lvNotenRows = array(); $resLvNoten = $this->LvgesamtnoteModel->getByLvStudiensemester($lv_id, $sem_kurzbz); foreach((hasData($resLvNoten) ? getData($resLvNoten) : array()) as $row) $lvNotenRows[$row->student_uid] = $row; foreach($student_uids as $uid) { $grades[$uid]['grades'] = []; $lvgesamtnote = $lvNotenRows[$uid] ?? null; if($lvgesamtnote !== null) { $grades[$uid]['note_lv'] = $lvgesamtnote->note; $grades[$uid]['freigabedatum'] = $lvgesamtnote->freigabedatum; $grades[$uid]['benotungsdatum'] = $lvgesamtnote->benotungsdatum; $grades[$uid]['punkte_lv'] = $lvgesamtnote->punkte; } else { $grades[$uid]['note_lv'] = null; $grades[$uid]['freigabedatum'] = null; $grades[$uid]['benotungsdatum'] = null; $grades[$uid]['punkte_lv'] = null; } } // send $grades reference to moodle addon try { Events::trigger( 'getExternalGrades', function & () use (&$grades) { return $grades; }, [ 'lvid' => $lv_id, 'stsem' => $sem_kurzbz ] ); } catch (Throwable $t) { // $this->addMeta('throwable', $t->getTrace()); $this->addMeta('getExternalGradesError', $t->getMessage()); } // assign the anw% to the students in the studentData loop $anwresult = $this->getAnwesenheiten($prestudent_ids, $lv_id, $sem_kurzbz); // calculate the grade proposals from the partial grades $notenJePunkte = array(); foreach($studentenData as $student) { // null when the Anwesenheiten addon is absent - the column stays empty in the UI $student->anwquote = $anwresult[$student->prestudent_id] ?? null; $g = $grades[$student->uid]['grades']; $note_lv = $grades[$student->uid]['note_lv']; // overwrite any calculation with lv note once available if(!is_null($note_lv)) { $student->note_vorschlag = $note_lv; } else if(count($g) > 0) { $notensumme = 0; $notensumme_gewichtet = 0; $gewichtsumme = 0; $punktesumme = 0; $punktesumme_gewichtet = 0; $anzahlnoten = 0; foreach($g as $teilnote) { $note = $teilnote['grade'] ?? null; $punkte = $teilnote['points'] ?? null; $gewicht = is_numeric($teilnote['weight'] ?? null) ? $teilnote['weight'] : 0; $hatNote = is_numeric($note); $hatPunkte = is_numeric($punkte); // A partial entry counts only for the value that the mode uses. A row without a // grade adds zero in the grade mode and still raises the divisor, which makes the // average better than the performance. if(!(CIS_GESAMTNOTE_PUNKTE ? $hatPunkte : $hatNote)) continue; if($hatNote) { $notensumme += $note; $notensumme_gewichtet += $note * $gewicht; } if($hatPunkte) { $punktesumme += $punkte; $punktesumme_gewichtet += $punkte * $gewicht; } $gewichtsumme += $gewicht; $anzahlnoten += 1; } // Without a partial grade that counts there is no average. Without this guard the // division uses zero. PHP 7 gives INF, and INF gives the best grade. PHP 8 stops with // a fatal error and the full grade table stays empty. $gewichtet = defined('CIS_GESAMTNOTE_GEWICHTUNG') && CIS_GESAMTNOTE_GEWICHTUNG; $divisor = $gewichtet ? $gewichtsumme : $anzahlnoten; if ($divisor > 0) { if (CIS_GESAMTNOTE_PUNKTE) { $punkte_vorschlag = round(($gewichtet ? $punktesumme_gewichtet : $punktesumme) / $divisor, $this->punkteNachkommastellen()); // equal points give an equal grade, so ask the grading scale once for each value if(!array_key_exists((string) $punkte_vorschlag, $notenJePunkte)) { $notenJePunkte[(string) $punkte_vorschlag] = $this->getDataOrTerminateWithError( $this->NotenschluesselaufteilungModel->getNote($punkte_vorschlag, $lv_id, $sem_kurzbz) ); } $note_vorschlag = $notenJePunkte[(string) $punkte_vorschlag]; } else { $note_vorschlag = $this->rundeNote(($gewichtet ? $notensumme_gewichtet : $notensumme) / $divisor); } $student->note_vorschlag = $note_vorschlag; } } } // get all exams with grades of that semester and that course $pruefungen = $this->LePruefungModel->getPruefungenByLvStudiensemester($lv_id, $sem_kurzbz); $pruefungenData = getData($pruefungen); // the server derives the history for each student; the client only reads it $proStudent = []; foreach($pruefungenData ?: [] as $p) { $proStudent[$p->student_uid][] = $p; } // the transcript grades come with the student list (tbl_zeugnisnote.note) $zeugnisnoten = []; foreach($studentenData as $s) $zeugnisnoten[$s->uid] = $s->note; $pruefungenAbgeleitet = []; foreach(array_unique(array_merge($student_uids, array_keys($proStudent))) as $uid) { $lvNote = isset($grades[$uid]) ? ($grades[$uid]['note_lv'] ?? null) : null; $verlauf = $this->VerlaufLib->buildVerlauf($proStudent[$uid] ?? [], $lvNote, $zeugnisnoten[$uid] ?? null); foreach($verlauf->pruefungen as $p) $pruefungenAbgeleitet[] = $p; if(isset($grades[$uid])) { // the row comes from the loop above; a second read gives the same answer $grades[$uid]['verlauf'] = $this->verlaufSummary( $verlauf, false, isset($lvNotenRows[$uid]) && $lvNotenRows[$uid] !== null ); } } $this->terminateWithSuccess(array($studentenData, $pruefungenAbgeleitet, DOMAIN, $grades, $anwresult)); } /** * POST 'sem_kurzbz', 'lv_id', 'student_uid', 'note'. Writes the LV-Note and the benotungsdatum, * which drives the offen/changed/freigegeben state. */ public function saveNotenvorschlag() { // role first: a caller without the action fails on the right, not on missing parameters $this->assertAktion('vorschlag'); $result = $this->getPostJSON(); if(!property_exists($result, 'lv_id') || !property_exists($result, 'sem_kurzbz') || !property_exists($result, 'student_uid') || !property_exists($result, 'note')) { $this->terminateWithError($this->p->t('global', 'missingParameters'), 'general'); } $lv_id = $result->lv_id; $student_uid = $result->student_uid; $sem_kurzbz = $result->sem_kurzbz; $note = $result->note; $punkte = $result->punkte; // the day the assessment took place. The dialog sends it, older callers do not. $datum = property_exists($result, 'datum') ? $result->datum : null; $this->assertLvAccess($lv_id, $sem_kurzbz); // examination rules: no entry and no change after the grade entry deadline $this->enforceNoteneintragungsfrist($sem_kurzbz); // only a participant of the course gets a grade if(!$this->lehreinheitenFuerStudent($lv_id, $student_uid, $sem_kurzbz)) { $this->terminateWithError($this->p->t('benotungstool', 'c4pruefungNichtGespeichert', [$student_uid]), 'general'); } // In the points mode the grading scale decides, not the grade from the client. If not, you // get a course grade that contradicts its own points. Without points there is nothing to // derive (for example a proposal from Moodle partial grades), then the given grade applies. if(CIS_GESAMTNOTE_PUNKTE && $punkte !== null && $punkte !== '') { $abgeleitet = $this->noteAusPunkten($punkte, $lv_id, $sem_kurzbz, $student_uid); if(is_string($abgeleitet)) $this->terminateWithError($abgeleitet, 'general'); $note = $abgeleitet; } $fehler = $this->validateNotenvorschlag($lv_id, $student_uid, $sem_kurzbz, $note); if($fehler !== null) $this->terminateWithError($fehler, 'general'); $fehler = $this->validateBenotungsdatum($datum, $student_uid, $sem_kurzbz); if($fehler !== null) $this->terminateWithError($fehler, 'general'); // Der gewählte Tag ist das Datum von Antritt 1, nicht das benotungsdatum. Das benotungsdatum // bleibt der Zeitpunkt der Eingabe, weil die Freigabe es mit dem freigabedatum vergleicht: // ein Tag in der Vergangenheit liesse die geänderte Note als freigegeben erscheinen. $erstantrittDatum = $datum === null || $datum === '' ? date("Y-m-d") : substr((string) $datum, 0, 10); $lvgesamtnote = null; // the course grade and attempt 1 are one change $this->sperreStudent($student_uid, $lv_id, $sem_kurzbz); $result = $this->LvgesamtnoteModel->getLvGesamtNoteVorschlag($lv_id, $student_uid, $sem_kurzbz); // $this->addMeta('LvgesamtnoteModelresult', $result); if(!isError($result) && hasData($result)) { $lvgesamtnote = getData($result)[0]; $id = $this->LvgesamtnoteModel->update( [$lvgesamtnote->student_uid, $lvgesamtnote->studiensemester_kurzbz, $lvgesamtnote->lehrveranstaltung_id], array( 'note' => $note, 'punkte' => $punkte, 'benotungsdatum' => date("Y-m-d H:i:s"), 'updateamum' => date("Y-m-d H:i:s"), 'updatevon' => getAuthUID() ) ); $res = null; if($id) { $res = $this->LvgesamtnoteModel->load($id->retval); if(hasData($res)) $lvgesamtnote = getData($res)[0]; } $this->logLib->logInfoDB(array('saveNotenvorschlag update lv gesamtnote',$res, getAuthUID(), getAuthPersonId())); } else if(!isError($result) && !hasData($result)) { $id = $this->LvgesamtnoteModel->insert( array( 'student_uid' => $student_uid, 'lehrveranstaltung_id' => $lv_id, 'studiensemester_kurzbz' => $sem_kurzbz, 'note' => $note, 'punkte' => $punkte, 'mitarbeiter_uid' => $this->benotenderMitarbeiterFuerStudent($lv_id, $student_uid, $sem_kurzbz), 'benotungsdatum' => date("Y-m-d H:i:s"), 'freigabedatum' => null, 'freigabevon_uid' => null, 'bemerkung' => null, 'updateamum' => null, 'updatevon' => null, 'insertamum' => date("Y-m-d H:i:s"), 'insertvon' => getAuthUID() ) ); $res = null; if($id) { $res = $this->LvgesamtnoteModel->load($id->retval); if(hasData($res)) $lvgesamtnote = getData($res)[0]; } $this->logLib->logInfoDB(array('saveNotenvorschlag insert lv gesamtnote',$res, getAuthUID(), getAuthPersonId())); } // Ohne geschriebene LV-Note entsteht kein Antritt: eine Prüfung ohne Note ist ein Zustand, // den jeder andere Pfad ablehnt (c4keineLvNoteEingetragen). if($lvgesamtnote === null) { $this->db->trans_rollback(); $this->terminateWithError($this->p->t('benotungstool', 'c4pruefungNichtGespeichert', [$student_uid]), 'general'); } // The course grade IS the first attempt. Write it as its own exam now, or the next exam // becomes attempt 2 and the legacy type of the whole chain moves one place. $this->erstantrittBeiUebernahme($lv_id, $student_uid, $sem_kurzbz, $note, $punkte, $erstantrittDatum); if(!$this->entsperreStudent()) { $this->terminateWithError($this->p->t('benotungstool', 'c4pruefungNichtGespeichert', [$student_uid]), 'general'); } // the client shows the new attempt at once, without a reload. The row is the one just written. $lvgesamtnote->verlauf = $this->buildVerlaufSummary($student_uid, $lv_id, $sem_kurzbz, $lvgesamtnote); $this->terminateWithSuccess(array($lvgesamtnote)); } /** * POST 'sem_kurzbz', 'lv_id', 'noten'. Bulk saveNotenvorschlag for the CSV import; the answer * is keyed by uid and holds the course grade or an error per row. */ public function saveNotenvorschlagBulk() { // role first: a caller without the action fails on the right, not on missing parameters $this->assertAktion('import'); $result = $this->getPostJSON(); if(!property_exists($result, 'lv_id') || !property_exists($result, 'sem_kurzbz') || !property_exists($result, 'noten')) { $this->terminateWithError($this->p->t('global', 'missingParameters'), 'general'); } $lv_id = $result->lv_id; $sem_kurzbz = $result->sem_kurzbz; $noten = $result->noten; $this->assertLvAccess($lv_id, $sem_kurzbz); // examination rules: no entry and no change after the grade entry deadline $this->enforceNoteneintragungsfrist($sem_kurzbz); $retLvNoten = []; foreach($noten as $note) { // je Zeile neu: sonst trägt die Variable die Zeile davor, und eine gescheiterte Zeile // meldet die Note der vorherigen Person zurück $lvgesamtnote = null; // only a participant of the course gets a grade if(!$this->lehreinheitenFuerStudent($lv_id, $note->uid, $sem_kurzbz)) { $retLvNoten[$note->uid] = $this->p->t('benotungstool', 'c4pruefungNichtGespeichert', [$note->uid]); if($this->importBrichtAb()) break; continue; } if(CIS_GESAMTNOTE_PUNKTE) { $abgeleitet = $this->noteAusPunkten($note->punkte, $lv_id, $sem_kurzbz, $note->uid); // no grade can be derived: skip the row, but do not stop the full request if(is_string($abgeleitet)) { $retLvNoten[$note->uid] = $abgeleitet; if($this->importBrichtAb()) break; continue; } $note->note = $abgeleitet; } // one bad row must not stop the import, so the message goes into this row $fehler = $this->validateNotenvorschlag($lv_id, $note->uid, $sem_kurzbz, $note->note); if($fehler !== null) { $retLvNoten[$note->uid] = $fehler; // stop here, or keep writing the remaining rows if($this->importBrichtAb()) break; continue; } // the course grade and attempt 1 are one change $this->sperreStudent($note->uid, $lv_id, $sem_kurzbz); $result = $this->LvgesamtnoteModel->getLvGesamtNoteVorschlag($lv_id, $note->uid, $sem_kurzbz); if(!isError($result) && hasData($result)) { $lvgesamtnote = getData($result)[0]; $id = $this->LvgesamtnoteModel->update( [$lvgesamtnote->student_uid, $lvgesamtnote->studiensemester_kurzbz, $lvgesamtnote->lehrveranstaltung_id], array( 'note' => trim($note->note), 'punkte' => $note->punkte, 'benotungsdatum' => date("Y-m-d H:i:s"), 'updateamum' => date("Y-m-d H:i:s"), 'updatevon' => getAuthUID() ) ); $res = null; if($id) { $res = $this->LvgesamtnoteModel->load($id->retval); if(hasData($res)) $lvgesamtnote = getData($res)[0]; } $this->logLib->logInfoDB(array('saveNotenvorschlagBulk update lv gesamtnote',$res, getAuthUID(), getAuthPersonId())); } else if(!isError($result) && !hasData($result)) { $id = $this->LvgesamtnoteModel->insert( array( 'student_uid' => $note->uid, 'lehrveranstaltung_id' => $lv_id, 'studiensemester_kurzbz' => $sem_kurzbz, 'note' => trim($note->note), 'punkte' => $note->punkte, 'mitarbeiter_uid' => $this->benotenderMitarbeiterFuerStudent($lv_id, $note->uid, $sem_kurzbz), 'benotungsdatum' => date("Y-m-d H:i:s"), 'freigabedatum' => null, 'freigabevon_uid' => null, 'bemerkung' => null, 'updateamum' => null, 'updatevon' => null, 'insertamum' => date("Y-m-d H:i:s"), 'insertvon' => getAuthUID() ) ); $res = null; if($id) { $res = $this->LvgesamtnoteModel->load($id->retval); if(hasData($res)) $lvgesamtnote = getData($res)[0]; } $this->logLib->logInfoDB(array('saveNotenvorschlagBulk insert lv gesamtnote',$res, getAuthUID(), getAuthPersonId())); } // Ohne geschriebene LV-Note entsteht kein Antritt, und die Zeile meldet den Fehler if($lvgesamtnote === null) { $this->db->trans_rollback(); $retLvNoten[$note->uid] = $this->p->t('benotungstool', 'c4pruefungNichtGespeichert', [$note->uid]); continue; } // the same rule as the single dialog: the course grade is attempt 1 $this->erstantrittBeiUebernahme($lv_id, $note->uid, $sem_kurzbz, trim($note->note), $note->punkte, date("Y-m-d")); if(!$this->entsperreStudent()) { $retLvNoten[$note->uid] = $this->p->t('benotungstool', 'c4pruefungNichtGespeichert', [$note->uid]); continue; } $lvgesamtnote->verlauf = $this->buildVerlaufSummary($note->uid, $lv_id, $sem_kurzbz, $lvgesamtnote); $retLvNoten[$note->uid] = $lvgesamtnote; } $this->terminateWithSuccess($retLvNoten); } /** * POST METHOD * expects 'lv_id', 'sem_kurzbz', 'pruefungen' * Bulk variant of saveStudentPruefung, used when importing pruefungsdata from csv with available noten. */ public function savePruefungenBulk() { // role first: a caller without the action fails on the right, not on missing parameters $this->assertAktion('import'); $result = $this->getPostJSON(); if(!property_exists($result, 'lv_id') || !property_exists($result, 'sem_kurzbz') || !property_exists($result, 'pruefungen')) { $this->terminateWithError($this->p->t('global', 'missingParameters'), 'general'); } $lv_id = $result->lv_id; $sem_kurzbz = $result->sem_kurzbz; $pruefungen = $result->pruefungen; $this->assertLvAccess($lv_id, $sem_kurzbz); // examination rules: no entry after the grade entry deadline $this->enforceNoteneintragungsfrist($sem_kurzbz); $ret = []; foreach ($pruefungen as $pruefung) { if(CIS_GESAMTNOTE_PUNKTE) { $note = $this->noteAusPunkten($pruefung->punkte, $lv_id, $sem_kurzbz, $pruefung->uid); // no grade can be derived: skip the row, but do not stop the full request if(is_string($note)) { $ret[$pruefung->uid] = $note; if($this->importBrichtAb()) break; continue; } $pruefung->note = $note; } // the same as the dialog in the table, but for each import row $ret[$pruefung->uid] = $this->savePruefungFuerStudent( null, $pruefung->uid, $lv_id, $sem_kurzbz, $pruefung->lehreinheit_id, $pruefung->note, $pruefung->punkte, $pruefung->datum, property_exists($pruefung, 'mitarbeiter_uid') ? $pruefung->mitarbeiter_uid : null ); } $this->logLib->logInfoDB(array('savePruefungenBulk',$ret, getAuthUID(), getAuthPersonId())); $this->terminateWithSuccess($ret); } /** * POST 'lv_id', 'sem_kurzbz', 'password', 'noten'. Releases the grades: sets freigabedatum, * which drives the offen/changed/freigegeben state, and mails a confirmation table. */ public function saveStudentenNoten() { // role first: a caller without the action fails on the right, not on missing parameters $this->assertAktion('freigabe'); $result = $this->getPostJSON(); if(!property_exists($result, 'sem_kurzbz') || !property_exists($result, 'lv_id') || !property_exists($result, 'password') || !property_exists($result, 'noten')) { $this->terminateWithError($this->p->t('global', 'missingParameters'), 'general'); } // second factor for a binding grade; an installation may drop it if($this->config->item('CIS_GESAMTNOTE_FREIGABE_PASSWORT') !== false && !$this->AuthLib->checkUserAuthByUsernamePassword(getAuthUID(), $result->password)->retval) { $this->terminateWithError($this->p->t('password', 'wrongPassword'), 'general'); } $lv_id = $result->lv_id; $sem_kurzbz = $result->sem_kurzbz; $this->assertLvAccess($lv_id, $sem_kurzbz); $ret = []; $res = $this->LehrveranstaltungModel->load($lv_id); if(isError($res) || !hasData($res)) { $this->terminateWithError($this->p->t('benotungstool', 'noValidLvFoundForId', [$lv_id])); } $lv = getData($res)[0]; $studiengang_kz = $lv->studiengang_kz; $res = $this->StudiengangModel->load($studiengang_kz); if(isError($res) || !hasData($res)) { $this->terminateWithError($this->p->t('benotungstool', 'noValidStudiengangFoundForId', [$studiengang_kz])); } $sg = getData($res)[0]; $lvaFullName = $sg->kurzbzlang . ' ' . $lv->semester . '.Semester ' . $lv->bezeichnung . " - " .$lv->lehrform_kurzbz. " " . $lv->orgform_kurzbz . " - " . $sem_kurzbz; $emails = explode(', ', $sg->email); $res = $this->PersonModel->load(getAuthPersonId()); if(isError($res) || !hasData($res)) { $this->terminateWithError($this->p->t('benotungstool', 'noValidPersonFoundForId', [getAuthPersonId()])); } $pers = getData($res)[0]; $lektorFullName = $pers->anrede.' '.$pers->vorname.' '.$pers->nachname; //.' ('.$pers->kurzbz.')'; $res = $this->StudienplanModel->getStudienplanByLvaSemKurzbz($lv_id, $sem_kurzbz); $data = getData($res); $studienplan_bezeichnung = ''; foreach ($data as $row) { $studienplan_bezeichnung .= $row->bezeichnung . ' '; } $betreff = $this->p->t('benotungstool','notenfreigabe').' ' . $lv->bezeichnung . ' ' . $lv->orgform_kurzbz . ' - ' . $studienplan_bezeichnung; // The mail names the person and the released grade. Both come from the database: a value from // the request could carry markup, and it could name a grade that was never released. $studenten = array(); $resStud = $this->LehrveranstaltungModel->getStudentsByLv($sem_kurzbz, $lv_id); if(!isError($resStud) && hasData($resStud)) { foreach(getData($resStud) as $s) $studenten[$s->uid] = $s; } $notenBezeichnungen = $this->aktiveNoten(); $studlist = "
| " . $this->p->t('person','personenkennzeichen') . " | \n" . $this->p->t('lehre','studiengang') . " | \n" . $this->p->t('benotungstool','c4nachname') . " | \n" . $this->p->t('benotungstool','c4vorname') . " | \n"; if(defined('CIS_GESAMTNOTE_PUNKTE') && CIS_GESAMTNOTE_PUNKTE) { $studlist .= "" . $this->p->t('benotungstool','c4punkte') . " | \n"; } $studlist .= "" . $this->p->t('benotungstool','c4grade') . " | \n"; $studlist .= "" . $this->p->t('ui','bearbeitetVon') . " | " . $this->p->t('person','uid') . " | \n"; } foreach($result->noten as $note) { // read, release and attempt 1 are one change $this->sperreStudent($note->uid, $lv_id, $sem_kurzbz); $resultLVGes = $this->LvgesamtnoteModel->getLvGesamtNoteVorschlag($lv_id, $note->uid, $sem_kurzbz); if (!isError($resultLVGes) && hasData($resultLVGes)) { $lvgesamtnote = getData($resultLVGes)[0]; // only what changed since the last release; same as the old tool if ($lvgesamtnote->benotungsdatum > $lvgesamtnote->freigabedatum) { $id = $this->LvgesamtnoteModel->update( [$lvgesamtnote->student_uid, $lvgesamtnote->studiensemester_kurzbz, $lvgesamtnote->lehrveranstaltung_id], array( 'note' => $lvgesamtnote->note, 'freigabevon_uid' => getAuthUID(), 'freigabedatum' => date("Y-m-d H:i:s"), 'updateamum' => date("Y-m-d H:i:s"), 'updatevon' => getAuthUID() ) ); if($id) { $res = $this->LvgesamtnoteModel->load($id->retval); if(hasData($res)) { $lvgesamtnote = getData($res)[0]; // The release makes the grade binding, therefore the first exam starts // here. A new exam never creates a second exam in addition. $this->upsertErstantritt( $lv_id, $lvgesamtnote->student_uid, $sem_kurzbz, $lvgesamtnote->note, $lvgesamtnote->punkte, $lvgesamtnote->benotungsdatum ); // The verlauf goes back with the answer, so the table shows the new exam // at once. Without it the row updates only after a reload. $ret[] = array( 'uid' => $note->uid, 'freigabedatum' => $lvgesamtnote->freigabedatum, 'benotungsdatum' => $lvgesamtnote->benotungsdatum, // the row is the one the release just wrote 'verlauf' => $this->buildVerlaufSummary($note->uid, $lv_id, $sem_kurzbz, $lvgesamtnote) ); } } if (defined('CIS_GESAMTNOTE_FREIGABEMAIL_NOTE') && CIS_GESAMTNOTE_FREIGABEMAIL_NOTE) { $stud = isset($studenten[$note->uid]) ? $studenten[$note->uid] : null; $noteKey = (string) $lvgesamtnote->note; $noteBez = isset($notenBezeichnungen[$noteKey]) ? $notenBezeichnungen[$noteKey]->bezeichnung : $noteKey; $studlist .= "
| " . $this->mailZelle($stud ? $stud->matrikelnr : $note->uid) . " | "; $studlist .= "" . $this->mailZelle($stud ? $stud->kuerzel : '') . " | "; $studlist .= "" . $this->mailZelle($stud ? $stud->nachname : '') . " | "; $studlist .= "" . $this->mailZelle($stud ? $stud->vorname : '') . " | "; if(defined('CIS_GESAMTNOTE_PUNKTE') && CIS_GESAMTNOTE_PUNKTE) { $studlist .= "" . $this->mailZelle($lvgesamtnote->punkte) . " | "; } $studlist .= "" . $this->mailZelle($noteBez) . " | "; $studlist .= "" . $this->mailZelle($lvgesamtnote->mitarbeiter_uid); if ($lvgesamtnote->updatevon != '') $studlist .= " (" . $this->mailZelle($lvgesamtnote->updatevon) . ")"; $studlist .= " |
| " . $this->mailZelle($note->uid) . " |