From 5d1686f24b30bc55e7764a730d26c0b089df81cd Mon Sep 17 00:00:00 2001 From: Emanuele Date: Mon, 1 Dec 2025 18:41:44 +0100 Subject: [PATCH 1/3] Added instructor model, admin and basic actions --- cntmanage/cntmanage/settings.py | 3 +- cntmanage/cntmanage/settings_prod.py | 3 +- .../flightslot/actions/assign_aircraft.py | 3 +- .../flightslot/actions/assign_profile.py | 20 ++++ cntmanage/flightslot/admin.py | 3 + .../flightslot/admins/instructor_admin.py | 65 ++++++++++++ ...nt_email_alter_student_phone_instructor.py | 41 ++++++++ cntmanage/flightslot/models/instructors.py | 99 +++++++++++++++++++ cntmanage/flightslot/models/missions.py | 6 +- cntmanage/flightslot/models/students.py | 18 ++-- cntmanage/poetry.lock | 34 ++++++- cntmanage/pyproject.toml | 1 + 12 files changed, 283 insertions(+), 13 deletions(-) create mode 100644 cntmanage/flightslot/actions/assign_profile.py create mode 100644 cntmanage/flightslot/admins/instructor_admin.py create mode 100644 cntmanage/flightslot/migrations/0026_alter_student_email_alter_student_phone_instructor.py create mode 100644 cntmanage/flightslot/models/instructors.py diff --git a/cntmanage/cntmanage/settings.py b/cntmanage/cntmanage/settings.py index 2f6c74d..15c1c07 100644 --- a/cntmanage/cntmanage/settings.py +++ b/cntmanage/cntmanage/settings.py @@ -43,7 +43,8 @@ INSTALLED_APPS = [ 'colorfield', 'import_export', 'django_admin_action_forms', - 'polymorphic' + 'polymorphic', + "phonenumber_field", ] # Import Export plugin settings diff --git a/cntmanage/cntmanage/settings_prod.py b/cntmanage/cntmanage/settings_prod.py index 74b066c..a1f4808 100644 --- a/cntmanage/cntmanage/settings_prod.py +++ b/cntmanage/cntmanage/settings_prod.py @@ -47,7 +47,8 @@ INSTALLED_APPS = [ 'colorfield', 'import_export', 'django_admin_action_forms', - 'polymorphic' + 'polymorphic', + "phonenumber_field", ] # Import Export plugin settings diff --git a/cntmanage/flightslot/actions/assign_aircraft.py b/cntmanage/flightslot/actions/assign_aircraft.py index 1e3a624..614ca2e 100644 --- a/cntmanage/flightslot/actions/assign_aircraft.py +++ b/cntmanage/flightslot/actions/assign_aircraft.py @@ -1,12 +1,13 @@ from django.db.models.query import QuerySet, Q +from ..models.instructors import Instructor from ..models.students import Student from ..models.missions import MissionProfile from ..models.aircrafts import Aircraft, AircraftTypes 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 ac_types: List[AircraftTypes] = data["aircrafts"] ac_query: Q = Q() # Build an or query to select all aircrafts of the specified types diff --git a/cntmanage/flightslot/actions/assign_profile.py b/cntmanage/flightslot/actions/assign_profile.py new file mode 100644 index 0000000..87c6cfb --- /dev/null +++ b/cntmanage/flightslot/actions/assign_profile.py @@ -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] diff --git a/cntmanage/flightslot/admin.py b/cntmanage/flightslot/admin.py index af1f613..4d3cb92 100644 --- a/cntmanage/flightslot/admin.py +++ b/cntmanage/flightslot/admin.py @@ -6,12 +6,14 @@ from .models.courses import Course from .models.students import Student from .models.missions import MissionProfile from .models.weekpref import WeekPreference +from.models.instructors import Instructor from .admins.aircraft_adm import AircraftAdmin from .admins.course_adm import CourseAdmin from .admins.student_adm import StudentAdmin from .admins.mission_adm import MissionProfileAdmin from .admins.weekpref_adm import WeekPreferenceAdmin +from .admins.instructor_admin import InstructorAdmin from django.contrib.admin import AdminSite @@ -48,3 +50,4 @@ admin.site.register(Course, CourseAdmin) admin.site.register(MissionProfile, MissionProfileAdmin) admin.site.register(Student, StudentAdmin) admin.site.register(WeekPreference, WeekPreferenceAdmin) +admin.site.register(Instructor, InstructorAdmin) diff --git a/cntmanage/flightslot/admins/instructor_admin.py b/cntmanage/flightslot/admins/instructor_admin.py new file mode 100644 index 0000000..154dabf --- /dev/null +++ b/cntmanage/flightslot/admins/instructor_admin.py @@ -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}") diff --git a/cntmanage/flightslot/migrations/0026_alter_student_email_alter_student_phone_instructor.py b/cntmanage/flightslot/migrations/0026_alter_student_email_alter_student_phone_instructor.py new file mode 100644 index 0000000..7c90c46 --- /dev/null +++ b/cntmanage/flightslot/migrations/0026_alter_student_email_alter_student_phone_instructor.py @@ -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)), + ], + ), + ] diff --git a/cntmanage/flightslot/models/instructors.py b/cntmanage/flightslot/models/instructors.py new file mode 100644 index 0000000..14663ab --- /dev/null +++ b/cntmanage/flightslot/models/instructors.py @@ -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" diff --git a/cntmanage/flightslot/models/missions.py b/cntmanage/flightslot/models/missions.py index a6125fd..816797e 100644 --- a/cntmanage/flightslot/models/missions.py +++ b/cntmanage/flightslot/models/missions.py @@ -5,7 +5,7 @@ from datetime import timedelta from ..models.weekpref import WeekPreference from ..models.aircrafts import Aircraft -class MissionType(models.TextChoices): +class MissionTypes(models.TextChoices): OTHER = "OTHER", _("OTHER") CHK = "CHK", _("CHK_6M") PPL = "PPL", _("PPL") @@ -24,8 +24,8 @@ class MissionProfile(models.Model): mtype = models.CharField( null=False, - default=MissionType.PPL, - choices=MissionType, + default=MissionTypes.PPL, + choices=MissionTypes, verbose_name="Mission Type" ) diff --git a/cntmanage/flightslot/models/students.py b/cntmanage/flightslot/models/students.py index bab8035..fb3374f 100644 --- a/cntmanage/flightslot/models/students.py +++ b/cntmanage/flightslot/models/students.py @@ -1,6 +1,8 @@ from django.db import models from django.contrib.auth.models import User, Group +from phonenumber_field import modelfields + from ..models.courses import Course from ..models.aircrafts import Aircraft @@ -11,12 +13,13 @@ class Student(models.Model): email = models.EmailField( null=False, - db_index=True + db_index=True, + unique=True ) - phone = models.CharField( + phone = modelfields.PhoneNumberField( null=True, - max_length=16 + unique=True ) name = models.CharField( @@ -52,7 +55,10 @@ class Student(models.Model): ) def default_password(self) -> str: # Maximum 4 digits for passowrd - return f"{self.name.lower()[0]}{self.surname.lower()}{self.id % 10000}" + 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: @@ -74,8 +80,8 @@ class Student(models.Model): counter += 1 # Create user user: User = User.objects.create_user( - first_name=self.name, - last_name=self.surname, + first_name=self.name.capitalize(), + last_name=self.surname.capitalize(), username=username, email=self.email, password=self.default_password(), diff --git a/cntmanage/poetry.lock b/cntmanage/poetry.lock index 7042e04..3efef29 100644 --- a/cntmanage/poetry.lock +++ b/cntmanage/poetry.lock @@ -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"] 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]] name = "django-polymorphic" version = "4.1.0" @@ -185,6 +205,18 @@ files = [ [package.dependencies] 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]] name = "pillow" version = "12.0.0" @@ -436,4 +468,4 @@ files = [ [metadata] lock-version = "2.1" python-versions = "^3.12" -content-hash = "e932d0af75c888d83fecefaaad1d018c508881a3bfde2ea640a82790e3567855" +content-hash = "5147211bd07992aff3915544175c8d95d77511b9d42273d17c4452fbef9299eb" diff --git a/cntmanage/pyproject.toml b/cntmanage/pyproject.toml index dfeccc8..35b3e97 100644 --- a/cntmanage/pyproject.toml +++ b/cntmanage/pyproject.toml @@ -18,6 +18,7 @@ django-colorfield = "^0.14.0" openpyxl = "^3.1.5" django-admin-action-forms = "^2.2.1" django-polymorphic = "^4.1.0" +django-phonenumber-field = {extras = ["phonenumberslite"], version = "^8.4.0"} [build-system] -- 2.49.1 From 3ee2269d701d08acf0ae08c28f64d84462d5dd3d Mon Sep 17 00:00:00 2001 From: Emanuele Date: Tue, 2 Dec 2025 12:00:15 +0100 Subject: [PATCH 2/3] Added instructor availability model and admin --- cntmanage/cntmanage/urls.py | 2 + cntmanage/flightslot/admin.py | 31 +++++++- .../flightslot/admins/availability_adm.py | 66 ++++++++++++++++ cntmanage/flightslot/admins/weekpref_adm.py | 4 +- .../flightslot/custom/student_permissions.py | 2 +- cntmanage/flightslot/middleware.py | 6 +- ...ter_weekpreference_student_availability.py | 36 +++++++++ .../0028_alter_availability_options.py | 17 ++++ cntmanage/flightslot/models/availabilities.py | 79 +++++++++++++++++++ cntmanage/flightslot/models/weekpref.py | 2 +- 10 files changed, 236 insertions(+), 9 deletions(-) create mode 100644 cntmanage/flightslot/admins/availability_adm.py create mode 100644 cntmanage/flightslot/migrations/0027_alter_weekpreference_student_availability.py create mode 100644 cntmanage/flightslot/migrations/0028_alter_availability_options.py create mode 100644 cntmanage/flightslot/models/availabilities.py diff --git a/cntmanage/cntmanage/urls.py b/cntmanage/cntmanage/urls.py index c485b3c..9b63692 100644 --- a/cntmanage/cntmanage/urls.py +++ b/cntmanage/cntmanage/urls.py @@ -2,10 +2,12 @@ from django.contrib import admin from django.urls import path from django.shortcuts import redirect from flightslot.admin import flightslot_user +from flightslot.admin import flightslot_staff urlpatterns = [ #path('', RedirectView.as_view(url='/admin/', permanent=False)), path('admin/', admin.site.urls), path('user/', flightslot_user.urls), + path('staff/', flightslot_staff.urls), path("", lambda r: redirect("/user/")), # la root porta gli utenti nella pagina giusta ] diff --git a/cntmanage/flightslot/admin.py b/cntmanage/flightslot/admin.py index 4d3cb92..f6dd88b 100644 --- a/cntmanage/flightslot/admin.py +++ b/cntmanage/flightslot/admin.py @@ -6,7 +6,8 @@ from .models.courses import Course from .models.students import Student from .models.missions import MissionProfile from .models.weekpref import WeekPreference -from.models.instructors import Instructor +from .models.instructors import Instructor +from .models.availabilities import Availability from .admins.aircraft_adm import AircraftAdmin from .admins.course_adm import CourseAdmin @@ -14,16 +15,19 @@ from .admins.student_adm import StudentAdmin from .admins.mission_adm import MissionProfileAdmin from .admins.weekpref_adm import WeekPreferenceAdmin from .admins.instructor_admin import InstructorAdmin +from .admins.availability_adm import AvailabilityAdmin from django.contrib.admin import AdminSite from os import environ -# User website under /user/ URL +################################## +# User website under /user/ URL # +################################## class FlightSlotUserSite(AdminSite): site_header = "Flight Scheduler 🛫" site_title = "Flight Scheduler 🛫" - index_title = "Welcome to CantorAir Flight Scheduler Portal" + index_title = "Welcome to CantorAir Flight Scheduler Student Portal" def get_app_list(self, request: HttpRequest, *args, **kwargs): app_list = super().get_app_list(request) @@ -37,6 +41,26 @@ class FlightSlotUserSite(AdminSite): flightslot_user = FlightSlotUserSite(name="user_site") flightslot_user.register(WeekPreference, WeekPreferenceAdmin) +################################## +# User website under /staff/ URL # +################################## +class FlightSlotStaffSite(AdminSite): + site_header = "Flight Scheduler Staff 🛫" + site_title = "Flight Scheduler Staff 🛫" + index_title = "Welcome to CantorAir Flight Scheduler Staff Portal" + + def get_app_list(self, request: HttpRequest, *args, **kwargs): + app_list = super().get_app_list(request) + + if not request.user.is_superuser: + self.enable_nav_sidebar = False + + return app_list + +# Register only user visible models +flightslot_staff = FlightSlotUserSite(name="staff_site") +flightslot_staff.register(Availability, AvailabilityAdmin) + # Get version for debug purposes ver: str = environ.get("VERSION", "dev") @@ -51,3 +75,4 @@ admin.site.register(MissionProfile, MissionProfileAdmin) admin.site.register(Student, StudentAdmin) admin.site.register(WeekPreference, WeekPreferenceAdmin) admin.site.register(Instructor, InstructorAdmin) +admin.site.register(Availability, AvailabilityAdmin) diff --git a/cntmanage/flightslot/admins/availability_adm.py b/cntmanage/flightslot/admins/availability_adm.py new file mode 100644 index 0000000..e28da7f --- /dev/null +++ b/cntmanage/flightslot/admins/availability_adm.py @@ -0,0 +1,66 @@ +from django import forms +from django.db.models.query import QuerySet +from django.http import HttpRequest +from django.contrib import admin +from django.utils.safestring import SafeText + +from ..models.instructors import Instructor +from ..models.availabilities import Availability + +from datetime import date +from typing import Any, List + +class AvailabilityForm(forms.ModelForm): + model=Availability + +class AvailabilityAdmin(admin.ModelAdmin): + model = Availability + list_display = ("week", "instructor__surname", "instructor__name", "days_available", "hours") + list_filter = ("week", ) + search_fields = ("instructor__surname","instructor__name", ) + #actions = ("export", ) + + @admin.display(description="Days Available") + def days_available(self, obj: Availability) -> SafeText: + if not obj: + return SafeText("") + days: List[str | None] = [ + "Mon" if obj.monday else None, + "Tue" if obj.tuesday else None, + "Wed" if obj.wednesday else None, + "Thu" if obj.thursday else None, + "Fri" if obj.friday else None, + "Sat" if obj.saturday else None, + "Sun" if obj.sunday else None, + ] + return SafeText("/".join(d if d else "" for d in days)) + + def get_queryset(self, request: HttpRequest) -> QuerySet: + return super().get_queryset(request).order_by("-week", "instructor__surname", "instructor__name") + + def get_form(self, request: HttpRequest, obj: Availability | None = None, change: bool = False, **kwargs: Any) -> AvailabilityForm: + form: AvailabilityForm = super().get_form(request, obj, change, **kwargs) + + if change: # if is only a form change do not set default values and return form + return form + + # If form contains the week field + current_week = date.today().isocalendar().week + if "week" in form.base_fields: + # Set default value as current week + form.base_fields["week"].initial = current_week + + # If student is current user making request + if hasattr(request.user, "instructor"): + instructor: Instructor = request.user.instructor + if "instructor" in form.base_fields: + form.base_fields["instructor"].initial = instructor + form.base_fields["instructor"].disabled = True + return form + + # Imposta automaticamente l'istruttore se non è già valorizzato + def save_model(self, request: HttpRequest, obj: Availability, form: AvailabilityForm, change: bool): + if hasattr(request.user, "instructor") and not obj.instructor_id: + obj.instructor = request.user.instructor + super().save_model(request, obj, form, change) + \ No newline at end of file diff --git a/cntmanage/flightslot/admins/weekpref_adm.py b/cntmanage/flightslot/admins/weekpref_adm.py index 123670a..bdd70f5 100644 --- a/cntmanage/flightslot/admins/weekpref_adm.py +++ b/cntmanage/flightslot/admins/weekpref_adm.py @@ -93,13 +93,13 @@ class WeekPreferenceAdmin(nested_admin.NestedPolymorphicModelAdmin): # If user is a student deny edit permission for week past the current one def has_add_permission(self, request: HttpRequest, obj: WeekPreference | None = None) -> bool: - return self.has_change_permission(request, obj) + return not obj and 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: 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: weekpref = WeekPreference.objects.get(id=object_id) diff --git a/cntmanage/flightslot/custom/student_permissions.py b/cntmanage/flightslot/custom/student_permissions.py index d995526..12b1b72 100644 --- a/cntmanage/flightslot/custom/student_permissions.py +++ b/cntmanage/flightslot/custom/student_permissions.py @@ -12,6 +12,6 @@ def has_edit_permission(request: HttpRequest, obj: WeekPreference | None = None) if not student.active: return False current_week: int = date.today().isocalendar().week - if obj and current_week > obj.week or not student.active: + if obj and (current_week > obj.week or not student.active): return False return True diff --git a/cntmanage/flightslot/middleware.py b/cntmanage/flightslot/middleware.py index e61654b..81387de 100644 --- a/cntmanage/flightslot/middleware.py +++ b/cntmanage/flightslot/middleware.py @@ -7,8 +7,10 @@ class RedirectNonSuperuserFromAdminMiddleware: self.get_response = get_response def __call__(self, request: HttpRequest): - # Se l'utente è loggato, non è superuser e prova ad andare in /admin/... + # Se l'utente è loggato, non è superuser e prova ad andare in /admin/... o qualsiasi altro path if hasattr(request, "user") and not request.user.is_superuser: - if "/admin/" in request.path: + if hasattr(request.user, "student") and not "/user/" in request.path: return redirect("/user/") # redirect automatico + elif hasattr(request.user, "instructor") and not "/staff/" in request.path: + return redirect("/staff/") # redirect automatico return self.get_response(request) diff --git a/cntmanage/flightslot/migrations/0027_alter_weekpreference_student_availability.py b/cntmanage/flightslot/migrations/0027_alter_weekpreference_student_availability.py new file mode 100644 index 0000000..a28ec07 --- /dev/null +++ b/cntmanage/flightslot/migrations/0027_alter_weekpreference_student_availability.py @@ -0,0 +1,36 @@ +# Generated by Django 5.2.8 on 2025-12-02 10:02 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('flightslot', '0026_alter_student_email_alter_student_phone_instructor'), + ] + + operations = [ + migrations.AlterField( + model_name='weekpreference', + name='student', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='flightslot.student', verbose_name='Student Selection'), + ), + migrations.CreateModel( + name='Availability', + fields=[ + ('week', models.PositiveSmallIntegerField(auto_created=True, db_default=49, db_index=True, verbose_name='Week Number')), + ('id', models.BigAutoField(primary_key=True, serialize=False)), + ('monday', models.BooleanField(default=True)), + ('tuesday', models.BooleanField(default=True)), + ('wednesday', models.BooleanField(default=True)), + ('thursday', models.BooleanField(default=True)), + ('friday', models.BooleanField(default=True)), + ('saturday', models.BooleanField(default=True)), + ('sunday', models.BooleanField(default=True)), + ('hours', models.DurationField(null=True, verbose_name='Available hours')), + ('notes', models.TextField(blank=True, max_length=140, null=True)), + ('instructor', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='flightslot.instructor', verbose_name='Instructor Selection')), + ], + ), + ] diff --git a/cntmanage/flightslot/migrations/0028_alter_availability_options.py b/cntmanage/flightslot/migrations/0028_alter_availability_options.py new file mode 100644 index 0000000..3238a02 --- /dev/null +++ b/cntmanage/flightslot/migrations/0028_alter_availability_options.py @@ -0,0 +1,17 @@ +# Generated by Django 5.2.8 on 2025-12-02 10:59 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('flightslot', '0027_alter_weekpreference_student_availability'), + ] + + operations = [ + migrations.AlterModelOptions( + name='availability', + options={'verbose_name': 'Instructor Availability', 'verbose_name_plural': 'Instructor Availabilities'}, + ), + ] diff --git a/cntmanage/flightslot/models/availabilities.py b/cntmanage/flightslot/models/availabilities.py new file mode 100644 index 0000000..db1cda9 --- /dev/null +++ b/cntmanage/flightslot/models/availabilities.py @@ -0,0 +1,79 @@ +from django.db import models +from datetime import date + +from ..models.instructors import Instructor + +class Availability(models.Model): + id = models.BigAutoField( + primary_key=True + ) + + week = models.PositiveSmallIntegerField( + null=False, + db_index=True, + db_default=date.today().isocalendar().week, + auto_created=True, + verbose_name="Week Number" + ) + + instructor = models.ForeignKey( + Instructor, + null=False, + db_index=True, + on_delete=models.CASCADE, + verbose_name="Instructor Selection" + ) + + monday = models.BooleanField( + default=True, + null=False + ) + + tuesday = models.BooleanField( + default=True, + null=False + ) + + wednesday = models.BooleanField( + default=True, + null=False + ) + + thursday = models.BooleanField( + default=True, + null=False + ) + + friday = models.BooleanField( + default=True, + null=False + ) + + saturday = models.BooleanField( + default=True, + null=False + ) + + sunday = models.BooleanField( + default=True, + null=False + ) + + hours = models.DurationField( + null=True, + verbose_name="Available hours" + ) + + notes = models.TextField( + max_length=140, + null=True, + blank=True + ) + + class Meta(): + verbose_name = "Instructor Availability" + verbose_name_plural = "Instructor Availabilities" + + def __str__(self): + return f"Week {self.week} - {self.instructor.surname} {self.instructor.name[0]}." + \ No newline at end of file diff --git a/cntmanage/flightslot/models/weekpref.py b/cntmanage/flightslot/models/weekpref.py index ac7fad5..1d9d7d6 100644 --- a/cntmanage/flightslot/models/weekpref.py +++ b/cntmanage/flightslot/models/weekpref.py @@ -20,7 +20,7 @@ class WeekPreference(models.Model): Student, null=False, db_index=True, - on_delete=models.DO_NOTHING, + on_delete=models.CASCADE, verbose_name="Student Selection" ) -- 2.49.1 From 4b5319f557e6f5d663a97b71367045bbbc460b33 Mon Sep 17 00:00:00 2001 From: Emanuele Date: Tue, 2 Dec 2025 12:21:11 +0100 Subject: [PATCH 3/3] Instructor import --- cntmanage/flightslot/admin.py | 3 +- .../flightslot/admins/instructor_admin.py | 32 ++++++++++++++++++- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/cntmanage/flightslot/admin.py b/cntmanage/flightslot/admin.py index f6dd88b..6987d40 100644 --- a/cntmanage/flightslot/admin.py +++ b/cntmanage/flightslot/admin.py @@ -60,7 +60,8 @@ class FlightSlotStaffSite(AdminSite): # Register only user visible models flightslot_staff = FlightSlotUserSite(name="staff_site") flightslot_staff.register(Availability, AvailabilityAdmin) - +flightslot_staff.register(MissionProfile, MissionProfileAdmin) +flightslot_staff.register(Instructor, InstructorAdmin) # Get version for debug purposes ver: str = environ.get("VERSION", "dev") diff --git a/cntmanage/flightslot/admins/instructor_admin.py b/cntmanage/flightslot/admins/instructor_admin.py index 154dabf..a3d5c64 100644 --- a/cntmanage/flightslot/admins/instructor_admin.py +++ b/cntmanage/flightslot/admins/instructor_admin.py @@ -6,6 +6,11 @@ from django.utils.safestring import SafeText from django_admin_action_forms import AdminActionFormsMixin, AdminActionForm, action_with_form +from import_export import fields +from import_export.admin import ImportMixin +from import_export.tmp_storages import CacheStorage +from import_export.resources import ModelResource + from ..models.instructors import Instructor from ..models.aircrafts import AircraftTypes from ..models.missions import MissionTypes @@ -15,6 +20,28 @@ from ..actions.assign_aircraft import assign_aircraft from typing import Dict, List +# Resource Class for Instructor data import +class InstructorResource(ModelResource): + surname = fields.Field(attribute="surname", column_name="surname") + name = fields.Field(attribute="name", column_name="name") + email = fields.Field(attribute="email", column_name="email") + phone = fields.Field(attribute="phone", column_name="phone") + + # Cleanup fields before entering + def before_import_row(self, row: Dict[str, str], **kwargs) -> None: + row["name"] = SafeText("-".join(c.capitalize() for c in row["name"].split(" ")).strip()) + row["surname"] = SafeText("-".join(c.capitalize() for c in row["surname"].split(" ")).strip()) + row["phone"] = SafeText(row["phone"].replace(" ","")) + row["email"] = SafeText(row["email"].lower().strip()) + return super().before_import_row(row, **kwargs) + + class Meta: + model = Instructor + skip_unchanged = True + report_skipped = True + fields = ("surname", "name", "email", "phone", ) + import_id_fields = ("surname", "name", ) + # Form class to assing aircrafts to instructors class AssignMissionForm(AdminActionForm): mission_profiles = TypedMultipleChoiceField(choices=MissionTypes) @@ -23,12 +50,15 @@ class AssignMissionForm(AdminActionForm): class AssignAircraftForm(AdminActionForm): aircrafts = TypedMultipleChoiceField(choices=AircraftTypes) -class InstructorAdmin(AdminActionFormsMixin, admin.ModelAdmin): +class InstructorAdmin(ImportMixin, 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", ) + resource_classes = [InstructorResource] + tmp_storage_class = CacheStorage + skip_admin_log = True @admin.display(description="Password") def password(self, obj: Instructor) -> SafeText: -- 2.49.1