views.py 9.31 KB
Newer Older
1 2 3 4 5 6
""" API implementation for course-oriented interactions. """

import logging

from django.conf import settings
from django.http import Http404
7 8
from rest_framework.authentication import SessionAuthentication
from rest_framework_oauth.authentication import OAuth2Authentication
Ned Batchelder committed
9
from rest_framework.exceptions import AuthenticationFailed
10
from rest_framework.generics import RetrieveAPIView, ListAPIView
11
from rest_framework.permissions import IsAuthenticated
12 13 14 15
from rest_framework.response import Response
from xmodule.modulestore.django import modulestore
from opaque_keys.edx.keys import CourseKey

16
from course_structure_api.v0 import serializers
17
from courseware import courses
18
from courseware.access import has_access
19
from openedx.core.djangoapps.content.course_structures.api.v0 import api, errors
20
from openedx.core.lib.exceptions import CourseNotFoundError
21 22 23 24 25 26 27 28 29 30 31 32
from student.roles import CourseInstructorRole, CourseStaffRole


log = logging.getLogger(__name__)


class CourseViewMixin(object):
    """
    Mixin for views dealing with course content. Also handles authorization and authentication.
    """
    lookup_field = 'course_id'
    authentication_classes = (OAuth2Authentication, SessionAuthentication,)
33
    permission_classes = (IsAuthenticated,)
34 35 36 37 38 39 40 41 42 43

    def get_course_or_404(self):
        """
        Retrieves the specified course, or raises an Http404 error if it does not exist.
        Also checks to ensure the user has permissions to view the course
        """
        try:
            course_id = self.kwargs.get('course_id')
            course_key = CourseKey.from_string(course_id)
            course = courses.get_course(course_key)
44
            self.check_course_permissions(self.request.user, course_key)
45 46 47 48 49

            return course
        except ValueError:
            raise Http404

50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69
    @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)
70
            except CourseNotFoundError:
71 72 73 74
                raise Http404

        return func_wrapper

75 76 77 78 79
    def user_can_access_course(self, user, course):
        """
        Determines if the user is staff or an instructor for the course.
        Always returns True if DEBUG mode is enabled.
        """
80 81 82 83 84
        return bool(
            settings.DEBUG
            or has_access(user, CourseStaffRole.ROLE, course)
            or has_access(user, CourseInstructorRole.ROLE, course)
        )
85 86 87 88

    def check_course_permissions(self, user, course):
        """
        Checks if the request user can access the course.
89
        Raises 404 if the user does not have course access.
90 91
        """
        if not self.user_can_access_course(user, course):
92
            raise Http404
93 94 95 96 97 98 99 100 101 102 103 104 105 106

    def perform_authentication(self, request):
        """
        Ensures that the user is authenticated (e.g. not an AnonymousUser), unless DEBUG mode is enabled.
        """
        super(CourseViewMixin, self).perform_authentication(request)
        if request.user.is_anonymous() and not settings.DEBUG:
            raise AuthenticationFailed


class CourseList(CourseViewMixin, ListAPIView):
    """
    **Use Case**

107
        Get a paginated list of courses in the edX Platform.
108

109 110 111 112 113
        The list can be filtered by course_id.

        Each page in the list can contain up to 10 courses.

    **Example Requests**
114 115

          GET /api/course_structure/v0/courses/
116

117 118 119 120
          GET /api/course_structure/v0/courses/?course_id={course_id1},{course_id2}

    **Response Values**

121
        * count: The number of courses in the edX platform.
122

123
        * next: The URI to the next page of courses.
124

125
        * previous: The URI to the previous page of courses.
126

127
        * num_pages: The number of pages listing courses.
128

129 130
        * results:  A list of courses returned. Each collection in the list
          contains these fields.
131

132
            * id: The unique identifier for the course.
133

134
            * name: The name of the course.
135

136 137
            * category: The type of content. In this case, the value is always
              "course".
138

139
            * org: The organization specified for the course.
140

141 142 143 144 145 146 147 148 149 150 151 152
            * run: The run of the course.

            * course: The course number.

            * uri: The URI to use to get details of the course.

            * image_url: The URI for the course's main image.

            * start: The course start date.

            * end: The course end date. If course end date is not specified, the
              value is null.
153 154 155 156
    """
    serializer_class = serializers.CourseSerializer

    def get_queryset(self):
157
        course_ids = self.request.query_params.get('course_id', None)
158

159
        results = []
160 161 162 163 164
        if course_ids:
            course_ids = course_ids.split(',')
            for course_id in course_ids:
                course_key = CourseKey.from_string(course_id)
                course_descriptor = courses.get_course(course_key)
165
                results.append(course_descriptor)
166
        else:
167
            results = modulestore().get_courses()
168

169 170
        # Ensure only course descriptors are returned.
        results = (course for course in results if course.scope_ids.block_type == 'course')
171

172 173
        # Ensure only courses accessible by the user are returned.
        results = (course for course in results if self.user_can_access_course(self.request.user, course))
174

175
        # Sort the results in a predictable manner.
Clinton Blackburn committed
176
        return sorted(results, key=lambda course: unicode(course.id))
177 178 179 180 181 182


class CourseDetail(CourseViewMixin, RetrieveAPIView):
    """
    **Use Case**

183
        Get details for a specific course.
184

185
    **Example Request**:
186 187 188 189 190

        GET /api/course_structure/v0/courses/{course_id}/

    **Response Values**

191
        * id: The unique identifier for the course.
192

193 194
        * name: The name of the course.

195 196
        * category: The type of content.

197
        * org: The organization that is offering the course.
198

199
        * run: The run of the course.
200 201 202

        * course: The course number.

203
        * uri: The URI to use to get details about the course.
204

205
        * image_url: The URI for the course's main image.
206

207 208 209 210
        * start: The course start date.

        * end: The course end date. If course end date is not specified, the
          value is null.
211 212 213 214 215 216 217 218 219 220 221
    """
    serializer_class = serializers.CourseSerializer

    def get_object(self, queryset=None):
        return self.get_course_or_404()


class CourseStructure(CourseViewMixin, RetrieveAPIView):
    """
    **Use Case**

222 223
        Get the course structure. This endpoint returns all blocks in the
        course.
224 225 226 227 228 229 230

    **Example requests**:

        GET /api/course_structure/v0/course_structures/{course_id}/

    **Response Values**

231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248
        * 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.
249

250
          * children: If the block has child blocks, a list of IDs of the child
251
            blocks in the order they appear in the course.
252 253
    """

254
    @CourseViewMixin.course_check
255
    def get(self, request, **kwargs):
256
        try:
257
            return Response(api.course_structure(self.course_key))
258
        except errors.CourseStructureNotAvailableError:
259 260
            # 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.
261 262 263 264 265 266 267
            return Response(status=503, headers={'Retry-After': '120'})


class CourseGradingPolicy(CourseViewMixin, ListAPIView):
    """
    **Use Case**

268
        Get the course grading policy.
269 270 271 272 273 274 275

    **Example requests**:

        GET /api/course_structure/v0/grading_policies/{course_id}/

    **Response Values**

276 277 278
        * 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.
279

280
        * count: The number of assignments of the type.
281 282 283

        * dropped: Number of assignments of the type that are dropped.

284 285
        * weight: The weight, or effect, of the assignment type on the learner's
          final grade.
286 287 288 289
    """

    allow_empty = False

290
    @CourseViewMixin.course_check
291
    def get(self, request, **kwargs):
292
        return Response(api.course_grading_policy(self.course_key))