instructor-class #3

Merged
Obbart merged 3 commits from instructor-class into flightslot 2025-12-02 12:33:42 +01:00
12 changed files with 283 additions and 13 deletions
Showing only changes of commit 5d1686f24b - Show all commits

View File

@@ -43,7 +43,8 @@ INSTALLED_APPS = [
'colorfield', 'colorfield',
'import_export', 'import_export',
'django_admin_action_forms', 'django_admin_action_forms',
'polymorphic' 'polymorphic',
"phonenumber_field",
] ]
# Import Export plugin settings # Import Export plugin settings

View File

@@ -47,7 +47,8 @@ INSTALLED_APPS = [
'colorfield', 'colorfield',
'import_export', 'import_export',
'django_admin_action_forms', 'django_admin_action_forms',
'polymorphic' 'polymorphic',
"phonenumber_field",
] ]
# Import Export plugin settings # Import Export plugin settings

View File

@@ -1,12 +1,13 @@
from django.db.models.query import QuerySet, Q from django.db.models.query import QuerySet, Q
from ..models.instructors import Instructor
from ..models.students import Student from ..models.students import Student
from ..models.missions import MissionProfile from ..models.missions import MissionProfile
from ..models.aircrafts import Aircraft, AircraftTypes from ..models.aircrafts import Aircraft, AircraftTypes
from typing import List, Dict, Tuple from typing import List, Dict, Tuple
def assign_aircraft(queryset: QuerySet[Student] | QuerySet[MissionProfile], data: Dict[str, List[AircraftTypes]]) -> Tuple[int, List[str]]: def assign_aircraft(queryset: QuerySet[Student] | QuerySet[MissionProfile] | QuerySet[Instructor], data: Dict[str, List[AircraftTypes]]) -> Tuple[int, List[str]]:
i: int = 0 i: int = 0
ac_types: List[AircraftTypes] = data["aircrafts"] ac_types: List[AircraftTypes] = data["aircrafts"]
ac_query: Q = Q() # Build an or query to select all aircrafts of the specified types ac_query: Q = Q() # Build an or query to select all aircrafts of the specified types

View File

@@ -0,0 +1,20 @@
from django.db.models.query import QuerySet, Q
from ..models.instructors import Instructor
from ..models.missions import MissionProfile, MissionTypes
from typing import List, Dict, Tuple
def assign_profile(queryset: QuerySet[Instructor], data: Dict[str, List[MissionTypes]]) -> Tuple[int, List[str]]:
i: int = 0
mix_types: List[MissionTypes] = data["mission_profiles"]
mix_query: Q = Q() # Build an or query to select all aircrafts of the specified types
for m in mix_types:
mix_query |= Q(mtype=m)
profiles: QuerySet[MissionProfile] = MissionProfile.objects.filter(mix_query).all() # Execute query
for obj in queryset:
obj.missions.clear()
for ac in profiles:
obj.missions.add(ac)
obj.save()
i += 1
return i, [m for m in mix_types]

View File

@@ -6,12 +6,14 @@ from .models.courses import Course
from .models.students import Student from .models.students import Student
from .models.missions import MissionProfile from .models.missions import MissionProfile
from .models.weekpref import WeekPreference from .models.weekpref import WeekPreference
from.models.instructors import Instructor
from .admins.aircraft_adm import AircraftAdmin from .admins.aircraft_adm import AircraftAdmin
from .admins.course_adm import CourseAdmin from .admins.course_adm import CourseAdmin
from .admins.student_adm import StudentAdmin from .admins.student_adm import StudentAdmin
from .admins.mission_adm import MissionProfileAdmin from .admins.mission_adm import MissionProfileAdmin
from .admins.weekpref_adm import WeekPreferenceAdmin from .admins.weekpref_adm import WeekPreferenceAdmin
from .admins.instructor_admin import InstructorAdmin
from django.contrib.admin import AdminSite from django.contrib.admin import AdminSite
@@ -48,3 +50,4 @@ admin.site.register(Course, CourseAdmin)
admin.site.register(MissionProfile, MissionProfileAdmin) admin.site.register(MissionProfile, MissionProfileAdmin)
admin.site.register(Student, StudentAdmin) admin.site.register(Student, StudentAdmin)
admin.site.register(WeekPreference, WeekPreferenceAdmin) admin.site.register(WeekPreference, WeekPreferenceAdmin)
admin.site.register(Instructor, InstructorAdmin)

