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:
Matthew Piatetsky
2019-04-10 14:07:14 -04:00
parent 05a682f55e
commit 62b9bb89c8
16 changed files with 112 additions and 88 deletions

View File

@@ -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

View File

@@ -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()

View File

@@ -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

View File

@@ -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

View File

@@ -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(

View File

@@ -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))

View File

@@ -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()

View File

@@ -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()

View File

@@ -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)