Populate experiment data with holdback key post enrollment and check holdback key when setting user group

This commit is contained in:
Matthew Piatetsky
2018-11-16 15:16:42 -05:00
parent d5751efac3
commit 815acda002
15 changed files with 223 additions and 79 deletions

View File

@@ -1168,6 +1168,7 @@ INSTALLED_APPS = [
'openedx.features.course_duration_limits',
'openedx.features.content_type_gating',
'experiments',
]

View File

@@ -244,18 +244,18 @@ class TestFieldOverrideMongoPerformance(FieldOverridePerformanceTestCase):
# # of sql queries to default,
# # of mongo queries,
# )
('no_overrides', 1, True, False): (26, 1),
('no_overrides', 2, True, False): (26, 1),
('no_overrides', 3, True, False): (26, 1),
('ccx', 1, True, False): (26, 1),
('ccx', 2, True, False): (26, 1),
('ccx', 3, True, False): (26, 1),
('no_overrides', 1, False, False): (26, 1),
('no_overrides', 2, False, False): (26, 1),
('no_overrides', 3, False, False): (26, 1),
('ccx', 1, False, False): (26, 1),
('ccx', 2, False, False): (26, 1),
('ccx', 3, False, False): (26, 1),
('no_overrides', 1, True, False): (31, 1),
('no_overrides', 2, True, False): (31, 1),
('no_overrides', 3, True, False): (31, 1),
('ccx', 1, True, False): (31, 1),
('ccx', 2, True, False): (31, 1),
('ccx', 3, True, False): (31, 1),
('no_overrides', 1, False, False): (31, 1),
('no_overrides', 2, False, False): (31, 1),
('no_overrides', 3, False, False): (31, 1),
('ccx', 1, False, False): (31, 1),
('ccx', 2, False, False): (31, 1),
('ccx', 3, False, False): (31, 1),
}
@@ -267,19 +267,19 @@ class TestFieldOverrideSplitPerformance(FieldOverridePerformanceTestCase):
__test__ = True
TEST_DATA = {
('no_overrides', 1, True, False): (26, 3),
('no_overrides', 2, True, False): (26, 3),
('no_overrides', 3, True, False): (26, 3),
('ccx', 1, True, False): (26, 3),
('ccx', 2, True, False): (26, 3),
('ccx', 3, True, False): (26, 3),
('ccx', 1, True, True): (27, 3),
('ccx', 2, True, True): (27, 3),
('ccx', 3, True, True): (27, 3),
('no_overrides', 1, False, False): (26, 3),
('no_overrides', 2, False, False): (26, 3),
('no_overrides', 3, False, False): (26, 3),
('ccx', 1, False, False): (26, 3),
('ccx', 2, False, False): (26, 3),
('ccx', 3, False, False): (26, 3),
('no_overrides', 1, True, False): (31, 3),
('no_overrides', 2, True, False): (31, 3),
('no_overrides', 3, True, False): (31, 3),
('ccx', 1, True, False): (31, 3),
('ccx', 2, True, False): (31, 3),
('ccx', 3, True, False): (31, 3),
('ccx', 1, True, True): (32, 3),
('ccx', 2, True, True): (32, 3),
('ccx', 3, True, True): (32, 3),
('no_overrides', 1, False, False): (31, 3),
('no_overrides', 2, False, False): (31, 3),
('no_overrides', 3, False, False): (31, 3),
('ccx', 1, False, False): (31, 3),
('ccx', 2, False, False): (31, 3),
('ccx', 3, False, False): (31, 3),
}

View File

@@ -213,8 +213,8 @@ class IndexQueryTestCase(ModuleStoreTestCase):
NUM_PROBLEMS = 20
@ddt.data(
(ModuleStoreEnum.Type.mongo, 10, 162),
(ModuleStoreEnum.Type.split, 4, 160),
(ModuleStoreEnum.Type.mongo, 10, 169),
(ModuleStoreEnum.Type.split, 4, 165),
)
@ddt.unpack
def test_index_query_counts(self, store_type, expected_mongo_query_count, expected_mysql_query_count):
@@ -1439,8 +1439,8 @@ class ProgressPageTests(ProgressPageBaseTests):
self.assertContains(resp, u"Download Your Certificate")
@ddt.data(
(True, 46),
(False, 45)
(True, 51),
(False, 50)
)
@ddt.unpack
def test_progress_queries_paced_courses(self, self_paced, query_count):
@@ -1452,8 +1452,8 @@ class ProgressPageTests(ProgressPageBaseTests):
@patch.dict(settings.FEATURES, {'ASSUME_ZERO_GRADE_IF_ABSENT_FOR_ALL_TESTS': False})
@ddt.data(
(False, 53, 33),
(True, 45, 29)
(False, 58, 38),
(True, 50, 34)
)
@ddt.unpack
def test_progress_queries(self, enable_waffle, initial, subsequent):

