|
38 | 38 | update_license_requests_after_assignments_task |
39 | 39 | ) |
40 | 40 | from enterprise_access.apps.api.utils import ( |
| 41 | + add_bulk_approve_operation_result, |
41 | 42 | get_enterprise_uuid_from_query_params, |
42 | 43 | get_enterprise_uuid_from_request_data, |
43 | 44 | validate_uuid |
|
83 | 84 |
|
84 | 85 | logger = logging.getLogger(__name__) |
85 | 86 |
|
| 87 | +# Maximum number of requests that can be approved in a single bulk approve operation |
| 88 | +BULK_APPROVE_MAX_REQUESTS = 500 |
| 89 | + |
86 | 90 |
|
87 | 91 | class SubsidyRequestViewSet(UserDetailsFromJwtMixin, viewsets.ModelViewSet): |
88 | 92 | """ |
@@ -734,6 +738,17 @@ def decline(self, *args, **kwargs): |
734 | 738 | summary='Approve a learner credit request.', |
735 | 739 | request=serializers.LearnerCreditRequestApproveRequestSerializer, |
736 | 740 | ), |
| 741 | + bulk_approve=extend_schema( |
| 742 | + tags=['Learner Credit Requests'], |
| 743 | + summary='Bulk approve learner credit requests.', |
| 744 | + description=( |
| 745 | + 'Bulk approve learner credit requests. Supports two modes:\n' |
| 746 | + '1. Specific UUID approval: provide subsidy_request_uuids\n' |
| 747 | + '2. Approve all: set approve_all=True (optionally with query filters)\n\n' |
| 748 | + 'Response contains categorized results with uuid, state, and detail for each request.' |
| 749 | + ), |
| 750 | + request=serializers.LearnerCreditRequestBulkApproveRequestSerializer, |
| 751 | + ), |
737 | 752 | overview=extend_schema( |
738 | 753 | tags=['Learner Credit Requests'], |
739 | 754 | summary='Learner credit request overview.', |
@@ -1022,6 +1037,146 @@ def approve(self, request, *args, **kwargs): |
1022 | 1037 | lc_request_action.save() |
1023 | 1038 | return Response({"detail": error_msg}, exc.status_code) |
1024 | 1039 |
|
| 1040 | + @permission_required( |
| 1041 | + constants.REQUESTS_ADMIN_ACCESS_PERMISSION, |
| 1042 | + fn=get_enterprise_uuid_from_request_data, |
| 1043 | + ) |
| 1044 | + @action(detail=False, url_path="bulk-approve", methods=["post"]) |
| 1045 | + def bulk_approve(self, request, *args, **kwargs): |
| 1046 | + """ |
| 1047 | + Bulk approve learner credit requests. |
| 1048 | +
|
| 1049 | + Supports two modes: |
| 1050 | + 1. Specific UUID approval: provide subsidy_request_uuids |
| 1051 | + 2. Approve all: set approve_all=True (optionally with query filters) |
| 1052 | +
|
| 1053 | + Processes each request independently and returns a summary with |
| 1054 | + approved and failed items. Partial success is allowed. |
| 1055 | + """ |
| 1056 | + serializer = ( |
| 1057 | + serializers.LearnerCreditRequestBulkApproveRequestSerializer( |
| 1058 | + data=request.data |
| 1059 | + ) |
| 1060 | + ) |
| 1061 | + serializer.is_valid(raise_exception=True) |
| 1062 | + policy_uuid = serializer.validated_data["policy_uuid"] |
| 1063 | + approve_all = serializer.validated_data.get("approve_all", False) |
| 1064 | + |
| 1065 | + if approve_all: |
| 1066 | + base_queryset = LearnerCreditRequest.objects.filter( |
| 1067 | + state=SubsidyRequestStates.REQUESTED, |
| 1068 | + learner_credit_request_config__learner_credit_config__uuid=policy_uuid, |
| 1069 | + ).select_related("user") |
| 1070 | + |
| 1071 | + requests_to_process = self.filter_queryset(base_queryset) |
| 1072 | + |
| 1073 | + requests_by_uuid = { |
| 1074 | + str(req.uuid): req for req in requests_to_process |
| 1075 | + } |
| 1076 | + else: |
| 1077 | + subsidy_request_uuids = serializer.validated_data["subsidy_request_uuids"] |
| 1078 | + requests_by_uuid = { |
| 1079 | + str(req.uuid): req |
| 1080 | + for req in LearnerCreditRequest.objects.select_related( |
| 1081 | + "user" |
| 1082 | + ).filter(uuid__in=subsidy_request_uuids) |
| 1083 | + } |
| 1084 | + |
| 1085 | + results = {"approved": [], "failed": [], "not_found": [], "skipped": []} |
| 1086 | + |
| 1087 | + # Collect successful approvals for bulk processing |
| 1088 | + approved_requests = [] |
| 1089 | + successful_request_data = [] |
| 1090 | + |
| 1091 | + for uuid_val, lc_request in requests_by_uuid.items(): |
| 1092 | + # For approve_all mode, we already filtered to REQUESTED state |
| 1093 | + # For specific UUID mode, check state and skip if not REQUESTED |
| 1094 | + if (not approve_all and lc_request.state != SubsidyRequestStates.REQUESTED): |
| 1095 | + add_bulk_approve_operation_result( |
| 1096 | + results, "skipped", uuid_val, lc_request.state, |
| 1097 | + f"Request already in {lc_request.state} state" |
| 1098 | + ) |
| 1099 | + continue |
| 1100 | + |
| 1101 | + learner_email = lc_request.user.email |
| 1102 | + content_key = lc_request.course_id |
| 1103 | + content_price_cents = lc_request.course_price |
| 1104 | + |
| 1105 | + lc_request_action = LearnerCreditRequestActions.create_action( |
| 1106 | + learner_credit_request=lc_request, |
| 1107 | + recent_action=get_action_choice( |
| 1108 | + SubsidyRequestStates.APPROVED |
| 1109 | + ), |
| 1110 | + status=get_user_message_choice(SubsidyRequestStates.APPROVED), |
| 1111 | + ) |
| 1112 | + |
| 1113 | + try: |
| 1114 | + with transaction.atomic(): |
| 1115 | + assignment = approve_learner_credit_request_via_policy( |
| 1116 | + policy_uuid, |
| 1117 | + content_key, |
| 1118 | + content_price_cents, |
| 1119 | + learner_email, |
| 1120 | + lc_request.user.lms_user_id, |
| 1121 | + ) |
| 1122 | + |
| 1123 | + # Prepare for bulk processing instead of individual saves |
| 1124 | + lc_request.assignment = assignment |
| 1125 | + |
| 1126 | + approved_requests.append(lc_request) |
| 1127 | + successful_request_data.append({ |
| 1128 | + 'uuid': uuid_val, |
| 1129 | + 'state': SubsidyRequestStates.APPROVED, |
| 1130 | + 'message': "Successfully approved", |
| 1131 | + 'assignment_uuid': assignment.uuid |
| 1132 | + }) |
| 1133 | + |
| 1134 | + except SubisidyAccessPolicyRequestApprovalError as exc: |
| 1135 | + error_msg = ( |
| 1136 | + f"[LC REQUEST BULK APPROVAL] Failed to approve learner credit request " |
| 1137 | + f"with UUID {uuid_val}. Reason: {exc.message}." |
| 1138 | + ) |
| 1139 | + logger.exception(error_msg) |
| 1140 | + # Update action with error |
| 1141 | + lc_request_action.status = get_user_message_choice( |
| 1142 | + SubsidyRequestStates.REQUESTED |
| 1143 | + ) |
| 1144 | + lc_request_action.error_reason = get_error_reason_choice( |
| 1145 | + LearnerCreditRequestActionErrorReasons.FAILED_APPROVAL |
| 1146 | + ) |
| 1147 | + lc_request_action.traceback = format_traceback(exc) |
| 1148 | + lc_request_action.save() |
| 1149 | + add_bulk_approve_operation_result(results, "failed", uuid_val, lc_request.state, exc.message) |
| 1150 | + |
| 1151 | + # Use clean model method for bulk approval |
| 1152 | + if approved_requests: |
| 1153 | + try: |
| 1154 | + with transaction.atomic(): |
| 1155 | + # Use the new model method for efficient bulk processing |
| 1156 | + LearnerCreditRequest.bulk_approve_requests(approved_requests, request.user) |
| 1157 | + |
| 1158 | + # Send notifications and record results |
| 1159 | + for request_data in successful_request_data: |
| 1160 | + send_learner_credit_bnr_request_approve_task.delay(request_data['assignment_uuid']) |
| 1161 | + add_bulk_approve_operation_result( |
| 1162 | + results, |
| 1163 | + "approved", |
| 1164 | + request_data['uuid'], |
| 1165 | + request_data['state'], |
| 1166 | + request_data['message'], |
| 1167 | + ) |
| 1168 | + |
| 1169 | + except (ValidationError, IntegrityError, DatabaseError) as exc: |
| 1170 | + error_msg = f"[LC REQUEST BULK APPROVAL] Bulk update failed: {exc}" |
| 1171 | + logger.exception(error_msg) |
| 1172 | + for request_data in successful_request_data: |
| 1173 | + add_bulk_approve_operation_result( |
| 1174 | + results, "failed", request_data['uuid'], |
| 1175 | + SubsidyRequestStates.REQUESTED, str(exc) |
| 1176 | + ) |
| 1177 | + |
| 1178 | + return Response(results, status=status.HTTP_200_OK) |
| 1179 | + |
1025 | 1180 | @permission_required( |
1026 | 1181 | constants.REQUESTS_ADMIN_ACCESS_PERMISSION, |
1027 | 1182 | fn=get_enterprise_uuid_from_request_data, |
|
0 commit comments