From 8b4b5a4547937982edb00462b338fcfe9089f2d3 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Mon, 1 Jul 2019 13:19:23 -0400 Subject: [PATCH] Move stable_bucketing into its own library to minimize circular dependencies --- .../experiments/stable_bucketing.py | 27 +++++++++++++++++++ lms/djangoapps/experiments/utils.py | 25 +++-------------- .../course_duration_limits/resolvers.py | 2 +- openedx/features/discounts/applicability.py | 2 ++ 4 files changed, 33 insertions(+), 23 deletions(-) create mode 100644 lms/djangoapps/experiments/stable_bucketing.py diff --git a/lms/djangoapps/experiments/stable_bucketing.py b/lms/djangoapps/experiments/stable_bucketing.py new file mode 100644 index 0000000000..0bc903ee8b --- /dev/null +++ b/lms/djangoapps/experiments/stable_bucketing.py @@ -0,0 +1,27 @@ +""" +An implementation of a stable bucketing algorithm that can be used +to reliably group users into experiments. +""" + +import hashlib +import re + + +def stable_bucketing_hash_group(group_name, group_count, username): + """ + Return the bucket that a user should be in for a given stable bucketing assignment. + + This function has been verified to return the same values as the stable bucketing + functions in javascript and the master experiments table. + + Arguments: + group_name: The name of the grouping/experiment. + group_count: How many groups to bucket users into. + username: The username of the user being bucketed. + """ + hasher = hashlib.md5() + hasher.update(group_name.encode('utf-8')) + hasher.update(username.encode('utf-8')) + hash_str = hasher.hexdigest() + + return int(re.sub('[8-9a-f]', '1', re.sub('[0-7]', '0', hash_str)), 2) % group_count diff --git a/lms/djangoapps/experiments/utils.py b/lms/djangoapps/experiments/utils.py index e921ac7392..3c00f6984c 100644 --- a/lms/djangoapps/experiments/utils.py +++ b/lms/djangoapps/experiments/utils.py @@ -4,9 +4,7 @@ Utilities to facilitate experimentation from __future__ import absolute_import -import hashlib import logging -import re from decimal import Decimal import six @@ -27,6 +25,9 @@ from openedx.features.course_duration_limits.models import CourseDurationLimitCo from student.models import CourseEnrollment from xmodule.partitions.partitions_service import get_all_partitions_for_course, get_user_partition_groups +# Import this for backwards compatibility (so that anyone importing this function from here doesn't break) +from .stable_bucketing import stable_bucketing_hash_group # pylint: disable=unused-import + logger = logging.getLogger(__name__) @@ -386,23 +387,3 @@ def get_program_context(course, user_enrollments): } return program_key # TODO: clean up as part of REVEM-199 (START) - - -def stable_bucketing_hash_group(group_name, group_count, username): - """ - Return the bucket that a user should be in for a given stable bucketing assignment. - - This function has been verified to return the same values as the stable bucketing - functions in javascript and the master experiments table. - - Arguments: - group_name: The name of the grouping/experiment. - group_count: How many groups to bucket users into. - username: The username of the user being bucketed. - """ - hasher = hashlib.md5() - hasher.update(group_name.encode('utf-8')) - hasher.update(username.encode('utf-8')) - hash_str = hasher.hexdigest() - - return int(re.sub('[8-9a-f]', '1', re.sub('[0-7]', '0', hash_str)), 2) % group_count diff --git a/openedx/features/course_duration_limits/resolvers.py b/openedx/features/course_duration_limits/resolvers.py index 31c627cac3..d22a36b83e 100644 --- a/openedx/features/course_duration_limits/resolvers.py +++ b/openedx/features/course_duration_limits/resolvers.py @@ -15,7 +15,7 @@ from eventtracking import tracker from course_modes.models import CourseMode from courseware.date_summary import verified_upgrade_deadline_link -from lms.djangoapps.experiments.utils import stable_bucketing_hash_group +from lms.djangoapps.experiments.stable_bucketing import stable_bucketing_hash_group from openedx.core.djangoapps.catalog.utils import get_course_run_details from openedx.core.djangoapps.schedules.resolvers import ( BinnedSchedulesBaseResolver, diff --git a/openedx/features/discounts/applicability.py b/openedx/features/discounts/applicability.py index 0c2bb26ea1..9caeb664bd 100644 --- a/openedx/features/discounts/applicability.py +++ b/openedx/features/discounts/applicability.py @@ -8,8 +8,10 @@ Keep in mind that the code in this file only applies to discounts controlled in not other discounts like coupons or enterprise/program offers configured in ecommerce. """ +import crum from course_modes.models import CourseMode from entitlements.models import CourseEntitlement +from lms.djangoapps.experiments.stable_bucketing import stable_bucketing_hash_group from openedx.core.djangoapps.waffle_utils import WaffleFlag, WaffleFlagNamespace from openedx.features.discounts.models import DiscountRestrictionConfig from student.models import CourseEnrollment