MA-1712: Update Mobile API to include course_about

This commit is contained in:
Nimisha Asthagiri
2015-11-24 09:20:13 -05:00
parent a7e21bf188
commit 330120bb7a
4 changed files with 104 additions and 67 deletions

View File

@@ -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)

View File

@@ -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
}

View File

@@ -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)

View File

@@ -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