Commit a07f6072 by Nimisha Asthagiri

Updated strategy per review feedback.

parent 920dfb52
...@@ -163,11 +163,7 @@ def _has_access_course_desc(user, action, course): ...@@ -163,11 +163,7 @@ def _has_access_course_desc(user, action, course):
# check start date # check start date
can_load() and can_load() and
# check mobile_available flag # check mobile_available flag
( is_mobile_available_for_user(user, course)
course.mobile_available or
auth.has_access(user, CourseBetaTesterRole(course.id)) or
_has_staff_access_to_descriptor(user, course, course.id)
)
) )
def can_enroll(): def can_enroll():
...@@ -559,6 +555,20 @@ def _has_staff_access_to_descriptor(user, descriptor, course_key): ...@@ -559,6 +555,20 @@ def _has_staff_access_to_descriptor(user, descriptor, course_key):
return _has_staff_access_to_location(user, descriptor.location, course_key) return _has_staff_access_to_location(user, descriptor.location, course_key)
def is_mobile_available_for_user(user, course):
"""
Returns whether the given course is mobile_available for the given user.
Checks:
mobile_available flag on the course
Beta User and staff access overrides the mobile_available flag
"""
return (
course.mobile_available or
auth.has_access(user, CourseBetaTesterRole(course.id)) or
_has_staff_access_to_descriptor(user, course, course.id)
)
def get_user_role(user, course_key): def get_user_role(user, course_key):
""" """
Return corresponding string if user has staff, instructor or student Return corresponding string if user has staff, instructor or student
......
...@@ -3,49 +3,9 @@ Tests for mobile API utilities. ...@@ -3,49 +3,9 @@ Tests for mobile API utilities.
""" """
import ddt import ddt
from mock import patch
from django.test import TestCase from django.test import TestCase
from xmodule.modulestore.tests.factories import CourseFactory from .utils import 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 TestMobileCourseListingAccess(MobileAPITestCase):
"""
Tests for mobile_access_when_enrolled utility function.
"""
@ddt.data(*ROLE_CASES)
@ddt.unpack
def test_mobile_role_access(self, role, should_have_access):
"""
Verifies that our mobile access function properly handles using roles to grant access
"""
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_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_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_course_listing_access(None, self.user))
@patch.dict('django.conf.settings.FEATURES', {'DISABLE_START_DATES': False})
def test_unreleased_course(self):
"""
Verifies that we allow the case where a course hasn't started
"""
self.assertTrue(mobile_course_listing_access(self.course, self.user))
@ddt.ddt @ddt.ddt
...@@ -65,42 +25,3 @@ class TestMobileAPIDecorators(TestCase): ...@@ -65,42 +25,3 @@ class TestMobileAPIDecorators(TestCase):
self.assertIn("Test docstring of decorated function.", decorated_func.__doc__) self.assertIn("Test docstring of decorated function.", decorated_func.__doc__)
self.assertEquals(decorated_func.__name__, "decorated_func") self.assertEquals(decorated_func.__name__, "decorated_func")
self.assertTrue(decorated_func.__module__.endswith("tests")) 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: ...@@ -10,7 +10,7 @@ Test utilities for mobile API tests:
MobileCourseAccessTestMixin - tests for APIs with mobile_course_access and verify_enrolled=False. MobileCourseAccessTestMixin - tests for APIs with mobile_course_access and verify_enrolled=False.
MobileEnrolledCourseAccessTestMixin - tests for APIs with mobile_course_access and verify_enrolled=True. MobileEnrolledCourseAccessTestMixin - tests for APIs with mobile_course_access and verify_enrolled=True.
""" """
# pylint: disable=no-member, invalid-name # pylint: disable=no-member
import ddt import ddt
from mock import patch from mock import patch
from rest_framework.test import APITestCase from rest_framework.test import APITestCase
...@@ -26,15 +26,6 @@ from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase ...@@ -26,15 +26,6 @@ from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory from xmodule.modulestore.tests.factories import CourseFactory
# A tuple of Role Types and Boolean values that indicate whether access should be given to that role.
ROLE_CASES = (
(auth.CourseBetaTesterRole, True),
(auth.CourseStaffRole, True),
(auth.CourseInstructorRole, True),
(None, False)
)
class MobileAPITestCase(ModuleStoreTestCase, APITestCase): class MobileAPITestCase(ModuleStoreTestCase, APITestCase):
""" """
Base class for testing Mobile APIs. Base class for testing Mobile APIs.
...@@ -140,7 +131,7 @@ class MobileCourseAccessTestMixin(object): ...@@ -140,7 +131,7 @@ class MobileCourseAccessTestMixin(object):
Subclasses are expected to inherit from MobileAPITestCase. Subclasses are expected to inherit from MobileAPITestCase.
Subclasses can override verify_success, verify_failure, and init_course_access methods. Subclasses can override verify_success, verify_failure, and init_course_access methods.
""" """
ALLOW_ACCESS_TO_UNRELEASED_COURSE = False ALLOW_ACCESS_TO_UNRELEASED_COURSE = False # pylint: disable=invalid-name
def verify_success(self, response): def verify_success(self, response):
"""Base implementation of verifying a successful response.""" """Base implementation of verifying a successful response."""
...@@ -177,7 +168,13 @@ class MobileCourseAccessTestMixin(object): ...@@ -177,7 +168,13 @@ class MobileCourseAccessTestMixin(object):
else: else:
self.verify_failure(response) self.verify_failure(response)
@ddt.data(*ROLE_CASES) # A tuple of Role Types and Boolean values that indicate whether access should be given to that role.
@ddt.data(
(auth.CourseBetaTesterRole, True),
(auth.CourseStaffRole, True),
(auth.CourseInstructorRole, True),
(None, False)
)
@ddt.unpack @ddt.unpack
def test_non_mobile_available(self, role, should_succeed): def test_non_mobile_available(self, role, should_succeed):
self.init_course_access() self.init_course_access()
......
...@@ -2,9 +2,6 @@ ...@@ -2,9 +2,6 @@
Views for user API Views for user API
""" """
from courseware.model_data import FieldDataCache
from courseware.module_render import get_module_for_descriptor
from django.shortcuts import redirect from django.shortcuts import redirect
from django.utils import dateparse from django.utils import dateparse
...@@ -12,11 +9,13 @@ from rest_framework import generics, views ...@@ -12,11 +9,13 @@ from rest_framework import generics, views
from rest_framework.decorators import api_view from rest_framework.decorators import api_view
from rest_framework.response import Response from rest_framework.response import Response
from courseware.views import get_current_child, save_positions_recursively_up
from opaque_keys.edx.keys import UsageKey from opaque_keys.edx.keys import UsageKey
from opaque_keys import InvalidKeyError from opaque_keys import InvalidKeyError
from courseware.access import is_mobile_available_for_user
from courseware.model_data import FieldDataCache
from courseware.module_render import get_module_for_descriptor
from courseware.views import get_current_child, save_positions_recursively_up
from student.models import CourseEnrollment, User from student.models import CourseEnrollment, User
from xblock.fields import Scope from xblock.fields import Scope
...@@ -25,8 +24,8 @@ from xmodule.modulestore.django import modulestore ...@@ -25,8 +24,8 @@ from xmodule.modulestore.django import modulestore
from xmodule.modulestore.exceptions import ItemNotFoundError from xmodule.modulestore.exceptions import ItemNotFoundError
from .serializers import CourseEnrollmentSerializer, UserSerializer from .serializers import CourseEnrollmentSerializer, UserSerializer
from mobile_api import errors from .. import errors
from mobile_api.utils import mobile_course_listing_access, mobile_view, mobile_course_access from ..utils import mobile_view, mobile_course_access
@mobile_view(is_user=True) @mobile_view(is_user=True)
...@@ -245,7 +244,7 @@ class UserCourseEnrollmentsList(generics.ListAPIView): ...@@ -245,7 +244,7 @@ class UserCourseEnrollmentsList(generics.ListAPIView):
enrollments = self.queryset.filter(user__username=self.kwargs['username'], is_active=True).order_by('created') enrollments = self.queryset.filter(user__username=self.kwargs['username'], is_active=True).order_by('created')
return [ return [
enrollment for enrollment in enrollments enrollment for enrollment in enrollments
if mobile_course_listing_access(enrollment.course, self.request.user) if enrollment.course and is_mobile_available_for_user(self.request.user, enrollment.course)
] ]
......
...@@ -4,9 +4,6 @@ Common utility methods and decorators for Mobile APIs. ...@@ -4,9 +4,6 @@ Common utility methods and decorators for Mobile APIs.
import functools import functools
from contextlib import contextmanager
from django.http import Http404
from django.conf import settings
from opaque_keys.edx.keys import CourseKey from opaque_keys.edx.keys import CourseKey
from courseware.courses import get_course_with_access from courseware.courses import get_course_with_access
...@@ -14,30 +11,6 @@ from rest_framework import permissions ...@@ -14,30 +11,6 @@ from rest_framework import permissions
from rest_framework.authentication import OAuth2Authentication, SessionAuthentication 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): 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. Method decorator for a mobile API endpoint that verifies the user has access to the course in a mobile context.
...@@ -63,26 +36,6 @@ def mobile_course_access(depth=0, verify_enrolled=True): ...@@ -63,26 +36,6 @@ def mobile_course_access(depth=0, verify_enrolled=True):
return _decorator return _decorator
def mobile_course_listing_access(course, user):
"""
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:
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
def mobile_view(is_user=False): def mobile_view(is_user=False):
""" """
Function and class decorator that abstracts the authentication and permission checks for mobile api views. Function and class decorator that abstracts the authentication and permission checks for mobile api views.
......
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