def access_denied_fragment(self, block, user, user_group, allowed_groups): course_key = self._get_course_key_from_course_block(block) course = CourseOverview.get_from_id(course_key) modes = CourseMode.modes_for_course_dict(course=course, include_expired=True) verified_mode = modes.get(CourseMode.VERIFIED) if (verified_mode is None or user_group == FULL_ACCESS or user_group in allowed_groups): return None expiration_datetime = verified_mode.expiration_datetime if expiration_datetime and expiration_datetime < datetime.datetime.now( pytz.UTC): ecommerce_checkout_link = None else: ecommerce_checkout_link = self._get_checkout_link( user, verified_mode.sku) request = crum.get_current_request() upgrade_price, _ = format_strikeout_price(user, course) frag = Fragment( render_to_string( 'content_type_gating/access_denied_message.html', { 'mobile_app': request and is_request_from_mobile_app(request), 'ecommerce_checkout_link': ecommerce_checkout_link, 'min_price': upgrade_price, })) return frag
def complete_course_mode_info(course_id, enrollment, modes=None): """ We would like to compute some more information from the given course modes and the user's current enrollment Returns the given information: - whether to show the course upsell information - numbers of days until they can't upsell anymore """ if modes is None: modes = CourseMode.modes_for_course_dict(course_id) mode_info = {'show_upsell': False, 'days_for_upsell': None} # we want to know if the user is already enrolled as verified or credit and # if verified is an option. if CourseMode.VERIFIED in modes and enrollment.mode in CourseMode.UPSELL_TO_VERIFIED_MODES: mode_info['show_upsell'] = True mode_info['verified_sku'] = modes['verified'].sku mode_info['verified_bulk_sku'] = modes['verified'].bulk_sku # if there is an expiration date, find out how long from now it is if modes['verified'].expiration_datetime: today = datetime.datetime.now(UTC).date() mode_info['days_for_upsell'] = (modes['verified'].expiration_datetime.date() - today).days return mode_info
def can_show_streak_discount_experiment_coupon(user, course): """ Check whether this combination of user and course can receive the AA-759 experiment discount. """ # Course end date needs to be in the future if course.has_ended(): return False # Course needs to have a non-expired verified mode modes_dict = CourseMode.modes_for_course_dict(course=course, include_expired=False) if 'verified' not in modes_dict: return False # Learner needs to be in an upgradeable mode try: enrollment = CourseEnrollment.objects.get( user=user, course=course.id, ) except CourseEnrollment.DoesNotExist: return False if not is_mode_upsellable(user, enrollment): return False # We can't import this at Django load time within the openedx tests settings context from openedx.features.enterprise_support.utils import is_enterprise_learner # Don't give discount to enterprise users if is_enterprise_learner(user): return False return True
def get_celebrations_dict(user, enrollment, course, browser_timezone): """ Returns a dict of celebrations that should be performed. """ if not enrollment: return { 'first_section': False, 'streak_length_to_celebrate': None, 'streak_discount_enabled': False, } streak_length_to_celebrate = UserCelebration.perform_streak_updates( user, course.id, browser_timezone ) celebrations = { 'first_section': CourseEnrollmentCelebration.should_celebrate_first_section(enrollment), 'streak_length_to_celebrate': streak_length_to_celebrate, 'streak_discount_enabled': False, } if streak_length_to_celebrate: # We only want to offer the streak discount # if the course has not ended, is upgradeable and the user is not an enterprise learner if can_show_streak_discount_coupon(user, course): # Send course streak coupon event course_key = str(course.id) modes_dict = CourseMode.modes_for_course_dict(course_id=course_key, include_expired=False) verified_mode = modes_dict.get('verified', None) if verified_mode: celebrations['streak_discount_enabled'] = True return celebrations
def _check_enrollment(self, user, course_key): """Check whether the user has an active enrollment and has paid. If a user is enrolled in a paid course mode, we assume that the user has paid. Arguments: user (User): The user to check. course_key (CourseKey): The key of the course to check. Returns: Tuple `(has_paid, is_active)` indicating whether the user has paid and whether the user has an active account. """ enrollment_mode, is_active = CourseEnrollment.enrollment_mode_for_user( user, course_key) has_paid = False if enrollment_mode is not None and is_active: all_modes = CourseMode.modes_for_course_dict(course_key, include_expired=True) course_mode = all_modes.get(enrollment_mode) has_paid = (course_mode and course_mode.min_price > 0) return (has_paid, bool(is_active))
def test_hide_credit_modes(self, available_modes, expected_selectable_modes): # Create the course modes for mode in available_modes: CourseModeFactory.create( course_id=self.course_key, mode_display_name=mode, mode_slug=mode, ) # Check the selectable modes, which should exclude credit selectable_modes = CourseMode.modes_for_course_dict(self.course_key) six.assertCountEqual(self, list(selectable_modes.keys()), expected_selectable_modes) # When we get all unexpired modes, we should see credit as well all_modes = CourseMode.modes_for_course_dict(self.course_key, only_selectable=False) six.assertCountEqual(self, list(all_modes.keys()), available_modes)
def test_contains_masters_mode(self, available_modes, expected_contains_masters_mode): for mode in available_modes: self.create_mode(mode, mode, 10) modes = CourseMode.modes_for_course_dict(self.course_key) assert CourseMode.contains_masters_mode( modes) == expected_contains_masters_mode
def can_receive_discount(user, course, discount_expiration_date=None): """ Check all the business logic about whether this combination of user and course can receive a discount. """ # Always disable discounts until we are ready to enable this feature with impersonate(user): if not DISCOUNT_APPLICABILITY_FLAG.is_enabled(): return False # TODO: Add additional conditions to return False here # Check if discount has expired if not discount_expiration_date: discount_expiration_date = get_discount_expiration_date(user, course) if discount_expiration_date is None: return False if discount_expiration_date < timezone.now(): return False # Course end date needs to be in the future if course.has_ended(): return False # Course needs to have a non-expired verified mode modes_dict = CourseMode.modes_for_course_dict(course=course, include_expired=False) verified_mode = modes_dict.get('verified', None) if not verified_mode: return False # Site, Partner, Course or Course Run not excluded from lms-controlled discounts if DiscountRestrictionConfig.disabled_for_course_stacked_config(course): return False # Don't allow users who have enrolled in any courses in non-upsellable # modes if CourseEnrollment.objects.filter(user=user).exclude( mode__in=CourseMode.UPSELL_TO_VERIFIED_MODES).exists(): return False # Don't allow any users who have entitlements (past or present) if CourseEntitlement.objects.filter(user=user).exists(): return False # We can't import this at Django load time within the openedx tests settings context from openedx.features.enterprise_support.utils import is_enterprise_learner # Don't give discount to enterprise users if is_enterprise_learner(user): return False # Turn holdback on if _is_in_holdback_and_bucket(user): return False return True
def test_course_has_professional_mode(self, mode): # check the professional mode. self.create_mode(mode, 'course mode', 10) modes_dict = CourseMode.modes_for_course_dict(self.course_key) if mode in ['professional', 'no-id-professional']: assert CourseMode.has_professional_mode(modes_dict) else: assert not CourseMode.has_professional_mode(modes_dict)
def has_course_goal_permission(request, course_id, user_access): """ Returns whether the user can access the course goal functionality. Only authenticated users that are enrolled in a verifiable course can use this feature. """ course_key = CourseKey.from_string(course_id) has_verified_mode = CourseMode.has_verified_mode(CourseMode.modes_for_course_dict(course_key)) return user_access['is_enrolled'] and has_verified_mode and ENABLE_COURSE_GOALS.is_enabled(course_key) \ and settings.FEATURES.get('ENABLE_COURSE_GOALS')
def access_denied_message(self, block_key, user, user_group, allowed_groups): course_key = block_key.course_key modes = CourseMode.modes_for_course_dict(course_key) verified_mode = modes.get(CourseMode.VERIFIED) if (verified_mode is None or user_group == FULL_ACCESS or user_group in allowed_groups): return None request = crum.get_current_request() if request and is_request_from_mobile_app(request): return _("Graded assessments are available to Verified Track learners.") else: return _("Graded assessments are available to Verified Track learners. Upgrade to Unlock.")
def test_nodes_for_course_single(self): """ Find the modes for a course with only one mode """ self.create_mode('verified', 'Verified Certificate', 10) modes = CourseMode.modes_for_course(self.course_key) mode = Mode(u'verified', u'Verified Certificate', 10, '', 'usd', None, None, None, None) assert [mode] == modes modes_dict = CourseMode.modes_for_course_dict(self.course_key) assert modes_dict['verified'] == mode assert CourseMode.mode_for_course(self.course_key, 'verified') == mode
def test_nodes_for_course_single(self): """ Find the modes for a course with only one mode """ self.create_mode('verified', 'Verified Certificate', 10) modes = CourseMode.modes_for_course(self.course_key) mode = Mode(u'verified', u'Verified Certificate', 10, '', 'usd', None, None, None, None) self.assertEqual([mode], modes) modes_dict = CourseMode.modes_for_course_dict(self.course_key) self.assertEqual(modes_dict['verified'], mode) self.assertEqual( CourseMode.mode_for_course(self.course_key, 'verified'), mode)
def unlink_program_enrollment(program_enrollment): """ Unlinks CourseEnrollments from the ProgramEnrollment by doing the following for each ProgramCourseEnrollment associated with the Program Enrollment. 1. unenrolling the corresponding user from the course 2. moving the user into the audit track, if the track exists 3. removing the link between the ProgramCourseEnrollment and the CourseEnrollment Arguments: program_enrollment: the ProgramEnrollment object """ program_course_enrollments = program_enrollment.program_course_enrollments.all( ) for pce in program_course_enrollments: course_key = pce.course_enrollment.course.id modes = CourseMode.modes_for_course_dict(course_key) update_enrollment_kwargs = { 'is_active': False, 'skip_refund': True, } if CourseMode.contains_audit_mode(modes): # if the course contains an audit mode, move the # learner's enrollment into the audit mode update_enrollment_kwargs['mode'] = 'audit' # deactive the learner's course enrollment and move them into the # audit track, if it exists pce.course_enrollment.update_enrollment(**update_enrollment_kwargs) # sever ties to the user from the ProgramCourseEnrollment pce.course_enrollment = None pce.save() program_enrollment.user = None program_enrollment.save()
def change_enrollment(request, check_access=True): """ Modify the enrollment status for the logged-in user. TODO: This is lms specific and does not belong in common code. The request parameter must be a POST request (other methods return 405) that specifies course_id and enrollment_action parameters. If course_id or enrollment_action is not specified, if course_id is not valid, if enrollment_action is something other than "enroll" or "unenroll", if enrollment_action is "enroll" and enrollment is closed for the course, or if enrollment_action is "unenroll" and the user is not enrolled in the course, a 400 error will be returned. If the user is not logged in, 403 will be returned; it is important that only this case return 403 so the front end can redirect the user to a registration or login page when this happens. This function should only be called from an AJAX request, so the error messages in the responses should never actually be user-visible. Args: request (`Request`): The Django request object Keyword Args: check_access (boolean): If True, we check that an accessible course actually exists for the given course_key before we enroll the student. The default is set to False to avoid breaking legacy code or code with non-standard flows (ex. beta tester invitations), but for any standard enrollment flow you probably want this to be True. Returns: Response """ # Get the user user = request.user # Ensure the user is authenticated if not user.is_authenticated: return HttpResponseForbidden() # Ensure we received a course_id action = request.POST.get("enrollment_action") if 'course_id' not in request.POST: return HttpResponseBadRequest(_("Course id not specified")) try: course_id = CourseKey.from_string(request.POST.get("course_id")) except InvalidKeyError: log.warning( "User %s tried to %s with invalid course id: %s", user.username, action, request.POST.get("course_id"), ) return HttpResponseBadRequest(_("Invalid course id")) # Allow us to monitor performance of this transaction on a per-course basis since we often roll-out features # on a per-course basis. monitoring_utils.set_custom_attribute('course_id', str(course_id)) if action == "enroll": # Make sure the course exists # We don't do this check on unenroll, or a bad course id can't be unenrolled from if not modulestore().has_course(course_id): log.warning( "User %s tried to enroll in non-existent course %s", user.username, course_id ) return HttpResponseBadRequest(_("Course id is invalid")) # Record the user's email opt-in preference if settings.FEATURES.get('ENABLE_MKTG_EMAIL_OPT_IN'): _update_email_opt_in(request, course_id.org) available_modes = CourseMode.modes_for_course_dict(course_id) # Check whether the user is blocked from enrolling in this course # This can occur if the user's IP is on a global blacklist # or if the user is enrolling in a country in which the course # is not available. redirect_url = embargo_api.redirect_if_blocked( course_id, user=user, ip_address=get_client_ip(request)[0], url=request.path ) if redirect_url: return HttpResponse(redirect_url) if CourseEntitlement.check_for_existing_entitlement_and_enroll(user=user, course_run_key=course_id): return HttpResponse(reverse('courseware', args=[str(course_id)])) # Check that auto enrollment is allowed for this course # (= the course is NOT behind a paywall) if CourseMode.can_auto_enroll(course_id): # Enroll the user using the default mode (audit) # We're assuming that users of the course enrollment table # will NOT try to look up the course enrollment model # by its slug. If they do, it's possible (based on the state of the database) # for no such model to exist, even though we've set the enrollment type # to "audit". try: enroll_mode = CourseMode.auto_enroll_mode(course_id, available_modes) if enroll_mode: CourseEnrollment.enroll(user, course_id, check_access=check_access, mode=enroll_mode) except Exception: # pylint: disable=broad-except return HttpResponseBadRequest(_("Could not enroll")) # If we have more than one course mode or professional ed is enabled, # then send the user to the choose your track page. # (In the case of no-id-professional/professional ed, this will redirect to a page that # funnels users directly into the verification / payment flow) if CourseMode.has_verified_mode(available_modes) or CourseMode.has_professional_mode(available_modes): return HttpResponse( reverse("course_modes_choose", kwargs={'course_id': str(course_id)}) ) # Otherwise, there is only one mode available (the default) return HttpResponse() elif action == "unenroll": enrollment = CourseEnrollment.get_enrollment(user, course_id) if not enrollment: return HttpResponseBadRequest(_("You are not enrolled in this course")) certificate_info = cert_info(user, enrollment.course_overview) if certificate_info.get('status') in DISABLE_UNENROLL_CERT_STATES: return HttpResponseBadRequest(_("Your certificate prevents you from unenrolling from this course")) CourseEnrollment.unenroll(user, course_id) REFUND_ORDER.send(sender=None, course_enrollment=enrollment) return HttpResponse() else: return HttpResponseBadRequest(_("Enrollment action is invalid"))
def get(self, request, course_id, error=None): # lint-amnesty, pylint: disable=too-many-statements """Displays the course mode choice page. Args: request (`Request`): The Django Request object. course_id (unicode): The slash-separated course key. Keyword Args: error (unicode): If provided, display this error message on the page. Returns: Response """ course_key = CourseKey.from_string(course_id) # Check whether the user has access to this course # based on country access rules. embargo_redirect = embargo_api.redirect_if_blocked( course_key, user=request.user, ip_address=get_client_ip(request)[0], url=request.path) if embargo_redirect: return redirect(embargo_redirect) enrollment_mode, is_active = CourseEnrollment.enrollment_mode_for_user( request.user, course_key) increment('track-selection.{}.{}'.format( enrollment_mode, 'active' if is_active else 'inactive')) increment('track-selection.views') if enrollment_mode is None: LOG.info( 'Rendering track selection for unenrolled user, referred by %s', request.META.get('HTTP_REFERER')) modes = CourseMode.modes_for_course_dict(course_key) ecommerce_service = EcommerceService() # We assume that, if 'professional' is one of the modes, it should be the *only* mode. # If there are both modes, default to 'no-id-professional'. has_enrolled_professional = ( CourseMode.is_professional_slug(enrollment_mode) and is_active) if CourseMode.has_professional_mode( modes) and not has_enrolled_professional: purchase_workflow = request.GET.get("purchase_workflow", "single") redirect_url = IDVerificationService.get_verify_location( course_id=course_key) if ecommerce_service.is_enabled(request.user): professional_mode = modes.get( CourseMode.NO_ID_PROFESSIONAL_MODE) or modes.get( CourseMode.PROFESSIONAL) if purchase_workflow == "single" and professional_mode.sku: redirect_url = ecommerce_service.get_checkout_page_url( professional_mode.sku) if purchase_workflow == "bulk" and professional_mode.bulk_sku: redirect_url = ecommerce_service.get_checkout_page_url( professional_mode.bulk_sku) return redirect(redirect_url) course = modulestore().get_course(course_key) # If there isn't a verified mode available, then there's nothing # to do on this page. Send the user to the dashboard. if not CourseMode.has_verified_mode(modes): return self._redirect_to_course_or_dashboard( course, course_key, request.user) # If a user has already paid, redirect them to the dashboard. if is_active and (enrollment_mode in CourseMode.VERIFIED_MODES + [CourseMode.NO_ID_PROFESSIONAL_MODE]): return self._redirect_to_course_or_dashboard( course, course_key, request.user) donation_for_course = request.session.get("donation_for_course", {}) chosen_price = donation_for_course.get(str(course_key), None) if CourseEnrollment.is_enrollment_closed(request.user, course): locale = to_locale(get_language()) enrollment_end_date = format_datetime(course.enrollment_end, 'short', locale=locale) params = six.moves.urllib.parse.urlencode( {'course_closed': enrollment_end_date}) return redirect('{}?{}'.format(reverse('dashboard'), params)) # When a credit mode is available, students will be given the option # to upgrade from a verified mode to a credit mode at the end of the course. # This allows students who have completed photo verification to be eligible # for university credit. # Since credit isn't one of the selectable options on the track selection page, # we need to check *all* available course modes in order to determine whether # a credit mode is available. If so, then we show slightly different messaging # for the verified track. has_credit_upsell = any( CourseMode.is_credit_mode(mode) for mode in CourseMode.modes_for_course(course_key, only_selectable=False)) course_id = str(course_key) gated_content = ContentTypeGatingConfig.enabled_for_enrollment( user=request.user, course_key=course_key) context = { "course_modes_choose_url": reverse("course_modes_choose", kwargs={'course_id': course_id}), "modes": modes, "has_credit_upsell": has_credit_upsell, "course_name": course.display_name_with_default, "course_org": course.display_org_with_default, "course_num": course.display_number_with_default, "chosen_price": chosen_price, "error": error, "responsive": True, "nav_hidden": True, "content_gating_enabled": gated_content, "course_duration_limit_enabled": CourseDurationLimitConfig.enabled_for_enrollment( request.user, course), } context.update( get_experiment_user_metadata_context( course, request.user, )) title_content = '' if enrollment_mode: title_content = _( "Congratulations! You are now enrolled in {course_name}" ).format(course_name=course.display_name_with_default) context["title_content"] = title_content if "verified" in modes: verified_mode = modes["verified"] context["suggested_prices"] = [ decimal.Decimal(x.strip()) for x in verified_mode.suggested_prices.split(",") if x.strip() ] price_before_discount = verified_mode.min_price course_price = price_before_discount enterprise_customer = enterprise_customer_for_request(request) LOG.info( '[e-commerce calculate API] Going to hit the API for user [%s] linked to [%s] enterprise', request.user.username, enterprise_customer.get('name') if isinstance( enterprise_customer, dict) else None # Test Purpose ) if enterprise_customer and verified_mode.sku: course_price = get_course_final_price(request.user, verified_mode.sku, price_before_discount) context["currency"] = verified_mode.currency.upper() context["currency_symbol"] = get_currency_symbol( verified_mode.currency.upper()) context["min_price"] = course_price context["verified_name"] = verified_mode.name context["verified_description"] = verified_mode.description # if course_price is equal to price_before_discount then user doesn't entitle to any discount. if course_price != price_before_discount: context["price_before_discount"] = price_before_discount if verified_mode.sku: context[ "use_ecommerce_payment_flow"] = ecommerce_service.is_enabled( request.user) context[ "ecommerce_payment_page"] = ecommerce_service.payment_page_url( ) context["sku"] = verified_mode.sku context["bulk_sku"] = verified_mode.bulk_sku context['currency_data'] = [] if waffle.switch_is_active('local_currency'): if 'edx-price-l10n' not in request.COOKIES: currency_data = get_currency_data() try: context['currency_data'] = json.dumps(currency_data) except TypeError: pass language = get_language() context['track_links'] = get_verified_track_links(language) duration = get_user_course_duration(request.user, course) deadline = duration and get_user_course_expiration_date( request.user, course) if deadline: formatted_audit_access_date = strftime_localized_html( deadline, 'SHORT_DATE') context['audit_access_deadline'] = formatted_audit_access_date fbe_is_on = deadline and gated_content # Route to correct Track Selection page. # REV-2133 TODO Value Prop: remove waffle flag after testing is completed # and happy path version is ready to be rolled out to all users. if VALUE_PROP_TRACK_SELECTION_FLAG.is_enabled(): if not error: # TODO: Remove by executing REV-2355 if not enterprise_customer_for_request( request): # TODO: Remove by executing REV-2342 if fbe_is_on: return render_to_response("course_modes/fbe.html", context) else: return render_to_response("course_modes/unfbe.html", context) # If error or enterprise_customer, failover to old choose.html page return render_to_response("course_modes/choose.html", context)
def post(self, request, course_id): """Takes the form submission from the page and parses it. Args: request (`Request`): The Django Request object. course_id (unicode): The slash-separated course key. Returns: Status code 400 when the requested mode is unsupported. When the honor mode is selected, redirects to the dashboard. When the verified mode is selected, returns error messages if the indicated contribution amount is invalid or below the minimum, otherwise redirects to the verification flow. """ course_key = CourseKey.from_string(course_id) user = request.user # This is a bit redundant with logic in student.views.change_enrollment, # but I don't really have the time to refactor it more nicely and test. course = modulestore().get_course(course_key) if not user.has_perm(ENROLL_IN_COURSE, course): error_msg = _("Enrollment is closed") return self.get(request, course_id, error=error_msg) requested_mode = self._get_requested_mode(request.POST) allowed_modes = CourseMode.modes_for_course_dict(course_key) if requested_mode not in allowed_modes: return HttpResponseBadRequest(_("Enrollment mode not supported")) if requested_mode == 'audit': # If the learner has arrived at this screen via the traditional enrollment workflow, # then they should already be enrolled in an audit mode for the course, assuming one has # been configured. However, alternative enrollment workflows have been introduced into the # system, such as third-party discovery. These workflows result in learners arriving # directly at this screen, and they will not necessarily be pre-enrolled in the audit mode. CourseEnrollment.enroll(request.user, course_key, CourseMode.AUDIT) return self._redirect_to_course_or_dashboard( course, course_key, user) if requested_mode == 'honor': CourseEnrollment.enroll(user, course_key, mode=requested_mode) return self._redirect_to_course_or_dashboard( course, course_key, user) mode_info = allowed_modes[requested_mode] if requested_mode == 'verified': amount = request.POST.get("contribution") or \ request.POST.get("contribution-other-amt") or 0 try: # Validate the amount passed in and force it into two digits amount_value = decimal.Decimal(amount).quantize( decimal.Decimal('.01'), rounding=decimal.ROUND_DOWN) except decimal.InvalidOperation: error_msg = _("Invalid amount selected.") return self.get(request, course_id, error=error_msg) # Check for minimum pricing if amount_value < mode_info.min_price: error_msg = _( "No selected price or selected price is too low.") return self.get(request, course_id, error=error_msg) donation_for_course = request.session.get("donation_for_course", {}) donation_for_course[str(course_key)] = amount_value request.session["donation_for_course"] = donation_for_course verify_url = IDVerificationService.get_verify_location( course_id=course_key) return redirect(verify_url)
def get_bucket(self, course_key=None, track=True): """ Return which bucket number the specified user is in. The user may be force-bucketed if matching subordinate flags of the form "main_flag.BUCKET_NUM" exist. Otherwise, they will be hashed into a default bucket based on their username, the experiment name, and the course-run key. If `self.use_course_aware_bucketing` is False, the course-run key will be omitted from the hashing formula, thus making it so a given user has the same default bucket across all course runs; however, subordinate flags that match the course-run key will still apply. If `course_key` argument is omitted altogether, then subordinate flags will be evaluated outside of the course-run context, and the default bucket will be calculated as if `self.use_course_aware_bucketing` is False. Finally, Bucket 0 is assumed to be the control bucket and will be returned if the experiment is not enabled for this user and course. Arguments: course_key (Optional[CourseKey]) This argument should always be passed in a course-aware context even if course aware bucketing is False. track (bool): Whether an analytics event should be generated if the user is bucketed for the first time. Returns: int """ # Keep some imports in here, because this class is commonly used at a module level, and we want to avoid # circular imports for any models. from lms.djangoapps.experiments.models import ExperimentKeyValue from lms.djangoapps.courseware.masquerade import get_specific_masquerading_user request = get_current_request() if not request: return 0 if hasattr(request, 'user'): user = get_specific_masquerading_user(request.user, course_key) if user is None: user = request.user masquerading_as_specific_student = False else: masquerading_as_specific_student = True # If a course key is passed in, include it in the experiment name # in order to separate caches and analytics calls per course-run. # If we are using course-aware bucketing, then also append that course key # to `bucketing_group_name`, such that users can be hashed into different # buckets for different course-runs. experiment_name = bucketing_group_name = self.name if course_key: experiment_name += f".{course_key}" if course_key and self.use_course_aware_bucketing: bucketing_group_name += f".{course_key}" # Check if we have a cache for this request already request_cache = RequestCache('experiments') cache_response = request_cache.get_cached_response(experiment_name) if cache_response.is_found: return cache_response.value # Check if the main flag is even enabled for this user and course. if not self.is_experiment_on(course_key): # grabs user from the current request, if any return self._cache_bucket(experiment_name, 0) # Check if the enrollment should even be considered (if it started before the experiment wants, we ignore) if course_key and self.experiment_id is not None: values = ExperimentKeyValue.objects.filter(experiment_id=self.experiment_id).values('key', 'value') values = {pair['key']: pair['value'] for pair in values} if not self._is_enrollment_inside_date_bounds(values, user, course_key): return self._cache_bucket(experiment_name, 0) # Determine the user's bucket. # First check if forced into a particular bucket, using our subordinate bucket flags. # If not, calculate their default bucket using a consistent hash function. for i, bucket_flag in enumerate(self.bucket_flags): if bucket_flag.is_enabled(course_key): bucket = i break else: bucket = stable_bucketing_hash_group( bucketing_group_name, self.num_buckets, user ) session_key = f'tracked.{experiment_name}' anonymous = not hasattr(request, 'user') or not request.user.id if ( track and hasattr(request, 'session') and session_key not in request.session and not masquerading_as_specific_student and not anonymous ): segment.track( user_id=user.id, event_name='edx.bi.experiment.user.bucketed', properties={ 'site': request.site.domain, 'app_label': self._app_label, 'experiment': self._experiment_name, 'course_id': str(course_key) if course_key else None, 'bucket': bucket, 'is_staff': user.is_staff, 'nonInteraction': 1, } ) # Mark that we've recorded this bucketing, so that we don't do it again this session request.session[session_key] = True # Temporary event for AA-759 experiment if course_key and self._experiment_name == 'discount_experiment_AA759': modes_dict = CourseMode.modes_for_course_dict(course_id=course_key, include_expired=False) verified_mode = modes_dict.get('verified', None) if verified_mode: segment.track( user_id=user.id, event_name='edx.bi.experiment.AA759.bucketed', properties={ 'course_id': str(course_key), 'bucket': bucket, 'sku': verified_mode.sku, } ) return self._cache_bucket(experiment_name, bucket)