From e4a7997073dfda9c993b48dadd3a1bbf7e168d7c Mon Sep 17 00:00:00 2001 From: Nicole Lee Date: Wed, 2 Nov 2022 00:04:16 +0900 Subject: [PATCH 1/3] Match code format --- apps/planner/views.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/planner/views.py b/apps/planner/views.py index c83de3477..e10d3725a 100644 --- a/apps/planner/views.py +++ b/apps/planner/views.py @@ -215,6 +215,7 @@ def post(self, request, user_id, planner_id): return HttpResponse() + @method_decorator(login_required_ajax, name="dispatch") class UserInstancePlannerInstancePlannerItemView(View): def patch(self, request, user_id, planner_id, planner_item_id): @@ -281,6 +282,7 @@ def delete(self, request, user_id, planner_id, planner_item_id): plannerItem.delete() return HttpResponse() + @method_decorator(login_required_ajax, name="dispatch") class UserInstancePlannerInstanceApplyHistoryView(View): def patch(self, request, user_id, planner_id): From b51704d0f8aed01c1052dc0da60e37bf821e6516 Mon Sep 17 00:00:00 2001 From: Nicole Lee Date: Wed, 2 Nov 2022 01:47:04 +0900 Subject: [PATCH 2/3] Edit Planner and PlannerItem model schema --- apps/planner/models.py | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/apps/planner/models.py b/apps/planner/models.py index d5b8669c9..b440863f6 100644 --- a/apps/planner/models.py +++ b/apps/planner/models.py @@ -3,11 +3,11 @@ from enum import Enum, auto from typing import Dict, List, Tuple, Union, cast +from django.core.validators import MinValueValidator, MaxValueValidator from django.db import models from apps.session.models import UserProfile - -from apps.subject.models import Department +from apps.subject.models import Department, Course # change into - TODO @@ -168,10 +168,23 @@ def credit_for_track(cls: Credit, user: UserProfile, track: Track) -> Dict[DeptC class Planner(models.Model): - user = models.ForeignKey(UserProfile, on_delete=models.CASCADE, db_index=True) + user = models.ForeignKey(UserProfile, + related_name="planners", on_delete=models.CASCADE, db_index=True) + entrance_year = models.IntegerField(db_index=True) - # TODO add track - # TODO add to_json() function + track = models.ForeignKey(Track, on_delete=models.CASCADE, db_index=True) + arrange_order = models.SmallIntegerField(db_index=True) + + def to_json(self, nested=False): + result = { + "id": self.id, + "arrange_order": self.arrange_order, + } + return result + + @classmethod + def get_related_planners(cls, user, entrance_year, track): + return Planner.objects.filter(user=user, entrance_year=entrance_year, track=track) class PlannerItem(models.Model): From 7285f23320a2b908656ea361dcc92d63b49a0d7a Mon Sep 17 00:00:00 2001 From: Nicole Lee Date: Wed, 2 Nov 2022 01:53:43 +0900 Subject: [PATCH 3/3] Fix Planner and PlannerItem APIs to match new model schema --- apps/planner/services.py | 22 ++++ apps/planner/views.py | 238 +++++++++++++++++++-------------------- otlplus/urls.py | 8 +- 3 files changed, 145 insertions(+), 123 deletions(-) create mode 100644 apps/planner/services.py diff --git a/apps/planner/services.py b/apps/planner/services.py new file mode 100644 index 000000000..572004861 --- /dev/null +++ b/apps/planner/services.py @@ -0,0 +1,22 @@ +from django.db.models import F, Case, When + +from .models import Planner + + +def reorder_planner(planner: Planner, target_arrange_order: int): + related_planners = Planner.get_related_planners(planner.user, + planner.entrance_year, planner.track) + original_arrange_order = planner.arrange_order + + if target_arrange_order < original_arrange_order: + related_planners.filter(arrange_order__gte=target_arrange_order, + arrange_order__lte=original_arrange_order) \ + .update(arrange_order=Case(When(arrange_order=original_arrange_order, + then=target_arrange_order), + default=F("arrange_order")+1)) + elif target_arrange_order > original_arrange_order: + related_planners.filter(arrange_order__gte=original_arrange_order, + arrange_order__lte=target_arrange_order) \ + .update(arrange_order=Case(When(arrange_order=original_arrange_order, + then=target_arrange_order), + default=F("arrange_order")-1)) \ No newline at end of file diff --git a/apps/planner/views.py b/apps/planner/views.py index e10d3725a..bd7b82371 100644 --- a/apps/planner/views.py +++ b/apps/planner/views.py @@ -1,25 +1,24 @@ +from django.db import transaction +from django.db.models import F from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseNotFound, JsonResponse -from django.shortcuts import get_object_or_404 -from django.http import JsonResponse, HttpResponse, HttpResponseNotFound, HttpResponseBadRequest from django.utils.decorators import method_decorator from django.views import View -from django.db import transaction from apps.subject.models import Department from .models import Planner, PlannerItem, Course +from .services import reorder_planner from utils.decorators import login_required_ajax from utils.util import ParseType, parse_params, parse_body, ORDER_DEFAULT_CONFIG, OFFSET_DEFAULT_CONFIG, LIMIT_DEFAULT_CONFIG, apply_offset_and_limit, apply_order, patch_object -from .models import BasicGraduationRequirement, MajorGraduationRequirement, Planner @method_decorator(login_required_ajax, name="dispatch") class UserInstancePlannerListView(View): def get(self, request, user_id): MAX_LIMIT = 50 - DEFAULT_ORDER = ['id'] # TODO: add arrange_order + DEFAULT_ORDER = ['arrange_order', 'id'] PARAMS_STRUCTURE = [ ORDER_DEFAULT_CONFIG, OFFSET_DEFAULT_CONFIG, @@ -40,60 +39,25 @@ def get(self, request, user_id): return JsonResponse(result, safe=False) def post(self, request, user_id): + BODY_STRUCTURE = [ + ("entrance_year", ParseType.INT, True, []), + ("track", ParseType.LIST_STR, True, []), + ] + userprofile = request.user.userprofile if userprofile.id != int(user_id): return HttpResponse(status=401) - # TODO: correct way to get entrance_year? - entrance_year = int(userprofile.student_id[:4]) - planner = Planner.objects.create(user=userprofile, entrance_year=entrance_year) # TODO: add arrange_order - - # TODO: use get_object_or_404 instead of objects.get? - basic_graduation_requirement = BasicGraduationRequirement.objects.get(entrance_year=entrance_year) - planner.basic_graduation_requirement.add(basic_graduation_requirement) + entrance_year, track, = parse_body(request.body, BODY_STRUCTURE) - # TODO: correct way to get majors? - for i, major in enumerate(userprofile.majors): - try: - if i == 0: - major_graduation_requirement = MajorGraduationRequirement.objects.get( - entrance_year=entrance_year, - department=major, - major_type=MajorGraduationRequirement.MajorType.MAJOR - ) - else: - major_graduation_requirement = MajorGraduationRequirement.objects.get( - entrance_year=entrance_year, - department=major, - major_type=MajorGraduationRequirement.MajorType.DOUBLE_MAJOR - ) - except MajorGraduationRequirement.DoesNotExist: - return HttpResponseBadRequest("Invalid MajorGraduationRequirement information in request data") - planner.major_graduation_requirements.add(major_graduation_requirement) + related_planners = Planner.get_related_planners(userprofile, entrance_year, track) + if related_planners.exists(): + arrange_order = related_planners.order_by("arrange_order").last().arrange_order + 1 + else: + arrange_order = 0 - for i, major in enumerate(userprofile.minors): - try: - major_graduation_requirement = MajorGraduationRequirement.objects.get( - entrance_year=entrance_year, - department=major, - major_type=MajorGraduationRequirement.MajorType.MINOR - ) - except MajorGraduationRequirement.DoesNotExist: - return HttpResponseBadRequest("Invalid MajorGraduationRequirement information in request data") - planner.major_graduation_requirements.add(major_graduation_requirement) - - for i, major in enumerate(userprofile.specialized_major): - try: - major_graduation_requirement = MajorGraduationRequirement.objects.get( - entrance_year=entrance_year, - department=major, - major_type=MajorGraduationRequirement.MajorType.SPECIALIZED_MAJOR - ) - except MajorGraduationRequirement.DoesNotExist: - return HttpResponseBadRequest("Invalid MajorGraduationRequirement information in request data") - planner.major_graduation_requirements.add(major_graduation_requirement) - - # TODO: major_graduation_requirements for self designed major? + planner = Planner.objects.create(user=userprofile, entrance_year=entrance_year, track=track, + arrange_order=arrange_order) return JsonResponse(planner.to_json()) @@ -114,51 +78,29 @@ def get(self, request, user_id, planner_id): def patch(self, request, user_id, planner_id): BODY_STRUCTURE = [ - ("entrance_year", ParseType.INT, True, [lambda entrance_year: 1900 <= entrance_year <= 9999]), # TODO: change range to entrance_year ~ current year - # TODO: add correct validators (list of existing majors, major types) - ("major", ParseType.STR, True, []), - ("additional_majors", ParseType.LIST_STR, True, []), - ("additional_major_types", ParseType.LIST_STR, True, []), + ("entrance_year", ParseType.INT, True, []), + ("track", ParseType.LIST_STR, True, []), ] - entrance_year, major, additional_majors, additional_major_types = parse_body(request.body, BODY_STRUCTURE) + entrance_year, track, = parse_body(request.body, BODY_STRUCTURE) userprofile = request.user.userprofile if userprofile.id != int(user_id): return HttpResponse(status=401) - planner = get_object_or_404(Planner, id=planner_id) - - # TODO: use get_object_or_404 instead of objects.get? - basic_graduation_requirement = BasicGraduationRequirement.objects.get(entrance_year=entrance_year) - major_graduation_requirements = [ - MajorGraduationRequirement.objects.get( - entrance_year=entrance_year, - department=major, - major_type=MajorGraduationRequirement.MajorType.MAJOR - ), - ] - - for i, major in enumerate(additional_majors): - try: - major_graduation_requirement = MajorGraduationRequirement.objects.get( - entrance_year=entrance_year, - department=major, - major_type=additional_major_types[i] - ) - except MajorGraduationRequirement.DoesNotExist: - return HttpResponseBadRequest("Invalid MajorGraduationRequirement information in request data") - major_graduation_requirements.add(major_graduation_requirement) - + try: + planner = userprofile.planners.get(id=planner_id) + except Planner.DoesNotExist: + return HttpResponseNotFound() patch_object( planner, { "entrance_year": entrance_year, - "basic_graduation_requirement": basic_graduation_requirement, - "major_graduation_requirements": major_graduation_requirements, + "track": track, }, ) + return JsonResponse(planner.to_json(user=request.user), safe=False) def delete(self, request, user_id, planner_id): @@ -172,52 +114,36 @@ def delete(self, request, user_id, planner_id): return HttpResponseNotFound() planner.delete() - - # TODO: add arrange_order + related_planners = Planner.get_related_planners(userprofile, + planner.entrance_year, planner.track) + related_planners.filter(arrange_order__gt=planner.arrange_order) \ + .update(arrange_order=F('arrange_order')-1) return HttpResponse() @method_decorator(login_required_ajax, name="dispatch") -class UserInstancePlannerInstanceAddPlannerItemView(View): - def post(self, request, user_id, planner_id): - BODY_STRUCTURE = [ - ("year", ParseType.INT, True, []), - ("semester", ParseType.INT, True, []), - ("is_generic", ParseType.STR, True, [lambda is_generic: is_generic == "true" or is_generic == "false"]), - ("course_id", ParseType.STR, False, []), - ("course_type", ParseType.STR, False, []), - ("department_id", ParseType.STR, False, []), - ("credit", ParseType.INT, False, []), - ] - +class UserInstancePlannerInstancePlannerItemView(View): + def get(self, request, user_id, planner_id, planner_item_id): userprofile = request.user.userprofile if userprofile.id != int(user_id): return HttpResponse(status=401) try: - Planner.objects.get(id=planner_id) + _ = userprofile.planners.get(id=planner_id) except Planner.DoesNotExist: return HttpResponseNotFound() - year, semester, is_generic, course_id, course_type, department_id, credit = parse_body(request.body, BODY_STRUCTURE) - if (is_generic == "false" and course_id == None) or (is_generic == "true" and (course_type == None or credit == None)): - return HttpResponseBadRequest() - if is_generic == "true": - try: - Course.objects.get(id=course_id) - Department.objects.get(id=department_id) - except (Course.DoesNotExist, Department.DoesNotExist): - return HttpResponseBadRequest() - - plannerItem = PlannerItem(planner_id, year, semester, is_generic == "true", course_id, course_type, department_id, credit) - plannerItem.save() + try: + plannerItem = PlannerItem.objects.get(id=planner_item_id) + except PlannerItem.DoesNotExist: + return HttpResponseNotFound() - return HttpResponse() + if not plannerItem.planner == Planner.objects.get(planner_id): + return HttpResponseBadRequest() + return JsonResponse(plannerItem.to_json()) -@method_decorator(login_required_ajax, name="dispatch") -class UserInstancePlannerInstancePlannerItemView(View): def patch(self, request, user_id, planner_id, planner_item_id): BODY_STRUCTURE = [ ("year", ParseType.INT, True, []), @@ -234,12 +160,19 @@ def patch(self, request, user_id, planner_id, planner_item_id): return HttpResponse(status=401) try: - Planner.objects.get(id=planner_id) - plannerItem = PlannerItem.objects.get(id=planner_item_id) + _ = userprofile.planners.get(id=planner_id) except Planner.DoesNotExist: return HttpResponseNotFound() - year, semester, is_generic, course_id, course_type, department_id, credit = parse_body(request.body, BODY_STRUCTURE) + try: + plannerItem = PlannerItem.objects.get(id=planner_item_id) + except PlannerItem.DoesNotExist: + return HttpResponseNotFound() + + if not plannerItem.planner == Planner.objects.get(planner_id): + return HttpResponseBadRequest() + + year, semester, is_generic, course_id, course_type, department_id, credit, = parse_body(request.body, BODY_STRUCTURE) if (is_generic == "false" and course_id == None) or (is_generic == "true" and (course_type == None or credit == None)): return HttpResponseBadRequest() if is_generic == "true": @@ -270,7 +203,7 @@ def delete(self, request, user_id, planner_id, planner_item_id): return HttpResponse(status=401) try: - _ = Planner.objects.get(id=planner_id) + _ = userprofile.planners.get(id=planner_id) except Planner.DoesNotExist: return HttpResponseNotFound() @@ -279,10 +212,53 @@ def delete(self, request, user_id, planner_id, planner_item_id): except PlannerItem.DoesNotExist: return HttpResponseNotFound() + if not plannerItem.planner == Planner.objects.get(planner_id): + return HttpResponseBadRequest() + plannerItem.delete() + return HttpResponse() +@method_decorator(login_required_ajax, name="dispatch") +class UserInstancePlannerInstanceAddPlannerItemView(View): + def post(self, request, user_id, planner_id): + BODY_STRUCTURE = [ + ("year", ParseType.INT, True, []), + ("semester", ParseType.INT, True, []), + ("is_generic", ParseType.STR, True, [lambda is_generic: is_generic == "true" or is_generic == "false"]), + ("course_id", ParseType.STR, False, []), + ("course_type", ParseType.STR, False, []), + ("department_id", ParseType.STR, False, []), + ("credit", ParseType.INT, False, []), + ] + + userprofile = request.user.userprofile + if userprofile.id != int(user_id): + return HttpResponse(status=401) + + try: + _ = userprofile.planners.get(id=planner_id) + except Planner.DoesNotExist: + return HttpResponseNotFound() + + year, semester, is_generic, course_id, course_type, department_id, credit, = parse_body(request.body, BODY_STRUCTURE) + if (is_generic == "false" and course_id == None) or (is_generic == "true" and (course_type == None or credit == None)): + return HttpResponseBadRequest() + if is_generic == "true": + try: + Course.objects.get(id=course_id) + Department.objects.get(id=department_id) + except (Course.DoesNotExist, Department.DoesNotExist): + return HttpResponseBadRequest() + + plannerItem = PlannerItem(planner_id, year, semester, is_generic == "true", course_id, course_type, department_id, credit) + plannerItem.save() + + return HttpResponse() + + +# TODO: check UserInstancePlannerInstanceApplyHistoryView class @method_decorator(login_required_ajax, name="dispatch") class UserInstancePlannerInstanceApplyHistoryView(View): def patch(self, request, user_id, planner_id): @@ -291,11 +267,11 @@ def patch(self, request, user_id, planner_id): return HttpResponse(status=401) try: - Planner.objects.get(id=planner_id) + _ = userprofile.planners.get(id=planner_id) except Planner.DoesNotExist: return HttpResponseNotFound() - takenLectures = userprofile.taken_lectures # ref: Lecture schema + takenLectures = userprofile.taken_lectures lastYear, lastSemester = 0, 0 plannerItems = [] @@ -316,3 +292,25 @@ def patch(self, request, user_id, planner_id): return HttpResponse(status=500) return HttpResponse(status=200) + + +@method_decorator(login_required_ajax, name="dispatch") +class UserInstancePlannerInstanceReorderView(View): + def post(self, request, user_id, planner_id): + BODY_STRUCTURE = [ + ("arrange_order", ParseType.INT, True, []), + ] + + userprofile = request.user.userprofile + if userprofile.id != int(user_id): + return HttpResponse(status=401) + + try: + planner = Planner.objects.get(id=planner_id) + except Planner.DoesNotExist: + return HttpResponseNotFound() + + arrange_order, = parse_body(request.body, BODY_STRUCTURE) + + reorder_planner(planner, arrange_order) + return JsonResponse(planner.to_json()) \ No newline at end of file diff --git a/otlplus/urls.py b/otlplus/urls.py index 69a979c4e..d4c91786a 100644 --- a/otlplus/urls.py +++ b/otlplus/urls.py @@ -97,12 +97,14 @@ planner_views.UserInstancePlannerListView.as_view()), url(r"^api/users/(?P\d+)/planners/(?P\d+)$", planner_views.UserInstancePlannerInstanceView.as_view()), - url(r"^api/users/(?P\d+)/planners/(?P\d+)$", - planner_views.UserInstancePlannerInstanceAddPlannerItemView.as_view()), - url(r"^api/users/(?P\d+)/planners/(?P\d+)/planner-item/(?P\d+)$", + url(r"^api/users/(?P\d+)/planners/(?P\d+)/planner-item/(?P\d+)$", planner_views.UserInstancePlannerInstancePlannerItemView.as_view()), + url(r"^api/users/(?P\d+)/planners/(?P\d+)/add-planner-item$", + planner_views.UserInstancePlannerInstanceAddPlannerItemView.as_view()), url(r"^api/users/(?P\d+)/planners/(?P\d+)/update-history$", planner_views.UserInstancePlannerInstanceApplyHistoryView.as_view()), + url(r"^api/users/(?P\d+)/planners/(?P\d+)/reorder", + planner_views.UserInstancePlannerInstanceReorderView.as_view()), url(r"^api/users/(?P\d+)/wishlist$", timetable_views.UserInstanceWishlistView.as_view()),