def process_request(self, request): """ Stores whether or not FORCE_DJANGO_CACHE_MISS_KEY was supplied in the request. Also, clears the request cache. """ RequestCache.clear() TieredCache._get_and_set_force_cache_miss(request) # pylint: disable=protected-access
def test_delete_missing_key(self): try: RequestCache.delete(TEST_KEY) except KeyError: self.fail( 'Deleting a missing key from the request cache should not cause an error.' )
def test_process_request(self): self.middleware.process_request(self.request) self.assertTrue(RequestCache.get_cached_response(TEST_KEY).is_miss) self.assertFalse( RequestCache.get_cached_response( SHOULD_FORCE_CACHE_MISS_KEY).value)
def test_delete(self, mock_cache_delete): TieredCache.set_all_tiers(TEST_KEY, EXPECTED_VALUE) TieredCache.set_all_tiers(TEST_KEY_2, EXPECTED_VALUE) TieredCache.delete_all_tiers(TEST_KEY) self.assertTrue(RequestCache.get_cached_response(TEST_KEY).is_miss) self.assertEqual( RequestCache.get_cached_response(TEST_KEY_2).value, EXPECTED_VALUE) mock_cache_delete.assert_called_with(TEST_KEY)
def test_process_request_force_django_cache_miss(self): request = RequestFactory().get( '/?{}=tRuE'.format(FORCE_CACHE_MISS_PARAM)) self.middleware.process_request(request) self.assertTrue(RequestCache.get_cached_response(TEST_KEY).is_miss) self.assertTrue( RequestCache.get_cached_response( SHOULD_FORCE_CACHE_MISS_KEY).value)
def test_get_cached_response_force_django_cache_miss(self, mock_cache_get): RequestCache.set(SHOULD_FORCE_CACHE_MISS_KEY, True) mock_cache_get.return_value = EXPECTED_VALUE cached_response = TieredCache.get_cached_response(TEST_KEY) self.assertTrue(cached_response.is_miss) cached_response = RequestCache.get_cached_response(TEST_KEY) self.assertTrue( cached_response.is_miss, 'Forced Django cache miss should not cache value in request cache.' )
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() RequestCache.set(TEMPORARY_BASKET_CACHE_KEY, True) with mock.patch.object(Client, 'track') as mock_track: basket.flush() mock_track.assert_not_called()
def test_set_all_tiers(self, mock_cache_set): mock_cache_set.return_value = EXPECTED_VALUE TieredCache.set_all_tiers(TEST_KEY, EXPECTED_VALUE, TEST_DJANGO_TIMEOUT_CACHE) mock_cache_set.assert_called_with(TEST_KEY, EXPECTED_VALUE, TEST_DJANGO_TIMEOUT_CACHE) self.assertEqual( RequestCache.get_cached_response(TEST_KEY).value, EXPECTED_VALUE)
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() basket = create_basket(empty=True) seat = course.create_or_update_seat('verified', True, 100, self.partner) RequestCache.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 test_delete(self): RequestCache.set(TEST_KEY, EXPECTED_VALUE) RequestCache.set(TEST_KEY_2, EXPECTED_VALUE) RequestCache.delete(TEST_KEY) cached_response = RequestCache.get_cached_response(TEST_KEY) self.assertTrue(cached_response.is_miss) cached_response = RequestCache.get_cached_response(TEST_KEY_2) self.assertTrue(cached_response.is_hit) self.assertEqual(cached_response.value, EXPECTED_VALUE)
def test_get_cached_response_django_cache_hit(self, mock_cache_get): mock_cache_get.return_value = EXPECTED_VALUE cached_response = TieredCache.get_cached_response(TEST_KEY) self.assertTrue(cached_response.is_hit) self.assertEqual(cached_response.value, EXPECTED_VALUE) cached_response = RequestCache.get_cached_response(TEST_KEY) self.assertTrue( cached_response.is_hit, 'Django cache hit should cache value in request cache.')
def flush(self): """Remove all products in basket and fire Segment 'Product Removed' Analytic event for each""" cached_response = RequestCache.get_cached_response(TEMPORARY_BASKET_CACHE_KEY) if cached_response.is_hit: # 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 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 = RequestCache.get_cached_response(TEMPORARY_BASKET_CACHE_KEY) if cached_response.is_hit: # 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 test_get_cached_response_request_cache_hit(self): RequestCache.set(TEST_KEY, EXPECTED_VALUE) cached_response = TieredCache.get_cached_response(TEST_KEY) self.assertTrue(cached_response.is_hit) self.assertEqual(cached_response.value, EXPECTED_VALUE)
def test_clear(self): RequestCache.set(TEST_KEY, EXPECTED_VALUE) RequestCache.clear() cached_response = RequestCache.get_cached_response(TEST_KEY) self.assertTrue(cached_response.is_miss)
def test_get_cached_response_miss(self): cached_response = RequestCache.get_cached_response(TEST_KEY) self.assertTrue(cached_response.is_miss)
def test_get_cached_response_hit_with_cached_none(self): RequestCache.set(TEST_KEY, None) cached_response = RequestCache.get_cached_response(TEST_KEY) self.assertFalse(cached_response.is_miss) self.assertEqual(cached_response.value, None)
def test_get_cached_response_hit(self): RequestCache.set(TEST_KEY, EXPECTED_VALUE) cached_response = RequestCache.get_cached_response(TEST_KEY) self.assertFalse(cached_response.is_miss) self.assertEqual(cached_response.value, EXPECTED_VALUE)
def setUp(self): RequestCache.clear()
def process_response(self, request, response): # pylint: disable=unused-argument """ Clear the request cache after processing a response. """ RequestCache.clear() return response
def _dirty_request_cache(): """ Dirties the request cache to ensure it is cleared later. """ RequestCache.set(TEST_KEY, EXPECTED_VALUE)
def test_process_exception(self): response = self.middleware.process_exception(self.request, EXPECTED_VALUE) self.assertEqual(response, None) self.assertTrue(RequestCache.get_cached_response(TEST_KEY).is_miss)
def process_exception(self, request, exception): # pylint: disable=unused-argument """ Clear the request cache after a failed request. """ RequestCache.clear() return None
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 } """ RequestCache.set(TEMPORARY_BASKET_CACHE_KEY, True) # TODO: LEARNER 5463 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_hit: return Response(cached_response.value) if waffle.flag_is_active( request, "disable_calculate_temporary_basket_atomic_transaction"): response = self._calculate_temporary_basket( basket_owner, request, products, voucher, skus, code) else: 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 test_clear_all_tiers(self, mock_cache_clear): TieredCache.set_all_tiers(TEST_KEY, EXPECTED_VALUE) TieredCache.clear_all_tiers() self.assertTrue(RequestCache.get_cached_response(TEST_KEY).is_miss) mock_cache_clear.assert_called_once_with()