From 9c32b1e878f804b5eae86d28841f728aa082fba1 Mon Sep 17 00:00:00 2001 From: Diana Huang Date: Tue, 21 Apr 2015 14:57:56 -0400 Subject: [PATCH] Refactor course_structure_api to have separate API Layer. --- lms/djangoapps/course_structure_api/v0/api.py | 94 +++++++++++++++++++ .../course_structure_api/v0/errors.py | 9 ++ .../course_structure_api/v0/views.py | 59 +++++++----- 3 files changed, 140 insertions(+), 22 deletions(-) create mode 100644 lms/djangoapps/course_structure_api/v0/api.py create mode 100644 lms/djangoapps/course_structure_api/v0/errors.py diff --git a/lms/djangoapps/course_structure_api/v0/api.py b/lms/djangoapps/course_structure_api/v0/api.py new file mode 100644 index 0000000000..5a0523f6b9 --- /dev/null +++ b/lms/djangoapps/course_structure_api/v0/api.py @@ -0,0 +1,94 @@ +""" +API implementation of the Course Structure API for Python code. + +Note: The course list and course detail functionality isn't currently supported here because of the tricky interactions between DRF and the code. +Most of that information is available by accessing the course objects directly. +""" + +from course_structure_api.v0 import serializers +from course_structure_api.v0.errors import CourseNotFoundError, CourseStructureNotAvailableError +from openedx.core.djangoapps.content.course_structures import models, tasks +from courseware import courses + + +def _retrieve_course(course_key): + """Retrieves the course for the given course key. + + Args: + course_key: The CourseKey for the course we'd like to retrieve. + Returns: + the course that matches the CourseKey + Raises: + CourseNotFoundError + + """ + try: + course = courses.get_course(course_key) + return course + except ValueError: + raise CourseNotFoundError + + +def course_structure(course_key): + """ + Retrieves the entire course structure, including information about all the blocks used in the course. + + Args: + course_key: the CourseKey of the course we'd like to retrieve. + Returns: + The serialized output of the course structure: + * root: The ID of the root node of the course structure. + + * blocks: A dictionary that maps block IDs to a collection of + information about each block. Each block contains the following + fields. + + * id: The ID of the block. + + * type: The type of block. Possible values include sequential, + vertical, html, problem, video, and discussion. The type can also be + the name of a custom type of block used for the course. + + * display_name: The display name configured for the block. + + * graded: Whether or not the sequential or problem is graded. The + value is true or false. + + * format: The assignment type. + + * children: If the block has child blocks, a list of IDs of the child + blocks. + Raises: + CourseStructureNotAvailableError, CourseNotFoundError + """ + course = _retrieve_course(course_key) + try: + course_structure = models.CourseStructure.objects.get(course_id=course.id) + return serializers.CourseStructureSerializer(course_structure.structure).data + except models.CourseStructure.DoesNotExist: + # If we don't have data stored, generate it and return an error. + tasks.update_course_structure.delay(unicode(course_key)) + raise CourseStructureNotAvailableError + + +def course_grading_policy(course_key): + """ + Retrieves the course grading policy. + + Args: + course_key: CourseKey the corresponds to the course we'd like to know grading policy information about. + Returns: + The serialized version of the course grading policy containing the following information: + * assignment_type: The type of the assignment, as configured by course + staff. For example, course staff might make the assignment types Homework, + Quiz, and Exam. + + * count: The number of assignments of the type. + + * dropped: Number of assignments of the type that are dropped. + + * weight: The weight, or effect, of the assignment type on the learner's + final grade. + """ + course = _retrieve_course(course_key) + return serializers.GradingPolicySerializer(course.raw_grader).data diff --git a/lms/djangoapps/course_structure_api/v0/errors.py b/lms/djangoapps/course_structure_api/v0/errors.py new file mode 100644 index 0000000000..91a3f24f4d --- /dev/null +++ b/lms/djangoapps/course_structure_api/v0/errors.py @@ -0,0 +1,9 @@ + +class CourseNotFoundError(Exception): + """ The course was not found. """ + pass + + +class CourseStructureNotAvailableError(Exception): + """ The course structure still needs to be generated. """ + pass diff --git a/lms/djangoapps/course_structure_api/v0/views.py b/lms/djangoapps/course_structure_api/v0/views.py index eb6625fdc1..fe9156c6ae 100644 --- a/lms/djangoapps/course_structure_api/v0/views.py +++ b/lms/djangoapps/course_structure_api/v0/views.py @@ -11,7 +11,8 @@ from rest_framework.response import Response from xmodule.modulestore.django import modulestore from opaque_keys.edx.keys import CourseKey -from course_structure_api.v0 import serializers +from course_structure_api.v0 import api, serializers +from course_structure_api.v0.errors import CourseNotFoundError, CourseStructureNotAvailableError from courseware import courses from courseware.access import has_access from openedx.core.djangoapps.content.course_structures import models, tasks @@ -40,13 +41,37 @@ class CourseViewMixin(object): course_id = self.kwargs.get('course_id') course_key = CourseKey.from_string(course_id) course = courses.get_course(course_key) - - self.check_course_permissions(self.request.user, course) + self.check_course_permissions(self.request.user, course_key) return course except ValueError: raise Http404 + @staticmethod + def course_check(func): + """Decorator responsible for catching errors finding and returning a 404 if the user does not have access + to the API function. + + :param func: function to be wrapped + :returns: the wrapped function + """ + def func_wrapper(self, *args, **kwargs): + """Wrapper function for this decorator. + + :param *args: the arguments passed into the function + :param **kwargs: the keyword arguments passed into the function + :returns: the result of the wrapped function + """ + try: + course_id = self.kwargs.get('course_id') + self.course_key = CourseKey.from_string(course_id) + self.check_course_permissions(self.request.user, self.course_key) + return func(self, *args, **kwargs) + except CourseNotFoundError: + raise Http404 + + return func_wrapper + def user_can_access_course(self, user, course): """ Determines if the user is staff or an instructor for the course. @@ -185,7 +210,6 @@ class CourseDetail(CourseViewMixin, RetrieveAPIView): * end: The course end date. If course end date is not specified, the value is null. """ - serializer_class = serializers.CourseSerializer def get_object(self, queryset=None): @@ -227,22 +251,16 @@ class CourseStructure(CourseViewMixin, RetrieveAPIView): * children: If the block has child blocks, a list of IDs of the child blocks. """ - serializer_class = serializers.CourseStructureSerializer - course = None - def retrieve(self, request, *args, **kwargs): + @CourseViewMixin.course_check + def get(self, request, course_id=None): try: - return super(CourseStructure, self).retrieve(request, *args, **kwargs) - except models.CourseStructure.DoesNotExist: - # If we don't have data stored, generate it and return a 503. - tasks.update_course_structure.delay(unicode(self.course.id)) + return Response(api.course_structure(self.course_key)) + except CourseStructureNotAvailableError: + # If we don't have data stored, we will try to regenerate it, so + # return a 503 and as them to retry in 2 minutes. return Response(status=503, headers={'Retry-After': '120'}) - def get_object(self, queryset=None): - # Make sure the course exists and the user has permissions to view it. - self.course = self.get_course_or_404() - course_structure = models.CourseStructure.objects.get(course_id=self.course.id) - return course_structure.structure class CourseGradingPolicy(CourseViewMixin, ListAPIView): @@ -269,11 +287,8 @@ class CourseGradingPolicy(CourseViewMixin, ListAPIView): final grade. """ - serializer_class = serializers.GradingPolicySerializer allow_empty = False - def get_queryset(self): - course = self.get_course_or_404() - - # Return the raw data. The serializer will handle the field mappings. - return course.raw_grader + @CourseViewMixin.course_check + def get(self, request, course_id=None): + return Response(api.course_grading_policy(self.course_key))