Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions FusionIIIT/applications/academic_information/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@
url(r'^calendar/export/$', views.export_calendar, name='export_calendar'),
url(r'^calendar/import/$', views.import_calendar, name='import_calendar'),

# Section assignment (Academics > Section Assignment)
url(r'^section/batches/$', views.section_batches, name='section_batches'),
url(r'^section/students/$', views.section_students, name='section_students'),
url(r'^section/assign/$', views.assign_section, name='assign_section'),


# url(r'^holiday',views.holiday_api,name='holiday-get-api'),

Expand Down
128 changes: 116 additions & 12 deletions FusionIIIT/applications/academic_information/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -697,6 +697,7 @@ def generate_xlsheet_api(request):
semester_type = request.data.get('semester_type')
list_type = request.data.get('list_type', '').strip()
programme_type = request.data.get('programme_type', '').strip()
section = (request.data.get('section') or '').strip().upper()
preview_only = request.data.get('preview_only', False)

if not list_type:
Expand All @@ -708,7 +709,7 @@ def generate_xlsheet_api(request):
return Response({
'error': 'Missing required parameters: course, academic_year, semester_type'
}, status=status.HTTP_400_BAD_REQUEST)
cache_key = f"student_list_{course_id}_{academic_year.replace('-', '_')}_{semester_type.replace(' ', '_')}_{(list_type or 'all').replace(' ', '_')}_{(programme_type or 'all').replace(' ', '_')}"
cache_key = f"student_list_{course_id}_{academic_year.replace('-', '_')}_{semester_type.replace(' ', '_')}_{(list_type or 'all').replace(' ', '_')}_{(programme_type or 'all').replace(' ', '_')}_{section or 'allsec'}"

cached_data = cache.get(cache_key)
if cached_data and not preview_only:
Expand All @@ -725,7 +726,8 @@ def generate_xlsheet_api(request):
cr.registration_type,
c.code as course_code,
c.name as course_name,
s.programme
s.programme,
s.section as section
FROM course_registration cr
INNER JOIN globals_extrainfo ei ON cr.student_id_id = ei.id
INNER JOIN auth_user u ON ei.user_id = u.id
Expand Down Expand Up @@ -757,7 +759,12 @@ def generate_xlsheet_api(request):
else:
sql += " AND s.programme = %s"
params.append(programme_type)


# Section filter (A-F): scope the roll list to one section's students.
if section:
sql += " AND s.section = %s"
params.append(section)

sql += " ORDER BY u.username"
try:
with connection.cursor() as cursor:
Expand All @@ -775,6 +782,7 @@ def generate_xlsheet_api(request):
'last_name': data['last_name'],
'full_name': data['full_name'],
'discipline': data['discipline'],
'section': data.get('section') or '',
'email': data['email'],
'registration_type': data['registration_type'],
'programme': data.get('programme', '')
Expand Down Expand Up @@ -818,7 +826,10 @@ def generate_xlsheet_api(request):
list_type_display += " (PG Only)"
else:
list_type_display += f" ({programme_type} Only)"


if section:
list_type_display += f" — Section {section}"

processing_time = time.time() - start_time
if preview_only:
preview_students = students[:350] if len(students) > 350 else students
Expand All @@ -845,7 +856,7 @@ def generate_xlsheet_api(request):
ws = wb.active
ws.title = "Student List"

column_widths = [8, 15, 30, 15, 35, 18, 15]
column_widths = [8, 15, 30, 15, 10, 35, 18, 15]
for i, width in enumerate(column_widths, 1):
ws.column_dimensions[chr(64 + i)].width = width

Expand Down Expand Up @@ -878,7 +889,9 @@ def generate_xlsheet_api(request):
year_int = int(year_parts[0])

instructor_name = "TBA"
course_instructor = CourseInstructor.objects.filter(course_id=course_id, year=year_int, semester_type=semester_type).first()
_ci_qs = CourseInstructor.objects.filter(course_id=course_id, year=year_int, semester_type=semester_type)
# When a section is selected, show that section's faculty; else the first offering.
course_instructor = (_ci_qs.filter(section_label=section).first() if section else None) or _ci_qs.first()
if course_instructor:
instructor_name = f"{course_instructor.instructor_id.id.user.first_name} {course_instructor.instructor_id.id.user.last_name}".strip()

Expand All @@ -895,7 +908,7 @@ def generate_xlsheet_api(request):
for row in range(3, 7):
ws[f'A{row}'].alignment = Alignment(horizontal="left", vertical="center")

headers = ['Sl. No', 'Roll No', 'Name', 'Discipline', 'Email', 'Reg. Type', 'Signature']
headers = ['Sl. No', 'Roll No', 'Name', 'Discipline', 'Section', 'Email', 'Reg. Type', 'Signature']
for col, header in enumerate(headers, 1):
cell = ws.cell(row=8, column=col, value=header)
cell.font = header_font
Expand All @@ -908,6 +921,7 @@ def generate_xlsheet_api(request):
student['roll_no'],
student['full_name'],
student['discipline'],
student.get('section', '') or '—',
student['email'],
student['registration_type'],
'' # Signature
Expand All @@ -917,7 +931,7 @@ def generate_xlsheet_api(request):
# Add borders only to the student list table section (header + data rows).
last_table_row = ws.max_row
for row in range(8, last_table_row + 1):
for col in range(1, 8):
for col in range(1, 9):
ws.cell(row=row, column=col).border = thin_border

from io import BytesIO
Expand Down Expand Up @@ -945,6 +959,8 @@ def generate_xlsheet_api(request):
else:
filename_suffix += f"_{programme_type.replace(' ', '_')}"

if section:
filename_suffix += f"_Section_{section}"
filename = f"{course_info['code']}_{filename_suffix}_CourseList.xlsx"
response['Content-Disposition'] = f'attachment; filename="{filename}"'

Expand Down Expand Up @@ -1353,7 +1369,14 @@ def available_courses(request):

course_ids = regs.values_list('course_id', flat=True).distinct()
courses = Courses.objects.filter(id__in=course_ids)


# Per-course sections = distinct Student.section of its enrolled students (empty => UI hides filter).
from collections import defaultdict
section_map = defaultdict(set)
for cid, sec in regs.values_list('course_id', 'student_id__section'):
if sec:
section_map[cid].add(sec)

data = []
# Calculate correct year based on semester type
year_parts = year.split('-')
Expand All @@ -1367,12 +1390,13 @@ def available_courses(request):
course_instructor = CourseInstructor.objects.filter(course_id=c, year=year_int, semester_type=sem).first()
if course_instructor:
instructor_name = f"{course_instructor.instructor_id.id.user.first_name} {course_instructor.instructor_id.id.user.last_name}".strip()

data.append({
"id": c.id,
"code": c.code,
"name": c.name,
"instructor": instructor_name
"instructor": instructor_name,
"sections": sorted(section_map.get(c.id, set())),
})
return Response(data)

Expand Down Expand Up @@ -1542,4 +1566,84 @@ def export_all_courses_zip(request):
response['Content-Disposition'] = f'attachment; filename="{zip_filename}"'
return response

return Response(data)
return Response(data)


# Section assignment (Academics > Section Assignment): acadadmin sets Student.section manually.

@api_view(['GET'])
@permission_classes([IsAuthenticated])
@authentication_classes([TokenAuthentication])
@role_required(['acadadmin'])
def section_batches(request):
"""Running batches, for the Batch and Discipline dropdowns."""
batches = (Batch.objects.filter(running_batch=True)
.select_related('discipline')
.order_by('-year', 'name'))
result = []
for b in batches:
disc = b.discipline
result.append({
'id': b.id,
'year': b.year,
'name': b.name,
'discipline_name': disc.name if disc else '',
'discipline_acronym': disc.acronym if disc else '',
'label': f"{b.name} {disc.acronym if disc else ''} {b.year}".strip(),
})
return Response(result)


@api_view(['GET'])
@permission_classes([IsAuthenticated])
@authentication_classes([TokenAuthentication])
@role_required(['acadadmin'])
def section_students(request):
"""Students of one batch (?batch_id=) with their assigned section, by roll no."""
batch_id = request.query_params.get('batch_id')
if not batch_id:
return Response({'detail': 'batch_id required.'},
status=status.HTTP_400_BAD_REQUEST)

