From 5aeccd520bab45a34a8fc483f7e80555e04d38de Mon Sep 17 00:00:00 2001 From: tlindaliu Date: Mon, 29 Jun 2015 14:56:45 -0400 Subject: [PATCH] MA-834: Add new fields to course enrollments endpoint in mobile API Fields are start_type, start_display, and courseware_access. --- .../mobile_api/users/serializers.py | 17 ++++ lms/djangoapps/mobile_api/users/tests.py | 96 +++++++++++++++++-- 2 files changed, 105 insertions(+), 8 deletions(-) diff --git a/lms/djangoapps/mobile_api/users/serializers.py b/lms/djangoapps/mobile_api/users/serializers.py index 1ebbb572de..2be28e1ae3 100644 --- a/lms/djangoapps/mobile_api/users/serializers.py +++ b/lms/djangoapps/mobile_api/users/serializers.py @@ -4,8 +4,12 @@ Serializer for user API from rest_framework import serializers from rest_framework.reverse import reverse +from django.template import defaultfilters + +from courseware.access import has_access from student.models import CourseEnrollment, User from certificates.models import certificate_status_for_student, CertificateStatuses +from xmodule.course_module import DEFAULT_START_DATE class CourseOverviewField(serializers.RelatedField): @@ -35,12 +39,24 @@ class CourseOverviewField(serializers.RelatedField): course_updates_url = None course_handouts_url = None + if course_overview.advertised_start is not None: + 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") + else: + start_type = "empty" + start_display = None + 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": { @@ -53,6 +69,7 @@ class CourseOverviewField(serializers.RelatedField): "course_updates": course_updates_url, "course_handouts": course_handouts_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 5f5d45df4a..d5c1adabf7 100644 --- a/lms/djangoapps/mobile_api/users/tests.py +++ b/lms/djangoapps/mobile_api/users/tests.py @@ -2,12 +2,28 @@ Tests for users API """ import datetime -from django.utils import timezone +import ddt +from mock import patch +import pytz + +from django.conf import settings +from django.utils import timezone +from django.template import defaultfilters -from xmodule.modulestore.tests.factories import ItemFactory, CourseFactory -from student.models import CourseEnrollment from certificates.models import CertificateStatuses from certificates.tests.factories import GeneratedCertificateFactory +from courseware.access_response import ( + MilestoneError, + StartDateError, + VisibilityError, +) +from student.models import CourseEnrollment +from util.milestones_helpers import ( + set_prerequisite_courses, + seed_milestone_relationship_types, +) +from xmodule.course_module import DEFAULT_START_DATE +from xmodule.modulestore.tests.factories import ItemFactory, CourseFactory from .. import errors from ..testutils import MobileAPITestCase, MobileAuthTestMixin, MobileAuthUserTestMixin, MobileCourseAccessTestMixin @@ -43,6 +59,7 @@ class TestUserInfoApi(MobileAPITestCase, MobileAuthTestMixin): self.assertTrue(self.username in response['location']) +@ddt.ddt class TestUserEnrollmentApi(MobileAPITestCase, MobileAuthUserTestMixin, MobileCourseAccessTestMixin): """ Tests for /api/mobile/v0.5/users//course_enrollments/ @@ -50,6 +67,9 @@ class TestUserEnrollmentApi(MobileAPITestCase, MobileAuthUserTestMixin, MobileCo REVERSE_INFO = {'name': 'courseenrollment-detail', 'params': ['username']} ALLOW_ACCESS_TO_UNRELEASED_COURSE = True ALLOW_ACCESS_TO_MILESTONE_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" def verify_success(self, response): super(TestUserEnrollmentApi, self).verify_success(response) @@ -73,18 +93,78 @@ class TestUserEnrollmentApi(MobileAPITestCase, MobileAuthUserTestMixin, MobileCo num_courses = 3 courses = [] - for course_num in range(num_courses): + for course_index in range(num_courses): courses.append(CourseFactory.create(mobile_available=True)) - self.enroll(courses[course_num].id) + self.enroll(courses[course_index].id) # verify courses are returned in the order of enrollment, with most recently enrolled first. response = self.api_response() - for course_num in range(num_courses): + for course_index in range(num_courses): self.assertEqual( - response.data[course_num]['course']['id'], # pylint: disable=no-member - unicode(courses[num_courses - course_num - 1].id) + response.data[course_index]['course']['id'], # pylint: disable=no-member + unicode(courses[num_courses - course_index - 1].id) ) + @patch.dict( + settings.FEATURES, {'ENABLE_PREREQUISITE_COURSES': True, 'MILESTONES_APP': True, 'DISABLE_START_DATES': False} + ) + def test_courseware_access(self): + seed_milestone_relationship_types() + self.login() + + course_with_prereq = CourseFactory.create(start=self.LAST_WEEK, mobile_available=True) + prerequisite_course = CourseFactory.create() + set_prerequisite_courses(course_with_prereq.id, [unicode(prerequisite_course.id)]) + + # Create list of courses with various expected courseware_access responses and corresponding expected codes + courses = [ + course_with_prereq, + CourseFactory.create(start=self.NEXT_WEEK, mobile_available=True), + CourseFactory.create(visible_to_staff_only=True, mobile_available=True), + CourseFactory.create(start=self.LAST_WEEK, mobile_available=True, visible_to_staff_only=False), + ] + + expected_error_codes = [ + MilestoneError().error_code, # 'unfulfilled_milestones' + StartDateError(self.NEXT_WEEK).error_code, # 'course_not_started' + VisibilityError().error_code, # 'not_visible_to_user' + None, + ] + + # Enroll in all the courses + for course in courses: + 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() + for course_index in range(len(courses)): + result = response.data[course_index]['course']['courseware_access'] # pylint: disable=no-member + self.assertEqual(result['error_code'], expected_error_codes[::-1][course_index]) + + if result['error_code'] is not None: + self.assertFalse(result['has_access']) + + @ddt.data( + (NEXT_WEEK, ADVERTISED_START, ADVERTISED_START, "string"), + (NEXT_WEEK, None, defaultfilters.date(NEXT_WEEK, "DATE_FORMAT"), "timestamp"), + (DEFAULT_START_DATE, ADVERTISED_START, ADVERTISED_START, "string"), + (DEFAULT_START_DATE, None, None, "empty") + ) + @ddt.unpack + @patch.dict('django.conf.settings.FEATURES', {'DISABLE_START_DATES': False}) + def test_start_type_and_display(self, start, advertised_start, expected_display, expected_type): + """ + Tests that the correct start_type and start_display are returned in the + case the course has not started + """ + self.login() + course = CourseFactory.create(start=start, advertised_start=advertised_start, mobile_available=True) + self.enroll(course.id) + + response = self.api_response() + self.assertEqual(response.data[0]['course']['start_type'], expected_type) # pylint: disable=no-member + self.assertEqual(response.data[0]['course']['start_display'], expected_display) # pylint: disable=no-member + def test_no_certificate(self): self.login_and_enroll()