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
7 changes: 4 additions & 3 deletions FusionIIIT/applications/academic_information/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -727,14 +727,15 @@ def generate_xlsheet_api(request):
c.code as course_code,
c.name as course_name,
s.programme,
s.section as section
COALESCE(ci.section_label, 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
LEFT JOIN academic_information_student s ON ei.id = s.id_id
LEFT JOIN programme_curriculum_batch b ON s.batch_id_id = b.id
LEFT JOIN programme_curriculum_discipline d ON b.discipline_id = d.id
INNER JOIN programme_curriculum_course c ON cr.course_id_id = c.id
LEFT JOIN programme_curriculum_courseinstructor ci ON cr.course_instructor_id = ci.id
WHERE cr.session = %s
AND cr.semester_type = %s
AND cr.course_id_id = %s
Expand All @@ -760,9 +761,9 @@ def generate_xlsheet_api(request):
sql += " AND s.programme = %s"
params.append(programme_type)

# Section filter (A-F): scope the roll list to one section's students.
# Section = the course offering's section (course_instructor), else home section.
if section:
sql += " AND s.section = %s"
sql += " AND COALESCE(ci.section_label, s.section) = %s"
params.append(section)

sql += " ORDER BY u.username"
Expand Down
133 changes: 123 additions & 10 deletions FusionIIIT/applications/academic_procedures/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,30 @@ def add_course(request):

current_year = datetime.datetime.now().year
session, semester_type = generate_current_session(current_year, student.curr_semester_no)
working_year = parse_academic_year(academic_year=session, semester_type=semester_type)[0]

# Resolve the offering (section) to register into. Prefer the student's own
# section; if that section doesn't run the course, they must pick one of the
# running sections (course_instructor id) — a cross-section backlog/improvement.
from applications.academic_information.models import resolve_offering
offering = resolve_offering(student, course, working_year, semester_type)
ci_id = request.data.get('course_instructor_id')
if ci_id:
offering = CourseInstructor.objects.filter(
id=ci_id, course_id=course, year=working_year, semester_type=semester_type,
).first()
if not offering:
return Response({
'error': 'Selected section is not running this course this semester.'
}, status=status.HTTP_400_BAD_REQUEST)
elif offering is None:
running = CourseInstructor.objects.filter(
course_id=course, year=working_year, semester_type=semester_type,
).exists()
if running:
return Response({
'error': 'This course is not running in your section. Please select a section to register in.'
}, status=status.HTTP_400_BAD_REQUEST)

old_course_reg = course_registration.objects.filter(
student_id=student,
Expand Down Expand Up @@ -316,6 +340,7 @@ def add_course(request):
academic_year=session,
semester_type=semester_type,
old_course_registration=old_course_reg,
course_instructor=offering,
status='Pending'
)
except Exception as create_error:
Expand Down Expand Up @@ -500,27 +525,53 @@ def get_student_add_courses(request):
name__startswith='BL'
)

# Current term, to find where each course is actually running this sem.
cur_year = datetime.datetime.now().year
session, sem_type = generate_current_session(cur_year, current_sem_no)
working_year = parse_academic_year(academic_year=session, semester_type=sem_type)[0]
student_section = student.section or ''

courses_list = []

for slot in bl_slots:
courses = slot.courses.all()

for course in courses:
already_registered = course_registration.objects.filter(
course_id=course,
student_id=student
).exists()


# Sections where this course is running this term. If the student's
# own section isn't among them, they pick one of these (backlog/improvement).
offerings = CourseInstructor.objects.filter(
course_id=course, year=working_year, semester_type=sem_type,
).select_related('instructor_id__id__user')
sections = []
own_section_running = False
for o in offerings:
u = o.instructor_id.id.user
sections.append({
'course_instructor_id': o.id,
'section': o.section_label or '',
'instructor': f"{u.first_name} {u.last_name}".strip(),
})
if student_section and o.section_label == student_section:
own_section_running = True

courses_list.append({
'id': course.id,
'code': course.code,
'name': course.name,
'credit': course.credit,
'slot': slot.name,
'slot_id': slot.id,
'already_registered': already_registered
'already_registered': already_registered,
'sections': sections,
'own_section_running': own_section_running,
'student_section': student_section,
})

