Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add backend for bookable office hours #324

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
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
62 changes: 62 additions & 0 deletions backend/ohq/migrations/0021_occurrence.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Generated by Django 5.0.3 on 2024-10-13 05:41

import django.core.validators
import django.db.models.deletion
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("ohq", "0020_auto_20240326_0226"),
("schedule", "0015_auto_20240825_0015"),
]

operations = [
migrations.CreateModel(
name="Occurrence",
fields=[
(
"id",
models.AutoField(
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
),
),
("title", models.CharField(blank=True, max_length=255, verbose_name="title")),
("location", models.CharField(blank=True, max_length=255, verbose_name="location")),
("description", models.TextField(blank=True, verbose_name="description")),
("start", models.DateTimeField(db_index=True, verbose_name="start")),
("end", models.DateTimeField(db_index=True, verbose_name="end")),
("cancelled", models.BooleanField(default=False, verbose_name="cancelled")),
("original_start", models.DateTimeField(verbose_name="original start")),
("original_end", models.DateTimeField(verbose_name="original end")),
("created_on", models.DateTimeField(auto_now_add=True, verbose_name="created on")),
("updated_on", models.DateTimeField(auto_now=True, verbose_name="updated on")),
(
"interval",
models.IntegerField(
blank=True,
validators=[
django.core.validators.MinValueValidator(5),
django.core.validators.MaxValueValidator(60),
],
verbose_name="interval",
),
),
(
"event",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="test_occurrence",
to="schedule.event",
verbose_name="event",
),
),
],
options={
"verbose_name": "occurrence",
"verbose_name_plural": "occurrences",
"index_together": {("start", "end")},
},
),
]
44 changes: 44 additions & 0 deletions backend/ohq/migrations/0022_booking.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Generated by Django 5.0.3 on 2024-10-13 17:12

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


class Migration(migrations.Migration):

dependencies = [
("ohq", "0021_occurrence"),
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, to="ohq.occurrence"
),
),
(
"user",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
),
),
],
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Generated by Django 5.0.3 on 2024-10-18 05:00

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


class Migration(migrations.Migration):

dependencies = [
("ohq", "0022_booking"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]

operations = [
migrations.AlterField(
model_name="booking",
name="end",
field=models.DateTimeField(blank=True, db_index=True, verbose_name="end"),
),
migrations.AlterField(
model_name="booking",
name="start",
field=models.DateTimeField(blank=True, db_index=True, verbose_name="start"),
),
migrations.AddConstraint(
model_name="booking",
constraint=models.UniqueConstraint(
fields=("occurrence", "user"), name="unique_reservation"
),
),
]
35 changes: 35 additions & 0 deletions backend/ohq/migrations/0024_alter_booking_options_and_more.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Generated by Django 5.0.3 on 2024-10-18 06:32

from django.conf import settings
from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
("ohq", "0023_alter_booking_end_alter_booking_start_and_more"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]

operations = [
migrations.AlterModelOptions(
name="booking",
options={
"ordering": ["start"],
"verbose_name": "booking",
"verbose_name_plural": "bookings",
},
),
migrations.RemoveConstraint(
model_name="booking",
name="unique_reservation",
),
migrations.AlterUniqueTogether(
name="booking",
unique_together={("occurrence", "user")},
),
migrations.AlterIndexTogether(
name="booking",
index_together={("start", "end")},
),
]
17 changes: 17 additions & 0 deletions backend/ohq/migrations/0025_alter_booking_unique_together.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Generated by Django 5.0.3 on 2024-10-18 06:33

from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
("ohq", "0024_alter_booking_options_and_more"),
]

operations = [
migrations.AlterUniqueTogether(
name="booking",
unique_together=set(),
),
]
23 changes: 22 additions & 1 deletion backend/ohq/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from django.dispatch import receiver
from email_tools.emails import send_email
from phonenumber_field.modelfields import PhoneNumberField

from schedule.models import Event, Occurrence

User = settings.AUTH_USER_MODEL

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

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

class Booking(models.Model):
"""
Booking within an occurrence
"""

occurrence = models.ForeignKey(Occurrence, on_delete=models.CASCADE)
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 __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}"
65 changes: 64 additions & 1 deletion backend/ohq/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from rest_framework import permissions
from schedule.models import Event, EventRelation, Occurrence

from ohq.models import Course, Membership, Question
from ohq.models import Course, Membership, Question, Booking


# Hierarchy of permissions is usually:
Expand Down Expand Up @@ -504,3 +504,66 @@ def has_permission(self, request, view):
return True

return True

class BookingPermission(permissions.BasePermission):
def get_membership_from_event(self, request, event):
event_course_relation = EventRelation.objects.filter(event=event).first()
membership = Membership.objects.filter(
course_id=event_course_relation.object_id, user=request.user
).first()
return membership

def has_object_permission(self, request, view, obj):
if view.action in ["retrieve"]:
booking = Booking.objects.filter(pk=view.kwargs["pk"]).first()
membership = self.get_membership_from_event(request=request, event=booking.occurrence.event)
return membership is not None

if view.action in ["update", "partial_update"]:
booking = Booking.objects.filter(pk=view.kwargs["pk"]).first()
membership = self.get_membership_from_event(request=request, event=booking.occurrence.event)

if membership is not None:
if membership.is_ta:
return True
else:
updated_fields = request.data.keys()
restricted_student_fields = ["occurrence", "start", "end"] # allowed_student_field would be "user"

for field in updated_fields:
if field in restricted_student_fields:
return False
if "user" in updated_fields:
return booking.user is None # Doesn’t allow a student to update user if someone already booked the slot
else:
return False

return False

def has_permission(self, request, view):
if not request.user.is_authenticated:
return False

# Anonymous users can't do anything
if view.action in ["create"]:
course_pk = request.data.get("course_id", None)
if course_pk is None:
return False

course = Course.objects.get(pk=course_pk)
membership = Membership.objects.filter(course=course, user=request.user).first()

if membership is None:
return False
return membership.is_ta

if view.action in ["list"]:
# if any member of the course in the list is not accessible, return false
course_ids = request.GET.getlist("course")
for course in course_ids:
membership = Membership.objects.filter(course=course, user=request.user).first()
if membership is None:
return False
return True

return True
15 changes: 14 additions & 1 deletion backend/ohq/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
QueueStatistic,
Semester,
Tag,
Booking,
)
from ohq.sms import sendSMSVerification
from ohq.tasks import sendUpNextNotificationTask
Expand Down Expand Up @@ -592,4 +593,16 @@ class OccurrenceSerializer(serializers.ModelSerializer):

class Meta:
model = Occurrence
fields = ("id", "title", "description", "location", "start", "end", "cancelled", "event")
fields = ("id", "title", "description", "location", "start", "end", "cancelled", "event", "interval")

class BookingSerializer(serializers.ModelSerializer):
"""
Serializer for booking
"""

occurrence = OccurrenceSerializer(read_only=True)

class Meta:
model = Booking
fields = ("id", "occurrence", "user", "start", "end")

2 changes: 2 additions & 0 deletions backend/ohq/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
SemesterViewSet,
TagViewSet,
UserView,
BookingViewSet,
)


Expand All @@ -29,6 +30,7 @@
router.register("courses", CourseViewSet, basename="course")
router.register("events", EventViewSet, basename="event")
router.register("occurrences", OccurrenceViewSet, basename="occurrence")
router.register("bookings", BookingViewSet, basename="booking")

course_router = routers.NestedSimpleRouter(router, "courses", lookup="course")
course_router.register("queues", QueueViewSet, basename="queue")
Expand Down
Loading
Loading