diff --git a/FusionIIIT/applications/globals/api/views.py b/FusionIIIT/applications/globals/api/views.py index 50b969321..f06e58379 100644 --- a/FusionIIIT/applications/globals/api/views.py +++ b/FusionIIIT/applications/globals/api/views.py @@ -1,3 +1,5 @@ +import ast + from django.contrib.auth import get_user_model from applications.academic_information.models import Student from applications.eis.api.views import profile as eis_profile @@ -22,6 +24,7 @@ Issue, IssueImage, DepartmentInfo, ModuleAccess) from .utils import get_and_authenticate_user from notifications.models import Notification +import ast User = get_user_model() @@ -107,7 +110,34 @@ def auth_view(request): @permission_classes([IsAuthenticated]) @authentication_classes([TokenAuthentication]) def notification(request): - notifications=serializers.NotificationSerializer(request.user.notifications.all(),many=True).data + user_notifications = request.user.notifications.all().order_by('-timestamp') + active_role = getattr(getattr(request.user, 'extrainfo', None), 'last_selected_role', None) + active_role = active_role.strip().lower() if isinstance(active_role, str) and active_role.strip() else None + if active_role and 'hod' in active_role: + active_role = 'hod' + + filtered_notifications = [] + for notification_obj in user_notifications: + data = notification_obj.data + parsed_data = {} + + if isinstance(data, dict): + parsed_data = data + elif isinstance(data, str): + try: + parsed_data = ast.literal_eval(data) + except Exception: + parsed_data = {} + + recipient_role = parsed_data.get('recipient_role') + recipient_role = recipient_role.strip().lower() if isinstance(recipient_role, str) and recipient_role.strip() else None + + if active_role and recipient_role and recipient_role != active_role: + continue + + filtered_notifications.append(notification_obj) + + notifications = serializers.NotificationSerializer(filtered_notifications, many=True).data resp={ 'notifications':notifications, diff --git a/FusionIIIT/applications/otheracademic/api/serializers.py b/FusionIIIT/applications/otheracademic/api/serializers.py index 4890ba22a..89d9aff4d 100644 --- a/FusionIIIT/applications/otheracademic/api/serializers.py +++ b/FusionIIIT/applications/otheracademic/api/serializers.py @@ -1,16 +1,140 @@ +""" +Serializers for otheracademic module. +Contains separate input and output serializers for validation and response formatting. +""" from rest_framework import serializers -from applications.otheracademic.models import LeaveFormTable,BonafideFormTableUpdated +from applications.otheracademic.models import ( + LeaveFormTable, + LeavePG, + BonafideFormTableUpdated, + AssistantshipClaimFormStatusUpd, + NoDues, + LeaveTypeChoices, + LeaveTypePGChoices, +) + + +# ==================== LEAVE SERIALIZERS ==================== + +class LeaveFormInputSerializer(serializers.Serializer): + """Input serializer for UG leave form submission.""" + date_from = serializers.DateField() + date_to = serializers.DateField() + leave_type = serializers.ChoiceField(choices=LeaveTypeChoices.choices) + address = serializers.CharField(max_length=100) + purpose = serializers.CharField() + hod_credential = serializers.CharField(max_length=100) + semester = serializers.IntegerField(min_value=1, max_value=8) + mobile_number = serializers.CharField(max_length=15, required=False, allow_blank=True) + parents_mobile = serializers.CharField(max_length=15, required=False, allow_blank=True) + mobile_during_leave = serializers.CharField(max_length=15, required=False, allow_blank=True) + + def validate(self, data): + if data['date_from'] > data['date_to']: + raise serializers.ValidationError("Start date cannot be after end date.") + return data + + +class LeavePGInputSerializer(serializers.Serializer): + """Input serializer for PG leave form submission.""" + date_from = serializers.DateField() + date_to = serializers.DateField() + leave_type = serializers.ChoiceField(choices=LeaveTypePGChoices.choices) + address = serializers.CharField(max_length=100) + purpose = serializers.CharField() + hod_credential = serializers.CharField(max_length=100) + ta_superCredential = serializers.CharField(max_length=100) + thesis_superCredential = serializers.CharField(max_length=100) + semester = serializers.IntegerField(min_value=1, max_value=8) + mobile_number = serializers.CharField(max_length=15, required=False, allow_blank=True) + parents_mobile = serializers.CharField(max_length=15, required=False, allow_blank=True) + mobile_during_leave = serializers.CharField(max_length=15, required=False, allow_blank=True) + + def validate(self, data): + if data['date_from'] > data['date_to']: + raise serializers.ValidationError("Start date cannot be after end date.") + return data + class LeaveFormSerializer(serializers.ModelSerializer): + """Output serializer for leave form.""" class Meta: model = LeaveFormTable - fields = '__all__' + fields = [ + 'id', + 'student_name', + 'roll_no', + 'date_from', + 'date_to', + 'date_of_application', + 'upload_file', + 'address', + 'purpose', + 'leave_type', + 'status', + 'hod', + 'stud_mobile_no', + 'parent_mobile_no', + 'leave_mobile_no', + 'curr_sem', + ] + + +class LeavePGSerializer(serializers.ModelSerializer): + """Output serializer for PG leave form.""" + class Meta: + model = LeavePG + fields = [ + 'id', + 'student_name', + 'roll_no', + 'date_from', + 'date_to', + 'date_of_application', + 'upload_file', + 'address', + 'purpose', + 'leave_type', + 'status', + 'hod', + 'ta_supervisor', + 'thesis_supervisor', + 'stud_mobile_no', + 'parent_mobile_no', + 'leave_mobile_no', + 'curr_sem', + ] + + +class LeaveStatusUpdateSerializer(serializers.Serializer): + """Input serializer for updating leave status.""" + approvedLeaves = serializers.ListField( + child=serializers.IntegerField(), + required=False, + default=[] + ) + rejectedLeaves = serializers.ListField( + child=serializers.IntegerField(), + required=False, + default=[] + ) + + +# ==================== BONAFIDE SERIALIZERS ==================== + +class BonafideFormInputSerializer(serializers.Serializer): + """Input serializer for bonafide form submission.""" + branch = serializers.CharField(max_length=50) + semester = serializers.CharField(max_length=20) + purpose = serializers.CharField() class BonafideFormSerializer(serializers.ModelSerializer): + """Output serializer for bonafide form.""" class Meta: model = BonafideFormTableUpdated fields = [ + 'id', 'student_names', 'roll_nos', 'branch_types', @@ -19,5 +143,228 @@ class Meta: 'date_of_applications', 'approve', 'reject', - 'download_file' - ] \ No newline at end of file + 'download_file', + ] + + +class BonafideStatusSerializer(serializers.Serializer): + """Output serializer for bonafide status.""" + rollNo = serializers.CharField() + name = serializers.CharField() + branch = serializers.CharField() + semester = serializers.CharField() + purpose = serializers.CharField() + dateApplied = serializers.DateField() + status = serializers.CharField() + downloadUrl = serializers.CharField(allow_null=True, required=False) + + +class BonafideStatusUpdateSerializer(serializers.Serializer): + """Input serializer for updating bonafide status.""" + approvedBonafides = serializers.ListField( + child=serializers.IntegerField(), + required=False, + default=[] + ) + rejectedBonafides = serializers.ListField( + child=serializers.IntegerField(), + required=False, + default=[] + ) + + +# ==================== ASSISTANTSHIP SERIALIZERS ==================== + +class AssistantshipFormInputSerializer(serializers.Serializer): + """Input serializer for assistantship form submission.""" + discipline = serializers.CharField(max_length=100) + date_from = serializers.DateField() + date_to = serializers.DateField() + date_applied = serializers.DateField() + bank_account_no = serializers.CharField(max_length=100) + ta_supervisor = serializers.CharField(max_length=100) + thesis_supervisor = serializers.CharField(max_length=100) + hod = serializers.CharField(max_length=100) + applicability = serializers.CharField(max_length=100) + + def validate(self, data): + if data['date_from'] > data['date_to']: + raise serializers.ValidationError("Start date cannot be after end date.") + return data + + +class AssistantshipFormSerializer(serializers.ModelSerializer): + """Output serializer for assistantship form.""" + class Meta: + model = AssistantshipClaimFormStatusUpd + fields = [ + 'id', + 'roll_no', + 'student_name', + 'discipline', + 'dateFrom', + 'dateTo', + 'bank_account', + 'dateApplied', + 'ta_supervisor', + 'thesis_supervisor', + 'hod', + 'applicability', + 'TA_approved', + 'TA_rejected', + 'Ths_approved', + 'Ths_rejected', + 'HOD_approved', + 'HOD_rejected', + 'Acad_approved', + 'Acad_rejected', + ] + + +class AssistantshipStatusUpdateSerializer(serializers.Serializer): + """Input serializer for updating assistantship status.""" + approvedRequests = serializers.ListField( + child=serializers.IntegerField(), + required=False, + default=[] + ) + rejectedRequests = serializers.ListField( + child=serializers.IntegerField(), + required=False, + default=[] + ) + + +class AssistantshipStatusSerializer(serializers.Serializer): + """Output serializer for assistantship status.""" + rollNo = serializers.CharField() + name = serializers.CharField() + discipline = serializers.CharField() + dateApplied = serializers.DateField() + bank_account = serializers.CharField() + status = serializers.CharField() + approvalStages = serializers.DictField() + + +class TAAssignmentItemSerializer(serializers.Serializer): + """Single TA assignment entry.""" + roll_no = serializers.CharField(max_length=20) + subject_ids = serializers.ListField( + child=serializers.IntegerField(min_value=1), + required=False, + allow_empty=True, + ) + subject_id = serializers.IntegerField(min_value=1, required=False) + + def validate(self, attrs): + subject_ids = attrs.get("subject_ids") + subject_id = attrs.get("subject_id") + + if subject_ids is not None: + attrs["subject_ids"] = list(dict.fromkeys(subject_ids)) + elif subject_id is not None: + attrs["subject_ids"] = [subject_id] + else: + raise serializers.ValidationError("Provide subject_ids or subject_id.") + + attrs.pop("subject_id", None) + return attrs + + +class TAAssignmentUpdateSerializer(serializers.Serializer): + """Payload for bulk TA assignment updates.""" + assignments = serializers.ListField( + child=TAAssignmentItemSerializer(), + required=True, + allow_empty=False, + ) + + +class FacultySupervisorAssignmentItemSerializer(serializers.Serializer): + """Single faculty supervisor assignment entry.""" + roll_no = serializers.CharField(max_length=20) + faculty_user_id = serializers.IntegerField(min_value=1) + + +class FacultySupervisorAssignmentUpdateSerializer(serializers.Serializer): + """Payload for bulk faculty supervisor assignment updates.""" + assignments = serializers.ListField( + child=FacultySupervisorAssignmentItemSerializer(), + required=True, + allow_empty=False, + ) + +# ==================== NO-DUES SERIALIZERS ==================== + +class NoDuesStatusSerializer(serializers.ModelSerializer): + """Output serializer for no-dues status.""" + roll_no_value = serializers.CharField(source='roll_no.roll_no', read_only=True) + + class Meta: + model = NoDues + fields = [ + 'id', + 'roll_no_value', + 'name', + 'library_clear', + 'library_notclear', + 'hostel_clear', + 'hostel_notclear', + 'mess_clear', + 'mess_notclear', + 'ece_clear', + 'ece_notclear', + 'physics_lab_clear', + 'physics_lab_notclear', + 'mechatronics_lab_clear', + 'mechatronics_lab_notclear', + 'cc_clear', + 'cc_notclear', + 'workshop_clear', + 'workshop_notclear', + 'signal_processing_lab_clear', + 'signal_processing_lab_notclear', + 'vlsi_clear', + 'vlsi_notclear', + 'design_studio_clear', + 'design_studio_notclear', + 'design_project_clear', + 'design_project_notclear', + 'bank_clear', + 'bank_notclear', + 'icard_dsa_clear', + 'icard_dsa_notclear', + 'account_clear', + 'account_notclear', + 'btp_supervisor_clear', + 'btp_supervisor_notclear', + 'discipline_office_clear', + 'discipline_office_notclear', + 'student_gymkhana_clear', + 'student_gymkhana_notclear', + 'alumni_clear', + 'alumni_notclear', + 'placement_cell_clear', + 'placement_cell_notclear', + ] + + +class NoDuesInitiateSerializer(serializers.Serializer): + """Input serializer for initiating no-dues clearance.""" + pass # No input needed for initiation, just triggers creation + + +class NoDuesVerificationSerializer(serializers.Serializer): + """Input serializer for department to verify no-dues clearance.""" + no_dues_id = serializers.IntegerField() + department = serializers.CharField(max_length=100) + is_clear = serializers.BooleanField() + + +class NoDuesCertificateSerializer(serializers.Serializer): + """Output serializer for no-dues certificate.""" + roll_no = serializers.CharField() + name = serializers.CharField() + all_clear = serializers.BooleanField() + issued_date = serializers.DateTimeField() + certificate_url = serializers.CharField(allow_null=True) \ No newline at end of file diff --git a/FusionIIIT/applications/otheracademic/api/urls.py b/FusionIIIT/applications/otheracademic/api/urls.py index 9fe6b1fd4..c71d7afe5 100644 --- a/FusionIIIT/applications/otheracademic/api/urls.py +++ b/FusionIIIT/applications/otheracademic/api/urls.py @@ -13,12 +13,16 @@ path('update-leave-status-thesis/', views.UpdateLeaveStatusThesis.as_view(), name='update-leave-status-thesis'), path('get-leave-requests/', views.GetLeaveRequests.as_view(), name='get-leave-requests'), path('get-pg-leave-requests/', views.GetPGLeaveRequests.as_view(), name='get-pg-leave-requests'), + path('withdraw-ug-leave//', views.WithdrawUGLeave.as_view(), name='withdraw-ug-leave'), + path('withdraw-pg-leave//', views.WithdrawPGLeave.as_view(), name='withdraw-pg-leave'), #Bonafide_form URLs path('bonafide-form-submit/', views.BonafideFormSubmitView.as_view(), name='bonafide-form-submit'), path('admin-bonafide-requests/',views.FetchPendingBonafideRequests.as_view(),name='admin-bonafide-requests'), path('admin-updates/',views.UpdateBonafideStatus.as_view(),name='admin-updates'), + path('bonafide-certificate-upload//', views.UploadBonafideCertificate.as_view(), name='bonafide-certificate-upload'), path('bonafide-status/',views.GetBonafideStatus.as_view(),name='bonafide-status'), + path('withdraw-bonafide//', views.WithdrawBonafide.as_view(), name='withdraw-bonafide'), #TA_Assiistantship URLs path('assistantship-form-submit/',views.AssistantshipFormSubmitView.as_view(),name='assistantship-form-submit'), @@ -35,5 +39,18 @@ path('director-pending-requests/', views.DirectorFetchPendingAssistantshipRequests.as_view(), name='director-pending-requests'), path('director-update-status/', views.DirectorUpdateAssistantshipStatus.as_view(), name='director-update-status'), path('get_assistantship_status/', views.GetAssistantshipStatus.as_view(), name='get_assistantship_status'), + path('withdraw-assistantship//', views.WithdrawAssistantship.as_view(), name='withdraw-assistantship'), + path('ta-assignment-options/', views.FetchTAAssignmentOptions.as_view(), name='ta-assignment-options'), + path('ta-assignment-update/', views.UpdateTAAssignments.as_view(), name='ta-assignment-update'), + path('faculty-supervisor-assignment-options/', views.FetchFacultySupervisorAssignmentOptions.as_view(), name='faculty-supervisor-assignment-options'), + path('faculty-supervisor-assignment-update/', views.UpdateFacultySupervisorAssignments.as_view(), name='faculty-supervisor-assignment-update'), # path('assistantship-status-update/', views.UpdateAssistantshipStatus.as_view(), name='assistantship-status-update'), + + # No-Dues URLs + path('no-dues-initiate/', views.InitiateNoDuesView.as_view(), name='no-dues-initiate'), + path('no-dues-status/', views.GetNoDuesStatusView.as_view(), name='no-dues-status'), + path('no-dues-verify/', views.VerifyNoDuesView.as_view(), name='no-dues-verify'), + path('no-dues-track/', views.TrackNoDuesProgressView.as_view(), name='no-dues-track'), + path('no-dues-pending/', views.ListPendingNoDuesView.as_view(), name='no-dues-pending'), + path('no-dues-certificate/', views.DownloadNoDuesCertificateView.as_view(), name='no-dues-certificate'), ] \ No newline at end of file diff --git a/FusionIIIT/applications/otheracademic/api/views.py b/FusionIIIT/applications/otheracademic/api/views.py index 8027e1c4a..526d83ed9 100644 --- a/FusionIIIT/applications/otheracademic/api/views.py +++ b/FusionIIIT/applications/otheracademic/api/views.py @@ -1,669 +1,378 @@ +""" +API views for otheracademic module. +Views are thin - they validate input, call services/selectors, and return responses. +All business logic is in services.py, all DB queries are in selectors.py. +""" +from datetime import datetime + +from django.utils import timezone from rest_framework.views import APIView from rest_framework.response import Response from rest_framework import status -from .serializers import LeaveFormSerializer ,BonafideFormSerializer -from datetime import date -from datetime import datetime -from django.shortcuts import render -from django.contrib import messages -from django.shortcuts import render, get_object_or_404, redirect,render -from rest_framework.permissions import IsAuthenticated -from rest_framework.decorators import api_view, permission_classes -from django.views.decorators.csrf import csrf_exempt -from django.contrib.auth.decorators import login_required -from django.contrib.auth.models import User -from applications.otheracademic.models import (LeaveFormTable, BonafideFormTableUpdated, GraduateSeminarFormTable,AssistantshipClaimFormStatusUpd, LeavePG, NoDues) -from datetime import date -from applications.filetracking.models import File -from applications.filetracking.sdk.methods import create_file -from notification.views import otheracademic_notif -from applications.filetracking.models import * -from applications.filetracking.sdk.methods import * -from applications.globals.models import ExtraInfo, HoldsDesignation, Designation -from django.http import JsonResponse -from django.db.models import F - +from rest_framework.permissions import IsAuthenticated +from rest_framework.exceptions import PermissionDenied +from notifications.signals import notify + +from applications.otheracademic import services, selectors +from applications.otheracademic.models import LeaveStatusChoices, NoDues +from .serializers import ( + LeaveFormInputSerializer, + LeavePGInputSerializer, + LeaveStatusUpdateSerializer, + BonafideFormInputSerializer, + BonafideStatusUpdateSerializer, + AssistantshipFormInputSerializer, + AssistantshipStatusUpdateSerializer, + TAAssignmentUpdateSerializer, + FacultySupervisorAssignmentUpdateSerializer, + NoDuesStatusSerializer, + NoDuesVerificationSerializer, + NoDuesCertificateSerializer, +) + + +# ==================== LEAVE VIEWS ==================== class LeaveFormSubmitView(APIView): - permission_classes = [IsAuthenticated] + """Submit a UG leave application.""" + permission_classes = [IsAuthenticated] def post(self, request): - # Extract data from the request data = request.POST file = request.FILES.get('related_document') - hodname = data.get('hod_credential') - print(data.get('mobile_number'),data.get('parents_mobile'),"hello ab") - - # Create a new LeaveFormTable instance and save it to the database - leave = LeaveFormTable.objects.create( - student_name=request.user.first_name+request.user.last_name, - roll_no=request.user.extrainfo, - date_from=data.get('date_from'), - date_to=data.get('date_to'), - leave_type=data.get('leave_type'), - upload_file=file, - address=data.get('address'), - purpose=data.get('purpose'), - date_of_application=date.today(), - # approved=False, # Initially not approved - # rejected=False, # Initially not rejected - stud_mobile_no=data.get('mobile_number'), - parent_mobile_no=data.get('parents_mobile'), - leave_mobile_no=data.get('mobile_during_leave'), - curr_sem=int(data.get('semester')), - hod=data.get('hod_credential') - ) - print(data.get('mobile_number'),data.get('parents_mobile')) - - leave_hod = User.objects.get(username=hodname) - receiver_value = User.objects.get(username=request.user.username) - receiver_value_designation = HoldsDesignation.objects.filter(user=receiver_value) - lis = list(receiver_value_designation) - obj = lis[0].designation - - file_id = create_file( - uploader=request.user.username, - uploader_designation=obj, - receiver=leave_hod, - receiver_designation="student", - src_module="otheracademic", - src_object_id=leave.id, - file_extra_JSON={"value": 2}, - attached_file=None, - subject='ug_leave' - ) - # new_tracking = Tracking.objects.create( - # file_id=file_id, # The newly created file object - # uploader=request.user.username, - # uploader_designation=obj, - # receiver=leave_hod, - # receive_design=receiver_designation_obj, # Receiver's designation object - # tracking_extra_JSON=file_extra_JSON, # Additional metadata in JSON format - # remarks=f"File with id:{file_id} created by {uploader} and sent to {receiver}" # Remarks for this tracking event - # ) - - message = "A new leave application" - otheracademic_notif(request.user, leave_hod, 'ug_leave_hod', leave.id, 'student', message) - - return Response({"message": "You successfully submitted your form"}, status=status.HTTP_201_CREATED) - + try: + leave = services.submit_ug_leave( + user=request.user, + date_from=data.get('date_from'), + date_to=data.get('date_to'), + leave_type=data.get('leave_type'), + address=data.get('address'), + purpose=data.get('purpose'), + hod_credential=data.get('hod_credential'), + semester=data.get('semester'), + mobile_number=data.get('mobile_number'), + parents_mobile=data.get('parents_mobile'), + mobile_during_leave=data.get('mobile_during_leave'), + upload_file=file, + ) + return Response( + {"message": "You successfully submitted your form"}, + status=status.HTTP_201_CREATED + ) + except services.LeaveServiceError as e: + return Response({"error": str(e)}, status=status.HTTP_400_BAD_REQUEST) class LeavePGSubmitView(APIView): - permission_classes = [IsAuthenticated] + """Submit a PG leave application.""" + permission_classes = [IsAuthenticated] def post(self, request): - # Extract data from the request data = request.POST file = request.FILES.get('related_document') - hodname = data.get('hod_credential') - ta_super = data.get('ta_superCredential') - thesis_super = data.get('thesis_superCredential') - print(data,"hello ab") - - leave = LeavePG.objects.create( - student_name=request.user.first_name+request.user.last_name, - roll_no=request.user.extrainfo, - date_from=data.get('date_from'), - date_to=data.get('date_to'), - leave_type=data.get('leave_type'), - upload_file=file, - address=data.get('address'), - purpose=data.get('purpose'), - date_of_application=date.today(), - stud_mobile_no=data.get('mobile_number'), - parent_mobile_no=data.get('parents_mobile'), - leave_mobile_no=data.get('mobile_during_leave'), - curr_sem=int(data.get('semester')), - hod=data.get('hod_credential'), - ta_supervisor=data.get('ta_superCredential'), - thesis_supervisor=data.get('thesis_superCredential'), - ) - print(data.get('ta_superCredential'),data.get('thesis_supercredential'),"check point") - - leave_ta = User.objects.get(username=ta_super) - leave_thesis = User.objects.get(username=thesis_super) - leave_hod = User.objects.get(username=hodname) - receiver_value = User.objects.get(username=request.user.username) - receiver_value_designation = HoldsDesignation.objects.filter(user=receiver_value) - lis = list(receiver_value_designation) - obj = lis[0].designation - - file_id = create_file( - uploader=request.user.username, - uploader_designation=obj, - receiver=leave_hod, - receiver_designation="student", - src_module="otheracademic", - src_object_id=leave.id, - file_extra_JSON={"value": 2}, - attached_file=None, - subject='pg_leave' - ) - - # new_tracking = Tracking.objects.create( - # file_id=file_id, # The newly created file object - # uploader=request.user.username, - # uploader_designation=obj, - # receiver=leave_hod, - # receive_design=receiver_designation_obj, # Receiver's designation object - # tracking_extra_JSON=file_extra_JSON, # Additional metadata in JSON format - # remarks=f"File with id:{file_id} created by {uploader} and sent to {receiver}" # Remarks for this tracking event - # ) - - message = "A new leave application" - otheracademic_notif(request.user, leave_ta, 'pg_leave_at', leave.id, 'student', message) - - return Response({"message": "You successfully submitted your form"}, status=status.HTTP_201_CREATED) - + try: + leave = services.submit_pg_leave( + user=request.user, + date_from=data.get('date_from'), + date_to=data.get('date_to'), + leave_type=data.get('leave_type'), + address=data.get('address'), + purpose=data.get('purpose'), + hod_credential=data.get('hod_credential'), + ta_supervisor_credential=data.get('ta_superCredential'), + thesis_supervisor_credential=data.get('thesis_superCredential'), + semester=data.get('semester'), + mobile_number=data.get('mobile_number'), + parents_mobile=data.get('parents_mobile'), + mobile_during_leave=data.get('mobile_during_leave'), + upload_file=file, + ) + return Response( + {"message": "You successfully submitted your form"}, + status=status.HTTP_201_CREATED + ) + except services.LeaveServiceError as e: + return Response({"error": str(e)}, status=status.HTTP_400_BAD_REQUEST) class FetchPendingLeaveRequests(APIView): + """Fetch pending leave requests for HOD approval.""" permission_classes = [IsAuthenticated] - def get(self, request, *args, **kwargs): # Add request as a parameter - # Filter for pending leave requests - pending_leaves = LeaveFormTable.objects.filter(status="Pending") - pending_leaves_pg = LeavePG.objects.filter(status=F('thesis_supervisor')) + def get(self, request, *args, **kwargs): + if not selectors.user_has_designation_contains(request.user, "hod"): + raise PermissionDenied("Only HOD can access this queue.") - # Serialize the data - data = [ - { - "id": leave.id, - "rollNo": leave.roll_no.id, # Assuming roll_number is the field in ExtraInfo - "name": leave.student_name, - "form": leave.upload_file.url if leave.upload_file else None, - "details": { - "dateFrom": leave.date_from, - "dateTo": leave.date_to, - "leaveType": leave.leave_type, - "address": leave.address, - "purpose": leave.purpose, - "hodCredential": leave.hod, - "mobileNumber": leave.stud_mobile_no, - "parentsMobile": leave.parent_mobile_no, - "mobileDuringLeave": leave.leave_mobile_no, - "semester": leave.curr_sem, - "academicYear": leave.date_of_application.year, - "dateOfApplication": leave.date_of_application, - }, - } - for leave in pending_leaves - ] - - for leave_pg in pending_leaves_pg: - data.append({ - "id": leave_pg.id, - "rollNo": leave_pg.roll_no.id, # Adjust this field based on your model - "name": leave_pg.student_name, - "form": leave_pg.upload_file.url if leave_pg.upload_file else None, - "details": { - "dateFrom": leave_pg.date_from, - "dateTo": leave_pg.date_to, - "leaveType": leave_pg.leave_type, - "address": leave_pg.address, - "purpose": leave_pg.purpose, - "hodCredential": leave_pg.hod, - "mobileNumber": leave_pg.stud_mobile_no, - "parentsMobile": leave_pg.parent_mobile_no, - "mobileDuringLeave": leave_pg.leave_mobile_no, - "semester": leave_pg.curr_sem, - "academicYear": leave_pg.date_of_application.year, - "dateOfApplication": leave_pg.date_of_application, - }, - }) + # Get pending UG leaves + pending_ug = selectors.get_pending_ug_leaves_for_hod(request.user.username) + data = [selectors.serialize_ug_leave(leave) for leave in pending_ug] + + # Get pending PG leaves (for HOD) + pending_pg = selectors.get_pending_pg_leaves_for_hod_user(request.user.username) + for leave in pending_pg: + data.append(selectors.serialize_pg_leave(leave)) return Response(data) - + class FetchPendingLeaveRequestsTA(APIView): + """Fetch pending PG leave requests for TA supervisor approval.""" permission_classes = [IsAuthenticated] - def get(self, request, *args, **kwargs): # Add request as a parameter - # Filter for pending leave requests - pending_leaves = LeavePG.objects.filter(status="Pending") - - # Serialize the data - data = [ - { - "id": leave.id, - "rollNo": leave.roll_no.id, # Assuming roll_number is the field in ExtraInfo - "name": leave.student_name, - "form": leave.upload_file.url if leave.upload_file else None, - "details": { - "dateFrom": leave.date_from, - "dateTo": leave.date_to, - "leaveType": leave.leave_type, - "address": leave.address, - "purpose": leave.purpose, - "hodCredential": leave.hod, - "mobileNumber": leave.stud_mobile_no, - "parentsMobile": leave.parent_mobile_no, - "mobileDuringLeave": leave.leave_mobile_no, - "semester": leave.curr_sem, - "academicYear": leave.date_of_application.year, - "dateOfApplication": leave.date_of_application, - }, - } - for leave in pending_leaves - ] - + def get(self, request, *args, **kwargs): + pending_leaves = selectors.get_pending_pg_leaves_for_ta_user(request.user.username) + data = [selectors.serialize_pg_leave(leave) for leave in pending_leaves] return Response(data) - + class FetchPendingLeaveRequestsThesis(APIView): + """Fetch pending PG leave requests for Thesis supervisor approval.""" permission_classes = [IsAuthenticated] - def get(self, request, *args, **kwargs): # Add request as a parameter - # Filter for pending leave requests - pending_leaves = LeavePG.objects.filter(status=F('ta_supervisor')) + def get(self, request, *args, **kwargs): + pending_leaves = selectors.get_pending_pg_leaves_for_thesis_user(request.user.username) + data = [selectors.serialize_pg_leave(leave) for leave in pending_leaves] + return Response(data) - # Serialize the data - data = [ - { - "id": leave.id, - "rollNo": leave.roll_no.id, # Assuming roll_number is the field in ExtraInfo - "name": leave.student_name, - "form": leave.upload_file.url if leave.upload_file else None, - "details": { - "dateFrom": leave.date_from, - "dateTo": leave.date_to, - "leaveType": leave.leave_type, - "address": leave.address, - "purpose": leave.purpose, - "hodCredential": leave.hod, - "mobileNumber": leave.stud_mobile_no, - "parentsMobile": leave.parent_mobile_no, - "mobileDuringLeave": leave.leave_mobile_no, - "semester": leave.curr_sem, - "academicYear": leave.date_of_application.year, - "dateOfApplication": leave.date_of_application, - }, - } - for leave in pending_leaves - ] - return Response(data) - class UpdateLeaveStatus(APIView): + """Update leave status (HOD approval for UG and final approval for PG).""" permission_classes = [IsAuthenticated] def post(self, request, *args, **kwargs): - # Get the list of approved and rejected leave ids from the request - approved_leaves_ids = request.data.get('approvedLeaves', []) - rejected_leaves_ids = request.data.get('rejectedLeaves', []) - approved_leaves_ids1 = request.data.get('approvedLeaves', []) - rejected_leaves_ids1 = request.data.get('rejectedLeaves', []) - # Update the status of approved leaves - approved_leaves = LeaveFormTable.objects.filter(id__in=approved_leaves_ids) - approved_leaves.update(status="Approved") - - # Update the status of rejected leaves - rejected_leaves = LeaveFormTable.objects.filter(id__in=rejected_leaves_ids) - rejected_leaves.update(status="Rejected") - - approved_leaves1 = LeavePG.objects.filter(id__in=approved_leaves_ids1) - approved_leaves1.update(status="Approved") - - # Update the status of rejected leaves - rejected_leaves1 = LeavePG.objects.filter(id__in=rejected_leaves_ids1) - rejected_leaves1.update(status="Rejected") + if not selectors.user_has_designation_contains(request.user, "hod"): + raise PermissionDenied("Only HOD can approve or reject leave requests.") + + serializer = LeaveStatusUpdateSerializer(data=request.data) + serializer.is_valid(raise_exception=True) + + approved_ids = serializer.validated_data.get('approvedLeaves', []) + rejected_ids = serializer.validated_data.get('rejectedLeaves', []) + + try: + services.update_ug_leave_status(approved_ids, rejected_ids, request.user) + services.update_pg_leave_status_hod(approved_ids, rejected_ids, request.user) + except services.LeaveServiceError as e: + return Response({"error": str(e)}, status=status.HTTP_400_BAD_REQUEST) return Response({"message": "Leave statuses updated successfully."}) - + + class UpdateLeaveStatusTA(APIView): + """Update PG leave status (TA supervisor approval).""" permission_classes = [IsAuthenticated] def post(self, request, *args, **kwargs): - # Get the list of approved and rejected leave ids from the request - approved_leaves_ids = request.data.get('approvedLeaves', []) - rejected_leaves_ids = request.data.get('rejectedLeaves', []) + serializer = LeaveStatusUpdateSerializer(data=request.data) + serializer.is_valid(raise_exception=True) - # Update the status of approved leaves - approved_leaves = LeavePG.objects.filter(id__in=approved_leaves_ids) - approved_leaves.update(status=F('ta_supervisor')) + approved_ids = serializer.validated_data.get('approvedLeaves', []) + rejected_ids = serializer.validated_data.get('rejectedLeaves', []) - # Update the status of rejected leaves - rejected_leaves = LeavePG.objects.filter(id__in=rejected_leaves_ids) - rejected_leaves.update(status="Rejected") + try: + services.update_pg_leave_status_ta(approved_ids, rejected_ids, request.user) + except services.LeaveServiceError as e: + return Response({"error": str(e)}, status=status.HTTP_400_BAD_REQUEST) return Response({"message": "Leave statuses updated successfully."}) - + + class UpdateLeaveStatusThesis(APIView): + """Update PG leave status (Thesis supervisor approval).""" permission_classes = [IsAuthenticated] def post(self, request, *args, **kwargs): - # Get the list of approved and rejected leave ids from the request - approved_leaves_ids = request.data.get('approvedLeaves', []) - rejected_leaves_ids = request.data.get('rejectedLeaves', []) + serializer = LeaveStatusUpdateSerializer(data=request.data) + serializer.is_valid(raise_exception=True) - # Update the status of approved leaves - approved_leaves = LeavePG.objects.filter(id__in=approved_leaves_ids) - approved_leaves.update(status=F('thesis_supervisor')) + approved_ids = serializer.validated_data.get('approvedLeaves', []) + rejected_ids = serializer.validated_data.get('rejectedLeaves', []) - # Update the status of rejected leaves - rejected_leaves = LeavePG.objects.filter(id__in=rejected_leaves_ids) - rejected_leaves.update(status="Rejected") + try: + services.update_pg_leave_status_thesis(approved_ids, rejected_ids, request.user) + except services.LeaveServiceError as e: + return Response({"error": str(e)}, status=status.HTTP_400_BAD_REQUEST) return Response({"message": "Leave statuses updated successfully."}) + class GetLeaveRequests(APIView): - + """Get leave requests for a specific student (UG).""" permission_classes = [IsAuthenticated] def get(self, request, *args, **kwargs): - # Get roll_no and username from query params - - roll_no_id = request.query_params.get('roll_no') - username = request.query_params.get('username') - print(roll_no_id,username) - - # print(f"Received roll_no: {roll_no_id}, username: {username}") - - - # # Filter the leave requests based on roll_no and student_name (username) - leave_requests = LeaveFormTable.objects.filter( - roll_no=roll_no_id - ) + roll_no_id = request.user.extrainfo.id - # Serialize the data (assuming the serializer is defined for LeaveFormTable) - data = [ - { - "rollNo": roll_no_id, # Assuming roll_number is the field in ExtraInfo - "name": leave.student_name, - "dateFrom": leave.date_from, - "dateTo": leave.date_to, - "leaveType": leave.leave_type, - "attachment": leave.upload_file.url if leave.upload_file else None, - "purpose": leave.purpose, - "address": leave.address, - "action": leave.status, - } - for leave in leave_requests - ] - print(data) + leave_requests = selectors.get_ug_leaves_by_roll_no(roll_no_id) + data = [selectors.serialize_leave_status(leave, roll_no_id) for leave in leave_requests] return Response(data, status=status.HTTP_200_OK) class GetPGLeaveRequests(APIView): - + """Get leave requests for a specific student (PG).""" permission_classes = [IsAuthenticated] def get(self, request, *args, **kwargs): - # Get roll_no and username from query params - - roll_no_id = request.query_params.get('roll_no') - username = request.query_params.get('username') - print(roll_no_id,username) - - # print(f"Received roll_no: {roll_no_id}, username: {username}") - - - # # Filter the leave requests based on roll_no and student_name (username) - leave_requests = LeavePG.objects.filter( - roll_no=roll_no_id - ) + roll_no_id = request.user.extrainfo.id - # Serialize the data (assuming the serializer is defined for LeaveFormTable) - data = [ - { - "rollNo": roll_no_id, # Assuming roll_number is the field in ExtraInfo - "name": leave.student_name, - "dateFrom": leave.date_from, - "dateTo": leave.date_to, - "leaveType": leave.leave_type, - "attachment": leave.upload_file.url if leave.upload_file else None, - "purpose": leave.purpose, - "address": leave.address, - "action": leave.status, - } - for leave in leave_requests - ] - print(data) + leave_requests = selectors.get_pg_leaves_by_roll_no(roll_no_id) + data = [selectors.serialize_leave_status(leave, roll_no_id) for leave in leave_requests] return Response(data, status=status.HTTP_200_OK) - -@csrf_exempt # Exempt CSRF verification for this view -@login_required -def leave_form_submit(request): - """ - View function for submitting a leave form. +class WithdrawUGLeave(APIView): + """Withdraw a UG leave request before HOD verifies it.""" + permission_classes = [IsAuthenticated] - Description: - This function handles form submission for leave requests, processes the data, and saves it to the database. - It also notifies the relevant authority about the new leave application. - """ - if request.method == 'POST': - # Extract data from the request - data = request.POST - file = request.FILES.get('related_document') - hodname = data.get('hod_credential') - - # Create a new LeaveFormTable instance and save it to the database - leave = LeaveFormTable.objects.create( - student_name=request.user.first_name+request.user.last_name, - roll_no=request.user.extrainfo, - date_from=data.get('date_from'), - date_to=data.get('date_to'), - leave_type=data.get('leave_type'), - upload_file=file, - address=data.get('address'), - purpose=data.get('purpose'), - date_of_application=date.today(), - hod=data.get('hod_credential') - ) - - leave_hod = User.objects.get(username=hodname) - receiver_value = User.objects.get(username=request.user.username) - receiver_value_designation = HoldsDesignation.objects.filter(user=receiver_value) - lis = list(receiver_value_designation) - obj = lis[0].designation - - file_id = create_file( - uploader=request.user.username, - uploader_designation=obj, - receiver=leave_hod, - receiver_designation="student", - src_module="otheracademic", - src_object_id=leave.id, - file_extra_JSON={"value": 2}, - attached_file=None, - subject='ug_leave' - ) + def post(self, request, leave_id, *args, **kwargs): + try: + services.withdraw_ug_leave(request.user, leave_id) + return Response({"message": "Leave request withdrawn successfully."}, status=status.HTTP_200_OK) + except services.LeaveServiceError as e: + return Response({"error": str(e)}, status=status.HTTP_400_BAD_REQUEST) - message = "A new leave application" - otheracademic_notif(request.user, leave_hod, 'ug_leave_hod', leave.id, 'student', message) - if leave: - messages.success(request, "You successfully submitted your form") - - # return HttpResponseRedirect('/otheracademic/leaveform') +class WithdrawPGLeave(APIView): + """Withdraw a PG leave request before HOD verifies it.""" + permission_classes = [IsAuthenticated] + def post(self, request, leave_id, *args, **kwargs): + try: + services.withdraw_pg_leave(request.user, leave_id) + return Response({"message": "PG leave request withdrawn successfully."}, status=status.HTTP_200_OK) + except services.LeaveServiceError as e: + return Response({"error": str(e)}, status=status.HTTP_400_BAD_REQUEST) -class BonafideFormSubmitView(APIView): - """ - API view to handle Bonafide form submission. - """ - permission_classes = [IsAuthenticated] +# ==================== BONAFIDE VIEWS ==================== - def post(self, request): - # Extract data from the request +class BonafideFormSubmitView(APIView): + """Submit a bonafide application.""" + permission_classes = [IsAuthenticated] + def post(self, request): data = request.POST - file = request.FILES.get('related_document') # Handle the file if uploaded + file = request.FILES.get('related_document') try: - # Create a new BonafideFormTableUpdated instance and save it to the database - bonafide_form = BonafideFormTableUpdated.objects.create( - student_names=f"{request.user.first_name} {request.user.last_name}", - roll_nos=request.user.extrainfo, # Assuming `extrainfo` is the user's ExtraInfo instance - branch_types=data.get('branch'), - semester_types=data.get('semester'), - purposes=data.get('purpose'), - date_of_applications=date.today(), - download_file=file.name if file else "unavailable", - approve=False, # Default value - reject=False, # Default value + bonafide = services.submit_bonafide( + user=request.user, + branch=data.get('branch'), + semester=data.get('semester'), + purpose=data.get('purpose'), + download_file=file, ) - - # Notify the academic admin about the new bonafide application - acad_admin_des_id = Designation.objects.get(name="acadadmin") - user_ids = HoldsDesignation.objects.filter(designation_id=acad_admin_des_id.id).values_list('user_id', flat=True) - - if user_ids.exists(): - bonafide_receiver = User.objects.get(id=user_ids[0]) - message = "A new Bonafide application has been submitted." - otheracademic_notif( - request.user, - bonafide_receiver, - 'bonafide', - bonafide_form.id, - 'student', - message - ) - return Response( {"message": "Your bonafide form has been successfully submitted."}, status=status.HTTP_201_CREATED ) - + except services.BonafideServiceError as e: + return Response({"error": str(e)}, status=status.HTTP_400_BAD_REQUEST) except Exception as e: return Response( {"error": f"An error occurred: {str(e)}"}, status=status.HTTP_400_BAD_REQUEST ) - - - - - class FetchPendingBonafideRequests(APIView): + """Fetch pending bonafide requests for admin approval.""" permission_classes = [IsAuthenticated] def get(self, request, *args, **kwargs): - # Fetch Bonafide requests where both approve and reject are False (unseen requests) - pending_bonafides = BonafideFormTableUpdated.objects.filter(approve=False, reject=False) - - # Prepare response data - data = [ - { - "id": bonafide.id, - "rollNo": bonafide.roll_nos_id, # Assuming roll_no is a field in ExtraInfo - "name": bonafide.student_names, - "details": { - "purpose": bonafide.purposes, - "dateOfApplication": bonafide.date_of_applications, - "semester": bonafide.semester_types, - }, - } - for bonafide in pending_bonafides - ] - - return Response(data) + if not selectors.user_has_designation(request.user, "acadadmin"): + raise PermissionDenied("Only Academic Administrator can access bonafide verification queue.") + pending_bonafides = selectors.get_pending_bonafides() + data = [selectors.serialize_pending_bonafide(b) for b in pending_bonafides] + return Response(data) - - - - class UpdateBonafideStatus(APIView): + """Update bonafide status (admin approval/rejection).""" permission_classes = [IsAuthenticated] def post(self, request, *args, **kwargs): - # Get the lists of approved and rejected bonafide request IDs from the request body - approved_bonafides_ids = request.data.get('approvedBonafides', []) - rejected_bonafides_ids = request.data.get('rejectedBonafides', []) + if not selectors.user_has_designation(request.user, "acadadmin"): + raise PermissionDenied("Only Academic Administrator can verify bonafide requests.") - try: - # Update the approve/reject status based on the provided lists - if approved_bonafides_ids: - BonafideFormTableUpdated.objects.filter(id__in=approved_bonafides_ids).update(approve=True, reject=False) - # Notify the respective students about approval - for bonafide_id in approved_bonafides_ids: - bonafide_form = BonafideFormTableUpdated.objects.get(id=bonafide_id) - student = User.objects.get(extrainfo=bonafide_form.roll_nos_id) # Assuming `extrainfo` is the student's unique identifier - # Send notification to the student about the approval - message = f"Your Bonafide application has been appr oved. Please check the status." - otheracademic_notif( - request.user, # The sender (admin) - student, # The receiver (student) - 'bonafide_accept', # Notification type - bonafide_form.id, # The ID of the Bonafide form - 'admin', # The role of the sender - message # The approval message - ) + serializer = BonafideStatusUpdateSerializer(data=request.data) + serializer.is_valid(raise_exception=True) - if rejected_bonafides_ids: - BonafideFormTableUpdated.objects.filter(id__in=rejected_bonafides_ids).update(approve=False, reject=True) - - # Notify the respective students about rejection - for bonafide_id in rejected_bonafides_ids: - bonafide_form = BonafideFormTableUpdated.objects.get(id=bonafide_id) - student = User.objects.get(extrainfo=bonafide_form.roll_nos) # Assuming `extrainfo` is the student's unique identifier - - # Send notification to the student about the rejection - message = f"Your Bonafide application has been rejected. Please check the status for further details." - otheracademic_notif( - request.user, # The sender (admin) - student, # The receiver (student) - 'bonafide_accept', # Notification type - bonafide_form.id, # The ID of the Bonafide form - 'admin', # The role of the sender - message # The rejection message - ) + approved_ids = serializer.validated_data.get('approvedBonafides', []) + rejected_ids = serializer.validated_data.get('rejectedBonafides', []) + try: + services.update_bonafide_status(approved_ids, rejected_ids, request.user) return Response({"message": "Bonafide statuses updated successfully."}) - except Exception as e: - return Response({"error": f"An error occurred: {str(e)}"}, status=status.HTTP_400_BAD_REQUEST) + return Response( + {"error": f"An error occurred: {str(e)}"}, + status=status.HTTP_400_BAD_REQUEST + ) + + +class UploadBonafideCertificate(APIView): + """Upload certificate file for a bonafide request (admin action).""" + permission_classes = [IsAuthenticated] + + def post(self, request, bonafide_id, *args, **kwargs): + if not selectors.user_has_designation(request.user, "acadadmin"): + raise PermissionDenied("Only Academic Administrator can upload bonafide certificates.") + + certificate = request.FILES.get("certificate") + if not certificate: + return Response( + {"error": "certificate file is required."}, + status=status.HTTP_400_BAD_REQUEST, + ) + + try: + bonafide = services.upload_bonafide_certificate(bonafide_id, certificate) + except services.BonafideServiceError as e: + return Response({"error": str(e)}, status=status.HTTP_400_BAD_REQUEST) + + return Response( + { + "message": "Certificate uploaded successfully.", + "bonafideId": bonafide.id, + "downloadUrl": request.build_absolute_uri(bonafide.download_file.url), + }, + status=status.HTTP_200_OK, + ) class GetBonafideStatus(APIView): + """Get bonafide status for a specific student.""" permission_classes = [IsAuthenticated] def post(self, request, *args, **kwargs): - # Get roll number and username from the request roll_no = request.data.get("roll_no") username = request.data.get("username") - # Check if roll number and username are provided if not roll_no or not username: return Response( {"error": "Roll number and username are required."}, status=status.HTTP_400_BAD_REQUEST, ) - try: - # Query bonafide forms for the given roll number - bonafide_requests = BonafideFormTableUpdated.objects.filter(roll_nos_id=roll_no) - - # Manually format the response data - response_data = [ - { - "rollNo": bonafide.roll_nos_id, - "name": bonafide.student_names, - "branch": bonafide.branch_types, - "semester": bonafide.semester_types, - "purpose": bonafide.purposes, - "dateApplied": bonafide.date_of_applications.strftime("%Y-%m-%d") if bonafide.date_of_applications else None, - "status": ( - "Approved" if bonafide.approve else "Rejected" if bonafide.reject else "Pending" - ), - } - for bonafide in bonafide_requests - ] + if str(request.user.extrainfo.id) != str(roll_no): + return Response( + {"error": "You can only view your own bonafide status."}, + status=status.HTTP_403_FORBIDDEN, + ) + try: + bonafide_requests = selectors.get_bonafides_by_roll_no(roll_no) + response_data = [selectors.serialize_bonafide_status(b) for b in bonafide_requests] + for item in response_data: + if item.get("downloadUrl"): + item["downloadUrl"] = request.build_absolute_uri(item["downloadUrl"]) return Response(response_data, status=status.HTTP_200_OK) - except Exception as e: return Response( {"error": "An error occurred while fetching bonafide status.", "details": str(e)}, @@ -671,727 +380,908 @@ def post(self, request, *args, **kwargs): ) - return Response({'message': 'Form submitted successfully', 'bonafide_id': bonafide.id}, status=status.HTTP_201_CREATED) - else: - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) +class WithdrawBonafide(APIView): + """Withdraw a pending bonafide request.""" + permission_classes = [IsAuthenticated] + + def post(self, request, bonafide_id, *args, **kwargs): + try: + services.withdraw_bonafide(request.user, bonafide_id) + return Response({"message": "Bonafide request withdrawn successfully."}, status=status.HTTP_200_OK) + except services.BonafideServiceError as e: + return Response({"error": str(e)}, status=status.HTTP_400_BAD_REQUEST) +# ==================== ASSISTANTSHIP VIEWS ==================== -@csrf_exempt # Exempt CSRF verification for this view -@login_required -def leave_form_submit(request): - """ - View function for submitting a leave form. +class AssistantshipFormSubmitView(APIView): + """Submit an assistantship claim form.""" + permission_classes = [IsAuthenticated] - Description: - This function handles form submission for leave requests, processes the data, and saves it to the database. - It also notifies the relevant authority about the new leave application. - """ - if request.method == 'POST': - # Extract data from the request + def post(self, request): data = request.POST - file = request.FILES.get('related_document') - hodname = data.get('hod_credential') - - # Create a new LeaveFormTable instance and save it to the database - leave = LeaveFormTable.objects.create( - student_name=request.user.first_name+request.user.last_name, - roll_no=request.user.extrainfo, - date_from=data.get('date_from'), - date_to=data.get('date_to'), - leave_type=data.get('leave_type'), - upload_file=file, - address=data.get('address'), - purpose=data.get('purpose'), - date_of_application=date.today(), - hod=data.get('hod_credential') - ) - - leave_hod = User.objects.get(username=hodname) - receiver_value = User.objects.get(username=request.user.username) - receiver_value_designation = HoldsDesignation.objects.filter(user=receiver_value) - lis = list(receiver_value_designation) - obj = lis[0].designation - - file_id = create_file( - uploader=request.user.username, - uploader_designation=obj, - receiver=leave_hod, - receiver_designation="student", - src_module="otheracademic", - src_object_id=leave.id, - file_extra_JSON={"value": 2}, - attached_file=None, - subject='ug_leave' - ) + files = request.FILES + try: + # Parse dates + date_from = datetime.strptime(data.get('date_from'), '%Y-%m-%d').date() + date_to = datetime.strptime(data.get('date_to'), '%Y-%m-%d').date() + date_applied = datetime.strptime(data.get('date_applied'), '%Y-%m-%d').date() + except (ValueError, TypeError): + return Response( + {"error": "Invalid date format. Please use YYYY-MM-DD."}, + status=status.HTTP_400_BAD_REQUEST + ) - message = "A new leave application" - otheracademic_notif(request.user, leave_hod, 'ug_leave_hod', leave.id, 'student', message) - if leave: - messages.success(request, "You successfully submitted your form") - - # return HttpResponseRedirect('/otheracademic/leaveform') + # Validate date range + if date_from > date_to: + return Response({"error": "Invalid date range."}, status=status.HTTP_400_BAD_REQUEST) + # Validate signature file + signature_file = files.get('signature') + if not signature_file: + return Response({"error": "Signature file is missing."}, status=status.HTTP_400_BAD_REQUEST) - + try: + assistantship = services.submit_assistantship( + user=request.user, + discipline=data.get('discipline'), + date_from=date_from, + date_to=date_to, + date_applied=date_applied, + bank_account=data.get('bank_account_no'), + signature_file=signature_file, + ta_supervisor=data.get('ta_supervisor'), + thesis_supervisor=data.get('thesis_supervisor'), + hod=data.get('hod'), + applicability=data.get('applicability'), + ) + return Response({"message": "Form submitted successfully."}, status=status.HTTP_201_CREATED) + except services.AssistantshipServiceError as e: + return Response({"error": str(e)}, status=status.HTTP_400_BAD_REQUEST) + except Exception as e: + return Response({"error": "An unexpected error occurred."}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) -class BonafideFormSubmitView(APIView): - """ - API view to handle Bonafide form submission. - """ - permission_classes = [IsAuthenticated] +class TA_SupervisorFetchPendingAssistantshipRequests(APIView): + """Fetch pending assistantship requests for faculty supervisor approval.""" + permission_classes = [IsAuthenticated] + + def get(self, request): + if not selectors.user_has_designation(request.user, "faculty_supervisor"): + raise PermissionDenied("Only Faculty Supervisor can access this queue.") + try: + pending_forms = selectors.get_pending_assistantships_for_ta_user(request.user.username) + response_data = [selectors.serialize_assistantship_pending(form) for form in pending_forms] + return Response(response_data, status=status.HTTP_200_OK) + except Exception as e: + return Response( + {"error": "Error fetching pending forms", "details": str(e)}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + + +class TA_SupervisorUpdateAssistantshipStatus(APIView): + """Update assistantship status (faculty supervisor approval).""" + permission_classes = [IsAuthenticated] def post(self, request): - # Extract data from the request - data = request.POST - file = request.FILES.get('related_document') # Handle the file if uploaded + if not selectors.user_has_designation(request.user, "faculty_supervisor"): + raise PermissionDenied("Only Faculty Supervisor can review assistantship forms.") + + serializer = AssistantshipStatusUpdateSerializer(data=request.data) + serializer.is_valid(raise_exception=True) + + approved_ids = serializer.validated_data.get('approvedRequests', []) + rejected_ids = serializer.validated_data.get('rejectedRequests', []) try: - # Create a new BonafideFormTableUpdated instance and save it to the database - bonafide_form = BonafideFormTableUpdated.objects.create( - student_names=f"{request.user.first_name} {request.user.last_name}", - roll_nos=request.user.extrainfo, # Assuming `extrainfo` is the user's ExtraInfo instance - branch_types=data.get('branch'), - semester_types=data.get('semester'), - purposes=data.get('purpose'), - date_of_applications=date.today(), - download_file=file.name if file else "unavailable", - approve=False, # Default value - reject=False, # Default value + services.update_assistantship_status_ta(approved_ids, rejected_ids, request.user) + return Response({"message": "Assistantship statuses updated successfully"}, status=status.HTTP_200_OK) + except services.AssistantshipServiceError as e: + return Response({"error": str(e)}, status=status.HTTP_400_BAD_REQUEST) + except Exception as e: + return Response( + {"error": "Error updating assistantship status", "details": str(e)}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR ) - # Notify the academic admin about the new bonafide application - acad_admin_des_id = Designation.objects.get(name="acadadmin") - user_ids = HoldsDesignation.objects.filter(designation_id=acad_admin_des_id.id).values_list('user_id', flat=True) - - if user_ids.exists(): - bonafide_receiver = User.objects.get(id=user_ids[0]) - message = "A new Bonafide application has been submitted." - otheracademic_notif( - request.user, - bonafide_receiver, - 'bonafide', - bonafide_form.id, - 'student', - message - ) +class Ths_SupervisorFetchPendingAssistantshipRequests(APIView): + """Fetch pending assistantship requests for Thesis supervisor approval.""" + permission_classes = [IsAuthenticated] + + def get(self, request): + try: + pending_forms = selectors.get_pending_assistantships_for_thesis() + response_data = [selectors.serialize_assistantship_pending(form) for form in pending_forms] + return Response(response_data, status=status.HTTP_200_OK) + except Exception as e: return Response( - {"message": "Your bonafide form has been successfully submitted."}, - status=status.HTTP_201_CREATED + {"error": "Error fetching pending forms", "details": str(e)}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR ) + +class Ths_SupervisorUpdateAssistantshipStatus(APIView): + """Update assistantship status (Thesis supervisor approval).""" + permission_classes = [IsAuthenticated] + + def post(self, request): + serializer = AssistantshipStatusUpdateSerializer(data=request.data) + serializer.is_valid(raise_exception=True) + + approved_ids = serializer.validated_data.get('approvedRequests', []) + rejected_ids = serializer.validated_data.get('rejectedRequests', []) + + try: + services.update_assistantship_status_thesis(approved_ids, rejected_ids) + return Response({"message": "Assistantship statuses updated successfully"}, status=status.HTTP_200_OK) except Exception as e: return Response( - {"error": f"An error occurred: {str(e)}"}, - status=status.HTTP_400_BAD_REQUEST + {"error": "Error updating assistantship status", "details": str(e)}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR ) - - +class HODFetchPendingAssistantshipRequests(APIView): + """Fetch pending assistantship requests for Department Admin approval.""" + permission_classes = [IsAuthenticated] + + def get(self, request): + if not selectors.user_has_designation(request.user, "dept_admin"): + raise PermissionDenied("Only Department Admin can access this queue.") + try: + pending_forms = selectors.get_pending_assistantships_for_hod() + response_data = [selectors.serialize_assistantship_pending(form) for form in pending_forms] + return Response(response_data, status=status.HTTP_200_OK) + except Exception as e: + return Response( + {"error": "Error fetching pending forms", "details": str(e)}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) - -class FetchPendingBonafideRequests(APIView): +class HODUpdateAssistantshipStatus(APIView): + """Update assistantship status (Department Admin final approval).""" permission_classes = [IsAuthenticated] - def get(self, request, *args, **kwargs): - # Fetch Bonafide requests where both approve and reject are False (unseen requests) - pending_bonafides = BonafideFormTableUpdated.objects.filter(approve=False, reject=False) - - # Prepare response data - data = [ - { - "id": bonafide.id, - "rollNo": bonafide.roll_nos_id, # Assuming roll_no is a field in ExtraInfo - "name": bonafide.student_names, - "details": { - "purpose": bonafide.purposes, - "dateOfApplication": bonafide.date_of_applications, - "semester": bonafide.semester_types, - }, - } - for bonafide in pending_bonafides - ] - - return Response(data) + def post(self, request, *args, **kwargs): + if not selectors.user_has_designation(request.user, "dept_admin"): + raise PermissionDenied("Only Department Admin can approve assistantship forms.") + serializer = AssistantshipStatusUpdateSerializer(data=request.data) + serializer.is_valid(raise_exception=True) + approved_ids = serializer.validated_data.get('approvedRequests', []) + rejected_ids = serializer.validated_data.get('rejectedRequests', []) - + try: + services.update_assistantship_status_hod(approved_ids, rejected_ids, request.user) + return Response({"message": "Assistantship statuses updated successfully."}) + except services.AssistantshipServiceError as e: + return Response({"error": str(e)}, status=status.HTTP_400_BAD_REQUEST) - -class UpdateBonafideStatus(APIView): +class AcadAdminFetchPendingAssistantshipRequests(APIView): + """Fetch pending assistantship requests for Academic Admin disbursement audit.""" + permission_classes = [IsAuthenticated] + + def get(self, request): + if not selectors.user_has_designation(request.user, "acadadmin"): + raise PermissionDenied("Only Academic Admin can access this queue.") + pending_forms = selectors.get_pending_assistantships_for_acad_admin() + response_data = [selectors.serialize_assistantship_pending(form) for form in pending_forms] + return Response(response_data, status=status.HTTP_200_OK) + + +class AcadAdminUpdateAssistantshipStatus(APIView): + """Update assistantship status (Academic Admin disbursement audit).""" permission_classes = [IsAuthenticated] def post(self, request, *args, **kwargs): - # Get the lists of approved and rejected bonafide request IDs from the request body - approved_bonafides_ids = request.data.get('approvedBonafides', []) - rejected_bonafides_ids = request.data.get('rejectedBonafides', []) + if not selectors.user_has_designation(request.user, "acadadmin"): + raise PermissionDenied("Only Academic Admin can update this stage.") - # Update the approve/reject status based on the provided lists - if approved_bonafides_ids: - BonafideFormTableUpdated.objects.filter(id__in=approved_bonafides_ids).update(approve=True, reject=False) + serializer = AssistantshipStatusUpdateSerializer(data=request.data) + serializer.is_valid(raise_exception=True) - if rejected_bonafides_ids: - BonafideFormTableUpdated.objects.filter(id__in=rejected_bonafides_ids).update(approve=False, reject=True) + approved_ids = serializer.validated_data.get('approvedRequests', []) + rejected_ids = serializer.validated_data.get('rejectedRequests', []) - return Response({"message": "Bonafide statuses updated successfully."}) + try: + services.update_assistantship_status_acad_admin(approved_ids, rejected_ids, request.user) + return Response({"message": "Assistantship statuses updated successfully."}) + except services.AssistantshipServiceError as e: + return Response({"error": str(e)}, status=status.HTTP_400_BAD_REQUEST) +class DeanAcadFetchPendingAssistantshipRequests(APIView): + """Fetch pending assistantship requests for HOD approval.""" + permission_classes = [IsAuthenticated] -class GetBonafideStatus(APIView): + def get(self, request): + if not selectors.user_has_designation_contains(request.user, "hod"): + raise PermissionDenied("Only HOD can access this queue.") + pending_forms = selectors.get_pending_assistantships_for_dean() + response_data = [selectors.serialize_assistantship_pending(form) for form in pending_forms] + return Response(response_data, status=status.HTTP_200_OK) + + +class DeanAcadUpdateAssistantshipStatus(APIView): + """Update assistantship status (HOD approval/rejection).""" + permission_classes = [IsAuthenticated] + + def post(self, request, *args, **kwargs): + if not selectors.user_has_designation_contains(request.user, "hod"): + raise PermissionDenied("Only HOD can update this stage.") + + serializer = AssistantshipStatusUpdateSerializer(data=request.data) + serializer.is_valid(raise_exception=True) + + approved_ids = serializer.validated_data.get('approvedRequests', []) + rejected_ids = serializer.validated_data.get('rejectedRequests', []) + + try: + services.update_assistantship_status_dean(approved_ids, rejected_ids, request.user) + return Response({"message": "Assistantship statuses updated successfully."}) + except services.AssistantshipServiceError as e: + return Response({"error": str(e)}, status=status.HTTP_400_BAD_REQUEST) + + +class DirectorFetchPendingAssistantshipRequests(APIView): + """Fetch pending assistantship requests for Director approval.""" + permission_classes = [IsAuthenticated] + + def get(self, request): + pending_forms = selectors.get_pending_assistantships_for_director() + response_data = [selectors.serialize_assistantship_pending(form) for form in pending_forms] + return Response(response_data, status=status.HTTP_200_OK) + + +class DirectorUpdateAssistantshipStatus(APIView): + """Update assistantship status (Director approval).""" + permission_classes = [IsAuthenticated] + + def post(self, request, *args, **kwargs): + serializer = AssistantshipStatusUpdateSerializer(data=request.data) + serializer.is_valid(raise_exception=True) + + approved_ids = serializer.validated_data.get('approvedRequests', []) + rejected_ids = serializer.validated_data.get('rejectedRequests', []) + + services.update_assistantship_status_director(approved_ids, rejected_ids) + return Response({"message": "Assistantship statuses updated successfully."}) + + +class GetAssistantshipStatus(APIView): + """Get assistantship status for a specific student.""" permission_classes = [IsAuthenticated] def post(self, request, *args, **kwargs): - # Get roll number and username from the request roll_no = request.data.get("roll_no") username = request.data.get("username") - # Check if roll number and username are provided if not roll_no or not username: return Response( {"error": "Roll number and username are required."}, - status=status.HTTP_400_BAD_REQUEST, + status=status.HTTP_400_BAD_REQUEST + ) + + if str(request.user.extrainfo.id) != str(roll_no): + return Response( + {"error": "You can only view your own assistantship status."}, + status=status.HTTP_403_FORBIDDEN, ) try: - # Query bonafide forms for the given roll number - bonafide_requests = BonafideFormTableUpdated.objects.filter(roll_nos_id=roll_no) - - # Manually format the response data - response_data = [ - { - "rollNo": bonafide.roll_nos_id, - "name": bonafide.student_names, - "branch": bonafide.branch_types, - "semester": bonafide.semester_types, - "purpose": bonafide.purposes, - "dateApplied": bonafide.date_of_applications.strftime("%Y-%m-%d") if bonafide.date_of_applications else None, - "status": ( - "Approved" if bonafide.approve else "Rejected" if bonafide.reject else "Pending" - ), - } - for bonafide in bonafide_requests - ] + assistantship_requests = selectors.get_assistantships_by_roll_no(roll_no) + + response_data = [{ + "rollNo": form.roll_no.id, + "name": form.student_name, + "discipline": form.discipline, + "id": form.id, + "dateApplied": form.dateApplied.strftime("%Y-%m-%d") if form.dateApplied else None, + "bank_account": form.bank_account, + "status": services.get_assistantship_status_text(form), + "approvalStages": services.get_assistantship_approval_stages(form), + "canWithdraw": not form.TA_approved and not form.TA_rejected, + } for form in assistantship_requests] return Response(response_data, status=status.HTTP_200_OK) except Exception as e: return Response( - {"error": "An error occurred while fetching bonafide status.", "details": str(e)}, - status=status.HTTP_500_INTERNAL_SERVER_ERROR, + {"error": "An error occurred while fetching assistantship status.", "details": str(e)}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR ) - return Response({'message': 'Form submitted successfully', 'bonafide_id': bonafide.id}, status=status.HTTP_201_CREATED) - else: - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) +class WithdrawAssistantship(APIView): + """Withdraw assistantship form before faculty supervisor review.""" + permission_classes = [IsAuthenticated] -class AssistantshipFormSubmitView(APIView): + def post(self, request, form_id, *args, **kwargs): + try: + services.withdraw_assistantship(request.user, form_id) + return Response({"message": "Assistantship form withdrawn successfully."}, status=status.HTTP_200_OK) + except services.AssistantshipServiceError as e: + return Response({"error": str(e)}, status=status.HTTP_400_BAD_REQUEST) + + +class FetchTAAssignmentOptions(APIView): + """Fetch PG students and subjects for dept_admin TA assignment.""" permission_classes = [IsAuthenticated] - def post(self, request): - data = request.POST - files=request.FILES + def get(self, request): + if not selectors.user_has_designation(request.user, "dept_admin"): + raise PermissionDenied("Only Department Admin can access TA assignment options.") + try: - # Log received data for debugging - print("Received data:", data) - print("Received FILES:", files) - - # Parse dates using datetime.strptime() - try: - date_from = datetime.strptime(data.get('date_from'), '%Y-%m-%d').date() - date_to = datetime.strptime(data.get('date_to'), '%Y-%m-%d').date() - date_applied = datetime.strptime(data.get('date_applied'), '%Y-%m-%d').date() - except ValueError: - return Response({"error": "Invalid date format. Please use YYYY-MM-DD."}, status=400) - - # Validate dates - if not date_from or not date_to or date_from > date_to: - return Response({"error": "Invalid date range."}, status=400) - # Check for duplicate form submission - if AssistantshipClaimFormStatusUpd.objects.filter( - roll_no=request.user.extrainfo, - dateFrom=date_from, - dateTo=date_to - ).exists(): - return Response({"error": "Form for this period already exists."}, status=400) - - # Validate HOD user - #hod_user = User.objects.filter(username=data.get('hod')).first() - #if not hod_user: - # return Response({"error": "HOD username not found."}, status=400) - ta_supervisor_user = User.objects.filter(username=data.get('ta_supervisor')).first() - if not ta_supervisor_user: - return Response({"error": "TA Supervisor username not found."}, status=400) - - thesis_supervisor_user = User.objects.filter(username=data.get('thesis_supervisor')).first() - if not thesis_supervisor_user: - return Response({"error": "Thesis Supervisor username not found."}, status=400) - # Handle signature file - signature_file = files.get('signature') - if not signature_file: - return Response({"error": "Signature file is missing."}, status=400) - - - - # Create form - assistantship_form = AssistantshipClaimFormStatusUpd.objects.create( - roll_no=request.user.extrainfo, - student_name=f"{request.user.first_name} {request.user.last_name}", - discipline=data.get('discipline'), - dateFrom=date_from, - dateTo=date_to, - bank_account=data.get('bank_account_no'), - student_signature=signature_file, - dateApplied=date_applied, - ta_supervisor=data.get('ta_supervisor'), - thesis_supervisor=data.get('thesis_supervisor'), - hod=data.get('hod'), - applicability=data.get('applicability'), - - # Existing approval/rejection fields - TA_approved=False, - TA_rejected=False, - Ths_approved=False, - Ths_rejected=False, - HOD_approved=False, - HOD_rejected=False, - - - # Newly added approval/rejection fields - Dean_approved=False, - Dean_rejected=False, - Director_approved=False, - Director_rejected=False, - AcadAdmin_approved=False, - AcadAdmin_rejected=False, + data = services.get_pg_ta_assignment_options() + return Response(data, status=status.HTTP_200_OK) + except Exception as e: + return Response( + {"error": "Error fetching TA assignment options", "details": str(e)}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR, ) - # Notify TA Supervisor - otheracademic_notif( + +class UpdateTAAssignments(APIView): + """Create/update TA subject assignments for PG students by dept_admin.""" + permission_classes = [IsAuthenticated] + + def post(self, request): + if not selectors.user_has_designation(request.user, "dept_admin"): + raise PermissionDenied("Only Department Admin can update TA assignments.") + + serializer = TAAssignmentUpdateSerializer(data=request.data) + serializer.is_valid(raise_exception=True) + + try: + updated_count = services.upsert_pg_ta_assignments( + serializer.validated_data.get("assignments", []), request.user, - ta_supervisor_user, - "assistantship_form", - assistantship_form.id, - "student", - "Assistantship form needs your (TA Supervisor) approval." ) - # Notify Thesis Supervisor - otheracademic_notif( - request.user, - thesis_supervisor_user, - "assistantship_form", - assistantship_form.id, - "student", - "Assistantship form needs your (Thesis Supervisor) approval." + return Response( + {"message": "TA assignments updated successfully.", "updated_count": updated_count}, + status=status.HTTP_200_OK, ) - - return Response({"message": "Form submitted successfully."}, status=201) - + except services.TAAssignmentServiceError as e: + return Response({"error": str(e)}, status=status.HTTP_400_BAD_REQUEST) except Exception as e: - print("Error occurred:", e) # Log error for debugging - return Response({"error": "An unexpected error occurred."},status=500) - + return Response( + {"error": "Error updating TA assignments", "details": str(e)}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR, + ) -class TA_SupervisorFetchPendingAssistantshipRequests(APIView): + +class FetchFacultySupervisorAssignmentOptions(APIView): + """Fetch PG students and faculty options for dept_admin supervisor assignment.""" permission_classes = [IsAuthenticated] def get(self, request): + if not selectors.user_has_designation(request.user, "dept_admin"): + raise PermissionDenied("Only Department Admin can access supervisor assignment options.") + try: - # Fetch forms where both TA and Thesis Supervisor have approved but Dept Admin hasn't taken action - pending_forms = AssistantshipClaimFormStatusUpd.objects.filter( - TA_approved=False, - TA_rejected=False - # Ths_approved=False, - # Ths_rejected=False + data = services.get_pg_faculty_supervisor_assignment_options() + return Response(data, status=status.HTTP_200_OK) + except Exception as e: + return Response( + {"error": "Error fetching faculty supervisor assignment options", "details": str(e)}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR, ) - response_data = [] - for form in pending_forms: - response_data.append({ - "id": form.id, - "student_name": form.student_name, - "roll_no": form.roll_no.id, - "discipline": form.discipline, - "dateFrom": form.dateFrom.strftime('%Y-%m-%d'), - "dateTo": form.dateTo.strftime('%Y-%m-%d'), - "applicability": form.applicability, - "dateApplied": form.dateApplied.strftime('%Y-%m-%d'), - }) - - return Response(response_data, status=200) - - except Exception as e: - return Response({"error": "Error fetching pending forms", "details": str(e)}, status=500) - -class TA_SupervisorUpdateAssistantshipStatus(APIView): +class UpdateFacultySupervisorAssignments(APIView): + """Create/update faculty supervisor assignments for PG students by dept_admin.""" permission_classes = [IsAuthenticated] def post(self, request): - """Update assistantship form status based on supervisor approval or rejection.""" - try: - role ="thesis" # Expecting 'ta' or 'thesis' - approved_ids = request.data.get("approvedRequests", []) - rejected_ids = request.data.get("rejectedRequests", []) - - if not role or role not in ["ta", "thesis"]: - return Response({"error": "Invalid or missing role parameter. Use 'ta' or 'thesis'."}, status=400) + if not selectors.user_has_designation(request.user, "dept_admin"): + raise PermissionDenied("Only Department Admin can update faculty supervisor assignments.") - if not approved_ids and not rejected_ids: - return Response({"error": "No forms provided for update."}, status=400) + serializer = FacultySupervisorAssignmentUpdateSerializer(data=request.data) + serializer.is_valid(raise_exception=True) - # Approving requests - if role == "thesis": - AssistantshipClaimFormStatusUpd.objects.filter(id__in=approved_ids).update(TA_approved=True) - AssistantshipClaimFormStatusUpd.objects.filter(id__in=rejected_ids).update(TA_rejected=True) + try: + updated_count = services.upsert_pg_faculty_supervisor_assignments( + serializer.validated_data.get("assignments", []), + request.user, + ) + return Response( + { + "message": "Faculty supervisor assignments updated successfully.", + "updated_count": updated_count, + }, + status=status.HTTP_200_OK, + ) + except services.TAAssignmentServiceError as e: + return Response({"error": str(e)}, status=status.HTTP_400_BAD_REQUEST) + except Exception as e: + return Response( + {"error": "Error updating faculty supervisor assignments", "details": str(e)}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR, + ) - else: - AssistantshipClaimFormStatusUpd.objects.filter(id__in=approved_ids).update(Ths_approved=True) - AssistantshipClaimFormStatusUpd.objects.filter(id__in=rejected_ids).update(Ths_rejected=True) - return Response({"message": "Assistantship statuses updated successfully"}, status=200) +# ==================== NO-DUES VIEWS ==================== - except Exception as e: - return Response({"error": "Error updating assistantship status", "details": str(e)}, status=500) +def _normalize_designation(name): + return str(name).strip().lower().replace(" ", "_") -class Ths_SupervisorFetchPendingAssistantshipRequests(APIView): - permission_classes = [IsAuthenticated] - def get(self, request): - try: - # Fetch forms where both TA and Thesis Supervisor have approved but Dept Admin hasn't taken action - pending_forms = AssistantshipClaimFormStatusUpd.objects.filter( - Ths_approved=False, - Ths_rejected=False - # Ths_approved=False, - # Ths_rejected=False - ) +def _ensure_no_dues_approver_designations(): + from applications.globals.models import Designation - response_data = [] - for form in pending_forms: - response_data.append({ - "id": form.id, - "student_name": form.student_name, - "roll_no": form.roll_no.id, - "discipline": form.discipline, - "dateFrom": form.dateFrom.strftime('%Y-%m-%d'), - "dateTo": form.dateTo.strftime('%Y-%m-%d'), - "applicability": form.applicability, - "dateApplied": form.dateApplied.strftime('%Y-%m-%d'), - }) + required_designations = [ + ("librarian", "Librarian"), + ("mess_incharge", "Mess Incharge"), + ("lab_supervisor", "Lab Supervisor"), + ("hostel_warden", "Hostel Warden"), + ] - return Response(response_data, status=200) + for name, full_name in required_designations: + Designation.objects.get_or_create( + name=name, + defaults={"full_name": full_name, "type": "administrative"}, + ) - except Exception as e: - return Response({"error": "Error fetching pending forms", "details": str(e)}, status=500) - -class Ths_SupervisorUpdateAssistantshipStatus(APIView): +def _get_user_no_dues_roles(user): + role_names = user.current_designation.values_list("designation__name", flat=True) + return {_normalize_designation(role_name) for role_name in role_names} + + +NO_DUES_ROLE_DEPARTMENT_MAP = { + "librarian": {"library"}, + "mess_incharge": {"mess"}, + "hostel_warden": {"hostel"}, + "lab_supervisor": { + "lab_supervisor", + "ece", + "physics_lab", + "mechatronics_lab", + "cc", + "workshop", + "signal_processing_lab", + "vlsi", + "design_studio", + "design_project", + }, + "acadadmin": {"acad_admin"}, +} + +NO_DUES_APPROVER_ROLES = set(NO_DUES_ROLE_DEPARTMENT_MAP.keys()) + + +def _approval_status(clear_flag, notclear_flag): + if clear_flag: + return "clear" + if notclear_flag: + return "not_clear" + return "pending" + + +def _lab_supervisor_status(no_dues): + lab_departments = { + "ece", + "physics_lab", + "mechatronics_lab", + "cc", + "workshop", + "signal_processing_lab", + "vlsi", + "design_studio", + "design_project", + } + has_clear = any(getattr(no_dues, f"{dept}_clear") for dept in lab_departments) + has_not_clear = any(getattr(no_dues, f"{dept}_notclear") for dept in lab_departments) + + if has_not_clear: + return "not_clear" + if has_clear: + return "clear" + return "pending" + + +def _no_dues_role_statuses(no_dues): + return { + "librarian": _approval_status(no_dues.library_clear, no_dues.library_notclear), + "mess_incharge": _approval_status(no_dues.mess_clear, no_dues.mess_notclear), + "hostel_warden": _approval_status(no_dues.hostel_clear, no_dues.hostel_notclear), + "lab_supervisor": _lab_supervisor_status(no_dues), + "acad_admin": _approval_status(no_dues.account_clear, no_dues.account_notclear), + } + + +def _no_dues_progress_summary(no_dues): + statuses = _no_dues_role_statuses(no_dues) + cleared_count = sum(1 for status_value in statuses.values() if status_value == "clear") + not_cleared_count = sum(1 for status_value in statuses.values() if status_value == "not_clear") + pending_count = sum(1 for status_value in statuses.values() if status_value == "pending") + total_count = len(statuses) + + return { + "statuses": statuses, + "cleared_count": cleared_count, + "not_cleared_count": not_cleared_count, + "pending_count": pending_count, + "total_count": total_count, + "progress_percentage": (cleared_count / total_count * 100) if total_count > 0 else 0, + "all_clear": cleared_count == total_count and not_cleared_count == 0, + } + + +class InitiateNoDuesView(APIView): + """Initiate no-dues clearance process for a student.""" permission_classes = [IsAuthenticated] def post(self, request): - """Update assistantship form status based on supervisor approval or rejection.""" try: - role ="thesis" # Expecting 'ta' or 'thesis' - approved_ids = request.data.get("approvedRequests", []) - rejected_ids = request.data.get("rejectedRequests", []) + from applications.globals.models import ExtraInfo - if not role or role not in ["ta", "thesis"]: - return Response({"error": "Invalid or missing role parameter. Use 'ta' or 'thesis'."}, status=400) + extra_info = ExtraInfo.objects.get(user=request.user) - if not approved_ids and not rejected_ids: - return Response({"error": "No forms provided for update."}, status=400) - - # Approving requests - if role == "ta": - AssistantshipClaimFormStatusUpd.objects.filter(id__in=approved_ids).update(TA_approved=True) - AssistantshipClaimFormStatusUpd.objects.filter(id__in=rejected_ids).update(TA_rejected=True) + if NoDues.objects.filter(roll_no=extra_info).exists(): + return Response( + {"error": "No-Dues clearance already initiated. You cannot initiate again."}, + status=status.HTTP_400_BAD_REQUEST, + ) - else: - AssistantshipClaimFormStatusUpd.objects.filter(id__in=approved_ids).update(Ths_approved=True) - AssistantshipClaimFormStatusUpd.objects.filter(id__in=rejected_ids).update(Ths_rejected=True) + no_dues = NoDues.objects.create( + roll_no=extra_info, + name=request.user.get_full_name() or request.user.username, + ) - return Response({"message": "Assistantship statuses updated successfully"}, status=200) + serializer = NoDuesStatusSerializer(no_dues) + return Response( + { + "message": "No-Dues clearance initiated successfully", + "data": serializer.data, + }, + status=status.HTTP_201_CREATED, + ) + except ExtraInfo.DoesNotExist: + return Response( + {"error": "Student information not found"}, + status=status.HTTP_404_NOT_FOUND, + ) except Exception as e: - return Response({"error": "Error updating assistantship status", "details": str(e)}, status=500) + return Response({"error": str(e)}, status=status.HTTP_400_BAD_REQUEST) - -class HODFetchPendingAssistantshipRequests(APIView): +class GetNoDuesStatusView(APIView): + """Get current no-dues status for a student.""" permission_classes = [IsAuthenticated] def get(self, request): try: - # Fetch forms where both TA and Thesis Supervisor have approved but Dept Admin hasn't taken action - pending_forms = AssistantshipClaimFormStatusUpd.objects.filter( - TA_approved=True, - Ths_approved=True, - HOD_approved=False, - HOD_rejected=False - ) + from applications.globals.models import ExtraInfo - response_data = [] - for form in pending_forms: - response_data.append({ - "id": form.id, - "student_name": form.student_name, - "roll_no": form.roll_no.id, - "discipline": form.discipline, - "dateFrom": form.dateFrom.strftime('%Y-%m-%d'), - "dateTo": form.dateTo.strftime('%Y-%m-%d'), - "applicability": form.applicability, - "dateApplied": form.dateApplied.strftime('%Y-%m-%d'), - }) - - return Response(response_data, status=200) + extra_info = ExtraInfo.objects.get(user=request.user) + no_dues = NoDues.objects.get(roll_no=extra_info) - except Exception as e: - return Response({"error": "Error fetching pending forms", "details": str(e)}, status=500) + serializer = NoDuesStatusSerializer(no_dues) + return Response(serializer.data, status=status.HTTP_200_OK) -class HODUpdateAssistantshipStatus(APIView): - permission_classes = [IsAuthenticated] + except NoDues.DoesNotExist: + return Response( + {"error": "No-Dues record not found. Please initiate first."}, + status=status.HTTP_404_NOT_FOUND, + ) + except ExtraInfo.DoesNotExist: + return Response( + {"error": "Student information not found"}, + status=status.HTTP_404_NOT_FOUND, + ) - def post(self, request, *args, **kwargs): - # Get the lists of approved and rejected bonafide request IDs from the request body - approved_hod = request.data.get('approvedRequests', []) - rejected_hod = request.data.get('rejectedRequests', []) - # Update the approve/reject status based on the provided lists - if approved_hod: - AssistantshipClaimFormStatusUpd.objects.filter(id__in=approved_hod).update(HOD_approved=True, HOD_rejected=False) +class VerifyNoDuesView(APIView): + """Verify no-dues clearance for a department.""" + permission_classes = [IsAuthenticated] - if rejected_hod: - AssistantshipClaimFormStatusUpd.objects.filter(id__in=rejected_hod).update(HOD_approved=False, HOD_rejected=True) + def post(self, request): + try: + from applications.globals.models import ExtraInfo - return Response({"message": "Bonafide statuses updated successfully."}) + _ensure_no_dues_approver_designations() -class AcadAdminFetchPendingAssistantshipRequests(APIView): - permission_classes = [IsAuthenticated] + roll_no = request.data.get('roll_no') + department = request.data.get('department') + is_clear = request.data.get('is_clear') - def get(self, request): - pending_forms = AssistantshipClaimFormStatusUpd.objects.filter( - TA_approved=True, - Ths_approved=True, - HOD_approved=True, - AcadAdmin_approved=False, - AcadAdmin_rejected=False - ) + if not all([roll_no, department, is_clear is not None]): + return Response( + {"error": "roll_no, department, and is_clear are required"}, + status=status.HTTP_400_BAD_REQUEST, + ) - response_data = [{ - "id": form.id, - "student_name": form.student_name, - "roll_no": form.roll_no.id, - "discipline": form.discipline, - "dateFrom": form.dateFrom.strftime('%Y-%m-%d'), - "dateTo": form.dateTo.strftime('%Y-%m-%d'), - "applicability": form.applicability, - "dateApplied": form.dateApplied.strftime('%Y-%m-%d'), - } for form in pending_forms] - - return Response(response_data, status=200) - -class AcadAdminUpdateAssistantshipStatus(APIView): - permission_classes = [IsAuthenticated] + user_roles = _get_user_no_dues_roles(request.user) + approver_roles = user_roles.intersection(NO_DUES_APPROVER_ROLES) + has_non_admin_role = any(role_name != 'acadadmin' for role_name in approver_roles) - def post(self, request, *args, **kwargs): - # Get the lists of approved and rejected bonafide request IDs from the request body - approved_bonafides_ids = request.data.get('approvedRequests', []) - rejected_bonafides_ids = request.data.get('rejectedRequests', []) - - # Update the approve/reject status based on the provided lists - if approved_bonafides_ids: - AssistantshipClaimFormStatusUpd.objects.filter(id__in=approved_bonafides_ids).update(AcadAdmin_approved=True, AcadAdmin_rejected=False) + if not approver_roles: + return Response( + { + "error": "Only librarian, mess incharge, lab supervisor, or hostel warden can verify no-dues." + }, + status=status.HTTP_403_FORBIDDEN, + ) - if rejected_bonafides_ids: - AssistantshipClaimFormStatusUpd.objects.filter(id__in=rejected_bonafides_ids).update(AcadAdmin_approved=False, AcadAdmin_rejected=True) + allowed_departments = set() + for role_name in approver_roles: + allowed_departments.update(NO_DUES_ROLE_DEPARTMENT_MAP[role_name]) - return Response({"message": "Bonafide statuses updated successfully."}) -class DeanAcadFetchPendingAssistantshipRequests(APIView): - permission_classes = [IsAuthenticated] + if department not in allowed_departments: + return Response( + {"error": f"You are not authorized to verify department: {department}"}, + status=status.HTTP_403_FORBIDDEN, + ) - def get(self, request): - pending_forms = AssistantshipClaimFormStatusUpd.objects.filter( - TA_approved=True, - Ths_approved=True, - HOD_approved=True, - AcadAdmin_approved=True, - Dean_approved=False, - Dean_rejected=False - ) + extra_info = ExtraInfo.objects.get(id=roll_no) + no_dues = NoDues.objects.get(roll_no=extra_info) + + dept_field_map = { + 'library': ('library_clear', 'library_notclear'), + 'hostel': ('hostel_clear', 'hostel_notclear'), + 'mess': ('mess_clear', 'mess_notclear'), + 'lab_supervisor': ('ece_clear', 'ece_notclear'), + 'acad_admin': ('account_clear', 'account_notclear'), + 'ece': ('ece_clear', 'ece_notclear'), + 'physics_lab': ('physics_lab_clear', 'physics_lab_notclear'), + 'mechatronics_lab': ('mechatronics_lab_clear', 'mechatronics_lab_notclear'), + 'cc': ('cc_clear', 'cc_notclear'), + 'workshop': ('workshop_clear', 'workshop_notclear'), + 'signal_processing_lab': ('signal_processing_lab_clear', 'signal_processing_lab_notclear'), + 'vlsi': ('vlsi_clear', 'vlsi_notclear'), + 'design_studio': ('design_studio_clear', 'design_studio_notclear'), + 'design_project': ('design_project_clear', 'design_project_notclear'), + 'bank': ('bank_clear', 'bank_notclear'), + 'icard_dsa': ('icard_dsa_clear', 'icard_dsa_notclear'), + 'account': ('account_clear', 'account_notclear'), + 'btp_supervisor': ('btp_supervisor_clear', 'btp_supervisor_notclear'), + 'discipline_office': ('discipline_office_clear', 'discipline_office_notclear'), + 'student_gymkhana': ('student_gymkhana_clear', 'student_gymkhana_notclear'), + 'alumni': ('alumni_clear', 'alumni_notclear'), + 'placement_cell': ('placement_cell_clear', 'placement_cell_notclear'), + } - response_data = [{ - "id": form.id, - "student_name": form.student_name, - "roll_no": form.roll_no.id, - "discipline": form.discipline, - "dateFrom": form.dateFrom.strftime('%Y-%m-%d'), - "dateTo": form.dateTo.strftime('%Y-%m-%d'), - "applicability": form.applicability, - "dateApplied": form.dateApplied.strftime('%Y-%m-%d'), - } for form in pending_forms] + if department == 'acad_admin': + statuses = _no_dues_role_statuses(no_dues) + first_four_clear = all( + statuses[role_name] == 'clear' + for role_name in ['librarian', 'mess_incharge', 'hostel_warden', 'lab_supervisor'] + ) + if not first_four_clear: + return Response( + {"error": "Acad Admin can finalize only after all four authorities clear."}, + status=status.HTTP_400_BAD_REQUEST, + ) - return Response(response_data, status=200) + if department == 'lab_supervisor': + lab_departments = [ + 'ece', + 'physics_lab', + 'mechatronics_lab', + 'cc', + 'workshop', + 'signal_processing_lab', + 'vlsi', + 'design_studio', + 'design_project', + ] + for lab_dept in lab_departments: + clear_field, notclear_field = dept_field_map[lab_dept] + if is_clear: + setattr(no_dues, clear_field, True) + setattr(no_dues, notclear_field, False) + else: + setattr(no_dues, clear_field, False) + setattr(no_dues, notclear_field, True) + else: + clear_field, notclear_field = dept_field_map[department] - -class DeanAcadUpdateAssistantshipStatus(APIView): - permission_classes = [IsAuthenticated] + if is_clear: + setattr(no_dues, clear_field, True) + setattr(no_dues, notclear_field, False) + else: + setattr(no_dues, clear_field, False) + setattr(no_dues, notclear_field, True) + + no_dues.save() + + if is_clear: + approval_label_map = { + 'library': 'Librarian', + 'mess': 'Mess Incharge', + 'hostel': 'Hostel Warden', + 'lab_supervisor': 'Lab Supervisor', + 'acad_admin': 'Acad Admin', + } + approval_label = approval_label_map.get(department, department) + notify.send( + sender=request.user, + recipient=no_dues.roll_no.user, + url='/other-academics', + module='Other Academic', + verb=f'Your no-dues request was approved by {approval_label}.', + ) - def post(self, request, *args, **kwargs): - # Get the lists of approved and rejected bonafide request IDs from the request body - approved_hod = request.data.get('approvedRequests', []) - rejected_hod = request.data.get('rejectedRequests', []) + serializer = NoDuesStatusSerializer(no_dues) + return Response( + { + "message": f"No-Dues cleared by {department}", + "data": serializer.data, + }, + status=status.HTTP_200_OK, + ) - # Update the approve/reject status based on the provided lists - if approved_hod: - AssistantshipClaimFormStatusUpd.objects.filter(id__in=approved_hod).update(Dean_approved=True, Dean_rejected=False) + except NoDues.DoesNotExist: + return Response( + {"error": "No-Dues record not found"}, + status=status.HTTP_404_NOT_FOUND, + ) + except Exception as e: + return Response({"error": str(e)}, status=status.HTTP_400_BAD_REQUEST) - if rejected_hod: - AssistantshipClaimFormStatusUpd.objects.filter(id__in=rejected_hod).update(Dean_approved=False, Dean_rejected=True) - return Response({"message": "Bonafide statuses updated successfully."}) - -class DirectorFetchPendingAssistantshipRequests(APIView): +class TrackNoDuesProgressView(APIView): + """Track progress of no-dues clearance.""" permission_classes = [IsAuthenticated] def get(self, request): - pending_forms = AssistantshipClaimFormStatusUpd.objects.filter( - TA_approved=True, - Ths_approved=True, - HOD_approved=True, - AcadAdmin_approved=True, - Dean_approved=True, - Director_approved=False, - Director_rejected=False - ) - - response_data = [{ - "id": form.id, - "student_name": form.student_name, - "roll_no": form.roll_no.id, - "discipline": form.discipline, - "dateFrom": form.dateFrom.strftime('%Y-%m-%d'), - "dateTo": form.dateTo.strftime('%Y-%m-%d'), - "applicability": form.applicability, - "dateApplied": form.dateApplied.strftime('%Y-%m-%d'), - } for form in pending_forms] - - return Response(response_data, status=200) - -class DirectorUpdateAssistantshipStatus(APIView): - permission_classes = [IsAuthenticated] + try: + from applications.globals.models import ExtraInfo + + extra_info = ExtraInfo.objects.get(user=request.user) + no_dues = NoDues.objects.get(roll_no=extra_info) + summary = _no_dues_progress_summary(no_dues) + + return Response({ + "roll_no": extra_info.id, + "name": no_dues.name, + "cleared": summary["cleared_count"], + "not_cleared": summary["not_cleared_count"], + "pending": summary["pending_count"], + "total": summary["total_count"], + "progress_percentage": summary["progress_percentage"], + "departments": summary["statuses"], + "all_clear": summary["all_clear"], + }, status=status.HTTP_200_OK) + + except NoDues.DoesNotExist: + return Response( + {"error": "No-Dues record not found"}, + status=status.HTTP_404_NOT_FOUND, + ) + except ExtraInfo.DoesNotExist: + return Response( + {"error": "Student information not found"}, + status=status.HTTP_404_NOT_FOUND, + ) - def post(self, request, *args, **kwargs): - # Get the lists of approved and rejected bonafide request IDs from the request body - approved_hod = request.data.get('approvedRequests', []) - rejected_hod = request.data.get('rejectedRequests', []) - # Update the approve/reject status based on the provided lists - if approved_hod: - AssistantshipClaimFormStatusUpd.objects.filter(id__in=approved_hod).update(Director_approved=True, Director_rejected=False) +class ListPendingNoDuesView(APIView): + """List all students with pending no-dues clearance requests.""" + permission_classes = [IsAuthenticated] - if rejected_hod: - AssistantshipClaimFormStatusUpd.objects.filter(id__in=rejected_hod).update(Director_approved=False,Director_rejected=True) + def get(self, request): + try: + _ensure_no_dues_approver_designations() + + user_roles = _get_user_no_dues_roles(request.user) + approver_roles = user_roles.intersection(NO_DUES_APPROVER_ROLES) + has_non_admin_role = any(role_name != 'acadadmin' for role_name in approver_roles) + if not approver_roles: + return Response( + { + "error": "Only librarian, mess incharge, lab supervisor, or hostel warden can view pending no-dues requests." + }, + status=status.HTTP_403_FORBIDDEN, + ) - return Response({"message": "Bonafide statuses updated successfully."}) + pending_clearances = NoDues.objects.all() + data = [] + for no_dues in pending_clearances: + summary = _no_dues_progress_summary(no_dues) + statuses = summary["statuses"] + first_four_clear = all( + statuses[role_name] == 'clear' + for role_name in ['librarian', 'mess_incharge', 'hostel_warden', 'lab_supervisor'] + ) + show_for_non_admin_queue = has_non_admin_role and not first_four_clear + show_for_acadadmin_queue = ( + 'acadadmin' in approver_roles + and first_four_clear + and statuses['acad_admin'] != 'clear' + ) -"""class GetAssistantshipStatus(APIView): - permission_classes = [IsAuthenticated] + if not (show_for_non_admin_queue or show_for_acadadmin_queue): + continue + + available_approvals = [] + if has_non_admin_role: + non_admin_targets = [ + ('library', 'librarian'), + ('mess', 'mess_incharge'), + ('hostel', 'hostel_warden'), + ('lab_supervisor', 'lab_supervisor'), + ] + available_approvals.extend( + target + for target, status_key in non_admin_targets + if statuses.get(status_key) == 'pending' + ) - def post(self, request, *args, **kwargs): - roll_no = request.data.get("roll_no") - username = request.data.get("username") + if 'acadadmin' in approver_roles and first_four_clear and statuses.get('acad_admin') == 'pending': + available_approvals.append('acad_admin') - if not roll_no or not username: - return Response( - {"error": "Roll number and username are required."}, - status=status.HTTP_400_BAD_REQUEST, - ) + if summary["all_clear"]: + continue - try: - assistantship_requests = AssistantshipClaimFormStatusUpd.objects.filter(roll_no_id=roll_no) - - response_data = [] - for form in assistantship_requests: - # Check if ANY stage rejected the form - is_rejected = any([ - form.Director_rejected, - form.Dean_rejected, - form.AcadAdmin_rejected, - form.HOD_rejected, - form.TA_rejected, - form.Ths_rejected - ]) - - # If rejected at any stage, status is "Rejected" and cannot be changed later - if is_rejected: - status_text = "Rejected" - # If Director has approved and no rejections, it's "Approved" - elif form.Director_approved: - status_text = "Approved" - # Otherwise, it's still "Pending" - else: - status_text = "Pending" - - response_data.append({ - "rollNo": form.roll_no.id, - "name": form.student_name, - "discipline": form.discipline, - "dateApplied": form.dateApplied.strftime("%Y-%m-%d") if form.dateApplied else None, - "bank_account": form.bank_account, - "status": status_text, + data.append({ + 'roll_no': no_dues.roll_no.id, + 'name': no_dues.name, + 'cleared_count': summary["cleared_count"], + 'total_count': summary["total_count"], + 'progress_percentage': summary["progress_percentage"], + 'departments': summary["statuses"], + 'available_approvals': list(dict.fromkeys(available_approvals)), }) - return Response(response_data, status=status.HTTP_200_OK) - + return Response(data, status=status.HTTP_200_OK) except Exception as e: return Response( - {"error": "An error occurred while fetching assistantship status.", "details": str(e)}, + {"error": str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR, ) -""" -class GetAssistantshipStatus(APIView): - permission_classes = [IsAuthenticated] - - def post(self, request, *args, **kwargs): - roll_no = request.data.get("roll_no") - username = request.data.get("username") - if not roll_no or not username: - return Response({"error": "Roll number and username are required."}, status=status.HTTP_400_BAD_REQUEST) +class DownloadNoDuesCertificateView(APIView): + """Download no-dues certificate (if fully cleared).""" + permission_classes = [IsAuthenticated] + def get(self, request): try: - assistantship_requests = AssistantshipClaimFormStatusUpd.objects.filter(roll_no_id=roll_no) + from applications.globals.models import ExtraInfo + from django.http import HttpResponse + from io import BytesIO + + extra_info = ExtraInfo.objects.get(user=request.user) + no_dues = NoDues.objects.get(roll_no=extra_info) + summary = _no_dues_progress_summary(no_dues) + + if not summary["all_clear"]: + return Response( + {"error": "Student has not cleared all departments yet"}, + status=status.HTTP_400_BAD_REQUEST, + ) - response_data = [{ - "rollNo": form.roll_no.id, - "name": form.student_name, - "discipline": form.discipline, - "dateApplied": form.dateApplied.strftime("%Y-%m-%d") if form.dateApplied else None, - "bank_account": form.bank_account, - "status": "Rejected" if any([form.Director_rejected, form.Dean_rejected, form.AcadAdmin_rejected, - form.HOD_rejected, form.TA_rejected, form.Ths_rejected]) - else "Approved" if form.Director_approved else "Pending", - "approvalStages": {stage: "Approved" if getattr(form, f"{prefix}_approved") else - "Rejected" if getattr(form, f"{prefix}_rejected") else "Pending" - for stage, prefix in { - "TA_Supervisor": "TA", "Thesis_Supervisor": "Ths", "HOD": "HOD", - "Academic_Admin": "AcadAdmin", "Dean_Academic": "Dean", "Director": "Director" - }.items()} - } for form in assistantship_requests] + blank_pdf = b"""%PDF-1.4\n1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Contents 4 0 R /Resources << >> >>\nendobj\n4 0 obj\n<< /Length 0 >>\nstream\n\nendstream\nendobj\nxref\n0 5\n0000000000 65535 f \n0000000010 00000 n \n0000000061 00000 n \n0000000118 00000 n \n0000000243 00000 n \ntrailer\n<< /Root 1 0 R /Size 5 >>\nstartxref\n284\n%%EOF""" - return Response(response_data, status=status.HTTP_200_OK) + response = HttpResponse(blank_pdf, content_type="application/pdf") + response["Content-Disposition"] = f'attachment; filename="{extra_info.id}_nodues.pdf"' + response["Content-Length"] = str(len(blank_pdf)) + return response + except NoDues.DoesNotExist: + return Response( + {"error": "No-Dues record not found"}, + status=status.HTTP_404_NOT_FOUND, + ) + except ExtraInfo.DoesNotExist: + return Response( + {"error": "Student information not found"}, + status=status.HTTP_404_NOT_FOUND, + ) except Exception as e: - return Response({"error": "An error occurred while fetching assistantship status.", "details": str(e)}, - status=status.HTTP_500_INTERNAL_SERVER_ERROR) + return Response( + {"error": f"Error generating certificate: {str(e)}"}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR, + ) diff --git a/FusionIIIT/applications/otheracademic/migrations/0002_pgtaassignment.py b/FusionIIIT/applications/otheracademic/migrations/0002_pgtaassignment.py new file mode 100644 index 000000000..cef44d127 --- /dev/null +++ b/FusionIIIT/applications/otheracademic/migrations/0002_pgtaassignment.py @@ -0,0 +1,32 @@ +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ("otheracademic", "0001_initial"), + ] + + operations = [ + migrations.CreateModel( + name="PGTAAssignment", + fields=[ + ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("updated_at", models.DateTimeField(auto_now=True)), + ( + "assigned_by", + models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to="auth.user"), + ), + ( + "pg_student", + models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to="globals.extrainfo"), + ), + ( + "subject", + models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="programme_curriculum.course"), + ), + ], + options={"db_table": "PGTAAssignment"}, + ), + ] diff --git a/FusionIIIT/applications/otheracademic/migrations/0003_pgfacultysupervisorassignment.py b/FusionIIIT/applications/otheracademic/migrations/0003_pgfacultysupervisorassignment.py new file mode 100644 index 000000000..1ccd475bc --- /dev/null +++ b/FusionIIIT/applications/otheracademic/migrations/0003_pgfacultysupervisorassignment.py @@ -0,0 +1,38 @@ +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ("otheracademic", "0002_pgtaassignment"), + ] + + operations = [ + migrations.CreateModel( + name="PGFacultySupervisorAssignment", + fields=[ + ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("updated_at", models.DateTimeField(auto_now=True)), + ( + "assigned_by", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="pg_faculty_supervisor_assigned_by", + to="auth.user", + ), + ), + ( + "faculty_supervisor", + models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="auth.user"), + ), + ( + "pg_student", + models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to="globals.extrainfo"), + ), + ], + options={"db_table": "PGFacultySupervisorAssignment"}, + ), + ] diff --git a/FusionIIIT/applications/otheracademic/migrations/0004_assignment_history.py b/FusionIIIT/applications/otheracademic/migrations/0004_assignment_history.py new file mode 100644 index 000000000..28e48c2d7 --- /dev/null +++ b/FusionIIIT/applications/otheracademic/migrations/0004_assignment_history.py @@ -0,0 +1,64 @@ +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ("otheracademic", "0003_pgfacultysupervisorassignment"), + ] + + operations = [ + migrations.CreateModel( + name="PGTAAssignmentHistory", + fields=[ + ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("changed_at", models.DateTimeField(auto_now_add=True)), + ( + "assigned_by", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="pg_ta_assignment_history_assigned_by", + to="auth.user", + ), + ), + ( + "pg_student", + models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="globals.extrainfo"), + ), + ( + "subject", + models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="programme_curriculum.course"), + ), + ], + options={"db_table": "PGTAAssignmentHistory"}, + ), + migrations.CreateModel( + name="PGFacultySupervisorAssignmentHistory", + fields=[ + ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("changed_at", models.DateTimeField(auto_now_add=True)), + ( + "assigned_by", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="pg_faculty_supervisor_assignment_history_assigned_by", + to="auth.user", + ), + ), + ( + "faculty_supervisor", + models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="auth.user"), + ), + ( + "pg_student", + models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="globals.extrainfo"), + ), + ], + options={"db_table": "PGFacultySupervisorAssignmentHistory"}, + ), + ] diff --git a/FusionIIIT/applications/otheracademic/migrations/0005_alter_pgtaassignment_multiple_subjects.py b/FusionIIIT/applications/otheracademic/migrations/0005_alter_pgtaassignment_multiple_subjects.py new file mode 100644 index 000000000..53a7798e4 --- /dev/null +++ b/FusionIIIT/applications/otheracademic/migrations/0005_alter_pgtaassignment_multiple_subjects.py @@ -0,0 +1,24 @@ +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ("otheracademic", "0004_assignment_history"), + ] + + operations = [ + migrations.AlterField( + model_name="pgtaassignment", + name="pg_student", + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="globals.extrainfo"), + ), + migrations.AddConstraint( + model_name="pgtaassignment", + constraint=models.UniqueConstraint( + fields=("pg_student", "subject"), + name="uniq_pg_ta_assignment_student_subject", + ), + ), + ] \ No newline at end of file diff --git a/FusionIIIT/applications/otheracademic/migrations/0006_leaveformtable_contact_fields.py b/FusionIIIT/applications/otheracademic/migrations/0006_leaveformtable_contact_fields.py new file mode 100644 index 000000000..c68b8e4c3 --- /dev/null +++ b/FusionIIIT/applications/otheracademic/migrations/0006_leaveformtable_contact_fields.py @@ -0,0 +1,31 @@ +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('otheracademic', '0005_alter_pgtaassignment_multiple_subjects'), + ] + + operations = [ + migrations.AddField( + model_name='leaveformtable', + name='stud_mobile_no', + field=models.CharField(blank=True, max_length=100, null=True), + ), + migrations.AddField( + model_name='leaveformtable', + name='parent_mobile_no', + field=models.CharField(blank=True, max_length=100, null=True), + ), + migrations.AddField( + model_name='leaveformtable', + name='leave_mobile_no', + field=models.CharField(blank=True, max_length=100, null=True), + ), + migrations.AddField( + model_name='leaveformtable', + name='curr_sem', + field=models.IntegerField(blank=True, null=True), + ), + ] \ No newline at end of file diff --git a/FusionIIIT/applications/otheracademic/models.py b/FusionIIIT/applications/otheracademic/models.py index f94ae4f83..cf952ebb9 100644 --- a/FusionIIIT/applications/otheracademic/models.py +++ b/FusionIIIT/applications/otheracademic/models.py @@ -5,20 +5,44 @@ from django import forms from django.contrib.auth.models import User from applications.academic_information.models import Student +from applications.programme_curriculum.models import Course as ProgrammeCourse from django.core.exceptions import ValidationError -class LeaveFormTable(models.Model): - LEAVE_TYPES = ( - ('Casual', 'Casual'), - ('Medical', 'Medical'), - ) +# ==================== SHARED TEXT CHOICES ==================== - STATUS_CHOICES = ( - ('Pending', 'Pending'), - ('Approved', 'Approved'), - ('Rejected', 'Rejected'), - ) +class LeaveTypeChoices(models.TextChoices): + """Shared leave type choices for all leave models.""" + CASUAL = 'Casual', 'Casual' + MEDICAL = 'Medical', 'Medical' + + +class LeaveTypePGChoices(models.TextChoices): + """Extended leave type choices for PG students.""" + CASUAL = 'Casual', 'Casual' + MEDICAL = 'Medical', 'Medical' + VACATION = 'Vacation', 'Vacation' + DUTY = 'Duty', 'Duty' + + +class LeaveStatusChoices(models.TextChoices): + """Shared status choices for leave requests.""" + PENDING = 'Pending', 'Pending' + APPROVED = 'Approved', 'Approved' + REJECTED = 'Rejected', 'Rejected' + + +class BonafideStatusChoices(models.TextChoices): + """Status choices for bonafide requests.""" + PENDING = 'Pending', 'Pending' + APPROVED = 'Approved', 'Approved' + REJECTED = 'Rejected', 'Rejected' + + +# ==================== MODELS ==================== + +class LeaveFormTable(models.Model): + """UG student leave request model.""" student_name = models.CharField(max_length=100) roll_no = models.ForeignKey(ExtraInfo, on_delete=models.CASCADE) @@ -28,13 +52,14 @@ class LeaveFormTable(models.Model): upload_file = models.FileField(upload_to='leave_documents/', blank=True, null=True) address = models.CharField(max_length=100) purpose = models.TextField() - leave_type = models.CharField(max_length=20, choices=LEAVE_TYPES) - status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='Pending') + leave_type = models.CharField(max_length=20, choices=LeaveTypeChoices.choices) + stud_mobile_no = models.CharField(max_length=100, blank=True, null=True) + parent_mobile_no = models.CharField(max_length=100, blank=True, null=True) + leave_mobile_no = models.CharField(max_length=100, blank=True, null=True) + curr_sem = models.IntegerField(blank=True, null=True) + approved = models.BooleanField(default=False) + rejected = models.BooleanField(default=False) hod = models.CharField(max_length=100) - stud_mobile_no = models.CharField(max_length=15, null=True, blank=True) - parent_mobile_no = models.CharField(max_length=15, null=True, blank=True) - leave_mobile_no = models.CharField(max_length=15, null=True, blank=True) - curr_sem=models.IntegerField(null=True) class Meta: db_table = 'LeaveFormTable' @@ -43,9 +68,10 @@ def clean(self): if self.date_from > self.date_to: raise ValidationError('The start date of leave cannot be later than the end date.') + class LeavePG(models.Model): """ - Records information related to student leave requests. + PG student leave request model. 'leave_from' and 'leave_to' store the start and end date of the leave request. 'date_of_application' stores the date when the leave request was applied. @@ -54,21 +80,11 @@ class LeavePG(models.Model): 'reason' stores the reason for the leave request. 'leave_type' stores the type of leave from a dropdown. """ - LEAVE_TYPES = ( - ('Casual', 'Casual'), - ('Medical', 'Medical'), - ('Vacation', 'Vacation'), - ('Duty', 'Duty') - - ) - - STATUS_CHOICES = ( - ('Pending', 'Pending'), - ('Approved', 'Approved'), - ('Rejected', 'Rejected'), - ) student_name = models.CharField(max_length=100) + programme = models.CharField(max_length=100) + discipline = models.CharField(max_length=100) + Semester = models.CharField(max_length=100) roll_no = models.ForeignKey(ExtraInfo, on_delete=models.CASCADE) date_from = models.DateField() date_to = models.DateField() @@ -76,45 +92,32 @@ class LeavePG(models.Model): upload_file = models.FileField(upload_to='leave_documents/', blank=True, null=True) address = models.CharField(max_length=100) purpose = models.TextField() - leave_type = models.CharField(max_length=20, choices=LEAVE_TYPES) - status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='Pending') + leave_type = models.CharField(max_length=20, choices=LeaveTypePGChoices.choices) + mobile_no = models.CharField(max_length=100) + parent_mobile_no = models.CharField(max_length=100) + alt_mobile_no = models.CharField(max_length=100) + ta_approved = models.BooleanField(default=False) + ta_rejected = models.BooleanField(default=False) + thesis_approved = models.BooleanField(default=False) + thesis_rejected = models.BooleanField(default=False) + hod_approved = models.BooleanField(default=False) + hod_rejected = models.BooleanField(default=False) hod = models.CharField(max_length=100) ta_supervisor = models.CharField(max_length=100) thesis_supervisor = models.CharField(max_length=100) - stud_mobile_no = models.CharField(max_length=15, null=True, blank=True) - parent_mobile_no = models.CharField(max_length=15, null=True, blank=True) - leave_mobile_no = models.CharField(max_length=15, null=True, blank=True) - curr_sem=models.IntegerField(null=True) - class Meta: - db_table='LeavePG' - + db_table = 'LeavePG' + def clean(self): if self.date_from > self.date_to: raise ValidationError('The start date of leave cannot be later than the end date.') - class LeavePGUpdTable(models.Model): """ - Records information related to student leave requests. - - 'leave_from' and 'leave_to' store the start and end date of the leave request. - 'date_of_application' stores the date when the leave request was applied. - 'related_document' stores any related documents or notes for the leave request. - 'place' stores the location where the leave is requested. - 'reason' stores the reason for the leave request. - 'leave_type' stores the type of leave from a dropdown. + PG Leave update table for tracking additional leave information. """ - LEAVE_TYPES = ( - ('Casual', 'Casual'), - ('Medical', 'Medical'), - ('Vacation', 'Vacation'), - ('Duty', 'Duty') - - ) - student_name = models.CharField(max_length=100) roll_no = models.ForeignKey(ExtraInfo, on_delete=models.CASCADE) @@ -127,7 +130,7 @@ class LeavePGUpdTable(models.Model): upload_file = models.FileField(upload_to='leave_doc') address = models.CharField(max_length=100) purpose = models.TextField() - leave_type = models.CharField(max_length=20, choices=LEAVE_TYPES) + leave_type = models.CharField(max_length=20, choices=LeaveTypePGChoices.choices) ta_supervisor = models.CharField(max_length=100) mobile_no = models.CharField(max_length=100) parent_mobile_no = models.CharField(max_length=100) @@ -136,50 +139,47 @@ class LeavePGUpdTable(models.Model): ta_rejected = models.BooleanField() hod_approved = models.BooleanField() hod_rejected = models.BooleanField() - ta_supervisor=models.CharField(max_length=100) - hod=models.CharField(max_length=100) - + hod = models.CharField(max_length=100) class Meta: - db_table='LeavePGUpdTable' - + db_table = 'LeavePGUpdTable' class GraduateSeminarFormTable(models.Model): - + """Graduate seminar form model.""" + roll_no = models.CharField(max_length=20) - semester= models.CharField(max_length=100) + semester = models.CharField(max_length=100) date_of_seminar = models.DateField() - class Meta: - db_table='GraduateSeminarFormTable' - + db_table = 'GraduateSeminarFormTable' class BonafideFormTableUpdated(models.Model): - - STATUS_CHOICES = ( - ('Pending', 'Pending'), - ('Approved', 'Approved'), - ('Rejected', 'Rejected'), - ) + """Bonafide application form model.""" student_names = models.CharField(max_length=100) roll_nos = models.ForeignKey(ExtraInfo, on_delete=models.CASCADE) branch_types = models.CharField(max_length=50) semester_types = models.CharField(max_length=20) purposes = models.TextField() - date_of_applications= models.DateField() - approve = models.BooleanField(default=False) # Make sure this exists - reject = models.BooleanField(default=False) # Ensu - # status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='Pending') - download_file = models.CharField(max_length=20, default='unavailable') - - + date_of_applications = models.DateField() + approve = models.BooleanField(default=False) + reject = models.BooleanField(default=False) + download_file = models.FileField(upload_to='Bonafide', blank=True, null=True) class Meta: - db_table='BonafideFormTableUpdated' + db_table = 'BonafideFormTableUpdated' + + @property + def status(self): + """Computed status based on approve/reject flags.""" + if self.approve: + return BonafideStatusChoices.APPROVED + elif self.reject: + return BonafideStatusChoices.REJECTED + return BonafideStatusChoices.PENDING @@ -237,12 +237,8 @@ class AssistantshipClaimFormStatusUpd(models.Model): Ths_rejected = models.BooleanField() HOD_approved = models.BooleanField() HOD_rejected = models.BooleanField() - Dean_approved = models.BooleanField(default=False) - Dean_rejected = models.BooleanField(default=False) - Director_approved = models.BooleanField(default=False) - Director_rejected = models.BooleanField(default=False) - AcadAdmin_approved = models.BooleanField(default=False) - AcadAdmin_rejected = models.BooleanField(default=False) + Acad_approved = models.BooleanField(default=False) + Acad_rejected = models.BooleanField(default=False) amount = models.DecimalField(max_digits=10, decimal_places=2, default=0) rate = models.DecimalField(max_digits=10, decimal_places=2, default=0) @@ -252,17 +248,92 @@ class AssistantshipClaimFormStatusUpd(models.Model): remark = models.TextField(default='') # New field with an empty default value def clean(self): - start_date = self.cleaned_data['start_date'] - end_date = self.cleaned_data['end_date'] - - if end_date <= start_date: - raise forms.ValidationError("End date must be later than start date") - - return super(AssistantshipClaimFormStatusUpd, self).clean() + """Validate that end date is after start date.""" + if self.dateFrom and self.dateTo and self.dateTo <= self.dateFrom: + raise ValidationError("End date must be later than start date") + return super().clean() class Meta: db_table = 'AssistantshipClaimFormStausUpd' + +class PGTAAssignment(models.Model): + """Stores TA subject assignment for PG students by dept admin.""" + + pg_student = models.ForeignKey(ExtraInfo, on_delete=models.CASCADE) + subject = models.ForeignKey(ProgrammeCourse, on_delete=models.CASCADE) + assigned_by = models.ForeignKey(User, null=True, blank=True, on_delete=models.SET_NULL) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + db_table = 'PGTAAssignment' + constraints = [ + models.UniqueConstraint( + fields=['pg_student', 'subject'], + name='uniq_pg_ta_assignment_student_subject', + ) + ] + + def __str__(self): + return f"{self.pg_student_id} -> {self.subject.code}" + + +class PGFacultySupervisorAssignment(models.Model): + """Stores faculty supervisor assignment for PG students by dept admin.""" + + pg_student = models.OneToOneField(ExtraInfo, on_delete=models.CASCADE) + faculty_supervisor = models.ForeignKey(User, on_delete=models.CASCADE) + assigned_by = models.ForeignKey( + User, + related_name="pg_faculty_supervisor_assigned_by", + null=True, + blank=True, + on_delete=models.SET_NULL, + ) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + db_table = 'PGFacultySupervisorAssignment' + + def __str__(self): + return f"{self.pg_student_id} -> {self.faculty_supervisor.username}" + + +class PGTAAssignmentHistory(models.Model): + """Immutable history of TA subject assignments.""" + + pg_student = models.ForeignKey(ExtraInfo, on_delete=models.CASCADE) + subject = models.ForeignKey(ProgrammeCourse, on_delete=models.CASCADE) + assigned_by = models.ForeignKey( + User, + related_name="pg_ta_assignment_history_assigned_by", + null=True, + blank=True, + on_delete=models.SET_NULL, + ) + changed_at = models.DateTimeField(auto_now_add=True) + + class Meta: + db_table = 'PGTAAssignmentHistory' + + +class PGFacultySupervisorAssignmentHistory(models.Model): + """Immutable history of faculty supervisor assignments.""" + + pg_student = models.ForeignKey(ExtraInfo, on_delete=models.CASCADE) + faculty_supervisor = models.ForeignKey(User, on_delete=models.CASCADE) + assigned_by = models.ForeignKey( + User, + related_name="pg_faculty_supervisor_assignment_history_assigned_by", + null=True, + blank=True, + on_delete=models.SET_NULL, + ) + changed_at = models.DateTimeField(auto_now_add=True) + + class Meta: + db_table = 'PGFacultySupervisorAssignmentHistory' + diff --git a/FusionIIIT/applications/otheracademic/selectors.py b/FusionIIIT/applications/otheracademic/selectors.py new file mode 100644 index 000000000..1af79ebc5 --- /dev/null +++ b/FusionIIIT/applications/otheracademic/selectors.py @@ -0,0 +1,650 @@ +""" +Selectors layer for otheracademic module. +Contains all database read operations (queries). +Views and services should call these selectors instead of querying directly. +""" +from django.db.models import F +from django.contrib.auth.models import User + +from applications.otheracademic.models import ( + LeaveFormTable, + LeavePG, + BonafideFormTableUpdated, + AssistantshipClaimFormStatusUpd, + PGTAAssignment, + PGFacultySupervisorAssignment, + PGTAAssignmentHistory, + PGFacultySupervisorAssignmentHistory, + NoDues, + LeaveStatusChoices, +) +from applications.globals.models import ExtraInfo, HoldsDesignation, Designation +from applications.globals.models import Faculty +from applications.academic_information.models import Student +from applications.programme_curriculum.models import Course as ProgrammeCourse + + +# ==================== USER/DESIGNATION SELECTORS ==================== + +def get_user_by_username(username): + """Get a user by username, returns None if not found.""" + if username is None: + return None + + token = str(username).strip() + if not token: + return None + + # Prefer exact match first to avoid ambiguous iexact collisions. + user = User.objects.filter(username=token).first() + if user: + return user + + return User.objects.filter(username__iexact=token).order_by("id").first() + + +def resolve_user_from_credential(credential): + """Resolve a user from username, full name, or designation-like credential.""" + if credential is None: + return None + + token = str(credential).strip() + if not token: + return None + + # 1) Direct username match + user = get_user_by_username(token) + if user: + return user + + # 2) Full-name match ("First Last" or underscore variants) + normalized = " ".join(token.replace("_", " ").split()) + parts = normalized.split() + if len(parts) >= 2: + first = parts[0] + last = " ".join(parts[1:]) + user = User.objects.filter(first_name__iexact=first, last_name__iexact=last).first() + if user: + return user + + # 3) Designation name exact/partial match; prefer active holder (`working`) + hold = HoldsDesignation.objects.filter( + designation__name__iexact=token + ).select_related("working").order_by("id").first() + if hold and hold.working: + return hold.working + + hold = HoldsDesignation.objects.filter( + designation__name__icontains=token + ).select_related("working").order_by("id").first() + if hold and hold.working: + return hold.working + + return None + + +def get_user_by_extrainfo_id(extrainfo_id): + """Get a user by their extrainfo ID.""" + try: + return User.objects.get(extrainfo=extrainfo_id) + except User.DoesNotExist: + return None + + +def get_user_by_extrainfo(extrainfo): + """Get a user by their extrainfo object.""" + try: + return User.objects.get(extrainfo=extrainfo) + except User.DoesNotExist: + return None + + +def get_first_designation_for_user(user): + """Get the first designation for a user.""" + designations = HoldsDesignation.objects.filter(user=user) + if designations.exists(): + return designations.first().designation + return None + + +def get_first_user_for_designation(designation_name): + """ + Get the first user who holds a specific designation. + Used for routing notifications to admin roles. + """ + try: + designation = Designation.objects.get(name=designation_name) + user_ids = HoldsDesignation.objects.filter( + designation_id=designation.id + ).order_by("user_id").values_list('user_id', flat=True) + + if user_ids.exists(): + return User.objects.get(id=user_ids[0]) + except (Designation.DoesNotExist, User.DoesNotExist): + pass + return None + + +def get_users_for_designation(designation_name): + """Get all users who hold a specific designation.""" + try: + designation = Designation.objects.get(name=designation_name) + except Designation.DoesNotExist: + return User.objects.none() + + user_ids = HoldsDesignation.objects.filter( + designation_id=designation.id + ).values_list("user_id", flat=True) + return User.objects.filter(id__in=user_ids).order_by("id").distinct() + + +def get_first_user_for_designation_contains(designation_keyword): + """Get first user with a designation containing the given keyword.""" + user_ids = HoldsDesignation.objects.filter( + designation__name__icontains=designation_keyword + ).values_list("user_id", flat=True) + if user_ids.exists(): + try: + return User.objects.get(id=user_ids[0]) + except User.DoesNotExist: + return None + return None + + +def get_users_for_designation_contains(designation_keyword): + """Get all users with a designation containing the given keyword.""" + user_ids = HoldsDesignation.objects.filter( + designation__name__icontains=designation_keyword + ).values_list("user_id", flat=True) + return User.objects.filter(id__in=user_ids).distinct() + + +def user_has_designation(user, designation_name): + """Return True if user has the given designation (case-insensitive).""" + return HoldsDesignation.objects.filter( + user=user, + designation__name__iexact=designation_name, + ).exists() + + +def user_has_designation_contains(user, designation_keyword): + """Return True if user has a designation containing the given keyword.""" + return HoldsDesignation.objects.filter( + user=user, + designation__name__icontains=designation_keyword, + ).exists() + + +# ==================== LEAVE SELECTORS ==================== + +def get_pending_ug_leaves(): + """Get all pending UG leave requests.""" + return LeaveFormTable.objects.filter(approved=False, rejected=False) + + +def get_pending_ug_leaves_for_hod(hod_username): + """Get pending UG leave requests assigned to a specific HOD.""" + return LeaveFormTable.objects.filter( + approved=False, + rejected=False, + hod__iexact=hod_username, + ) + + +def get_pending_pg_leaves_for_ta(): + """Get all pending PG leave requests (for TA approval).""" + return LeavePG.objects.filter(ta_approved=False, ta_rejected=False) + + +def get_pending_pg_leaves_for_ta_user(ta_username): + """Get pending PG leave requests assigned to a specific TA supervisor.""" + return LeavePG.objects.filter( + ta_approved=False, + ta_rejected=False, + ta_supervisor__iexact=ta_username, + ) + + +def get_pending_pg_leaves_for_thesis(): + """Get PG leave requests pending thesis supervisor approval.""" + return LeavePG.objects.filter( + ta_approved=True, + ta_rejected=False, + thesis_approved=False, + thesis_rejected=False, + ) + + +def get_pending_pg_leaves_for_thesis_user(thesis_username): + """Get PG leave requests pending review for a specific thesis supervisor.""" + return LeavePG.objects.filter( + ta_approved=True, + ta_rejected=False, + thesis_approved=False, + thesis_rejected=False, + thesis_supervisor__iexact=thesis_username, + ) + + +def get_pending_pg_leaves_for_hod(): + """Get PG leave requests pending HOD approval.""" + return LeavePG.objects.filter( + thesis_approved=True, + thesis_rejected=False, + hod_approved=False, + hod_rejected=False, + ) + + +def get_pending_pg_leaves_for_hod_user(hod_username): + """Get PG leave requests pending approval for a specific HOD.""" + return LeavePG.objects.filter( + thesis_approved=True, + thesis_rejected=False, + hod_approved=False, + hod_rejected=False, + hod__iexact=hod_username, + ) + + +def get_ug_leaves_by_roll_no(roll_no_id): + """Get all UG leave requests for a specific roll number.""" + return LeaveFormTable.objects.filter(roll_no=roll_no_id) + + +def get_pg_leaves_by_roll_no(roll_no_id): + """Get all PG leave requests for a specific roll number.""" + return LeavePG.objects.filter(roll_no=roll_no_id) + + +def ug_leave_overlap_exists(roll_no, date_from, date_to): + """Check if a UG leave overlaps with an existing non-rejected request.""" + return LeaveFormTable.objects.filter( + roll_no=roll_no, + rejected=False, + date_from__lte=date_to, + date_to__gte=date_from, + ).exists() + + +def pg_leave_overlap_exists(roll_no, date_from, date_to): + """Check if a PG leave overlaps with an existing non-rejected workflow request.""" + return LeavePG.objects.filter( + roll_no=roll_no, + ta_rejected=False, + thesis_rejected=False, + hod_rejected=False, + date_from__lte=date_to, + date_to__gte=date_from, + ).exists() + + +def get_leave_by_id(leave_id, is_pg=False): + """Get a leave request by ID.""" + model = LeavePG if is_pg else LeaveFormTable + try: + return model.objects.get(id=leave_id) + except model.DoesNotExist: + return None + + +def serialize_ug_leave(leave): + """Serialize a UG leave request to dictionary format.""" + mobile_number = getattr(leave, "stud_mobile_no", None) or getattr(leave, "mobile_no", None) + parents_mobile = getattr(leave, "parent_mobile_no", None) + mobile_during_leave = getattr(leave, "leave_mobile_no", None) or getattr(leave, "alt_mobile_no", None) + semester = getattr(leave, "curr_sem", None) + student_user = getattr(getattr(leave.roll_no, "user", None), "get_full_name", lambda: "")() + student_name = (student_user or leave.student_name or getattr(getattr(leave.roll_no, "user", None), "username", "")).strip() + return { + "id": leave.id, + "rollNo": leave.roll_no.id, + "name": student_name, + "form": leave.upload_file.url if leave.upload_file else None, + "details": { + "dateFrom": leave.date_from, + "dateTo": leave.date_to, + "leaveType": leave.leave_type, + "address": leave.address, + "purpose": leave.purpose, + "hodCredential": leave.hod, + "mobileNumber": mobile_number, + "parentsMobile": parents_mobile, + "mobileDuringLeave": mobile_during_leave, + "semester": str(semester) if semester is not None else None, + "academicYear": leave.date_of_application.year, + "dateOfApplication": leave.date_of_application, + }, + } + + +def serialize_pg_leave(leave): + """Serialize a PG leave request to dictionary format.""" + return { + "id": leave.id, + "rollNo": leave.roll_no.id, + "name": leave.student_name, + "form": leave.upload_file.url if leave.upload_file else None, + "details": { + "dateFrom": leave.date_from, + "dateTo": leave.date_to, + "leaveType": leave.leave_type, + "address": leave.address, + "purpose": leave.purpose, + "hodCredential": leave.hod, + "mobileNumber": leave.mobile_no, + "parentsMobile": leave.parent_mobile_no, + "mobileDuringLeave": leave.alt_mobile_no, + "semester": leave.Semester, + "academicYear": leave.date_of_application.year, + "dateOfApplication": leave.date_of_application, + }, + } + + +def serialize_leave_status(leave, roll_no_id): + """Serialize leave status for student view.""" + if hasattr(leave, 'approved'): + status_text = ( + LeaveStatusChoices.APPROVED + if leave.approved + else LeaveStatusChoices.REJECTED + if leave.rejected + else LeaveStatusChoices.PENDING + ) + is_final = leave.approved or leave.rejected + else: + is_rejected = any([ + leave.ta_rejected, + leave.thesis_rejected, + leave.hod_rejected, + ]) + status_text = ( + LeaveStatusChoices.APPROVED + if leave.hod_approved + else LeaveStatusChoices.REJECTED + if is_rejected + else LeaveStatusChoices.PENDING + ) + is_final = leave.hod_approved or leave.hod_rejected + + student_user = getattr(getattr(leave.roll_no, "user", None), "get_full_name", lambda: "")() + student_name = (student_user or leave.student_name or getattr(getattr(leave.roll_no, "user", None), "username", "")).strip() + + return { + "id": leave.id, + "rollNo": roll_no_id, + "name": student_name, + "dateApplied": leave.date_of_application.strftime("%Y-%m-%d") if leave.date_of_application else None, + "dateFrom": leave.date_from, + "dateTo": leave.date_to, + "leaveType": leave.leave_type, + "attachment": leave.upload_file.url if leave.upload_file else None, + "purpose": leave.purpose, + "address": leave.address, + "action": status_text, + "canWithdraw": not is_final, + } + + +# ==================== BONAFIDE SELECTORS ==================== + +def get_pending_bonafides(): + """Get all pending bonafide requests.""" + return BonafideFormTableUpdated.objects.filter(approve=False, reject=False) + + +def get_bonafide_by_id(bonafide_id): + """Get a bonafide request by ID.""" + try: + return BonafideFormTableUpdated.objects.get(id=bonafide_id) + except BonafideFormTableUpdated.DoesNotExist: + return None + + +def get_bonafides_by_roll_no(roll_no_id): + """Get all bonafide requests for a specific roll number.""" + return BonafideFormTableUpdated.objects.filter(roll_nos_id=roll_no_id) + + +def serialize_pending_bonafide(bonafide): + """Serialize a pending bonafide request.""" + return { + "id": bonafide.id, + "rollNo": bonafide.roll_nos_id, + "name": bonafide.student_names, + "details": { + "purpose": bonafide.purposes, + "dateOfApplication": bonafide.date_of_applications, + "semester": bonafide.semester_types, + }, + } + + +def serialize_bonafide_status(bonafide): + """Serialize bonafide status for student view.""" + status = "Approved" if bonafide.approve else "Rejected" if bonafide.reject else "Pending" + download_url = None + if bonafide.download_file: + try: + download_url = bonafide.download_file.url + except ValueError: + # File field can be empty or point to a non-resolved path. + download_url = None + + return { + "id": bonafide.id, + "rollNo": bonafide.roll_nos_id, + "name": bonafide.student_names, + "branch": bonafide.branch_types, + "semester": bonafide.semester_types, + "purpose": bonafide.purposes, + "dateApplied": bonafide.date_of_applications.strftime("%Y-%m-%d") if bonafide.date_of_applications else None, + "status": status, + "downloadUrl": download_url, + "canWithdraw": not bonafide.approve and not bonafide.reject, + } + + +# ==================== ASSISTANTSHIP SELECTORS ==================== + +def assistantship_exists_for_period(roll_no, date_from, date_to): + """Check if an assistantship form already exists for the given period.""" + return AssistantshipClaimFormStatusUpd.objects.filter( + roll_no=roll_no, + dateFrom=date_from, + dateTo=date_to + ).exists() + + +def get_pending_assistantships_for_ta(): + """Get assistantship forms pending TA approval.""" + return AssistantshipClaimFormStatusUpd.objects.filter( + TA_approved=False, + TA_rejected=False + ) + + +def get_pending_assistantships_for_ta_user(ta_username): + """Get assistantship forms pending review for a specific faculty supervisor.""" + return AssistantshipClaimFormStatusUpd.objects.filter( + TA_approved=False, + TA_rejected=False, + ta_supervisor__iexact=ta_username, + ) + + +def get_pending_assistantships_for_thesis(): + """Get assistantship forms pending Thesis supervisor approval.""" + return AssistantshipClaimFormStatusUpd.objects.filter( + Ths_approved=False, + Ths_rejected=False + ) + + +def get_pending_assistantships_for_hod(): + """Get assistantship forms pending Department Admin verification.""" + return AssistantshipClaimFormStatusUpd.objects.filter( + TA_approved=True, + TA_rejected=False, + HOD_approved=False, + HOD_rejected=False + ) + + +def get_pending_assistantships_for_hod_user(hod_username): + """Get assistantship forms pending for a specific Department Admin user.""" + return AssistantshipClaimFormStatusUpd.objects.filter( + TA_approved=True, + TA_rejected=False, + HOD_approved=False, + HOD_rejected=False, + hod__iexact=hod_username, + ) + + +def get_assistantship_by_id(form_id): + """Get assistantship form by id.""" + try: + return AssistantshipClaimFormStatusUpd.objects.get(id=form_id) + except AssistantshipClaimFormStatusUpd.DoesNotExist: + return None + + +def get_pending_assistantships_for_acad_admin(): + """Get assistantship forms pending Academic Admin disbursement audit.""" + return AssistantshipClaimFormStatusUpd.objects.filter( + TA_approved=True, + TA_rejected=False, + HOD_approved=True, + HOD_rejected=False, + Acad_approved=True, + Acad_rejected=False, + ).exclude(remark="Stipend disbursed (audit completed)") + + +def get_pending_assistantships_for_dean(): + """Get assistantship forms pending HOD approval.""" + return AssistantshipClaimFormStatusUpd.objects.filter( + TA_approved=True, + TA_rejected=False, + HOD_approved=True, + HOD_rejected=False, + Acad_approved=False, + Acad_rejected=False, + ) + + +def get_pending_assistantships_for_director(): + """Get assistantship forms pending Director approval.""" + return AssistantshipClaimFormStatusUpd.objects.none() + + +def get_assistantships_by_roll_no(roll_no_id): + """Get all assistantship forms for a specific roll number.""" + return AssistantshipClaimFormStatusUpd.objects.filter(roll_no_id=roll_no_id) + + +def serialize_assistantship_pending(form): + """Serialize an assistantship form for pending requests view.""" + return { + "id": form.id, + "student_name": form.student_name, + "roll_no": form.roll_no.id, + "discipline": form.discipline, + "dateFrom": form.dateFrom.strftime('%Y-%m-%d'), + "dateTo": form.dateTo.strftime('%Y-%m-%d'), + "applicability": form.applicability, + "dateApplied": form.dateApplied.strftime('%Y-%m-%d'), + "ta_supervisor": form.ta_supervisor, + "thesis_supervisor": form.thesis_supervisor, + "hod": form.hod, + } + + +# ==================== NO DUES SELECTORS ==================== + +def get_nodues_by_roll_no(roll_no): + """Get no dues record for a specific roll number.""" + try: + return NoDues.objects.get(roll_no=roll_no) + except NoDues.DoesNotExist: + return None + + +def get_all_nodues_requests(): + """Get all no dues requests.""" + return NoDues.objects.all() + + +# ==================== PG TA ASSIGNMENT SELECTORS ==================== + +def get_pg_students_for_assignment(): + """Get PG students (M.Tech/PhD/M.Des) for assignment workflows.""" + return Student.objects.select_related("id__user").filter( + programme__in=["M.Tech", "PhD", "M.Des"] + ).exclude(id__id__iexact="23BCS229").order_by("id__id") + + +def get_pg_students_for_ta_assignment(): + """Get PG students (M.Tech/PhD/M.Des) for TA assignment.""" + return get_pg_students_for_assignment() + + +def get_subject_options_for_ta_assignment(): + """Get available subjects from programme curriculum.""" + return ProgrammeCourse.objects.filter(working_course=True).order_by("code", "name") + + +def get_all_pg_ta_assignments(): + """Get existing TA assignments for PG students.""" + return PGTAAssignment.objects.select_related("pg_student", "subject").order_by( + "pg_student__id", "subject__code", "subject__name" + ) + + +def get_pg_ta_assignment_for_student(pg_student_id): + """Get TA assignment rows for a PG student.""" + return PGTAAssignment.objects.select_related("subject", "assigned_by").filter( + pg_student_id=pg_student_id + ).order_by("subject__code", "subject__name") + + +def get_faculty_members_for_supervisor_assignment(): + """Get faculty users for faculty supervisor assignment dropdown.""" + return Faculty.objects.select_related("id__user").order_by("id__id") + + +def get_all_pg_faculty_supervisor_assignments(): + """Get existing faculty supervisor assignments for PG students.""" + return PGFacultySupervisorAssignment.objects.select_related( + "pg_student", "faculty_supervisor", "assigned_by" + ) + + +def get_pg_ta_assignment_history(pg_student_id=None): + """Get TA assignment history rows.""" + qs = PGTAAssignmentHistory.objects.select_related("pg_student", "subject", "assigned_by") + if pg_student_id: + qs = qs.filter(pg_student_id=pg_student_id) + return qs.order_by("-changed_at") + + +def get_pg_faculty_supervisor_assignment_history(pg_student_id=None): + """Get faculty supervisor assignment history rows.""" + qs = PGFacultySupervisorAssignmentHistory.objects.select_related( + "pg_student", "faculty_supervisor", "assigned_by" + ) + if pg_student_id: + qs = qs.filter(pg_student_id=pg_student_id) + return qs.order_by("-changed_at") + + +def get_pg_faculty_supervisor_assignment_for_student(pg_student_id): + """Get faculty supervisor assignment row for a PG student.""" + try: + return PGFacultySupervisorAssignment.objects.select_related("faculty_supervisor").get( + pg_student_id=pg_student_id + ) + except PGFacultySupervisorAssignment.DoesNotExist: + return None diff --git a/FusionIIIT/applications/otheracademic/services.py b/FusionIIIT/applications/otheracademic/services.py new file mode 100644 index 000000000..f1e9de961 --- /dev/null +++ b/FusionIIIT/applications/otheracademic/services.py @@ -0,0 +1,1294 @@ +""" +Services layer for otheracademic module. +Contains all business logic and write operations. +Views should call these services instead of containing business logic directly. +""" +from datetime import date, datetime +from django.contrib.auth.models import User +from django.core.exceptions import ValidationError +from django.db import transaction + +from applications.otheracademic.models import ( + LeaveFormTable, + LeavePG, + BonafideFormTableUpdated, + AssistantshipClaimFormStatusUpd, + PGTAAssignment, + PGFacultySupervisorAssignment, + PGTAAssignmentHistory, + PGFacultySupervisorAssignmentHistory, + NoDues, + LeaveStatusChoices, + LeaveTypeChoices, +) +from applications.globals.models import ExtraInfo, HoldsDesignation, Designation +from applications.filetracking.sdk.methods import create_file +from notification.views import otheracademic_notif + +from . import selectors + + +class LeaveServiceError(Exception): + """Custom exception for leave-related service errors.""" + pass + + +class BonafideServiceError(Exception): + """Custom exception for bonafide-related service errors.""" + pass + + +class AssistantshipServiceError(Exception): + """Custom exception for assistantship-related service errors.""" + pass + + +class TAAssignmentServiceError(Exception): + """Custom exception for PG TA assignment-related errors.""" + pass + + +def _get_bonafide_admin_recipients(): + """Return all academic-admin users eligible for bonafide notifications.""" + recipients = selectors.get_users_for_designation("acadadmin") + if recipients.exists(): + return recipients + + # Backward-compatible fallback for alternate naming in some deployments. + return selectors.get_users_for_designation("acad_admin") + + +# ==================== LEAVE SERVICES ==================== + +def submit_ug_leave( + user, + date_from, + date_to, + leave_type, + address, + purpose, + hod_credential, + semester, + mobile_number=None, + parents_mobile=None, + mobile_during_leave=None, + upload_file=None, +): + """ + Submit a UG leave application. + Creates leave record, file tracking, and sends notification to HOD. + """ + try: + parsed_date_from = datetime.strptime(str(date_from), "%Y-%m-%d").date() + parsed_date_to = datetime.strptime(str(date_to), "%Y-%m-%d").date() + except (TypeError, ValueError): + raise LeaveServiceError("Invalid date format. Please use YYYY-MM-DD.") + + if parsed_date_from > parsed_date_to: + raise LeaveServiceError("Invalid leave dates: end date must be on or after start date.") + + if selectors.ug_leave_overlap_exists(user.extrainfo, parsed_date_from, parsed_date_to): + raise LeaveServiceError("Overlapping leave request already exists for the selected dates.") + + # Validate HOD exists + hod_user = selectors.resolve_user_from_credential(hod_credential) + if not hod_user: + raise LeaveServiceError(f"HOD with username '{hod_credential}' not found.") + + # Create leave record + student_name = (user.get_full_name() or "").strip() or user.username + + leave = LeaveFormTable.objects.create( + student_name=student_name, + roll_no=user.extrainfo, + date_from=parsed_date_from, + date_to=parsed_date_to, + leave_type=leave_type, + upload_file=upload_file, + address=address, + purpose=purpose, + date_of_application=date.today(), + stud_mobile_no=mobile_number or "", + parent_mobile_no=parents_mobile or "", + leave_mobile_no=mobile_during_leave or "", + curr_sem=int(semester) if semester not in (None, "") else None, + approved=False, + rejected=False, + hod=hod_user.username, + ) + + # Get uploader designation for file tracking + uploader_designation = selectors.get_first_designation_for_user(user) + + # Create file tracking record + create_file( + uploader=user.username, + uploader_designation=uploader_designation, + receiver=hod_user, + receiver_designation="student", + src_module="otheracademic", + src_object_id=leave.id, + file_extra_JSON={"value": 2}, + attached_file=None, + subject='ug_leave' + ) + + # Send notification to HOD + otheracademic_notif(user, hod_user, 'ug_leave_hod', leave.id, 'student', "A new leave application") + + return leave + + +def submit_pg_leave( + user, + date_from, + date_to, + leave_type, + address, + purpose, + hod_credential, + ta_supervisor_credential, + thesis_supervisor_credential, + semester, + mobile_number=None, + parents_mobile=None, + mobile_during_leave=None, + upload_file=None, +): + """ + Submit a PG leave application. + Creates leave record, file tracking, and sends notification to TA supervisor. + """ + try: + parsed_date_from = datetime.strptime(str(date_from), "%Y-%m-%d").date() + parsed_date_to = datetime.strptime(str(date_to), "%Y-%m-%d").date() + except (TypeError, ValueError): + raise LeaveServiceError("Invalid date format. Please use YYYY-MM-DD.") + + if parsed_date_from > parsed_date_to: + raise LeaveServiceError("Invalid leave dates: end date must be on or after start date.") + + if selectors.pg_leave_overlap_exists(user.extrainfo, parsed_date_from, parsed_date_to): + raise LeaveServiceError("Overlapping leave request already exists for the selected dates.") + + # Validate all supervisors exist + ta_user = selectors.resolve_user_from_credential(ta_supervisor_credential) + if not ta_user: + raise LeaveServiceError(f"TA Supervisor with username '{ta_supervisor_credential}' not found.") + + thesis_user = selectors.resolve_user_from_credential(thesis_supervisor_credential) + if not thesis_user: + raise LeaveServiceError(f"Thesis Supervisor with username '{thesis_supervisor_credential}' not found.") + + hod_user = selectors.resolve_user_from_credential(hod_credential) + if not hod_user: + raise LeaveServiceError(f"HOD with username '{hod_credential}' not found.") + + # Create leave record + leave = LeavePG.objects.create( + student_name=f"{user.first_name}{user.last_name}", + programme="", + discipline="", + Semester=str(semester) if semester else "", + roll_no=user.extrainfo, + date_from=parsed_date_from, + date_to=parsed_date_to, + leave_type=leave_type, + upload_file=upload_file, + address=address, + purpose=purpose, + date_of_application=date.today(), + mobile_no=mobile_number or "", + parent_mobile_no=parents_mobile or "", + alt_mobile_no=mobile_during_leave or "", + ta_approved=False, + ta_rejected=False, + thesis_approved=False, + thesis_rejected=False, + hod_approved=False, + hod_rejected=False, + hod=hod_user.username, + ta_supervisor=ta_user.username, + thesis_supervisor=thesis_user.username, + ) + + # Get uploader designation for file tracking + uploader_designation = selectors.get_first_designation_for_user(user) + + # Create file tracking record + create_file( + uploader=user.username, + uploader_designation=uploader_designation, + receiver=hod_user, + receiver_designation="student", + src_module="otheracademic", + src_object_id=leave.id, + file_extra_JSON={"value": 2}, + attached_file=None, + subject='pg_leave' + ) + + # Send notification to TA supervisor + otheracademic_notif(user, ta_user, 'pg_leave_ta', leave.id, 'student', "A new leave application") + + return leave + + +def update_ug_leave_status(approved_ids, rejected_ids, actor_user): + """Update status of UG leave requests (by HOD).""" + if approved_ids: + for leave_id in approved_ids: + leave = selectors.get_leave_by_id(leave_id, is_pg=False) + if not leave: + continue + if leave.hod.lower() != actor_user.username.lower(): + raise LeaveServiceError("You can only act on leave requests assigned to you.") + if leave.approved or leave.rejected: + raise LeaveServiceError("This leave request has already been finalized.") + leave.approved = True + leave.rejected = False + leave.save(update_fields=["approved", "rejected"]) + if leave: + student = selectors.get_user_by_extrainfo_id(leave.roll_no_id) + if student: + otheracademic_notif( + actor_user, + student, + 'ug_leave_hod_approve', + leave.id, + 'admin', + "Your leave request has been approved by HOD.", + ) + if rejected_ids: + for leave_id in rejected_ids: + leave = selectors.get_leave_by_id(leave_id, is_pg=False) + if not leave: + continue + if leave.hod.lower() != actor_user.username.lower(): + raise LeaveServiceError("You can only act on leave requests assigned to you.") + if leave.approved or leave.rejected: + raise LeaveServiceError("This leave request has already been finalized.") + leave.approved = False + leave.rejected = True + leave.save(update_fields=["approved", "rejected"]) + if leave: + student = selectors.get_user_by_extrainfo_id(leave.roll_no_id) + if student: + otheracademic_notif( + actor_user, + student, + 'ug_leave_hod_approve', + leave.id, + 'admin', + "Your leave request has been rejected by HOD.", + ) + + +def update_pg_leave_status_hod(approved_ids, rejected_ids, actor_user): + """Update status of PG leave requests (by HOD - final approval).""" + if approved_ids: + for leave_id in approved_ids: + leave = selectors.get_leave_by_id(leave_id, is_pg=True) + if not leave: + continue + if leave.hod.lower() != actor_user.username.lower(): + raise LeaveServiceError("You can only act on leave requests assigned to you.") + if not leave.thesis_approved or leave.thesis_rejected: + raise LeaveServiceError("HOD can only act after thesis supervisor approval.") + if leave.hod_approved or leave.hod_rejected: + raise LeaveServiceError("This leave request has already been finalized by HOD.") + leave.hod_approved = True + leave.hod_rejected = False + leave.save(update_fields=["hod_approved", "hod_rejected"]) + if leave: + student = selectors.get_user_by_extrainfo_id(leave.roll_no_id) + if student: + otheracademic_notif( + actor_user, + student, + 'pg_leave_ta_approve', + leave.id, + 'admin', + "Your PG leave request has been approved by HOD.", + ) + if rejected_ids: + for leave_id in rejected_ids: + leave = selectors.get_leave_by_id(leave_id, is_pg=True) + if not leave: + continue + if leave.hod.lower() != actor_user.username.lower(): + raise LeaveServiceError("You can only act on leave requests assigned to you.") + if not leave.thesis_approved or leave.thesis_rejected: + raise LeaveServiceError("HOD can only act after thesis supervisor approval.") + if leave.hod_approved or leave.hod_rejected: + raise LeaveServiceError("This leave request has already been finalized by HOD.") + leave.hod_approved = False + leave.hod_rejected = True + leave.save(update_fields=["hod_approved", "hod_rejected"]) + if leave: + student = selectors.get_user_by_extrainfo_id(leave.roll_no_id) + if student: + otheracademic_notif( + actor_user, + student, + 'pg_leave_ta_approve', + leave.id, + 'admin', + "Your PG leave request has been rejected by HOD.", + ) + + +def update_pg_leave_status_ta(approved_ids, rejected_ids, actor_user): + """Update status of PG leave requests (by TA supervisor).""" + if approved_ids: + for leave_id in approved_ids: + leave = selectors.get_leave_by_id(leave_id, is_pg=True) + if not leave: + continue + if leave.ta_supervisor.lower() != actor_user.username.lower(): + raise LeaveServiceError("You can only act on leave requests assigned to you.") + if leave.ta_approved or leave.ta_rejected: + raise LeaveServiceError("TA supervisor decision already exists for this request.") + leave.ta_approved = True + leave.ta_rejected = False + leave.save(update_fields=["ta_approved", "ta_rejected"]) + if leave: + thesis_user = selectors.get_user_by_username(leave.thesis_supervisor) + student = selectors.get_user_by_extrainfo_id(leave.roll_no_id) + if thesis_user: + otheracademic_notif( + actor_user, + thesis_user, + 'pg_leave_thesis', + leave.id, + 'student', + "A PG leave request is forwarded to you for thesis supervisor review.", + ) + if student: + otheracademic_notif( + actor_user, + student, + 'pg_leave_ta_approve', + leave.id, + 'admin', + "Your PG leave request has been approved by TA supervisor and moved to thesis supervisor.", + ) + if rejected_ids: + for leave_id in rejected_ids: + leave = selectors.get_leave_by_id(leave_id, is_pg=True) + if not leave: + continue + if leave.ta_supervisor.lower() != actor_user.username.lower(): + raise LeaveServiceError("You can only act on leave requests assigned to you.") + if leave.ta_approved or leave.ta_rejected: + raise LeaveServiceError("TA supervisor decision already exists for this request.") + leave.ta_approved = False + leave.ta_rejected = True + leave.save(update_fields=["ta_approved", "ta_rejected"]) + if leave: + student = selectors.get_user_by_extrainfo_id(leave.roll_no_id) + if student: + otheracademic_notif( + actor_user, + student, + 'pg_leave_ta_approve', + leave.id, + 'admin', + "Your PG leave request has been rejected at TA supervisor level.", + ) + + +def update_pg_leave_status_thesis(approved_ids, rejected_ids, actor_user): + """Update status of PG leave requests (by Thesis supervisor).""" + if approved_ids: + for leave_id in approved_ids: + leave = selectors.get_leave_by_id(leave_id, is_pg=True) + if not leave: + continue + if leave.thesis_supervisor.lower() != actor_user.username.lower(): + raise LeaveServiceError("You can only act on leave requests assigned to you.") + if not leave.ta_approved or leave.ta_rejected: + raise LeaveServiceError("Thesis supervisor can act only after TA approval.") + if leave.thesis_approved or leave.thesis_rejected: + raise LeaveServiceError("Thesis supervisor decision already exists for this request.") + leave.thesis_approved = True + leave.thesis_rejected = False + leave.save(update_fields=["thesis_approved", "thesis_rejected"]) + if leave: + hod_user = selectors.get_user_by_username(leave.hod) + student = selectors.get_user_by_extrainfo_id(leave.roll_no_id) + if hod_user: + otheracademic_notif( + actor_user, + hod_user, + 'pg_leave_hod', + leave.id, + 'student', + "A PG leave request is forwarded to you for HOD review.", + ) + if student: + otheracademic_notif( + actor_user, + student, + 'pg_leave_ta_approve', + leave.id, + 'admin', + "Your PG leave request has been approved by thesis supervisor and moved to HOD.", + ) + if rejected_ids: + for leave_id in rejected_ids: + leave = selectors.get_leave_by_id(leave_id, is_pg=True) + if not leave: + continue + if leave.thesis_supervisor.lower() != actor_user.username.lower(): + raise LeaveServiceError("You can only act on leave requests assigned to you.") + if not leave.ta_approved or leave.ta_rejected: + raise LeaveServiceError("Thesis supervisor can act only after TA approval.") + if leave.thesis_approved or leave.thesis_rejected: + raise LeaveServiceError("Thesis supervisor decision already exists for this request.") + leave.thesis_approved = False + leave.thesis_rejected = True + leave.save(update_fields=["thesis_approved", "thesis_rejected"]) + if leave: + student = selectors.get_user_by_extrainfo_id(leave.roll_no_id) + if student: + otheracademic_notif( + actor_user, + student, + 'pg_leave_ta_approve', + leave.id, + 'admin', + "Your PG leave request has been rejected at thesis supervisor level.", + ) + + +def withdraw_ug_leave(user, leave_id): + """Allow student to withdraw a UG leave request before final HOD decision.""" + leave = selectors.get_leave_by_id(leave_id, is_pg=False) + if not leave: + raise LeaveServiceError("Leave request not found.") + if leave.roll_no_id != user.extrainfo.id: + raise LeaveServiceError("You are not authorized to withdraw this leave request.") + if leave.approved or leave.rejected: + raise LeaveServiceError("Cannot withdraw a leave request that has already been verified by HOD.") + + hod_user = selectors.get_user_by_username(leave.hod) + if hod_user: + otheracademic_notif( + user, + hod_user, + 'ug_leave_hod', + leave.id, + 'student', + "A leave request has been withdrawn by the student.", + ) + leave.delete() + + +def withdraw_pg_leave(user, leave_id): + """Allow student to withdraw a PG leave request before final HOD decision.""" + leave = selectors.get_leave_by_id(leave_id, is_pg=True) + if not leave: + raise LeaveServiceError("Leave request not found.") + if leave.roll_no_id != user.extrainfo.id: + raise LeaveServiceError("You are not authorized to withdraw this leave request.") + if leave.hod_approved or leave.hod_rejected: + raise LeaveServiceError("Cannot withdraw a leave request that has already been verified by HOD.") + + hod_user = selectors.get_user_by_username(leave.hod) + if hod_user: + otheracademic_notif( + user, + hod_user, + 'pg_leave_hod', + leave.id, + 'student', + "A PG leave request has been withdrawn by the student.", + ) + leave.delete() + + +# ==================== BONAFIDE SERVICES ==================== + +def submit_bonafide(user, branch, semester, purpose, download_file=None): + """ + Submit a bonafide application. + Creates bonafide record and sends notification to academic admin. + """ + if not branch or not semester or not purpose: + raise BonafideServiceError("Branch, semester, and purpose are required.") + + bonafide_form = BonafideFormTableUpdated.objects.create( + student_names=f"{user.first_name} {user.last_name}", + roll_nos=user.extrainfo, + branch_types=branch, + semester_types=semester, + purposes=purpose, + date_of_applications=date.today(), + # Store an optional supporting document uploaded at submission time. + download_file=download_file, + approve=False, + reject=False, + ) + + # Notify all academic admins + for acad_admin_user in _get_bonafide_admin_recipients(): + otheracademic_notif( + user, + acad_admin_user, + 'bonafide_acadadmin', + bonafide_form.id, + 'student', + "A Bonafide request is pending for your approval." + ) + + return bonafide_form + + +def update_bonafide_status(approved_ids, rejected_ids, actor_user): + """ + Update bonafide status and send notifications to students. + """ + # Process approvals + if approved_ids: + for bonafide_id in approved_ids: + bonafide = selectors.get_bonafide_by_id(bonafide_id) + if bonafide: + if bonafide.approve or bonafide.reject: + raise BonafideServiceError("Bonafide request is already finalized.") + bonafide.approve = True + bonafide.reject = False + bonafide.save(update_fields=["approve", "reject"]) + student = selectors.get_user_by_extrainfo_id(bonafide.roll_nos_id) + if student: + otheracademic_notif( + actor_user, + student, + 'bonafide_accept', + bonafide.id, + 'admin', + "Your Bonafide application has been approved. Please check the status." + ) + + # Process rejections + if rejected_ids: + for bonafide_id in rejected_ids: + bonafide = selectors.get_bonafide_by_id(bonafide_id) + if bonafide: + if bonafide.approve or bonafide.reject: + raise BonafideServiceError("Bonafide request is already finalized.") + bonafide.approve = False + bonafide.reject = True + bonafide.save(update_fields=["approve", "reject"]) + student = selectors.get_user_by_extrainfo(bonafide.roll_nos) + if student: + otheracademic_notif( + actor_user, + student, + 'bonafide_accept', + bonafide.id, + 'admin', + "Your Bonafide application has been rejected. Please check the status for further details." + ) + + +def upload_bonafide_certificate(bonafide_id, certificate): + """Upload bonafide certificate for a pending or approved request.""" + bonafide = selectors.get_bonafide_by_id(bonafide_id) + if not bonafide: + raise BonafideServiceError("Bonafide request not found.") + if bonafide.reject: + raise BonafideServiceError("Certificate cannot be uploaded for a rejected bonafide request.") + + bonafide.download_file = certificate + bonafide.save(update_fields=["download_file"]) + return bonafide + + +def withdraw_bonafide(user, bonafide_id): + """Allow student to withdraw only pending bonafide requests.""" + bonafide = selectors.get_bonafide_by_id(bonafide_id) + if not bonafide: + raise BonafideServiceError("Bonafide request not found.") + if bonafide.roll_nos_id != user.extrainfo.id: + raise BonafideServiceError("You are not authorized to withdraw this bonafide request.") + if bonafide.approve or bonafide.reject: + raise BonafideServiceError("Only pending bonafide requests can be withdrawn.") + + for acad_admin_user in _get_bonafide_admin_recipients(): + otheracademic_notif( + user, + acad_admin_user, + 'bonafide_acadadmin', + bonafide.id, + 'student', + "A Bonafide application has been withdrawn by the student.", + ) + bonafide.delete() + + +# ==================== ASSISTANTSHIP SERVICES ==================== + +def submit_assistantship( + user, + discipline, + date_from, + date_to, + date_applied, + bank_account, + signature_file, + ta_supervisor, + thesis_supervisor, + hod, + applicability, +): + """ + Submit an assistantship claim form. + Validates supervisors, creates record, and sends notifications. + """ + # Check for duplicate submission + if selectors.assistantship_exists_for_period(user.extrainfo, date_from, date_to): + raise AssistantshipServiceError("Form for this period already exists.") + + # PG-only eligibility for assistantship. + is_pg_student = selectors.get_pg_students_for_assignment().filter(id=user.extrainfo).exists() + if not is_pg_student: + raise AssistantshipServiceError("Only PG students can submit assistantship claims.") + + # Resolve assigned faculty supervisor for this PG student (if configured). + supervisor_assignment = selectors.get_pg_faculty_supervisor_assignment_for_student( + user.extrainfo.id + ) + if not supervisor_assignment: + raise AssistantshipServiceError( + "Faculty Supervisor is not assigned for this PG student. Please contact Department Admin." + ) + + ta_supervisor_user = supervisor_assignment.faculty_supervisor + if ta_supervisor and ta_supervisor_user.username.lower() != str(ta_supervisor).lower(): + raise AssistantshipServiceError( + "Faculty Supervisor does not match the configured assignment for this PG student." + ) + + # Resolve Department Admin for next stage. + dept_admin_user = None + if hod: + candidate = selectors.get_user_by_username(hod) + if candidate and selectors.user_has_designation(candidate, "dept_admin"): + dept_admin_user = candidate + + if not dept_admin_user: + dept_admin_user = ( + selectors.get_first_user_for_designation("dept_admin") + or selectors.get_first_user_for_designation("deptadmin") + ) + + if not dept_admin_user: + raise AssistantshipServiceError("Department Admin is not configured.") + + # Create assistantship form + assistantship_form = AssistantshipClaimFormStatusUpd.objects.create( + roll_no=user.extrainfo, + student_name=f"{user.first_name} {user.last_name}", + discipline=discipline, + dateFrom=date_from, + dateTo=date_to, + bank_account=bank_account, + student_signature=signature_file, + dateApplied=date_applied, + ta_supervisor=ta_supervisor_user.username, + thesis_supervisor=thesis_supervisor or "", + hod=dept_admin_user.username, + applicability=applicability, + TA_approved=False, + TA_rejected=False, + Ths_approved=False, + Ths_rejected=False, + HOD_approved=False, + HOD_rejected=False, + Acad_approved=False, + Acad_rejected=False, + remark="", + ) + + # Send notification to faculty supervisor (first review stage). + otheracademic_notif( + user, + ta_supervisor_user, + "ast_ta", + assistantship_form.id, + "student", + "A PG assistantship form is waiting for your verification.", + ) + + return assistantship_form + + +def update_assistantship_status_ta(approved_ids, rejected_ids, actor_user): + """Update assistantship status by faculty supervisor.""" + if approved_ids: + for form_id in approved_ids: + form = selectors.get_assistantship_by_id(form_id) + if not form: + continue + if form.ta_supervisor.lower() != actor_user.username.lower(): + raise AssistantshipServiceError("You can only review forms assigned to you.") + if form.TA_approved or form.TA_rejected: + raise AssistantshipServiceError("This assistantship form is already reviewed.") + + form.TA_approved = True + form.TA_rejected = False + form.save(update_fields=["TA_approved", "TA_rejected"]) + + student_user = selectors.get_user_by_extrainfo_id(form.roll_no_id) + dept_admin_users = selectors.get_users_for_designation("dept_admin") + if not dept_admin_users.exists(): + dept_admin_users = selectors.get_users_for_designation("deptadmin") + + if student_user: + otheracademic_notif( + actor_user, + student_user, + "ast_ta_accept", + form.id, + "admin", + "Your assistantship form has been verified by Faculty Supervisor.", + ) + for dept_admin_user in dept_admin_users: + otheracademic_notif( + actor_user, + dept_admin_user, + "ast_hod", + form.id, + "admin", + "A verified assistantship form is waiting for Department Admin approval.", + ) + if rejected_ids: + for form_id in rejected_ids: + form = selectors.get_assistantship_by_id(form_id) + if not form: + continue + if form.ta_supervisor.lower() != actor_user.username.lower(): + raise AssistantshipServiceError("You can only review forms assigned to you.") + if form.TA_approved or form.TA_rejected: + raise AssistantshipServiceError("This assistantship form is already reviewed.") + + form.TA_approved = False + form.TA_rejected = True + form.save(update_fields=["TA_approved", "TA_rejected"]) + + student_user = selectors.get_user_by_extrainfo_id(form.roll_no_id) + if student_user: + otheracademic_notif( + actor_user, + student_user, + "ast_ta_accept", + form.id, + "admin", + "Your assistantship form has been rejected by Faculty Supervisor.", + ) + + +def update_assistantship_status_thesis(approved_ids, rejected_ids): + """Update assistantship status by Thesis supervisor.""" + if approved_ids: + AssistantshipClaimFormStatusUpd.objects.filter(id__in=approved_ids).update(Ths_approved=True) + if rejected_ids: + AssistantshipClaimFormStatusUpd.objects.filter(id__in=rejected_ids).update(Ths_rejected=True) + + +def update_assistantship_status_hod(approved_ids, rejected_ids, actor_user): + """Update assistantship status by Department Admin (stage 2 verification).""" + if approved_ids: + for form_id in approved_ids: + form = selectors.get_assistantship_by_id(form_id) + if not form: + continue + if not form.TA_approved or form.TA_rejected: + raise AssistantshipServiceError("Department Admin can act only after Faculty Supervisor verification.") + if form.HOD_approved or form.HOD_rejected: + raise AssistantshipServiceError("This assistantship form is already reviewed by Department Admin.") + + form.HOD_approved = True + form.HOD_rejected = False + form.save(update_fields=["HOD_approved", "HOD_rejected", "remark"]) + + student_user = selectors.get_user_by_extrainfo_id(form.roll_no_id) + hod_users = selectors.get_users_for_designation_contains("hod") + if student_user: + otheracademic_notif( + actor_user, + student_user, + "ast_ta_accept", + form.id, + "admin", + "Your assistantship has been verified by Department Admin and forwarded to HOD.", + ) + for hod_user in hod_users: + if hod_user.id == actor_user.id: + continue + otheracademic_notif( + actor_user, + hod_user, + "ast_hod", + form.id, + "admin", + "A verified assistantship form is waiting for HOD approval.", + ) + if rejected_ids: + for form_id in rejected_ids: + form = selectors.get_assistantship_by_id(form_id) + if not form: + continue + if not form.TA_approved or form.TA_rejected: + raise AssistantshipServiceError("Department Admin can act only after Faculty Supervisor verification.") + if form.HOD_approved or form.HOD_rejected: + raise AssistantshipServiceError("This assistantship form is already reviewed by Department Admin.") + + form.HOD_approved = False + form.HOD_rejected = True + form.remark = "Rejected by Department Admin" + form.save(update_fields=["HOD_approved", "HOD_rejected", "remark"]) + + student_user = selectors.get_user_by_extrainfo_id(form.roll_no_id) + if student_user: + otheracademic_notif( + actor_user, + student_user, + "ast_ta_accept", + form.id, + "admin", + "Your assistantship has been rejected by Department Admin.", + ) + + +def withdraw_assistantship(user, form_id): + """Allow PG student to withdraw assistantship before faculty review.""" + form = selectors.get_assistantship_by_id(form_id) + if not form: + raise AssistantshipServiceError("Assistantship form not found.") + if form.roll_no_id != user.extrainfo.id: + raise AssistantshipServiceError("You are not authorized to withdraw this assistantship form.") + if form.TA_approved or form.TA_rejected: + raise AssistantshipServiceError("Cannot withdraw after faculty supervisor has reviewed the form.") + + supervisor_user = selectors.get_user_by_username(form.ta_supervisor) + if supervisor_user: + otheracademic_notif( + user, + supervisor_user, + "ast_ta", + form.id, + "student", + "A PG assistantship form was withdrawn by the student.", + ) + form.delete() + + +def update_assistantship_status_acad_admin(approved_ids, rejected_ids, actor_user): + """Update assistantship status by Academic Admin (stage 5 disbursement audit).""" + if approved_ids: + for form_id in approved_ids: + form = selectors.get_assistantship_by_id(form_id) + if not form: + continue + if not form.Acad_approved or form.Acad_rejected: + raise AssistantshipServiceError( + "Academic Admin can disburse only after HOD approval." + ) + if form.remark == "Stipend disbursed (audit completed)": + raise AssistantshipServiceError("This assistantship form is already marked as disbursed.") + + form.remark = "Stipend disbursed (audit completed)" + form.save(update_fields=["remark"]) + + student_user = selectors.get_user_by_extrainfo_id(form.roll_no_id) + if student_user and actor_user: + otheracademic_notif( + actor_user, + student_user, + "ast_ta_accept", + form.id, + "admin", + "Your assistantship stipend has been marked as disbursed by Academic Admin.", + ) + + if rejected_ids: + for form_id in rejected_ids: + form = selectors.get_assistantship_by_id(form_id) + if not form: + continue + if not form.Acad_approved or form.Acad_rejected: + raise AssistantshipServiceError( + "Academic Admin can act only after HOD approval." + ) + if form.remark == "Stipend disbursed (audit completed)": + raise AssistantshipServiceError("Cannot reject after stipend is marked disbursed.") + + form.remark = "Disbursement held by Academic Admin" + form.save(update_fields=["remark"]) + + student_user = selectors.get_user_by_extrainfo_id(form.roll_no_id) + if student_user and actor_user: + otheracademic_notif( + actor_user, + student_user, + "ast_ta_accept", + form.id, + "admin", + "Your assistantship disbursement has been put on hold by Academic Admin.", + ) + + +def update_assistantship_status_dean(approved_ids, rejected_ids, actor_user): + """Update assistantship status by HOD (stage 4 final approval/rejection).""" + if approved_ids: + for form_id in approved_ids: + form = selectors.get_assistantship_by_id(form_id) + if not form: + continue + if not form.HOD_approved or form.HOD_rejected: + raise AssistantshipServiceError("HOD can act only after Department Admin verification.") + if form.Acad_approved or form.Acad_rejected: + raise AssistantshipServiceError("This assistantship form is already reviewed by HOD.") + + form.Acad_approved = True + form.Acad_rejected = False + form.remark = "Approved by HOD" + form.save(update_fields=["Acad_approved", "Acad_rejected", "remark"]) + + student_user = selectors.get_user_by_extrainfo_id(form.roll_no_id) + acad_admin_user = selectors.get_first_user_for_designation("acadadmin") + if student_user and actor_user: + otheracademic_notif( + actor_user, + student_user, + "ast_ta_accept", + form.id, + "admin", + "Your assistantship has been approved by HOD and forwarded to Academic Admin for disbursement audit.", + ) + if acad_admin_user and actor_user: + otheracademic_notif( + actor_user, + acad_admin_user, + "ast_hod", + form.id, + "admin", + "An HOD-approved assistantship form is waiting for disbursement audit.", + ) + + if rejected_ids: + for form_id in rejected_ids: + form = selectors.get_assistantship_by_id(form_id) + if not form: + continue + if not form.HOD_approved or form.HOD_rejected: + raise AssistantshipServiceError("HOD can act only after Department Admin verification.") + if form.Acad_approved or form.Acad_rejected: + raise AssistantshipServiceError("This assistantship form is already reviewed by HOD.") + + form.Acad_approved = False + form.Acad_rejected = True + form.remark = "Rejected by HOD" + form.save(update_fields=["Acad_approved", "Acad_rejected", "remark"]) + + student_user = selectors.get_user_by_extrainfo_id(form.roll_no_id) + if student_user and actor_user: + otheracademic_notif( + actor_user, + student_user, + "ast_ta_accept", + form.id, + "admin", + "Your assistantship has been rejected by HOD.", + ) + + +def update_assistantship_status_director(approved_ids, rejected_ids): + """Update assistantship status by Director.""" + return None + + +def get_assistantship_status_text(form): + """ + Determine the overall status text for an assistantship form. + Returns 'Rejected', 'Approved', or 'Pending'. + """ + is_rejected = any([ + form.TA_rejected, + form.HOD_rejected, + form.Acad_rejected, + ]) + + if is_rejected: + return "Rejected" + elif form.remark == "Stipend disbursed (audit completed)": + return "Approved" + else: + return "Pending" + + +def get_assistantship_approval_stages(form): + """Get approval status for each stage of the PG assistantship workflow.""" + stages = { + "Faculty_Supervisor": ("TA_approved", "TA_rejected"), + "Department_Admin": ("HOD_approved", "HOD_rejected"), + "HOD": ("Acad_approved", "Acad_rejected"), + } + + result = {} + for stage_name, (approved_field, rejected_field) in stages.items(): + if getattr(form, approved_field): + result[stage_name] = "Approved" + elif getattr(form, rejected_field): + result[stage_name] = "Rejected" + else: + result[stage_name] = "Pending" + + if form.remark == "Stipend disbursed (audit completed)": + result["Acad_Admin_Audit"] = "Disbursed" + elif form.Acad_approved and not form.Acad_rejected: + result["Acad_Admin_Audit"] = "Pending" + elif form.Acad_rejected: + result["Acad_Admin_Audit"] = "On Hold" + else: + result["Acad_Admin_Audit"] = "Pending" + + return result + + +# ==================== PG TA ASSIGNMENT SERVICES ==================== + +def get_pg_ta_assignment_options(): + """Return PG students, subject options, and existing TA assignments.""" + students = selectors.get_pg_students_for_ta_assignment() + subjects = selectors.get_subject_options_for_ta_assignment() + assignments = selectors.get_all_pg_ta_assignments() + + assignment_map = {} + for row in assignments: + assignment_map.setdefault(row.pg_student_id, []).append(row) + + student_rows = [] + for student in students: + existing_assignments = assignment_map.get(student.id_id, []) + full_name = f"{student.id.user.first_name} {student.id.user.last_name}".strip() or student.id.user.username + student_rows.append({ + "roll_no": student.id_id, + "name": full_name, + "programme": student.programme, + "assigned_subject_ids": [row.subject_id for row in existing_assignments], + "assigned_subjects": [ + { + "id": row.subject_id, + "code": row.subject.code, + "name": row.subject.name, + "label": f"{row.subject.code} - {row.subject.name}", + } + for row in existing_assignments + ], + }) + + subject_rows = [ + { + "id": subject.id, + "code": subject.code, + "name": subject.name, + "label": f"{subject.code} - {subject.name}", + } + for subject in subjects + ] + + return { + "students": student_rows, + "subjects": subject_rows, + } + + +def upsert_pg_ta_assignments(assignments, actor_user): + """Create, update, or remove TA assignments for PG students.""" + if not isinstance(assignments, list) or not assignments: + raise TAAssignmentServiceError("At least one assignment is required.") + + updated_count = 0 + desired_subjects_by_student = {} + + for item in assignments: + roll_no = item.get("roll_no") + subject_ids = item.get("subject_ids") + + if not roll_no: + raise TAAssignmentServiceError("Each assignment must include roll_no.") + + if subject_ids is None: + subject_id = item.get("subject_id") + subject_ids = [subject_id] if subject_id else [] + + if not isinstance(subject_ids, list): + raise TAAssignmentServiceError("subject_ids must be a list of subject ids.") + + clean_subject_ids = [int(subject_id) for subject_id in subject_ids if subject_id] + desired_subjects_by_student.setdefault(str(roll_no), set()).update(clean_subject_ids) + + with transaction.atomic(): + for roll_no, desired_subject_ids in desired_subjects_by_student.items(): + student_user = selectors.get_user_by_username(str(roll_no)) + if not student_user: + raise TAAssignmentServiceError(f"Student '{roll_no}' not found.") + + student = selectors.get_pg_students_for_ta_assignment().filter(id=student_user.extrainfo).first() + if not student: + raise TAAssignmentServiceError(f"Student '{roll_no}' is not a PG student.") + + existing_assignments = { + row.subject_id: row + for row in PGTAAssignment.objects.filter(pg_student=student_user.extrainfo) + } + + subjects_qs = selectors.get_subject_options_for_ta_assignment().filter( + id__in=desired_subject_ids + ) + found_subjects = {subject.id: subject for subject in subjects_qs} + missing_subject_ids = desired_subject_ids - set(found_subjects) + if missing_subject_ids: + missing_list = ", ".join(str(subject_id) for subject_id in sorted(missing_subject_ids)) + raise TAAssignmentServiceError(f"Subject id(s) '{missing_list}' not found.") + + for subject_id in desired_subject_ids: + subject = found_subjects[subject_id] + if subject_id not in existing_assignments: + PGTAAssignment.objects.create( + pg_student=student_user.extrainfo, + subject=subject, + assigned_by=actor_user, + ) + PGTAAssignmentHistory.objects.create( + pg_student=student_user.extrainfo, + subject=subject, + assigned_by=actor_user, + ) + updated_count += 1 + + for subject_id, assignment in existing_assignments.items(): + if subject_id not in desired_subjects_by_student[roll_no]: + assignment.delete() + updated_count += 1 + + return updated_count + + +def get_pg_faculty_supervisor_assignment_options(): + """Return PG students, faculty options, and existing faculty supervisor assignments.""" + students = selectors.get_pg_students_for_assignment() + faculties = selectors.get_faculty_members_for_supervisor_assignment() + assignments = selectors.get_all_pg_faculty_supervisor_assignments() + + assignment_map = {row.pg_student_id: row for row in assignments} + + student_rows = [] + for student in students: + existing = assignment_map.get(student.id_id) + full_name = f"{student.id.user.first_name} {student.id.user.last_name}".strip() or student.id.user.username + student_rows.append({ + "roll_no": student.id_id, + "name": full_name, + "programme": student.programme, + "assigned_faculty_id": existing.faculty_supervisor_id if existing else None, + "assigned_faculty": ( + existing.faculty_supervisor.get_full_name().strip() or existing.faculty_supervisor.username + ) if existing else None, + }) + + faculty_rows = [] + for faculty in faculties: + user = faculty.id.user + label_name = user.get_full_name().strip() or user.username + faculty_rows.append( + { + "id": user.id, + "username": user.username, + "name": label_name, + "label": f"{label_name} ({user.username})", + } + ) + + return { + "students": student_rows, + "faculties": faculty_rows, + } + + +def upsert_pg_faculty_supervisor_assignments(assignments, actor_user): + """Create or update faculty supervisor assignments for PG students.""" + if not isinstance(assignments, list) or not assignments: + raise TAAssignmentServiceError("At least one assignment is required.") + + valid_faculty_user_ids = set( + selectors.get_faculty_members_for_supervisor_assignment().values_list("id__user_id", flat=True) + ) + + designation, _ = Designation.objects.get_or_create( + name="faculty_supervisor", + defaults={"full_name": "Faculty Supervisor", "type": "academic"}, + ) + + updated_count = 0 + with transaction.atomic(): + for item in assignments: + roll_no = item.get("roll_no") + faculty_user_id = item.get("faculty_user_id") + + if not roll_no or not faculty_user_id: + raise TAAssignmentServiceError("Each assignment must include roll_no and faculty_user_id.") + + student_user = selectors.get_user_by_username(str(roll_no)) + if not student_user: + raise TAAssignmentServiceError(f"Student '{roll_no}' not found.") + + student = selectors.get_pg_students_for_assignment().filter(id=student_user.extrainfo).first() + if not student: + raise TAAssignmentServiceError(f"Student '{roll_no}' is not a valid PG student for assignment.") + + try: + faculty_user_id = int(faculty_user_id) + except (TypeError, ValueError): + raise TAAssignmentServiceError("Invalid faculty_user_id.") + + if faculty_user_id not in valid_faculty_user_ids: + raise TAAssignmentServiceError(f"Faculty user id '{faculty_user_id}' is not valid.") + + faculty_user = User.objects.filter(id=faculty_user_id).first() + if not faculty_user: + raise TAAssignmentServiceError(f"Faculty user id '{faculty_user_id}' not found.") + + # BR-52: restrict to faculty from relevant student department when both are available. + student_department_id = getattr(student_user.extrainfo, "department_id", None) + faculty_department_id = getattr(faculty_user.extrainfo, "department_id", None) + if student_department_id and faculty_department_id and student_department_id != faculty_department_id: + raise TAAssignmentServiceError( + "Faculty Supervisor must belong to the student's department." + ) + + PGFacultySupervisorAssignment.objects.update_or_create( + pg_student=student_user.extrainfo, + defaults={ + "faculty_supervisor": faculty_user, + "assigned_by": actor_user, + }, + ) + + PGFacultySupervisorAssignmentHistory.objects.create( + pg_student=student_user.extrainfo, + faculty_supervisor=faculty_user, + assigned_by=actor_user, + ) + + HoldsDesignation.objects.get_or_create( + user=faculty_user, + working=faculty_user, + designation=designation, + ) + updated_count += 1 + + return updated_count diff --git a/FusionIIIT/applications/otheracademic/tempCodeRunnerFile.py b/FusionIIIT/applications/otheracademic/tempCodeRunnerFile.py new file mode 100644 index 000000000..fe0c61d98 --- /dev/null +++ b/FusionIIIT/applications/otheracademic/tempCodeRunnerFile.py @@ -0,0 +1,124 @@ + +from applications.otheracademic.models import ( + LeaveFormTable, + LeavePG, + BonafideFormTableUpdated, + AssistantshipClaimFormStatusUpd, + NoDues, + LeaveStatusChoices, +) +from applications.globals.models import ExtraInfo, HoldsDesignation, Designation + + +# ==================== USER/DESIGNATION SELECTORS ==================== + +def get_user_by_username(username): + """Get a user by username, returns None if not found.""" + try: + return User.objects.get(username=username) + except User.DoesNotExist: + return None + + +def get_user_by_extrainfo_id(extrainfo_id): + """Get a user by their extrainfo ID.""" + try: + return User.objects.get(extrainfo=extrainfo_id) + except User.DoesNotExist: + return None + + +def get_user_by_extrainfo(extrainfo): + """Get a user by their extrainfo object.""" + try: + return User.objects.get(extrainfo=extrainfo) + except User.DoesNotExist: + return None + + +def get_first_designation_for_user(user): + """Get the first designation for a user.""" + designations = HoldsDesignation.objects.filter(user=user) + if designations.exists(): + return designations.first().designation + return None + + +def get_first_user_for_designation(designation_name): + """ + Get the first user who holds a specific designation. + Used for routing notifications to admin roles. + """ + try: + designation = Designation.objects.get(name=designation_name) + user_ids = HoldsDesignation.objects.filter( + designation_id=designation.id + ).values_list('user_id', flat=True) + + if user_ids.exists(): + return User.objects.get(id=user_ids[0]) + except (Designation.DoesNotExist, User.DoesNotExist): + pass + return None + + +# ==================== LEAVE SELECTORS ==================== + +def get_pending_ug_leaves(): + """Get all pending UG leave requests.""" + return LeaveFormTable.objects.filter(status=LeaveStatusChoices.PENDING) + + +def get_pending_pg_leaves_for_ta(): + """Get all pending PG leave requests (for TA approval).""" + return LeavePG.objects.filter(status=LeaveStatusChoices.PENDING) + + +def get_pending_pg_leaves_for_thesis(): + """Get PG leave requests pending thesis supervisor approval.""" + return LeavePG.objects.filter(status=F('ta_supervisor')) + + +def get_pending_pg_leaves_for_hod(): + """Get PG leave requests pending HOD approval.""" + return LeavePG.objects.filter(status=F('thesis_supervisor')) + + +def get_ug_leaves_by_roll_no(roll_no_id): + """Get all UG leave requests for a specific roll number.""" + return LeaveFormTable.objects.filter(roll_no=roll_no_id) + + +def get_pg_leaves_by_roll_no(roll_no_id): + """Get all PG leave requests for a specific roll number.""" + return LeavePG.objects.filter(roll_no=roll_no_id) + + +def get_leave_by_id(leave_id, is_pg=False): + """Get a leave request by ID.""" + model = LeavePG if is_pg else LeaveFormTable + try: + return model.objects.get(id=leave_id) + except model.DoesNotExist: + return None + + +def serialize_ug_leave(leave): + """Serialize a UG leave request to dictionary format.""" + return { + "id": leave.id, + "rollNo": leave.roll_no.id, + "name": leave.student_name, + "form": leave.upload_file.url if leave.upload_file else None, + "details": { + "dateFrom": leave.date_from, + "dateTo": leave.date_to, + "leaveType": leave.leave_type, + "address": leave.address, + "purpose": leave.purpose, + "hodCredential": leave.hod, + "mobileNumber": leave.stud_mobile_no, + "parentsMobile": leave.parent_mobile_no, + "mobileDuringLeave": leave.leave_mobile_no, + "semester": leave.curr_sem, + "academicYear": leave.date_of_application.year, \ No newline at end of file diff --git a/FusionIIIT/applications/otheracademic/tests/__init__.py b/FusionIIIT/applications/otheracademic/tests/__init__.py new file mode 100644 index 000000000..67fd17363 --- /dev/null +++ b/FusionIIIT/applications/otheracademic/tests/__init__.py @@ -0,0 +1 @@ +# Tests package for otheracademic module diff --git a/FusionIIIT/applications/otheracademic/tests/test_api.py b/FusionIIIT/applications/otheracademic/tests/test_api.py new file mode 100644 index 000000000..57d3a7250 --- /dev/null +++ b/FusionIIIT/applications/otheracademic/tests/test_api.py @@ -0,0 +1,118 @@ +""" +Tests for otheracademic API endpoints. +""" +from django.test import TestCase +from django.contrib.auth.models import User +from rest_framework.test import APIClient +from rest_framework import status +from unittest.mock import patch, MagicMock + + +class LeaveAPITestCase(TestCase): + """Tests for leave API endpoints.""" + + def setUp(self): + self.client = APIClient() + self.user = User.objects.create_user( + username='testuser', + password='testpass', + first_name='Test', + last_name='User' + ) + + def test_leave_form_submit_requires_auth(self): + """Test that leave form submission requires authentication.""" + response = self.client.post('/otheracademic/api/leave-form-submit/') + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + + def test_fetch_pending_leaves_requires_auth(self): + """Test that fetching pending leaves requires authentication.""" + response = self.client.get('/otheracademic/api/fetch-pending-leaves/') + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + + def test_get_leave_requests_requires_auth(self): + """Test that getting leave requests requires authentication.""" + response = self.client.get('/otheracademic/api/get-leave-requests/') + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + + +class BonafideAPITestCase(TestCase): + """Tests for bonafide API endpoints.""" + + def setUp(self): + self.client = APIClient() + self.user = User.objects.create_user( + username='testuser', + password='testpass', + first_name='Test', + last_name='User' + ) + + def test_bonafide_form_submit_requires_auth(self): + """Test that bonafide form submission requires authentication.""" + response = self.client.post('/otheracademic/api/bonafide-form-submit/') + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + + def test_fetch_pending_bonafides_requires_auth(self): + """Test that fetching pending bonafides requires authentication.""" + response = self.client.get('/otheracademic/api/admin-bonafide-requests/') + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + + +class AssistantshipAPITestCase(TestCase): + """Tests for assistantship API endpoints.""" + + def setUp(self): + self.client = APIClient() + self.user = User.objects.create_user( + username='testuser', + password='testpass', + first_name='Test', + last_name='User' + ) + + def test_assistantship_form_submit_requires_auth(self): + """Test that assistantship form submission requires authentication.""" + response = self.client.post('/otheracademic/api/assistantship-form-submit/') + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + + def test_ta_supervisor_pending_requires_auth(self): + """Test that TA supervisor pending requests requires authentication.""" + response = self.client.get('/otheracademic/api/TA-supervisor-pending-requests/') + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + + def test_hod_pending_requires_auth(self): + """Test that HOD pending requests requires authentication.""" + response = self.client.get('/otheracademic/api/deptadmin-pending-requests/') + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + + def test_acad_admin_pending_requires_auth(self): + """Test that Academic Admin pending requests requires authentication.""" + response = self.client.get('/otheracademic/api/acadadmin-pending-requests/') + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + + def test_ta_assignment_options_requires_auth(self): + """Test that TA assignment options requires authentication.""" + response = self.client.get('/otheracademic/api/ta-assignment-options/') + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + + def test_faculty_supervisor_assignment_options_requires_auth(self): + """Test that faculty supervisor assignment options requires authentication.""" + response = self.client.get('/otheracademic/api/faculty-supervisor-assignment-options/') + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + + def test_ta_assignment_options_forbidden_without_dept_admin_role(self): + """Authenticated non-dept-admin user should be forbidden.""" + self.client.force_authenticate(user=self.user) + response = self.client.get('/otheracademic/api/ta-assignment-options/') + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_faculty_supervisor_assignment_update_forbidden_without_dept_admin_role(self): + """Authenticated non-dept-admin user should be forbidden.""" + self.client.force_authenticate(user=self.user) + response = self.client.post( + '/otheracademic/api/faculty-supervisor-assignment-update/', + {'assignments': [{'roll_no': '23MCS111', 'faculty_user_id': 1}]}, + format='json', + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) diff --git a/FusionIIIT/applications/otheracademic/tests/test_selectors.py b/FusionIIIT/applications/otheracademic/tests/test_selectors.py new file mode 100644 index 000000000..70c6052b6 --- /dev/null +++ b/FusionIIIT/applications/otheracademic/tests/test_selectors.py @@ -0,0 +1,108 @@ +""" +Tests for otheracademic selectors. +""" +from django.test import TestCase +from django.contrib.auth.models import User +from unittest.mock import patch, MagicMock + +from applications.otheracademic import selectors +from applications.otheracademic.models import ( + LeaveFormTable, + LeavePG, + BonafideFormTableUpdated, + AssistantshipClaimFormStatusUpd, + LeaveStatusChoices, +) +from applications.globals.models import ExtraInfo, DepartmentInfo +from applications.academic_information.models import Student + + +class UserSelectorsTestCase(TestCase): + """Tests for user-related selectors.""" + + def test_get_user_by_username_exists(self): + """Test getting a user that exists.""" + user = User.objects.create_user(username='testuser', password='testpass') + result = selectors.get_user_by_username('testuser') + self.assertEqual(result, user) + + def test_get_user_by_username_not_exists(self): + """Test getting a user that doesn't exist returns None.""" + result = selectors.get_user_by_username('nonexistent') + self.assertIsNone(result) + + +class LeaveSelectorsTestCase(TestCase): + """Tests for leave-related selectors.""" + + def test_get_pending_ug_leaves_empty(self): + """Test getting pending UG leaves when none exist.""" + result = selectors.get_pending_ug_leaves() + self.assertEqual(list(result), []) + + def test_get_pending_pg_leaves_for_ta_empty(self): + """Test getting pending PG leaves for TA when none exist.""" + result = selectors.get_pending_pg_leaves_for_ta() + self.assertEqual(list(result), []) + + +class BonafideSelectorsTestCase(TestCase): + """Tests for bonafide-related selectors.""" + + def test_get_pending_bonafides_empty(self): + """Test getting pending bonafides when none exist.""" + result = selectors.get_pending_bonafides() + self.assertEqual(list(result), []) + + +class AssistantshipSelectorsTestCase(TestCase): + """Tests for assistantship-related selectors.""" + + def test_get_pending_assistantships_for_ta_empty(self): + """Test getting pending assistantships for TA when none exist.""" + result = selectors.get_pending_assistantships_for_ta() + self.assertEqual(list(result), []) + + def test_get_pending_assistantships_for_hod_empty(self): + """Test getting pending assistantships for HOD when none exist.""" + result = selectors.get_pending_assistantships_for_hod() + self.assertEqual(list(result), []) + + +class AssignmentSelectorsTestCase(TestCase): + """Tests for assignment-related selectors.""" + + def test_get_pg_students_for_assignment_excludes_23bcs229(self): + dept = DepartmentInfo.objects.create(name="CSE") + + excluded_user = User.objects.create_user(username='23BCS229', password='x') + excluded_extrainfo = ExtraInfo.objects.create( + id='23BCS229', + user=excluded_user, + user_type='student', + department=dept, + ) + Student.objects.create( + id=excluded_extrainfo, + programme='M.Tech', + category='GEN', + ) + + included_user = User.objects.create_user(username='23MCS999', password='x') + included_extrainfo = ExtraInfo.objects.create( + id='23MCS999', + user=included_user, + user_type='student', + department=dept, + ) + Student.objects.create( + id=included_extrainfo, + programme='M.Tech', + category='GEN', + ) + + students = selectors.get_pg_students_for_assignment() + ids = list(students.values_list('id_id', flat=True)) + + self.assertIn('23MCS999', ids) + self.assertNotIn('23BCS229', ids) diff --git a/FusionIIIT/applications/otheracademic/tests/test_services.py b/FusionIIIT/applications/otheracademic/tests/test_services.py new file mode 100644 index 000000000..c08c28b8d --- /dev/null +++ b/FusionIIIT/applications/otheracademic/tests/test_services.py @@ -0,0 +1,186 @@ +""" +Tests for otheracademic services. +""" +from django.test import TestCase +from unittest.mock import patch, MagicMock +from datetime import date + +from applications.otheracademic import services + + +class LeaveServicesTestCase(TestCase): + """Tests for leave-related services.""" + + @patch('applications.otheracademic.services.selectors.get_user_by_username') + def test_submit_ug_leave_invalid_hod(self, mock_get_user): + """Test that submitting leave with invalid HOD raises error.""" + mock_get_user.return_value = None + user = MagicMock() + user.first_name = 'Test' + user.last_name = 'User' + + with self.assertRaises(services.LeaveServiceError) as context: + services.submit_ug_leave( + user=user, + date_from=date.today(), + date_to=date.today(), + leave_type='Casual', + address='Test Address', + purpose='Test Purpose', + hod_credential='invalid_hod', + semester='1', + ) + self.assertIn('not found', str(context.exception)) + + +class BonafideServicesTestCase(TestCase): + """Tests for bonafide-related services.""" + + @patch('applications.otheracademic.services.selectors.get_first_user_for_designation') + @patch('applications.otheracademic.services.otheracademic_notif') + def test_submit_bonafide_creates_record(self, mock_notif, mock_get_admin): + """Test that submitting bonafide creates a record.""" + mock_get_admin.return_value = None # No admin to notify + + user = MagicMock() + user.first_name = 'Test' + user.last_name = 'User' + user.extrainfo = MagicMock() + + result = services.submit_bonafide( + user=user, + branch='CSE', + semester='3', + purpose='Test Purpose', + ) + + self.assertIsNotNone(result) + self.assertEqual(result.branch_types, 'CSE') + self.assertEqual(result.semester_types, '3') + + +class AssistantshipServicesTestCase(TestCase): + """Tests for assistantship-related services.""" + + def test_get_assistantship_status_text_rejected(self): + """Test status text when rejected at any stage.""" + form = MagicMock() + form.TA_rejected = False + form.HOD_rejected = False + form.Acad_rejected = True + form.remark = "" + + result = services.get_assistantship_status_text(form) + self.assertEqual(result, "Rejected") + + def test_get_assistantship_status_text_approved(self): + """Test status text when fully approved.""" + form = MagicMock() + form.TA_rejected = False + form.HOD_rejected = False + form.Acad_rejected = False + form.remark = "Stipend disbursed (audit completed)" + + result = services.get_assistantship_status_text(form) + self.assertEqual(result, "Approved") + + def test_get_assistantship_status_text_pending(self): + """Test status text when pending.""" + form = MagicMock() + form.TA_rejected = False + form.HOD_rejected = False + form.Acad_rejected = False + form.remark = "" + + result = services.get_assistantship_status_text(form) + self.assertEqual(result, "Pending") + + def test_get_assistantship_approval_stages(self): + """Test getting approval stages.""" + form = MagicMock() + form.TA_approved = True + form.TA_rejected = False + form.HOD_approved = True + form.HOD_rejected = False + form.Acad_approved = True + form.Acad_rejected = False + form.remark = "Stipend disbursed (audit completed)" + + result = services.get_assistantship_approval_stages(form) + + self.assertEqual(result['Faculty_Supervisor'], 'Approved') + self.assertEqual(result['Department_Admin'], 'Approved') + self.assertEqual(result['HOD'], 'Approved') + self.assertEqual(result['Acad_Admin_Audit'], 'Disbursed') + + @patch('applications.otheracademic.services.otheracademic_notif') + @patch('applications.otheracademic.services.AssistantshipClaimFormStatusUpd.objects.create') + @patch('applications.otheracademic.services.selectors.get_first_user_for_designation') + @patch('applications.otheracademic.services.selectors.assistantship_exists_for_period') + @patch('applications.otheracademic.services.selectors.get_pg_faculty_supervisor_assignment_for_student') + def test_submit_assistantship_uses_assigned_supervisor( + self, + mock_get_assignment, + mock_exists, + mock_get_dept_admin, + mock_create, + mock_notif, + ): + """Submission should use configured faculty supervisor assignment.""" + student_user = MagicMock() + student_user.extrainfo.id = "23MCS111" + student_user.first_name = "PG" + student_user.last_name = "Student" + + assigned_supervisor = MagicMock() + assigned_supervisor.username = "fac1" + mock_get_assignment.return_value = MagicMock(faculty_supervisor=assigned_supervisor) + mock_exists.return_value = False + mock_get_dept_admin.return_value = MagicMock(username="dept1") + mock_create.return_value = MagicMock(id=101) + + services.submit_assistantship( + user=student_user, + discipline="CSE", + date_from=date(2026, 4, 1), + date_to=date(2026, 4, 30), + date_applied=date(2026, 4, 1), + bank_account="123", + signature_file="sig.jpg", + ta_supervisor="fac1", + thesis_supervisor="", + hod="", + applicability="monthly", + ) + + self.assertTrue(mock_create.called) + kwargs = mock_create.call_args.kwargs + self.assertEqual(kwargs["ta_supervisor"], "fac1") + + @patch('applications.otheracademic.services.selectors.get_pg_faculty_supervisor_assignment_for_student') + @patch('applications.otheracademic.services.selectors.assistantship_exists_for_period') + def test_submit_assistantship_fails_without_supervisor_assignment( + self, + mock_exists, + mock_get_assignment, + ): + """Submission should fail when no supervisor is assigned and no valid fallback is provided.""" + student_user = MagicMock() + student_user.extrainfo.id = "23MCS111" + mock_exists.return_value = False + mock_get_assignment.return_value = None + + with self.assertRaises(services.AssistantshipServiceError): + services.submit_assistantship( + user=student_user, + discipline="CSE", + date_from=date(2026, 4, 1), + date_to=date(2026, 4, 30), + date_applied=date(2026, 4, 1), + bank_account="123", + signature_file="sig.jpg", + ta_supervisor="", + thesis_supervisor="", + hod="", + applicability="monthly", + ) diff --git a/FusionIIIT/applications/otheracademic/views.py b/FusionIIIT/applications/otheracademic/views.py index 4f4f8d8ed..1a5e1d51b 100644 --- a/FusionIIIT/applications/otheracademic/views.py +++ b/FusionIIIT/applications/otheracademic/views.py @@ -71,11 +71,11 @@ def leave_form_submit(request): data = request.POST file = request.FILES.get('related_document') hodname = data.get('hod_credential') - print(data.get('mobile_number'),data.get('parents_mobile'),"hello ab") + student_name = (request.user.get_full_name() or "").strip() or request.user.username # Create a new LeaveFormTable instance and save it to the database leave = LeaveFormTable.objects.create( - student_name=request.user.first_name+request.user.last_name, + student_name=student_name, roll_no=request.user.extrainfo, date_from=data.get('date_from'), date_to=data.get('date_to'), @@ -89,10 +89,9 @@ def leave_form_submit(request): stud_mobile_no=data.get('mobile_number'), parent_mobile_no=data.get('parents_mobile'), leave_mobile_no=data.get('mobile_during_leave'), - curr_sem=int(data.get('semester')), + curr_sem=int(data.get('semester')) if data.get('semester') else None, hod=data.get('hod_credential') ) - print(data.get('mobile_number'),data.get('parents_mobile')) leave_hod = User.objects.get(username=hodname) receiver_value = User.objects.get(username=request.user.username) @@ -687,7 +686,7 @@ def bonafide_form_submit(request): date_of_applications = data.get('date_of_application'), approve=False, # Initially not approved reject=False, # Initially not rejected - download_file = "not available", + download_file = file, ) messages.success(request,'form submitted successfully') bonafide.save() @@ -722,7 +721,7 @@ def approve_bonafide(request, leave_id): leave_entry = BonafideFormTableUpdated.objects.get(id=leave_id) leave_entry.approve = True leave_entry.save() - bonafide_aceptor = User.objects.get(username=leave_entry.roll_nos_id) + bonafide_aceptor = leave_entry.roll_nos.user message='A Bonafide uploaded' otheracademic_notif(request.user,bonafide_aceptor, 'bonafide_accept', 1, 'student', message) return redirect('/otheracademic/bonafideApproveForm') # Redirect to appropriate page after approval @@ -734,7 +733,7 @@ def reject_bonafide(request, leave_id): leave_entry = BonafideFormTableUpdated.objects.get(id=leave_id) leave_entry.reject = True leave_entry.save() - bonafide_aceptor = User.objects.get(username=leave_entry.roll_no_id) + bonafide_aceptor = leave_entry.roll_nos.user message='A Bonafide rejected' otheracademic_notif(request.user,bonafide_aceptor, 'bonafide_accept', 1, 'student', message) return redirect('/otheracademic/bonafideApproveForm') # Redirect to appropriate page after rejection diff --git a/FusionIIIT/notification/views.py b/FusionIIIT/notification/views.py index ca732a7b7..cd0e18b69 100644 --- a/FusionIIIT/notification/views.py +++ b/FusionIIIT/notification/views.py @@ -500,71 +500,103 @@ def course_management_notif(sender, recipient, type, course, course_name, cours notify.send(sender=sender, recipient=recipient, url=url, module=module, verb=verb, flag=flag, course_code=course_code, course=course, cname = course_name) -def otheracademic_notif(sender, recipient, type, otheracademic_id,student,message): - if(type=='ug_leave_hod'): - url = ('otheracademic:otheracademic') - elif type=='pg_leave_ta' : - url = ('otheracademic:leaveApproveTA') - elif type=='pg_leave_hod' : - url = ('otheracademic:otheracademic') - elif type=='ast_ta' : - url = ('otheracademic:assistantship_form_approval') - elif type=='ast_thesis' : - url = ('otheracademic:assistantship_thesis') - - elif type=='ast_acadadmin' : - url = ('otheracademic:assistantship_acad_approveform') - elif type=='ast_hod' : - url = ('otheracademic:assistantship_hod') - elif type=='hostel_nodues' : - url = ('otheracademic:hostel_nodues') - elif type=='bank_nodues' : - url = ('otheracademic:Bank_nodues') - elif type=='btp_nodues' : - url = ('otheracademic:BTP_nodues') - elif type=='cse_nodues' : - url = ('otheracademic:CSE_nodues') - elif type=='design_nodues' : - url = ('otheracademic:Design_nodues') - elif type=='acad_nodues' : - url = ('otheracademic:dsa_nodues') - elif type=='ece_nodues' : - url = ('otheracademic:Ece_nodues') - elif type=='library_nodues' : - url = ('otheracademic:library_nodues') - elif type=='mess_nodues' : - url = ('otheracademic:mess_nodues') - elif type=='physics_nodues' : - url = ('otheracademic:Physics_nodues') - elif type=='discipline_nodues' : - url = ('otheracademic:discipline_nodues') - elif type=='me_nodues' : - url = ('otheracademic:ME_nodues') - elif type=="ug_leave_hod_approve": - url = ('otheracademic:leaveStatus') - elif type=="bonafide_acadadmin": - url = ('otheracademic:bonafideApproveForm') - elif type=="bonafide_accept": - url = ('otheracademic:bonafideStatus') - elif type=="ast_ta_accept": - url = ('otheracademic:assistantship_status') - elif type=="nodues_status": - url = ('otheracademic:nodues_status') - elif type=="pg_leave_ta_approve": - url = ('otheracademic:leaveStatusPG') - elif type=="pg_leave_thesis": - url = ('otheracademic:leaveApproveThesis') +def otheracademic_notif(sender, recipient, type, otheracademic_id, student, message): + recipient_role = None + + if type == 'ug_leave_hod': + url = 'otheracademic:otheracademic' + recipient_role = 'hod' + elif type == 'pg_leave_ta': + url = 'otheracademic:leaveApproveTA' + recipient_role = 'ta_supervisor' + elif type == 'pg_leave_hod': + url = 'otheracademic:otheracademic' + recipient_role = 'hod' + elif type == 'ast_ta': + url = 'otheracademic:assistantship_form_approval' + recipient_role = 'ta_supervisor' + elif type == 'ast_thesis': + url = 'otheracademic:assistantship_thesis' + recipient_role = 'thesis_supervisor' + elif type == 'ast_acadadmin': + url = 'otheracademic:assistantship_acad_approveform' + recipient_role = 'acadadmin' + elif type == 'ast_hod': + url = 'otheracademic:assistantship_hod' + recipient_role = 'hod' + elif type == 'hostel_nodues': + url = 'otheracademic:hostel_nodues' + recipient_role = 'hostel_warden' + elif type == 'bank_nodues': + url = 'otheracademic:Bank_nodues' + recipient_role = 'bank' + elif type == 'btp_nodues': + url = 'otheracademic:BTP_nodues' + recipient_role = 'btp_supervisor' + elif type == 'cse_nodues': + url = 'otheracademic:CSE_nodues' + recipient_role = 'dept_admin' + elif type == 'design_nodues': + url = 'otheracademic:Design_nodues' + recipient_role = 'design_supervisor' + elif type == 'acad_nodues': + url = 'otheracademic:dsa_nodues' + recipient_role = 'acadadmin' + elif type == 'ece_nodues': + url = 'otheracademic:Ece_nodues' + recipient_role = 'lab_supervisor' + elif type == 'library_nodues': + url = 'otheracademic:library_nodues' + recipient_role = 'librarian' + elif type == 'mess_nodues': + url = 'otheracademic:mess_nodues' + recipient_role = 'mess_incharge' + elif type == 'physics_nodues': + url = 'otheracademic:Physics_nodues' + recipient_role = 'lab_supervisor' + elif type == 'discipline_nodues': + url = 'otheracademic:discipline_nodues' + recipient_role = 'discipline_office' + elif type == 'me_nodues': + url = 'otheracademic:ME_nodues' + recipient_role = 'lab_supervisor' + elif type == 'ug_leave_hod_approve': + url = 'otheracademic:leaveStatus' + recipient_role = 'student' + elif type == 'bonafide_acadadmin': + url = 'otheracademic:bonafideApproveForm' + recipient_role = 'acadadmin' + elif type == 'bonafide_accept': + url = 'otheracademic:bonafideStatus' + recipient_role = 'student' + elif type == 'ast_ta_accept': + url = 'otheracademic:assistantship_status' + recipient_role = 'student' + elif type == 'nodues_status': + url = 'otheracademic:nodues_status' + recipient_role = 'student' + elif type == 'pg_leave_ta_approve': + url = 'otheracademic:leaveStatusPG' + recipient_role = 'student' + elif type == 'pg_leave_thesis': + url = 'otheracademic:leaveApproveThesis' + recipient_role = 'student' else: - url=('otheracademic:otheracademic') + url = 'otheracademic:otheracademic' - - module='otheracademic' - sender = sender - recipient = recipient + module = 'otheracademic' verb = message description = otheracademic_id - notify.send(sender=sender, recipient=recipient, url=url, module=module, verb=verb,description=description) + notify.send( + sender=sender, + recipient=recipient, + url=url, + module=module, + verb=verb, + description=description, + recipient_role=recipient_role, + ) def iwd_notif(sender,recipient,type): module= 'iwdModuleV2' url= 'iwdModuleV2:iwdModuleV2' diff --git a/FusionIIIT/templates/otheracademic/bonafideForm.html b/FusionIIIT/templates/otheracademic/bonafideForm.html index 571d5c647..632e7594d 100644 --- a/FusionIIIT/templates/otheracademic/bonafideForm.html +++ b/FusionIIIT/templates/otheracademic/bonafideForm.html @@ -34,7 +34,7 @@

Bonafide Form

-
+ {% csrf_token %} {% comment %}
@@ -60,6 +60,10 @@

Bonafide Form

+
+ + +
diff --git a/FusionIIIT/templates/otheracademic/bonafideStatus.html b/FusionIIIT/templates/otheracademic/bonafideStatus.html index 9bec3e5a1..b76af541c 100644 --- a/FusionIIIT/templates/otheracademic/bonafideStatus.html +++ b/FusionIIIT/templates/otheracademic/bonafideStatus.html @@ -59,7 +59,13 @@

Bonafide Status

{{ entry.semester_types }} {{ entry.purposes }} {{ entry.date_of_applications }} - Download + + {% if entry.download_file %} + Download + {% else %} + - + {% endif %} + {% if entry.approve %} Approved