View File

@@ -431,18 +431,18 @@ class SingleThreadQueryCountTestCase(ForumsEnableMixin, ModuleStoreTestCase):
# course is outside the context manager that is verifying the number of queries,
# and with split mongo, that method ends up querying disabled_xblocks (which is then
# cached and hence not queried as part of call_single_thread).
(ModuleStoreEnum.Type.mongo, False, 1, 5, 2, 21, 6),
(ModuleStoreEnum.Type.mongo, False, 50, 5, 2, 21, 6),
(ModuleStoreEnum.Type.mongo, False, 1, 5, 2, 22, 7),
(ModuleStoreEnum.Type.mongo, False, 50, 5, 2, 22, 7),
# split mongo: 3 queries, regardless of thread response size.
(ModuleStoreEnum.Type.split, False, 1, 3, 3, 21, 6),
(ModuleStoreEnum.Type.split, False, 50, 3, 3, 21, 6),
(ModuleStoreEnum.Type.split, False, 1, 3, 3, 22, 7),
(ModuleStoreEnum.Type.split, False, 50, 3, 3, 22, 7),
# Enabling Enterprise integration should have no effect on the number of mongo queries made.
(ModuleStoreEnum.Type.mongo, True, 1, 5, 2, 21, 6),
(ModuleStoreEnum.Type.mongo, True, 50, 5, 2, 21, 6),
(ModuleStoreEnum.Type.mongo, True, 1, 5, 2, 22, 7),
(ModuleStoreEnum.Type.mongo, True, 50, 5, 2, 22, 7),
# split mongo: 3 queries, regardless of thread response size.
(ModuleStoreEnum.Type.split, True, 1, 3, 3, 21, 6),
(ModuleStoreEnum.Type.split, True, 50, 3, 3, 21, 6),
(ModuleStoreEnum.Type.split, True, 1, 3, 3, 22, 7),
(ModuleStoreEnum.Type.split, True, 50, 3, 3, 22, 7),
)
@ddt.unpack
def test_number_of_mongo_queries(

View File

@@ -403,8 +403,8 @@ class ViewsQueryCountTestCase(
return inner
@ddt.data(
(ModuleStoreEnum.Type.mongo, 3, 4, 39),
(ModuleStoreEnum.Type.split, 3, 13, 39),
(ModuleStoreEnum.Type.mongo, 3, 4, 40),
(ModuleStoreEnum.Type.split, 3, 13, 40),
)
@ddt.unpack
@count_queries
@@ -412,8 +412,8 @@ class ViewsQueryCountTestCase(
self.create_thread_helper(mock_request)
@ddt.data(
(ModuleStoreEnum.Type.mongo, 3, 3, 35),
(ModuleStoreEnum.Type.split, 3, 10, 35),
(ModuleStoreEnum.Type.mongo, 3, 3, 36),
(ModuleStoreEnum.Type.split, 3, 10, 36),
)
@ddt.unpack
@count_queries

View File

@@ -92,35 +92,35 @@ class TestCourseGradeFactory(GradeTestBase):
[self.sequence.display_name, self.sequence2.display_name]
)
with self.assertNumQueries(3), mock_get_score(1, 2):
with self.assertNumQueries(4), mock_get_score(1, 2):
_assert_read(expected_pass=False, expected_percent=0) # start off with grade of 0
num_queries = 42
num_queries = 43
with self.assertNumQueries(num_queries), mock_get_score(1, 2):
grade_factory.update(self.request.user, self.course, force_update_subsections=True)
with self.assertNumQueries(3):
with self.assertNumQueries(4):
_assert_read(expected_pass=True, expected_percent=0.5) # updated to grade of .5
num_queries = 7
num_queries = 8
with self.assertNumQueries(num_queries), mock_get_score(1, 4):
grade_factory.update(self.request.user, self.course, force_update_subsections=False)
with self.assertNumQueries(3):
with self.assertNumQueries(4):
_assert_read(expected_pass=True, expected_percent=0.5) # NOT updated to grade of .25
num_queries = 21
num_queries = 22
with self.assertNumQueries(num_queries), mock_get_score(2, 2):
grade_factory.update(self.request.user, self.course, force_update_subsections=True)
with self.assertNumQueries(3):
with self.assertNumQueries(4):
_assert_read(expected_pass=True, expected_percent=1.0) # updated to grade of 1.0
num_queries = 24
num_queries = 25
with self.assertNumQueries(num_queries), mock_get_score(0, 0): # the subsection now is worth zero
grade_factory.update(self.request.user, self.course, force_update_subsections=True)
with self.assertNumQueries(3):
with self.assertNumQueries(4):
_assert_read(expected_pass=False, expected_percent=0.0) # updated to grade of 0.0
@patch.dict(settings.FEATURES, {'ASSUME_ZERO_GRADE_IF_ABSENT_FOR_ALL_TESTS': False})

View File

@@ -11,9 +11,14 @@ from django.db import models
from django.utils.encoding import python_2_unicode_compatible
from django.utils.translation import ugettext_lazy as _
from experiments.models import ExperimentData
from student.models import CourseEnrollment
from openedx.core.djangoapps.config_model_utils.models import StackedConfigurationModel
from openedx.features.course_duration_limits.config import CONTENT_TYPE_GATING_FLAG
from openedx.features.course_duration_limits.config import (
CONTENT_TYPE_GATING_FLAG,
EXPERIMENT_ID,
EXPERIMENT_DATA_HOLDBACK_KEY
)
@python_2_unicode_compatible
@@ -84,6 +89,20 @@ class ContentTypeGatingConfig(StackedConfigurationModel):
if enrollment is None:
return cls.enabled_for_course(course_key=course_key, target_date=datetime.utcnow().date())
else:
# TODO: clean up as part of REV-100
experiment_data_holdback_key = EXPERIMENT_DATA_HOLDBACK_KEY.format(user)
is_in_holdback = False
try:
holdback_value = ExperimentData.objects.get(
user=user,
experiment_id=EXPERIMENT_ID,
key=experiment_data_holdback_key,
).value
is_in_holdback = holdback_value == 'True'
except ExperimentData.DoesNotExist:
pass
if is_in_holdback:
return False
current_config = cls.current(course_key=enrollment.course_id)
return current_config.enabled_as_of_date(target_date=enrollment.created.date())

View File

@@ -11,19 +11,8 @@ from django.urls import reverse
from mock import patch
from course_modes.tests.factories import CourseModeFactory
from experiments.models import ExperimentKeyValue
from lms.djangoapps.courseware.module_render import load_single_xblock
from openedx.core.djangoapps.user_api.tests.factories import UserCourseTagFactory
from openedx.core.djangoapps.util.testing import TestConditionalContent
from openedx.core.djangoapps.waffle_utils.testutils import override_waffle_flag
from openedx.core.lib.url_utils import quote_slashes
from openedx.features.content_type_gating.partitions import CONTENT_GATING_PARTITION_ID
from openedx.features.content_type_gating.models import ContentTypeGatingConfig
from student.roles import CourseBetaTesterRole, CourseInstructorRole, CourseStaffRole
from student.tests.factories import (
CourseEnrollmentFactory,
UserFactory,
TEST_PASSWORD
)
from lms.djangoapps.courseware.tests.factories import (
InstructorFactory,
StaffFactory,
@@ -32,6 +21,23 @@ from lms.djangoapps.courseware.tests.factories import (
OrgInstructorFactory,
GlobalStaffFactory,
)
from openedx.core.djangoapps.user_api.tests.factories import UserCourseTagFactory
from openedx.core.djangoapps.util.testing import TestConditionalContent
from openedx.core.djangoapps.waffle_utils.testutils import override_waffle_flag
from openedx.core.lib.url_utils import quote_slashes
from openedx.features.content_type_gating.partitions import CONTENT_GATING_PARTITION_ID
from openedx.features.content_type_gating.models import ContentTypeGatingConfig
from openedx.features.course_duration_limits.config import (
EXPERIMENT_DATA_HOLDBACK_KEY,
EXPERIMENT_ID,
)
from student.models import CourseEnrollment
from student.roles import CourseBetaTesterRole, CourseInstructorRole, CourseStaffRole
from student.tests.factories import (
CourseEnrollmentFactory,
UserFactory,
TEST_PASSWORD
)
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
@@ -430,8 +436,36 @@ class TestProblemTypeAccess(SharedModuleStoreTestCase):
request_factory=self.factory,
)
@ddt.data(
(False, True),
(True, False),
)
@ddt.unpack
def test_content_gating_holdback(self, put_user_in_holdback, is_gated):
"""
Test that putting a user in the content gating holdback disables content gating.
"""
if put_user_in_holdback:
ExperimentKeyValue.objects.create(
experiment_id=EXPERIMENT_ID,
key="content_type_gating_holdback_percentage",
value="100"
).value
user = UserFactory.create()
CourseEnrollment.enroll(user, self.course.id)
graded, has_score, weight = True, True, 1
block = self.graded_score_weight_blocks[(graded, has_score, weight)]
_assert_block_is_gated(
block=block,
user_id=user.id,
course=self.course,
is_gated=is_gated,
request_factory=self.factory,
)
@ddt.ddt
@override_settings(FIELD_OVERRIDE_PROVIDERS=(
'openedx.features.content_type_gating.field_override.ContentTypeGatingFieldOverride',
))

View File

@@ -68,10 +68,9 @@ class TestContentTypeGatingConfig(CacheIsolationTestCase):
user = self.user
course_key = self.course_overview.id
if pass_enrollment:
query_count = 4
else:
query_count = 5
query_count = 5
if not pass_enrollment and already_enrolled:
query_count = 6
with self.assertNumQueries(query_count):
enabled = ContentTypeGatingConfig.enabled_for_enrollment(

View File

@@ -1,7 +1,14 @@
"""
Content type gating waffle flag
"""
import random
from django.dispatch import receiver
from experiments.models import ExperimentData, ExperimentKeyValue
from openedx.core.djangoapps.waffle_utils import WaffleFlagNamespace, WaffleFlag
from student.models import EnrollStatusChange
from student.signals import ENROLL_STATUS_CHANGE
WAFFLE_FLAG_NAMESPACE = WaffleFlagNamespace(name=u'content_type_gating')
@@ -11,3 +18,35 @@ CONTENT_TYPE_GATING_FLAG = WaffleFlag(
flag_name=u'debug',
flag_undefined_default=False
)
EXPERIMENT_ID = 11
EXPERIMENT_DATA_HOLDBACK_KEY = 'holdback_{0}'
@receiver(ENROLL_STATUS_CHANGE)
def set_value_for_content_type_gating_holdback(sender, event=None, user=None, **kwargs): # pylint: disable=unused-argument
experiment_data_holdback_key = EXPERIMENT_DATA_HOLDBACK_KEY.format(user)
if event == EnrollStatusChange.enroll:
user_holdback_data = ExperimentData.objects.filter(
user=user,
experiment_id=EXPERIMENT_ID,
key=experiment_data_holdback_key,
)
user_holdback_data_already_set = user_holdback_data.exists()
if not user_holdback_data_already_set:
try:
content_type_gating_holdback_percentage_value = ExperimentKeyValue.objects.get(
experiment_id=EXPERIMENT_ID,
key="content_type_gating_holdback_percentage"
).value
content_type_gating_holdback_percentage = float(content_type_gating_holdback_percentage_value) / 100
is_in_holdback = str(random.random() < content_type_gating_holdback_percentage)
ExperimentData.objects.create(
user=user,
experiment_id=EXPERIMENT_ID,
key=experiment_data_holdback_key,
value=is_in_holdback
)
except (ExperimentKeyValue.DoesNotExist, AttributeError):
pass

View File

@@ -11,9 +11,14 @@ from django.db import models
from django.utils.encoding import python_2_unicode_compatible
from django.utils.translation import ugettext_lazy as _
from experiments.models import ExperimentData
from student.models import CourseEnrollment
from openedx.core.djangoapps.config_model_utils.models import StackedConfigurationModel
from openedx.features.course_duration_limits.config import CONTENT_TYPE_GATING_FLAG
from openedx.features.course_duration_limits.config import (
CONTENT_TYPE_GATING_FLAG,
EXPERIMENT_ID,
EXPERIMENT_DATA_HOLDBACK_KEY
)
@python_2_unicode_compatible
@@ -75,6 +80,20 @@ class CourseDurationLimitConfig(StackedConfigurationModel):
if enrollment is None:
return cls.enabled_for_course(course_key=course_key, target_date=datetime.utcnow().date())
else:
# TODO: clean up as part of REV-100
experiment_data_holdback_key = EXPERIMENT_DATA_HOLDBACK_KEY.format(user)
is_in_holdback = False
try:
holdback_value = ExperimentData.objects.get(
user=user,
experiment_id=EXPERIMENT_ID,
key=experiment_data_holdback_key,
).value
is_in_holdback = holdback_value == 'True'
except ExperimentData.DoesNotExist:
pass
if is_in_holdback:
return False
current_config = cls.current(course_key=enrollment.course_id)
return current_config.enabled_as_of_date(target_date=enrollment.created.date())

View File

@@ -76,10 +76,9 @@ class TestCourseDurationLimitConfig(CacheIsolationTestCase):
user = self.user
course_key = self.course_overview.id
if pass_enrollment:
query_count = 4
else:
query_count = 5
query_count = 5
if not pass_enrollment and already_enrolled:
query_count = 6
with self.assertNumQueries(query_count):
enabled = CourseDurationLimitConfig.enabled_for_enrollment(

View File

@@ -18,6 +18,7 @@ from waffle.testutils import override_flag
from course_modes.models import CourseMode
from course_modes.tests.factories import CourseModeFactory
from courseware.tests.helpers import get_expiration_banner_text
from experiments.models import ExperimentKeyValue
from lms.djangoapps.commerce.models import CommerceConfiguration
from lms.djangoapps.commerce.utils import EcommerceService
from lms.djangoapps.course_goals.api import add_course_goal, remove_course_goal
@@ -32,6 +33,7 @@ from lms.djangoapps.courseware.tests.factories import (
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
from openedx.core.djangoapps.schedules.tests.factories import ScheduleFactory
from openedx.core.djangoapps.waffle_utils.testutils import WAFFLE_TABLES, override_waffle_flag
from openedx.features.course_duration_limits.config import EXPERIMENT_ID
from openedx.features.course_duration_limits.models import CourseDurationLimitConfig
from openedx.features.course_experience import (
SHOW_REVIEWS_TOOL_FLAG,
@@ -192,7 +194,7 @@ class TestCourseHomePage(CourseHomePageTestCase):
course_home_url(self.course)
# Fetch the view and verify the query counts
with self.assertNumQueries(68, table_blacklist=QUERY_COUNT_TABLE_BLACKLIST):
with self.assertNumQueries(76, table_blacklist=QUERY_COUNT_TABLE_BLACKLIST):
with check_mongo_calls(4):
url = course_home_url(self.course)
self.client.get(url)
@@ -475,6 +477,37 @@ class TestCourseHomePageAccess(CourseHomePageTestCase):
self.assertContains(response, TEST_COURSE_TODAY)
self.assertNotContains(response, TEST_BANNER_CLASS)
@mock.patch.dict(settings.FEATURES, {'DISABLE_START_DATES': False})
def test_expired_course_in_holdback(self):
"""
Ensure that a user accessing an expired course that is in the holdback
does not get redirected to the student dashboard, not a 404.
"""
CourseDurationLimitConfig.objects.create(enabled=True, enabled_as_of=date(2010, 1, 1))
course = CourseFactory.create(start=THREE_YEARS_AGO)
url = course_home_url(course)
for mode in [CourseMode.AUDIT, CourseMode.VERIFIED]:
CourseModeFactory.create(course_id=course.id, mode_slug=mode)
ExperimentKeyValue.objects.create(
experiment_id=EXPERIMENT_ID,
key="content_type_gating_holdback_percentage",
value="100"
)
# assert that an if an expired audit user in the holdback tries to access the course
# they are not redirected to the dashboard
audit_user = UserFactory(password=self.TEST_PASSWORD)
self.client.login(username=audit_user.username, password=self.TEST_PASSWORD)
audit_enrollment = CourseEnrollment.enroll(audit_user, course.id, mode=CourseMode.AUDIT)
ScheduleFactory(start=THREE_YEARS_AGO, enrollment=audit_enrollment)
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
@mock.patch.dict(settings.FEATURES, {'DISABLE_START_DATES': False})
@mock.patch("util.date_utils.strftime_localized")
def test_non_live_course_other_language(self, mock_strftime_localized):

View File

@@ -129,7 +129,7 @@ class TestCourseUpdatesPage(SharedModuleStoreTestCase):
course_updates_url(self.course)
# Fetch the view and verify that the query counts haven't changed
with self.assertNumQueries(46, 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)

View File

@@ -75,6 +75,7 @@ INSTALLED_APPS = (
'openedx.core.djangoapps.content.block_structure.apps.BlockStructureConfig',
'openedx.core.djangoapps.catalog',
'openedx.core.djangoapps.self_paced',
'experiments',
'openedx.features.content_type_gating',
'openedx.features.course_duration_limits',
'milestones',