Skip to content

Commit

Permalink
add backend support for dock
Browse files Browse the repository at this point in the history
  • Loading branch information
August Fu committed Mar 1, 2024
1 parent a815007 commit 5918a2f
Show file tree
Hide file tree
Showing 12 changed files with 213 additions and 57 deletions.
40 changes: 0 additions & 40 deletions backend/courses/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -303,46 +303,6 @@ class Meta:
]
read_only_fields = fields

class SimpleCourseSerializer(serializers.ModelSerializer):
id = serializers.ReadOnlyField(
source="full_code",
help_text=dedent(
"""
The full code of the course, in the form '{dept code}-{course code}'
dash-joined department and code of the course, e.g. `CIS-120` for CIS-120."""
),
)

course_quality = serializers.DecimalField(
max_digits=4, decimal_places=3, read_only=True, help_text=course_quality_help
)
difficulty = serializers.DecimalField(
max_digits=4, decimal_places=3, read_only=True, help_text=difficulty_help
)
instructor_quality = serializers.DecimalField(
max_digits=4,
decimal_places=3,
read_only=True,
help_text=instructor_quality_help,
)
work_required = serializers.DecimalField(
max_digits=4, decimal_places=3, read_only=True, help_text=work_required_help
)

class Meta:
model = Course
fields = [
"id",
"title",
"credits",
"semester",
"course_quality",
"instructor_quality",
"difficulty",
"work_required",
]
read_only_fields = fields

class CourseDetailSerializer(CourseListSerializer):
crosslistings = serializers.SlugRelatedField(
slug_field="full_code",
Expand Down
Empty file removed backend/degree/__init__.py
Empty file.
28 changes: 28 additions & 0 deletions backend/degree/migrations/0006_auto_20240229_1903.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Generated by Django 3.2.24 on 2024-03-01 00:03

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


class Migration(migrations.Migration):

dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('degree', '0005_degree_credits'),
]

