summaryrefslogtreecommitdiffstats
path: root/modules-available
diff options
context:
space:
mode:
authorSimon Rettberg2026-01-15 11:21:11 +0100
committerSimon Rettberg2026-01-15 11:21:11 +0100
commit60dbee86166915bc24d70782132eb38aa5cfb6db (patch)
tree48b4d784e0112ed70cd31d17ff0b6074f0f66396 /modules-available
parent[dozmod] Disable checkboxes *after* submit (diff)
downloadslx-admin-60dbee86166915bc24d70782132eb38aa5cfb6db.tar.gz
slx-admin-60dbee86166915bc24d70782132eb38aa5cfb6db.tar.xz
slx-admin-60dbee86166915bc24d70782132eb38aa5cfb6db.zip
[exams] Add check and warning for colliding exams
If two (or more) exams share at least one location and their start/end times overlap, display a warning to the user.
Diffstat (limited to 'modules-available')
-rw-r--r--modules-available/exams/lang/de/template-tags.json2
-rw-r--r--modules-available/exams/lang/en/template-tags.json2
-rw-r--r--modules-available/exams/page.inc.php52
-rw-r--r--modules-available/exams/templates/page-exams.html17
4 files changed, 69 insertions, 4 deletions
diff --git a/modules-available/exams/lang/de/template-tags.json b/modules-available/exams/lang/de/template-tags.json
index f88ce5af..0ac88155 100644
--- a/modules-available/exams/lang/de/template-tags.json
+++ b/modules-available/exams/lang/de/template-tags.json
@@ -11,6 +11,7 @@
"lang_begin_date": "Beginn Datum",
"lang_begin_time": "Uhrzeit",
"lang_checkLocationSelectionHint": "Stellen Sie sicher, dass die gew\u00fcnschte(n) Pr\u00fcfungsveranstaltung(en) in ihrer Raumbeschr\u00e4nkung mit den hier ausgew\u00e4hlten R\u00e4umen \u00fcbereinstimmen.",
+ "lang_collision": "Kollision",
"lang_comfirmGlobalExam": "Wollen Sie wirklich eine globale Pr\u00fcfung definieren? Im gew\u00e4hlten Zeitraum werden s\u00e4mtliche R\u00e4ume in den Pr\u00fcfungsmodus geschaltet.",
"lang_currentlyActiveExams": "Aktuell laufende Pr\u00fcfungen",
"lang_dateTime": "Datum\/Uhrzeit",
@@ -26,6 +27,7 @@
"lang_examStartAfterLectureEnd": "Der gew\u00e4hlte Klausurzeitraum beginnt, nachdem die gew\u00e4hlte Veranstaltung endet, oder kurz vor derem Ende.",
"lang_examStartAfterLectureStart": "Der gew\u00e4hlte Klausurzeitraum beginnt sp\u00e4ter, als die gew\u00e4hlte Veranstaltung startet.",
"lang_examStartBeforeLectureStart": "Der gew\u00e4hlte Klausurzeitraum beginnt, bevor die gew\u00e4hlte Veranstaltung startet. Ein Autostart der Veranstaltung wird fehlschlagen.",
+ "lang_examsWithCollisions": "Sich r\u00e4umlich und zeitlich \u00fcberschneidende Pr\u00fcfungen",
"lang_global": "Global",
"lang_headingAddExam": "Zeitraum hinzuf\u00fcgen",
"lang_headingAllExamLectures": "Ausstehende Pr\u00fcfungsveranstaltungen (30 Tage)",
diff --git a/modules-available/exams/lang/en/template-tags.json b/modules-available/exams/lang/en/template-tags.json
index 59c159e9..97b6ce21 100644
--- a/modules-available/exams/lang/en/template-tags.json
+++ b/modules-available/exams/lang/en/template-tags.json
@@ -11,6 +11,7 @@
"lang_begin_date": "Begin Date",
"lang_begin_time": "Time",
"lang_checkLocationSelectionHint": "Make sure that the according lecture(s) will have their location restrictions set accordingly.",
+ "lang_collision": "Collision",
"lang_comfirmGlobalExam": "Do you really want to create a global exam? Every single room will be set to lecture mode during the selected time period.",
"lang_currentlyActiveExams": "Currently running exams",
"lang_dateTime": "Date\/Time",
@@ -26,6 +27,7 @@
"lang_examStartAfterLectureEnd": "Specified exam interval starts after selected lecture ends (or shortly before it ends).",
"lang_examStartAfterLectureStart": "Specified exam interval starts after selected lecture starts.",
"lang_examStartBeforeLectureStart": "Specified exam interval starts before selected lecture starts. (Auto-)starting the lecture before it's valid will fail.",
+ "lang_examsWithCollisions": "Exams with conflicting schedule and location",
"lang_global": "Global",
"lang_headingAddExam": "Add Exam Period",
"lang_headingAllExamLectures": "Upcoming Lectures Marked As Exams (30 Days)",
diff --git a/modules-available/exams/page.inc.php b/modules-available/exams/page.inc.php
index f42711a2..2bd7a215 100644
--- a/modules-available/exams/page.inc.php
+++ b/modules-available/exams/page.inc.php
@@ -224,8 +224,51 @@ class Page_Exams extends Page
{
$out = [];
$now = time();
+
+ // Pre-compute collisions: exams overlap in time and share at least one location
+ $collisionFlags = [];
+ $parsedExams = [];
if (is_array($this->exams)) {
- foreach ($this->exams as $exam) {
+ foreach ($this->exams as $idx => $exam) {
+ IF ($exam['endtime'] < $now)
+ continue; // Don't care about past exams
+ // Parse location IDs: treat NULL as global (0) to be consistent with permission checks
+ if (empty($exam['locationids'])) {
+ $lids = Location::getAllLocationIds(0);
+ } else {
+ $lids = array_map(function($val) {
+ return Location::getAllLocationIds($val, true);
+ }, explode(',', (string)$exam['locationids']));
+ $lids = array_unique(array_merge(...$lids));
+ }
+ $parsedExams[$idx] = [
+ 'start' => (int)$exam['starttime'],
+ 'end' => (int)$exam['endtime'],
+ 'lids' => $lids,
+ ];
+ $collisionFlags[$idx] = false;
+ }
+
+ $indices = array_keys($parsedExams);
+ $cnt = count($indices);
+ for ($i = 0; $i < $cnt; $i++) {
+ for ($j = $i + 1; $j < $cnt; $j++) {
+ $a = $parsedExams[$indices[$i]];
+ $b = $parsedExams[$indices[$j]];
+ // Time overlap if startA < endB and startB < endA
+ if ($a['start'] < $b['end'] && $b['start'] < $a['end']) {
+ // Location overlap
+ if (!empty(array_intersect($a['lids'], $b['lids']))) {
+ $collisionFlags[$indices[$i]] = true;
+ $collisionFlags[$indices[$j]] = true;
+ }
+ }
+ }
+ }
+
+ // Build output, attaching visual classes and collision flag
+ $numCollisions = 0;
+ foreach ($this->exams as $idx => $exam) {
if ($exam['endtime'] < $now) {
$exam['rowClass'] = 'text-muted';
$exam['btnClass'] = 'btn-default';
@@ -238,10 +281,15 @@ class Page_Exams extends Page
}
$exam['starttime_s'] = date('Y-m-d H:i', $exam['starttime']);
$exam['endtime_s'] = date('Y-m-d H:i', $exam['endtime']);
+ if ($collisionFlags[$idx] ?? false) {
+ $exam['rowClass'] = 'danger slx-bold';
+ $exam['collision'] = true;
+ $numCollisions++;
+ }
$out[] = $exam;
}
}
- return ['exams' => $out];
+ return ['exams' => $out, 'collisions' => $numCollisions];
}
protected function makeLectureExamList(): array
diff --git a/modules-available/exams/templates/page-exams.html b/modules-available/exams/templates/page-exams.html
index 178a4f8b..e4b6328c 100644
--- a/modules-available/exams/templates/page-exams.html
+++ b/modules-available/exams/templates/page-exams.html
@@ -4,6 +4,12 @@
{{lang_allExamPeriods}}
</div>
<div class="panel-body">
+ {{#collisions}}
+ <div class="alert alert-danger">
+ <span class="glyphicon glyphicon-warning-sign"></span>
+ {{lang_examsWithCollisions}}: {{.}}
+ </div>
+ {{/collisions}}
{{#active_exams}}
<div class="alert alert-info">
<span class="glyphicon glyphicon-info-sign"></span>
@@ -28,6 +34,9 @@
<tr class="{{rowClass}}">
<td>{{examid}}</td>
<td width="100%">
+ {{#collision}}
+ <span class="glyphicon glyphicon-flash" title="{{lang_collision}}"></span>
+ {{/collision}}
{{locationnames}}
{{^locationnames}}
<i>{{lang_global}}</i>
@@ -44,8 +53,12 @@
{{/description}}
</div>
</td>
- <td class="text-nowrap" data-sort-value={{starttime}}>{{starttime_s}}</td>
- <td class="text-nowrap" data-sort-value={{endtime}}>{{endtime_s}}</td>
+ <td class="text-nowrap" data-sort-value={{starttime}}>
+ {{starttime_s}}
+ </td>
+ <td class="text-nowrap" data-sort-value={{endtime}}>
+ {{endtime_s}}
+ </td>
<td class="text-nowrap text-right">
{{^liesInPast}}
<a onclick="slxShow({{starttime}}, {{endtime}})" class="btn btn-default btn-sm"><span class="glyphicon glyphicon-eye-open"></span></a>