Merge pull request #19146 from edx/bfiller/add-expired-mobile

mobile api changes for course expiration
This commit is contained in:
Bill Filler
2018-11-13 13:22:59 -05:00
committed by GitHub
12 changed files with 432 additions and 169 deletions

View File

@@ -11,23 +11,30 @@ from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.xml_importer import import_course_from_xml
from ..testutils import MobileAPITestCase, MobileAuthTestMixin, MobileCourseAccessTestMixin
from mobile_api.testutils import MobileAPITestCase, MobileAuthTestMixin, MobileCourseAccessTestMixin
from mobile_api.utils import API_V05, API_V1
@ddt.ddt
class TestUpdates(MobileAPITestCase, MobileAuthTestMixin, MobileCourseAccessTestMixin, MilestonesTestCaseMixin):
"""
Tests for /api/mobile/v0.5/course_info/{course_id}/updates
Tests for /api/mobile/{api_version}/course_info/{course_id}/updates
"""
REVERSE_INFO = {'name': 'course-updates-list', 'params': ['course_id']}
REVERSE_INFO = {'name': 'course-updates-list', 'params': ['course_id', 'api_version']}
shard = 3
def verify_success(self, response):
super(TestUpdates, self).verify_success(response)
self.assertEqual(response.data, [])
@ddt.data(True, False)
def test_updates(self, new_format):
@ddt.data(
(True, API_V05),
(True, API_V1),
(False, API_V05),
(False, API_V1),
)
@ddt.unpack
def test_updates(self, new_format, api_version):
"""
Tests updates endpoint with /static in the content.
Tests both new updates format (using "items") and old format (using "data").
@@ -64,7 +71,7 @@ class TestUpdates(MobileAPITestCase, MobileAuthTestMixin, MobileCourseAccessTest
modulestore().update_item(course_updates, self.user.id)
# call API
response = self.api_response()
response = self.api_response(api_version=api_version)
# verify static URLs are replaced in the content returned by the API
self.assertNotIn("\"/static/", response.content)
@@ -85,20 +92,32 @@ class TestUpdates(MobileAPITestCase, MobileAuthTestMixin, MobileCourseAccessTest
@ddt.ddt
class TestHandouts(MobileAPITestCase, MobileAuthTestMixin, MobileCourseAccessTestMixin, MilestonesTestCaseMixin):
"""
Tests for /api/mobile/v0.5/course_info/{course_id}/handouts
Tests for /api/mobile/{api_version}/course_info/{course_id}/handouts
"""
REVERSE_INFO = {'name': 'course-handouts-list', 'params': ['course_id']}
REVERSE_INFO = {'name': 'course-handouts-list', 'params': ['course_id', 'api_version']}
shard = 3
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
def test_handouts(self, default_ms):
@ddt.data(
(ModuleStoreEnum.Type.mongo, API_V05),
(ModuleStoreEnum.Type.mongo, API_V1),
(ModuleStoreEnum.Type.split, API_V05),
(ModuleStoreEnum.Type.split, API_V1),
)
@ddt.unpack
def test_handouts(self, default_ms, api_version):
with self.store.default_store(default_ms):
self.add_mobile_available_toy_course()
response = self.api_response(expected_response_code=200)
response = self.api_response(expected_response_code=200, api_version=api_version)
self.assertIn("Sample", response.data['handouts_html'])
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
def test_no_handouts(self, default_ms):
@ddt.data(
(ModuleStoreEnum.Type.mongo, API_V05),
(ModuleStoreEnum.Type.mongo, API_V1),
(ModuleStoreEnum.Type.split, API_V05),
(ModuleStoreEnum.Type.split, API_V1),
)
@ddt.unpack
def test_no_handouts(self, default_ms, api_version):
with self.store.default_store(default_ms):
self.add_mobile_available_toy_course()
@@ -107,11 +126,17 @@ class TestHandouts(MobileAPITestCase, MobileAuthTestMixin, MobileCourseAccessTes
with self.store.branch_setting(ModuleStoreEnum.Branch.draft_preferred, self.course.id):
self.store.delete_item(handouts_usage_key, self.user.id)
response = self.api_response(expected_response_code=200)
response = self.api_response(expected_response_code=200, api_version=api_version)
self.assertIsNone(response.data['handouts_html'])
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
def test_empty_handouts(self, default_ms):
@ddt.data(
(ModuleStoreEnum.Type.mongo, API_V05),
(ModuleStoreEnum.Type.mongo, API_V1),
(ModuleStoreEnum.Type.split, API_V05),
(ModuleStoreEnum.Type.split, API_V1),
)
@ddt.unpack
def test_empty_handouts(self, default_ms, api_version):
with self.store.default_store(default_ms):
self.add_mobile_available_toy_course()
@@ -120,11 +145,17 @@ class TestHandouts(MobileAPITestCase, MobileAuthTestMixin, MobileCourseAccessTes
underlying_handouts = self.store.get_item(handouts_usage_key)
underlying_handouts.data = "<ol></ol>"
self.store.update_item(underlying_handouts, self.user.id)
response = self.api_response(expected_response_code=200)
response = self.api_response(expected_response_code=200, api_version=api_version)
self.assertIsNone(response.data['handouts_html'])
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
def test_handouts_static_rewrites(self, default_ms):
@ddt.data(
(ModuleStoreEnum.Type.mongo, API_V05),
(ModuleStoreEnum.Type.mongo, API_V1),
(ModuleStoreEnum.Type.split, API_V05),
(ModuleStoreEnum.Type.split, API_V1),
)
@ddt.unpack
def test_handouts_static_rewrites(self, default_ms, api_version):
with self.store.default_store(default_ms):
self.add_mobile_available_toy_course()
@@ -134,11 +165,17 @@ class TestHandouts(MobileAPITestCase, MobileAuthTestMixin, MobileCourseAccessTes
self.assertIn('\'/static/', underlying_handouts.data)
# but shouldn't finish with any
response = self.api_response()
response = self.api_response(api_version=api_version)
self.assertNotIn('\'/static/', response.data['handouts_html'])
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
def test_jump_to_id_handout_href(self, default_ms):
@ddt.data(
(ModuleStoreEnum.Type.mongo, API_V05),
(ModuleStoreEnum.Type.mongo, API_V1),
(ModuleStoreEnum.Type.split, API_V05),
(ModuleStoreEnum.Type.split, API_V1),
)
@ddt.unpack
def test_jump_to_id_handout_href(self, default_ms, api_version):
with self.store.default_store(default_ms):
self.add_mobile_available_toy_course()
@@ -149,11 +186,17 @@ class TestHandouts(MobileAPITestCase, MobileAuthTestMixin, MobileCourseAccessTes
self.store.update_item(underlying_handouts, self.user.id)
# but shouldn't finish with any
response = self.api_response()
response = self.api_response(api_version=api_version)
self.assertIn("/courses/{}/jump_to_id/".format(self.course.id), response.data['handouts_html'])
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
def test_course_url_handout_href(self, default_ms):
@ddt.data(
(ModuleStoreEnum.Type.mongo, API_V05),
(ModuleStoreEnum.Type.mongo, API_V1),
(ModuleStoreEnum.Type.split, API_V05),
(ModuleStoreEnum.Type.split, API_V1),
)
@ddt.unpack
def test_course_url_handout_href(self, default_ms, api_version):
with self.store.default_store(default_ms):
self.add_mobile_available_toy_course()
@@ -164,7 +207,7 @@ class TestHandouts(MobileAPITestCase, MobileAuthTestMixin, MobileCourseAccessTes
self.store.update_item(underlying_handouts, self.user.id)
# but shouldn't finish with any
response = self.api_response()
response = self.api_response(api_version=api_version)
self.assertIn("/courses/{}/".format(self.course.id), response.data['handouts_html'])
def add_mobile_available_toy_course(self):

View File

@@ -25,6 +25,7 @@ from courseware.access_response import MobileAvailabilityError, StartDateError,
from courseware.tests.factories import UserFactory
from mobile_api.models import IgnoreMobileAvailableFlagConfig
from mobile_api.tests.test_milestones import MobileAPIMilestonesMixin
from mobile_api.utils import API_V1
from student import auth
from student.models import CourseEnrollment
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
@@ -49,6 +50,7 @@ class MobileAPITestCase(ModuleStoreTestCase, APITestCase):
self.user = UserFactory.create()
self.password = 'test'
self.username = self.user.username
self.api_version = API_V1
IgnoreMobileAvailableFlagConfig(enabled=False).save()
def tearDown(self):
@@ -94,6 +96,8 @@ class MobileAPITestCase(ModuleStoreTestCase, APITestCase):
reverse_args.update({'course_id': unicode(kwargs.get('course_id', self.course.id))})
if 'username' in self.REVERSE_INFO['params']:
reverse_args.update({'username': kwargs.get('username', self.user.username)})
if 'api_version' in self.REVERSE_INFO['params']:
reverse_args.update({'api_version': kwargs.get('api_version', self.api_version)})
return reverse(self.REVERSE_INFO['name'], kwargs=reverse_args)
def url_method(self, url, data=None, **kwargs): # pylint: disable=unused-argument

View File

@@ -8,7 +8,7 @@ from .users.views import my_user_info
urlpatterns = [
url(r'^users/', include('mobile_api.users.urls')),
url(r'^my_user_info', my_user_info),
url(r'^my_user_info', my_user_info, name='user-info'),
url(r'^video_outlines/', include('mobile_api.video_outlines.urls')),
url(r'^course_info/', include('mobile_api.course_info.urls')),
]

View File

@@ -1,3 +0,0 @@
"""
A models.py is required to make this an app (until we move to Django 1.7)
"""

View File

@@ -7,6 +7,7 @@ from rest_framework.reverse import reverse
from lms.djangoapps.certificates.api import certificate_downloadable_status
from courseware.access import has_access
from openedx.features.course_duration_limits.access import get_user_course_expiration_date
from student.models import CourseEnrollment, User
from util.course import get_encoded_course_sharing_utm_params, get_link_for_about_page
@@ -15,10 +16,11 @@ class CourseOverviewField(serializers.RelatedField):
"""
Custom field to wrap a CourseOverview object. Read-only.
"""
def to_representation(self, course_overview):
course_id = unicode(course_overview.id)
request = self.context.get('request')
api_version = self.context.get('api_version')
return {
# identifiers
'id': course_id,
@@ -56,12 +58,12 @@ class CourseOverviewField(serializers.RelatedField):
'course_sharing_utm_parameters': get_encoded_course_sharing_utm_params(),
'course_updates': reverse(
'course-updates-list',
kwargs={'course_id': course_id},
kwargs={'api_version': api_version, 'course_id': course_id},
request=request,
),
'course_handouts': reverse(
'course-handouts-list',
kwargs={'course_id': course_id},
kwargs={'api_version': api_version, 'course_id': course_id},
request=request,
),
'discussion_url': reverse(
@@ -72,7 +74,7 @@ class CourseOverviewField(serializers.RelatedField):
'video_outline': reverse(
'video-summary-list',
kwargs={'course_id': course_id},
kwargs={'api_version': api_version, 'course_id': course_id},
request=request,
),
}
@@ -84,6 +86,13 @@ class CourseEnrollmentSerializer(serializers.ModelSerializer):
"""
course = CourseOverviewField(source="course_overview", read_only=True)
certificate = serializers.SerializerMethodField()
audit_access_expires = serializers.SerializerMethodField()
def get_audit_access_expires(self, model):
"""
Returns expiration date for a course audit expiration, if any or null
"""
return get_user_course_expiration_date(model.user, model.course)
def get_certificate(self, model):
"""Returns the information about the user's certificate in the course."""
@@ -97,21 +106,39 @@ class CourseEnrollmentSerializer(serializers.ModelSerializer):
else:
return {}
class Meta(object):
model = CourseEnrollment
fields = ('audit_access_expires', 'created', 'mode', 'is_active', 'course', 'certificate')
lookup_field = 'username'
class CourseEnrollmentSerializerv05(CourseEnrollmentSerializer):
"""
Serializes CourseEnrollment models for v0.5 api
Does not include 'audit_access_expires' field that is present in v1 api
"""
class Meta(object):
model = CourseEnrollment
fields = ('created', 'mode', 'is_active', 'course', 'certificate')
lookup_field = 'username'
class UserSerializer(serializers.HyperlinkedModelSerializer):
class UserSerializer(serializers.ModelSerializer):
"""
Serializes User models
"""
name = serializers.ReadOnlyField(source='profile.name')
course_enrollments = serializers.HyperlinkedIdentityField(
view_name='courseenrollment-detail',
lookup_field='username'
)
course_enrollments = serializers.SerializerMethodField()
def get_course_enrollments(self, model):
request = self.context.get('request')
api_version = self.context.get('api_version')
return reverse(
'courseenrollment-detail',
kwargs={'api_version': api_version, 'username': model.username},
request=request
)
class Meta(object):
model = User

