From f10f69dba59b7eed4a5c45adc9a40c3b877cb97f Mon Sep 17 00:00:00 2001 From: Vincent Cai <72774400+vcai122@users.noreply.github.com> Date: Fri, 26 Jan 2024 17:15:53 -0500 Subject: [PATCH] Revert "Revert "GSR Revamp"" --- backend/gsr_booking/api_wrapper.py | 763 +++++++++--------- backend/gsr_booking/group_logic.py | 106 --- .../0011_alter_reservation_group.py | 24 + backend/gsr_booking/models.py | 24 +- backend/gsr_booking/urls.py | 2 - backend/gsr_booking/views.py | 61 +- backend/setup.cfg | 1 + backend/tests/gsr_booking/test_gsr_views.py | 16 +- backend/tests/gsr_booking/test_gsr_wrapper.py | 118 +-- 9 files changed, 512 insertions(+), 603 deletions(-) delete mode 100644 backend/gsr_booking/group_logic.py create mode 100644 backend/gsr_booking/migrations/0011_alter_reservation_group.py diff --git a/backend/gsr_booking/api_wrapper.py b/backend/gsr_booking/api_wrapper.py index 49e9b3ad..b6a2c2c5 100644 --- a/backend/gsr_booking/api_wrapper.py +++ b/backend/gsr_booking/api_wrapper.py @@ -1,18 +1,25 @@ import datetime +from abc import ABC, abstractmethod from enum import Enum +from random import randint import requests from bs4 import BeautifulSoup from django.conf import settings +from django.contrib.auth import get_user_model +from django.db.models import F, Prefetch, Q, Sum +from django.db.models.functions import Coalesce from django.shortcuts import get_object_or_404 from django.utils import timezone -from django.utils.timezone import make_aware from requests.exceptions import ConnectionError, ConnectTimeout, ReadTimeout -from gsr_booking.models import GSR, Group, GroupMembership, GSRBooking, Reservation +from gsr_booking.models import GSR, GroupMembership, GSRBooking, Reservation from gsr_booking.serializers import GSRBookingSerializer, GSRSerializer +User = get_user_model() + + BASE_URL = "https://libcal.library.upenn.edu" API_URL = "https://api2.libcal.com" WHARTON_URL = "https://apps.wharton.upenn.edu/gsr/api/v1/" @@ -21,6 +28,9 @@ LOCATION_BLACKLIST = {3620, 2636, 2611, 3217, 2637, 2634} ROOM_BLACKLIST = {7176, 16970, 16998, 17625} +WHARTON_CREDIT_LIMIT = 6 +LIBCAL_CREDIT_LIMIT = 6 + class CreditType(Enum): LIBCAL = "Libcal" @@ -32,174 +42,63 @@ class APIError(ValueError): pass -class BookingWrapper: - def __init__(self): - self.WLW = WhartonLibWrapper() - self.LCW = LibCalWrapper() - - def is_wharton(self, user): - penn_labs = Group.objects.get(name="Penn Labs") - me_group = Group.objects.get(name="Me", owner=user) - membership = GroupMembership.objects.filter(group=me_group).first() - return membership.check_wharton() or user in penn_labs.members.all() - - def book_room(self, gid, rid, room_name, start, end, user, group_book=None): - gsr = get_object_or_404(GSR, gid=gid) - - # error catching on view side - if gsr.kind == GSR.KIND_WHARTON: - booking_id = self.WLW.book_room(rid, start, end, user, gsr.lid).get("booking_id") - else: - booking_id = self.LCW.book_room(rid, start, end, user).get("booking_id") - - # creates booking on database - # TODO: break start / end time into smaller chunks and pool credit for group booking - booking = GSRBooking.objects.create( - user=user, - booking_id=str(booking_id), - gsr=gsr, - room_id=rid, - room_name=room_name, - start=datetime.datetime.strptime(start, "%Y-%m-%dT%H:%M:%S%z"), - end=datetime.datetime.strptime(end, "%Y-%m-%dT%H:%M:%S%z"), - ) - - # create reservation with single-person-group containing user - # TODO: create reservation with group that frontend passes in - if not group_book: - single_person_group = get_object_or_404(Group, owner=user) - reservation = Reservation.objects.create( - start=start, end=end, creator=user, group=single_person_group - ) - booking.reservation = reservation - booking.save() - - return booking - - def get_availability(self, lid, gid, start, end, user): - # checks which GSR class to use - gsr = get_object_or_404(GSR, gid=gid) - - if gsr.kind == GSR.KIND_WHARTON: - rooms = self.WLW.get_availability(lid, start, end, user.username) - return {"name": gsr.name, "gid": gsr.gid, "rooms": rooms} - else: - rooms = self.LCW.get_availability(lid, start, end) - # cleans data to match Wharton wrapper - try: - lc_gsr = [x for x in rooms["categories"] if x["cid"] == int(gid)][0] - except IndexError: - raise APIError("Unknown GSR") - - for room in lc_gsr["rooms"]: - for availability in room["availability"]: - availability["start_time"] = availability["from"] - availability["end_time"] = availability["to"] - del availability["from"] - del availability["to"] - - context = {"name": lc_gsr["name"], "gid": lc_gsr["cid"]} - context["rooms"] = [ - {"room_name": x["name"], "id": x["id"], "availability": x["availability"]} - for x in lc_gsr["rooms"] - ] - return context +class AbstractBookingWrapper(ABC): + @abstractmethod + def book_room(self, rid, start, end, user): + raise NotImplementedError # pragma: no cover + @abstractmethod def cancel_room(self, booking_id, user): - try: - # gets reservations from wharton for a user - wharton_bookings = self.WLW.get_reservations(user) - except APIError as e: - # don't throw error if the student is non-wharton - if str(e) == "Wharton: GSR view restricted to Wharton Pennkeys": - wharton_bookings = [] - else: - raise APIError(f"Error: {str(e)}") - wharton_booking_ids = [str(x["booking_id"]) for x in wharton_bookings] - try: - gsr_booking = GSRBooking.objects.filter(booking_id=booking_id).first() - # checks if the booking_id is a wharton booking_id - if booking_id in wharton_booking_ids: - self.WLW.cancel_room(user, booking_id) - else: - # defaults to wharton because it is in wharton_booking_ids - self.LCW.cancel_room(user, booking_id) - except APIError as e: - raise APIError(f"Error: {str(e)}") - - if gsr_booking: - # updates GSR booking after done - gsr_booking.is_cancelled = True - gsr_booking.save() + raise NotImplementedError # pragma: no cover - reservation = gsr_booking.reservation - all_cancelled = True - # loops through all reservation bookings and checks if all - # corresponding bookings are cancelled - for booking in GSRBooking.objects.filter(reservation=reservation): - if not booking.is_cancelled: - all_cancelled = False - break - if all_cancelled: - reservation.is_cancelled = True - reservation.save() + @abstractmethod + def get_availability(self, lid, start, end, user): + raise NotImplementedError # pragma: no cover + @abstractmethod def get_reservations(self, user): - bookings = self.LCW.get_reservations(user) + self.WLW.get_reservations(user) + raise NotImplementedError # pragma: no cover - # TODO: toggle this for everyone - group = Group.objects.get(name="Penn Labs") - if user in group.members.all(): - for booking in bookings: - gsr_booking = GSRBooking.objects.filter(booking_id=booking["booking_id"]).first() - if not gsr_booking: - booking["room_name"] = "[Me] " + booking["room_name"] - else: - # TODO: change this once we release the "Me" group - if user == gsr_booking.reservation.creator: - booking["room_name"] = "[Me] " + booking["room_name"] - else: - booking["room_name"] = ( - f"[{gsr_booking.reservation.group.name}] " + booking["room_name"] - ) - return bookings - - def check_credits(self, user): - wharton_booking_credits = self.WLW.check_credits(user) - libcal_booking_credits = self.LCW.check_credits(user) - credits_merged = libcal_booking_credits.copy() - credits_merged.update(wharton_booking_credits) - return credits_merged - - -class WhartonLibWrapper: + +class WhartonBookingWrapper(AbstractBookingWrapper): def request(self, *args, **kwargs): """Make a signed request to the libcal API.""" - - headers = {"Authorization": f"Token {settings.WHARTON_TOKEN}"} - # add authorization headers - kwargs["headers"] = headers + kwargs["headers"] = {"Authorization": f"Token {settings.WHARTON_TOKEN}"} try: response = requests.request(*args, **kwargs) except (ConnectTimeout, ReadTimeout, ConnectionError): raise APIError("Wharton: Connection timeout") - # only wharton students can access these routes - if response.status_code == 403 or response.status_code == 401: - raise APIError("Wharton: GSR view restricted to Wharton Pennkeys") + if not response.ok: + raise APIError(f"Wharton: Error {response.status_code} when reserving data") + return response - def is_wharton(self, username): - url = f"{WHARTON_URL}{username}/privileges" - try: - response = self.request("GET", url).json() - return response["type"] != "None" - except APIError: - return False + def book_room(self, rid, start, end, user): + """Books room if pennkey is valid""" + payload = { + "start": start, + "end": end, + "pennkey": user.username, + "room": rid, + } + url = f"{WHARTON_URL}{user.username}/student_reserve" + response = self.request("POST", url, json=payload).json() + if "error" in response: + raise APIError("Wharton: " + response["error"]) + return response - def get_availability(self, lid, start, end, username): + def cancel_room(self, booking_id, user): + """Cancels reservation given booking id""" + url = f"{WHARTON_URL}{user.username}/reservations/{booking_id}/cancel" + response = self.request("DELETE", url).json() + if "detail" in response: + raise APIError("Wharton: " + response["detail"]) + return response + + def get_availability(self, lid, start, end, user): """Returns a list of rooms and their availabilities""" current_time = timezone.localtime() search_date = ( @@ -209,8 +108,9 @@ def get_availability(self, lid, start, end, username): ) # hits availability route for a given lid and date - url = f"{WHARTON_URL}{username}/availability/{lid}/{str(search_date)}" + url = f"{WHARTON_URL}{user.username}/availability/{lid}/{str(search_date)}" rooms = self.request("GET", url).json() + if "closed" in rooms and rooms["closed"]: return [] @@ -235,116 +135,37 @@ def get_availability(self, lid, start, end, username): room["availability"] = valid_slots return rooms - def book_room(self, rid, start, end, user, lid): - """Books room if pennkey is valid""" - - start_date = datetime.datetime.strptime(start, "%Y-%m-%dT%H:%M:%S%z") - end_date = datetime.datetime.strptime(end, "%Y-%m-%dT%H:%M:%S%z") - duration = int((end_date.timestamp() - start_date.timestamp()) / 60) - - if self.check_credits(user)[lid] < duration: - raise APIError("Not Enough Credits to Book") - - payload = { - "start": start, - "end": end, - "pennkey": user.username, - "room": rid, - } - url = f"{WHARTON_URL}{user.username}/student_reserve" - response = self.request("POST", url, json=payload).json() - if "error" in response: - raise APIError("Wharton: " + response["error"]) - return response - def get_reservations(self, user): - booking_ids = set() - - reservations = Reservation.objects.filter( - creator=user, end__gte=timezone.localtime(), is_cancelled=False - ) - group_gsrs = GSRBooking.objects.filter( - reservation__in=reservations, gsr__in=GSR.objects.filter(kind=GSR.KIND_WHARTON) - ) - - wharton_bookings = GSRBookingSerializer( - GSRBooking.objects.filter( - user=user, - gsr__in=GSR.objects.filter(kind=GSR.KIND_WHARTON), - end__gte=timezone.localtime(), - is_cancelled=False, - ).union(group_gsrs), - many=True, - ).data - - for wharton_booking in wharton_bookings: - booking_ids.add(wharton_booking["booking_id"]) + url = f"{WHARTON_URL}{user.username}/reservations" + bookings = self.request("GET", url).json()["bookings"] + + now = timezone.localtime() + return [ + { + "booking_id": str(booking["booking_id"]), + "gid": booking["lid"], # their lid is our gid + "room_id": booking["rid"], + "room_name": booking["room"], + "start": booking["start"], + "end": booking["end"], + } + for booking in bookings + if datetime.datetime.strptime(booking["end"], "%Y-%m-%dT%H:%M:%S%z") >= now + ] + def is_wharton(self, user): + url = f"{WHARTON_URL}{user.username}/privileges" try: - url = f"{WHARTON_URL}{user.username}/reservations" - bookings = self.request("GET", url).json()["bookings"] - # ignore this because this route is used by everyone - for booking in bookings: - booking["lid"] = GSR.objects.get(gid=booking["lid"]).lid - # checks if reservation is within time range - if ( - datetime.datetime.strptime(booking["end"], "%Y-%m-%dT%H:%M:%S%z") - >= timezone.localtime() - ): - # filtering for lid here works because Wharton buildings have distinct lid's - if str(booking["booking_id"]) not in booking_ids: - context = { - "booking_id": str(booking["booking_id"]), - "gsr": GSRSerializer(GSR.objects.get(lid=booking["lid"])).data, - "room_id": booking["rid"], - "room_name": booking["room"], - "start": booking["start"], - "end": booking["end"], - } - wharton_bookings.append(context) - booking_ids.add(str(booking["booking_id"])) + response = self.request("GET", url) + if response.status_code != 200: + return None + res_json = response.json() + return res_json.get("type") == "whartonMBA" or res_json.get("type") == "whartonUGR" except APIError: - pass + return None - return wharton_bookings - - def cancel_room(self, user, booking_id): - """Cancels reservation given booking id""" - wharton_booking = GSRBooking.objects.filter(booking_id=booking_id) - username = user.username - if wharton_booking.exists(): - gsr_booking = wharton_booking.first() - gsr_booking.is_cancelled = True - gsr_booking.save() - # changing username if booking is in database - username = gsr_booking.user.username - url = f"{WHARTON_URL}{username}/reservations/{booking_id}/cancel" - response = self.request("DELETE", url).json() - if "detail" in response: - raise APIError("Wharton: " + response["detail"]) - return response - def check_credits(self, user): - # gets all current reservations from wharton availability route - wharton_lids = GSR.objects.filter(kind=GSR.KIND_WHARTON).values_list("lid", flat=True) - # wharton get 90 minutes of credit at any moment - default_credit = 90 if self.is_wharton(user.username) else 0 - wharton_credits = {lid: default_credit for lid in wharton_lids} - reservations = self.get_reservations(user) - for reservation in reservations: - # determines if ARB or Huntsman - room_type = reservation["gsr"]["lid"] - if room_type in wharton_credits: - # accumulates total minutes - start = datetime.datetime.strptime(reservation["start"], "%Y-%m-%dT%H:%M:%S%z") - end = datetime.datetime.strptime(reservation["end"], "%Y-%m-%dT%H:%M:%S%z") - wharton_credits[room_type] -= int((end.timestamp() - start.timestamp()) / 60) - - # 90 minutes at any given time - return wharton_credits - - -class LibCalWrapper: +class LibCalBookingWrapper(AbstractBookingWrapper): def __init__(self): self.token = None self.expiration = timezone.localtime() @@ -358,6 +179,7 @@ def update_token(self): "client_secret": settings.LIBCAL_SECRET, "grant_type": "client_credentials", } + response = requests.post(f"{API_URL}/1.1/oauth/token", body).json() if "error" in response: @@ -382,86 +204,8 @@ def request(self, *args, **kwargs): except (ConnectTimeout, ReadTimeout, ConnectionError): raise APIError("LibCal: Connection timeout") - def get_availability(self, lid, start=None, end=None): - """Returns a list of rooms and their availabilities""" - - # adjusts url based on start and end times - range_str = "availability" - if start: - start_datetime = datetime.datetime.combine( - datetime.datetime.strptime(start, "%Y-%m-%d").date(), datetime.datetime.min.time() - ) - range_str += "=" + start - if end and not start == end: - range_str += "," + end - else: - start_datetime = None - - response = self.request("GET", f"{API_URL}/1.1/space/categories/{lid}").json() - if "error" in response: - raise APIError("LibCal: " + response["error"]) - output = {"id": lid, "categories": []} - - # if there aren't any rooms associated with this location, return - if len(response) < 1: - return output - - if "error" in response[0]: - raise APIError("LibCal: " + response[0]["error"]) - - if "categories" not in response[0]: - return output - - # filters categories and then gets extra information on each room - categories = response[0]["categories"] - id_to_category = {i["cid"]: i["name"] for i in categories} - categories = ",".join([str(x["cid"]) for x in categories]) - response = self.request("GET", f"{API_URL}/1.1/space/category/{categories}").json() - for category in response: - cat_out = {"cid": category["cid"], "name": id_to_category[category["cid"]], "rooms": []} - - # ignore equipment categories - if cat_out["name"].endswith("Equipment"): - continue - - items = category["items"] - items = ",".join([str(x) for x in items]) - # hits this route for extra information - response = self.request("GET", f"{API_URL}/1.1/space/item/{items}?{range_str}") - - if response.ok: - for room in response.json(): - if room["id"] in ROOM_BLACKLIST: - continue - # remove extra fields - if "formid" in room: - del room["formid"] - # enforce date filter - # API returns dates outside of the range, fix this manually - if start_datetime: - out_times = [] - for time in room["availability"]: - parsed_start = datetime.datetime.strptime( - time["from"][:-6], "%Y-%m-%dT%H:%M:%S" - ) - if parsed_start >= start_datetime: - out_times.append(time) - room["availability"] = out_times - cat_out["rooms"].append(room) - if cat_out["rooms"]: - output["categories"].append(cat_out) - return output - - def book_room(self, rid, start, end, user, test=False): - """Books a room given the required information.""" - - start_date = datetime.datetime.strptime(start, "%Y-%m-%dT%H:%M:%S%z") - end_date = datetime.datetime.strptime(end, "%Y-%m-%dT%H:%M:%S%z") - duration = int((end_date.timestamp() - start_date.timestamp()) / 60) - - if self.check_credits(user, start_date)[CreditType.LIBCAL.value] < duration: - raise APIError("Not Enough Credits to Book") - + def book_room(self, rid, start, end, user): + """Books room if pennkey is valid""" # turns parameters into valid json format, then books room payload = { "start": start, @@ -471,7 +215,7 @@ def book_room(self, rid, start, end, user, test=False): "nickname": f"{user.username} GSR Booking", "q43": f"{user.username} GSR Booking", "bookings": [{"id": rid, "to": end}], - "test": test, + "test": False, "q2555": "5", "q2537": "5", "q3699": self.get_affiliation(user.email), @@ -489,37 +233,70 @@ def book_room(self, rid, start, end, user, test=False): res_json = response.json() # corrects keys in response - if "error" not in res_json: - if "errors" in res_json: - errors = res_json["errors"] - if isinstance(errors, list): - errors = " ".join(errors) - res_json["error"] = BeautifulSoup( - errors.replace("\n", " "), "html.parser" - ).text.strip() - del res_json["errors"] + if "error" not in res_json and "errors" in res_json: + errors = res_json["errors"] + if isinstance(errors, list): + errors = " ".join(errors) + res_json["error"] = BeautifulSoup(errors.replace("\n", " "), "html.parser").text.strip() + del res_json["errors"] if "error" in res_json: raise APIError("LibCal: " + res_json["error"]) return res_json def get_reservations(self, user): + pass - reservations = Reservation.objects.filter( - creator=user, end__gte=timezone.localtime(), is_cancelled=False - ) - group_gsrs = GSRBooking.objects.filter( - reservation__in=reservations, gsr__in=GSR.objects.filter(kind=GSR.KIND_LIBCAL) - ) + def cancel_room(self, booking_id, user): + """Cancels room""" + response = self.request("POST", f"{API_URL}/1.1/space/cancel/{booking_id}").json() + if "error" in response[0]: + raise APIError("LibCal: " + response[0]["error"]) + return response - return GSRBookingSerializer( - GSRBooking.objects.filter( - user=user, - gsr__in=GSR.objects.filter(kind=GSR.KIND_LIBCAL), - end__gte=timezone.localtime(), - is_cancelled=False, - ).union(group_gsrs), - many=True, - ).data + def get_availability(self, gid, start, end, user): + """Returns a list of rooms and their availabilities""" + + # adjusts url based on start and end times + range_str = "availability" + if start: + start_datetime = datetime.datetime.combine( + datetime.datetime.strptime(start, "%Y-%m-%d").date(), datetime.datetime.min.time() + ) + range_str += "=" + start + if end and not start == end: + range_str += "," + end + else: + start_datetime = None + + # filters categories and then gets extra information on each room + + response = self.request("GET", f"{API_URL}/1.1/space/category/{gid}").json() + items = response[0]["items"] + items = ",".join([str(item) for item in items]) + response = self.request("GET", f"{API_URL}/1.1/space/item/{items}?{range_str}") + + if response.status_code != 200: + raise APIError(f"GSR Reserve: Error {response.status_code} when reserving data") + + rooms = [ + {"room_name": room["name"], "id": room["id"], "availability": room["availability"]} + for room in response.json() + if room["id"] not in ROOM_BLACKLIST + ] + for room in rooms: + # remove extra fields + if "formid" in room: + del room["formid"] + # enforce date filter + # API returns dates outside of the range, fix this manually + room["availability"] = [ + {"start_time": time["from"], "end_time": time["to"]} + for time in room["availability"] + if not start_datetime + or datetime.datetime.strptime(time["from"][:-6], "%Y-%m-%dT%H:%M:%S") + >= start_datetime + ] + return rooms def get_affiliation(self, email): """Gets school from email""" @@ -532,38 +309,240 @@ def get_affiliation(self, email): else: return "Other" - def cancel_room(self, user, booking_id): - """Cancels room""" - gsr_booking = get_object_or_404(GSRBooking, booking_id=booking_id) - if gsr_booking: - if user != gsr_booking.user and user != gsr_booking.reservation.creator: + +class BookingHandler: + def __init__(self, WBW=None, LBW=None): + self.WBW = WBW or WhartonBookingWrapper() + self.LBW = LBW or LibCalBookingWrapper() + + def format_members(self, members): + PREFIX = "user__" + return [ + ( + User( + **{k[len(PREFIX) :]: v for k, v in member.items() if k.startswith(PREFIX)} + ), # temp user object + member["credits"], + ) + for member in members + ] + + def get_wharton_members(self, group, gsr_id): + now = timezone.localtime() + ninty_min = datetime.timedelta(minutes=90) + zero_min = datetime.timedelta(minutes=0) + + # Wharton allows 90 minutes at a time + ret = ( + GroupMembership.objects.filter(group=group, is_wharton=True) + .values("user") + .annotate( + credits=ninty_min + - Coalesce( + Sum( + F("user__gsrbooking__end") - F("user__gsrbooking__start"), + filter=Q(user__gsrbooking__gsr__gid=gsr_id) + & Q(user__gsrbooking__is_cancelled=False) + & Q(user__gsrbooking__end__gte=now), + ), + zero_min, + ) + ) + .filter(Q(credits__gt=zero_min)) + .values("user__id", "user__username", "credits") + .order_by("?")[:WHARTON_CREDIT_LIMIT] + ) + return self.format_members(ret) + + def get_libcal_members(self, group): + day_start = timezone.localtime().replace(hour=0, minute=0, second=0, microsecond=0) + day_end = day_start + datetime.timedelta(days=1) + two_hours = datetime.timedelta(hours=2) + zero_min = datetime.timedelta(minutes=0) + + # LibCal allows 2 hours a day, needs extra user fields for booking purposes + ret = ( + GroupMembership.objects.filter(group=group) + .values("user") + .annotate( + credits=two_hours + - Coalesce( + Sum( + F("user__gsrbooking__end") - F("user__gsrbooking__start"), + filter=Q(user__gsrbooking__gsr__kind=GSR.KIND_LIBCAL) + & Q(user__gsrbooking__is_cancelled=False) + & Q(user__gsrbooking__start__gte=day_start) + & Q(user__gsrbooking__end__lte=day_end), + ), + zero_min, + ) + ) + .filter(Q(credits__gt=zero_min)) + .values( + "user__id", + "user__username", + "user__first_name", + "user__last_name", + "user__email", + "credits", + ) + .order_by("?")[:LIBCAL_CREDIT_LIMIT] + ) + return self.format_members(ret) + + def book_room(self, gid, rid, room_name, start, end, user, group=None): + # NOTE when booking with a group, we are only querying our db for existing bookings, + # so users in a group who book through wharton may screw up the query + gsr = get_object_or_404(GSR, gid=gid) + start = datetime.datetime.strptime(start, "%Y-%m-%dT%H:%M:%S%z") + end = datetime.datetime.strptime(end, "%Y-%m-%dT%H:%M:%S%z") + + book_func = self.WBW.book_room if gsr.kind == GSR.KIND_WHARTON else self.LBW.book_room + members = ( + [(user, datetime.timedelta(days=99))] + if group is None + else self.get_wharton_members(group, gsr.id) + if gsr.kind == GSR.KIND_WHARTON + else self.get_libcal_members(group) + ) + + total_time_available = sum( + [time_available for _, time_available in members], datetime.timedelta(minutes=0) + ) + + if (end - start) >= total_time_available: + raise APIError("Error: Not enough credits") + + reservation = Reservation.objects.create(start=start, end=end, creator=user, group=group) + + curr_start = start + try: + for curr_user, time_available in members: + curr_end = curr_start + min(time_available, end - curr_start) + + booking_id = book_func( + rid, + curr_start.strftime("%Y-%m-%dT%H:%M:%S%z"), + curr_end.strftime("%Y-%m-%dT%H:%M:%S%z"), + curr_user, + )["booking_id"] + booking = GSRBooking.objects.create( + user_id=curr_user.id, + booking_id=str(booking_id), + gsr=gsr, + room_id=rid, + room_name=room_name, + start=curr_start, + end=curr_end, + ) + booking.reservation = reservation + booking.save() + + if (curr_start := curr_end) >= end: + break + except APIError as e: + raise APIError( + f"{str(e)}. Was only able to book {start.strftime('%H:%M')}" + f" - {curr_start.strftime('%H:%M')}" + ) + + return reservation + + def cancel_room(self, booking_id, user): + if ( + gsr_booking := GSRBooking.objects.filter(booking_id=booking_id) + .prefetch_related(Prefetch("reservation__gsrbooking_set"), Prefetch("gsr")) + .first() + ) : + if gsr_booking.user != user and gsr_booking.reservation.creator != user: raise APIError("Error: Unauthorized: This reservation was booked by someone else.") + + (self.WBW if gsr_booking.gsr.kind == GSR.KIND_WHARTON else self.LBW).cancel_room( + booking_id, gsr_booking.user + ) + gsr_booking.is_cancelled = True gsr_booking.save() - response = self.request("POST", f"{API_URL}/1.1/space/cancel/{booking_id}").json() - if "error" in response[0]: - raise APIError("LibCal: " + response[0]["error"]) - return response - def check_credits(self, user, lc_start=None): - # default to beginning of day - if lc_start is None: - lc_start = make_aware(datetime.datetime.now()) - lc_start = lc_start.replace(second=0, microsecond=0, minute=0, hour=0) - - lc_end = lc_start + datetime.timedelta(days=1) - - # filters for all reservations for the given date - reservations = GSRBooking.objects.filter( - gsr__in=GSR.objects.filter(kind=GSR.KIND_LIBCAL), - start__gte=lc_start, - end__lte=lc_end, - is_cancelled=False, - user=user, + reservation = gsr_booking.reservation + if all(booking.is_cancelled for booking in reservation.gsrbooking_set.all()): + reservation.is_cancelled = True + reservation.save() + else: + for service in [self.WBW, self.LBW]: + try: + service.cancel_room(booking_id, user) + return + except APIError: + pass + raise APIError("Error: Unknown booking id") + + def get_availability(self, lid, gid, start, end, user, group=None): + gsr = get_object_or_404(GSR, gid=gid) + + # select a random user from the group if booking wharton gsr + if gsr.kind == GSR.KIND_WHARTON and group is not None: + wharton_members = group.memberships.filter(is_wharton=True) + if (n := wharton_members.count()) == 0: + raise APIError("Error: Non Wharton cannot book Wharton GSR") + user = wharton_members[randint(0, n - 1)].user + + rooms = ( + self.WBW.get_availability(lid, start, end, user) + if gsr.kind == GSR.KIND_WHARTON + else self.LBW.get_availability(gid, start, end, user) ) - total_minutes = 0 - for reservation in reservations: - # accumulates total minutes over all reservations - total_minutes += int((reservation.end.timestamp() - reservation.start.timestamp()) / 60) - # 120 minutes per day - return {CreditType.LIBCAL.value: 120 - total_minutes} + return {"name": gsr.name, "gid": gsr.gid, "rooms": rooms} + + def get_reservations(self, user, group=None): + q = Q(user=user) | Q(reservation__creator=user) if group else Q(user=user) + bookings = GSRBooking.objects.filter( + q, is_cancelled=False, end__gte=timezone.localtime() + ).prefetch_related(Prefetch("reservation")) + + if group: + ret = [] + for booking in bookings: + data = GSRBookingSerializer(booking).data + if booking.reservation.creator == user: + data["room_name"] = f"[Me] {data['room_name']}" + else: + data["room_name"] = f"[{group.name}] {data['room_name']}" + ret.append(data) + else: + ret = GSRBookingSerializer(bookings, many=True).data + + # deal with bookings made directly through wharton (not us) + try: + wharton_bookings = self.WBW.get_reservations(user) + except APIError: + return ret + + if len(wharton_bookings) == 0: + return ret + + booking_ids = set([booking["booking_id"] for booking in ret]) + wharton_bookings = [ + booking for booking in wharton_bookings if booking["booking_id"] not in booking_ids + ] + if len(wharton_bookings) == 0: + return ret + + wharton_gsr_datas = { + gsr.gid: GSRSerializer(gsr).data for gsr in GSR.objects.filter(kind=GSR.KIND_WHARTON) + } + for booking in wharton_bookings: + booking["gsr"] = wharton_gsr_datas[booking["gid"]] + del booking["gid"] + ret.append(booking) + return ret + + # seems like its unused on the frontend + # def check_credits(self, user): + # pass + + +# initialize singletons +WhartonGSRBooker = WhartonBookingWrapper() +LibCalGSRBooker = LibCalBookingWrapper() +GSRBooker = BookingHandler(WhartonGSRBooker, LibCalGSRBooker) diff --git a/backend/gsr_booking/group_logic.py b/backend/gsr_booking/group_logic.py deleted file mode 100644 index df821a53..00000000 --- a/backend/gsr_booking/group_logic.py +++ /dev/null @@ -1,106 +0,0 @@ -import datetime -import random - -from django.contrib.auth import get_user_model -from django.shortcuts import get_object_or_404 - -from gsr_booking.api_wrapper import APIError, BookingWrapper, CreditType -from gsr_booking.models import GSR, GroupMembership, Reservation - - -User = get_user_model() - - -class GroupBook: - def __init__(self): - self.bw = BookingWrapper() - - def get_wharton_users(self, group): - """ - Returns list of wharton users of a Group in random ordering - """ - # TODO: filter for pennkey allowed - wharton_users = list(GroupMembership.objects.filter(group=group, is_wharton=True)) - # shuffle to prevent sequential booking - random.shuffle(wharton_users) - return wharton_users - - def get_all_users(self, group): - """ - Returns list of all users of a Group in random ordering - """ - # TODO: filter for pennkey allowed - all_users = list(GroupMembership.objects.filter(group=group)) - # shuffle to prevent sequential booking - random.shuffle(all_users) - return all_users - - def book_room(self, gid, rid, room_name, start, end, user, group): - """ - Book function for Group - """ - # TODO: check credits - gsr = get_object_or_404(GSR, gid=gid) - - start = datetime.datetime.strptime(start, "%Y-%m-%dT%H:%M:%S%z") - end = datetime.datetime.strptime(end, "%Y-%m-%dT%H:%M:%S%z") - - # sets users and credit_id depending on the gsr type - if gsr.kind == GSR.KIND_WHARTON: - users = self.get_wharton_users(group) - credit_id = gsr.lid - else: - users = self.get_all_users(group) - credit_id = CreditType.LIBCAL.value - - total_credits = sum([self.bw.check_credits(usr.user).get(credit_id, 0) for usr in users]) - duration = int((end.timestamp() - start.timestamp()) / 60) - if total_credits < duration: - raise APIError("Not Enough Credits to Book") - if duration % 30 != 0: - raise APIError("Invalid duration") - - # creates reservation object to be used to group each booking - reservation = Reservation.objects.create(start=start, end=end, creator=user, group=group) - - # we could potentially repeat using a user to make a 30 min booking so - # loop until total duration booked - while duration > 0: - # loop through each user and try to make a 30 min booking - # under that user if they have enough credits - for usr in users: - credit = self.bw.check_credits(usr.user).get(credit_id, 0) - if credit < 30: - continue - curr_end = start + datetime.timedelta(minutes=30) # end of current booking - booking = self.bw.book_room( - gid, - rid, - room_name, - start.strftime("%Y-%m-%dT%H:%M:%S%z"), - curr_end.strftime("%Y-%m-%dT%H:%M:%S%z"), - usr.user, - group_book=True, - ) - booking.reservation = reservation - booking.save() - # update new start and duration appropriately - start = curr_end - duration -= 30 - if duration <= 0: - break - return reservation - - def get_availability(self, lid, gid, start, end, user, group): - """ - Availability function for Group - """ - - gsr = GSR.objects.filter(gid=gid).first() - if gsr.kind == GSR.KIND_WHARTON: - # check if wharton users is non-empty - wharton_user = GroupMembership.objects.filter(group=group, is_wharton=True).first() - if wharton_user: - return self.bw.get_availability(lid, gid, start, end, wharton_user.user) - - return self.bw.get_availability(lid, gid, start, end, user) diff --git a/backend/gsr_booking/migrations/0011_alter_reservation_group.py b/backend/gsr_booking/migrations/0011_alter_reservation_group.py new file mode 100644 index 00000000..df639bad --- /dev/null +++ b/backend/gsr_booking/migrations/0011_alter_reservation_group.py @@ -0,0 +1,24 @@ +# Generated by Django 3.2.19 on 2023-11-12 19:21 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("gsr_booking", "0010_remove_gsrbooking_reminder_sent_and_more"), + ] + + operations = [ + migrations.AlterField( + model_name="reservation", + name="group", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="gsr_booking.group", + ), + ), + ] diff --git a/backend/gsr_booking/models.py b/backend/gsr_booking/models.py index fe36fec9..95740bcc 100644 --- a/backend/gsr_booking/models.py +++ b/backend/gsr_booking/models.py @@ -1,9 +1,6 @@ -import requests -from django.conf import settings from django.contrib.auth import get_user_model from django.db import models from django.utils import timezone -from requests.exceptions import ConnectionError, ConnectTimeout, ReadTimeout User = get_user_model() @@ -45,20 +42,7 @@ def save(self, *args, **kwargs): super().save(*args, **kwargs) def check_wharton(self): - # not using api_wrapper.py to prevent circular dependency - url = f"https://apps.wharton.upenn.edu/gsr/api/v1/{self.user.username}/privileges" - try: - response = requests.get( - url, headers={"Authorization": f"Token {settings.WHARTON_TOKEN}"} - ) - - if response.status_code != 200: - return None - - res_json = response.json() - return res_json.get("type") == "whartonMBA" or res_json.get("type") == "whartonUGR" - except (ConnectTimeout, ReadTimeout, KeyError, ConnectionError): - return None + return WhartonGSRBooker.is_wharton(self.user) class Meta: verbose_name = "Group Membership" @@ -120,7 +104,7 @@ class Reservation(models.Model): start = models.DateTimeField(default=timezone.now) end = models.DateTimeField(default=timezone.now) creator = models.ForeignKey(User, on_delete=models.CASCADE) - group = models.ForeignKey(Group, on_delete=models.CASCADE) + group = models.ForeignKey(Group, on_delete=models.CASCADE, null=True, blank=True) is_cancelled = models.BooleanField(default=False) reminder_sent = models.BooleanField(default=False) @@ -136,3 +120,7 @@ class GSRBooking(models.Model): start = models.DateTimeField(default=timezone.now) end = models.DateTimeField(default=timezone.now) is_cancelled = models.BooleanField(default=False) + + +# import at end to prevent circular dependency +from gsr_booking.api_wrapper import WhartonGSRBooker # noqa: E402 diff --git a/backend/gsr_booking/urls.py b/backend/gsr_booking/urls.py index 8baac9d3..25d35263 100644 --- a/backend/gsr_booking/urls.py +++ b/backend/gsr_booking/urls.py @@ -7,7 +7,6 @@ BookRoom, CancelRoom, CheckWharton, - CreditsView, GroupMembershipViewSet, GroupViewSet, Locations, @@ -33,5 +32,4 @@ path("book/", BookRoom.as_view(), name="book"), path("cancel/", CancelRoom.as_view(), name="cancel"), path("reservations/", ReservationsView.as_view(), name="reservations"), - path("credits/", CreditsView.as_view(), name="credits"), ] diff --git a/backend/gsr_booking/views.py b/backend/gsr_booking/views.py index c6dd3ab1..200d96c0 100644 --- a/backend/gsr_booking/views.py +++ b/backend/gsr_booking/views.py @@ -9,8 +9,7 @@ from rest_framework.response import Response from rest_framework.views import APIView -from gsr_booking.api_wrapper import APIError, BookingWrapper -from gsr_booking.group_logic import GroupBook +from gsr_booking.api_wrapper import APIError, GSRBooker, WhartonGSRBooker from gsr_booking.models import GSR, Group, GroupMembership, GSRBooking from gsr_booking.serializers import ( GroupMembershipSerializer, @@ -164,11 +163,6 @@ def get_queryset(self): ) -# umbrella class used for accessing GSR API's (needed for token authentication) -BW = BookingWrapper() -GB = GroupBook() - - class Locations(generics.ListAPIView): """Lists all available locations to book from""" @@ -192,8 +186,11 @@ def get_queryset(self): class CheckWharton(APIView): + + permission_classes = [IsAuthenticated] + def get(self, request): - return Response({"is_wharton": BW.is_wharton(request.user)}) + return Response({"is_wharton": WhartonGSRBooker.is_wharton(request.user)}) class Availability(APIView): @@ -211,11 +208,16 @@ def get(self, request, lid, gid): end = request.GET.get("end") try: - group = Group.objects.get(name="Penn Labs") - if request.user in group.members.all(): - return Response(GB.get_availability(lid, gid, start, end, request.user, group)) - else: - return Response(BW.get_availability(lid, gid, start, end, request.user)) + return Response( + GSRBooker.get_availability( + lid, + gid, + start, + end, + request.user, + request.user.booking_groups.filter(name="Penn Labs").first(), + ) + ) except APIError as e: return Response({"error": str(e)}, status=400) @@ -233,11 +235,15 @@ def post(self, request): room_name = request.data["room_name"] try: - group = Group.objects.get(name="Penn Labs") - if request.user in group.members.all(): - GB.book_room(gid, room_id, room_name, start, end, request.user, group) - else: - BW.book_room(gid, room_id, room_name, start, end, request.user) + GSRBooker.book_room( + gid, + room_id, + room_name, + start, + end, + request.user, + request.user.booking_groups.filter(name="Penn Labs").first(), + ) return Response({"detail": "success"}) except APIError as e: return Response({"error": str(e)}, status=400) @@ -254,7 +260,7 @@ def post(self, request): booking_id = request.data["booking_id"] try: - BW.cancel_room(booking_id, request.user) + GSRBooker.cancel_room(booking_id, request.user) return Response({"detail": "success"}) except APIError as e: return Response({"error": str(e)}, status=400) @@ -268,15 +274,8 @@ class ReservationsView(APIView): permission_classes = [IsAuthenticated] def get(self, request): - return Response(BW.get_reservations(request.user)) - - -class CreditsView(APIView): - """ - Gets credits for a User - """ - - permission_classes = [IsAuthenticated] - - def get(self, request): - return Response(BW.check_credits(request.user)) + return Response( + GSRBooker.get_reservations( + request.user, request.user.booking_groups.filter(name="Penn Labs").first() + ) + ) diff --git a/backend/setup.cfg b/backend/setup.cfg index 9c6ee1b5..278639ed 100644 --- a/backend/setup.cfg +++ b/backend/setup.cfg @@ -2,6 +2,7 @@ max-line-length = 100 exclude = .venv, migrations inline-quotes = double +ignore = E203, W503 [isort] default_section = THIRDPARTY diff --git a/backend/tests/gsr_booking/test_gsr_views.py b/backend/tests/gsr_booking/test_gsr_views.py index ff2d4a73..c749d2b9 100644 --- a/backend/tests/gsr_booking/test_gsr_views.py +++ b/backend/tests/gsr_booking/test_gsr_views.py @@ -92,21 +92,21 @@ def test_recent(self): self.assertIn("image_url", res_json[0]) self.assertNotEqual(res_json[0]["id"], res_json[1]["id"]) - @mock.patch("gsr_booking.views.BW.is_wharton", is_wharton_false) + @mock.patch("gsr_booking.api_wrapper.WhartonBookingWrapper.is_wharton", is_wharton_false) def test_get_wharton_false(self): response = self.client.get(reverse("is-wharton")) res_json = json.loads(response.content) self.assertEqual(1, len(res_json)) self.assertFalse(res_json["is_wharton"]) - @mock.patch("gsr_booking.views.BW.is_wharton", is_wharton_true) + @mock.patch("gsr_booking.api_wrapper.WhartonBookingWrapper.is_wharton", is_wharton_true) def test_get_wharton_true(self): response = self.client.get(reverse("is-wharton")) res_json = json.loads(response.content) self.assertEqual(1, len(res_json)) self.assertTrue(res_json["is_wharton"]) - @mock.patch("gsr_booking.views.BW.get_availability", libcal_availability) + @mock.patch("gsr_booking.api_wrapper.BookingHandler.get_availability", libcal_availability) def test_availability_libcal(self): response = self.client.get(reverse("availability", args=["1086", "1889"])) res_json = json.loads(response.content) @@ -120,7 +120,7 @@ def test_availability_libcal(self): self.assertIn("id", room) self.assertIn("availability", room) - @mock.patch("gsr_booking.views.BW.get_availability", wharton_availability) + @mock.patch("gsr_booking.api_wrapper.BookingHandler.get_availability", wharton_availability) def test_availability_wharton(self): response = self.client.get(reverse("availability", args=["JMHH", "1"])) res_json = json.loads(response.content) @@ -134,7 +134,7 @@ def test_availability_wharton(self): self.assertIn("id", room) self.assertIn("availability", room) - @mock.patch("gsr_booking.views.BW.book_room", book_cancel_room) + @mock.patch("gsr_booking.api_wrapper.BookingHandler.book_room", book_cancel_room) def test_book_libcal(self): payload = { "start_time": "2021-11-21T18:30:00-05:00", @@ -150,7 +150,7 @@ def test_book_libcal(self): self.assertEqual(1, len(res_json)) self.assertEqual("success", res_json["detail"]) - @mock.patch("gsr_booking.views.BW.book_room", book_cancel_room) + @mock.patch("gsr_booking.api_wrapper.BookingHandler.book_room", book_cancel_room) def test_book_wharton(self): payload = { "start_time": "2021-11-21T18:30:00-05:00", @@ -166,7 +166,7 @@ def test_book_wharton(self): self.assertEqual(1, len(res_json)) self.assertEqual("success", res_json["detail"]) - @mock.patch("gsr_booking.views.BW.cancel_room", book_cancel_room) + @mock.patch("gsr_booking.api_wrapper.BookingHandler.cancel_room", book_cancel_room) def test_cancel_room(self): payload = {"booking_id": "booking id"} response = self.client.post( @@ -176,7 +176,7 @@ def test_cancel_room(self): self.assertEqual(1, len(res_json)) self.assertEqual("success", res_json["detail"]) - @mock.patch("gsr_booking.views.BW.get_reservations", reservations) + @mock.patch("gsr_booking.api_wrapper.BookingHandler.get_reservations", reservations) def test_reservations(self): response = self.client.get(reverse("reservations")) res_json = json.loads(response.content) diff --git a/backend/tests/gsr_booking/test_gsr_wrapper.py b/backend/tests/gsr_booking/test_gsr_wrapper.py index 1c2fa586..aa214b95 100644 --- a/backend/tests/gsr_booking/test_gsr_wrapper.py +++ b/backend/tests/gsr_booking/test_gsr_wrapper.py @@ -5,10 +5,10 @@ from django.contrib.auth import get_user_model from django.core.management import call_command from django.test import TestCase +from django.utils import timezone from rest_framework.test import APIClient -from gsr_booking.api_wrapper import BookingWrapper -from gsr_booking.group_logic import GroupBook +from gsr_booking.api_wrapper import APIError, GSRBooker, WhartonGSRBooker from gsr_booking.models import GSR, Group, GroupMembership, GSRBooking, Reservation @@ -51,11 +51,6 @@ def json(self): return Mock(json.load(data), 200) -def mock_check_credits(self, user): - wharton_lids = GSR.objects.filter(kind=GSR.KIND_WHARTON).values_list("lid", flat=True) - return {lid: 90 for lid in wharton_lids} - - class TestBookingWrapper(TestCase): def setUp(self): call_command("load_gsrs") @@ -65,17 +60,15 @@ def setUp(self): ) self.client = APIClient() self.client.force_authenticate(user=self.user) - self.bw = BookingWrapper() - self.gb = GroupBook() self.group = Group.objects.create(owner=self.group_user, name="Penn Labs", color="blue") - @mock.patch("gsr_booking.api_wrapper.WhartonLibWrapper.request", mock_requests_get) + @mock.patch("gsr_booking.api_wrapper.WhartonBookingWrapper.request", mock_requests_get) def test_is_wharton(self): - self.assertFalse(self.bw.is_wharton(self.user)) + self.assertFalse(WhartonGSRBooker.is_wharton(self.user)) - @mock.patch("gsr_booking.api_wrapper.WhartonLibWrapper.request", mock_requests_get) + @mock.patch("gsr_booking.api_wrapper.WhartonBookingWrapper.request", mock_requests_get) def test_wharton_availability(self): - availability = self.bw.get_availability("JMHH", 1, "2021-01-07", "2022-01-08", self.user) + availability = GSRBooker.get_availability("JMHH", 1, "2021-01-07", "2022-01-08", self.user) self.assertIn("name", availability) self.assertIn("gid", availability) self.assertIn("rooms", availability) @@ -83,29 +76,31 @@ def test_wharton_availability(self): self.assertIn("id", availability["rooms"][0]) self.assertIn("availability", availability["rooms"][0]) - @mock.patch("gsr_booking.api_wrapper.WhartonLibWrapper.check_credits", mock_check_credits) - @mock.patch("gsr_booking.api_wrapper.WhartonLibWrapper.request", mock_requests_get) + # @mock.patch("gsr_booking.api_wrapper.WhartonBookingWrapper.check_credits", mock_check_credits) + @mock.patch("gsr_booking.api_wrapper.WhartonBookingWrapper.request", mock_requests_get) def test_book_wharton(self): - book_wharton = self.bw.book_room( + book_wharton = GSRBooker.book_room( 1, 94, "241", "2021-12-05T16:00:00-05:00", "2021-12-05T16:30:00-05:00", self.user ) - self.assertEquals("241", book_wharton.room_name) + self.assertEquals("241", book_wharton.gsrbooking_set.first().room_name) - @mock.patch("gsr_booking.api_wrapper.WhartonLibWrapper.request", mock_requests_get) + @mock.patch("gsr_booking.api_wrapper.WhartonBookingWrapper.request", mock_requests_get) def test_wharton_reservations(self): - reservations = self.bw.WLW.get_reservations(self.user) + reservations = WhartonGSRBooker.get_reservations(self.user) self.assertTrue(isinstance(reservations, list)) self.assertIn("booking_id", reservations[0]) - self.assertIn("gsr", reservations[0]) + self.assertIn("gid", reservations[0]) - @mock.patch("gsr_booking.api_wrapper.WhartonLibWrapper.request", mock_requests_get) + @mock.patch("gsr_booking.api_wrapper.WhartonBookingWrapper.request", mock_requests_get) def test_cancel_wharton(self): - cancel = self.bw.cancel_room("987654", self.user) + cancel = GSRBooker.cancel_room("987654", self.user) self.assertIsNone(cancel) - @mock.patch("gsr_booking.api_wrapper.LibCalWrapper.request", mock_requests_get) + @mock.patch("gsr_booking.api_wrapper.LibCalBookingWrapper.request", mock_requests_get) def test_libcal_availability(self): - availability = self.bw.get_availability("1086", 1889, "2021-01-07", "2022-01-08", self.user) + availability = GSRBooker.get_availability( + "1086", 1889, "2021-01-07", "2022-01-08", self.user + ) self.assertIn("name", availability) self.assertIn("gid", availability) self.assertIn("rooms", availability) @@ -113,9 +108,9 @@ def test_libcal_availability(self): self.assertIn("id", availability["rooms"][0]) self.assertIn("availability", availability["rooms"][0]) - @mock.patch("gsr_booking.api_wrapper.LibCalWrapper.request", mock_requests_get) + @mock.patch("gsr_booking.api_wrapper.LibCalBookingWrapper.request", mock_requests_get) def test_book_libcal(self): - book_libcal = self.bw.book_room( + book_libcal = GSRBooker.book_room( 1889, 7192, "VP WIC Booth 01", @@ -123,18 +118,18 @@ def test_book_libcal(self): "2021-12-05T16:30:00-05:00", self.user, ) - self.assertEquals("VP WIC Booth 01", book_libcal.room_name) + self.assertEquals("VP WIC Booth 01", book_libcal.gsrbooking_set.first().room_name) @mock.patch( - "gsr_booking.api_wrapper.WhartonLibWrapper.request", mock_requests_get + "gsr_booking.api_wrapper.WhartonBookingWrapper.request", mock_requests_get ) # purposefully wharton request here def test_libcal_reservations(self): - reservations = self.bw.get_reservations(self.user) + reservations = GSRBooker.get_reservations(self.user) self.assertTrue(isinstance(reservations, list)) self.assertIn("booking_id", reservations[0]) self.assertIn("gsr", reservations[0]) - @mock.patch("gsr_booking.api_wrapper.LibCalWrapper.request", mock_requests_get) + @mock.patch("gsr_booking.api_wrapper.LibCalBookingWrapper.request", mock_requests_get) def test_cancel_libcal(self): group = Group.objects.create(owner=self.user) reservation = Reservation.objects.create(creator=self.user, group=group) @@ -142,17 +137,14 @@ def test_cancel_libcal(self): reservation=reservation, user=self.user, booking_id="123", - gsr=GSR.objects.all().first(), + gsr=GSR.objects.filter(kind="LIBCAL").first(), room_id=1, room_name="room", ) - cancel = self.bw.cancel_room("123", self.user) + cancel = GSRBooker.cancel_room("123", self.user) self.assertIsNone(cancel) - @mock.patch( - "gsr_booking.api_wrapper.WhartonLibWrapper.check_credits", mock_check_credits - ) # purposefully use mock_check_credits to mock being wharton user with credits - @mock.patch("gsr_booking.api_wrapper.WhartonLibWrapper.request", mock_requests_get) + @mock.patch("gsr_booking.api_wrapper.WhartonBookingWrapper.request", mock_requests_get) def test_group_book_wharton(self): # make sure group_user is treated as a wharton user so they # are returned in list of wharton users in gb.book_room @@ -166,7 +158,7 @@ def test_group_book_wharton(self): ) # reservation under user with group - reservation = self.gb.book_room( + reservation = GSRBooker.book_room( 1, 94, "241", @@ -177,27 +169,30 @@ def test_group_book_wharton(self): ) bookings = list(reservation.gsrbooking_set.all()) - self.assertEqual(len(bookings), 4) bookings.sort(key=lambda x: x.start) # check bookings cover entire range and enough time for i in range(len(bookings) - 1): self.assertEqual(bookings[i].end, bookings[i + 1].start) - for booking in bookings: - self.assertEqual(booking.end - booking.start, timedelta(minutes=30)) + total_time = sum([booking.end - booking.start for booking in bookings], timedelta()) + self.assertEqual(total_time, timedelta(hours=2)) + # check reservation exists self.assertIsNotNone(Reservation.objects.get(pk=reservation.id)) - @mock.patch("gsr_booking.api_wrapper.LibCalWrapper.request", mock_requests_get) + @mock.patch("gsr_booking.api_wrapper.LibCalBookingWrapper.request", mock_requests_get) def test_group_book_libcal(self): # add user to the group GroupMembership.objects.create(user=self.user, group=self.group, accepted=True) - reservation = self.gb.book_room( + start = timezone.localtime() + end = start + timedelta(hours=2) + + reservation = GSRBooker.book_room( 1889, 7192, "VP WIC Booth 01", - "2021-12-05T16:00:00-05:00", - "2021-12-05T18:00:00-05:00", + start.strftime("%Y-%m-%dT%H:%M:%S%z"), + end.strftime("%Y-%m-%dT%H:%M:%S%z"), self.user, self.group, ) @@ -207,7 +202,38 @@ def test_group_book_libcal(self): # check bookings cover entire range and enough time for i in range(len(bookings) - 1): self.assertEqual(bookings[i].end, bookings[i + 1].start) - for booking in bookings: - self.assertEqual(booking.end - booking.start, timedelta(minutes=30)) + total_time = sum([booking.end - booking.start for booking in bookings], timedelta()) + self.assertEqual(total_time, timedelta(hours=2)) # check reservation exists self.assertIsNotNone(Reservation.objects.get(pk=reservation.id)) + + res = GSRBooker.get_reservations(self.user, self.group) + self.assertEqual(len(res), 1) + self.assertEqual(res[0]["room_name"], "[Me] VP WIC Booth 01") + + credit_owner = reservation.gsrbooking_set.first().user + res = GSRBooker.get_reservations(credit_owner, self.group) + self.assertEqual(len(res), 1) + self.assertEqual( + res[0]["room_name"], + f"{'[Penn Labs]' if credit_owner != self.user else '[Me]'} VP WIC Booth 01", + ) + + @mock.patch("gsr_booking.api_wrapper.WhartonBookingWrapper.request", mock_requests_get) + def test_group_wharton_availability(self): + with self.assertRaises(APIError): + GSRBooker.get_availability( + "JMHH", 1, "2021-01-07", "2022-01-08", self.group_user, self.group + ) + GroupMembership.objects.create( + user=self.user, group=self.group, accepted=True, is_wharton=True + ) + availability = GSRBooker.get_availability( + "JMHH", 1, "2021-01-07", "2022-01-08", self.group_user, self.group + ) + self.assertIn("name", availability) + self.assertIn("gid", availability) + self.assertIn("rooms", availability) + self.assertIn("room_name", availability["rooms"][0]) + self.assertIn("id", availability["rooms"][0]) + self.assertIn("availability", availability["rooms"][0])