Skip to content

Commit

Permalink
Merge branch 'main' of github.com:openedx/enterprise-access
Browse files Browse the repository at this point in the history
  • Loading branch information
brobro10000 committed Sep 10, 2024
2 parents c13c560 + 53875bd commit 9c122f9
Show file tree
Hide file tree
Showing 21 changed files with 384 additions and 95 deletions.
7 changes: 7 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
version: 2
updates:
# Adding new check for github-actions
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,8 @@ class Meta:
'learner_email',
'lms_user_id',
'content_key',
'parent_content_key',
'is_assigned_course_run',
'content_title',
'content_quantity',
'state',
Expand Down Expand Up @@ -184,8 +186,12 @@ def get_error_reason(self, assignment):
Resolves the error reason for the assignment, if any, for display purposes based on
any associated actions.
"""
# If the assignment is not in an errored state, there should be no error reason.
if assignment.state != LearnerContentAssignmentStateChoices.ERRORED:
# If the assignment is not in an errored or allocated state,
# there should be no error reason.
if assignment.state not in (
LearnerContentAssignmentStateChoices.ERRORED,
LearnerContentAssignmentStateChoices.ALLOCATED,
):
return None

# Assignment is an errored state, so determine the appropriate error reason so clients don't need to.
Expand Down
57 changes: 41 additions & 16 deletions enterprise_access/apps/api/v1/tests/test_allocation_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,8 @@ def setUp(self):
self.mock_catalog_result = {
'count': 2,
'results': [
{'key': 'course+A', 'data': 'things'}, {'key': 'course+B', 'data': 'stuff'},
{'key': 'course+A', 'data': 'things'},
{'key': 'course+B', 'data': 'stuff'},
],
}

Expand Down Expand Up @@ -229,6 +230,8 @@ def test_allocate_happy_path(self, mock_catalog_client, mock_allocate, mock_subs
'learner_email': '[email protected]',
'lms_user_id': None,
'content_key': self.content_key,
'parent_content_key': None,
'is_assigned_course_run': False,
'content_title': self.content_title,
'content_quantity': -123,
'state': LearnerContentAssignmentStateChoices.ERRORED,
Expand All @@ -249,6 +252,8 @@ def test_allocate_happy_path(self, mock_catalog_client, mock_allocate, mock_subs
'learner_email': '[email protected]',
'lms_user_id': None,
'content_key': self.content_key,
'parent_content_key': None,
'is_assigned_course_run': False,
'content_title': self.content_title,
'content_quantity': -456,
'state': LearnerContentAssignmentStateChoices.ALLOCATED,
Expand All @@ -269,6 +274,8 @@ def test_allocate_happy_path(self, mock_catalog_client, mock_allocate, mock_subs
'learner_email': '[email protected]',
'lms_user_id': None,
'content_key': self.content_key,
'parent_content_key': None,
'is_assigned_course_run': False,
'content_title': self.content_title,
'content_quantity': -789,
'state': LearnerContentAssignmentStateChoices.ALLOCATED,
Expand Down Expand Up @@ -420,7 +427,8 @@ class TestSubsidyAccessPolicyAllocationEndToEnd(APITestWithMocks):
def setUpTestData(cls):
super().setUpTestData()
cls.enterprise_uuid = OTHER_TEST_ENTERPRISE_UUID
cls.content_key = 'course-v1:edX+edXPrivacy101+3T2020'
cls.content_key = 'course-v1:edX+Privacy101+3T2020'
cls.parent_content_key = 'edX+Privacy101'
cls.content_title = 'edX: Privacy 101'

# Create a pair of AssignmentConfiguration + SubsidyAccessPolicy for the main test customer.
Expand All @@ -435,6 +443,18 @@ def setUpTestData(cls):
spend_limit=10000 * 100,
)

# Mock results from the catalog content metadata API endpoint.
cls.mock_catalog_result = {
'count': 2,
'results': [
{
'key': cls.content_key,
'parent_content_key': cls.parent_content_key,
'data': 'things',
},
],
}

def setUp(self):
super().setUp()
self.maxDiff = None
Expand All @@ -453,14 +473,6 @@ def setUp(self):
def delete_assignments():
return self.assignment_configuration.assignments.all().delete()

# Mock results from the catalog content metadata API endpoint.
self.mock_catalog_result = {
'count': 2,
'results': [
{'key': 'course+A', 'data': 'things'}, {'key': 'course+B', 'data': 'stuff'},
],
}

self.addCleanup(delete_assignments)

@mock.patch.object(
Expand Down Expand Up @@ -509,6 +521,8 @@ def test_allocate_happy_path_e2e(
"""
mock_get_and_cache_content_metadata.return_value = {
'content_title': self.content_title,
'content_key': self.parent_content_key,
'course_run_key': self.content_key,
}
mock_get_content_price.return_value = 123.45 * 100
mock_aggregates_for_policy.return_value = {
Expand Down Expand Up @@ -539,6 +553,8 @@ def test_allocate_happy_path_e2e(
lms_user_id=None,
cancelled_at=timezone.now(),
content_key=self.content_key,
parent_content_key=self.parent_content_key,
is_assigned_course_run=True,
content_title=self.content_title,
content_quantity=-12345,
state=LearnerContentAssignmentStateChoices.CANCELLED,
Expand All @@ -547,15 +563,18 @@ def test_allocate_happy_path_e2e(
email='[email protected]',
lms_user_id=4277
)
assignment_content_quantity_usd_cents = 12345
LearnerContentAssignmentFactory(
assignment_configuration=self.assignment_configuration,
learner_email='[email protected]',
lms_user_id=expired_user.lms_user_id,
expired_at=timezone.now(),
errored_at=timezone.now(),
content_key=self.content_key,
parent_content_key=self.parent_content_key,
is_assigned_course_run=True,
content_title=self.content_title,
content_quantity=-12345,
content_quantity=-assignment_content_quantity_usd_cents,
state=LearnerContentAssignmentStateChoices.EXPIRED,
)

Expand All @@ -571,7 +590,7 @@ def test_allocate_happy_path_e2e(
allocate_payload = {
'learner_emails': ['[email protected]', '[email protected]', '[email protected]'],
'content_key': self.content_key,
'content_price_cents': 123.45 * 100, # policy limit is 100000.00 USD, so this should be well below limit
'content_price_cents': assignment_content_quantity_usd_cents, # this should be well below limit
}

response = self.client.post(allocate_url, data=allocate_payload)
Expand All @@ -581,7 +600,7 @@ def test_allocate_happy_path_e2e(
for assignment in self.assignment_configuration.assignments.filter(
state=LearnerContentAssignmentStateChoices.ALLOCATED,
content_key=self.content_key,
content_quantity=-123.45 * 100,
content_quantity=-assignment_content_quantity_usd_cents
)
}
self.assertEqual(3, len(allocation_records_by_email))
Expand All @@ -594,8 +613,10 @@ def test_allocate_happy_path_e2e(
'learner_email': '[email protected]',
'lms_user_id': foo_user.lms_user_id,
'content_key': self.content_key,
'parent_content_key': self.parent_content_key,
'is_assigned_course_run': True,
'content_title': self.content_title,
'content_quantity': -123.45 * 100,
'content_quantity': -assignment_content_quantity_usd_cents,
'state': LearnerContentAssignmentStateChoices.ALLOCATED,
'transaction_uuid': None,
'uuid': str(foo_record.uuid),
Expand All @@ -617,8 +638,10 @@ def test_allocate_happy_path_e2e(
'learner_email': '[email protected]',
'lms_user_id': None,
'content_key': self.content_key,
'parent_content_key': self.parent_content_key,
'is_assigned_course_run': True,
'content_title': self.content_title,
'content_quantity': -123.45 * 100,
'content_quantity': -assignment_content_quantity_usd_cents,
'state': LearnerContentAssignmentStateChoices.ALLOCATED,
'transaction_uuid': None,
'uuid': str(canceled_record.uuid),
Expand All @@ -635,8 +658,10 @@ def test_allocate_happy_path_e2e(
'learner_email': '[email protected]',
'lms_user_id': expired_user.lms_user_id,
'content_key': self.content_key,
'parent_content_key': self.parent_content_key,
'is_assigned_course_run': True,
'content_title': self.content_title,
'content_quantity': -123.45 * 100,
'content_quantity': -assignment_content_quantity_usd_cents,
'state': LearnerContentAssignmentStateChoices.ALLOCATED,
'transaction_uuid': None,
'uuid': str(expired_record.uuid),
Expand Down
89 changes: 79 additions & 10 deletions enterprise_access/apps/api/v1/tests/test_assignment_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

from enterprise_access.apps.content_assignments.constants import (
NUM_DAYS_BEFORE_AUTO_EXPIRATION,
AssignmentActionErrors,
AssignmentActions,
AssignmentAutomaticExpiredReason,
AssignmentLearnerStates,
Expand Down Expand Up @@ -105,6 +106,19 @@ def setUp(self):

self.now = localized_utcnow()

self.content_metadata_one = {
'content_key': 'course-v1:edX+Accessibility101+T2024a',
'parent_content_key': 'edX+Accessibility101',
'content_title': 'edx: Accessibility 101',
'content_quantity': -123,
}
self.content_metadata_two = {
'content_key': 'course-v1:edX+Privacy101+T2024a',
'parent_content_key': 'edX+Privacy101',
'content_title': 'edx: Privacy 101',
'content_quantity': -321,
}

# This assignment has just been allocated, so its lms_user_id is null.
self.assignment_allocated_pre_link = LearnerContentAssignmentFactory(
state=LearnerContentAssignmentStateChoices.ALLOCATED,
Expand All @@ -119,9 +133,11 @@ def setUp(self):
lms_user_id=TEST_OTHER_LMS_USER_ID,
transaction_uuid=None,
assignment_configuration=self.assignment_configuration,
content_key='edX+edXPrivacy101',
content_quantity=-321,
content_title='edx: Privacy 101'
content_key=self.content_metadata_two['content_key'],
parent_content_key=self.content_metadata_two['parent_content_key'],
is_assigned_course_run=True,
content_quantity=self.content_metadata_two['content_quantity'],
content_title=self.content_metadata_two['content_title'],
)
self.assignment_allocated_post_link.add_successful_linked_action()
self.assignment_allocated_post_link.add_successful_notified_action()
Expand All @@ -145,9 +161,11 @@ def setUp(self):
lms_user_id=TEST_OTHER_LMS_USER_ID,
transaction_uuid=uuid4(),
assignment_configuration=self.assignment_configuration,
content_key='edX+edXAccessibility101',
content_quantity=-123,
content_title='edx: Accessibility 101'
content_key=self.content_metadata_one['content_key'],
parent_content_key=self.content_metadata_one['parent_content_key'],
is_assigned_course_run=True,
content_quantity=self.content_metadata_one['content_quantity'],
content_title=self.content_metadata_one['content_title'],
)
self.assignment_accepted.add_successful_linked_action()
self.assignment_accepted.add_successful_notified_action()
Expand Down Expand Up @@ -417,6 +435,8 @@ def test_retrieve(self, role_context_dict, mock_subsidy_record, mock_catalog_cli
'uuid': str(self.assignment_allocated_pre_link.uuid),
'assignment_configuration': str(self.assignment_allocated_pre_link.assignment_configuration.uuid),
'content_key': self.assignment_allocated_pre_link.content_key,
'parent_content_key': self.assignment_allocated_pre_link.parent_content_key,
'is_assigned_course_run': self.assignment_allocated_pre_link.is_assigned_course_run,
'content_title': self.assignment_allocated_pre_link.content_title,
'content_quantity': self.assignment_allocated_pre_link.content_quantity,
'learner_email': self.assignment_allocated_pre_link.learner_email,
Expand Down Expand Up @@ -448,6 +468,53 @@ def test_retrieve(self, role_context_dict, mock_subsidy_record, mock_catalog_cli
},
}

@ddt.data(
# A good admin role, and with a context matching the main testing customer.
{'system_wide_role': SYSTEM_ENTERPRISE_ADMIN_ROLE, 'context': str(TEST_ENTERPRISE_UUID)},
# A good operator role, and with a context matching the main testing customer.
{'system_wide_role': SYSTEM_ENTERPRISE_OPERATOR_ROLE, 'context': str(TEST_ENTERPRISE_UUID)},
)
@mock.patch('enterprise_access.apps.content_metadata.api.EnterpriseCatalogApiClient', autospec=True)
@mock.patch.object(SubsidyAccessPolicy, 'subsidy_record', autospec=True)
def test_retrieve_allocated_with_notification_error(
self, role_context_dict, mock_subsidy_record, mock_catalog_client
):
assignment = LearnerContentAssignmentFactory(
state=LearnerContentAssignmentStateChoices.ALLOCATED,
lms_user_id=98123,
transaction_uuid=None,
assignment_configuration=self.assignment_configuration,
content_key='edX+edXPrivacy101',
content_quantity=-321,
content_title='edx: Privacy 101'
)
assignment.add_errored_notified_action(Exception('foo'))

# Set the JWT-based auth that we'll use for every request.
self.set_jwt_cookie([role_context_dict])

# Mock results from the catalog content metadata API endpoint.
mock_catalog_client.return_value.catalog_content_metadata.return_value = self.mock_catalog_result

# Mock results from the subsidy record.
mock_subsidy_record.return_value = self.mock_subsidy_record

# Setup and call the retrieve endpoint.
detail_kwargs = {
'assignment_configuration_uuid': str(TEST_ASSIGNMENT_CONFIG_UUID),
'uuid': str(assignment.uuid),
}
detail_url = reverse('api:v1:admin-assignments-detail', kwargs=detail_kwargs)
response = self.client.get(detail_url)

assert response.status_code == status.HTTP_200_OK
assert response.json().get('state') == LearnerContentAssignmentStateChoices.ALLOCATED
assert response.json().get('learner_state') == AssignmentLearnerStates.FAILED
assert response.json().get('error_reason') == {
'action_type': AssignmentActions.NOTIFIED,
'error_reason': AssignmentActionErrors.EMAIL_ERROR,
}

@ddt.data(
# A good admin role, and with a context matching the main testing customer.
{'system_wide_role': SYSTEM_ENTERPRISE_ADMIN_ROLE, 'context': str(TEST_ENTERPRISE_UUID)},
Expand Down Expand Up @@ -748,13 +815,13 @@ def test_nudge_happy_path(self, mock_send_nudge_email, mock_content_metadata_for

# Mock content metadata for assignment
mock_content_metadata_for_assignments.return_value = {
'edX+edXAccessibility101': {
'key': 'edX+edXAccessibility101',
self.content_metadata_one['content_key']: {
'key': self.content_metadata_one['parent_content_key'],
'normalized_metadata': {
'start_date': start_date.strftime("%Y-%m-%dT%H:%M:%SZ"),
'end_date': end_date.strftime("%Y-%m-%dT%H:%M:%SZ"),
'enroll_by_date': enrollment_end.strftime("%Y-%m-%dT%H:%M:%SZ"),
'content_price': 123,
'content_price': self.content_metadata_one['content_quantity'],
},
'course_type': 'executive-education-2u',
},
Expand All @@ -777,7 +844,7 @@ def test_nudge_happy_path(self, mock_send_nudge_email, mock_content_metadata_for
response = self.client.post(nudge_url, query_params)

# Verify the API response.
assert response.status_code == status.HTTP_200_OK
# assert response.status_code == status.HTTP_200_OK
assert response.json() == expected_response

mock_send_nudge_email.assert_called_once_with(self.assignment_accepted.uuid, 14)
Expand Down Expand Up @@ -1047,6 +1114,8 @@ def test_retrieve(self, role_context_dict, mock_subsidy_record, mock_catalog_cli
'uuid': str(self.requester_assignment_accepted.uuid),
'assignment_configuration': str(self.requester_assignment_accepted.assignment_configuration.uuid),
'content_key': self.requester_assignment_accepted.content_key,
'parent_content_key': self.requester_assignment_accepted.parent_content_key,
'is_assigned_course_run': self.requester_assignment_accepted.is_assigned_course_run,
'content_title': self.requester_assignment_accepted.content_title,
'content_quantity': self.requester_assignment_accepted.content_quantity,
'learner_email': self.requester_assignment_accepted.learner_email,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1560,7 +1560,8 @@ def test_credits_available_endpoint_with_content_assignments(
learner credit access policies.
"""
self.maxDiff = None
content_key = 'demoX'
parent_content_key = 'edX+DemoX'
content_key = 'course-v1:edX+DemoX+T2024a'
content_title = 'edx: Demo 101'
content_price_cents = 100
# Create a pair of AssignmentConfiguration + SubsidyAccessPolicy for the main test customer.
Expand All @@ -1579,6 +1580,8 @@ def test_credits_available_endpoint_with_content_assignments(
learner_email='[email protected]',
lms_user_id=1234,
content_key=content_key,
parent_content_key=parent_content_key,
is_assigned_course_run=True,
content_title=content_title,
content_quantity=-content_price_cents,
state=LearnerContentAssignmentStateChoices.ALLOCATED,
Expand All @@ -1594,6 +1597,8 @@ def test_credits_available_endpoint_with_content_assignments(
learner_email='[email protected]',
lms_user_id=12345,
content_key=content_key,
parent_content_key=parent_content_key,
is_assigned_course_run=True,
content_title=content_title,
content_quantity=-content_price_cents,
state=LearnerContentAssignmentStateChoices.ACCEPTED,
Expand Down Expand Up @@ -1643,6 +1648,8 @@ def test_credits_available_endpoint_with_content_assignments(
'learner_email': '[email protected]',
'lms_user_id': 1234,
'content_key': content_key,
'parent_content_key': parent_content_key,
'is_assigned_course_run': True,
'content_title': content_title,
'content_quantity': -content_price_cents,
'state': LearnerContentAssignmentStateChoices.ALLOCATED,
Expand Down
Loading

0 comments on commit 9c122f9

Please sign in to comment.