diff --git a/cntmanage/docker/build.sh b/cntmanage/docker/build.sh new file mode 100755 index 0000000..acd9c88 --- /dev/null +++ b/cntmanage/docker/build.sh @@ -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} diff --git a/cntmanage/docker/docker-compose.yml b/cntmanage/docker/docker-compose.yml index 6a1fd0b..2dc0d18 100644 --- a/cntmanage/docker/docker-compose.yml +++ b/cntmanage/docker/docker-compose.yml @@ -22,6 +22,8 @@ services: build: context: .. dockerfile: ./docker/flightslot.Dockerfile + args: + GIT_HASH: image: flightslot:latest container_name: tech-flightslot restart: unless-stopped diff --git a/cntmanage/docker/entrypoint.sh b/cntmanage/docker/entrypoint.sh index 8491eef..ff3a37f 100755 --- a/cntmanage/docker/entrypoint.sh +++ b/cntmanage/docker/entrypoint.sh @@ -23,5 +23,5 @@ else echo "๐Ÿ‘๏ธ Superuser ${DJANGO_SUPERUSER_USERNAME} created successfully ..." fi -echo "๐Ÿš€ Launching Flightslot..." +echo "๐Ÿš€ Launching Flightslot version ${VERSION} ..." exec "$@" diff --git a/cntmanage/docker/flightslot.Dockerfile b/cntmanage/docker/flightslot.Dockerfile index a5c3a90..7fea967 100644 --- a/cntmanage/docker/flightslot.Dockerfile +++ b/cntmanage/docker/flightslot.Dockerfile @@ -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", "--timeout", "600"] diff --git a/cntmanage/flightslot/actions/exportweek.py b/cntmanage/flightslot/actions/exportweek.py index c2e82ab..d0bb756 100644 --- a/cntmanage/flightslot/actions/exportweek.py +++ b/cntmanage/flightslot/actions/exportweek.py @@ -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: @@ -83,8 +82,17 @@ def export_selected(request: HttpRequest, queryset: QuerySet[WeekPreference]) -> 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"] diff --git a/cntmanage/flightslot/admin.py b/cntmanage/flightslot/admin.py index 59022a9..af1f613 100644 --- a/cntmanage/flightslot/admin.py +++ b/cntmanage/flightslot/admin.py @@ -1,11 +1,13 @@ from django.contrib import admin from django.http import HttpRequest +from .models.aircrafts import Aircraft from .models.courses import Course from .models.students import Student from .models.missions import MissionProfile from .models.weekpref import WeekPreference +from .admins.aircraft_adm import AircraftAdmin from .admins.course_adm import CourseAdmin from .admins.student_adm import StudentAdmin from .admins.mission_adm import MissionProfileAdmin @@ -13,6 +15,9 @@ 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 ๐Ÿ›ซ" @@ -26,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) diff --git a/cntmanage/flightslot/admins/aircraft_adm.py b/cntmanage/flightslot/admins/aircraft_adm.py new file mode 100644 index 0000000..306c184 --- /dev/null +++ b/cntmanage/flightslot/admins/aircraft_adm.py @@ -0,0 +1,17 @@ +from django.contrib import admin +from django.db.models.query import QuerySet +from django.http import HttpRequest + +from ..models.aircrafts import Aircraft + +class AircraftAdmin(admin.ModelAdmin): + model = Aircraft + list_display = ("type", "markings", "avail_hours", "complex", ) + list_filter = ("type", ) + actions = ("reset_maint") + + def get_queryset(self, request: HttpRequest) -> QuerySet[Aircraft]: + qs: QuerySet[Aircraft] = super().get_queryset(request) + qs.order_by("type", "markings") + return qs + \ No newline at end of file diff --git a/cntmanage/flightslot/admins/hourbuilding_adm.py b/cntmanage/flightslot/admins/hourbuilding_adm.py index 5f6794f..133d185 100644 --- a/cntmanage/flightslot/admins/hourbuilding_adm.py +++ b/cntmanage/flightslot/admins/hourbuilding_adm.py @@ -73,15 +73,15 @@ class HourBuildingInLine(nested_admin.NestedTabularInline): } # If user is a student deny edit permission for week past the current one - def has_change_permission(self, request: HttpRequest, obj: WeekPreference | None = None): + def has_change_permission(self, request: HttpRequest, obj: WeekPreference | None = None) -> bool: if hasattr(request.user, 'student') and obj: current_week: int = date.today().isocalendar().week if current_week > obj.week: return False return True - def has_delete_permission(self, request: HttpRequest, obj: WeekPreference | None = None): + def has_delete_permission(self, request: HttpRequest, obj: WeekPreference | None = None) -> bool: return self.has_change_permission(request=request, obj=obj) - def has_add_permission(self, request: HttpRequest, obj: WeekPreference | None = None): + def has_add_permission(self, request: HttpRequest, obj: WeekPreference | None = None) -> bool: return self.has_change_permission(request=request, obj=obj) diff --git a/cntmanage/flightslot/admins/mission_adm.py b/cntmanage/flightslot/admins/mission_adm.py index 3dafa1d..9932f43 100644 --- a/cntmanage/flightslot/admins/mission_adm.py +++ b/cntmanage/flightslot/admins/mission_adm.py @@ -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",) -class MissionProfileAdmin(AdminActionFormsMixin, ImportMixin, admin.ModelAdmin): - list_display = ("mtype", "mnum", "notes") - list_filter = ("mtype",) +# 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", "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())) + \ No newline at end of file diff --git a/cntmanage/flightslot/admins/student_adm.py b/cntmanage/flightslot/admins/student_adm.py index 91bc269..419f61c 100644 --- a/cntmanage/flightslot/admins/student_adm.py +++ b/cntmanage/flightslot/admins/student_adm.py @@ -1,5 +1,5 @@ -from django import forms -from django.db.models.query import QuerySet +from django.forms import ModelChoiceField, TypedMultipleChoiceField, ModelMultipleChoiceField +from django.db.models.query import QuerySet, Q from django.http import HttpRequest from django.contrib import admin, messages from django.utils.safestring import SafeText @@ -12,16 +12,17 @@ from import_export.forms import ConfirmImportForm, ImportForm from django_admin_action_forms import AdminActionFormsMixin, AdminActionForm, action_with_form -from typing import Any, Dict - +from ..models.aircrafts import Aircraft, AircraftTypes from ..models.courses import Course from ..models.students import Student from ..custom.colortag import course_color +from typing import Any, Dict + # Custom import form to select a course for student input class StudentCustomConfirmImportForm(ConfirmImportForm): - course = forms.ModelChoiceField( + course = ModelChoiceField( queryset=Course.objects.all(), required=False) @@ -55,13 +56,18 @@ class StudentResource(ModelResource): # Form Class for Student course change class ChangeCourseForm(AdminActionForm): - course = forms.ModelChoiceField(queryset=Course.objects.all()) + course = TypedMultipleChoiceField(choices=AircraftTypes) + +# Form class to assing aircrafts to students +class ChangeAircraftForm(AdminActionForm): + aircrafts = ModelMultipleChoiceField(queryset=Aircraft.objects.distinct('type').all()) class StudentAdmin(ImportMixin, AdminActionFormsMixin, admin.ModelAdmin): - list_display = ("surname", "name", "course", "course_color", "email", "phone", "username", "password", "active",) - list_filter = ("course", "active",) - search_fields = ("surname", "name", "phone", "email",) - actions = ("change_course", "disable_students",) + model = Student + list_display = ("surname", "name", "course", "course_color", "email", "phone", "username", "password", "active", ) + list_filter = ("course", "active", ) + search_fields = ("surname", "name", "phone", "email", ) + actions = ("change_course", "disable_students", "change_aircraft", ) resource_classes = [StudentResource] confirm_form_class = StudentCustomConfirmImportForm tmp_storage_class = CacheStorage @@ -89,7 +95,6 @@ class StudentAdmin(ImportMixin, AdminActionFormsMixin, admin.ModelAdmin): q.user.save() count: int = queryset.update(active = False) messages.success(request, f"{count} students deactivated") - pass @action_with_form(ChangeCourseForm, description="Change Student Course") def change_course(self, request: HttpRequest, queryset: QuerySet[Student], data): @@ -97,16 +102,32 @@ class StudentAdmin(ImportMixin, AdminActionFormsMixin, admin.ModelAdmin): count: int = queryset.update(course=course) messages.success(request, f"{count} students updated to {course}") + @action_with_form(ChangeAircraftForm, description="Assign Aircraft") + def change_aircraft(self, request: HttpRequest, queryset: QuerySet[Student], data: Dict[str, QuerySet[Aircraft]]): + ac_types = [t.type for t in data["aircrafts"]] + ac_query: Q = Q() # Build an or query to select all aircrafts of the specified types + for a in ac_types: + ac_query |= Q(type=a) + aircrafts: QuerySet[Aircraft] = Aircraft.objects.filter(ac_query).all() # Execute query + i: int = 0 + for student in queryset: + student.aircrafts.clear() + for ac in aircrafts: + student.aircrafts.add(ac) + student.save() + i += 1 + messages.success(request, f"{i} Students updated to {ac_types}") + # Return the initial form for import confirmations, request course to user - def get_confirm_form_initial(self, request: HttpRequest, import_form): - initial = super().get_confirm_form_initial(request, import_form) + def get_confirm_form_initial(self, request: HttpRequest, import_form) -> Dict[str, Any]: + initial: Dict[str, Any] = super().get_confirm_form_initial(request, import_form) if import_form and hasattr(import_form.cleaned_data, "course"): course: Course = import_form.cleaned_data["course"] initial["course"] = course.id return initial # Add course to import form kwargs to be used by resource to associate course with all imported students - def get_import_data_kwargs(self, request: HttpRequest, *args, **kwargs): + def get_import_data_kwargs(self, request: HttpRequest, *args, **kwargs) -> Dict[str, Any]: form: ImportForm | None = kwargs.get("form", None) if form and hasattr(form, "cleaned_data"): kwargs["course"] = form.cleaned_data.get("course", None) diff --git a/cntmanage/flightslot/admins/training_adm.py b/cntmanage/flightslot/admins/training_adm.py index 5127211..3d7d464 100644 --- a/cntmanage/flightslot/admins/training_adm.py +++ b/cntmanage/flightslot/admins/training_adm.py @@ -26,15 +26,15 @@ class TrainingInLIne(nested_admin.NestedTabularInline): } # If user is a student deny edit permission for week past the current one - def has_change_permission(self, request: HttpRequest, obj: WeekPreference | None = None): + def has_change_permission(self, request: HttpRequest, obj: WeekPreference | None = None) -> bool: if hasattr(request.user, 'student') and obj: current_week: int = date.today().isocalendar().week if current_week > obj.week: return False return True - def has_delete_permission(self, request: HttpRequest, obj: WeekPreference | None = None): + def has_delete_permission(self, request: HttpRequest, obj: WeekPreference | None = None) -> bool: return self.has_change_permission(request=request, obj=obj) - def has_add_permission(self, request: HttpRequest, obj: WeekPreference | None = None): + def has_add_permission(self, request: HttpRequest, obj: WeekPreference | None = None) -> bool: return self.has_change_permission(request=request, obj=obj) diff --git a/cntmanage/flightslot/admins/weekpref_adm.py b/cntmanage/flightslot/admins/weekpref_adm.py index 47763d6..66c94b2 100644 --- a/cntmanage/flightslot/admins/weekpref_adm.py +++ b/cntmanage/flightslot/admins/weekpref_adm.py @@ -1,7 +1,8 @@ import nested_admin -from django import forms +from django.forms import Form from django.db.models.query import QuerySet +from django.contrib.auth.models import User from django.http import HttpRequest, HttpResponse from django.contrib import admin, messages from django.utils.translation import ngettext @@ -17,6 +18,7 @@ from ..custom.colortag import course_color from ..actions.exportweek import export_selected from datetime import date +from typing import Dict, List, Any class WeekPreferenceAdmin(nested_admin.NestedPolymorphicModelAdmin): inlines = (TrainingInLIne, HourBuildingInLine, ) @@ -45,83 +47,75 @@ class WeekPreferenceAdmin(nested_admin.NestedPolymorphicModelAdmin): return course_color(obj.student.course.color) # If a user is registered as student hide filters - def get_list_filter(self, request): + def get_list_filter(self, request: HttpRequest) -> List[str]: list_filter = super().get_list_filter(request) - if hasattr(request.user, 'student'): + if hasattr(request.user, "student"): return [] return list_filter # If a user is registered as student do not show actions - def get_actions(self, request): + def get_actions(self, request: HttpRequest) -> Dict[str, Any]: actions = super().get_actions(request) - if hasattr(request.user, 'student'): - return [] + if hasattr(request.user, "student"): + return {} return actions # If a user is registered as student show only their preferences - def get_queryset(self, request): + def get_queryset(self, request: HttpRequest) -> QuerySet[WeekPreference]: qs = super().get_queryset(request) - if hasattr(request.user, 'student'): + if hasattr(request.user, "student"): return qs.filter(student=request.user.student) # If admin show everything return qs - def get_form(self, request, obj=None, **kwargs): - form: forms.Form = super().get_form(request, obj, **kwargs) + def get_form(self, request: HttpRequest, obj: WeekPreference | None = None, **kwargs: Dict[str, Any]) -> Form: + form: Form = super().get_form(request, obj, **kwargs) current_week = date.today().isocalendar().week # If form contains the week field - if 'week' in form.base_fields: + if "week" in form.base_fields: # Set default value as current week - form.base_fields['week'].initial = current_week + form.base_fields["week"].initial = current_week # If student is current user making request - if hasattr(request.user, 'student'): + if hasattr(request.user, "student"): student = request.user.student - if 'student' in form.base_fields: - form.base_fields['student'].initial = student - form.base_fields['student'].disabled = True - form.base_fields['week'].disabled = True # student cannot change week + if "student" in form.base_fields: + form.base_fields["student"].initial = student + form.base_fields["student"].disabled = True + form.base_fields["week"].disabled = True # student cannot change week return form # If user is a student deny edit permission for week past the current one - def has_change_permission(self, request, obj: WeekPreference | None = None): - if hasattr(request.user, 'student') and obj: + def has_change_permission(self, request: HttpRequest, obj: WeekPreference | None = None) -> bool: + if hasattr(request.user, "student") and obj: current_week = date.today().isocalendar().week if current_week > obj.week: return False return True # If user is a student deny edit permission for week past the current one - def has_add_permission(self, request, obj: WeekPreference | None = None): - if hasattr(request.user, 'student') and obj: - current_week = date.today().isocalendar().week - if current_week > obj.week: - return False - return True + def has_add_permission(self, request: HttpRequest, obj: WeekPreference | None = None) -> bool: + return self.has_change_permission(request, obj) # If user is a student deny edit permission for week past the current one - def has_delete_permission(self, request, obj: WeekPreference | None = None): - if hasattr(request.user, 'student') and obj: - current_week = date.today().isocalendar().week - if current_week > obj.week: - return False - return True + def has_delete_permission(self, request: HttpRequest, obj: WeekPreference | None = None)-> bool: + return self.has_change_permission(request, obj) - def changeform_view(self, request: HttpRequest, object_id: int | None = None, form_url: str = '', extra_context=None): + def changeform_view(self, request: HttpRequest, object_id: int | None = None, form_url: str = "", extra_context=None): extra_context = extra_context or {} - if hasattr(request.user, 'student') and object_id: + if hasattr(request.user, "student") and object_id: current_week = date.today().isocalendar().week weekpref = WeekPreference.objects.get(id=object_id) if current_week > weekpref.week: - extra_context['show_save'] = False - extra_context['show_save_and_continue'] = False - extra_context['show_save_and_add_another'] = False - extra_context['show_delete'] = False + extra_context["show_save"] = False + extra_context["show_save_and_continue"] = False + extra_context["show_save_and_add_another"] = False + extra_context["show_delete"] = False return super().changeform_view(request, object_id, form_url, extra_context) - def save_model(self, request, obj, form, change): + def save_model(self, request: HttpRequest, obj, form: Form, change: bool): # Imposta automaticamente lo studente se non รจ giร  valorizzato - if hasattr(request.user, 'student') and not obj.student_id: + if hasattr(request.user, "student") and not obj.student_id: obj.student = request.user.student super().save_model(request, obj, form, change) diff --git a/cntmanage/flightslot/migrations/0022_aircraft_alter_course_color_and_more.py b/cntmanage/flightslot/migrations/0022_aircraft_alter_course_color_and_more.py new file mode 100644 index 0000000..b27983b --- /dev/null +++ b/cntmanage/flightslot/migrations/0022_aircraft_alter_course_color_and_more.py @@ -0,0 +1,39 @@ +# Generated by Django 5.2.8 on 2025-11-27 13:34 + +import colorfield.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('flightslot', '0021_alter_hourbuildinglegstop_refuel'), + ] + + operations = [ + migrations.CreateModel( + name='Aircraft', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('type', models.CharField(choices=[('C152', 'Cessna 152'), ('P208', 'Tecnam P2008'), ('PA28', 'Piper PA28R'), ('PA34', 'Piper PA34'), ('C182', 'Cessna 182Q'), ('TWEN', 'Tecnam P2010'), ('FSTD', 'Alsim ALX40')], max_length=4)), + ('markings', models.CharField(max_length=6)), + ('complex', models.BooleanField(default=False)), + ('avail_hours', models.DurationField(null=True, verbose_name='Time until maintenance')), + ], + ), + migrations.AlterField( + model_name='course', + name='color', + field=colorfield.fields.ColorField(default='#FFFFFF', image_field=None, max_length=25, samples=[('#bfbfbf', 'GREY'), ('#ff0000', 'RED'), ('#ffc000', 'ORANGE'), ('#ffff00', 'YELLOW'), ('#92d050', 'GREEN'), ('#00b0f0', 'CYAN'), ('#b1a0c7', 'MAGENTA'), ('#fabcfb', 'PINK'), ('#f27ae4', 'VIOLET')], verbose_name='Binder Color'), + ), + migrations.AlterField( + model_name='hourbuilding', + name='aircraft', + field=models.CharField(choices=[('C152', 'Cessna 152'), ('P208', 'Tecnam P2008'), ('PA28', 'Piper PA28R'), ('PA34', 'Piper PA34'), ('C182', 'Cessna 182Q'), ('TWEN', 'Tecnam P2010'), ('FSTD', 'Alsim ALX40')]), + ), + migrations.AddField( + model_name='student', + name='aircrafts', + field=models.ManyToManyField(to='flightslot.aircraft'), + ), + ] diff --git a/cntmanage/flightslot/migrations/0023_missionprofile_aircrafts.py b/cntmanage/flightslot/migrations/0023_missionprofile_aircrafts.py new file mode 100644 index 0000000..32d0789 --- /dev/null +++ b/cntmanage/flightslot/migrations/0023_missionprofile_aircrafts.py @@ -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'), + ), + ] diff --git a/cntmanage/flightslot/migrations/0024_alter_missionprofile_mtype.py b/cntmanage/flightslot/migrations/0024_alter_missionprofile_mtype.py new file mode 100644 index 0000000..3012159 --- /dev/null +++ b/cntmanage/flightslot/migrations/0024_alter_missionprofile_mtype.py @@ -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'), + ), + ] diff --git a/cntmanage/flightslot/models/aircrafts.py b/cntmanage/flightslot/models/aircrafts.py index 4afb9c2..53e3935 100644 --- a/cntmanage/flightslot/models/aircrafts.py +++ b/cntmanage/flightslot/models/aircrafts.py @@ -5,5 +5,45 @@ class AircraftTypes(models.TextChoices): C152 = "C152", _("Cessna 152") P208 = "P208", _("Tecnam P2008") PA28 = "PA28", _("Piper PA28R") + PA34 = "PA34", _("Piper PA34") C182 = "C182", _("Cessna 182Q") P210 = "TWEN", _("Tecnam P2010") + ALX40 = "FSTD", _("Alsim ALX40") + +class Aircraft(models.Model): + id = models.AutoField( + primary_key=True + ) + + type = models.CharField( + null=False, + blank=False, + max_length=4, # ICAO naming of aircraft, + choices=AircraftTypes + ) + + markings = models.CharField( + null=False, + blank=False, + max_length=6 + ) + + complex = models.BooleanField( + null=False, + default=False + ) + + avail_hours = models.DurationField( + null=True, + verbose_name=_("Time until maintenance") + ) + + def __str__(self) -> str: + return f"{self.type} ({self.markings})" + + # Insert dash between first and rest, I-OASM + def save(self, *args, **kwargs): + self.markings = self.markings.upper() + if not "-" in self.markings: + self.markings = self.markings[0] + "-" + self.markings[1:] + super().save(*args, **kwargs) diff --git a/cntmanage/flightslot/models/courses.py b/cntmanage/flightslot/models/courses.py index d88c047..b9ab541 100644 --- a/cntmanage/flightslot/models/courses.py +++ b/cntmanage/flightslot/models/courses.py @@ -10,14 +10,18 @@ class CourseTypes(models.TextChoices): DISTANCE = "DL", _("DISTANCE") OTHER = "OTHER",_("OTHER") - class Course(models.Model): # Add colors according to table from Alessia COLOR_PALETTE = [ - ("#ffffff","WHITE"), + ("#bfbfbf","GREY"), ("#ff0000", "RED"), - ("#00ff00", "GREEN"), - ("#0000ff", "BLUE") + ("#ffc000", "ORANGE"), + ("#ffff00", "YELLOW"), + ("#92d050", "GREEN"), + ("#00b0f0", "CYAN"), + ("#b1a0c7", "MAGENTA"), + ("#fabcfb", "PINK"), + ("#f27ae4", "VIOLET"), ] id = models.AutoField( @@ -43,8 +47,8 @@ class Course(models.Model): ) color = ColorField ( - samples=COLOR_PALETTE, - verbose_name=_("Binder Color") + verbose_name=_("Binder Color"), + samples=COLOR_PALETTE ) def __str__(self): diff --git a/cntmanage/flightslot/models/missions.py b/cntmanage/flightslot/models/missions.py index 2be822d..a6125fd 100644 --- a/cntmanage/flightslot/models/missions.py +++ b/cntmanage/flightslot/models/missions.py @@ -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, diff --git a/cntmanage/flightslot/models/students.py b/cntmanage/flightslot/models/students.py index b4014ce..bab8035 100644 --- a/cntmanage/flightslot/models/students.py +++ b/cntmanage/flightslot/models/students.py @@ -2,6 +2,7 @@ from django.db import models from django.contrib.auth.models import User, Group from ..models.courses import Course +from ..models.aircrafts import Aircraft class Student(models.Model): id = models.AutoField( @@ -46,6 +47,10 @@ class Student(models.Model): blank=True ) + aircrafts = models.ManyToManyField( + Aircraft + ) + def default_password(self) -> str: # Maximum 4 digits for passowrd return f"{self.name.lower()[0]}{self.surname.lower()}{self.id % 10000}" diff --git a/cntmanage/poetry.lock b/cntmanage/poetry.lock index b0c1f3c..7042e04 100644 --- a/cntmanage/poetry.lock +++ b/cntmanage/poetry.lock @@ -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]] diff --git a/cntmanage/pyproject.toml b/cntmanage/pyproject.toml index aa51730..dfeccc8 100644 --- a/cntmanage/pyproject.toml +++ b/cntmanage/pyproject.toml @@ -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 "]