From ae8ebc777a99d22ef90d7f2ad68d75cadc32274d Mon Sep 17 00:00:00 2001 From: Vikrant Kumar Date: Wed, 12 Aug 2026 23:24:04 +0530 Subject: [PATCH 1/6] Add hindi_name, photo, signature student fields (aadhaar wiring) Add hindi_name (CharField) and photo/signature (ImageField, upload_to media) to StudentBatchUpload and PhdStudentBatchUpload, plus migration 0049. - add_single_student: persist hindi_name + aadhar_number (was dropped) and decode base64 photo/signature into ImageFields via a shared helper. - update_student: hindi_name/aadhar_number via direct_fields; photo/signature replaced only when a new base64 image is sent (unchanged edits keep the file). - get_batch_students: return hindi_name, aadhaar, and photo/signature URLs so UG/PG/PhD lists show them. Images arrive as base64 in the existing JSON payload; stored as real files. --- .../api/views_student_management.py | 48 ++++++++++++++++++- .../migrations/0049_auto_20260812_2309.py | 43 +++++++++++++++++ .../models_student_management.py | 8 +++- 3 files changed, 97 insertions(+), 2 deletions(-) create mode 100644 FusionIIIT/applications/programme_curriculum/migrations/0049_auto_20260812_2309.py diff --git a/FusionIIIT/applications/programme_curriculum/api/views_student_management.py b/FusionIIIT/applications/programme_curriculum/api/views_student_management.py index da6bb662e..e4221ee30 100644 --- a/FusionIIIT/applications/programme_curriculum/api/views_student_management.py +++ b/FusionIIIT/applications/programme_curriculum/api/views_student_management.py @@ -1,4 +1,5 @@ import json +import base64 import pandas as pd import openpyxl import random @@ -7,6 +8,7 @@ import sys import os from io import BytesIO +from django.core.files.base import ContentFile from datetime import datetime, date from django.http import JsonResponse, HttpResponse from django.views.decorators.csrf import csrf_exempt @@ -43,6 +45,26 @@ from django.contrib.auth.models import User pass +def _decode_base64_image(data_url, name_prefix="image"): + """ + Convert a base64 data URL (data:image/png;base64,...) to a ContentFile for an + ImageField. Returns None for empty/invalid input or an already-stored path, so + re-saving an unchanged edit form leaves the existing image untouched. + """ + if not data_url or not isinstance(data_url, str) or ";base64," not in data_url: + return None + header, encoded = data_url.split(";base64,", 1) + try: + raw = base64.b64decode(encoded) + except Exception: + return None + ext = "png" + if "/" in header: + ext = (header.split("/")[-1].split(";")[0] or "png").lower() + if ext == "jpeg": + ext = "jpg" + return ContentFile(raw, name="{}.{}".format(name_prefix, ext)) + def parse_request_data(request, field_mappings=None): """ Helper function to parse request data from various formats with field mapping @@ -1315,8 +1337,13 @@ def add_single_student(request): dob = parse_date_flexible(data.get('date_of_birth')) # Build shared kwargs used by both PhD and UG/PG models + _roll_for_file = str(student_data.get('roll_number') or data.get('rollNumber') or data.get('roll_number') or 'student') _shared_kwargs = dict( name=student_data.get('name') or data.get('name', ''), + hindi_name=data.get('hindi_name', '') or data.get('hindiName', ''), + aadhar_number=(data.get('aadhar_number') or data.get('aadharNumber') or data.get('aadharNo') or ''), + photo=_decode_base64_image(data.get('photo'), "photo_" + _roll_for_file), + signature=_decode_base64_image(data.get('signature'), "sign_" + _roll_for_file), roll_number=student_data.get('roll_number') or data.get('rollNumber', '') or data.get('roll_number', ''), institute_email=student_data.get('institute_email') or data.get('instituteEmail', '') or data.get('institute_email', ''), father_name=data.get('father_name', ''), @@ -3687,7 +3714,8 @@ def update_student(request, student_id): pass direct_fields = [ - 'name', 'gender', 'category', 'pwd', 'minority', 'address', 'state', 'branch', 'specialization', + 'name', 'hindi_name', 'aadhar_number', + 'gender', 'category', 'pwd', 'minority', 'address', 'state', 'branch', 'specialization', 'personal_email', 'parent_email', 'country', 'nationality', 'blood_group', 'blood_group_remarks', 'pwd_category', 'pwd_category_remarks', 'admission_mode', 'admission_mode_remarks', 'income_group', 'income' @@ -3715,6 +3743,17 @@ def update_student(request, student_id): except AttributeError: pass + # Photo / signature: replace only when a new base64 image is sent; an + # unchanged edit form re-sends the existing file path, which is ignored. + for _img_field in ('photo', 'signature'): + if _img_field in data: + _decoded = _decode_base64_image( + data[_img_field], + "{}_{}".format(_img_field, student.roll_number or student_id), + ) + if _decoded is not None: + setattr(student, _img_field, _decoded) + dob_value = data.get('dob') or data.get('dateOfBirth') or data.get('date_of_birth') if dob_value: try: @@ -4434,6 +4473,13 @@ def get_batch_students(request, batch_id): 'mother_occupation': getattr(student, 'mother_occupation', ''), 'mother_mobile': getattr(student, 'mother_mobile', ''), 'aadhar_number': getattr(student, 'aadhar_number', ''), + 'aadharNo': getattr(student, 'aadhar_number', ''), + 'hindi_name': getattr(student, 'hindi_name', ''), + 'hindiName': getattr(student, 'hindi_name', ''), + 'photo': student.photo.url if getattr(student, 'photo', None) else '', + 'signature': student.signature.url + if getattr(student, 'signature', None) + else '', 'allotted_category': getattr(student, 'allotted_category', ''), 'allotted_gender': getattr(student, 'allotted_gender', ''), diff --git a/FusionIIIT/applications/programme_curriculum/migrations/0049_auto_20260812_2309.py b/FusionIIIT/applications/programme_curriculum/migrations/0049_auto_20260812_2309.py new file mode 100644 index 000000000..9e57b6715 --- /dev/null +++ b/FusionIIIT/applications/programme_curriculum/migrations/0049_auto_20260812_2309.py @@ -0,0 +1,43 @@ +# Generated by Django 3.1.5 on 2026-08-12 23:09 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('programme_curriculum', '0048_merge_20260805_2220'), + ] + + operations = [ + migrations.AddField( + model_name='phdstudentbatchupload', + name='hindi_name', + field=models.CharField(blank=True, help_text='Full Name in Hindi', max_length=200, null=True), + ), + migrations.AddField( + model_name='phdstudentbatchupload', + name='photo', + field=models.ImageField(blank=True, help_text='Passport photo (max 200KB)', null=True, upload_to='programme_curriculum/student_photos'), + ), + migrations.AddField( + model_name='phdstudentbatchupload', + name='signature', + field=models.ImageField(blank=True, help_text='Signature image (max 30KB)', null=True, upload_to='programme_curriculum/student_signatures'), + ), + migrations.AddField( + model_name='studentbatchupload', + name='hindi_name', + field=models.CharField(blank=True, help_text='Full Name in Hindi', max_length=200, null=True), + ), + migrations.AddField( + model_name='studentbatchupload', + name='photo', + field=models.ImageField(blank=True, help_text='Passport photo (max 200KB)', null=True, upload_to='programme_curriculum/student_photos'), + ), + migrations.AddField( + model_name='studentbatchupload', + name='signature', + field=models.ImageField(blank=True, help_text='Signature image (max 30KB)', null=True, upload_to='programme_curriculum/student_signatures'), + ), + ] diff --git a/FusionIIIT/applications/programme_curriculum/models_student_management.py b/FusionIIIT/applications/programme_curriculum/models_student_management.py index 94a9fb8e1..6de2b47cd 100644 --- a/FusionIIIT/applications/programme_curriculum/models_student_management.py +++ b/FusionIIIT/applications/programme_curriculum/models_student_management.py @@ -163,6 +163,7 @@ class StudentBatchUpload(models.Model): # Personal information name = models.CharField(max_length=200, help_text="Full Name") + hindi_name = models.CharField(max_length=200, blank=True, null=True, help_text="Full Name in Hindi") father_name = models.CharField(max_length=200, help_text="Father's Name") mother_name = models.CharField(max_length=200, help_text="Mother's Name") gender = models.CharField(max_length=10, choices=GENDER_CHOICES) @@ -203,7 +204,9 @@ class StudentBatchUpload(models.Model): 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) - + photo = models.ImageField(upload_to='programme_curriculum/student_photos', blank=True, null=True, help_text="Passport photo (max 200KB)") + signature = models.ImageField(upload_to='programme_curriculum/student_signatures', blank=True, null=True, help_text="Signature image (max 30KB)") + # Allotment details allotted_category = models.CharField(max_length=50, blank=True, null=True) allotted_gender = models.CharField(max_length=50, blank=True, null=True) @@ -656,6 +659,7 @@ class PhdStudentBatchUpload(models.Model): # ---- Personal information (col 4, 6, 7, 8, 9, 10, 11, 12) ---- name = models.CharField(max_length=200, help_text="Full Name (col: Name)") + hindi_name = models.CharField(max_length=200, blank=True, null=True, help_text="Full Name in Hindi") discipline = models.CharField(max_length=200, help_text="Discipline / Branch (col: Discipline)") admission_type = models.CharField( max_length=100, choices=ADMISSION_TYPE_CHOICES, blank=True, null=True, @@ -728,6 +732,8 @@ class PhdStudentBatchUpload(models.Model): max_length=12, blank=True, null=True, help_text="Aadhaar Number (12 digits)" ) + photo = models.ImageField(upload_to='programme_curriculum/student_photos', blank=True, null=True, help_text="Passport photo (max 200KB)") + signature = models.ImageField(upload_to='programme_curriculum/student_signatures', blank=True, null=True, help_text="Signature image (max 30KB)") # ---- System / batch tracking fields ---- admission_semester = models.CharField( From dbeab9cbbde96bf8b87bf93bae29b6e2c15e8f2c Mon Sep 17 00:00:00 2001 From: Vikrant Kumar Date: Wed, 12 Aug 2026 23:39:59 +0530 Subject: [PATCH 2/6] Reject non-PNG/JPG student images in base64 decoder Defense-in-depth: _decode_base64_image now returns None for any type other than png/jpg/jpeg, matching the frontend restriction. --- .../programme_curriculum/api/views_student_management.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/FusionIIIT/applications/programme_curriculum/api/views_student_management.py b/FusionIIIT/applications/programme_curriculum/api/views_student_management.py index e4221ee30..37ff9f8c0 100644 --- a/FusionIIIT/applications/programme_curriculum/api/views_student_management.py +++ b/FusionIIIT/applications/programme_curriculum/api/views_student_management.py @@ -63,6 +63,8 @@ def _decode_base64_image(data_url, name_prefix="image"): ext = (header.split("/")[-1].split(";")[0] or "png").lower() if ext == "jpeg": ext = "jpg" + if ext not in ("png", "jpg"): + return None return ContentFile(raw, name="{}.{}".format(name_prefix, ext)) def parse_request_data(request, field_mappings=None): From 6b8d7013118c7241eb9c50afc566f9791404a290 Mon Sep 17 00:00:00 2001 From: Vikrant Kumar Date: Wed, 12 Aug 2026 23:51:29 +0530 Subject: [PATCH 3/6] Name uploaded student images by roll number Save photo/signature as _photo. and _sign. on both manual add and update. --- .../programme_curriculum/api/views_student_management.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/FusionIIIT/applications/programme_curriculum/api/views_student_management.py b/FusionIIIT/applications/programme_curriculum/api/views_student_management.py index 37ff9f8c0..2da69edb2 100644 --- a/FusionIIIT/applications/programme_curriculum/api/views_student_management.py +++ b/FusionIIIT/applications/programme_curriculum/api/views_student_management.py @@ -1344,8 +1344,8 @@ def add_single_student(request): name=student_data.get('name') or data.get('name', ''), hindi_name=data.get('hindi_name', '') or data.get('hindiName', ''), aadhar_number=(data.get('aadhar_number') or data.get('aadharNumber') or data.get('aadharNo') or ''), - photo=_decode_base64_image(data.get('photo'), "photo_" + _roll_for_file), - signature=_decode_base64_image(data.get('signature'), "sign_" + _roll_for_file), + photo=_decode_base64_image(data.get('photo'), _roll_for_file + "_photo"), + signature=_decode_base64_image(data.get('signature'), _roll_for_file + "_sign"), roll_number=student_data.get('roll_number') or data.get('rollNumber', '') or data.get('roll_number', ''), institute_email=student_data.get('institute_email') or data.get('instituteEmail', '') or data.get('institute_email', ''), father_name=data.get('father_name', ''), @@ -3749,9 +3749,10 @@ def update_student(request, student_id): # unchanged edit form re-sends the existing file path, which is ignored. for _img_field in ('photo', 'signature'): if _img_field in data: + _suffix = 'photo' if _img_field == 'photo' else 'sign' _decoded = _decode_base64_image( data[_img_field], - "{}_{}".format(_img_field, student.roll_number or student_id), + "{}_{}".format(student.roll_number or student_id, _suffix), ) if _decoded is not None: setattr(student, _img_field, _decoded) From de152c997d9d57a490283a0dbeba037563c8a3e0 Mon Sep 17 00:00:00 2001 From: Vikrant Kumar Date: Thu, 13 Aug 2026 00:25:51 +0530 Subject: [PATCH 4/6] Enforce photo/signature size caps server-side _decode_base64_image now rejects payloads over max_kb (photo 200KB, signature 30KB) so the frontend caps can't be bypassed via a direct API call. --- .../api/views_student_management.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/FusionIIIT/applications/programme_curriculum/api/views_student_management.py b/FusionIIIT/applications/programme_curriculum/api/views_student_management.py index 2da69edb2..87baa37b8 100644 --- a/FusionIIIT/applications/programme_curriculum/api/views_student_management.py +++ b/FusionIIIT/applications/programme_curriculum/api/views_student_management.py @@ -45,11 +45,12 @@ from django.contrib.auth.models import User pass -def _decode_base64_image(data_url, name_prefix="image"): +def _decode_base64_image(data_url, name_prefix="image", max_kb=None): """ Convert a base64 data URL (data:image/png;base64,...) to a ContentFile for an - ImageField. Returns None for empty/invalid input or an already-stored path, so - re-saving an unchanged edit form leaves the existing image untouched. + ImageField. Returns None for empty/invalid input, an already-stored path (so + re-saving an unchanged edit form leaves the existing image untouched), a + non-PNG/JPG type, or a payload larger than max_kb (server-side size guard). """ if not data_url or not isinstance(data_url, str) or ";base64," not in data_url: return None @@ -58,6 +59,8 @@ def _decode_base64_image(data_url, name_prefix="image"): raw = base64.b64decode(encoded) except Exception: return None + if max_kb is not None and len(raw) > max_kb * 1024: + return None ext = "png" if "/" in header: ext = (header.split("/")[-1].split(";")[0] or "png").lower() @@ -1344,8 +1347,8 @@ def add_single_student(request): name=student_data.get('name') or data.get('name', ''), hindi_name=data.get('hindi_name', '') or data.get('hindiName', ''), aadhar_number=(data.get('aadhar_number') or data.get('aadharNumber') or data.get('aadharNo') or ''), - photo=_decode_base64_image(data.get('photo'), _roll_for_file + "_photo"), - signature=_decode_base64_image(data.get('signature'), _roll_for_file + "_sign"), + photo=_decode_base64_image(data.get('photo'), _roll_for_file + "_photo", max_kb=200), + signature=_decode_base64_image(data.get('signature'), _roll_for_file + "_sign", max_kb=30), roll_number=student_data.get('roll_number') or data.get('rollNumber', '') or data.get('roll_number', ''), institute_email=student_data.get('institute_email') or data.get('instituteEmail', '') or data.get('institute_email', ''), father_name=data.get('father_name', ''), @@ -3753,6 +3756,7 @@ def update_student(request, student_id): _decoded = _decode_base64_image( data[_img_field], "{}_{}".format(student.roll_number or student_id, _suffix), + max_kb=200 if _img_field == 'photo' else 30, ) if _decoded is not None: setattr(student, _img_field, _decoded) From b2bf5083c4b72cda0448ae36c6e45c67a779480e Mon Sep 17 00:00:00 2001 From: Vikrant Kumar Date: Thu, 13 Aug 2026 00:30:50 +0530 Subject: [PATCH 5/6] Delete old photo/signature file when replaced on edit Prevents orphaned image files accumulating in media/ on re-upload. --- .../programme_curriculum/api/views_student_management.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/FusionIIIT/applications/programme_curriculum/api/views_student_management.py b/FusionIIIT/applications/programme_curriculum/api/views_student_management.py index 87baa37b8..762802f4c 100644 --- a/FusionIIIT/applications/programme_curriculum/api/views_student_management.py +++ b/FusionIIIT/applications/programme_curriculum/api/views_student_management.py @@ -3759,6 +3759,13 @@ def update_student(request, student_id): max_kb=200 if _img_field == 'photo' else 30, ) if _decoded is not None: + # Remove the previous file so replacements don't orphan on disk. + _old = getattr(student, _img_field, None) + if _old: + try: + _old.delete(save=False) + except Exception: + pass setattr(student, _img_field, _decoded) dob_value = data.get('dob') or data.get('dateOfBirth') or data.get('date_of_birth') From 570561a89949bc1818a04bbfd46a462ae0527003 Mon Sep 17 00:00:00 2001 From: Vikrant Kumar Date: Thu, 13 Aug 2026 00:51:44 +0530 Subject: [PATCH 6/6] Add regression tests for student image upload feature 13 tests covering: - _decode_base64_image branches (valid png/jpeg naming, oversize/type/empty rejects) - model persistence of hindi_name/aadhar/photo/signature on both upload models - update_student endpoint: auth required, image+text round-trip, oversize rejected, replacement deletes old file, unchanged path preserved Add Fusion.settings.test_settings (disables historical migrations) so the suite builds a schema from current models; several legacy RunSQL migrations can't run against an empty DB. Run: manage.py test applications.programme_curriculum.test_upcoming_batches \ --settings=Fusion.settings.test_settings --noinput --- FusionIIIT/Fusion/settings/test_settings.py | 16 ++ .../test_upcoming_batches.py | 195 ++++++++++++++++++ 2 files changed, 211 insertions(+) create mode 100644 FusionIIIT/Fusion/settings/test_settings.py create mode 100644 FusionIIIT/applications/programme_curriculum/test_upcoming_batches.py diff --git a/FusionIIIT/Fusion/settings/test_settings.py b/FusionIIIT/Fusion/settings/test_settings.py new file mode 100644 index 000000000..4093b58c3 --- /dev/null +++ b/FusionIIIT/Fusion/settings/test_settings.py @@ -0,0 +1,16 @@ +# Test settings: build the test schema directly from the current models instead +# of replaying historical migrations. Several legacy migrations reference tables +# via raw SQL before those tables exist, so they cannot run against an empty +# database (the dev DB is loaded from a prod dump, not migrated from zero). +from Fusion.settings.development import * # noqa: F401,F403 + + +class _DisableMigrations: + def __contains__(self, item): + return True + + def __getitem__(self, item): + return None + + +MIGRATION_MODULES = _DisableMigrations() diff --git a/FusionIIIT/applications/programme_curriculum/test_upcoming_batches.py b/FusionIIIT/applications/programme_curriculum/test_upcoming_batches.py new file mode 100644 index 000000000..9837e29a5 --- /dev/null +++ b/FusionIIIT/applications/programme_curriculum/test_upcoming_batches.py @@ -0,0 +1,195 @@ +""" +Regression tests for the Admin "Upcoming Batches" student-image feature: +hindi_name / aadhar_number text fields and base64 photo/signature uploads +persisted through the model and the update_student endpoint. + +Run: python manage.py test applications.programme_curriculum.test_upcoming_batches +""" +import base64 +import json +import shutil +import tempfile + +from django.test import TestCase, Client, override_settings +from django.utils import timezone +from django.contrib.auth.models import User +from rest_framework.authtoken.models import Token + +from applications.globals.models import Designation, HoldsDesignation +from applications.programme_curriculum.models_student_management import ( + StudentBatchUpload, + PhdStudentBatchUpload, +) +from applications.programme_curriculum.api.views_student_management import ( + _decode_base64_image, +) + +# 1x1 pixel PNG as a data URL. +PNG_1x1 = ( + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0l" + "EQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==" +) +JPG_1x1 = "data:image/jpeg;base64," + PNG_1x1.split(",", 1)[1] +GIF_1x1 = ( + "data:image/gif;base64,R0lGODdhAQABAIAAAP///////ywAAAAAAQABAAACAkQBADs=" +) + + +def _oversized_png(kb): + raw = base64.b64encode(b"\x00" * (kb * 1024)).decode() + return "data:image/png;base64," + raw + + +class DecodeBase64ImageTests(TestCase): + def test_valid_png_returns_named_contentfile(self): + cf = _decode_base64_image(PNG_1x1, "26MCSA01_photo", max_kb=200) + self.assertIsNotNone(cf) + self.assertEqual(cf.name, "26MCSA01_photo.png") + self.assertGreater(cf.size, 0) + + def test_jpeg_is_named_jpg(self): + cf = _decode_base64_image(JPG_1x1, "x_sign", max_kb=30) + self.assertIsNotNone(cf) + self.assertEqual(cf.name, "x_sign.jpg") + + def test_oversized_is_rejected(self): + self.assertIsNone(_decode_base64_image(_oversized_png(210), "x", max_kb=200)) + + def test_non_png_jpg_type_is_rejected(self): + self.assertIsNone(_decode_base64_image(GIF_1x1, "x", max_kb=200)) + + def test_existing_media_path_is_ignored(self): + self.assertIsNone(_decode_base64_image("/media/x/y_photo.png", "x")) + + def test_empty_or_none_is_ignored(self): + self.assertIsNone(_decode_base64_image("", "x")) + self.assertIsNone(_decode_base64_image(None, "x")) + + +@override_settings(MEDIA_ROOT=tempfile.mkdtemp(prefix="fusion_test_media_")) +class StudentImageModelTests(TestCase): + def test_new_fields_and_image_persist(self): + cf = _decode_base64_image(PNG_1x1, "R1_photo", max_kb=200) + student = StudentBatchUpload.objects.create( + name="Test Student", + father_name="Father", + mother_name="Mother", + gender="Male", + category="GEN", + address="Somewhere", + branch="Computer Science and Engineering", + programme_type="pg", + hindi_name="टेस्ट नाम", + aadhar_number="123456789012", + photo=cf, + ) + student.refresh_from_db() + self.assertEqual(student.hindi_name, "टेस्ट नाम") + self.assertEqual(student.aadhar_number, "123456789012") + self.assertTrue(student.photo) + self.assertIn("R1_photo", student.photo.name) + + def test_phd_model_has_the_new_fields(self): + for field in ("hindi_name", "photo", "signature", "aadhar_number"): + # Raises FieldDoesNotExist if a field is missing. + PhdStudentBatchUpload._meta.get_field(field) + + +@override_settings(MEDIA_ROOT=tempfile.mkdtemp(prefix="fusion_test_media_")) +class UpdateStudentImageEndpointTests(TestCase): + def setUp(self): + self.client = Client() + self.user = User.objects.create_user(username="acad_test", password="pw") + self.token = Token.objects.create(user=self.user) + designation = Designation.objects.create(name="acadadmin") + HoldsDesignation.objects.create( + user=self.user, + working=self.user, + designation=designation, + held_at=timezone.now(), + ) + self.student = StudentBatchUpload.objects.create( + name="Round Trip", + father_name="Father", + mother_name="Mother", + gender="Male", + category="GEN", + address="Somewhere", + branch="Computer Science and Engineering", + programme_type="pg", + roll_number="26MCSA99", + ) + + def tearDown(self): + # Isolate tests: clear uploaded files so filenames don't collide/dedup. + import os + from django.conf import settings + + for root, _dirs, files in os.walk(settings.MEDIA_ROOT): + for name in files: + os.remove(os.path.join(root, name)) + + def _put(self, payload): + return self.client.put( + "/programme_curriculum/api/student/{}/update/".format(self.student.id), + data=json.dumps(payload), + content_type="application/json", + HTTP_AUTHORIZATION="Token {}".format(self.token.key), + ) + + def test_requires_authorization(self): + resp = self.client.put( + "/programme_curriculum/api/student/{}/update/".format(self.student.id), + data=json.dumps({"hindi_name": "x"}), + content_type="application/json", + ) + self.assertEqual(resp.status_code, 403) + + def test_update_persists_text_and_images(self): + resp = self._put( + { + "programmeType": "pg", + "hindi_name": "हिंदी नाम", + "aadhar_number": "111122223333", + "photo": PNG_1x1, + "signature": JPG_1x1, + } + ) + self.assertEqual(resp.status_code, 200) + self.student.refresh_from_db() + self.assertEqual(self.student.hindi_name, "हिंदी नाम") + self.assertEqual(self.student.aadhar_number, "111122223333") + import os + + photo_name = os.path.basename(self.student.photo.name) + sign_name = os.path.basename(self.student.signature.name) + self.assertTrue(photo_name.startswith("26MCSA99_photo")) + self.assertTrue(photo_name.endswith(".png")) + self.assertTrue(sign_name.startswith("26MCSA99_sign")) + self.assertTrue(sign_name.endswith(".jpg")) + + def test_oversized_image_is_not_saved(self): + resp = self._put({"programmeType": "pg", "photo": _oversized_png(210)}) + self.assertEqual(resp.status_code, 200) + self.student.refresh_from_db() + self.assertFalse(self.student.photo) + + def test_replacing_image_deletes_old_file(self): + self._put({"programmeType": "pg", "photo": PNG_1x1}) + self.student.refresh_from_db() + old_path = self.student.photo.path + import os + + self.assertTrue(os.path.exists(old_path)) + self._put({"programmeType": "pg", "photo": JPG_1x1}) + self.student.refresh_from_db() + self.assertFalse(os.path.exists(old_path)) + + def test_unchanged_image_path_is_preserved(self): + self._put({"programmeType": "pg", "photo": PNG_1x1}) + self.student.refresh_from_db() + saved_name = self.student.photo.name + # Re-sending the stored path (as the edit form does) must not wipe it. + self._put({"programmeType": "pg", "photo": "/media/" + saved_name}) + self.student.refresh_from_db() + self.assertEqual(self.student.photo.name, saved_name)