Skip to content

Commit

Permalink
feat: initialize enterprise metadata in HandlerContext; integrate APIs
Browse files Browse the repository at this point in the history
  • Loading branch information
adamstankiewicz committed Nov 13, 2024
1 parent 5545e9e commit b460c5e
Show file tree
Hide file tree
Showing 6 changed files with 903 additions and 203 deletions.
72 changes: 68 additions & 4 deletions enterprise_access/apps/api_client/lms_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -406,13 +416,67 @@ 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/'
)
enterprise_course_enrollments_endpoint = (
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.
Expand Down
221 changes: 206 additions & 15 deletions enterprise_access/apps/bffs/context.py
Original file line number Diff line number Diff line change
@@ -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


Expand All @@ -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):
Expand All @@ -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):
Expand All @@ -43,23 +54,203 @@ 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

@property
def enterprise_customer(self):
return self.data.get('enterprise_customer')

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

# Initialize the enterprise customer users metatata derived from the LMS
self._initialize_enterprise_customer_users()

if not (enterprise_customer := self.data.get('enterprise_customer')):
# If no enterprise customer is found, return early
return

# Otherwise, update the enterprise customer UUID and slug if not already set
if not self.enterprise_customer_slug:
self._enterprise_customer_slug = enterprise_customer.get('slug')
if not self.enterprise_customer_uuid:
self._enterprise_customer_uuid = 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: # pylint: disable=broad-except
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', {})

# Parse the enterprise customer user data
enterprise_customer_users = 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
):
try:
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,
)
except Exception as e: # pylint: disable=broad-except
self.add_error(
user_message='Error retrieving enterprise customer data',
developer_message=str(e)
)

# Determine the enterprise customer user to display
requested_enterprise_customer = (
enterprise_customer_user_for_requested_customer.get('enterprise_customer')
if enterprise_customer_user_for_requested_customer else None
)
enterprise_customer = self._determine_enterprise_customer_for_display(
active_enterprise_customer=active_enterprise_customer,
requested_enterprise_customer=requested_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': 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.
"""
if not enterprise_customer:
return False

# 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.get('slug') == self.enterprise_customer_slug or
enterprise_customer.get('uuid') == self.enterprise_customer_uuid
)

def add_error(self, **kwargs):
"""
Expand Down
Loading

0 comments on commit b460c5e

Please sign in to comment.