Commit 115ab3c1 by Nimisha Asthagiri

Merge pull request #10720 from edx/mobile/course-about-url-MA-1712

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