Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
155 changes: 155 additions & 0 deletions home/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,12 @@
from datetime import timedelta

from django.contrib import admin
from django.contrib import messages
from django.contrib.admin.helpers import ACTION_CHECKBOX_NAME
from django.core.exceptions import ValidationError
from django.http import HttpRequest, HttpResponse
from django.shortcuts import render
from django.urls import path
from import_export.admin import ImportExportMixin, ImportExportModelAdmin

from home.models import (
Expand Down Expand Up @@ -282,6 +286,7 @@ class about_Admin(admin.ModelAdmin):

@admin.register(Student)
class about_Admin(ImportExportMixin, admin.ModelAdmin):
change_list_template = "admin/home/student/change_list.html"
resource_class = StudentResource
model = Student
search_fields = ("name", "roll_no", "hostel", "degree", "department", "email")
Expand All @@ -308,6 +313,156 @@ class about_Admin(ImportExportMixin, admin.ModelAdmin):
)
actions = ["export_as_csv", "generate_table", "disable_allocation"]

def get_urls(self):
urls = super().get_urls()
custom_urls = [
path(
"import-csv/",
self.admin_site.admin_view(self.import_csv),
name="home_student_import_csv",
),
]
return custom_urls + urls

def import_csv(self, request):
failures = []
imported_count = 0

if request.method == "POST":
hostel = request.POST.get("hostel", "").strip()
uploaded_file = request.FILES.get("csv_file")

if not hostel:
failures.append({"row": "-", "error": "Hostel name is required."})
elif len(hostel) > Student._meta.get_field("hostel").max_length:
failures.append({"row": "-", "error": "Hostel name is too long."})

if uploaded_file is None:
failures.append({"row": "-", "error": "Please select a CSV file."})

if not failures:
try:
decoded_file = uploaded_file.read().decode("utf-8-sig")
sample = decoded_file[:4096]
try:
dialect = csv.Sniffer().sniff(sample, delimiters=",;\t")
except csv.Error:
dialect = csv.excel

rows = list(csv.reader(decoded_file.splitlines(), dialect))
if not rows:
raise ValueError("The uploaded file is empty.")

def normalize(value):
return "".join(
character.lower() for character in value if character.isalnum()
)

header_aliases = {
"room_no": {"roomno", "roomnumber"},
"roll_no": {"rollno", "rollnumber"},
"name": {"name"},
"degree": {"course", "degree"},
"department": {"department", "dept"},
"email": {
"instituteemailid",
"instituteemail",
"emailid",
"email",
},
}
normalized_header = [normalize(value) for value in rows[0]]
column_indexes = {}
for field, aliases in header_aliases.items():
matching_indexes = [
index
for index, value in enumerate(normalized_header)
if value in aliases
]
if not matching_indexes:
raise ValueError(
f"Missing required column: {field.replace('_', ' ')}."
)
column_indexes[field] = matching_indexes[0]

imported_emails = set()
imported_roll_numbers = set()
for row_number, row in enumerate(rows[1:], start=2):
if not any(value.strip() for value in row):
continue
try:
if len(row) <= max(column_indexes.values()):
raise ValueError(
"The row does not contain all required columns."
)

values = {
field: row[index].strip()
for field, index in column_indexes.items()
}
missing_fields = [
field.replace("_", " ")
for field, value in values.items()
if not value
]
if missing_fields:
raise ValueError(
"Missing value for: " + ", ".join(missing_fields) + "."
)
if values["email"].lower() in imported_emails:
raise ValueError("Duplicate email in the uploaded file.")
if values["roll_no"] in imported_roll_numbers:
raise ValueError("Duplicate roll number in the uploaded file.")
if Student.objects.filter(email__iexact=values["email"]).exists():
raise ValueError("A student with this email already exists.")
if Student.objects.filter(roll_no=values["roll_no"]).exists():
raise ValueError("A student with this roll number already exists.")

student = Student(
hostel=hostel,
room_no=values["room_no"],
roll_no=values["roll_no"],
name=values["name"],
degree=values["degree"],
department=values["department"],
email=values["email"],
)
student.full_clean()
student.save()
imported_count += 1
imported_emails.add(values["email"].lower())
imported_roll_numbers.add(values["roll_no"])
except (ValidationError, ValueError, IndexError) as error:
if isinstance(error, ValidationError):
error_message = "; ".join(error.messages)
else:
error_message = str(error)
failures.append({"row": row_number, "error": error_message})
except (UnicodeDecodeError, ValueError, csv.Error) as error:
failures.append({"row": "-", "error": str(error)})

