Commit 6b56ae6a by Clinton Blackburn

Merge pull request #6154 from edx/clintonb/courses-api

Courses API
parents 2a8b7148 2854bb95
......@@ -33,7 +33,7 @@ class CountMongoCallsXMLRoundtrip(TestCase):
self.addCleanup(rmtree, self.export_dir, ignore_errors=True)
@ddt.data(
(MIXED_OLD_MONGO_MODULESTORE_BUILDER, 287, 780, 702, 702),
(MIXED_OLD_MONGO_MODULESTORE_BUILDER, 287, 779, 702, 702),
(MIXED_SPLIT_MODULESTORE_BUILDER, 37, 16, 190, 189),
)
@ddt.unpack
......
""" This file is intentionally empty. """
"""
Course Structure API URI specification.
Patterns here should simply point to version-specific patterns.
"""
from django.conf.urls import patterns, url, include
urlpatterns = patterns(
'',
url(r'^v0/', include('course_structure_api.v0.urls', namespace='v0'))
)
""" Django REST Framework Serializers """
from django.core.urlresolvers import reverse
from rest_framework import serializers
from courseware.courses import course_image_url
class CourseSerializer(serializers.Serializer):
""" Serializer for Courses """
id = serializers.CharField() # pylint: disable=invalid-name
name = serializers.CharField(source='display_name')
category = serializers.CharField()
org = serializers.SerializerMethodField('get_org')
run = serializers.SerializerMethodField('get_run')
course = serializers.SerializerMethodField('get_course')
uri = serializers.SerializerMethodField('get_uri')
image_url = serializers.SerializerMethodField('get_image_url')
start = serializers.DateTimeField()
end = serializers.DateTimeField()
def get_org(self, course):
""" Gets the course org """
return course.id.org
def get_run(self, course):
""" Gets the course run """
return course.id.run
def get_course(self, course):
""" Gets the course """
return course.id.course
def get_uri(self, course):
""" Builds course detail uri """
# pylint: disable=no-member
request = self.context['request']
return request.build_absolute_uri(reverse('course_structure_api:v0:detail', kwargs={'course_id': course.id}))
def get_image_url(self, course):
""" Get the course image URL """
return course_image_url(course)
class GradingPolicySerializer(serializers.Serializer):
""" Serializer for course grading policy. """
assignment_type = serializers.CharField(source='type')
count = serializers.IntegerField(source='min_count')
dropped = serializers.IntegerField(source='drop_count')
weight = serializers.FloatField()
# pylint: disable=invalid-name
class BlockSerializer(serializers.Serializer):
""" Serializer for course structure block. """
id = serializers.CharField(source='usage_key')
type = serializers.CharField(source='block_type')
display_name = serializers.CharField()
graded = serializers.BooleanField(default=False)
format = serializers.CharField()
children = serializers.CharField()
class CourseStructureSerializer(serializers.Serializer):
""" Serializer for course structure. """
root = serializers.CharField(source='root')
blocks = serializers.SerializerMethodField('get_blocks')
def get_blocks(self, structure):
""" Serialize the individual blocks. """
serialized = {}
for key, block in structure['blocks'].iteritems():
serialized[key] = BlockSerializer(block).data
return serialized
"""
Courses Structure API v0 URI specification
"""
from django.conf import settings
from django.conf.urls import patterns, url
from course_structure_api.v0 import views
COURSE_ID_PATTERN = settings.COURSE_ID_PATTERN
urlpatterns = patterns(
'',
url(r'^courses/$', views.CourseList.as_view(), name='list'),
url(r'^courses/{}/$'.format(COURSE_ID_PATTERN), views.CourseDetail.as_view(), name='detail'),
url(r'^course_structures/{}/$'.format(COURSE_ID_PATTERN), views.CourseStructure.as_view(), name='structure'),
url(r'^grading_policies/{}/$'.format(COURSE_ID_PATTERN), views.CourseGradingPolicy.as_view(), name='grading_policy')
)
""" API implementation for course-oriented interactions. """
import logging
from operator import attrgetter
from django.conf import settings
from django.http import Http404
from rest_framework.authentication import OAuth2Authentication, SessionAuthentication
from rest_framework.exceptions import PermissionDenied, AuthenticationFailed
from rest_framework.generics import RetrieveAPIView, ListAPIView
from rest_framework.response import Response
from xmodule.modulestore.django import modulestore
from opaque_keys.edx.keys import CourseKey
from courseware.access import has_access
from openedx.core.djangoapps.content.course_structures import models
from openedx.core.lib.api.permissions import IsAuthenticatedOrDebug
from openedx.core.lib.api.serializers import PaginationSerializer
from courseware import courses
from course_structure_api.v0 import serializers
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,)
permission_classes = (IsAuthenticatedOrDebug,)
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)
self.check_course_permissions(self.request.user, course)
return course
except ValueError:
raise Http404
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.
"""
return (settings.DEBUG
or has_access(user, CourseStaffRole.ROLE, course)
or has_access(user, CourseInstructorRole.ROLE, course))
def check_course_permissions(self, user, course):
"""
Checks if the request user can access the course.
Raises PermissionDenied if the user does not have course access.
"""
if not self.user_can_access_course(user, course):
raise PermissionDenied
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**
CourseList returns paginated list of courses in the edX Platform. The list can be filtered by course_id.
**Example Request**
GET /api/course_structure/v0/courses/
GET /api/course_structure/v0/courses/?course_id={course_id1},{course_id2}
**Response Values**
* id: The unique identifier for the course.
* name: The name of the course.
* category: The type of content. In this case, the value is always "course".
* org: The organization specified for the course.
* course: The course number.
* org: The run for the course.
* uri: The URI to use to get details of the course.
* image_url: The URI for the course's main image.
* start: Course start date
* end: Course end date
"""
paginate_by = 10
paginate_by_param = 'page_size'
pagination_serializer_class = PaginationSerializer
serializer_class = serializers.CourseSerializer
def get_queryset(self):
course_ids = self.request.QUERY_PARAMS.get('course_id', None)
course_descriptors = []
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)
course_descriptors.append(course_descriptor)
else:
course_descriptors = modulestore().get_courses()
results = [course for course in course_descriptors if self.user_can_access_course(self.request.user, course)]
# Sort the results in a predictable manner.
results.sort(key=attrgetter('id'))
return results
class CourseDetail(CourseViewMixin, RetrieveAPIView):
"""
**Use Case**
CourseDetail returns details for a course.
**Example requests**:
GET /api/course_structure/v0/courses/{course_id}/
**Response Values**
* category: The type of content.
* name: The name of the course.
* uri: The URI to use to get details of the course.
* course: The course number.
* due: The due date. For courses, the value is always null.
* org: The organization specified for the course.
* id: The unique identifier for the course.
"""
serializer_class = serializers.CourseSerializer
def get_object(self, queryset=None):
return self.get_course_or_404()
class CourseStructure(CourseViewMixin, RetrieveAPIView):
"""
**Use Case**
Retrieves course structure.
**Example requests**:
GET /api/course_structure/v0/course_structures/{course_id}/
**Response Values**
* root: ID of the root node of the structure
* blocks: Dictionary mapping IDs to block nodes.
"""
serializer_class = serializers.CourseStructureSerializer
course = None
def retrieve(self, request, *args, **kwargs):
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.
models.update_course_structure.delay(self.course.id)
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):
"""
**Use Case**
Retrieves course grading policy.
**Example requests**:
GET /api/course_structure/v0/grading_policies/{course_id}/
**Response Values**
* assignment_type: The type of the assignment (e.g. Exam, Homework). Note: These values are course-dependent.
Do not make any assumptions based on assignment type.
* count: Number of assignments of the type.
* dropped: Number of assignments of the type that are dropped.
* weight: Effect of the assignment type on grading.
"""
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
......@@ -3,7 +3,7 @@ from rest_framework.viewsets import ReadOnlyModelViewSet
from notification_prefs import NOTIFICATION_PREF_KEY
from notifier_api.serializers import NotifierUserSerializer
from openedx.core.djangoapps.user_api.views import ApiKeyHeaderPermission
from openedx.core.lib.api.permissions import ApiKeyHeaderPermission
class NotifierUsersViewSet(ReadOnlyModelViewSet):
......
......@@ -1627,6 +1627,7 @@ INSTALLED_APPS = (
'lms.djangoapps.lms_xblock',
'openedx.core.djangoapps.content.course_structures',
'course_structure_api',
)
######################### MARKETING SITE ###############################
......
......@@ -92,6 +92,8 @@ CC_PROCESSOR = {
}
########################### External REST APIs #################################
FEATURES['ENABLE_OAUTH2_PROVIDER'] = True
OAUTH_OIDC_ISSUER = 'http://127.0.0.1:8000/oauth2'
FEATURES['ENABLE_MOBILE_REST_API'] = True
FEATURES['ENABLE_VIDEO_ABSTRACTION_LAYER_API'] = True
......
......@@ -81,6 +81,8 @@ urlpatterns = ('', # nopep8
# Courseware search endpoints
url(r'^search/', include('search.urls')),
# Course content API
url(r'^api/course_structure/', include('course_structure_api.urls', namespace='course_structure_api')),
)
if settings.FEATURES["ENABLE_COMBINED_LOGIN_REGISTRATION"]:
......@@ -115,7 +117,6 @@ urlpatterns += (
url(r'^course_modes/', include('course_modes.urls')),
)
js_info_dict = {
'domain': 'djangojs',
# We need to explicitly include external Django apps that are not in LOCALE_PATHS.
......
"""HTTP end-points for the User API. """
import copy
from openedx.core.lib.api.permissions import ApiKeyHeaderPermission
from opaque_keys import InvalidKeyError
import third_party_auth
......@@ -15,7 +16,6 @@ from opaque_keys.edx import locator
from rest_framework import authentication
from rest_framework import filters
from rest_framework import generics
from rest_framework import permissions
from rest_framework import status
from rest_framework import viewsets
from rest_framework.views import APIView
......@@ -32,23 +32,6 @@ from .models import UserPreference, UserProfile
from .serializers import UserSerializer, UserPreferenceSerializer
class ApiKeyHeaderPermission(permissions.BasePermission):
def has_permission(self, request, view):
"""
Check for permissions by matching the configured API key and header
If settings.DEBUG is True and settings.EDX_API_KEY is not set or None,
then allow the request. Otherwise, allow the request if and only if
settings.EDX_API_KEY is set and the X-Edx-Api-Key HTTP header is
present in the request and matches the setting.
"""
api_key = getattr(settings, "EDX_API_KEY", None)
return (
(settings.DEBUG and api_key is None) or
(api_key is not None and request.META.get("HTTP_X_EDX_API_KEY") == api_key)
)
class LoginSessionView(APIView):
"""HTTP end-points for logging in users. """
......
"""
Package for common functionality shared across various APIs.
"""
from django.conf import settings
from rest_framework import permissions
from rest_framework.exceptions import PermissionDenied
class ApiKeyHeaderPermission(permissions.BasePermission):
def has_permission(self, request, view):
"""
Check for permissions by matching the configured API key and header
If settings.DEBUG is True and settings.EDX_API_KEY is not set or None,
then allow the request. Otherwise, allow the request if and only if
settings.EDX_API_KEY is set and the X-Edx-Api-Key HTTP header is
present in the request and matches the setting.
"""
api_key = getattr(settings, "EDX_API_KEY", None)
return (
(settings.DEBUG and api_key is None) or
(api_key is not None and request.META.get("HTTP_X_EDX_API_KEY") == api_key)
)
class IsAuthenticatedOrDebug(permissions.BasePermission):
"""
Allows access only to authenticated users, or anyone if debug mode is enabled.
"""
def has_permission(self, request, view):
if settings.DEBUG:
return True
user = getattr(request, 'user', None)
return user and user.is_authenticated()
from rest_framework import pagination, serializers
class PaginationSerializer(pagination.PaginationSerializer):
"""
Custom PaginationSerializer to include num_pages field
"""
num_pages = serializers.Field(source='paginator.num_pages')
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment