18 Commits

Author SHA1 Message Date
5291956a31 Add assigned aircraft tp output excel 2025-11-28 12:06:28 +01:00
7f908157bf Added git hash as version number 2025-11-28 11:48:29 +01:00
35f773047d Updated version, Updated poetry 2025-11-28 11:19:45 +01:00
65444e786b Added MEPIR and UPRT mix profile types 2025-11-28 11:14:07 +01:00
7a392df8ad Added assigned aircrafts to mission profiles 2025-11-28 11:04:12 +01:00
de39913275 Adde Aircraft class and associate students with aircrafts 2025-11-28 10:16:01 +01:00
33c610dcbc order export by week first 2025-11-27 13:07:08 +01:00
1c0b287666 Max 4 digits for user password 2025-11-27 12:56:46 +01:00
cc833c475f Fix gunicorn timeout and add coruse to student 2025-11-27 12:48:51 +01:00
09584e22fd Added search filed for student in weekpreference, refuel stop non null default false, migrations 2025-11-27 11:59:11 +01:00
97b14ae1d7 Add select course upon student bulk import, search field for students 2025-11-27 11:52:25 +01:00
18d2604121 Fixed user redirect middleware 2025-11-27 10:17:11 +01:00
b32d0fd032 Install polymorphic in prod 2025-11-25 12:26:48 +01:00
bf9f43eed8 Try fix 500 server error 2025-11-25 12:07:21 +01:00
18953e06b7 Added migrations 2025-11-25 12:01:48 +01:00
b79f0c318a Remove add permission if week expired 2025-11-25 11:51:46 +01:00
c91f603a50 Remove add permission if week expired 2025-11-25 11:51:24 +01:00
f7030e8da1 Merge pull request 'polymorphic' (#1) from polymorphic into flightslot
Reviewed-on: #1
2025-11-24 12:22:01 +01:00
27 changed files with 442 additions and 125 deletions

View File

@@ -53,7 +53,6 @@ IMPORT_EXPORT_SKIP_ADMIN_LOG = True
IMPORT_FORMATS = [CSV]
MIDDLEWARE = [
'flightslot.middleware.RedirectNonSuperuserFromAdminMiddleware', # custom middleware to show "user" page to non superuser
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
@@ -61,6 +60,7 @@ MIDDLEWARE = [
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'flightslot.middleware.RedirectNonSuperuserFromAdminMiddleware', # custom middleware to show "user" page to non superuser
]
ROOT_URLCONF = 'cntmanage.urls'

View File

@@ -47,6 +47,7 @@ INSTALLED_APPS = [
'colorfield',
'import_export',
'django_admin_action_forms',
'polymorphic'
]
# Import Export plugin settings
@@ -56,7 +57,6 @@ IMPORT_EXPORT_SKIP_ADMIN_LOG = True
IMPORT_FORMATS = [CSV]
MIDDLEWARE = [
'flightslot.middleware.RedirectNonSuperuserFromAdminMiddleware',
'django.middleware.security.SecurityMiddleware',
'whitenoise.middleware.WhiteNoiseMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
@@ -65,6 +65,7 @@ MIDDLEWARE = [
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'flightslot.middleware.RedirectNonSuperuserFromAdminMiddleware', # custom middleware to show "user" page to non superuser
]
ROOT_URLCONF = 'cntmanage.urls'

5
cntmanage/docker/build.sh Executable file
View File

@@ -0,0 +1,5 @@
#!/bin/bash
GIT_HASH=$(git rev-parse --short HEAD)
echo "Building Flightslot version ${GIT_HASH}"
docker compose build --build-arg GIT_HASH=${GIT_HASH}

View File

@@ -22,6 +22,8 @@ services:
build:
context: ..
dockerfile: ./docker/flightslot.Dockerfile
args:
GIT_HASH:
image: flightslot:latest
container_name: tech-flightslot
restart: unless-stopped

View File

@@ -23,5 +23,5 @@ else
echo "👁️ Superuser ${DJANGO_SUPERUSER_USERNAME} created successfully ..."
fi
echo "🚀 Launching Flightslot..."
echo "🚀 Launching Flightslot version ${VERSION} ..."
exec "$@"

View File

@@ -4,7 +4,6 @@ FROM python:3.12 AS builder
# Install Poetry
RUN curl -sSL https://install.python-poetry.org | python3 -
ENV PATH="${PATH}:/root/.local/bin"
RUN env
# Create build directory
WORKDIR /build
# Copy project files
@@ -14,28 +13,26 @@ RUN poetry update --no-interaction --no-ansi
# Build project
RUN poetry build
### STAGE 2 — Final image
### STAGE 2 — Final image ###
FROM python:3.12-slim AS deploy
# Create app run directory
WORKDIR /app
# Copy application custom static files
RUN mkdir -p static
COPY ./static/cantorair.jpg ./static
COPY ./static/cantorair_blue.jpg ./static
# Copy application custom templates for admin page
RUN mkdir -p /templates/admin
COPY ./templates/admin/* ./templates/admin/
# Copy and install application wheel package
COPY --from=builder /build/dist/*.whl ./
RUN pip install --no-cache-dir *.whl
RUN pip install gunicorn whitenoise
# Copy entryupoint bash script
COPY ./docker/entrypoint.sh ./
# Collect build number from build arg
ARG GIT_HASH
ENV VERSION=${GIT_HASH}
ENTRYPOINT ["/app/entrypoint.sh"]
# Command to be executed after entry point
CMD ["gunicorn", "cntmanage.wsgi:application", "--bind", "0.0.0.0:8000"]
CMD ["gunicorn", "cntmanage.wsgi:application", "--bind", "0.0.0.0:8000", "--timeout", "600"]

View File

@@ -5,13 +5,12 @@ from openpyxl import Workbook
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
from openpyxl.utils import get_column_letter
from datetime import date, datetime
from typing import List
from ..models.weekpref import WeekPreference
from ..models.missions import Training
from ..models.weekpref import WeekPreference
from ..models.hourbuildings import HourBuilding, HourBuildingLegFlight, HourBuildingLegStop, HourBuildingLegBase
from datetime import date, datetime
from typing import List
def export_selected(request: HttpRequest, queryset: QuerySet[WeekPreference]) -> HttpResponse:
@@ -79,12 +78,21 @@ def export_selected(request: HttpRequest, queryset: QuerySet[WeekPreference]) ->
# Each of this iterations fills the table for a student
row: int = 2
row_offset: int = 0
for i, q in enumerate(queryset.order_by("student__surname", "student__name", "student__course"), start=1):
for i, q in enumerate(queryset.order_by("week", "student__surname", "student__name", "student__course"), start=1):
student_data: List[str]
student_phone: str = q.student.phone if q.student.phone else ""
student_email: str = q.student.email
student_course_type: str
student_course_number: str
student_course_ac: str
if q.student.course:
student_data = [f"{q.student.surname} {q.student.name}", f"{q.student.course.ctype}-{q.student.course.cnumber}"]
student_course_type = q.student.course.ctype
student_course_number = str(q.student.course.cnumber)
student_course_ac = " / ".join(t.type for t in q.student.aircrafts.distinct("type").all())
student_data = [
f"{q.student.surname} {q.student.name}\n{student_course_ac}",
f"{student_course_type}-{student_course_number}"
]
else:
student_data = [f"{q.student.surname} {q.student.name}", f"No Course Assigned"]

View File

@@ -1,19 +1,23 @@
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
from .admins.weekpred_adm import WeekPreferenceAdmin
#from .admins.hourbuilding_adm import HourBuilding, HourBuildingInLine
from .admins.weekpref_adm import WeekPreferenceAdmin
from django.contrib.admin import AdminSite
from os import environ
# User website under /user/ URL
class FlightSlotUserSite(AdminSite):
site_header = "Flight Scheduler 🛫"
site_title = "Flight Scheduler 🛫"
@@ -27,15 +31,19 @@ class FlightSlotUserSite(AdminSite):
return app_list
# Register only user visible models
flightslot_user = FlightSlotUserSite(name="user_site")
# registra SOLO i modelli autorizzati
flightslot_user.register(WeekPreference, WeekPreferenceAdmin)
admin.site.site_header = "Flight Scheduler Admin 🛫"
admin.site.site_title = "Flight Scheduler Admin 🛫"
# Get version for debug purposes
ver: str = environ.get("VERSION", "dev")
# Register all visible models
admin.site.site_header = f"Flight Scheduler Admin 🛫 - ver.{ver}"
admin.site.site_title = f"Flight Scheduler Admin 🛫 - ver.{ver}"
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)

View File

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

View File

@@ -51,7 +51,6 @@ class HourBuildingLegBaseInLine(nested_admin.NestedStackedPolymorphicInline):
form = HourBuildingLegFlightForm
fk_name = "hourbuildinglegbase_ptr"
fields = ("departure", "time", "destination", "pax", )
hide_title = True
class HourBuildingLegStopInLine(nested_admin.NestedStackedPolymorphicInline.Child):
model = HourBuildingLegStop
@@ -61,18 +60,6 @@ class HourBuildingLegBaseInLine(nested_admin.NestedStackedPolymorphicInline):
child_inlines = (HourBuildingLegFlightInLine, HourBuildingLegStopInLine, )
# If user is a student deny edit permission for week past the current one
def has_change_permission(self, request: HttpRequest, obj: HourBuilding | None = None):
if hasattr(request.user, "student") and obj:
current_week = date.today().isocalendar().week
if not obj.DoesNotExist and current_week > obj.weekpref.week:
return False
return True
def has_delete_permission(self, request: HttpRequest, obj: HourBuilding | None = None):
return self.has_change_permission(request=request, obj=obj)
class HourBuildingInLine(nested_admin.NestedTabularInline):
model = HourBuilding
inlines = (HourBuildingLegBaseInLine,)
@@ -85,3 +72,16 @@ class HourBuildingInLine(nested_admin.NestedTabularInline):
models.TextField: {"widget": Textarea(attrs={"rows":4, "cols":35})},
}
# 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
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) -> bool:
return self.has_change_permission(request=request, obj=obj)

View File

@@ -1,17 +1,21 @@
from django.contrib import admin
from django.forms import ModelMultipleChoiceField
from django.contrib import admin, messages
from django.http import HttpRequest
from django.db.models.query import QuerySet, Q
from django.utils.safestring import SafeText
from typing import Any
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.aircrafts import Aircraft
from ..models.missions import MissionProfile
from django_admin_action_forms import AdminActionFormsMixin
from datetime import timedelta
from typing import Any, Dict
# Resource Class for Student data import
class MissionProfileResource(ModelResource):
@@ -20,20 +24,50 @@ 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
skip_unchanged = True
report_skipped = True
fields = ("mtype", "mnum", "duration")
import_id_fields = ("mtype", "mnum")
fields = ("mtype", "mnum", "duration",)
import_id_fields = ("mtype", "mnum",)
# Form class to assing aircrafts to students
class ChangeAircraftForm(AdminActionForm):
aircrafts = ModelMultipleChoiceField(queryset=Aircraft.objects.distinct('type').all())
class MissionProfileAdmin(ImportMixin, AdminActionFormsMixin, admin.ModelAdmin):
list_display = ("mtype", "mnum", "notes")
list_display = ("mtype", "mnum", "assigned_aircrafts", "duration", "notes", )
list_filter = ("mtype", )
actions = ("assign_aircraft", )
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}")
@admin.display(description="Assigned Aircrafts")
def assigned_aircrafts(self, obj: MissionProfile) -> SafeText:
if not obj.aircrafts:
return SafeText("")
return SafeText("/".join(ac.markings for ac in obj.aircrafts.all()))

View File

@@ -1,21 +1,31 @@
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
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 import_export.widgets import CharWidget
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.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 = ModelChoiceField(
queryset=Course.objects.all(),
required=False)
# Resource Class for Student data import
class StudentResource(ModelResource):
surname = fields.Field(attribute="surname", column_name="surname")
@@ -24,29 +34,44 @@ class StudentResource(ModelResource):
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(row["name"].capitalize().strip())
row["surname"] = SafeText(row["surname"].capitalize().strip())
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)
# If course was addedd as a form kwasrg add it to the student after creation
def after_init_instance(self, instance: Student, new: bool, row: Dict[str, str], **kwargs: Dict[str, Any | Course]):
course = kwargs.get("course", None)
if course and isinstance(course, Course):
instance.course = course
class Meta:
model = Student
skip_unchanged = True
report_skipped = True
fields = ("surname", "name", "email", "phone")
import_id_fields = ("email", "phone")
fields = ("surname", "name", "email", "phone",)
import_id_fields = ("email", "phone",)
# Form Class for Student course change
class ChangeCourseForm(AdminActionForm):
course = forms.ModelChoiceField(queryset=Course.objects.all())
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", "password", "active")
list_filter = ("course", "active")
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
skip_admin_log = True
@admin.display(description="Color")
def course_color(self, obj: Student) -> SafeText:
@@ -58,6 +83,10 @@ class StudentAdmin(ImportMixin, AdminActionFormsMixin, admin.ModelAdmin):
def password(self, obj: Student) -> SafeText:
return SafeText(obj.default_password())
@admin.display(description="Username")
def username(self, obj: Student) -> SafeText:
return SafeText(obj.default_username())
@admin.action(description="Deactivate Students")
def disable_students(self, request: HttpRequest, queryset: QuerySet[Student]):
for q in queryset.all():
@@ -66,10 +95,40 @@ 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):
course = data["course"]
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) -> 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) -> 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)
return kwargs

View File

@@ -26,12 +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) -> bool:
return self.has_change_permission(request=request, obj=obj)

View File

@@ -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,11 +18,13 @@ 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, )
list_display = ("week", "student__surname","student__name", "student__course", "course_color", "student_brief_mix",)
list_filter = ("week", "student__course", "student",)
list_filter = ("week", "student__course",)
search_fields = ("student__surname","student__name",)
actions = ("export",)
@admin.action(description="Export Selected Preferences")
@@ -44,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)

View File

@@ -8,11 +8,7 @@ class RedirectNonSuperuserFromAdminMiddleware:
def __call__(self, request: HttpRequest):
# Se l'utente è loggato, non è superuser e prova ad andare in /admin/...
if hasattr(request,"user"):
if (
request.path.startswith("/admin/") and
hasattr(request.user, 'student')
):
if hasattr(request, "user") and not request.user.is_superuser:
if "/admin/" in request.path:
return redirect("/user/") # redirect automatico
return self.get_response(request)

View File

@@ -0,0 +1,35 @@
# Generated by Django 5.2.8 on 2025-11-25 11:01
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('flightslot', '0019_remove_hourbuildinglegstop_location_and_more'),
]
operations = [
migrations.AlterModelOptions(
name='hourbuildinglegbase',
options={'base_manager_name': 'objects', 'verbose_name': 'Flight Leg or Stop', 'verbose_name_plural': 'Flight Legs or Stops'},
),
migrations.AlterModelOptions(
name='hourbuildinglegflight',
options={'base_manager_name': 'objects', 'verbose_name': 'Flight leg', 'verbose_name_plural': 'Flight legs'},
),
migrations.AlterModelOptions(
name='hourbuildinglegstop',
options={'base_manager_name': 'objects', 'verbose_name': 'Stop', 'verbose_name_plural': 'Stops'},
),
migrations.AlterField(
model_name='hourbuildinglegflight',
name='pax',
field=models.CharField(blank=True, max_length=16, null=True, verbose_name='Pax (optional)'),
),
migrations.AlterField(
model_name='weekpreference',
name='week',
field=models.PositiveSmallIntegerField(auto_created=True, db_default=48, db_index=True, verbose_name='Week Number'),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.2.8 on 2025-11-27 10:56
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('flightslot', '0020_alter_hourbuildinglegbase_options_and_more'),
]
operations = [
migrations.AlterField(
model_name='hourbuildinglegstop',
name='refuel',
field=models.BooleanField(default=False, verbose_name='Stop for Refuelling'),
),
]

View File

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

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.2.8 on 2025-11-28 09:28
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('flightslot', '0022_aircraft_alter_course_color_and_more'),
]
operations = [
migrations.AddField(
model_name='missionprofile',
name='aircrafts',
field=models.ManyToManyField(to='flightslot.aircraft'),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.2.8 on 2025-11-28 10:12
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('flightslot', '0023_missionprofile_aircrafts'),
]
operations = [
migrations.AlterField(
model_name='missionprofile',
name='mtype',
field=models.CharField(choices=[('OTHER', 'OTHER'), ('CHK', 'CHK_6M'), ('PPL', 'PPL'), ('IR', 'IR'), ('MEP', 'MEP'), ('MEP_IR', 'MEP_IR'), ('CPL', 'CPL'), ('UPRT', 'UPRT'), ('FI', 'FI'), ('PC', 'PC')], default='PPL', verbose_name='Mission Type'),
),
]

View File

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

View File

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

View File

@@ -125,7 +125,9 @@ class HourBuildingLegFlight(HourBuildingLegBase):
class HourBuildingLegStop(HourBuildingLegBase):
refuel = models.BooleanField (
default=False
null=False,
default=False,
verbose_name="Stop for Refuelling"
)
# Change displayed name in the inline form

View File

@@ -3,16 +3,19 @@ from django.db import models
from datetime import timedelta
from ..models.weekpref import WeekPreference
from ..models.aircrafts import Aircraft
class MissionType(models.TextChoices):
OTHER = "OTHER", _("OTHER")
CHK = "CHK", _("CHK_6M")
PPL = "PPL", _("PPL")
IR = "IR", _("IR")
MEP = "MEP", _("MEP")
MEP_IR = "MEP_IR", _("MEP_IR")
CPL = "CPL", _("CPL")
UPRT = "UPRT", _("UPRT")
FI = "FI", _("FI")
PC = "PC", _("PC")
CHK = "CHK", _("CHK_6M")
class MissionProfile(models.Model):
id = models.AutoField(
@@ -37,6 +40,10 @@ class MissionProfile(models.Model):
default=timedelta(hours=1)
)
aircrafts = models.ManyToManyField(
Aircraft
)
notes = models.TextField(
max_length=140,
null=True,

View File

@@ -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,8 +47,18 @@ class Student(models.Model):
blank=True
)
def default_password(self) -> str:
return f"{self.name.lower()[0]}{self.surname.lower()}{self.id}"
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}"
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):

8
cntmanage/poetry.lock generated
View File

@@ -385,18 +385,18 @@ files = [
[[package]]
name = "sqlparse"
version = "0.5.3"
version = "0.5.4"
description = "A non-validating SQL parser."
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "sqlparse-0.5.3-py3-none-any.whl", hash = "sha256:cf2196ed3418f3ba5de6af7e82c694a9fbdbfecccdfc72e281548517081f16ca"},
{file = "sqlparse-0.5.3.tar.gz", hash = "sha256:09f67787f56a0b16ecdbde1bfc7f5d9c3371ca683cfeaa8e6ff60b4807ec9272"},
{file = "sqlparse-0.5.4-py3-none-any.whl", hash = "sha256:99a9f0314977b76d776a0fcb8554de91b9bb8a18560631d6bc48721d07023dcb"},
{file = "sqlparse-0.5.4.tar.gz", hash = "sha256:4396a7d3cf1cd679c1be976cf3dc6e0a51d0111e87787e7a8d780e7d5a998f9e"},
]
[package.extras]
dev = ["build", "hatch"]
dev = ["build"]
doc = ["sphinx"]
[[package]]

View File

@@ -1,6 +1,6 @@
[tool.poetry]
name = "cntmanage"
version = "0.1.0"
version = "0.2.0"
packages = [{include = "flightslot"}, {include = "cntmanage"}]
description = "CantorAir Flight Scheduler"
authors = ["Emanuele <ema.trabattoni@gmail.com>"]