Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
162b299
Change availability add to list of timeslots
jzgom067 Dec 27, 2025
551013b
Replace is_available column with status
jzgom067 Dec 27, 2025
fb0e553
Remove is_available references
jzgom067 Dec 27, 2025
2fc3c1e
Remove unused imports
jzgom067 Dec 27, 2025
db27ce1
Rework event endpoints to use timeslots
jzgom067 Dec 27, 2025
0488f51
Fix migration default value
jzgom067 Dec 27, 2025
69e67cd
Reorganize util functions
jzgom067 Dec 27, 2025
ddcd6b7
Update event details docstring
jzgom067 Dec 28, 2025
b5f8592
Rework dashboard logic to use timeslots
jzgom067 Dec 28, 2025
19005cb
Fix duplicate timeslot error
jzgom067 Dec 28, 2025
0d47508
Add TimeField to docs endpoint field types
jzgom067 Dec 28, 2025
0062c67
Remove unused import
jzgom067 Dec 28, 2025
874ff83
Update dashboard date/time range calculation
jzgom067 Dec 28, 2025
af5b35a
Remove unnecessary comment
jzgom067 Dec 28, 2025
424b1a5
Add error message combination
jzgom067 Dec 28, 2025
887821b
Add clarifying comment
jzgom067 Dec 28, 2025
885aad2
Add stricter timeslot validation
jzgom067 Dec 28, 2025
d68a12a
Add empty event error catching
jzgom067 Dec 28, 2025
b7808de
Add empty event error handling on edit
jzgom067 Dec 28, 2025
3be5e97
Remove redundant check
jzgom067 Dec 28, 2025
c5fb052
Add extra empty timeslot list checks
jzgom067 Dec 28, 2025
c2a5492
Add explicit irreversible code in migration
jzgom067 Dec 28, 2025
aa16e38
Add nonexistent url_code handling
jzgom067 Dec 28, 2025
8ee2086
Add proper deduplication for week event creation
jzgom067 Dec 28, 2025
ad37d3f
Remove time zone query param from dashboard
jzgom067 Jan 3, 2026
20864ba
Move event info formatting to main utils
jzgom067 Jan 3, 2026
2c37419
Add duration to event format
jzgom067 Jan 3, 2026
ac50135
Add event info formatting to event details
jzgom067 Jan 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions api/availability/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,7 @@ class DisplayNameCheckSerializer(EventCodeSerializer, DisplayNameSerializer):

class AvailabilitySerializer(serializers.Serializer):
availability = serializers.ListField(
child=serializers.ListField(
child=serializers.BooleanField(), required=True, min_length=4
),
child=serializers.DateTimeField(required=True),
required=True,
min_length=1,
)
Expand Down
18 changes: 2 additions & 16 deletions api/availability/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,32 +10,18 @@
)


class EventGridDimensionError(Exception):
pass


def get_event_grid(event):
def get_timeslots(event):
timeslots = []
num_days = 0
if event.date_type == UserEvent.EventType.SPECIFIC:
timeslots = EventDateTimeslot.objects.filter(user_event=event).order_by(
"timeslot"
)
num_days = (
timeslots.last().timeslot.date() - timeslots.first().timeslot.date()
).days + 1
else:
timeslots = EventWeekdayTimeslot.objects.filter(user_event=event).order_by(
"weekday", "timeslot"
)
num_days = timeslots.last().weekday - timeslots.first().weekday + 1

num_slots = timeslots.count() / num_days
if timeslots.count() % num_days != 0:
raise EventGridDimensionError(
"Event timeslots are not evenly distributed across days."
)
return timeslots, int(num_days), int(num_slots)
return timeslots


def check_name_available(event, user, display_name):
Expand Down
87 changes: 36 additions & 51 deletions api/availability/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,9 @@
EventAvailabilitySerializer,
EventCodeSerializer,
)
from api.availability.utils import (
EventGridDimensionError,
check_name_available,
get_event_grid,
get_weekday_date,
)
from api.availability.utils import check_name_available, get_timeslots, get_weekday_date
from api.models import (
AvailabilityStatus,
EventDateAvailability,
EventParticipant,
EventWeekdayAvailability,
Expand All @@ -42,7 +38,7 @@ class AvailabilityAddThrottle(AnonRateThrottle):
scope = "availability_add"


class AvailabilityInputInvalidError(Exception):
class InvalidTimeslotError(Exception):
pass


Expand Down Expand Up @@ -85,17 +81,7 @@ def add_availability(request):
status=400,
)

timeslots, num_days, num_times = get_event_grid(user_event)

