Skip to content

Commit 1285e12

Browse files
authored
Merge pull request #1878 from vikrantwiz02/prod/acad-react
feat: enhance Excel report generation with improved styling and warning
2 parents 766be50 + eecb613 commit 1285e12

File tree

4 files changed

+140
-16
lines changed

4 files changed

+140
-16
lines changed

FusionIIIT/applications/academic_information/api/views.py

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -507,7 +507,7 @@ def generate_xlsheet_api(request):
507507

508508
# OPTIMIZATION 7: Fast Excel generation with minimal formatting
509509
from openpyxl import Workbook
510-
from openpyxl.styles import Font, PatternFill, Alignment
510+
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
511511

512512
wb = Workbook()
513513
ws = wb.active
@@ -520,6 +520,12 @@ def generate_xlsheet_api(request):
520520
# Minimal header formatting (single operation)
521521
header_font = Font(bold=True, color="FFFFFF")
522522
header_fill = PatternFill(start_color="366092", end_color="366092", fill_type="solid")
523+
thin_border = Border(
524+
left=Side(style="thin"),
525+
right=Side(style="thin"),
526+
top=Side(style="thin"),
527+
bottom=Side(style="thin"),
528+
)
523529

524530
# Add title rows efficiently
525531
ws.merge_cells('A1:G1')
@@ -544,18 +550,18 @@ def generate_xlsheet_api(request):
544550
if course_instructor:
545551
instructor_name = f"{course_instructor.instructor_id.id.user.first_name} {course_instructor.instructor_id.id.user.last_name}".strip()
546552

547-
# Course details
548-
ws['A3'] = "Course No:"
549-
ws['B3'] = course_info['code']
550-
ws['A4'] = "Course Title:"
551-
ws.merge_cells('B4:G4')
552-
ws['B4'] = course_info['name']
553-
ws['A5'] = "Instructor:"
554-
ws.merge_cells('B5:G5')
555-
ws['B5'] = instructor_name
556-
ws['A6'] = "List Type:"
557-
ws.merge_cells('B6:G6')
558-
ws['B6'] = list_type_display
553+
# Course details merged into single full-width cells (no borders on metadata rows).
554+
ws.merge_cells('A3:G3')
555+
ws['A3'] = f"Course No: {course_info['code']}"
556+
ws.merge_cells('A4:G4')
557+
ws['A4'] = f"Course Title: {course_info['name']}"
558+
ws.merge_cells('A5:G5')
559+
ws['A5'] = f"Instructor: {instructor_name}"
560+
ws.merge_cells('A6:G6')
561+
ws['A6'] = f"List Type: {list_type_display}"
562+
563+
for row in range(3, 7):
564+
ws[f'A{row}'].alignment = Alignment(horizontal="left", vertical="center")
559565

560566
headers = ['Sl. No', 'Roll No', 'Name', 'Discipline', 'Email', 'Reg. Type', 'Signature']
561567
for col, header in enumerate(headers, 1):
@@ -576,6 +582,12 @@ def generate_xlsheet_api(request):
576582
]
577583
ws.append(row_data)
578584

585+
# Add borders only to the student list table section (header + data rows).
586+
last_table_row = ws.max_row
587+
for row in range(8, last_table_row + 1):
588+
for col in range(1, 8):
589+
ws.cell(row=row, column=col).border = thin_border
590+
579591
from io import BytesIO
580592
output = BytesIO()
581593
wb.save(output)

FusionIIIT/applications/examination/api/views.py

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1314,6 +1314,8 @@ def post(self, request):
13141314

