Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: initialize enterprise metadata in HandlerContext; complete auto-apply business logic #586

Merged
merged 7 commits into from
Nov 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 69 additions & 5 deletions enterprise_access/apps/api_client/lms_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ def all_pages_enterprise_group_members_cache_key(


class LmsApiClient(BaseOAuthClient):

"""
API client for calls to the LMS service.
"""
Expand All @@ -64,21 +63,32 @@ 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:
# Returns a dict
endpoint = f'{self.enterprise_customer_endpoint}{enterprise_customer_uuid}/'
elif enterprise_customer_slug:
# Returns a list of dicts
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()
payload = response.json()
if results := payload.get('results'):
return results[0]
return payload
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,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

initial_response_data will contain keys like results, next, etc. right? Is that intended?

Copy link
Member Author

@adamstankiewicz adamstankiewicz Nov 14, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct, it is intended. Then the subsequent lines below overrides relevant fields like next, prev, etc. to reflect the traversed pagination results being a single page.

The primary reason to spread **initial_response_data is bring along any other existing pagination fields unrelated to page length, i.e. I believe **initial_response_data brings along start: 0 as well without needing to explicitly define it. The start value from the initial response is unchanged regardless of traverse_pagination.

'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
43 changes: 39 additions & 4 deletions enterprise_access/apps/api_client/tests/test_lms_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,21 +109,48 @@ def test_get_enterprise_admin_users(self, mock_oauth_client, mock_json):
timeout=settings.LMS_CLIENT_TIMEOUT,
)

@ddt.data(
{
'enterprise_uuid': 'some-uuid',
'enterprise_slug': None
},
{
'enterprise_uuid': None,
'enterprise_slug': 'some-slug',
},
{
'enterprise_uuid': 'some-uuid',
'enterprise_slug': 'some-slug',
},
)
@mock.patch('requests.Response.json')
@mock.patch('enterprise_access.apps.api_client.base_oauth.OAuthAPIClient')
def test_get_enterprise_customer_data(self, mock_oauth_client, mock_json):
@ddt.unpack
def test_get_enterprise_customer_data(
self,
mock_oauth_client,
mock_json,
enterprise_uuid,
enterprise_slug,
):
"""
Verify client hits the right URL for entepriseCustomer data.
"""
mock_json.return_value = {
mock_enterprise_customer = {
'uuid': 'some-uuid',
'slug': 'some-test-slug',
}
mock_json.return_value = mock_enterprise_customer
if not enterprise_uuid and enterprise_slug:
mock_json.return_value = {'results': [mock_enterprise_customer]}
mock_oauth_client.return_value.get.return_value = requests.Response()
mock_oauth_client.return_value.get.return_value.status_code = 200

client = LmsApiClient()
customer_data = client.get_enterprise_customer_data('some-uuid')
customer_data = client.get_enterprise_customer_data(
enterprise_customer_uuid=enterprise_uuid,
enterprise_customer_slug=enterprise_slug,
)

assert customer_data['uuid'] == 'some-uuid'
assert customer_data['slug'] == 'some-test-slug'
Expand All @@ -132,8 +159,16 @@ def test_get_enterprise_customer_data(self, mock_oauth_client, mock_json):
'http://edx-platform.example.com/'
'enterprise/api/v1/'
'enterprise-customer/'
'some-uuid/'
f'{enterprise_uuid}/'
)
if not enterprise_uuid and enterprise_slug:
expected_url = (
'http://edx-platform.example.com/'
'enterprise/api/v1/'
'enterprise-customer/'
f'?slug={enterprise_slug}'
)

mock_oauth_client.return_value.get.assert_called_with(
expected_url,
timeout=settings.LMS_CLIENT_TIMEOUT,
Expand Down
Loading