Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 6 additions & 1 deletion backend/api/availability/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from django.db.models import Q

from api.models import (
EventCalendarTimeslot,
EventDateTimeslot,
EventParticipant,
EventWeekdayTimeslot,
Expand All @@ -16,10 +17,14 @@ def get_timeslots(event):
timeslots = EventDateTimeslot.objects.filter(user_event=event).order_by(
"utc_timeslot"
)
else:
elif event.date_type == UserEvent.EventType.GENERIC:
timeslots = EventWeekdayTimeslot.objects.filter(user_event=event).order_by(
"weekday", "local_timeslot"
)
elif event.date_type == UserEvent.EventType.CALENDAR:
timeslots = EventCalendarTimeslot.objects.filter(user_event=event).order_by(
"date"
)

return timeslots

Expand Down
100 changes: 91 additions & 9 deletions backend/api/availability/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@
EventAvailabilitySerializer,
EventCodeSerializer,
)
from api.availability.utils import check_name_available, get_timeslots, get_weekday_date
from api.availability.utils import (
check_name_available,
get_timeslots,
get_weekday_date,
)
from api.decorators import (
api_endpoint,
check_auth,
Expand All @@ -21,6 +25,7 @@
)
from api.models import (
AvailabilityStatus,
EventCalendarAvailability,
EventDateAvailability,
EventParticipant,
EventWeekdayAvailability,
Expand All @@ -36,6 +41,7 @@
check_rate_limit,
event_lookup,
notify_live_update,
to_datetime,
)

logger = logging.getLogger("api")
Expand All @@ -51,14 +57,16 @@ class InvalidTimeslotError(Exception):
@validate_output(MessageOutputSerializer)
def add_availability(request):
"""
Adds availability for the current user to an event. This endpoint supports both types
Adds availability for the current user to an event. This endpoint supports all types
of events.

If the current user already added availability for the event, their data will be
overridden.

The availability must be supplied in a 2D array, with the outermost array representing
days, and the innermost representing timeslots within that day.
The availability must be supplied in an array of timeslots that represent the dates
and times where the user is available.

The timeslots must be in ISO format, even for calendar events.
"""
user = request.user
event_code = request.validated_data.get("event_code")
Expand All @@ -70,6 +78,12 @@ def add_availability(request):
with transaction.atomic():
user_event = event_lookup(event_code)

if user_event.date_type == UserEvent.EventType.CALENDAR:
# Convert the availability to date objects for calendar events
availability = [a.date() for a in availability]
# Deduplicate any timeslots
availability = list(set(availability))

if not check_name_available(user_event, user, display_name):
return Response(
{
Expand Down Expand Up @@ -99,10 +113,14 @@ def add_availability(request):
EventDateAvailability.objects.filter(
event_participant=participant
).delete()
else:
elif user_event.date_type == UserEvent.EventType.GENERIC:
EventWeekdayAvailability.objects.filter(
event_participant=participant
).delete()
elif user_event.date_type == UserEvent.EventType.CALENDAR:
EventCalendarAvailability.objects.filter(
event_participant=participant
).delete()

# Add new availability
if user_event.date_type == UserEvent.EventType.SPECIFIC:
Expand Down Expand Up @@ -135,6 +153,20 @@ def add_availability(request):
)
)
EventWeekdayAvailability.objects.bulk_create(new_availabilities)
elif user_event.date_type == UserEvent.EventType.CALENDAR:
timeslot_dict = {t.date: t for t in timeslots}
new_availabilities = []
for timeslot in availability:
if timeslot not in timeslot_dict:
raise InvalidTimeslotError()
new_availabilities.append(
EventCalendarAvailability(
event_participant=participant,
event_calendar_timeslot=timeslot_dict[timeslot],
status=AvailabilityStatus.AVAILABLE,
)
)
EventCalendarAvailability.objects.bulk_create(new_availabilities)

