def test_flush_with_product_is_not_tracked_for_temporary_basket_calculation(self): """ Verify the method does NOT fire 'Product Removed' Segment for temporary basket calculation """ basket = self._create_basket_with_product() DEFAULT_REQUEST_CACHE.set(TEMPORARY_BASKET_CACHE_KEY, True) with mock.patch.object(Client, 'track') as mock_track: basket.flush() mock_track.assert_not_called()
def test_add_product_not_tracked_for_temporary_basket_calculation(self): """ Verify the method does NOT fire Product Added analytic event when a product is added to the basket """ course = CourseFactory(partner=self.partner) basket = create_basket(empty=True) seat = course.create_or_update_seat('verified', True, 100) DEFAULT_REQUEST_CACHE.set(TEMPORARY_BASKET_CACHE_KEY, True) with mock.patch('ecommerce.extensions.basket.models.track_segment_event') as mock_track: basket.add_product(seat) properties = translate_basket_line_for_segment(basket.lines.first()) properties['cart_id'] = basket.id mock_track.assert_not_called()
def _cache_if_authenticated_user_found_in_middleware(self, request, value): """ Updates the cached process step in which the authenticated user was found, if it hasn't already been found. """ cached_response = DEFAULT_REQUEST_CACHE.get_cached_response( self.AUTHENTICATED_USER_FOUND_CACHE_KEY) if cached_response.is_found: # since we are tracking the earliest point the authenticated user was found, # and the value was already set in earlier middleware step, do not set again. return if hasattr(request, 'user') and request.user and request.user.is_authenticated: DEFAULT_REQUEST_CACHE.set(self.AUTHENTICATED_USER_FOUND_CACHE_KEY, value)
def _set_request_authenticated_user_found_in_middleware_attribute(self): """ Add custom attribute 'request_authenticated_user_found_in_middleware' if authenticated user was found. """ cached_response = DEFAULT_REQUEST_CACHE.get_cached_response( self.AUTHENTICATED_USER_FOUND_CACHE_KEY) if cached_response.is_found: monitoring.set_custom_attribute( 'request_authenticated_user_found_in_middleware', cached_response.value)
def _set_owner_metrics_for_request(self, request): """ Uses the request path to find the view_func and then sets code owner metrics based on the view. """ if not is_code_owner_mappings_configured(): return try: view_func, _, _ = resolve(request.path) view_func_module = view_func.__module__ DEFAULT_REQUEST_CACHE.set(self._VIEW_FUNC_MODULE_METRIC_CACHE_KEY, view_func_module) set_custom_metric('view_func_module', view_func_module) code_owner = get_code_owner_from_module(view_func_module) if code_owner: set_custom_metric('code_owner', code_owner) except Resolver404: set_custom_metric( 'code_owner_mapping_error', "Couldn't resolve view for request path {}".format( request.path)) except Exception as e: set_custom_metric('code_owner_mapping_error', e)
def flush(self): """Remove all products in basket and fire Segment 'Product Removed' Analytic event for each""" cached_response = DEFAULT_REQUEST_CACHE.get_cached_response(TEMPORARY_BASKET_CACHE_KEY) if cached_response.is_found: # Do not track anything. This is a temporary basket calculation. return for line in self.all_lines(): # Do not fire events for free items. The volume we see for edX.org leads to a dramatic increase in CPU # usage. Given that orders for free items are ignored, there is no need for these events. if line.stockrecord.price_excl_tax > 0: properties = translate_basket_line_for_segment(line) track_segment_event(self.site, self.owner, 'Product Removed', properties) # Call flush after we fetch all_lines() which is cleared during flush() super(Basket, self).flush() # pylint: disable=bad-super-call
def _set_view_func_compare_metric(self, view_func): """ Set temporary metric to ensure that the view_func of `process_view` always matches the one from using `resolve` on the request. """ try: view_func_module = view_func.__module__ cached_response = DEFAULT_REQUEST_CACHE.get_cached_response( self._VIEW_FUNC_MODULE_METRIC_CACHE_KEY) if cached_response.is_found: view_func_compare = 'success' if view_func_module == cached_response.value else view_func_module else: view_func_compare = 'missing' set_custom_metric('temp_view_func_compare', view_func_compare) except Exception as e: set_custom_metric('temp_view_func_compare_error', e)
def flush(self): """Remove all products in basket and fire Segment 'Product Removed' Analytic event for each""" cached_response = DEFAULT_REQUEST_CACHE.get_cached_response( TEMPORARY_BASKET_CACHE_KEY) if cached_response.is_found: # Do not track anything. This is a temporary basket calculation. return product_removed_event_fired = False for line in self.all_lines(): # Do not fire events for free items. The volume we see for edX.org leads to a dramatic increase in CPU # usage. Given that orders for free items are ignored, there is no need for these events. if line.stockrecord.price_excl_tax > 0: properties = translate_basket_line_for_segment(line) track_segment_event(self.site, self.owner, 'Product Removed', properties) product_removed_event_fired = True # Validate we sent an event for > 0 products to check if the bundle event is even necessary if product_removed_event_fired: try: bundle_id = BasketAttribute.objects.get( basket=self, attribute_type__name=BUNDLE).value_text program = get_program(bundle_id, self.site.siteconfiguration) bundle_properties = { 'bundle_id': bundle_id, 'title': program.get('title'), 'total_price': self.total_excl_tax, 'quantity': self.lines.count(), } if program.get( 'type_attrs', {}).get('slug') and program.get('marketing_slug'): bundle_properties['marketing_slug'] = ( program['type_attrs']['slug'] + '/' + program.get('marketing_slug')) track_segment_event(self.site, self.owner, 'edx.bi.ecommerce.basket.bundle_removed', bundle_properties) except BasketAttribute.DoesNotExist: # Nothing to do here. It's not a bundle ¯\_(ツ)_/¯ pass # Call flush after we fetch all_lines() which is cleared during flush() super(Basket, self).flush() # pylint: disable=bad-super-call
def add_product(self, product, quantity=1, options=None): """ Add the indicated product to basket. Performs AbstractBasket add_product method and fires Google Analytics 'Product Added' event. """ line, created = super(Basket, self).add_product(product, quantity, options) # pylint: disable=bad-super-call cached_response = DEFAULT_REQUEST_CACHE.get_cached_response(TEMPORARY_BASKET_CACHE_KEY) if cached_response.is_found: # Do not track anything. This is a temporary basket calculation. return line, created # Do not fire events for free items. The volume we see for edX.org leads to a dramatic increase in CPU # usage. Given that orders for free items are ignored, there is no need for these events. if line.stockrecord.price_excl_tax > 0: properties = translate_basket_line_for_segment(line) properties['cart_id'] = self.id track_segment_event(self.site, self.owner, 'Product Added', properties) return line, created
def get(self, request): """ Calculate basket totals given a list of sku's Create a temporary basket add the sku's and apply an optional voucher code. Then calculate the total price less discounts. If a voucher code is not provided apply a voucher in the Enterprise entitlements available to the user. Query Params: sku (string): A list of sku(s) to calculate code (string): Optional voucher code to apply to the basket. username (string): Optional username of a user for which to calculate the basket. Returns: JSON: { 'total_incl_tax_excl_discounts': basket.total_incl_tax_excl_discounts, 'total_incl_tax': basket.total_incl_tax, 'currency': basket.currency } """ DEFAULT_REQUEST_CACHE.set(TEMPORARY_BASKET_CACHE_KEY, True) partner = get_partner_for_site(request) skus = request.GET.getlist('sku') if not skus: return HttpResponseBadRequest(_('No SKUs provided.')) skus.sort() code = request.GET.get('code', None) try: voucher = Voucher.objects.get(code=code) if code else None except Voucher.DoesNotExist: voucher = None products = Product.objects.filter(stockrecords__partner=partner, stockrecords__partner_sku__in=skus) if not products: return HttpResponseBadRequest( _('Products with SKU(s) [{skus}] do not exist.').format( skus=', '.join(skus))) # If there is only one product apply an Enterprise entitlement voucher if not voucher and len(products) == 1: voucher = get_entitlement_voucher(request, products[0]) basket_owner = request.user requested_username = request.GET.get('username', default='') is_anonymous = request.GET.get('is_anonymous', 'false').lower() == 'true' use_default_basket = is_anonymous # validate query parameters if requested_username and is_anonymous: return HttpResponseBadRequest( _('Provide username or is_anonymous query param, but not both') ) elif not requested_username and not is_anonymous: logger.warning( "Request to Basket Calculate must supply either username or is_anonymous query" " param. Requesting user=%s. Future versions of this API will treat this " "WARNING as an ERROR and raise an exception.", basket_owner.username) requested_username = request.user.username # If a username is passed in, validate that the user has staff access or is the same user. if requested_username: if basket_owner.username.lower() == requested_username.lower(): pass elif basket_owner.is_staff: try: basket_owner = User.objects.get( username=requested_username) except User.DoesNotExist: # This case represents a user who is logged in to marketing, but # doesn't yet have an account in ecommerce. These users have # never purchased before. use_default_basket = True else: return HttpResponseForbidden('Unauthorized user credentials') if basket_owner.username == self.MARKETING_USER and not use_default_basket: # For legacy requests that predate is_anonymous parameter, we will calculate # an anonymous basket if the calculated user is the marketing user. # TODO: LEARNER-5057: Remove this special case for the marketing user # once logs show no more requests with no parameters (see above). use_default_basket = True if use_default_basket: basket_owner = None cache_key = None if use_default_basket: # For an anonymous user we can directly get the cached price, because # there can't be any enrollments or entitlements. cache_key = get_cache_key(site_comain=request.site, resource_name='calculate', skus=skus) cached_response = TieredCache.get_cached_response(cache_key) if cached_response.is_found: return Response(cached_response.value) response = self._calculate_temporary_basket_atomic( basket_owner, request, products, voucher, skus, code) if response and use_default_basket: TieredCache.set_all_tiers( cache_key, response, settings.ANONYMOUS_BASKET_CALCULATE_CACHE_TIMEOUT) return Response(response)
def get(self, request): # pylint: disable=too-many-statements """ Calculate basket totals given a list of sku's Create a temporary basket add the sku's and apply an optional voucher code. Then calculate the total price less discounts. If a voucher code is not provided apply a voucher in the Enterprise entitlements available to the user. Query Params: sku (string): A list of sku(s) to calculate code (string): Optional voucher code to apply to the basket. username (string): Optional username of a user for which to calculate the basket. Returns: JSON: { 'total_incl_tax_excl_discounts': basket.total_incl_tax_excl_discounts, 'total_incl_tax': basket.total_incl_tax, 'currency': basket.currency } Side effects: If the basket owner does not have an LMS user id, tries to find it. If found, adds the id to the user and saves the user. If the id cannot be found, writes custom metrics to record this fact. """ DEFAULT_REQUEST_CACHE.set(TEMPORARY_BASKET_CACHE_KEY, True) partner = get_partner_for_site(request) skus = request.GET.getlist('sku') if not skus: return HttpResponseBadRequest(_('No SKUs provided.')) skus.sort() code = request.GET.get('code', None) try: voucher = Voucher.objects.get(code=code) if code else None except Voucher.DoesNotExist: voucher = None products = Product.objects.filter(stockrecords__partner=partner, stockrecords__partner_sku__in=skus) if not products: return HttpResponseBadRequest( _('Products with SKU(s) [{skus}] do not exist.').format( skus=', '.join(skus))) basket_owner = request.user requested_username = request.GET.get('username', default='') is_anonymous = request.GET.get('is_anonymous', 'false').lower() == 'true' use_default_basket = is_anonymous use_default_basket_case = 0 # validate query parameters if requested_username and is_anonymous: return HttpResponseBadRequest( _('Provide username or is_anonymous query param, but not both') ) if not requested_username and not is_anonymous: logger.warning( "Request to Basket Calculate must supply either username or is_anonymous query" " param. Requesting user=%s. Future versions of this API will treat this " "WARNING as an ERROR and raise an exception.", basket_owner.username) requested_username = request.user.username # If a username is passed in, validate that the user has staff access or is the same user. if requested_username: if basket_owner.username.lower() == requested_username.lower(): pass elif basket_owner.is_staff: try: basket_owner = User.objects.get( username=requested_username) except User.DoesNotExist: # This case represents a user who is logged in to marketing, but # doesn't yet have an account in ecommerce. These users have # never purchased before. use_default_basket = True use_default_basket_case = 1 else: return HttpResponseForbidden('Unauthorized user credentials') if basket_owner.username == self.MARKETING_USER and not use_default_basket: # For legacy requests that predate is_anonymous parameter, we will calculate # an anonymous basket if the calculated user is the marketing user. # TODO: LEARNER-5057: Remove this special case for the marketing user # once logs show no more requests with no parameters (see above). use_default_basket = True use_default_basket_case = 2 if use_default_basket: basket_owner = None # If we have a basket owner, ensure they have an LMS user id try: if basket_owner: called_from = u'calculation of basket total' basket_owner.add_lms_user_id( 'ecommerce_missing_lms_user_id_calculate_basket_total', called_from) except MissingLmsUserIdException: return self._report_bad_request( api_exceptions.LMS_USER_ID_NOT_FOUND_DEVELOPER_MESSAGE.format( user_id=basket_owner.id), api_exceptions.LMS_USER_ID_NOT_FOUND_USER_MESSAGE) cache_key = None if use_default_basket: # For an anonymous user we can directly get the cached price, because # there can't be any enrollments or entitlements. cache_key = get_cache_key(site_comain=request.site, resource_name='calculate', skus=skus) cached_response = TieredCache.get_cached_response(cache_key) logger.info( 'bundle debugging 2: request [%s] referrer [%s] url [%s] Cache key [%s] response [%s]' 'skus [%s] case [%s] basket_owner [%s]', str(request), str(request.META.get('HTTP_REFERER')), str(request._request), str(cache_key), # pylint: disable=protected-access str(cached_response), str(skus), str(use_default_basket_case), str(basket_owner)) if cached_response.is_found: return Response(cached_response.value) response = self._calculate_temporary_basket_atomic( basket_owner, request, products, voucher, skus, code) if response and use_default_basket: logger.info( 'bundle debugging 3: request [%s] referrer [%s] url [%s] Cache key [%s] response [%s]' 'skus [%s] case [%s] basket_owner [%s]', str(request), str(request.META.get('HTTP_REFERER')), str(request._request), str(cache_key), # pylint: disable=protected-access str(response), str(skus), str(use_default_basket_case), str(basket_owner)) TieredCache.set_all_tiers( cache_key, response, settings.ANONYMOUS_BASKET_CALCULATE_CACHE_TIMEOUT) return Response(response)