8 Commits

28 changed files with 671 additions and 83 deletions

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,23 @@
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] | 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
for a in ac_types:
ac_query |= Q(type=a)
aircrafts: QuerySet[Aircraft] = Aircraft.objects.filter(ac_query).all() # Execute query
for obj in queryset:
obj.aircrafts.clear()
for ac in aircrafts:
obj.aircrafts.add(ac)
obj.save()
i += 1
return i, [a for a in ac_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,22 +6,28 @@ 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.availabilities import Availability
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 .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)
@@ -35,6 +41,27 @@ 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)
flightslot_staff.register(MissionProfile, MissionProfileAdmin)
flightslot_staff.register(Instructor, InstructorAdmin)
# Get version for debug purposes
ver: str = environ.get("VERSION", "dev")
@@ -48,3 +75,5 @@ 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)
admin.site.register(Availability, AvailabilityAdmin)

View File

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

View File

@@ -1,13 +1,25 @@
from django.contrib import admin
from django.db.models.query import QuerySet
from django.http import HttpRequest
from django.utils.safestring import SafeText
from ..models.students import Student
from ..models.courses import Course
from ..custom.colortag import course_color
class CourseAdmin(admin.ModelAdmin):
list_display = ("ctype", "cnumber","color_display", "year")
list_display = ("ctype", "cnumber","color_display", "course_students", "year")
list_filter = ("ctype", "year")
def get_queryset(self, request: HttpRequest) -> QuerySet:
return super().get_queryset(request).order_by("ctype", "cnumber")
@admin.display(description="Student Number")
def course_students(self, obj: Course) -> SafeText:
if not obj.pk:
return SafeText("")
return SafeText(f"{Student.objects.filter(course = obj.id).count()}")
# Dinamically add color_display property to show a colored dot
@admin.display(description="Color")
def color_display(self, obj: Course) -> SafeText:

View File

@@ -10,7 +10,7 @@ from durationwidget.widgets import TimeDurationWidget
from ..models.hourbuildings import HourBuilding, HourBuildingLegBase, HourBuildingLegFlight, HourBuildingLegStop
from ..models.weekpref import WeekPreference
from datetime import date
from ..custom.student_permissions import has_edit_permission
class HourBuildingLegFlightForm(forms.ModelForm):
class Meta:
@@ -74,11 +74,7 @@ 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) -> bool:
if hasattr(request.user, 'student') and obj:
current_week: int = date.today().isocalendar().week
if current_week > obj.week:
return False
return True
return has_edit_permission(request=request, obj=obj)
def has_delete_permission(self, request: HttpRequest, obj: WeekPreference | None = None) -> bool:
return self.has_change_permission(request=request, obj=obj)

View File

@@ -0,0 +1,95 @@
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 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
from ..actions.assign_profile import assign_profile
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)
# Form class to assing aircrafts to instructors
class AssignAircraftForm(AdminActionForm):
aircrafts = TypedMultipleChoiceField(choices=AircraftTypes)
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:
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