operations = [
migrations.CreateModel(
name='DockedCourse',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('full_code', models.CharField(blank=True, help_text='The dash-joined department and code of the course, e.g., `CIS-120`', max_length=16)),
('person', models.ForeignKey(help_text='The user the docked course belongs to.', on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
migrations.AddConstraint(
model_name='dockedcourse',
constraint=models.UniqueConstraint(fields=('person', 'full_code'), name='unique docked course'),
),
]
18 changes: 18 additions & 0 deletions backend/degree/migrations/0007_alter_dockedcourse_full_code.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 3.2.24 on 2024-03-01 00:56

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('degree', '0006_auto_20240229_1903'),
]

operations = [
migrations.AlterField(
model_name='dockedcourse',
name='full_code',
field=models.CharField(blank=True, db_index=True, help_text='The dash-joined department and code of the course, e.g., `CIS-120`', max_length=16),
),
]
28 changes: 28 additions & 0 deletions backend/degree/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
program_code_to_name = dict(program_choices)



class Degree(models.Model):
"""
This model represents a degree for a specific year.
Expand Down Expand Up @@ -232,6 +233,7 @@ def get_json_q_object(self) -> dict | None:
return None
return json_parser.parse(self.q)


class DegreePlan(models.Model):
"""
Stores a users plan for an associated degree.
Expand Down Expand Up @@ -498,3 +500,29 @@ def is_double_count_violated(self, degree_plan: DegreePlan) -> bool:
intersection_cus = 0

return self.max_credits and intersection_cus > self.max_credits


class DockedCourse(models.Model):
'''
This represents a course docked by a user.
This is keyed by user but not degree plan, so when a user switches degree plan, the docked courses will not change.
'''
person = models.ForeignKey(
get_user_model(),
on_delete=models.CASCADE,
help_text="The user the docked course belongs to.",
)

full_code = models.CharField(
max_length=16,
blank=True,
db_index=True,
help_text="The dash-joined department and code of the course, e.g., `CIS-120`",
)
class Meta:
constraints = [
models.UniqueConstraint(
fields=["person", "full_code"],
name="unique docked course",
)
]
53 changes: 50 additions & 3 deletions backend/degree/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,54 @@
from rest_framework import serializers

from courses.models import Course
from courses.serializers import CourseListSerializer, CourseDetailSerializer, SimpleCourseSerializer
from degree.models import Degree, DegreePlan, DoubleCountRestriction, Fulfillment, Rule
from courses.serializers import CourseListSerializer, CourseDetailSerializer
from degree.models import Degree, DegreePlan, DoubleCountRestriction, Fulfillment, Rule, DockedCourse
from courses.util import get_current_semester


class DegreeListSerializer(serializers.ModelSerializer):
class Meta:
model = Degree
fields = "__all__"

class SimpleCourseSerializer(serializers.ModelSerializer):
id = serializers.ReadOnlyField(
source="full_code",
help_text=dedent(
"""
The full code of the course, in the form '{dept code}-{course code}'
dash-joined department and code of the course, e.g. `CIS-120` for CIS-120."""
),
)

course_quality = serializers.DecimalField(
max_digits=4, decimal_places=3, read_only=True, help_text='course_quality_help'
)
difficulty = serializers.DecimalField(
max_digits=4, decimal_places=3, read_only=True, help_text='difficulty_help'
)
instructor_quality = serializers.DecimalField(
max_digits=4,
decimal_places=3,
read_only=True,
help_text='instructor_quality_help',
)
work_required = serializers.DecimalField(
max_digits=4, decimal_places=3, read_only=True, help_text='work_required_help'
)

class Meta:
model = Course
fields = [
"id",
"title",
"credits",
"semester",
"course_quality",
"instructor_quality",
"difficulty",
"work_required",
]
read_only_fields = fields

class RuleSerializer(serializers.ModelSerializer):
q_json = serializers.ReadOnlyField(help_text="JSON representation of the q object")
Expand Down Expand Up @@ -132,3 +170,12 @@ class DegreePlanDetailSerializer(serializers.ModelSerializer):
class Meta:
model = DegreePlan
fields = "__all__"


class DockedCourseSerializer(serializers.ModelSerializer):
id = serializers.ReadOnlyField(help_text="The id of the docked course")
person = serializers.HiddenField(default=serializers.CurrentUserDefault())

class Meta:
model = DockedCourse
fields = "__all__"
3 changes: 2 additions & 1 deletion backend/degree/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@
from rest_framework.routers import DefaultRouter
from rest_framework_nested.routers import NestedDefaultRouter

from degree.views import DegreePlanViewset, DegreeViewset, FulfillmentViewSet, courses_for_rule
from degree.views import DegreePlanViewset, DegreeViewset, FulfillmentViewSet, courses_for_rule, DockedCourseViewset


router = DefaultRouter(trailing_slash=False)
router.register(r"degreeplans", DegreePlanViewset, basename="degreeplan")
router.register(r"degrees", DegreeViewset, basename="degree")
router.register(r"docked", DockedCourseViewset)
fulfillments_router = NestedDefaultRouter(router, r"degreeplans", lookup="degreeplan", trailing_slash=False)
fulfillments_router.register(r"fulfillments", FulfillmentViewSet, basename="degreeplan-fulfillment")

Expand Down
55 changes: 52 additions & 3 deletions backend/degree/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,14 @@

from courses.models import Course
from courses.serializers import CourseListSerializer
from degree.models import Degree, DegreePlan, Fulfillment, Rule
from degree.models import Degree, DegreePlan, Fulfillment, Rule, DockedCourse
from degree.serializers import (
DegreeDetailSerializer,
DegreeListSerializer,
DegreePlanDetailSerializer,
DegreePlanListSerializer,
FulfillmentSerializer,
DockedCourseSerializer
)


Expand Down Expand Up @@ -45,7 +46,6 @@ def get_serializer_class(self):
return DegreeListSerializer
return DegreeDetailSerializer


class DegreePlanViewset(AutoPrefetchViewSetMixin, viewsets.ModelViewSet):
"""
List, retrieve, create, destroy, and update a DegreePlan.
Expand Down Expand Up @@ -152,4 +152,53 @@ def create(self, request, *args, **kwargs):
def courses_for_rule(request, rule_id: int):
"""
Search for courses that fulfill a given rule.
"""
"""


class DockedCourseViewset(viewsets.ModelViewSet):
"""
List, retrieve, create, destroy, and update docked courses
"""
permission_classes = [IsAuthenticated]
serializer_class = DockedCourseSerializer
# http_method_names = ["get", "post", "head", "delete"]
queryset = DockedCourse.objects.all()
lookup_field = "full_code"

def get_queryset(self):
queryset = DockedCourse.objects.filter(person=self.request.user)
return queryset

def get_serializer_context(self):
context = super().get_serializer_context()
context.update({"request": self.request}) # used to get the user
return context

# def retrieve(self, request, *args, **kwargs):
# dockedCourse = self.get_object()
# serializer = self.get_serializer(dockedCourse)
# return Response(serializer.data, status=status.HTTP_200_OK)

def create(self, request, *args, **kwargs):
if request.data.get("full_code") is None:
raise ValidationError({ "full_code": "This field is required." })
self.kwargs["full_code"] = request.data["full_code"]
self.kwargs["person"] = self.request.user
try:
return self.partial_update(request, *args, **kwargs)
except Http404:
return super().create(request, *args, **kwargs)

def destroy(self, request, *args, **kwargs):
if kwargs["full_code"] is None:
raise ValidationError({ "full_code": "This field is required." })

instances_to_delete = self.get_queryset().filter(full_code=kwargs["full_code"])

if not instances_to_delete.exists():
raise Http404("No instances matching the provided full_code were found.")

for instance in instances_to_delete:
self.perform_destroy(instance)

return Response(status.HTTP_200_OK)
32 changes: 25 additions & 7 deletions frontend/degree-plan/components/Dock/Dock.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@

import styled from '@emotion/styled';
import { DarkGrayIcon } from '../Requirements/QObject';
import React, { useContext } from "react";
import React, { useContext, useEffect } from "react";
import { useDrag, useDrop } from "react-dnd";
import { Course } from "@/types";
import { Course, IDockedCourse } from "@/types";
import { ItemTypes } from "../dnd/constants";
import DockedCourse from './DockedCourse';
import { SearchPanelContext } from '../Search/SearchPanel';
import { useSWRCrud } from '@/hooks/swrcrud';
import useSWR, { useSWRConfig } from 'swr';


const DockWrapper = styled.div`
Expand Down Expand Up @@ -57,17 +59,33 @@ const DockedCourses = styled.div`
const Dock = () => {
const { setSearchPanelOpen, setSearchRuleQuery, setSearchRuleId } = useContext(SearchPanelContext)
const [dockedCourses, setDockedCourses] = React.useState<string[]>([]);
const { createOrUpdate,remove } = useSWRCrud<IDockedCourse>(`/api/degree/docked`, {idKey: 'full_code'});
const {data: dockedCourseObjs, isLoading} = useSWR<IDockedCourse[]>(`/api/degree/docked`, {fallback: []})

const removeDockedCourse = (full_code: string) => {
const removeDockedCourse = async (full_code: string) => {
/** Preemtively update frontend */
setDockedCourses((dockedCourses) => dockedCourses.filter(c => c !== full_code));
/** Update backend */
await remove(full_code);
}

useEffect(() => {
if (dockedCourseObjs !== undefined) {
setDockedCourses(dockedCourseObjs.map(obj => obj.full_code));
}
}, [dockedCourseObjs])

const [{ isOver, canDrop }, drop] = useDrop(() => ({
accept: ItemTypes.COURSE,
drop: (course: Course) => {
console.log("DROPPED", course.full_code, 'from', course.semester);
setDockedCourses((dockedCourses) => dockedCourses.filter(c => c !== course.full_code)); // to prevent duplicates
setDockedCourses((dockedCourses) => [...dockedCourses, course.full_code]);
const repeated = dockedCourses.filter(c => c === course.full_code)
if (!repeated.length) {
/** Preemtively update frontend */
setDockedCourses((dockedCourses) => [...dockedCourses, course.full_code]);
/** Update backend */
createOrUpdate({"full_code": course.full_code}, course.full_code);
}
},
canDrop: () => {return true},
collect: monitor => ({
Expand All @@ -92,11 +110,11 @@ const Dock = () => {
</DockerElm>
<Divider/>
<DockedCoursesWrapper>
<DockedCourses>
{!isLoading && <DockedCourses>
{dockedCourses.map((full_code, i) =>
<DockedCourse removeDockedCourse={removeDockedCourse} full_code={full_code}/>
)}
</DockedCourses>
</DockedCourses>}
</DockedCoursesWrapper>
</DockContainer>
</DockWrapper>
Expand Down
5 changes: 3 additions & 2 deletions frontend/degree-plan/hooks/swrcrud.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export const baseFetcher = (init: RequestInit, jsonify: boolean = true) => async
body: body === undefined ? undefined : JSON.stringify(body)
});
if (!res.ok) {
console.log('res is not ok', res)
const error = new Error('An error occurred while fetching the data.') as SWRCrudError;
// Attach extra info to the error object.
error.info = await res.json()
Expand Down Expand Up @@ -152,10 +153,10 @@ export const useSWRCrud = <T extends DBObject, idType = Number | string | null>(
return updated;
}

const remove = (id: idType) => {
const remove = async (id: idType) => {
if (!id) return;
const key = normalizeFinalSlash(endpoint) + id;
const removed = removeFetcher(key);
const removed = await removeFetcher(key)
mutate(endpoint, removed, {
optimisticData: (list?: Array<T>) => list ? list.filter((item: T) => String(item[idKey]) !== id) : [],
populateCache: (_, list?: Array<T>) => {
Expand Down
Loading

0 comments on commit 5918a2f

Please sign in to comment.