return Response(courses_list, status=status.HTTP_200_OK)

except Exception as e:
Expand Down Expand Up @@ -1904,6 +1955,43 @@ def acad_add_course(request):
reg_type = data["registration_type"]
old_id = data.get("old_course")
sem_type = data["semester_type"]
working_year = parse_academic_year(academic_year=session, semester_type=sem_type)[0]

# Resolve the offering (section). Prefer the chosen section, else the student's
# own. Registering into a section other than the student's own is only allowed
# for Backlog/Improvement.
from applications.academic_information.models import resolve_offering
ci_id = data.get("course_instructor_id")
if ci_id:
offering = CourseInstructor.objects.filter(
id=ci_id, course_id=course, year=working_year, semester_type=sem_type,
).first()
if not offering:
return Response({"error": "Selected section is not running this course this semester."},
status=status.HTTP_400_BAD_REQUEST)
else:
offering = resolve_offering(student, course, working_year, sem_type)

# A sectioned course can't be registered without a chosen section.
if offering is None:
sectioned = CourseInstructor.objects.filter(
course_id=course, year=working_year, semester_type=sem_type,
).exclude(section_label__isnull=True).exclude(section_label="").exists()
if sectioned:
return Response(
{"error": "This course runs in sections — select a section to register in."},
status=status.HTTP_400_BAD_REQUEST,
)

student_section = getattr(student, "section", None)
if (offering is not None and offering.section_label
and offering.section_label != student_section
and reg_type not in ("Backlog", "Improvement")):
return Response(
{"error": "Registering into another section is only allowed as Backlog/Improvement."},
status=status.HTTP_400_BAD_REQUEST,
)

with transaction.atomic():
cr = course_registration.objects.create(
student_id = student,
Expand All @@ -1913,7 +2001,8 @@ def acad_add_course(request):
session = session,
registration_type= reg_type,
semester_type = sem_type,
working_year = parse_academic_year(academic_year=session, semester_type=sem_type)[0]
working_year = working_year,
course_instructor = offering,
)
if old_id:
old = course_registration.objects.filter(id=old_id).first()
Expand Down Expand Up @@ -3662,9 +3751,31 @@ def get_add_course_courses(request):
# ensure the slot exists (404 if not)
slot = get_object_or_404(CourseSlot, id=slot_id)

# via the M2M relationship .courses
courses = slot.courses.all().values("id", "code", "name", "credit")
return Response(list(courses), status=status.HTTP_200_OK)
# When the term is supplied, include the sections each course is running in
# (so the admin can pick a section for a cross-section backlog/improvement).
academic_year = request.query_params.get("academic_year")
semester_type = request.query_params.get("semester_type")
working_year = None
if academic_year and semester_type:
try:
working_year = parse_academic_year(academic_year=academic_year, semester_type=semester_type)[0]
except Exception:
working_year = None

result = []
for c in slot.courses.all():
entry = {"id": c.id, "code": c.code, "name": c.name, "credit": c.credit}
if working_year is not None:
offerings = CourseInstructor.objects.filter(
course_id=c, year=working_year, semester_type=semester_type,
).select_related("instructor_id__id__user")
entry["sections"] = [{
"course_instructor_id": o.id,
"section": o.section_label or "",
"instructor": f"{o.instructor_id.id.user.first_name} {o.instructor_id.id.user.last_name}".strip(),
} for o in offerings]
result.append(entry)
return Response(result, status=status.HTTP_200_OK)


def roman_to_int(s):
Expand Down Expand Up @@ -5061,7 +5172,9 @@ def approve_add_requests(request):
session=add_request.academic_year,
semester_type=add_request.semester_type,
working_year=datetime.datetime.now().year,
registration_type=registration_type
registration_type=registration_type,
# Section chosen at request time (cross-section backlog/improvement).
course_instructor=add_request.course_instructor,
)
reg.save()

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
"""Bulk-assign a section to unsectioned backlog/improvement registrations (once per term)."""

from collections import defaultdict

from django.core.management.base import BaseCommand
from django.db import transaction

from applications.academic_procedures.models import course_registration
from applications.programme_curriculum.models import CourseInstructor


def _working_year(session, semester_type):
# "2026-27" -> 2026; Even semester falls in the next calendar year.
start = int(str(session).split("-")[0])
return start + 1 if semester_type == "Even Semester" else start


class Command(BaseCommand):
help = "Assign a section (course_instructor) to unsectioned backlog/improvement registrations."

def add_arguments(self, parser):
parser.add_argument("--year", required=True, help="Session, e.g. 2026-27")
parser.add_argument("--sem", required=True, help='Semester type, e.g. "Odd Semester"')
parser.add_argument("--course", help="Optional course code to limit to")
parser.add_argument("--dry-run", action="store_true", help="Preview only, no writes")

def handle(self, *args, **opts):
session, sem, dry = opts["year"], opts["sem"], opts["dry_run"]
working_year = _working_year(session, sem)

regs = course_registration.objects.filter(
session=session,
semester_type=sem,
registration_type__in=["Backlog", "Improvement"],
course_instructor__isnull=True,
).select_related("student_id", "course_id")
if opts.get("course"):
regs = regs.filter(course_id__code=opts["course"])

# Seed per-offering counts so "least-full" balances against those already placed.
counts = defaultdict(int)
for ci_id in course_registration.objects.filter(
session=session, semester_type=sem, course_instructor__isnull=False,
).values_list("course_instructor_id", flat=True):
counts[ci_id] += 1

offerings_by_course = {}

def offerings_for(course):
if course.id not in offerings_by_course:
offerings_by_course[course.id] = list(CourseInstructor.objects.filter(
course_id=course, year=working_year, semester_type=sem,
))
return offerings_by_course[course.id]

plan, skipped = [], 0
for reg in regs:
offs = offerings_for(reg.course_id)
if not offs:
skipped += 1
continue
if len(offs) == 1:
chosen = offs[0]
else:
sec = getattr(reg.student_id, "section", None)
chosen = next(
(o for o in offs if o.section_label and o.section_label == sec),
None,
) or min(offs, key=lambda o: counts[o.id])
counts[chosen.id] += 1
plan.append((reg, chosen))

self.stdout.write(
f"Unsectioned backlog/improvement regs: {regs.count()} | "
f"to assign: {len(plan)} | skipped (course has no offering): {skipped}"
)
for reg, chosen in plan[:50]:
self.stdout.write(
f" {reg.student_id_id} {reg.course_id.code} -> "
f"section {chosen.section_label or '-'} (offering {chosen.id})"
)
if len(plan) > 50:
self.stdout.write(f" ... and {len(plan) - 50} more")

if dry:
self.stdout.write(self.style.WARNING("DRY RUN — no changes written."))
return

with transaction.atomic():
for reg, chosen in plan:
reg.course_instructor = chosen
reg.save(update_fields=["course_instructor"])
self.stdout.write(self.style.SUCCESS(f"Assigned {len(plan)} registration(s)."))
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Generated by Django 3.1.5 on 2026-08-03 02:34

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
('programme_curriculum', '0034_studentbatchupload_section'),
('academic_procedures', '0022_feedbackresponse_course_instructor'),
]

operations = [
migrations.AddField(
model_name='courseaddrequest',
name='course_instructor',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='programme_curriculum.courseinstructor'),
),
]
7 changes: 7 additions & 0 deletions FusionIIIT/applications/academic_procedures/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -1037,6 +1037,13 @@ class CourseAddRequest(models.Model):
on_delete=models.SET_NULL,
help_text="Reference to the previous course registration being replaced (for backlog/improvement)"
)
# The running section (offering) the student chose when the course isn't run
# in their own section; applied as course_instructor on approval.
course_instructor = models.ForeignKey(
'programme_curriculum.CourseInstructor',
null=True, blank=True,
on_delete=models.SET_NULL,
)
status = models.CharField(
max_length=20,
choices=STATUS_CHOICES,
Expand Down
Loading