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

Break CRUD routes #703

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 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
23 changes: 23 additions & 0 deletions backend/courses/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -1126,10 +1126,20 @@ class Meeting(models.Model):

section = models.ForeignKey(
Section,
null=True,
on_delete=models.CASCADE,
related_name="meetings",
help_text="The Section object to which this class meeting belongs.",
)

associated_break = models.ForeignKey(
"plan.Break",
null=True,
on_delete=models.CASCADE,
related_name="meetings",
help_text="The Section object to which this class meeting belongs.",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Meeting object"

)

day = models.CharField(
max_length=1,
help_text="The single day on which the meeting takes place (one of M, T, W, R, or F).",
Expand Down Expand Up @@ -1180,6 +1190,19 @@ class Meeting(models.Model):
),
)

def clean(self):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider using Django's built in CONSTRAINT libraries. It is probably more natural to SQL
https://docs.djangoproject.com/en/5.1/ref/models/constraints/

super().clean()
if (self.section is None and self.associated_break is None) or (
self.section is not None and self.associated_break is not None
):
raise ValidationError(
"Either the section field of associated_break field must be populated, but not both"
)

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

class Meta:
unique_together = (("section", "day", "start", "end", "room"),)

Expand Down
11 changes: 7 additions & 4 deletions backend/courses/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
StatusUpdate,
User,
)
from plan.models import Break
from review.management.commands.mergeinstructors import resolve_duplicates


Expand Down Expand Up @@ -465,17 +466,17 @@ def clean_meetings(meetings):
}.values()


def set_meetings(section, meetings):
def set_meetings(obj, meetings):
meetings = clean_meetings(meetings)

for meeting in meetings:
meeting["days"] = "".join(sorted(list(set(meeting["days"]))))
meeting_times = [
f"{meeting['days']} {meeting['begin_time']} - {meeting['end_time']}" for meeting in meetings
]
section.meeting_times = json.dumps(meeting_times)
obj.meeting_times = json.dumps(meeting_times)

