diff --git a/enterprise_access/apps/api/v1/tests/test_bff_views.py b/enterprise_access/apps/api/v1/tests/test_bff_views.py index e0bbd917..9402ecca 100644 --- a/enterprise_access/apps/api/v1/tests/test_bff_views.py +++ b/enterprise_access/apps/api/v1/tests/test_bff_views.py @@ -183,7 +183,7 @@ def setUp(self): ) @ddt.unpack @mock_dashboard_dependencies - def test_dashboard_empty_state( + def test_dashboard_empty_state_with_permissions( self, mock_get_enterprise_customers_for_user, mock_get_subscription_licenses_for_learner, diff --git a/enterprise_access/apps/bffs/api.py b/enterprise_access/apps/bffs/api.py index f80e8954..5445b33f 100644 --- a/enterprise_access/apps/bffs/api.py +++ b/enterprise_access/apps/bffs/api.py @@ -6,6 +6,7 @@ from django.conf import settings from edx_django_utils.cache import TieredCache +from enterprise_access.apps.api_client.license_manager_client import LicenseManagerUserApiClient from enterprise_access.apps.api_client.lms_client import LmsApiClient, LmsUserApiClient from enterprise_access.cache_utils import versioned_cache_key @@ -24,7 +25,23 @@ def enterprise_customer_cache_key(enterprise_customer_slug, enterprise_customer_ return versioned_cache_key('enterprise_customer', enterprise_customer_slug, enterprise_customer_uuid) -def get_and_cache_enterprise_customer_users(request, **kwargs): +def subscription_licenses_cache_key(enterprise_customer_uuid, lms_user_id): + return versioned_cache_key('get_subscription_licenses_for_learner', enterprise_customer_uuid, lms_user_id) + + +def default_enterprise_enrollment_intentions_learner_status_cache_key(enterprise_customer_uuid, lms_user_id): + return versioned_cache_key( + 'get_default_enterprise_enrollment_intentions_learner_status', + enterprise_customer_uuid, + lms_user_id + ) + + +def enterprise_course_enrollments_cache_key(enterprise_customer_uuid, lms_user_id): + return versioned_cache_key('get_enterprise_course_enrollments', enterprise_customer_uuid, lms_user_id) + + +def get_and_cache_enterprise_customer_users(request, timeout=settings.ENTERPRISE_USER_RECORD_CACHE_TIMEOUT, **kwargs): """ Retrieves and caches enterprise learner data. """ @@ -42,13 +59,14 @@ def get_and_cache_enterprise_customer_users(request, **kwargs): username=username, **kwargs, ) - TieredCache.set_all_tiers(cache_key, response_payload, settings.LMS_CLIENT_TIMEOUT) + TieredCache.set_all_tiers(cache_key, response_payload, timeout) return response_payload def get_and_cache_enterprise_customer( enterprise_customer_slug=None, enterprise_customer_uuid=None, + timeout=settings.ENTERPRISE_USER_RECORD_CACHE_TIMEOUT, ): """ Retrieves and caches enterprise customer data. @@ -70,10 +88,117 @@ def get_and_cache_enterprise_customer( enterprise_customer_uuid=enterprise_customer_uuid, enterprise_customer_slug=enterprise_customer_slug, ) - TieredCache.set_all_tiers(cache_key, response_payload, settings.LMS_CLIENT_TIMEOUT) + TieredCache.set_all_tiers(cache_key, response_payload, timeout) + return response_payload + + +def get_and_cache_subscription_licenses_for_learner( + request, + enterprise_customer_uuid, + timeout=settings.SUBSCRIPTION_LICENSES_LEARNER_CACHE_TIMEOUT, + **kwargs +): + """ + Retrieves and caches subscription licenses for a learner. + """ + cache_key = subscription_licenses_cache_key(enterprise_customer_uuid, request.user.id) + cached_response = TieredCache.get_cached_response(cache_key) + if cached_response.is_found: + logger.info( + f'subscription_licenses cache hit for enterprise_customer_uuid {enterprise_customer_uuid}' + ) + return cached_response.value + + client = LicenseManagerUserApiClient(request) + response_payload = client.get_subscription_licenses_for_learner( + enterprise_customer_uuid=enterprise_customer_uuid, + **kwargs, + ) + TieredCache.set_all_tiers(cache_key, response_payload, timeout) + return response_payload + + +def get_and_cache_default_enterprise_enrollment_intentions_learner_status( + request, + enterprise_customer_uuid, + timeout=settings.DEFAULT_ENTERPRISE_ENROLLMENT_INTENTIONS_CACHE_TIMEOUT, +): + """ + Retrieves and caches default enterprise enrollment intentions for a learner. + """ + cache_key = default_enterprise_enrollment_intentions_learner_status_cache_key( + enterprise_customer_uuid, + request.user.id, + ) + cached_response = TieredCache.get_cached_response(cache_key) + if cached_response.is_found: + logger.info( + f'default_enterprise_enrollment_intentions cache hit ' + f'for enterprise_customer_uuid {enterprise_customer_uuid}' + ) + return cached_response.value + + client = LmsUserApiClient(request) + response_payload = client.get_default_enterprise_enrollment_intentions_learner_status( + enterprise_customer_uuid=enterprise_customer_uuid, + ) + TieredCache.set_all_tiers(cache_key, response_payload, timeout) return response_payload +def get_and_cache_enterprise_course_enrollments( + request, + enterprise_customer_uuid, + timeout=settings.ENTERPRISE_COURSE_ENROLLMENTS_CACHE_TIMEOUT, + **kwargs +): + """ + Retrieves and caches enterprise course enrollments for a learner. + """ + cache_key = enterprise_course_enrollments_cache_key(enterprise_customer_uuid, request.user.id) + cached_response = TieredCache.get_cached_response(cache_key) + if cached_response.is_found: + logger.info( + f'enterprise_course_enrollments cache hit for enterprise_customer_uuid {enterprise_customer_uuid}' + ) + return cached_response.value + + client = LmsUserApiClient(request) + response_payload = client.get_enterprise_course_enrollments( + enterprise_customer_uuid=enterprise_customer_uuid, + **kwargs, + ) + TieredCache.set_all_tiers(cache_key, response_payload, timeout) + return response_payload + + +def invalidate_default_enterprise_enrollment_intentions_learner_status_cache(enterprise_customer_uuid, lms_user_id): + """ + Invalidates the default enterprise enrollment intentions cache for a learner. + """ + cache_key = default_enterprise_enrollment_intentions_learner_status_cache_key( + enterprise_customer_uuid, + lms_user_id, + ) + TieredCache.delete_all_tiers(cache_key) + + +def invalidate_enterprise_course_enrollments_cache(enterprise_customer_uuid, lms_user_id): + """ + Invalidates the enterprise course enrollments cache for a learner. + """ + cache_key = enterprise_course_enrollments_cache_key(enterprise_customer_uuid, lms_user_id) + TieredCache.delete_all_tiers(cache_key) + + +def invalidate_subscription_licenses_cache(enterprise_customer_uuid, lms_user_id): + """ + Invalidates the subscription licenses cache for a learner. + """ + cache_key = subscription_licenses_cache_key(enterprise_customer_uuid, lms_user_id) + TieredCache.delete_all_tiers(cache_key) + + def _get_active_enterprise_customer(enterprise_customer_users): """ Get the active enterprise customer user from the list of enterprise customer users. diff --git a/enterprise_access/apps/bffs/context.py b/enterprise_access/apps/bffs/context.py index 380ba12d..4dedfaa3 100644 --- a/enterprise_access/apps/bffs/context.py +++ b/enterprise_access/apps/bffs/context.py @@ -5,7 +5,6 @@ from rest_framework import status -from enterprise_access.apps.api_client.lms_client import LmsApiClient, LmsUserApiClient from enterprise_access.apps.bffs import serializers from enterprise_access.apps.bffs.api import ( get_and_cache_enterprise_customer_users, @@ -48,10 +47,6 @@ def __init__(self, request): self._enterprise_features = {} self.data = {} # Stores processed data for the response - # API clients - self.lms_api_client = LmsApiClient() - self.lms_user_api_client = LmsUserApiClient(request) - # Initialize common context data self._initialize_common_context_data() diff --git a/enterprise_access/apps/bffs/handlers.py b/enterprise_access/apps/bffs/handlers.py index 561df15d..90014e05 100644 --- a/enterprise_access/apps/bffs/handlers.py +++ b/enterprise_access/apps/bffs/handlers.py @@ -5,7 +5,15 @@ import logging from enterprise_access.apps.api_client.license_manager_client import LicenseManagerUserApiClient -from enterprise_access.apps.api_client.lms_client import LmsApiClient, LmsUserApiClient +from enterprise_access.apps.api_client.lms_client import LmsApiClient +from enterprise_access.apps.bffs.api import ( + get_and_cache_default_enterprise_enrollment_intentions_learner_status, + get_and_cache_enterprise_course_enrollments, + get_and_cache_subscription_licenses_for_learner, + invalidate_default_enterprise_enrollment_intentions_learner_status_cache, + invalidate_enterprise_course_enrollments_cache, + invalidate_subscription_licenses_cache +) from enterprise_access.apps.bffs.context import HandlerContext from enterprise_access.apps.bffs.mixins import BaseLearnerDataMixin from enterprise_access.apps.bffs.serializers import EnterpriseCustomerUserSubsidiesSerializer @@ -66,8 +74,8 @@ def __init__(self, context): super().__init__(context) # API Clients - self.license_manager_client = LicenseManagerUserApiClient(self.context.request) - self.lms_user_api_client = LmsUserApiClient(self.context.request) + self.license_manager_user_api_client = LicenseManagerUserApiClient(self.context.request) + self.lms_api_client = LmsApiClient() def load_and_process(self): """ @@ -174,7 +182,8 @@ def load_subscription_licenses(self): Load subscription licenses for the learner. """ try: - subscriptions_result = self.license_manager_client.get_subscription_licenses_for_learner( + subscriptions_result = get_and_cache_subscription_licenses_for_learner( + request=self.context.request, enterprise_customer_uuid=self.context.enterprise_customer_uuid, include_revoked=True, current_plans_only=False, @@ -288,7 +297,14 @@ def check_and_activate_assigned_license(self): if activation_key: try: # Perform side effect: Activate the assigned license - activated_license = self.license_manager_client.activate_license(activation_key) + activated_license = self.license_manager_user_api_client.activate_license(activation_key) + + # Invalidate the subscription licenses cache as the cached data changed + # with the now-activated license. + invalidate_subscription_licenses_cache( + enterprise_customer_uuid=self.context.enterprise_customer_uuid, + lms_user_id=self.context.lms_user_id, + ) except Exception as e: # pylint: disable=broad-exception-caught logger.exception(f"Error activating license {subscription_license.get('uuid')}") self.add_error( @@ -370,16 +386,22 @@ def check_and_auto_apply_license(self): try: # Perform side effect: Auto-apply license - auto_applied_license = self.license_manager_client.auto_apply_license(customer_agreement.get('uuid')) - if auto_applied_license: - # Update the context with the auto-applied license data - transformed_auto_applied_licenses = self.transform_subscription_licenses([auto_applied_license]) - licenses = self.subscription_licenses + transformed_auto_applied_licenses - subscription_licenses_by_status['activated'] = transformed_auto_applied_licenses - self.context.data['enterprise_customer_user_subsidies']['subscriptions'].update({ - 'subscription_licenses': licenses, - 'subscription_licenses_by_status': subscription_licenses_by_status, - }) + auto_applied_license = self.license_manager_user_api_client.auto_apply_license( + customer_agreement.get('uuid') + ) + # Invalidate the subscription licenses cache as the cached data changed with the auto-applied license. + invalidate_subscription_licenses_cache( + enterprise_customer_uuid=self.context.enterprise_customer_uuid, + lms_user_id=self.context.lms_user_id, + ) + # Update the context with the auto-applied license data + transformed_auto_applied_licenses = self.transform_subscription_licenses([auto_applied_license]) + licenses = self.subscription_licenses + transformed_auto_applied_licenses + subscription_licenses_by_status['activated'] = transformed_auto_applied_licenses + self.context.data['enterprise_customer_user_subsidies']['subscriptions'].update({ + 'subscription_licenses': licenses, + 'subscription_licenses_by_status': subscription_licenses_by_status, + }) except Exception as e: # pylint: disable=broad-exception-caught logger.exception("Error auto-applying license") self.add_error( @@ -391,12 +413,13 @@ def load_default_enterprise_enrollment_intentions(self): """ Load default enterprise course enrollments (stubbed) """ - client = self.lms_user_api_client try: - default_enrollment_intentions = client.get_default_enterprise_enrollment_intentions_learner_status( - enterprise_customer_uuid=self.context.enterprise_customer_uuid, - ) - self.context.data['default_enterprise_enrollment_intentions'] = default_enrollment_intentions + default_enterprise_enrollment_intentions =\ + get_and_cache_default_enterprise_enrollment_intentions_learner_status( + request=self.context.request, + enterprise_customer_uuid=self.context.enterprise_customer_uuid, + ) + self.context.data['default_enterprise_enrollment_intentions'] = default_enterprise_enrollment_intentions except Exception as e: # pylint: disable=broad-exception-caught logger.exception("Error loading default enterprise courses") self.add_error( @@ -436,9 +459,8 @@ def enroll_in_redeemable_default_enterprise_enrollment_intentions(self): 'is_default_auto_enrollment': True, }) - client = LmsApiClient() try: - response_payload = client.bulk_enroll_enterprise_learners( + response_payload = self.lms_api_client.bulk_enroll_enterprise_learners( self.context.enterprise_customer_uuid, bulk_enrollment_payload, ) @@ -466,6 +488,17 @@ def enroll_in_redeemable_default_enterprise_enrollment_intentions(self): 'subscription_license_uuid': license_uuids_by_course_run_key.get(course_run_key), }) + # Invalidate the default enterprise enrollment intentions and enterprise course enrollments cache + # as the previously redeemable enrollment intentions have been processed/enrolled. + invalidate_default_enterprise_enrollment_intentions_learner_status_cache( + enterprise_customer_uuid=self.context.enterprise_customer_uuid, + lms_user_id=self.context.lms_user_id, + ) + invalidate_enterprise_course_enrollments_cache( + enterprise_customer_uuid=self.context.enterprise_customer_uuid, + lms_user_id=self.context.lms_user_id, + ) + class DashboardHandler(BaseLearnerPortalHandler): """ @@ -501,7 +534,8 @@ def load_enterprise_course_enrollments(self): list: A list of enterprise course enrollments. """ try: - enterprise_course_enrollments = self.lms_user_api_client.get_enterprise_course_enrollments( + enterprise_course_enrollments = get_and_cache_enterprise_course_enrollments( + request=self.context.request, enterprise_customer_uuid=self.context.enterprise_customer_uuid, is_active=True, ) diff --git a/enterprise_access/apps/content_metadata/api.py b/enterprise_access/apps/content_metadata/api.py index f4ee7d2f..ea9a5513 100644 --- a/enterprise_access/apps/content_metadata/api.py +++ b/enterprise_access/apps/content_metadata/api.py @@ -15,10 +15,12 @@ logger = logging.getLogger(__name__) -DEFAULT_CACHE_TIMEOUT = getattr(settings, 'CONTENT_METADATA_CACHE_TIMEOUT', 60 * 5) - -def get_and_cache_catalog_content_metadata(enterprise_catalog_uuid, content_keys, timeout=None): +def get_and_cache_catalog_content_metadata( + enterprise_catalog_uuid, + content_keys, + timeout=settings.CONTENT_METADATA_CACHE_TIMEOUT, +): """ Returns the metadata corresponding to the requested ``content_keys`` within the provided ``enterprise_catalog_uuid``, @@ -70,7 +72,7 @@ def get_and_cache_catalog_content_metadata(enterprise_catalog_uuid, content_keys cache_key = cache_keys_by_content_key.get(fetched_record.get('key')) content_metadata_to_cache[cache_key] = fetched_record - cache.set_many(content_metadata_to_cache, timeout or DEFAULT_CACHE_TIMEOUT) + cache.set_many(content_metadata_to_cache, timeout) # Add to our results list everything we just had to fetch metadata_results_list.extend(fetched_metadata) diff --git a/enterprise_access/apps/subsidy_access_policy/customer_api.py b/enterprise_access/apps/subsidy_access_policy/customer_api.py index 0949b52b..bc752844 100644 --- a/enterprise_access/apps/subsidy_access_policy/customer_api.py +++ b/enterprise_access/apps/subsidy_access_policy/customer_api.py @@ -13,10 +13,12 @@ logger = logging.getLogger(__name__) -DEFAULT_CACHE_TIMEOUT = settings.ENTERPRISE_USER_RECORD_CACHE_TIMEOUT - -def get_and_cache_enterprise_learner_record(enterprise_customer_uuid, learner_id, timeout=DEFAULT_CACHE_TIMEOUT): +def get_and_cache_enterprise_learner_record( + enterprise_customer_uuid, + learner_id, + timeout=settings.ENTERPRISE_USER_RECORD_CACHE_TIMEOUT, +): """ Fetches the enterprise learner record from the Lms client if it exists. Uses the `learner_id` and `enterprise_customer_uuid` to determine if diff --git a/enterprise_access/settings/base.py b/enterprise_access/settings/base.py index 175efc02..ea84b161 100644 --- a/enterprise_access/settings/base.py +++ b/enterprise_access/settings/base.py @@ -467,6 +467,7 @@ def root(*path_fragments): ENTERPRISE_SUBSIDY_URL = '' ENTERPRISE_ACCESS_URL = '' +# API Client timeouts LICENSE_MANAGER_CLIENT_TIMEOUT = os.environ.get('LICENSE_MANAGER_CLIENT_TIMEOUT', 45) LMS_CLIENT_TIMEOUT = os.environ.get('LMS_CLIENT_TIMEOUT', 45) ECOMMERCE_CLIENT_TIMEOUT = os.environ.get('ECOMMERCE_CLIENT_TIMEOUT', 45) @@ -487,6 +488,7 @@ def root(*path_fragments): BRAZE_ASSIGNMENT_CANCELLED_NOTIFICATION_CAMPAIGN = '' BRAZE_ASSIGNMENT_AUTOMATIC_CANCELLATION_NOTIFICATION_CAMPAIGN = '' +# Braze configuration BRAZE_API_URL = '' BRAZE_API_KEY = os.environ.get('BRAZE_API_KEY', '') BRAZE_APP_ID = os.environ.get('BRAZE_APP_ID', '') @@ -505,11 +507,15 @@ def root(*path_fragments): SIMPLE_HISTORY_DATE_INDEX = False # Cache timeouts -SUBSIDY_RECORD_CACHE_TIMEOUT = 60 * 5 -ENTERPRISE_USER_RECORD_CACHE_TIMEOUT = 60 * 5 -SUBSIDY_AGGREGATES_CACHE_TIMEOUT = 60 * 10 - -ALL_ENTERPRISE_GROUP_MEMBERS_CACHE_TIMEOUT = 60 * 5 +DEFAULT_CACHE_TIMEOUT = 60 * 5 # 5 minutes +CONTENT_METADATA_CACHE_TIMEOUT = 60 * 30 # 30 minutes +ENTERPRISE_USER_RECORD_CACHE_TIMEOUT = 60 * 10 # 10 minutes +SUBSIDY_AGGREGATES_CACHE_TIMEOUT = 60 * 10 # 10 minutes +SUBSCRIPTION_LICENSES_LEARNER_CACHE_TIMEOUT = 60 * 2 # 2 minutes +SUBSIDY_RECORD_CACHE_TIMEOUT = DEFAULT_CACHE_TIMEOUT +DEFAULT_ENTERPRISE_ENROLLMENT_INTENTIONS_CACHE_TIMEOUT = DEFAULT_CACHE_TIMEOUT +ENTERPRISE_COURSE_ENROLLMENTS_CACHE_TIMEOUT = DEFAULT_CACHE_TIMEOUT +ALL_ENTERPRISE_GROUP_MEMBERS_CACHE_TIMEOUT = DEFAULT_CACHE_TIMEOUT BRAZE_GROUP_EMAIL_FORCE_REMIND_ALL_PENDING_LEARNERS = False BRAZE_GROUPS_EMAIL_AUTO_REMINDER_DAY_5_CAMPAIGN = ''