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
1 change: 1 addition & 0 deletions FusionIIIT/applications/academic_information/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
# url(r'^delete-calendar',views.delete_calendar,name='calendar-delete-api'),
url(r'^check-allocation$', views.check_allocation_api, name='check-allocation-api'),
url(r'^start-allocation$', views.start_allocation_api, name='start-allocation-api'),
url(r'^add-course-to-slots$', views.add_course_to_slots_api, name='add-course-to-slots-api'),
url(r'^allocation-results$', views.get_allocation_results, name='allocation-results-api'),
url(r'^allocation-results/export$', views.export_allocation_course, name='allocation-results-export-api'),
url(r'^allocation-results/export-all$', views.export_all_allocation_courses, name='allocation-results-export-all-api'),
Expand Down
58 changes: 56 additions & 2 deletions FusionIIIT/applications/academic_information/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
from django.db.models import Q
from applications.academic_procedures.api.views import role_required
from django.core.cache import cache
from django.db import connection
from django.db import connection, transaction

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -318,8 +318,18 @@ def start_allocation_api(request):
batch = int(batch)
semester = int(semester)

skip_course_ids = data.get("skip_course_ids") or []
if not isinstance(skip_course_ids, list):
return JsonResponse({"error": "skip_course_ids must be a list"}, status=400)
try:
skip_course_ids = [int(c) for c in skip_course_ids]
except (TypeError, ValueError):
return JsonResponse({"error": "skip_course_ids must be course ids"}, status=400)

mock_request = type('MockRequest', (), {})()
mock_request.POST = {'batch': batch, 'sem': semester, 'year': year, 'programme_type': programme_type}
mock_request.POST = {'batch': batch, 'sem': semester, 'year': year,
'programme_type': programme_type,
'skip_course_ids': skip_course_ids}

return allocate(mock_request)

Expand All @@ -330,6 +340,50 @@ def start_allocation_api(request):
return JsonResponse({"error": str(e)}, status=500)


@api_view(['POST'])
@permission_classes([IsAuthenticated])
@authentication_classes([TokenAuthentication])
@role_required(['acadadmin'])
def add_course_to_slots_api(request):
"""Adds one course to the CourseSlot rows it was flagged as missing from."""
course_id = request.data.get('course_id')
slot_ids = request.data.get('slot_ids') or []

if not course_id or not slot_ids:
return Response({'status': -1, 'message': 'course_id and slot_ids are required'},
status=status.HTTP_400_BAD_REQUEST)
if not isinstance(slot_ids, list):
return Response({'status': -1, 'message': 'slot_ids must be a list'},
status=status.HTTP_400_BAD_REQUEST)

try:
course = Courses.objects.get(id=int(course_id))
except (Courses.DoesNotExist, TypeError, ValueError):
return Response({'status': -1, 'message': 'No such course'},
status=status.HTTP_404_NOT_FOUND)

slots = CourseSlot.objects.filter(id__in=[int(s) for s in slot_ids])
found = {s.id for s in slots}
missing = [s for s in slot_ids if int(s) not in found]
if missing:
return Response({'status': -1, 'message': f'Unknown course slot(s): {missing}'},
status=status.HTTP_404_NOT_FOUND)

added = []
with transaction.atomic():
for slot in slots:
if not slot.courses.filter(id=course.id).exists():
slot.courses.add(course)
added.append({'slot_id': slot.id, 'slot_name': slot.name})

return Response({
'status': 1,
'message': f'{course.code} added to {len(added)} slot(s)',
'course_code': course.code,
'added': added,
}, status=status.HTTP_200_OK)


def _semester_type(sem):
return "Even Semester" if int(sem) % 2 == 0 else "Odd Semester"

Expand Down
105 changes: 97 additions & 8 deletions FusionIIIT/applications/academic_information/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,72 @@
from django.http import HttpResponse, JsonResponse
from django.utils import timezone
from django.core import serializers
from django.db.models import Q
from django.db.models import Q, Count
import datetime
import random
from django.db import transaction
time = timezone.now()


def validate_course_slots(batch, sem, programme_type, skip_course_ids=None):
"""
Finds courses registered under a CourseSlot that does not list them.

random_algo pools students by slot *name* across curricula, so a course in
one curriculum's slot but absent from another's gets allotted into a slot
that does not contain it. Empty list means allocation is safe to proceed.
"""
skip_course_ids = {int(c) for c in (skip_course_ids or [])}

pairs = (InitialRegistration.objects
.filter(Q(semester_id__semester_no=sem)
& Q(student_id__batch=batch)
& Q(student_id__batch_id__curriculum__programme__category=programme_type))
.exclude(course_id__isnull=True)
.exclude(course_slot_id__isnull=True)
.values('course_id', 'course_slot_id')
.annotate(students=Count('id')))
pairs = [p for p in pairs if p['course_id'] not in skip_course_ids]
if not pairs:
return []

slot_ids = {p['course_slot_id'] for p in pairs}
valid = set(CourseSlot.courses.through.objects
.filter(courseslot_id__in=slot_ids)
.values_list('courseslot_id', 'course_id'))

offending = [p for p in pairs if (p['course_slot_id'], p['course_id']) not in valid]
if not offending:
return []

courses = {c.id: c for c in Course.objects.filter(
id__in={p['course_id'] for p in offending})}
slots = {s.id: s for s in CourseSlot.objects
.filter(id__in={p['course_slot_id'] for p in offending})
.select_related('semester', 'semester__curriculum')}

problems = {}
for p in offending:
course, slot = courses[p['course_id']], slots[p['course_slot_id']]
entry = problems.setdefault((course.id, slot.name), {
'course_id': course.id,
'course_code': course.code,
'course_name': course.name,
'slot_name': slot.name,
'slot_type': slot.type,
'missing_from': [],
'students': 0,
})
entry['missing_from'].append({
'slot_id': slot.id,
'semester_no': slot.semester.semester_no,
'curriculum_id': slot.semester.curriculum_id,
'curriculum_name': str(slot.semester.curriculum.name),
'students': p['students'],
})
entry['students'] += p['students']

return sorted(problems.values(), key=lambda e: -e['students'])
def check_for_registration_complete(batch, sem, year, programme_type):
date = datetime.date.today()
try:
Expand All @@ -40,16 +101,22 @@ def check_for_registration_complete(batch, sem, year, programme_type):
return {"status": -3, "message": f"Internal Server Error: {str(e)}"}

@transaction.atomic
def random_algo(batch,sem,year,course_slot, programme_type) :
def random_algo(batch,sem,year,course_slot, programme_type, skip_course_ids=None) :
# zero seats makes the "slot full" branch below push these students to
# their next priority, so skipping reuses the existing fall-through
skip_course_ids = {int(c) for c in (skip_course_ids or [])}
unique_course = InitialRegistration.objects.filter(Q(semester_id__semester_no = sem) & Q( course_slot_id__name = course_slot ) & Q(student_id__batch = batch) & Q(student_id__batch_id__curriculum__programme__category=programme_type)).values_list('course_id',flat=True).distinct()
max_seats={}
seats_alloted = {}
present_priority = {}
next_priority = {}
total_seats = 0
for course in unique_course :
max_seats[course] = Course.objects.get(id=course).max_seats
total_seats+=max_seats[course]
if course in skip_course_ids :
max_seats[course] = 0
else :
max_seats[course] = Course.objects.get(id=course).max_seats
total_seats+=max_seats[course]
seats_alloted[course] = FinalRegistration.objects.filter(
course_id_id=course,
semester_id__semester_no=sem,
Expand Down Expand Up @@ -108,12 +175,23 @@ def allocate(request):
sem = request.POST.get('sem')
year = request.POST.get('year')
programme_type = request.POST.get('programme_type')
skip_course_ids = {int(c) for c in (request.POST.get('skip_course_ids') or [])}

# write nothing until every mis-slotted course is added or skipped
problems = validate_course_slots(batch, sem, programme_type, skip_course_ids)
if problems:
return JsonResponse({
'status': 0,
'message': "Some registered courses are missing from their course slot.",
'needs_action': problems,
}, status=409)

unique_course_slot = InitialRegistration.objects.filter(
Q(semester_id__semester_no=sem) & Q(student_id__batch=batch) & Q(student_id__batch_id__curriculum__programme__category=programme_type)
).values('course_slot_id').distinct()

unique_course_name = []
skipped_students = 0

try:
with transaction.atomic():
Expand Down Expand Up @@ -143,6 +221,11 @@ def allocate(request):
student_id_id=student_id
).values_list('course_id', flat=True).first()

# no alternative in a single-choice slot
if course_id in skip_course_ids:
skipped_students += 1
continue

course = Course.objects.get(id=course_id)

prev_registration_id = InitialRegistration.objects.filter(
Expand All @@ -167,15 +250,21 @@ def allocate(request):

elif course_slot_object.type == "Open Elective":
if course_slot_object.name not in unique_course_name:
stat = random_algo(batch, sem, year, course_slot_object.name, programme_type)
stat = random_algo(batch, sem, year, course_slot_object.name,
programme_type, skip_course_ids)
unique_course_name.append(course_slot_object.name)
if stat == -1:
raise Exception(f"Seats not enough for course_slot {course_slot_object.name}")

return JsonResponse({'status': 1, 'message': "Course allocation successful"})
message = "Course allocation successful"
if skip_course_ids:
message += (f" ({len(skip_course_ids)} course(s) skipped"
f"; {skipped_students} student(s) left without a course in a single-choice slot)")
return JsonResponse({'status': 1, 'message': message,
'skipped_course_ids': sorted(skip_course_ids)})

except:
return JsonResponse({'status': -1, 'message': "Seats not enough for some course_slot"})
except Exception as e:
return JsonResponse({'status': -1, 'message': str(e) or "Allocation failed"})

def view_alloted_course(request) :
batch = request.POST.get('batch')
Expand Down
Loading