This repository has been archived by the owner on May 10, 2024. It is now read-only.
/
views.py
380 lines (303 loc) · 13.9 KB
/
views.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
"""
Course API Views
"""
from django.core.exceptions import ValidationError
from django.core.paginator import InvalidPage
from edx_rest_framework_extensions.paginators import NamespacedPageNumberPagination
from rest_framework.generics import ListAPIView, RetrieveAPIView
from rest_framework.throttling import UserRateThrottle
from rest_framework.exceptions import NotFound
from openedx.core.lib.api.view_utils import DeveloperErrorViewMixin, view_auth_classes
from . import USE_RATE_LIMIT_2_FOR_COURSE_LIST_API, USE_RATE_LIMIT_10_FOR_COURSE_LIST_API
from .api import course_detail, list_course_keys, list_courses
from .forms import CourseDetailGetForm, CourseIdListGetForm, CourseListGetForm
from .serializers import CourseDetailSerializer, CourseKeySerializer, CourseSerializer
@view_auth_classes(is_authenticated=False)
class CourseDetailView(DeveloperErrorViewMixin, RetrieveAPIView):
"""
**Use Cases**
Request details for a course
**Example Requests**
GET /api/courses/v1/courses/{course_key}/
**Response Values**
Body consists of the following fields:
* effort: A textual description of the weekly hours of effort expected
in the course.
* end: Date the course ends, in ISO 8601 notation
* enrollment_end: Date enrollment ends, in ISO 8601 notation
* enrollment_start: Date enrollment begins, in ISO 8601 notation
* id: A unique identifier of the course; a serialized representation
of the opaque key identifying the course.
* media: An object that contains named media items. Included here:
* course_image: An image to show for the course. Represented
as an object with the following fields:
* uri: The location of the image
* name: Name of the course
* number: Catalog number of the course
* org: Name of the organization that owns the course
* overview: A possibly verbose HTML textual description of the course.
Note: this field is only included in the Course Detail view, not
the Course List view.
* short_description: A textual description of the course
* start: Date the course begins, in ISO 8601 notation
* start_display: Readably formatted start of the course
* start_type: Hint describing how `start_display` is set. One of:
* `"string"`: manually set by the course author
* `"timestamp"`: generated from the `start` timestamp
* `"empty"`: no start date is specified
* pacing: Course pacing. Possible values: instructor, self
Deprecated fields:
* blocks_url: Used to fetch the course blocks
* course_id: Course key (use 'id' instead)
**Parameters:**
username (optional):
The username of the specified user for whom the course data
is being accessed. The username is not only required if the API is
requested by an Anonymous user.
**Returns**
* 200 on success with above fields.
* 400 if an invalid parameter was sent or the username was not provided
for an authenticated request.
* 403 if a user who does not have permission to masquerade as
another user specifies a username other than their own.
* 404 if the course is not available or cannot be seen.
Example response:
{
"blocks_url": "/api/courses/v1/blocks/?course_id=edX%2Fexample%2F2012_Fall",
"media": {
"course_image": {
"uri": "/c4x/edX/example/asset/just_a_test.jpg",
"name": "Course Image"
}
},
"description": "An example course.",
"end": "2015-09-19T18:00:00Z",
"enrollment_end": "2015-07-15T00:00:00Z",
"enrollment_start": "2015-06-15T00:00:00Z",
"course_id": "edX/example/2012_Fall",
"name": "Example Course",
"number": "example",
"org": "edX",
"overview: "<p>A verbose description of the course.</p>"
"start": "2015-07-17T12:00:00Z",
"start_display": "July 17, 2015",
"start_type": "timestamp",
"pacing": "instructor"
}
"""
serializer_class = CourseDetailSerializer
def get_object(self):
"""
Return the requested course object, if the user has appropriate
permissions.
"""
requested_params = self.request.query_params.copy()
requested_params.update({'course_key': self.kwargs['course_key_string']})
form = CourseDetailGetForm(requested_params, initial={'requesting_user': self.request.user})
if not form.is_valid():
raise ValidationError(form.errors)
return course_detail(
self.request,
form.cleaned_data['username'],
form.cleaned_data['course_key'],
)
class CourseListUserThrottle(UserRateThrottle):
"""Limit the number of requests users can make to the course list API."""
# The course list endpoint is likely being inefficient with how it's querying
# various parts of the code and can take courseware down, it needs to be rate
# limited until optimized. LEARNER-5527
THROTTLE_RATES = {
'user': '20/minute',
'staff': '40/minute',
}
def check_for_switches(self):
if USE_RATE_LIMIT_2_FOR_COURSE_LIST_API.is_enabled():
self.THROTTLE_RATES = {
'user': '2/minute',
'staff': '10/minute',
}
elif USE_RATE_LIMIT_10_FOR_COURSE_LIST_API.is_enabled():
self.THROTTLE_RATES = {
'user': '10/minute',
'staff': '20/minute',
}
def allow_request(self, request, view):
self.check_for_switches()
# Use a special scope for staff to allow for a separate throttle rate
user = request.user
if user.is_authenticated and (user.is_staff or user.is_superuser):
self.scope = 'staff'
self.rate = self.get_rate()
self.num_requests, self.duration = self.parse_rate(self.rate)
return super(CourseListUserThrottle, self).allow_request(request, view)
class LazyPageNumberPagination(NamespacedPageNumberPagination):
"""
NamespacedPageNumberPagination that works with a LazySequence queryset.
The paginator cache uses ``@cached_property`` to cache the property values for
count and num_pages. It assumes these won't change, but in the case of a
LazySquence, its count gets updated as we move through it. This class clears
the cached property values before reporting results so they will be recalculated.
"""
def get_paginated_response(self, data):
# Clear the cached property values to recalculate the estimated count from the LazySequence
del self.page.paginator.__dict__['count']
del self.page.paginator.__dict__['num_pages']
# Paginate queryset function is using cached number of pages and sometime after
# deleting from cache when we recalculate number of pages are different and it raises
# EmptyPage error while accessing the previous page link. So we are catching that exception
# and raising 404. For more detail checkout PROD-1222
page_number = self.request.query_params.get(self.page_query_param, 1)
try:
self.page.paginator.validate_number(page_number)
except InvalidPage as exc:
msg = self.invalid_page_message.format(
page_number=page_number, message=str(exc)
)
self.page.number = self.page.paginator.num_pages
raise NotFound(msg)
return super(LazyPageNumberPagination, self).get_paginated_response(data)
@view_auth_classes(is_authenticated=False)
class CourseListView(DeveloperErrorViewMixin, ListAPIView):
"""
**Use Cases**
Request information on all courses visible to the specified user.
**Example Requests**
GET /api/courses/v1/courses/
**Response Values**
Body comprises a list of objects as returned by `CourseDetailView`.
**Parameters**
search_term (optional):
Search term to filter courses (used by ElasticSearch).
username (optional):
The username of the specified user whose visible courses we
want to see. The username is not required only if the API is
requested by an Anonymous user.
org (optional):
If specified, visible `CourseOverview` objects are filtered
such that only those belonging to the organization with the
provided org code (e.g., "HarvardX") are returned.
Case-insensitive.
**Returns**
* 200 on success, with a list of course discovery objects as returned
by `CourseDetailView`.
* 400 if an invalid parameter was sent or the username was not provided
for an authenticated request.
* 403 if a user who does not have permission to masquerade as
another user specifies a username other than their own.
* 404 if the specified user does not exist, or the requesting user does
not have permission to view their courses.
Example response:
[
{
"blocks_url": "/api/courses/v1/blocks/?course_id=edX%2Fexample%2F2012_Fall",
"media": {
"course_image": {
"uri": "/c4x/edX/example/asset/just_a_test.jpg",
"name": "Course Image"
}
},
"description": "An example course.",
"end": "2015-09-19T18:00:00Z",
"enrollment_end": "2015-07-15T00:00:00Z",
"enrollment_start": "2015-06-15T00:00:00Z",
"course_id": "edX/example/2012_Fall",
"name": "Example Course",
"number": "example",
"org": "edX",
"start": "2015-07-17T12:00:00Z",
"start_display": "July 17, 2015",
"start_type": "timestamp"
}
]
"""
class CourseListPageNumberPagination(LazyPageNumberPagination):
max_page_size = 100
pagination_class = CourseListPageNumberPagination
serializer_class = CourseSerializer
throttle_classes = (CourseListUserThrottle,)
def get_queryset(self):
"""
Yield courses visible to the user.
"""
form = CourseListGetForm(self.request.query_params, initial={'requesting_user': self.request.user})
if not form.is_valid():
raise ValidationError(form.errors)
return list_courses(
self.request,
form.cleaned_data['username'],
org=form.cleaned_data['org'],
filter_=form.cleaned_data['filter_'],
search_term=form.cleaned_data['search_term']
)
class CourseIdListUserThrottle(UserRateThrottle):
"""Limit the number of requests users can make to the course list id API."""
THROTTLE_RATES = {
'user': '20/minute',
'staff': '40/minute',
}
def allow_request(self, request, view):
# Use a special scope for staff to allow for a separate throttle rate
user = request.user
if user.is_authenticated and (user.is_staff or user.is_superuser):
self.scope = 'staff'
self.rate = self.get_rate()
self.num_requests, self.duration = self.parse_rate(self.rate)
return super(CourseIdListUserThrottle, self).allow_request(request, view)
@view_auth_classes()
class CourseIdListView(DeveloperErrorViewMixin, ListAPIView):
"""
**Use Cases**
Request a list of course IDs for all courses the specified user can
access based on the provided parameters.
**Example Requests**
GET /api/courses/v1/courses_ids/
**Response Values**
Body comprises a list of course ids and pagination details.
**Parameters**
username (optional):
The username of the specified user whose visible courses we
want to see.
role (required):
Course ids are filtered such that only those for which the
user has the specified role are returned. Role can be "staff"
or "instructor".
Case-insensitive.
**Returns**
* 200 on success, with a list of course ids and pagination details
* 400 if an invalid parameter was sent or the username was not provided
for an authenticated request.
* 403 if a user who does not have permission to masquerade as
another user who specifies a username other than their own.
* 404 if the specified user does not exist, or the requesting user does
not have permission to view their courses.
Example response:
{
"results":
[
"course-v1:edX+DemoX+Demo_Course"
],
"pagination": {
"previous": null,
"num_pages": 1,
"next": null,
"count": 1
}
}
"""
class CourseIdListPageNumberPagination(LazyPageNumberPagination):
max_page_size = 1000
pagination_class = CourseIdListPageNumberPagination
serializer_class = CourseKeySerializer
throttle_classes = (CourseIdListUserThrottle,)
def get_queryset(self):
"""
Returns CourseKeys for courses which the user has the provided role.
"""
form = CourseIdListGetForm(self.request.query_params, initial={'requesting_user': self.request.user})
if not form.is_valid():
raise ValidationError(form.errors)
return list_course_keys(
self.request,
form.cleaned_data['username'],
role=form.cleaned_data['role'],
)