# Update participant updated_at
participant.save()
Expand Down Expand Up @@ -167,7 +199,9 @@ def add_availability(request):
joined_at=participant.created_at.isoformat(),
updated_at=participant.updated_at.isoformat(),
time_zone=participant.time_zone,
availability=[time.isoformat() for time in availability],
availability=[
to_datetime(timeslot).isoformat() for timeslot in availability
],
),
)
)
Expand Down Expand Up @@ -254,7 +288,7 @@ def get_self_availability(request):
.order_by("event_date_timeslot__utc_timeslot")
)
data = [a.event_date_timeslot.utc_timeslot for a in availabilities]
else:
elif event.date_type == UserEvent.EventType.GENERIC:
availabilities = (
EventWeekdayAvailability.objects.filter(event_participant=participant)
.select_related("event_weekday_timeslot")
Expand All @@ -270,6 +304,14 @@ def get_self_availability(request):
)
for a in availabilities
]
elif event.date_type == UserEvent.EventType.CALENDAR:
availabilities = (
EventCalendarAvailability.objects.filter(event_participant=participant)
.select_related("event_calendar_timeslot")
.order_by("event_calendar_timeslot__date")
)
# Convert dates to datetimes
data = [to_datetime(a.event_calendar_timeslot.date) for a in availabilities]

