From b851f120356369607a297827ff4b0104aff51216 Mon Sep 17 00:00:00 2001 From: Vikrant Kumar Date: Wed, 7 Oct 2026 11:56:20 +0530 Subject: [PATCH] Add a batch-wise total-credits-earned Database report Splits each student's earned credits into regular, backlog/improvement and swayam, deduped to the best attempt per course code so the three columns always sum to the degree-credit total (the same rule calculate_cpi_for_student uses for the transcript's TU figure). Runs the per-student query across a thread pool rather than in series -- the underlying helper calls calculate_cpi_for_student once per semester for its CPI-progression display, and each of those calls re-queries grades and replacements from scratch, so a 600+ student batch was taking minutes one student at a time. --- .../academic_information/api/urls.py | 2 + .../academic_information/api/views.py | 85 +++++++++++++++++++ 2 files changed, 87 insertions(+) diff --git a/FusionIIIT/applications/academic_information/api/urls.py b/FusionIIIT/applications/academic_information/api/urls.py index ed2bafb6a..426733998 100644 --- a/FusionIIIT/applications/academic_information/api/urls.py +++ b/FusionIIIT/applications/academic_information/api/urls.py @@ -40,6 +40,8 @@ name='database_swayam_registrations'), url(r'^database/semester-registrations/$', views.database_semester_registrations, name='database_semester_registrations'), + url(r'^database/credits-earned/$', views.database_credits_earned, + name='database_credits_earned'), url(r'^export-all-courses-zip/$', views.export_all_courses_zip, name='export-all-courses-zip'), url(r'^generate_preregistration_report',views.generate_preregistration_report, name = "generate_preregistration_report"), url(r'^calendar/$', views.list_calendar, name='list_calendar'), diff --git a/FusionIIIT/applications/academic_information/api/views.py b/FusionIIIT/applications/academic_information/api/views.py index d2f4a14d8..84ee43489 100644 --- a/FusionIIIT/applications/academic_information/api/views.py +++ b/FusionIIIT/applications/academic_information/api/views.py @@ -1838,6 +1838,91 @@ def database_semester_registrations(request): }) +def _credits_earned_row(student): + """One student's regular / backlog-improvement / swayam credit split. + Runs _build_grade_validation_semesters's ~26 queries per call, so the view + fans this out across a thread pool rather than running it per student in + series -- each call opens its own DB connection, so it must close it.""" + from django.db import connection + from applications.examination.api.views import ( + _build_grade_validation_semesters, grade_conversion, + ) + + NON_EARN = {"F", "I", "X", "AU", "CD", "—", ""} + try: + _, semesters_data = _build_grade_validation_semesters(student) + graded_sems = [s for s in semesters_data if not s.get('is_registered_only')] + + best_by_code = {} + for gs in graded_sems: + for c in gs.get('courses', []): + code = str(c.get('code', '')).strip().upper() + if not code or c.get('superseded'): + continue + grade = (c.get('grade') or '').strip() + if grade in NON_EARN: + continue + prev = best_by_code.get(code) + if prev is None or (grade_conversion.get(grade, -1) + > grade_conversion.get(prev[0], -1)): + best_by_code[code] = ( + grade, float(c.get('credits') or 0), c.get('remark', 'Regular')) + + regular = backlog_imp = swayam = 0.0 + for code, (grade, credit, remark) in best_by_code.items(): + if code.startswith('SW'): + swayam += credit + elif remark in ('Backlog', 'Improvement'): + backlog_imp += credit + else: + regular += credit + + user = student.id.user + return { + 'roll_no': student.id_id, + 'student_name': f'{user.first_name} {user.last_name}'.strip(), + 'regular_credits': regular, + 'backlog_improvement_credits': backlog_imp, + 'swayam_credits': swayam, + 'total_credits_earned': regular + backlog_imp + swayam, + } + finally: + connection.close() + + +@api_view(['GET']) +@permission_classes([IsAuthenticated]) +@authentication_classes([TokenAuthentication]) +@role_required(DATABASE_REPORT_ROLES) +def database_credits_earned(request): + """Each student's earned credits to date, split regular / backlog-improvement / swayam.""" + from concurrent.futures import ThreadPoolExecutor + from applications.globals.programme_scope import scopes_for, scope_students + + batch = (request.query_params.get('batch') or '').strip() + if not batch: + return Response({'detail': 'Required: batch.'}, status=400) + try: + batch_year = int(batch) + except ValueError: + return Response({'detail': f"Batch '{batch}' is not a year."}, status=400) + + students = list(scope_students( + Student.objects.filter(batch=batch_year), + scopes_for(request.user), + ).select_related('id__user').order_by('id_id')) + + with ThreadPoolExecutor(max_workers=16) as pool: + rows = list(pool.map(_credits_earned_row, students)) + + return Response({ + 'count': len(rows), + 'students': len(rows), + 'credits': sum(r['total_credits_earned'] for r in rows), + 'rows': rows, + }) + + @api_view(['POST']) @permission_classes([IsAuthenticated]) @authentication_classes([TokenAuthentication])