From 0c305c6f39033ef26141af8885032bfdd8d03239 Mon Sep 17 00:00:00 2001 From: Ashley Zhang Date: Tue, 12 Nov 2024 21:26:46 -0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=93=9A=20gsr=20booking=20type=20fixes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/gsr_booking/admin.py | 16 +- backend/gsr_booking/api_wrapper.py | 174 ++++++++++-------- .../management/commands/change_group.py | 3 +- .../management/commands/get_reservations.py | 3 +- .../management/commands/individual_usage.py | 3 +- backend/gsr_booking/models.py | 118 +++++++----- backend/gsr_booking/serializers.py | 22 +-- backend/gsr_booking/views.py | 54 +++--- backend/laundry/views.py | 4 +- backend/penndata/types.py | 1 + backend/penndata/views.py | 2 +- .../commands/load_target_populations.py | 4 +- backend/tests/gsr_booking/test_gsr_booking.py | 93 +++++----- backend/tests/gsr_booking/test_gsr_views.py | 58 +++--- backend/tests/gsr_booking/test_gsr_wrapper.py | 66 ++++--- 15 files changed, 341 insertions(+), 280 deletions(-) diff --git a/backend/gsr_booking/admin.py b/backend/gsr_booking/admin.py index 5c1586d0..494f2886 100644 --- a/backend/gsr_booking/admin.py +++ b/backend/gsr_booking/admin.py @@ -1,8 +1,11 @@ +from typing import Any, cast + from django.contrib import admin -from django.db.models import QuerySet -from rest_framework.request import Request +from django.db.models import Manager, QuerySet +from django.http import HttpRequest from gsr_booking.models import GSR, Group, GroupMembership, GSRBooking, Reservation +from utils.types import UserType class GroupMembershipInline(admin.TabularInline): @@ -12,12 +15,13 @@ class GroupMembershipInline(admin.TabularInline): readonly_fields = ["name"] def name(self, obj: GroupMembership) -> str: - return obj.user.get_full_name() + user = cast(UserType, obj.user) + return str(user.get_full_name()) - def get_fields(self, request, obj=None) -> list[str]: + def get_fields(self, request: HttpRequest, obj: Any = None) -> list[str]: fields = super().get_fields(request, obj) to_remove = ["user", "name"] - return ["name"] + [f for f in fields if f not in to_remove] + return ["name"] + [str(f) for f in fields if f not in to_remove] class GroupAdmin(admin.ModelAdmin): @@ -33,7 +37,7 @@ class GroupMembershipAdmin(admin.ModelAdmin): class GSRAdmin(admin.ModelAdmin): - def get_queryset(self, request: Request) -> QuerySet[GSR]: + def get_queryset(self, request: HttpRequest) -> QuerySet[GSR, Manager[GSR]]: return GSR.all_objects.all() list_display = ["name", "kind", "lid", "gid", "in_use"] diff --git a/backend/gsr_booking/api_wrapper.py b/backend/gsr_booking/api_wrapper.py index 22c6b67c..b3d23239 100644 --- a/backend/gsr_booking/api_wrapper.py +++ b/backend/gsr_booking/api_wrapper.py @@ -2,7 +2,7 @@ from abc import ABC, abstractmethod from enum import Enum from random import randint -from typing import TYPE_CHECKING, Any +from typing import Any, Optional, TypedDict, cast import requests from bs4 import BeautifulSoup @@ -17,15 +17,9 @@ from gsr_booking.models import GSR, Group, GroupMembership, GSRBooking, Reservation from gsr_booking.serializers import GSRBookingSerializer, GSRSerializer from utils.errors import APIError +from utils.types import UserType -if TYPE_CHECKING: - from django.contrib.auth.models import AbstractUser - - UserType = AbstractUser -else: - UserType = Any - User = get_user_model() BASE_URL = "https://libcal.library.upenn.edu" @@ -46,23 +40,40 @@ class CreditType(Enum): ARB = "ARB" +class AvailabilityTime(TypedDict): + start_time: str + end_time: str + + +class RoomInfo(TypedDict): + room_name: str + id: int + availability: list[AvailabilityTime] + + +class AvailabilityResponse(TypedDict): + name: str + gid: int + rooms: list[RoomInfo] + + class AbstractBookingWrapper(ABC): @abstractmethod - def book_room(self, rid: int, start: str, end: str, user: "UserType") -> dict[str, Any]: + def book_room(self, rid: int, start: str, end: str, user: UserType) -> dict[str, Any]: raise NotImplementedError # pragma: no cover @abstractmethod - def cancel_room(self, booking_id: str, user: "UserType") -> dict[str, Any]: + def cancel_room(self, booking_id: str, user: UserType) -> dict[str, Any]: raise NotImplementedError # pragma: no cover @abstractmethod def get_availability( - self, lid: int, start: str | None, end: str | None, user: "UserType" - ) -> list[dict[str, Any]]: + self, lid: int, start: str | None, end: str | None, user: UserType + ) -> list[RoomInfo]: raise NotImplementedError # pragma: no cover @abstractmethod - def get_reservations(self, user: "UserType") -> list[dict[str, Any]]: + def get_reservations(self, user: UserType) -> list[dict[str, Any]]: raise NotImplementedError # pragma: no cover @@ -82,7 +93,7 @@ def request(self, *args: Any, **kwargs: Any) -> requests.Response: return response - def book_room(self, rid: int, start: str, end: str, user: "UserType") -> dict[str, Any]: + def book_room(self, rid: int, start: str, end: str, user: UserType) -> dict[str, Any]: """Books room if pennkey is valid""" payload = {"start": start, "end": end, "pennkey": user.username, "room": rid} url = f"{WHARTON_URL}{user.username}/student_reserve" @@ -91,7 +102,7 @@ def book_room(self, rid: int, start: str, end: str, user: "UserType") -> dict[st raise APIError("Wharton: " + response["error"]) return response - def cancel_room(self, booking_id: str, user: "UserType") -> dict[str, Any]: + def cancel_room(self, booking_id: str, user: UserType) -> dict[str, Any]: """Cancels reservation given booking id""" url = f"{WHARTON_URL}{user.username}/reservations/{booking_id}/cancel" response = self.request("DELETE", url).json() @@ -100,8 +111,8 @@ def cancel_room(self, booking_id: str, user: "UserType") -> dict[str, Any]: return response def get_availability( - self, lid: int, start: str | None, end: str | None, user: "UserType" - ) -> list[dict[str, Any]]: + self, lid: int, start: str | None, end: str | None, user: UserType + ) -> list[RoomInfo]: """Returns a list of rooms and their availabilities""" current_time = timezone.localtime() search_date = ( @@ -139,7 +150,7 @@ def get_availability( room["availability"] = valid_slots return rooms - def get_reservations(self, user: "UserType") -> list[dict[str, Any]]: + def get_reservations(self, user: UserType) -> list[dict[str, Any]]: url = f"{WHARTON_URL}{user.username}/reservations" bookings = self.request("GET", url).json()["bookings"] @@ -157,7 +168,7 @@ def get_reservations(self, user: "UserType") -> list[dict[str, Any]]: if datetime.datetime.strptime(booking["end"], "%Y-%m-%dT%H:%M:%S%z") >= now ] - def is_wharton(self, user: "UserType") -> bool | None: + def is_wharton(self, user: UserType) -> bool | None: url = f"{WHARTON_URL}{user.username}/privileges" try: response = self.request("GET", url) @@ -170,7 +181,7 @@ def is_wharton(self, user: "UserType") -> bool | None: class LibCalBookingWrapper(AbstractBookingWrapper): - def __init__(self): + def __init__(self) -> None: self.token = None self.expiration = timezone.localtime() @@ -208,7 +219,7 @@ def request(self, *args: Any, **kwargs: Any) -> requests.Response: except (ConnectTimeout, ReadTimeout, ConnectionError): raise APIError("LibCal: Connection timeout") - def book_room(self, rid: int, start: str, end: str, user: "UserType") -> dict[str, Any]: + def book_room(self, rid: int, start: str, end: str, user: UserType) -> dict[str, Any]: """ Books room if pennkey is valid @@ -253,10 +264,10 @@ def book_room(self, rid: int, start: str, end: str, user: "UserType") -> dict[st raise APIError("LibCal: " + res_json["error"]) return res_json - def get_reservations(self, user: "UserType") -> list[dict[str, Any]]: - pass + def get_reservations(self, user: UserType) -> list[dict[str, Any]]: + return [] - def cancel_room(self, booking_id: str, user: "UserType") -> dict[str, Any]: + def cancel_room(self, booking_id: str, user: UserType) -> dict[str, Any]: """Cancels room""" response = self.request("POST", f"{API_URL}/1.1/space/cancel/{booking_id}").json() if "error" in response[0]: @@ -264,8 +275,8 @@ def cancel_room(self, booking_id: str, user: "UserType") -> dict[str, Any]: return response def get_availability( - self, gid: int, start: str | None, end: str | None, user: "UserType" - ) -> list[dict[str, Any]]: + self, gid: int, start: str | None, end: str | None, user: UserType + ) -> list[RoomInfo]: """Returns a list of rooms and their availabilities""" # adjusts url based on start and end times @@ -290,7 +301,7 @@ def get_availability( if response.status_code != 200: raise APIError(f"GSR Reserve: Error {response.status_code} when reserving data") - rooms = [ + rooms: list[dict[str, Any]] = [ {"room_name": room["name"], "id": room["id"], "availability": room["availability"]} for room in response.json() if room["id"] not in ROOM_BLACKLIST @@ -308,7 +319,7 @@ def get_availability( or datetime.datetime.strptime(time["from"][:-6], "%Y-%m-%dT%H:%M:%S") >= start_datetime ] - return rooms + return cast(list[RoomInfo], rooms) def get_affiliation(self, email: str) -> str: """Gets school from email""" @@ -323,13 +334,15 @@ def get_affiliation(self, email: str) -> str: class BookingHandler: - def __init__(self, WBW=None, LBW=None): + def __init__( + self, + WBW: Optional[AbstractBookingWrapper] = None, + LBW: Optional[AbstractBookingWrapper] = None, + ) -> None: self.WBW = WBW or WhartonBookingWrapper() self.LBW = LBW or LibCalBookingWrapper() - def format_members( - self, members: QuerySet[GroupMembership] - ) -> list[tuple["UserType", datetime.timedelta]]: + def format_members(self, members: QuerySet) -> list[tuple[UserType, datetime.timedelta]]: PREFIX = "user__" return [ ( @@ -343,7 +356,7 @@ def format_members( def get_wharton_members( self, group: Group, gsr_id: int - ) -> list[tuple["UserType", datetime.timedelta]]: + ) -> list[tuple[UserType, datetime.timedelta]]: now = timezone.localtime() ninty_min = datetime.timedelta(minutes=90) zero_min = datetime.timedelta(minutes=0) @@ -354,15 +367,18 @@ def get_wharton_members( .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), + - cast( + datetime.timedelta, + 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, ), - zero_min, - ) + ), ) .filter(Q(credits__gt=zero_min)) .values("user__id", "user__username", "credits") @@ -370,7 +386,7 @@ def get_wharton_members( ) return self.format_members(ret) - def get_libcal_members(self, group: Group) -> list[tuple["UserType", datetime.timedelta]]: + def get_libcal_members(self, group: Group) -> list[tuple[UserType, datetime.timedelta]]: 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) @@ -382,15 +398,18 @@ def get_libcal_members(self, group: Group) -> list[tuple["UserType", datetime.ti .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), + - cast( + datetime.timedelta, + 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, ), - zero_min, ) ) .filter(Q(credits__gt=zero_min)) @@ -413,14 +432,14 @@ def book_room( room_name: str, start: str, end: str, - user: "UserType", - group: Group | None = None, + user: UserType, + group: Optional[Group] = None, ) -> Reservation: # 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") + start_dt = datetime.datetime.strptime(start, "%Y-%m-%dT%H:%M:%S%z") + end_dt = 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 = ( @@ -437,15 +456,17 @@ def book_room( [time_available for _, time_available in members], datetime.timedelta(minutes=0) ) - if (end - start) >= total_time_available: + if (end_dt - start_dt) >= total_time_available: raise APIError("Error: Not enough credits") - reservation = Reservation.objects.create(start=start, end=end, creator=user, group=group) + reservation = Reservation.objects.create( + start=start_dt, end=end_dt, creator=user, group=group + ) - curr_start = start + curr_start = start_dt try: for curr_user, time_available in members: - curr_end = curr_start + min(time_available, end - curr_start) + curr_end = curr_start + min(time_available, end_dt - curr_start) booking_id = book_func( rid, @@ -465,17 +486,17 @@ def book_room( booking.reservation = reservation booking.save() - if (curr_start := curr_end) >= end: + if (curr_start := curr_end) >= end_dt: break except APIError as e: raise APIError( - f"{str(e)}. Was only able to book {start.strftime('%H:%M')}" + f"{str(e)}. Was only able to book {start_dt.strftime('%H:%M')}" f" - {curr_start.strftime('%H:%M')}" ) return reservation - def cancel_room(self, booking_id: str, user: "UserType") -> None: + def cancel_room(self, booking_id: str, user: UserType) -> None | APIError: if ( gsr_booking := GSRBooking.objects.filter(booking_id=booking_id) .prefetch_related(Prefetch("reservation__gsrbooking_set"), Prefetch("gsr")) @@ -499,22 +520,25 @@ def cancel_room(self, booking_id: str, user: "UserType") -> None: for service in [self.WBW, self.LBW]: try: service.cancel_room(booking_id, user) - return + return None except APIError: - pass - raise APIError("Error: Unknown booking id") + raise APIError("Error: Unknown booking id") + return None def get_availability( self, - lid: int, - gid: int, + lid: str | int, + gid: str | int, start: str | None, end: str | None, - user: "UserType", - group: Group | None = None, - ) -> list[dict[str, Any]]: + user: UserType, + group: Optional[Group] = None, + ) -> AvailabilityResponse: gsr = get_object_or_404(GSR, gid=gid) + lid_int = int(lid) if isinstance(lid, str) else lid + gid_int = int(gid) if isinstance(gid, str) else 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) @@ -523,14 +547,14 @@ def get_availability( user = wharton_members[randint(0, n - 1)].user rooms = ( - self.WBW.get_availability(lid, start, end, user) + self.WBW.get_availability(lid_int, start, end, user) if gsr.kind == GSR.KIND_WHARTON - else self.LBW.get_availability(gid, start, end, user) + else self.LBW.get_availability(gid_int, start, end, user) ) return {"name": gsr.name, "gid": gsr.gid, "rooms": rooms} def get_reservations( - self, user: "UserType", group: Group | None = None + self, user: UserType, group: Optional[Group] = None ) -> list[dict[str, Any]]: q = Q(user=user) | Q(reservation__creator=user) if group else Q(user=user) bookings = GSRBooking.objects.filter( @@ -538,16 +562,16 @@ def get_reservations( ).prefetch_related(Prefetch("reservation")) if group: - ret = [] + ret: list[dict[str, Any]] = [] for booking in bookings: - data = GSRBookingSerializer(booking).data + data = cast(dict[str, Any], 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 + ret = cast(list[dict[str, Any]], GSRBookingSerializer(bookings, many=True).data) # deal with bookings made directly through wharton (not us) try: diff --git a/backend/gsr_booking/management/commands/change_group.py b/backend/gsr_booking/management/commands/change_group.py index b2832087..28eeb7f2 100644 --- a/backend/gsr_booking/management/commands/change_group.py +++ b/backend/gsr_booking/management/commands/change_group.py @@ -1,3 +1,4 @@ +from argparse import ArgumentParser from typing import Any from django.contrib.auth import get_user_model @@ -15,7 +16,7 @@ class Command(BaseCommand): Adds/remove users to a group. """ - def add_arguments(self, parser) -> None: + def add_arguments(self, parser: ArgumentParser) -> None: parser.add_argument("usernames", type=str, help="list of pennkeys") parser.add_argument("group", type=str, help="group name") parser.add_argument("mode", type=str, help="mode of operation (add/remove/reset)") diff --git a/backend/gsr_booking/management/commands/get_reservations.py b/backend/gsr_booking/management/commands/get_reservations.py index 609478da..9e644b0e 100644 --- a/backend/gsr_booking/management/commands/get_reservations.py +++ b/backend/gsr_booking/management/commands/get_reservations.py @@ -1,3 +1,4 @@ +from argparse import ArgumentParser from datetime import datetime from typing import Any @@ -28,7 +29,7 @@ class Command(BaseCommand): Note: --start/--end and --current are mutually exclusive """ - def add_arguments(self, parser) -> None: + def add_arguments(self, parser: ArgumentParser) -> None: # optional flags parser.add_argument("--group", type=str, default=None) parser.add_argument("--start", type=str, default=None) diff --git a/backend/gsr_booking/management/commands/individual_usage.py b/backend/gsr_booking/management/commands/individual_usage.py index 6b27fbaf..80dbf32c 100644 --- a/backend/gsr_booking/management/commands/individual_usage.py +++ b/backend/gsr_booking/management/commands/individual_usage.py @@ -1,3 +1,4 @@ +from argparse import ArgumentParser from typing import Any from django.core.management.base import BaseCommand @@ -9,7 +10,7 @@ class Command(BaseCommand): help = "Provides usage stats for a given user." - def add_arguments(self, parser) -> None: + def add_arguments(self, parser: ArgumentParser) -> None: parser.add_argument("pennkey", type=str, help="Pennkey of user to check") def handle(self, *args: Any, **kwargs: Any) -> None: diff --git a/backend/gsr_booking/models.py b/backend/gsr_booking/models.py index 003832c3..e2ef5b8f 100644 --- a/backend/gsr_booking/models.py +++ b/backend/gsr_booking/models.py @@ -1,17 +1,12 @@ -from typing import TYPE_CHECKING, Any +from typing import Any from django.contrib.auth import get_user_model from django.db import models from django.db.models import QuerySet from django.utils import timezone +from utils.types import UserType -if TYPE_CHECKING: - from django.contrib.auth.models import AbstractUser - - UserType = AbstractUser -else: - UserType = Any User = get_user_model() @@ -19,54 +14,65 @@ class GroupMembership(models.Model): # INVARIANT: either user or username should always be set. if user is not None, then the # username should the be username of the associated user. - user = models.ForeignKey( + user: models.ForeignKey = models.ForeignKey( User, on_delete=models.CASCADE, related_name="memberships", blank=True, null=True ) - group = models.ForeignKey("Group", on_delete=models.CASCADE, related_name="memberships") + group: models.ForeignKey = models.ForeignKey( + "Group", on_delete=models.CASCADE, related_name="memberships" + ) + group_id: int # When accepted is False, this is a request, otherwise this is an active membership. - accepted = models.BooleanField(default=False) + accepted: models.BooleanField = models.BooleanField(default=False) ADMIN = "A" MEMBER = "M" - type = models.CharField(max_length=10, choices=[(ADMIN, "Admin"), (MEMBER, "Member")]) + type: models.CharField = models.CharField( + max_length=10, choices=[(ADMIN, "Admin"), (MEMBER, "Member")] + ) - pennkey_allow = models.BooleanField(default=False) + pennkey_allow: models.BooleanField = models.BooleanField(default=False) - notifications = models.BooleanField(default=True) + notifications: models.BooleanField = models.BooleanField(default=True) - is_wharton = models.BooleanField(blank=True, null=True, default=None) + is_wharton: models.BooleanField = models.BooleanField(blank=True, null=True, default=None) @property - def is_invite(self): + def is_invite(self) -> bool: return not self.accepted - def __str__(self): + def __str__(self) -> str: return f"{self.user}<->{self.group}" - def save(self, *args, **kwargs): + def save(self, *args: Any, **kwargs: Any) -> None: # determines whether user is wharton or not if self.is_wharton is None: self.is_wharton = self.check_wharton() super().save(*args, **kwargs) - def check_wharton(self): - return WhartonGSRBooker.is_wharton(self.user) + def check_wharton(self) -> bool: + if check := WhartonGSRBooker.is_wharton(self.user): + return check + return False class Meta: verbose_name = "Group Membership" class Group(models.Model): - owner = models.ForeignKey(User, on_delete=models.CASCADE) - members = models.ManyToManyField(User, through=GroupMembership, related_name="booking_groups") + id: int + owner: models.ForeignKey = models.ForeignKey(User, on_delete=models.CASCADE) + members: models.ManyToManyField = models.ManyToManyField( + User, through=GroupMembership, related_name="booking_groups" + ) + memberships: QuerySet - name = models.CharField(max_length=255) - color = models.CharField(max_length=255) + name: models.CharField = models.CharField(max_length=255) + color: models.CharField = models.CharField(max_length=255) - created_at = models.DateTimeField(auto_now_add=True) - updated_at = models.DateTimeField(auto_now=True) + created_at: models.DateTimeField = models.DateTimeField(auto_now_add=True) + updated_at: models.DateTimeField = models.DateTimeField(auto_now=True) ADMIN = "A" MEMBER = "M" @@ -74,15 +80,15 @@ class Group(models.Model): def __str__(self) -> str: return f"{self.name}-{self.pk}" - def has_member(self, user: "UserType") -> bool: + def has_member(self, user: UserType) -> bool: memberships = GroupMembership.objects.filter(group=self, user=user) return memberships.all().exists() - def has_admin(self, user: "UserType") -> bool: + def has_admin(self, user: UserType) -> bool: memberships = GroupMembership.objects.filter(group=self, accepted=True) return memberships.all().filter(type="A").filter(user=user).exists() - def get_pennkey_active_members(self): + def get_pennkey_active_members(self) -> list[UserType]: memberships = GroupMembership.objects.filter(group=self, accepted=True) pennkey_active_members_list = memberships.all().filter(pennkey_allow=True).all() return [member for member in pennkey_active_members_list] @@ -95,7 +101,7 @@ def save(self, *args: Any, **kwargs: Any) -> None: class GSRManager(models.Manager): - def get_queryset(self) -> QuerySet[Any]: + def get_queryset(self) -> QuerySet: return super().get_queryset().filter(in_use=True) @@ -105,41 +111,51 @@ class GSR(models.Model): KIND_LIBCAL = "LIBCAL" KIND_OPTIONS = ((KIND_WHARTON, "Wharton"), (KIND_LIBCAL, "Libcal")) - kind = models.CharField(max_length=7, choices=KIND_OPTIONS, default=KIND_LIBCAL) - lid = models.CharField(max_length=255) - gid = models.IntegerField(null=True) - name = models.CharField(max_length=255) - image_url = models.URLField() + kind: models.CharField = models.CharField( + max_length=7, choices=KIND_OPTIONS, default=KIND_LIBCAL + ) + id: int + lid: models.CharField = models.CharField(max_length=255) + gid: models.IntegerField = models.IntegerField(null=True) + name: models.CharField = models.CharField(max_length=255) + image_url: models.URLField = models.URLField() - in_use = models.BooleanField(default=True) + in_use: models.BooleanField = models.BooleanField(default=True) objects = GSRManager() - all_objects = models.Manager() # for admin page + all_objects: models.Manager = models.Manager() # for admin page def __str__(self) -> str: return f"{self.name}: {self.lid}-{self.gid}" 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, null=True, blank=True) - is_cancelled = models.BooleanField(default=False) - reminder_sent = models.BooleanField(default=False) + id: int + start: models.DateTimeField = models.DateTimeField(default=timezone.now) + end: models.DateTimeField = models.DateTimeField(default=timezone.now) + creator: models.ForeignKey = models.ForeignKey(User, on_delete=models.CASCADE) + group: models.ForeignKey = models.ForeignKey( + Group, on_delete=models.CASCADE, null=True, blank=True + ) + is_cancelled: models.BooleanField = models.BooleanField(default=False) + reminder_sent: models.BooleanField = models.BooleanField(default=False) + + gsrbooking_set: QuerySet class GSRBooking(models.Model): # TODO: change to non-null after reservations are created for current bookings - reservation = models.ForeignKey(Reservation, on_delete=models.CASCADE, null=True) - user = models.ForeignKey(User, on_delete=models.CASCADE, null=True) - booking_id = models.CharField(max_length=255, null=True, blank=True) - gsr = models.ForeignKey(GSR, on_delete=models.CASCADE) - room_id = models.IntegerField() - room_name = models.CharField(max_length=255) - start = models.DateTimeField(default=timezone.now) - end = models.DateTimeField(default=timezone.now) - is_cancelled = models.BooleanField(default=False) + reservation: models.ForeignKey = models.ForeignKey( + Reservation, on_delete=models.CASCADE, null=True + ) + user: models.ForeignKey = models.ForeignKey(User, on_delete=models.CASCADE, null=True) + booking_id: models.CharField = models.CharField(max_length=255, null=True, blank=True) + gsr: models.ForeignKey = models.ForeignKey(GSR, on_delete=models.CASCADE) + room_id: models.IntegerField = models.IntegerField() + room_name: models.CharField = models.CharField(max_length=255) + start: models.DateTimeField = models.DateTimeField(default=timezone.now) + end: models.DateTimeField = models.DateTimeField(default=timezone.now) + is_cancelled: models.BooleanField = models.BooleanField(default=False) def __str__(self) -> str: return f"{self.user} - {self.gsr.name} - {self.start} - {self.end}" diff --git a/backend/gsr_booking/serializers.py b/backend/gsr_booking/serializers.py index f01e077b..1dbf50d7 100644 --- a/backend/gsr_booking/serializers.py +++ b/backend/gsr_booking/serializers.py @@ -1,18 +1,12 @@ -from typing import TYPE_CHECKING, Any, TypeAlias +from typing import Any, TypeAlias from django.contrib.auth import get_user_model from rest_framework import serializers from gsr_booking.models import GSR, Group, GroupMembership, GSRBooking +from utils.types import UserType -if TYPE_CHECKING: - from django.contrib.auth.models import AbstractUser - - UserType = AbstractUser -else: - UserType = Any - ValidatedData: TypeAlias = dict[str, Any] User = get_user_model() @@ -38,8 +32,12 @@ class Meta: class GroupMembershipSerializer(serializers.ModelSerializer): user = MiniUserSerializer(read_only=True) - group = serializers.SlugRelatedField(slug_field="name", queryset=Group.objects.all()) - color = serializers.SlugRelatedField(slug_field="color", read_only=True, source="group") + group: serializers.SlugRelatedField = serializers.SlugRelatedField( + slug_field="name", queryset=Group.objects.all() + ) + color: serializers.SlugRelatedField = serializers.SlugRelatedField( + slug_field="color", read_only=True, source="group" + ) class Meta: model = GroupMembership @@ -47,7 +45,7 @@ class Meta: class GroupSerializer(serializers.ModelSerializer): - owner = serializers.SlugRelatedField( + owner: serializers.SlugRelatedField = serializers.SlugRelatedField( slug_field="username", queryset=User.objects.all(), required=False ) memberships = GroupMembershipSerializer(many=True, read_only=True) @@ -78,7 +76,7 @@ def to_internal_value(self, data: ValidatedData) -> None: class UserSerializer(serializers.ModelSerializer): booking_groups = serializers.SerializerMethodField() - def get_booking_groups(self, obj: "UserType") -> list[dict[str, Any]]: + def get_booking_groups(self, obj: UserType) -> list[dict[str, Any]]: result = [] for membership in GroupMembership.objects.filter(accepted=True, user=obj): result.append( diff --git a/backend/gsr_booking/views.py b/backend/gsr_booking/views.py index 83f12913..73e60350 100644 --- a/backend/gsr_booking/views.py +++ b/backend/gsr_booking/views.py @@ -1,4 +1,4 @@ -from typing import TYPE_CHECKING, Any, Optional, cast +from typing import Optional from django.contrib.auth import get_user_model from django.db.models import Manager, Prefetch, Q, QuerySet @@ -22,20 +22,14 @@ ) from pennmobile.analytics import Metric, record_analytics from utils.errors import APIError +from utils.types import UserType, get_user -if TYPE_CHECKING: - from django.contrib.auth.models import AbstractUser - - UserType = AbstractUser -else: - UserType = Any - User = get_user_model() # TODO: user model doesn't have a `booking_groups` attribute, so placing Any type for now -class UserViewSet(viewsets.ReadOnlyModelViewSet["UserType"]): +class UserViewSet(viewsets.ReadOnlyModelViewSet[UserType]): """ Can specify `me` instead of the `username` to retrieve details on the current user. """ @@ -49,18 +43,18 @@ class UserViewSet(viewsets.ReadOnlyModelViewSet["UserType"]): filter_backends = [DjangoFilterBackend] filterset_fields = ["username", "first_name", "last_name"] - def get_object(self) -> "UserType": + def get_object(self) -> UserType: lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field param = self.kwargs[lookup_url_kwarg] if param == "me": - return cast("UserType", self.request.user) + return get_user(self.request) return super().get_object() - def get_queryset(self) -> QuerySet["UserType", Manager["UserType"]]: + def get_queryset(self) -> QuerySet[UserType, Manager[UserType]]: if not self.request.user.is_authenticated: return User.objects.none() - user: Any = self.request.user + user = get_user(self.request) queryset = User.objects.all() queryset = queryset.prefetch_related( Prefetch( @@ -77,7 +71,7 @@ def invites(self, request: Request, username: Optional[str] = None) -> Response: """ user = get_object_or_404(User, username=username) - request_user: Any = self.request.user + request_user = get_user(self.request) return Response( GroupMembershipSerializer( GroupMembership.objects.filter( @@ -96,7 +90,7 @@ class GroupMembershipViewSet(viewsets.ModelViewSet[GroupMembership]): serializer_class = GroupMembershipSerializer def get_queryset(self) -> QuerySet[GroupMembership, Manager[GroupMembership]]: - user: Any = self.request.user + user = get_user(self.request) if not user.is_authenticated: return GroupMembership.objects.none() @@ -118,7 +112,7 @@ def invite(self, request: Request) -> Response | HttpResponseForbidden: group = get_object_or_404(Group, pk=group_id) # don't invite when user already in group - if group.has_member(cast(Any, request.user)): + if group.has_member(get_user(request)): return HttpResponseForbidden() return Response({"message": "invite(s) sent."}) @@ -128,7 +122,7 @@ def accept( self, request: Request, pk: Optional[int] = None ) -> Response | HttpResponseForbidden: membership = get_object_or_404(GroupMembership, pk=pk, accepted=False) - user = cast("UserType", request.user) + user = get_user(request) if membership.user is None or membership.user != user: return HttpResponseForbidden() @@ -150,7 +144,7 @@ def decline( self, request: Request, pk: Optional[int] = None ) -> Response | HttpResponseForbidden: membership = get_object_or_404(GroupMembership, pk=pk, accepted=False) - if membership.user is None or membership.user != cast("UserType", request.user): + if membership.user is None or membership.user != get_user(request): return HttpResponseForbidden() if not membership.is_invite: return Response({"message": "cannot decline an invite that has been accepted."}, 400) @@ -170,7 +164,7 @@ class GroupViewSet(viewsets.ModelViewSet[Group]): permission_classes = [IsAuthenticated] def get_queryset(self) -> QuerySet[Group, Manager[Group]]: - user = cast("UserType", self.request.user) + user = get_user(self.request) if not user.is_authenticated: return Group.objects.none() return ( @@ -197,9 +191,8 @@ class RecentGSRs(generics.ListAPIView[GSR]): permission_classes = [IsAuthenticated] def get_queryset(self) -> QuerySet[GSR, Manager[GSR]]: - user = cast("UserType", self.request.user) return GSR.objects.filter( - id__in=GSRBooking.objects.filter(user=user, is_cancelled=False) + id__in=GSRBooking.objects.filter(user=get_user(self.request), is_cancelled=False) .distinct() .order_by("-end")[:2] .values_list("gsr", flat=True) @@ -211,7 +204,7 @@ class CheckWharton(APIView): permission_classes = [IsAuthenticated] def get(self, request: Request) -> Response: - user: Any = request.user + user = get_user(request) return Response( { "is_wharton": user.booking_groups.filter(name="Penn Labs").exists() @@ -231,16 +224,21 @@ class Availability(APIView): permission_classes = [IsAuthenticated] - def get(self, request: Request, lid: int, gid: int) -> Response: + def get(self, request: Request, lid: int, gid: str) -> Response: start = request.GET.get("start", None) end = request.GET.get("end", None) try: - user: Any = request.user + user = get_user(request) return Response( GSRBooker.get_availability( - lid, gid, start, end, user, user.booking_groups.filter(name="Penn Labs").first() + lid, + gid, + start, + end, + user, + user.booking_groups.filter(name="Penn Labs").first(), ) ) except APIError as e: @@ -258,7 +256,7 @@ def post(self, request: Request) -> Response: gid = request.data["gid"] room_id = request.data["id"] room_name = request.data["room_name"] - user: Any = request.user + user = get_user(request) try: GSRBooker.book_room( @@ -287,7 +285,7 @@ class CancelRoom(APIView): def post(self, request: Request) -> Response: booking_id = request.data["booking_id"] - user: Any = request.user + user = get_user(request) try: GSRBooker.cancel_room(booking_id, user) return Response({"detail": "success"}) @@ -303,7 +301,7 @@ class ReservationsView(APIView): permission_classes = [IsAuthenticated] def get(self, request: Request) -> Response: - user: Any = request.user + user = get_user(request) return Response( GSRBooker.get_reservations(user, user.booking_groups.filter(name="Penn Labs").first()) ) diff --git a/backend/laundry/views.py b/backend/laundry/views.py index 0405e68c..eccf04a1 100644 --- a/backend/laundry/views.py +++ b/backend/laundry/views.py @@ -1,6 +1,6 @@ import calendar import datetime -from typing import Any, cast +from typing import Any, Optional, cast from django.core.cache import cache from django.db.models import Manager, Q, QuerySet @@ -67,7 +67,7 @@ class HallUsage(APIView): """ @staticmethod - def safe_division(a: int | None, b: int | None) -> float | None: + def safe_division(a: Optional[int] = None, b: Optional[int] = None) -> float | None: if a is None or b is None or b <= 0: return 0.0 return round(a / float(b), 3) diff --git a/backend/penndata/types.py b/backend/penndata/types.py index a11b00a6..36ce7426 100644 --- a/backend/penndata/types.py +++ b/backend/penndata/types.py @@ -1,6 +1,7 @@ from typing import Any, TypeAlias from django.db.models import Manager, QuerySet + from penndata.models import CalendarEvent, Event, HomePageOrder diff --git a/backend/penndata/views.py b/backend/penndata/views.py index feb72384..4f0a2af9 100644 --- a/backend/penndata/views.py +++ b/backend/penndata/views.py @@ -156,7 +156,7 @@ class HomePage(APIView): class Cell: def __init__( - self, myType: str, myInfo: ValidatedData | None = None, myWeight: int = 0 + self, myType: str, myInfo: Optional[ValidatedData] = None, myWeight: int = 0 ) -> None: self.type = myType self.info = myInfo diff --git a/backend/portal/management/commands/load_target_populations.py b/backend/portal/management/commands/load_target_populations.py index b83910de..6244e73a 100644 --- a/backend/portal/management/commands/load_target_populations.py +++ b/backend/portal/management/commands/load_target_populations.py @@ -1,5 +1,5 @@ from argparse import ArgumentParser -from typing import Any +from typing import Any, Optional import requests from django.core.management.base import BaseCommand @@ -42,7 +42,7 @@ def handle(self, *args: Any, **kwargs: Any) -> None: def get_degrees(self) -> list[str]: return ["BACHELORS", "MASTERS", "PHD", "PROFESSIONAL"] - def get_years(self, years: str | None) -> list[int]: + def get_years(self, years: Optional[str] = None) -> list[int]: # creates new class year in August in preparation for upcoming school year if years is None: return ( diff --git a/backend/tests/gsr_booking/test_gsr_booking.py b/backend/tests/gsr_booking/test_gsr_booking.py index 1559dbbd..3ff3be94 100644 --- a/backend/tests/gsr_booking/test_gsr_booking.py +++ b/backend/tests/gsr_booking/test_gsr_booking.py @@ -1,89 +1,92 @@ -from django.contrib.auth import get_user_model +from typing import cast + from django.test import TestCase from rest_framework.test import APIClient from gsr_booking.models import Group, GroupMembership - - -User = get_user_model() +from utils.types import DjangoUserModel, UserType class UserViewTestCase(TestCase): - def setUp(self): - self.user1 = User.objects.create_user( + def setUp(self) -> None: + self.user1: UserType = DjangoUserModel.objects.create_user( username="user1", password="password", first_name="user", last_name="one" ) - self.user2 = User.objects.create_user( + self.user2: UserType = DjangoUserModel.objects.create_user( username="user2", password="password", first_name="user", last_name="two" ) self.group = Group.objects.create(owner=self.user1, name="g1", color="blue") self.group.members.add(self.user1) - memship = self.group.memberships.all()[0] + memship = self.group.memberships.all()[0] # type: ignore memship.accepted = True memship.save() - self.client = APIClient() + self.client: APIClient = APIClient() self.client.login(username="user1", password="password") - def test_user_list(self): + def test_user_list(self) -> None: response = self.client.get("/gsr/users/") self.assertTrue(200, response.status_code) self.assertEqual(2, len(response.data)) - def test_user_detail_in_group(self): + def test_user_detail_in_group(self) -> None: response = self.client.get("/gsr/users/user1/") self.assertTrue(200, response.status_code) self.assertEqual(2, len(response.data["booking_groups"])) - def test_me_user_detail_in_group(self): + def test_me_user_detail_in_group(self) -> None: response = self.client.get("/gsr/users/me/") self.assertTrue(200, response.status_code) self.assertEqual(2, len(response.data["booking_groups"])) class MembershipViewTestCase(TestCase): - def setUp(self): - self.user1 = User.objects.create_user(username="user1", password="password") - self.user2 = User.objects.create_user(username="user2", password="password") + def setUp(self) -> None: + self.user1: UserType = DjangoUserModel.objects.create_user( + username="user1", password="password" + ) + self.user2: UserType = DjangoUserModel.objects.create_user( + username="user2", password="password" + ) self.group = Group.objects.create(owner=self.user1, name="g1", color="blue") self.group2 = Group.objects.create(owner=self.user2, name="g2", color="white") - self.client = APIClient() + self.client: APIClient = APIClient() self.client.login(username="user1", password="password") - def test_invite_single(self): + def test_invite_single(self) -> None: self.client.login(username="user2", password="password") response = self.client.post( "/gsr/membership/invite/", {"user": "user2", "group": self.group.pk} ) self.assertEqual(200, response.status_code) - def test_bulk_invite(self): - User.objects.create_user(username="user3", password="password") + def test_bulk_invite(self) -> None: + DjangoUserModel.objects.create_user(username="user3", password="password") self.client.login(username="user2", password="password") response = self.client.post( "/gsr/membership/invite/", {"user": "user2,user3", "group": self.group.pk} ) self.assertEqual(200, response.status_code) - def test_invite_no_permission(self): + def test_invite_no_permission(self) -> None: self.client.login(username="user2", password="password") response = self.client.post( "/gsr/membership/invite/", {"user": "user2", "group": self.group.pk} ) self.assertEqual(200, response.status_code) - def test_invite_logged_out_fails(self): + def test_invite_logged_out_fails(self) -> None: self.client.logout() response = self.client.post( "/gsr/membership/invite/", {"user": "user2", "group": self.group.pk} ) self.assertEqual(403, response.status_code) - def test_invite_bad_group_fails(self): + def test_invite_bad_group_fails(self) -> None: response = self.client.post("/gsr/membership/invite/", {"user": "user2", "group": 300}) self.assertEqual(404, response.status_code) - def test_duplicate_invite_fails(self): + def test_duplicate_invite_fails(self) -> None: GroupMembership.objects.create(user=self.user2, group=self.group, accepted=False) self.client.force_authenticate(user=self.user2) response = self.client.post( @@ -91,7 +94,7 @@ def test_duplicate_invite_fails(self): ) self.assertEqual(403, response.status_code) - def test_already_member_invite_fails(self): + def test_already_member_invite_fails(self) -> None: GroupMembership.objects.create(user=self.user2, group=self.group, accepted=True) self.client.force_authenticate(user=self.user2) response = self.client.post( @@ -99,44 +102,44 @@ def test_already_member_invite_fails(self): ) self.assertEqual(403, response.status_code) - def test_accept_invite(self): + def test_accept_invite(self) -> None: mem = GroupMembership.objects.create(user=self.user1, group=self.group2, accepted=False) response = self.client.post(f"/gsr/membership/{mem.pk}/accept/") self.assertEqual(200, response.status_code) self.assertTrue(GroupMembership.objects.get(pk=mem.pk).accepted) - def test_wrong_user_accept_invite_fails(self): - user3 = User.objects.create_user(username="user3", password="password") + def test_wrong_user_accept_invite_fails(self) -> None: + user3: UserType = DjangoUserModel.objects.create_user(username="user3", password="password") mem = GroupMembership.objects.create(user=user3, group=self.group2, accepted=False) response = self.client.post(f"/gsr/membership/{mem.pk}/accept/") self.assertEqual(403, response.status_code) self.assertFalse(GroupMembership.objects.get(pk=mem.pk).accepted) - def test_accept_invite_again_fails(self): + def test_accept_invite_again_fails(self) -> None: mem = GroupMembership.objects.create(user=self.user1, group=self.group2, accepted=True) response = self.client.post(f"/gsr/membership/{mem.pk}/accept/") self.assertEqual(404, response.status_code) - def test_decline_invite(self): + def test_decline_invite(self) -> None: mem = GroupMembership.objects.create(user=self.user1, group=self.group2, accepted=False) response = self.client.post(f"/gsr/membership/{mem.pk}/decline/") self.assertEqual(200, response.status_code) self.assertFalse(GroupMembership.objects.filter(pk=mem.pk).exists()) - def test_wrong_user_decline_invite_fails(self): - user3 = User.objects.create_user(username="user3", password="password") + def test_wrong_user_decline_invite_fails(self) -> None: + user3: UserType = DjangoUserModel.objects.create_user(username="user3", password="password") mem = GroupMembership.objects.create(user=user3, group=self.group2, accepted=False) response = self.client.post(f"/gsr/membership/{mem.pk}/decline/") self.assertEqual(403, response.status_code) self.assertTrue(GroupMembership.objects.filter(pk=mem.pk).exists()) self.assertFalse(GroupMembership.objects.get(pk=mem.pk).accepted) - def test_decline_invite_again_fails(self): + def test_decline_invite_again_fails(self) -> None: mem = GroupMembership.objects.create(user=self.user1, group=self.group2, accepted=True) response = self.client.post(f"/gsr/membership/{mem.pk}/decline/") self.assertEqual(404, response.status_code) - def test_promote_to_admin(self): + def test_promote_to_admin(self) -> None: GroupMembership.objects.create(user=self.user1, group=self.group, accepted=True, type="A") mem = GroupMembership.objects.create( user=self.user2, group=self.group, accepted=True, type="M" @@ -148,35 +151,39 @@ def test_promote_to_admin(self): class GroupTestCase(TestCase): - def setUp(self): - self.user1 = User.objects.create_user(username="user1", password="password") - self.user2 = User.objects.create_user(username="user2", password="password") + def setUp(self) -> None: + self.user1: UserType = DjangoUserModel.objects.create_user( + username="user1", password="password" + ) + self.user2: UserType = DjangoUserModel.objects.create_user( + username="user2", password="password" + ) self.group = Group.objects.create(owner=self.user1, name="g1", color="blue") self.group2 = Group.objects.create(owner=self.user2, name="g2", color="white") - self.client = APIClient() + self.client: APIClient = APIClient() self.client.login(username="user1", password="password") - def test_get_groups(self): + def test_get_groups(self) -> None: response = self.client.get("/gsr/groups/") self.assertEqual(200, response.status_code) self.assertEqual(2, len(response.data)) - def test_get_groups_includes_invites(self): + def test_get_groups_includes_invites(self) -> None: GroupMembership.objects.create(user=self.user1, group=self.group2, accepted=False) response = self.client.get(f"/gsr/groups/{self.group2.pk}/") self.assertEqual(200, response.status_code) - def test_get_group_not_involved_fails(self): + def test_get_group_not_involved_fails(self) -> None: response = self.client.get(f"/gsr/groups/{self.group2.pk}/") self.assertEqual(404, response.status_code) - def test_make_group(self): + def test_make_group(self) -> None: response = self.client.post("/gsr/groups/", {"name": "gx", "color": "blue"}) self.assertEqual(201, response.status_code, response.data) self.assertEqual(5, Group.objects.count()) - self.assertEqual("user1", Group.objects.get(name="gx").owner.username) + self.assertEqual("user1", cast(UserType, Group.objects.get(name="gx").owner).username) - def test_only_accepted_memberships(self): + def test_only_accepted_memberships(self) -> None: gm = GroupMembership.objects.create(user=self.user2, group=self.group, accepted=False) response = self.client.get(f"/gsr/groups/{self.group.pk}/") self.assertEqual(200, response.status_code) diff --git a/backend/tests/gsr_booking/test_gsr_views.py b/backend/tests/gsr_booking/test_gsr_views.py index c749d2b9..3dcbb772 100644 --- a/backend/tests/gsr_booking/test_gsr_views.py +++ b/backend/tests/gsr_booking/test_gsr_views.py @@ -1,56 +1,56 @@ import json +from typing import Any from unittest import mock -from django.contrib.auth import get_user_model from django.core.management import call_command from django.test import TestCase from django.urls import reverse from rest_framework.test import APIClient from gsr_booking.models import GSR, Group, GSRBooking +from utils.types import DjangoUserModel, UserType -User = get_user_model() - - -def is_wharton_false(*args): +def is_wharton_false(*args: Any) -> bool: return False -def is_wharton_true(*args): +def is_wharton_true(*args: Any) -> bool: return True -def libcal_availability(*args): +def libcal_availability(*args: Any) -> list[dict]: with open("tests/gsr_booking/views_libcal_availability.json") as data: return json.load(data) -def wharton_availability(*args): +def wharton_availability(*args: Any) -> list[dict]: with open("tests/gsr_booking/views_wharton_availability.json") as data: return json.load(data) -def book_cancel_room(*args): +def book_cancel_room(*args: Any) -> None: pass -def reservations(*args): +def reservations(*args: Any) -> list[dict]: with open("tests/gsr_booking/views_reservations.json") as data: return json.load(data) class TestGSRs(TestCase): - def setUp(self): + def setUp(self) -> None: call_command("load_gsrs") - self.user = User.objects.create_user("user", "user@seas.upenn.edu", "user") - self.client = APIClient() + self.user: UserType = DjangoUserModel.objects.create_user( + "user", "user@seas.upenn.edu", "user" + ) + self.client: APIClient = APIClient() self.client.force_authenticate(user=self.user) - test_user = User.objects.create_user("user1", "user") + test_user = DjangoUserModel.objects.create_user("user1", "user") Group.objects.create(owner=test_user, name="Penn Labs", color="blue") - def test_get_location(self): + def test_get_location(self) -> None: response = self.client.get(reverse("locations")) res_json = json.loads(response.content) for entry in res_json: @@ -63,16 +63,18 @@ def test_get_location(self): class TestGSRFunctions(TestCase): - def setUp(self): + def setUp(self) -> None: call_command("load_gsrs") - self.user = User.objects.create_user("user", "user@sas.upenn.edu", "user") - self.client = APIClient() + self.user: UserType = DjangoUserModel.objects.create_user( + "user", "user@sas.upenn.edu", "user" + ) + self.client: APIClient = APIClient() self.client.force_authenticate(user=self.user) - test_user = User.objects.create_user("user1", "user") + test_user = DjangoUserModel.objects.create_user("user1", "user") Group.objects.create(owner=test_user, name="Penn Labs", color="blue") - def test_recent(self): + def test_recent(self) -> None: gsrs = list(GSR.objects.all()) GSRBooking.objects.create(user=self.user, room_id=1, room_name="Room 1", gsr=gsrs[0]) GSRBooking.objects.create(user=self.user, room_id=3, room_name="Room 3", gsr=gsrs[0]) @@ -93,21 +95,21 @@ def test_recent(self): self.assertNotEqual(res_json[0]["id"], res_json[1]["id"]) @mock.patch("gsr_booking.api_wrapper.WhartonBookingWrapper.is_wharton", is_wharton_false) - def test_get_wharton_false(self): + def test_get_wharton_false(self) -> None: 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.api_wrapper.WhartonBookingWrapper.is_wharton", is_wharton_true) - def test_get_wharton_true(self): + def test_get_wharton_true(self) -> None: 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.api_wrapper.BookingHandler.get_availability", libcal_availability) - def test_availability_libcal(self): + def test_availability_libcal(self) -> None: response = self.client.get(reverse("availability", args=["1086", "1889"])) res_json = json.loads(response.content) self.assertEqual(3, len(res_json)) @@ -121,7 +123,7 @@ def test_availability_libcal(self): self.assertIn("availability", room) @mock.patch("gsr_booking.api_wrapper.BookingHandler.get_availability", wharton_availability) - def test_availability_wharton(self): + def test_availability_wharton(self) -> None: response = self.client.get(reverse("availability", args=["JMHH", "1"])) res_json = json.loads(response.content) self.assertEqual(3, len(res_json)) @@ -135,7 +137,7 @@ def test_availability_wharton(self): self.assertIn("availability", room) @mock.patch("gsr_booking.api_wrapper.BookingHandler.book_room", book_cancel_room) - def test_book_libcal(self): + def test_book_libcal(self) -> None: payload = { "start_time": "2021-11-21T18:30:00-05:00", "end_time": "2021-11-21T19:00:00-05:00", @@ -151,7 +153,7 @@ def test_book_libcal(self): self.assertEqual("success", res_json["detail"]) @mock.patch("gsr_booking.api_wrapper.BookingHandler.book_room", book_cancel_room) - def test_book_wharton(self): + def test_book_wharton(self) -> None: payload = { "start_time": "2021-11-21T18:30:00-05:00", "end_time": "2021-11-21T19:00:00-05:00", @@ -167,7 +169,7 @@ def test_book_wharton(self): self.assertEqual("success", res_json["detail"]) @mock.patch("gsr_booking.api_wrapper.BookingHandler.cancel_room", book_cancel_room) - def test_cancel_room(self): + def test_cancel_room(self) -> None: payload = {"booking_id": "booking id"} response = self.client.post( reverse("cancel"), json.dumps(payload), content_type="application/json" @@ -177,7 +179,7 @@ def test_cancel_room(self): self.assertEqual("success", res_json["detail"]) @mock.patch("gsr_booking.api_wrapper.BookingHandler.get_reservations", reservations) - def test_reservations(self): + def test_reservations(self) -> None: response = self.client.get(reverse("reservations")) res_json = json.loads(response.content) self.assertEqual(6, len(res_json)) diff --git a/backend/tests/gsr_booking/test_gsr_wrapper.py b/backend/tests/gsr_booking/test_gsr_wrapper.py index aa214b95..23a307c3 100644 --- a/backend/tests/gsr_booking/test_gsr_wrapper.py +++ b/backend/tests/gsr_booking/test_gsr_wrapper.py @@ -1,8 +1,8 @@ import json from datetime import timedelta +from typing import Any, cast from unittest import mock -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 @@ -10,19 +10,17 @@ from gsr_booking.api_wrapper import APIError, GSRBooker, WhartonGSRBooker from gsr_booking.models import GSR, Group, GroupMembership, GSRBooking, Reservation +from utils.types import DjangoUserModel, UserType -User = get_user_model() - - -def mock_requests_get(obj, *args, **kwargs): +def mock_requests_get(obj: Any, *args: Any, **kwargs: Any) -> Any: class Mock: - def __init__(self, json_data, status_code): + def __init__(self, json_data: dict, status_code: int) -> None: self.json_data = json_data self.status_code = status_code self.ok = True - def json(self): + def json(self) -> dict: return self.json_data url = args[1] @@ -52,22 +50,24 @@ def json(self): class TestBookingWrapper(TestCase): - def setUp(self): + def setUp(self) -> None: call_command("load_gsrs") - self.user = User.objects.create_user("user", "user@seas.upenn.edu", "user") - self.group_user = User.objects.create_user( + self.user: UserType = DjangoUserModel.objects.create_user( + "user", "user@seas.upenn.edu", "user" + ) + self.group_user: UserType = DjangoUserModel.objects.create_user( "grou_user", "group_user@seas.upenn.edu", "group_user" ) - self.client = APIClient() + self.client: APIClient = APIClient() self.client.force_authenticate(user=self.user) self.group = Group.objects.create(owner=self.group_user, name="Penn Labs", color="blue") @mock.patch("gsr_booking.api_wrapper.WhartonBookingWrapper.request", mock_requests_get) - def test_is_wharton(self): + def test_is_wharton(self) -> None: self.assertFalse(WhartonGSRBooker.is_wharton(self.user)) @mock.patch("gsr_booking.api_wrapper.WhartonBookingWrapper.request", mock_requests_get) - def test_wharton_availability(self): + def test_wharton_availability(self) -> None: availability = GSRBooker.get_availability("JMHH", 1, "2021-01-07", "2022-01-08", self.user) self.assertIn("name", availability) self.assertIn("gid", availability) @@ -78,26 +78,28 @@ def test_wharton_availability(self): # @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): + def test_book_wharton(self) -> None: 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.gsrbooking_set.first().room_name) + first_booking = book_wharton.gsrbooking_set.first() + assert first_booking is not None + self.assertEquals("241", first_booking.room_name) @mock.patch("gsr_booking.api_wrapper.WhartonBookingWrapper.request", mock_requests_get) - def test_wharton_reservations(self): + def test_wharton_reservations(self) -> None: reservations = WhartonGSRBooker.get_reservations(self.user) self.assertTrue(isinstance(reservations, list)) self.assertIn("booking_id", reservations[0]) self.assertIn("gid", reservations[0]) @mock.patch("gsr_booking.api_wrapper.WhartonBookingWrapper.request", mock_requests_get) - def test_cancel_wharton(self): + def test_cancel_wharton(self) -> None: cancel = GSRBooker.cancel_room("987654", self.user) self.assertIsNone(cancel) @mock.patch("gsr_booking.api_wrapper.LibCalBookingWrapper.request", mock_requests_get) - def test_libcal_availability(self): + def test_libcal_availability(self) -> None: availability = GSRBooker.get_availability( "1086", 1889, "2021-01-07", "2022-01-08", self.user ) @@ -109,7 +111,7 @@ def test_libcal_availability(self): self.assertIn("availability", availability["rooms"][0]) @mock.patch("gsr_booking.api_wrapper.LibCalBookingWrapper.request", mock_requests_get) - def test_book_libcal(self): + def test_book_libcal(self) -> None: book_libcal = GSRBooker.book_room( 1889, 7192, @@ -118,19 +120,21 @@ def test_book_libcal(self): "2021-12-05T16:30:00-05:00", self.user, ) - self.assertEquals("VP WIC Booth 01", book_libcal.gsrbooking_set.first().room_name) + first_booking = book_libcal.gsrbooking_set.first() + assert first_booking is not None + self.assertEquals("VP WIC Booth 01", first_booking.room_name) @mock.patch( "gsr_booking.api_wrapper.WhartonBookingWrapper.request", mock_requests_get ) # purposefully wharton request here - def test_libcal_reservations(self): + def test_libcal_reservations(self) -> None: 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.LibCalBookingWrapper.request", mock_requests_get) - def test_cancel_libcal(self): + def test_cancel_libcal(self) -> None: group = Group.objects.create(owner=self.user) reservation = Reservation.objects.create(creator=self.user, group=group) GSRBooking.objects.create( @@ -145,10 +149,11 @@ def test_cancel_libcal(self): self.assertIsNone(cancel) @mock.patch("gsr_booking.api_wrapper.WhartonBookingWrapper.request", mock_requests_get) - def test_group_book_wharton(self): + def test_group_book_wharton(self) -> None: # make sure group_user is treated as a wharton user so they # are returned in list of wharton users in gb.book_room membership1 = GroupMembership.objects.filter(group=self.group).first() + assert membership1 is not None membership1.is_wharton = True membership1.save() @@ -180,7 +185,7 @@ def test_group_book_wharton(self): self.assertIsNotNone(Reservation.objects.get(pk=reservation.id)) @mock.patch("gsr_booking.api_wrapper.LibCalBookingWrapper.request", mock_requests_get) - def test_group_book_libcal(self): + def test_group_book_libcal(self) -> None: # add user to the group GroupMembership.objects.create(user=self.user, group=self.group, accepted=True) @@ -211,7 +216,9 @@ def test_group_book_libcal(self): self.assertEqual(len(res), 1) self.assertEqual(res[0]["room_name"], "[Me] VP WIC Booth 01") - credit_owner = reservation.gsrbooking_set.first().user + first_booking = reservation.gsrbooking_set.first() + assert first_booking is not None + credit_owner = first_booking.user res = GSRBooker.get_reservations(credit_owner, self.group) self.assertEqual(len(res), 1) self.assertEqual( @@ -220,7 +227,7 @@ def test_group_book_libcal(self): ) @mock.patch("gsr_booking.api_wrapper.WhartonBookingWrapper.request", mock_requests_get) - def test_group_wharton_availability(self): + def test_group_wharton_availability(self) -> None: with self.assertRaises(APIError): GSRBooker.get_availability( "JMHH", 1, "2021-01-07", "2022-01-08", self.group_user, self.group @@ -234,6 +241,7 @@ def test_group_wharton_availability(self): 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]) + room_info = cast(dict[str, Any], availability["rooms"][0]) + self.assertIn("room_name", room_info) + self.assertIn("id", room_info) + self.assertIn("availability", room_info)