Commit e63194c1 by Giovanni Di Milia

Added CCX REST APIs

CCX REST APIs
OAUTH2 authorization for CCX APIs
- oauth2 authorization required for ccx list.
- Course-instructor permission for ccx api endpoint
- Protection for detail view too.

Tests for CCX REST APIs and OAUTH2 authorization
parent f59144b7
""" CCX API URLs. """
from django.conf.urls import patterns, url, include
urlpatterns = patterns(
'',
url(r'^v0/', include('lms.djangoapps.ccx.api.v0.urls', namespace='v0')),
)
""" CCX API v0 Paginators. """
from openedx.core.lib.api.paginators import DefaultPagination
class CCXAPIPagination(DefaultPagination):
"""
Pagination format used by the CCX API.
"""
page_size_query_param = "page_size"
def get_paginated_response(self, data):
"""
Annotate the response with pagination information.
"""
response = super(CCXAPIPagination, self).get_paginated_response(data)
# Add the current page to the response.
response.data["current_page"] = self.page.number
# This field can be derived from other fields in the response,
# so it may make sense to have the JavaScript client calculate it
# instead of including it in the response.
response.data["start"] = (self.page.number - 1) * self.get_page_size(self.request)
return response
""" CCX API v0 Serializers. """
from rest_framework import serializers
from lms.djangoapps.ccx.models import CustomCourseForEdX
from ccx_keys.locator import CCXLocator
class CCXCourseSerializer(serializers.ModelSerializer):
"""
Serializer for CCX courses
"""
ccx_course_id = serializers.SerializerMethodField()
master_course_id = serializers.CharField(source='course_id')
display_name = serializers.CharField()
coach_email = serializers.EmailField(source='coach.email')
start = serializers.CharField(allow_blank=True)
due = serializers.CharField(allow_blank=True)
max_students_allowed = serializers.IntegerField(source='max_student_enrollments_allowed')
class Meta(object):
model = CustomCourseForEdX
fields = (
"ccx_course_id",
"master_course_id",
"display_name",
"coach_email",
"start",
"due",
"max_students_allowed",
)
read_only_fields = (
"ccx_course_id",
"master_course_id",
"start",
"due",
)
@staticmethod
def get_ccx_course_id(obj):
"""
Getter for the CCX Course ID
"""
return unicode(CCXLocator.from_course_locator(obj.course.id, obj.id))
""" CCX API v0 URLs. """
from django.conf import settings
from django.conf.urls import patterns, url, include
from lms.djangoapps.ccx.api.v0 import views
CCX_COURSE_ID_PATTERN = settings.COURSE_ID_PATTERN.replace('course_id', 'ccx_course_id')
CCX_URLS = patterns(
'',
url(r'^$', views.CCXListView.as_view(), name='list'),
url(r'^{}/?$'.format(CCX_COURSE_ID_PATTERN), views.CCXDetailView.as_view(), name='detail'),
)
urlpatterns = patterns(
'',
url(r'^ccx/', include(CCX_URLS, namespace='ccx')),
)
"""
Dummy factories for tests
"""
from factory import SubFactory
from factory import SubFactory, Sequence
from factory.django import DjangoModelFactory
from student.tests.factories import UserFactory
from lms.djangoapps.ccx.models import CustomCourseForEdX
......@@ -11,6 +11,6 @@ class CcxFactory(DjangoModelFactory): # pylint: disable=missing-docstring
class Meta(object):
model = CustomCourseForEdX
display_name = "Test CCX"
display_name = Sequence(lambda n: 'Test CCX #{0}'.format(n)) # pylint: disable=unnecessary-lambda
id = None # pylint: disable=invalid-name
coach = SubFactory(UserFactory)
......@@ -20,7 +20,7 @@ from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from lms.djangoapps.ccx.models import CustomCourseForEdX
from lms.djangoapps.ccx.overrides import override_field_for_ccx
from lms.djangoapps.ccx.tests.test_views import flatten, iter_blocks
from lms.djangoapps.ccx.tests.utils import flatten, iter_blocks
@attr('shard_1')
......
......@@ -16,8 +16,6 @@ from courseware.tests.factories import StudentModuleFactory
from courseware.tests.helpers import LoginEnrollmentTestCase
from courseware.tabs import get_course_tab_list
from django.conf import settings
from django.core.exceptions import ValidationError
from django.core.validators import validate_email
from django.core.urlresolvers import reverse, resolve
from django.utils.timezone import UTC
from django.test.utils import override_settings
......@@ -52,6 +50,11 @@ from ccx_keys.locator import CCXLocator
from lms.djangoapps.ccx.models import CustomCourseForEdX
from lms.djangoapps.ccx.overrides import get_override_for_ccx, override_field_for_ccx
from lms.djangoapps.ccx.tests.factories import CcxFactory
from lms.djangoapps.ccx.tests.utils import (
CcxTestCase,
flatten,
)
from lms.djangoapps.ccx.utils import is_email
from lms.djangoapps.ccx.views import get_date
......@@ -114,96 +117,24 @@ def setup_students_and_grades(context):
)
def is_email(identifier):
"""
Checks if an `identifier` string is a valid email
"""
try:
validate_email(identifier)
except ValidationError:
return False
return True
@attr('shard_1')
@ddt.ddt
class TestCoachDashboard(SharedModuleStoreTestCase, LoginEnrollmentTestCase):
class TestCoachDashboard(CcxTestCase, LoginEnrollmentTestCase):
"""
Tests for Custom Courses views.
"""
MODULESTORE = TEST_DATA_SPLIT_MODULESTORE
@classmethod
def setUpClass(cls):
super(TestCoachDashboard, cls).setUpClass()
cls.course = course = CourseFactory.create()
# Create a course outline
cls.mooc_start = start = datetime.datetime(
2010, 5, 12, 2, 42, tzinfo=pytz.UTC
)
cls.mooc_due = due = datetime.datetime(
2010, 7, 7, 0, 0, tzinfo=pytz.UTC
)
cls.chapters = [
ItemFactory.create(start=start, parent=course) for _ in xrange(2)
]
cls.sequentials = flatten([
[
ItemFactory.create(parent=chapter) for _ in xrange(2)
] for chapter in cls.chapters
])
cls.verticals = flatten([
[
ItemFactory.create(
start=start, due=due, parent=sequential, graded=True, format='Homework', category=u'vertical'
) for _ in xrange(2)
] for sequential in cls.sequentials
])
# Trying to wrap the whole thing in a bulk operation fails because it
# doesn't find the parents. But we can at least wrap this part...
with cls.store.bulk_operations(course.id, emit_signals=False):
blocks = flatten([ # pylint: disable=unused-variable
[
ItemFactory.create(parent=vertical) for _ in xrange(2)
] for vertical in cls.verticals
])
def setUp(self):
"""
Set up tests
"""
super(TestCoachDashboard, self).setUp()
# Create instructor account
self.coach = coach = AdminFactory.create()
self.client.login(username=coach.username, password="test")
# create an instance of modulestore
self.mstore = modulestore()
def make_coach(self):
"""
create coach user
"""
role = CourseCcxCoachRole(self.course.id)
role.add_users(self.coach)
def make_ccx(self, max_students_allowed=settings.CCX_MAX_STUDENTS_ALLOWED):
"""
create ccx
"""
ccx = CcxFactory(course_id=self.course.id, coach=self.coach)
override_field_for_ccx(ccx, self.course, 'max_student_enrollments_allowed', max_students_allowed)
return ccx
def get_outbox(self):
"""
get fake outbox
"""
from django.core import mail
return mail.outbox
# Login with the instructor account
self.client.login(username=self.coach.username, password="test")
def assert_elements_in_schedule(self, url, n_chapters=2, n_sequentials=4, n_verticals=8):
"""
......@@ -1005,23 +936,3 @@ class TestStudentDashboardWithCCX(ModuleStoreTestCase):
response = self.client.get(reverse('dashboard'))
self.assertEqual(response.status_code, 200)
self.assertTrue(re.search('Test CCX', response.content))
def flatten(seq):
"""
For [[1, 2], [3, 4]] returns [1, 2, 3, 4]. Does not recurse.
"""
return [x for sub in seq for x in sub]
def iter_blocks(course):
"""
Returns an iterator over all of the blocks in a course.
"""
def visit(block):
""" get child blocks """
yield block
for child in block.get_children():
for descendant in visit(child): # wish they'd backport yield from
yield descendant
return visit(course)
"""
Test utils for CCX
"""
import datetime
import pytz
from django.conf import settings
from lms.djangoapps.ccx.overrides import override_field_for_ccx
from lms.djangoapps.ccx.tests.factories import CcxFactory
from student.roles import CourseCcxCoachRole
from student.tests.factories import (
AdminFactory,
)
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.tests.django_utils import (
SharedModuleStoreTestCase,
TEST_DATA_SPLIT_MODULESTORE
)
from xmodule.modulestore.tests.factories import (
CourseFactory,
ItemFactory,
)
class CcxTestCase(SharedModuleStoreTestCase):
"""
General test class to be used in other CCX tests classes.
It creates a course that can be used as master course for CCXs.
"""
MODULESTORE = TEST_DATA_SPLIT_MODULESTORE
@classmethod
def setUpClass(cls):
super(CcxTestCase, cls).setUpClass()
cls.course = course = CourseFactory.create()
# Create a course outline
cls.mooc_start = start = datetime.datetime(
2010, 5, 12, 2, 42, tzinfo=pytz.UTC
)
cls.mooc_due = due = datetime.datetime(
2010, 7, 7, 0, 0, tzinfo=pytz.UTC
)
cls.chapters = [
ItemFactory.create(start=start, parent=course) for _ in xrange(2)
]
cls.sequentials = flatten([
[
ItemFactory.create(parent=chapter) for _ in xrange(2)
] for chapter in cls.chapters
])
cls.verticals = flatten([
[
ItemFactory.create(
start=start, due=due, parent=sequential, graded=True, format='Homework', category=u'vertical'
) for _ in xrange(2)
] for sequential in cls.sequentials
])
# Trying to wrap the whole thing in a bulk operation fails because it
# doesn't find the parents. But we can at least wrap this part...
with cls.store.bulk_operations(course.id, emit_signals=False):
blocks = flatten([ # pylint: disable=unused-variable
[
ItemFactory.create(parent=vertical) for _ in xrange(2)
] for vertical in cls.verticals
])
def setUp(self):
"""
Set up tests
"""
super(CcxTestCase, self).setUp()
# Create instructor account
self.coach = AdminFactory.create()
# create an instance of modulestore
self.mstore = modulestore()
def make_coach(self):
"""
create coach user
"""
role = CourseCcxCoachRole(self.course.id)
role.add_users(self.coach)
def make_ccx(self, max_students_allowed=settings.CCX_MAX_STUDENTS_ALLOWED):
"""
create ccx
"""
ccx = CcxFactory(course_id=self.course.id, coach=self.coach)
override_field_for_ccx(ccx, self.course, 'max_student_enrollments_allowed', max_students_allowed)
return ccx
def get_outbox(self):
"""
get fake outbox
"""
from django.core import mail
return mail.outbox
def flatten(seq):
"""
For [[1, 2], [3, 4]] returns [1, 2, 3, 4]. Does not recurse.
"""
return [x for sub in seq for x in sub]
def iter_blocks(course):
"""
Returns an iterator over all of the blocks in a course.
"""
def visit(block):
""" get child blocks """
yield block
for child in block.get_children():
for descendant in visit(child): # wish they'd backport yield from
yield descendant
return visit(course)
......@@ -6,14 +6,12 @@ Does not include any access control, be sure to check access before calling.
import datetime
import logging
import pytz
from contextlib import contextmanager
from django.contrib.auth.models import User
from django.core.exceptions import ValidationError
from django.utils.translation import ugettext as _
from django.core.validators import validate_email
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
from courseware.courses import get_course_by_id
from courseware.model_data import FieldDataCache
......@@ -24,6 +22,7 @@ from instructor.enrollment import (
)
from instructor.access import allow_access
from instructor.views.tools import get_student_from_identifier
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
from student.models import CourseEnrollment
from student.roles import CourseCcxCoachRole
......@@ -250,3 +249,14 @@ def assign_coach_role_to_ccx(ccx_locator, user, master_course_id):
# assign user role coach on ccx
with ccx_course(ccx_locator) as course:
allow_access(course, user, "ccx_coach", send_email=False)
def is_email(identifier):
"""
Checks if an `identifier` string is a valid email
"""
try:
validate_email(identifier)
except ValidationError:
return False
return True
......@@ -14,7 +14,7 @@ import pystache_custom as pystache
from opaque_keys.edx.locations import i4xEncoder
from opaque_keys.edx.keys import CourseKey
from xmodule.modulestore.django import modulestore
from ccx.overrides import get_current_ccx
from lms.djangoapps.ccx.overrides import get_current_ccx
from django_comment_common.models import Role, FORUM_ROLE_STUDENT
from django_comment_client.permissions import check_permissions_by_view, has_permission, get_team
......
......@@ -10,7 +10,7 @@ from django.test.client import RequestFactory
from django.test.utils import override_settings
from edxmako.shortcuts import render_to_response
from ccx.tests.test_views import setup_students_and_grades
from lms.djangoapps.ccx.tests.test_views import setup_students_and_grades
from courseware.tabs import get_course_tab_list
from courseware.tests.factories import UserFactory
from courseware.tests.helpers import LoginEnrollmentTestCase
......
......@@ -935,6 +935,7 @@ if settings.FEATURES["CUSTOM_COURSES_EDX"]:
urlpatterns += (
url(r'^courses/{}/'.format(settings.COURSE_ID_PATTERN),
include('ccx.urls')),
url(r'^api/ccx/', include('lms.djangoapps.ccx.api.urls', namespace='ccx_api')),
)
# Access to courseware as an LTI provider
......
......@@ -6,7 +6,7 @@ from django.conf import settings
from django.http import Http404
from rest_framework import permissions
from student.roles import CourseStaffRole
from student.roles import CourseStaffRole, CourseInstructorRole
class ApiKeyHeaderPermission(permissions.BasePermission):
......@@ -64,6 +64,15 @@ class IsUserInUrl(permissions.BasePermission):
return True
class IsCourseInstructor(permissions.BasePermission):
"""
Permission to check that user is a course instructor.
"""
def has_object_permission(self, request, view, obj):
return hasattr(request, 'user') and CourseInstructorRole(obj.course_id).has_user(request.user)
class IsUserInUrlOrStaff(IsUserInUrl):
"""
Permission that checks to see if the request user matches the user in the URL or has is_staff access.
......
......@@ -3,13 +3,48 @@
import ddt
from django.test import TestCase, RequestFactory
from openedx.core.lib.api.permissions import IsStaffOrOwner
from student.roles import CourseStaffRole, CourseInstructorRole
from openedx.core.lib.api.permissions import IsStaffOrOwner, IsCourseInstructor
from student.tests.factories import UserFactory
from opaque_keys.edx.keys import CourseKey
class TestObject(object):
""" Fake class for object permission tests. """
user = None
def __init__(self, user=None, course_id=None):
self.user = user
self.course_id = course_id
class IsCourseInstructorTests(TestCase):
""" Test for IsCourseInstructor permission class. """
def setUp(self):
super(IsCourseInstructorTests, self).setUp()
self.permission = IsCourseInstructor()
self.request = RequestFactory().get('/')
self.course_key = CourseKey.from_string('edx/test123/run')
self.obj = TestObject(course_id=self.course_key)
def test_course_staff_has_no_access(self):
user = UserFactory.create()
self.request.user = user
CourseStaffRole(course_key=self.course_key).add_users(user)
self.assertFalse(
self.permission.has_object_permission(self.request, None, self.obj))
def test_course_instructor_has_access(self):
user = UserFactory.create()
self.request.user = user
CourseInstructorRole(course_key=self.course_key).add_users(user)
self.assertTrue(
self.permission.has_object_permission(self.request, None, self.obj))
def test_anonymous_has_no_access(self):
self.assertFalse(
self.permission.has_object_permission(self.request, None, self.obj))
@ddt.ddt
......
......@@ -10,7 +10,8 @@ import branding
# app that handles site status messages
from status.status import get_site_status_msg
from ccx.overrides import get_current_ccx
from microsite_configuration import microsite
from lms.djangoapps.ccx.overrides import get_current_ccx
%>
## Provide a hook for themes to inject branding on top.
......
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