View File

@@ -9,6 +9,7 @@ from django.conf import settings
from django.template import defaultfilters
from django.test import RequestFactory, override_settings
from django.utils import timezone
from django.utils.timezone import now
from milestones.tests.utils import MilestonesTestCaseMixin
from mock import patch
@@ -24,46 +25,54 @@ from mobile_api.testutils import (
MobileAuthUserTestMixin,
MobileCourseAccessTestMixin
)
from mobile_api.utils import API_V05, API_V1
from openedx.core.lib.courses import course_image_url
from openedx.core.lib.tests import attr
from openedx.core.djangoapps.schedules.tests.factories import ScheduleFactory
from openedx.core.djangoapps.waffle_utils.testutils import override_waffle_flag
from openedx.features.course_duration_limits.config import CONTENT_TYPE_GATING_FLAG
from student.models import CourseEnrollment
from student.tests.factories import CourseEnrollmentFactory
from util.milestones_helpers import set_prerequisite_courses
from util.testing import UrlResetMixin
from xmodule.course_module import DEFAULT_START_DATE
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from .. import errors
from .serializers import CourseEnrollmentSerializer
from .serializers import CourseEnrollmentSerializer, CourseEnrollmentSerializerv05
@attr(shard=9)
@ddt.ddt
class TestUserDetailApi(MobileAPITestCase, MobileAuthUserTestMixin):
"""
Tests for /api/mobile/v0.5/users/<user_name>...
Tests for /api/mobile/{api_version}/users/<user_name>...
"""
REVERSE_INFO = {'name': 'user-detail', 'params': ['username']}
REVERSE_INFO = {'name': 'user-detail', 'params': ['username', 'api_version']}
def test_success(self):
@ddt.data(API_V05, API_V1)
def test_success(self, api_version):
self.login()
response = self.api_response()
response = self.api_response(api_version=api_version)
self.assertEqual(response.data['username'], self.user.username)
self.assertEqual(response.data['email'], self.user.email)
@attr(shard=9)
@ddt.ddt
class TestUserInfoApi(MobileAPITestCase, MobileAuthTestMixin):
"""
Tests for /api/mobile/v0.5/my_user_info
Tests for /api/mobile/{api_version}/my_user_info
"""
def reverse_url(self, reverse_args=None, **kwargs):
return '/api/mobile/v0.5/my_user_info'
REVERSE_INFO = {'name': 'user-info', 'params': ['api_version']}
def test_success(self):
@ddt.data(API_V05, API_V1)
def test_success(self, api_version):
"""Verify the endpoint redirects to the user detail endpoint"""
self.login()
response = self.api_response(expected_response_code=302)
response = self.api_response(expected_response_code=302, api_version=api_version)
self.assertIn(self.username, response['location'])
@@ -73,14 +82,15 @@ class TestUserInfoApi(MobileAPITestCase, MobileAuthTestMixin):
class TestUserEnrollmentApi(UrlResetMixin, MobileAPITestCase, MobileAuthUserTestMixin,
MobileCourseAccessTestMixin, MilestonesTestCaseMixin):
"""
Tests for /api/mobile/v0.5/users/<user_name>/course_enrollments/
Tests for /api/mobile/{api_version}/users/<user_name>/course_enrollments/
"""
REVERSE_INFO = {'name': 'courseenrollment-detail', 'params': ['username']}
REVERSE_INFO = {'name': 'courseenrollment-detail', 'params': ['username', 'api_version']}
ALLOW_ACCESS_TO_UNRELEASED_COURSE = True
ALLOW_ACCESS_TO_MILESTONE_COURSE = True
ALLOW_ACCESS_TO_NON_VISIBLE_COURSE = True
NEXT_WEEK = datetime.datetime.now(pytz.UTC) + datetime.timedelta(days=7)
LAST_WEEK = datetime.datetime.now(pytz.UTC) - datetime.timedelta(days=7)
THREE_YEARS_AGO = now() - datetime.timedelta(days=(365 * 3))
ADVERTISED_START = "Spring 2016"
ENABLED_SIGNALS = ['course_published']
DATES = {
@@ -121,7 +131,8 @@ class TestUserEnrollmentApi(UrlResetMixin, MobileAPITestCase, MobileAuthUserTest
self.assertEqual(len(courses), 0)
@patch.dict(settings.FEATURES, {'ENABLE_MKTG_SITE': True})
def test_sort_order(self):
@ddt.data(API_V05, API_V1)
def test_sort_order(self, api_version):
self.login()
num_courses = 3
@@ -131,19 +142,20 @@ class TestUserEnrollmentApi(UrlResetMixin, MobileAPITestCase, MobileAuthUserTest
self.enroll(courses[course_index].id)
# verify courses are returned in the order of enrollment, with most recently enrolled first.
response = self.api_response()
response = self.api_response(api_version=api_version)
for course_index in range(num_courses):
self.assertEqual(
response.data[course_index]['course']['id'],
unicode(courses[num_courses - course_index - 1].id)
)
@ddt.data(API_V05, API_V1)
@patch.dict(settings.FEATURES, {
'ENABLE_PREREQUISITE_COURSES': True,
'DISABLE_START_DATES': False,
'ENABLE_MKTG_SITE': True,
})
def test_courseware_access(self):
def test_courseware_access(self, api_version):
self.login()
course_with_prereq = CourseFactory.create(start=self.LAST_WEEK, mobile_available=True)
@@ -170,7 +182,7 @@ class TestUserEnrollmentApi(UrlResetMixin, MobileAPITestCase, MobileAuthUserTest
self.enroll(course.id)
# Verify courses have the correct response through error code. Last enrolled course is first course in response
response = self.api_response()
response = self.api_response(api_version=api_version)
for course_index in range(len(courses)):
result = response.data[course_index]['course']['courseware_access']
self.assertEqual(result['error_code'], expected_error_codes[::-1][course_index])
@@ -179,16 +191,22 @@ class TestUserEnrollmentApi(UrlResetMixin, MobileAPITestCase, MobileAuthUserTest
self.assertFalse(result['has_access'])
@ddt.data(
('next_week', ADVERTISED_START, ADVERTISED_START, "string"),
('next_week', None, defaultfilters.date(NEXT_WEEK, "DATE_FORMAT"), "timestamp"),
('next_week', '', defaultfilters.date(NEXT_WEEK, "DATE_FORMAT"), "timestamp"),
('default_start_date', ADVERTISED_START, ADVERTISED_START, "string"),
('default_start_date', '', None, "empty"),
('default_start_date', None, None, "empty"),
('next_week', ADVERTISED_START, ADVERTISED_START, "string", API_V05),
('next_week', ADVERTISED_START, ADVERTISED_START, "string", API_V1),
('next_week', None, defaultfilters.date(NEXT_WEEK, "DATE_FORMAT"), "timestamp", API_V05),
('next_week', None, defaultfilters.date(NEXT_WEEK, "DATE_FORMAT"), "timestamp", API_V1),
('next_week', '', defaultfilters.date(NEXT_WEEK, "DATE_FORMAT"), "timestamp", API_V05),
('next_week', '', defaultfilters.date(NEXT_WEEK, "DATE_FORMAT"), "timestamp", API_V1),
('default_start_date', ADVERTISED_START, ADVERTISED_START, "string", API_V05),
('default_start_date', ADVERTISED_START, ADVERTISED_START, "string", API_V1),
('default_start_date', '', None, "empty", API_V05),
('default_start_date', '', None, "empty", API_V1),
('default_start_date', None, None, "empty", API_V05),
('default_start_date', None, None, "empty", API_V1),
)
@ddt.unpack
@patch.dict(settings.FEATURES, {'DISABLE_START_DATES': False, 'ENABLE_MKTG_SITE': True})
def test_start_type_and_display(self, start, advertised_start, expected_display, expected_type):
def test_start_type_and_display(self, start, advertised_start, expected_display, expected_type, api_version):
"""
Tests that the correct start_type and start_display are returned in the
case the course has not started
@@ -197,19 +215,21 @@ class TestUserEnrollmentApi(UrlResetMixin, MobileAPITestCase, MobileAuthUserTest
course = CourseFactory.create(start=self.DATES[start], advertised_start=advertised_start, mobile_available=True)
self.enroll(course.id)
response = self.api_response()
response = self.api_response(api_version=api_version)
self.assertEqual(response.data[0]['course']['start_type'], expected_type)
self.assertEqual(response.data[0]['course']['start_display'], expected_display)
@ddt.data(API_V05, API_V1)
@patch.dict(settings.FEATURES, {"ENABLE_DISCUSSION_SERVICE": True, 'ENABLE_MKTG_SITE': True})
def test_discussion_url(self):
def test_discussion_url(self, api_version):
self.login_and_enroll()
response = self.api_response()
response = self.api_response(api_version=api_version)
response_discussion_url = response.data[0]['course']['discussion_url']
self.assertIn('/api/discussion/v1/courses/{}'.format(self.course.id), response_discussion_url)
def test_org_query(self):
@ddt.data(API_V05, API_V1)
def test_org_query(self, api_version):
self.login()
# Create list of courses with various organizations
@@ -226,7 +246,7 @@ class TestUserEnrollmentApi(UrlResetMixin, MobileAPITestCase, MobileAuthUserTest
for course in courses:
self.enroll(course.id)
response = self.api_response(data={'org': 'edX'})
response = self.api_response(data={'org': 'edX'}, api_version=api_version)
# Test for 3 expected courses
self.assertEqual(len(response.data), 3)
@@ -235,14 +255,73 @@ class TestUserEnrollmentApi(UrlResetMixin, MobileAPITestCase, MobileAuthUserTest
for entry in response.data:
self.assertEqual(entry['course']['org'], 'edX')
def create_enrollment(self, expired):
if expired:
course = CourseFactory.create(start=self.THREE_YEARS_AGO, mobile_available=True)
enrollment = CourseEnrollmentFactory.create(
user=self.user,
course_id=course.id
)
ScheduleFactory(start=self.THREE_YEARS_AGO, enrollment=enrollment)
else:
course = CourseFactory.create(start=self.LAST_WEEK, mobile_available=True)
self.enroll(course.id)
def _get_enrollment_data(self, api_version, expired):
self.login()
self.create_enrollment(expired)
return self.api_response(api_version=api_version).data
def _assert_enrollment_results(self, api_version, courses, num_courses_returned):
self.assertEqual(len(courses), num_courses_returned)
if api_version == API_V05:
if num_courses_returned:
self.assertFalse('audit_access_expires' in courses[0])
else:
self.assertTrue('audit_access_expires' in courses[0])
self.assertIsNotNone(courses[0].get('audit_access_expires'))
@ddt.data(
(API_V05, True, 0),
(API_V05, False, 1),
(API_V1, True, 1),
(API_V1, False, 1),
)
@ddt.unpack
@override_waffle_flag(CONTENT_TYPE_GATING_FLAG, True)
def test_enrollment_with_gating(self, api_version, expired, num_courses_returned):
'''
Test that expired courses are only returned in v1 of API
when waffle flag enabled, and un-expired courses always returned
'''
courses = self._get_enrollment_data(api_version, expired)
self._assert_enrollment_results(api_version, courses, num_courses_returned)
@ddt.data(
(API_V05, True, 1),
(API_V05, False, 1),
(API_V1, True, 1),
(API_V1, False, 1),
)
@ddt.unpack
@override_waffle_flag(CONTENT_TYPE_GATING_FLAG, False)
def test_enrollment_no_gating(self, api_version, expired, num_courses_returned):
'''
Test that expired and non-expired courses returned if waffle flag is disabled
regarless of version of API
'''
courses = self._get_enrollment_data(api_version, expired)
self._assert_enrollment_results(api_version, courses, num_courses_returned)
@attr(shard=9)
@override_settings(MKTG_URLS={'ROOT': 'dummy-root'})
class TestUserEnrollmentCertificates(UrlResetMixin, MobileAPITestCase, MilestonesTestCaseMixin):
"""
Tests for /api/mobile/v0.5/users/<user_name>/course_enrollments/
Tests for /api/mobile/{api_version}/users/<user_name>/course_enrollments/
"""
REVERSE_INFO = {'name': 'courseenrollment-detail', 'params': ['username']}
REVERSE_INFO = {'name': 'courseenrollment-detail', 'params': ['username', 'api_version']}
ENABLED_SIGNALS = ['course_published']
def verify_pdf_certificate(self):
@@ -315,9 +394,9 @@ class TestUserEnrollmentCertificates(UrlResetMixin, MobileAPITestCase, Milestone
@attr(shard=9)
class CourseStatusAPITestCase(MobileAPITestCase):
"""
Base test class for /api/mobile/v0.5/users/<user_name>/course_status_info/{course_id}
Base test class for /api/mobile/{api_version}/users/<user_name>/course_status_info/{course_id}
"""
REVERSE_INFO = {'name': 'user-course-status', 'params': ['username', 'course_id']}
REVERSE_INFO = {'name': 'user-course-status', 'params': ['username', 'course_id', 'api_version']}
def setUp(self):
"""
@@ -472,6 +551,7 @@ class TestCourseStatusPATCH(CourseStatusAPITestCase, MobileAuthUserTestMixin,
@attr(shard=9)
@ddt.ddt
@patch.dict(settings.FEATURES, {'ENABLE_MKTG_SITE': True})
@override_settings(MKTG_URLS={'ROOT': 'dummy-root'})
class TestCourseEnrollmentSerializer(MobileAPITestCase, MilestonesTestCaseMixin):
@@ -486,14 +566,38 @@ class TestCourseEnrollmentSerializer(MobileAPITestCase, MilestonesTestCaseMixin)
self.request = RequestFactory().get('/')
self.request.user = self.user
def test_success(self):
serialized = CourseEnrollmentSerializer(
def get_serialized_data(self, api_version):
'''
Return data from CourseEnrollmentSerializer
'''
if api_version == API_V05:
serializer = CourseEnrollmentSerializerv05
else:
serializer = CourseEnrollmentSerializer
return serializer(
CourseEnrollment.enrollments_for_user(self.user)[0],
context={'request': self.request},
context={'request': self.request, 'api_version': api_version},
).data
def _expiration_in_response(self, response, api_version):
'''
Assert that audit_access_expires field in present in response
based on version of api being used
'''
if api_version != API_V05:
self.assertTrue('audit_access_expires' in response)
self.assertIsNotNone(response.get('audit_access_expires'))
else:
self.assertFalse('audit_access_expires' in response)
@ddt.data(API_V05, API_V1)
def test_success(self, api_version):
serialized = self.get_serialized_data(api_version)
self.assertEqual(serialized['course']['name'], self.course.display_name)
self.assertEqual(serialized['course']['number'], self.course.id.course)
self.assertEqual(serialized['course']['org'], self.course.id.org)
self._expiration_in_response(serialized, api_version)
# Assert utm parameters
expected_utm_parameters = {
@@ -502,14 +606,13 @@ class TestCourseEnrollmentSerializer(MobileAPITestCase, MilestonesTestCaseMixin)
}
self.assertEqual(serialized['course']['course_sharing_utm_parameters'], expected_utm_parameters)
def test_with_display_overrides(self):
@ddt.data(API_V05, API_V1)
def test_with_display_overrides(self, api_version):
self.course.display_coursenumber = "overridden_number"
self.course.display_organization = "overridden_org"
self.store.update_item(self.course, self.user.id)
serialized = CourseEnrollmentSerializer(
CourseEnrollment.enrollments_for_user(self.user)[0],
context={'request': self.request},
).data
serialized = self.get_serialized_data(api_version)
self.assertEqual(serialized['course']['number'], self.course.display_coursenumber)
self.assertEqual(serialized['course']['org'], self.course.display_organization)
self._expiration_in_response(serialized, api_version)

View File

@@ -19,13 +19,17 @@ from courseware.model_data import FieldDataCache
from courseware.module_render import get_module_for_descriptor
from courseware.views.index import save_positions_recursively_up
from experiments.models import ExperimentData, ExperimentKeyValue
from lms.djangoapps.courseware.access_utils import ACCESS_GRANTED
from mobile_api.utils import API_V05
from openedx.features.course_duration_limits.access import check_course_expired
from openedx.features.course_duration_limits.config import CONTENT_TYPE_GATING_FLAG
from student.models import CourseEnrollment, User
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.exceptions import ItemNotFoundError
from .. import errors
from ..decorators import mobile_course_access, mobile_view
from .serializers import CourseEnrollmentSerializer, UserSerializer
from .serializers import CourseEnrollmentSerializer, CourseEnrollmentSerializerv05, UserSerializer
@mobile_view(is_user=True)
@@ -43,7 +47,7 @@ class UserDetail(generics.RetrieveAPIView):
**Example Request**
GET /api/mobile/v0.5/users/{username}
GET /api/mobile/{version}/users/{username}
**Response Values**
@@ -64,6 +68,11 @@ class UserDetail(generics.RetrieveAPIView):
serializer_class = UserSerializer
lookup_field = 'username'
def get_serializer_context(self):
context = super(UserDetail, self).get_serializer_context()
context['api_version'] = self.kwargs.get('api_version')
return context
@mobile_view(is_user=True)
class UserCourseStatus(views.APIView):
@@ -75,9 +84,9 @@ class UserCourseStatus(views.APIView):
**Example Requests**
GET /api/mobile/v0.5/users/{username}/course_status_info/{course_id}
GET /api/mobile/{version}/users/{username}/course_status_info/{course_id}
PATCH /api/mobile/v0.5/users/{username}/course_status_info/{course_id}
PATCH /api/mobile/{version}/users/{username}/course_status_info/{course_id}
**PATCH Parameters**
@@ -209,9 +218,14 @@ class UserCourseEnrollmentsList(generics.ListAPIView):
Get information about the courses that the currently signed in user is
enrolled in.
v1 differs from v0.5 version by returning ALL enrollments for
a user rather than only the enrollments the user has access to (that haven't expired).
An additional attribute "expiration" has been added to the response, which lists the date
when access to the course will expire or null if it doesn't expire.
**Example Request**
GET /api/mobile/v0.5/users/{username}/course_enrollments/
GET /api/mobile/v1/users/{username}/course_enrollments/
**Response Values**
@@ -220,6 +234,8 @@ class UserCourseEnrollmentsList(generics.ListAPIView):
The HTTP 200 response has the following values.
* expiration: The course expiration date for given user course pair
or null if the course does not expire.
* certificate: Information about the user's earned certificate in the
course.
* course: A collection of the following data about the course.
@@ -258,7 +274,6 @@ class UserCourseEnrollmentsList(generics.ListAPIView):
* url: URL to the downloadable version of the certificate, if exists.
"""
queryset = CourseEnrollment.objects.all()
serializer_class = CourseEnrollmentSerializer
lookup_field = 'username'
# In Django Rest Framework v3, there is a default pagination
@@ -313,24 +328,48 @@ class UserCourseEnrollmentsList(generics.ListAPIView):
return False
def get_serializer_context(self):
context = super(UserCourseEnrollmentsList, self).get_serializer_context()
context['api_version'] = self.kwargs.get('api_version')
return context
def get_serializer_class(self):
api_version = self.kwargs.get('api_version')
if api_version == API_V05:
return CourseEnrollmentSerializerv05
return CourseEnrollmentSerializer
def get_queryset(self):
api_version = self.kwargs.get('api_version')
enrollments = self.queryset.filter(
user__username=self.kwargs['username'],
is_active=True
).order_by('created').reverse()
org = self.request.query_params.get('org', None)
return [
enrollment for enrollment in enrollments
if enrollment.course_overview and self.is_org(org, enrollment.course_overview.org) and
is_mobile_available_for_user(self.request.user, enrollment.course_overview) and
not self.hide_course_for_enrollment_fee_experiment(self.request.user, enrollment)
]
if api_version == API_V05:
# for v0.5 don't return expired courses
return [
enrollment for enrollment in enrollments
if enrollment.course_overview and self.is_org(org, enrollment.course_overview.org) and
is_mobile_available_for_user(self.request.user, enrollment.course_overview) and
not self.hide_course_for_enrollment_fee_experiment(self.request.user, enrollment) and
(not CONTENT_TYPE_GATING_FLAG.is_enabled() or
check_course_expired(self.request.user, enrollment.course) == ACCESS_GRANTED)
]
else:
# return all courses, with associated expiration
return [
enrollment for enrollment in enrollments
if enrollment.course_overview and self.is_org(org, enrollment.course_overview.org) and
is_mobile_available_for_user(self.request.user, enrollment.course_overview)
]
@api_view(["GET"])
@mobile_view()
def my_user_info(request):
def my_user_info(request, api_version):
"""
Redirect to the currently-logged-in user's info page
"""
return redirect("user-detail", username=request.user.username)
return redirect("user-detail", api_version=api_version, username=request.user.username)

View File

@@ -1,6 +1,8 @@
"""
Common utility methods for Mobile APIs.
"""
API_V05 = 'v0.5'
API_V1 = 'v1'
def parsed_version(version):

View File

@@ -17,11 +17,12 @@ class BlockOutline(object):
"""
Serializes course videos, pulling data from VAL and the video modules.
"""
def __init__(self, course_id, start_block, block_types, request, video_profiles):
def __init__(self, course_id, start_block, block_types, request, video_profiles, api_version):
"""Create a BlockOutline using `start_block` as a starting point."""
self.start_block = start_block
self.block_types = block_types
self.course_id = course_id
self.api_version = api_version
self.request = request # needed for making full URLS
self.local_cache = {}
try:
@@ -81,7 +82,13 @@ class BlockOutline(object):
"named_path": [b["name"] for b in block_path],
"unit_url": unit_url,
"section_url": section_url,
"summary": summary_fn(self.course_id, curr_block, self.request, self.local_cache)
"summary": summary_fn(
self.course_id,
curr_block,
self.request,
self.local_cache,
self.api_version
)
}
if curr_block.has_children:
@@ -104,7 +111,7 @@ def path(block, child_to_parent, start_block):
if block is not start_block:
block_path.append({
# to be consistent with other edx-platform clients, return the defaulted display name
'name': block.display_name_with_default_escaped,
'name': block.display_name_with_default_escaped, # xss-lint: disable=python-deprecated-display-name
'category': block.category,
'id': unicode(block.location)
})
@@ -160,7 +167,7 @@ def find_urls(course_id, block, child_to_parent, request):
return unit_url, section_url
def video_summary(video_profiles, course_id, video_descriptor, request, local_cache):
def video_summary(video_profiles, course_id, video_descriptor, request, local_cache, api_version):
"""
returns summary dict for the given video module
"""
@@ -224,7 +231,8 @@ def video_summary(video_profiles, course_id, video_descriptor, request, local_ca
kwargs={
'course_id': unicode(course_id),
'block_id': video_descriptor.scope_ids.usage_id.block_id,
'lang': lang
'lang': lang,
'api_version': api_version
},
request=request,
)

View File

@@ -16,7 +16,12 @@ from milestones.tests.utils import MilestonesTestCaseMixin
from mock import patch
from mobile_api.models import MobileApiConfig
from mobile_api.testutils import MobileAPITestCase, MobileAuthTestMixin, MobileCourseAccessTestMixin
from mobile_api.testutils import (
MobileAPITestCase,
MobileAuthTestMixin,
MobileCourseAccessTestMixin
)
from mobile_api.utils import API_V05, API_V1
from openedx.core.djangoapps.course_groups.cohorts import add_user_to_cohort, remove_user_from_cohort
from openedx.core.djangoapps.course_groups.models import CourseUserGroupPartitionGroup
from openedx.core.djangoapps.course_groups.tests.helpers import CohortFactory
@@ -204,11 +209,12 @@ class TestVideoAPIMixin(object):
@attr(shard=9)
@ddt.ddt
class TestNonStandardCourseStructure(MobileAPITestCase, TestVideoAPIMixin, MilestonesTestCaseMixin):
"""
Tests /api/mobile/v0.5/video_outlines/courses/{course_id} with no course set
Tests /api/mobile/{api_version}/video_outlines/courses/{course_id} with no course set
"""
REVERSE_INFO = {'name': 'video-summary-list', 'params': ['course_id']}
REVERSE_INFO = {'name': 'video-summary-list', 'params': ['course_id', 'api_version']}
def setUp(self):
super(TestNonStandardCourseStructure, self).setUp()
@@ -238,7 +244,8 @@ class TestNonStandardCourseStructure(MobileAPITestCase, TestVideoAPIMixin, Miles
display_name=u"test factory vertical under section omega \u03a9",
)
def test_structure_course_video(self):
@ddt.data(API_V05, API_V1)
def test_structure_course_video(self, api_version):
"""
Tests when there is a video without a vertical directly under course
"""
@@ -248,7 +255,7 @@ class TestNonStandardCourseStructure(MobileAPITestCase, TestVideoAPIMixin, Miles
category="video",
display_name=u"test factory video omega \u03a9",
)
course_outline = self.api_response().data
course_outline = self.api_response(api_version=api_version).data
self.assertEqual(len(course_outline), 1)
section_url = course_outline[0]["section_url"]
unit_url = course_outline[0]["unit_url"]
@@ -257,7 +264,8 @@ class TestNonStandardCourseStructure(MobileAPITestCase, TestVideoAPIMixin, Miles
self._verify_paths(course_outline, [])
def test_structure_course_vert_video(self):
@ddt.data(API_V05, API_V1)
def test_structure_course_vert_video(self, api_version):
"""
Tests when there is a video under vertical directly under course
"""
@@ -267,7 +275,7 @@ class TestNonStandardCourseStructure(MobileAPITestCase, TestVideoAPIMixin, Miles
category="video",
display_name=u"test factory video omega \u03a9",
)
course_outline = self.api_response().data
course_outline = self.api_response(api_version=api_version).data
self.assertEqual(len(course_outline), 1)
section_url = course_outline[0]["section_url"]
unit_url = course_outline[0]["unit_url"]
@@ -284,7 +292,8 @@ class TestNonStandardCourseStructure(MobileAPITestCase, TestVideoAPIMixin, Miles
]
)
def test_structure_course_chap_video(self):
@ddt.data(API_V05, API_V1)
def test_structure_course_chap_video(self, api_version):
"""
Tests when there is a video directly under chapter
"""
@@ -295,7 +304,7 @@ class TestNonStandardCourseStructure(MobileAPITestCase, TestVideoAPIMixin, Miles
category="video",
display_name=u"test factory video omega \u03a9",
)
course_outline = self.api_response().data
course_outline = self.api_response(api_version=api_version).data
self.assertEqual(len(course_outline), 1)
section_url = course_outline[0]["section_url"]
unit_url = course_outline[0]["unit_url"]
@@ -313,7 +322,8 @@ class TestNonStandardCourseStructure(MobileAPITestCase, TestVideoAPIMixin, Miles
]
)
def test_structure_course_section_video(self):
@ddt.data(API_V05, API_V1)
def test_structure_course_section_video(self, api_version):
"""
Tests when chapter is none, and video under section under course
"""
@@ -323,7 +333,7 @@ class TestNonStandardCourseStructure(MobileAPITestCase, TestVideoAPIMixin, Miles
category="video",
display_name=u"test factory video omega \u03a9",
)
course_outline = self.api_response().data
course_outline = self.api_response(api_version=api_version).data
self.assertEqual(len(course_outline), 1)
section_url = course_outline[0]["section_url"]
unit_url = course_outline[0]["unit_url"]
@@ -341,7 +351,8 @@ class TestNonStandardCourseStructure(MobileAPITestCase, TestVideoAPIMixin, Miles
]
)
def test_structure_course_chap_section_video(self):
@ddt.data(API_V05, API_V1)
def test_structure_course_chap_section_video(self, api_version):
"""
Tests when chapter and sequential exists, with a video with no vertical.
"""
@@ -352,7 +363,7 @@ class TestNonStandardCourseStructure(MobileAPITestCase, TestVideoAPIMixin, Miles
category="video",
display_name=u"meow factory video omega \u03a9",
)
course_outline = self.api_response().data
course_outline = self.api_response(api_version=api_version).data
self.assertEqual(len(course_outline), 1)
section_url = course_outline[0]["section_url"]
unit_url = course_outline[0]["unit_url"]
@@ -374,7 +385,8 @@ class TestNonStandardCourseStructure(MobileAPITestCase, TestVideoAPIMixin, Miles
]
)
def test_structure_course_section_vert_video(self):
@ddt.data(API_V05, API_V1)
def test_structure_course_section_vert_video(self, api_version):
"""
Tests chapter->section->vertical->unit
"""
@@ -384,7 +396,7 @@ class TestNonStandardCourseStructure(MobileAPITestCase, TestVideoAPIMixin, Miles
category="video",
display_name=u"test factory video omega \u03a9",
)
course_outline = self.api_response().data
course_outline = self.api_response(api_version=api_version).data
self.assertEqual(len(course_outline), 1)
section_url = course_outline[0]["section_url"]
unit_url = course_outline[0]["unit_url"]
@@ -418,14 +430,15 @@ class TestNonStandardCourseStructure(MobileAPITestCase, TestVideoAPIMixin, Miles
class TestVideoSummaryList(TestVideoAPITestCase, MobileAuthTestMixin, MobileCourseAccessTestMixin,
TestVideoAPIMixin, MilestonesTestCaseMixin):
"""
Tests for /api/mobile/v0.5/video_outlines/courses/{course_id}..
Tests for /api/mobile/{api_version}/video_outlines/courses/{course_id}..
"""
REVERSE_INFO = {'name': 'video-summary-list', 'params': ['course_id']}
REVERSE_INFO = {'name': 'video-summary-list', 'params': ['course_id', 'api_version']}
def test_only_on_web(self):
@ddt.data(API_V05, API_V1)
def test_only_on_web(self, api_version):
self.login_and_enroll()
course_outline = self.api_response().data
course_outline = self.api_response(api_version=api_version).data
self.assertEqual(len(course_outline), 0)
subid = uuid4().hex
@@ -448,7 +461,7 @@ class TestVideoSummaryList(TestVideoAPITestCase, MobileAuthTestMixin, MobileCour
subid=subid
)
course_outline = self.api_response().data
course_outline = self.api_response(api_version=api_version).data
self.assertEqual(len(course_outline), 1)
@@ -462,7 +475,8 @@ class TestVideoSummaryList(TestVideoAPITestCase, MobileAuthTestMixin, MobileCour
self.assertEqual(course_outline[0]["summary"]["category"], "video")
self.assertTrue(course_outline[0]["summary"]["only_on_web"])
def test_mobile_api_video_profiles(self):
@ddt.data(API_V05, API_V1)
def test_mobile_api_video_profiles(self, api_version):
"""
Tests VideoSummaryList with different MobileApiConfig video_profiles
"""
@@ -505,7 +519,7 @@ class TestVideoSummaryList(TestVideoAPITestCase, MobileAuthTestMixin, MobileCour
'video_url': self.video_url_high,
'duration': 12.0,
'transcripts': {
'en': 'http://testserver/api/mobile/v0.5/video_outlines/transcripts/{}/testing_mobile_high_video/en'.format(self.course.id) # pylint: disable=line-too-long
'en': 'http://testserver/api/mobile/{api_version}/video_outlines/transcripts/{course_id}/testing_mobile_high_video/en'.format(api_version=api_version, course_id=self.course.id) # pylint: disable=line-too-long
},
'only_on_web': False,
'encoded_videos': {
@@ -529,29 +543,28 @@ class TestVideoSummaryList(TestVideoAPITestCase, MobileAuthTestMixin, MobileCour
with patch.dict(settings.FEATURES, FALLBACK_TO_ENGLISH_TRANSCRIPTS=False):
# Other platform installations may override this setting
# This ensures that the server don't return empty English transcripts when there's none!
self.assertFalse(self.api_response().data[0]['summary'].get('transcripts'))
self.assertFalse(self.api_response(api_version=api_version).data[0]['summary'].get('transcripts'))
# Testing when video_profiles='mobile_low,mobile_high,youtube'
course_outline = self.api_response().data
course_outline = self.api_response(api_version=api_version).data
course_outline[0]['summary'].pop("id")
self.assertEqual(course_outline[0]['summary'], expected_output)
# Testing when there is no mobile_low, and that mobile_high doesn't show
MobileApiConfig(video_profiles="mobile_low,youtube").save()
course_outline = self.api_response().data
course_outline = self.api_response(api_version=api_version).data
expected_output['encoded_videos'].pop('mobile_high')
expected_output['video_url'] = self.youtube_url
expected_output['size'] = 2222
course_outline[0]['summary'].pop("id")
self.assertEqual(course_outline[0]['summary'], expected_output)
# Testing where youtube is the default video over mobile_high
MobileApiConfig(video_profiles="youtube,mobile_high").save()
course_outline = self.api_response().data
course_outline = self.api_response(api_version=api_version).data
expected_output['encoded_videos']['mobile_high'] = {
'url': self.video_url_high,
@@ -561,7 +574,8 @@ class TestVideoSummaryList(TestVideoAPITestCase, MobileAuthTestMixin, MobileCour
course_outline[0]['summary'].pop("id")
self.assertEqual(course_outline[0]['summary'], expected_output)
def test_mobile_api_html5_sources(self):
@ddt.data(API_V05, API_V1)
def test_mobile_api_html5_sources(self, api_version):
"""
Tests VideoSummaryList without the video pipeline, using fallback HTML5 video URLs
"""
@@ -584,17 +598,18 @@ class TestVideoSummaryList(TestVideoAPITestCase, MobileAuthTestMixin, MobileCour
'video_url': self.video_url_low,
'duration': None,
'transcripts': {
'en': 'http://testserver/api/mobile/v0.5/video_outlines/transcripts/{}/testing_html5_sources/en'.format(self.course.id) # pylint: disable=line-too-long
'en': 'http://testserver/api/mobile/{api_version}/video_outlines/transcripts/{course_id}/testing_html5_sources/en'.format(api_version=api_version, course_id=self.course.id) # pylint: disable=line-too-long
},
'only_on_web': False,
'encoded_videos': None,
'size': 0,
}
course_outline = self.api_response().data
course_outline = self.api_response(api_version=api_version).data
self.assertEqual(course_outline[0]['summary'], expected_output)
def test_video_not_in_val(self):
@ddt.data(API_V05, API_V1)
def test_video_not_in_val(self, api_version):
self.login_and_enroll()
self._create_video_with_subs()
ItemFactory.create(
@@ -605,14 +620,15 @@ class TestVideoSummaryList(TestVideoAPITestCase, MobileAuthTestMixin, MobileCour
html5_sources=[self.html5_video_url]
)
summary = self.api_response().data[1]['summary']
summary = self.api_response(api_version=api_version).data[1]['summary']
self.assertEqual(summary['name'], "some non existent video in val")
self.assertIsNone(summary['encoded_videos'])
self.assertIsNone(summary['duration'])
self.assertEqual(summary['size'], 0)
self.assertEqual(summary['video_url'], self.html5_video_url)
def test_course_list(self):
@ddt.data(API_V05, API_V1)
def test_course_list(self, api_version):
self.login_and_enroll()
self._create_video_with_subs()
ItemFactory.create(
@@ -635,7 +651,7 @@ class TestVideoSummaryList(TestVideoAPITestCase, MobileAuthTestMixin, MobileCour
visible_to_staff_only=True,
)
course_outline = self.api_response().data
course_outline = self.api_response(api_version=api_version).data
self.assertEqual(len(course_outline), 3)
vid = course_outline[0]
self.assertIn('test_subsection_omega_%CE%A9', vid['section_url'])
@@ -654,7 +670,8 @@ class TestVideoSummaryList(TestVideoAPITestCase, MobileAuthTestMixin, MobileCour
self.assertEqual(course_outline[2]['summary']['size'], 0)
self.assertFalse(course_outline[2]['summary']['only_on_web'])
def test_with_nameless_unit(self):
@ddt.data(API_V05, API_V1)
def test_with_nameless_unit(self, api_version):
self.login_and_enroll()
ItemFactory.create(
parent=self.nameless_unit,
@@ -662,11 +679,12 @@ class TestVideoSummaryList(TestVideoAPITestCase, MobileAuthTestMixin, MobileCour
edx_video_id=self.edx_video_id,
display_name=u"test draft video omega 2 \u03a9"
)
course_outline = self.api_response().data
course_outline = self.api_response(api_version=api_version).data
self.assertEqual(len(course_outline), 1)
self.assertEqual(course_outline[0]['path'][2]['name'], self.nameless_unit.location.block_id)
def test_with_video_in_sub_section(self):
@ddt.data(API_V05, API_V1)
def test_with_video_in_sub_section(self, api_version):
"""
Tests a non standard xml format where a video is underneath a sequential
@@ -680,7 +698,7 @@ class TestVideoSummaryList(TestVideoAPITestCase, MobileAuthTestMixin, MobileCour
edx_video_id=self.edx_video_id,
display_name=u"video in the sub section"
)
course_outline = self.api_response().data
course_outline = self.api_response(api_version=api_version).data
self.assertEqual(len(course_outline), 1)
self.assertEqual(len(course_outline[0]['path']), 2)
section_url = course_outline[0]["section_url"]
@@ -695,17 +713,17 @@ class TestVideoSummaryList(TestVideoAPITestCase, MobileAuthTestMixin, MobileCour
self.assertEqual(section_url, unit_url)
@ddt.data(
*itertools.product([True, False], ["video", "problem"])
*itertools.product([True, False], ["video", "problem"], [API_V05, API_V1])
)
@ddt.unpack
def test_with_split_block(self, is_user_staff, sub_block_category):
def test_with_split_block(self, is_user_staff, sub_block_category, api_version):
"""Test with split_module->sub_block_category and for both staff and non-staff users."""
self.login_and_enroll()
self.user.is_staff = is_user_staff
self.user.save()
self._setup_split_module(sub_block_category)
video_outline = self.api_response().data
video_outline = self.api_response(api_version=api_version).data
num_video_blocks = 1 if sub_block_category == "video" else 0
self.assertEqual(len(video_outline), num_video_blocks)
for block_index in range(num_video_blocks):
@@ -721,7 +739,8 @@ class TestVideoSummaryList(TestVideoAPITestCase, MobileAuthTestMixin, MobileCour
)
self.assertIn(u"split test block", video_outline[block_index]["summary"]["name"])
def test_with_split_vertical(self):
@ddt.data(API_V05, API_V1)
def test_with_split_vertical(self, api_version):
"""Test with split_module->vertical->video structure."""
self.login_and_enroll()
split_vertical_a, split_vertical_b = self._setup_split_module("vertical")
@@ -737,7 +756,7 @@ class TestVideoSummaryList(TestVideoAPITestCase, MobileAuthTestMixin, MobileCour
display_name=u"video in vertical b",
)
video_outline = self.api_response().data
video_outline = self.api_response(api_version=api_version).data
# user should see only one of the videos (a or b).
self.assertEqual(len(video_outline), 1)
@@ -777,8 +796,14 @@ class TestVideoSummaryList(TestVideoAPITestCase, MobileAuthTestMixin, MobileCour
display_name=u"video for group " + unicode(group_id),
)
@ddt.data("_create_cohorted_video", "_create_cohorted_vertical_with_video")
def test_with_cohorted_content(self, content_creator_method_name):
@ddt.data(
("_create_cohorted_video", API_V05),
("_create_cohorted_video", API_V1),
("_create_cohorted_vertical_with_video", API_V05),
("_create_cohorted_vertical_with_video", API_V1),
)
@ddt.unpack
def test_with_cohorted_content(self, content_creator_method_name, api_version):
self.login_and_enroll()
self._setup_course_partitions(scheme_id='cohort', is_cohorted=True)
@@ -799,7 +824,7 @@ class TestVideoSummaryList(TestVideoAPITestCase, MobileAuthTestMixin, MobileCour
add_user_to_cohort(cohorts[cohort_index], self.user.username)
# should only see video for this cohort
video_outline = self.api_response().data
video_outline = self.api_response(api_version=api_version).data
self.assertEqual(len(video_outline), 1)
self.assertEquals(
u"video for group " + unicode(cohort_index),
@@ -810,16 +835,17 @@ class TestVideoSummaryList(TestVideoAPITestCase, MobileAuthTestMixin, MobileCour
remove_user_from_cohort(cohorts[cohort_index], self.user.username)
# un-cohorted user should see no videos
video_outline = self.api_response().data
video_outline = self.api_response(api_version=api_version).data
self.assertEqual(len(video_outline), 0)
# staff user sees all videos
self.user.is_staff = True
self.user.save()
video_outline = self.api_response().data
video_outline = self.api_response(api_version=api_version).data
self.assertEqual(len(video_outline), 2)
def test_with_hidden_blocks(self):
@ddt.data(API_V05, API_V1)
def test_with_hidden_blocks(self, api_version):
self.login_and_enroll()
hidden_subsection = ItemFactory.create(
parent=self.section,
@@ -845,10 +871,11 @@ class TestVideoSummaryList(TestVideoAPITestCase, MobileAuthTestMixin, MobileCour
category="video",
edx_video_id=self.edx_video_id,
)
course_outline = self.api_response().data
course_outline = self.api_response(api_version=api_version).data
self.assertEqual(len(course_outline), 0)
def test_language(self):
@ddt.data(API_V05, API_V1)
def test_language(self, api_version):
self.login_and_enroll()
video = ItemFactory.create(
parent=self.nameless_unit,
@@ -873,11 +900,12 @@ class TestVideoSummaryList(TestVideoAPITestCase, MobileAuthTestMixin, MobileCour
for case in language_cases:
video.transcripts = case.transcripts
modulestore().update_item(video, self.user.id)
course_outline = self.api_response().data
course_outline = self.api_response(api_version=api_version).data
self.assertEqual(len(course_outline), 1)
self.assertEqual(course_outline[0]['summary']['language'], case.expected_language)
def test_transcripts(self):
@ddt.data(API_V05, API_V1)
def test_transcripts(self, api_version):
self.login_and_enroll()
video = ItemFactory.create(
parent=self.nameless_unit,
@@ -906,7 +934,7 @@ class TestVideoSummaryList(TestVideoAPITestCase, MobileAuthTestMixin, MobileCour
video.transcripts = case.transcripts
video.sub = case.english_subtitle
modulestore().update_item(video, self.user.id)
course_outline = self.api_response().data
course_outline = self.api_response(api_version=api_version).data
self.assertEqual(len(course_outline), 1)
self.assertSetEqual(
set(course_outline[0]['summary']['transcripts'].keys()),
@@ -914,17 +942,24 @@ class TestVideoSummaryList(TestVideoAPITestCase, MobileAuthTestMixin, MobileCour
)
@ddt.data(
({}, '', [], ['en']),
({}, '', ['de'], ['de']),
({}, '', ['en', 'de'], ['en', 'de']),
({}, 'en-subs', ['de'], ['en', 'de']),
({'uk': 1}, 'en-subs', ['de'], ['en', 'uk', 'de']),
({'uk': 1, 'de': 1}, 'en-subs', ['de', 'en'], ['en', 'uk', 'de']),
({}, '', [], ['en'], API_V05),
({}, '', [], ['en'], API_V1),
({}, '', ['de'], ['de'], API_V05),
({}, '', ['de'], ['de'], API_V1),
({}, '', ['en', 'de'], ['en', 'de'], API_V05),
({}, '', ['en', 'de'], ['en', 'de'], API_V1),
({}, 'en-subs', ['de'], ['en', 'de'], API_V05),
({}, 'en-subs', ['de'], ['en', 'de'], API_V1),
({'uk': 1}, 'en-subs', ['de'], ['en', 'uk', 'de'], API_V05),
({'uk': 1}, 'en-subs', ['de'], ['en', 'uk', 'de'], API_V1),
({'uk': 1, 'de': 1}, 'en-subs', ['de', 'en'], ['en', 'uk', 'de'], API_V05),
({'uk': 1, 'de': 1}, 'en-subs', ['de', 'en'], ['en', 'uk', 'de'], API_V1),
)
@ddt.unpack
@patch('xmodule.video_module.transcripts_utils.edxval_api.get_available_transcript_languages')
def test_val_transcripts_with_feature_enabled(self, transcripts, english_sub, val_transcripts,
expected_transcripts, mock_get_transcript_languages):
expected_transcripts, api_version,
mock_get_transcript_languages):
self.login_and_enroll()
video = ItemFactory.create(
parent=self.nameless_unit,
@@ -938,18 +973,19 @@ class TestVideoSummaryList(TestVideoAPITestCase, MobileAuthTestMixin, MobileCour
video.sub = english_sub
modulestore().update_item(video, self.user.id)
course_outline = self.api_response().data
course_outline = self.api_response(api_version=api_version).data
self.assertEqual(len(course_outline), 1)
self.assertItemsEqual(course_outline[0]['summary']['transcripts'].keys(), expected_transcripts)
@attr(shard=9)
@ddt.ddt
class TestTranscriptsDetail(TestVideoAPITestCase, MobileAuthTestMixin, MobileCourseAccessTestMixin,
TestVideoAPIMixin, MilestonesTestCaseMixin):
"""
Tests for /api/mobile/v0.5/video_outlines/transcripts/{course_id}..
Tests for /api/mobile/{api_version}/video_outlines/transcripts/{course_id}..
"""
REVERSE_INFO = {'name': 'video-transcripts-detail', 'params': ['course_id']}
REVERSE_INFO = {'name': 'video-transcripts-detail', 'params': ['course_id', 'api_version']}
def setUp(self):
super(TestTranscriptsDetail, self).setUp()
@@ -963,21 +999,24 @@ class TestTranscriptsDetail(TestVideoAPITestCase, MobileAuthTestMixin, MobileCou
})
return super(TestTranscriptsDetail, self).reverse_url(reverse_args, **kwargs)
def test_incorrect_language(self):
@ddt.data(API_V05, API_V1)
def test_incorrect_language(self, api_version):
self.login_and_enroll()
self.api_response(expected_response_code=404, lang='pl')
self.api_response(expected_response_code=404, lang='pl', api_version=api_version)
def test_transcript_with_unicode_file_name(self):
@ddt.data(API_V05, API_V1)
def test_transcript_with_unicode_file_name(self, api_version):
self.video = self._create_video_with_subs(custom_subid=u'你好')
self.login_and_enroll()
self.api_response(expected_response_code=200, lang='en')
self.api_response(expected_response_code=200, lang='en', api_version=api_version)
@ddt.data(API_V05, API_V1)
@patch(
'xmodule.video_module.transcripts_utils.edxval_api.get_available_transcript_languages',
Mock(return_value=['uk']),
)
@patch('xmodule.video_module.transcripts_utils.edxval_api.get_video_transcript_data')
def test_val_transcript(self, mock_get_video_transcript_content):
def test_val_transcript(self, api_version, mock_get_video_transcript_content):
"""
Tests transcript retrieval view with val transcripts.
"""
@@ -992,7 +1031,7 @@ class TestTranscriptsDetail(TestVideoAPITestCase, MobileAuthTestMixin, MobileCou
self.login_and_enroll()
# Now, make request to retrieval endpoint
response = self.api_response(expected_response_code=200, lang='uk')
response = self.api_response(expected_response_code=200, lang='uk', api_version=api_version)
# Expected headers
expected_content = u'0\n00:00:00,010 --> 00:00:00,100\nHi, welcome to Edx.\n\n'

View File

@@ -84,6 +84,7 @@ class VideoSummaryList(generics.ListAPIView):
{"video": partial(video_summary, video_profiles)},
request,
video_profiles,
kwargs.get('api_version')
)
)
return Response(video_outline)

View File

@@ -147,7 +147,7 @@ urlpatterns = [
if settings.FEATURES.get('ENABLE_MOBILE_REST_API'):
urlpatterns += [
url(r'^api/mobile/v0.5/', include('mobile_api.urls')),
url(r'^api/mobile/(?P<api_version>v(1|0.5))/', include('mobile_api.urls')),
]
if settings.FEATURES.get('ENABLE_OPENBADGES'):