View File

@@ -0,0 +1,65 @@
from django.forms import TypedMultipleChoiceField
from django.db.models.query import QuerySet
from django.http import HttpRequest
from django.contrib import admin, messages
from django.utils.safestring import SafeText
from django_admin_action_forms import AdminActionFormsMixin, AdminActionForm, action_with_form
from ..models.instructors import Instructor
from ..models.aircrafts import AircraftTypes
from ..models.missions import MissionTypes
from ..actions.assign_profile import assign_profile
from ..actions.assign_aircraft import assign_aircraft
from typing import Dict, List
# Form class to assing aircrafts to instructors
class AssignMissionForm(AdminActionForm):
mission_profiles = TypedMultipleChoiceField(choices=MissionTypes)
# Form class to assing aircrafts to instructors
class AssignAircraftForm(AdminActionForm):
aircrafts = TypedMultipleChoiceField(choices=AircraftTypes)
class InstructorAdmin(AdminActionFormsMixin, admin.ModelAdmin):
model = Instructor
list_display = ("surname", "name", "email", "phone", "assigned_profiles", "assigned_aircrafts", "active", )
search_fields = ("surname", "name", "phone", "email", )
readonly_fields = ("username", "password", )
actions = ("assign_aircraft", "assign_profile", )
@admin.display(description="Password")
def password(self, obj: Instructor) -> SafeText:
return SafeText(obj.default_password())
@admin.display(description="Username")
def username(self, obj: Instructor) -> SafeText:
return SafeText(obj.default_username())
@admin.display(description="Assigned Profiles")
def assigned_profiles(self, obj: Instructor) -> SafeText:
if not obj.aircrafts:
return SafeText("")
return SafeText("/".join(mix.mtype for mix in obj.missions.distinct("mtype").order_by("mtype").all()))
@admin.display(description="Assigned Aircrafts")
def assigned_aircrafts(self, obj: Instructor) -> SafeText:
if not obj.aircrafts:
return SafeText("")
return SafeText("/".join(ac.type for ac in obj.aircrafts.distinct("type").order_by("type").all()))
@action_with_form(AssignAircraftForm, description="Assign Aircraft Type")
def assign_aircraft(self, request: HttpRequest, queryset: QuerySet[Instructor], data: Dict[str, List[AircraftTypes]]):
i: int
ac_types: List[str]
i, ac_types = assign_aircraft(queryset=queryset, data=data)
messages.success(request, f"{i} Instructors updated to {ac_types}")
@action_with_form(AssignMissionForm, description="Assign Mission Type")
def assign_profile(self, request: HttpRequest, queryset: QuerySet[Instructor], data: Dict[str, List[MissionTypes]]):
i: int
mix_types: List[str]
i, mix_types = assign_profile(queryset=queryset, data=data)
messages.success(request, f"{i} Instructors updated to {mix_types}")

View File

