diff --git a/backend/api/coworking/ambassador.py b/backend/api/coworking/ambassador.py
index cc6e9e9ca..8e0e2052b 100644
--- a/backend/api/coworking/ambassador.py
+++ b/backend/api/coworking/ambassador.py
@@ -17,15 +17,26 @@
api = APIRouter(prefix="/api/coworking/ambassador")
-@api.get("", tags=["Coworking"])
-def active_and_upcoming_reservations(
+@api.get("/xl", tags=["Coworking"])
+def active_and_upcoming_reservations_for_xl(
subject: User = Depends(registered_user),
reservation_svc: ReservationService = Depends(),
) -> Sequence[Reservation]:
- """List active and upcoming reservations.
+ """List active and upcoming reservations for the XL.
This list drives the ambassador's checkin UI."""
- return reservation_svc.list_all_active_and_upcoming(subject)
+ return reservation_svc.list_all_active_and_upcoming_for_xl(subject)
+
+
+@api.get("/rooms", tags=["Coworking"])
+def active_and_upcoming_reservations_for_rooms(
+ subject: User = Depends(registered_user),
+ reservation_svc: ReservationService = Depends(),
+) -> Sequence[Reservation]:
+ """List active and upcoming reservations for the rooms.
+
+ This list drives the ambassador's checkin UI."""
+ return reservation_svc.list_all_active_and_upcoming_for_rooms(subject)
@api.put("/checkin", tags=["Coworking"])
diff --git a/backend/api/coworking/reservation.py b/backend/api/coworking/reservation.py
index 8af862931..3f4877d2d 100644
--- a/backend/api/coworking/reservation.py
+++ b/backend/api/coworking/reservation.py
@@ -3,17 +3,22 @@
This API is used to make and manage reservations."""
from fastapi import APIRouter, Depends, HTTPException
+from typing import Sequence
+from datetime import datetime
+
+from backend.models.room import Room
from ..authentication import registered_user
-from ...services.coworking.reservation import ReservationService
+from ...services.coworking.reservation import ReservationException, ReservationService
from ...models import User
from ...models.coworking import (
Reservation,
ReservationRequest,
ReservationPartial,
ReservationState,
+ ReservationMapDetails
)
-__authors__ = ["Kris Jordan"]
+__authors__ = ["Kris Jordan, Yuvraj Jain"]
__copyright__ = "Copyright 2023"
__license__ = "MIT"
@@ -44,6 +49,20 @@ def get_reservation(
return reservation_svc.get_reservation(subject, id)
+@api.get("/room-reservations/", tags=["Coworking"])
+def get_all_reservations_by_state(
+ state: ReservationState,
+ subject: User = Depends(registered_user),
+ reservation_svc: ReservationService = Depends(),
+) -> Sequence[Reservation]:
+ try:
+ return reservation_svc.get_current_reservations_for_user(
+ subject=subject, focus=subject, state=state
+ )
+ except Exception as e:
+ raise HTTPException(status_code=404, detail=str(e))
+
+
@api.put("/reservation/{id}", tags=["Coworking"])
def update_reservation(
reservation: ReservationPartial,
@@ -64,3 +83,25 @@ def cancel_reservation(
return reservation_svc.change_reservation(
subject, ReservationPartial(id=id, state=ReservationState.CANCELLED)
)
+
+
+@api.get("/room-reservation/", tags=["Coworking"])
+def get_reservations_for_rooms_by_date(
+ date: datetime,
+ subject: User = Depends(registered_user),
+ reservation_svc: ReservationService = Depends(),
+) -> ReservationMapDetails:
+ """See available rooms for any given day."""
+ try:
+ return reservation_svc.get_map_reserved_times_by_date(date, subject)
+ except Exception as e:
+ raise HTTPException(status_code=404, detail=str(e))
+
+
+@api.get("/user-reservations/", tags=["Coworking"])
+def get_total_hours_study_room_reservations(
+ subject: User = Depends(registered_user),
+ reservation_svc: ReservationService = Depends(),
+) -> str:
+ """Allows a user to know how many hours they have reserved in all study rooms (Excludes CSXL)."""
+ return reservation_svc._get_total_time_user_reservations(subject)
diff --git a/backend/models/coworking/__init__.py b/backend/models/coworking/__init__.py
index 7c839c4c7..ef78a4d91 100644
--- a/backend/models/coworking/__init__.py
+++ b/backend/models/coworking/__init__.py
@@ -10,11 +10,12 @@
ReservationRequest,
ReservationState,
ReservationPartial,
+ ReservationMapDetails,
ReservationIdentity,
)
from .availability_list import AvailabilityList
-from .availability import SeatAvailability, RoomAvailability
+from .availability import RoomState, SeatAvailability, RoomAvailability
from .status import Status
diff --git a/backend/models/coworking/availability.py b/backend/models/coworking/availability.py
index b584af262..08bf9871c 100644
--- a/backend/models/coworking/availability.py
+++ b/backend/models/coworking/availability.py
@@ -1,5 +1,6 @@
"""Models for the availability of rooms and seats over a time range."""
+from enum import Enum
from pydantic import BaseModel, validator
from ..room import Room
@@ -7,6 +8,18 @@
from .time_range import TimeRange
from .availability_list import AvailabilityList
+__authors__ = ["Kris Jordan, Yuvraj Jain"]
+__copyright__ = "Copyright 2024"
+__license__ = "MIT"
+
+
+class RoomState(int, Enum):
+ AVAILABLE = 0
+ RESERVED = 1
+ SELECTED = 2
+ UNAVAILABLE = 3
+ SUBJECT_RESERVED = 4
+
class RoomAvailability(Room, AvailabilityList):
"""A room that is available for a given time range."""
diff --git a/backend/models/coworking/operating_hours.py b/backend/models/coworking/operating_hours.py
index f7c15366b..bec959b78 100644
--- a/backend/models/coworking/operating_hours.py
+++ b/backend/models/coworking/operating_hours.py
@@ -5,6 +5,10 @@
from .time_range import TimeRange
+__authors__ = ["Kris Jordan, Yuvraj Jain"]
+__copyright__ = "Copyright 2024"
+__license__ = "MIT"
+
class OperatingHours(TimeRange, BaseModel):
"""The operating hours of the XL."""
diff --git a/backend/models/coworking/reservation.py b/backend/models/coworking/reservation.py
index 48850bbad..dc4f3f3e9 100644
--- a/backend/models/coworking/reservation.py
+++ b/backend/models/coworking/reservation.py
@@ -2,10 +2,14 @@
from pydantic import BaseModel
from datetime import datetime
from ...models.user import User, UserIdentity
-from ..room import Room
+from ..room import Room, RoomPartial
from .seat import Seat, SeatIdentity
from .time_range import TimeRange
+__authors__ = ["Kris Jordan, Yuvraj Jain"]
+__copyright__ = "Copyright 2024"
+__license__ = "MIT"
+
class ReservationState(str, Enum):
DRAFT = "DRAFT"
@@ -22,6 +26,7 @@ class ReservationIdentity(BaseModel):
class ReservationRequest(TimeRange):
users: list[UserIdentity] = []
seats: list[SeatIdentity] = []
+ room: RoomPartial | None = None
class Reservation(ReservationIdentity, TimeRange):
@@ -34,6 +39,13 @@ class Reservation(ReservationIdentity, TimeRange):
updated_at: datetime
+class ReservationMapDetails(BaseModel):
+ reserved_date_map: dict[str, list[int]] = {}
+ operating_hours_start: datetime | None = None
+ operating_hours_end: datetime | None = None
+ number_of_time_slots: int | None = None
+
+
class ReservationPartial(Reservation, BaseModel):
start: datetime | None = None
end: datetime | None = None
diff --git a/backend/models/coworking/time_range.py b/backend/models/coworking/time_range.py
index 95220d082..e2c5cef3e 100644
--- a/backend/models/coworking/time_range.py
+++ b/backend/models/coworking/time_range.py
@@ -5,7 +5,7 @@
from pydantic import BaseModel, field_validator, ValidationInfo, validator
from typing import Self
-__authors__ = ["Kris Jordan"]
+__authors__ = ["Kris Jordan, Yuvraj Jain"]
__copyright__ = "Copyright 2023"
__license__ = "MIT"
diff --git a/backend/models/room.py b/backend/models/room.py
index 6e7aae3a0..431e272d3 100644
--- a/backend/models/room.py
+++ b/backend/models/room.py
@@ -7,7 +7,9 @@
__copyright__ = "Copyright 2023"
__license__ = "MIT"
-
-class Room(BaseModel):
+class RoomPartial(BaseModel):
id: str
+
+class Room(RoomPartial):
nickname: str = ""
+
diff --git a/backend/script/deployments/coworking_launch.py b/backend/script/deployments/coworking_launch.py
index 78ab33b8f..e75e343ae 100644
--- a/backend/script/deployments/coworking_launch.py
+++ b/backend/script/deployments/coworking_launch.py
@@ -15,9 +15,8 @@
from ...entities.coworking import SeatEntity, OperatingHoursEntity, RoomEntity
from ...test.services.reset_table_id_seq import reset_table_id_seq
-from ...test.services import role_data, user_data, permission_data
+from ...test.services import role_data, user_data, permission_data, room_data
from ...test.services.coworking import (
- room_data,
seat_data,
operating_hours_data,
time,
diff --git a/backend/script/reset_demo.py b/backend/script/reset_demo.py
index 2a4f7ce48..b1200f43a 100644
--- a/backend/script/reset_demo.py
+++ b/backend/script/reset_demo.py
@@ -21,7 +21,6 @@
from ..test.services import role_data, user_data, permission_data, room_data
from ..test.services.organization import organization_demo_data
from ..test.services.event import event_demo_data
-
from ..test.services.coworking import seat_data, operating_hours_data, time
from ..test.services.coworking.reservation import reservation_data
from ..test.services.academics import course_data, term_data, section_data
diff --git a/backend/services/coworking/policy.py b/backend/services/coworking/policy.py
index 723ef6f15..813eaac73 100644
--- a/backend/services/coworking/policy.py
+++ b/backend/services/coworking/policy.py
@@ -2,7 +2,7 @@
from fastapi import Depends
from sqlalchemy.orm import Session
-from datetime import timedelta
+from datetime import timedelta, datetime, time
from ...database import db_session
from ...models import User
@@ -10,6 +10,13 @@
__copyright__ = "Copyright 2023"
__license__ = "MIT"
+MONDAY = 0
+TUESDAY = 1
+WEDNESDAY = 2
+THURSDAY = 3
+FRIDAY = 4
+SATURDAY = 5
+SUNDAY = 6
class PolicyService:
"""RoleService is the access layer to the role data model, its members, and permissions.
@@ -18,8 +25,7 @@ class PolicyService:
for different groups of users (e.g. majors, ambassadors, LAs, etc).
"""
- def __init__(self):
- ...
+ def __init__(self): ...
def walkin_window(self, _subject: User) -> timedelta:
"""How far into the future can walkins be reserved?"""
@@ -51,6 +57,103 @@ def maximum_initial_reservation_duration(self, _subject: User) -> timedelta:
def reservation_draft_timeout(self) -> timedelta:
return timedelta(minutes=5)
+
def reservation_checkin_timeout(self) -> timedelta:
return timedelta(minutes=10)
+
+
+ def room_reservation_weekly_limit(self) -> timedelta:
+ """The maximum amount of hours a student can reserve the study rooms outside of the csxl."""
+ return timedelta(hours=6)
+
+ def non_reservable_rooms(self) -> list[str]:
+ return ['404']
+
+ def office_hours(self, date: datetime):
+ day = date.weekday()
+ if day == MONDAY:
+ return {
+ 'SN135' : [
+ (time(hour=16, minute=30), time(hour=17, minute=00)),
+ ],
+ 'SN137' : [
+ (time(hour=15, minute=00), time(hour=16, minute=30))
+ ],
+ 'SN139' : [],
+ 'SN141' : [
+ (time(hour=16, minute=00), time(hour=17, minute=30))
+ ]
+ }
+ elif day == TUESDAY:
+ return {
+ 'SN135' : [
+ (time(hour=14, minute=30), time(hour=16, minute=00)),
+ (time(hour=11, minute=00), time(hour=12, minute=00))
+ ],
+ 'SN137' : [],
+ 'SN139' : [
+ (time(hour=10, minute=30), time(hour=11, minute=00)),
+ (time(hour=11, minute=30), time(hour=13, minute=00)),
+ ],
+ 'SN141' : [
+ (time(hour=10, minute=00), time(hour=11, minute=00))
+ ]
+ }
+ elif day == WEDNESDAY:
+ return {
+ 'SN135' : [
+ (time(hour=11, minute=00), time(hour=12, minute=00))
+ ],
+ 'SN137' : [],
+ 'SN139' : [
+ (time(hour=10, minute=30), time(hour=11, minute=00)),
+ (time(hour=11, minute=30), time(hour=13, minute=00)),
+ (time(hour=14, minute=30), time(hour=15, minute=00))
+ ],
+ 'SN141' : [
+ (time(hour=10, minute=00), time(hour=11, minute=00))
+ ]
+ }
+ elif day == THURSDAY:
+ return {
+ 'SN135' : [
+ (time(hour=14, minute=30), time(hour=16, minute=00)),
+ (time(hour=11, minute=00), time(hour=12, minute=00))
+ ],
+ 'SN137' : [],
+ 'SN139' : [
+ (time(hour=10, minute=30), time(hour=11, minute=00)),
+ (time(hour=11, minute=30), time(hour=13, minute=00)),
+ (time(hour=14, minute=30), time(hour=15, minute=00))
+ ],
+ 'SN141' : []
+ }
+ elif day == FRIDAY:
+ return {
+ 'SN135' : [
+ (time(hour=11, minute=00), time(hour=12, minute=00))
+ ],
+ 'SN137' : [],
+ 'SN139' : [
+ (time(hour=10, minute=30), time(hour=11, minute=00)),
+ (time(hour=14, minute=30), time(hour=15, minute=00))
+ ],
+ 'SN141' : [
+ (time(hour=10, minute=00), time(hour=11, minute=00))
+ ]
+ }
+ elif day == SATURDAY:
+ return {
+ 'SN135' : [],
+ 'SN137' : [],
+ 'SN139' : [],
+ 'SN141' : []
+ }
+ elif day == SUNDAY:
+ return {
+ 'SN135' : [],
+ 'SN137' : [],
+ 'SN139' : [],
+ 'SN141' : []
+ }
\ No newline at end of file
diff --git a/backend/services/coworking/reservation.py b/backend/services/coworking/reservation.py
index 0ccac6643..38b38d5bc 100644
--- a/backend/services/coworking/reservation.py
+++ b/backend/services/coworking/reservation.py
@@ -5,17 +5,22 @@
from random import random
from typing import Sequence
from sqlalchemy.orm import Session, joinedload
+from backend.entities.room_entity import RoomEntity
+
+from backend.models.room_details import RoomDetails
from ...database import db_session
from ...models.user import User, UserIdentity
from ..exceptions import UserPermissionException, ResourceNotFoundException
from ...models.coworking import (
Seat,
Reservation,
+ ReservationMapDetails,
ReservationRequest,
ReservationPartial,
TimeRange,
SeatAvailability,
ReservationState,
+ RoomState,
AvailabilityList,
OperatingHours,
)
@@ -26,8 +31,8 @@
from .operating_hours import OperatingHoursService
from ..permission import PermissionService
-__authors__ = ["Kris Jordan", "Matt Vu"]
-__copyright__ = "Copyright 2023 - 2024"
+__authors__ = ["Kris Jordan", "Matt Vu","Yuvraj Jain"]
+__copyright__ = "Copyright 2023"
__license__ = "MIT"
@@ -92,7 +97,7 @@ def get_reservation(self, subject: User, id: int) -> Reservation:
return reservation.to_model()
def get_current_reservations_for_user(
- self, subject: User, focus: User
+ self, subject: User, focus: User, state: ReservationState | None = None
) -> Sequence[Reservation]:
"""Find current and upcoming reservations for a given user.
The subject must either also be the focus or have permission to view reservations of
@@ -114,12 +119,18 @@ def get_current_reservations_for_user(
"coworking.reservation.read",
f"user/{focus.id}",
)
- #
+
now = datetime.now()
time_range = TimeRange(
start=now - timedelta(days=1),
end=now + self._policy_svc.reservation_window(focus),
)
+
+ if state:
+ return self._get_active_reservations_for_user_by_state(
+ focus, time_range, state
+ )
+
return self._get_active_reservations_for_user(focus, time_range)
def _get_active_reservations_for_user(
@@ -149,6 +160,374 @@ def _get_active_reservations_for_user(
return [reservation.to_model() for reservation in reservations]
+ def _get_active_reservations_for_user_by_state(
+ self,
+ focus: UserIdentity,
+ time_range: TimeRange,
+ state: ReservationState,
+ ) -> Sequence[Reservation]:
+ reservations = (
+ self._session.query(ReservationEntity)
+ .join(ReservationEntity.users)
+ .filter(
+ ReservationEntity.start < time_range.end,
+ ReservationEntity.end > time_range.start,
+ ReservationEntity.state == state,
+ UserEntity.id == focus.id,
+ )
+ .options(
+ joinedload(ReservationEntity.users), joinedload(ReservationEntity.seats)
+ )
+ .order_by(ReservationEntity.start)
+ .all()
+ )
+
+ reservations = self._state_transition_reservation_entities_by_time(
+ datetime.now(), reservations
+ )
+
+ return [reservation.to_model() for reservation in reservations]
+
+ def _check_user_reservation_duration(
+ self, user: UserIdentity, bounds: TimeRange
+ ) -> bool:
+ """Helper method to check if the total reservation duration for a user exceeds 6 hours.
+
+ Args:
+ user (User): The user for whom to check reservation duration.
+ bounds (TimeRange): The time range to check for reservation duration.
+
+ Returns:
+ True if a user has >= 6 total hours reserved
+ False if a user has exceeded the limit
+ """
+ reservations = self.get_current_reservations_for_user(user, user)
+ total_duration = timedelta()
+ total_duration += bounds.end - bounds.start
+
+ for reservation in reservations:
+ if reservation.room:
+ total_duration += reservation.end - reservation.start
+ if total_duration > self._policy_svc.room_reservation_weekly_limit():
+ return False
+ return True
+
+ def _get_total_time_user_reservations(self, user: UserIdentity) -> str:
+ """Calculate the total duration (in hours) of study room reservations for the given user.
+ Args:
+ user (UserIdentity): The user for whom to calculate the total reservation time.
+ Returns:
+ str: The total reservation time in hours.
+ """
+ reservations = self.get_current_reservations_for_user(user, user)
+ duration = timedelta()
+ for reservation in reservations:
+ if reservation.room:
+ duration += reservation.end - reservation.start
+ str_duration = str(6 - (round((duration.total_seconds() / 3600) * 2) / 2))
+ if str_duration[2] == "0":
+ return str_duration.rstrip("0").rstrip(".")
+ return str_duration
+
+ def get_map_reserved_times_by_date(
+ self, date: datetime, subject: User
+ ) -> ReservationMapDetails:
+ """
+ Retrieves a detailed mapping of room reservation statuses for a specific date, tailored for a given user.
+
+ This method returns an instance of ReservationMapDetails, which includes:
+ - A dictionary (`reserved_date_map`) where keys are room IDs and values are lists of time slot statuses
+ for each room. Statuses are integers representing:
+ 0 (Available - Green)
+ 1 (Reserved - Red)
+ 2 (Selected - Orange)
+ 3 (Unavailable - Grayed out)
+ 4 (Subject's Reservation - Blue).
+ - The start (`operating_hours_start`) and end (`operating_hours_end`) times of operating hours for
+ the date queried.
+ - The total number of time slots (`number_of_time_slots`) available within the operating hours,
+ based on 30-minute intervals.
+
+ It handles various scenarios including days without operating hours by providing a default schedule
+ (10 am to 6 pm) and adjusting time slots based on current time to mark past slots as unavailable.
+ It supports rounding start and end times to the nearest half-hour and excludes reservations that
+ are outside the operating hours.
+
+ Args:
+ date (datetime): The date for which the reservation statuses are to be fetched.
+ subject (User): The user for whom the reservation statuses are being determined, to highlight
+ their own reservations.
+
+ Returns:
+ ReservationMapDetails: An object containing the mapping of room reservation statuses,
+ operating hours, and the number of time slots.
+
+ Note:
+ This method assumes individual user reservations. Group reservations require adjustments to
+ the implementation.
+
+ Future reservations are shown up to the current time, with past slots marked as unavailable
+ for today's date.
+ """
+ reserved_date_map: dict[str, list[int]] = {}
+
+ # Query DB to get reservable rooms. You can change coworking policy to change
+ # which rooms are reservable. SN156 should not go in coworking policy.
+ rooms = self._get_reservable_rooms()
+
+ # Generate a 1 day time range to get operating hours on date.
+ date_midnight = date.replace(hour=0, minute=0, second=0)
+ tomorrow_midnight = date_midnight + timedelta(days=1)
+ day_range = TimeRange(start=date_midnight, end=tomorrow_midnight)
+
+ # Check if operating hours exist on date
+ try:
+ operating_hours_on_date = self._operating_hours_svc.schedule(day_range)[0]
+ except:
+ # TODO: Possibly consider thowing exception and handling on the frontend?
+ # If operating hours don't exist, then return an all grayed out table
+ # from 10 am to 6 pm which is the standard office hours.
+ for room in rooms:
+ if room.id:
+ reserved_date_map[room.id] = [RoomState.UNAVAILABLE.value] * 16
+ return ReservationMapDetails(
+ reserved_date_map=reserved_date_map,
+ operating_hours_start=datetime.now().replace(hour=10, minute=0),
+ operating_hours_end=datetime.now().replace(hour=18, minute=0),
+ number_of_time_slots=16,
+ )
+
+ # Extract the start time and end time for operating hours rounded to the closest half hour
+ operating_hours_start = self._round_to_closest_half_hour(
+ operating_hours_on_date.start, round_up=True
+ )
+ operating_hours_end = self._round_to_closest_half_hour(
+ operating_hours_on_date.end, round_up=False
+ )
+ operating_hours_time_delta = operating_hours_end - operating_hours_start
+
+ # Multiply by 2 because 30 min interval indexes
+ operating_hours_duration = int(
+ 2 * operating_hours_time_delta.total_seconds() / 3600
+ )
+
+ # Need current time to gray out slots in the past on that day.
+ current_time = datetime.now()
+ current_time_idx = (
+ self._idx_calculation(current_time, operating_hours_start) + 1
+ )
+
+ for room in rooms:
+ time_slots_for_room = [0] * operating_hours_duration
+
+ # Making slots up till current time gray
+ if date.date() == current_time.date():
+ for i in range(0, current_time_idx):
+ time_slots_for_room[i] = RoomState.UNAVAILABLE.value
+
+ room_id = room.id if room else "SN156"
+ reservations = self._query_confirmed_reservations_by_date_and_room(
+ date, room_id
+ )
+ for reservation in reservations:
+ start_idx = self._idx_calculation(
+ reservation.start, operating_hours_start
+ )
+ end_idx = self._idx_calculation(reservation.end, operating_hours_start)
+
+ if start_idx < 0 or end_idx > operating_hours_duration:
+ continue
+
+ # Gray out previous time slots for today only
+ if date.date() == current_time.date():
+ if end_idx < current_time_idx:
+ continue
+ start_idx = max(current_time_idx, start_idx)
+
+ for idx in range(start_idx, end_idx):
+ # Currently only assuming single user.
+ # TODO: If making group reservations, need to change this.
+ if reservation.users[0].id == subject.id:
+ time_slots_for_room[idx] = RoomState.SUBJECT_RESERVED.value
+ else:
+ if time_slots_for_room[idx] != RoomState.SUBJECT_RESERVED.value:
+ time_slots_for_room[idx] = RoomState.RESERVED.value
+ reserved_date_map[room.id] = time_slots_for_room
+
+ self._transform_date_map_for_unavailable(reserved_date_map)
+ del reserved_date_map["SN156"]
+ self._transform_date_map_for_officehours(
+ date, reserved_date_map, operating_hours_start, operating_hours_duration
+ )
+
+ return ReservationMapDetails(
+ reserved_date_map=reserved_date_map,
+ operating_hours_start=operating_hours_start,
+ operating_hours_end=operating_hours_end,
+ number_of_time_slots=operating_hours_duration,
+ )
+
+ def _round_to_closest_half_hour(
+ self, dt: datetime, round_up: bool = True
+ ) -> datetime:
+ """
+ This helper rounds a datetime object to the closest half hour either up or down based on the round_up flag.
+
+ Args:
+ dt (datetime): The datetime object you want to round.
+ round_up (bool): If True, rounds up to the closest half hour. If False, rounds down to the closest half hour.
+
+ Returns:
+ datetime: Rounded datetime object.
+ """
+ minutes = dt.minute
+
+ if round_up:
+ if minutes < 30:
+ to_add = timedelta(minutes=(30 - minutes))
+ else:
+ to_add = timedelta(minutes=(60 - minutes))
+ rounded_dt = dt + to_add
+ else:
+ if minutes > 30:
+ to_subtract = timedelta(minutes=(minutes - 30))
+ else:
+ to_subtract = timedelta(minutes=minutes)
+ rounded_dt = dt - to_subtract
+
+ rounded_dt = rounded_dt.replace(second=0, microsecond=0)
+
+ return rounded_dt
+
+ def _idx_calculation(self, time: datetime, operating_hours_start: datetime) -> int:
+ """
+ Calculates the index of a time slot based on a given time.
+
+ This function converts a datetime object into an index representing a specific
+ time slot in the reservation system. Each hour is divided into two slots.
+
+ Args:
+ time (datetime): The time to convert into an index.
+ operating_hours_start (int): The hour when the XL opens as an int.
+
+ Returns:
+ int: The index of the time slot corresponding to the given time.
+ """
+ return int(2 * (time.hour - operating_hours_start.hour)) + (
+ (time.minute - operating_hours_start.minute) // 30
+ )
+
+ def _transform_date_map_for_unavailable(
+ self, reserved_date_map: dict[str, list[int]]
+ ) -> None:
+ """
+ Modifies the reserved date map to mark certain slots as unavailable.
+
+ This function updates the reserved date map so that if a slot is reserved by the subject
+ (indicated by a 4), then any available slots (indicated by 0) in the same column across
+ all rooms are marked as unavailable (changed to 3).
+
+ Args:
+ reserved_date_map (dict[str, list[int]]): The map of room reservations to be transformed.
+
+ Returns:
+ None: This function modifies the reserved_date_map in place.
+ """
+ # Identify the columns where 4 appears
+ columns_with_4 = set()
+ for key, values in reserved_date_map.items():
+ for i, value in enumerate(values):
+ if value == RoomState.SUBJECT_RESERVED.value:
+ columns_with_4.add(i)
+
+ # Transform the dictionary as per the rules
+ for key, values in reserved_date_map.items():
+ for i in columns_with_4:
+ if values[i] == RoomState.AVAILABLE.value:
+ values[i] = RoomState.UNAVAILABLE.value
+
+ def _transform_date_map_for_officehours(
+ self,
+ date: datetime,
+ reserved_date_map: dict[str, list[int]],
+ operating_hours_start: datetime,
+ operating_hours_duration: int,
+ ) -> None:
+ """
+ Transforms date map in place.
+ """
+ office_hours = self._policy_svc.office_hours(date=date)
+ for room_id, hours in office_hours.items():
+ for start, end in hours:
+ start_idx = max(self._idx_calculation(start, operating_hours_start), 0)
+ end_idx = min(
+ self._idx_calculation(end, operating_hours_start),
+ operating_hours_duration,
+ )
+ if start_idx < end_idx:
+ for idx in range(start_idx, end_idx):
+ reserved_date_map[room_id][idx] = RoomState.UNAVAILABLE.value
+
+ def _query_confirmed_reservations_by_date_and_room(
+ self, date: datetime, room_id: str
+ ) -> Sequence[Reservation]:
+ """
+ Queries and returns confirmed and checked-in reservations for a given date and room.
+
+ This function fetches all confirmed and checked-in reservations from the database for a specified date and room.
+ It includes reservations that have any overlap with the 24-hour period starting from the
+ beginning of the given date, and are associated with a specific room ID.
+
+ Args:
+ date (datetime): The date for which to query confirmed reservations.
+ room_id (str): The ID of the room for which to query confirmed reservations.
+
+ Returns:
+ Sequence[Reservation]: A sequence of Reservation model objects representing the confirmed reservations for the specified date and room.
+ """
+ start = date.replace(hour=0, minute=0, second=0, microsecond=0)
+ reservations = (
+ self._session.query(ReservationEntity)
+ .join(ReservationEntity.room)
+ .filter(
+ ReservationEntity.start < start + timedelta(hours=24),
+ ReservationEntity.end > start,
+ ReservationEntity.state.not_in(
+ [ReservationState.CANCELLED, ReservationState.CHECKED_OUT]
+ ),
+ RoomEntity.id == room_id,
+ )
+ .options(
+ joinedload(ReservationEntity.users), joinedload(ReservationEntity.seats)
+ )
+ .order_by(ReservationEntity.start)
+ .all()
+ )
+
+ return [reservation.to_model() for reservation in reservations]
+
+ def _get_reservable_rooms(self) -> Sequence[RoomDetails]:
+ """
+ Retrieves a list of all reservable rooms.
+ This method queries the RoomEntity table to find all rooms that are marked as reservable
+ (i.e., their 'reservable' attribute is True) and are not the room with ID 'SN156'.
+ The rooms are then ordered by their ID in ascending order.
+
+ Each room entity is converted to a RoomDetails model before being returned.
+
+ Returns:
+ Sequence[RoomDetails]: A sequence of RoomDetails models representing all the reservable rooms, excluding room 'SN156'.
+ """
+
+ rooms = (
+ self._session.query(RoomEntity)
+ .filter(RoomEntity.id.not_in(self._policy_svc.non_reservable_rooms()))
+ .order_by(RoomEntity.id)
+ .all()
+ )
+
+ return [room.to_details_model() for room in rooms]
+
def get_seat_reservations(
self, seats: Sequence[Seat], time_range: TimeRange
) -> Sequence[Reservation]:
@@ -381,6 +760,13 @@ def draft_reservation(
# Enforce request range is within bounds of walkin vs. pre-reserved policies
bounds = TimeRange(start=start, end=end)
+ # Check if user has exceeded reservation limit
+ if request.room:
+ if not self._check_user_reservation_duration(request.users[0], bounds):
+ raise ReservationException(
+ "Oops! Looks like you've reached your weekly study room reservation limit"
+ )
+
# Fetch User entities for all requested in reservation
user_entities = (
self._session.query(UserEntity)
@@ -402,12 +788,13 @@ def draft_reservation(
)
nonconflicting = bounds.subtract(conflict)
- if len(nonconflicting) == 1:
+ if len(nonconflicting) >= 1:
bounds = nonconflicting[0]
else:
raise ReservationException(
"Users may not have conflicting reservations."
)
+
# Dead code because of the NotImplementedError testing for multiple users at the top
# else:
# # Draft of expected functionality (needs testing and sanity checking)
@@ -421,23 +808,32 @@ def draft_reservation(
# Look at the seats - match bounds of assigned seat's availability
# TODO: Fetch all seats
- seats: list[Seat] = SeatEntity.get_models_from_identities(
- self._session, request.seats
- )
- seat_availability = self.seat_availability(seats, bounds)
+ if request.room is None:
+ seats: list[Seat] = SeatEntity.get_models_from_identities(
+ self._session, request.seats
+ )
+ seat_availability = self.seat_availability(seats, bounds)
- if not is_walkin:
- seat_availability = [seat for seat in seat_availability if seat.reservable]
+ if not is_walkin:
+ seat_availability = [
+ seat for seat in seat_availability if seat.reservable
+ ]
+
+ if len(seat_availability) == 0:
+ raise ReservationException(
+ "The requested seat(s) are no longer available."
+ )
- if len(seat_availability) == 0:
- raise ReservationException("The requested seat(s) are no longer available.")
+ # TODO (limit to # of users on request if multiple users)
+ # Here we constrain the reservation start/end to that of the best available seat requested.
+ # This matters as walk-in availability becomes scarce (may start in the near future even though request
+ # start is for right now), alternatively may end early due to reserved seat on backend.
+ seat_entities = [self._session.get(SeatEntity, seat_availability[0].id)]
+ bounds = seat_availability[0].availability[0]
+ else:
+ seat_entities = []
- # TODO (limit to # of users on request if multiple users)
- # Here we constrain the reservation start/end to that of the best available seat requested.
- # This matters as walk-in availability becomes scarce (may start in the near future even though request
- # start is for right now), alternatively may end early due to reserved seat on backend.
- seat_entities = [self._session.get(SeatEntity, seat_availability[0].id)]
- bounds = seat_availability[0].availability[0]
+ room_id = request.room.id if request.room else None
draft = ReservationEntity(
state=ReservationState.DRAFT,
@@ -445,7 +841,7 @@ def draft_reservation(
end=bounds.end,
users=user_entities,
walkin=is_walkin,
- room_id=None,
+ room_id=room_id,
seats=seat_entities,
)
@@ -521,6 +917,7 @@ def _change_state(self, entity: ReservationEntity, delta: ReservationState) -> b
transition = (entity.state, delta)
valid_transition = False
+
match transition:
case (RS.DRAFT, RS.CONFIRMED):
valid_transition = True
@@ -531,15 +928,22 @@ def _change_state(self, entity: ReservationEntity, delta: ReservationState) -> b
case (RS.CHECKED_IN, RS.CHECKED_OUT):
valid_transition = True
case _:
- return False
+ valid_transition = False
+
+ if entity.room:
+ match transition:
+ case (RS.CONFIRMED, RS.CHECKED_IN):
+ valid_transition = True
if valid_transition:
entity.state = delta
- return True
+ return valid_transition
- def list_all_active_and_upcoming(self, subject: User) -> Sequence[Reservation]:
- """Ambassadors need to see all active and upcoming reservations.
+ def list_all_active_and_upcoming_for_xl(
+ self, subject: User
+ ) -> Sequence[Reservation]:
+ """Ambassadors need to see all active and upcoming reservations for the XL.
This method queries all future events. When pre-reservations are added, this method
will need redesign to support date/time based pagination.
@@ -571,6 +975,7 @@ def list_all_active_and_upcoming(self, subject: User) -> Sequence[Reservation]:
ReservationState.CHECKED_OUT,
)
),
+ ReservationEntity.room == None,
)
.options(
joinedload(ReservationEntity.users), joinedload(ReservationEntity.seats)
@@ -580,6 +985,47 @@ def list_all_active_and_upcoming(self, subject: User) -> Sequence[Reservation]:
)
return [reservation.to_model() for reservation in reservations]
+ def list_all_active_and_upcoming_for_rooms(
+ self, subject: User
+ ) -> Sequence[Reservation]:
+ """Ambassadors need to see all active and upcoming reservations for the rooms.
+
+ This method queries all future events. When pre-reservations are added, this method
+ will need redesign to support date/time based pagination.
+
+ Args:
+ subject (User): The user initiating the reservation change request.
+
+ Returns:
+ Sequence[Reservation] - all active and upcoming reservations for rooms.
+
+ Raises:
+ UserPermissionException when user does not have permission to read reservations
+ """
+ self._permission_svc.enforce(subject, "coworking.reservation.read", f"user/*")
+ now = datetime.now()
+ reservations = (
+ self._session.query(ReservationEntity)
+ .join(ReservationEntity.users)
+ .filter(
+ ReservationEntity.start >= now - timedelta(minutes=10),
+ ReservationEntity.state.in_(
+ (
+ ReservationState.CONFIRMED,
+ ReservationState.CHECKED_IN,
+ ReservationState.CHECKED_OUT,
+ )
+ ),
+ ReservationEntity.room != None,
+ )
+ .options(
+ joinedload(ReservationEntity.users), joinedload(ReservationEntity.seats)
+ )
+ .order_by(ReservationEntity.start.asc())
+ .all()
+ )
+ return [reservation.to_model() for reservation in reservations]
+
def staff_checkin_reservation(
self, subject: User, reservation: Reservation
) -> Reservation:
diff --git a/backend/test/services/coworking/fixtures.py b/backend/test/services/coworking/fixtures.py
index 70b54b22d..cdcd78561 100644
--- a/backend/test/services/coworking/fixtures.py
+++ b/backend/test/services/coworking/fixtures.py
@@ -3,7 +3,10 @@
import pytest
from unittest.mock import create_autospec
from sqlalchemy.orm import Session
-from ....services import PermissionService
+from ....services import (
+ PermissionService,
+ RoomService,
+)
from ....services.coworking import (
OperatingHoursService,
SeatService,
@@ -12,7 +15,13 @@
StatusService,
)
-__authors__ = ["Kris Jordan"]
+__authors__ = [
+ "Kris Jordan",
+ "Aarjav Jain",
+ "John Schachte",
+ "Nick Wherthey",
+ "Yuvraj Jain",
+]
__copyright__ = "Copyright 2023"
__license__ = "MIT"
@@ -29,6 +38,12 @@ def operating_hours_svc(session: Session, permission_svc: PermissionService):
return OperatingHoursService(session, permission_svc)
+@pytest.fixture()
+def room_svc(session: Session):
+ """RoomService fixture."""
+ return RoomService(session)
+
+
@pytest.fixture()
def seat_svc(session: Session):
"""SeatService fixture."""
diff --git a/backend/test/services/coworking/reservation/get_current_reservations_for_user_test.py b/backend/test/services/coworking/reservation/get_current_reservations_for_user_test.py
index 93e8ea26b..4ae74c40a 100644
--- a/backend/test/services/coworking/reservation/get_current_reservations_for_user_test.py
+++ b/backend/test/services/coworking/reservation/get_current_reservations_for_user_test.py
@@ -2,6 +2,8 @@
from unittest.mock import create_autospec
+from backend.models.coworking.reservation import ReservationState
+
from .....services.coworking import ReservationService
# Imported fixtures provide dependencies injected for the tests as parameters.
@@ -29,7 +31,13 @@
from .. import seat_data
from . import reservation_data
-__authors__ = ["Kris Jordan"]
+__authors__ = [
+ "Kris Jordan",
+ "Nick Wherthey",
+ "Yuvraj Jain",
+ "Aarjav Jain",
+ "John Schachte",
+]
__copyright__ = "Copyright 2023"
__license__ = "MIT"
@@ -41,9 +49,8 @@ def test_get_current_reservations_for_user_as_user(
reservations = reservation_svc.get_current_reservations_for_user(
user_data.user, user_data.user
)
- assert len(reservations) == 2
+ assert len(reservations) == 3
assert reservations[0].id == reservation_data.reservation_1.id
- assert reservations[1].id == reservation_data.reservation_5.id
reservations = reservation_svc.get_current_reservations_for_user(
user_data.ambassador, user_data.ambassador
@@ -66,3 +73,13 @@ def test_get_current_reservations_for_user_permissions(
"coworking.reservation.read",
f"user/{user_data.user.id}",
)
+
+
+def test_get_current_reservation_for_user_by_state(reservation_svc: ReservationService):
+ """Get reservation for user by state."""
+ reservations = reservation_svc.get_current_reservations_for_user(
+ user_data.user, user_data.user, ReservationState.CHECKED_IN
+ )
+ assert len(reservations) == 1
+ assert reservations[0].id == reservation_data.reservation_1.id
+ assert reservations[0].state == reservation_data.reservation_1.state
diff --git a/backend/test/services/coworking/reservation/get_map_current_reservations_test.py b/backend/test/services/coworking/reservation/get_map_current_reservations_test.py
new file mode 100644
index 000000000..446c41ffe
--- /dev/null
+++ b/backend/test/services/coworking/reservation/get_map_current_reservations_test.py
@@ -0,0 +1,136 @@
+"""Tests for ReservationService#get_map_reservations_for_date and helper functions."""
+
+from backend.models.coworking.availability import RoomState
+from backend.models.coworking.reservation import ReservationState
+from datetime import date
+
+from .....services.coworking import ReservationService
+
+# Imported fixtures provide dependencies injected for the tests as parameters.
+# Dependent fixtures (seat_svc) are required to be imported in the testing module.
+from ..fixtures import (
+ reservation_svc,
+ permission_svc,
+ seat_svc,
+ policy_svc,
+ operating_hours_svc,
+)
+from ..time import *
+
+# Import the setup_teardown fixture explicitly to load entities in database.
+# The order in which these fixtures run is dependent on their imported alias.
+# Since there are relationship dependencies between the entities, order matters.
+from ...core_data import user_data
+from ...core_data import setup_insert_data_fixture as insert_order_0
+from ..operating_hours_data import fake_data_fixture as insert_order_1
+from ...room_data import fake_data_fixture as insert_order_2
+from ..seat_data import fake_data_fixture as insert_order_3
+from .reservation_data import fake_data_fixture as insert_order_4
+
+# Import the fake model data in a namespace for test assertions
+from ...core_data import user_data
+from .. import seat_data
+from . import reservation_data
+
+__authors__ = [
+ "Nick Wherthey",
+ "Yuvraj Jain",
+]
+__copyright__ = "Copyright 2023"
+__license__ = "MIT"
+
+SATURDAY, SUNDAY = [5, 6]
+
+
+def test_transform_date_map_for_unavailable_simple(reservation_svc: ReservationService):
+ """
+ Validates the transformation of the date map to indicate unavailable time slots.
+
+ This test ensures that time slots are appropriately grayed out for all other rooms
+ once a user has made a reservation. For example, if Sally Student reserves room SN135
+ from 1 pm to 3 pm on February 29, she should be prevented from booking any other room
+ during these hours. The function verifies that the data map returned by the endpoint
+ accurately reflects these unavailable slots, enhancing the user experience by
+ preventing double bookings.
+ """
+
+ sample_date_map_1 = {
+ 'SN135': [0, 0, 0, 0],
+ 'SN137': [0, 0, 4, 4],
+ 'SN139': [0, 0, 0, 0]
+ }
+
+ expected_transformed_date_map_1 = {
+ 'SN135': [0, 0, 3, 3],
+ 'SN137': [0, 0, 4, 4],
+ 'SN139': [0, 0, 3, 3]
+ }
+
+ reservation_svc._transform_date_map_for_unavailable(sample_date_map_1)
+ assert sample_date_map_1 == expected_transformed_date_map_1
+
+
+def test_transform_date_map_for_unavailable_complex(reservation_svc: ReservationService):
+ sample_date_map_2 = {
+ 'SN135': [0, 0, 0, 0, 0, 0, 1, 1, 1, 1],
+ 'SN137': [0, 0, 1, 1, 4, 4, 4, 4, 0, 0],
+ 'SN139': [0, 4, 4, 1, 1, 0, 0, 0, 0, 0]
+ }
+
+ expected_transformed_date_map_2 = {
+ 'SN135': [0, 3, 3, 0, 3, 3, 1, 1, 1, 1],
+ 'SN137': [0, 3, 1, 1, 4, 4, 4, 4, 0, 0],
+ 'SN139': [0, 4, 4, 1, 1, 3, 3, 3, 0, 0]
+ }
+
+ reservation_svc._transform_date_map_for_unavailable(sample_date_map_2)
+ assert expected_transformed_date_map_2 == sample_date_map_2
+
+
+def test_idx_calculation(reservation_svc: ReservationService):
+ time_1 = datetime.now().replace(hour=10, minute=12)
+ oh_start = datetime.now().replace(hour=10, minute=0)
+ assert reservation_svc._idx_calculation(time_1, oh_start) == 0
+
+ time_2 = datetime.now().replace(hour=12, minute=30)
+ assert reservation_svc._idx_calculation(time_2, oh_start) == 5
+
+ time_3 = datetime.now().replace(hour=13, minute=40)
+ assert reservation_svc._idx_calculation(time_3, oh_start) == 7
+
+
+def test_query_confirmed_reservations_by_date_and_room(
+ reservation_svc: ReservationService, time: dict[str, datetime]
+):
+ """Test getting all reservations for a particular date."""
+ reservations = reservation_svc._query_confirmed_reservations_by_date_and_room(time[TOMORROW], 'SN135')
+ assert True
+ #TODO: Add in better assert statements here.
+
+def test_get_reservable_rooms(reservation_svc: ReservationService):
+ rooms = reservation_svc._get_reservable_rooms()
+ assert rooms[0].id == 'SN135' and rooms[0].reservable is True
+ assert rooms[1].id == 'SN137' and rooms[1].reservable is True
+ assert rooms[2].id == 'SN139' and rooms[2].reservable is True
+ assert rooms[3].id == 'SN141' and rooms[3].reservable is True
+
+def test_get_map_reserved_times_by_date(
+ reservation_svc: ReservationService, time: dict[str, datetime]
+):
+ """Test for getting a dictionary where keys are room ids and time slots array are values.
+
+ If this test fails, consider running the reset_demo script before running this test again.
+ This is hard function to test, and this test does not ensure 100% coverage due to the
+ multiple edge cases that arise out of it. I recommend setting a breakpoint and looking at
+ the reserved_date_map in the debugger.
+ """
+ test_time = time[NOW]
+ reserved_date_map = reservation_svc.get_map_reserved_times_by_date(
+ test_time + timedelta(days=2), user_data.user
+ )
+
+ reserved_date_map_root = reservation_svc.get_map_reserved_times_by_date(
+ test_time, user_data.root
+ )
+
+ assert True
\ No newline at end of file
diff --git a/backend/test/services/coworking/reservation/list_all_active_and_upcoming_test.py b/backend/test/services/coworking/reservation/list_all_active_and_upcoming_test.py
index 00f0b82fa..01b448d23 100644
--- a/backend/test/services/coworking/reservation/list_all_active_and_upcoming_test.py
+++ b/backend/test/services/coworking/reservation/list_all_active_and_upcoming_test.py
@@ -35,8 +35,8 @@
__license__ = "MIT"
-def test_list_all_active_and_upcoming(reservation_svc: ReservationService):
- all = reservation_svc.list_all_active_and_upcoming(user_data.ambassador)
+def test_list_all_active_and_upcoming_for_xl(reservation_svc: ReservationService):
+ all = reservation_svc.list_all_active_and_upcoming_for_xl(user_data.ambassador)
assert len(all) == len(reservation_data.active_reservations) + len(
reservation_data.confirmed_reservations
)
@@ -46,7 +46,7 @@ def test_list_all_active_and_upcoming_permission(reservation_svc: ReservationSer
permission_svc = create_autospec(PermissionService)
permission_svc.enforce.return_value = None
reservation_svc._permission_svc = permission_svc
- reservation_svc.list_all_active_and_upcoming(user_data.ambassador)
+ reservation_svc.list_all_active_and_upcoming_for_xl(user_data.ambassador)
permission_svc.enforce.assert_called_once_with(
user_data.ambassador,
"coworking.reservation.read",
diff --git a/backend/test/services/coworking/reservation/reservation_data.py b/backend/test/services/coworking/reservation/reservation_data.py
index 1f3728480..24c954dd7 100644
--- a/backend/test/services/coworking/reservation/reservation_data.py
+++ b/backend/test/services/coworking/reservation/reservation_data.py
@@ -13,6 +13,7 @@
from ...reset_table_id_seq import reset_table_id_seq
from .. import seat_data
from .. import operating_hours_data
+from ... import room_data
__authors__ = ["Kris Jordan"]
@@ -29,6 +30,8 @@
reservation_4: Reservation
# Draft reservation for user tomorrow
reservation_5: Reservation
+# Future room reservation
+reservation_6: Reservation
# Lists used for access
active_reservations: list[Reservation]
@@ -38,7 +41,7 @@
def instantiate_global_models(time: dict[str, datetime]):
- global reservation_1, reservation_2, reservation_3, reservation_4, reservation_5
+ global reservation_1, reservation_2, reservation_3, reservation_4, reservation_5, reservation_6
global active_reservations, reservations, draft_reservations, confirmed_reservations
reservation_1 = Reservation(
id=1,
@@ -109,6 +112,22 @@ def instantiate_global_models(time: dict[str, datetime]):
seats=[seat_data.reservable_seats[0]],
)
+ # Confirm Room Reservation
+ reservation_6 = Reservation(
+ id=6,
+ start=operating_hours_data.tomorrow.start.replace(hour=12, minute=0)
+ + timedelta(hours=24),
+ end=operating_hours_data.tomorrow.end.replace(hour=14, minute=30)
+ + timedelta(hours=24),
+ created_at=time[NOW],
+ updated_at=time[NOW],
+ walkin=False,
+ room=room_data.group_b,
+ state=ReservationState.CONFIRMED,
+ users=[user_data.user],
+ seats=[],
+ )
+
active_reservations = [reservation_1]
confirmed_reservations = [reservation_4]
draft_reservations = [reservation_5]
@@ -118,6 +137,7 @@ def instantiate_global_models(time: dict[str, datetime]):
reservation_3,
reservation_4,
reservation_5,
+ reservation_6,
]
diff --git a/backend/test/services/coworking/time.py b/backend/test/services/coworking/time.py
index 8ed48dc93..b7cff8258 100644
--- a/backend/test/services/coworking/time.py
+++ b/backend/test/services/coworking/time.py
@@ -17,16 +17,19 @@
# Constants are keys to the times fixture
NOW = "NOW"
+MIDNIGHT_TODAY = "MIDNIGHT_TODAY"
# Past
A_WEEK_AGO = "A_WEEK_AGO"
AN_HOUR_AGO = "AN_HOUR_AGO"
THIRTY_MINUTES_AGO = "THIRTY_MINUTES_AGO"
# Future
+MIDNIGHT_TOMORROW = "MIDNIGHT_TOMORROW"
IN_THIRTY_MINUTES = "IN_THIRTY_MINUTES"
TOMORROW = "TOMORROW"
IN_ONE_HOUR = "IN_ONE_HOUR"
IN_TWO_HOURS = "IN_TWO_HOURS"
IN_THREE_HOURS = "IN_THREE_HOURS"
+IN_EIGHT_HOURS = "IN_EIGHT_HOURS"
@pytest.fixture()
@@ -46,16 +49,21 @@ def time_data() -> dict[str, datetime]:
return {
# Times
NOW: now,
+ MIDNIGHT_TODAY: now.replace(hour=0, minute=0, second=0, microsecond=0),
# Past
A_WEEK_AGO: now - 7 * ONE_DAY,
AN_HOUR_AGO: now - ONE_HOUR,
THIRTY_MINUTES_AGO: now - THIRTY_MINUTES,
# Future
+ MIDNIGHT_TOMORROW: (now + ONE_DAY).replace(
+ hour=0, minute=0, second=0, microsecond=0
+ ),
IN_THIRTY_MINUTES: now + THIRTY_MINUTES,
TOMORROW: now + ONE_DAY,
IN_ONE_HOUR: now + ONE_HOUR,
IN_TWO_HOURS: now + 2 * ONE_HOUR,
IN_THREE_HOURS: now + 3 * ONE_HOUR,
+ IN_EIGHT_HOURS: now + 8 * ONE_HOUR
}
diff --git a/docs/images/make-reservations.png b/docs/images/make-reservations.png
new file mode 100644
index 000000000..1c7feb759
Binary files /dev/null and b/docs/images/make-reservations.png differ
diff --git a/docs/images/specs/room-reservation/active-reservations.png b/docs/images/specs/room-reservation/active-reservations.png
new file mode 100644
index 000000000..5d6920a5d
Binary files /dev/null and b/docs/images/specs/room-reservation/active-reservations.png differ
diff --git a/docs/images/specs/room-reservation/confirmation-page.png b/docs/images/specs/room-reservation/confirmation-page.png
new file mode 100644
index 000000000..6f4d86e75
Binary files /dev/null and b/docs/images/specs/room-reservation/confirmation-page.png differ
diff --git a/docs/images/specs/room-reservation/make-reservation.png b/docs/images/specs/room-reservation/make-reservation.png
new file mode 100644
index 000000000..711e5af47
Binary files /dev/null and b/docs/images/specs/room-reservation/make-reservation.png differ
diff --git a/docs/images/specs/room-reservation/reservation-confirmed.png b/docs/images/specs/room-reservation/reservation-confirmed.png
new file mode 100644
index 000000000..523fb1dc9
Binary files /dev/null and b/docs/images/specs/room-reservation/reservation-confirmed.png differ
diff --git a/docs/images/upcoming-reservations.png b/docs/images/upcoming-reservations.png
new file mode 100644
index 000000000..73268dda5
Binary files /dev/null and b/docs/images/upcoming-reservations.png differ
diff --git a/docs/specs/room-reservation.md b/docs/specs/room-reservation.md
new file mode 100644
index 000000000..7fbfcd345
--- /dev/null
+++ b/docs/specs/room-reservation.md
@@ -0,0 +1,386 @@
+# Room Reservation Technical Specifications
+
+This document contains the technical specifications, including sample data representation of our feature, descriptions of underlying database / entity-level representation decisions and development concerns.
+
+# Authors
+
+- [Aarjav Jain](https://github.com/aarjavjain2002)
+- [John Schachte](https://github.com/JohnSchachte)
+- [Nick Wherthey](https://github.com/wherthey)
+- [Yuvraj Jain](https://github.com/yuvrajjain2003)
+
+# Table of Contents
+
+- [Introduction to Demo](#introduction-to-demo)
+ - [How do I reserve a room?](#how-do-i-reserve-a-room)
+ - [How do I view my room reservation?](#how-do-i-view-my-room-reservation)
+ - [How do I cancel my room reservation](#how-do-i-cancel-my-room-reservation)
+- [Description and Sample Data](#descriptions-and-sample-data-representation-of-feature)
+ - [0. Room Partial](#0-room-partial)
+ - [1. Reservation Request](#1-reservation-request)
+ - [2. Reservation Service](#2-reservation-service)
+ - [3. Route to get all upcoming reservations for a user.](#3-route-to-get-all-upcoming-reservations-for-a-user)
+- [Underlying Database / Entity-Level Representation decisions](#underlying-database--entity-level-representation-decisions)
+- [Technical and User Experience Design Choices](#technical-and-user-experience-design-choices)
+- [Development Concerns](#development-concerns)
+- [For Future Developers](#future-developers)
+
+# Introduction to Demo
+
+This section of the documents contains the instructions to replicate the steps we take in our demo. By reading this, you should be able to replicate how we create room reservations, view upcoming room reservations, and delete room reservations. We would recommend running the `reset_demo` script to populate the backend with some sample reservations to ease the testing process.
+
+## How do I reserve a room?
+
+- From the sidebar, click on Coworking > Room Reservations
+- Now you should see a date selector and a table with certain time intervals as columns and room names as rows. To reserve a room, click on **adjacent cells** in a table showing time intervals and room names. You can select up to 4 adjacent slots, equaling a 2-hour reservation. Selecting more than 4 slots resets the selection to one slot. This 2-hour limit is set by a function in the frontend service files and can be adjusted if needed.
+- While picking the slots, you will notice a legend with 5 colors. Please bear with us as we figure out more ways to make this accessible to people with color blindness. Currently, the structure is as follows:
+ - Available (Green): These slots are ones which available to be reserved by the user. You can try clicking on one of these slots which will turn into into a "Selected" time slot which will appear orange.
+ - Reserved (Red): These slots are the ones which have been reserved by someone other than you.
+ - Selected (Orange): These slots represent your selection.
+ - Unavailable (Gray): These slots are unavailble to be reserved because they are either in the past or you have a conflicting reservation in another room or at the CSXL.
+ - Your Reservations (Blue): These slots represent your reservations. You will notice that once you make a reservation, all other time slots in the same column turn gray.
+
+![Make Reservations](../images/specs/room-reservation/make-reservation.png)
+
+- Once you have picked the slots you like, click on the **Select** button, which will draft a reservation for you, and redirect you to the confirmation page.
+- On the confirmation page, you can view the details of your selection including the time, location, and date for your reservation. If you are happy with your selection, click on the **Confirm** button. Otherwise, click on **Cancel**. Note that your reservation draft will automatically be cancelled within 5 minutes if you don't press anything. Navigating out of the page also cancels your reservation.
+
+![Confirmation Page](../images/specs/room-reservation/confirmation-page.png)
+
+## How do I view my room reservation?
+
+The natural next question from our previous steps would be, "How do I know that my reservation actually exists?"
+
+Well, our team has come up with a visually succinct way of displaying this information through coworking cards that were already present in the codebase. If you hit confirm, follow the steps below to see your reservations:
+
+- From the sidebar, click on Coworking > Room Reservations.
+
+- Now you should be able to view all your upcoming reservations below the reservation table under the "Upcoming Reservations" header.
+
+![Profile Card After Reservation](../images/specs/room-reservation/reservation-confirmed.png)
+
+- Please note that our upcoming reservations only show reservations to the rooms and not to the Colab CSXL SN156 room, since SN156 currently doesn't accept pre-reservations and only takes walk-ins.
+
+## How do I cancel my room reservation?
+
+- From the sidebar, click on Coworking > Room Reservations.
+
+- On the card for the reservation you want to cancel, simply click on the cancel button. Note that this feature doesn't exist for active reservations, since you are already checked-in. You can instead simply check-out for active reservations.
+
+## Where can I find my active reservations?
+
+- Once you have checked in, the reservation becomes active. The active reservations can be found on the Coworking page.
+
+![Active Reservations](../images/specs/room-reservation/active-reservations.png)
+
+# Descriptions and Sample Data Representation of feature
+
+We have added / modified the following models / API routes:
+
+## 0. Room Partial
+
+Before:
+
+```py3
+class Room(BaseModel):
+ id: str
+ nickname: str = ""
+```
+
+After:
+
+```py3
+class RoomPartial(BaseModel):
+ id: str | None = None
+
+class Room(RoomPartial):
+ nickname: str = ""
+```
+
+We had to modify the following class because it felt weird to make a frontend request to draft a reservation and send in the entire `Room` object. This would require the frontend to know both the `id` and the `nickname` for the room that they were trying to book. Instead, we broke up the `Room` class into `Room` and `RoomPartial`, and `ReservationRequest`now uses the `RoomPartial` class instead.
+
+Tl;dr: The frontend now only needs to send in the `id` of the room they want to reserve through the HTTP request.
+
+## 1. Reservation Request
+
+Before:
+
+```py3
+class ReservationRequest(TimeRange):
+ users: list[UserIdentity] = []
+ seats: list[SeatIdentity] = []
+```
+
+After:
+
+```py3
+class ReservationRequest(TimeRange):
+ users: list[UserIdentity] = []
+ seats: list[SeatIdentity] = []
+ room: RoomPartial | None = None
+```
+
+The model did not account for different rooms when making a ReservationRequest. And all reservation requests were hardcoded to make reservations for the CSXL Colab. But, now we have extended this feature to make reservations for any room the user wants.
+
+## 2. Reservation Service
+
+We had to modify the `draft_reservation()` method inside the ReservationService because previously the service would only cater to reservation requests to the XL since the room parameter was hardcoded to be None. We have laxed this condition, and instead feed the room paramater the input we get from the frontend as a FastAPI parameter in `backend/api/coworking/reservation.py`.
+
+We added the following methods into the registration_service file. For sake of brevity, we will only include the method signatures here, but you can locate the actual implementation in the repo.
+
+```py3
+def get_current_reservations_for_user(
+ self, subject: User, focus: User, state: ReservationState | None = None
+) -> Sequence[Reservation]:
+ ...
+
+
+def _get_active_reservations_for_user(
+ self, focus: UserIdentity, time_range: TimeRange
+) -> Sequence[Reservation]:
+ ...
+
+
+def _get_active_reservations_for_user_by_state(
+ self,
+ focus: UserIdentity,
+ time_range: TimeRange,
+ state: ReservationState,
+) -> Sequence[Reservation]:
+ ...
+
+
+def get_map_reserved_times_by_date(
+ self, date: datetime, subject: User
+) -> dict[str, list[int]]:
+ ...
+
+
+def _idx_calculation(self, time: datetime) -> int:
+ ...
+
+def _transform_date_map_for_unavailable(
+ self, reserved_date_map: dict[str, list[int]]
+) -> None:
+ ...
+
+def _query_confirmed_reservations_by_date(
+ self, date: datetime
+ ) -> Sequence[Reservation]:
+ ...
+```
+
+If you want a deeper understanding of how these functions works, we recommened reading [this section.](#future-developers)
+
+## 3. Route to get all upcoming reservations for a user
+
+We added the following code into the backend API layer:
+
+```py3
+@api.get("/room-reservations/", tags=["Coworking"])
+def get_all_reservations_by_state(
+ state: ReservationState,
+ subject: User = Depends(registered_user),
+ reservation_svc: ReservationService = Depends(),
+) -> Sequence[Reservation]:
+ try:
+ return reservation_svc.get_current_reservations_for_user(
+ subject=subject, focus=subject, state=state
+ )
+ except Exception as e:
+ raise HTTPException(status_code=404, detail=str(e))
+
+```
+
+We needed a way to view all upcoming reservations for a given user so that we can display this information. So we added the following API route into the codebase.
+
+## 4. Route to get timeslots of all reservations for all users
+
+We needed this endpoint because we needed a way for the user to know which timeslots and rooms have already been booked, so that they could make their selection accordingly. We make sure that we only get the time slots and not any private information like names of users who made the reservations. This is the route we added in the `backend/api/coworking/reservation.py` file.
+
+```py3
+@api.get("/room-reservation/", tags=["Coworking"])
+def get_reservations_for_rooms_by_date(
+ date: datetime,
+ subject: User = Depends(registered_user),
+ reservation_svc: ReservationService = Depends(),
+) -> dict[str, list[int]]:
+ """See available rooms for any given day."""
+ try:
+ return reservation_svc.get_map_reserved_times_by_date(date, subject)
+ except Exception as e:
+ raise HTTPException(status_code=404, detail=str(e))
+```
+
+Note that we had to make a novel method to get this map of reserved times. You can find this implementation under `backend/services/coworking/reservation.py`.
+
+# Underlying Database / Entity-Level Representation decisions
+
+We have not interefered with the way that our underlying database stores the data since the structure still remains the same. We have built upon this structure by making new reservations. We are now utilizing the room column within our database which was earlier set to be only None.
+
+# Technical and User Experience Design Choices
+
+## 1. Cards or Table for viewing upcoming reservation
+
+While implementing the upcoming reservations for the user, our team considered between using a table to view all possible upcoming reservations or using a widget card. The trade-off we considered were that tables were a more efficient way of presenting information, and they provide a more efficient way of searching and modifying data. Widgets are much harder to change inputs for and modify, but they provide a more aesthetic and consistent User Experience.
+
+Our team ended up picking the coworking card widgets to display this information because it was more consistent with how the website is set up right now. It actually ended up providing more modularity than a table than we had anticipated and is actually much easier to manage.
+
+## 2. API Routes for drafting reservations
+
+While implementing the API routes for drafting reservations, our team also considering making a separate endpoint for drafting room reservations which were not for the XL, but this ended up becoming very complicated. In the end, we decided that it is probably for the best to reduce the number of endpoints we have and instead modifying the current endpoint to be more expansive and inclusive so that we can make reservations for room other than the XL.
+
+## 3. List of Reservation Time Ranges or Matrix of Available Time Slots
+
+This is in context to making new reservations as illustrated in our Figma wireframe below.
+
+![Make Reservations](../images/make-reservations.png)
+
+When making reservations, you can see that some of the slots are marked red. These are possibly other reservations that other people in the XL might have made while Sally is trying to make her reservation. In order to actually get this information from the backend, we were debating whether we want to send back a list of reservation objects from the backend to the frontend or send back a matrix of 0s and 1s to represent time slots that are available vs time slots that are reserved. These are the possible trade-offs we considered.
+
+| List of Reservations | Matrix of Numbers |
+| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| This would be easier to implement on the backend. | This would be considerably harder to implement on the backend |
+| This would require then iterating through the list of reservations on the frontend, and it is awkwards to have business logic on the frontend for such an algorthmic process. | This would make it much easier to implement in the frontend since we can easily iterate over the matix and apply the correct widget depending on what value we see in the matrix. |
+| This would be much more hardcoded with less modularity for changes in the future. For example, if we toggle to make a room not reservable anymore in the backend, it would either require making an endpoint connect which is awkward or change the hardcoded values which is also awkward. | This would be much more modular since we can actually fetch the operating hours and currently reservable rooms to dynamically update our matrix with updated values. |
+
+Based on the above analysis of trade-offs, we decided to actually go with the matrix approach, but we made a slight change to it by making it a Map instead, so that the number of rows and columns are not hardcoded, but rather retrived from the backend operating hours page.
+
+# Development Concerns
+
+We have a few development concerns that we would like to hopefully address in the future:
+
+- There should be a limit on how many rooms a user can reserve i.e how many reservations a user can have at any given time.
+- Possibly consider adding group members to a reservation. (Add a button in the card to add a user.)
+- Allow for pre-reservations in the XL as well. However this will require some clever thinking as the table cannot be extended to just incorporate SN156.
+- There are some issues with how room reservations have been currently integrated with XL drop-in reservations, which make it a little harder for the user to navigate.
+
+# Future Developers
+
+This section is designed for developers interested in enhancing or expanding our features. Our goal is to provide a comprehensive overview that facilitates a smooth development experience. We've organized this guide into two main subsections: Frontend Concerns and Backend Concerns. This division reflects the separation of concerns inherent in the RestAPI architecture. If your focus is on frontend development, we strongly advise a thorough examination of the Frontend section and a cursory review of the Backend section. Conversely, backend developers should primarily focus on the Backend section while also familiarizing themselves with key aspects of the Frontend section. This approach ensures a holistic understanding of our system's architecture and functionalities, aiding in more effective and efficient development.
+
+## Frontend Concerns
+
+The Room Reservation feature introduces 2 components, 2 services, and 2 widgets. Users can access this feature through the coworking tab on the navigation bar. It was important to us to maintain the existing structure while developing an interface for users to book reservations, so future developers have less code to learn.
+
+### Components
+
+The components introduced in this feature are the new-reservation and confirmation-reservation components.
+
+#### New-Reservation Component:
+
+This component holds a table for users to make reservations with. In its view, it is the parent component for the Room Reservation Table and Date Selector widget.
+It has access to the table service, which holds the business logic for this component to maintain appropriate barriers. A user may also see their Upcoming Reservations under the reservation table.
+We decided to use the Coworking Reservation Card to display an upcoming reservation. Once the user clicks the 'Select' button, the component will navigate the user to the Confirm Reservation Component.
+Availability is requested from the backend with the help of the Room Reservation Table Service and the Date Selector widget.
+
+#### Confirm Reservation Component:
+
+This is where a user may inspect the reservation they are about to make. This component uses the Coworking Reservation Card widget to display the reservation to the user. If the user does not confirm the draft reservation, this reservation draft will be cancelled on ng destroy. If either cancel or confirm are clicked, the user will be navigated to the Coworking Home Component.
+
+### Services
+
+The two services introduced are the Room Reservation Service and Reservation Table Service.
+
+#### Room Reservation Service:
+
+The Room Reservation Service extends the Reservation Service by adding 3 methods:
+
+1. getReservationsByState: Retrieves reservations by state.
+2. checkin: Checks in a confirmed reservation.
+3. deleteRoomReservation: Updates a reservation to a cancelled state.
+
+These methods are used to add and update room reservations with the backend. This service is the primary way of interacting with room reservations in the backend.
+
+#### Reservation Table Service:
+
+The Reservation Table Service holds all the business logic for the Room Reservation Table widget while also enumerating the table's cell states.
+This service also has logic for interacting with the backend by making draft reservations and getting room reservations by date.
+
+A large portion of this service is aimed at encapsulating rules for maintaining a legitimate reservation request while users interact with the table. For example, a reservation should not have gaps between the reservation start and end times.
+
+### Widgets
+
+The widgets introduced in this feature are the Room Reservation Table and Date Selector. The Coworking Reservation Card widget was heavily used but was preexisting. These were discussed with how they were used within their parent components above.
+
+#### Room Reservation Table:
+
+A widget that uses a table to encapsulate the availability of room reservations for a specific day.
+
+#### Date Selector:
+
+A widget that uses a material date selector class to emit events to parent components. When a user wants to see room reservation availability for a different day, they will use the Date Selector widget to facilitate this change.
+
+#### Coworking Reservation Card:
+
+This preexisting widget is the method by which reservations are displayed to the user.
+
+## Backend Concerns
+
+For this feature, the backend primarily focuses on identifying available and reserved rooms, and also displays users' reservations. To understand the backend functionality, it's recommended to follow the outlined path where we move top-down i.e, we start at the API layer and move down to the queries that interact with the persistent storage.
+
+### 1. API Layer
+
+We recommend by checking out the code present in `backend/api/coworking/reservation.py`. Since this is the first layer that interacts with the frontend, it is the best to understand how the code is working. In this file we added the following routes:
+
+```py3
+@api.get("/room-reservations/", tags=["Coworking"])
+def get_all_reservations_by_state(
+ state: ReservationState,
+ subject: User = Depends(registered_user),
+ reservation_svc: ReservationService = Depends(),
+) -> Sequence[Reservation]:
+ try:
+ return reservation_svc.get_current_reservations_for_user(
+ subject=subject, focus=subject, state=state
+ )
+ except Exception as e:
+ raise HTTPException(status_code=404, detail=str(e))
+
+@api.get("/room-reservation/", tags=["Coworking"])
+def get_reservations_for_rooms_by_date(
+ date: datetime,
+ subject: User = Depends(registered_user),
+ reservation_svc: ReservationService = Depends(),
+) -> dict[str, list[int]]:
+ """See available rooms for any given day."""
+ try:
+ return reservation_svc.get_map_reserved_times_by_date(date, subject)
+ except Exception as e:
+ raise HTTPException(status_code=404, detail=str(e))
+```
+
+The `get_all_reservations_by_state()` method takes in a state as an argument and retreives all reservations of that state. To understand what a state is, we recommend checking out the `backend/models/coworking/reservation.py` and look at the class `ReservationState`.
+
+The `get_reservations_for_rooms_by_date` returns a dictionary of lists where the keys i.e the rows represents different rooms and the columns represent 30 minute time slots from 10:00 am to 12:00 pm. We will investigate how this dictionary is made within the next section.
+
+### 2. Reservation Service
+
+A keen reader would observe that within the routes we called the `get_map_reserved_times_by_date()` method. This is what does the majority of the work in the backend, and it is highly recommended to become familiar with this method.
+
+The main function `get_map_reserved_times_by_date()` uses the other 3 methods as helper functions to query through the database and turn that into a dictionary of lists where 0 represents available, 1 represents reserved, 2 represents selected, 3 represents unavailable, and 4 represents subject reservations.
+
+An example of what this dictionary would look like is represented below. This is the exact dictionary that is returned by the backend to the frontend. Index 0 represents the timeslot 10:00 - 10:30 am. Index 1 represents 10:30 am - 11:00 am. And so on...
+
+```py3
+{
+ "SN135" : [0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 4, 4, 4, 4],
+ "SN137" : [0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 3, 3, 3, 3],
+ "SN139" : [1, 1, 0, 0, 1, 1, 0, 1, 1, 1, 0, 0, 3, 3, 3, 3],
+ "SN141" : [0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 3, 3]
+}
+```
+
+So you can imagine that some other people have already made reservations for example in SN139 from 10:00 am to 11:00 am and SN141 from 12:00 pm to 2:00 pm. Obviously this list does not contain all the reservations listed above, but hopefully should give you a good understanding how this dictionary works. Also note that the columns which have a 4 all have 3s. This is because if you have a reservation from 4:00 pm to 6:00 pm as in the example above, you cannot make another reservation in the time slot. So those time slots become unavailable to be reserved.
+
+## 3. Models
+
+The only model we really implemented in the backend is the RoomPartial and Room model shown below. But it is recommened to go through the other models including the Reservation and User model that are frequently used through our codebase.
+
+```py3
+class RoomPartial(BaseModel):
+ id: str | None = None
+
+class Room(RoomPartial):
+ nickname: str = ""
+```
diff --git a/frontend/src/app/coworking/ambassador-home/ambassador-home-routing.module.ts b/frontend/src/app/coworking/ambassador-home/ambassador-home-routing.module.ts
new file mode 100644
index 000000000..edb0235a3
--- /dev/null
+++ b/frontend/src/app/coworking/ambassador-home/ambassador-home-routing.module.ts
@@ -0,0 +1,22 @@
+import { NgModule } from '@angular/core';
+import { RouterModule, Routes } from '@angular/router';
+import { AmbassadorXlListComponent } from './ambassador-xl/list/ambassador-xl-list.component';
+import { AmbassadorPageComponent } from './ambassador-home.component';
+import { AmbassadorRoomListComponent } from './ambassador-room/list/ambassador-room-list.component';
+
+const routes: Routes = [
+ {
+ path: '',
+ component: AmbassadorPageComponent,
+ children: [
+ AmbassadorXlListComponent.Route,
+ AmbassadorRoomListComponent.Route
+ ]
+ }
+];
+
+@NgModule({
+ imports: [RouterModule.forChild(routes)],
+ exports: [RouterModule]
+})
+export class AmbassadorHomeRoutingModule {}
diff --git a/frontend/src/app/coworking/ambassador-home/ambassador-home.component.css b/frontend/src/app/coworking/ambassador-home/ambassador-home.component.css
index 79b4759b9..e69de29bb 100644
--- a/frontend/src/app/coworking/ambassador-home/ambassador-home.component.css
+++ b/frontend/src/app/coworking/ambassador-home/ambassador-home.component.css
@@ -1,15 +0,0 @@
-.mat-mdc-card {
- max-width: 100%;
-}
-
-.mat-mdc-card-header {
- margin-bottom: 16px;
-}
-
-.walkinReservation.mat-mdc-card-content:last-child {
- padding-bottom: 0;
-}
-
-button {
- margin-right: 1vw;
-}
diff --git a/frontend/src/app/coworking/ambassador-home/ambassador-home.component.html b/frontend/src/app/coworking/ambassador-home/ambassador-home.component.html
index 429470d20..20fe6ba7e 100644
--- a/frontend/src/app/coworking/ambassador-home/ambassador-home.component.html
+++ b/frontend/src/app/coworking/ambassador-home/ambassador-home.component.html
@@ -1,164 +1,15 @@
-
-
-
- Reserve a Drop-in at the Welcome Desk
- Create a walk-in reservation for an XL community member at the welcome
- desk. Members must be registered with the XL and accept the Community
- Agreement.
-
-
-
- 0">
-
-
-
-
-
-
-
+
+
+ Reserve a Drop-in at the Welcome Desk
+ Create a walk-in reservation for an XL community member at the welcome
+ desk. Members must be registered with the XL and accept the Community
+ Agreement.
+
+
+
+ 0">
+
+
+
+
+
+
+