if len(availability) != num_days:
raise AvailabilityInputInvalidError(
f"Invalid availability days. Expected {num_days}, got {len(availability)}."
)
for day in availability:
if len(day) != num_times:
raise AvailabilityInputInvalidError(
f"Invalid availability timeslots. Expected {num_times}, got {len(day)}."
)
timeslots = get_timeslots(user_event)

participant, new = EventParticipant.objects.get_or_create(
user_event=user_event,
Expand All @@ -107,6 +93,7 @@ def add_availability(request):
participant.display_name = display_name
participant.save()

# Remove existing availability
if user_event.date_type == UserEvent.EventType.SPECIFIC:
EventDateAvailability.objects.filter(
event_participant=participant
Expand All @@ -116,50 +103,54 @@ def add_availability(request):
event_participant=participant
).delete()

# Flatten the availability array to match the timeslots array format
flattened_availability = [
timeslot for day in availability for timeslot in day
]
new_availabilities = []
# Add new availability
if user_event.date_type == UserEvent.EventType.SPECIFIC:
for i, timeslot in enumerate(timeslots):
timeslot_dict = {t.timeslot: t for t in timeslots}
new_availabilities = []
for timeslot in availability:
if timeslot not in timeslot_dict:
raise InvalidTimeslotError()
new_availabilities.append(
EventDateAvailability(
event_participant=participant,
event_date_timeslot=timeslot,
is_available=flattened_availability[i],
event_date_timeslot=timeslot_dict[timeslot],
status=AvailabilityStatus.AVAILABLE,
)
)
EventDateAvailability.objects.bulk_create(new_availabilities)
elif user_event.date_type == UserEvent.EventType.GENERIC:
for i, timeslot in enumerate(timeslots):
timeslot_dict = {
get_weekday_date(t.weekday, t.timeslot): t for t in timeslots
}
new_availabilities = []
for timeslot in availability:
if timeslot not in timeslot_dict:
raise InvalidTimeslotError()
new_availabilities.append(
EventWeekdayAvailability(
event_participant=participant,
event_weekday_timeslot=timeslot,
is_available=flattened_availability[i],
event_weekday_timeslot=timeslot_dict[timeslot],
status=AvailabilityStatus.AVAILABLE,
)
)
EventWeekdayAvailability.objects.bulk_create(new_availabilities)

except AvailabilityInputInvalidError as e:
logger.warning(str(e) + " Event code: " + event_code)
except UserEvent.DoesNotExist:
return Response(
{"error": {"event_code": ["Event not found."]}},
status=404,
)
except InvalidTimeslotError:
return Response(
{
"error": {
"availability": [str(e)],
"availability": [
"One or more timeslots are invalid, check if the event has been updated."
]
}
},
status=400,
)
except EventGridDimensionError as e:
logger.critical(e)
return GENERIC_ERR_RESPONSE
except UserEvent.DoesNotExist:
return Response(
{"error": {"event_code": ["Event not found."]}},
status=404,
)
except DatabaseError as e:
logger.db_error(e)
return GENERIC_ERR_RESPONSE
Expand Down Expand Up @@ -250,18 +241,14 @@ def get_self_availability(request):

if event.date_type == UserEvent.EventType.SPECIFIC:
availabilities = (
EventDateAvailability.objects.filter(
event_participant=participant, is_available=True
)
EventDateAvailability.objects.filter(event_participant=participant)
.select_related("event_date_timeslot")
.order_by("event_date_timeslot__timeslot")
)
data = [a.event_date_timeslot.timeslot for a in availabilities]
else:
availabilities = (
EventWeekdayAvailability.objects.filter(
event_participant=participant, is_available=True
)
EventWeekdayAvailability.objects.filter(event_participant=participant)
.select_related("event_weekday_timeslot")
.order_by(
"event_weekday_timeslot__weekday",
Expand Down Expand Up @@ -327,7 +314,7 @@ def get_all_availability(request):

# Prep the dictionary with empty arrays for the return value
availability_dict = {}
timeslots, _, _ = get_event_grid(event)
timeslots = get_timeslots(event)
if event.date_type == UserEvent.EventType.SPECIFIC:
for slot in timeslots:
availability_dict[slot.timeslot.isoformat()] = []
Expand Down Expand Up @@ -369,8 +356,7 @@ def get_all_availability(request):
f"Timeslot {timeslot} not found in availability dict for event {event_code}"
)
continue
if t.is_available:
availability_dict[timeslot].append(t.event_participant.display_name)
availability_dict[timeslot].append(t.event_participant.display_name)

return Response(
{
Expand Down Expand Up @@ -403,8 +389,7 @@ def get_all_availability(request):
f"Timeslot {timeslot} not found in availability dict for event {event_code}"
)
continue
if t.is_available:
availability_dict[timeslot].append(t.event_participant.display_name)
availability_dict[timeslot].append(t.event_participant.display_name)

return Response(
{
Expand Down
24 changes: 17 additions & 7 deletions api/dashboard/views.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,38 @@
import logging
from zoneinfo import ZoneInfo

from django.db import DatabaseError
from django.db.models import Prefetch, Q
from rest_framework import serializers
from rest_framework.response import Response

from api.event.serializers import EventDetailSerializer
from api.event.utils import format_event_info
from api.models import (
EventDateTimeslot,
EventParticipant,
EventWeekdayTimeslot,
UserEvent,
)
from api.settings import GENERIC_ERR_RESPONSE
from api.utils import api_endpoint, check_auth, validate_output
from api.utils import (
TimeZoneField,
api_endpoint,
check_auth,
format_event_info,
validate_output,
)

logger = logging.getLogger("api")


class DashboardEventSerializer(EventDetailSerializer):
class DashboardEventSerializer(serializers.Serializer):
title = serializers.CharField(required=True, max_length=255)
event_type = serializers.ChoiceField(required=True, choices=["Date", "Week"])
duration = serializers.IntegerField(required=False)
start_date = serializers.DateField(required=True)
end_date = serializers.DateField(required=True)
start_time = serializers.TimeField(required=True)
end_time = serializers.TimeField(required=True)
time_zone = TimeZoneField(required=True)
event_code = serializers.CharField(required=True, max_length=255)


Expand Down Expand Up @@ -99,12 +112,9 @@ def get_dashboard(request):
my_events = []
for event in created_events:
my_events.append(format_event_info(event))
my_events[-1]["event_code"] = event.url_code.url_code
their_events = []
for event in participations:
their_events.append(format_event_info(event.user_event))
if event.user_event.url_code is not None:
their_events[-1]["event_code"] = event.user_event.url_code.url_code

except DatabaseError as e:
logger.db_error(e)
Expand Down
2 changes: 2 additions & 0 deletions api/docs/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ def get_readable_field_name(field_name):
return "boolean"
case "DateField":
return "date"
case "TimeField":
return "time"
case "DateTimeField":
return "datetime"
case "EmailField":
Expand Down
31 changes: 11 additions & 20 deletions api/event/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@ class EventCodeSerializer(serializers.Serializer):
class EventInfoSerializer(serializers.Serializer):
title = serializers.CharField(required=True, max_length=50)
duration = serializers.IntegerField(required=False)
start_hour = serializers.IntegerField(required=True, min_value=0, max_value=24)
end_hour = serializers.IntegerField(required=True, min_value=0, max_value=24)
timeslots = serializers.ListField(
child=serializers.DateTimeField(), required=True, allow_empty=False
)
time_zone = TimeZoneField(required=True)

def validate(self, attrs):
Expand All @@ -32,35 +33,25 @@ def validate(self, attrs):
return super().validate(attrs)


class DateEventInfoSerializer(EventInfoSerializer):
start_date = serializers.DateField(required=True)
end_date = serializers.DateField(required=True)


class WeekEventInfoSerializer(EventInfoSerializer):
start_weekday = serializers.IntegerField(required=True, min_value=0, max_value=6)
end_weekday = serializers.IntegerField(required=True, min_value=0, max_value=6)


class DateEventCreateSerializer(DateEventInfoSerializer, CustomCodeSerializer):
class DateEventCreateSerializer(EventInfoSerializer, CustomCodeSerializer):
pass


class WeekEventCreateSerializer(WeekEventInfoSerializer, CustomCodeSerializer):
class WeekEventCreateSerializer(EventInfoSerializer, CustomCodeSerializer):
pass


class DateEventEditSerializer(DateEventInfoSerializer, EventCodeSerializer):
class DateEventEditSerializer(EventInfoSerializer, EventCodeSerializer):
pass


class WeekEventEditSerializer(WeekEventInfoSerializer, EventCodeSerializer):
class WeekEventEditSerializer(EventInfoSerializer, EventCodeSerializer):
pass


class EventDetailSerializer(EventInfoSerializer):
event_type = serializers.ChoiceField(required=True, choices=["Date", "Week"])
start_date = serializers.DateField(required=False)
end_date = serializers.DateField(required=False)
start_weekday = serializers.IntegerField(required=False, min_value=0, max_value=6)
end_weekday = serializers.IntegerField(required=False, min_value=0, max_value=6)
start_date = serializers.DateField(required=True)
end_date = serializers.DateField(required=True)
start_time = serializers.TimeField(required=True)
end_time = serializers.TimeField(required=True)
Loading