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.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")

View File

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

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

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)"]
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"

View File

@@ -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]

View File

@@ -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',