Skip to content

Commit 2fa8c13

Browse files
committed
Improved filename
1 parent 76c4692 commit 2fa8c13

File tree

1 file changed

+87
-67
lines changed
  • FusionIIIT/applications/examination/api

1 file changed

+87
-67
lines changed

FusionIIIT/applications/examination/api/views.py

Lines changed: 87 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
from django.db.models.query_utils import Q
2-
from django.http import HttpResponse
3-
from django.shortcuts import get_object_or_404, HttpResponse
4-
from django.http import HttpResponse
5-
from django.http import JsonResponse
2+
from django.http import HttpResponse, JsonResponse
3+
from django.shortcuts import get_object_or_404
64
from django.db import transaction
75
from decimal import Decimal, ROUND_HALF_UP
86
from applications.academic_procedures.models import(course_registration, course_replacement)
@@ -18,7 +16,7 @@
1816
from io import StringIO, BytesIO
1917
from django.contrib.auth import get_user_model
2018
from rest_framework.views import APIView
21-
from django.db.models import IntegerField, Sum
19+
from django.db.models import IntegerField, Sum, Case, When
2220
from django.db.models.functions import Cast
2321
from rest_framework.parsers import MultiPartParser, FormParser
2422
from openpyxl import Workbook
@@ -34,7 +32,16 @@
3432
from reportlab.lib.units import inch
3533
from django.core.exceptions import ObjectDoesNotExist
3634
from collections import defaultdict
37-
from django.db.models import Case, When, IntegerField
35+
import re
36+
37+
38+
def _safe_filename(name: str, extension: str = "") -> str:
39+
"""Strip/replace characters unsafe in HTTP Content-Disposition filenames."""
40+
safe = re.sub(r'[^\w\s\-.]', '_', str(name)).strip()
41+
safe = re.sub(r'\s+', '_', safe)
42+
safe = safe[:100]
43+
return f"{safe}{extension}" if extension else safe
44+
3845