13151315
# Define a fill style for header cells: light grey background.
13161316
header_fill = PatternFill(start_color="D3D3D3", end_color="D3D3D3", fill_type="solid")
1317+
zero_spi_fill = PatternFill(start_color="B30016", end_color="B30016", fill_type="solid")
1318+
low_spi_fill = PatternFill(start_color="F8F40F", end_color="F8F40F", fill_type="solid")
13171319
thin_border = Border(
13181320
left=Side(style="thin"), right=Side(style="thin"),
13191321
top=Side(style="thin"), bottom=Side(style="thin")
@@ -1415,13 +1417,19 @@ def post(self, request):
14151417
cell.font = Font(bold=True)
14161418
cell.fill = header_fill
14171419

1418-
# Ensure full header rows (1 to 4) are highlighted.
1420+
cell = ws.cell(row=1, column=col_idx+6)
1421+
cell.value = "WARNING"
1422+
cell.alignment = Alignment(horizontal="center", vertical="center")
1423+
cell.font = Font(bold=True)
1424+
cell.fill = header_fill
1425+
14191426
max_col = ws.max_column
14201427
for row in range(1, 5):
14211428
for col in range(1, max_col + 1):
14221429
cell = ws.cell(row=row, column=col)
14231430
cell.fill = header_fill
14241431
cell.border = thin_border
1432+
total_columns = ws.max_column
14251433

14261434
# Fill in student rows, starting from row 5.
14271435
row_idx = 5
@@ -1501,10 +1509,33 @@ def post(self, request):
15011509
ws.cell(row=row_idx, column=col_ptr+3).value = TU
15021510
ws.cell(row=row_idx, column=col_ptr+4).value = SP
15031511
ws.cell(row=row_idx, column=col_ptr+5).value = TP
1504-
for c in [col_ptr, col_ptr+1]:
1512+
ws.cell(row=row_idx, column=col_ptr+6).value = ""
1513+
for c in [col_ptr, col_ptr+1, col_ptr+2, col_ptr+3, col_ptr+4, col_ptr+5, col_ptr+6]:
15051514
ws.cell(row=row_idx, column=c).alignment = Alignment(horizontal="center", vertical="center")
1515+
1516+
# Highlight rows based on SPI and write warning in dedicated warning column.
1517+
try:
1518+
spi_numeric = float(spi_val)
1519+
except (TypeError, ValueError):
1520+
spi_numeric = None
1521+
1522+
if spi_numeric is not None and spi_numeric == 0:
1523+
for c in range(1, total_columns + 1):
1524+
ws.cell(row=row_idx, column=c).fill = zero_spi_fill
1525+
ws.cell(row=row_idx, column=col_ptr+6).value = ""
1526+
elif spi_numeric is not None and spi_numeric < 5:
1527+
for c in range(1, total_columns + 1):
1528+
ws.cell(row=row_idx, column=c).fill = low_spi_fill
1529+
ws.cell(row=row_idx, column=col_ptr+6).value = "WARNING"
1530+
15061531
row_idx += 1
15071532

1533+
# Apply borders on all populated cells (headers + student rows).
1534+
last_data_row = row_idx - 1
1535+
for row in range(1, last_data_row + 1):
1536+
for col in range(1, total_columns + 1):
1537+
ws.cell(row=row, column=col).border = thin_border
1538+
15081539
response = HttpResponse(content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
15091540
response['Content-Disposition'] = 'attachment; filename="student_grades.xlsx"'
15101541
wb.save(response)

FusionIIIT/applications/programme_curriculum/api/urls.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@
116116
path('admin_list_students/', views_student_management.list_students, name='admin_list_students'),
117117

118118
# Individual student CRUD
119+
path('student_my_info/', views_student_management.student_my_info, name='student_my_info'),
119120
path('student/<int:student_id>/', views_student_management.get_student, name='get_student'),
120121
path('student/<int:student_id>/update/', views_student_management.update_student, name='update_student'),
121122
path('student/<int:student_id>/delete/', views_student_management.delete_student, name='delete_student'),

FusionIIIT/applications/programme_curriculum/api/views_student_management.py

Lines changed: 81 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,9 @@
1717
from django.db.models import Q, Count
1818
from django.conf import settings
1919
from django.utils import timezone
20-
from rest_framework.decorators import api_view, permission_classes
20+
from rest_framework.decorators import api_view, permission_classes, authentication_classes
2121
from rest_framework.permissions import IsAuthenticated
22+
from rest_framework.authentication import TokenAuthentication
2223
from rest_framework.response import Response
2324
from rest_framework import status
2425

@@ -5131,3 +5132,82 @@ def ensure_default_curriculum_exists(discipline_obj, programme_name):
51315132
# Log the error but don't raise it to avoid breaking the student reporting process
51325133
pass
51335134
return None
5135+
5136+
@api_view(['GET'])
5137+
@authentication_classes([TokenAuthentication])
5138+
@permission_classes([IsAuthenticated])
5139+
def student_my_info(request):
5140+
"""
5141+
Returns the authenticated student's programme type and linked curriculum IDs.
5142+
"""
5143+
try:
5144+
acad_student = (
5145+
AcademicStudent.objects
5146+
.select_related('batch_id__curriculum__programme', 'id__department')
5147+
.get(id__user=request.user)
5148+
)
5149+
except AcademicStudent.DoesNotExist:
5150+
return Response(
5151+
{'error': 'No student record found for this user. Contact the academic office.'},
5152+
status=status.HTTP_400_BAD_REQUEST,
5153+
)
5154+
5155+
programme_category = None
5156+
curriculum_ids = []
5157+
5158+
if acad_student.batch_id and acad_student.batch_id.curriculum_id:
5159+
curriculum_ids = [acad_student.batch_id.curriculum_id]
5160+
programme_category = acad_student.batch_id.curriculum.programme.category
5161+
5162+
if not programme_category:
5163+
programme_category = (
5164+
Programme.objects
5165+
.filter(name=acad_student.programme)
5166+
.values_list('category', flat=True)
5167+
.first()
5168+
)
5169+
5170+
if not programme_category:
5171+
return Response(
5172+
{
5173+
'error': (
5174+
'Programme type could not be determined. '
5175+
'Your batch or curriculum may not be properly linked. '
5176+
'Contact the academic office.'
5177+
)
5178+
},
5179+
status=status.HTTP_400_BAD_REQUEST,
5180+
)
5181+
5182+
if not acad_student.batch:
5183+
return Response(
5184+
{'error': 'Batch year is not set on your student record. Contact the academic office.'},
5185+
status=status.HTTP_400_BAD_REQUEST,
5186+
)
5187+
5188+
branch = (
5189+
acad_student.id.department.name
5190+
if acad_student.id_id and acad_student.id.department
5191+
else None
5192+
)
5193+
if not branch:
5194+
return Response(
5195+
{'error': 'Department/branch is not assigned to your student record. Contact the academic office.'},
5196+
status=status.HTTP_400_BAD_REQUEST,
5197+
)
5198+
5199+
if not curriculum_ids:
5200+
return Response(
5201+
{'error': 'No curriculum is linked to your batch yet. Contact the academic office.'},
5202+
status=status.HTTP_400_BAD_REQUEST,
5203+
)
5204+
5205+
return Response(
5206+
{
5207+
'programme_type': programme_category.lower(),
5208+
'year': acad_student.batch,
5209+
'branch': branch,
5210+
'curriculum_ids': curriculum_ids,
5211+
},
5212+
status=status.HTTP_200_OK,
5213+
)

0 commit comments

Comments
 (0)