From d04cca4dac8e79e7af3a530f77d2c791af3b9143 Mon Sep 17 00:00:00 2001 From: Adam Stankiewicz Date: Tue, 12 Nov 2024 12:16:57 -0500 Subject: [PATCH] feat: initialize enterprise metadata in HandlerContext --- .../apps/api_client/lms_client.py | 72 +++++- enterprise_access/apps/bffs/context.py | 244 ++++++++++++++++-- enterprise_access/apps/bffs/handlers.py | 5 +- 3 files changed, 299 insertions(+), 22 deletions(-) diff --git a/enterprise_access/apps/api_client/lms_client.py b/enterprise_access/apps/api_client/lms_client.py index b73a7b7b..7f22f087 100755 --- a/enterprise_access/apps/api_client/lms_client.py +++ b/enterprise_access/apps/api_client/lms_client.py @@ -64,21 +64,31 @@ def enterprise_group_members_endpoint(self, group_uuid): "learners/", ) - def get_enterprise_customer_data(self, enterprise_customer_uuid): + def get_enterprise_customer_data(self, enterprise_customer_uuid=None, enterprise_customer_slug=None): """ - Gets the data for an EnterpriseCustomer for the given uuid. + Gets the data for an EnterpriseCustomer for the given uuid or slug. Arguments: enterprise_customer_uuid (string): id of the enterprise customer + enterprise_customer_slug (string): slug of the enterprise customer Returns: dictionary containing enterprise customer metadata """ + if enterprise_customer_uuid: + endpoint = f'{self.enterprise_customer_endpoint}{enterprise_customer_uuid}/' + elif enterprise_customer_slug: + endpoint = f'{self.enterprise_customer_endpoint}?slug={enterprise_customer_slug}' + else: + raise ValueError('Either enterprise_customer_uuid or enterprise_customer_slug is required.') try: - endpoint = f'{self.enterprise_customer_endpoint}{enterprise_customer_uuid}/' response = self.client.get(endpoint, timeout=settings.LMS_CLIENT_TIMEOUT) response.raise_for_status() - return response.json() + if enterprise_customer_uuid: + # If we're fetching by UUID, we expect a single result + return response.json() + # If we're fetching by slug, we expect a list of results + return response.json().get('results', [])[0] except requests.exceptions.HTTPError as exc: logger.exception(exc) raise @@ -406,6 +416,7 @@ class LmsUserApiClient(BaseUserApiClient): enterprise_api_base_url = f"{settings.LMS_URL}/enterprise/api/v1/" enterprise_learner_portal_api_base_url = f"{settings.LMS_URL}/enterprise_learner_portal/api/v1/" + enterprise_learner_endpoint = f"{enterprise_api_base_url}enterprise-learner/" default_enterprise_enrollment_intentions_learner_status_endpoint = ( f'{enterprise_api_base_url}default-enterprise-enrollment-intentions/learner-status/' ) @@ -413,6 +424,59 @@ class LmsUserApiClient(BaseUserApiClient): f'{enterprise_learner_portal_api_base_url}enterprise_course_enrollments/' ) + def get_enterprise_customers_for_user(self, username, traverse_pagination=False): + """ + Fetches enterprise learner data for a given username. + + Arguments: + username (str): Username of the learner + + Returns: + dict: Dictionary representation of the JSON response from the API + """ + query_params = { + 'username': username, + } + results = [] + initial_response_data = None + current_response = None + next_url = self.enterprise_learner_endpoint + try: + while next_url: + current_response = self.get( + next_url, + params=query_params, + timeout=settings.LMS_CLIENT_TIMEOUT + ) + current_response.raise_for_status() + data = current_response.json() + + if not initial_response_data: + # Store the initial response data (first page) for later use + initial_response_data = data + + # Collect results from the current page + results.extend(data.get('results', [])) + + # If pagination is enabled, continue with the next page; otherwise, break + next_url = data.get('next') if traverse_pagination else None + + consolidated_response = { + **initial_response_data, + 'next': None, + 'previous': None, + 'count': len(results), + 'num_pages': 1, + 'results': results, + } + return consolidated_response + except requests.exceptions.HTTPError as exc: + logger.exception( + f"Failed to fetch enterprise learner for learner {username}: {exc} " + f"Response content: {current_response.content if current_response else None}" + ) + raise + def get_default_enterprise_enrollment_intentions_learner_status(self, enterprise_customer_uuid): """ Fetches learner status from the default enterprise enrollment intentions endpoint. diff --git a/enterprise_access/apps/bffs/context.py b/enterprise_access/apps/bffs/context.py index ea056078..8dc16a11 100644 --- a/enterprise_access/apps/bffs/context.py +++ b/enterprise_access/apps/bffs/context.py @@ -1,6 +1,8 @@ """ HandlerContext for bffs app. """ + +from enterprise_access.apps.api_client.lms_client import LmsApiClient, LmsUserApiClient from enterprise_access.apps.bffs import serializers @@ -15,8 +17,10 @@ class HandlerContext: data: A dictionary to store data loaded and processed by the handlers. errors: A list to store errors that occur during request processing. warnings: A list to store warnings that occur during the request processing. - enterprise_customer_uuid: The enterprise customer the user is associated with. + enterprise_customer_uuid: The enterprise customer uuid associated with this request. + enterprise_customer_slug: The enterprise customer slug associated with this request. lms_user_id: The id associated with the authenticated user. + enterprise_features: A dictionary to store enterprise features associated with the authenticated user. """ def __init__(self, request): @@ -26,14 +30,21 @@ def __init__(self, request): request: The incoming HTTP request. """ self._request = request + self._errors = [] # Stores any errors that occur during processing + self._warnings = [] # Stores any warnings that occur during processing + self._enterprise_customer_uuid = None + self._enterprise_customer_slug = None + self._lms_user_id = getattr(self.user, 'lms_user_id', None) + self._enterprise_features = {} + self.data = {} # Stores processed data for the response - self.errors = [] # Stores any errors that occur during processing - self.warnings = [] # Stores any warnings that occur during processing - self.enterprise_customer_uuid = None - self.lms_user_id = None - # Set common context attributes - self.initialize_common_context_data() + # Common API clients + self.lms_api_client = LmsApiClient() + self.lms_user_api_client = LmsUserApiClient(request) + + # Initialize common context data + self._initialize_common_context_data() @property def request(self): @@ -43,23 +54,226 @@ def request(self): def user(self): return self._request.user - def initialize_common_context_data(self): + @property + def errors(self): + return self._errors + + @property + def warnings(self): + return self._warnings + + @property + def enterprise_customer_uuid(self): + return self._enterprise_customer_uuid + + @property + def enterprise_customer_slug(self): + return self._enterprise_customer_slug + + @property + def lms_user_id(self): + return self._lms_user_id + + @property + def enterprise_features(self): + return self._enterprise_features + + def _initialize_common_context_data(self): """ - Initialize commonly used context attributes, such as enterprise customer UUID and LMS user ID. + Initializes common context data, like enterprise customer UUID and user ID. """ enterprise_uuid_query_param = self.request.query_params.get('enterprise_customer_uuid') + enterprise_slug_query_param = self.request.query_params.get('enterprise_customer_slug') + enterprise_uuid_post_param = None + enterprise_slug_post_param = None if self.request.method == 'POST': enterprise_uuid_post_param = self.request.data.get('enterprise_customer_uuid') + enterprise_slug_post_param = self.request.data.get('enterprise_customer_slug') enterprise_customer_uuid = enterprise_uuid_query_param or enterprise_uuid_post_param - if enterprise_customer_uuid: - self.enterprise_customer_uuid = enterprise_customer_uuid - else: - raise ValueError("enterprise_customer_uuid is required for this request.") + self._enterprise_customer_uuid = enterprise_customer_uuid + enterprise_customer_slug = enterprise_slug_query_param or enterprise_slug_post_param + self._enterprise_customer_slug = enterprise_customer_slug + + self._initialize_enterprise_customer_users() + + if not self.enterprise_customer_slug: + self._enterprise_customer_slug = self.data.get('enterprise_customer', {}).get('slug') + + if not self.enterprise_customer_uuid: + self._enterprise_customer_uuid = self.data.get('enterprise_customer', {}).get('uuid') + + def _initialize_enterprise_customer_users(self): + """ + Initializes the enterprise customer users for the request user. + """ + try: + enterprise_customer_users_data = self.lms_user_api_client.get_enterprise_customers_for_user( + self.user.username, + traverse_pagination=True + ) + except Exception as e: + self.add_error( + user_message='Error retrieving linked enterprise customers', + developer_message=str(e) + ) + return + + # Set enterprise features from the response + self._enterprise_features = enterprise_customer_users_data.get('enterprise_features', {}) + + # Transform the enterprise customer user data + enterprise_customer_users = [ + self._transform_enterprise_customer_user(enterprise_customer_user) + for enterprise_customer_user in enterprise_customer_users_data.get('results', []) + ] + active_enterprise_customer_user = next( + ( + enterprise_customer_user + for enterprise_customer_user in enterprise_customer_users + if enterprise_customer_user.get('active', False) + ), + None + ) + active_enterprise_customer = ( + active_enterprise_customer_user.get('enterprise_customer') + if active_enterprise_customer_user else None + ) + enterprise_customer_user_for_requested_customer = next( + ( + enterprise_customer_user + for enterprise_customer_user in enterprise_customer_users + if self._enterprise_customer_matches_slug_or_uuid(enterprise_customer_user.get('enterprise_customer')) + ), + None + ) + + # If no enterprise customer user is found for the requested customer (i.e., request user not explicitly + # linked), but the request user is staff, attempt to retrieve enterprise customer metadata from the + # `/enterprise-customer/` LMS API endpoint instead. + staff_enterprise_customer = None + has_enterprise_customer_slug_or_uuid = self.enterprise_customer_slug or self.enterprise_customer_uuid + if ( + not enterprise_customer_user_for_requested_customer + and has_enterprise_customer_slug_or_uuid + and self.user.is_staff + ): + staff_enterprise_customer = self.lms_api_client.get_enterprise_customer_data( + enterprise_customer_uuid=self.enterprise_customer_uuid, + enterprise_customer_slug=self.enterprise_customer_slug, + ) + + # Determine the enterprise customer user to display + enterprise_customer = self._determine_enterprise_customer_for_display( + active_enterprise_customer=active_enterprise_customer, + requested_enterprise_customer=enterprise_customer_user_for_requested_customer.get('enterprise_customer'), + staff_enterprise_customer=staff_enterprise_customer, + ) + + # Update the context data with the enterprise customer user information + self.data = { + **self.data, + 'enterprise_customer': enterprise_customer, + 'active_enterprise_customer': active_enterprise_customer, + 'all_linked_enterprise_customer_users': enterprise_customer_users, + 'staff_enterprise_customer': self._transform_enterprise_customer(staff_enterprise_customer), + } + + def _determine_enterprise_customer_for_display( + self, + active_enterprise_customer=None, + requested_enterprise_customer=None, + staff_enterprise_customer=None, + ): + """ + Determine the enterprise customer user for display. + + Returns: + The enterprise customer user for display. + """ + if not self.enterprise_customer_slug and not self.enterprise_customer_uuid: + # No enterprise customer specified in the request, so return the active enterprise customer user + return active_enterprise_customer + + slug_matches_active_enterprise_customer = ( + active_enterprise_customer and active_enterprise_customer.get('slug') == self.enterprise_customer_slug + ) + uuid_matches_active_enterprise_customer = ( + active_enterprise_customer and active_enterprise_customer.get('uuid') == self.enterprise_customer_uuid + ) + request_matches_active_enterprise_customer = ( + slug_matches_active_enterprise_customer or uuid_matches_active_enterprise_customer + ) + + # If the requested enterprise does not match the active enterprise customer user's slug/uuid + # and there is a linked enterprise customer user for the requested enterprise, return the + # linked enterprise customer. + if not request_matches_active_enterprise_customer and requested_enterprise_customer: + return requested_enterprise_customer + + # If the request user is staff and the requested enterprise does not match the active enterprise + # customer user's slug/uuid, return the staff-enterprise customer. + if staff_enterprise_customer: + return staff_enterprise_customer + + # Otherwise, return the active enterprise customer. + return active_enterprise_customer + + def _enterprise_customer_matches_slug_or_uuid(self, enterprise_customer): + """ + Check if the enterprise customer matches the slug or UUID. + Args: + enterprise_customer: The enterprise customer data. + Returns: + True if the enterprise customer matches the slug or UUID, otherwise False. + """ + return ( + enterprise_customer.get('slug') == self.enterprise_customer_slug + or enterprise_customer.get('uuid') == self.enterprise_customer_uuid + ) + + def _transform_enterprise_customer_user(self, enterprise_customer_user): + """ + Transform the enterprise customer user data. + + Args: + enterprise_customer_user: The enterprise customer user data. + Returns: + The transformed enterprise customer user data. + """ + enterprise_customer = enterprise_customer_user.get('enterprise_customer') + return { + **enterprise_customer_user, + 'enterprise_customer': self._transform_enterprise_customer(enterprise_customer) + } + + def _transform_enterprise_customer(self, enterprise_customer): + """ + Transform the enterprise customer data. + + Args: + enterprise_customer: The enterprise customer data. + Returns: + The transformed enterprise customer data. + """ + if not enterprise_customer or not enterprise_customer.get('enable_learner_portal', False): + # If the enterprise customer does not exist or the learner portal is not enabled, return None + return None + + # Learner Portal is enabled, so transform the enterprise customer data. + identity_provider = enterprise_customer.get("identity_provider") + disable_search = bool( + not enterprise_customer.get("enable_integrated_customer_learner_portal_search", False) + and identity_provider + ) + show_integration_warning = bool(not disable_search and identity_provider) - # Set lms_user_id from the authenticated user object in the request - self.lms_user_id = getattr(self.user, 'lms_user_id', None) + return { + **enterprise_customer, + 'disable_search': disable_search, + 'show_integration_warning': show_integration_warning, + } def add_error(self, **kwargs): """ diff --git a/enterprise_access/apps/bffs/handlers.py b/enterprise_access/apps/bffs/handlers.py index da5c130b..1d02fb96 100644 --- a/enterprise_access/apps/bffs/handlers.py +++ b/enterprise_access/apps/bffs/handlers.py @@ -70,8 +70,6 @@ def load_and_process(self): """ try: # Retrieve and process subscription licenses. Handles activation and auto-apply logic. - # TODO: retrieve enterprise customer metadata,ENT-9629 - # self.load_enterprise_customer() self.load_and_process_subscription_licenses() # Retrieve default enterprise courses and enroll in the redeemable ones @@ -265,8 +263,9 @@ def check_and_auto_apply_license(self): bool(customer_agreement.get('subscription_for_auto_applied_licenses')) and customer_agreement.get('net_days_until_expiration') > 0 ) + enterprise_customer = self.context.data.get('enterprise_customer', {}) idp_or_univeral_link_enabled = ( - # TODO: IDP from customer, ENT-9629 + enterprise_customer.get('identity_provider') or customer_agreement.get('enable_auto_applied_subscriptions_with_universal_link') ) is_eligible_for_auto_apply = has_subscription_plan_for_auto_apply and idp_or_univeral_link_enabled