Bulk import students, bulk change course

This commit is contained in:
2025-11-18 22:18:12 +01:00
parent 34eabe6af7
commit edb54e9f6f
8 changed files with 113 additions and 52 deletions

View File

@@ -1,15 +1,21 @@
from django import forms from django import forms
from django.db.models.query import QuerySet from django.db.models.query import QuerySet
from django.http import HttpRequest, HttpResponse from django.http import HttpRequest, HttpResponse
from django.contrib import admin from django.contrib import admin, messages
from django.contrib import messages
from django.utils.translation import ngettext from django.utils.translation import ngettext
from django.utils.safestring import SafeText from django.utils.safestring import SafeText
from durationwidget.widgets import TimeDurationWidget
from datetime import date
import nested_admin 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.courses import Course
from .models.hourbuildings import HourBuilding, HourBuildingLeg from .models.hourbuildings import HourBuilding, HourBuildingLeg
from .models.missions import Training, MissionProfile from .models.missions import Training, MissionProfile
@@ -17,10 +23,10 @@ from .models.students import Student
from .models.weekpref import WeekPreference from .models.weekpref import WeekPreference
from .custom.colortag import course_color from .custom.colortag import course_color
from .custom.defpassword import default_password
from .actions.exportweek import export_selected from .actions.exportweek import export_selected
from datetime import date
class TrainingForm(forms.ModelForm): class TrainingForm(forms.ModelForm):
model=Training model=Training
@@ -198,10 +204,38 @@ class WeekPreferenceAdmin(nested_admin.NestedModelAdmin):
obj.student = request.user.student obj.student = request.user.student
super().save_model(request, obj, form, change) 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_display = ("surname", "name", "course", "course_color", "email", "phone", "password", "active")
list_filter = ("course", "active") list_filter = ("course", "active")
actions = ("disable_students",) actions = ("change_course", "disable_students")
resource_classes = [StudentResource]
@admin.display(description="Color") @admin.display(description="Color")
def course_color(self, obj: Student) -> SafeText: def course_color(self, obj: Student) -> SafeText:
@@ -211,13 +245,24 @@ class StudentAdmin(admin.ModelAdmin):
@admin.display(description="Password") @admin.display(description="Password")
def password(self, obj: Student) -> SafeText: def password(self, obj: Student) -> SafeText:
return SafeText(default_password(student=obj)) return SafeText(obj.default_password())
@admin.action(description="Disable Students") @admin.action(description="Disable Students")
def disable_students(modeladmin, request: HttpRequest, queryset: QuerySet[Student]): def disable_students(self, request: HttpRequest, queryset: QuerySet[Student]):
queryset.update(active = False) 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 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): class CourseAdmin(admin.ModelAdmin):
list_display = ("ctype", "cnumber","color_display", "year") list_display = ("ctype", "cnumber","color_display", "year")
list_filter = ("ctype", "year") list_filter = ("ctype", "year")

View File

@@ -3,7 +3,3 @@ from django.apps import AppConfig
class FlightslotConfig(AppConfig): class FlightslotConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField' default_auto_field = 'django.db.models.BigAutoField'
name = 'flightslot' name = 'flightslot'
def ready(self):
# Import only when application is ready otherwise signals will not be called
from . import signals

View File

@@ -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}"

View File

@@ -1,5 +1,5 @@
from django.db import models 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 from ..models.courses import Course
@@ -46,5 +46,35 @@ class Student(models.Model):
blank=True 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): def __str__(self):
return f"{self.surname} {self.name[0]}. => {self.course}" return f"{self.surname} {self.name[0]}. => {self.course}"

View File

@@ -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()

17
techdb/poetry.lock generated
View File

@@ -51,6 +51,21 @@ tzdata = {version = "*", markers = "sys_platform == \"win32\""}
argon2 = ["argon2-cffi (>=19.1.0)"] argon2 = ["argon2-cffi (>=19.1.0)"]
bcrypt = ["bcrypt"] 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]] [[package]]
name = "django-colorfield" name = "django-colorfield"
version = "0.14.0" version = "0.14.0"
@@ -406,4 +421,4 @@ files = [
[metadata] [metadata]
lock-version = "2.1" lock-version = "2.1"
python-versions = "^3.12" python-versions = "^3.12"
content-hash = "e231b5570d8b02b46736a58612eab986373b3231b23437cad90d489ea97ecb5b" content-hash = "6bf43236f441d8b6bf8d1928910d169d3b29cfa499bb7d09d97ea227f8115658"

View File

@@ -14,6 +14,7 @@ django-durationwidget = "^1.0.5"
django-import-export = "^4.3.13" django-import-export = "^4.3.13"
django-colorfield = "^0.14.0" django-colorfield = "^0.14.0"
openpyxl = "^3.1.5" openpyxl = "^3.1.5"
django-admin-action-forms = "^2.2.1"
[build-system] [build-system]

View File

@@ -41,9 +41,17 @@ INSTALLED_APPS = [
'nested_admin', 'nested_admin',
'flightslot', 'flightslot',
'durationwidget', '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 = [ MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware', 'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware',