From de39913275290bace5de4f4be03dfc15be6f09a5 Mon Sep 17 00:00:00 2001 From: Emanuele Date: Fri, 28 Nov 2025 10:16:01 +0100 Subject: [PATCH] Adde Aircraft class and associate students with aircrafts --- cntmanage/flightslot/admin.py | 3 + cntmanage/flightslot/admins/aircraft_adm.py | 17 +++++ .../flightslot/admins/hourbuilding_adm.py | 6 +- cntmanage/flightslot/admins/mission_adm.py | 4 +- cntmanage/flightslot/admins/student_adm.py | 49 +++++++++---- cntmanage/flightslot/admins/training_adm.py | 6 +- cntmanage/flightslot/admins/weekpref_adm.py | 72 +++++++++---------- ...22_aircraft_alter_course_color_and_more.py | 39 ++++++++++ cntmanage/flightslot/models/aircrafts.py | 40 +++++++++++ cntmanage/flightslot/models/courses.py | 16 +++-- cntmanage/flightslot/models/students.py | 5 ++ 11 files changed, 190 insertions(+), 67 deletions(-) create mode 100644 cntmanage/flightslot/admins/aircraft_adm.py create mode 100644 cntmanage/flightslot/migrations/0022_aircraft_alter_course_color_and_more.py diff --git a/cntmanage/flightslot/admin.py b/cntmanage/flightslot/admin.py index 59022a9..c42ebce 100644 --- a/cntmanage/flightslot/admin.py +++ b/cntmanage/flightslot/admin.py @@ -1,11 +1,13 @@ from django.contrib import admin from django.http import HttpRequest +from .models.aircrafts import Aircraft from .models.courses import Course from .models.students import Student from .models.missions import MissionProfile from .models.weekpref import WeekPreference +from .admins.aircraft_adm import AircraftAdmin from .admins.course_adm import CourseAdmin from .admins.student_adm import StudentAdmin from .admins.mission_adm import MissionProfileAdmin @@ -35,6 +37,7 @@ admin.site.site_header = "Flight Scheduler Admin 🛫" admin.site.site_title = "Flight Scheduler Admin 🛫" admin.site.index_title = "Welcome to CantorAir Flight Scheduler Administrator Portal" +admin.site.register(Aircraft, AircraftAdmin) admin.site.register(Course, CourseAdmin) admin.site.register(MissionProfile, MissionProfileAdmin) admin.site.register(Student, StudentAdmin) diff --git a/cntmanage/flightslot/admins/aircraft_adm.py b/cntmanage/flightslot/admins/aircraft_adm.py new file mode 100644 index 0000000..306c184 --- /dev/null +++ b/cntmanage/flightslot/admins/aircraft_adm.py @@ -0,0 +1,17 @@ +from django.contrib import admin +from django.db.models.query import QuerySet +from django.http import HttpRequest + +from ..models.aircrafts import Aircraft + +class AircraftAdmin(admin.ModelAdmin): + model = Aircraft + list_display = ("type", "markings", "avail_hours", "complex", ) + list_filter = ("type", ) + actions = ("reset_maint") + + def get_queryset(self, request: HttpRequest) -> QuerySet[Aircraft]: + qs: QuerySet[Aircraft] = super().get_queryset(request) + qs.order_by("type", "markings") + return qs + \ No newline at end of file diff --git a/cntmanage/flightslot/admins/hourbuilding_adm.py b/cntmanage/flightslot/admins/hourbuilding_adm.py index 5f6794f..133d185 100644 --- a/cntmanage/flightslot/admins/hourbuilding_adm.py +++ b/cntmanage/flightslot/admins/hourbuilding_adm.py @@ -73,15 +73,15 @@ class HourBuildingInLine(nested_admin.NestedTabularInline): } # If user is a student deny edit permission for week past the current one - def has_change_permission(self, request: HttpRequest, obj: WeekPreference | None = None): + def has_change_permission(self, request: HttpRequest, obj: WeekPreference | None = None) -> bool: if hasattr(request.user, 'student') and obj: current_week: int = date.today().isocalendar().week if current_week > obj.week: return False return True - def has_delete_permission(self, request: HttpRequest, obj: WeekPreference | None = None): + def has_delete_permission(self, request: HttpRequest, obj: WeekPreference | None = None) -> bool: return self.has_change_permission(request=request, obj=obj) - def has_add_permission(self, request: HttpRequest, obj: WeekPreference | None = None): + def has_add_permission(self, request: HttpRequest, obj: WeekPreference | None = None) -> bool: return self.has_change_permission(request=request, obj=obj) diff --git a/cntmanage/flightslot/admins/mission_adm.py b/cntmanage/flightslot/admins/mission_adm.py index 3dafa1d..b44f95f 100644 --- a/cntmanage/flightslot/admins/mission_adm.py +++ b/cntmanage/flightslot/admins/mission_adm.py @@ -20,12 +20,12 @@ class MissionProfileResource(ModelResource): duration = fields.Field(attribute="duration", column_name="duration") # Cleanup fields before entering - def before_import_row(self, row: dict[str, str | Any], **kwargs) -> None: + def before_import_row(self, row: dict[str, str | Any], **kwargs): row["mtype"] = SafeText(row["mtype"].upper().strip()) row["mnum"] = SafeText(row["mnum"].upper().strip()) h, m, _ = row["duration"].split(":") row["duration"] = timedelta(hours=float(h), minutes=float(m)) - return super().before_import_row(row, **kwargs) + super().before_import_row(row, **kwargs) class Meta: model = MissionProfile diff --git a/cntmanage/flightslot/admins/student_adm.py b/cntmanage/flightslot/admins/student_adm.py index 91bc269..419f61c 100644 --- a/cntmanage/flightslot/admins/student_adm.py +++ b/cntmanage/flightslot/admins/student_adm.py @@ -1,5 +1,5 @@ -from django import forms -from django.db.models.query import QuerySet +from django.forms import ModelChoiceField, TypedMultipleChoiceField, ModelMultipleChoiceField +from django.db.models.query import QuerySet, Q from django.http import HttpRequest from django.contrib import admin, messages from django.utils.safestring import SafeText @@ -12,16 +12,17 @@ from import_export.forms import ConfirmImportForm, ImportForm from django_admin_action_forms import AdminActionFormsMixin, AdminActionForm, action_with_form -from typing import Any, Dict - +from ..models.aircrafts import Aircraft, AircraftTypes from ..models.courses import Course from ..models.students import Student from ..custom.colortag import course_color +from typing import Any, Dict + # Custom import form to select a course for student input class StudentCustomConfirmImportForm(ConfirmImportForm): - course = forms.ModelChoiceField( + course = ModelChoiceField( queryset=Course.objects.all(), required=False) @@ -55,13 +56,18 @@ class StudentResource(ModelResource): # Form Class for Student course change class ChangeCourseForm(AdminActionForm): - course = forms.ModelChoiceField(queryset=Course.objects.all()) + course = TypedMultipleChoiceField(choices=AircraftTypes) + +# Form class to assing aircrafts to students +class ChangeAircraftForm(AdminActionForm): + aircrafts = ModelMultipleChoiceField(queryset=Aircraft.objects.distinct('type').all()) class StudentAdmin(ImportMixin, AdminActionFormsMixin, admin.ModelAdmin): - list_display = ("surname", "name", "course", "course_color", "email", "phone", "username", "password", "active",) - list_filter = ("course", "active",) - search_fields = ("surname", "name", "phone", "email",) - actions = ("change_course", "disable_students",) + model = Student + list_display = ("surname", "name", "course", "course_color", "email", "phone", "username", "password", "active", ) + list_filter = ("course", "active", ) + search_fields = ("surname", "name", "phone", "email", ) + actions = ("change_course", "disable_students", "change_aircraft", ) resource_classes = [StudentResource] confirm_form_class = StudentCustomConfirmImportForm tmp_storage_class = CacheStorage @@ -89,7 +95,6 @@ class StudentAdmin(ImportMixin, AdminActionFormsMixin, admin.ModelAdmin): 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): @@ -97,16 +102,32 @@ class StudentAdmin(ImportMixin, AdminActionFormsMixin, admin.ModelAdmin): count: int = queryset.update(course=course) messages.success(request, f"{count} students updated to {course}") + @action_with_form(ChangeAircraftForm, description="Assign Aircraft") + def change_aircraft(self, request: HttpRequest, queryset: QuerySet[Student], data: Dict[str, QuerySet[Aircraft]]): + ac_types = [t.type for t in data["aircrafts"]] + ac_query: Q = Q() # Build an or query to select all aircrafts of the specified types + for a in ac_types: + ac_query |= Q(type=a) + aircrafts: QuerySet[Aircraft] = Aircraft.objects.filter(ac_query).all() # Execute query + i: int = 0 + for student in queryset: + student.aircrafts.clear() + for ac in aircrafts: + student.aircrafts.add(ac) + student.save() + i += 1 + messages.success(request, f"{i} Students updated to {ac_types}") + # Return the initial form for import confirmations, request course to user - def get_confirm_form_initial(self, request: HttpRequest, import_form): - initial = super().get_confirm_form_initial(request, import_form) + def get_confirm_form_initial(self, request: HttpRequest, import_form) -> Dict[str, Any]: + initial: Dict[str, Any] = super().get_confirm_form_initial(request, import_form) if import_form and hasattr(import_form.cleaned_data, "course"): course: Course = import_form.cleaned_data["course"] initial["course"] = course.id return initial # Add course to import form kwargs to be used by resource to associate course with all imported students - def get_import_data_kwargs(self, request: HttpRequest, *args, **kwargs): + def get_import_data_kwargs(self, request: HttpRequest, *args, **kwargs) -> Dict[str, Any]: form: ImportForm | None = kwargs.get("form", None) if form and hasattr(form, "cleaned_data"): kwargs["course"] = form.cleaned_data.get("course", None) diff --git a/cntmanage/flightslot/admins/training_adm.py b/cntmanage/flightslot/admins/training_adm.py index 5127211..3d7d464 100644 --- a/cntmanage/flightslot/admins/training_adm.py +++ b/cntmanage/flightslot/admins/training_adm.py @@ -26,15 +26,15 @@ class TrainingInLIne(nested_admin.NestedTabularInline): } # If user is a student deny edit permission for week past the current one - def has_change_permission(self, request: HttpRequest, obj: WeekPreference | None = None): + def has_change_permission(self, request: HttpRequest, obj: WeekPreference | None = None) -> bool: if hasattr(request.user, 'student') and obj: current_week: int = date.today().isocalendar().week if current_week > obj.week: return False return True - def has_delete_permission(self, request: HttpRequest, obj: WeekPreference | None = None): + def has_delete_permission(self, request: HttpRequest, obj: WeekPreference | None = None) -> bool: return self.has_change_permission(request=request, obj=obj) - def has_add_permission(self, request: HttpRequest, obj: WeekPreference | None = None): + def has_add_permission(self, request: HttpRequest, obj: WeekPreference | None = None) -> bool: return self.has_change_permission(request=request, obj=obj) diff --git a/cntmanage/flightslot/admins/weekpref_adm.py b/cntmanage/flightslot/admins/weekpref_adm.py index 47763d6..66c94b2 100644 --- a/cntmanage/flightslot/admins/weekpref_adm.py +++ b/cntmanage/flightslot/admins/weekpref_adm.py @@ -1,7 +1,8 @@ import nested_admin -from django import forms +from django.forms import Form from django.db.models.query import QuerySet +from django.contrib.auth.models import User from django.http import HttpRequest, HttpResponse from django.contrib import admin, messages from django.utils.translation import ngettext @@ -17,6 +18,7 @@ from ..custom.colortag import course_color from ..actions.exportweek import export_selected from datetime import date +from typing import Dict, List, Any class WeekPreferenceAdmin(nested_admin.NestedPolymorphicModelAdmin): inlines = (TrainingInLIne, HourBuildingInLine, ) @@ -45,83 +47,75 @@ class WeekPreferenceAdmin(nested_admin.NestedPolymorphicModelAdmin): return course_color(obj.student.course.color) # If a user is registered as student hide filters - def get_list_filter(self, request): + def get_list_filter(self, request: HttpRequest) -> List[str]: list_filter = super().get_list_filter(request) - if hasattr(request.user, 'student'): + if hasattr(request.user, "student"): return [] return list_filter # If a user is registered as student do not show actions - def get_actions(self, request): + def get_actions(self, request: HttpRequest) -> Dict[str, Any]: actions = super().get_actions(request) - if hasattr(request.user, 'student'): - return [] + if hasattr(request.user, "student"): + return {} return actions # If a user is registered as student show only their preferences - def get_queryset(self, request): + def get_queryset(self, request: HttpRequest) -> QuerySet[WeekPreference]: qs = super().get_queryset(request) - if hasattr(request.user, 'student'): + if hasattr(request.user, "student"): return qs.filter(student=request.user.student) # If admin show everything return qs - def get_form(self, request, obj=None, **kwargs): - form: forms.Form = super().get_form(request, obj, **kwargs) + def get_form(self, request: HttpRequest, obj: WeekPreference | None = None, **kwargs: Dict[str, Any]) -> Form: + form: Form = super().get_form(request, obj, **kwargs) current_week = date.today().isocalendar().week # If form contains the week field - if 'week' in form.base_fields: + if "week" in form.base_fields: # Set default value as current week - form.base_fields['week'].initial = current_week + form.base_fields["week"].initial = current_week # If student is current user making request - if hasattr(request.user, 'student'): + if hasattr(request.user, "student"): student = request.user.student - if 'student' in form.base_fields: - form.base_fields['student'].initial = student - form.base_fields['student'].disabled = True - form.base_fields['week'].disabled = True # student cannot change week + if "student" in form.base_fields: + form.base_fields["student"].initial = student + form.base_fields["student"].disabled = True + form.base_fields["week"].disabled = True # student cannot change week return form # If user is a student deny edit permission for week past the current one - def has_change_permission(self, request, obj: WeekPreference | None = None): - if hasattr(request.user, 'student') and obj: + def has_change_permission(self, request: HttpRequest, obj: WeekPreference | None = None) -> bool: + if hasattr(request.user, "student") and obj: current_week = date.today().isocalendar().week if current_week > obj.week: return False return True # If user is a student deny edit permission for week past the current one - def has_add_permission(self, request, obj: WeekPreference | None = None): - if hasattr(request.user, 'student') and obj: - current_week = date.today().isocalendar().week - if current_week > obj.week: - return False - return True + def has_add_permission(self, request: HttpRequest, obj: WeekPreference | None = None) -> bool: + return self.has_change_permission(request, obj) # If user is a student deny edit permission for week past the current one - def has_delete_permission(self, request, obj: WeekPreference | None = None): - if hasattr(request.user, 'student') and obj: - current_week = date.today().isocalendar().week - if current_week > obj.week: - return False - return True + def has_delete_permission(self, request: HttpRequest, obj: WeekPreference | None = None)-> bool: + return self.has_change_permission(request, obj) - def changeform_view(self, request: HttpRequest, object_id: int | None = None, form_url: str = '', extra_context=None): + def changeform_view(self, request: HttpRequest, object_id: int | None = None, form_url: str = "", extra_context=None): extra_context = extra_context or {} - if hasattr(request.user, 'student') and object_id: + if hasattr(request.user, "student") and object_id: current_week = date.today().isocalendar().week weekpref = WeekPreference.objects.get(id=object_id) if current_week > weekpref.week: - extra_context['show_save'] = False - extra_context['show_save_and_continue'] = False - extra_context['show_save_and_add_another'] = False - extra_context['show_delete'] = False + extra_context["show_save"] = False + extra_context["show_save_and_continue"] = False + extra_context["show_save_and_add_another"] = False + extra_context["show_delete"] = False return super().changeform_view(request, object_id, form_url, extra_context) - def save_model(self, request, obj, form, change): + def save_model(self, request: HttpRequest, obj, form: Form, change: bool): # Imposta automaticamente lo studente se non è già valorizzato - if hasattr(request.user, 'student') and not obj.student_id: + if hasattr(request.user, "student") and not obj.student_id: obj.student = request.user.student super().save_model(request, obj, form, change) diff --git a/cntmanage/flightslot/migrations/0022_aircraft_alter_course_color_and_more.py b/cntmanage/flightslot/migrations/0022_aircraft_alter_course_color_and_more.py new file mode 100644 index 0000000..b27983b --- /dev/null +++ b/cntmanage/flightslot/migrations/0022_aircraft_alter_course_color_and_more.py @@ -0,0 +1,39 @@ +# Generated by Django 5.2.8 on 2025-11-27 13:34 + +import colorfield.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('flightslot', '0021_alter_hourbuildinglegstop_refuel'), + ] + + operations = [ + migrations.CreateModel( + name='Aircraft', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('type', models.CharField(choices=[('C152', 'Cessna 152'), ('P208', 'Tecnam P2008'), ('PA28', 'Piper PA28R'), ('PA34', 'Piper PA34'), ('C182', 'Cessna 182Q'), ('TWEN', 'Tecnam P2010'), ('FSTD', 'Alsim ALX40')], max_length=4)), + ('markings', models.CharField(max_length=6)), + ('complex', models.BooleanField(default=False)), + ('avail_hours', models.DurationField(null=True, verbose_name='Time until maintenance')), + ], + ), + migrations.AlterField( + model_name='course', + name='color', + field=colorfield.fields.ColorField(default='#FFFFFF', image_field=None, max_length=25, samples=[('#bfbfbf', 'GREY'), ('#ff0000', 'RED'), ('#ffc000', 'ORANGE'), ('#ffff00', 'YELLOW'), ('#92d050', 'GREEN'), ('#00b0f0', 'CYAN'), ('#b1a0c7', 'MAGENTA'), ('#fabcfb', 'PINK'), ('#f27ae4', 'VIOLET')], verbose_name='Binder Color'), + ), + migrations.AlterField( + model_name='hourbuilding', + name='aircraft', + field=models.CharField(choices=[('C152', 'Cessna 152'), ('P208', 'Tecnam P2008'), ('PA28', 'Piper PA28R'), ('PA34', 'Piper PA34'), ('C182', 'Cessna 182Q'), ('TWEN', 'Tecnam P2010'), ('FSTD', 'Alsim ALX40')]), + ), + migrations.AddField( + model_name='student', + name='aircrafts', + field=models.ManyToManyField(to='flightslot.aircraft'), + ), + ] diff --git a/cntmanage/flightslot/models/aircrafts.py b/cntmanage/flightslot/models/aircrafts.py index 4afb9c2..53e3935 100644 --- a/cntmanage/flightslot/models/aircrafts.py +++ b/cntmanage/flightslot/models/aircrafts.py @@ -5,5 +5,45 @@ class AircraftTypes(models.TextChoices): C152 = "C152", _("Cessna 152") P208 = "P208", _("Tecnam P2008") PA28 = "PA28", _("Piper PA28R") + PA34 = "PA34", _("Piper PA34") C182 = "C182", _("Cessna 182Q") P210 = "TWEN", _("Tecnam P2010") + ALX40 = "FSTD", _("Alsim ALX40") + +class Aircraft(models.Model): + id = models.AutoField( + primary_key=True + ) + + type = models.CharField( + null=False, + blank=False, + max_length=4, # ICAO naming of aircraft, + choices=AircraftTypes + ) + + markings = models.CharField( + null=False, + blank=False, + max_length=6 + ) + + complex = models.BooleanField( + null=False, + default=False + ) + + avail_hours = models.DurationField( + null=True, + verbose_name=_("Time until maintenance") + ) + + def __str__(self) -> str: + return f"{self.type} ({self.markings})" + + # Insert dash between first and rest, I-OASM + def save(self, *args, **kwargs): + self.markings = self.markings.upper() + if not "-" in self.markings: + self.markings = self.markings[0] + "-" + self.markings[1:] + super().save(*args, **kwargs) diff --git a/cntmanage/flightslot/models/courses.py b/cntmanage/flightslot/models/courses.py index d88c047..b9ab541 100644 --- a/cntmanage/flightslot/models/courses.py +++ b/cntmanage/flightslot/models/courses.py @@ -10,14 +10,18 @@ class CourseTypes(models.TextChoices): DISTANCE = "DL", _("DISTANCE") OTHER = "OTHER",_("OTHER") - class Course(models.Model): # Add colors according to table from Alessia COLOR_PALETTE = [ - ("#ffffff","WHITE"), + ("#bfbfbf","GREY"), ("#ff0000", "RED"), - ("#00ff00", "GREEN"), - ("#0000ff", "BLUE") + ("#ffc000", "ORANGE"), + ("#ffff00", "YELLOW"), + ("#92d050", "GREEN"), + ("#00b0f0", "CYAN"), + ("#b1a0c7", "MAGENTA"), + ("#fabcfb", "PINK"), + ("#f27ae4", "VIOLET"), ] id = models.AutoField( @@ -43,8 +47,8 @@ class Course(models.Model): ) color = ColorField ( - samples=COLOR_PALETTE, - verbose_name=_("Binder Color") + verbose_name=_("Binder Color"), + samples=COLOR_PALETTE ) def __str__(self): diff --git a/cntmanage/flightslot/models/students.py b/cntmanage/flightslot/models/students.py index b4014ce..bab8035 100644 --- a/cntmanage/flightslot/models/students.py +++ b/cntmanage/flightslot/models/students.py @@ -2,6 +2,7 @@ from django.db import models from django.contrib.auth.models import User, Group from ..models.courses import Course +from ..models.aircrafts import Aircraft class Student(models.Model): id = models.AutoField( @@ -46,6 +47,10 @@ class Student(models.Model): blank=True ) + aircrafts = models.ManyToManyField( + Aircraft + ) + def default_password(self) -> str: # Maximum 4 digits for passowrd return f"{self.name.lower()[0]}{self.surname.lower()}{self.id % 10000}"