diff --git a/application/config/noten.php b/application/config/noten.php
index 6fdee8b86..7bdfce1ad 100644
--- a/application/config/noten.php
+++ b/application/config/noten.php
@@ -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)
\ No newline at end of file
+$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)
\ No newline at end of file
diff --git a/application/controllers/Cis/Benotungstool.php b/application/controllers/Cis/Benotungstool.php
index 94faeff98..fe01f3595 100644
--- a/application/controllers/Cis/Benotungstool.php
+++ b/application/controllers/Cis/Benotungstool.php
@@ -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();
diff --git a/application/controllers/api/frontend/v1/Noten.php b/application/controllers/api/frontend/v1/Noten.php
index 0cabb639f..98ba825a8 100644
--- a/application/controllers/api/frontend/v1/Noten.php
+++ b/application/controllers/api/frontend/v1/Noten.php
@@ -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