class SectionDetail(generics.RetrieveAPIView, BaseCourseMixin): """ Retrieve a detailed look at a specific course section. """ schema = PcxAutoSchema( response_codes={ reverse_func("sections-detail", args=["semester", "full_code"]): { "GET": { 200: "[DESCRIBE_RESPONSE_SCHEMA]Section detail retrieved successfully." } } }, custom_path_parameter_desc={ reverse_func("sections-detail", args=["semester", "full_code"]): { "GET": { "semester": SEMESTER_PARAM_DESCRIPTION } } }, ) serializer_class = SectionDetailSerializer queryset = Section.with_reviews.all() lookup_field = "full_code" def get_semester_field(self): return "course__semester"
class StatusUpdateView(generics.ListAPIView): """ Retrieve all Status Update objects from the current semester for a specific section. """ schema = PcxAutoSchema( response_codes={ reverse_func("statusupdate", args=["full_code"]): { "GET": { 200: "[DESCRIBE_RESPONSE_SCHEMA]Status Updates for section listed successfully." } } }, custom_path_parameter_desc={ reverse_func("statusupdate", args=["full_code"]): { "GET": { "full_code": ("The code of the section which this status update applies to, in the " "form '{dept code}-{course code}-{section code}', e.g. `CIS-120-001` for " "the 001 section of CIS-120.") } } }, ) serializer_class = StatusUpdateSerializer http_method_names = ["get"] lookup_field = "section__full_code" def get_queryset(self): return StatusUpdate.objects.filter( section__full_code=self.kwargs["full_code"], section__course__semester=get_current_semester(), in_add_drop_period=True, ).order_by("created_at")
class SectionList(generics.ListAPIView, BaseCourseMixin): """ Retrieve a list of sections (less detailed than [PCx] Section, or SectionDetail on the backend). The sections are filtered by the search term (assumed to be a prefix of a section's full code, with each chunk either space-delimited, dash-delimited, or not delimited). """ schema = PcxAutoSchema( response_codes={ reverse_func("section-search", args=["semester"]): { "GET": { 200: "[DESCRIBE_RESPONSE_SCHEMA]Sections Listed Successfully." } } }, custom_path_parameter_desc={ reverse_func("section-search", args=["semester"]): { "GET": { "semester": SEMESTER_PARAM_DESCRIPTION } } }, ) serializer_class = MiniSectionSerializer queryset = Section.with_reviews.all().exclude(activity="") filter_backends = [TypedSectionSearchBackend] search_fields = ["^full_code"] @staticmethod def get_semester_field(): return "course__semester"
class PreNGSSRequirementList(generics.ListAPIView, BaseCourseMixin): """ Retrieve a list of all pre-NGSS (deprecated since 2022C) academic requirements in the database for this semester. """ schema = PcxAutoSchema( response_codes={ reverse_func("requirements-list", args=["semester"]): { "GET": { 200: "[DESCRIBE_RESPONSE_SCHEMA]Requirements listed successfully." } }, }, custom_path_parameter_desc={ reverse_func("requirements-list", args=["semester"]): { "GET": { "semester": ("The semester of the requirement (of the form YYYYx where x is A " "[for spring], B [summer], or C [fall]), e.g. `2019C` for fall 2019. " "We organize requirements by semester so that we don't get huge related " "sets which don't give particularly good info.") } } }, ) serializer_class = PreNGSSRequirementListSerializer queryset = PreNGSSRequirement.objects.all()
class CourseDetail(generics.RetrieveAPIView, BaseCourseMixin): """ Retrieve a detailed look at a specific course. Includes all details necessary to display course info, including requirements this class fulfills, and all sections. """ schema = PcxAutoSchema( response_codes={ reverse_func("courses-detail", args=["semester", "full_code"]): { "GET": { 200: "[DESCRIBE_RESPONSE_SCHEMA]Courses detail retrieved successfully." } } }, custom_path_parameter_desc={ reverse_func("courses-detail", args=["semester", "full_code"]): { "GET": { "semester": SEMESTER_PARAM_DESCRIPTION } } }, ) serializer_class = CourseDetailSerializer lookup_field = "full_code" queryset = Course.with_reviews.all() # included redundantly for docs def get_queryset(self): queryset = Course.with_reviews.all() queryset = queryset.prefetch_related( Prefetch( "sections", Section.with_reviews.all().filter( credits__isnull=False).filter( Q(status="O") | Q(status="C")).distinct().prefetch_related( "course", "meetings", "associated_sections", "meetings__room", "instructors"), )) queryset = self.filter_by_semester(queryset) return queryset
class CourseList(generics.ListAPIView, BaseCourseMixin): """ Retrieve a list of (all) courses for the provided semester. """ schema = PcxAutoSchema( response_codes={ reverse_func("courses-list", args=["semester"]): { "GET": { 200: "[DESCRIBE_RESPONSE_SCHEMA]Courses listed successfully." } } }, custom_path_parameter_desc={ reverse_func("courses-list", args=["semester"]): { "GET": { "semester": SEMESTER_PARAM_DESCRIPTION } } }, ) serializer_class = CourseListSerializer queryset = Course.with_reviews.filter( sections__isnull=False) # included redundantly for docs def get_queryset(self): queryset = Course.with_reviews.filter(sections__isnull=False) queryset = queryset.prefetch_related( Prefetch( "sections", Section.with_reviews.all().filter( credits__isnull=False).filter( Q(status="O") | Q(status="C")).distinct().prefetch_related( "course", "meetings__room"), )) queryset = self.filter_by_semester(queryset) return queryset
class NGSSRestrictionList(generics.ListAPIView): """ Retrieve a list of unique restrictions (introduced post-NGSS) """ schema = PcxAutoSchema(response_codes={ reverse_func("restrictions-list"): { "GET": { 200: "[DESCRIBE_RESPONSE_SCHEMA]Restrictions listed successfully." } }, }, ) serializer_class = NGSSRestrictionListSerializer queryset = NGSSRestriction.objects.all()
class CourseListSearch(CourseList): """ This route allows you to list courses by certain search terms and/or filters. Without any GET parameters, this route simply returns all courses for a given semester. There are a few filter query parameters which constitute ranges of floating-point numbers. The values for these are <min>-<max> , with minimum excluded. For example, looking for classes in the range of 0-2.5 in difficulty, you would add the parameter difficulty=0-2.5. If you are a backend developer, you can find these filters in backend/plan/filters.py/CourseSearchFilterBackend. If you are reading the frontend docs, these filters are listed below in the query parameters list (with description starting with "Filter"). """ schema = PcxAutoSchema( response_codes={ reverse_func("courses-search", args=["semester"]): { "GET": { 200: "[DESCRIBE_RESPONSE_SCHEMA]Courses listed successfully.", 400: "Bad request (invalid query).", } } }, custom_path_parameter_desc={ reverse_func("courses-search", args=["semester"]): { "GET": { "semester": SEMESTER_PARAM_DESCRIPTION } } }, ) def get_serializer_context(self): """ This method overrides the default `get_serializer_context` (from super class) in order to add the `user_vector` and `curr_course_vectors_dict` key/value pairs to the serializer context dictionary. If there is no authenticated user (ie `self.request.user.is_authenticated` is `False`) or `self.request` is `None`, the value associated with the `user_vector` and `curr_course_vectors_dict`key are not set. All other key/value pairs that would have been returned by the default `get_serializer_context` (which is `CourseList.get_serializer_context`) are in the dictionary returned in this method. `user_vector` and `curr_course_vectors_dict` encode the vectors used to calculate the recommendation score for a course for a user (see `backend/plan/management/commands/recommendcourses.py` for details on the vectors). Note that for testing purposes, this implementation of get_serializer_context is replaced with simply `CourseList.get_serializer_context` to reduce the costly process of training the model in unrelated tests. You can see how this is done and how to override that behavior in in `backend/tests/__init__.py`. """ context = super().get_serializer_context() if self.request is None or not self.request.user or not self.request.user.is_authenticated: return context _, _, curr_course_vectors_dict, past_course_vectors_dict = retrieve_course_clusters( ) user_vector, _ = vectorize_user(self.request.user, curr_course_vectors_dict, past_course_vectors_dict) context.update({ "user_vector": user_vector, "curr_course_vectors_dict": curr_course_vectors_dict }) return context filter_backends = [TypedCourseSearchBackend, CourseSearchFilterBackend] search_fields = ("full_code", "title", "sections__instructors__name")
class ScheduleViewSet(AutoPrefetchViewSetMixin, viewsets.ModelViewSet): """ list: Get a list of all the logged-in user's schedules for the current semester. Normally, the response code is 200. Each object in the returned list is of the same form as the object returned by Retrieve Schedule. retrieve: Get one of the logged-in user's schedules for the current semester, using the schedule's ID. If a schedule with the specified ID exists, a 200 response code is returned, along with the schedule object. If the given id does not exist, a 404 is returned. create: Use this route to create a schedule for the authenticated user. This route will return a 201 if it succeeds (or a 200 if the POST specifies an id which already is associated with a schedule, causing that schedule to be updated), with a JSON in the same format as if you were to get the schedule you just posted (the 200 response schema for Retrieve Schedule). At a minimum, you must include the `name` and `sections` list (`meetings` can be substituted for `sections`; if you don't know why, ignore this and just use `sections`, or see below for an explanation... TLDR: it is grandfathered in from the old version of PCP). The `name` is the name of the schedule (all names must be distinct for a single user in a single semester; otherwise the response will be a 400). The sections list must be a list of objects with minimum fields `id` (dash-separated, e.g. `CIS-121-001`) and `semester` (5 character string, e.g. `2020A`). If any of the sections are invalid, a 404 is returned with data `{"detail": "One or more sections not found in database."}`. If any two sections in the `sections` list have differing semesters, a 400 is returned. Optionally, you can also include a `semester` field (5 character string, e.g. `2020A`) in the posted object, which will set the academic semester which the schedule is planning. If the `semester` field is omitted, the semester of the first section in the `sections` list will be used (or if the `sections` list is empty, the current semester will be used). If the schedule's semester differs from any of the semesters of the sections in the `sections` list, a 400 will be returned. Optionally, you can also include an `id` field (an integer) in the posted object; if you include it, it will update the schedule with the given id (if such a schedule exists), or if the schedule does not exist, it will create a new schedule with that id. Note that your posted object can include either a `sections` field or a `meetings` field to list all sections you would like to be in the schedule (mentioned above). If both fields exist in the object, only `meetings` will be considered. In all cases, the field in question will be renamed to `sections`, so that will be the field name whenever you GET from the server. (Sorry for this confusing behavior, it is grandfathered in from when the PCP frontend was referring to sections as meetings, before schedules were stored on the backend.) update: Send a put request to this route to update a specific schedule. The `id` path parameter (an integer) specifies which schedule you want to update. If a schedule with the specified id does not exist, a 404 is returned. In the body of the PUT, use the same format as a POST request (see the create schedule docs). This is an alternate way to update schedules (you can also just include the id field in a schedule when you post and it will update that schedule if the id exists). Note that in a put request the id field in the putted object is ignored; the id taken from the route always takes precedence. If the request succeeds, it will return a 200 and a JSON in the same format as if you were to get the schedule you just updated (in the same format as returned by the GET /schedules/ route). delete: Send a delete request to this route to delete a specific schedule. The `id` path parameter (an integer) specifies which schedule you want to update. If a schedule with the specified id does not exist, a 404 is returned. If the delete is successful, a 204 is returned. """ schema = PcxAutoSchema( response_codes={ reverse_func("schedules-list"): { "GET": { 200: "[DESCRIBE_RESPONSE_SCHEMA]Schedules listed successfully.", }, "POST": { 201: "Schedule successfully created.", 200: "Schedule successfully updated (a schedule with the " "specified id already existed).", 400: "Bad request (see description above).", }, }, reverse_func("schedules-detail", args=["id"]): { "GET": { 200: "[DESCRIBE_RESPONSE_SCHEMA]Successful retrieve " "(the specified schedule exists).", 404: "No schedule with the specified id exists.", }, "PUT": { 200: "Successful update (the specified schedule was found and updated).", 400: "Bad request (see description above).", 404: "No schedule with the specified id exists.", }, "DELETE": { 204: "Successful delete (the specified schedule was found and deleted).", 404: "No schedule with the specified id exists.", }, }, }, ) serializer_class = ScheduleSerializer http_method_names = ["get", "post", "delete", "put"] permission_classes = [IsAuthenticated] @staticmethod def get_sections(data): raw_sections = [] if "meetings" in data: raw_sections = data.get("meetings") elif "sections" in data: raw_sections = data.get("sections") sections = [] for s in raw_sections: _, section = get_course_and_section(s.get("id"), s.get("semester")) sections.append(section) return sections @staticmethod def check_semester(data, sections): for i, s in enumerate(sections): if i == 0 and "semester" not in data: data["semester"] = s.course.semester elif s.course.semester != data.get("semester"): return Response( {"detail": "Semester uniformity invariant violated."}, status=status.HTTP_400_BAD_REQUEST, ) def update(self, request, pk=None): if not Schedule.objects.filter(id=pk).exists(): return Response({"detail": "Not found."}, status=status.HTTP_404_NOT_FOUND) try: schedule = self.get_queryset().get(id=pk) except Schedule.DoesNotExist: return Response( {"detail": "You do not have access to the specified schedule."}, status=status.HTTP_403_FORBIDDEN, ) try: sections = self.get_sections(request.data) except ObjectDoesNotExist: return Response( {"detail": "One or more sections not found in database."}, status=status.HTTP_400_BAD_REQUEST, ) semester_check_response = self.check_semester(request.data, sections) if semester_check_response is not None: return semester_check_response try: schedule.person = request.user schedule.semester = request.data.get("semester", get_current_semester()) schedule.name = request.data.get("name") schedule.save() schedule.sections.set(sections) return Response({"message": "success", "id": schedule.id}, status=status.HTTP_200_OK) except IntegrityError as e: return Response( { "detail": "IntegrityError encountered while trying to update: " + str(e.__cause__) }, status=status.HTTP_400_BAD_REQUEST, ) def create(self, request, *args, **kwargs): if Schedule.objects.filter(id=request.data.get("id")).exists(): return self.update(request, request.data.get("id")) try: sections = self.get_sections(request.data) except ObjectDoesNotExist: return Response( {"detail": "One or more sections not found in database."}, status=status.HTTP_400_BAD_REQUEST, ) semester_check_response = self.check_semester(request.data, sections) if semester_check_response is not None: return semester_check_response try: if ( "id" in request.data ): # Also from above we know that this id does not conflict with existing schedules. schedule = self.get_queryset().create( person=request.user, semester=request.data.get("semester", get_current_semester()), name=request.data.get("name"), id=request.data.get("id"), ) else: schedule = self.get_queryset().create( person=request.user, semester=request.data.get("semester", get_current_semester()), name=request.data.get("name"), ) schedule.sections.set(sections) return Response( {"message": "success", "id": schedule.id}, status=status.HTTP_201_CREATED ) except IntegrityError as e: return Response( { "detail": "IntegrityError encountered while trying to create: " + str(e.__cause__) }, status=status.HTTP_400_BAD_REQUEST, ) queryset = Schedule.objects.none() # included redundantly for docs def get_queryset(self): sem = get_current_semester() queryset = Schedule.objects.filter(person=self.request.user, semester=sem) queryset = queryset.prefetch_related( Prefetch("sections", Section.with_reviews.all()), "sections__associated_sections", "sections__instructors", "sections__meetings", "sections__meetings__room", ) return queryset
from plan.management.commands.recommendcourses import ( clean_course_input, recommend_courses, retrieve_course_clusters, vectorize_user, vectorize_user_by_courses, ) from plan.models import Schedule from plan.serializers import ScheduleSerializer @api_view(["POST"]) @schema( PcxAutoSchema( response_codes={ reverse_func("recommend-courses"): { "POST": { 200: "[DESCRIBE_RESPONSE_SCHEMA]Response returned successfully.", 201: "[UNDOCUMENTED]", 400: "Invalid curr_courses, past_courses, or n_recommendations (see response).", } } }, override_request_schema={ reverse_func("recommend-courses"): { "POST": { "type": "object", "properties": { "curr_courses": { "type": "array", "description": (