MA-779 Make has_access work on CourseOverview objects
This commit is contained in:
@@ -9,6 +9,7 @@ from django.utils.translation import ugettext as _
|
||||
from opaque_keys import InvalidKeyError
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
|
||||
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
|
||||
from xmodule.modulestore.django import modulestore
|
||||
|
||||
NAMESPACE_CHOICES = {
|
||||
@@ -86,33 +87,46 @@ def set_prerequisite_courses(course_key, prerequisite_course_keys):
|
||||
add_prerequisite_course(course_key, prerequisite_course_key)
|
||||
|
||||
|
||||
def get_pre_requisite_courses_not_completed(user, enrolled_courses):
|
||||
def get_pre_requisite_courses_not_completed(user, enrolled_courses): # pylint: disable=invalid-name
|
||||
"""
|
||||
It would make dict of prerequisite courses not completed by user among courses
|
||||
user has enrolled in. It calls the fulfilment api of milestones app and
|
||||
iterates over all fulfilment milestones not achieved to make dict of
|
||||
prerequisite courses yet to be completed.
|
||||
"""
|
||||
pre_requisite_courses = {}
|
||||
if settings.FEATURES.get('ENABLE_PREREQUISITE_COURSES', False):
|
||||
from milestones import api as milestones_api
|
||||
for course_key in enrolled_courses:
|
||||
required_courses = []
|
||||
fulfilment_paths = milestones_api.get_course_milestones_fulfillment_paths(course_key, {'id': user.id})
|
||||
for milestone_key, milestone_value in fulfilment_paths.items(): # pylint: disable=unused-variable
|
||||
for key, value in milestone_value.items():
|
||||
if key == 'courses' and value:
|
||||
for required_course in value:
|
||||
required_course_key = CourseKey.from_string(required_course)
|
||||
required_course_descriptor = modulestore().get_course(required_course_key)
|
||||
required_courses.append({
|
||||
'key': required_course_key,
|
||||
'display': get_course_display_name(required_course_descriptor)
|
||||
})
|
||||
Makes a dict mapping courses to their unfulfilled milestones using the
|
||||
fulfillment API of the milestones app.
|
||||
|
||||
Arguments:
|
||||
user (User): the user for whom we are checking prerequisites.
|
||||
enrolled_courses (CourseKey): a list of keys for the courses to be
|
||||
checked. The given user must be enrolled in all of these courses.
|
||||
|
||||
Returns:
|
||||
dict[CourseKey: dict[
|
||||
'courses': list[dict['key': CourseKey, 'display': str]]
|
||||
]]
|
||||
If a course has no incomplete prerequisites, it will be excluded from the
|
||||
dictionary.
|
||||
"""
|
||||
if not settings.FEATURES.get('ENABLE_PREREQUISITE_COURSES', False):
|
||||
return {}
|
||||
|
||||
from milestones import api as milestones_api
|
||||
pre_requisite_courses = {}
|
||||
|
||||
for course_key in enrolled_courses:
|
||||
required_courses = []
|
||||
fulfillment_paths = milestones_api.get_course_milestones_fulfillment_paths(course_key, {'id': user.id})
|
||||
for __, milestone_value in fulfillment_paths.items():
|
||||
for key, value in milestone_value.items():
|
||||
if key == 'courses' and value:
|
||||
for required_course in value:
|
||||
required_course_key = CourseKey.from_string(required_course)
|
||||
required_course_overview = CourseOverview.get_from_id(required_course_key)
|
||||
required_courses.append({
|
||||
'key': required_course_key,
|
||||
'display': get_course_display_string(required_course_overview)
|
||||
})
|
||||
# If there are required courses, add them to the result dict.
|
||||
if required_courses:
|
||||
pre_requisite_courses[course_key] = {'courses': required_courses}
|
||||
|
||||
# if there are required courses add to dict
|
||||
if required_courses:
|
||||
pre_requisite_courses[course_key] = {'courses': required_courses}
|
||||
return pre_requisite_courses
|
||||
|
||||
|
||||
@@ -129,15 +143,18 @@ def get_prerequisite_courses_display(course_descriptor):
|
||||
required_course_descriptor = modulestore().get_course(course_key)
|
||||
prc = {
|
||||
'key': course_key,
|
||||
'display': get_course_display_name(required_course_descriptor)
|
||||
'display': get_course_display_string(required_course_descriptor)
|
||||
}
|
||||
pre_requisite_courses.append(prc)
|
||||
return pre_requisite_courses
|
||||
|
||||
|
||||
def get_course_display_name(descriptor):
|
||||
def get_course_display_string(descriptor):
|
||||
"""
|
||||
It would return display name from given course descriptor
|
||||
Returns a string to display for a course or course overview.
|
||||
|
||||
Arguments:
|
||||
descriptor (CourseDescriptor|CourseOverview): a course or course overview.
|
||||
"""
|
||||
return ' '.join([
|
||||
descriptor.display_org_with_default,
|
||||
|
||||
@@ -33,6 +33,7 @@ from xmodule.util.django import get_current_request_hostname
|
||||
|
||||
from external_auth.models import ExternalAuthMap
|
||||
from courseware.masquerade import get_masquerade_role, is_masquerading_as_student
|
||||
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
|
||||
from student import auth
|
||||
from student.models import CourseEnrollmentAllowed
|
||||
from student.roles import (
|
||||
@@ -100,6 +101,9 @@ def has_access(user, action, obj, course_key=None):
|
||||
if isinstance(obj, CourseDescriptor):
|
||||
return _has_access_course_desc(user, action, obj)
|
||||
|
||||
if isinstance(obj, CourseOverview):
|
||||
return _has_access_course_overview(user, action, obj)
|
||||
|
||||
if isinstance(obj, ErrorDescriptor):
|
||||
return _has_access_error_desc(user, action, obj, course_key)
|
||||
|
||||
@@ -129,6 +133,87 @@ def has_access(user, action, obj, course_key=None):
|
||||
|
||||
|
||||
# ================ Implementation helpers ================================
|
||||
def _can_access_descriptor_with_start_date(user, descriptor, course_key): # pylint: disable=invalid-name
|
||||
"""
|
||||
Checks if a user has access to a descriptor based on its start date.
|
||||
|
||||
If there is no start date specified, grant access.
|
||||
Else, check if we're past the start date.
|
||||
|
||||
Note:
|
||||
We do NOT check whether the user is staff or if the descriptor
|
||||
is detached... it is assumed both of these are checked by the caller.
|
||||
|
||||
Arguments:
|
||||
user (User): the user whose descriptor access we are checking.
|
||||
descriptor (AType): the descriptor for which we are checking access.
|
||||
where AType is any descriptor that has the attributes .location and
|
||||
.days_early_for_beta
|
||||
"""
|
||||
start_dates_disabled = settings.FEATURES['DISABLE_START_DATES']
|
||||
if start_dates_disabled and not is_masquerading_as_student(user, course_key):
|
||||
return True
|
||||
else:
|
||||
now = datetime.now(UTC())
|
||||
effective_start = _adjust_start_date_for_beta_testers(
|
||||
user,
|
||||
descriptor,
|
||||
course_key=course_key
|
||||
)
|
||||
return (
|
||||
descriptor.start is None
|
||||
or now > effective_start
|
||||
or in_preview_mode()
|
||||
)
|
||||
|
||||
|
||||
def _can_view_courseware_with_prerequisites(user, course): # pylint: disable=invalid-name
|
||||
"""
|
||||
Checks if a user has access to a course based on its prerequisites.
|
||||
|
||||
If the user is staff or anonymous, immediately grant access.
|
||||
Else, return whether or not the prerequisite courses have been passed.
|
||||
|
||||
Arguments:
|
||||
user (User): the user whose course access we are checking.
|
||||
course (AType): the course for which we are checking access.
|
||||
where AType is CourseDescriptor, CourseOverview, or any other class that
|
||||
represents a course and has the attributes .location and .id.
|
||||
"""
|
||||
return (
|
||||
not settings.FEATURES['ENABLE_PREREQUISITE_COURSES']
|
||||
or _has_staff_access_to_descriptor(user, course, course.id)
|
||||
or not course.pre_requisite_courses
|
||||
or user.is_anonymous()
|
||||
or not get_pre_requisite_courses_not_completed(user, [course.id])
|
||||
)
|
||||
|
||||
|
||||
def _can_load_course_on_mobile(user, course):
|
||||
"""
|
||||
Checks if a user can view the given course on a mobile device.
|
||||
|
||||
This function only checks mobile-specific access restrictions. Other access
|
||||
restrictions such as start date and the .visible_to_staff_only flag must
|
||||
be checked by callers in *addition* to the return value of this function.
|
||||
|
||||
Arguments:
|
||||
user (User): the user whose course access we are checking.
|
||||
course (CourseDescriptor|CourseOverview): the course for which we are
|
||||
checking access.
|
||||
|
||||
Returns:
|
||||
bool: whether the course can be accessed on mobile.
|
||||
"""
|
||||
return (
|
||||
is_mobile_available_for_user(user, course) and
|
||||
(
|
||||
_has_staff_access_to_descriptor(user, course, course.id) or
|
||||
not any_unfulfilled_milestones(course.id, user.id)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def _has_access_course_desc(user, action, course):
|
||||
"""
|
||||
Check if user has access to a course descriptor.
|
||||
@@ -154,23 +239,6 @@ def _has_access_course_desc(user, action, course):
|
||||
# delegate to generic descriptor check to check start dates
|
||||
return _has_access_descriptor(user, 'load', course, course.id)
|
||||
|
||||
def can_load_mobile():
|
||||
"""
|
||||
Can this user access this course from a mobile device?
|
||||
"""
|
||||
return (
|
||||
# check start date
|
||||
can_load() and
|
||||
# check mobile_available flag
|
||||
is_mobile_available_for_user(user, course) and
|
||||
(
|
||||
# either is a staff user or
|
||||
_has_staff_access_to_descriptor(user, course, course.id) or
|
||||
# check for unfulfilled milestones
|
||||
not any_unfulfilled_milestones(course.id, user.id)
|
||||
)
|
||||
)
|
||||
|
||||
def can_enroll():
|
||||
"""
|
||||
First check if restriction of enrollment by login method is enabled, both
|
||||
@@ -274,25 +342,11 @@ def _has_access_course_desc(user, action, course):
|
||||
_has_staff_access_to_descriptor(user, course, course.id)
|
||||
)
|
||||
|
||||
def can_view_courseware_with_prerequisites(): # pylint: disable=invalid-name
|
||||
"""
|
||||
Checks if prerequisite courses feature is enabled and course has prerequisites
|
||||
and user is neither staff nor anonymous then it returns False if user has not
|
||||
passed prerequisite courses otherwise return True.
|
||||
"""
|
||||
if settings.FEATURES['ENABLE_PREREQUISITE_COURSES'] \
|
||||
and not _has_staff_access_to_descriptor(user, course, course.id) \
|
||||
and course.pre_requisite_courses \
|
||||
and not user.is_anonymous() \
|
||||
and get_pre_requisite_courses_not_completed(user, [course.id]):
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
checkers = {
|
||||
'load': can_load,
|
||||
'view_courseware_with_prerequisites': can_view_courseware_with_prerequisites,
|
||||
'load_mobile': can_load_mobile,
|
||||
'view_courseware_with_prerequisites':
|
||||
lambda: _can_view_courseware_with_prerequisites(user, course),
|
||||
'load_mobile': lambda: can_load() and _can_load_course_on_mobile(user, course),
|
||||
'enroll': can_enroll,
|
||||
'see_exists': see_exists,
|
||||
'staff': lambda: _has_staff_access_to_descriptor(user, course, course.id),
|
||||
@@ -304,6 +358,51 @@ def _has_access_course_desc(user, action, course):
|
||||
return _dispatch(checkers, action, user, course)
|
||||
|
||||
|
||||
def _can_load_course_overview(user, course_overview):
|
||||
"""
|
||||
Check if a user can load a course overview.
|
||||
|
||||
Arguments:
|
||||
user (User): the user whose course access we are checking.
|
||||
course_overview (CourseOverview): a course overview.
|
||||
|
||||
Note:
|
||||
The user doesn't have to be enrolled in the course in order to have load
|
||||
load access.
|
||||
"""
|
||||
return (
|
||||
not course_overview.visible_to_staff_only
|
||||
and _can_access_descriptor_with_start_date(user, course_overview, course_overview.id)
|
||||
) or _has_staff_access_to_descriptor(user, course_overview, course_overview.id)
|
||||
|
||||
|
||||
_COURSE_OVERVIEW_CHECKERS = {
|
||||
'load': _can_load_course_overview,
|
||||
'load_mobile': lambda user, course_overview: (
|
||||
_can_load_course_overview(user, course_overview)
|
||||
and _can_load_course_on_mobile(user, course_overview)
|
||||
),
|
||||
'view_courseware_with_prerequisites': _can_view_courseware_with_prerequisites
|
||||
}
|
||||
COURSE_OVERVIEW_SUPPORTED_ACTIONS = _COURSE_OVERVIEW_CHECKERS.keys() # pylint: disable=invalid-name
|
||||
|
||||
|
||||
def _has_access_course_overview(user, action, course_overview):
|
||||
"""
|
||||
Check if user has access to a course overview.
|
||||
|
||||
Arguments:
|
||||
user (User): the user whose course access we are checking.
|
||||
action (str): the action the user is trying to perform.
|
||||
See COURSE_OVERVIEW_SUPPORTED_ACTIONS for valid values.
|
||||
course_overview (CourseOverview): overview of the course in question.
|
||||
"""
|
||||
if action in _COURSE_OVERVIEW_CHECKERS:
|
||||
return _COURSE_OVERVIEW_CHECKERS[action](user, course_overview)
|
||||
else:
|
||||
raise ValueError(u"Unknown action for object type 'CourseOverview': '{}'".format(action))
|
||||
|
||||
|
||||
def _has_access_error_desc(user, action, descriptor, course_key):
|
||||
"""
|
||||
Only staff should see error descriptors.
|
||||
@@ -408,38 +507,14 @@ def _has_access_descriptor(user, action, descriptor, course_key=None):
|
||||
students to see modules. If not, views should check the course, so we
|
||||
don't have to hit the enrollments table on every module load.
|
||||
"""
|
||||
if descriptor.visible_to_staff_only and not _has_staff_access_to_descriptor(user, descriptor, course_key):
|
||||
return False
|
||||
|
||||
# enforce group access
|
||||
if not _has_group_access(descriptor, user, course_key):
|
||||
# if group_access check failed, deny access unless the requestor is staff,
|
||||
# in which case immediately grant access.
|
||||
return _has_staff_access_to_descriptor(user, descriptor, course_key)
|
||||
|
||||
# If start dates are off, can always load
|
||||
if settings.FEATURES['DISABLE_START_DATES'] and not is_masquerading_as_student(user, course_key):
|
||||
debug("Allow: DISABLE_START_DATES")
|
||||
return True
|
||||
|
||||
# Check start date
|
||||
if 'detached' not in descriptor._class_tags and descriptor.start is not None:
|
||||
now = datetime.now(UTC())
|
||||
effective_start = _adjust_start_date_for_beta_testers(
|
||||
user,
|
||||
descriptor,
|
||||
course_key=course_key
|
||||
return (
|
||||
not descriptor.visible_to_staff_only
|
||||
and _has_group_access(descriptor, user, course_key)
|
||||
and (
|
||||
'detached' in descriptor._class_tags # pylint: disable=protected-access
|
||||
or _can_access_descriptor_with_start_date(user, descriptor, course_key)
|
||||
)
|
||||
if in_preview_mode() or now > effective_start:
|
||||
# after start date, everyone can see it
|
||||
debug("Allow: now > effective start date")
|
||||
return True
|
||||
# otherwise, need staff access
|
||||
return _has_staff_access_to_descriptor(user, descriptor, course_key)
|
||||
|
||||
# No start date, so can always load.
|
||||
debug("Allow: no start date")
|
||||
return True
|
||||
) or _has_staff_access_to_descriptor(user, descriptor, course_key)
|
||||
|
||||
checkers = {
|
||||
'load': can_load,
|
||||
@@ -700,4 +775,4 @@ def in_preview_mode():
|
||||
Returns whether the user is in preview mode or not.
|
||||
"""
|
||||
hostname = get_current_request_hostname()
|
||||
return hostname and settings.PREVIEW_DOMAIN in hostname.split('.')
|
||||
return bool(hostname and settings.PREVIEW_DOMAIN in hostname.split('.'))
|
||||
|
||||
@@ -5,6 +5,8 @@ from django.core.urlresolvers import reverse
|
||||
from django.test import TestCase
|
||||
from django.test.client import RequestFactory
|
||||
|
||||
from courseware.access import has_access, COURSE_OVERVIEW_SUPPORTED_ACTIONS
|
||||
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
|
||||
from student.models import Registration
|
||||
|
||||
|
||||
@@ -137,3 +139,50 @@ class LoginEnrollmentTestCase(TestCase):
|
||||
'course_id': course.id.to_deprecated_string(),
|
||||
}
|
||||
self.assert_request_status_code(200, url, method="POST", data=request_data)
|
||||
|
||||
|
||||
class CourseAccessTestMixin(TestCase):
|
||||
"""
|
||||
Utility mixin for asserting access (or lack thereof) to courses.
|
||||
If relevant, also checks access for courses' corresponding CourseOverviews.
|
||||
"""
|
||||
|
||||
def assertCanAccessCourse(self, user, action, course):
|
||||
"""
|
||||
Assert that a user has access to the given action for a given course.
|
||||
|
||||
Test with both the given course and, if the action is supported, with
|
||||
a CourseOverview of the given course.
|
||||
|
||||
Arguments:
|
||||
user (User): a user.
|
||||
action (str): type of access to test.
|
||||
See access.py:COURSE_OVERVIEW_SUPPORTED_ACTIONS.
|
||||
course (CourseDescriptor): a course.
|
||||
"""
|
||||
self.assertTrue(has_access(user, action, course))
|
||||
if action in COURSE_OVERVIEW_SUPPORTED_ACTIONS:
|
||||
self.assertTrue(has_access(user, action, CourseOverview.get_from_id(course.id)))
|
||||
|
||||
def assertCannotAccessCourse(self, user, action, course):
|
||||
"""
|
||||
Assert that a user lacks access to the given action the given course.
|
||||
|
||||
Test with both the given course and, if the action is supported, with
|
||||
a CourseOverview of the given course.
|
||||
|
||||
Arguments:
|
||||
user (User): a user.
|
||||
action (str): type of access to test.
|
||||
See access.py:COURSE_OVERVIEW_SUPPORTED_ACTIONS.
|
||||
course (CourseDescriptor): a course.
|
||||
|
||||
Note:
|
||||
It may seem redundant to have one method for testing access
|
||||
and another method for testing lack thereof (why not just combine
|
||||
them into one method with a boolean flag?), but it makes reading
|
||||
stack traces of failed tests easier to understand at a glance.
|
||||
"""
|
||||
self.assertFalse(has_access(user, action, course))
|
||||
if action in COURSE_OVERVIEW_SUPPORTED_ACTIONS:
|
||||
self.assertFalse(has_access(user, action, CourseOverview.get_from_id(course.id)))
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import datetime
|
||||
import ddt
|
||||
import itertools
|
||||
import pytz
|
||||
|
||||
from django.test import TestCase
|
||||
@@ -9,8 +11,9 @@ from opaque_keys.edx.locations import SlashSeparatedCourseKey
|
||||
|
||||
import courseware.access as access
|
||||
from courseware.masquerade import CourseMasquerade
|
||||
from courseware.tests.factories import UserFactory, StaffFactory, InstructorFactory
|
||||
from courseware.tests.factories import UserFactory, StaffFactory, InstructorFactory, BetaTesterFactory
|
||||
from courseware.tests.helpers import LoginEnrollmentTestCase
|
||||
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
|
||||
from student.tests.factories import AnonymousUserFactory, CourseEnrollmentAllowedFactory, CourseEnrollmentFactory
|
||||
from xmodule.course_module import (
|
||||
CATALOG_VISIBILITY_CATALOG_AND_ABOUT, CATALOG_VISIBILITY_ABOUT,
|
||||
@@ -18,6 +21,7 @@ from xmodule.course_module import (
|
||||
)
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from util.milestones_helpers import fulfill_course_milestone
|
||||
|
||||
from util.milestones_helpers import (
|
||||
set_prerequisite_courses,
|
||||
@@ -424,3 +428,91 @@ class UserRoleTestCase(TestCase):
|
||||
'student',
|
||||
access.get_user_role(self.anonymous_user, self.course_key)
|
||||
)
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class CourseOverviewAccessTestCase(ModuleStoreTestCase):
|
||||
"""
|
||||
Tests confirming that has_access works equally on CourseDescriptors and
|
||||
CourseOverviews.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super(CourseOverviewAccessTestCase, self).setUp()
|
||||
|
||||
today = datetime.datetime.now(pytz.UTC)
|
||||
last_week = today - datetime.timedelta(days=7)
|
||||
next_week = today + datetime.timedelta(days=7)
|
||||
|
||||
self.course_default = CourseFactory.create()
|
||||
self.course_started = CourseFactory.create(start=last_week)
|
||||
self.course_not_started = CourseFactory.create(start=next_week, days_early_for_beta=10)
|
||||
self.course_staff_only = CourseFactory.create(visible_to_staff_only=True)
|
||||
self.course_mobile_available = CourseFactory.create(mobile_available=True)
|
||||
self.course_with_pre_requisite = CourseFactory.create(
|
||||
pre_requisite_courses=[str(self.course_started.id)]
|
||||
)
|
||||
self.course_with_pre_requisites = CourseFactory.create(
|
||||
pre_requisite_courses=[str(self.course_started.id), str(self.course_not_started.id)]
|
||||
)
|
||||
|
||||
self.user_normal = UserFactory.create()
|
||||
self.user_beta_tester = BetaTesterFactory.create(course_key=self.course_not_started.id)
|
||||
self.user_completed_pre_requisite = UserFactory.create() # pylint: disable=invalid-name
|
||||
fulfill_course_milestone(self.user_completed_pre_requisite, self.course_started.id)
|
||||
self.user_staff = UserFactory.create(is_staff=True)
|
||||
self.user_anonymous = AnonymousUserFactory.create()
|
||||
|
||||
LOAD_TEST_DATA = list(itertools.product(
|
||||
['user_normal', 'user_beta_tester', 'user_staff'],
|
||||
['load'],
|
||||
['course_default', 'course_started', 'course_not_started', 'course_staff_only'],
|
||||
))
|
||||
|
||||
LOAD_MOBILE_TEST_DATA = list(itertools.product(
|
||||
['user_normal', 'user_staff'],
|
||||
['load_mobile'],
|
||||
['course_default', 'course_mobile_available'],
|
||||
))
|
||||
|
||||
PREREQUISITES_TEST_DATA = list(itertools.product(
|
||||
['user_normal', 'user_completed_pre_requisite', 'user_staff', 'user_anonymous'],
|
||||
['view_courseware_with_prerequisites'],
|
||||
['course_default', 'course_with_pre_requisite', 'course_with_pre_requisites'],
|
||||
))
|
||||
|
||||
@ddt.data(*(LOAD_TEST_DATA + LOAD_MOBILE_TEST_DATA + PREREQUISITES_TEST_DATA))
|
||||
@ddt.unpack
|
||||
def test_course_overview_access(self, user_attr_name, action, course_attr_name):
|
||||
"""
|
||||
Check that a user's access to a course is equal to the user's access to
|
||||
the corresponding course overview.
|
||||
|
||||
Instead of taking a user and course directly as arguments, we have to
|
||||
take their attribute names, as ddt doesn't allow us to reference self.
|
||||
|
||||
Arguments:
|
||||
user_attr_name (str): the name of the attribute on self that is the
|
||||
User to test with.
|
||||
action (str): action to test with.
|
||||
See COURSE_OVERVIEW_SUPPORTED_ACTIONS for valid values.
|
||||
course_attr_name (str): the name of the attribute on self that is
|
||||
the CourseDescriptor to test with.
|
||||
"""
|
||||
user = getattr(self, user_attr_name)
|
||||
course = getattr(self, course_attr_name)
|
||||
|
||||
course_overview = CourseOverview.get_from_id(course.id)
|
||||
self.assertEqual(
|
||||
access.has_access(user, action, course, course_key=course.id),
|
||||
access.has_access(user, action, course_overview, course_key=course.id)
|
||||
)
|
||||
|
||||
def test_course_overivew_unsupported_action(self):
|
||||
"""
|
||||
Check that calling has_access with an unsupported action raises a
|
||||
ValueError.
|
||||
"""
|
||||
overview = CourseOverview.get_from_id(self.course_default.id)
|
||||
with self.assertRaises(ValueError):
|
||||
access.has_access(self.user, '_non_existent_action', overview)
|
||||
|
||||
@@ -6,7 +6,7 @@ from mock import patch
|
||||
from nose.plugins.attrib import attr
|
||||
|
||||
from courseware.access import has_access
|
||||
from courseware.tests.helpers import LoginEnrollmentTestCase
|
||||
from courseware.tests.helpers import CourseAccessTestMixin, LoginEnrollmentTestCase
|
||||
from courseware.tests.factories import (
|
||||
BetaTesterFactory,
|
||||
StaffFactory,
|
||||
@@ -389,7 +389,7 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
|
||||
|
||||
@attr('shard_1')
|
||||
class TestBetatesterAccess(ModuleStoreTestCase):
|
||||
class TestBetatesterAccess(ModuleStoreTestCase, CourseAccessTestMixin):
|
||||
"""
|
||||
Tests for the beta tester feature
|
||||
"""
|
||||
@@ -411,12 +411,8 @@ class TestBetatesterAccess(ModuleStoreTestCase):
|
||||
Check that beta-test access works for courses.
|
||||
"""
|
||||
self.assertFalse(self.course.has_started())
|
||||
|
||||
# student user shouldn't see it
|
||||
self.assertFalse(has_access(self.normal_student, 'load', self.course))
|
||||
|
||||
# now the student should see it
|
||||
self.assertTrue(has_access(self.beta_tester, 'load', self.course))
|
||||
self.assertCannotAccessCourse(self.normal_student, 'load', self.course)
|
||||
self.assertCanAccessCourse(self.beta_tester, 'load', self.course)
|
||||
|
||||
@patch.dict('courseware.access.settings.FEATURES', {'DISABLE_START_DATES': False})
|
||||
def test_content_beta_period(self):
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from south.utils import datetime_utils as datetime
|
||||
from south.db import db
|
||||
from south.v2 import SchemaMigration
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Migration(SchemaMigration):
|
||||
|
||||
def forwards(self, orm):
|
||||
# Adding field 'CourseOverview.days_early_for_beta'
|
||||
|
||||
# The default value for the days_early_for_beta column is null. However,
|
||||
# for courses already in the table that have a non-null value for
|
||||
# days_early_for_beta, this would be invalid. So, we must clear the
|
||||
# table before adding the new column.
|
||||
|
||||
db.clear_table('course_overviews_courseoverview')
|
||||
db.add_column('course_overviews_courseoverview', 'days_early_for_beta',
|
||||
self.gf('django.db.models.fields.FloatField')(null=True),
|
||||
keep_default=False)
|
||||
|
||||
|
||||
def backwards(self, orm):
|
||||
# Deleting field 'CourseOverview.days_early_for_beta'
|
||||
db.delete_column('course_overviews_courseoverview', 'days_early_for_beta')
|
||||
|
||||
|
||||
models = {
|
||||
'course_overviews.courseoverview': {
|
||||
'Meta': {'object_name': 'CourseOverview'},
|
||||
'_location': ('xmodule_django.models.UsageKeyField', [], {'max_length': '255'}),
|
||||
'_pre_requisite_courses_json': ('django.db.models.fields.TextField', [], {}),
|
||||
'advertised_start': ('django.db.models.fields.TextField', [], {'null': 'True'}),
|
||||
'cert_name_long': ('django.db.models.fields.TextField', [], {}),
|
||||
'cert_name_short': ('django.db.models.fields.TextField', [], {}),
|
||||
'certificates_display_behavior': ('django.db.models.fields.TextField', [], {'null': 'True'}),
|
||||
'certificates_show_before_end': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'course_image_url': ('django.db.models.fields.TextField', [], {}),
|
||||
'days_early_for_beta': ('django.db.models.fields.FloatField', [], {'null': 'True'}),
|
||||
'display_name': ('django.db.models.fields.TextField', [], {'null': 'True'}),
|
||||
'display_number_with_default': ('django.db.models.fields.TextField', [], {}),
|
||||
'display_org_with_default': ('django.db.models.fields.TextField', [], {}),
|
||||
'end': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
|
||||
'end_of_course_survey_url': ('django.db.models.fields.TextField', [], {'null': 'True'}),
|
||||
'facebook_url': ('django.db.models.fields.TextField', [], {'null': 'True'}),
|
||||
'has_any_active_web_certificate': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'id': ('xmodule_django.models.CourseKeyField', [], {'max_length': '255', 'primary_key': 'True', 'db_index': 'True'}),
|
||||
'lowest_passing_grade': ('django.db.models.fields.DecimalField', [], {'max_digits': '5', 'decimal_places': '2'}),
|
||||
'mobile_available': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'social_sharing_url': ('django.db.models.fields.TextField', [], {'null': 'True'}),
|
||||
'start': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
|
||||
'visible_to_staff_only': ('django.db.models.fields.BooleanField', [], {'default': 'False'})
|
||||
}
|
||||
}
|
||||
|
||||
complete_apps = ['course_overviews']
|
||||
@@ -5,11 +5,9 @@ Declaration of CourseOverview model
|
||||
import json
|
||||
|
||||
import django.db.models
|
||||
from django.db.models.fields import BooleanField, DateTimeField, DecimalField, TextField
|
||||
from django.db.models.fields import BooleanField, DateTimeField, DecimalField, TextField, FloatField
|
||||
from django.utils.translation import ugettext
|
||||
|
||||
from lms.djangoapps.certificates.api import get_active_web_certificate
|
||||
from lms.djangoapps.courseware.courses import course_image_url
|
||||
from util.date_utils import strftime_localized
|
||||
from xmodule import course_metadata_utils
|
||||
from xmodule.modulestore.django import modulestore
|
||||
@@ -54,6 +52,7 @@ class CourseOverview(django.db.models.Model):
|
||||
lowest_passing_grade = DecimalField(max_digits=5, decimal_places=2)
|
||||
|
||||
# Access parameters
|
||||
days_early_for_beta = FloatField(null=True)
|
||||
mobile_available = BooleanField()
|
||||
visible_to_staff_only = BooleanField()
|
||||
_pre_requisite_courses_json = TextField() # JSON representation of list of CourseKey strings
|
||||
@@ -72,6 +71,9 @@ class CourseOverview(django.db.models.Model):
|
||||
Returns:
|
||||
CourseOverview: overview extracted from the given course
|
||||
"""
|
||||
from lms.djangoapps.certificates.api import get_active_web_certificate
|
||||
from lms.djangoapps.courseware.courses import course_image_url
|
||||
|
||||
return CourseOverview(
|
||||
id=course.id,
|
||||
_location=course.location,
|
||||
@@ -95,6 +97,7 @@ class CourseOverview(django.db.models.Model):
|
||||
lowest_passing_grade=course.lowest_passing_grade,
|
||||
end_of_course_survey_url=course.end_of_course_survey_url,
|
||||
|
||||
days_early_for_beta=course.days_early_for_beta,
|
||||
mobile_available=course.mobile_available,
|
||||
visible_to_staff_only=course.visible_to_staff_only,
|
||||
_pre_requisite_courses_json=json.dumps(course.pre_requisite_courses)
|
||||
|
||||
Reference in New Issue
Block a user