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.encode('utf-8')).hexdigest() verification_cached_response = TieredCache.get_cached_response(cache_key) if verification_cached_response.is_found: return verification_cached_response.value api = site.siteconfiguration.user_api_client 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 (ReqConnectionError, SlumberBaseException, Timeout): msg = 'Failed to retrieve verification status details for [{username}]'.format(username=self.username) log.warning(msg) return False
def consent_needed_for_course(request, user, course_id, enrollment_exists=False): """ Wrap the enterprise app check to determine if the user needs to grant data sharing permissions before accessing a course. """ consent_cache_key = get_data_consent_share_cache_key(user.id, course_id) data_sharing_consent_needed_cache = TieredCache.get_cached_response(consent_cache_key) if data_sharing_consent_needed_cache.is_found and data_sharing_consent_needed_cache.value is 0: return False enterprise_learner_details = get_enterprise_learner_data(user) if not enterprise_learner_details: consent_needed = False else: client = ConsentApiClient(user=request.user) consent_needed = any( client.consent_required( username=user.username, course_id=course_id, enterprise_customer_uuid=learner['enterprise_customer']['uuid'], enrollment_exists=enrollment_exists, ) for learner in enterprise_learner_details ) if not consent_needed: # Set an ephemeral item in the cache to prevent us from needing # to make a Consent API request every time this function is called. TieredCache.set_all_tiers(consent_cache_key, 0, settings.DATA_CONSENT_SHARE_CACHE_TIMEOUT) return consent_needed
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_found: 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['BACKEND_SERVICE_EDX_OAUTH2_KEY'], # pylint: disable=unsubscriptable-object self.oauth_settings['BACKEND_SERVICE_EDX_OAUTH2_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_and_cache_oauth_access_token(url, client_id, client_secret, token_type='jwt', grant_type='client_credentials', refresh_token=None, timeout=(REQUEST_CONNECT_TIMEOUT, REQUEST_READ_TIMEOUT)): """ Retrieves a possibly cached OAuth 2.0 access token using the given grant type. See ``get_oauth_access_token`` for usage details. First retrieves the access token from the cache and ensures it has not expired. If the access token either wasn't found in the cache, or was expired, retrieves a new access token and caches it for the lifetime of the token. Note: Consider tokens to be expired ACCESS_TOKEN_EXPIRED_THRESHOLD_SECONDS early to ensure the token won't expire while it is in use. Returns: tuple: Tuple containing (access token string, expiration datetime). """ oauth_url = _get_oauth_url(url) cache_key = 'edx_rest_api_client.access_token.{}.{}.{}.{}'.format( token_type, grant_type, client_id, oauth_url, ) cached_response = TieredCache.get_cached_response(cache_key) # Attempt to get an unexpired cached access token if cached_response.is_found: _, expiration = cached_response.value # Double-check the token hasn't already expired as a safety net. adjusted_expiration = expiration - datetime.timedelta( seconds=ACCESS_TOKEN_EXPIRED_THRESHOLD_SECONDS) if datetime.datetime.utcnow() < adjusted_expiration: return cached_response.value # Get a new access token if no unexpired access token was found in the cache. oauth_access_token_response = get_oauth_access_token( oauth_url, client_id, client_secret, grant_type=grant_type, refresh_token=refresh_token, timeout=timeout, ) # Cache the new access token with an expiration matching the lifetime of the token. _, expiration = oauth_access_token_response expires_in = (expiration - datetime.datetime.utcnow() ).seconds - ACCESS_TOKEN_EXPIRED_THRESHOLD_SECONDS TieredCache.set_all_tiers(cache_key, oauth_access_token_response, expires_in) return oauth_access_token_response
def record_user_activity(cls, user, course_key, request=None, only_if_mobile_app=False): ''' Update the user activity table with a record for this activity. Since we store one activity per date, we don't need to query the database for every activity on a given date. To avoid unnecessary queries, we store a record in a cache once we have an activity for the date, which times out at the end of that date (in the user's timezone). The request argument is only used to check if the request is coming from a mobile app. Once the only_if_mobile_app argument is removed the request argument can be removed as well. The return value is the id of the object that was created, or retrieved. A return value of None signifies that there was an issue with the parameters (or the user was masquerading). ''' if not (user and user.id) or not course_key: return None if only_if_mobile_app and request and not is_request_from_mobile_app(request): return None if is_masquerading(user, course_key): return None user_preferences = get_user_preferences(user) timezone = pytz.timezone(user_preferences.get('time_zone', 'UTC')) now = datetime.now(timezone) date = now.date() cache_key = 'goals_user_activity_{}_{}_{}'.format(str(user.id), str(course_key), str(date)) cached_value = TieredCache.get_cached_response(cache_key) if cached_value.is_found: # Temporary debugging log for testing mobile app connection if request: log.info( 'Retrieved cached value with request {} for user and course combination {} {}'.format( str(request.build_absolute_uri()), str(user.id), str(course_key) ) ) return cached_value.value, False activity_object, __ = cls.objects.get_or_create(user=user, course_key=course_key, date=date) # Cache result until the end of the day to avoid unnecessary database requests tomorrow = now + timedelta(days=1) midnight = datetime(year=tomorrow.year, month=tomorrow.month, day=tomorrow.day, hour=0, minute=0, second=0, tzinfo=timezone) seconds_until_midnight = (midnight - now).seconds TieredCache.set_all_tiers(cache_key, activity_object.id, seconds_until_midnight) # Temporary debugging log for testing mobile app connection if request: log.info( 'Set cached value with request {} for user and course combination {} {}'.format( str(request.build_absolute_uri()), str(user.id), str(course_key) ) ) return activity_object.id
def clear_data_consent_share_cache(user_id, course_id, enterprise_customer_uuid): """ clears data_sharing_consent_needed cache """ consent_cache_key = get_data_consent_share_cache_key( user_id, course_id, enterprise_customer_uuid) TieredCache.delete_all_tiers(consent_cache_key)
def get_applicable_lines(self, offer, basket, range=None): # pylint: disable=redefined-builtin """ Returns the basket lines for which the benefit is applicable. """ applicable_range = range if range else self.range if applicable_range and applicable_range.catalog_query is not None: query = applicable_range.catalog_query applicable_lines = self._filter_for_paid_course_products( basket.all_lines(), applicable_range) site = basket.site partner_code = site.siteconfiguration.partner.short_code course_run_ids, course_uuids, applicable_lines = self._identify_uncached_product_identifiers( applicable_lines, site.domain, partner_code, query) if course_run_ids or course_uuids: # Hit Discovery Service to determine if remaining courses and runs are in the range. try: response = site.siteconfiguration.discovery_api_client.catalog.query_contains.get( course_run_ids=','.join( [metadata['id'] for metadata in course_run_ids]), course_uuids=','.join( [metadata['id'] for metadata in course_uuids]), query=query, partner=partner_code) except Exception as err: # pylint: disable=bare-except logger.exception( '[Code Redemption Failure] Unable to apply benefit because we failed to query the ' 'Discovery Service for catalog data. ' 'User: %s, Offer: %s, Basket: %s, Message: %s', basket.owner.username, offer.id, basket.id, err) raise Exception( 'Failed to contact Discovery Service to retrieve offer catalog_range data.' ) # Cache range-state individually for each course or run identifier and remove lines not in the range. for metadata in course_run_ids + course_uuids: in_range = response[str(metadata['id'])] # Convert to int, because this is what memcached will return, and the request cache should return # the same value. # Note: once the TieredCache is fixed to handle this case, we could remove this line. in_range = int(in_range) TieredCache.set_all_tiers( metadata['cache_key'], in_range, settings.COURSES_API_CACHE_TIMEOUT) if not in_range: applicable_lines.remove(metadata['line']) return [(line.product.stockrecords.first().price_excl_tax, line) for line in applicable_lines] else: return super(Benefit, self).get_applicable_lines(offer, basket, range=range) # pylint: disable=bad-super-call
def test_ingest_verified_deadline(self, mock_push_to_ecomm): """ Verify the method ingests data from the Courses API. """ TieredCache.dangerous_clear_all_tiers() api_data = self.mock_api() self.assertEqual(Course.objects.count(), 0) self.assertEqual(CourseRun.objects.count(), 0) # Assume that while we are relying on ORGS_ON_OLD_PUBLISHER it will never be empty with self.settings(ORGS_ON_OLD_PUBLISHER='OTHER'): self.loader.ingest() # Verify the API was called with the correct authorization header self.assert_api_called(4) runs = CourseRun.objects.all() # Run with a verified entitlement, but no change in end date run1 = runs[0] run1.seats.add(SeatFactory(course_run=run1, type=SeatTypeFactory.verified())) run1.save() # Run with a verified entitlement, and the end date has changed run2 = runs[1] run2.seats.add(SeatFactory( course_run=run2, type=SeatTypeFactory.verified(), upgrade_deadline=datetime.datetime.now(pytz.UTC), )) original_run2_deadline = run2.seats.first().upgrade_deadline run2.end = datetime.datetime.now(pytz.UTC) run2.save() # Run with a credit entitlement, and the end date has changed should not run3 = runs[2] run3.seats.add(SeatFactory( course_run=run3, type=SeatTypeFactory.credit(), upgrade_deadline=None, )) run3.end = datetime.datetime.now(pytz.UTC) run3.save() # Verify the CourseRuns were created correctly expected_num_course_runs = len(api_data) self.assertEqual(CourseRun.objects.count(), expected_num_course_runs) # Verify multiple calls to ingest data do NOT result in data integrity errors. self.loader.ingest() calls = [ mock.call(run2), mock.call(run3), ] mock_push_to_ecomm.assert_has_calls(calls) # Make sure the verified seat with a course run end date is changed self.assertNotEqual(original_run2_deadline, run2.seats.first().upgrade_deadline) # Make sure the credit seat with a course run end date is unchanged self.assertIsNone(run3.seats.first().upgrade_deadline)
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_found: 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 get_enterprise_catalog(site, enterprise_catalog, limit, page, endpoint_request_url=None): """ Get the EnterpriseCustomerCatalog for a given catalog uuid. Args: site (Site): The site which is handling the current request enterprise_catalog (str): The uuid of the Enterprise Catalog limit (int): The number of results to return per page. page (int): The page number to fetch. endpoint_request_url (str): This is used to replace the lms url with ecommerce url Returns: dict: The result set containing the content objects associated with the Enterprise Catalog. NoneType: Return None if no catalog with that uuid is found. """ resource = 'enterprise_catalogs' partner_code = site.siteconfiguration.partner.short_code cache_key = u'{site_domain}_{partner_code}_{resource}_{catalog}_{limit}_{page}'.format( site_domain=site.domain, partner_code=partner_code, resource=resource, catalog=enterprise_catalog, limit=limit, page=page ) cache_key = hashlib.md5(cache_key.encode('utf-8')).hexdigest() cached_response = TieredCache.get_cached_response(cache_key) if cached_response.is_found: return cached_response.value api_client = site.siteconfiguration.oauth_api_client enterprise_api_url = urljoin( f"{site.siteconfiguration.enterprise_api_url}/", f"{resource}/{str(enterprise_catalog)}/" ) response = api_client.get( enterprise_api_url, params={ "limit": limit, "page": page, } ) response.raise_for_status() result = response.json() if endpoint_request_url: result = update_paginated_response(endpoint_request_url, result) TieredCache.set_all_tiers(cache_key, result, settings.CATALOG_RESULTS_CACHE_TIMEOUT) return result
def invalidate_processor_cache(*_args, **kwargs): """ When Waffle switches for payment processors are toggled, the payment processor list view cache must be invalidated. """ switch = kwargs['instance'] parts = switch.name.split(settings.PAYMENT_PROCESSOR_SWITCH_PREFIX) if len(parts) == 2: processor = parts[1] logger.info('Switched payment processor [%s] %s.', processor, 'on' if switch.active else 'off') TieredCache.delete_all_tiers(PAYMENT_PROCESSOR_CACHE_KEY) logger.info('Invalidated payment processor cache after toggling [%s].', switch.name)
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_found: 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 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: HTTPError: requests exception "HTTPError". """ 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_found: return cached_response.value api_client = site.siteconfiguration.oauth_api_client api_url = urljoin(f"{site.siteconfiguration.discovery_api_url}/", f"{api_resource}/{catalog_id}/") response = api_client.get(api_url) if response.status_code == 404: logger.exception("Catalog '%s' not found.", catalog_id) response.raise_for_status() result = response.json() TieredCache.set_all_tiers(cache_key, result, settings.COURSES_API_CACHE_TIMEOUT) return result
def setUp(self): super().setUp() cache.clear() # Set the domain used for all test requests domain = "testserver.fake" self.client = self.client_class(SERVER_NAME=domain) Site.objects.all().delete() self.site_configuration = SiteConfigurationFactory(site__domain=domain, site__id=settings.SITE_ID) self.site = self.site_configuration.site # Clear edx rest api client cache TieredCache.dangerous_clear_all_tiers()
def get_enterprise_catalog(site, enterprise_catalog, limit, page, endpoint_request_url=None): """ Get the EnterpriseCustomerCatalog for a given catalog uuid. Args: site (Site): The site which is handling the current request enterprise_catalog (str): The uuid of the Enterprise Catalog limit (int): The number of results to return per page. page (int): The page number to fetch. endpoint_request_url (str): This is used to replace the lms url with ecommerce url Returns: dict: The result set containing the content objects associated with the Enterprise Catalog. NoneType: Return None if no catalog with that uuid is found. """ resource = 'enterprise_catalogs' partner_code = site.siteconfiguration.partner.short_code cache_key = '{site_domain}_{partner_code}_{resource}_{catalog}_{limit}_{page}'.format( site_domain=site.domain, partner_code=partner_code, resource=resource, catalog=enterprise_catalog, limit=limit, page=page) cache_key = hashlib.md5(cache_key).hexdigest() cached_response = TieredCache.get_cached_response(cache_key) if cached_response.is_found: return cached_response.value client = get_enterprise_api_client(site) path = [resource, str(enterprise_catalog)] client = reduce(getattr, path, client) response = client.get( limit=limit, page=page, ) if endpoint_request_url: response = update_paginated_response(endpoint_request_url, response) TieredCache.set_all_tiers(cache_key, response, settings.CATALOG_RESULTS_CACHE_TIMEOUT) return response
def set_last_seen_courseware_timezone(self, user): """ The timezone in the user's account is frequently not set. This method sets a user's recent timezone that can be used as a fallback """ cache_key = 'browser_timezone_{}'.format(str(user.id)) browser_timezone = self.request.query_params.get('browser_timezone', None) cached_value = TieredCache.get_cached_response(cache_key) if not cached_value.is_found: if browser_timezone: TieredCache.set_all_tiers(cache_key, str(browser_timezone), 86400) # Refresh the cache daily LastSeenCoursewareTimezone.objects.update_or_create( user=user, last_seen_courseware_timezone=browser_timezone, )
def get_enterprise_customer(site, uuid): """ Return a single enterprise customer """ resource = 'enterprise-customer' cache_key = u'{site_domain}_{partner_code}_{resource}_{enterprise_uuid}'.format( site_domain=site.domain, partner_code=site.siteconfiguration.partner.short_code, resource=resource, enterprise_uuid=uuid, ) cache_key = hashlib.md5(cache_key.encode('utf-8')).hexdigest() cached_response = TieredCache.get_cached_response(cache_key) if cached_response.is_found: return cached_response.value api_client = site.siteconfiguration.oauth_api_client enterprise_api_url = urljoin( f"{site.siteconfiguration.enterprise_api_url}/", f"{resource}/{str(uuid)}/" ) try: response = api_client.get(enterprise_api_url) response.raise_for_status() response = response.json() except (ReqConnectionError, HTTPError, Timeout): log.exception("Failed to fetch enterprise customer") return None enterprise_customer_response = { 'name': response['name'], 'id': response['uuid'], 'enable_data_sharing_consent': response['enable_data_sharing_consent'], 'enforce_data_sharing_consent': response['enforce_data_sharing_consent'], 'contact_email': response.get('contact_email', ''), 'slug': response.get('slug'), 'sender_alias': response.get('sender_alias', ''), 'reply_to': response.get('reply_to', ''), } TieredCache.set_all_tiers( cache_key, enterprise_customer_response, settings.ENTERPRISE_CUSTOMER_RESULTS_CACHE_TIMEOUT ) return enterprise_customer_response
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. """ uncached_course_run_ids = [] uncached_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 ) in_catalog_range_cached_response = TieredCache.get_cached_response(cache_key) if not in_catalog_range_cached_response.is_found: if line.product.is_seat_product: uncached_course_run_ids.append({'id': product_id, 'cache_key': cache_key, 'line': line}) else: uncached_course_uuids.append({'id': product_id, 'cache_key': cache_key, 'line': line}) elif not in_catalog_range_cached_response.value: applicable_lines.remove(line) return uncached_course_run_ids, uncached_course_uuids, applicable_lines
def test_get_access_token_from_cache_valid(self): encrypted_access_token = self.handler._encrypt_token('12345') encrypted_refresh_token = self.handler._encrypt_token('67890') tokens = { 'access_token': encrypted_access_token, 'refresh_token': encrypted_refresh_token, 'expires_at': datetime.datetime.utcnow() + datetime.timedelta(seconds=20) } TieredCache.set_all_tiers('badgr-test-cache-key', tokens, None) access_token = self.handler._get_access_token() assert access_token == self.handler._decrypt_token( tokens.get('access_token'))
def _get_access_token(self): """ Get an access token from cache if one is present and valid. If a token is cached but expired, renew it. If all fails or a token has not yet been cached, create a new one. """ tokens = {} cached_response = TieredCache.get_cached_response( settings.BADGR_TOKENS_CACHE_KEY) if cached_response.is_found: cached_tokens = cached_response.value # add a 5 seconds buffer to the cutoff timestamp to make sure # the token will not expire while in use expiry_cutoff = (datetime.datetime.utcnow() + datetime.timedelta(seconds=5)) if cached_tokens.get('expires_at') > expiry_cutoff: tokens = cached_tokens else: # renew the tokens with the cached `refresh_token` refresh_token = self._decrypt_token( cached_tokens.get('refresh_token')) tokens = self._get_and_cache_oauth_tokens( refresh_token=refresh_token) # if no tokens are cached or something went wrong with # retreiving/renewing them, go and create new tokens if not tokens: tokens = self._get_and_cache_oauth_tokens() return self._decrypt_token(tokens.get('access_token'))
def delete_data_sharing_consent(course_id, customer_uuid, user_email): """ Delete the DSC records from the DB for given learner, course and customer, also its cache. """ # Deleting the DSC record. user = User.objects.get(email=user_email) enterprise_customer_user = get_enterprise_customer_user( user.id, customer_uuid) enterprise_customer_user.data_sharing_consent_records.filter( course_id=course_id).delete() # Deleting the DCS cache consent_cache_key = get_cache_key(type='data_sharing_consent_needed', user_id=user.id, course_id=course_id) TieredCache.delete_all_tiers(consent_cache_key)
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_found: 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 test_get_order_attribute_from_ecommerce(self, mock_ecommerce_api_client): """ Assert that the get_order_attribute_from_ecommerce method returns order details if it's already cached, without calling ecommerce. """ order_details = {"number": self.ORDER_NUMBER, "vouchers": [{"end_datetime": '2025-09-25T00:00:00Z'}]} cache_key = get_cache_key(user_id=self.user.id, order_number=self.ORDER_NUMBER) TieredCache.set_all_tiers(cache_key, order_details, 60) self.enrollment.attributes.create( enrollment=self.enrollment, namespace='order', name='order_number', value=self.ORDER_NUMBER ) assert self.enrollment.get_order_attribute_from_ecommerce("vouchers") == order_details["vouchers"] mock_ecommerce_api_client.assert_not_called()
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_found: 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 test_ingest(self): """ Verify the method ingests data from the Organizations API. """ TieredCache.dangerous_clear_all_tiers() api_data = self.mock_api() self.assertEqual(Program.objects.count(), 0) self.loader.ingest() # Verify the API was called with the correct authorization header self.assert_api_called(6) # Verify the Programs were created correctly self.assertEqual(Program.objects.count(), len(api_data)) for datum in api_data: self.assert_program_loaded(datum) self.loader.ingest()
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 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_found) # Toggle a switch to trigger cache deletion Switch.objects.get_or_create( name=settings.PAYMENT_PROCESSOR_SWITCH_PREFIX + 'dummy') self.assertFalse( TieredCache.get_cached_response( PAYMENT_PROCESSOR_CACHE_KEY).is_found)
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.partner.short_code) cache_key = hashlib.md5(cache_key).hexdigest() course_before_cached_response = TieredCache.get_cached_response(cache_key) self.assertFalse(course_before_cached_response.is_found) 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, request=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 api = site.siteconfiguration.enterprise_api_client # Temporarily gate enterprise catalog api usage behind waffle flag if request and can_use_enterprise_catalog(request, enterprise_customer_uuid): api = site.siteconfiguration.enterprise_catalog_api_client if enterprise_customer_catalog_uuid: api_resource_name = 'enterprise-catalogs' 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_found: return contains_content_cached_response.value endpoint = getattr(api, api_resource_name)(api_resource_id) 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) return contains_content
def get_with_access_to(self, user, enterprise_id): """ Get the enterprises that this user has access to for the data api permission django group. """ cache_key = get_cache_key( resource='enterprise-customer', user=user.username, enterprise_customer=enterprise_id, ) cached_response = TieredCache.get_cached_response(cache_key) if cached_response.is_found: return cached_response.value try: querystring = { 'permissions': [self.ENTERPRISE_DATA_API_GROUP], 'enterprise_id': enterprise_id, } endpoint = getattr(self, 'enterprise-customer') # pylint: disable=literal-used-as-attribute endpoint = endpoint.with_access_to response = endpoint.get(**querystring) except (HttpClientError, HttpServerError) as exc: LOGGER.warning( "Unable to retrieve Enterprise Customer with_access_to details for user {}: {}" .format(user.username, exc)) raise exc if response.get('results', None) is None: raise NotFound( 'Unable to process Enterprise Customer with_access_to details for user {}, enterprise {}:' ' No Results Found'.format(user.username, enterprise_id)) if response['count'] > 1: raise ParseError( 'Multiple Enterprise Customers found for user {}, enterprise id {}' .format(user.username, enterprise_id)) if response['count'] == 0: return None TieredCache.set_all_tiers(cache_key, response['results'][0], DEFAULT_REPORTING_CACHE_TIMEOUT) return response['results'][0]
def get_cached_module_engagement_count(self): """ Caches Module Engagement records count for specific enterprise. """ enterprise_id = self.kwargs.get('enterprise_customer') cache_key = get_cache_key( resource='module_engagement_count', resource_id=enterprise_id, ) module_engagement_count_cache = TieredCache.get_cached_response( cache_key) if module_engagement_count_cache.is_found: return module_engagement_count_cache.value queryset = self._get_queryset() count = queryset.count() TieredCache.set_all_tiers(cache_key, count, settings.ENGAGEMENT_CACHE_TIMEOUT) return count
def _is_dsc_cache_found(user_id, course_id): consent_cache_key = get_data_consent_share_cache_key(user_id, course_id) data_sharing_consent_needed_cache = TieredCache.get_cached_response(consent_cache_key) return data_sharing_consent_needed_cache.is_found
def _create_dsc_cache(user_id, course_id): consent_cache_key = get_data_consent_share_cache_key(user_id, course_id) TieredCache.set_all_tiers(consent_cache_key, 0)
def clear_data_consent_share_cache(user_id, course_id): """ clears data_sharing_consent_needed cache """ consent_cache_key = get_data_consent_share_cache_key(user_id, course_id) TieredCache.delete_all_tiers(consent_cache_key)