3946
grade_conversion = {
4047
"O": 1.0, "A+": 1.0, "A": 0.9, "B+": 0.8, "B": 0.7,
@@ -78,21 +85,9 @@ def format_semester_display(semester_no, semester_type=None, semester_label=None
7885
return str(semester_no)
7986

8087
def round_from_last_decimal(number, decimal_places=1):
88+
"""Round a number to `decimal_places` using ROUND_HALF_UP."""
8189
d = Decimal(str(number))
8290
return Decimal(d).quantize(Decimal('0.1'), rounding=ROUND_HALF_UP)
83-
# d = Decimal(str(number))
84-
# current_places = abs(d.as_tuple().exponent)
85-
86-
# # Keep rounding from the last decimal place until we reach the desired one
87-
# while current_places > decimal_places:
88-
# quantize_str = '0.' + '0' * (current_places - 1) + '1'
89-
# d = d.quantize(Decimal(quantize_str), rounding=ROUND_HALF_UP)
90-
# current_places -= 1
91-
92-
# # Final rounding to target place
93-
# final_quantize = '0.' + '0' * (decimal_places - 1) + '1'
94-
# return float(d.quantize(Decimal(final_quantize), rounding=ROUND_HALF_UP))
95-
9691

9792
def calculate_spi_for_student(student, selected_semester, semester_type):
9893
semester_unit = Decimal('0')
@@ -217,22 +212,26 @@ def calculate_cpi_for_student(student, selected_semester, semester_type):
217212
def parse_academic_year(academic_year, semester_type):
218213
"""
219214
Parse academic_year string (e.g., "2024-25") and determine the working_year based on semester type.
220-
For Odd Semester, working_year = first part (e.g., 2024).
221-
For Even Semester, working_year = second part prefixed by '20' (e.g., 2025 if academic_year is "2024-25").
215+
For Odd Semester, working_year = first part (e.g., 2024).
216+
For Even/Summer, working_year = second part, expanded to 4 digits when only 2 are given
217+
(e.g., "25" → 2025 for "2024-25").
222218
The session is set to the academic_year string.
223219
"""
224220
parts = academic_year.split("-")
225221
if len(parts) != 2:
226222
raise ValueError("Invalid academic year format. Expected format like '2024-25'.")
227223
first_year = parts[0].strip()
228224
second_year = parts[1].strip()
225+
if not first_year.isdigit() or not second_year.isdigit():
226+
raise ValueError("Academic year parts must be numeric.")
229227
if semester_type == "Odd Semester":
230228
working_year = int(first_year)
231-
elif semester_type == "Even Semester":
232-
working_year = int("20" + second_year)
233229
else:
234-
# For any other semester type (e.g., Summer Semester) use the first year by default.
235-
working_year = int("20"+second_year)
230+
if len(second_year) == 2:
231+
century = (int(first_year) // 100) * 100
232+
working_year = century + int(second_year)
233+
else:
234+
working_year = int(second_year)
236235
session = academic_year # Use the complete academic year string as session.
237236
return working_year, session
238237

@@ -430,9 +429,10 @@ def download_template(request):
430429
# Get course information from the first matched registration.
431430
course_obj = course_info.first().course_id
432431
response = HttpResponse(content_type="text/csv")
433-
course_name_clean = course_obj.name.replace(' ', '_').replace('/', '-')[:50]
434-
semester_type_clean = semester_type.replace(' ', '_')
435-
filename = f"{course_name_clean}_{semester_type_clean}_{session_year}.csv"
432+
filename = _safe_filename(
433+
f"{course_obj.name}_{semester_type}_{session_year}",
434+
extension=".csv"
435+
)
436436
response['Content-Disposition'] = f'attachment; filename="{filename}"'
437437

438438
writer = csv.writer(response)
@@ -441,7 +441,10 @@ def download_template(request):
441441
# Write a CSV row for each student registration.
442442
for entry in course_info:
443443
student_entry = entry.student_id
444-
student_user = User.objects.get(username=student_entry.id_id)
444+
try:
445+
student_user = User.objects.get(username=student_entry.id_id)
446+
except User.DoesNotExist:
447+
student_user = None
445448
branch_acronym = ""
446449
if student_entry.batch_id:
447450
try:
@@ -454,9 +457,13 @@ def download_template(request):
454457
if entry.course_slot_id and entry.course_slot_id.semester:
455458
semester_no = entry.course_slot_id.semester.semester_no
456459

460+
full_name = (
461+
f"{student_user.first_name} {student_user.last_name}".strip()
462+
if student_user else student_entry.id_id
463+
)
457464
writer.writerow([
458465
student_entry.id_id,
459-
f"{student_user.first_name} {student_user.last_name}",
466+
full_name,
460467
branch_acronym,
461468
"",
462469
"",
@@ -588,7 +595,6 @@ def post(self, request):
588595
)
589596

590597

591-
from django.db import transaction
592598
class UploadGradesAPI(APIView):
593599
permission_classes = [IsAuthenticated]
594600
parser_classes = [MultiPartParser, FormParser]
@@ -985,29 +991,43 @@ def post(self, request):
985991
status=status.HTTP_400_BAD_REQUEST,
986992
)
987993

988-
# Update or create grades
989-
for student_id, semester_id, course_id, grade,remark in zip(
990-
student_ids, semester_ids, course_ids, grades,remarks
991-
):
994+
for student_id, course_id, grade in zip(student_ids, course_ids, grades):
992995
try:
993-
grade_of_student = Student_grades.objects.get(
994-
course_id=course_id, roll_no=student_id, semester=semester_id
996+
course_obj = Courses.objects.get(id=course_id)
997+
except Courses.DoesNotExist:
998+
return Response(
999+
{"error": f"Course ID {course_id} does not exist."},
1000+
status=status.HTTP_400_BAD_REQUEST,
9951001
)
996-
grade_of_student.remarks=remark
997-
grade_of_student.grade = grade
998-
grade_of_student.verified = True
999-
if allow_resubmission.upper() == "YES":
1000-
grade_of_student.reSubmit = True
1001-
grade_of_student.save()
1002-
except Student_grades.DoesNotExist:
1003-
# Create a new hidden grade if the student grade doesn't exist
1004-
hidden_grades.objects.create(
1005-
course_id=course_id,
1006-
student_id=student_id,
1007-
semester_id=semester_id,
1008-
grade=grade,
1002+
if not is_valid_grade(grade, course_obj.code):
1003+
allowed_list = ", ".join(sorted(ALLOWED_GRADES))
1004+
return Response(
1005+
{"error": f"Invalid grade '{grade}' for course '{course_obj.code}'. Allowed: {allowed_list}."},
1006+
status=status.HTTP_400_BAD_REQUEST,
10091007
)
10101008

1009+
with transaction.atomic():
1010+
for student_id, semester_id, course_id, grade, remark in zip(
1011+
student_ids, semester_ids, course_ids, grades, remarks
1012+
):
1013+
try:
1014+
grade_of_student = Student_grades.objects.get(
1015+
course_id=course_id, roll_no=student_id, semester=semester_id
1016+
)
1017+
grade_of_student.remarks = remark
1018+
grade_of_student.grade = grade
1019+
grade_of_student.verified = True
1020+
if allow_resubmission.upper() == "YES":
1021+
grade_of_student.reSubmit = True
1022+
grade_of_student.save()
1023+
except Student_grades.DoesNotExist:
1024+
hidden_grades.objects.create(
1025+
course_id=course_id,
1026+
student_id=student_id,
1027+
semester_id=semester_id,
1028+
grade=grade,
1029+
)
1030+
10111031
# Generate CSV file as the response
10121032
response = HttpResponse(content_type="text/csv")
10131033
response["Content-Disposition"] = 'attachment; filename="grades.csv"'
@@ -1075,8 +1095,10 @@ def post(self, request):
10751095
student = Student.objects.get(id_id=student_id)
10761096
cpi, tu, _ = calculate_cpi_for_student(student, semester_number, semester_type)
10771097
spi, su, _ = calculate_spi_for_student(student, semester_number, semester_type)
1078-
except:
1079-
return Response({"error": "Student ID does not exist."}, status=status.HTTP_400_BAD_REQUEST)
1098+
except Student.DoesNotExist:
1099+
return Response({"error": "Student ID does not exist."}, status=status.HTTP_404_NOT_FOUND)
1100+
except Exception as e:
1101+
return Response({"error": f"Error computing grades: {str(e)}"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
10801102

10811103
course_grades = {}
10821104
courses_registered = Student_grades.objects.filter(roll_no=student_id, semester=semester_number, semester_type = semester_type)
@@ -1411,7 +1433,7 @@ def post(self, request):
14111433
try:
14121434
student_user = User.objects.get(username=student.id_id)
14131435
student_name = f"{student_user.first_name} {student_user.last_name}".strip() or student_user.username
1414-
except:
1436+
except Exception:
14151437
student_name = student.id_id
14161438

14171439
ws.cell(row=row_idx, column=3).value = student_name
@@ -1946,10 +1968,10 @@ def post(self, request):
19461968
continue
19471969

19481970
# c) VALID GRADE?
1949-
if grade not in ALLOWED_GRADES:
1971+
if not is_valid_grade(grade, course.code):
19501972
allowed = ", ".join(sorted(ALLOWED_GRADES))
19511973
errors.append(
1952-
f"Row {idx}: Invalid grade '{grade}'. Allowed: {allowed}."
1974+
f"Row {idx}: Invalid grade '{grade}' for course '{course.code}'. Allowed: {allowed}."
19531975
)
19541976
continue
19551977

@@ -2145,7 +2167,7 @@ def post(self, request):
21452167

21462168
# prepare PDF response
21472169
response = HttpResponse(content_type="application/pdf")
2148-
response["Content-Disposition"] = f'attachment; filename="{course_info.code}_grades.pdf"'
2170+
response["Content-Disposition"] = f'attachment; filename="{_safe_filename(course_info.code)}_grades.pdf"'
21492171

21502172
# Create PDF metadata to fix "(anonymous)" title issue
21512173
pdf_title = f"Course Grades - {course_info.code} - {course_info.name}"
@@ -2470,13 +2492,14 @@ def generate_student_result_pdf(self, request):
24702492

24712493
response = HttpResponse(pdf_data, content_type='application/pdf')
24722494
# Create filename with formatted semester for clarity
2473-
semester_suffix = formatted_semester.replace(' ', '_').replace(':', '').lower()
2474-
filename = f"result_{student_info.get('rollNumber', student_info.get('roll_number', 'student'))}_{semester_suffix}.pdf"
2495+
semester_suffix = re.sub(r'\s+', '_', formatted_semester.replace(':', '')).lower()
2496+
roll = student_info.get('rollNumber', student_info.get('roll_number', 'student'))
2497+
filename = _safe_filename(f"result_{roll}_{semester_suffix}", extension=".pdf")
24752498
response['Content-Disposition'] = f'attachment; filename="{filename}"'
24762499
response['Content-Length'] = len(pdf_data)
2477-
2500+
24782501
return response
2479-
2502+
24802503
except Exception as e:
24812504
return JsonResponse({'error': f'PDF generation failed: {str(e)}'}, status=500)
24822505

@@ -3700,7 +3723,8 @@ def post(self, request):
37003723

37013724
# Choose prefix based on document type
37023725
prefix = "transcript_" if is_transcript else "result_"
3703-
filename = f"{prefix}{student_info.get('rollNumber', student_info.get('roll_number', 'student'))}_{semester_suffix}.pdf"
3726+
roll = student_info.get('rollNumber', student_info.get('roll_number', 'student'))
3727+
filename = _safe_filename(f"{prefix}{roll}_{semester_suffix}", extension=".pdf")
37043728
response['Content-Disposition'] = f'attachment; filename="{filename}"'
37053729
response['Content-Length'] = len(pdf_data)
37063730

@@ -3747,14 +3771,9 @@ def post(self, request):
37473771

37483772
course_grades = {}
37493773
courses_registered = Student_grades.objects.filter(
3750-
roll_no=student_id, semester=semester_number, semester_type=semester_type
3774+
roll_no=student_id, semester=semester_number, semester_type=semester_type,
37513775
).select_related('course_id')
37523776

3753-
if not courses_registered.exists():
3754-
courses_registered = Student_grades.objects.filter(
3755-
roll_no=student_id, semester=semester_number
3756-
).select_related('course_id')
3757-
37583777
academic_year = None
37593778
if courses_registered.exists():
37603779
academic_year = courses_registered.first().academic_year
@@ -3765,6 +3784,7 @@ def post(self, request):
37653784
student_regs = course_registration.objects.filter(
37663785
student_id=student,
37673786
semester_id__semester_no=semester_number,
3787+
semester_type=semester_type,
37683788
).select_related('course_id')
37693789

37703790
substituted_ids = set(

0 commit comments

Comments
 (0)