section.meetings.all().delete()
obj.meetings.all().delete()
for meeting in meetings:
online = (
not meeting["building_code"]
Expand All @@ -492,8 +493,10 @@ def set_meetings(section, meetings):
start_date = extract_date(meeting.get("start_date"))
end_date = extract_date(meeting.get("end_date"))
for day in list(meeting["days"]):

meeting = Meeting.objects.update_or_create(
section=section,
section=obj if isinstance(obj, Section) else None,
associated_break=obj if isinstance(obj, Break) else None,
day=day,
start=start_time,
end=end_time,
Expand Down
56 changes: 56 additions & 0 deletions backend/plan/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,3 +90,59 @@ class Meta:

def __str__(self):
return f"PrimarySchedule(User: {self.user}, Schedule ID: {self.schedule_id})"


class Break(models.Model):
"""
Holds break objects created by users on PCP.
"""

person = models.ForeignKey(
get_user_model(),
on_delete=models.CASCADE,
help_text="The person (user) who created this break.",
)

location_string = models.CharField(
max_length=80,
null=True,
help_text=dedent(
"""
This represents the location that the user can input themselves.
Will use a building object from a drop down or have it validated
or something so it can interact with map.
Didn't want to run into issue of users creating arbitrary
Room objects, so just using a char field
"""
), # TODO: Don't know how I want to do buildings yet.
)

name = models.CharField(
max_length=255,
help_text=dedent(
"""
The user's name for the break. No two breaks can match in all of the fields
`[name, person]`
"""
),
)

meeting_times = models.TextField(
blank=True,
help_text=dedent(
"""
A JSON-stringified list of meeting times of the form
`{days code} {start time} - {end time}`, e.g.
`["MWF 09:00 AM - 10:00 AM","F 11:00 AM - 12:00 PM","T 05:00 PM - 06:00 PM"]` for
PHYS-151-001 (2020A). Each letter of the days code is of the form M, T, W, R, F for each
day of the work week, respectively (and multiple days are combined with concatenation).
To access the Meeting objects for this section, the related field `meetings` can be used.
"""
),
)

class Meta:
unique_together = (("person"),)

def __str__(self):
return "User: %s, Break ID: %s" % (self.person, self.id)
40 changes: 38 additions & 2 deletions backend/plan/serializers.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
from textwrap import dedent

from rest_framework import serializers

from courses.serializers import PublicUserSerializer, SectionDetailSerializer
from plan.models import PrimarySchedule, Schedule
from courses.serializers import (
MeetingSerializer,
MeetingWithBuildingSerializer,
PublicUserSerializer,
SectionDetailSerializer,
)
from plan.models import Break, PrimarySchedule, Schedule


class ScheduleSerializer(serializers.ModelSerializer):
Expand Down Expand Up @@ -34,3 +41,32 @@ class PrimaryScheduleSerializer(serializers.ModelSerializer):
class Meta:
model = PrimarySchedule
fields = ["user", "schedule"]


class BreakSerializer(serializers.ModelSerializer):

meetings = serializers.SerializerMethodField(
read_only=True,
help_text=dedent(
"""
A list of the meetings of this section (each meeting is a continuous span of time
during which a section would meet).
"""
),
)
id = serializers.IntegerField(
read_only=False, required=False, help_text="The id of the schedule."
)

class Meta:
model = Break
exclude = ["person"]

def get_meetings(self, obj):
include_location = self.context.get("include_location", False)
if include_location:
meetings_serializer = MeetingWithBuildingSerializer(obj.meetings, many=True)
else:
meetings_serializer = MeetingSerializer(obj.meetings, many=True)

return meetings_serializer.data
2 changes: 2 additions & 0 deletions backend/plan/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from rest_framework_nested import routers

from plan.views import (
BreakViewSet,
CalendarAPIView,
PrimaryScheduleViewSet,
ScheduleViewSet,
Expand All @@ -13,6 +14,7 @@
router = routers.DefaultRouter()
router.register(r"schedules", ScheduleViewSet, basename="schedules")
router.register(r"primary-schedules", PrimaryScheduleViewSet, basename="primary-schedules")
router.register(r"breaks", BreakViewSet, basename="breaks")

urlpatterns = [
path("<int:schedule_pk>/calendar/", CalendarAPIView.as_view(), name="calendar-view"),
Expand Down
123 changes: 120 additions & 3 deletions backend/plan/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,12 @@

from courses.models import Course, Meeting, Section
from courses.serializers import CourseListSerializer
from courses.util import get_course_and_section, get_current_semester, normalize_semester
from courses.util import (
get_course_and_section,
get_current_semester,
normalize_semester,
set_meetings,
)
from courses.views import get_accepted_friends
from PennCourses.docs_settings import PcxAutoSchema
from PennCourses.settings.base import PATH_REGISTRATION_SCHEDULE_NAME
Expand All @@ -31,8 +36,8 @@
vectorize_user,
vectorize_user_by_courses,
)
from plan.models import PrimarySchedule, Schedule
from plan.serializers import PrimaryScheduleSerializer, ScheduleSerializer
from plan.models import Break, PrimarySchedule, Schedule
from plan.serializers import BreakSerializer, PrimaryScheduleSerializer, ScheduleSerializer


@api_view(["POST"])
Expand Down Expand Up @@ -618,6 +623,118 @@ def get_queryset(self, semester=None):
return queryset


class BreakViewSet(AutoPrefetchViewSetMixin, viewsets.ModelViewSet):

serializer_class = BreakSerializer
permission_classes = [IsAuthenticated]

def get_serializer_context(self):
context = super().get_serializer_context()
include_location_str = "False"
# TODO: figure out how we want to do locations.
context.update({"include_location": eval(include_location_str)})
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lets try not to use eval, if locations are not considered in this v1 version, we can comment out?

https://www.udacity.com/blog/2023/03/pythons-eval-the-most-powerful-function-you-should-never-use.html

return context

def update(self, request):
break_id = request.data.get("id")
if not break_id:
return Response(
{"detail": "Break id is required for update."}, status=status.HTTP_400_BAD_REQUEST
)

try:
current_break = self.get_queryset().get(id=break_id)
except Break.DoesNotExist:
return Response({"detail": "Break not found."}, status=status.HTTP_404_NOT_FOUND)
except Exception as e:
return Response(
{"detail": "Error retrieving break: " + str(e)}, status=status.HTTP_400_BAD_REQUEST
)

name = request.data.get("name")
if not name:
return Response(
{"detail": "Break name is required."}, status=status.HTTP_400_BAD_REQUEST
)
location_string = request.data.get("location_string")

current_break.name = name
current_break.location_string = location_string

meetings = request.data.get("meetings")
try:
set_meetings(current_break, meetings)
except Exception as e:
return Response(
{"detail": "Error setting meetings: " + str(e)}, status=status.HTTP_400_BAD_REQUEST
)

try:
current_break.save()
except Exception as e:
return Response(
{"detail": "Error saving break: " + str(e)}, status=status.HTTP_400_BAD_REQUEST
)

return Response({"message": "success", "id": current_break.id}, status=status.HTTP_200_OK)

def create(self, request, *args, **kwargs):
break_id = request.data.get("id")
if break_id and Break.objects.filter(id=break_id).exists():
return self.update(request)

name = request.data.get("name")
if not name:
return Response(
{"detail": "Break name is required."}, status=status.HTTP_400_BAD_REQUEST
)
location_string = request.data.get("location_string")

try:
if break_id:
new_break = self.get_queryset().create(
person=request.user,
name=name,
location_string=location_string,
id=break_id,
)
else:
new_break = self.get_queryset().create(
person=request.user,
name=name,
location_string=location_string,
)
except IntegrityError as e:
return Response(
{
"detail": "IntegrityError encountered while trying to create: "
+ str(e.__cause__)
},
status=status.HTTP_400_BAD_REQUEST,
)
except Exception as e:
return Response(
{"detail": "Error creating break: " + str(e)},
status=status.HTTP_400_BAD_REQUEST,
)

meetings = request.data.get("meetings")
try:
set_meetings(new_break, meetings)
except Exception as e:
return Response(
{"detail": "Error setting meetings: " + str(e)}, status=status.HTTP_400_BAD_REQUEST
)

return Response({"message": "success", "id": new_break.id}, status=status.HTTP_201_CREATED)

def get_queryset(self):
return Break.objects.filter(person=self.request.user).prefetch_related(
"meetings", # Prefetch the related meetings
"meetings__room", # Prefetch the related rooms for each meeting
)


class CalendarAPIView(APIView):
schema = PcxAutoSchema(
custom_path_parameter_desc={
Expand Down
Loading
Loading