From 9531f91d0bdaf418898ea8b8581ea2e6922e8466 Mon Sep 17 00:00:00 2001 From: vikrantwiz02 Date: Wed, 5 Nov 2025 16:26:49 +0530 Subject: [PATCH 1/3] Added new fields and validations to StudentBatchUpload model; enhance data handling for student information --- .../programme_curriculum/api/serializers.py | 6 + .../api/views_student_management.py | 310 ++++++++++++------ .../migrations/0028_add_new_student_fields.py | 68 ++++ .../0029_increase_allotted_field_lengths.py | 23 ++ .../models_student_management.py | 92 +++++- 5 files changed, 387 insertions(+), 112 deletions(-) create mode 100644 FusionIIIT/applications/programme_curriculum/migrations/0028_add_new_student_fields.py create mode 100644 FusionIIIT/applications/programme_curriculum/migrations/0029_increase_allotted_field_lengths.py diff --git a/FusionIIIT/applications/programme_curriculum/api/serializers.py b/FusionIIIT/applications/programme_curriculum/api/serializers.py index 26541fb66..821f207ba 100644 --- a/FusionIIIT/applications/programme_curriculum/api/serializers.py +++ b/FusionIIIT/applications/programme_curriculum/api/serializers.py @@ -1,5 +1,6 @@ from rest_framework import serializers from applications.programme_curriculum.models import Programme, Discipline, Curriculum, Semester, Course, Batch, CourseSlot, CourseInstructor +from applications.programme_curriculum.models_student_management import StudentBatchUpload # this is for Programme model .... @@ -122,3 +123,8 @@ class ProgrammePostSerializer(serializers.ModelSerializer): class Meta: model = Programme fields = ['id', 'category', 'name', 'programme_begin_year'] + +class StudentBatchUploadSerializer(serializers.ModelSerializer): + class Meta: + model = StudentBatchUpload + fields = '__all__' diff --git a/FusionIIIT/applications/programme_curriculum/api/views_student_management.py b/FusionIIIT/applications/programme_curriculum/api/views_student_management.py index 10fc48715..24748eb27 100644 --- a/FusionIIIT/applications/programme_curriculum/api/views_student_management.py +++ b/FusionIIIT/applications/programme_curriculum/api/views_student_management.py @@ -294,7 +294,7 @@ def process_excel_upload(request): 'minority': ['minority', 'minority status'], 'phone_number': ['mobileno', 'mobile', 'phone', 'mobile no'], 'institute_email': ['institute email id', 'institute email', 'inst email id'], - 'personal_email': ['alternet email id', 'alternate email id', 'email', 'personal email'], + 'personal_email': ['alternate email id', 'email', 'personal email'], 'father_name': ['father\'s name', 'father name', 'fathers name'], 'father_occupation': ['father\'s occupation', 'father occupation', 'fathers occupation'], 'father_mobile': ['father mobile number', 'father mobile', 'fathers mobile'], @@ -307,7 +307,18 @@ def process_excel_upload(request): 'allotted_category': ['allottedcat', 'allotted category', 'alloted category'], 'allotted_gender': ['allotted gender', 'alloted gender'], 'state': ['state'], - 'address': ['full address', 'address', 'complete address'] + 'address': ['full address', 'address', 'complete address'], + 'parent_email': ['parent\'s email', 'parent email', 'parents email'], + 'country': ['country'], + 'nationality': ['nationality'], + 'blood_group': ['blood group', 'bloodgroup'], + 'blood_group_remarks': ['blood group remarks', 'bloodgroup remarks'], + 'pwd_category': ['pwd category', 'pwb category', 'disability category'], + 'pwd_category_remarks': ['pwd category remarks', 'pwb category remarks', 'disability remarks'], + 'admission_mode': ['admission mode', 'admission type'], + 'admission_mode_remarks': ['admission mode remarks', 'admission type remarks'], + 'income_group': ['income group', 'family income group'], + 'income': ['income', 'family income', 'annual income'] } df.columns = df.columns.str.lower().str.strip() @@ -397,7 +408,7 @@ def process_excel_upload(request): 'Minority': student_data.get('minority', ''), 'Mobile No': student_data.get('phone_number', ''), 'Institute Email ID': student_data.get('institute_email', ''), - 'Alternet Email ID': student_data.get('personal_email', ''), + 'Alternate Email ID': student_data.get('personal_email', ''), 'Father\'s Name': student_data.get('father_name', ''), 'Father\'s Occupation': student_data.get('father_occupation', ''), 'Father Mobile Number': student_data.get('father_mobile', ''), @@ -410,7 +421,18 @@ def process_excel_upload(request): 'allottedcat': student_data.get('allotted_category', ''), 'Allotted Gender': student_data.get('allotted_gender', ''), 'State': student_data.get('state', ''), - 'Full Address': student_data.get('address', '') + 'Full Address': student_data.get('address', ''), + 'Parent\'s Email': student_data.get('parent_email', ''), + 'Country': student_data.get('country', ''), + 'Nationality': student_data.get('nationality', ''), + 'Blood Group': student_data.get('blood_group', ''), + 'Blood Group Remarks': student_data.get('blood_group_remarks', ''), + 'PwD Category': student_data.get('pwd_category', ''), + 'PwD Category Remarks': student_data.get('pwd_category_remarks', ''), + 'Admission Mode': student_data.get('admission_mode', ''), + 'Admission Mode Remarks': student_data.get('admission_mode_remarks', ''), + 'Income Group': student_data.get('income_group', ''), + 'Income': student_data.get('income', '') } valid_students.append(cleaned_data) @@ -640,9 +662,20 @@ def save_students_batch(request): minority=student_data.get('Minority') or student_data.get('minority', ''), phone_number=sanitize_phone_number(student_data.get('Mobile No') or student_data.get('phoneNumber', '')), - personal_email=student_data.get('Alternet Email ID') or student_data.get('email', ''), + personal_email=student_data.get('Alternate Email ID') or student_data.get('email', '') or student_data.get('alternateEmail', '') or student_data.get('personalEmail', '') or student_data.get('personal_email', ''), + parent_email=student_data.get('Parent Email') or student_data.get('parentEmail', '') or student_data.get('parent_email', ''), address=student_data.get('Full Address') or student_data.get('address', ''), state=student_data.get('State') or student_data.get('state', ''), + country=student_data.get('Country') or student_data.get('country', 'India'), + nationality=student_data.get('Nationality') or student_data.get('nationality', 'Indian'), + blood_group=student_data.get('Blood Group') or student_data.get('bloodGroup', ''), + blood_group_remarks=student_data.get('Blood Group Remarks') or student_data.get('bloodGroupRemarks', ''), + pwd_category=student_data.get('PWD Category') or student_data.get('pwdCategory', ''), + pwd_category_remarks=student_data.get('PWD Category Remarks') or student_data.get('pwdCategoryRemarks', ''), + admission_mode=student_data.get('Admission Mode') or student_data.get('admissionMode', ''), + admission_mode_remarks=student_data.get('Admission Mode Remarks') or student_data.get('admissionModeRemarks', ''), + income_group=student_data.get('Income Group') or student_data.get('incomeGroup', ''), + income=student_data.get('Income') or student_data.get('income', None), branch=student_data.get('Discipline') or student_data.get('branch', ''), date_of_birth=dob, @@ -912,9 +945,14 @@ def add_single_student(request): 'fatherMobile': 'father_mobile', 'motherOccupation': 'mother_occupation', 'motherMobile': 'mother_mobile', - # DON'T map these - process_batch_allocation needs original names - # 'rollNumber': 'roll_number', - # 'instituteEmail': 'institute_email' + 'parentEmail': 'parent_email', + 'bloodGroup': 'blood_group', + 'bloodGroupRemarks': 'blood_group_remarks', + 'pwdCategory': 'pwd_category', + 'pwdCategoryRemarks': 'pwd_category_remarks', + 'admissionMode': 'admission_mode', + 'admissionModeRemarks': 'admission_mode_remarks', + 'incomeGroup': 'income_group', } mapped_data = {} @@ -959,94 +997,55 @@ def add_single_student(request): # Save to database with ALL Excel-equivalent fields for complete synchronization with transaction.atomic(): - student = StudentBatchUpload.objects.create( - name=student_data.get('name'), - jee_app_no=student_data.get('jee_app_no'), - roll_number=student_data.get('roll_number'), - institute_email=student_data.get('institute_email'), - - father_name=student_data.get('father_name'), - mother_name=student_data.get('mother_name'), - gender=student_data.get('gender'), - category=student_data.get('category'), - pwd=student_data.get('pwd'), - minority=data.get('minority', ''), - date_of_birth=dob or data.get('date_of_birth'), - - phone_number=sanitize_phone_number(data.get('phone_number', '') or data.get('MobileNo', '')), - personal_email=data.get('personal_email', '') or data.get('Alternet Email ID', ''), - address=student_data.get('address'), - state=data.get('state', '') or data.get('State', ''), - - father_occupation=data.get('father_occupation', '') or data.get("Father's Occupation", ''), - father_mobile=sanitize_phone_number(data.get('father_mobile', '') or data.get('Father Mobile Number', '')), - mother_occupation=data.get('mother_occupation', '') or data.get("Mother's Occupation", ''), - mother_mobile=sanitize_phone_number(data.get('mother_mobile', '') or data.get('Mother Mobile Number', '')), - - branch=student_data.get('branch'), - ai_rank=sanitize_rank_value(data.get('ai_rank') or data.get('AI rank')), - category_rank=sanitize_rank_value(data.get('category_rank') or data.get('Category Rank')), - - allotted_category=data.get('allotted_category', '') or data.get('allottedcat', ''), - allotted_gender=data.get('allotted_gender', '') or data.get('Allotted Gender', ''), - - year=batch_year, - programme_type=programme_type, - reported_status='NOT_REPORTED', - academic_year=academic_year, - allocation_status='ALLOCATED', - source='manual_entry' - ) - - # Automatically transfer manual entries to main academic tables - auto_generated_password = None - user_created = False - transfer_success = False - - try: - from django.http import HttpRequest - from django.contrib.auth.models import AnonymousUser - - # Create a mock request for the status update function - mock_request = HttpRequest() - mock_request.method = 'PUT' - mock_request._body = json.dumps({ - 'studentId': student.id, - 'reportedStatus': 'REPORTED' - }).encode('utf-8') - mock_request.user = request.user if hasattr(request, 'user') else AnonymousUser() - - from django.test import RequestFactory - factory = RequestFactory() - update_request = factory.put( - '/api/students/status/', - data=json.dumps({ - 'studentId': student.id, - 'reportedStatus': 'REPORTED' - }), - content_type='application/json' + student = StudentBatchUpload.objects.create( + name=student_data.get('name'), + jee_app_no=student_data.get('jee_app_no'), + roll_number=student_data.get('roll_number'), + institute_email=student_data.get('institute_email'), + + father_name=student_data.get('father_name'), + mother_name=student_data.get('mother_name'), + gender=student_data.get('gender'), + category=student_data.get('category'), + pwd=student_data.get('pwd'), + minority=data.get('minority', ''), + date_of_birth=dob or data.get('date_of_birth'), + + phone_number=sanitize_phone_number(data.get('phone_number', '') or data.get('MobileNo', '')), + personal_email=data.get('personal_email', '') or data.get('email', '') or data.get('alternateEmail', '') or data.get('Alternate Email ID', ''), + parent_email=data.get('parent_email', '') or data.get('parentEmail', ''), + address=student_data.get('address'), + state=data.get('state', '') or data.get('State', ''), + country=data.get('country', 'India'), + nationality=data.get('nationality', 'Indian'), + blood_group=data.get('blood_group', '') or data.get('bloodGroup', ''), + blood_group_remarks=data.get('blood_group_remarks', '') or data.get('bloodGroupRemarks', ''), + pwd_category=data.get('pwd_category', '') or data.get('pwdCategory', ''), + pwd_category_remarks=data.get('pwd_category_remarks', '') or data.get('pwdCategoryRemarks', ''), + admission_mode=data.get('admission_mode', '') or data.get('admissionMode', ''), + admission_mode_remarks=data.get('admission_mode_remarks', '') or data.get('admissionModeRemarks', ''), + income_group=data.get('income_group', '') or data.get('incomeGroup', ''), + income=data.get('income', None), + + father_occupation=data.get('father_occupation', '') or data.get("Father's Occupation", ''), + father_mobile=sanitize_phone_number(data.get('father_mobile', '') or data.get('Father Mobile Number', '')), + mother_occupation=data.get('mother_occupation', '') or data.get("Mother's Occupation", ''), + mother_mobile=sanitize_phone_number(data.get('mother_mobile', '') or data.get('Mother Mobile Number', '')), + + branch=student_data.get('branch'), + ai_rank=sanitize_rank_value(data.get('ai_rank') or data.get('AI rank')), + category_rank=sanitize_rank_value(data.get('category_rank') or data.get('Category Rank')), + + allotted_category=data.get('allotted_category', '') or data.get('allottedcat', ''), + allotted_gender=data.get('allotted_gender', '') or data.get('allottedGender', ''), + + year=batch_year, + programme_type=programme_type, + reported_status='NOT_REPORTED', + academic_year=academic_year, + allocation_status='ALLOCATED', + source='manual_entry' ) - update_request.user = request.user if hasattr(request, 'user') else AnonymousUser() - - # Call update_student_status function directly - status_response = update_student_status(update_request) - - if status_response.status_code == 200: - response_data = json.loads(status_response.content) - if response_data.get('success'): - transfer_success = True - auto_generated_password = response_data.get('password') - user_created = response_data.get('user_created', False) - - except Exception as transfer_error: - transfer_success = False - - # Build success message - success_message = f'Student added successfully' - if transfer_success: - success_message += f' and transferred to main academic tables' - if user_created: - success_message += f' with auto-generated login credentials' return JsonResponse({ 'success': True, @@ -1054,12 +1053,14 @@ def add_single_student(request): 'student_id': student.id, 'roll_number': student.roll_number or student_data.get('roll_number'), 'institute_email': student.institute_email or student_data.get('institute_email'), - 'password': auto_generated_password, - 'user_created': user_created, - 'username': student.roll_number if user_created else None, - 'transferred_to_academic': transfer_success + 'name': student.name, + 'personal_email': student.personal_email, + 'parent_email': student.parent_email, + 'blood_group': student.blood_group, + 'country': student.country, + 'nationality': student.nationality }, - 'message': success_message + 'message': f'Student {student.name} added successfully. Use "Update Status" to transfer to main academic system when ready.' }) except json.JSONDecodeError as e: @@ -1898,7 +1899,27 @@ def list_students(request): 'reported_status': student.reported_status, 'branch': student.branch, 'year': student.year, - 'source': getattr(student, 'source', 'unknown') # Include source field + 'source': getattr(student, 'source', 'unknown'), # Include source field + 'parent_email': getattr(student, 'parent_email', ''), + 'parentEmail': getattr(student, 'parent_email', ''), # Camel case for frontend + 'alternateEmail': getattr(student, 'personal_email', ''), + 'country': getattr(student, 'country', ''), + 'nationality': getattr(student, 'nationality', ''), + 'blood_group': getattr(student, 'blood_group', ''), + 'bloodGroup': getattr(student, 'blood_group', ''), # Camel case for frontend + 'blood_group_remarks': getattr(student, 'blood_group_remarks', ''), + 'bloodGroupRemarks': getattr(student, 'blood_group_remarks', ''), # Camel case for frontend + 'pwd_category': getattr(student, 'pwd_category', ''), + 'pwdCategory': getattr(student, 'pwd_category', ''), # Camel case for frontend + 'pwd_category_remarks': getattr(student, 'pwd_category_remarks', ''), + 'pwdCategoryRemarks': getattr(student, 'pwd_category_remarks', ''), # Camel case for frontend + 'admission_mode': getattr(student, 'admission_mode', ''), + 'admissionMode': getattr(student, 'admission_mode', ''), # Camel case for frontend + 'admission_mode_remarks': getattr(student, 'admission_mode_remarks', ''), + 'admissionModeRemarks': getattr(student, 'admission_mode_remarks', ''), # Camel case for frontend + 'income_group': getattr(student, 'income_group', ''), + 'incomeGroup': getattr(student, 'income_group', ''), # Camel case for frontend + 'income': getattr(student, 'income', '') }) return JsonResponse({ @@ -2458,7 +2479,6 @@ def get_student(request, student_id): 'phone_number': student.phone_number, 'phoneNumber': student.phone_number, 'mobile': student.phone_number, # Additional alias - 'personal_email': student.personal_email, 'personalEmail': student.personal_email, 'address': student.address, 'state': student.state, @@ -2488,6 +2508,25 @@ def get_student(request, student_id): 'allottedGender': student.allotted_gender, 'aadhar_number': student.aadhar_number, 'aadharNumber': student.aadhar_number, + 'parent_email': getattr(student, 'parent_email', ''), + 'parentEmail': getattr(student, 'parent_email', ''), # Camel case for frontend + 'alternateEmail': getattr(student, 'personal_email', ''), + 'nationality': getattr(student, 'nationality', ''), + 'blood_group': getattr(student, 'blood_group', ''), + 'bloodGroup': getattr(student, 'blood_group', ''), # Camel case for frontend + 'blood_group_remarks': getattr(student, 'blood_group_remarks', ''), + 'bloodGroupRemarks': getattr(student, 'blood_group_remarks', ''), # Camel case for frontend + 'pwd_category': getattr(student, 'pwd_category', ''), + 'pwdCategory': getattr(student, 'pwd_category', ''), # Camel case for frontend + 'pwd_category_remarks': getattr(student, 'pwd_category_remarks', ''), + 'pwdCategoryRemarks': getattr(student, 'pwd_category_remarks', ''), # Camel case for frontend + 'admission_mode': getattr(student, 'admission_mode', ''), + 'admissionMode': getattr(student, 'admission_mode', ''), # Camel case for frontend + 'admission_mode_remarks': getattr(student, 'admission_mode_remarks', ''), + 'admissionModeRemarks': getattr(student, 'admission_mode_remarks', ''), # Camel case for frontend + 'income_group': getattr(student, 'income_group', ''), + 'incomeGroup': getattr(student, 'income_group', ''), # Camel case for frontend + 'income': getattr(student, 'income', ''), 'reported_status': student.reported_status, 'reportedStatus': student.reported_status, @@ -2531,34 +2570,65 @@ def update_student(request, student_id): data = json.loads(request.body) field_mapping = { + # Identification fields 'jeeAppNo': 'jee_app_no', - 'rollNumber': 'roll_number', + 'rollNumber': 'roll_number', 'instituteEmail': 'institute_email', + + # Names 'fatherName': 'father_name', 'fname': 'father_name', - 'mname': 'mother_name', + 'mname': 'mother_name', 'motherName': 'mother_name', + + # Dates & phones 'dateOfBirth': 'date_of_birth', 'dob': 'date_of_birth', 'phoneNumber': 'phone_number', 'mobile': 'phone_number', - 'personalEmail': 'personal_email', - 'email': 'personal_email', - 'aiRank': 'ai_rank', + + # Email / alternate + 'email': 'personal_email', + 'alternateEmail': 'personal_email', + + # Academic ranks 'jeeRank': 'ai_rank', 'categoryRank': 'category_rank', 'tenthMarks': 'tenth_marks', 'twelfthMarks': 'twelfth_marks', + + # Parents / contacts 'fatherOccupation': 'father_occupation', 'fatherMobile': 'father_mobile', 'motherOccupation': 'mother_occupation', 'motherMobile': 'mother_mobile', + 'parentEmail': 'parent_email', + 'parent_email': 'parent_email', + + # Allotment 'allottedCategory': 'allotted_category', 'allottedGender': 'allotted_gender', + + # New fields added + 'country': 'country', + 'nationality': 'nationality', + 'bloodGroup': 'blood_group', + 'blood_group': 'blood_group', + 'bloodGroupRemarks': 'blood_group_remarks', + 'blood_group_remarks': 'blood_group_remarks', + 'pwdCategory': 'pwd_category', + 'pwd_category': 'pwd_category', + 'pwdCategoryRemarks': 'pwd_category_remarks', + 'admissionMode': 'admission_mode', + 'admissionModeRemarks': 'admission_mode_remarks', + 'incomeGroup': 'income_group', + 'income': 'income', + 'aadharNumber': 'aadhar_number', 'reportedStatus': 'reported_status', 'programmeType': 'programme_type' } + jee_app_no = data.get('jeeAppNo') or data.get('jee_app_no') roll_number = data.get('rollNumber') or data.get('roll_number') @@ -2592,7 +2662,10 @@ def update_student(request, student_id): setattr(student, backend_field, value) direct_fields = [ - 'name', 'gender', 'category', 'pwd', 'minority', 'address', 'state', 'branch' + 'name', 'gender', 'category', 'pwd', 'minority', 'address', 'state', 'branch', + 'personal_email', 'parent_email', 'country', 'nationality', + 'blood_group', 'blood_group_remarks', 'pwd_category', 'pwd_category_remarks', + 'admission_mode', 'admission_mode_remarks', 'income_group', 'income' ] for field in direct_fields: @@ -3000,7 +3073,6 @@ def get_batch_students(request, batch_id): 'date_of_birth': student.date_of_birth.isoformat() if student.date_of_birth else '', 'phone_number': getattr(student, 'phone_number', ''), - 'personal_email': getattr(student, 'personal_email', ''), 'address': getattr(student, 'address', ''), 'state': getattr(student, 'state', ''), @@ -3018,6 +3090,26 @@ def get_batch_students(request, batch_id): 'allotted_category': getattr(student, 'allotted_category', ''), 'allotted_gender': getattr(student, 'allotted_gender', ''), + 'parent_email': getattr(student, 'parent_email', ''), + 'parentEmail': getattr(student, 'parent_email', ''), # Camel case for frontend + 'alternateEmail': getattr(student, 'personal_email', ''), + 'country': getattr(student, 'country', ''), + 'nationality': getattr(student, 'nationality', ''), + 'blood_group': getattr(student, 'blood_group', ''), + 'bloodGroup': getattr(student, 'blood_group', ''), # Camel case for frontend + 'blood_group_remarks': getattr(student, 'blood_group_remarks', ''), + 'bloodGroupRemarks': getattr(student, 'blood_group_remarks', ''), # Camel case for frontend + 'pwd_category': getattr(student, 'pwd_category', ''), + 'pwdCategory': getattr(student, 'pwd_category', ''), # Camel case for frontend + 'pwd_category_remarks': getattr(student, 'pwd_category_remarks', ''), + 'pwdCategoryRemarks': getattr(student, 'pwd_category_remarks', ''), # Camel case for frontend + 'admission_mode': getattr(student, 'admission_mode', ''), + 'admissionMode': getattr(student, 'admission_mode', ''), # Camel case for frontend + 'admission_mode_remarks': getattr(student, 'admission_mode_remarks', ''), + 'admissionModeRemarks': getattr(student, 'admission_mode_remarks', ''), # Camel case for frontend + 'income_group': getattr(student, 'income_group', ''), + 'incomeGroup': getattr(student, 'income_group', ''), # Camel case for frontend + 'income': getattr(student, 'income', ''), 'year': student.year, 'academic_year': getattr(student, 'academic_year', ''), diff --git a/FusionIIIT/applications/programme_curriculum/migrations/0028_add_new_student_fields.py b/FusionIIIT/applications/programme_curriculum/migrations/0028_add_new_student_fields.py new file mode 100644 index 000000000..52b12300d --- /dev/null +++ b/FusionIIIT/applications/programme_curriculum/migrations/0028_add_new_student_fields.py @@ -0,0 +1,68 @@ +# Generated by Django 3.1.5 on 2025-11-05 02:53 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('programme_curriculum', '0027_auto_20250918_1616'), + ] + + operations = [ + migrations.AddField( + model_name='studentbatchupload', + name='admission_mode', + field=models.CharField(blank=True, choices=[('JEE Main', 'JEE Main'), ('JEE Advanced', 'JEE Advanced'), ('GATE', 'GATE'), ('DASA', 'DASA'), ('Foreign National', 'Foreign National'), ('Sponsored', 'Sponsored'), ('Any other (remarks)', 'Any other (remarks)')], max_length=50, null=True), + ), + migrations.AddField( + model_name='studentbatchupload', + name='admission_mode_remarks', + field=models.TextField(blank=True, help_text="Required when admission mode is 'Any other (remarks)'", null=True), + ), + migrations.AddField( + model_name='studentbatchupload', + name='blood_group', + field=models.CharField(blank=True, choices=[('A+', 'A+'), ('A-', 'A-'), ('B+', 'B+'), ('B-', 'B-'), ('AB+', 'AB+'), ('AB-', 'AB-'), ('O+', 'O+'), ('O-', 'O-'), ('Other', 'Other')], max_length=10, null=True), + ), + migrations.AddField( + model_name='studentbatchupload', + name='blood_group_remarks', + field=models.TextField(blank=True, help_text="Blood group remarks when 'Other' is selected", null=True), + ), + migrations.AddField( + model_name='studentbatchupload', + name='country', + field=models.CharField(blank=True, default='India', max_length=100, null=True), + ), + migrations.AddField( + model_name='studentbatchupload', + name='income', + field=models.DecimalField(blank=True, decimal_places=2, help_text='Annual family income', max_digits=10, null=True), + ), + migrations.AddField( + model_name='studentbatchupload', + name='income_group', + field=models.CharField(blank=True, choices=[('Below 1 Lakh', 'Below 1 Lakh'), ('1-2.5 Lakhs', '1-2.5 Lakhs'), ('2.5-5 Lakhs', '2.5-5 Lakhs'), ('5-8 Lakhs', '5-8 Lakhs'), ('Above 8 Lakhs', 'Above 8 Lakhs')], max_length=30, null=True), + ), + migrations.AddField( + model_name='studentbatchupload', + name='nationality', + field=models.CharField(blank=True, default='Indian', max_length=100, null=True), + ), + migrations.AddField( + model_name='studentbatchupload', + name='parent_email', + field=models.EmailField(blank=True, help_text='Parent Email ID', max_length=254, null=True), + ), + migrations.AddField( + model_name='studentbatchupload', + name='pwd_category', + field=models.CharField(blank=True, choices=[('Locomotor Disability', 'Locomotor Disability'), ('Visual Impairment', 'Visual Impairment'), ('Hearing Impairment', 'Hearing Impairment'), ('Speech and Language Disability', 'Speech and Language Disability'), ('Intellectual Disability', 'Intellectual Disability'), ('Autism Spectrum Disorder', 'Autism Spectrum Disorder'), ('Multiple Disabilities', 'Multiple Disabilities'), ('Any other (remarks)', 'Any other (remarks)')], help_text='Required when PWD is YES', max_length=100, null=True), + ), + migrations.AddField( + model_name='studentbatchupload', + name='pwd_category_remarks', + field=models.TextField(blank=True, help_text="Required when PWD category is 'Any other (remarks)'", null=True), + ), + ] diff --git a/FusionIIIT/applications/programme_curriculum/migrations/0029_increase_allotted_field_lengths.py b/FusionIIIT/applications/programme_curriculum/migrations/0029_increase_allotted_field_lengths.py new file mode 100644 index 000000000..35a0a30a3 --- /dev/null +++ b/FusionIIIT/applications/programme_curriculum/migrations/0029_increase_allotted_field_lengths.py @@ -0,0 +1,23 @@ +# Generated by Django 3.1.5 on 2025-11-05 14:25 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('programme_curriculum', '0028_add_new_student_fields'), + ] + + operations = [ + migrations.AlterField( + model_name='studentbatchupload', + name='allotted_category', + field=models.CharField(blank=True, max_length=50, null=True), + ), + migrations.AlterField( + model_name='studentbatchupload', + name='allotted_gender', + field=models.CharField(blank=True, max_length=50, null=True), + ), + ] diff --git a/FusionIIIT/applications/programme_curriculum/models_student_management.py b/FusionIIIT/applications/programme_curriculum/models_student_management.py index fb66cc3a6..968646a8f 100644 --- a/FusionIIIT/applications/programme_curriculum/models_student_management.py +++ b/FusionIIIT/applications/programme_curriculum/models_student_management.py @@ -118,7 +118,46 @@ class StudentBatchUpload(models.Model): ('NO', 'No'), ] - + PWD_CATEGORY_CHOICES = [ + ('Locomotor Disability', 'Locomotor Disability'), + ('Visual Impairment', 'Visual Impairment'), + ('Hearing Impairment', 'Hearing Impairment'), + ('Speech and Language Disability', 'Speech and Language Disability'), + ('Intellectual Disability', 'Intellectual Disability'), + ('Autism Spectrum Disorder', 'Autism Spectrum Disorder'), + ('Multiple Disabilities', 'Multiple Disabilities'), + ('Any other (remarks)', 'Any other (remarks)'), + ] + + BLOOD_GROUP_CHOICES = [ + ('A+', 'A+'), + ('A-', 'A-'), + ('B+', 'B+'), + ('B-', 'B-'), + ('AB+', 'AB+'), + ('AB-', 'AB-'), + ('O+', 'O+'), + ('O-', 'O-'), + ('Other', 'Other'), + ] + + ADMISSION_MODE_CHOICES = [ + ('JEE Main', 'JEE Main'), + ('JEE Advanced', 'JEE Advanced'), + ('GATE', 'GATE'), + ('DASA', 'DASA'), + ('Foreign National', 'Foreign National'), + ('Sponsored', 'Sponsored'), + ('Any other (remarks)', 'Any other (remarks)'), + ] + + INCOME_GROUP_CHOICES = [ + ('Below 1 Lakh', 'Below 1 Lakh'), + ('1-2.5 Lakhs', '1-2.5 Lakhs'), + ('2.5-5 Lakhs', '2.5-5 Lakhs'), + ('5-8 Lakhs', '5-8 Lakhs'), + ('Above 8 Lakhs', 'Above 8 Lakhs'), + ] REPORTED_STATUS_CHOICES = [ ('NOT_REPORTED', 'Not Reported'), @@ -145,18 +184,26 @@ class StudentBatchUpload(models.Model): category = models.CharField(max_length=10, choices=CATEGORY_CHOICES) pwd = models.CharField(max_length=3, choices=PWD_CHOICES, default='NO', help_text="Person with Disability") minority = models.TextField(blank=True, null=True, help_text="Minority Status (e.g., Muslim, Christian, Sikh, Buddhist, Jain, Parsi, etc.)") + blood_group = models.CharField(max_length=10, choices=BLOOD_GROUP_CHOICES, blank=True, null=True) + blood_group_remarks = models.TextField(blank=True, null=True, help_text="Blood group remarks when 'Other' is selected") + pwd_category = models.CharField(max_length=100, choices=PWD_CATEGORY_CHOICES, blank=True, null=True, help_text="Required when PWD is YES") + pwd_category_remarks = models.TextField(blank=True, null=True, help_text="Required when PWD category is 'Any other (remarks)'") # Contact and address phone_number = models.CharField(max_length=15, blank=True, null=True, help_text="Mobile Number") personal_email = models.EmailField(blank=True, null=True, help_text="Personal Email ID") address = models.TextField(help_text="Full Address") state = models.CharField(max_length=100, blank=True, null=True) + country = models.CharField(max_length=100, blank=True, null=True, default='India') + nationality = models.CharField(max_length=100, blank=True, null=True, default='Indian') # Academic information branch = models.CharField(max_length=200, help_text="Discipline/Branch") date_of_birth = models.DateField(blank=True, null=True) ai_rank = models.IntegerField(blank=True, null=True, help_text="JEE AI Rank", db_column='jee_rank') category_rank = models.IntegerField(blank=True, null=True, help_text="Category Rank") + income_group = models.CharField(max_length=30, choices=INCOME_GROUP_CHOICES, blank=True, null=True) + income = models.DecimalField(max_digits=10, decimal_places=2, blank=True, null=True, help_text="Annual family income") # Additional academic details tenth_marks = models.FloatField(blank=True, null=True, help_text="10th Class Marks (%)") @@ -167,11 +214,14 @@ class StudentBatchUpload(models.Model): father_mobile = models.CharField(max_length=15, blank=True, null=True) mother_occupation = models.CharField(max_length=200, blank=True, null=True) mother_mobile = models.CharField(max_length=15, blank=True, null=True) + parent_email = models.EmailField(blank=True, null=True, help_text="Parent Email ID") aadhar_number = models.CharField(max_length=12, blank=True, null=True) # Allotment details - allotted_category = models.CharField(max_length=20, blank=True, null=True) - allotted_gender = models.CharField(max_length=20, blank=True, null=True) + allotted_category = models.CharField(max_length=50, blank=True, null=True) + allotted_gender = models.CharField(max_length=50, blank=True, null=True) + admission_mode = models.CharField(max_length=50, choices=ADMISSION_MODE_CHOICES, blank=True, null=True) + admission_mode_remarks = models.TextField(blank=True, null=True, help_text="Required when admission mode is 'Any other (remarks)'") # System fields year = models.IntegerField(help_text="Academic Year", db_column='batch_year', default=get_current_academic_year) @@ -263,6 +313,35 @@ def last_name(self): return ' '.join(name_parts[1:]) return '' + def clean_branch_name(self, branch_text): + """Clean branch name by removing parenthetical content""" + import re + if not branch_text: + return branch_text + # Remove content in parentheses like "(4 Years, Bachelor of Technology)" + cleaned = re.sub(r'\s*\([^)]*\)', '', branch_text) + return cleaned.strip() + + def clean(self): + """Validate conditional field requirements""" + from django.core.exceptions import ValidationError + + # PWD Category validation + if self.pwd == 'YES' and not self.pwd_category: + raise ValidationError('PWD Category is required when PWD is YES') + + # PWD Category remarks validation + if self.pwd_category == 'Any other (remarks)' and not self.pwd_category_remarks: + raise ValidationError('PWD Category remarks are required when selecting "Any other (remarks)"') + + # Admission mode remarks validation + if self.admission_mode == 'Any other (remarks)' and not self.admission_mode_remarks: + raise ValidationError('Admission mode remarks are required when selecting "Any other (remarks)"') + + # Blood group remarks validation + if self.blood_group == 'Other' and not self.blood_group_remarks: + raise ValidationError('Blood group remarks are required when selecting "Other"') + def save(self, *args, **kwargs): """Override save to automatically set academic_year string and normalize emails""" if not self.academic_year: @@ -273,10 +352,17 @@ def save(self, *args, **kwargs): if self.roll_number and not self.institute_email: self.institute_email = f"{self.roll_number.lower()}@iiitdmj.ac.in" + # Normalize email addresses to lowercase if self.institute_email: self.institute_email = self.institute_email.lower() if self.personal_email: self.personal_email = self.personal_email.lower() + if self.parent_email: + self.parent_email = self.parent_email.lower() + + # Clean branch name + if self.branch: + self.branch = self.clean_branch_name(self.branch) super().save(*args, **kwargs) From e3e20d9e92783ce0e4d90adf06429fd38a8cb984 Mon Sep 17 00:00:00 2001 From: vikrantwiz02 Date: Wed, 5 Nov 2025 23:37:42 +0530 Subject: [PATCH 2/3] Enhance student data processing: sync branch, department, and specialization during batch changes; add validation for father's mobile number to ensure it differs from student's mobile number --- .../academic_procedures/api/views.py | 18 ++ .../api/views_student_management.py | 156 ++++++++++++++++-- 2 files changed, 161 insertions(+), 13 deletions(-) diff --git a/FusionIIIT/applications/academic_procedures/api/views.py b/FusionIIIT/applications/academic_procedures/api/views.py index e0f9f7e8d..2f08bb6e9 100644 --- a/FusionIIIT/applications/academic_procedures/api/views.py +++ b/FusionIIIT/applications/academic_procedures/api/views.py @@ -3599,6 +3599,24 @@ def apply_batch_changes(request): student.batch_id = new_batch student.batch = nyear student.save() + + # Sync branch, department, and specialization + try: + from applications.programme_curriculum.models_student_management import StudentBatchUpload + from applications.globals.models import DepartmentInfo + student_upload = StudentBatchUpload.objects.filter(roll_number=student.id_id).first() + if student_upload: + student_upload.branch = new_batch.discipline.name + student_upload.save() + dept_name = new_batch.discipline.acronym + department = DepartmentInfo.objects.filter(name=dept_name).first() + if department: + student.id.department = department + student.id.save() + student.specialization = dept_name + student.save() + except: + pass if errors: return Response({"errors": errors}, status=status.HTTP_207_MULTI_STATUS) diff --git a/FusionIIIT/applications/programme_curriculum/api/views_student_management.py b/FusionIIIT/applications/programme_curriculum/api/views_student_management.py index 24748eb27..87b0e4618 100644 --- a/FusionIIIT/applications/programme_curriculum/api/views_student_management.py +++ b/FusionIIIT/applications/programme_curriculum/api/views_student_management.py @@ -47,13 +47,27 @@ def sanitize_phone_number(phone_value): return phone_str def sanitize_rank_value(rank_value): - if rank_value is None: - return rank_value - rank_str = str(rank_value) + if rank_value is None or rank_value == '' or rank_value == 'null': + return None + rank_str = str(rank_value).strip() + if not rank_str or rank_str.lower() == 'nan': + return None if rank_str.endswith('.0'): rank_str = rank_str[:-2] return rank_str +def _safe_int_conversion(value): + """Convert a value to int, returning None for empty/invalid values""" + if value is None or value == '' or value == 'null': + return None + try: + value_str = str(value).replace(',', '').strip() + if value_str and value_str.isdigit(): + return int(value_str) + except (ValueError, TypeError): + pass + return None + def parse_date_flexible(date_value): if date_value is None or date_value == '': return None @@ -381,6 +395,19 @@ def process_excel_upload(request): phone = sanitized_phone.replace(' ', '').replace('-', '') if sanitized_phone else '' if not phone.isdigit() or len(phone) != 10: errors.append(f'Invalid {phone_field}: {student_data[phone_field]}') + + # Validation: father's mobile should not be same as student's mobile + student_phone = None + father_phone = None + if student_data.get('phone_number'): + sp = sanitize_phone_number(student_data.get('phone_number')) + student_phone = sp.replace(' ', '').replace('-', '') if sp else None + if student_data.get('father_mobile'): + fp = sanitize_phone_number(student_data.get('father_mobile')) + father_phone = fp.replace(' ', '').replace('-', '') if fp else None + + if student_phone and father_phone and student_phone == father_phone: + errors.append("Father's mobile number should not be same as student's mobile number") if missing_fields or errors: error_msg = [] @@ -601,8 +628,33 @@ def save_students_batch(request): students = filtered_students - # Process batch allocation (remove atomic transaction to allow individual student processing) - processed_students = process_batch_allocation(students, programme_type, batch_year) + # Filter out students with validation errors before processing + valid_students_only = [] + skipped_invalid = 0 + + for student in students: + has_validation_errors = False + student_phone = None + father_phone = None + phone_number = student.get('Mobile No') or student.get('phoneNumber', '') + father_mobile = student.get('Father Mobile Number') or student.get('fatherMobile', '') + + if phone_number: + sp = sanitize_phone_number(phone_number) + student_phone = sp.replace(' ', '').replace('-', '') if sp else None + if father_mobile: + fp = sanitize_phone_number(father_mobile) + father_phone = fp.replace(' ', '').replace('-', '') if fp else None + if student_phone and father_phone and student_phone == father_phone: + has_validation_errors = True + skipped_invalid += 1 + errors.append(f"Skipped student {student.get('Name', 'Unknown')}: Father's mobile number should not be same as student's mobile number") + continue + + if not has_validation_errors: + valid_students_only.append(student) + + processed_students = process_batch_allocation(valid_students_only, programme_type, batch_year) for student_data in processed_students: try: @@ -679,8 +731,8 @@ def save_students_batch(request): branch=student_data.get('Discipline') or student_data.get('branch', ''), date_of_birth=dob, - ai_rank=int(sanitize_rank_value(student_data.get('AI rank') or student_data.get('jeeRank', 0) or 0).replace(',', '')) if (student_data.get('AI rank') or student_data.get('jeeRank')) and sanitize_rank_value(student_data.get('AI rank') or student_data.get('jeeRank', 0) or 0).replace(',', '').isdigit() else None, - category_rank=int(sanitize_rank_value(student_data.get('Category Rank') or student_data.get('categoryRank', 0) or 0).replace(',', '')) if (student_data.get('Category Rank') or student_data.get('categoryRank')) and sanitize_rank_value(student_data.get('Category Rank') or student_data.get('categoryRank', 0) or 0).replace(',', '').isdigit() else None, + ai_rank=_safe_int_conversion(sanitize_rank_value(student_data.get('AI rank') or student_data.get('jeeRank'))), + category_rank=_safe_int_conversion(sanitize_rank_value(student_data.get('Category Rank') or student_data.get('categoryRank'))), father_occupation=student_data.get("Father's Occupation") or student_data.get('fatherOccupation', ''), father_mobile=sanitize_phone_number(student_data.get('Father Mobile Number') or student_data.get('fatherMobile', '')), @@ -723,7 +775,7 @@ def save_students_batch(request): error_msg = f"Failed to save student {student_name}: {str(e)}" errors.append(error_msg) - total_processed = successful_uploads + failed_uploads + validation_errors + total_processed = successful_uploads + failed_uploads + validation_errors + skipped_invalid response_data = { 'success': True, 'data': { @@ -731,6 +783,7 @@ def save_students_batch(request): 'failed_uploads': failed_uploads, 'skipped_duplicates': skipped_duplicates, 'validation_errors': validation_errors, + 'skipped_invalid': skipped_invalid, 'total_processed': total_processed, 'original_count': len(data.get('students', [])) }, @@ -744,6 +797,8 @@ def save_students_batch(request): messages.append(f'{successful_uploads} students uploaded successfully') if skipped_duplicates > 0: messages.append(f'{skipped_duplicates} duplicates skipped') + if skipped_invalid > 0: + messages.append(f'{skipped_invalid} students with validation errors skipped') if failed_uploads > 0: messages.append(f'{failed_uploads} uploads failed') if validation_errors > 0: @@ -2568,6 +2623,8 @@ def update_student(request, student_id): try: student = StudentBatchUpload.objects.get(id=student_id) data = json.loads(request.body) + old_discipline = student.branch + discipline_changed = False field_mapping = { # Identification fields @@ -2670,6 +2727,8 @@ def update_student(request, student_id): for field in direct_fields: if field in data: + if field == 'branch' and data[field] != old_discipline: + discipline_changed = True setattr(student, field, data[field]) dob_value = data.get('dob') or data.get('dateOfBirth') or data.get('date_of_birth') @@ -2703,14 +2762,24 @@ def update_student(request, student_id): elif field == 'twelfth_marks': frontend_field = 'twelfthMarks' - value = data.get(frontend_field) or data.get(field) - if value: + # Get value from either frontend field name or backend field name + value = data.get(frontend_field, data.get(field)) + if value is None or value == '' or value == 'null': + setattr(student, field, None) + else: try: if field == 'category_rank': sanitized_value = sanitize_rank_value(value) - setattr(student, field, int(sanitized_value.replace(',', '')) if sanitized_value.replace(',', '').isdigit() else None) + if sanitized_value and sanitized_value.replace(',', '').isdigit(): + setattr(student, field, int(sanitized_value.replace(',', ''))) + else: + setattr(student, field, None) else: - setattr(student, field, int(str(value).replace(',', '')) if str(value).replace(',', '').isdigit() else None) + value_str = str(value).replace(',', '') + if value_str.isdigit(): + setattr(student, field, int(value_str)) + else: + setattr(student, field, None) except (ValueError, TypeError): setattr(student, field, None) @@ -2720,10 +2789,71 @@ def update_student(request, student_id): student.save() + # Handle discipline change using existing batch change API logic if discipline was changed + if discipline_changed and student.reported_status == 'REPORTED': + try: + from applications.academic_information.models import Student as AcademicStudent + from applications.programme_curriculum.models import Batch, Discipline + from applications.academic_procedures.models import BatchChangeHistory + + academic_student = AcademicStudent.objects.filter(id__id=student.roll_number).first() + + if academic_student: + new_discipline_obj = Discipline.objects.filter(name__iexact=student.branch).first() + if not new_discipline_obj: + discipline_upper = student.branch.upper() + if 'COMPUTER SCIENCE' in discipline_upper or 'CSE' in discipline_upper: + new_discipline_obj = Discipline.objects.filter(name__icontains='Computer Science').first() + elif 'ELECTRONICS' in discipline_upper or 'ECE' in discipline_upper: + new_discipline_obj = Discipline.objects.filter(name__icontains='Electronics').first() + elif 'MECHANICAL' in discipline_upper or 'ME' in discipline_upper: + new_discipline_obj = Discipline.objects.filter(name__icontains='Mechanical').first() + elif 'SMART' in discipline_upper or 'MANUFACTURING' in discipline_upper: + new_discipline_obj = Discipline.objects.filter(name__icontains='Smart Manufacturing').first() + elif 'DESIGN' in discipline_upper: + new_discipline_obj = Discipline.objects.filter(name__icontains='Design').first() + + if new_discipline_obj: + new_batch = Batch.objects.filter( + discipline=new_discipline_obj, + year=student.year, + running_batch=True + ).first() + + if new_batch and academic_student.batch_id != new_batch: + old_batch = academic_student.batch_id + BatchChangeHistory.objects.create( + student=academic_student, + old_batch=old_batch, + new_batch=new_batch, + ) + + academic_student.batch_id = new_batch + academic_student.save() + + # Update department and specialization + try: + from applications.globals.models import DepartmentInfo + dept_name = new_batch.discipline.acronym + department = DepartmentInfo.objects.filter(name=dept_name).first() + if department: + academic_student.id.department = department + academic_student.id.save() + academic_student.specialization = dept_name + academic_student.save() + except: + pass + + except Exception as batch_change_error: + pass + return JsonResponse({ 'success': True, 'message': f'Student {student.name} updated successfully', - 'student_id': student.id + 'student_id': student.id, + 'discipline_changed': discipline_changed, + 'old_discipline': old_discipline if discipline_changed else None, + 'new_discipline': student.branch if discipline_changed else None }) except StudentBatchUpload.DoesNotExist: From eb5d63d8ae4a767f7c98f2ea150843fa10f02283 Mon Sep 17 00:00:00 2001 From: vikrantwiz02 Date: Sun, 9 Nov 2025 23:07:40 +0530 Subject: [PATCH 3/3] Filter batch students by programme type based on batch name --- .../programme_curriculum/api/views_student_management.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/FusionIIIT/applications/programme_curriculum/api/views_student_management.py b/FusionIIIT/applications/programme_curriculum/api/views_student_management.py index 87b0e4618..1a860db57 100644 --- a/FusionIIIT/applications/programme_curriculum/api/views_student_management.py +++ b/FusionIIIT/applications/programme_curriculum/api/views_student_management.py @@ -3180,9 +3180,12 @@ def get_batch_students(request, batch_id): 'message': f'Batch with ID {batch_id} not found' }, status=404) + programme_type = 'ug' if batch.name.startswith('B.') else 'pg' if batch.name.startswith('M.') else 'phd' + students = StudentBatchUpload.objects.filter( year=batch.year, - branch__icontains=batch.discipline.name + branch__icontains=batch.discipline.name, + programme_type=programme_type ).order_by('roll_number') upload_students = []