def waffle_flags(): """ Returns the namespaced, cached, audited Waffle flags dictionary for Grades. """ namespace = WaffleFlagNamespace(name=WAFFLE_NAMESPACE, log_prefix=u'Grades: ') return { # By default, enable rejected exam grade overrides. Can be disabled on a course-by-course basis. REJECTED_EXAM_OVERRIDES_GRADE: CourseWaffleFlag( namespace, REJECTED_EXAM_OVERRIDES_GRADE, flag_undefined_default=True, ), ENFORCE_FREEZE_GRADE_AFTER_COURSE_END: CourseWaffleFlag( namespace, ENFORCE_FREEZE_GRADE_AFTER_COURSE_END, flag_undefined_default=True, ), # Have this course override flag so we can selectively turn off the gradebook for courses. WRITABLE_GRADEBOOK: CourseWaffleFlag( namespace, WRITABLE_GRADEBOOK, flag_undefined_default=True, ), BULK_MANAGEMENT: CourseWaffleFlag( namespace, BULK_MANAGEMENT, flag_undefined_default=False, ), }
def waffle_flags(): """ Returns the namespaced, cached, audited Waffle flags dictionary for Grades. WARNING: do not replicate this pattern. Instead of declaring waffle flag names as strings, you should create LegacyWaffleFlag and CourseWaffleFlag objects as top-level constants. """ namespace = LegacyWaffleFlagNamespace(name=WAFFLE_NAMESPACE, log_prefix=u'Grades: ') return { # By default, enable rejected exam grade overrides. Can be disabled on a course-by-course basis. # TODO: After removing this flag, add a migration to remove waffle flag in a follow-up deployment. REJECTED_EXAM_OVERRIDES_GRADE: CourseWaffleFlag( namespace, REJECTED_EXAM_OVERRIDES_GRADE, __name__, ), # TODO: After removing this flag, add a migration to remove waffle flag in a follow-up deployment. ENFORCE_FREEZE_GRADE_AFTER_COURSE_END: CourseWaffleFlag( namespace, ENFORCE_FREEZE_GRADE_AFTER_COURSE_END, __name__, ), # Have this course override flag so we can selectively turn off the gradebook for courses. # TODO: After removing this flag, add a migration to remove waffle flag in a follow-up deployment. WRITABLE_GRADEBOOK: CourseWaffleFlag( namespace, WRITABLE_GRADEBOOK, __name__, ), BULK_MANAGEMENT: CourseWaffleFlag( namespace, BULK_MANAGEMENT, __name__, ), }
def waffle_flags(): """ Returns the namespaced, cached, audited Waffle flags dictionary for Grades. """ namespace = WaffleFlagNamespace(name=WAFFLE_NAMESPACE, log_prefix=u'Grades: ') return { # By default, enable rejected exam grade overrides. Can be disabled on a course-by-course basis. # TODO: After removing this flag, add a migration to remove waffle flag in a follow-up deployment. REJECTED_EXAM_OVERRIDES_GRADE: CourseWaffleFlag( namespace, REJECTED_EXAM_OVERRIDES_GRADE, ), # TODO: After removing this flag, add a migration to remove waffle flag in a follow-up deployment. ENFORCE_FREEZE_GRADE_AFTER_COURSE_END: CourseWaffleFlag( namespace, ENFORCE_FREEZE_GRADE_AFTER_COURSE_END, ), # Have this course override flag so we can selectively turn off the gradebook for courses. # TODO: After removing this flag, add a migration to remove waffle flag in a follow-up deployment. WRITABLE_GRADEBOOK: CourseWaffleFlag( namespace, WRITABLE_GRADEBOOK, ), BULK_MANAGEMENT: CourseWaffleFlag( namespace, BULK_MANAGEMENT, ), }
def waffle_flags(): """ Returns the namespaced, cached, audited Waffle flags dictionary for Videos. IMPORTANT: Do NOT copy this dict pattern and do NOT add new flags to this dict. Instead, replace the string constants above with the actual flag instances. """ namespace = LegacyWaffleFlagNamespace(name=WAFFLE_NAMESPACE, log_prefix=u'Videos: ') return { DEPRECATE_YOUTUBE: CourseWaffleFlag( waffle_namespace=namespace, flag_name=DEPRECATE_YOUTUBE, module_name=__name__, ), ENABLE_DEVSTACK_VIDEO_UPLOADS: LegacyWaffleFlag( waffle_namespace=namespace, flag_name=ENABLE_DEVSTACK_VIDEO_UPLOADS, module_name=__name__, ), ENABLE_VEM_PIPELINE: CourseWaffleFlag( waffle_namespace=namespace, flag_name=ENABLE_VEM_PIPELINE, module_name=__name__, ) }
def waffle_flags(): """ Returns the namespaced, cached, audited Waffle flags dictionary for Videos. """ namespace = WaffleFlagNamespace(name=WAFFLE_NAMESPACE, log_prefix=u'Videos: ') return { DEPRECATE_YOUTUBE: CourseWaffleFlag( waffle_namespace=namespace, flag_name=DEPRECATE_YOUTUBE, module_name=__name__, ), ENABLE_DEVSTACK_VIDEO_UPLOADS: WaffleFlag( waffle_namespace=namespace, flag_name=ENABLE_DEVSTACK_VIDEO_UPLOADS, module_name=__name__, ), ENABLE_VEM_PIPELINE: CourseWaffleFlag( waffle_namespace=namespace, flag_name=ENABLE_VEM_PIPELINE, module_name=__name__, ) }
def test_without_request_and_undefined_waffle(self): """ Test the flag behavior when outside a request context and waffle data undefined. """ crum.set_current_request(None) test_course_flag = CourseWaffleFlag(self.NAMESPACED_FLAG_NAME, __name__) assert test_course_flag.is_enabled(self.TEST_COURSE_KEY) is False
def test_without_request_and_everyone_active_waffle(self): """ Test the flag behavior when outside a request context and waffle active for everyone. """ crum.set_current_request(None) test_course_flag = CourseWaffleFlag(self.NAMESPACED_FLAG_NAME, __name__) with override_waffle_flag(self.TEST_COURSE_FLAG, active=True): assert test_course_flag.is_enabled(self.TEST_COURSE_KEY) is True
def _is_staff_grader_enabled(self, course_key): """ Helper to evaluate if the staff grader flag / overrides are enabled """ # This toggle is documented on the edx-ora2 repo in openassessment/xblock/config_mixin.py # pylint: disable=toggle-missing-annotation enhanced_staff_grader_flag = CourseWaffleFlag( WAFFLE_NAMESPACE, ENHANCED_STAFF_GRADER, module_name='openassessment.xblock.config_mixin' ) return enhanced_staff_grader_flag.is_enabled(course_key)
def _is_staff_grader_enabled(self, course_key): """ Helper to evaluate if the staff grader flag / overrides are enabled """ # This toggle is documented on the edx-ora2 repo in openassessment/xblock/config_mixin.py # Note: Do not copy this practice of directly using a toggle from a library. # Instead, see docs for exposing a wrapper api: # https://edx.readthedocs.io/projects/edx-toggles/en/latest/how_to/implement_the_right_toggle_type.html#using-other-toggles pylint: disable=line-too-long # pylint: disable=toggle-missing-annotation enhanced_staff_grader_flag = CourseWaffleFlag( f"{WAFFLE_NAMESPACE}.{ENHANCED_STAFF_GRADER}", module_name='openassessment.xblock.config_mixin') return enhanced_staff_grader_flag.is_enabled(course_key)
def _course_waffle_flag(flag_name): """ Returns a ``CourseWaffleFlag`` object in WAFFLE_NAMESPACE with the given ``flag_name``. """ CourseWaffleFlag = import_course_waffle_flag() # pylint: disable=invalid-name return CourseWaffleFlag(WAFFLE_NAMESPACE, flag_name) # pylint: disable=feature-toggle-needs-doc
def waffle_flags(): """ Returns the namespaced, cached, audited Waffle flags dictionary for Grades. """ return { GENERATE_PROBLEM_GRADE_REPORT_VERIFIED_ONLY: CourseWaffleFlag( waffle_namespace=INSTRUCTOR_TASK_WAFFLE_FLAG_NAMESPACE, flag_name=GENERATE_PROBLEM_GRADE_REPORT_VERIFIED_ONLY, module_name=__name__, ), GENERATE_COURSE_GRADE_REPORT_VERIFIED_ONLY: CourseWaffleFlag( waffle_namespace=INSTRUCTOR_TASK_WAFFLE_FLAG_NAMESPACE, flag_name=GENERATE_COURSE_GRADE_REPORT_VERIFIED_ONLY, module_name=__name__, ), }
def waffle_flags(): """ Returns the namespaced, cached, audited Waffle flags dictionary for Grades. """ return { PROBLEM_GRADE_REPORT_VERIFIED_ONLY: CourseWaffleFlag( waffle_namespace=INSTRUCTOR_TASK_WAFFLE_FLAG_NAMESPACE, flag_name=PROBLEM_GRADE_REPORT_VERIFIED_ONLY, flag_undefined_default=False, ), COURSE_GRADE_REPORT_VERIFIED_ONLY: CourseWaffleFlag( waffle_namespace=INSTRUCTOR_TASK_WAFFLE_FLAG_NAMESPACE, flag_name=COURSE_GRADE_REPORT_VERIFIED_ONLY, flag_undefined_default=False, ), }
def __init__(self, waffle_namespace, flag_name, num_buckets=2, experiment_id=None, **kwargs): super().__init__(waffle_namespace, flag_name, **kwargs) self.num_buckets = num_buckets self.experiment_id = experiment_id self.bucket_flags = [ CourseWaffleFlag(waffle_namespace, '{}.{}'.format(flag_name, bucket), flag_undefined_default=False) for bucket in range(num_buckets) ]
def _course_waffle_flag(flag_name): """ Returns a ``CourseWaffleFlag`` object in WAFFLE_NAMESPACE with the given ``flag_name``. """ CourseWaffleFlag = import_course_waffle_flag() # pylint: disable=invalid-name # pylint: disable=toggle-missing-annotation return CourseWaffleFlag(WAFFLE_NAMESPACE, flag_name, module_name=__name__)
def test_undefined_waffle_flag(self): """ Test flag with undefined waffle flag. """ test_course_flag = CourseWaffleFlag(self.NAMESPACED_FLAG_NAME, __name__) with patch.object( WaffleFlagCourseOverrideModel, 'override_value', return_value=WaffleFlagCourseOverrideModel.ALL_CHOICES.unset): # check twice to test that the result is properly cached assert test_course_flag.is_enabled(self.TEST_COURSE_KEY) is False assert test_course_flag.is_enabled(self.TEST_COURSE_KEY) is False # result is cached, so override check should happen once # pylint: disable=no-member WaffleFlagCourseOverrideModel.override_value.assert_called_once_with( self.NAMESPACED_FLAG_NAME, self.TEST_COURSE_KEY)
def waffle_flags(): """ Returns the namespaced, cached, audited Waffle flags dictionary for Videos. """ namespace = WaffleFlagNamespace(name=WAFFLE_NAMESPACE, log_prefix=u'Videos: ') return { DEPRECATE_YOUTUBE: CourseWaffleFlag(waffle_namespace=namespace, flag_name=DEPRECATE_YOUTUBE), ENABLE_DEVSTACK_VIDEO_UPLOADS: WaffleFlag(waffle_namespace=namespace, flag_name=ENABLE_DEVSTACK_VIDEO_UPLOADS, flag_undefined_default=False), SAVE_CREDENTIALS_IN_VAL: CourseWaffleFlag(waffle_namespace=namespace, flag_name=SAVE_CREDENTIALS_IN_VAL, flag_undefined_default=False) }
def are_team_submissions_enabled(course_key): """ Checks to see if the CourseWaffleFlag or Django setting for team submissions is enabled """ if CourseWaffleFlag(WAFFLE_NAMESPACE, TEAM_SUBMISSIONS_FLAG, __name__).is_enabled(course_key): return True if settings.FEATURES.get(TEAM_SUBMISSIONS_FEATURE, False): return True return False
def waffle_flags(): """ Returns the namespaced, cached, audited Waffle flags dictionary for course detail. """ return { COURSE_DETAIL_UPDATE_CERTIFICATE_DATE: CourseWaffleFlag( waffle_namespace=COURSE_DETAIL_WAFFLE_NAMESPACE, flag_name=COURSE_DETAIL_UPDATE_CERTIFICATE_DATE, ) }
def waffle_flags(): """ Returns the namespaced, cached, audited Waffle flags dictionary for Videos. """ namespace = WaffleFlagNamespace(name=WAFFLE_NAMESPACE, log_prefix=u'Videos: ') return { DEPRECATE_YOUTUBE: CourseWaffleFlag(waffle_namespace=namespace, flag_name=DEPRECATE_YOUTUBE) }
def waffle_flags(): """ Deprecated: Returns the namespaced, cached, audited Waffle flags dictionary for Grades. IMPORTANT: Do NOT copy this pattern and do NOT use this to reference new flags. Instead, replace the string constants above with the flag declarations below, and use them directly. """ namespace = LegacyWaffleFlagNamespace(name=WAFFLE_NAMESPACE, log_prefix='Grades: ') return { # By default, enable rejected exam grade overrides. Can be disabled on a course-by-course basis. # TODO: After removing this flag, add a migration to remove waffle flag in a follow-up deployment. REJECTED_EXAM_OVERRIDES_GRADE: CourseWaffleFlag( namespace, REJECTED_EXAM_OVERRIDES_GRADE, __name__, ), # TODO: After removing this flag, add a migration to remove waffle flag in a follow-up deployment. ENFORCE_FREEZE_GRADE_AFTER_COURSE_END: CourseWaffleFlag( namespace, ENFORCE_FREEZE_GRADE_AFTER_COURSE_END, __name__, ), # Have this course override flag so we can selectively turn off the gradebook for courses. # TODO: After removing this flag, add a migration to remove waffle flag in a follow-up deployment. WRITABLE_GRADEBOOK: CourseWaffleFlag( namespace, WRITABLE_GRADEBOOK, __name__, ), BULK_MANAGEMENT: CourseWaffleFlag( namespace, BULK_MANAGEMENT, __name__, ), }
def waffle_flags(): """ Returns the namespaced, cached, audited Waffle flags dictionary for Grades. IMPORTANT: Do NOT copy this dict pattern and do NOT add new flags to this dict. Instead, replace the string constants above with the actual flag instances. """ return { GENERATE_PROBLEM_GRADE_REPORT_VERIFIED_ONLY: CourseWaffleFlag( waffle_namespace=INSTRUCTOR_TASK_WAFFLE_FLAG_NAMESPACE, flag_name=GENERATE_PROBLEM_GRADE_REPORT_VERIFIED_ONLY, module_name=__name__, ), GENERATE_COURSE_GRADE_REPORT_VERIFIED_ONLY: CourseWaffleFlag( waffle_namespace=INSTRUCTOR_TASK_WAFFLE_FLAG_NAMESPACE, flag_name=GENERATE_COURSE_GRADE_REPORT_VERIFIED_ONLY, module_name=__name__, ), }
def waffle_flag(): """ Returns the namespaced, cached, audited Waffle flags dictionary for course experience. """ namespace = WaffleFlagNamespace(name=WAFFLE_NAMESPACE, log_prefix=u'course_experience: ') # By default, disable the new course outline. Can be enabled on a course-by-course basis. # And overridden site-globally by ENABLE_SITE_NEW_COURSE_OUTLINE return CourseWaffleFlag( namespace, ENABLE_NEW_COURSE_OUTLINE_FOR_COURSE, flag_undefined_default=False )
def waffle_flags(): """ Returns the namespaced, cached, audited Waffle flags dictionary for Grades. """ namespace = WaffleFlagNamespace(name=WAFFLE_NAMESPACE, log_prefix=u'Grades: ') return { # By default, enable rejected exam grade overrides. Can be disabled on a course-by-course basis. REJECTED_EXAM_OVERRIDES_GRADE: CourseWaffleFlag( namespace, REJECTED_EXAM_OVERRIDES_GRADE, flag_undefined_default=True, ), ENFORCE_FREEZE_GRADE_AFTER_COURSE_END: CourseWaffleFlag( namespace, ENFORCE_FREEZE_GRADE_AFTER_COURSE_END, flag_undefined_default=True, ) }
def waffle_flag(): """ Returns the namespaced, cached, audited Waffle flags dictionary for Completion. By default, disable visual progress. Can be enabled on a course-by-course basis. And overridden site-globally by ENABLE_VISUAL_PROGRESS """ namespace = WaffleFlagNamespace(name=WAFFLE_NAMESPACE, log_prefix=u'completion: ') return CourseWaffleFlag(namespace, ENABLE_COURSE_VISUAL_PROGRESS, flag_undefined_default=False)
def __init__(self, flag_name, module_name, num_buckets=2, experiment_id=None, use_course_aware_bucketing=True, **kwargs): super().__init__(flag_name, module_name, **kwargs) self.num_buckets = num_buckets self.experiment_id = experiment_id self.bucket_flags = [ CourseWaffleFlag(f'{flag_name}.{bucket}', module_name) # lint-amnesty, pylint: disable=toggle-missing-annotation for bucket in range(num_buckets) ] self.use_course_aware_bucketing = use_course_aware_bucketing
def are_team_submissions_enabled(course_key): """ Checks to see if the CourseWaffleFlag or Django setting for team submissions is enabled """ if CourseWaffleFlag(f'{WAFFLE_NAMESPACE}.{TEAM_SUBMISSIONS_FLAG}', __name__).is_enabled(course_key): return True # TODO: this behaviour differs from edx-ora2, where the WaffleSwitch overrides the setting. # https://github.com/edx/edx-ora2/blob/ac502d8301cb987c9885aaefbaeddaf456c13fb9/openassessment/xblock/config_mixin.py#L96 if TEAM_SUBMISSIONS_FEATURE.is_enabled(): return True return False
def __init__(self, waffle_namespace, flag_name, num_buckets=2, experiment_id=None, use_course_aware_bucketing=True, **kwargs): super().__init__(waffle_namespace, flag_name, **kwargs) self.num_buckets = num_buckets self.experiment_id = experiment_id self.bucket_flags = [ CourseWaffleFlag(waffle_namespace, '{}.{}'.format(flag_name, bucket)) for bucket in range(num_buckets) ] self.use_course_aware_bucketing = use_course_aware_bucketing
def test_forcing_bucket(self, active, expected_bucket): bucket_flag = CourseWaffleFlag('experiments', 'test.0', __name__) with override_waffle_flag(bucket_flag, active=active): self.assertEqual(self.get_bucket(), expected_bucket)
class ExperimentWaffleFlagCourseAwarenessTest(SharedModuleStoreTestCase): """ Tests for how course context awareness/unawareness interacts with the ExperimentWaffleFlag class. """ course_aware_flag = ExperimentWaffleFlag( 'exp', 'aware', __name__, num_buckets=20, use_course_aware_bucketing=True, ) course_aware_subflag = CourseWaffleFlag('exp', 'aware.1', __name__) course_unaware_flag = ExperimentWaffleFlag( 'exp', 'unaware', __name__, num_buckets=20, use_course_aware_bucketing=False, ) course_unaware_subflag = CourseWaffleFlag('exp', 'unaware.1', __name__) course_key_1 = CourseKey.from_string("x/y/1") course_key_2 = CourseKey.from_string("x/y/22") course_key_3 = CourseKey.from_string("x/y/333") @classmethod def setUpTestData(cls): super().setUpTestData() # Force all users into Bucket 1 for course at `course_key_1`. WaffleFlagCourseOverrideModel.objects.create( waffle_flag="exp.aware.1", course_id=cls.course_key_1, enabled=True) WaffleFlagCourseOverrideModel.objects.create( waffle_flag="exp.unaware.1", course_id=cls.course_key_1, enabled=True) cls.user = UserFactory() def setUp(self): super().setUp() self.request = RequestFactory().request() self.request.session = {} self.request.site = SiteFactory() self.request.user = self.user self.addCleanup(set_current_request, None) set_current_request(self.request) self.addCleanup(RequestCache.clear_all_namespaces) # Enable all experiment waffle flags. experiment_waffle_flag_patcher = patch.object(ExperimentWaffleFlag, 'is_experiment_on', return_value=True) experiment_waffle_flag_patcher.start() self.addCleanup(experiment_waffle_flag_patcher.stop) # Use our custom fake `stable_bucketing_hash_group` implementation. stable_bucket_patcher = patch( 'lms.djangoapps.experiments.flags.stable_bucketing_hash_group', self._mock_stable_bucket) stable_bucket_patcher.start() self.addCleanup(stable_bucket_patcher.stop) @staticmethod def _mock_stable_bucket(group_name, *_args, **_kwargs): """ A fake version of `stable_bucketing_hash_group` that just returns the length of `group_name`. """ return len(group_name) def test_course_aware_bucketing(self): """ Test behavior of an experiment flag configured wtih course-aware bucket hashing. """ # Expect queries for Course 1 to be forced into Bucket 1 # due to `course_aware_subflag`. assert self.course_aware_flag.get_bucket(self.course_key_1) == 1 # Because we are using course-aware bucket hashing, different # courses may default to different buckets. # In the case of Courses 2 and 3 here, we expect two different buckets. assert self.course_aware_flag.get_bucket(self.course_key_2) == 16 assert self.course_aware_flag.get_bucket(self.course_key_3) == 17 # We can still query a course-aware flag outside of course context, # which has its own default bucket. assert self.course_aware_flag.get_bucket() == 9 def test_course_unaware_bucketing(self): """ Test behavior of an experiment flag configured wtih course-unaware bucket hashing. """ # Expect queries for Course 1 to be forced into Bucket 1 # due to `course_unaware_subflag`. # This should happen in spite of the fact that *default* bucketing # is unaware of courses. assert self.course_unaware_flag.get_bucket(self.course_key_1) == 1 # Expect queries for Course 2, queries for Course 3, and queries outside # the context of the course to all be hashed into the same default bucket. assert self.course_unaware_flag.get_bucket(self.course_key_2) == 11 assert self.course_unaware_flag.get_bucket(self.course_key_3) == 11 assert self.course_unaware_flag.get_bucket() == 11
def waffle(): """ Returns the namespaced, cached, audited Waffle Switch class for Studio pages. """ return WaffleSwitchNamespace(name=WAFFLE_NAMESPACE, log_prefix=u'Studio: ') def waffle_flags(): """ Returns the namespaced, cached, audited Waffle Flag class for Studio pages. """ return WaffleFlagNamespace(name=WAFFLE_NAMESPACE, log_prefix=u'Studio: ') # Flags ENABLE_PROCTORING_PROVIDER_OVERRIDES = CourseWaffleFlag( waffle_namespace=waffle_flags(), flag_name=u'enable_proctoring_provider_overrides', flag_undefined_default=False) ENABLE_CHECKLISTS_QUALITY = CourseWaffleFlag( waffle_namespace=waffle_flags(), flag_name=u'enable_checklists_quality', flag_undefined_default=True) SHOW_REVIEW_RULES_FLAG = CourseWaffleFlag(waffle_namespace=waffle_flags(), flag_name=u'show_review_rules', flag_undefined_default=False)
def enable_anonymous_courseware_access(self): waffle_flag = CourseWaffleFlag(WaffleFlagNamespace(name='seo'), 'enable_anonymous_courseware_access') return waffle_flag.is_enabled(self.course_key)