@@ -1,7 +1,7 @@
from django.forms import ModelMultipleChoiceField
from django.forms import TypedMultipleChoiceField
from django.contrib import admin, messages
from django.http import HttpRequest
from django.db.models.query import QuerySet, Q
from django.db.models.query import QuerySet
from django.utils.safestring import SafeText
from django_admin_action_forms import AdminActionFormsMixin, AdminActionForm, action_with_form
@@ -11,11 +11,13 @@ from import_export.admin import ImportMixin
from import_export.tmp_storages import CacheStorage
from import_export.resources import ModelResource
from ..models.aircrafts import Aircraft
from ..models.aircrafts import AircraftTypes
from ..models.missions import MissionProfile
from ..actions.assign_aircraft import assign_aircraft
from datetime import timedelta
from typing import Any, Dict
from typing import Any, Dict, List
# Resource Class for Student data import
class MissionProfileResource(ModelResource):
@@ -40,30 +42,25 @@ class MissionProfileResource(ModelResource):
# Form class to assing aircrafts to students
class ChangeAircraftForm(AdminActionForm):
aircrafts = ModelMultipleChoiceField(queryset=Aircraft.objects.distinct('type').all())
aircrafts = TypedMultipleChoiceField(choices=AircraftTypes)
class MissionProfileAdmin(ImportMixin, AdminActionFormsMixin, admin.ModelAdmin):
list_display = ("mtype", "mnum", "assigned_aircrafts", "duration", "notes", )
list_filter = ("mtype", )
actions = ("assign_aircraft", )
resource_classes = [MissionProfileResource]
tmp_storage_class = CacheStorage
skip_admin_log = True
@action_with_form(ChangeAircraftForm, description="Assign Aircraft")
def assign_aircraft(self, request: HttpRequest, queryset: QuerySet[MissionProfile], 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 mix in queryset:
mix.aircrafts.clear()
for ac in aircrafts:
mix.aircrafts.add(ac)
mix.save()
i += 1
messages.success(request, f"{i} Students updated to {ac_types}")
def get_queryset(self, request: HttpRequest) -> QuerySet[MissionProfile]:
return super().get_queryset(request).order_by("mtype", "mnum")
@action_with_form(ChangeAircraftForm, description="Assign Aircraft Type")
def assign_aircraft(self, request: HttpRequest, queryset: QuerySet[MissionProfile], 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} Missions updated to {ac_types}")
@admin.display(description="Assigned Aircrafts")
def assigned_aircrafts(self, obj: MissionProfile) -> SafeText:

View File

@@ -1,5 +1,5 @@
from django.forms import ModelChoiceField, TypedMultipleChoiceField, ModelMultipleChoiceField
from django.db.models.query import QuerySet, Q
from django.forms import ModelChoiceField, 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
@@ -12,13 +12,15 @@ from import_export.forms import ConfirmImportForm, ImportForm
from django_admin_action_forms import AdminActionFormsMixin, AdminActionForm, action_with_form
from ..models.aircrafts import Aircraft, AircraftTypes
from ..models.aircrafts import AircraftTypes
from ..models.courses import Course
from ..models.students import Student
from ..actions.assign_aircraft import assign_aircraft
from ..custom.colortag import course_color
from typing import Any, Dict
from typing import Any, Dict, List
# Custom import form to select a course for student input
class StudentCustomConfirmImportForm(ConfirmImportForm):
@@ -56,18 +58,18 @@ class StudentResource(ModelResource):
# Form Class for Student course change
class ChangeCourseForm(AdminActionForm):
course = TypedMultipleChoiceField(choices=AircraftTypes)
course = ModelChoiceField(queryset=Course.objects.all().order_by("ctype", "-cnumber"))
# Form class to assing aircrafts to students
class ChangeAircraftForm(AdminActionForm):
aircrafts = ModelMultipleChoiceField(queryset=Aircraft.objects.distinct('type').all())
aircrafts = TypedMultipleChoiceField(choices=AircraftTypes)
class StudentAdmin(ImportMixin, AdminActionFormsMixin, admin.ModelAdmin):
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", )
actions = ("change_course", "deactivate_students", "change_aircraft", )
resource_classes = [StudentResource]
confirm_form_class = StudentCustomConfirmImportForm
tmp_storage_class = CacheStorage
@@ -88,7 +90,7 @@ class StudentAdmin(ImportMixin, AdminActionFormsMixin, admin.ModelAdmin):
return SafeText(obj.default_username())
@admin.action(description="Deactivate Students")
def disable_students(self, request: HttpRequest, queryset: QuerySet[Student]):
def deactivate_students(self, request: HttpRequest, queryset: QuerySet[Student]):
for q in queryset.all():
if q.user:
q.user.is_staff = False
@@ -102,20 +104,11 @@ 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
@action_with_form(ChangeAircraftForm, description="Assign Aircraft Type")
def change_aircraft(self, request: HttpRequest, queryset: QuerySet[Student], 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} Students updated to {ac_types}")
# Return the initial form for import confirmations, request course to user

View File

@@ -7,6 +7,8 @@ from django.http import HttpRequest
from ..models.missions import Training
from ..models.weekpref import WeekPreference
from ..custom.student_permissions import has_edit_permission
from datetime import date
class TrainingForm(forms.ModelForm):
@@ -27,11 +29,7 @@ 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) -> bool:
if hasattr(request.user, 'student') and obj:
current_week: int = date.today().isocalendar().week
if current_week > obj.week:
return False
return True
return has_edit_permission(request=request, obj=obj)
def has_delete_permission(self, request: HttpRequest, obj: WeekPreference | None = None) -> bool:
return self.has_change_permission(request=request, obj=obj)

