From 60dbee86166915bc24d70782132eb38aa5cfb6db Mon Sep 17 00:00:00 2001 From: Simon Rettberg Date: Thu, 15 Jan 2026 11:21:11 +0100 Subject: [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. --- modules-available/exams/lang/de/template-tags.json | 2 + modules-available/exams/lang/en/template-tags.json | 2 + modules-available/exams/page.inc.php | 52 +++++++++++++++++++++- modules-available/exams/templates/page-exams.html | 17 ++++++- 4 files changed, 69 insertions(+), 4 deletions(-) (limited to 'modules-available') 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}}