mirror of
https://github.com/FH-Complete/FHC-Core.git
synced 2026-07-23 18:02:18 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 32de4aac6b |
@@ -14,4 +14,14 @@ $config['NOTE_ENTSCHULDIGT'] = 17;
|
||||
// availability of the two Benotungstool import flows. When both are true they are shown as
|
||||
// separate buttons/dialogs.
|
||||
$config['CIS_GESAMTNOTE_PRUEFUNGSIMPORT'] = true; // dated import that creates a pruefung per row
|
||||
$config['CIS_GESAMTNOTE_NOTENIMPORT'] = false; // classic note-only import (uid + note, no date)
|
||||
$config['CIS_GESAMTNOTE_NOTENIMPORT'] = false; // classic note-only import (uid + note, no date)
|
||||
|
||||
$config['SHOW_BENOTUNGSDATUM_ON_NOTENVORSCHLAG_UEBERNAHME'] = true;
|
||||
|
||||
// Noteneintragungsfrist (Prüfungsordnung §1): grade/pruefung entry is only permitted up to this
|
||||
// deadline. The month/day below is applied to the studiensemester's year:
|
||||
// Sommersemester (SSyyyy) -> deadline in the SAME calendar year (default 15th November)
|
||||
// Wintersemester (WSyyyy) -> deadline in the FOLLOWING calendar year (default 15th May)
|
||||
$config['CIS_GESAMTNOTE_NOTENEINTRAGUNGSFRIST'] = false; // switch to use the window enforcement
|
||||
$config['NOTENEINTRAGUNGSFRIST_SS'] = ['month' => 11, 'day' => 15]; // Sommersemester deadline (same year)
|
||||
$config['NOTENEINTRAGUNGSFRIST_WS'] = ['month' => 5, 'day' => 15]; // Wintersemester deadline (following year)
|
||||
@@ -13,7 +13,7 @@ class Benotungstool extends Auth_Controller
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct([
|
||||
'index' => self::PERM_LOGGED
|
||||
'index' => array('lehre/benotungstool:rw', 'lehre/benotungstool_assistenz:rw')
|
||||
]);
|
||||
|
||||
$this->_ci =& get_instance();
|
||||
|
||||
@@ -28,18 +28,21 @@ class Noten extends FHCAPI_Controller
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$permissions = array('lehre/benotungstool:rw', 'lehre/benotungstool_assistenz:rw');
|
||||
parent::__construct([
|
||||
'getStudentenNoten' => array('lehre/benotungstool:rw'),
|
||||
'getNoten' => array('lehre/benotungstool:rw'),
|
||||
'saveStudentenNoten' => array('lehre/benotungstool:rw'),
|
||||
'getNotenvorschlagStudent' => array('lehre/benotungstool:rw'),
|
||||
'saveNotenvorschlag' => array('lehre/benotungstool:rw'),
|
||||
'saveStudentPruefung' => array('lehre/benotungstool:rw'),
|
||||
'createPruefungen' => array('lehre/benotungstool:rw'),
|
||||
'saveNotenvorschlagBulk' => array('lehre/benotungstool:rw'),
|
||||
'savePruefungenBulk' => array('lehre/benotungstool:rw'),
|
||||
'getCisConfig' => array('lehre/benotungstool:rw'),
|
||||
'getNoteByPunkte' => array('lehre/benotungstool:rw')
|
||||
'getStudentenNoten' => $permissions,
|
||||
'getNoten' => $permissions,
|
||||
'saveStudentenNoten' => $permissions,
|
||||
'getNotenvorschlagStudent' => $permissions,
|
||||
'saveNotenvorschlag' => $permissions,
|
||||
'saveStudentPruefung' => $permissions,
|
||||
'createPruefungen' => $permissions,
|
||||
'saveNotenvorschlagBulk' => $permissions,
|
||||
'savePruefungenBulk' => $permissions,
|
||||
'getCisConfig' => $permissions,
|
||||
'getNoteByPunkte' => $permissions,
|
||||
'getBenotungstoolContext' => $permissions,
|
||||
'getLvForStudiengang' => $permissions
|
||||
]);
|
||||
|
||||
$this->load->library('AuthLib', null, 'AuthLib');
|
||||
@@ -72,6 +75,7 @@ class Noten extends FHCAPI_Controller
|
||||
$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');
|
||||
@@ -84,9 +88,11 @@ class Noten extends FHCAPI_Controller
|
||||
}
|
||||
|
||||
public function getCisConfig() {
|
||||
$NOTEN_OHNE_ANTRITT = $this->config->item('NOTEN_OHNE_ANTRITT');
|
||||
$NOTEN_OCCURANCE_LIMIT_MAP = $this->config->item('NOTEN_OCCURANCE_LIMIT_MAP');
|
||||
$NOTE_ENTSCHULDIGT = $this->config->item('NOTE_ENTSCHULDIGT');
|
||||
// resolved from tbl_note (Bezeichnung) with config fallback -> single source of truth
|
||||
$special = $this->resolveSpecialNotes();
|
||||
$NOTEN_OHNE_ANTRITT = $special['ohneAntritt'];
|
||||
$NOTEN_OCCURANCE_LIMIT_MAP = $special['limitMap'];
|
||||
$NOTE_ENTSCHULDIGT = $special['entschuldigt'];
|
||||
|
||||
$this->terminateWithSuccess(
|
||||
array(
|
||||
@@ -99,12 +105,12 @@ class Noten extends FHCAPI_Controller
|
||||
// only relevant in punkte calculation in backend
|
||||
// 'CIS_GESAMTNOTE_GEWICHTUNG' => CIS_GESAMTNOTE_GEWICHTUNG,
|
||||
|
||||
// this one should always be set true since fh prüfungsordnung requires at least 3 antritte (t1+t2+kP)
|
||||
// send it anyway to use in maxAntritte calculation
|
||||
// enables the first in-tool retake (Termin2).
|
||||
// Used in the maxAntritte calculation.
|
||||
'CIS_GESAMTNOTE_PRUEFUNG_TERMIN2' => CIS_GESAMTNOTE_PRUEFUNG_TERMIN2,
|
||||
|
||||
// should in 99% of cases be kept true to enable 4 antritte in total, but if a certain
|
||||
// fh still works with 3 antritte per note this can limit the max number of antritte accordingly
|
||||
|
||||
// legacy: enables a SECOND in-tool retake (Termin3) for other installations whose ruleset
|
||||
// uses three explicit Nachprüfungs-Termine. Each enabled retake type raises the maxAntritte cap.
|
||||
'CIS_GESAMTNOTE_PRUEFUNG_TERMIN3' => CIS_GESAMTNOTE_PRUEFUNG_TERMIN3,
|
||||
|
||||
// used to toggle availability of kommPruef type pruefungen
|
||||
@@ -133,11 +139,144 @@ class Noten extends FHCAPI_Controller
|
||||
'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
|
||||
'NOTE_ENTSCHULDIGT' => $NOTE_ENTSCHULDIGT,
|
||||
|
||||
// show the Benotungsdatum of the first Antritt in the "Ursprüngliche Zeugnisnote" column
|
||||
'SHOW_BENOTUNGSDATUM_ON_NOTENVORSCHLAG_UEBERNAHME' => $this->config->item('SHOW_BENOTUNGSDATUM_ON_NOTENVORSCHLAG_UEBERNAHME'),
|
||||
|
||||
// Noteneintragungsfrist window (enforced server-side; also surfaced so the UI can hint at it)
|
||||
'CIS_GESAMTNOTE_NOTENEINTRAGUNGSFRIST' => $this->config->item('CIS_GESAMTNOTE_NOTENEINTRAGUNGSFRIST')
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET METHOD
|
||||
* expects (optional) 'sem_kurzbz'
|
||||
* Single role-determining entry point for the tool: the server decides which flow the current user
|
||||
* gets and returns exactly the initial dropdown data for it.
|
||||
* A teacher (holds lehre/benotungstool) gets their assigned
|
||||
* Lehrveranstaltungen directly. Assistenz (only lehre/benotungstool_assistenz) gets the
|
||||
* Studiengänge they are entitled for and picks one before an LV is loaded.
|
||||
*/
|
||||
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 (teachers to their own LVs, Assistenzen to their entitled Studiengänge).
|
||||
$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) {
|
||||
$this->load->model('organisation/Studiengang_model', 'StudiengangModel');
|
||||
$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
|
||||
if (isset($lv_id) && !isEmptyString($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
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* GET METHOD
|
||||
* expects 'studiengang_kz', 'sem_kurzbz'
|
||||
* Returns the Lehrveranstaltungen of a Studiengang in a Studiensemester for the Assistenz flow.
|
||||
* The caller may only load Studiengänge they are entitled for via lehre/benotungstool_assistenz.
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scopes data access to the Lehrveranstaltungen a user may act on, so a shared/guessed URL cannot be
|
||||
* used to read or edit foreign grades. A teacher (lehre/benotungstool) may
|
||||
* only touch LVs they are assigned to teach in the given Studiensemester. Assistenz may
|
||||
* touch LVs of a Studiengang they are entitled for.
|
||||
*/
|
||||
private function assertLvAccess($lv_id, $sem_kurzbz = null)
|
||||
{
|
||||
$this->load->library('PermissionLib');
|
||||
|
||||
// admins keep full access
|
||||
if ($this->permissionlib->isBerechtigt('admin')) return;
|
||||
|
||||
// teachers: only their own LVs (assigned as lehreinheitmitarbeiter in this semester)
|
||||
if ($this->permissionlib->isBerechtigt('lehre/benotungstool')) {
|
||||
$res = $this->LehrveranstaltungModel->getLektorIsTeachingLva($lv_id, getAuthUID(), $sem_kurzbz);
|
||||
$rows = getData($res);
|
||||
if (!isError($res) && !empty($rows) && $rows[0]->teaches > 0) return;
|
||||
// not a teacher of this LV -> fall through (a both-role user may still be entitled as Assistenz)
|
||||
}
|
||||
|
||||
// (pure or additional) Assistenz: only LVs of an entitled Studiengang
|
||||
$entitledStgs = $this->permissionlib->getSTG_isEntitledFor('lehre/benotungstool_assistenz');
|
||||
$lv = null;
|
||||
if (is_array($entitledStgs) && count($entitledStgs) > 0) {
|
||||
$res = $this->LehrveranstaltungModel->load($lv_id);
|
||||
if (!isError($res) && hasData($res)) {
|
||||
$lv = getData($res)[0];
|
||||
if (in_array($lv->studiengang_kz, $entitledStgs)) return;
|
||||
}
|
||||
}
|
||||
|
||||
if ($lv === null) {
|
||||
$res = $this->LehrveranstaltungModel->load($lv_id);
|
||||
if (!isError($res) && hasData($res)) $lv = getData($res)[0];
|
||||
}
|
||||
$bezeichnung = $lv !== null ? $lv->bezeichnung : $lv_id;
|
||||
|
||||
$this->terminateWithError(
|
||||
$this->p->t('benotungstool', 'keineBerechtigungNoten', [$bezeichnung, $sem_kurzbz]),
|
||||
'general'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET METHOD
|
||||
* expects 'lv_id', 'sem_kurzbz'
|
||||
@@ -153,13 +292,15 @@ class Noten extends FHCAPI_Controller
|
||||
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('No students found for lva and semester');
|
||||
$this->terminateWithError($this->p->t('benotungstool', 'c4keineStudentenGefunden'));
|
||||
}
|
||||
|
||||
$func = function ($value) {
|
||||
@@ -325,9 +466,11 @@ class Noten extends FHCAPI_Controller
|
||||
|
||||
$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]));
|
||||
@@ -370,7 +513,7 @@ class Noten extends FHCAPI_Controller
|
||||
<td><b>" . $this->p->t('lehre','studiengang') . "</b></td>\n
|
||||
<td><b>" . $this->p->t('benotungstool','c4nachname') . "</b></td>\n
|
||||
<td><b>" . $this->p->t('benotungstool','c4vorname') . "</b></td>\n";
|
||||
if(defined(CIS_GESAMTNOTE_PUNKTE) && CIS_GESAMTNOTE_PUNKTE) {
|
||||
if(defined('CIS_GESAMTNOTE_PUNKTE') && CIS_GESAMTNOTE_PUNKTE) {
|
||||
$studlist .= "<td><b>" . $this->p->t('benotungstool','c4punkte') . "</b></td>\n";
|
||||
}
|
||||
$studlist .= "<td><b>" . $this->p->t('benotungstool','c4grade') . "</b></td>\n";
|
||||
@@ -382,7 +525,6 @@ class Noten extends FHCAPI_Controller
|
||||
foreach($result->noten as $note) {
|
||||
|
||||
$resultLVGes = $this->LvgesamtnoteModel->getLvGesamtNoteVorschlag($lv_id, $note->uid, $sem_kurzbz);
|
||||
// $this->addMeta($note->uid.'$resultLVGes', $resultLVGes);
|
||||
if (!isError($resultLVGes) && hasData($resultLVGes))
|
||||
{
|
||||
$lvgesamtnote = getData($resultLVGes)[0];
|
||||
@@ -416,7 +558,7 @@ class Noten extends FHCAPI_Controller
|
||||
$studlist .= "<td>" . trim($note->nachname) . "</td>";
|
||||
$studlist .= "<td>" . trim($note->vorname) . "</td>";
|
||||
|
||||
if(defined(CIS_GESAMTNOTE_PUNKTE) && CIS_GESAMTNOTE_PUNKTE) {
|
||||
if(defined('CIS_GESAMTNOTE_PUNKTE') && CIS_GESAMTNOTE_PUNKTE) {
|
||||
$studlist .= "<td>" . trim($lvgesamtnote->punkte) . "</td>";
|
||||
}
|
||||
$studlist .= "<td>" .$note->noteBezeichnung. "</td>";
|
||||
@@ -493,10 +635,9 @@ class Noten extends FHCAPI_Controller
|
||||
|| $lv_id === NULL || trim((string)$lv_id) === '') {
|
||||
$this->terminateWithError($this->p->t('global', 'missingParameters'), 'general');
|
||||
}
|
||||
|
||||
$this->assertLvAccess($lv_id, $sem_kurzbz);
|
||||
|
||||
// TODO: we need a zuordnungscheck here? any lektor can get any grades?
|
||||
// what about assistenz with different rights doing lectors job once again?
|
||||
// students checking their own grades?
|
||||
|
||||
$result = $this->LvgesamtnoteModel->getLvGesamtNoteVorschlag($lv_id, $uid, $sem_kurzbz);
|
||||
$data = $this->getDataOrTerminateWithError($result);
|
||||
@@ -540,6 +681,11 @@ class Noten extends FHCAPI_Controller
|
||||
$stsem = $result->sem_kurzbz;
|
||||
$typ = $result->typ;
|
||||
|
||||
$this->assertLvAccess($lva_id, $stsem);
|
||||
|
||||
// Prüfungsordnung §1: no entry/change past the Noteneintragungsfrist
|
||||
$this->enforceNoteneintragungsfrist($stsem);
|
||||
|
||||
$jetzt = date("Y-m-d H:i:s");
|
||||
|
||||
if(CIS_GESAMTNOTE_PUNKTE && isset($punkte) && $punkte >= 0) {
|
||||
@@ -572,19 +718,28 @@ class Noten extends FHCAPI_Controller
|
||||
$this->terminateWithError($editError, 'general');
|
||||
}
|
||||
|
||||
// for a NEW attempt (no pruefung_id) enforce the add rules server-side
|
||||
// maxAntritte calc, chronological order, occurrence limit for entschuldigt
|
||||
if($pruefung_id === null || $pruefung_id === '') {
|
||||
$addError = $this->validatePruefungAdd($student_uid, $lva_id, $stsem, $note, $datum);
|
||||
if($addError !== null) {
|
||||
$this->terminateWithError($addError, 'general');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
$this->load->model('education/Lehrveranstaltung_model', 'LehrveranstaltungModel');
|
||||
$this->load->model('organisation/Studiengang_model', 'StudiengangModel');
|
||||
|
||||
$res = $this->LehrveranstaltungModel->load($lva_id);
|
||||
if(isError($res) || !hasData($res)) {
|
||||
$this->terminateWithError('Keine gültige Lehrveranstaltung gefunden für ID: '.$lva_id);
|
||||
$this->terminateWithError($this->p->t('benotungstool', 'noValidLvFoundForId', [$lva_id]));
|
||||
}
|
||||
|
||||
|
||||
$studiengang_kz = getData($res)[0]->studiengang_kz;
|
||||
$res = $this->StudiengangModel->load($studiengang_kz);
|
||||
if(isError($res) || !hasData($res)) {
|
||||
$this->terminateWithError('Kein gültiger Studiengang gefunden für ID: '.$studiengang_kz);
|
||||
$this->terminateWithError($this->p->t('benotungstool', 'noValidStudiengangFoundForId', [$studiengang_kz]));
|
||||
}
|
||||
|
||||
|
||||
@@ -670,7 +825,7 @@ class Noten extends FHCAPI_Controller
|
||||
/**
|
||||
* private helper method to update/insert pruefungstermine
|
||||
*/
|
||||
private function savePruefungstermin($typ, $student_uid, $lva_id, $stsem, $lehreinheit_id, $note, $punkte = '', $datum, $origLvNote = null, $origLvPunkte = null, $origBenotungsdatum = null)
|
||||
private function savePruefungstermin($typ, $student_uid, $lva_id, $stsem, $lehreinheit_id, $note, $punkte, $datum, $origLvNote = null, $origLvPunkte = null, $origBenotungsdatum = null)
|
||||
{
|
||||
|
||||
// extra check if the student has lvnote and a zeugnisnote in the relevant lva
|
||||
@@ -697,9 +852,17 @@ class Noten extends FHCAPI_Controller
|
||||
);
|
||||
|
||||
if(count($status) > 0 && $status[0] == true) {
|
||||
$this->load->model('education/Note_model', 'NoteModel');
|
||||
$result = $this->NoteModel->getEntschuldigtNote();
|
||||
$note = getData($result)[0]->note;
|
||||
$entschuldigtNote = $this->resolveSpecialNotes()['entschuldigt'];
|
||||
|
||||
// the occurrence limit was validated on the PRE-override note, so re-check it here: only
|
||||
// apply the auto-excuse if it would not exceed the allowed number of excused Termine
|
||||
if($entschuldigtNote !== null) {
|
||||
$allRes = $this->LePruefungModel->getPruefungenByUidTypLvStudiensemester($student_uid, null, $lva_id, $stsem);
|
||||
$existing = (!isError($allRes) && hasData($allRes)) ? getData($allRes) : [];
|
||||
if(!$this->exceedsNoteOccuranceLimit($existing, $entschuldigtNote, null)) {
|
||||
$note = $entschuldigtNote;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$jetzt = date("Y-m-d H:i:s");
|
||||
@@ -923,7 +1086,7 @@ class Noten extends FHCAPI_Controller
|
||||
*/
|
||||
private function isEntschuldigtNote($note)
|
||||
{
|
||||
$entschuldigt = $this->config->item('NOTE_ENTSCHULDIGT');
|
||||
$entschuldigt = $this->resolveSpecialNotes()['entschuldigt'];
|
||||
return $entschuldigt !== null && $note == $entschuldigt;
|
||||
}
|
||||
|
||||
@@ -998,9 +1161,231 @@ class Noten extends FHCAPI_Controller
|
||||
return $this->p->t('benotungstool', 'pruefungDatumOutOfRange', [$student_uid]);
|
||||
}
|
||||
|
||||
// an edit may also change the note (e.g. to 'entschuldigt') -> keep the occurrence limit
|
||||
if($this->exceedsNoteOccuranceLimit($pruefungen, $newNote, $current->pruefung_id)) {
|
||||
return $this->p->t('benotungstool', 'noteOccuranceLimitReached', [$student_uid]);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates ADDING a new pruefung (attempt). Returns an error string if the add is not
|
||||
* allowed, or null if it is..
|
||||
*
|
||||
* Rules (Prüfungsordnung §1):
|
||||
* - Rule A: the number of counting Prüfungsantritte may not exceed the configured maximum
|
||||
* - Rule B: attempts are taken in chronological order
|
||||
* - Rule C: a note carrying an occurrence limit (entschuldigt) may not exceed it.
|
||||
*
|
||||
* @param string $student_uid
|
||||
* @param int $lva_id
|
||||
* @param string $stsem
|
||||
* @param int $note the (already resolved) note being saved
|
||||
* @param string $datum the datum being saved (Y-m-d)
|
||||
* @return string|null
|
||||
*/
|
||||
private function validatePruefungAdd($student_uid, $lva_id, $stsem, $note, $datum)
|
||||
{
|
||||
$result = $this->LePruefungModel->getPruefungenByUidTypLvStudiensemester($student_uid, null, $lva_id, $stsem);
|
||||
$pruefungen = (!isError($result) && hasData($result)) ? getData($result) : [];
|
||||
|
||||
// current LV note (the implicit first Antritt), read before it gets mutated by the caller
|
||||
$lvNote = null;
|
||||
$resLv = $this->LvgesamtnoteModel->getLvGesamtNoten($lva_id, $student_uid, $stsem);
|
||||
if(!isError($resLv) && hasData($resLv)) $lvNote = getData($resLv)[0]->note;
|
||||
|
||||
// Rule A: max Antritte
|
||||
if($this->computeAntrittCount($pruefungen, $lvNote) >= $this->computeMaxAntritte()) {
|
||||
return $this->p->t('benotungstool', 'maxAntritteReached', [$student_uid, $this->computeMaxAntritte()]);
|
||||
}
|
||||
|
||||
// Rule B: no new attempt on/before an existing dated attempt
|
||||
$newDate = substr((string)$datum, 0, 10);
|
||||
foreach($pruefungen as $p) {
|
||||
$d = substr((string)$p->datum, 0, 10);
|
||||
if($d !== '' && $d >= $newDate) {
|
||||
return $this->p->t('benotungstool', 'pruefungDatumBeforeExisting', [$student_uid]);
|
||||
}
|
||||
}
|
||||
|
||||
// Rule C: occurrence limit (e.g. only one 'entschuldigt' across the fixed Antritte)
|
||||
if($this->exceedsNoteOccuranceLimit($pruefungen, $note, null)) {
|
||||
return $this->p->t('benotungstool', 'noteOccuranceLimitReached', [$student_uid]);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Number of counting Prüfungsantritte for a student, mirroring the frontend getAntrittCountStudent:
|
||||
* a kommPruef caps the count at 4, notes in NOTEN_OHNE_ANTRITT (entschuldigt / noch nicht
|
||||
* eingetragen) do not count, and when no pruefung counts yet the original LV note counts as the
|
||||
* first Antritt (unless it is a non-lehre note such as 'angerechnet').
|
||||
*/
|
||||
private function computeAntrittCount($pruefungen, $lvNote)
|
||||
{
|
||||
$notenOhneAntritt = $this->resolveSpecialNotes()['ohneAntritt'];
|
||||
|
||||
foreach($pruefungen as $p) {
|
||||
if($p->pruefungstyp_kurzbz == 'kommPruef') return 4;
|
||||
}
|
||||
|
||||
$count = 0;
|
||||
foreach($pruefungen as $p) {
|
||||
if(!in_array($p->note, $notenOhneAntritt)) $count++;
|
||||
}
|
||||
|
||||
if($count === 0 && $lvNote !== null && $this->isLehreNote($lvNote)) return 1;
|
||||
|
||||
return $count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Maximum number of counting Prüfungsantritte enterable IN THIS TOOL, mirroring the frontend
|
||||
* maxAntrittCount: the original note (always 1) plus each enabled in-tool retake type (Termin2 and
|
||||
* the legacy Termin3). kommPruef is the terminal attempt, entered elsewhere, and is intentionally
|
||||
* not part of this cap. For our installation (Termin2 on, Termin3 off) this is 2; excused Termine do
|
||||
* not count (see computeAntrittCount), which is what lets Termin2 occur twice within the cap.
|
||||
*/
|
||||
private function computeMaxAntritte()
|
||||
{
|
||||
$max = 1;
|
||||
if(defined('CIS_GESAMTNOTE_PRUEFUNG_TERMIN2') && CIS_GESAMTNOTE_PRUEFUNG_TERMIN2) $max++;
|
||||
if(defined('CIS_GESAMTNOTE_PRUEFUNG_TERMIN3') && CIS_GESAMTNOTE_PRUEFUNG_TERMIN3) $max++;
|
||||
return $max;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the given note is flagged as a 'lehre' note in tbl_note.
|
||||
*/
|
||||
private function isLehreNote($note)
|
||||
{
|
||||
$res = $this->NoteModel->load($note);
|
||||
if(isError($res) || !hasData($res)) return false;
|
||||
return (bool) getData($res)[0]->lehre;
|
||||
}
|
||||
|
||||
/**
|
||||
* Single source of truth for the "special" note PKs (entschuldigt & noch nicht eingetragen).
|
||||
* tbl_note is resolved by Bezeichnung (install-independent) and takes precedence; the hardcoded
|
||||
* config PKs (NOTE_ENTSCHULDIGT / NOTEN_OHNE_ANTRITT) are only a fallback. This removes the second
|
||||
* source of truth so the entschuldigt-preservation and Antritt-counting logic can't disagree with
|
||||
* the Bezeichnung lookups on installs whose PKs differ. Cached per request.
|
||||
*
|
||||
* @return array{entschuldigt: mixed, ohneAntritt: array, limitMap: array}
|
||||
*/
|
||||
private function resolveSpecialNotes()
|
||||
{
|
||||
static $cache = null;
|
||||
if($cache !== null) return $cache;
|
||||
|
||||
$cfgEntschuldigt = $this->config->item('NOTE_ENTSCHULDIGT');
|
||||
$cfgOhneAntritt = $this->config->item('NOTEN_OHNE_ANTRITT');
|
||||
$cfgLimitMap = $this->config->item('NOTEN_OCCURANCE_LIMIT_MAP');
|
||||
if(!is_array($cfgOhneAntritt)) $cfgOhneAntritt = [];
|
||||
if(!is_array($cfgLimitMap)) $cfgLimitMap = [];
|
||||
|
||||
// resolve by Bezeichnung, fall back to the config PKs
|
||||
$resEnt = $this->NoteModel->getEntschuldigtNote();
|
||||
$entschuldigt = (!isError($resEnt) && hasData($resEnt)) ? getData($resEnt)[0]->note : $cfgEntschuldigt;
|
||||
|
||||
$resNn = $this->NoteModel->getNochNichtEingetragenNote();
|
||||
$nochNicht = (!isError($resNn) && hasData($resNn)) ? getData($resNn)[0]->note : ($cfgOhneAntritt[0] ?? null);
|
||||
|
||||
$ohneAntritt = array_values(array_filter([$nochNicht, $entschuldigt], function($v){ return $v !== null; }));
|
||||
|
||||
// re-key the configured entschuldigt limit onto the resolved PK (keep any other entries as-is)
|
||||
$limitMap = [];
|
||||
foreach($cfgLimitMap as $k => $v) {
|
||||
$limitMap[($k == $cfgEntschuldigt) ? $entschuldigt : $k] = $v;
|
||||
}
|
||||
|
||||
$cache = [
|
||||
'entschuldigt' => $entschuldigt,
|
||||
'ohneAntritt' => $ohneAntritt,
|
||||
'limitMap' => $limitMap
|
||||
];
|
||||
return $cache;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for configured occurrence limit (NOTEN_OCCURANCE_LIMIT_MAP) among the given pruefungen.
|
||||
* $excludePruefungId skips the record currently being edited
|
||||
* Noten without a configured limit never exceed.
|
||||
*/
|
||||
private function exceedsNoteOccuranceLimit($pruefungen, $note, $excludePruefungId = null)
|
||||
{
|
||||
$limitMap = $this->resolveSpecialNotes()['limitMap'];
|
||||
if(!is_array($limitMap)) return false;
|
||||
|
||||
$limit = null;
|
||||
foreach($limitMap as $limitNote => $limitVal) {
|
||||
if($limitNote == $note) { $limit = $limitVal; break; }
|
||||
}
|
||||
if($limit === null) return false;
|
||||
|
||||
$count = 0;
|
||||
foreach($pruefungen as $p) {
|
||||
if($excludePruefungId !== null && $p->pruefung_id == $excludePruefungId) continue;
|
||||
if($p->note == $note) $count++;
|
||||
}
|
||||
|
||||
return ($count + 1) > $limit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enforces the Noteneintragungsfrist (Prüfungsordnung §1) for grade/pruefung entry. Terminates the
|
||||
* request with an error when the deadline for the given studiensemester has passed. Does nothing
|
||||
* when the window enforcement is disabled or the deadline cannot be determined.
|
||||
*/
|
||||
private function enforceNoteneintragungsfrist($sem_kurzbz)
|
||||
{
|
||||
if(!$this->config->item('CIS_GESAMTNOTE_NOTENEINTRAGUNGSFRIST')) return;
|
||||
|
||||
$deadline = $this->computeNoteneintragungsfrist($sem_kurzbz);
|
||||
if($deadline === null) return;
|
||||
|
||||
if(new DateTime() > $deadline) {
|
||||
$this->terminateWithError(
|
||||
$this->p->t('benotungstool', 'noteneintragungsfristVorbei', [$deadline->format('d.m.Y')]),
|
||||
'general'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the Noteneintragungsfrist deadline (end of day) for a studiensemester_kurzbz of the
|
||||
* form '{SS|WS}yyyy'. SS -> deadline in the same calendar year, WS -> in the following year. The
|
||||
* month/day come from the noten config. Returns a DateTime, or null when it can't be determined.
|
||||
*/
|
||||
private function computeNoteneintragungsfrist($sem_kurzbz)
|
||||
{
|
||||
if(!is_string($sem_kurzbz) || strlen($sem_kurzbz) < 6) return null;
|
||||
|
||||
$type = strtoupper(substr($sem_kurzbz, 0, 2));
|
||||
$year = (int) substr($sem_kurzbz, 2, 4);
|
||||
if($year <= 0) return null;
|
||||
|
||||
if($type === 'SS') {
|
||||
$cfg = $this->config->item('NOTENEINTRAGUNGSFRIST_SS');
|
||||
$deadlineYear = $year;
|
||||
} elseif($type === 'WS') {
|
||||
$cfg = $this->config->item('NOTENEINTRAGUNGSFRIST_WS');
|
||||
$deadlineYear = $year + 1;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
$month = (is_array($cfg) && isset($cfg['month'])) ? (int)$cfg['month'] : ($type === 'SS' ? 11 : 5);
|
||||
$day = (is_array($cfg) && isset($cfg['day'])) ? (int)$cfg['day'] : 15;
|
||||
|
||||
$deadline = new DateTime();
|
||||
$deadline->setDate($deadlineYear, $month, $day);
|
||||
$deadline->setTime(23, 59, 59);
|
||||
return $deadline;
|
||||
}
|
||||
|
||||
/**
|
||||
* POST METHOD
|
||||
* expects 'sem_kurzbz', 'lv_id', 'student_uid', 'note'
|
||||
@@ -1020,7 +1405,12 @@ class Noten extends FHCAPI_Controller
|
||||
$sem_kurzbz = $result->sem_kurzbz;
|
||||
$note = $result->note;
|
||||
$punkte = $result->punkte;
|
||||
|
||||
|
||||
$this->assertLvAccess($lv_id, $sem_kurzbz);
|
||||
|
||||
// Prüfungsordnung §1: no entry/change past the Noteneintragungsfrist
|
||||
$this->enforceNoteneintragungsfrist($sem_kurzbz);
|
||||
|
||||
$result = $this->LvgesamtnoteModel->getLvGesamtNoteVorschlag($lv_id, $student_uid, $sem_kurzbz);
|
||||
|
||||
// $this->addMeta('LvgesamtnoteModelresult', $result);
|
||||
@@ -1092,7 +1482,12 @@ class Noten extends FHCAPI_Controller
|
||||
$lv_id = $result->lv_id;
|
||||
$sem_kurzbz = $result->sem_kurzbz;
|
||||
$noten = $result->noten;
|
||||
|
||||
|
||||
$this->assertLvAccess($lv_id, $sem_kurzbz);
|
||||
|
||||
// Prüfungsordnung §1: no entry/change past the Noteneintragungsfrist
|
||||
$this->enforceNoteneintragungsfrist($sem_kurzbz);
|
||||
|
||||
$retLvNoten = [];
|
||||
|
||||
foreach($noten as $note)
|
||||
@@ -1177,21 +1572,34 @@ class Noten extends FHCAPI_Controller
|
||||
$uids = $result->uids;
|
||||
$datum = $result->datum;
|
||||
$lva_id = $result->lva_id;
|
||||
|
||||
|
||||
$stsem = $result->sem_kurzbz;
|
||||
|
||||
|
||||
$this->assertLvAccess($lva_id, $stsem);
|
||||
|
||||
// Prüfungsordnung §1: no entry past the Noteneintragungsfrist
|
||||
$this->enforceNoteneintragungsfrist($stsem);
|
||||
|
||||
$ret = [];
|
||||
|
||||
$this->load->model('education/Note_model', 'NoteModel');
|
||||
$result = $this->NoteModel->getNochNichtEingetragenNote();
|
||||
$note = getData($result)[0]->note;
|
||||
|
||||
|
||||
foreach ($uids as $student) {
|
||||
$student_uid = $student->uid;
|
||||
$typ = $student->typ;
|
||||
$punkte = null; // new pruefungen never have punkte,
|
||||
|
||||
$lehreinheit_id = $student->lehreinheit_id;
|
||||
|
||||
// server-side add guards (max Antritte, chronological order, occurrence limit)
|
||||
$addError = $this->validatePruefungAdd($student_uid, $lva_id, $stsem, $note, $datum);
|
||||
if($addError !== null) {
|
||||
$ret[$student->uid] = $addError; // string result is surfaced per-student in the UI
|
||||
continue;
|
||||
}
|
||||
|
||||
$ret[$student->uid] = $this->savePruefungstermin($typ, $student_uid, $lva_id, $stsem, $lehreinheit_id, $note, $punkte, $datum);
|
||||
}
|
||||
|
||||
@@ -1216,7 +1624,12 @@ class Noten extends FHCAPI_Controller
|
||||
$lv_id = $result->lv_id;
|
||||
$sem_kurzbz = $result->sem_kurzbz;
|
||||
$pruefungen = $result->pruefungen;
|
||||
|
||||
|
||||
$this->assertLvAccess($lv_id, $sem_kurzbz);
|
||||
|
||||
// Prüfungsordnung §1: no entry past the Noteneintragungsfrist
|
||||
$this->enforceNoteneintragungsfrist($sem_kurzbz);
|
||||
|
||||
$ret = [];
|
||||
|
||||
foreach ($pruefungen as $pruefung) {
|
||||
@@ -1235,6 +1648,14 @@ class Noten extends FHCAPI_Controller
|
||||
$punkte = $pruefung->punkte;
|
||||
|
||||
$lehreinheit_id = $pruefung->lehreinheit_id;
|
||||
|
||||
// server-side add guards (max Antritte, chronological order, occurrence limit)
|
||||
$addError = $this->validatePruefungAdd($student_uid, $lv_id, $sem_kurzbz, $note, $datum);
|
||||
if($addError !== null) {
|
||||
$ret[$student_uid] = $addError; // string result is surfaced per-student in the UI
|
||||
continue;
|
||||
}
|
||||
|
||||
$ret[$student_uid] = $this->savePruefungstermin($typ, $student_uid, $lv_id, $sem_kurzbz, $lehreinheit_id, $note, $punkte, $datum);
|
||||
}
|
||||
|
||||
|
||||
@@ -1369,4 +1369,50 @@ class Lehrveranstaltung_model extends DB_Model
|
||||
|
||||
return $this->execReadOnlyQuery($qry, array($sem_kurzbz, $uid));
|
||||
}
|
||||
|
||||
/**
|
||||
* All Lehrveranstaltungen of a Studiengang offered in a Studiensemester.
|
||||
*/
|
||||
public function getLvForStudiengangInSemester($sem_kurzbz, $studiengang_kz) {
|
||||
$qry = "SELECT DISTINCT (tbl_lehrveranstaltung.lehrveranstaltung_id),
|
||||
UPPER(tbl_studiengang.typ::varchar(1) || tbl_studiengang.kurzbz) as stg_kurzbz,
|
||||
tbl_studiengang.studiengang_kz,
|
||||
tbl_lehrveranstaltung.semester as lv_semester,
|
||||
tbl_lehrveranstaltung.bezeichnung as lv_bezeichnung,
|
||||
tbl_lehrveranstaltung.orgform_kurzbz as orgform
|
||||
FROM
|
||||
lehre.tbl_lehrveranstaltung JOIN public.tbl_studiengang USING(studiengang_kz)
|
||||
WHERE
|
||||
tbl_lehrveranstaltung.studiengang_kz = ?
|
||||
AND tbl_lehrveranstaltung.aktiv = TRUE
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM lehre.tbl_lehreinheit le
|
||||
WHERE le.lehrveranstaltung_id = tbl_lehrveranstaltung.lehrveranstaltung_id
|
||||
AND le.studiensemester_kurzbz = ?
|
||||
)
|
||||
ORDER BY stg_kurzbz,lv_semester,lv_bezeichnung";
|
||||
|
||||
return $this->execReadOnlyQuery($qry, array($studiengang_kz, $sem_kurzbz));
|
||||
}
|
||||
|
||||
/**
|
||||
* Number of Lehreinheit assignments the given mitarbeiter has for a Lehrveranstaltung (i.e. whether
|
||||
* they teach it), optionally restricted to a Studiensemester. Returns a single row with a `teaches`
|
||||
* count - check `teaches > 0`. Used to scope teacher access in the Benotungstool to their own LVs.
|
||||
*/
|
||||
public function getLektorIsTeachingLva($lva_id, $ma_uid, $studiensemester_kurzbz = null)
|
||||
{
|
||||
$params = array($lva_id, $ma_uid);
|
||||
$query = "SELECT COUNT(*) AS teaches
|
||||
FROM lehre.tbl_lehreinheitmitarbeiter
|
||||
JOIN lehre.tbl_lehreinheit USING (lehreinheit_id)
|
||||
JOIN lehre.tbl_lehrveranstaltung USING (lehrveranstaltung_id)
|
||||
WHERE lehrveranstaltung_id = ? AND mitarbeiter_uid = ?";
|
||||
if ($studiensemester_kurzbz !== null && $studiensemester_kurzbz !== '') {
|
||||
$query .= " AND tbl_lehreinheit.studiensemester_kurzbz = ?";
|
||||
$params[] = $studiensemester_kurzbz;
|
||||
}
|
||||
|
||||
return $this->execReadOnlyQuery($query, $params);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,5 +83,19 @@ export default {
|
||||
url: '/api/frontend/v1/Noten/getNoteByPunkte',
|
||||
params: { punkte, lv_id, sem_kurzbz }
|
||||
};
|
||||
},
|
||||
getBenotungstoolContext(sem_kurzbz, lv_id = null) {
|
||||
return {
|
||||
method: 'get',
|
||||
url: '/api/frontend/v1/Noten/getBenotungstoolContext',
|
||||
params: { sem_kurzbz, lv_id }
|
||||
};
|
||||
},
|
||||
getLvForStudiengang(studiengang_kz, sem_kurzbz) {
|
||||
return {
|
||||
method: 'get',
|
||||
url: '/api/frontend/v1/Noten/getLvForStudiengang',
|
||||
params: { studiengang_kz, sem_kurzbz }
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
import {CoreFilterCmpt} from "../../filter/Filter.js";
|
||||
import ApiLehre from "../../../api/factory/lehre.js";
|
||||
import ApiNoten from "../../../api/factory/noten.js";
|
||||
import ApiAuthinfo from "../../../api/factory/authinfo.js";
|
||||
import ApiStudiensemester from "../../../api/factory/studiensemester.js";
|
||||
import BsModal from '../../Bootstrap/Modal.js';
|
||||
import BsOffcanvas from '../../Bootstrap/Offcanvas.js';
|
||||
@@ -42,7 +40,6 @@ export const Benotungstool = {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
uid: null,
|
||||
headerFiltersRestored: false,
|
||||
filtersRestored: false,
|
||||
filteredRows: null,
|
||||
@@ -85,6 +82,9 @@ export const Benotungstool = {
|
||||
pruefungen: null,
|
||||
studiensemester: null,
|
||||
selectedSemester: null,
|
||||
isAssistenz: false,
|
||||
assistenzStudiengaenge: null,
|
||||
selectedStudiengang: null,
|
||||
lehrveranstaltungen: null,
|
||||
selectedLehrveranstaltung: null,
|
||||
tableBuiltResolve: null,
|
||||
@@ -206,10 +206,6 @@ export const Benotungstool = {
|
||||
]};
|
||||
},
|
||||
methods: {
|
||||
async fetchAuthUID() {
|
||||
const authIdResponse = await this.$api.call(ApiAuthinfo.getAuthUID());
|
||||
this.uid = authIdResponse.data.uid;
|
||||
},
|
||||
loadState() {
|
||||
return JSON.parse(localStorage.getItem(this.persistenceID) || "null");
|
||||
},
|
||||
@@ -621,6 +617,18 @@ export const Benotungstool = {
|
||||
this.$api.call(ApiNoten.saveStudentPruefungBulk(this.lv_id, this.sem_kurzbz, pruefungenbulk))
|
||||
.then((res)=> {
|
||||
if(res.meta.status === 'success') {
|
||||
// separate per-row backend rejections (localized string messages) from actual saves
|
||||
let errorList = ''
|
||||
Object.keys(res.data ?? {}).forEach(uid => {
|
||||
const entry = res.data[uid]
|
||||
if(!entry?.savedPruefung && !entry?.extraPruefung) {
|
||||
errorList += entry + '\n'
|
||||
}
|
||||
})
|
||||
if(errorList !== '') {
|
||||
this.$fhcAlert.alertError(errorList)
|
||||
}
|
||||
|
||||
this.$fhcAlert.alertDefault(
|
||||
'success',
|
||||
'Info',
|
||||
@@ -637,12 +645,17 @@ export const Benotungstool = {
|
||||
|
||||
const pruefungen = res.data
|
||||
uids.forEach(entry => {
|
||||
const saved = pruefungen[entry.uid].savedPruefung?.[0]
|
||||
const extra = pruefungen[entry.uid].extraPruefung?.[0]
|
||||
const rowResult = pruefungen[entry.uid]
|
||||
const saved = rowResult?.savedPruefung?.[0]
|
||||
const extra = rowResult?.extraPruefung?.[0]
|
||||
|
||||
const student = this.studenten.find(s => s.uid == entry.uid)
|
||||
if(!student) return
|
||||
|
||||
// a per-row backend rejection returns a localized error string (or produced nothing) ->
|
||||
// skip it here; callers surface those messages to the user separately
|
||||
if(!saved) return
|
||||
|
||||
// check for extra pruefung (termin1) to add before
|
||||
if(extra) {
|
||||
student["Termin1"] = extra
|
||||
@@ -1381,9 +1394,21 @@ export const Benotungstool = {
|
||||
|
||||
// no actions on kommPruef allowed
|
||||
// no actions on termin1 aka pruefung 0 aka ursprüngliche note erlaubt
|
||||
if(field === 'kommPruef' || field === "Termin1") {
|
||||
if(field === 'kommPruef' || field === "Termin1") {
|
||||
// Prüfungsordnung §2: for the original Zeugnisnote (Termin1 = first Antritt) optionally
|
||||
// show its Benotungsdatum inline next to the grade
|
||||
if(field === "Termin1"
|
||||
&& this.config?.SHOW_BENOTUNGSDATUM_ON_NOTENVORSCHLAG_UEBERNAHME
|
||||
&& studentPruefung?.datum) {
|
||||
const dateDiv = createCol(this.formatDatumDMY(studentPruefung.datum), 'col-auto d-flex justify-content-center align-items-center')
|
||||
dateDiv.classList.add('text-muted')
|
||||
dateDiv.style.fontSize = '0.75rem'
|
||||
dateDiv.style.whiteSpace = 'nowrap'
|
||||
dateDiv.style.marginLeft = '6px'
|
||||
rowDiv.appendChild(dateDiv)
|
||||
}
|
||||
return rowDiv
|
||||
}
|
||||
}
|
||||
|
||||
if(data[field]?.pruefungstyp_kurzbz !== 'Termin1') {
|
||||
// once a later-dated or higher-attempt pruefung exists (e.g. a Termin3/kommPruef has
|
||||
@@ -1431,6 +1456,12 @@ export const Benotungstool = {
|
||||
if(parts.length !== 3) return null
|
||||
return new Date(Number(parts[0]), Number(parts[1]) - 1, Number(parts[2]))
|
||||
},
|
||||
formatDatumDMY(datum) {
|
||||
// 'YYYY-MM-DD[ ...]' -> 'DD.MM.YYYY'
|
||||
const parts = (datum ?? '').slice(0, 10).split('-')
|
||||
if(parts.length !== 3) return ''
|
||||
return `${parts[2]}.${parts[1]}.${parts[0]}`
|
||||
},
|
||||
addDays(date, days) {
|
||||
const d = new Date(date)
|
||||
d.setDate(d.getDate() + days)
|
||||
@@ -1775,22 +1806,73 @@ export const Benotungstool = {
|
||||
this.notenTableOptions.height = window.visualViewport.height - rect.top - 50
|
||||
this.$refs.notenTable.tabulator.setHeight(this.notenTableOptions.height)
|
||||
},
|
||||
loadLehrveranstaltungen(sem_kurzbz, preselectLvId = null) {
|
||||
return this.$api.call(ApiLehre.getZugewieseneLv(this.uid, sem_kurzbz)).then(res => {
|
||||
this.lehrveranstaltungen = res.data
|
||||
this.lehrveranstaltungen.forEach(lva => {
|
||||
lva.fullString = `${lva.stg_kurzbz} - ${lva.lv_semester} - ${lva.orgform}: ${lva.lv_bezeichnung}`
|
||||
})
|
||||
this.selectedLehrveranstaltung = preselectLvId
|
||||
? this.lehrveranstaltungen.find(lva => lva.lehrveranstaltung_id == preselectLvId) ?? null
|
||||
: null
|
||||
setLehrveranstaltungen(lvaData, preselectLvId = null) {
|
||||
this.lehrveranstaltungen = lvaData
|
||||
this.lehrveranstaltungen.forEach(lva => {
|
||||
lva.fullString = `${lva.stg_kurzbz} - ${lva.lv_semester} - ${lva.orgform}: ${lva.lv_bezeichnung}`
|
||||
})
|
||||
this.selectedLehrveranstaltung = preselectLvId
|
||||
? this.lehrveranstaltungen.find(lva => lva.lehrveranstaltung_id == preselectLvId) ?? null
|
||||
: null
|
||||
},
|
||||
// Decides how the LV dropdown is fed for the current user: Assistenzen pick a Studiengang first
|
||||
// (LVs scoped by entitlement), teachers get their assigned LVs directly. Also loads the
|
||||
// entitled Studiengänge for the Studiengang dropdown when in Assistenz mode.
|
||||
setupLvSource(sem_kurzbz, preselectLvId = null) {
|
||||
return this.$api.call(ApiNoten.getBenotungstoolContext(sem_kurzbz, preselectLvId)).then(res => {
|
||||
this.isAssistenz = !!res.data?.isAssistenz
|
||||
|
||||
if (this.isAssistenz) {
|
||||
this.setAssistenzStudiengaenge(res.data?.studiengaenge ?? [])
|
||||
|
||||
// deep-link: preselect the requested LV's Studiengang, load its LVs and preselect the LV.
|
||||
// Without a (resolvable) deep-link, wait for a manual Studiengang selection.
|
||||
const preStgKz = res.data?.preselectStudiengang_kz
|
||||
this.selectedStudiengang = preStgKz
|
||||
? this.assistenzStudiengaenge.find(s => s.studiengang_kz == preStgKz) ?? null
|
||||
: null
|
||||
|
||||
if (this.selectedStudiengang) {
|
||||
return this.loadLehrveranstaltungenForStudiengang(this.selectedStudiengang.studiengang_kz, sem_kurzbz, preselectLvId)
|
||||
}
|
||||
|
||||
this.lehrveranstaltungen = null
|
||||
this.selectedLehrveranstaltung = null
|
||||
} else {
|
||||
// teacher: assigned LVs delivered by the same context call (deep-linked lv_id preselected)
|
||||
this.setLehrveranstaltungen(res.data?.lehrveranstaltungen ?? [], preselectLvId)
|
||||
}
|
||||
})
|
||||
},
|
||||
setAssistenzStudiengaenge(studiengaenge) {
|
||||
this.assistenzStudiengaenge = studiengaenge
|
||||
this.assistenzStudiengaenge.forEach(stg => {
|
||||
stg.fullString = `${stg.kuerzel} - ${stg.bezeichnung}`
|
||||
})
|
||||
},
|
||||
loadLehrveranstaltungenForStudiengang(studiengang_kz, sem_kurzbz, preselectLvId = null) {
|
||||
return this.$api.call(ApiNoten.getLvForStudiengang(studiengang_kz, sem_kurzbz)).then(res => {
|
||||
this.setLehrveranstaltungen(res.data, preselectLvId)
|
||||
})
|
||||
},
|
||||
stgChanged(e) {
|
||||
const sem = this.selectedSemester?.studiensemester_kurzbz ?? this.sem_kurzbz
|
||||
const stg = e.value?.studiengang_kz ?? null
|
||||
|
||||
this.selectedLehrveranstaltung = null
|
||||
if (stg && sem) {
|
||||
this.loading = true
|
||||
this.loadLehrveranstaltungenForStudiengang(stg, sem).finally(() => this.loading = false)
|
||||
} else {
|
||||
this.lehrveranstaltungen = null
|
||||
}
|
||||
},
|
||||
getOptionLabelStg(option) {
|
||||
return option.fullString
|
||||
},
|
||||
async setupCreated() {
|
||||
this.loading = true
|
||||
|
||||
await this.fetchAuthUID()
|
||||
|
||||
this.debouncedFetchPunkteForPruefung = debounce(this.fetchNoteForPunktePruefung, 500)
|
||||
|
||||
// fetch cis config regarding gesamtnoteneingabe, needs to be fetched before setup can finish
|
||||
@@ -1818,7 +1900,7 @@ export const Benotungstool = {
|
||||
this.selectedSemester = defaultSem ?? this.studiensemester[0] ?? null
|
||||
|
||||
const sem = this.selectedSemester?.studiensemester_kurzbz
|
||||
if (sem) this.loadLehrveranstaltungen(sem, this.lv_id)
|
||||
if (sem) this.setupLvSource(sem, this.lv_id)
|
||||
})
|
||||
|
||||
LehreinheitenModule.setupContext(this.$.appContext.config.globalProperties)
|
||||
@@ -1864,23 +1946,45 @@ export const Benotungstool = {
|
||||
ssChanged(e) {
|
||||
const sem = e.value.studiensemester_kurzbz
|
||||
const keepLvId = this.selectedLehrveranstaltung?.lehrveranstaltung_id ?? this.lv_id
|
||||
const keepStg = this.selectedStudiengang?.studiengang_kz
|
||||
|
||||
this.loading = true
|
||||
this.loadLehrveranstaltungen(sem, keepLvId).then(() => {
|
||||
const lvId = this.selectedLehrveranstaltung?.lehrveranstaltung_id ?? null
|
||||
this.$api.call(ApiNoten.getBenotungstoolContext(sem)).then(res => {
|
||||
this.isAssistenz = !!res.data?.isAssistenz
|
||||
|
||||
this.$router.push({
|
||||
name: "Benotungstool",
|
||||
params: { sem_kurzbz: sem, lv_id: lvId ?? undefined }
|
||||
})
|
||||
if (this.isAssistenz) {
|
||||
// keep the selected Studiengang if it still exists this semester, then reload its LVs
|
||||
this.setAssistenzStudiengaenge(res.data?.studiengaenge ?? [])
|
||||
this.selectedStudiengang = keepStg
|
||||
? this.assistenzStudiengaenge.find(s => s.studiengang_kz == keepStg) ?? null
|
||||
: null
|
||||
|
||||
if (lvId) {
|
||||
this.loadNoten(lvId, sem)
|
||||
} else if (this.$refs.notenTable?.tabulator) {
|
||||
this.$refs.notenTable.tabulator.setData([])
|
||||
const loadLvs = this.selectedStudiengang
|
||||
? this.loadLehrveranstaltungenForStudiengang(this.selectedStudiengang.studiengang_kz, sem, keepLvId)
|
||||
: Promise.resolve(this.lehrveranstaltungen = null)
|
||||
|
||||
return loadLvs.then(() => this.afterSemesterChange(sem))
|
||||
}
|
||||
|
||||
// teacher: assigned LVs for the new semester come from the same context call
|
||||
this.setLehrveranstaltungen(res.data?.lehrveranstaltungen ?? [], keepLvId)
|
||||
return this.afterSemesterChange(sem)
|
||||
}).finally(() => this.loading = false)
|
||||
},
|
||||
afterSemesterChange(sem) {
|
||||
const lvId = this.selectedLehrveranstaltung?.lehrveranstaltung_id ?? null
|
||||
|
||||
this.$router.push({
|
||||
name: "Benotungstool",
|
||||
params: { sem_kurzbz: sem, lv_id: lvId ?? undefined }
|
||||
})
|
||||
|
||||
if (lvId) {
|
||||
this.loadNoten(lvId, sem)
|
||||
} else if (this.$refs.notenTable?.tabulator) {
|
||||
this.$refs.notenTable.tabulator.setData([])
|
||||
}
|
||||
},
|
||||
getOptionLabel(option) {
|
||||
return option.studiensemester_kurzbz
|
||||
},
|
||||
@@ -1890,21 +1994,24 @@ export const Benotungstool = {
|
||||
getOptionLabelLe(option) {
|
||||
return option.infoString
|
||||
},
|
||||
getPruefungstypForStudentByAntritt(student) {
|
||||
// when adding new pruefungen, determine the next pruefungstyp by using the antritt counter
|
||||
switch (student.hoechsterAntritt) {
|
||||
case 0:
|
||||
return "Termin2"
|
||||
break
|
||||
case 1:
|
||||
return "Termin2"
|
||||
break
|
||||
case 2:
|
||||
return "Termin3"
|
||||
break
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
getPruefungstypForStudentByAntritt(student) {
|
||||
// Determine the next in-tool pruefungstyp when adding a new attempt. The order of retake
|
||||
// types is driven by config so each installation keeps its own ruleset:
|
||||
// - Termin1 (the original Zeugnisnote) is attempt 1 and is not offered here.
|
||||
// - each enabled retake type is the next offered attempt: Termin2, then legacy Termin3.
|
||||
// - kommPruef is the terminal attempt and is entered elsewhere, so it is never offered here.
|
||||
// Our installation enables only Termin2 (Termin3 is legacy for other unis), so every offered
|
||||
// retake resolves to Termin2 - which is also why Termin2 can occur twice: the excused Termin
|
||||
// does not increment hoechsterAntritt (see getAntrittCountStudent), so the next offered attempt
|
||||
// stays Termin2 until a counting Termin2 exists.
|
||||
const retakeTypes = []
|
||||
if(this.config?.CIS_GESAMTNOTE_PRUEFUNG_TERMIN2) retakeTypes.push("Termin2")
|
||||
if(this.config?.CIS_GESAMTNOTE_PRUEFUNG_TERMIN3) retakeTypes.push("Termin3")
|
||||
|
||||
// hoechsterAntritt is the number of counting attempts already taken (>=1 once an original
|
||||
// note exists). The next retake sits at index hoechsterAntritt-1 (attempt 1 -> first retake).
|
||||
const idx = Math.max(0, student.hoechsterAntritt - 1)
|
||||
return retakeTypes[idx] ?? ""
|
||||
},
|
||||
savePruefungEingabe() {
|
||||
// keep the date within the neighbouring exam dates (a typed value can bypass the picker bounds)
|
||||
@@ -2279,14 +2386,17 @@ export const Benotungstool = {
|
||||
}).finally(()=> this.loading = false)
|
||||
},
|
||||
getAntrittCountStudent(student) {
|
||||
// checks for existence of a prüfung with a note that resolves to a
|
||||
// "angetretene Prüfung" -> anything except "entschuldigt" & "noch nicht eingetragen"
|
||||
// and returns the next allowed pruefungstyp from the number of taken pruefungen
|
||||
|
||||
// 1 -> reguläre note
|
||||
// 2 -> erste Nachprüfung / Termin2
|
||||
// 3 -> 2te Nachprüfung / Termin3
|
||||
// 4 -> kommPruef
|
||||
// Number of counting Prüfungsantritte the student has taken. Counting is by NOTE, not by
|
||||
// pruefungstyp: any note except the "antrittslose" ones (entschuldigt & noch nicht eingetragen,
|
||||
// see NOTEN_OHNE_ANTRITT) counts as an attempt. That is exactly what makes an excused Termin2
|
||||
// not consume an attempt, so Termin2 can be entered a second time.
|
||||
//
|
||||
// Antritt numbering in our installation: 1 = original note, 2 = Termin2 (may be entered twice,
|
||||
// one excused), terminal = kommPruef. (Termin3 is a legacy retake for other installations and
|
||||
// is simply counted like any other pruefung when enabled.)
|
||||
//
|
||||
// A present kommPruef is terminal: return a sentinel (4) that is >= any maxAntrittCount, so no
|
||||
// further attempts can be added here regardless of installation.
|
||||
if(student['kommPruef']) return 4
|
||||
|
||||
let pruefungsAntrittCount = 0
|
||||
@@ -2410,11 +2520,17 @@ export const Benotungstool = {
|
||||
+ ': <strong>' + (this.studenten?.length || 0) + '</strong>';
|
||||
},
|
||||
maxAntrittCount() {
|
||||
// Maximum number of counting Prüfungsantritte enterable IN THIS TOOL: the original note
|
||||
// (always 1) plus each enabled in-tool retake type. kommPruef is the terminal attempt and is
|
||||
// entered elsewhere, so it is intentionally NOT part of this cap. For our installation
|
||||
// (Termin2 on, Termin3 off) this is 2 (original + Termin2); Termin3 is legacy for other unis.
|
||||
// Excused Termine do not count (see getAntrittCountStudent), which is what lets Termin2 occur
|
||||
// twice within this cap.
|
||||
let maxAntritte = 1;
|
||||
|
||||
|
||||
if(this.config?.CIS_GESAMTNOTE_PRUEFUNG_TERMIN2) maxAntritte++
|
||||
if(this.config?.CIS_GESAMTNOTE_PRUEFUNG_TERMIN3) maxAntritte++
|
||||
|
||||
|
||||
return maxAntritte;
|
||||
},
|
||||
getFreigabeCounter() {
|
||||
@@ -2684,53 +2800,64 @@ export const Benotungstool = {
|
||||
|
||||
<FhcOverlay :active="loading"></FhcOverlay>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-auto me-auto">
|
||||
<h2>{{$capitalize($p.t('benotungstool/benotungstoolTitle'))}}</h2>
|
||||
<h5>{{ selectedLehrveranstaltung?.lv_bezeichnung }}</h5>
|
||||
<div class="row align-items-center gy-2 mb-2">
|
||||
<div class="col-12 col-xxl-auto">
|
||||
<h2 class="mb-0">{{$capitalize($p.t('benotungstool/benotungstoolTitle'))}}</h2>
|
||||
<h5 class="mb-0 text-truncate" style="max-width: 22rem;">{{ selectedLehrveranstaltung?.lv_bezeichnung }}</h5>
|
||||
</div>
|
||||
|
||||
<div class="col-auto d-none d-xxl-flex">
|
||||
<label class="col-form-label">{{$capitalize($p.t('lehre/lehrveranstaltung'))}}:</label>
|
||||
</div>
|
||||
<div class="col-2">
|
||||
<Dropdown @change="lvChanged" :style="{'width': '100%'}" :optionLabel="getOptionLabelLv"
|
||||
:placeholder="$capitalize($p.t('lehre/lehrveranstaltung'))"
|
||||
v-model="selectedLehrveranstaltung" :options="lehrveranstaltungen" appendTo="self">
|
||||
<template #optionsgroup="slotProps">
|
||||
<div> {{ option.fullString }} </div>
|
||||
</template>
|
||||
</Dropdown>
|
||||
</div>
|
||||
|
||||
<div class="col-auto d-none d-xxl-flex">
|
||||
<label class="col-form-label">{{$capitalize($p.t('lehre/lehreinheit'))}}:</label>
|
||||
</div>
|
||||
<div class="col-2">
|
||||
<Dropdown :style="{'width': '100%'}" v-bind="LehreinheitenModule"
|
||||
v-model="selectedLehreinheit" showClear appendTo="self">
|
||||
<template #option="slotProps">
|
||||
<div>
|
||||
{{ slotProps.option.infoString }}
|
||||
<i class="fa-solid fa-user"></i>
|
||||
{{ slotProps.option.studentcount }}
|
||||
<i class="fa-solid fa-calendar-days"></i>
|
||||
{{ slotProps.option.termincount }}
|
||||
</div>
|
||||
</template>
|
||||
</Dropdown>
|
||||
</div>
|
||||
|
||||
<div class="col-auto d-none d-xxl-flex">
|
||||
<label class="col-form-label">{{$capitalize($p.t('lehre/studiensemester'))}}:</label>
|
||||
</div>
|
||||
<div class="col-2">
|
||||
<Dropdown @change="ssChanged" :style="{'width': '100%'}" :optionLabel="getOptionLabel"
|
||||
v-model="selectedSemester" :options="studiensemester" appendTo="self">
|
||||
<template #optionsgroup="slotProps">
|
||||
<div> {{ option.studiensemester_kurzbz }} </div>
|
||||
</template>
|
||||
</Dropdown>
|
||||
|
||||
<div class="col-12 col-xxl">
|
||||
<div class="d-flex flex-wrap align-items-center justify-content-xxl-end" style="gap: 0.5rem;">
|
||||
|
||||
<div class="d-flex align-items-center" style="flex: 1 1 12rem; min-width: 9rem; gap: 0.35rem;" v-if="isAssistenz">
|
||||
<label class="col-form-label py-0 text-nowrap flex-shrink-0 d-none d-xxl-inline">{{$capitalize($p.t('lehre/studiengang'))}}:</label>
|
||||
<Dropdown @change="stgChanged" class="flex-grow-1" :style="{'minWidth': '0'}" :optionLabel="getOptionLabelStg"
|
||||
:placeholder="$capitalize($p.t('lehre/studiengang'))"
|
||||
v-model="selectedStudiengang" :options="assistenzStudiengaenge" appendTo="self">
|
||||
<template #optionsgroup="slotProps">
|
||||
<div> {{ option.fullString }} </div>
|
||||
</template>
|
||||
</Dropdown>
|
||||
</div>
|
||||
|
||||
<div class="d-flex align-items-center" style="flex: 1 1 12rem; min-width: 9rem; gap: 0.35rem;">
|
||||
<label class="col-form-label py-0 text-nowrap flex-shrink-0 d-none d-xxl-inline">{{$capitalize($p.t('lehre/lehrveranstaltung'))}}:</label>
|
||||
<Dropdown @change="lvChanged" class="flex-grow-1" :style="{'minWidth': '0'}" :optionLabel="getOptionLabelLv"
|
||||
:placeholder="$capitalize($p.t('lehre/lehrveranstaltung'))"
|
||||
v-model="selectedLehrveranstaltung" :options="lehrveranstaltungen" appendTo="self">
|
||||
<template #optionsgroup="slotProps">
|
||||
<div> {{ option.fullString }} </div>
|
||||
</template>
|
||||
</Dropdown>
|
||||
</div>
|
||||
|
||||
<div class="d-flex align-items-center" style="flex: 1 1 12rem; min-width: 9rem; gap: 0.35rem;">
|
||||
<label class="col-form-label py-0 text-nowrap flex-shrink-0 d-none d-xxl-inline">{{$capitalize($p.t('lehre/lehreinheit'))}}:</label>
|
||||
<Dropdown class="flex-grow-1" :style="{'minWidth': '0'}" v-bind="LehreinheitenModule"
|
||||
v-model="selectedLehreinheit" showClear appendTo="self">
|
||||
<template #option="slotProps">
|
||||
<div>
|
||||
{{ slotProps.option.infoString }}
|
||||
<i class="fa-solid fa-user"></i>
|
||||
{{ slotProps.option.studentcount }}
|
||||
<i class="fa-solid fa-calendar-days"></i>
|
||||
{{ slotProps.option.termincount }}
|
||||
</div>
|
||||
</template>
|
||||
</Dropdown>
|
||||
</div>
|
||||
|
||||
<div class="d-flex align-items-center" style="flex: 1 1 8rem; min-width: 7rem; gap: 0.35rem;">
|
||||
<label class="col-form-label py-0 text-nowrap flex-shrink-0 d-none d-xxl-inline">{{$capitalize($p.t('lehre/studiensemester'))}}:</label>
|
||||
<Dropdown @change="ssChanged" class="flex-grow-1" :style="{'minWidth': '0'}" :optionLabel="getOptionLabel"
|
||||
v-model="selectedSemester" :options="studiensemester" appendTo="self">
|
||||
<template #optionsgroup="slotProps">
|
||||
<div> {{ option.studiensemester_kurzbz }} </div>
|
||||
</template>
|
||||
</Dropdown>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -151,6 +151,7 @@ $berechtigungen = array(
|
||||
array('lehre/abgabetool','Projektabgabetool, Studentenansicht'),
|
||||
array('lehre/abgabetool:download','Download von Projektarbeitsabgaben'),
|
||||
array('lehre/benotungstool','Cis4 Gesamtnoteneingabe'),
|
||||
array('lehre/benotungstool_assistenz','Cis4 Gesamtnoteneingabe für Assistenz'),
|
||||
array('lehre/freifach','Freifachverwaltung'),
|
||||
array('lehre/lehrfach','Lehrfachverwaltung'),
|
||||
array('lehre/lehrfach:begrenzt','Lehrfachverwaltung - nur aktiv aenderbar, nur aktive LF werden angezeigt'),
|
||||
|
||||
@@ -59258,6 +59258,106 @@ I have been informed that I am under no obligation to consent to the transmissio
|
||||
)
|
||||
)
|
||||
),
|
||||
array(
|
||||
'app' => 'core',
|
||||
'category' => 'benotungstool',
|
||||
'phrase' => 'maxAntritteReached',
|
||||
'insertvon' => 'system',
|
||||
'phrases' => array(
|
||||
array(
|
||||
'sprache' => 'German',
|
||||
'text' => 'Für Studierenden {0} sind bereits die maximal zulässigen Prüfungsantritte ({1}) vergeben. Es wurde keine Prüfung angelegt.',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
),
|
||||
array(
|
||||
'sprache' => 'English',
|
||||
'text' => 'Student {0} has already used the maximum number of exam attempts ({1}). No exam was created.',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
)
|
||||
)
|
||||
),
|
||||
array(
|
||||
'app' => 'core',
|
||||
'category' => 'benotungstool',
|
||||
'phrase' => 'pruefungDatumBeforeExisting',
|
||||
'insertvon' => 'system',
|
||||
'phrases' => array(
|
||||
array(
|
||||
'sprache' => 'German',
|
||||
'text' => 'Das Prüfungsdatum für Studierenden {0} darf nicht vor oder am selben Tag wie ein bereits bestehender Prüfungstermin liegen.',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
),
|
||||
array(
|
||||
'sprache' => 'English',
|
||||
'text' => 'The exam date for student {0} must not be on or before an existing exam date.',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
)
|
||||
)
|
||||
),
|
||||
array(
|
||||
'app' => 'core',
|
||||
'category' => 'benotungstool',
|
||||
'phrase' => 'noteOccuranceLimitReached',
|
||||
'insertvon' => 'system',
|
||||
'phrases' => array(
|
||||
array(
|
||||
'sprache' => 'German',
|
||||
'text' => 'Die gewählte Note darf für Studierenden {0} nicht so oft vergeben werden. Es wurde keine Prüfung angelegt.',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
),
|
||||
array(
|
||||
'sprache' => 'English',
|
||||
'text' => 'The selected grade may not be assigned that many times for student {0}. No exam was created.',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
)
|
||||
)
|
||||
),
|
||||
array(
|
||||
'app' => 'core',
|
||||
'category' => 'benotungstool',
|
||||
'phrase' => 'noteneintragungsfristVorbei',
|
||||
'insertvon' => 'system',
|
||||
'phrases' => array(
|
||||
array(
|
||||
'sprache' => 'German',
|
||||
'text' => 'Die Noteneintragungsfrist ({0}) für dieses Studiensemester ist abgelaufen. Es können keine Noten oder Prüfungen mehr eingetragen werden.',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
),
|
||||
array(
|
||||
'sprache' => 'English',
|
||||
'text' => 'The grade entry deadline ({0}) for this study semester has passed. Grades and exams can no longer be entered.',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
)
|
||||
)
|
||||
),
|
||||
array(
|
||||
'app' => 'core',
|
||||
'category' => 'benotungstool',
|
||||
'phrase' => 'keineBerechtigungNoten',
|
||||
'insertvon' => 'system',
|
||||
'phrases' => array(
|
||||
array(
|
||||
'sprache' => 'German',
|
||||
'text' => 'Keine Berechtigung, um in der Lehrveranstaltung "{0}" im Semester {1} Noten einzutragen.',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
),
|
||||
array(
|
||||
'sprache' => 'English',
|
||||
'text' => 'No permission to enter grades for the course "{0}" in semester {1}.',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
)
|
||||
)
|
||||
),
|
||||
array(
|
||||
'app' => 'core',
|
||||
'category' => 'benotungstool',
|
||||
|
||||
Reference in New Issue
Block a user