return Response(
{
Expand Down Expand Up @@ -320,11 +362,14 @@ def get_all_availability(request):
if event.date_type == UserEvent.EventType.SPECIFIC:
for slot in timeslots:
availability_dict[slot.utc_timeslot.isoformat()] = []
else:
elif event.date_type == UserEvent.EventType.GENERIC:
for slot in timeslots:
availability_dict[
get_weekday_date(slot.weekday, slot.local_timeslot).isoformat()
] = []
elif event.date_type == UserEvent.EventType.CALENDAR:
for slot in timeslots:
availability_dict[to_datetime(slot.date).isoformat()] = []

if not len(participants):
return Response(
Expand Down Expand Up @@ -377,7 +422,7 @@ def get_all_availability(request):
},
status=200,
)
else:
elif event.date_type == UserEvent.EventType.GENERIC:
availabilities = (
EventWeekdayAvailability.objects.filter(
event_participant__in=participants
Expand Down Expand Up @@ -418,6 +463,43 @@ def get_all_availability(request):
},
status=200,
)
elif event.date_type == UserEvent.EventType.CALENDAR:
availabilities = (
EventCalendarAvailability.objects.filter(
event_participant__in=participants
)
.select_related("event_calendar_timeslot", "event_participant")
.order_by(
"event_calendar_timeslot__date",
"event_participant__display_name",
)
)
for t in availabilities:
timeslot = to_datetime(t.event_calendar_timeslot.date).isoformat()
if timeslot not in availability_dict:
logger.error(
f"Timeslot {timeslot} not found in availability dict for event {event_code}"
)
continue
availability_dict[timeslot].append(t.event_participant.display_name)

return Response(
{
"user_display_name": user_display_name,
"participants": [
{
"public_id": str(p.public_id),
"display_name": p.display_name,
"joined_at": p.created_at.isoformat(),
"updated_at": p.updated_at.isoformat(),
"time_zone": p.time_zone,
}
for p in participants
],
"availability": availability_dict,
},
status=200,
)

except UserEvent.DoesNotExist:
return Response(
Expand Down
13 changes: 12 additions & 1 deletion backend/api/dashboard/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

from api.decorators import api_endpoint, check_auth, validate_output
from api.models import (
EventCalendarTimeslot,
EventDateTimeslot,
EventParticipant,
EventWeekdayTimeslot,
Expand All @@ -18,7 +19,9 @@

class DashboardEventSerializer(serializers.Serializer):
title = serializers.CharField(required=True, max_length=255)
event_type = serializers.ChoiceField(required=True, choices=["Date", "Week"])
event_type = serializers.ChoiceField(
required=True, choices=["Date", "Week", "Calendar"]
)
start_date = serializers.DateField(required=True)
end_date = serializers.DateField(required=True)
start_time = serializers.TimeField(required=True)
Expand Down Expand Up @@ -78,6 +81,10 @@ def get_dashboard(request):
"weekday", "local_timeslot"
),
),
Prefetch(
"calendar_timeslots",
queryset=EventCalendarTimeslot.objects.order_by("date"),
),
Comment thread
jzgom067 marked this conversation as resolved.
Prefetch(
"participants",
queryset=EventParticipant.objects.order_by("created_at"),
Expand All @@ -104,6 +111,10 @@ def get_dashboard(request):
"weekday", "local_timeslot"
),
),
Prefetch(
"user_event__calendar_timeslots",
queryset=EventCalendarTimeslot.objects.order_by("date"),
),
Prefetch(
"user_event__participants",
queryset=EventParticipant.objects.order_by("created_at"),
Expand Down
16 changes: 5 additions & 11 deletions backend/api/event/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,25 +28,19 @@ class EventInfoSerializer(serializers.Serializer):
time_zone = TimeZoneField(required=True)


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


class WeekEventCreateSerializer(EventInfoSerializer, CustomCodeSerializer):
pass


class DateEventEditSerializer(EventInfoSerializer, EventCodeSerializer):
pass


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


class EventDetailSerializer(EventInfoSerializer):
is_creator = serializers.BooleanField(required=True)
event_type = serializers.ChoiceField(required=True, choices=["Date", "Week"])
event_type = serializers.ChoiceField(
required=True, choices=["Date", "Week", "Calendar"]
)
start_date = serializers.DateField(required=True)
end_date = serializers.DateField(required=True)
start_time = serializers.TimeField(required=True)
Expand Down
2 changes: 2 additions & 0 deletions backend/api/event/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@
urlpatterns = [
path("date-create/", views.create_date_event),
path("week-create/", views.create_week_event),
path("calendar-create/", views.create_calendar_event),
path("check-code/", views.check_code),
path("get-true-code/", views.get_true_code),
path("date-edit/", views.edit_date_event),
path("week-edit/", views.edit_week_event),
path("calendar-edit/", views.edit_calendar_event),
path("delete/", views.delete_event),
path("get-details/", views.get_event_details),
path("get-updates/<str:event_code>/", views.get_live_updates),
Expand Down
47 changes: 44 additions & 3 deletions backend/api/event/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,18 @@
import random
import re
import string
from datetime import datetime
from datetime import date, datetime
from zoneinfo import ZoneInfo

from django.db.models import Prefetch

from api.models import EventDateTimeslot, EventWeekdayTimeslot, UrlCode, UserEvent
from api.models import (
EventCalendarTimeslot,
EventDateTimeslot,
EventWeekdayTimeslot,
UrlCode,
UserEvent,
)
from api.settings import MAX_EVENT_DAYS, RAND_URL_CODE_ATTEMPTS, RAND_URL_CODE_LENGTH

logger = logging.getLogger("api")
Expand Down Expand Up @@ -80,7 +86,7 @@ def check_timeslot_times(timeslots):

def validate_date_timeslots(
timeslots: list[datetime],
earliest_date_local: datetime.date,
earliest_date_local: date,
user_time_zone: str,
editing: bool = False,
):
Expand Down Expand Up @@ -131,6 +137,37 @@ def validate_weekday_timeslots(timeslots):
return {}


def validate_calendar_timeslots(
timeslots: list[datetime], earliest_date_local: date, editing: bool = False
):
if not timeslots:
return {"timeslots": ["At least one timeslot is required."]}

start_date = min(ts.date() for ts in timeslots)
end_date = max(ts.date() for ts in timeslots)

errors = {}

def add_error(message):
if "timeslots" not in errors:
errors["timeslots"] = []
errors["timeslots"].append(message)

# The earliest date allowed is "today" in the user's local time zone, which is why
# this uses a time zone conversion instead of UTC
if start_date < earliest_date_local:
if editing:
add_error(
"Event cannot start earlier than today, or be moved earlier if already before today."
)
else:
add_error("Event must start today or in the future.")
if (end_date - start_date).days > MAX_EVENT_DAYS:
add_error(f"Max event length is {MAX_EVENT_DAYS} days.")

return errors


def event_lookup_prefetch(event_code: str):
"""
Looks up an event by its URL code.
Expand All @@ -146,6 +183,10 @@ def event_lookup_prefetch(event_code: str):
"weekday_timeslots",
queryset=EventWeekdayTimeslot.objects.order_by("weekday", "local_timeslot"),
),
Prefetch(
"calendar_timeslots",
queryset=EventCalendarTimeslot.objects.order_by("date"),
),
).get(url_code__url_code__iexact=event_code)


Expand Down
Loading
Loading