MA-1712: Update Mobile API to include course_about
This commit is contained in:
@@ -139,14 +139,17 @@ class MobileCourseAccessTestMixin(MobileAPIMilestonesMixin):
|
||||
Subclasses can override verify_success, verify_failure, and init_course_access methods.
|
||||
"""
|
||||
ALLOW_ACCESS_TO_UNRELEASED_COURSE = False # pylint: disable=invalid-name
|
||||
ALLOW_ACCESS_TO_NON_VISIBLE_COURSE = False # pylint: disable=invalid-name
|
||||
|
||||
def verify_success(self, response):
|
||||
"""Base implementation of verifying a successful response."""
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def verify_failure(self, response):
|
||||
def verify_failure(self, response, error_type=None):
|
||||
"""Base implementation of verifying a failed response."""
|
||||
self.assertEqual(response.status_code, 404)
|
||||
if error_type:
|
||||
self.assertEqual(response.data, error_type.to_json())
|
||||
|
||||
def init_course_access(self, course_id=None):
|
||||
"""Base implementation of initializing the user for each test."""
|
||||
@@ -201,6 +204,8 @@ class MobileCourseAccessTestMixin(MobileAPIMilestonesMixin):
|
||||
self.init_course_access()
|
||||
self.course.visible_to_staff_only = True
|
||||
self.store.update_item(self.course, self.user.id)
|
||||
if self.ALLOW_ACCESS_TO_NON_VISIBLE_COURSE:
|
||||
should_succeed = True
|
||||
self._verify_response(should_succeed, VisibilityError(), role)
|
||||
|
||||
def _verify_response(self, should_succeed, error_type, role=None):
|
||||
@@ -216,5 +221,4 @@ class MobileCourseAccessTestMixin(MobileAPIMilestonesMixin):
|
||||
if should_succeed:
|
||||
self.verify_success(response)
|
||||
else:
|
||||
self.verify_failure(response)
|
||||
self.assertEqual(response.data, error_type.to_json())
|
||||
self.verify_failure(response, error_type)
|
||||
|
||||
@@ -13,70 +13,83 @@ from xmodule.course_module import DEFAULT_START_DATE
|
||||
|
||||
|
||||
class CourseOverviewField(serializers.RelatedField):
|
||||
"""Custom field to wrap a CourseDescriptor object. Read-only."""
|
||||
"""
|
||||
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', None)
|
||||
if request:
|
||||
video_outline_url = reverse(
|
||||
'video-summary-list',
|
||||
kwargs={'course_id': course_id},
|
||||
request=request
|
||||
)
|
||||
course_updates_url = reverse(
|
||||
'course-updates-list',
|
||||
kwargs={'course_id': course_id},
|
||||
request=request
|
||||
)
|
||||
course_handouts_url = reverse(
|
||||
'course-handouts-list',
|
||||
kwargs={'course_id': course_id},
|
||||
request=request
|
||||
)
|
||||
discussion_url = reverse(
|
||||
'discussion_course',
|
||||
kwargs={'course_id': course_id},
|
||||
request=request
|
||||
) if course_overview.is_discussion_tab_enabled() else None
|
||||
else:
|
||||
video_outline_url = None
|
||||
course_updates_url = None
|
||||
course_handouts_url = None
|
||||
discussion_url = None
|
||||
|
||||
if course_overview.advertised_start is not None:
|
||||
start_type = "string"
|
||||
start_type = 'string'
|
||||
start_display = course_overview.advertised_start
|
||||
elif course_overview.start != DEFAULT_START_DATE:
|
||||
start_type = "timestamp"
|
||||
start_display = defaultfilters.date(course_overview.start, "DATE_FORMAT")
|
||||
start_type = 'timestamp'
|
||||
start_display = defaultfilters.date(course_overview.start, 'DATE_FORMAT')
|
||||
else:
|
||||
start_type = "empty"
|
||||
start_type = 'empty'
|
||||
start_display = None
|
||||
|
||||
request = self.context.get('request')
|
||||
return {
|
||||
"id": course_id,
|
||||
"name": course_overview.display_name,
|
||||
"number": course_overview.display_number_with_default,
|
||||
"org": course_overview.display_org_with_default,
|
||||
"start": course_overview.start,
|
||||
"start_display": start_display,
|
||||
"start_type": start_type,
|
||||
"end": course_overview.end,
|
||||
"course_image": course_overview.course_image_url,
|
||||
"social_urls": {
|
||||
"facebook": course_overview.facebook_url,
|
||||
# identifiers
|
||||
'id': course_id,
|
||||
'name': course_overview.display_name,
|
||||
'number': course_overview.display_number_with_default,
|
||||
'org': course_overview.display_org_with_default,
|
||||
|
||||
# dates
|
||||
'start': course_overview.start,
|
||||
'start_display': start_display,
|
||||
'start_type': start_type,
|
||||
'end': course_overview.end,
|
||||
|
||||
# notification info
|
||||
'subscription_id': course_overview.clean_id(padding_char='_'),
|
||||
|
||||
# access info
|
||||
'courseware_access': has_access(
|
||||
request.user,
|
||||
'load_mobile',
|
||||
course_overview
|
||||
).to_json(),
|
||||
|
||||
# various URLs
|
||||
'course_image': course_overview.course_image_url,
|
||||
'course_about': reverse(
|
||||
'about_course',
|
||||
kwargs={'course_id': course_id},
|
||||
request=request,
|
||||
),
|
||||
'course_updates': reverse(
|
||||
'course-updates-list',
|
||||
kwargs={'course_id': course_id},
|
||||
request=request,
|
||||
),
|
||||
'course_handouts': reverse(
|
||||
'course-handouts-list',
|
||||
kwargs={'course_id': course_id},
|
||||
request=request,
|
||||
),
|
||||
'discussion_url': reverse(
|
||||
'discussion_course',
|
||||
kwargs={'course_id': course_id},
|
||||
request=request,
|
||||
) if course_overview.is_discussion_tab_enabled() else None,
|
||||
|
||||
'video_outline': reverse(
|
||||
'video-summary-list',
|
||||
kwargs={'course_id': course_id},
|
||||
request=request,
|
||||
),
|
||||
|
||||
# Note: The following 2 should be deprecated.
|
||||
'social_urls': {
|
||||
'facebook': course_overview.facebook_url,
|
||||
},
|
||||
"latest_updates": {
|
||||
"video": None
|
||||
'latest_updates': {
|
||||
'video': None
|
||||
},
|
||||
"video_outline": video_outline_url,
|
||||
"course_updates": course_updates_url,
|
||||
"course_handouts": course_handouts_url,
|
||||
"discussion_url": discussion_url,
|
||||
"subscription_id": course_overview.clean_id(padding_char='_'),
|
||||
"courseware_access": has_access(request.user, 'load_mobile', course_overview).to_json() if request else None
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import pytz
|
||||
from django.conf import settings
|
||||
from django.utils import timezone
|
||||
from django.template import defaultfilters
|
||||
from django.test import RequestFactory
|
||||
|
||||
from certificates.models import CertificateStatuses
|
||||
from certificates.tests.factories import GeneratedCertificateFactory
|
||||
@@ -24,11 +25,11 @@ from util.milestones_helpers import (
|
||||
)
|
||||
from xmodule.course_module import DEFAULT_START_DATE
|
||||
from xmodule.modulestore.tests.factories import ItemFactory, CourseFactory
|
||||
from util.testing import UrlResetMixin
|
||||
|
||||
from .. import errors
|
||||
from ..testutils import MobileAPITestCase, MobileAuthTestMixin, MobileAuthUserTestMixin, MobileCourseAccessTestMixin
|
||||
from .serializers import CourseEnrollmentSerializer
|
||||
from util.testing import UrlResetMixin
|
||||
|
||||
|
||||
class TestUserDetailApi(MobileAPITestCase, MobileAuthUserTestMixin):
|
||||
@@ -61,13 +62,14 @@ class TestUserInfoApi(MobileAPITestCase, MobileAuthTestMixin):
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class TestUserEnrollmentApi(UrlResetMixin, MobileAPITestCase, MobileAuthUserTestMixin):
|
||||
class TestUserEnrollmentApi(UrlResetMixin, MobileAPITestCase, MobileAuthUserTestMixin, MobileCourseAccessTestMixin):
|
||||
"""
|
||||
Tests for /api/mobile/v0.5/users/<user_name>/course_enrollments/
|
||||
"""
|
||||
REVERSE_INFO = {'name': 'courseenrollment-detail', 'params': ['username']}
|
||||
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)
|
||||
ADVERTISED_START = "Spring 2016"
|
||||
@@ -85,13 +87,15 @@ class TestUserEnrollmentApi(UrlResetMixin, MobileAPITestCase, MobileAuthUserTest
|
||||
self.assertEqual(len(courses), 1)
|
||||
|
||||
found_course = courses[0]['course']
|
||||
self.assertTrue('video_outline' in found_course)
|
||||
self.assertTrue('course_handouts' in found_course)
|
||||
self.assertIn('courses/{}/about'.format(self.course.id), found_course['course_about'])
|
||||
self.assertIn('course_info/{}/updates'.format(self.course.id), found_course['course_updates'])
|
||||
self.assertIn('course_info/{}/handouts'.format(self.course.id), found_course['course_handouts'])
|
||||
self.assertIn('video_outlines/courses/{}'.format(self.course.id), found_course['video_outline'])
|
||||
self.assertEqual(found_course['id'], unicode(self.course.id))
|
||||
self.assertEqual(courses[0]['mode'], 'honor')
|
||||
self.assertEqual(courses[0]['course']['subscription_id'], self.course.clean_id(padding_char='_'))
|
||||
|
||||
def verify_failure(self, response):
|
||||
def verify_failure(self, response, error_type=None):
|
||||
self.assertEqual(response.status_code, 200)
|
||||
courses = response.data
|
||||
self.assertEqual(len(courses), 0)
|
||||
@@ -380,22 +384,29 @@ class TestCourseEnrollmentSerializer(MobileAPITestCase):
|
||||
"""
|
||||
Test the course enrollment serializer
|
||||
"""
|
||||
def test_success(self):
|
||||
def setUp(self):
|
||||
super(TestCourseEnrollmentSerializer, self).setUp()
|
||||
self.login_and_enroll()
|
||||
self.request = RequestFactory().get('/')
|
||||
self.request.user = self.user
|
||||
|
||||
serialized = CourseEnrollmentSerializer(CourseEnrollment.enrollments_for_user(self.user)[0]).data
|
||||
self.assertEqual(serialized['course']['video_outline'], None)
|
||||
def test_success(self):
|
||||
serialized = CourseEnrollmentSerializer(
|
||||
CourseEnrollment.enrollments_for_user(self.user)[0],
|
||||
context={'request': self.request},
|
||||
).data
|
||||
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)
|
||||
|
||||
def test_with_display_overrides(self):
|
||||
self.login_and_enroll()
|
||||
|
||||
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]).data
|
||||
serialized = CourseEnrollmentSerializer(
|
||||
CourseEnrollment.enrollments_for_user(self.user)[0],
|
||||
context={'request': self.request},
|
||||
).data
|
||||
self.assertEqual(serialized['course']['number'], self.course.display_coursenumber)
|
||||
self.assertEqual(serialized['course']['org'], self.course.display_organization)
|
||||
|
||||
@@ -225,9 +225,15 @@ class UserCourseEnrollmentsList(generics.ListAPIView):
|
||||
course.
|
||||
* course: A collection of the following data about the course.
|
||||
|
||||
* courseware_access: A JSON representation with access information for the course,
|
||||
including any access errors.
|
||||
|
||||
* course_about: The URL to the course about page.
|
||||
* course_handouts: The URI to get data for course handouts.
|
||||
* course_image: The path to the course image.
|
||||
* course_updates: The URI to get data for course updates.
|
||||
* discussion_url: The URI to access data for course discussions if
|
||||
it is enabled, otherwise null.
|
||||
* end: The end date of the course.
|
||||
* id: The unique ID of the course.
|
||||
* latest_updates: Reserved for future use.
|
||||
@@ -235,12 +241,15 @@ class UserCourseEnrollmentsList(generics.ListAPIView):
|
||||
* number: The course number.
|
||||
* org: The organization that created the course.
|
||||
* start: The date and time when the course starts.
|
||||
* start_display:
|
||||
If start_type is a string, then the advertised_start date for the course.
|
||||
If start_type is a timestamp, then a formatted date for the start of the course.
|
||||
If start_type is empty, then the value is None and it indicates that the course has not yet started.
|
||||
* start_type: One of either "string", "timestamp", or "empty"
|
||||
* subscription_id: A unique "clean" (alphanumeric with '_') ID of
|
||||
the course.
|
||||
* video_outline: The URI to get the list of all videos that the user
|
||||
can access in the course.
|
||||
* discussion_url: The URI to access data for course discussions if
|
||||
it is enabled, otherwise null.
|
||||
|
||||
* created: The date the course was created.
|
||||
* is_active: Whether the course is currently active. Possible values
|
||||
|
||||
Reference in New Issue
Block a user