if imported_count:
self.message_user(
request,
f"Successfully imported {imported_count} student(s).",
messages.SUCCESS,
)
if failures:
self.message_user(
request,
f"{len(failures)} row(s) could not be imported.",
messages.WARNING,
)

context = {
**self.admin_site.each_context(request),
"opts": self.model._meta,
"title": "Import students from CSV",
"failures": failures,
"imported_count": imported_count,
}
return render(request, "admin/home/student/import_csv.html", context)

def generate_table(Student, request, queryset):
"""
Generate action available in the admin page
Expand Down
25 changes: 25 additions & 0 deletions home/migrations/0013_alter_student_hostel.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Generated by Django 5.1.14 on 2026-08-22

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("home", "0012_merge_20260819_1633"),
]

operations = [
migrations.AlterField(
model_name="student",
name="hostel",
field=models.CharField(
blank=True,
default="",
help_text="This contains the hostel of the Student",
max_length=5,
null=True,
verbose_name="Hostel of Student",
),
),
]
2 changes: 1 addition & 1 deletion home/models/students.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ class Student(models.Model):
)
hostel = models.CharField(
_("Hostel of Student"),
max_length=3,
max_length=5,
help_text="This contains the hostel of the Student",
null=True,
default="",
Expand Down
40 changes: 39 additions & 1 deletion home/tests.py
Original file line number Diff line number Diff line change
@@ -1 +1,39 @@
# Create your tests here.
from django.contrib.auth import get_user_model
from django.core.files.uploadedfile import SimpleUploadedFile
from django.test import TestCase
from django.urls import reverse

from home.models import Student


class StudentCsvImportTests(TestCase):
def setUp(self):
user = get_user_model().objects.create_superuser(
username="admin",
email="admin@example.com",
password="test-password",
)
self.client.force_login(user)

def test_imports_valid_rows_and_reports_invalid_rows(self):
csv_content = (
"S.No.,Unit No.,Room No,Roll No,Name,Course,Department,Institute Email ID\n"
"1,A,201,260003011,JOHN DOE,B.Tech,ME,me260003011@iiti.ac.in\n"
"2,B,202,,Missing Roll,B.Tech,ME,missing@example.com\n"
)
upload = SimpleUploadedFile(
"students.csv", csv_content.encode("utf-8"), content_type="text/csv"
)

response = self.client.post(
reverse("admin:home_student_import_csv"),
{"hostel": "AG", "csv_file": upload},
)

self.assertEqual(response.status_code, 200)
self.assertEqual(Student.objects.count(), 1)
student = Student.objects.get()
self.assertEqual(student.hostel, "AG")
self.assertEqual(student.roll_no, "260003011")
self.assertContains(response, "Row")
self.assertContains(response, "Missing value for: roll no.")
8 changes: 8 additions & 0 deletions templates/admin/home/student/change_list.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{% extends "admin/change_list.html" %}

{% block object-tools-items %}
<li>
<a href="import-csv/">Import students from CSV</a>
</li>
{{ block.super }}
{% endblock %}
41 changes: 41 additions & 0 deletions templates/admin/home/student/import_csv.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{% extends "admin/base_site.html" %}

{% block content %}
<h1>Import students from CSV</h1>
<p>Enter the hostel that applies to every row, then upload a CSV or tab-separated file.</p>
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
<fieldset class="module aligned">
<div class="form-row">
<div>
<label for="id_hostel">Hostel name:</label>
<input type="text" name="hostel" id="id_hostel" maxlength="5" required>
</div>
</div>
<div class="form-row">
<div>
<label for="id_csv_file">CSV file:</label>
<input type="file" name="csv_file" id="id_csv_file" accept=".csv,text/csv" required>
</div>
</div>
</fieldset>
<div class="submit-row">
<input type="submit" value="Import students" class="default">
<a href="../" class="button cancel-link">Cancel</a>
</div>
</form>

{% if failures %}
<h2>Rows that could not be imported</h2>
<table>
<thead>
<tr><th>Row</th><th>Reason</th></tr>
</thead>
<tbody>
{% for failure in failures %}
<tr><td>{{ failure.row }}</td><td>{{ failure.error }}</td></tr>
{% endfor %}
</tbody>
</table>
{% endif %}
{% endblock %}
Loading