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
3 changes: 2 additions & 1 deletion backend/officehoursqueue/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@
"email_tools.apps.EmailToolsConfig",
"accounts.apps.AccountsConfig",
"ohq.apps.OhqConfig",
"schedule",
"ohq_schedule.apps.OHQScheduleConfig",
"schedule"
]

MIDDLEWARE = [
Expand Down
53 changes: 53 additions & 0 deletions backend/ohq/migrations/0022_booking.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Generated by Django 5.0.3 on 2025-04-12 03:39

import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("ohq", "0021_queue_question_timer_enabled_and_more"),
("ohq_schedule", "0015_event_bookable_event_interval_event_location_and_more"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]

operations = [
migrations.CreateModel(
name="Booking",
fields=[
(
"id",
models.AutoField(
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
),
),
("start", models.DateTimeField(db_index=True, verbose_name="start")),
("end", models.DateTimeField(db_index=True, verbose_name="end")),
(
"occurrence",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="bookings",
to="ohq_schedule.occurrence",
),
),
(
"user",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"verbose_name": "booking",
"verbose_name_plural": "bookings",
"ordering": ["start"],
"index_together": {("start", "end")},
},
),
]
108 changes: 108 additions & 0 deletions backend/ohq/migrations/0023_migrate_schedule_data.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
# Generated by Django 5.0.3 on 2025-04-13 15:24

from django.db import migrations

def migrate_schedule_data(apps, schema_editor):
OldCalendarRelation = apps.get_model('schedule', 'CalendarRelation')
OldCalendar = apps.get_model('schedule', 'Calendar')
OldEvent = apps.get_model('schedule', 'Event')
OldOccurrence = apps.get_model('schedule', 'Occurrence')
OldRule = apps.get_model('schedule', 'Rule')
OldEventRelation = apps.get_model('schedule', 'EventRelation')

NewCalendarRelation = apps.get_model('ohq_schedule', 'CalendarRelation')
NewCalendar = apps.get_model('ohq_schedule', 'Calendar')
NewEvent = apps.get_model('ohq_schedule', 'Event')
NewOccurrence = apps.get_model('ohq_schedule', 'Occurrence')
NewRule = apps.get_model('ohq_schedule', 'Rule')
NewEventRelation = apps.get_model('ohq_schedule', 'EventRelation')

for old_calendar_relation in OldCalendarRelation.objects.all():
NewCalendarRelation.objects.create(
id=old_calendar_relation.id,
calendar_id=old_calendar_relation.calendar_id,
content_type_id=old_calendar_relation.content_type_id,
object_id=old_calendar_relation.object_id,
distinction=old_calendar_relation.distinction,
inheritable=old_calendar_relation.inheritable
)

for old_calendar in OldCalendar.objects.all():
NewCalendar.objects.create(
id=old_calendar.id,
name=old_calendar.name,
slug=old_calendar.slug
)

for old_rule in OldRule.objects.all():
NewRule.objects.create(
id=old_rule.id,
name=old_rule.name,
description=old_rule.description,
frequency=old_rule.frequency,
params=old_rule.params
)

for old_event in OldEvent.objects.all():
NewEvent.objects.create(
id=old_event.id,
start=old_event.start,
end=old_event.end,
title=old_event.title,
description=old_event.description,
calendar_id=old_event.calendar_id,
rule_id=old_event.rule_id,
end_recurring_period=old_event.end_recurring_period,
location=old_event.location if hasattr(old_event, 'location') else '',
bookable=old_event.bookable if hasattr(old_event, 'bookable') else False,
interval=old_event.interval if hasattr(old_event, 'interval') else None
)

for old_occurrence in OldOccurrence.objects.all():
NewOccurrence.objects.create(
id=old_occurrence.id,
event_id=old_occurrence.event_id,
title=old_occurrence.title,
description=old_occurrence.description,
start=old_occurrence.start,
end=old_occurrence.end,
cancelled=old_occurrence.cancelled,
original_start=old_occurrence.original_start,
original_end=old_occurrence.original_end,
location=old_occurrence.location if hasattr(old_occurrence, 'location') else '',
interval=old_occurrence.interval if hasattr(old_occurrence, 'interval') else None
)

for old_relation in OldEventRelation.objects.all():
NewEventRelation.objects.create(
id=old_relation.id,
event_id=old_relation.event_id,
content_type_id=old_relation.content_type_id,
object_id=old_relation.object_id,
distinction=old_relation.distinction
)

def reverse_migrate_schedule_data(apps, schema_editor):
NewCalendarRelation = apps.get_model('ohq_schedule', 'CalendarRelation')
NewCalendar = apps.get_model('ohq_schedule', 'Calendar')
NewEvent = apps.get_model('ohq_schedule', 'Event')
NewOccurrence = apps.get_model('ohq_schedule', 'Occurrence')
NewRule = apps.get_model('ohq_schedule', 'Rule')
NewEventRelation = apps.get_model('ohq_schedule', 'EventRelation')

NewCalendarRelation.objects.all().delete()
NewCalendar.objects.all().delete()
NewEvent.objects.all().delete()
NewOccurrence.objects.all().delete()
NewRule.objects.all().delete()
NewEventRelation.objects.all().delete()

class Migration(migrations.Migration):
dependencies = [
('ohq', '0022_booking'),
('schedule', '0001_initial'),
('ohq_schedule', '0015_event_bookable_event_interval_event_location_and_more'),
]
operations = [
migrations.RunPython(migrate_schedule_data, reverse_migrate_schedule_data),
]
72 changes: 71 additions & 1 deletion backend/ohq/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
from django.dispatch import receiver
from email_tools.emails import send_email
from phonenumber_field.modelfields import PhoneNumberField

from ohq_schedule.models import Event, Occurrence
from schedule.models import Event as OldEvent
from schedule.models import Occurrence as OldOccurrence

User = settings.AUTH_USER_MODEL

Expand Down Expand Up @@ -447,3 +449,71 @@ class Meta:

def __str__(self):
return f"{self.user}: {self.metric}"

class Booking(models.Model):
"""
Booking within an occurrence.
Bookings can only be created with start times of 5-minute intervals.
"""

occurrence = models.ForeignKey(Occurrence, on_delete=models.CASCADE, related_name="bookings")
user = models.ForeignKey(User, on_delete=models.CASCADE, blank=True, null=True)
start = models.DateTimeField("start", db_index=True)
end = models.DateTimeField("end", db_index=True)

class Meta:
verbose_name = ("booking")
verbose_name_plural = ("bookings")
ordering = ["start"]
index_together = (("start", "end"),)

def clean(self):
if self.start >= self.end:
raise ValidationError('Start time must be before end time.')

if self.start.minute % 5 != 0:
raise ValidationError('Start time must be on a 5-minute interval (e.g., :00, :05, :10, :15, etc).')

if self.start < self.occurrence.start or self.end > self.occurrence.end:
raise ValidationError('Booking times must be within the occurrence\'s start and end times.')

duration = self.end - self.start
duration_minutes = duration.total_seconds() / 60

if duration_minutes != self.occurrence.interval:
raise ValidationError(f'Booking duration must be {self.occurrence.interval} minutes.')

overlapping_bookings = Booking.objects.filter(
occurrence=self.occurrence,
start__lt=self.end,
end__gt=self.start
).exclude(id=self.id)
if overlapping_bookings.exists():
raise ValidationError('Booking times cannot overlap with existing bookings.')

super().clean()

def save(self, *args, **kwargs):
self.clean()
super().save(*args, **kwargs)

def __str__(self):
start_str = self.start.strftime("%Y-%m-%d %H:%M:%S")
end_str = self.end.strftime("%Y-%m-%d %H:%M:%S")
return f"{start_str} to {end_str}"

Event.add_to_class('location', models.CharField(max_length=255, blank=True))
Occurrence.add_to_class('location', models.CharField(max_length=255, blank=True))
Occurrence.add_to_class('interval', models.IntegerField(blank=True, null=True))

def new_occurrence_init(self, *args, **kwargs):
super(Occurrence, self).__init__(*args, **kwargs)
event = kwargs.get("event", None)
if not self.title and event:
self.title = event.title
if not self.description and event:
self.description = event.description
if not self.location and event:
self.location = event.location

Occurrence.__init__ = new_occurrence_init
11 changes: 7 additions & 4 deletions backend/ohq/permissions.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
from django.db.models import Q
from rest_framework import permissions
from schedule.models import Event, EventRelation, Occurrence

from ohq.models import Course, Membership, Question

from ohq_schedule.models import Event, EventRelation, Occurrence
from ohq.models import (
Course,
Membership,
Question,
Booking,
)

# Hierarchy of permissions is usually:
# Professor > Head TA > TA > Student > User
Expand Down
3 changes: 1 addition & 2 deletions backend/ohq/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@
from phonenumber_field.serializerfields import PhoneNumberField
from rest_framework import serializers
from rest_live.signals import save_handler
from schedule.models import Calendar, Event, EventRelation, EventRelationManager, Rule
from schedule.models.events import Occurrence
from ohq_schedule.models import Calendar, Event, Occurrence, EventRelation, EventRelationManager, Rule

from ohq.models import (
Announcement,
Expand Down
2 changes: 1 addition & 1 deletion backend/ohq/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
from rest_framework.settings import api_settings
from rest_framework.views import APIView
from rest_live.mixins import RealtimeMixin
from schedule.models import Event, EventRelationManager, Occurrence
from ohq_schedule.models import Event, EventRelationManager, Occurrence
from http import HTTPStatus

from ohq.filters import CourseStatisticFilter, QuestionSearchFilter, QueueStatisticFilter
Expand Down
4 changes: 4 additions & 0 deletions backend/ohq_schedule/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import django

if django.VERSION < (3, 2):
default_app_config = "ohq_schedule.apps.OHQScheduleConfig"
80 changes: 80 additions & 0 deletions backend/ohq_schedule/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
from django.contrib import admin

from ohq_schedule.forms import EventAdminForm
from ohq_schedule.models import (
Calendar,
CalendarRelation,
Event,
EventRelation,
Occurrence,
Rule,
)

@admin.register(Calendar)
class CalendarAdmin(admin.ModelAdmin):
list_display = ("name", "slug")
prepopulated_fields = {"slug": ("name",)}
search_fields = ["name"]
fieldsets = ((None, {"fields": [("name", "slug")]}),)


@admin.register(CalendarRelation)
class CalendarRelationAdmin(admin.ModelAdmin):
list_display = ("calendar", "content_object")
list_filter = ("inheritable",)
fieldsets = (
(
None,
{
"fields": [
"calendar",
("content_type", "object_id", "distinction"),
"inheritable",
]
},
),
)


@admin.register(EventRelation)
class EventRelationAdmin(admin.ModelAdmin):
list_display = ("event", "content_object", "distinction")
fieldsets = (
(None, {"fields": ["event", ("content_type", "object_id", "distinction")]}),
)


@admin.register(Event)
class EventAdmin(admin.ModelAdmin):
list_display = ("title", "start", "end")
list_filter = ("start",)
ordering = ("-start",)
date_hierarchy = "start"
search_fields = ("title", "description")
fieldsets = (
(
None,
{
"fields": [
("title", "color_event"),
("description",),
("start", "end"),
("creator", "calendar"),
("rule", "end_recurring_period"),
"location",
("interval", "bookable"),
]
},
),
)
form = EventAdminForm


admin.site.register(Occurrence, admin.ModelAdmin)


@admin.register(Rule)
class RuleAdmin(admin.ModelAdmin):
list_display = ("name",)
list_filter = ("frequency",)
search_fields = ("name", "description")
8 changes: 8 additions & 0 deletions backend/ohq_schedule/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from django.apps import AppConfig
from django.utils.translation import gettext_lazy as _


class OHQScheduleConfig(AppConfig):
name = "ohq_schedule"
verbose_name = _("ohq_schedules")
default_auto_field = "django.db.models.AutoField"
Loading
Loading