View File

@@ -2,12 +2,12 @@ import nested_admin
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
from django.utils.safestring import SafeText
from ..models.students import Student
from ..models.missions import Training
from ..models.weekpref import WeekPreference
@@ -15,6 +15,7 @@ from .training_adm import TrainingInLIne
from .hourbuilding_adm import HourBuildingInLine
from ..custom.colortag import course_color
from ..custom.student_permissions import has_edit_permission
from ..actions.exportweek import export_selected
from datetime import date
@@ -62,7 +63,7 @@ class WeekPreferenceAdmin(nested_admin.NestedPolymorphicModelAdmin):
# If a user is registered as student show only their preferences
def get_queryset(self, request: HttpRequest) -> QuerySet[WeekPreference]:
qs = super().get_queryset(request)
qs = super().get_queryset(request).order_by("-week", "-student__course", "student__surname", "student__name")
if hasattr(request.user, "student"):
return qs.filter(student=request.user.student)
# If admin show everything
@@ -79,7 +80,7 @@ class WeekPreferenceAdmin(nested_admin.NestedPolymorphicModelAdmin):
# If student is current user making request
if hasattr(request.user, "student"):
student = request.user.student
student: Student = request.user.student
if "student" in form.base_fields:
form.base_fields["student"].initial = student
form.base_fields["student"].disabled = True
@@ -88,26 +89,21 @@ class WeekPreferenceAdmin(nested_admin.NestedPolymorphicModelAdmin):
# 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) -> bool:
if hasattr(request.user, "student") and obj:
current_week = date.today().isocalendar().week
if current_week > obj.week:
return False
return True
return has_edit_permission(request=request, obj=obj)
# 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:
current_week = date.today().isocalendar().week
weekpref = WeekPreference.objects.get(id=object_id)
if current_week > weekpref.week:
if not has_edit_permission(request=request, obj=weekpref):
extra_context["show_save"] = False
extra_context["show_save_and_continue"] = False
extra_context["show_save_and_add_another"] = False

View File

@@ -0,0 +1,17 @@
from django.http import HttpRequest
from ..models.students import Student
from ..models.weekpref import WeekPreference
from datetime import date
# allow add, modify, delete depending on a set of requirements
def has_edit_permission(request: HttpRequest, obj: WeekPreference | None = None) -> bool:
if hasattr(request.user, 'student'):
student: Student = request.user.student
if not student.active:
return False
current_week: int = date.today().isocalendar().week
if obj and (current_week > obj.week or not student.active):
return False
return True

View File

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

View File

@@ -0,0 +1,28 @@
# Generated by Django 5.2.8 on 2025-12-01 12:37
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('flightslot', '0024_alter_missionprofile_mtype'),
]
operations = [
migrations.AlterField(
model_name='aircraft',
name='type',
field=models.CharField(choices=[('C152', 'Cessna 152'), ('P208', 'Tecnam P2008'), ('PA28', 'Piper PA28R'), ('PA34', 'Piper PA34'), ('C182', 'Cessna 182Q'), ('TWEN', 'Tecnam P2010'), ('CP10', 'Cap 10'), ('FSTD', 'Alsim ALX40')], max_length=4),
),
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'), ('CP10', 'Cap 10'), ('FSTD', 'Alsim ALX40')]),
),
migrations.AlterField(
model_name='weekpreference',
name='week',
field=models.PositiveSmallIntegerField(auto_created=True, db_default=49, db_index=True, verbose_name='Week Number'),
),
]

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,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')),
],
),
]

View File

@@ -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'},
),
]

View File

@@ -8,6 +8,7 @@ class AircraftTypes(models.TextChoices):
PA34 = "PA34", _("Piper PA34")
C182 = "C182", _("Cessna 182Q")
P210 = "TWEN", _("Tecnam P2010")
CP10 = "CP10", _("Cap 10")
ALX40 = "FSTD", _("Alsim ALX40")
class Aircraft(models.Model):

View File

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

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

View File

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

View File

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

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

View File

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