views.py 9.4 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 edx_rest_framework_extensions.authentication import JwtAuthentication
from opaque_keys.edx.keys import CourseKey
9
from rest_framework.authentication import SessionAuthentication
Ned Batchelder committed
10
from rest_framework.exceptions import AuthenticationFailed
11
from rest_framework.generics import RetrieveAPIView, ListAPIView
12
from rest_framework.permissions import IsAuthenticated
13
from rest_framework.response import Response
14
from rest_framework_oauth.authentication import OAuth2Authentication
15 16
from xmodule.modulestore.django import modulestore

17
from course_structure_api.v0 import serializers
18
from courseware import courses
19
from courseware.access import has_access
20
from openedx.core.djangoapps.content.course_structures.api.v0 import api, errors
21
from openedx.core.lib.exceptions import CourseNotFoundError
22 23 24 25 26 27 28 29 30 31
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'
32
    authentication_classes = (JwtAuthentication, 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
    @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
        """
58

59 60 61 62 63 64 65 66 67 68 69 70
        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)
71
            except CourseNotFoundError:
72 73 74 75
                raise Http404

        return func_wrapper

76 77 78 79 80
    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.
        """
81 82 83 84 85
        return bool(
            settings.DEBUG
            or has_access(user, CourseStaffRole.ROLE, course)
            or has_access(user, CourseInstructorRole.ROLE, course)
        )
86 87 88 89

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

    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**

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

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

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

    **Example Requests**
115 116

          GET /api/course_structure/v0/courses/
117

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

    **Response Values**

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

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

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

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

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

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

135
            * name: The name of the course.
136

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

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

142 143 144 145 146 147 148 149 150 151 152 153
            * 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.
154 155 156 157
    """
    serializer_class = serializers.CourseSerializer

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

160
        results = []
161 162 163 164 165
        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)
166
                results.append(course_descriptor)
167
        else:
168
            results = modulestore().get_courses()
169

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

173 174
        # 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))
175

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


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

184
        Get details for a specific course.
185

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

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

    **Response Values**

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

194 195
        * name: The name of the course.

196 197
        * category: The type of content.

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

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

        * course: The course number.

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

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

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

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

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


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

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

    **Example requests**:

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

    **Response Values**

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

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

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


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

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

    **Example requests**:

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

    **Response Values**

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

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

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

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

    allow_empty = False

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