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/classes | |
| parent | f637e7ae2d086941a11297ccc29ac273ad6759b0 (diff) | |
allow booking separable rooms in courses, closes #639
Closes #639
Merge request studip/studip!4039
Diffstat (limited to 'lib/classes')
| -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 |
6 files changed, 224 insertions, 18 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, |
