From 330120bb7ad4ca4df5068f434fab9645525b81da Mon Sep 17 00:00:00 2001 From: Nimisha Asthagiri Date: Tue, 24 Nov 2015 09:20:13 -0500 Subject: [PATCH] MA-1712: Update Mobile API to include course_about --- lms/djangoapps/mobile_api/testutils.py | 10 +- .../mobile_api/users/serializers.py | 115 ++++++++++-------- lms/djangoapps/mobile_api/users/tests.py | 33 +++-- lms/djangoapps/mobile_api/users/views.py | 13 +- 4 files changed, 104 insertions(+), 67 deletions(-) diff --git a/lms/djangoapps/mobile_api/testutils.py b/lms/djangoapps/mobile_api/testutils.py index a57375eae8..a1cce2f9d4 100644 --- a/lms/djangoapps/mobile_api/testutils.py +++ b/lms/djangoapps/mobile_api/testutils.py @@ -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) diff --git a/lms/djangoapps/mobile_api/users/serializers.py b/lms/djangoapps/mobile_api/users/serializers.py index e40df4de9c..31de1c10b7 100644 --- a/lms/djangoapps/mobile_api/users/serializers.py +++ b/lms/djangoapps/mobile_api/users/serializers.py @@ -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 } diff --git a/lms/djangoapps/mobile_api/users/tests.py b/lms/djangoapps/mobile_api/users/tests.py index 98100b1590..84b333d74d 100644 --- a/lms/djangoapps/mobile_api/users/tests.py +++ b/lms/djangoapps/mobile_api/users/tests.py @@ -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//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) diff --git a/lms/djangoapps/mobile_api/users/views.py b/lms/djangoapps/mobile_api/users/views.py index a5f5825427..879e365577 100644 --- a/lms/djangoapps/mobile_api/users/views.py +++ b/lms/djangoapps/mobile_api/users/views.py @@ -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