예제 #1
0
    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()
예제 #2
0
 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)
예제 #5
0
    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)
예제 #6
0
    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
예제 #7
0
 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)
예제 #8
0
    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
예제 #9
0
    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
예제 #10
0
    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)
예제 #11
0
    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)