students = (Student.objects
.filter(batch_id__id=batch_id)
.select_related('id', 'id__user')
.order_by('id_id'))

result = []
for idx, st in enumerate(students, start=1):
user = getattr(st.id, 'user', None)
full_name = ''
if user:
full_name = (f"{user.first_name} {user.last_name}").strip() or user.username
result.append({
'sno': idx,
'roll_no': st.id_id,
'name': full_name,
'section': st.section or '',
})
return Response(result)


@api_view(['POST'])
@permission_classes([IsAuthenticated])
@authentication_classes([TokenAuthentication])
@role_required(['acadadmin'])
def assign_section(request):
"""Bulk-set section for {"roll_numbers": [...], "section": "A"}; empty section clears it."""
roll_numbers = request.data.get('roll_numbers') or []
section = (request.data.get('section') or '').strip().upper()

if not roll_numbers:
return Response({'detail': 'roll_numbers required.'},
status=status.HTTP_400_BAD_REQUEST)
if section and (len(section) > 2 or not section.isalpha()):
return Response({'detail': 'section must be one or two letters.'},
status=status.HTTP_400_BAD_REQUEST)

updated = (Student.objects
.filter(id_id__in=roll_numbers)
.update(section=section or None))
return Response({'detail': f'Section updated for {updated} student(s).',
'updated': updated,
'section': section or None})
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
"""Backfill Student.section for existing undergraduate students.

Section is derived from (discipline, roll-number parity) via
``academic_information.models.compute_section``. This is a one-time,
idempotent backfill for students who predate the section feature; new
students get their section at onboarding and it is recomputed on branch
change, so this command can safely be re-run at any time.

python manage.py backfill_sections # apply
python manage.py backfill_sections --dry-run # report only
"""

from collections import Counter

from django.core.management.base import BaseCommand

from applications.academic_information.models import Student, compute_section


class Command(BaseCommand):
help = "Backfill Student.section from discipline + roll parity (UG only)."

def add_arguments(self, parser):
parser.add_argument(
'--dry-run', action='store_true',
help='Report what would change without writing to the database.',
)

def handle(self, *args, **options):
dry_run = options['dry_run']
students = Student.objects.select_related('batch_id__discipline').all()

to_update = []
dist = Counter()
no_discipline = 0
non_ug = 0

for student in students:
discipline = None
if student.batch_id and student.batch_id.discipline:
discipline = student.batch_id.discipline.name

section = compute_section(discipline, student.id_id, student.programme)

if section is None:
if str(student.programme or '').strip().upper() not in ('B.TECH', 'B.DES'):
non_ug += 1
elif not discipline:
no_discipline += 1
# leave section as-is (None) when unresolved
if student.section is not None:
# UG student whose discipline no longer maps — clear stale value
student.section = None
to_update.append(student)
continue

dist[section] += 1
if student.section != section:
student.section = section
to_update.append(student)

self.stdout.write(f"Scanned {students.count()} students.")
self.stdout.write(f"Section distribution (UG resolved): {dict(sorted(dist.items()))}")
self.stdout.write(f"Non-UG (skipped): {non_ug} UG without discipline (skipped): {no_discipline}")
self.stdout.write(f"Rows needing update: {len(to_update)}")

if dry_run:
self.stdout.write(self.style.WARNING("Dry run — no changes written."))
return

if to_update:
Student.objects.bulk_update(to_update, ['section'], batch_size=500)
self.stdout.write(self.style.SUCCESS(f"Updated {len(to_update)} students."))
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 3.1.5 on 2026-07-04 10:47

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('academic_information', '0002_auto_20260210_1820'),
]

operations = [
migrations.AddField(
model_name='student',
name='section',
field=models.CharField(blank=True, choices=[('A', 'A'), ('B', 'B'), ('C', 'C'), ('D', 'D'), ('E', 'E'), ('F', 'F')], max_length=2, null=True),
),
]
Loading
Loading