* fix: fixed atmoic block error in course_status_info We are getting "An error occurred in the current transaction. You cant execute queries until the end of the atomic block." error in get course status info on both versions i.e. 0.5 and 1. I was unable to reproduce it locally. But by looking in django documentation and some other online helps I concluded that it might be because of some db intefrity error that we didnt handle properly and now the cursor is broken for this transaction. Therefore i am making it non atomic transaction. Since its a get request we can make transactions run independentantly. LEARNER-8267
654 lines
26 KiB
Python
654 lines
26 KiB
Python
"""
|
|
Tests for users API
|
|
"""
|
|
|
|
|
|
import datetime
|
|
from unittest.mock import patch
|
|
from urllib.parse import parse_qs
|
|
|
|
import ddt
|
|
import pytz
|
|
from completion.test_utils import CompletionWaffleTestMixin, submit_completions_for_testing
|
|
from django.conf import settings
|
|
from django.db import transaction
|
|
from django.template import defaultfilters
|
|
from django.test import RequestFactory, override_settings
|
|
from django.utils import timezone
|
|
from django.utils.timezone import now
|
|
from milestones.tests.utils import MilestonesTestCaseMixin
|
|
from opaque_keys.edx.keys import CourseKey
|
|
|
|
from common.djangoapps.course_modes.models import CourseMode
|
|
from common.djangoapps.student.models import CourseEnrollment
|
|
from common.djangoapps.student.tests.factories import CourseEnrollmentFactory, UserFactory
|
|
from common.djangoapps.util.milestones_helpers import set_prerequisite_courses
|
|
from common.djangoapps.util.testing import UrlResetMixin
|
|
from lms.djangoapps.certificates.data import CertificateStatuses
|
|
from lms.djangoapps.certificates.tests.factories import GeneratedCertificateFactory
|
|
from lms.djangoapps.courseware.access_response import MilestoneAccessError, StartDateError, VisibilityError
|
|
from lms.djangoapps.mobile_api.testutils import (
|
|
MobileAPITestCase,
|
|
MobileAuthTestMixin,
|
|
MobileAuthUserTestMixin,
|
|
MobileCourseAccessTestMixin
|
|
)
|
|
from lms.djangoapps.mobile_api.utils import API_V1, API_V05
|
|
from openedx.core.lib.courses import course_image_url
|
|
from openedx.features.course_duration_limits.models import CourseDurationLimitConfig
|
|
from openedx.features.course_experience.tests.views.helpers import add_course_mode
|
|
from xmodule.course_module import DEFAULT_START_DATE
|
|
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
|
|
|
|
from .. import errors
|
|
from .serializers import CourseEnrollmentSerializer, CourseEnrollmentSerializerv05
|
|
|
|
|
|
@ddt.ddt
|
|
class TestUserDetailApi(MobileAPITestCase, MobileAuthUserTestMixin):
|
|
"""
|
|
Tests for /api/mobile/{api_version}/users/<user_name>...
|
|
"""
|
|
REVERSE_INFO = {'name': 'user-detail', 'params': ['username', 'api_version']}
|
|
|
|
@ddt.data(API_V05, API_V1)
|
|
def test_success(self, api_version):
|
|
self.login()
|
|
|
|
response = self.api_response(api_version=api_version)
|
|
assert response.data['username'] == self.user.username
|
|
assert response.data['email'] == self.user.email
|
|
|
|
|
|
@ddt.ddt
|
|
class TestUserInfoApi(MobileAPITestCase, MobileAuthTestMixin):
|
|
"""
|
|
Tests for /api/mobile/{api_version}/my_user_info
|
|
"""
|
|
REVERSE_INFO = {'name': 'user-info', 'params': ['api_version']}
|
|
|
|
@ddt.data(API_V05, API_V1)
|
|
def test_success(self, api_version):
|
|
"""Verify the endpoint redirects to the user detail endpoint"""
|
|
self.login()
|
|
|
|
response = self.api_response(expected_response_code=302, api_version=api_version)
|
|
assert self.username in response['location']
|
|
|
|
@ddt.data(API_V05, API_V1)
|
|
def test_last_loggedin_updated(self, api_version):
|
|
"""Verify that a user's last logged in value updates after hitting the my_user_info endpoint"""
|
|
self.login()
|
|
|
|
self.user.refresh_from_db()
|
|
last_login_before = self.user.last_login
|
|
|
|
# just hit the api endpoint; we don't care about the response here (tested previously)
|
|
self.api_response(expected_response_code=302, api_version=api_version)
|
|
|
|
self.user.refresh_from_db()
|
|
last_login_after = self.user.last_login
|
|
assert last_login_after > last_login_before
|
|
|
|
|
|
@ddt.ddt
|
|
@override_settings(MKTG_URLS={'ROOT': 'dummy-root'})
|
|
class TestUserEnrollmentApi(UrlResetMixin, MobileAPITestCase, MobileAuthUserTestMixin,
|
|
MobileCourseAccessTestMixin, MilestonesTestCaseMixin):
|
|
"""
|
|
Tests for /api/mobile/{api_version}/users/<user_name>/course_enrollments/
|
|
"""
|
|
REVERSE_INFO = {'name': 'courseenrollment-detail', 'params': ['username', 'api_version']}
|
|
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)
|
|
THREE_YEARS_AGO = now() - datetime.timedelta(days=(365 * 3))
|
|
ADVERTISED_START = "Spring 2016"
|
|
ENABLED_SIGNALS = ['course_published']
|
|
DATES = {
|
|
'next_week': NEXT_WEEK,
|
|
'last_week': LAST_WEEK,
|
|
'default_start_date': DEFAULT_START_DATE,
|
|
}
|
|
|
|
@patch.dict(settings.FEATURES, {"ENABLE_DISCUSSION_SERVICE": True})
|
|
def setUp(self):
|
|
super().setUp()
|
|
|
|
def verify_success(self, response):
|
|
"""
|
|
Verifies user course enrollment response for success
|
|
"""
|
|
super().verify_success(response)
|
|
courses = response.data
|
|
assert len(courses) == 1
|
|
|
|
found_course = courses[0]['course']
|
|
assert f'courses/{self.course.id}/about' in found_course['course_about']
|
|
assert f'course_info/{self.course.id}/updates' in found_course['course_updates']
|
|
assert f'course_info/{self.course.id}/handouts' in found_course['course_handouts']
|
|
assert found_course['id'] == str(self.course.id)
|
|
assert courses[0]['mode'] == CourseMode.DEFAULT_MODE_SLUG
|
|
assert courses[0]['course']['subscription_id'] == self.course.clean_id(padding_char='_')
|
|
|
|
expected_course_image_url = course_image_url(self.course)
|
|
assert expected_course_image_url is not None
|
|
assert expected_course_image_url in found_course['course_image']
|
|
assert expected_course_image_url in found_course['media']['course_image']['uri']
|
|
|
|
def verify_failure(self, response, error_type=None):
|
|
assert response.status_code == 200
|
|
courses = response.data
|
|
assert len(courses) == 0
|
|
|
|
@patch.dict(settings.FEATURES, {'ENABLE_MKTG_SITE': True})
|
|
@ddt.data(API_V05, API_V1)
|
|
def test_sort_order(self, api_version):
|
|
self.login()
|
|
|
|
num_courses = 3
|
|
courses = []
|
|
for course_index in range(num_courses):
|
|
courses.append(CourseFactory.create(mobile_available=True))
|
|
self.enroll(courses[course_index].id)
|
|
|
|
# verify courses are returned in the order of enrollment, with most recently enrolled first.
|
|
response = self.api_response(api_version=api_version)
|
|
for course_index in range(num_courses):
|
|
assert response.data[course_index]['course']['id'] ==\
|
|
str(courses[((num_courses - course_index) - 1)].id)
|
|
|
|
@ddt.data(API_V05, API_V1)
|
|
@patch.dict(settings.FEATURES, {
|
|
'ENABLE_PREREQUISITE_COURSES': True,
|
|
'DISABLE_START_DATES': False,
|
|
'ENABLE_MKTG_SITE': True,
|
|
})
|
|
def test_courseware_access(self, api_version):
|
|
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, [str(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 = [
|
|
MilestoneAccessError().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(api_version=api_version)
|
|
for course_index in range(len(courses)):
|
|
result = response.data[course_index]['course']['courseware_access']
|
|
assert result['error_code'] == expected_error_codes[::(- 1)][course_index]
|
|
|
|
if result['error_code'] is not None:
|
|
assert not result['has_access']
|
|
|
|
@ddt.data(
|
|
('next_week', ADVERTISED_START, ADVERTISED_START, "string", API_V05),
|
|
('next_week', ADVERTISED_START, ADVERTISED_START, "string", API_V1),
|
|
('next_week', None, defaultfilters.date(NEXT_WEEK, "DATE_FORMAT"), "timestamp", API_V05),
|
|
('next_week', None, defaultfilters.date(NEXT_WEEK, "DATE_FORMAT"), "timestamp", API_V1),
|
|
('next_week', '', defaultfilters.date(NEXT_WEEK, "DATE_FORMAT"), "timestamp", API_V05),
|
|
('next_week', '', defaultfilters.date(NEXT_WEEK, "DATE_FORMAT"), "timestamp", API_V1),
|
|
('default_start_date', ADVERTISED_START, ADVERTISED_START, "string", API_V05),
|
|
('default_start_date', ADVERTISED_START, ADVERTISED_START, "string", API_V1),
|
|
('default_start_date', '', None, "empty", API_V05),
|
|
('default_start_date', '', None, "empty", API_V1),
|
|
('default_start_date', None, None, "empty", API_V05),
|
|
('default_start_date', None, None, "empty", API_V1),
|
|
)
|
|
@ddt.unpack
|
|
@patch.dict(settings.FEATURES, {'DISABLE_START_DATES': False, 'ENABLE_MKTG_SITE': True})
|
|
def test_start_type_and_display(self, start, advertised_start, expected_display, expected_type, api_version):
|
|
"""
|
|
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=self.DATES[start], advertised_start=advertised_start, mobile_available=True)
|
|
self.enroll(course.id)
|
|
|
|
response = self.api_response(api_version=api_version)
|
|
assert response.data[0]['course']['start_type'] == expected_type
|
|
assert response.data[0]['course']['start_display'] == expected_display
|
|
|
|
@ddt.data(API_V05, API_V1)
|
|
@patch.dict(settings.FEATURES, {"ENABLE_DISCUSSION_SERVICE": True, 'ENABLE_MKTG_SITE': True})
|
|
def test_discussion_url(self, api_version):
|
|
self.login_and_enroll()
|
|
|
|
response = self.api_response(api_version=api_version)
|
|
response_discussion_url = response.data[0]['course']['discussion_url']
|
|
assert f'/api/discussion/v1/courses/{self.course.id}' in response_discussion_url
|
|
|
|
@ddt.data(API_V05, API_V1)
|
|
def test_org_query(self, api_version):
|
|
self.login()
|
|
|
|
# Create list of courses with various organizations
|
|
courses = [
|
|
CourseFactory.create(org='edX', mobile_available=True),
|
|
CourseFactory.create(org='edX', mobile_available=True),
|
|
CourseFactory.create(org='edX', mobile_available=True, visible_to_staff_only=True),
|
|
CourseFactory.create(org='Proversity.org', mobile_available=True),
|
|
CourseFactory.create(org='MITx', mobile_available=True),
|
|
CourseFactory.create(org='HarvardX', mobile_available=True),
|
|
]
|
|
|
|
# Enroll in all the courses
|
|
for course in courses:
|
|
self.enroll(course.id)
|
|
|
|
response = self.api_response(data={'org': 'edX'}, api_version=api_version)
|
|
|
|
# Test for 3 expected courses
|
|
assert len(response.data) == 3
|
|
|
|
# Verify only edX courses are returned
|
|
for entry in response.data:
|
|
assert entry['course']['org'] == 'edX'
|
|
|
|
def create_enrollment(self, expired):
|
|
"""
|
|
Create an enrollment
|
|
"""
|
|
if expired:
|
|
course = CourseFactory.create(start=self.THREE_YEARS_AGO, mobile_available=True)
|
|
enrollment = CourseEnrollmentFactory.create(
|
|
user=self.user,
|
|
course_id=course.id
|
|
)
|
|
enrollment.created = self.THREE_YEARS_AGO + datetime.timedelta(days=1)
|
|
enrollment.save()
|
|
else:
|
|
course = CourseFactory.create(start=self.LAST_WEEK, mobile_available=True)
|
|
self.enroll(course.id)
|
|
|
|
add_course_mode(course, mode_slug=CourseMode.AUDIT)
|
|
add_course_mode(course)
|
|
|
|
def _get_enrollment_data(self, api_version, expired):
|
|
self.login()
|
|
self.create_enrollment(expired)
|
|
return self.api_response(api_version=api_version).data
|
|
|
|
def _assert_enrollment_results(self, api_version, courses, num_courses_returned, gating_enabled=True): # lint-amnesty, pylint: disable=missing-function-docstring
|
|
assert len(courses) == num_courses_returned
|
|
|
|
if api_version == API_V05:
|
|
if num_courses_returned:
|
|
assert 'audit_access_expires' not in courses[0]
|
|
else:
|
|
assert 'audit_access_expires' in courses[0]
|
|
if gating_enabled:
|
|
assert courses[0].get('audit_access_expires') is not None
|
|
|
|
@ddt.data(
|
|
(API_V05, True, 0),
|
|
(API_V05, False, 1),
|
|
(API_V1, True, 1),
|
|
(API_V1, False, 1),
|
|
)
|
|
@ddt.unpack
|
|
def test_enrollment_with_gating(self, api_version, expired, num_courses_returned):
|
|
"""
|
|
Test that expired courses are only returned in v1 of API
|
|
when waffle flag enabled, and un-expired courses always returned
|
|
"""
|
|
CourseDurationLimitConfig.objects.create(enabled=True, enabled_as_of=datetime.datetime(2015, 1, 1))
|
|
courses = self._get_enrollment_data(api_version, expired)
|
|
self._assert_enrollment_results(api_version, courses, num_courses_returned, True)
|
|
|
|
@ddt.data(
|
|
(API_V05, True, 1),
|
|
(API_V05, False, 1),
|
|
(API_V1, True, 1),
|
|
(API_V1, False, 1),
|
|
)
|
|
@ddt.unpack
|
|
def test_enrollment_no_gating(self, api_version, expired, num_courses_returned):
|
|
"""
|
|
Test that expired and non-expired courses are returned if the waffle flag is disabled,
|
|
regardless of the API version
|
|
"""
|
|
CourseDurationLimitConfig.objects.create(enabled=False)
|
|
courses = self._get_enrollment_data(api_version, expired)
|
|
self._assert_enrollment_results(api_version, courses, num_courses_returned, False)
|
|
|
|
|
|
@override_settings(MKTG_URLS={'ROOT': 'dummy-root'})
|
|
class TestUserEnrollmentCertificates(UrlResetMixin, MobileAPITestCase, MilestonesTestCaseMixin):
|
|
"""
|
|
Tests for /api/mobile/{api_version}/users/<user_name>/course_enrollments/
|
|
"""
|
|
REVERSE_INFO = {'name': 'courseenrollment-detail', 'params': ['username', 'api_version']}
|
|
ENABLED_SIGNALS = ['course_published']
|
|
|
|
def verify_pdf_certificate(self):
|
|
"""
|
|
Verifies the correct URL is returned in the response
|
|
for PDF certificates.
|
|
"""
|
|
self.login_and_enroll()
|
|
|
|
certificate_url = "https://test_certificate_url"
|
|
GeneratedCertificateFactory.create(
|
|
user=self.user,
|
|
course_id=self.course.id,
|
|
status=CertificateStatuses.downloadable,
|
|
mode='verified',
|
|
download_url=certificate_url,
|
|
)
|
|
|
|
response = self.api_response()
|
|
certificate_data = response.data[0]['certificate']
|
|
assert certificate_data['url'] == certificate_url
|
|
|
|
@patch.dict(settings.FEATURES, {'ENABLE_MKTG_SITE': True})
|
|
def test_no_certificate(self):
|
|
self.login_and_enroll()
|
|
|
|
response = self.api_response()
|
|
certificate_data = response.data[0]['certificate']
|
|
self.assertDictEqual(certificate_data, {})
|
|
|
|
@patch.dict(settings.FEATURES, {'CERTIFICATES_HTML_VIEW': False, 'ENABLE_MKTG_SITE': True})
|
|
def test_pdf_certificate_with_html_cert_disabled(self):
|
|
"""
|
|
Tests PDF certificates with CERTIFICATES_HTML_VIEW set to True.
|
|
"""
|
|
self.verify_pdf_certificate()
|
|
|
|
@patch.dict(settings.FEATURES, {'CERTIFICATES_HTML_VIEW': True, 'ENABLE_MKTG_SITE': True})
|
|
def test_pdf_certificate_with_html_cert_enabled(self):
|
|
"""
|
|
Tests PDF certificates with CERTIFICATES_HTML_VIEW set to True.
|
|
"""
|
|
self.verify_pdf_certificate()
|
|
|
|
@patch.dict(settings.FEATURES, {'CERTIFICATES_HTML_VIEW': True, 'ENABLE_MKTG_SITE': True})
|
|
def test_web_certificate(self):
|
|
self.login_and_enroll()
|
|
|
|
GeneratedCertificateFactory.create(
|
|
user=self.user,
|
|
course_id=self.course.id,
|
|
status=CertificateStatuses.downloadable
|
|
)
|
|
|
|
response = self.api_response()
|
|
certificate_data = response.data[0]['certificate']
|
|
self.assertRegex(
|
|
certificate_data['url'],
|
|
r'http.*/certificates/[0-9a-f]{32}'
|
|
)
|
|
|
|
|
|
class CourseStatusAPITestCase(MobileAPITestCase):
|
|
"""
|
|
Base test class for /api/mobile/{api_version}/users/<user_name>/course_status_info/{course_id}
|
|
"""
|
|
REVERSE_INFO = {'name': 'user-course-status', 'params': ['username', 'course_id', 'api_version']}
|
|
|
|
def setUp(self):
|
|
"""
|
|
Creates a basic course structure for our course
|
|
"""
|
|
super().setUp()
|
|
|
|
self.section = ItemFactory.create(
|
|
parent=self.course,
|
|
category='chapter',
|
|
)
|
|
self.sub_section = ItemFactory.create(
|
|
parent=self.section,
|
|
category='sequential',
|
|
)
|
|
self.unit = ItemFactory.create(
|
|
parent=self.sub_section,
|
|
category='vertical',
|
|
)
|
|
self.other_sub_section = ItemFactory.create(
|
|
parent=self.section,
|
|
category='sequential',
|
|
)
|
|
self.other_unit = ItemFactory.create(
|
|
parent=self.other_sub_section,
|
|
category='vertical',
|
|
)
|
|
|
|
|
|
class TestCourseStatusGET(CourseStatusAPITestCase, MobileAuthUserTestMixin,
|
|
MobileCourseAccessTestMixin, MilestonesTestCaseMixin, CompletionWaffleTestMixin):
|
|
"""
|
|
Tests for GET of /api/mobile/v<version_number>/users/<user_name>/course_status_info/{course_id}
|
|
"""
|
|
|
|
def test_success_v0(self):
|
|
self.login_and_enroll()
|
|
|
|
response = self.api_response(api_version=API_V05)
|
|
assert response.data['last_visited_module_id'] == str(self.sub_section.location)
|
|
assert response.data['last_visited_module_path'] == [str(module.location) for module in
|
|
[self.sub_section, self.section, self.course]]
|
|
|
|
def test_success_v1(self):
|
|
self.override_waffle_switch(True)
|
|
self.login_and_enroll()
|
|
submit_completions_for_testing(self.user, [self.unit.location])
|
|
response = self.api_response(api_version=API_V1)
|
|
assert response.data['last_visited_block_id'] == str(self.unit.location)
|
|
|
|
# Since we are testing an non atomic view in atomic test case, therefore we are expecting error on failures
|
|
def api_error_response(self, reverse_args=None, data=None, **kwargs):
|
|
"""
|
|
Same as api response from MobileAPITestCase but handle views which throw errors
|
|
"""
|
|
url = self.reverse_url(reverse_args, **kwargs)
|
|
try:
|
|
with transaction.atomic():
|
|
self.url_method(url, data=data, **kwargs)
|
|
assert False
|
|
except transaction.TransactionManagementError:
|
|
assert True
|
|
|
|
def test_invalid_user(self):
|
|
self.login_and_enroll()
|
|
self.api_error_response(username='no_user')
|
|
|
|
def test_other_user(self):
|
|
# login and enroll as the test user
|
|
self.login_and_enroll()
|
|
self.logout()
|
|
|
|
# login and enroll as another user
|
|
other = UserFactory.create()
|
|
self.client.login(username=other.username, password='test')
|
|
self.enroll()
|
|
self.logout()
|
|
|
|
# now login and call the API as the test user
|
|
self.login()
|
|
self.api_error_response(username=other.username)
|
|
|
|
def test_course_not_found(self):
|
|
non_existent_course_id = CourseKey.from_string('a/b/c')
|
|
self.init_course_access(course_id=non_existent_course_id)
|
|
|
|
self.api_error_response(course_id=non_existent_course_id)
|
|
|
|
def test_unenrolled_user(self):
|
|
self.login()
|
|
self.unenroll()
|
|
self.api_error_response(expected_response_code=None)
|
|
|
|
def test_no_auth(self):
|
|
self.logout()
|
|
self.api_error_response()
|
|
|
|
|
|
class TestCourseStatusPATCH(CourseStatusAPITestCase, MobileAuthUserTestMixin,
|
|
MobileCourseAccessTestMixin, MilestonesTestCaseMixin):
|
|
"""
|
|
Tests for PATCH of /api/mobile/v0.5/users/<user_name>/course_status_info/{course_id}
|
|
"""
|
|
def url_method(self, url, **kwargs): # pylint: disable=arguments-differ
|
|
# override implementation to use PATCH method.
|
|
return self.client.patch(url, data=kwargs.get('data', None))
|
|
|
|
def test_success(self):
|
|
self.login_and_enroll()
|
|
response = self.api_response(data={"last_visited_module_id": str(self.other_unit.location)})
|
|
assert response.data['last_visited_module_id'] == str(self.other_sub_section.location)
|
|
|
|
def test_invalid_module(self):
|
|
self.login_and_enroll()
|
|
response = self.api_response(data={"last_visited_module_id": "abc"}, expected_response_code=400)
|
|
assert response.data == errors.ERROR_INVALID_MODULE_ID
|
|
|
|
def test_nonexistent_module(self):
|
|
self.login_and_enroll()
|
|
non_existent_key = self.course.id.make_usage_key('video', 'non-existent')
|
|
response = self.api_response(data={"last_visited_module_id": non_existent_key}, expected_response_code=400)
|
|
assert response.data == errors.ERROR_INVALID_MODULE_ID
|
|
|
|
def test_no_timezone(self):
|
|
self.login_and_enroll()
|
|
past_date = datetime.datetime.now()
|
|
response = self.api_response(
|
|
data={
|
|
"last_visited_module_id": str(self.other_unit.location),
|
|
"modification_date": past_date.isoformat()
|
|
},
|
|
expected_response_code=400
|
|
)
|
|
assert response.data == errors.ERROR_INVALID_MODIFICATION_DATE
|
|
|
|
def _date_sync(self, date, initial_unit, update_unit, expected_subsection):
|
|
"""
|
|
Helper for test cases that use a modification to decide whether
|
|
to update the course status
|
|
"""
|
|
self.login_and_enroll()
|
|
|
|
# save something so we have an initial date
|
|
self.api_response(data={"last_visited_module_id": str(initial_unit.location)})
|
|
|
|
# now actually update it
|
|
response = self.api_response(
|
|
data={
|
|
"last_visited_module_id": str(update_unit.location),
|
|
"modification_date": date.isoformat()
|
|
}
|
|
)
|
|
assert response.data['last_visited_module_id'] == str(expected_subsection.location)
|
|
|
|
def test_old_date(self):
|
|
self.login_and_enroll()
|
|
date = timezone.now() + datetime.timedelta(days=-100)
|
|
self._date_sync(date, self.unit, self.other_unit, self.sub_section)
|
|
|
|
def test_new_date(self):
|
|
self.login_and_enroll()
|
|
date = timezone.now() + datetime.timedelta(days=100)
|
|
self._date_sync(date, self.unit, self.other_unit, self.other_sub_section)
|
|
|
|
def test_no_initial_date(self):
|
|
self.login_and_enroll()
|
|
response = self.api_response(
|
|
data={
|
|
"last_visited_module_id": str(self.other_unit.location),
|
|
"modification_date": timezone.now().isoformat()
|
|
}
|
|
)
|
|
assert response.data['last_visited_module_id'] == str(self.other_sub_section.location)
|
|
|
|
def test_invalid_date(self):
|
|
self.login_and_enroll()
|
|
response = self.api_response(data={"modification_date": "abc"}, expected_response_code=400)
|
|
assert response.data == errors.ERROR_INVALID_MODIFICATION_DATE
|
|
|
|
|
|
@ddt.ddt
|
|
@patch.dict(settings.FEATURES, {'ENABLE_MKTG_SITE': True})
|
|
@override_settings(MKTG_URLS={'ROOT': 'dummy-root'})
|
|
class TestCourseEnrollmentSerializer(MobileAPITestCase, MilestonesTestCaseMixin):
|
|
"""
|
|
Test the course enrollment serializer
|
|
"""
|
|
ENABLED_SIGNALS = ['course_published']
|
|
|
|
def setUp(self):
|
|
super().setUp()
|
|
self.login_and_enroll()
|
|
self.request = RequestFactory().get('/')
|
|
self.request.user = self.user
|
|
|
|
def get_serialized_data(self, api_version):
|
|
"""
|
|
Return data from CourseEnrollmentSerializer
|
|
"""
|
|
if api_version == API_V05:
|
|
serializer = CourseEnrollmentSerializerv05
|
|
else:
|
|
serializer = CourseEnrollmentSerializer
|
|
|
|
return serializer(
|
|
CourseEnrollment.enrollments_for_user(self.user)[0],
|
|
context={'request': self.request, 'api_version': api_version},
|
|
).data
|
|
|
|
def _expiration_in_response(self, response, api_version):
|
|
"""
|
|
Assert that audit_access_expires field in present in response
|
|
based on version of api being used
|
|
"""
|
|
if api_version != API_V05:
|
|
assert 'audit_access_expires' in response
|
|
else:
|
|
assert 'audit_access_expires' not in response
|
|
|
|
@ddt.data(API_V05, API_V1)
|
|
def test_success(self, api_version):
|
|
serialized = self.get_serialized_data(api_version)
|
|
assert serialized['course']['name'] == self.course.display_name
|
|
assert serialized['course']['number'] == self.course.id.course
|
|
assert serialized['course']['org'] == self.course.id.org
|
|
self._expiration_in_response(serialized, api_version)
|
|
|
|
# Assert utm parameters
|
|
qstwitter = parse_qs('utm_campaign=social-sharing-db&utm_medium=social&utm_source=twitter')
|
|
qsfacebook = parse_qs('utm_campaign=social-sharing-db&utm_medium=social&utm_source=facebook')
|
|
|
|
self.assertDictEqual(qsfacebook, parse_qs(serialized['course']['course_sharing_utm_parameters']['facebook']))
|
|
self.assertDictEqual(qstwitter, parse_qs(serialized['course']['course_sharing_utm_parameters']['twitter']))
|
|
|
|
@ddt.data(API_V05, API_V1)
|
|
def test_with_display_overrides(self, api_version):
|
|
self.course.display_coursenumber = "overridden_number"
|
|
self.course.display_organization = "overridden_org"
|
|
self.store.update_item(self.course, self.user.id)
|
|
|
|
serialized = self.get_serialized_data(api_version)
|
|
assert serialized['course']['number'] == self.course.display_coursenumber
|
|
assert serialized['course']['org'] == self.course.display_organization
|
|
self._expiration_in_response(serialized, api_version)
|