aboutsummaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorMoritz Strohm <strohm@data-quest.de>2026-01-14 10:29:35 +0000
committerMoritz Strohm <strohm@data-quest.de>2026-01-14 10:29:35 +0000
commit78e46de33b3f205aae375d1ea6d4fe088e0e5124 (patch)
tree4b305bf3f7b5d066ac28f011fe752e98901e714c /lib
parentf637e7ae2d086941a11297ccc29ac273ad6759b0 (diff)
allow booking separable rooms in courses, closes #639
Closes #639 Merge request studip/studip!4039
Diffstat (limited to 'lib')
-rw-r--r--lib/classes/CourseDateList.php24
-rw-r--r--lib/classes/InstituteCalendarHelper.php10
-rw-r--r--lib/classes/JsonApi/RouteMap.php3
-rw-r--r--lib/classes/JsonApi/Routes/AvailableRooms.php201
-rw-r--r--lib/classes/JsonApi/SchemaMap.php2
-rw-r--r--lib/classes/calendar/ICalendarExport.php2
-rw-r--r--lib/dates.inc.php14
-rw-r--r--lib/extern/ExternPageTimetable.php19
-rw-r--r--lib/models/Course.php2
-rw-r--r--lib/models/CourseDate.php127
-rw-r--r--lib/models/CourseExDate.php8
-rw-r--r--lib/models/SeminarCycleDate.php26
-rw-r--r--lib/models/resources/Resource.php7
-rw-r--r--lib/models/resources/ResourceBooking.php19
-rw-r--r--lib/models/resources/ResourceRequest.php143
-rw-r--r--lib/resources/RoomManager.php9
16 files changed, 460 insertions, 156 deletions
diff --git a/lib/classes/CourseDateList.php b/lib/classes/CourseDateList.php
index 8a6be35..5aa1a02 100644
--- a/lib/classes/CourseDateList.php
+++ b/lib/classes/CourseDateList.php
@@ -204,12 +204,12 @@ class CourseDateList implements Stringable
}
}
foreach ($this->single_dates as $date) {
- $room_name = $date->getRoomName();
- if ($room_name) {
- if (!array_key_exists($room_name, $grouped_dates)) {
- $grouped_dates[$room_name] = new CourseDateList();
+ $room_names = $date->getRoomNames();
+ if ($room_names) {
+ if (!array_key_exists($room_names, $grouped_dates)) {
+ $grouped_dates[$room_names] = new CourseDateList();
}
- $grouped_dates[$room_name]->addSingleDate($date);
+ $grouped_dates[$room_names]->addSingleDate($date);
} else {
if (!array_key_exists(_('Ohne Raum'), $grouped_dates)) {
$grouped_dates[_('Ohne Raum')] = new CourseDateList();
@@ -269,16 +269,16 @@ class CourseDateList implements Stringable
foreach ($this->single_dates as $single_date) {
$date_line = $single_date->getFullName($with_room_names ? 'long-include-room' : 'long');
if ($group_by_rooms) {
- $room_name = _('Kein Raum');
- if ($single_date->room_booking) {
- $room_name = $single_date->room_booking->room_name;
+ $room_names = _('Kein Raum');
+ if ($single_date->room_bookingn) {
+ $room_names = $single_date->getRoomNames();
} elseif ($single_date->raum) {
- $room_name = $single_date->raum;
+ $room_names = $single_date->raum;
}
- if (!isset($output[$room_name])) {
- $output[$room_name] = [];
+ if (!isset($output[$room_names])) {
+ $output[$room_names] = [];
}
- $output[$room_name][] = $date_line;
+ $output[$room_names][] = $date_line;
} else {
$output[] = $date_line;
}
diff --git a/lib/classes/InstituteCalendarHelper.php b/lib/classes/InstituteCalendarHelper.php
index 13677ea..54393fa 100644
--- a/lib/classes/InstituteCalendarHelper.php
+++ b/lib/classes/InstituteCalendarHelper.php
@@ -438,7 +438,7 @@ class InstituteCalendarHelper
$next_single_date = CourseDate::getNextDateByMetadate($cycle_date->metadate_id);
if ($next_single_date) {
- $room_name = $next_single_date->getRoomName() ?: _('ohne Raumangabe');
+ $room_name = $next_single_date->getRoomNames() ?: _('ohne Raumangabe');
}
$fields = [
@@ -618,9 +618,11 @@ class InstituteCalendarHelper
$rooms = [];
foreach ($cycle_date->getAllDates() as $course_date) {
- $room = $course_date->getRoom();
- if ($room) {
- $rooms[$room->id] = $room->name;
+ $course_date_rooms = $course_date->getRooms();
+ if ($course_date_rooms) {
+ foreach ($course_date_rooms as $room) {
+ $rooms[$room->id] = $room->name;
+ }
}
}
if ($rooms) {
diff --git a/lib/classes/JsonApi/RouteMap.php b/lib/classes/JsonApi/RouteMap.php
index f3e97c0..bd5a58a 100644
--- a/lib/classes/JsonApi/RouteMap.php
+++ b/lib/classes/JsonApi/RouteMap.php
@@ -9,6 +9,7 @@ use JsonApi\Routes\Consultations\SlotCreationCount;
use JsonApi\Routes\Holidays\HolidaysShow;
use JsonApi\Routes\Vacations\VacationsShow;
use Slim\Routing\RouteCollectorProxy;
+use Studip\JsonApi\Routes\AvailableRooms;
/**
* Diese Klasse ist die JSON-API-Routemap, in der alle Routen
@@ -116,6 +117,8 @@ class RouteMap
$group->get('/status-groups/{id}', Routes\StatusgroupShow::class);
+ $group->get('/available-rooms', AvailableRooms::class);
+
$this->addAuthenticatedAdmissionRoutes($group);
$this->addAuthenticatedBlubberRoutes($group);
$this->addAuthenticatedClipboardRoutes($group);
diff --git a/lib/classes/JsonApi/Routes/AvailableRooms.php b/lib/classes/JsonApi/Routes/AvailableRooms.php
new file mode 100644
index 0000000..4600db3
--- /dev/null
+++ b/lib/classes/JsonApi/Routes/AvailableRooms.php
@@ -0,0 +1,201 @@
+<?php
+
+namespace Studip\JsonApi\Routes;
+
+use JsonApi\NonJsonApiController;
+use Psr\Http\Message\ServerRequestInterface as Request;
+use Psr\Http\Message\ResponseInterface as Response;
+
+class AvailableRooms extends NonJsonApiController
+{
+ public function __invoke(Request $request, Response $response, $args)
+ {
+ $raw_time_ranges = \Request::get('time_ranges');
+ if ($raw_time_ranges) {
+ $raw_time_ranges = json_decode($raw_time_ranges, true);
+ } else {
+ $raw_time_ranges = [];
+ }
+ $course_date_ids = \Request::get('course_date_ids');
+ if ($course_date_ids) {
+ $course_date_ids = explode(',', $course_date_ids);
+ }
+ $current_user = \User::findCurrent();
+ if (empty($raw_time_ranges)) {
+ //No time ranges given.
+ return $response->withStatus(400, 'No time ranges given.');
+ }
+ //Convert the time ranges to the appropriate format:
+ $time_ranges = [];
+ foreach ($raw_time_ranges as $raw_time_range) {
+ $start_str = $raw_time_range['start'] ?? '';
+ $end_str = $raw_time_range['end'] ?? '';
+ if (!$start_str || !$end_str) {
+ //Invalid time range.
+ return $response->withStatus(400, 'Invalid time range.');
+ }
+ //The timestamps are either in the extended RFC3339 format or in unix timestamps.
+ $timezone = new \DateTime();
+ $start_datetime = \DateTime::createFromFormat(\DateTimeInterface::RFC3339_EXTENDED, $start_str, $timezone->getTimezone());
+ $end_datetime = \DateTime::createFromFormat(\DateTimeInterface::RFC3339_EXTENDED, $end_str, $timezone->getTimezone());
+ if (!$start_datetime || !$end_datetime) {
+ //Try unix timestamps as fallback.
+ $start_datetime = \DateTime::createFromFormat('U', $start_str, $timezone->getTimezone());
+ $end_datetime = \DateTime::createFromFormat('U', $end_str, $timezone->getTimezone());
+ }
+ if (!$start_datetime || !$end_datetime) {
+ //Invalid date or time format.
+ return $response->withStatus(400, 'Invalid date or time format.');
+ }
+ $time_ranges[] = [
+ 'begin' => $start_datetime,
+ 'end' => $end_datetime
+ ];
+ }
+
+ if (!\ResourceManager::userHasGlobalPermission($current_user, 'autor')
+ && !\ResourceManager::userHasResourcePermissions($current_user, 'autor')) {
+ //The user must not book any room.
+ throw new \AccessDeniedException();
+ }
+
+ //Collect the booking-IDs for the course date:
+ $booking_ids = [];
+ if (!empty($course_date_ids)) {
+ $course_dates = \CourseDate::findMany($course_date_ids);
+ if ($course_dates) {
+ foreach ($course_dates as $course_date) {
+ foreach ($course_date->room_bookings as $booking) {
+ $booking_ids[] = $booking->id;
+ }
+ }
+ }
+ }
+
+ $available_rooms = \RoomManager::findRooms(
+ '',
+ null,
+ null,
+ [],
+ $time_ranges,
+ 'name ASC',
+ false,
+ [],
+ true,
+ $booking_ids
+ );
+
+ $bookable_rooms = [];
+ foreach ($available_rooms as $room) {
+ $all_ranges_bookable = true;
+ foreach ($time_ranges as $time_range) {
+ if (empty($time_range['begin']) || empty($time_range['end'])) {
+ //Invalid time range. We cannot continue.
+ return $response->withStatus(400, 'Invalid time range.');
+ }
+ if (!$room->userHasBookingRights($current_user, $time_range['begin'], $time_range['end'])) {
+ $all_ranges_bookable = false;
+ break;
+ }
+ }
+ if ($all_ranges_bookable) {
+ $bookable_rooms[$room->id] = $room;
+ }
+ }
+
+ $separable_rooms = \SeparableRoom::findBySQL(
+ "JOIN `separable_room_parts` srp
+ ON `separable_rooms`.`id` = `srp`.`separable_room_id`
+ WHERE `srp`.`room_id` IN ( :room_ids )
+ GROUP BY `separable_rooms`.`id`",
+ [
+ 'room_ids' => array_keys($bookable_rooms)
+ ]
+ );
+
+ $selectable_room_data = [];
+
+ foreach ($separable_rooms as $separable_room) {
+ //Check if all the room parts are available and bookable. If so, include the separable room
+ //in the $selectable_room_data array.
+
+ $unavailable_parts = \SeparableRoomPart::countBySQL(
+ "`separable_room_id` = :separable_room_id
+ AND `room_id` NOT IN ( :room_ids )",
+ [
+ 'separable_room_id' => $separable_room->id,
+ 'room_ids' => array_keys($bookable_rooms)
+ ]
+ ) > 0;
+
+ if (!$unavailable_parts) {
+ //The separable room is fully available. Include it in the list of bookable rooms
+ //before its room parts.
+ $selectable_room_data[] = [
+ 'id' => sprintf('separable_room-%s', $separable_room->id),
+ 'name' => sprintf(
+ '%1$s (%2$s)',
+ $separable_room->name,
+ _('Teilbarer Raum')
+ ),
+ 'separable_room_id' => $separable_room->id,
+ 'info_text' => sprintf('%1$s: %2$s', $separable_room->name, $separable_room->description)
+ ];
+ }
+
+ //Add the room parts from the $bookable_rooms array:
+ $room_part_data = [];
+ foreach ($separable_room->parts as $part) {
+ if (in_array($part->room_id, array_keys($bookable_rooms))) {
+ $room = $bookable_rooms[$part->room_id];
+
+ $room_part_data[] = [
+ 'id' => sprintf('room-%s', $room->id),
+ 'name' => studip_interpolate(
+ _('%{room_name} (%{seats} Sitzplätze) [Teil von %{separable_room_name}]'),
+ [
+ 'room_name' => $room->getFullName(),
+ 'seats' => $room->seats,
+ 'separable_room_name' => $separable_room->name
+ ]
+ ),
+ 'separable_room_id' => $separable_room->id,
+ 'info_text' => sprintf('%1$s: %2$s', $separable_room->name, $separable_room->description)
+ ];
+
+ unset($bookable_rooms[$part->room_id]);
+ }
+ }
+
+ //Sort $room_part_data by name:
+ uasort($room_part_data, function ($a, $b) {
+ if ($a['name'] > $b['name']) {
+ return 1;
+ } elseif ($a['name'] < $b['name']) {
+ return -1;
+ } else {
+ return 0;
+ }
+ });
+
+ $selectable_room_data = array_merge($selectable_room_data, $room_part_data);
+ }
+
+ //Add all remaining rooms:
+ foreach ($bookable_rooms as $room) {
+ $selectable_room_data[] = [
+ 'id' => sprintf('room-%s', $room->id),
+ 'name' => studip_interpolate(
+ _('%{room_name} (%{seats} Sitzplätze)'),
+ [
+ 'room_name' => $room->getFullName(),
+ 'seats' => $room->seats
+ ]
+ )
+ ];
+ }
+
+ $response->getBody()->write(json_encode($selectable_room_data));
+ return $response;
+ }
+}
diff --git a/lib/classes/JsonApi/SchemaMap.php b/lib/classes/JsonApi/SchemaMap.php
index 7432abc..5923c48 100644
--- a/lib/classes/JsonApi/SchemaMap.php
+++ b/lib/classes/JsonApi/SchemaMap.php
@@ -3,6 +3,7 @@
namespace JsonApi;
use JsonApi\Schemas\ShortUrl;
+use JsonApi\Schemas\Room;
/**
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
@@ -104,7 +105,6 @@ class SchemaMap
\Courseware\Unit::class => Schemas\Courseware\Unit::class,
\Courseware\UserDataField::class => Schemas\Courseware\UserDataField::class,
\Courseware\UserProgress::class => Schemas\Courseware\UserProgress::class,
- \ShortUrl::class => Schemas\ShortUrl::class,
];
}
}
diff --git a/lib/classes/calendar/ICalendarExport.php b/lib/classes/calendar/ICalendarExport.php
index 21bb47f..a2dada9 100644
--- a/lib/classes/calendar/ICalendarExport.php
+++ b/lib/classes/calendar/ICalendarExport.php
@@ -147,7 +147,7 @@ class ICalendarExport
return [
'SUMMARY' => $summary,
'DESCRIPTION' => $description,
- 'LOCATION' => $date->getRoomName(),
+ 'LOCATION' => $date->getRoomNames(),
'CATEGORIES' => $categories,
'LAST-MODIFIED' => $date->chdate,
'CREATED' => $date->mkdate,
diff --git a/lib/dates.inc.php b/lib/dates.inc.php
index 5ebd84d..93def75 100644
--- a/lib/dates.inc.php
+++ b/lib/dates.inc.php
@@ -200,18 +200,22 @@ function vorbesprechung(string $seminar_id, string $type = 'standard'): false|st
$termin = new CourseDate($termin_id);
$ret = (string) $termin;
- if (!empty($termin->room_booking->resource)) {
+ if (!empty($termin->room_bookings)) {
$ret .= ', '._("Ort:").' ';
switch ($type) {
case 'export':
- $ret .= $termin->room_booking->resource->name;
+ $ret .= $termin->getRoomNames();
break;
case 'standard':
- default:
- $ret .= '<a href="' . $termin->room_booking->resource->getActionLink('show') . '" data-dialog>'
- . htmlReady($termin->room_booking->resource->name) . '</a>';
+ default: {
+ $rooms = $termin->getRooms();
+ foreach ($rooms as $room) {
+ $ret .= '<a href="' . $room->getActionLink('show') . '" data-dialog>'
+ . htmlReady($room->name) . '</a>';
+ }
break;
+ }
}
}
return $ret;
diff --git a/lib/extern/ExternPageTimetable.php b/lib/extern/ExternPageTimetable.php
index 1e0e66a..6823001 100644
--- a/lib/extern/ExternPageTimetable.php
+++ b/lib/extern/ExternPageTimetable.php
@@ -259,7 +259,7 @@ class ExternPageTimetable extends ExternPage
'TYPE' => $GLOBALS['TERMIN_TYP'][$date->date_typ]['name'],
'LECTURERS' => $this->getLecturers($date),
'TOPICS' => $this->getTopics($date),
- 'BOOKED_ROOM' => $this->getBookedRoomData($date)
+ 'BOOKED_ROOMS' => $this->getBookedRoomData($date)
];
return $date_content;
}
@@ -312,11 +312,18 @@ class ExternPageTimetable extends ExternPage
private function getBookedRoomData(CourseDate $date): array
{
- if ($date->room_booking) {
- return [
- 'NAME' => $date->room_booking->resource->name,
- 'ID' => $date->room_booking->resource->id
- ];
+ if ($date->room_bookings) {
+ $booking_data = [];
+ foreach ($date->room_bookings as $booking) {
+ $booking_data[] = [
+ 'NAME' => $booking->resource->name ?? '',
+ 'ID' => $booking->resource->id ?? ''
+ ];
+ }
+ uasort($booking_data, function($a, $b) {
+ return strnatcmp($a['NAME'], $b['NAME']);
+ });
+ return $booking_data;
}
return [];
}
diff --git a/lib/models/Course.php b/lib/models/Course.php
index 831fe07..d35c6c3 100644
--- a/lib/models/Course.php
+++ b/lib/models/Course.php
@@ -1997,7 +1997,7 @@ class Course extends SimpleORMap implements Range, PrivacyObject, StudipItem, Fe
}
$dates->uasort(function($a, $b) {
return $a->date - $b->date
- ?: strnatcasecmp($a->getRoomName(), $b->getRoomName());
+ ?: strnatcasecmp($a->getRoomNames(), $b->getRoomNames());
});
return $dates;
}
diff --git a/lib/models/CourseDate.php b/lib/models/CourseDate.php
index 038a120..87a56e8 100644
--- a/lib/models/CourseDate.php
+++ b/lib/models/CourseDate.php
@@ -29,7 +29,7 @@
* @property User $author belongs_to User
* @property Course $course belongs_to Course
* @property SeminarCycleDate|null $cycle belongs_to SeminarCycleDate
- * @property ResourceBooking $room_booking has_one ResourceBooking
+ * @property ResourceBooking[] $room_bookings has_many ResourceBooking
* @property SimpleORMapCollection<CourseTopic> $topics has_and_belongs_to_many CourseTopic
* @property SimpleORMapCollection<Statusgruppen> $statusgruppen has_and_belongs_to_many Statusgruppen
* @property SimpleORMapCollection<User> $dozenten has_and_belongs_to_many User
@@ -89,7 +89,7 @@ class CourseDate extends SimpleORMap implements PrivacyObject, Event
'class_name' => SeminarCycleDate::class,
'foreign_key' => 'metadate_id'
];
- $config['has_one']['room_booking'] = [
+ $config['has_many']['room_bookings'] = [
'class_name' => ResourceBooking::class,
'foreign_key' => 'termin_id',
'assoc_foreign_key' => 'range_id',
@@ -225,25 +225,49 @@ class CourseDate extends SimpleORMap implements PrivacyObject, Event
*
* @return String containing the room name
*/
- public function getRoomName()
+ public function getRoomNames()
{
- if (Config::get()->RESOURCES_ENABLE && isset($this->room_booking->resource)) {
- return $this->getRoom()->name;
+ if (Config::get()->RESOURCES_ENABLE) {
+ $room_names = [];
+ foreach ($this->getRooms() as $room) {
+ $room_names[] = $room->getFullName();
+ }
+ if (!empty($room_names)) {
+ return implode(', ', $room_names);
+ }
}
+ //The room management is disabled or there are no room bookings for this course date.
+ //Maybe a freetext room name exists.
return $this->raum ?? '';
}
/**
- * Returns the assigned room for this date as an object.
+ * Returns the assigned rooms for this date as objects.
*
- * @return Resource Either the object or null if no room is assigned
+ * @return Room[] Either the room objects or an empty array if no rooms are assigned.
*/
- public function getRoom()
- {
- if (Config::get()->RESOURCES_ENABLE && !empty($this->room_booking->resource)) {
- return $this->room_booking->resource->getDerivedClassInstance();
+ public function getRooms() : array
+ {
+ if (Config::get()->RESOURCES_ENABLE) {
+ $rooms = [];
+ foreach ($this->room_bookings as $booking) {
+ $room = $booking->resource->getDerivedClassInstance();
+ if ($room instanceof Room) {
+ $rooms[] = $room;
+ }
+ }
+ uasort($rooms, function ($a, $b) {
+ if ($a->name < $b->name) {
+ return -1;
+ } elseif ($a->name > $b->name) {
+ return 1;
+ } else {
+ return 0;
+ }
+ });
+ return $rooms;
}
- return null;
+ return [];
}
/**
@@ -271,32 +295,21 @@ class CourseDate extends SimpleORMap implements PrivacyObject, Event
$this->raum = '';
$this->store();
- //If there is already a room assigned, "change" the booking.
- //Otherwise, create a new one.
- if ($this->room_booking instanceof ResourceBooking) {
- $this->room_booking->begin = $this->date;
- $this->room_booking->end = $this->end_time;
- $this->room_booking->resource_id = $room->id;
- $this->room_booking->preparation_time = $preparation_time * 60;
- $this->room_booking->subsequent_time = $subsequent_time * 60;
- $this->room_booking->store();
- } else {
- $room->createBooking(
- User::findCurrent(),
- $this->id,
- [['begin' => $this->date, 'end' => $this->end_time]],
- null,
- 0,
- null,
- $preparation_time * 60,
- '',
- '',
- ResourceBooking::TYPE_NORMAL,
- false,
- '',
- $subsequent_time * 60
- );
- }
+ $room->createBooking(
+ User::findCurrent(),
+ $this->id,
+ [['begin' => $this->date, 'end' => $this->end_time]],
+ null,
+ 0,
+ null,
+ $preparation_time,
+ '',
+ '',
+ ResourceBooking::TYPE_NORMAL,
+ false,
+ '',
+ $subsequent_time
+ );
return true;
}
@@ -354,13 +367,15 @@ class CourseDate extends SimpleORMap implements PrivacyObject, Event
}
if (in_array($format, ['include-room', 'long-include-room'])) {
- $room = $this->getRoom();
- if ($room) {
- $string = sprintf('%s <a href="%s" target="_blank">%s</a>',
- $string,
- $room->getActionURL('booking_plan'),
- htmlReady($room->name)
- );
+ $rooms = $this->getRooms();
+ if ($rooms) {
+ foreach ($rooms as $room) {
+ $string = sprintf('%s <a href="%s" target="_blank">%s</a>',
+ $string,
+ $room->getActionURL('booking_plan'),
+ htmlReady($room->name)
+ );
+ }
} elseif ($this->raum) {
//Use the freetext room name:
$string .= ' ' . $this->raum;
@@ -402,10 +417,18 @@ class CourseDate extends SimpleORMap implements PrivacyObject, Event
$ex_date = new CourseExDate();
$ex_date->setData($date);
- if ($room = $this->getRoom()) {
- $ex_date['resource_id'] = $room->getId();
- }
$ex_date->setId($ex_date->getNewId());
+ if ($rooms = $this->getRooms()) {
+ $db = DBManager::get();
+ $stmt = $db->prepare(
+ "INSERT INTO `ex_termin_rooms` (`ex_termin_id`, `room_id`, `mkdate`, `chdate`)
+ VALUES (:date_id, :room_id, UNIX_TIMESTAMP(), UNIX_TIMESTAMP())"
+ );
+ foreach ($rooms as $room) {
+ $stmt->execute(['date_id' => $ex_date->id, 'room_id' => $room->id]);
+ }
+ }
+
if ($ex_date->store()) {
//Update some (but not all) relations to the date so that they
@@ -441,7 +464,7 @@ class CourseDate extends SimpleORMap implements PrivacyObject, Event
public function store()
{
// load room-booking, if any
- $this->room_booking;
+ $this->room_bookings;
$cache = \Studip\Cache\Factory::getCache();
$cache->expire('course/undecorated_data/'. $this->range_id);
@@ -489,8 +512,8 @@ class CourseDate extends SimpleORMap implements PrivacyObject, Event
$warnings[] = _('Diesem Termin ist ein Thema zugeordnet.');
}
- if (Config::get()->RESOURCES_ENABLE && $this->getRoom()) {
- $warnings[] = _('Dieser Termin hat eine Raumbuchung, welche mit dem Termin gelöscht wird.');
+ if (Config::get()->RESOURCES_ENABLE && $this->getRooms()) {
+ $warnings[] = _('Dieser Termin hat Raumbuchungen, die mit dem Termin gelöscht werden.');
}
return $warnings;
@@ -628,7 +651,7 @@ class CourseDate extends SimpleORMap implements PrivacyObject, Event
public function getLocation(): string
{
- return $this->getRoomName();
+ return $this->getRoomNames();
}
public function getUniqueId(): string
diff --git a/lib/models/CourseExDate.php b/lib/models/CourseExDate.php
index ee38f2f..59a7947 100644
--- a/lib/models/CourseExDate.php
+++ b/lib/models/CourseExDate.php
@@ -97,7 +97,7 @@ class CourseExDate extends SimpleORMap implements PrivacyObject, Event
*
* @return String that is always empty
*/
- public function getRoomName()
+ public function getRoomNames()
{
return '';
}
@@ -107,9 +107,9 @@ class CourseExDate extends SimpleORMap implements PrivacyObject, Event
*
* @return null. always. canceled dates need no room.
*/
- public function getRoom()
+ public function getRooms()
{
- return null;
+ return [];
}
/**
@@ -339,7 +339,7 @@ class CourseExDate extends SimpleORMap implements PrivacyObject, Event
public function getLocation(): string
{
- return $this->getRoomName();
+ return $this->getRoomNames();
}
public function getUniqueId(): string
diff --git a/lib/models/SeminarCycleDate.php b/lib/models/SeminarCycleDate.php
index 4f7b6dc..e400665 100644
--- a/lib/models/SeminarCycleDate.php
+++ b/lib/models/SeminarCycleDate.php
@@ -539,17 +539,19 @@ class SeminarCycleDate extends SimpleORMap
$date->date = mktime(date('G', strtotime($this->start_time)), date('i', strtotime($this->start_time)), 0, date('m', $tos), date('d', $tos), date('Y', $tos)) + $day * 24 * 60 * 60;
$date->end_time = mktime(date('G', strtotime($this->end_time)), date('i', strtotime($this->end_time)), 0, date('m', $toe), date('d', $toe), date('Y', $toe)) + $day * 24 * 60 * 60;
- if ($date instanceof CourseDate && !is_null($date->room_booking)) {
+ if ($date instanceof CourseDate && !empty($date->room_bookings)) {
//Check if the time range of the date has decreased and did not exceed the
- //boundaries of the existing room booking. In that case, the room booking is shortened.
- if ($date->date < $tos || $date->end_time > $toe) {
- //The room booking must be deleted.
- $date->room_booking->delete();
- } else {
- //The room booking must be shortened.
- $date->room_booking->begin = $date->date;
- $date->room_booking->end = $date->end_time;
- $date->room_booking->store();
+ //boundaries of the existing room booking. In that case, the room bookings are shortened.
+ foreach ($date->room_bookings as $room_booking) {
+ if ($date->date < $tos || $date->end_time > $toe) {
+ //The room booking must be deleted.
+ $room_booking->delete();
+ } else {
+ //The room booking must be shortened.
+ $room_booking->begin = $date->date;
+ $room_booking->end = $date->end_time;
+ $room_booking->store();
+ }
}
}
@@ -1037,7 +1039,7 @@ class SeminarCycleDate extends SimpleORMap
$booked_c = 0;
foreach ($this->dates as $course_date) {
- if ($course_date->room_booking) {
+ if (count($course_date->room_bookings)) {
$booked_c++;
}
}
@@ -1088,7 +1090,7 @@ class SeminarCycleDate extends SimpleORMap
//List the dates that have no room booking:
$unbooked_dates = [];
foreach ($this->dates as $course_date) {
- if (!$course_date->room_booking) {
+ if (empty($course_date->room_bookings)) {
$unbooked_dates[] = $course_date;
}
}
diff --git a/lib/models/resources/Resource.php b/lib/models/resources/Resource.php
index 8a7fd06..9730128 100644
--- a/lib/models/resources/Resource.php
+++ b/lib/models/resources/Resource.php
@@ -93,6 +93,13 @@ class Resource extends SimpleORMap implements StudipItem
'foreign_key' => 'parent_id'
];
+ $config['has_and_belongs_to_many']['separable_room'] = [
+ 'class_name' => SeparableRoom::class,
+ 'thru_table' => 'separable_room_parts',
+ 'thru_key' => 'room_id',
+ 'thru_assoc_key' => 'separable_room_id'
+ ];
+
$config['i18n_fields']['description'] = true;
$config['additional_fields']['class_name'] = ['category', 'class_name'];
diff --git a/lib/models/resources/ResourceBooking.php b/lib/models/resources/ResourceBooking.php
index 805cff0..ef98e71 100644
--- a/lib/models/resources/ResourceBooking.php
+++ b/lib/models/resources/ResourceBooking.php
@@ -435,24 +435,7 @@ class ResourceBooking extends SimpleORMap implements PrivacyObject, Studip\Calen
}
$this->deleteOverlappingReservations();
- if (parent::store()) {
- //Check if the booking is bound to a course date.
- //If this is the case, check for existing bookings
- //and delete them, so that there is only one booking
- //for a course date:
- $course_date_exists = CourseDate::exists($this->range_id);
- if ($course_date_exists) {
- self::deleteBySql(
- 'range_id = :range_id AND id <> :this_id',
- [
- 'this_id' => $this->id,
- 'range_id' => $this->range_id
- ]
- );
- }
- return true;
- }
- return false;
+ return parent::store();
}
diff --git a/lib/models/resources/ResourceRequest.php b/lib/models/resources/ResourceRequest.php
index 2d08ee7..12a1e41 100644
--- a/lib/models/resources/ResourceRequest.php
+++ b/lib/models/resources/ResourceRequest.php
@@ -983,11 +983,17 @@ class ResourceRequest extends SimpleORMap implements PrivacyObject, Studip\Calen
];
}
- $date = CourseDate::find($appointment->appointment_id);
+ $date = CourseDate::find($appointment->appointment_id);
+ $booked_rooms = [];
+ $booking_ids = [];
+ foreach ($date->room_bookings as $booking) {
+ $booked_rooms[] = $booking->resource_id;
+ $booking_ids[] = $booking->id;
+ }
$interval['range'] = 'CourseDate';
$interval['range_id'] = $appointment->appointment_id;
- $interval['booked_room'] = $date->room_booking->resource_id ?? '';
- $interval['booking_id'] = $date->room_booking->id ?? '';
+ $interval['booked_rooms'] = $booked_rooms;
+ $interval['booking_ids'] = $booking_ids;
$time_intervals['']['intervals'][] = $interval;
}
@@ -1012,11 +1018,17 @@ class ResourceRequest extends SimpleORMap implements PrivacyObject, Studip\Calen
];
}
- $date = CourseDate::find($this->termin_id);
- $interval['range'] = 'CourseDate';
- $interval['range_id'] = $this->termin_id;
- $interval['booked_room'] = $date->room_booking->resource_id ?? null;
- $interval['booking_id'] = $date->room_booking->id ?? null;
+ $date = CourseDate::find($this->termin_id);
+ $booked_rooms = [];
+ $booking_ids = [];
+ foreach ($date->room_bookings as $booking) {
+ $booked_rooms[] = $booking->resource_id;
+ $booking_ids[] = $booking->id;
+ }
+ $interval['range'] = 'CourseDate';
+ $interval['range_id'] = $this->termin_id;
+ $interval['booked_rooms'] = $booked_rooms;
+ $interval['booking_ids'] = $booking_ids;
if (!empty($interval)) {
return [
@@ -1050,10 +1062,16 @@ class ResourceRequest extends SimpleORMap implements PrivacyObject, Studip\Calen
'end' => $date->end_time
];
}
+ $booked_rooms = [];
+ $booking_ids = [];
+ foreach ($date->room_bookings as $booking) {
+ $booked_rooms[] = $booking->resource_id;
+ $booking_ids[] = $booking->id;
+ }
$interval['range'] = 'CourseDate';
$interval['range_id'] = $date->id;
- $interval['booked_room'] = $date->room_booking->resource_id ?? null;
- $interval['booking_id'] = $date->room_booking->id ?? null;
+ $interval['booked_rooms'] = $booked_rooms;
+ $interval['booking_ids'] = $booking_ids;
$time_intervals[$this->metadate_id]['intervals'][] = $interval;
}
return $time_intervals;
@@ -1081,10 +1099,16 @@ class ResourceRequest extends SimpleORMap implements PrivacyObject, Studip\Calen
'end' => $date->end_time
];
}
+ $booked_rooms = [];
+ $booking_ids = [];
+ foreach ($date->room_bookings as $booking) {
+ $booked_rooms[] = $booking->resource_id;
+ $booking_ids[] = $booking->id;
+ }
$interval['range'] = CourseDate::class;
- $interval['range_id'] = $date->id;
- $interval['booked_room'] = $date->room_booking->resource_id ?? null;
- $interval['booking_id'] = $date->room_booking->id ?? null;
+ $interval['range_id'] = $date->id;
+ $interval['booked_rooms'] = $booked_rooms;
+ $interval['booking_ids'] = $booking_ids;
$time_intervals[$cycle->id]['intervals'][] = $interval;
}
}
@@ -1114,10 +1138,17 @@ class ResourceRequest extends SimpleORMap implements PrivacyObject, Studip\Calen
'end' => $date->end_time
];
}
+
+ $booked_rooms = [];
+ $booking_ids = [];
+ foreach ($date->room_bookings as $booking) {
+ $booked_rooms[] = $booking->resource_id;
+ $booking_ids[] = $booking->id;
+ }
$interval['range'] = 'CourseDate';
$interval['range_id'] = $date->id;
- $interval['booked_room'] = $date->room_booking->resource_id ?? null;
- $interval['booking_id'] = $date->room_booking->id ?? null;
+ $interval['booked_rooms'] = $booked_rooms;
+ $interval['booking_ids'] = $booking_ids;
$time_intervals['']['intervals'][] = $interval;
}
@@ -1141,8 +1172,10 @@ class ResourceRequest extends SimpleORMap implements PrivacyObject, Studip\Calen
'end' => $this->end
];
}
- $interval['range'] = 'User';
- $interval['range_id'] = $this->user_id;
+ $interval['range'] = 'User';
+ $interval['range_id'] = $this->user_id;
+ $interval['booked_rooms'] = [];
+ $interval['booking_ids'] = [];
return [
'' => [
@@ -1209,11 +1242,16 @@ class ResourceRequest extends SimpleORMap implements PrivacyObject, Studip\Calen
}
if ($with_range) {
$date = CourseDate::find($appointment->appointment_id);
-
- $interval['range'] = ResourceRequestAppointment::class;
- $interval['range_id'] = $appointment->appointment_id;
- $interval['booked_room'] = $date->room_booking->resource_id ?? null;
- $interval['booking_id'] = $date->room_booking->id ?? null;
+ $booked_rooms = [];
+ $booking_ids = [];
+ foreach ($date->room_bookings as $booking) {
+ $booked_rooms[] = $booking->resource_id;
+ $booking_ids[] = $booking->id;
+ }
+ $interval['range'] = ResourceRequestAppointment::class;
+ $interval['range_id'] = $appointment->appointment_id;
+ $interval['booked_rooms'] = $booked_rooms;
+ $interval['booking_ids'] = $booking_ids;
}
$time_intervals[] = $interval;
@@ -1235,10 +1273,18 @@ class ResourceRequest extends SimpleORMap implements PrivacyObject, Studip\Calen
];
}
if ($with_range) {
- $interval['range'] = CourseDate::class;
- $interval['range_id'] = $this->termin_id;
- $interval['booked_room'] = $this->date->room_booking->resource_id ?? null;
- $interval['booking_id'] = $this->date->room_booking->id ?? null;
+ if ($this->date->room_bookings) {
+ $booked_rooms = [];
+ $booking_ids = [];
+ foreach ($this->date->room_bookings as $booking) {
+ $booked_rooms[] = $booking->resource_id;
+ $booking_ids[] = $booking->id;
+ }
+ }
+ $interval['range'] = CourseDate::class;
+ $interval['range_id'] = $this->termin_id;
+ $interval['booked_rooms'] = $booked_rooms;
+ $interval['booking_ids'] = $booking_ids;
}
return [$interval];
} elseif ($this->metadate_id) {
@@ -1259,10 +1305,19 @@ class ResourceRequest extends SimpleORMap implements PrivacyObject, Studip\Calen
];
}
if ($with_range) {
- $interval['range'] = CourseDate::class;
- $interval['range_id'] = $date->id;
- $interval['booked_room'] = $date->room_booking->resource_id ?? null;
- $interval['booking_id'] = $date->room_booking->id ?? null;
+ if ($this->date->room_bookings) {
+ $booked_rooms = [];
+ $booking_ids = [];
+ foreach ($this->date->room_bookings as $booking) {
+ $booked_rooms[] = $booking->resource_id;
+ $booking_ids[] = $booking->id;
+ }
+ }
+
+ $interval['range'] = CourseDate::class;
+ $interval['range_id'] = $date->id;
+ $interval['booked_rooms'] = $booked_rooms;
+ $interval['booking_ids'] = $booking_ids;
}
$time_intervals[] = $interval;
}
@@ -1286,10 +1341,19 @@ class ResourceRequest extends SimpleORMap implements PrivacyObject, Studip\Calen
];
}
if ($with_range) {
- $interval['range'] = CourseDate::class;
- $interval['range_id'] = $date->id;
- $interval['booked_room'] = $date->room_booking->resource_id ?? null;
- $interval['booking_id'] = $date->room_booking->id ?? null;
+ if ($this->date->room_bookings) {
+ $booked_rooms = [];
+ $booking_ids = [];
+ foreach ($this->date->room_bookings as $booking) {
+ $booked_rooms[] = $booking->resource_id;
+ $booking_ids[] = $booking->id;
+ }
+ }
+
+ $interval['range'] = CourseDate::class;
+ $interval['range_id'] = $date->id;
+ $interval['booked_rooms'] = $booked_rooms;
+ $interval['booking_ids'] = $booking_ids;
}
$time_intervals[] = $interval;
}
@@ -1333,11 +1397,14 @@ class ResourceRequest extends SimpleORMap implements PrivacyObject, Studip\Calen
if ($interval['range'] === 'CourseDate') {
$date = call_user_func([$interval['range'], 'find'], $interval['range_id']);
- if ($date->room_booking) {
- $room_obj = Room::find($date->room_booking->resource_id);
- if ($room_obj) {
- $room = $room_obj->name;
+ if ($date->room_bookings) {
+ $room_names = [];
+ foreach ($date->room_bookings as $room_booking) {
+ if (!empty($room_booking->resource)) {
+ $room_names[] = $room_booking->resource->name;
+ }
}
+ $room = implode(', ', $room_names);
}
}
diff --git a/lib/resources/RoomManager.php b/lib/resources/RoomManager.php
index 9036c4e..5746115 100644
--- a/lib/resources/RoomManager.php
+++ b/lib/resources/RoomManager.php
@@ -527,6 +527,10 @@ class RoomManager
* partially available in those time ranges (false).
* Defaults to true.
*
+ * @param string[] $excluded_booking_ids The IDs of bookings that shall be
+ * excluded when checking the availability of rooms in the specified
+ * time ranges.
+ *
* @return array
*/
public static function findRooms(
@@ -538,7 +542,8 @@ class RoomManager
$order_by = null,
$only_requestable_rooms = true,
$excluded_room_ids = [],
- $only_fully_available = true
+ $only_fully_available = true,
+ $excluded_booking_ids = []
)
{
$sql = "INNER JOIN resource_categories rc
@@ -649,7 +654,7 @@ class RoomManager
$end = new DateTime();
$end->setTimestamp($time_range['end']);
}
- if ($room->isAvailable($begin, $end)) {
+ if ($room->isAvailable($begin, $end, $excluded_booking_ids)) {
if (!$only_fully_available) {
$room_is_available = true;
break;