""" Utilities to facilitate experimentation """ import hashlib import re import logging from student.models import CourseEnrollment from django_comment_common.models import Role from course_modes.models import get_cosmetic_verified_display_price from courseware.access import has_staff_access_to_preview_mode from courseware.date_summary import verified_upgrade_deadline_link, verified_upgrade_link_is_valid from xmodule.partitions.partitions_service import get_user_partition_groups, get_all_partitions_for_course from opaque_keys.edx.keys import CourseKey from opaque_keys import InvalidKeyError from openedx.core.djangoapps.catalog.utils import get_programs from openedx.core.djangoapps.waffle_utils import WaffleFlag, WaffleFlagNamespace logger = logging.getLogger(__name__) # TODO: clean up as part of REVEM-199 (START) # .. feature_toggle_name: experiments.add_programs # .. feature_toggle_type: flag # .. feature_toggle_default: False # .. feature_toggle_description: Toggle for adding the current course's program information to user metadata # .. feature_toggle_category: experiments # .. feature_toggle_use_cases: monitored_rollout # .. feature_toggle_creation_date: 2019-2-25 # .. feature_toggle_expiration_date: None # .. feature_toggle_warnings: None # .. feature_toggle_tickets: REVEM-63, REVEM-198 # .. feature_toggle_status: supported PROGRAM_INFO_FLAG = WaffleFlag( waffle_namespace=WaffleFlagNamespace(name=u'experiments'), flag_name=u'add_programs', flag_undefined_default=False ) # TODO: clean up as part of REVEM-199 (END) def check_and_get_upgrade_link_and_date(user, enrollment=None, course=None): """ For an authenticated user, return a link to allow them to upgrade in the specified course. Returns the upgrade link and upgrade deadline for a user in a given course given that the user is within the window to upgrade defined by our dynamic pacing feature; otherwise, returns None for both the link and date. """ if enrollment is None and course is None: raise ValueError("Must specify either an enrollment or a course") if enrollment: if course is None: course = enrollment.course elif enrollment.course_id != course.id: raise ValueError(u"{} refers to a different course than {} which was supplied".format( enrollment, course )) if enrollment.user_id != user.id: raise ValueError(u"{} refers to a different user than {} which was supplied".format( enrollment, user )) if enrollment is None: enrollment = CourseEnrollment.get_enrollment(user, course.id) if user.is_authenticated and verified_upgrade_link_is_valid(enrollment): return ( verified_upgrade_deadline_link(user, course), enrollment.upgrade_deadline ) return (None, None) # TODO: clean up as part of REVEM-199 (START) def is_enrolled_in_all_courses_in_program(courses_in_program, user_enrollments): """ Determine if the user is enrolled in all courses in this program """ # Get the enrollment course ids here, so we don't need to loop through them for every course run enrollment_course_ids = {enrollment.course_id for enrollment in user_enrollments} for course in courses_in_program: if not is_enrolled_in_course(course, enrollment_course_ids): # User is not enrolled in this course, meaning they are not enrolled in all courses in the program return False # User is enrolled in all courses in the program return True def is_enrolled_in_course(course, enrollment_course_ids): """ Determine if the user is enrolled in this course """ course_runs = course.get('course_runs') if course_runs: for course_run in course_runs: if is_enrolled_in_course_run(course_run, enrollment_course_ids): return True return False def is_enrolled_in_course_run(course_run, enrollment_course_ids): """ Determine if the user is enrolled in this course run """ key = None try: key = course_run.get('key') course_run_key = CourseKey.from_string(key) return course_run_key in enrollment_course_ids except InvalidKeyError: logger.warn( u'Unable to determine if user was enrolled since the course key {} is invalid'.format(key) ) return False # Invalid course run key. Assume user is not enrolled. # TODO: clean up as part of REVEM-199 (END) def get_experiment_user_metadata_context(course, user): """ Return a context dictionary with the keys used by the user_metadata.html. """ enrollment_mode = None enrollment_time = None enrollment = None # TODO: clean up as part of REVO-28 (START) has_non_audit_enrollments = None # TODO: clean up as part of REVO-28 (END) # TODO: clean up as part of REVEM-199 (START) program_key = None # TODO: clean up as part of REVEM-199 (END) try: # TODO: clean up as part of REVO-28 (START) user_enrollments = CourseEnrollment.objects.select_related('course').filter(user_id=user.id) audit_enrollments = user_enrollments.filter(mode='audit') has_non_audit_enrollments = (len(audit_enrollments) != len(user_enrollments)) # TODO: clean up as part of REVO-28 (END) # TODO: clean up as part of REVEM-199 (START) if PROGRAM_INFO_FLAG.is_enabled(): programs = get_programs(course=course.id) if programs: # A course can be in multiple programs, but we're just grabbing the first one program = programs[0] complete_enrollment = False total_courses = None courses = program.get('courses') if courses is not None: total_courses = len(courses) complete_enrollment = is_enrolled_in_all_courses_in_program(courses, user_enrollments) program_key = { 'uuid': program.get('uuid'), 'title': program.get('title'), 'marketing_url': program.get('marketing_url'), 'total_courses': total_courses, 'complete_enrollment': complete_enrollment, } # TODO: clean up as part of REVEM-199 (END) enrollment = CourseEnrollment.objects.select_related( 'course' ).get(user_id=user.id, course_id=course.id) if enrollment.is_active: enrollment_mode = enrollment.mode enrollment_time = enrollment.created except CourseEnrollment.DoesNotExist: pass # Not enrolled, used the default None values # upgrade_link and upgrade_date should be None if user has passed their dynamic pacing deadline. upgrade_link, upgrade_date = check_and_get_upgrade_link_and_date(user, enrollment, course) has_staff_access = has_staff_access_to_preview_mode(user, course.id) forum_roles = [] if user.is_authenticated: forum_roles = list(Role.objects.filter(users=user, course_id=course.id).values_list('name').distinct()) # get user partition data if user.is_authenticated(): partition_groups = get_all_partitions_for_course(course) user_partitions = get_user_partition_groups(course.id, partition_groups, user, 'name') else: user_partitions = {} return { 'upgrade_link': upgrade_link, 'upgrade_price': unicode(get_cosmetic_verified_display_price(course)), 'enrollment_mode': enrollment_mode, 'enrollment_time': enrollment_time, 'pacing_type': 'self_paced' if course.self_paced else 'instructor_paced', 'upgrade_deadline': upgrade_date, 'course_key': course.id, 'course_start': course.start, 'course_end': course.end, 'has_staff_access': has_staff_access, 'forum_roles': forum_roles, 'partition_groups': user_partitions, # TODO: clean up as part of REVO-28 (START) 'has_non_audit_enrollments': has_non_audit_enrollments, # TODO: clean up as part of REVO-28 (END) # TODO: clean up as part of REVEM-199 (START) 'program_key_fields': program_key, # TODO: clean up as part of REVEM-199 (END) } #TODO START: Clean up REVEM-205 def get_experiment_dashboard_metadata_context(enrollments): """ Given a list of enrollments return a dict of course ids with their prices. Utility function for experimental metadata. See experiments/dashboard_metadata.html. :param enrollments: :return: dict of courses: course price for dashboard metadata """ return {str(enrollment.course): enrollment.course_price for enrollment in enrollments} #TODO END: Clean up REVEM-205 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