@@ -0,0 +1,41 @@
# Generated by Django 5.2.8 on 2025-12-01 17:27
import django.db.models.deletion
import phonenumber_field.modelfields
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('flightslot', '0025_alter_aircraft_type_alter_hourbuilding_aircraft_and_more'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AlterField(
model_name='student',
name='email',
field=models.EmailField(db_index=True, max_length=254, unique=True),
),
migrations.AlterField(
model_name='student',
name='phone',
field=phonenumber_field.modelfields.PhoneNumberField(max_length=128, null=True, region=None, unique=True),
),
migrations.CreateModel(
name='Instructor',
fields=[
('id', models.AutoField(primary_key=True, serialize=False)),
('email', models.EmailField(db_index=True, max_length=254)),
('phone', phonenumber_field.modelfields.PhoneNumberField(max_length=128, null=True, region=None, unique=True)),
('name', models.CharField(max_length=32)),
('surname', models.CharField(max_length=32)),
('active', models.BooleanField(default=True)),
('aircrafts', models.ManyToManyField(to='flightslot.aircraft')),
('missions', models.ManyToManyField(to='flightslot.missionprofile')),
('user', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
]

View File

@@ -0,0 +1,99 @@
from django.db import models
from django.contrib.auth.models import User, Group
from phonenumber_field import modelfields
from ..models.missions import MissionProfile
from ..models.aircrafts import Aircraft
class Instructor(models.Model):
id = models.AutoField(
primary_key=True
)
email = models.EmailField(
null=False,
db_index=True
)
phone = modelfields.PhoneNumberField(
null=True,
unique=True
)
name = models.CharField(
null=False,
blank=False,
max_length=32
)
surname = models.CharField(
null=False,
blank=False,
max_length=32
)
active = models.BooleanField(
null=False,
default=True
)
aircrafts = models.ManyToManyField(
Aircraft
)
missions = models.ManyToManyField(
MissionProfile
)
user = models.OneToOneField(
User,
on_delete=models.CASCADE,
null=True,
blank=True
)
def default_password(self) -> str: # Maximum 4 digits for passowrd
if self.pk:
return f"{self.name.lower()[0]}{self.surname.lower()}{self.id % 10000}"
else:
return ""
def default_username(self) -> str:
if self.pk and self.user:
return self.user.username
else:
return ""
# Override save method to add user for login upon Student creation
def save(self, *args, **kwargs):
creating: bool = self.pk is None
super().save(*args, **kwargs)
if creating and not self.user:
username: str = f"{self.name.lower()}.{self.surname.lower()}"
# Avoid username conflict with progressive number
base_username = username
counter: int = 1
while User.objects.filter(username=username).exists():
username = f"{base_username}{counter}"
counter += 1
# Create user
user: User = User.objects.create_user(
first_name=self.name.capitalize(),
last_name=self.surname.capitalize(),
username=username,
email=self.email,
password=self.default_password(),
is_staff=True # allows access to admin page
)
instructor_group, _ = Group.objects.get_or_create(name="InstructorGroup")
user.groups.add(instructor_group)
self.user = user
self.save()
def __str__(self):
if self.pk:
return f"{self.surname} {self.name[0]}."
else:
return "New Instructor"

View File

@@ -5,7 +5,7 @@ from datetime import timedelta
from ..models.weekpref import WeekPreference from ..models.weekpref import WeekPreference
from ..models.aircrafts import Aircraft from ..models.aircrafts import Aircraft
class MissionType(models.TextChoices): class MissionTypes(models.TextChoices):
OTHER = "OTHER", _("OTHER") OTHER = "OTHER", _("OTHER")
CHK = "CHK", _("CHK_6M") CHK = "CHK", _("CHK_6M")
PPL = "PPL", _("PPL") PPL = "PPL", _("PPL")
@@ -24,8 +24,8 @@ class MissionProfile(models.Model):
mtype = models.CharField( mtype = models.CharField(
null=False, null=False,
default=MissionType.PPL, default=MissionTypes.PPL,
choices=MissionType, choices=MissionTypes,
verbose_name="Mission Type" verbose_name="Mission Type"
) )

View File

@@ -1,6 +1,8 @@
from django.db import models from django.db import models
from django.contrib.auth.models import User, Group from django.contrib.auth.models import User, Group
from phonenumber_field import modelfields
from ..models.courses import Course from ..models.courses import Course
from ..models.aircrafts import Aircraft from ..models.aircrafts import Aircraft
@@ -11,12 +13,13 @@ class Student(models.Model):
email = models.EmailField( email = models.EmailField(
null=False, null=False,
db_index=True db_index=True,
unique=True
) )
phone = models.CharField( phone = modelfields.PhoneNumberField(
null=True, null=True,
max_length=16 unique=True
) )
name = models.CharField( name = models.CharField(
@@ -52,7 +55,10 @@ class Student(models.Model):
) )
def default_password(self) -> str: # Maximum 4 digits for passowrd def default_password(self) -> str: # Maximum 4 digits for passowrd
if self.pk:
return f"{self.name.lower()[0]}{self.surname.lower()}{self.id % 10000}" return f"{self.name.lower()[0]}{self.surname.lower()}{self.id % 10000}"
else:
return ""
def default_username(self) -> str: def default_username(self) -> str:
if self.pk and self.user: if self.pk and self.user:
@@ -74,8 +80,8 @@ class Student(models.Model):
counter += 1 counter += 1
# Create user # Create user
user: User = User.objects.create_user( user: User = User.objects.create_user(
first_name=self.name, first_name=self.name.capitalize(),
last_name=self.surname, last_name=self.surname.capitalize(),
username=username, username=username,
email=self.email, email=self.email,
password=self.default_password(), password=self.default_password(),

34
cntmanage/poetry.lock generated
View File

@@ -143,6 +143,26 @@ python-monkey-business = ">=1.0.0"
dev = ["Pillow", "black", "dj-database-url", "django-selenosis", "flake8", "pytest", "pytest-cov", "pytest-django", "pytest-xdist", "selenium"] dev = ["Pillow", "black", "dj-database-url", "django-selenosis", "flake8", "pytest", "pytest-cov", "pytest-django", "pytest-xdist", "selenium"]
test = ["Pillow", "dj-database-url", "django-selenosis", "pytest", "pytest-cov", "pytest-django", "pytest-xdist", "selenium"] test = ["Pillow", "dj-database-url", "django-selenosis", "pytest", "pytest-cov", "pytest-django", "pytest-xdist", "selenium"]
[[package]]
name = "django-phonenumber-field"
version = "8.4.0"
description = "An international phone number field for django models."
optional = false
python-versions = ">=3.10"
groups = ["main"]
files = [
{file = "django_phonenumber_field-8.4.0-py3-none-any.whl", hash = "sha256:7a1cb3a6456edb54d879f11ffa0acb227ded08c93b587035d0f28093f0e46511"},
{file = "django_phonenumber_field-8.4.0.tar.gz", hash = "sha256:2b83e843dac35eec6a69880a166487235b737a71a1e38c9a52e5ad67d6996083"},
]
[package.dependencies]
Django = ">=4.2"
phonenumberslite = {version = ">=7.0.2", optional = true, markers = "extra == \"phonenumberslite\""}
[package.extras]
phonenumbers = ["phonenumbers (>=7.0.2)"]
phonenumberslite = ["phonenumberslite (>=7.0.2)"]
[[package]] [[package]]
name = "django-polymorphic" name = "django-polymorphic"
version = "4.1.0" version = "4.1.0"
@@ -185,6 +205,18 @@ files = [
[package.dependencies] [package.dependencies]
et-xmlfile = "*" et-xmlfile = "*"
[[package]]
name = "phonenumberslite"
version = "9.0.19"
description = "Python version of Google's common library for parsing, formatting, storing and validating international phone numbers."
optional = false
python-versions = "*"
groups = ["main"]
files = [
{file = "phonenumberslite-9.0.19-py2.py3-none-any.whl", hash = "sha256:92a2426808e7d40b4acf36c97dcc436747807419c5dbc035330df28c13d41c0f"},
{file = "phonenumberslite-9.0.19.tar.gz", hash = "sha256:3794fcec9d2a6510a806187de750853c73ea5dabaac4ecd7fa36e79f869b3c2e"},
]
[[package]] [[package]]
name = "pillow" name = "pillow"
version = "12.0.0" version = "12.0.0"
@@ -436,4 +468,4 @@ files = [
[metadata] [metadata]
lock-version = "2.1" lock-version = "2.1"
python-versions = "^3.12" python-versions = "^3.12"
content-hash = "e932d0af75c888d83fecefaaad1d018c508881a3bfde2ea640a82790e3567855" content-hash = "5147211bd07992aff3915544175c8d95d77511b9d42273d17c4452fbef9299eb"

View File

@@ -18,6 +18,7 @@ django-colorfield = "^0.14.0"
openpyxl = "^3.1.5" openpyxl = "^3.1.5"
django-admin-action-forms = "^2.2.1" django-admin-action-forms = "^2.2.1"
django-polymorphic = "^4.1.0" django-polymorphic = "^4.1.0"
django-phonenumber-field = {extras = ["phonenumberslite"], version = "^8.4.0"}
[build-system] [build-system]