def test_get_course_run_info_from_catalog(self, course_run): """ Check to see if course info gets cached """ self.mock_access_token_response() if course_run: course = CourseFactory() product = course.create_or_update_seat( 'verified', None, 100, self.site.siteconfiguration.partner) key = CourseKey.from_string(product.attr.course_key) self.mock_course_run_detail_endpoint( course, discovery_api_url=self.site_configuration.discovery_api_url) else: product = create_or_update_course_entitlement( 'verified', 100, self.partner, 'foo-bar', 'Foo Bar Entitlement') key = product.attr.UUID self.mock_course_detail_endpoint( product, discovery_api_url=self.site_configuration.discovery_api_url) cache_key = 'courses_api_detail_{}{}'.format( key, self.site.siteconfiguration.partner.short_code) cache_key = hashlib.md5(cache_key).hexdigest() course_cached_response = TieredCache.get_cached_response(cache_key) self.assertTrue(course_cached_response.is_miss) response = get_course_info_from_catalog(self.request.site, product) if course_run: self.assertEqual(response['title'], course.name) else: self.assertEqual(response['title'], product.title) course_cached_response = TieredCache.get_cached_response(cache_key) self.assertEqual(course_cached_response.value, response)
def test_get_course_catalogs_for_single_catalog_with_id(self): """ Verify that method "get_course_catalogs" returns proper response for a single catalog by its id. """ self.mock_access_token_response() self.mock_catalog_detail_endpoint( self.site_configuration.discovery_api_url) catalog_id = 1 cache_key = '{}.catalog.api.data.{}'.format(self.request.site.domain, catalog_id) cache_key = hashlib.md5(cache_key).hexdigest() course_catalogs_cached_response = TieredCache.get_cached_response( cache_key) self.assertTrue(course_catalogs_cached_response.is_miss) response = get_course_catalogs(self.request.site, catalog_id) self.assertEqual(response['name'], 'All Courses') course_cached_response = TieredCache.get_cached_response(cache_key) self.assertEqual(course_cached_response.value, response) # Verify the API was actually hit (not the cache) self._assert_num_requests(2)
def get_credit_providers(self): """ Retrieve all credit providers from LMS. Results will be sorted alphabetically by display name. """ key = 'credit_providers' credit_providers_cache_response = TieredCache.get_cached_response(key) if credit_providers_cache_response.is_hit: return credit_providers_cache_response.value try: credit_api = EdxRestApiClient( get_lms_url('/api/credit/v1/'), oauth_access_token=self.request.user.access_token) credit_providers = credit_api.providers.get() credit_providers.sort( key=lambda provider: provider['display_name']) # Update the cache TieredCache.set_all_tiers(key, credit_providers, settings.CREDIT_PROVIDER_CACHE_TIMEOUT) except (SlumberBaseException, Timeout): logger.exception('Failed to retrieve credit providers!') credit_providers = [] return credit_providers
def catalog_contains_product(self, product): """ Retrieve the results from using the catalog contains endpoint for catalog service for the catalog id contained in field "course_catalog". """ request = get_current_request() partner_code = request.site.siteconfiguration.partner.short_code cache_key = get_cache_key(site_domain=request.site.domain, partner_code=partner_code, resource='catalogs.contains', course_id=product.course_id, catalog_id=self.course_catalog) cached_response = TieredCache.get_cached_response(cache_key) if cached_response.is_hit: return cached_response.value discovery_api_client = request.site.siteconfiguration.discovery_api_client try: # GET: /api/v1/catalogs/{catalog_id}/contains?course_run_id={course_run_ids} response = discovery_api_client.catalogs( self.course_catalog).contains.get( course_run_id=product.course_id) TieredCache.set_all_tiers(cache_key, response, settings.COURSES_API_CACHE_TIMEOUT) return response except (ConnectionError, SlumberBaseException, Timeout): raise Exception( 'Unable to connect to Discovery Service for catalog contains endpoint.' )
def fetch_journal_bundle(site, journal_bundle_uuid): """ Retrieve journal bundle for given uuid. Retrieve it from cache if present, otherwise send GET request to journal bundle discovery api and store in cache. Args: site (Site): site for current request journal_bundle_uuid (str): uuid for desired journal bundle Returns: (dict): contains dict of journal_bundle attributes Raises: ConnectionError: raised if ecommerce is unable to connect to enterprise api server. SlumberBaseException: raised if API response contains http error status like 4xx, 5xx etc... Timeout: request is raised if API is taking too long to respond """ api_resource = 'journal_bundle' cache_key = get_cache_key(site_domain=site.domain, resource=api_resource, journal_bundle_uuid=journal_bundle_uuid) journal_bundle_cached_response = TieredCache.get_cached_response(cache_key) if journal_bundle_cached_response.is_hit: return journal_bundle_cached_response.value client = site.siteconfiguration.journal_discovery_api_client journal_bundle = client.journal_bundles(journal_bundle_uuid).get() TieredCache.set_all_tiers(cache_key, journal_bundle, JOURNAL_BUNDLE_CACHE_TIMEOUT) return journal_bundle
def access_token(self): """ Returns an access token for this site's service user. The access token is retrieved using the current site's OAuth credentials and the client credentials grant. The token is cached for the lifetime of the token, as specified by the OAuth provider's response. The token type is JWT. Returns: str: JWT access token """ key = 'siteconfiguration_access_token_{}'.format(self.id) access_token_cached_response = TieredCache.get_cached_response(key) if access_token_cached_response.is_hit: return access_token_cached_response.value url = '{root}/access_token'.format(root=self.oauth2_provider_url) access_token, expiration_datetime = EdxRestApiClient.get_oauth_access_token( url, self.oauth_settings['SOCIAL_AUTH_EDX_OIDC_KEY'], # pylint: disable=unsubscriptable-object self.oauth_settings['SOCIAL_AUTH_EDX_OIDC_SECRET'], # pylint: disable=unsubscriptable-object token_type='jwt') expires = (expiration_datetime - datetime.datetime.utcnow()).seconds TieredCache.set_all_tiers(key, access_token, expires) return access_token
def get_program(self, uuid): """ Retrieve the details for a single program. Args: uuid (str|uuid): Program UUID. Returns: dict """ program_uuid = str(uuid) cache_key = '{site_domain}-program-{uuid}'.format( site_domain=self.site_domain, uuid=program_uuid) program_cached_response = TieredCache.get_cached_response(cache_key) if program_cached_response.is_hit: # pragma: no cover logger.debug('Program [%s] was found in the cache.', program_uuid) return program_cached_response.value logging.info('Retrieving details of of program [%s]...', program_uuid) program = self.client.programs(program_uuid).get() TieredCache.set_all_tiers(cache_key, program, self.cache_ttl) logging.info('Program [%s] was successfully retrieved and cached.', program_uuid) return program
def is_entitlement_expired(entitlement_uuid, site): """ Checks to see if a given entitlement is expired. Args: entitlement_uuid: UUID site: (Site) Returns: bool: True if the entitlement is expired """ entitlement_api_client = EdxRestApiClient(get_lms_entitlement_api_url(), jwt=site.siteconfiguration.access_token) partner_short_code = site.siteconfiguration.partner.short_code key = 'course_entitlement_detail_{}{}'.format(entitlement_uuid, partner_short_code) entitlement_cached_response = TieredCache.get_cached_response(key) if entitlement_cached_response.is_hit: entitlement = entitlement_cached_response.value else: logger.debug('Trying to get entitlement {%s}', entitlement_uuid) entitlement = entitlement_api_client.entitlements(entitlement_uuid).get() TieredCache.set_all_tiers(key, entitlement, settings.COURSES_API_CACHE_TIMEOUT) expired = entitlement.get('expired_at') logger.debug('Entitlement {%s} expired = {%s}', entitlement_uuid, expired) return expired
def test_invalidate_processor_cache(self): """ Verify the payment processor cache is invalidated when payment processor switches are toggled. """ user = self.create_user() self.client.login(username=user.username, password=self.password) # Make a call that triggers cache creation response = self.client.get(reverse('api:v2:payment:list_processors')) self.assertEqual(response.status_code, 200) self.assertTrue( TieredCache.get_cached_response( PAYMENT_PROCESSOR_CACHE_KEY).is_hit) # Toggle a switch to trigger cache deletion Switch.objects.get_or_create( name=settings.PAYMENT_PROCESSOR_SWITCH_PREFIX + 'dummy') self.assertTrue( TieredCache.get_cached_response( PAYMENT_PROCESSOR_CACHE_KEY).is_miss)
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 _assert_get_course_catalogs(self, catalog_name_list): """ Helper method to validate the response from the method "get_course_catalogs". """ cache_key = '{}.catalog.api.data'.format(self.request.site.domain) cache_key = hashlib.md5(cache_key).hexdigest() course_catalogs_cached_response = TieredCache.get_cached_response( cache_key) self.assertTrue(course_catalogs_cached_response.is_miss) response = get_course_catalogs(self.request.site) self.assertEqual(len(response), len(catalog_name_list)) for catalog_index, catalog in enumerate(response): self.assertEqual(catalog['name'], catalog_name_list[catalog_index]) course_cached_response = TieredCache.get_cached_response(cache_key) self.assertEqual(course_cached_response.value, response)
def fetch_course_catalog(site, catalog_id): """ Fetch course catalog for the given catalog id. This method will fetch catalog for given catalog id, if there is no catalog with the given catalog id, method will return `None`. Arguments: site (Site): Instance of the current site. catalog_id (int): An integer specifying the primary key value of catalog to fetch. Example: >>> fetch_course_catalog(site, catalog_id=1) { "id": 1, "name": "All Courses", "query": "*:*", ... } Returns: (dict): A dictionary containing key/value pairs corresponding to catalog attribute/values. Raises: ConnectionError: requests exception "ConnectionError", raised if if ecommerce is unable to connect to enterprise api server. SlumberBaseException: base slumber exception "SlumberBaseException", raised if API response contains http error status like 4xx, 5xx etc. Timeout: requests exception "Timeout", raised if enterprise API is taking too long for returning a response. This exception is raised for both connection timeout and read timeout. """ api_resource = 'catalogs' cache_key = get_cache_key( site_domain=site.domain, resource=api_resource, catalog_id=catalog_id, ) cached_response = TieredCache.get_cached_response(cache_key) if cached_response.is_hit: return cached_response.value api = site.siteconfiguration.discovery_api_client endpoint = getattr(api, api_resource) try: response = endpoint(catalog_id).get() except HttpNotFoundError: logger.exception("Catalog '%s' not found.", catalog_id) raise TieredCache.set_all_tiers(cache_key, response, settings.COURSES_API_CACHE_TIMEOUT) return response
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 _assert_fetch_enterprise_learner_data(self): """ Helper method to validate the response from the method "fetch_enterprise_learner_data". """ cache_key = get_cache_key(site_domain=self.request.site.domain, partner_code=self.request.site. siteconfiguration.partner.short_code, resource='enterprise-learner', username=self.learner.username) enterprise_learner_cached_response = TieredCache.get_cached_response( cache_key) self.assertTrue(enterprise_learner_cached_response.is_miss) response = enterprise_api.fetch_enterprise_learner_data( self.request.site, self.learner) self.assertEqual(len(response['results']), 1) course_cached_response = TieredCache.get_cached_response(cache_key) self.assertEqual(course_cached_response.value, response)
def fetch_enterprise_learner_entitlements(site, learner_id): """ Fetch enterprise learner entitlements along-with data sharing consent requirement. Arguments: site (Site): site instance. learner_id (int): Primary key identifier for the enterprise learner. Example: >>> from django.contrib.sites.shortcuts import get_current_site >>> site = get_current_site() >>> fetch_enterprise_learner_entitlements(site, 1) [ { "requires_consent": False, "entitlement_id": 1 }, ] Returns: (list): Containing dicts of the following structure { "requires_consent": True, "entitlement_id": 1 } Raises: ConnectionError: requests exception "ConnectionError", raised if if ecommerce is unable to connect to enterprise api server. SlumberBaseException: base slumber exception "SlumberBaseException", raised if API response contains http error status like 4xx, 5xx etc. Timeout: requests exception "Timeout", raised if enterprise API is taking too long for returning a response. This exception is raised for both connection timeout and read timeout. """ resource_url = 'enterprise-learner/{learner_id}/entitlements'.format( learner_id=learner_id) cache_key = get_cache_key( site_domain=site.domain, partner_code=site.siteconfiguration.partner.short_code, resource=resource_url, learner_id=learner_id) entitlements_cached_response = TieredCache.get_cached_response(cache_key) if entitlements_cached_response.is_hit: return entitlements_cached_response.value api = site.siteconfiguration.enterprise_api_client entitlements = getattr(api, resource_url).get() TieredCache.set_all_tiers(cache_key, entitlements, settings.ENTERPRISE_API_CACHE_TIMEOUT) return entitlements
def test_cached_course(self): """ Verify that the course info is cached. """ seat = self.create_seat(self.course, 50) basket = self.create_basket_and_add_product(seat) self.mock_access_token_response() self.assertEqual(basket.lines.count(), 1) self.mock_course_run_detail_endpoint( self.course, discovery_api_url=self.site_configuration.discovery_api_url) cache_key = 'courses_api_detail_{}{}'.format( self.course.id, self.site.siteconfiguration.partner.short_code) cache_key = hashlib.md5(cache_key).hexdigest() course_before_cached_response = TieredCache.get_cached_response( cache_key) self.assertTrue(course_before_cached_response.is_miss) response = self.client.get(self.path) self.assertEqual(response.status_code, 200) course_after_cached_response = TieredCache.get_cached_response( cache_key) self.assertEqual(course_after_cached_response.value['title'], self.course.name)
def catalog_contains_course_runs(site, course_run_ids, enterprise_customer_uuid, enterprise_customer_catalog_uuid=None): """ Determine if course runs are associated with the EnterpriseCustomer. """ query_params = {'course_run_ids': course_run_ids} api_resource_name = 'enterprise-customer' api_resource_id = enterprise_customer_uuid if enterprise_customer_catalog_uuid: api_resource_name = 'enterprise_catalogs' api_resource_id = enterprise_customer_catalog_uuid cache_key = get_cache_key( site_domain=site.domain, resource='{resource}-{resource_id}-contains_content_items'.format( resource=api_resource_name, resource_id=api_resource_id, ), query_params=urlencode(query_params, True)) contains_content_cached_response = TieredCache.get_cached_response( cache_key) if contains_content_cached_response.is_hit: return contains_content_cached_response.value api = site.siteconfiguration.enterprise_api_client endpoint = getattr(api, api_resource_name)(api_resource_id) try: contains_content = endpoint.contains_content_items.get( **query_params)['contains_content_items'] TieredCache.set_all_tiers(cache_key, contains_content, settings.ENTERPRISE_API_CACHE_TIMEOUT) except (ConnectionError, KeyError, SlumberHttpBaseException, Timeout): logger.exception( 'Failed to check if course_runs [%s] exist in ' 'EnterpriseCustomerCatalog [%s]' 'for EnterpriseCustomer [%s].', course_run_ids, enterprise_customer_catalog_uuid, enterprise_customer_uuid, ) contains_content = False return contains_content
def _get_lms_resource_for_user(self, basket, resource_name, endpoint): cache_key = get_cache_key( site_domain=basket.site.domain, resource=resource_name, username=basket.owner.username, ) data_list_cached_response = TieredCache.get_cached_response(cache_key) if data_list_cached_response.is_hit: return data_list_cached_response.value user = basket.owner.username try: data_list = endpoint.get(user=user) or [] TieredCache.set_all_tiers(cache_key, data_list, settings.LMS_API_CACHE_TIMEOUT) except (ConnectionError, SlumberBaseException, Timeout) as exc: logger.error('Failed to retrieve %s : %s', resource_name, str(exc)) data_list = [] return data_list
def is_verified(self, site): """ Check if a user has verified his/her identity. Calls the LMS verification status API endpoint and returns the verification status information. The status information is stored in cache, if the user is verified, until the verification expires. Args: site (Site): The site object from which the LMS account API endpoint is created. Returns: True if the user is verified, false otherwise. """ try: cache_key = 'verification_status_{username}'.format( username=self.username) cache_key = hashlib.md5(cache_key).hexdigest() verification_cached_response = TieredCache.get_cached_response( cache_key) if verification_cached_response.is_hit: return verification_cached_response.value api = EdxRestApiClient( site.siteconfiguration.build_lms_url('api/user/v1/'), oauth_access_token=self.access_token) response = api.accounts(self.username).verification_status().get() verification = response.get('is_verified', False) if verification: cache_timeout = int( (parse(response.get('expiration_datetime')) - now()).total_seconds()) TieredCache.set_all_tiers(cache_key, verification, cache_timeout) return verification except HttpNotFoundError: log.debug('No verification data found for [%s]', self.username) return False except (ConnectionError, SlumberBaseException, Timeout): msg = 'Failed to retrieve verification status details for [{username}]'.format( username=self.username) log.warning(msg) return False
def get_course_catalogs(site, resource_id=None): """ Get details related to course catalogs from Discovery Service. Arguments: site (Site): Site object containing Site Configuration data resource_id (int or str): Identifies a specific resource to be retrieved Returns: dict: Course catalogs received from Discovery API Raises: ConnectionError: requests exception "ConnectionError" SlumberBaseException: slumber exception "SlumberBaseException" Timeout: requests exception "Timeout" """ resource = 'catalogs' base_cache_key = '{}.catalog.api.data'.format(site.domain) cache_key = '{}.{}'.format(base_cache_key, resource_id) if resource_id else base_cache_key cache_key = hashlib.md5(cache_key).hexdigest() cached_response = TieredCache.get_cached_response(cache_key) if cached_response.is_hit: return cached_response.value api = site.siteconfiguration.discovery_api_client endpoint = getattr(api, resource) response = endpoint(resource_id).get() if resource_id: results = response else: results = deprecated_traverse_pagination(response, endpoint) TieredCache.set_all_tiers(cache_key, results, settings.COURSES_API_CACHE_TIMEOUT) return results
def is_course_in_enterprise_catalog(site, course_id, enterprise_catalog_id): """ Verify that the provided course id exists in the site base list of course run keys from the provided enterprise course catalog. Arguments: course_id (str): The course ID. site: (django.contrib.sites.Site) site instance enterprise_catalog_id (Int): Course catalog id of enterprise Returns: Boolean """ partner_code = site.siteconfiguration.partner.short_code cache_key = get_cache_key(site_domain=site.domain, partner_code=partner_code, resource='catalogs.contains', course_id=course_id, catalog_id=enterprise_catalog_id) cached_response = TieredCache.get_cached_response(cache_key) if cached_response.is_hit: response = cached_response.value else: try: response = site.siteconfiguration.discovery_api_client.catalogs( enterprise_catalog_id).contains.get(course_run_id=course_id) TieredCache.set_all_tiers(cache_key, response, settings.COURSES_API_CACHE_TIMEOUT) except (ConnectionError, SlumberBaseException, Timeout): logger.exception( 'Unable to connect to Discovery Service for catalog contains endpoint.' ) return False try: return response['courses'][course_id] except KeyError: return False
def get_course_info_from_catalog(site, product): """ Get course or course_run information from Discovery Service and cache """ if product.is_course_entitlement_product: key = product.attr.UUID else: key = CourseKey.from_string(product.attr.course_key) api = site.siteconfiguration.discovery_api_client partner_short_code = site.siteconfiguration.partner.short_code cache_key = 'courses_api_detail_{}{}'.format(key, partner_short_code) cache_key = hashlib.md5(cache_key).hexdigest() course_cached_response = TieredCache.get_cached_response(cache_key) if course_cached_response.is_hit: return course_cached_response.value if product.is_course_entitlement_product: course = api.courses(key).get() else: course = api.course_runs(key).get(partner=partner_short_code) TieredCache.set_all_tiers(cache_key, course, settings.COURSES_API_CACHE_TIMEOUT) return course
def get_cached_voucher(code): """ Returns a voucher from cache if one is stored to cache, if not the voucher is retrieved from database and stored to cache. Arguments: code (str): The code of a coupon voucher. Returns: voucher (Voucher): The Voucher for the passed code. Raises: Voucher.DoesNotExist: When no vouchers with provided code exist. """ voucher_code = 'voucher_{code}'.format(code=code) cache_key = hashlib.md5(voucher_code).hexdigest() voucher_cached_response = TieredCache.get_cached_response(cache_key) if voucher_cached_response.is_hit: return voucher_cached_response.value voucher = Voucher.objects.get(code=code) TieredCache.set_all_tiers(cache_key, voucher, settings.VOUCHER_CACHE_TIMEOUT) return voucher
def _identify_uncached_product_identifiers(self, lines, domain, partner_code, query): """ Checks the cache to see if each line is in the catalog range specified by the given query and tracks identifiers for which discovery service data is still needed. """ course_run_ids = [] course_uuids = [] applicable_lines = lines for line in applicable_lines: if line.product.is_seat_product: product_id = line.product.course.id else: # All lines passed to this method should either have a seat or an entitlement product product_id = line.product.attr.UUID cache_key = get_cache_key(site_domain=domain, partner_code=partner_code, resource='catalog_query.contains', course_id=product_id, query=query) cached_response = TieredCache.get_cached_response(cache_key) if cached_response.is_miss: if line.product.is_seat_product: course_run_ids.append({ 'id': product_id, 'cache_key': cache_key, 'line': line }) else: course_uuids.append({ 'id': product_id, 'cache_key': cache_key, 'line': line }) elif cached_response.value is False: applicable_lines.remove(line) return course_run_ids, course_uuids, applicable_lines
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_get_cached_response_all_tier_miss(self): cached_response = TieredCache.get_cached_response(TEST_KEY) self.assertTrue(cached_response.is_miss)
def get_catalog_course_runs(site, query, limit=None, offset=None): """ Get course runs for a site on the basis of provided query from the Course Catalog API. This method will get all course runs by recursively retrieving API next urls in the API response if no limit is provided. Arguments: limit (int): Number of results per page offset (int): Page offset query (str): ElasticSearch Query site (Site): Site object containing Site Configuration data Example: >>> get_catalog_course_runs(site, query, limit=1) { "count": 1, "next": "None", "previous": "None", "results": [{ "key": "course-v1:edX+DemoX+Demo_Course", "title": edX Demonstration Course, "start": "2016-05-01T00:00:00Z", "image": { "src": "path/to/the/course/image" }, "enrollment_end": None }], } Returns: dict: Query search results for course runs received from Course Catalog API Raises: ConnectionError: requests exception "ConnectionError" SlumberBaseException: slumber exception "SlumberBaseException" Timeout: requests exception "Timeout" """ api_resource_name = 'course_runs' partner_code = site.siteconfiguration.partner.short_code cache_key = '{site_domain}_{partner_code}_{resource}_{query}_{limit}_{offset}'.format( site_domain=site.domain, partner_code=partner_code, resource=api_resource_name, query=query, limit=limit, offset=offset ) cache_key = hashlib.md5(cache_key).hexdigest() cached_response = TieredCache.get_cached_response(cache_key) if cached_response.is_hit: return cached_response.value api = site.siteconfiguration.discovery_api_client endpoint = getattr(api, api_resource_name) response = endpoint().get( partner=partner_code, q=query, limit=limit, offset=offset ) TieredCache.set_all_tiers(cache_key, response, settings.COURSES_API_CACHE_TIMEOUT) return response
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_fetch_journal_bundle(self): """ Test 'fetch_journal_bundle' properly calls API and uses cache """ # The first time it is called the journal discovery api should get hit # and store the journal bundle in the cache # The second time the api should not be called, the bundle should be retrieved from the cache self.mock_access_token_response() test_bundle = { "uuid": "4786e7be-2390-4332-a20e-e24895c38109", "title": "Transfiguration Bundle", "partner": "edX", "journals": [{ "uuid": "a3db3f6e-f290-4eae-beea-873034c5a967", "partner": "edx", "organization": "edX", "title": "Intermediate Transfiguration", "price": "120.00", "currency": "USD", "sku": "88482D8", "card_image_url": "http://localhost:18606/media/original_images/transfiguration.jpg", "short_description": "Turning things into different things!", "full_description": "", "access_length": 365, "status": "active", "slug": "intermediate-transfiguration-about-page" }], "courses": [{ "key": "HogwartsX+TR301", "uuid": "6d7c2805-ec9c-4961-8b0d-c8d608cc948e", "title": "Transfiguration 301", "course_runs": [{ "key": "course-v1:HogwartsX+TR301+TR301_2014", "uuid": "ddaa84ce-e99c-4e3d-a3ca-7d5b4978b43b", "title": "Transfiguration 301", "image": 'fake_image_url', "short_description": 'fake_description', "marketing_url": 'fake_marketing_url', "seats": [], "start": "2030-01-01T00:00:00Z", "end": "2040-01-01T00:00:00Z", "enrollment_start": "2020-01-01T00:00:00Z", "enrollment_end": "2040-01-01T00:00:00Z", "pacing_type": "instructor_paced", "type": "fake_course_type", "status": "published" }], "entitlements": [], "owners": [{ "uuid": "becfbab0-c78d-42f1-b44e-c92abb99011a", "key": "HogwartsX", "name": "" }], "image": "fake_image_url", "short_description": "fake_description" }], "applicable_seat_types": ["verified"] } journal_bundle_uuid = test_bundle['uuid'] test_url = urljoin(self.journal_discovery_url, 'journal_bundles/{}/'.format(journal_bundle_uuid)) responses.add(responses.GET, test_url, json=test_bundle, status=200) # First call, should hit journal discovery api and store in cache journal_bundle_response = fetch_journal_bundle( site=self.site, journal_bundle_uuid=journal_bundle_uuid) # The first call (response.calls[0]) is to get post the access token # The second call (response.calls[1]) is the 'fetch_journal_bundle' call self.assertEqual(len(responses.calls), 2, "Incorrect number of API calls") self.assertEqual(journal_bundle_response, test_bundle) # check that the journal bundle was stored in the cache cache_key = get_cache_key(site_domain=self.site.domain, resource='journal_bundle', journal_bundle_uuid=journal_bundle_uuid) journal_bundle_cached_response = TieredCache.get_cached_response( cache_key) self.assertTrue(journal_bundle_cached_response is not None) self.assertEqual(journal_bundle_cached_response.value, test_bundle) # Call 'fetch_journal_bundle' again, the api should not get hit again and response should be the same journal_bundle_response = fetch_journal_bundle( site=self.site, journal_bundle_uuid=journal_bundle_uuid) self.assertEqual(len(responses.calls), 2, "Should have hit cache, not called API") self.assertEqual(journal_bundle_response, test_bundle)
def fetch_enterprise_learner_data(site, user): """ Fetch information related to enterprise and its entitlements from the Enterprise Service. Example: fetch_enterprise_learner_data(site, user) Arguments: site: (Site) site instance user: (User) django auth user Returns: dict: { "enterprise_api_response_for_learner": { "count": 1, "num_pages": 1, "current_page": 1, "results": [ { "enterprise_customer": { "uuid": "cf246b88-d5f6-4908-a522-fc307e0b0c59", "name": "BigEnterprise", "catalog": 2, "active": true, "site": { "domain": "example.com", "name": "example.com" }, "enable_data_sharing_consent": true, "enforce_data_sharing_consent": "at_login", "branding_configuration": { "enterprise_customer": "cf246b88-d5f6-4908-a522-fc307e0b0c59", "logo": "https://open.edx.org/sites/all/themes/edx_open/logo.png" }, "enterprise_customer_entitlements": [ { "enterprise_customer": "cf246b88-d5f6-4908-a522-fc307e0b0c59", "entitlement_id": 69 } ] }, "user_id": 5, "user": { "username": "******", "first_name": "", "last_name": "", "email": "*****@*****.**", "is_staff": true, "is_active": true, "date_joined": "2016-09-01T19:18:26.026495Z" }, "data_sharing_consent_records": [ { "username": "******", "enterprise_customer_uuid": "cf246b88-d5f6-4908-a522-fc307e0b0c59", "exists": true, "consent_provided": true, "consent_required": false, "course_id": "course-v1:edX DemoX Demo_Course", } ] } ], "next": null, "start": 0, "previous": null } } Raises: ConnectionError: requests exception "ConnectionError", raised if if ecommerce is unable to connect to enterprise api server. SlumberBaseException: base slumber exception "SlumberBaseException", raised if API response contains http error status like 4xx, 5xx etc. Timeout: requests exception "Timeout", raised if enterprise API is taking too long for returning a response. This exception is raised for both connection timeout and read timeout. """ api_resource_name = 'enterprise-learner' partner_code = site.siteconfiguration.partner.short_code cache_key = get_cache_key(site_domain=site.domain, partner_code=partner_code, resource=api_resource_name, username=user.username) cached_response = TieredCache.get_cached_response(cache_key) if cached_response.is_hit: return cached_response.value api = site.siteconfiguration.enterprise_api_client endpoint = getattr(api, api_resource_name) querystring = {'username': user.username} response = endpoint().get(**querystring) TieredCache.set_all_tiers(cache_key, response, settings.ENTERPRISE_API_CACHE_TIMEOUT) return response