From edb54e9f6f5f26915878d52c17ec6af02a663e45 Mon Sep 17 00:00:00 2001 From: Emanuele Date: Tue, 18 Nov 2025 22:18:12 +0100 Subject: [PATCH] Bulk import students, bulk change course --- techdb/flightslot/admin.py | 67 +++++++++++++++++++++---- techdb/flightslot/apps.py | 4 -- techdb/flightslot/custom/defpassword.py | 4 -- techdb/flightslot/models/students.py | 32 +++++++++++- techdb/flightslot/signals.py | 30 ----------- techdb/poetry.lock | 17 ++++++- techdb/pyproject.toml | 1 + techdb/techdb/settings.py | 10 +++- 8 files changed, 113 insertions(+), 52 deletions(-) delete mode 100644 techdb/flightslot/custom/defpassword.py delete mode 100644 techdb/flightslot/signals.py diff --git a/techdb/flightslot/admin.py b/techdb/flightslot/admin.py index 47dc88f..f60549c 100644 --- a/techdb/flightslot/admin.py +++ b/techdb/flightslot/admin.py @@ -1,15 +1,21 @@ from django import forms from django.db.models.query import QuerySet from django.http import HttpRequest, HttpResponse -from django.contrib import admin -from django.contrib import messages +from django.contrib import admin, messages from django.utils.translation import ngettext from django.utils.safestring import SafeText -from durationwidget.widgets import TimeDurationWidget -from datetime import date import nested_admin +from durationwidget.widgets import TimeDurationWidget + +from import_export import fields +from import_export.admin import ImportMixin +from import_export.resources import ModelResource +from import_export.widgets import CharWidget + +from django_admin_action_forms import AdminActionFormsMixin, AdminActionForm, action_with_form + from .models.courses import Course from .models.hourbuildings import HourBuilding, HourBuildingLeg from .models.missions import Training, MissionProfile @@ -17,10 +23,10 @@ from .models.students import Student from .models.weekpref import WeekPreference from .custom.colortag import course_color -from .custom.defpassword import default_password - from .actions.exportweek import export_selected +from datetime import date + class TrainingForm(forms.ModelForm): model=Training @@ -198,10 +204,38 @@ class WeekPreferenceAdmin(nested_admin.NestedModelAdmin): obj.student = request.user.student super().save_model(request, obj, form, change) -class StudentAdmin(admin.ModelAdmin): + +# Resource Class for Student data import +class StudentResource(ModelResource): + surname = fields.Field(attribute="surname", column_name="surname", widget=CharWidget()) + name = fields.Field(attribute="name", column_name="name", widget=CharWidget()) + email = fields.Field(attribute="email", column_name="email", widget=CharWidget()) + phone = fields.Field(attribute="phone", column_name="phone", widget=CharWidget()) + + # Cleanup fields before entering + def before_import_row(self, row: dict[str, str], **kwargs) -> None: + row['name'] = SafeText(row['name'].capitalize().strip()) + row['surname'] = SafeText(row['surname'].capitalize().strip()) + row['phone'] = SafeText(row['phone'].replace(' ','')) + row['email'] = SafeText(row['email'].lower().strip()) + return super().before_import_row(row, **kwargs) + + class Meta: + model = Student + skip_unchanged = True + report_skipped = True + fields = ('surname', 'name', 'email', 'phone') + import_id_fields = ['email', 'phone'] + +# Form Class for Student course change +class ChangeCourseForm(AdminActionForm): + course = forms.ModelChoiceField(queryset=Course.objects.all()) + +class StudentAdmin(ImportMixin, AdminActionFormsMixin, admin.ModelAdmin): list_display = ("surname", "name", "course", "course_color", "email", "phone", "password", "active") list_filter = ("course", "active") - actions = ("disable_students",) + actions = ("change_course", "disable_students") + resource_classes = [StudentResource] @admin.display(description="Color") def course_color(self, obj: Student) -> SafeText: @@ -211,12 +245,23 @@ class StudentAdmin(admin.ModelAdmin): @admin.display(description="Password") def password(self, obj: Student) -> SafeText: - return SafeText(default_password(student=obj)) + return SafeText(obj.default_password()) @admin.action(description="Disable Students") - def disable_students(modeladmin, request: HttpRequest, queryset: QuerySet[Student]): - queryset.update(active = False) + def disable_students(self, request: HttpRequest, queryset: QuerySet[Student]): + for q in queryset.all(): + if q.user: + q.user.is_staff = False + q.user.save() + count: int = queryset.update(active = False) + messages.success(request, f"{count} students deactivated") pass + + @action_with_form(ChangeCourseForm, description="Change Student Course") + def change_course(self, request: HttpRequest, queryset: QuerySet[Student], data): + course = data["course"] + count: int = queryset.update(course=course) + messages.success(request, f"{count} students updated to {course}") class CourseAdmin(admin.ModelAdmin): list_display = ("ctype", "cnumber","color_display", "year") diff --git a/techdb/flightslot/apps.py b/techdb/flightslot/apps.py index a8acc38..d016070 100644 --- a/techdb/flightslot/apps.py +++ b/techdb/flightslot/apps.py @@ -3,7 +3,3 @@ from django.apps import AppConfig class FlightslotConfig(AppConfig): default_auto_field = 'django.db.models.BigAutoField' name = 'flightslot' - - def ready(self): - # Import only when application is ready otherwise signals will not be called - from . import signals \ No newline at end of file diff --git a/techdb/flightslot/custom/defpassword.py b/techdb/flightslot/custom/defpassword.py deleted file mode 100644 index 97aedff..0000000 --- a/techdb/flightslot/custom/defpassword.py +++ /dev/null @@ -1,4 +0,0 @@ -from ..models.students import Student - -def default_password(student: Student) -> str: - return f"{student.name.lower()[0]}{student.surname.lower()}{student.id}" diff --git a/techdb/flightslot/models/students.py b/techdb/flightslot/models/students.py index 14dc2f0..973e9d7 100644 --- a/techdb/flightslot/models/students.py +++ b/techdb/flightslot/models/students.py @@ -1,5 +1,5 @@ from django.db import models -from django.contrib.auth.models import User +from django.contrib.auth.models import User, Group from ..models.courses import Course @@ -46,5 +46,35 @@ class Student(models.Model): blank=True ) + def default_password(self) -> str: + return f"{self.name.lower()[0]}{self.surname.lower()}{self.id}" + + # Override save method to add user for login upon Student creation + def save(self, *args, **kwargs): + creating = self.pk is None + super().save(*args, **kwargs) + if creating and not self.user: + username = f"{self.name.lower()}.{self.surname.lower()}" + # Avoid username conflict with progressive number + base_username = username + counter = 1 + while User.objects.filter(username=username).exists(): + username = f"{base_username}{counter}" + counter += 1 + # Create user + user = User.objects.create_user( + first_name=self.name, + last_name=self.surname, + username=username, + email=self.email, + password=self.default_password(), + is_staff=True + ) + + student_group, _ = Group.objects.get_or_create(name="StudentGroup") + user.groups.add(student_group) + self.user = user + self.save() + def __str__(self): return f"{self.surname} {self.name[0]}. => {self.course}" diff --git a/techdb/flightslot/signals.py b/techdb/flightslot/signals.py deleted file mode 100644 index 39433e9..0000000 --- a/techdb/flightslot/signals.py +++ /dev/null @@ -1,30 +0,0 @@ -from django.db.models.signals import post_save -from django.dispatch import receiver -from django.contrib.auth.models import User -from django.contrib.auth.models import Group -from .models.students import Student -from .custom.defpassword import default_password - - -# Create a Django user every time a new student is created -@receiver(post_save, sender=Student) -def create_user_for_student(sender: Student, student: Student, created, **kwargs): - if created and not student.user: - username = f"{student.name.lower()}.{student.surname.lower()}" - # Avoid username conflict with progressive number - base_username = username - counter = 1 - while User.objects.filter(username=username).exists(): - username = f"{base_username}{counter}" - counter += 1 - # Create user - user = User.objects.create_user( - username=username, - email=student.email, - password=default_password(student=student) - ) - - student_group, _ = Group.objects.get_or_create(name="StudentGroup") - user.groups.add(student_group) - student.user = user - student.save() diff --git a/techdb/poetry.lock b/techdb/poetry.lock index b2b4481..11506dc 100644 --- a/techdb/poetry.lock +++ b/techdb/poetry.lock @@ -51,6 +51,21 @@ tzdata = {version = "*", markers = "sys_platform == \"win32\""} argon2 = ["argon2-cffi (>=19.1.0)"] bcrypt = ["bcrypt"] +[[package]] +name = "django-admin-action-forms" +version = "2.2.1" +description = "Extension for the Django admin panel that allows passing additional parameters to actions by creating intermediate pages with forms." +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "django_admin_action_forms-2.2.1-py3-none-any.whl", hash = "sha256:597d20d36fcb6cfbb0b5e0ed83df0c9dbd5af6b225f7af24f5b96a2ed84d4d35"}, + {file = "django_admin_action_forms-2.2.1.tar.gz", hash = "sha256:94ff59964ece5d6b8d2c9c307f22837863be173e3fb64fdcd64d6f301e1d0c9d"}, +] + +[package.dependencies] +django = ">=3.2" + [[package]] name = "django-colorfield" version = "0.14.0" @@ -406,4 +421,4 @@ files = [ [metadata] lock-version = "2.1" python-versions = "^3.12" -content-hash = "e231b5570d8b02b46736a58612eab986373b3231b23437cad90d489ea97ecb5b" +content-hash = "6bf43236f441d8b6bf8d1928910d169d3b29cfa499bb7d09d97ea227f8115658" diff --git a/techdb/pyproject.toml b/techdb/pyproject.toml index 20a396e..3735a1f 100644 --- a/techdb/pyproject.toml +++ b/techdb/pyproject.toml @@ -14,6 +14,7 @@ django-durationwidget = "^1.0.5" django-import-export = "^4.3.13" django-colorfield = "^0.14.0" openpyxl = "^3.1.5" +django-admin-action-forms = "^2.2.1" [build-system] diff --git a/techdb/techdb/settings.py b/techdb/techdb/settings.py index 3257511..bedb4b9 100644 --- a/techdb/techdb/settings.py +++ b/techdb/techdb/settings.py @@ -41,9 +41,17 @@ INSTALLED_APPS = [ 'nested_admin', 'flightslot', 'durationwidget', - 'colorfield' + 'colorfield', + 'import_export', + 'django_admin_action_forms', ] +# Import Export plugin settings +IMPORT_EXPORT_USE_TRANSACTIONS = True +IMPORT_EXPORT_SKIP_ADMIN_LOG = True +from import_export.formats.base_formats import CSV, XLSX +IMPORT_FORMATS = [CSV, XLSX] + MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware',