From ef1d153f7a342971beecdbfaed7970a9fe58179f Mon Sep 17 00:00:00 2001 From: Alex Dusenbery Date: Mon, 23 Oct 2023 10:36:11 -0400 Subject: [PATCH] spike, no merge --- .../apps/content_assignments/admin.py | 2 +- .../apps/content_assignments/api.py | 19 +++++++++-- .../apps/content_assignments/models.py | 17 ++++++++++ .../apps/subsidy_access_policy/models.py | 34 +++++++++++++++++-- 4 files changed, 65 insertions(+), 7 deletions(-) diff --git a/enterprise_access/apps/content_assignments/admin.py b/enterprise_access/apps/content_assignments/admin.py index e7e7c5b0..c7bc407e 100644 --- a/enterprise_access/apps/content_assignments/admin.py +++ b/enterprise_access/apps/content_assignments/admin.py @@ -88,7 +88,7 @@ class LearnerContentAssignmentAdmin(DjangoQLSearchMixin, SimpleHistoryAdmin): 'last_notification_at', 'created', 'modified', - 'lms_user_id', + #'lms_user_id', 'get_enterprise_customer_uuid', ) autocomplete_fields = ['assignment_configuration'] diff --git a/enterprise_access/apps/content_assignments/api.py b/enterprise_access/apps/content_assignments/api.py index c5ba9757..962b7309 100644 --- a/enterprise_access/apps/content_assignments/api.py +++ b/enterprise_access/apps/content_assignments/api.py @@ -9,8 +9,11 @@ from django.db.models import Sum +from enterprise_access.apps.core.models import User + from .constants import LearnerContentAssignmentStateChoices from .models import AssignmentConfiguration, LearnerContentAssignment +from .tasks import create_pending_enterprise_learner_for_assignment_task logger = logging.getLogger(__name__) @@ -136,21 +139,27 @@ def allocate_assignments(assignment_configuration, learner_emails, content_key, learner_emails_with_existing_assignments = set() # Split up the existing assignment records by state + updated_fields = {'content_quantity', 'state'} for assignment in existing_assignments: learner_emails_with_existing_assignments.add(assignment.learner_email) if assignment.state in LearnerContentAssignmentStateChoices.REALLOCATE_STATES: assignment.content_quantity = content_quantity assignment.state = LearnerContentAssignmentStateChoices.ALLOCATED + if not assignment.lms_user_id: + first_user = User.objects.filter(email=assignment.learner_email).first() + if first_user: + assignment.lms_user_id = first_user.lms_user_id + updated_fields.add('lms_user_id') assignment.full_clean() cancelled_or_errored_to_update.append(assignment) else: already_allocated_or_accepted.append(assignment) # Bulk update and get a list of refreshed objects - updated_assignments = _update_and_refresh_assignments( + updated_assignments = list(_update_and_refresh_assignments( cancelled_or_errored_to_update, - ['content_quantity', 'state'] - ) + list(updated_fields), + )) # Narrow down creation list of learner emails learner_emails_for_assignment_creation = set(learner_emails) - learner_emails_with_existing_assignments @@ -163,6 +172,10 @@ def allocate_assignments(assignment_configuration, learner_emails, content_key, content_quantity, ) + # Link all non-cancelled assignment learners to the enterprise customer record. + for assignment in updated_assignments + created_assignments: + create_pending_enterprise_learner_for_assignment_task.delay(assignment.uuid) + # Return a mapping of the action we took to lists of relevant assignment records. return { 'updated': updated_assignments, diff --git a/enterprise_access/apps/content_assignments/models.py b/enterprise_access/apps/content_assignments/models.py index 3aaf4990..99519531 100644 --- a/enterprise_access/apps/content_assignments/models.py +++ b/enterprise_access/apps/content_assignments/models.py @@ -8,11 +8,14 @@ from django.db.models import Case, Exists, F, Max, OuterRef, Q, Value, When from django.db.models.fields import BooleanField, CharField, DateTimeField, IntegerField from django.db.models.functions import Coalesce +from django.dispatch import receiver from django.utils import timezone from django_extensions.db.models import TimeStampedModel from simple_history.models import HistoricalRecords from simple_history.utils import bulk_create_with_history, bulk_update_with_history +from enterprise_access.apps.core.models import User + from .constants import ( AssignmentActionErrors, AssignmentActions, @@ -448,3 +451,17 @@ def __str__(self): return ( f'uuid={self.uuid}, action_type={self.action_type}, error_reason={self.error_reason}' ) + + +@receiver(models.signals.post_save, sender=User) +def update_assignment_lms_user_id_from_user_email(sender, **kwargs): # pylint: disable=unused-argument + """ Post save hook to update assignment lms_user_id from core user records.""" + + user = kwargs['instance'] + if user.lms_user_id: + LearnerContentAssignment.objects.filter( + learner_email=user.email, + lms_user_id=None, + ).update( + lms_user_id=user.lms_user_id, + ) diff --git a/enterprise_access/apps/subsidy_access_policy/models.py b/enterprise_access/apps/subsidy_access_policy/models.py index c19ce843..90f87e60 100644 --- a/enterprise_access/apps/subsidy_access_policy/models.py +++ b/enterprise_access/apps/subsidy_access_policy/models.py @@ -18,6 +18,7 @@ from enterprise_access.apps.content_assignments import api as assignments_api from enterprise_access.utils import is_none, is_not_none +from ..content_assignments.constants import LearnerContentAssignmentStateChoices from ..content_assignments.models import AssignmentConfiguration from .constants import ( CREDIT_POLICY_TYPE_PRIORITY, @@ -645,7 +646,7 @@ def redeem(self, lms_user_id, content_key, all_transactions, metadata=None): Returns: A ledger transaction, or None if the subsidy was not redeemed. """ - if self.access_method == AccessMethods.DIRECT: + if self.access_method in (AccessMethods.DIRECT, AccessMethods.ASSIGNED): idempotency_key = create_idempotency_key_for_transaction( subsidy_uuid=str(self.subsidy_uuid), lms_user_id=lms_user_id, @@ -1011,10 +1012,37 @@ def spend_available(self): return max(0, super().spend_available + self.total_allocated) def can_redeem(self, lms_user_id, content_key, skip_customer_user_check=False): - raise NotImplementedError + redeemable, reason, existing_transactions = super().can_redeem( + lms_user_id, content_key, skip_customer_user_check + ) + if not redeemable: + return (redeemable, reason, existing_transactions) + + course_for_key = 'course-v1:edX+DemoX+Demo_Course' + has_allocated_assignment_for_course = self.assignment_configuration.assignments.filter( + lms_user_id=lms_user_id, + content_key=course_for_key, + state=LearnerContentAssignmentStateChoices.ALLOCATED, + ).exists() + if has_allocated_assignment_for_course: + return (True, None, existing_transactions) + return (False, 'REASON_NO_ALLOCATION', existing_transactions) def redeem(self, lms_user_id, content_key, all_transactions, metadata=None): - raise NotImplementedError + course_for_key = 'course-v1:edX+DemoX+Demo_Course' + allocated_assignment = self.assignment_configuration.assignments.filter( + lms_user_id=lms_user_id, + content_key=course_for_key, + state=LearnerContentAssignmentStateChoices.ALLOCATED, + ).first() + if not allocated_assignment: + raise Exception('no allocation') + + transaction = super().redeem(lms_user_id, content_key, all_transactions, metadata=metadata) + allocated_assignment.state = LearnerContentAssignmentStateChoices.ACCEPTED + allocated_assignment.transaction_uuid = transaction['uuid'] + allocated_assignment.save() + return transaction def can_allocate(self, number_of_learners, content_key, content_price_cents): """