def test_fetch_enterprise_learner_entitlements(self): """ Verify that method "fetch_enterprise_learner_data" returns a proper response for the enterprise learner. """ # API should be hit only twice in this test case, # once by `fetch_enterprise_learner_data` and once by `fetch_enterprise_learner_entitlements`. expected_number_of_requests = 3 self.mock_access_token_response() self.mock_enterprise_learner_api() enterprise_learners = enterprise_api.fetch_enterprise_learner_data( self.request.site, self.learner) enterprise_learner_id = enterprise_learners['results'][0]['id'] self.mock_enterprise_learner_entitlements_api(enterprise_learner_id) enterprise_api.fetch_enterprise_learner_entitlements( self.request.site, enterprise_learner_id) # Verify the API was hit just two times, once by `fetch_enterprise_learner_data` # and once by `fetch_enterprise_learner_entitlements` self._assert_num_requests(expected_number_of_requests) # Now fetch the enterprise learner entitlements again and verify that there was # no actual call to Enterprise API, as the data will be taken from # the cache enterprise_api.fetch_enterprise_learner_entitlements( self.request.site, enterprise_learner_id) self._assert_num_requests(expected_number_of_requests)
def add_message_if_enterprise_user(self, request): enterprise_customer = None learner_data = None try: # If enterprise feature is enabled return all the enterprise_customer associated with user. learner_data = fetch_enterprise_learner_data(request.site, request.user) except (ReqConnectionError, KeyError, SlumberHttpBaseException, Timeout) as exc: logging.exception('[enterprise learner message] Exception while retrieving enterprise learner data for' 'User: %s, Exception: %s', request.user, exc) if learner_data: try: enterprise_customer = learner_data['results'][0]['enterprise_customer'] except IndexError: # If enterprise feature is enable and user is not associated to any enterprise pass if enterprise_customer: enable_learner_portal = enterprise_customer['enable_learner_portal'] learner_portal_hostname = enterprise_customer['learner_portal_hostname'] if enable_learner_portal and learner_portal_hostname: message = ( 'Your company, {enterprise_customer_name}, has a dedicated page where ' 'you can see all of your sponsored courses. ' 'Go to <a href="{scheme}://{hostname}">your learner portal</a>.' ).format( enterprise_customer_name=enterprise_customer['name'], hostname=learner_portal_hostname, scheme=request.scheme ) messages.add_message(request, messages.INFO, message, extra_tags='safe')
def get_course_entitlements_for_learner(site, user, course_id): """ Get entitlements for the provided learner against the provided course id if the provided learner is affiliated with an enterprise. Arguments: course_id (str): The course ID. site: (django.contrib.sites.Site) site instance user: (django.contrib.auth.User) django auth user Returns: (list): List of entitlement ids, where entitlement id is actually a voucher id. """ try: enterprise_learner_data = enterprise_api.fetch_enterprise_learner_data(site, user)['results'] except (ConnectionError, SlumberBaseException, Timeout, KeyError, TypeError): logger.exception( 'Failed to retrieve enterprise info for the learner [%s]', user.username ) return None if not enterprise_learner_data: logger.info('Learner with username [%s] in not affiliated with any enterprise', user.username) return None try: enterprise_catalog_id = enterprise_learner_data[0]['enterprise_customer']['catalog'] learner_id = enterprise_learner_data[0]['id'] except KeyError: logger.exception('Invalid structure for enterprise learner API response for the learner [%s]', user.username) return None # Before returning entitlements verify that the provided course exists in # the enterprise course catalog if not is_course_in_enterprise_catalog(site, course_id, enterprise_catalog_id): return None try: entitlements = enterprise_api.fetch_enterprise_learner_entitlements(site, learner_id) except (ConnectionError, SlumberBaseException, Timeout): logger.exception( 'Failed to retrieve entitlements for enterprise learner [%s].', learner_id ) return None try: # Currently, we are returning only those entitlements that # do not require any further action on part of enterprise learner entitlements = [item['entitlement_id'] for item in entitlements['entitlements'] if not item['requires_consent']] except KeyError: logger.exception( 'Invalid structure for enterprise learner entitlements API response for enterprise learner [%s].', learner_id, ) return None return entitlements
def test_fetch_enterprise_learner_data(self): """ Verify that method "fetch_enterprise_learner_data" returns a proper response for the enterprise learner. """ self.mock_access_token_response() self.mock_enterprise_learner_api() self._assert_fetch_enterprise_learner_data() # API should be hit only once in this test case expected_number_of_requests = 2 # Verify the API was hit once self._assert_num_requests(expected_number_of_requests) # Now fetch the enterprise learner data again and verify that there was # no actual call to Enterprise API, as the data will be fetched from # the cache enterprise_api.fetch_enterprise_learner_data(self.request.site, self.learner) self._assert_num_requests(expected_number_of_requests)
def is_satisfied(self, offer, basket): # pylint: disable=unused-argument """ Determines if a user is eligible for an enterprise customer offer based on their association with the enterprise customer. Args: basket (Basket): Contains information about order line items, the current site, and the user attempting to make the purchase. Returns: bool """ if not basket.owner: # An anonymous user is never linked to any EnterpriseCustomer. return False try: learner_data = fetch_enterprise_learner_data( basket.site, basket.owner)['results'][0] except (ConnectionError, KeyError, SlumberHttpBaseException, Timeout): logger.exception( 'Failed to retrieve enterprise learner data for site [%s] and user [%s].', basket.site.domain, basket.owner.username, ) return False except IndexError: return False enterprise_customer = learner_data['enterprise_customer'] if str(self.enterprise_customer_uuid) != enterprise_customer['uuid']: # Learner is not linked to the EnterpriseCustomer associated with this condition. return False course_run_ids = [] for line in basket.all_lines(): course = line.product.course if not course: # Basket contains products not related to a course_run. return False course_run_ids.append(course.id) if not catalog_contains_course_runs( basket.site, course_run_ids, self.enterprise_customer_uuid, enterprise_customer_catalog_uuid=self. enterprise_customer_catalog_uuid): # Basket contains course runs that do not exist in the EnterpriseCustomerCatalogs # associated with the EnterpriseCustomer. return False return True
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.assertFalse(enterprise_learner_cached_response.is_found) 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 get_enterprise_id_for_user(site, user): enterprise_from_jwt = get_enterprise_id_for_current_request_user_from_jwt() if enterprise_from_jwt: return enterprise_from_jwt try: enterprise_learner_response = fetch_enterprise_learner_data(site, user) except (ConnectionError, KeyError, SlumberHttpBaseException, Timeout) as exc: logging.exception( 'Unable to retrieve enterprise learner data for the user!' 'User: %s, Exception: %s', user, exc) return None try: return enterprise_learner_response['results'][0][ 'enterprise_customer']['uuid'] except IndexError: pass return None
def add_message_if_enterprise_user(self, request): try: # If enterprise feature is enabled return all the enterprise_customer associated with user. learner_data = fetch_enterprise_learner_data( request.site, request.user) except (ReqConnectionError, KeyError, SlumberHttpBaseException, Timeout) as exc: logging.exception( '[enterprise learner message] Exception while retrieving enterprise learner data for' 'User: %s, Exception: %s', request.user, exc) return None try: enterprise_customer = learner_data['results'][0][ 'enterprise_customer'] except (IndexError, KeyError): # If enterprise feature is enabled and user is not associated to any enterprise return None enable_learner_portal = enterprise_customer.get( 'enable_learner_portal') enterprise_learner_portal_slug = enterprise_customer.get('slug') if enable_learner_portal and enterprise_learner_portal_slug: learner_portal_url = '{scheme}://{hostname}/{slug}'.format( scheme=request.scheme, hostname=settings.ENTERPRISE_LEARNER_PORTAL_HOSTNAME, slug=enterprise_learner_portal_slug, ) message = ( 'Your company, {enterprise_customer_name}, has a dedicated page where ' 'you can see all of your sponsored courses. ' 'Go to <a href="{url}">your learner portal</a>.').format( enterprise_customer_name=enterprise_customer['name'], url=learner_portal_url) messages.add_message(request, messages.INFO, message, extra_tags='safe') return learner_portal_url return None
def add_message_if_enterprise_user(self, request): learner_data = fetch_enterprise_learner_data(request.site, request.user) learner_data_results = learner_data.get('results') if learner_data_results: enterprise_customer = learner_data_results[0][ 'enterprise_customer'] enable_learner_portal = enterprise_customer[ 'enable_learner_portal'] learner_portal_hostname = enterprise_customer[ 'learner_portal_hostname'] if enable_learner_portal and learner_portal_hostname: message = ( 'Your company, {enterprise_customer_name}, has a dedicated page where ' 'you can see all of your sponsored courses. ' 'Go to <a href="{scheme}://{hostname}">your learner portal</a>.' ).format(enterprise_customer_name=enterprise_customer['name'], hostname=learner_portal_hostname, scheme=request.scheme) messages.add_message(request, messages.INFO, message, extra_tags='safe')
def is_satisfied(self, offer, basket): # pylint: disable=unused-argument """ Determines if a user is eligible for an enterprise customer offer based on their association with the enterprise customer. It also filter out the offer if the `enterprise_customer_catalog_uuid` value set on the offer condition does not match with the basket catalog value when explicitly provided by the enterprise learner. Note: Currently there is no mechanism to prioritize or apply multiple offers that may apply as opposed to disqualifying offers if the catalog doesn't explicitly match. Arguments: basket (Basket): Contains information about order line items, the current site, and the user attempting to make the purchase. Returns: bool """ if not basket.owner: # An anonymous user is never linked to any EnterpriseCustomer. return False enterprise_customer = str(self.enterprise_customer_uuid) enterprise_catalog = str(self.enterprise_customer_catalog_uuid) username = basket.owner.username course_run_ids = [] for line in basket.all_lines(): course = line.product.course if not course: # Basket contains products not related to a course_run. # Only log for non-site offers to avoid noise. if offer.offer_type != ConditionalOffer.SITE: logger.warning( '[Code Redemption Failure] Unable to apply enterprise offer because ' 'the Basket contains a product not related to a course_run. ' 'User: %s, Offer: %s, Product: %s, Enterprise: %s, Catalog: %s', username, offer.id, line.product.id, enterprise_customer, enterprise_catalog) return False course_run_ids.append(course.id) courses_in_basket = ','.join(course_run_ids) learner_data = {} try: learner_data = fetch_enterprise_learner_data( basket.site, basket.owner)['results'][0] except (ConnectionError, KeyError, SlumberHttpBaseException, Timeout) as exc: logger.exception( '[Code Redemption Failure] Unable to apply enterprise offer because ' 'we failed to retrieve enterprise learner data for the user. ' 'User: %s, Offer: %s, Message: %s, Enterprise: %s, Catalog: %s, Courses: %s', username, offer.id, exc, enterprise_customer, enterprise_catalog, courses_in_basket) return False except IndexError: if offer.offer_type == ConditionalOffer.SITE: logger.debug( 'Unable to apply enterprise site offer %s because no learner data was returned for user %s', offer.id, basket.owner) return False if (learner_data and 'enterprise_customer' in learner_data and enterprise_customer != learner_data['enterprise_customer']['uuid']): # Learner is not linked to the EnterpriseCustomer associated with this condition. if offer.offer_type == ConditionalOffer.VOUCHER: logger.warning( '[Code Redemption Failure] Unable to apply enterprise offer because Learner\'s ' 'enterprise (%s) does not match this conditions\'s enterprise (%s). ' 'User: %s, Offer: %s, Enterprise: %s, Catalog: %s, Courses: %s', learner_data['enterprise_customer']['uuid'], enterprise_customer, username, offer.id, enterprise_customer, enterprise_catalog, courses_in_basket) return False # Verify that the current conditional offer is related to the provided # enterprise catalog, this will also filter out offers which don't # have `enterprise_customer_catalog_uuid` value set on the condition. catalog = self._get_enterprise_catalog_uuid_from_basket(basket) if catalog: if offer.condition.enterprise_customer_catalog_uuid != catalog: logger.warning( 'Unable to apply enterprise offer %s because ' 'Enterprise catalog id on the basket (%s) ' 'does not match the catalog for this condition (%s).', offer.id, catalog, offer.condition.enterprise_customer_catalog_uuid) return False try: catalog_contains_course = catalog_contains_course_runs( basket.site, course_run_ids, enterprise_customer, enterprise_customer_catalog_uuid=enterprise_catalog) except (ConnectionError, KeyError, SlumberHttpBaseException, Timeout) as exc: logger.exception( '[Code Redemption Failure] Unable to apply enterprise offer because ' 'we failed to check if course_runs exist in the catalog. ' 'User: %s, Offer: %s, Message: %s, Enterprise: %s, Catalog: %s, Courses: %s', username, offer.id, exc, enterprise_customer, enterprise_catalog, courses_in_basket) return False if not catalog_contains_course: # Basket contains course runs that do not exist in the EnterpriseCustomerCatalogs # associated with the EnterpriseCustomer. logger.warning( '[Code Redemption Failure] Unable to apply enterprise offer because ' 'Enterprise catalog does not contain the course(s) in this basket. ' 'User: %s, Offer: %s, Enterprise: %s, Catalog: %s, Courses: %s', username, offer.id, enterprise_customer, enterprise_catalog, courses_in_basket) return False return True
def is_satisfied(self, offer, basket): # pylint: disable=unused-argument """ Determines if a user is eligible for an enterprise customer offer based on their association with the enterprise customer. It also filter out the offer if the `enterprise_customer_catalog_uuid` value set on the offer condition does not match with the basket catalog value when explicitly provided by the enterprise learner. Note: Currently there is no mechanism to prioritize or apply multiple offers that may apply as opposed to disqualifying offers if the catalog doesn't explicitly match. Arguments: basket (Basket): Contains information about order line items, the current site, and the user attempting to make the purchase. Returns: bool """ if not basket.owner: # An anonymous user is never linked to any EnterpriseCustomer. return False if offer.offer_type == ConditionalOffer.VOUCHER: logger.info( 'Skipping Voucher type enterprise conditional offer until we are ready to support it.' ) return False try: learner_data = fetch_enterprise_learner_data( basket.site, basket.owner)['results'][0] except (ConnectionError, KeyError, SlumberHttpBaseException, Timeout): logger.exception( 'Failed to retrieve enterprise learner data for site [%s] and user [%s].', basket.site.domain, basket.owner.username, ) return False except IndexError: return False enterprise_customer = learner_data['enterprise_customer'] if str(self.enterprise_customer_uuid) != enterprise_customer['uuid']: # Learner is not linked to the EnterpriseCustomer associated with this condition. return False course_run_ids = [] for line in basket.all_lines(): course = line.product.course if not course: # Basket contains products not related to a course_run. return False course_run_ids.append(course.id) # Verify that the current conditional offer is related to the provided # enterprise catalog, this will also filter out offers which don't # have `enterprise_customer_catalog_uuid` value set on the condition. catalog = self._get_enterprise_catalog_uuid_from_basket(basket) if catalog: if offer.condition.enterprise_customer_catalog_uuid != catalog: return False if not catalog_contains_course_runs( basket.site, course_run_ids, self.enterprise_customer_uuid, enterprise_customer_catalog_uuid=self. enterprise_customer_catalog_uuid): # Basket contains course runs that do not exist in the EnterpriseCustomerCatalogs # associated with the EnterpriseCustomer. return False return True
def is_satisfied(self, offer, basket): # pylint: disable=unused-argument """ Determines if a user is eligible for an enterprise customer offer based on their association with the enterprise customer. It also filter out the offer if the `enterprise_customer_catalog_uuid` value set on the offer condition does not match with the basket catalog value when explicitly provided by the enterprise learner. Note: Currently there is no mechanism to prioritize or apply multiple offers that may apply as opposed to disqualifying offers if the catalog doesn't explicitly match. Arguments: basket (Basket): Contains information about order line items, the current site, and the user attempting to make the purchase. Returns: bool """ if not basket.owner: # An anonymous user is never linked to any EnterpriseCustomer. return False if (offer.offer_type == ConditionalOffer.VOUCHER and not waffle.switch_is_active( ENTERPRISE_OFFERS_FOR_COUPONS_SWITCH)): logger.info( 'Skipping Voucher type enterprise conditional offer until we are ready to support it.' ) return False learner_data = {} try: learner_data = fetch_enterprise_learner_data( basket.site, basket.owner)['results'][0] except (ConnectionError, KeyError, SlumberHttpBaseException, Timeout): logger.exception( 'Failed to retrieve enterprise learner data for site [%s] and user [%s].', basket.site.domain, basket.owner.username, ) return False except IndexError: if offer.offer_type == ConditionalOffer.SITE: logger.debug( 'Unable to apply enterprise site offer %s because no learner data was returned for user %s', offer.id, basket.owner) return False if (learner_data and 'enterprise_customer' in learner_data and str(self.enterprise_customer_uuid) != learner_data['enterprise_customer']['uuid']): # Learner is not linked to the EnterpriseCustomer associated with this condition. logger.debug( 'Unable to apply enterprise offer %s because Learner\'s enterprise (%s)' 'does not match this conditions\'s enterprise (%s).', offer.id, learner_data['enterprise_customer']['uuid'], str(self.enterprise_customer_uuid)) return False course_run_ids = [] for line in basket.all_lines(): course = line.product.course if not course: # Basket contains products not related to a course_run. logger.warning( 'Unable to apply enterprise offer %s because ' 'the Basket (#%s) contains a product (#%s) not related to a course_run.', offer.id, basket.id, line.product.id) return False course_run_ids.append(course.id) # Verify that the current conditional offer is related to the provided # enterprise catalog, this will also filter out offers which don't # have `enterprise_customer_catalog_uuid` value set on the condition. catalog = self._get_enterprise_catalog_uuid_from_basket(basket) if catalog: if offer.condition.enterprise_customer_catalog_uuid != catalog: logger.warning( 'Unable to apply enterprise offer %s because ' 'Enterprise catalog id on the basket (%s) ' 'does not match the catalog for this condition (%s).', offer.id, catalog, offer.condition.enterprise_customer_catalog_uuid) return False if not catalog_contains_course_runs( basket.site, course_run_ids, self.enterprise_customer_uuid, enterprise_customer_catalog_uuid=self. enterprise_customer_catalog_uuid): # Basket contains course runs that do not exist in the EnterpriseCustomerCatalogs # associated with the EnterpriseCustomer. logger.warning( 'Unable to apply enterprise offer %s because ' 'Enterprise catalog (%s) does not contain the course(s) (%s) in this basket.', offer.id, self.enterprise_customer_catalog_uuid, ','.join(course_run_ids)) return False return True