Commit 920dfb52 by Nimisha Asthagiri

MA-216 Mobile API: display unreleased courses in enrollment list.

parent c11a9f05
......@@ -8,12 +8,12 @@ from django.test import TestCase
from xmodule.modulestore.tests.factories import CourseFactory
from .utils import mobile_access_when_enrolled, mobile_course_access, mobile_view
from .utils import mobile_course_listing_access, mobile_course_access, mobile_view, dict_value
from .testutils import MobileAPITestCase, ROLE_CASES
@ddt.ddt
class TestMobileAccessWhenEnrolled(MobileAPITestCase):
class TestMobileCourseListingAccess(MobileAPITestCase):
"""
Tests for mobile_access_when_enrolled utility function.
"""
......@@ -26,26 +26,26 @@ class TestMobileAccessWhenEnrolled(MobileAPITestCase):
non_mobile_course = CourseFactory.create(mobile_available=False)
if role:
role(non_mobile_course.id).add_users(self.user)
self.assertEqual(should_have_access, mobile_access_when_enrolled(non_mobile_course, self.user))
self.assertEqual(should_have_access, mobile_course_listing_access(non_mobile_course, self.user))
def test_mobile_explicit_access(self):
"""
Verifies that our mobile access function listens to the mobile_available flag as it should
"""
self.assertTrue(mobile_access_when_enrolled(self.course, self.user))
self.assertTrue(mobile_course_listing_access(self.course, self.user))
def test_missing_course(self):
"""
Verifies that we handle the case where a course doesn't exist
"""
self.assertFalse(mobile_access_when_enrolled(None, self.user))
self.assertFalse(mobile_course_listing_access(None, self.user))
@patch.dict('django.conf.settings.FEATURES', {'DISABLE_START_DATES': False})
def test_unreleased_course(self):
"""
Verifies that we handle the case where a course hasn't started
Verifies that we allow the case where a course hasn't started
"""
self.assertFalse(mobile_access_when_enrolled(self.course, self.user))
self.assertTrue(mobile_course_listing_access(self.course, self.user))
@ddt.ddt
......@@ -65,3 +65,42 @@ class TestMobileAPIDecorators(TestCase):
self.assertIn("Test docstring of decorated function.", decorated_func.__doc__)
self.assertEquals(decorated_func.__name__, "decorated_func")
self.assertTrue(decorated_func.__module__.endswith("tests"))
@ddt.ddt
class TestDictContextManager(TestCase):
"""
Tests for dict contextmanager.
"""
def setUp(self):
super(TestDictContextManager, self).setUp()
self.test_dict = {}
self.test_key = 'test key'
def call_context_manager(self, raise_exception):
"""Helper method that calls the context manager."""
new_value = "new value"
try:
with dict_value(self.test_dict, self.test_key, new_value):
# verify test_key is assigned to new_value within the context.
self.assertEquals(self.test_dict[self.test_key], new_value)
if raise_exception:
raise StandardError
except StandardError:
pass
@ddt.data(True, False)
def test_no_previous_value(self, raise_exception):
self.call_context_manager(raise_exception)
# verify test_key no longer exists in the dict.
self.assertNotIn(self.test_key, self.test_dict)
@ddt.data(True, False)
def test_has_previous_value(self, raise_exception):
old_value = "old value"
self.test_dict[self.test_key] = old_value
self.call_context_manager(raise_exception)
# verify test_key's value is reverted back to old_value.
self.assertEquals(self.test_dict[self.test_key], old_value)
......@@ -10,7 +10,7 @@ Test utilities for mobile API tests:
MobileCourseAccessTestMixin - tests for APIs with mobile_course_access and verify_enrolled=False.
MobileEnrolledCourseAccessTestMixin - tests for APIs with mobile_course_access and verify_enrolled=True.
"""
# pylint: disable=no-member
# pylint: disable=no-member, invalid-name
import ddt
from mock import patch
from rest_framework.test import APITestCase
......@@ -140,6 +140,8 @@ class MobileCourseAccessTestMixin(object):
Subclasses are expected to inherit from MobileAPITestCase.
Subclasses can override verify_success, verify_failure, and init_course_access methods.
"""
ALLOW_ACCESS_TO_UNRELEASED_COURSE = False
def verify_success(self, response):
"""Base implementation of verifying a successful response."""
self.assertEqual(response.status_code, 200)
......@@ -170,7 +172,10 @@ class MobileCourseAccessTestMixin(object):
self.init_course_access()
response = self.api_response(expected_response_code=None)
self.verify_failure(response) # allow subclasses to override verification
if self.ALLOW_ACCESS_TO_UNRELEASED_COURSE:
self.verify_success(response)
else:
self.verify_failure(response)
@ddt.data(*ROLE_CASES)
@ddt.unpack
......
......@@ -48,6 +48,7 @@ class TestUserEnrollmentApi(MobileAPITestCase, MobileAuthUserTestMixin, MobileEn
Tests for /api/mobile/v0.5/users/<user_name>/course_enrollments/
"""
REVERSE_INFO = {'name': 'courseenrollment-detail', 'params': ['username']}
ALLOW_ACCESS_TO_UNRELEASED_COURSE = True
def verify_success(self, response):
super(TestUserEnrollmentApi, self).verify_success(response)
......
......@@ -26,7 +26,7 @@ from xmodule.modulestore.exceptions import ItemNotFoundError
from .serializers import CourseEnrollmentSerializer, UserSerializer
from mobile_api import errors
from mobile_api.utils import mobile_access_when_enrolled, mobile_view, mobile_course_access
from mobile_api.utils import mobile_course_listing_access, mobile_view, mobile_course_access
@mobile_view(is_user=True)
......@@ -46,6 +46,7 @@ class UserDetail(generics.RetrieveAPIView):
GET /api/mobile/v0.5/users/{username}
**Response Values**
* id: The ID of the user.
......@@ -244,7 +245,7 @@ class UserCourseEnrollmentsList(generics.ListAPIView):
enrollments = self.queryset.filter(user__username=self.kwargs['username'], is_active=True).order_by('created')
return [
enrollment for enrollment in enrollments
if mobile_access_when_enrolled(enrollment.course, self.request.user)
if mobile_course_listing_access(enrollment.course, self.request.user)
]
......
......@@ -4,7 +4,9 @@ Common utility methods and decorators for Mobile APIs.
import functools
from contextlib import contextmanager
from django.http import Http404
from django.conf import settings
from opaque_keys.edx.keys import CourseKey
from courseware.courses import get_course_with_access
......@@ -12,6 +14,30 @@ from rest_framework import permissions
from rest_framework.authentication import OAuth2Authentication, SessionAuthentication
# TODO This contextmanager should be moved to a common utility library.
@contextmanager
def dict_value(dictionary, key, value):
"""
A context manager that assigns 'value' to the 'key' in the 'dictionary' when entering the context,
and then resets the key upon exiting the context.
"""
# cache previous values
has_previous_value = key in dictionary
previous_value = dictionary[key] if has_previous_value else None
try:
# temporarily set to new value
dictionary[key] = value
yield
finally:
# reset to previous values
if has_previous_value:
dictionary[key] = previous_value
else:
dictionary.pop(key, None)
def mobile_course_access(depth=0, verify_enrolled=True):
"""
Method decorator for a mobile API endpoint that verifies the user has access to the course in a mobile context.
......@@ -37,18 +63,22 @@ def mobile_course_access(depth=0, verify_enrolled=True):
return _decorator
def mobile_access_when_enrolled(course, user):
def mobile_course_listing_access(course, user):
"""
Determines whether a user has access to a course in a mobile context.
Checks the mobile_available flag and the start_date.
Note: Does not check if the user is actually enrolled in the course.
Determines whether a user has access to a course' listing in a mobile context.
Checks the mobile_available flag.
Checks roles including Beta Tester and staff roles.
Note:
Does not check if the user is actually enrolled in the course.
Does not check the start_date.
"""
# The course doesn't always really exist -- we can have bad data in the enrollments
# pointing to non-existent (or removed) courses, in which case `course` is None.
if not course:
return False
try:
return get_course_with_access(user, 'load_mobile_no_enrollment_check', course.id) is not None
with dict_value(settings.FEATURES, 'DISABLE_START_DATES', True):
return get_course_with_access(user, 'load_mobile_no_enrollment_check', course.id) is not None
except Http404:
return False
......
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