diff options
| author | Moritz Strohm <strohm@data-quest.de> | 2026-01-14 10:29:35 +0000 |
|---|---|---|
| committer | Moritz Strohm <strohm@data-quest.de> | 2026-01-14 10:29:35 +0000 |
| commit | 78e46de33b3f205aae375d1ea6d4fe088e0e5124 (patch) | |
| tree | 4b305bf3f7b5d066ac28f011fe752e98901e714c /lib | |
| parent | f637e7ae2d086941a11297ccc29ac273ad6759b0 (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.php | 24 | ||||
| -rw-r--r-- | lib/classes/InstituteCalendarHelper.php | 10 | ||||
| -rw-r--r-- | lib/classes/JsonApi/RouteMap.php | 3 | ||||
| -rw-r--r-- | lib/classes/JsonApi/Routes/AvailableRooms.php | 201 | ||||
| -rw-r--r-- | lib/classes/JsonApi/SchemaMap.php | 2 | ||||
| -rw-r--r-- | lib/classes/calendar/ICalendarExport.php | 2 | ||||
| -rw-r--r-- | lib/dates.inc.php | 14 | ||||
| -rw-r--r-- | lib/extern/ExternPageTimetable.php | 19 | ||||
| -rw-r--r-- | lib/models/Course.php | 2 | ||||
| -rw-r--r-- | lib/models/CourseDate.php | 127 | ||||
| -rw-r--r-- | lib/models/CourseExDate.php | 8 | ||||
| -rw-r--r-- | lib/models/SeminarCycleDate.php | 26 | ||||
| -rw-r--r-- | lib/models/resources/Resource.php | 7 | ||||
| -rw-r--r-- | lib/models/resources/ResourceBooking.php | 19 | ||||
| -rw-r--r-- | lib/models/resources/ResourceRequest.php | 143 | ||||
| -rw-r--r-- | lib/resources/RoomManager.php | 9 |
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; |
