|
| 1 | +from datetime import datetime |
| 2 | + |
| 3 | +import pandas as pd |
1 | 4 | from django.contrib.auth import get_user_model
|
| 5 | +from django.db import transaction |
2 | 6 | from django_filters import rest_framework as django_filters
|
3 | 7 | from drf_spectacular.utils import OpenApiParameter, OpenApiTypes, extend_schema
|
4 |
| -from rest_framework import filters, viewsets |
| 8 | +from rest_framework import filters, status, viewsets |
5 | 9 | from rest_framework.permissions import IsAuthenticated
|
| 10 | +from rest_framework.response import Response |
| 11 | +from rest_framework.views import APIView |
6 | 12 |
|
| 13 | +from apps.entities.models.faculty_models import Department, Faculty |
| 14 | +from apps.entities.models.student_models import Student |
7 | 15 | from apps.entities.serializers.entity_serializers import EntitySerializer
|
| 16 | +from apps.entities.utils import generate_password |
| 17 | +from apps.rbac.models.role_models import Role, UserRole |
8 | 18 | from apps.rbac.permissions import IsUserSuperAdmin, IsUserSuperAdminOrFacultyAdmin
|
9 | 19 |
|
10 | 20 | User = get_user_model()
|
@@ -38,7 +48,11 @@ class EntityViewSet(viewsets.ModelViewSet):
|
38 | 48 |
|
39 | 49 | permission_classes = (IsAuthenticated,)
|
40 | 50 | serializer_class = EntitySerializer
|
41 |
| - filter_backends = (django_filters.DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter) |
| 51 | + filter_backends = ( |
| 52 | + django_filters.DjangoFilterBackend, |
| 53 | + filters.SearchFilter, |
| 54 | + filters.OrderingFilter, |
| 55 | + ) |
42 | 56 | filterset_fields = (
|
43 | 57 | "student__faculty",
|
44 | 58 | "student__department",
|
@@ -106,3 +120,126 @@ def get_serializer_context(self):
|
106 | 120 | entity_type = self.kwargs.get("entity_type")
|
107 | 121 | context.update({"entity_type": entity_type})
|
108 | 122 | return context
|
| 123 | + |
| 124 | + |
| 125 | +class UploadStudents(APIView): |
| 126 | + def parse_date(self, date_str): |
| 127 | + try: |
| 128 | + return datetime.strptime(date_str, "%d-%m-%Y").strftime("%Y-%m-%d") |
| 129 | + except ValueError: |
| 130 | + return None |
| 131 | + |
| 132 | + def post(self, request, *args, **kwargs): |
| 133 | + file_obj = request.FILES.get("file") |
| 134 | + |
| 135 | + if not file_obj: |
| 136 | + return Response({"error": "No file provided"}, status=status.HTTP_400_BAD_REQUEST) |
| 137 | + |
| 138 | + file_extension = file_obj.name.split(".")[-1].lower() |
| 139 | + if file_extension not in ["xlsx", "xls", "csv"]: |
| 140 | + return Response( |
| 141 | + {"error": "Unsupported file type. Only Excel and CSV files are accepted."}, |
| 142 | + status=status.HTTP_400_BAD_REQUEST, |
| 143 | + ) |
| 144 | + |
| 145 | + try: |
| 146 | + if file_extension in ["xlsx", "xls"]: |
| 147 | + df = pd.read_excel(file_obj) |
| 148 | + elif file_extension == "csv": |
| 149 | + df = pd.read_csv(file_obj) |
| 150 | + |
| 151 | + required_columns = { |
| 152 | + "username", |
| 153 | + "first_name", |
| 154 | + "middle_name", |
| 155 | + "last_name", |
| 156 | + "gender", |
| 157 | + "faculty", |
| 158 | + "department", |
| 159 | + "year_in_school", |
| 160 | + "admission_date", |
| 161 | + "graduation_date", |
| 162 | + } |
| 163 | + if not required_columns.issubset(df.columns): |
| 164 | + print(df.columns) |
| 165 | + return Response( |
| 166 | + {"error": "All fields are required in the file."}, |
| 167 | + status=status.HTTP_400_BAD_REQUEST, |
| 168 | + ) |
| 169 | + |
| 170 | + errors = [] |
| 171 | + success_count = 0 |
| 172 | + |
| 173 | + # TODO This should be a background task (Use Celery) |
| 174 | + for index, entry in df.iterrows(): |
| 175 | + error = {} |
| 176 | + if not all([entry[col] for col in required_columns]): |
| 177 | + error["row"] = index + 1 |
| 178 | + error["message"] = "Missing required fields" |
| 179 | + errors.append(error) |
| 180 | + continue |
| 181 | + |
| 182 | + if User.objects.filter(username=entry["username"]).exists(): |
| 183 | + error["row"] = index + 1 |
| 184 | + error["message"] = "Username already exists" |
| 185 | + errors.append(error) |
| 186 | + continue |
| 187 | + |
| 188 | + admission_date = self.parse_date(entry["admission_date"]) |
| 189 | + graduation_date = self.parse_date(entry["graduation_date"]) |
| 190 | + |
| 191 | + if not admission_date or not graduation_date: |
| 192 | + error["row"] = index + 1 |
| 193 | + error["message"] = "Invalid date format" |
| 194 | + errors.append(error) |
| 195 | + continue |
| 196 | + |
| 197 | + first_name = entry["first_name"] |
| 198 | + middle_name = entry["middle_name"] |
| 199 | + last_name = entry["last_name"] |
| 200 | + |
| 201 | + password = generate_password(last_name) |
| 202 | + |
| 203 | + with transaction.atomic(): |
| 204 | + try: |
| 205 | + user = User.objects.create_user( |
| 206 | + first_name=first_name, |
| 207 | + middle_name=middle_name, |
| 208 | + last_name=last_name, |
| 209 | + gender=entry["gender"], |
| 210 | + username=entry["username"], |
| 211 | + password=password, |
| 212 | + is_first_time_login=True, |
| 213 | + ) |
| 214 | + faculty = Faculty.objects.get(name=entry["faculty"]) |
| 215 | + department = Department.objects.get(name=entry["department"]) |
| 216 | + Student.objects.create( |
| 217 | + user=user, |
| 218 | + faculty=faculty, |
| 219 | + department=department, |
| 220 | + year_in_school=entry["year_in_school"], |
| 221 | + admission_date=admission_date, |
| 222 | + graduation_date=graduation_date, |
| 223 | + ) |
| 224 | + role = Role.objects.get(name="Student") |
| 225 | + UserRole.objects.create(user=user, role=role) |
| 226 | + success_count += 1 |
| 227 | + except Exception: |
| 228 | + error["row"] = index + 1 |
| 229 | + error["message"] = "Something went wrong." |
| 230 | + errors.append(error) |
| 231 | + |
| 232 | + # TODO Allow users to download error log |
| 233 | + |
| 234 | + return Response( |
| 235 | + { |
| 236 | + "message": f"{success_count} students created successfully. {len(errors)} entries failed.", |
| 237 | + }, |
| 238 | + status=status.HTTP_201_CREATED, |
| 239 | + ) |
| 240 | + |
| 241 | + except Exception: |
| 242 | + return Response( |
| 243 | + {"error": "Something went wrong. Please try again later."}, |
| 244 | + status=status.HTTP_400_BAD_REQUEST, |
| 245 | + ) |
0 commit comments