Change content_type_gating enabled_for_enrollment to return false if a course has no verified mode or a user is not enrolled in the audit mode
This commit is contained in:
@@ -1,8 +1,10 @@
|
||||
"""
|
||||
Helper functions used by both content_type_gating and course_duration_limits.
|
||||
"""
|
||||
import logging
|
||||
|
||||
from xmodule.partitions.partitions import Group
|
||||
from course_modes.models import CourseMode
|
||||
|
||||
# Studio generates partition IDs starting at 100. There is already a manually generated
|
||||
# partition for Enrollment Track that uses ID 50, so we'll use 51.
|
||||
@@ -14,3 +16,42 @@ CONTENT_TYPE_GATE_GROUP_IDS = {
|
||||
}
|
||||
LIMITED_ACCESS = Group(CONTENT_TYPE_GATE_GROUP_IDS['limited_access'], 'Limited-access Users')
|
||||
FULL_ACCESS = Group(CONTENT_TYPE_GATE_GROUP_IDS['full_access'], 'Full-access Users')
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def correct_modes_for_fbe(course_key, enrollment=None, user=None):
|
||||
"""
|
||||
If CONTENT_TYPE_GATING is enabled use the following logic to determine whether
|
||||
enabled_for_enrollment should be false
|
||||
"""
|
||||
if course_key is None:
|
||||
return True
|
||||
|
||||
modes = CourseMode.modes_for_course(course_key, include_expired=True, only_selectable=False)
|
||||
modes_dict = {mode.slug: mode for mode in modes}
|
||||
|
||||
# If there is no audit mode or no verified mode, FBE will not be enabled
|
||||
if (CourseMode.AUDIT not in modes_dict) or (CourseMode.VERIFIED not in modes_dict):
|
||||
return False
|
||||
|
||||
if enrollment and user:
|
||||
mode_slug = enrollment.mode
|
||||
if enrollment.is_active:
|
||||
course_mode = CourseMode.mode_for_course(
|
||||
course_key,
|
||||
mode_slug,
|
||||
modes=modes,
|
||||
)
|
||||
if course_mode is None:
|
||||
LOG.error(
|
||||
u"User %s is in an unknown CourseMode '%s'"
|
||||
u" for course %s. Granting full access to content for this user",
|
||||
user.username,
|
||||
mode_slug,
|
||||
course_key,
|
||||
)
|
||||
return False
|
||||
|
||||
if mode_slug != CourseMode.AUDIT:
|
||||
return False
|
||||
return True
|
||||
|
||||
@@ -20,7 +20,7 @@ from lms.djangoapps.courseware.masquerade import (
|
||||
)
|
||||
from openedx.core.djangoapps.config_model_utils.models import StackedConfigurationModel
|
||||
from openedx.core.djangoapps.config_model_utils.utils import is_in_holdback
|
||||
from openedx.features.content_type_gating.helpers import FULL_ACCESS, LIMITED_ACCESS
|
||||
from openedx.features.content_type_gating.helpers import FULL_ACCESS, LIMITED_ACCESS, correct_modes_for_fbe
|
||||
from student.models import CourseEnrollment
|
||||
from student.role_helpers import has_staff_roles
|
||||
from xmodule.partitions.partitions import ENROLLMENT_TRACK_PARTITION_ID
|
||||
@@ -137,14 +137,18 @@ class ContentTypeGatingConfig(StackedConfigurationModel):
|
||||
if user_variable_represents_correct_user and is_in_holdback(user):
|
||||
return False
|
||||
|
||||
if not correct_modes_for_fbe(course_key, enrollment, user):
|
||||
return False
|
||||
|
||||
# enrollment might be None if the user isn't enrolled. In that case,
|
||||
# return enablement as if the user enrolled today
|
||||
# Also, ignore enrollment creation date if the user is masquerading.
|
||||
if enrollment is None or course_masquerade:
|
||||
return cls.enabled_for_course(course_key=course_key, target_datetime=timezone.now())
|
||||
target_datetime = timezone.now()
|
||||
else:
|
||||
current_config = cls.current(course_key=enrollment.course_id)
|
||||
return current_config.enabled_as_of_datetime(target_datetime=enrollment.created)
|
||||
target_datetime = enrollment.created
|
||||
current_config = cls.current(course_key=course_key)
|
||||
return current_config.enabled_as_of_datetime(target_datetime=target_datetime)
|
||||
|
||||
@classmethod
|
||||
def enabled_for_course(cls, course_key, target_datetime=None):
|
||||
@@ -161,6 +165,8 @@ class ContentTypeGatingConfig(StackedConfigurationModel):
|
||||
course_key: The CourseKey of the course being queried.
|
||||
target_datetime: The datetime to checked enablement as of. Defaults to the current date and time.
|
||||
"""
|
||||
if not correct_modes_for_fbe(course_key):
|
||||
return False
|
||||
|
||||
if target_datetime is None:
|
||||
target_datetime = timezone.now()
|
||||
|
||||
@@ -177,51 +177,10 @@ class ContentTypeGatingPartitionScheme(object):
|
||||
"""
|
||||
Returns the Group for the specified user.
|
||||
"""
|
||||
|
||||
# For now, treat everyone as a Full-access user, until we have the rest of the
|
||||
# feature gating logic in place.
|
||||
|
||||
if not ContentTypeGatingConfig.enabled_for_enrollment(user=user, course_key=course_key,
|
||||
user_partition=user_partition):
|
||||
return FULL_ACCESS
|
||||
|
||||
# If CONTENT_TYPE_GATING is enabled use the following logic to determine whether a user should have FULL_ACCESS
|
||||
# or LIMITED_ACCESS
|
||||
|
||||
course_mode = apps.get_model('course_modes.CourseMode')
|
||||
modes = course_mode.modes_for_course(course_key, include_expired=True, only_selectable=False)
|
||||
modes_dict = {mode.slug: mode for mode in modes}
|
||||
|
||||
# If there is no verified mode, all users are granted FULL_ACCESS
|
||||
if not course_mode.has_verified_mode(modes_dict):
|
||||
return FULL_ACCESS
|
||||
|
||||
course_enrollment = apps.get_model('student.CourseEnrollment')
|
||||
|
||||
mode_slug, is_active = course_enrollment.enrollment_mode_for_user(user, course_key)
|
||||
|
||||
if mode_slug and is_active:
|
||||
course_mode = course_mode.mode_for_course(
|
||||
course_key,
|
||||
mode_slug,
|
||||
modes=modes,
|
||||
)
|
||||
if course_mode is None:
|
||||
LOG.error(
|
||||
u"User %s is in an unknown CourseMode '%s'"
|
||||
u" for course %s. Granting full access to content for this user",
|
||||
user.username,
|
||||
mode_slug,
|
||||
course_key,
|
||||
)
|
||||
return FULL_ACCESS
|
||||
|
||||
if mode_slug == CourseMode.AUDIT:
|
||||
return LIMITED_ACCESS
|
||||
else:
|
||||
return FULL_ACCESS
|
||||
else:
|
||||
# Unenrolled users don't get gated content
|
||||
return LIMITED_ACCESS
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -365,7 +365,7 @@ class TestProblemTypeAccess(SharedModuleStoreTestCase):
|
||||
category=component_type,
|
||||
display_name=component_type,
|
||||
graded=True,
|
||||
metadata={} if component_type == 'html' else METADATA
|
||||
metadata={} if (component_type == 'html' or len(modes) == 1) else METADATA
|
||||
)
|
||||
blocks_dict[component_type] = block
|
||||
|
||||
|
||||
@@ -8,6 +8,8 @@ import pytz
|
||||
|
||||
from edx_django_utils.cache import RequestCache
|
||||
from opaque_keys.edx.locator import CourseLocator
|
||||
|
||||
from course_modes.tests.factories import CourseModeFactory
|
||||
from openedx.core.djangoapps.config_model_utils.models import Provenance
|
||||
from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory
|
||||
from openedx.core.djangoapps.site_configuration.tests.factories import SiteConfigurationFactory
|
||||
@@ -24,6 +26,8 @@ class TestContentTypeGatingConfig(CacheIsolationTestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.course_overview = CourseOverviewFactory.create()
|
||||
CourseModeFactory.create(course_id=self.course_overview.id, mode_slug='audit')
|
||||
CourseModeFactory.create(course_id=self.course_overview.id, mode_slug='verified')
|
||||
self.user = UserFactory.create()
|
||||
super(TestContentTypeGatingConfig, self).setUp()
|
||||
|
||||
@@ -73,9 +77,9 @@ class TestContentTypeGatingConfig(CacheIsolationTestCase):
|
||||
user = self.user
|
||||
course_key = self.course_overview.id
|
||||
|
||||
query_count = 6
|
||||
query_count = 7
|
||||
if not already_enrolled or not pass_enrollment and already_enrolled:
|
||||
query_count = 7
|
||||
query_count = 8
|
||||
|
||||
with self.assertNumQueries(query_count):
|
||||
enabled = ContentTypeGatingConfig.enabled_for_enrollment(
|
||||
|
||||
@@ -25,6 +25,8 @@ class TestContentTypeGatingPartition(CacheIsolationTestCase):
|
||||
def test_create_content_gating_partition_happy_path(self):
|
||||
|
||||
mock_course = Mock(id=self.course_key, user_partitions={})
|
||||
CourseModeFactory.create(course_id=mock_course.id, mode_slug='audit')
|
||||
CourseModeFactory.create(course_id=mock_course.id, mode_slug='verified')
|
||||
ContentTypeGatingConfig.objects.create(enabled=True, enabled_as_of=datetime(2018, 1, 1))
|
||||
|
||||
with patch('openedx.features.content_type_gating.partitions.ContentTypeGatingPartitionScheme.create_user_partition') as mock_create:
|
||||
@@ -133,6 +135,7 @@ class TestContentTypeGatingPartition(CacheIsolationTestCase):
|
||||
mock_request = None
|
||||
mock_course = Mock(id=self.course_key, user_partitions={})
|
||||
mock_block = Mock(scope_ids=Mock(usage_id=Mock(course_key=mock_course.id)))
|
||||
CourseModeFactory.create(course_id=mock_course.id, mode_slug='audit')
|
||||
CourseModeFactory.create(course_id=mock_course.id, mode_slug='verified')
|
||||
global_staff = GlobalStaffFactory.create()
|
||||
ContentTypeGatingConfig.objects.create(enabled=True, enabled_as_of=datetime(2018, 1, 1))
|
||||
|
||||
@@ -20,7 +20,11 @@ from lms.djangoapps.courseware.masquerade import (
|
||||
)
|
||||
from openedx.core.djangoapps.config_model_utils.models import StackedConfigurationModel
|
||||
from openedx.core.djangoapps.config_model_utils.utils import is_in_holdback
|
||||
from openedx.features.content_type_gating.helpers import CONTENT_GATING_PARTITION_ID, CONTENT_TYPE_GATE_GROUP_IDS
|
||||
from openedx.features.content_type_gating.helpers import (
|
||||
CONTENT_GATING_PARTITION_ID,
|
||||
CONTENT_TYPE_GATE_GROUP_IDS,
|
||||
correct_modes_for_fbe
|
||||
)
|
||||
from student.models import CourseEnrollment
|
||||
from student.role_helpers import has_staff_roles
|
||||
from xmodule.partitions.partitions import ENROLLMENT_TRACK_PARTITION_ID
|
||||
@@ -136,10 +140,14 @@ class CourseDurationLimitConfig(StackedConfigurationModel):
|
||||
# When masquerading as a specific learner, course duration limits
|
||||
# will be on if they are currently on for the learner.
|
||||
if enrollment is None or not_student_masquerade:
|
||||
return cls.enabled_for_course(course_key=course_key, target_datetime=timezone.now())
|
||||
# we bypass enabled_for_course here and use enabled_as_of_datetime directly
|
||||
# because the correct_modes_for_fbe for FBE check contained in enabled_for_course
|
||||
# is redundant with checks done upstream of this code
|
||||
target_datetime = timezone.now()
|
||||
else:
|
||||
current_config = cls.current(course_key=enrollment.course_id)
|
||||
return current_config.enabled_as_of_datetime(target_datetime=enrollment.created)
|
||||
target_datetime = enrollment.created
|
||||
current_config = cls.current(course_key=course_key)
|
||||
return current_config.enabled_as_of_datetime(target_datetime=target_datetime)
|
||||
|
||||
@classmethod
|
||||
def enabled_for_course(cls, course_key, target_datetime=None):
|
||||
@@ -156,6 +164,8 @@ class CourseDurationLimitConfig(StackedConfigurationModel):
|
||||
course_key: The CourseKey of the course being queried.
|
||||
target_datetime: The datetime to checked enablement as of. Defaults to the current date and time.
|
||||
"""
|
||||
if not correct_modes_for_fbe(course_key):
|
||||
return False
|
||||
|
||||
if target_datetime is None:
|
||||
target_datetime = timezone.now()
|
||||
|
||||
@@ -11,6 +11,7 @@ from mock import Mock
|
||||
import pytz
|
||||
|
||||
from edx_django_utils.cache import RequestCache
|
||||
from course_modes.tests.factories import CourseModeFactory
|
||||
from opaque_keys.edx.locator import CourseLocator
|
||||
from openedx.core.djangoapps.config_model_utils.models import Provenance
|
||||
from openedx.core.djangoapps.site_configuration.tests.factories import SiteConfigurationFactory
|
||||
@@ -31,6 +32,8 @@ class TestCourseDurationLimitConfig(CacheIsolationTestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.course_overview = CourseOverviewFactory.create()
|
||||
CourseModeFactory.create(course_id=self.course_overview.id, mode_slug='audit')
|
||||
CourseModeFactory.create(course_id=self.course_overview.id, mode_slug='verified')
|
||||
self.user = UserFactory.create()
|
||||
super(TestCourseDurationLimitConfig, self).setUp()
|
||||
|
||||
|
||||
@@ -130,7 +130,7 @@ class TestCourseUpdatesPage(SharedModuleStoreTestCase):
|
||||
|
||||
# Fetch the view and verify that the query counts haven't changed
|
||||
# TODO: decrease query count as part of REVO-28
|
||||
with self.assertNumQueries(53, table_blacklist=QUERY_COUNT_TABLE_BLACKLIST):
|
||||
with self.assertNumQueries(50, table_blacklist=QUERY_COUNT_TABLE_BLACKLIST):
|
||||
with check_mongo_calls(4):
|
||||
url = course_updates_url(self.course)
|
||||
self.client.get(url)
|
||||
|
||||
Reference in New Issue
Block a user