14 Commits

22 changed files with 599 additions and 130 deletions

View File

@@ -11,6 +11,7 @@ import os
from django.core.asgi import get_asgi_application from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'cntmanage.settings') settings = os.environ.get("DJANGO_SETTINGS_MODULE", "cntmanage.settings")
os.environ.setdefault("DJANGO_SETTINGS_MODULE", settings)
application = get_asgi_application() application = get_asgi_application()

View File

@@ -28,7 +28,6 @@ DEBUG = True
ALLOWED_HOSTS = [] ALLOWED_HOSTS = []
# Application definition # Application definition
INSTALLED_APPS = [ INSTALLED_APPS = [
@@ -44,6 +43,7 @@ INSTALLED_APPS = [
'colorfield', 'colorfield',
'import_export', 'import_export',
'django_admin_action_forms', 'django_admin_action_forms',
'polymorphic'
] ]
# Import Export plugin settings # Import Export plugin settings
@@ -53,6 +53,7 @@ IMPORT_EXPORT_SKIP_ADMIN_LOG = True
IMPORT_FORMATS = [CSV] IMPORT_FORMATS = [CSV]
MIDDLEWARE = [ MIDDLEWARE = [
'flightslot.middleware.RedirectNonSuperuserFromAdminMiddleware', # custom middleware to show "user" page to non superuser
'django.middleware.security.SecurityMiddleware', 'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware', 'django.middleware.common.CommonMiddleware',
@@ -92,9 +93,9 @@ STATICFILES_DIRS = [
DATABASES = { DATABASES = {
'default': { 'default': {
'ENGINE': 'django.db.backends.postgresql', 'ENGINE': 'django.db.backends.postgresql',
'NAME': 'techstorage', 'NAME': 'flightslot_db',
'USER': 'tech', 'USER': 'flightslot',
'PASSWORD': 'tech', 'PASSWORD': 'flightslot',
'HOST': 'localhost', 'HOST': 'localhost',
'PORT': '5432' 'PORT': '5432'
} }
@@ -127,9 +128,9 @@ LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC' TIME_ZONE = 'UTC'
USE_I18N = True USE_I18N = False # Disable translation engine
USE_TZ = True USE_TZ = True
# Static files (CSS, JavaScript, Images) # Static files (CSS, JavaScript, Images)

View File

@@ -0,0 +1,144 @@
"""
Django settings for techdb project.
Generated by 'django-admin startproject' using Django 5.1.2.
For more information on this file, see
https://docs.djangoproject.com/en/5.1/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/5.1/ref/settings/
"""
from pathlib import Path
import os
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/5.1/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = os.environ.get('SECRET_KEY', 'django-insecure-s%(9^y#!1*ge)7u%$vf3zp0lisgd%=(k@$13&ej13p5(ei71hi')
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = False
# Allowed hosts list is necessary for when in production
ALLOWED_HOSTS = ['*']
# Using a secure-only session cookie makes it more difficult for network traffic sniffers to hijack user sessions.
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
CSRF_TRUSTED_ORIGINS = ["http://localhost:8000", "http://127.0.0.1:8000", "http://10.0.2.249:8000", "https://*.etss.it"]
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'nested_admin',
'flightslot',
'durationwidget',
'colorfield',
'import_export',
'django_admin_action_forms',
]
# Import Export plugin settings
from import_export.formats.base_formats import CSV
IMPORT_EXPORT_USE_TRANSACTIONS = True
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',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'cntmanage.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': ['/var/www/templates'],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'cntmanage.wsgi.application'
# Database
# https://docs.djangoproject.com/en/5.1/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': os.environ.get('DB_NAME','techstorage'),
'USER': os.environ.get('DB_USER','tech'),
'PASSWORD': os.environ.get('DB_PASSWORD','tech'),
'HOST': os.environ.get('DB_HOST','postgresql'),
'PORT': os.environ.get('DB_PORT','5432')
}
}
# Password validation
# https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/5.1/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = False # Disable translation engine
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/5.1/howto/static-files/
STATIC_URL = "static/"
STATIC_ROOT = "/var/www/static/"
STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"
# Default primary key field type
# https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'

View File

@@ -1,26 +1,11 @@
"""
URL configuration for techdb project.
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/5.1/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin from django.contrib import admin
from django.urls import path from django.urls import path
from django.shortcuts import redirect
from flightslot.admin import flightslot_user
urlpatterns = [ urlpatterns = [
#path('', RedirectView.as_view(url='/admin/', permanent=False)),
path('admin/', admin.site.urls), path('admin/', admin.site.urls),
path('user/', flightslot_user.urls),
path("", lambda r: redirect("/user/")), # la root porta gli utenti nella pagina giusta
] ]
admin.site.site_header = "CantorAir Flight Scheduler"
admin.site.site_title = "CantorAir Flight Scheduler"
admin.site.index_title = "Welcome to CantorAir Flight Scheduler Portal"

View File

@@ -11,6 +11,7 @@ import os
from django.core.wsgi import get_wsgi_application from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'cntmanage.settings') settings = os.environ.get("DJANGO_SETTINGS_MODULE", "cntmanage.settings")
os.environ.setdefault("DJANGO_SETTINGS_MODULE", settings)
application = get_wsgi_application() application = get_wsgi_application()

View File

@@ -1,8 +1,5 @@
# Use postgres/example user/password credentials # Use postgres/example user/password credentials
version: '3.9'
services: services:
postgresql: postgresql:
image: postgres:17.0 image: postgres:17.0
container_name: tech-postgresql container_name: tech-postgresql
@@ -10,12 +7,46 @@ services:
# set shared memory limit when using docker-compose # set shared memory limit when using docker-compose
shm_size: 128mb shm_size: 128mb
volumes: volumes:
- /mnt/data/postgresql:/var/lib/postgresql/data - postgresql_data:/var/lib/postgresql/data
ports: ports:
- 5432:5432 - 5432:5432
networks:
- cntnet
environment: environment:
POSTGRES_USER: tech POSTGRES_DB: flightslot_db
POSTGRES_PASSWORD: tech POSTGRES_USER: flightslot
POSTGRED_DB: techstorage POSTGRES_PASSWORD: flightslot
PGDATA: /var/lib/postgresql/data PGDATA: /var/lib/postgresql/data
flightslot:
build:
context: ..
dockerfile: ./docker/flightslot.Dockerfile
image: flightslot:latest
container_name: tech-flightslot
restart: unless-stopped
ports:
- 8000:8000
networks:
- cntnet
depends_on:
- postgresql
environment:
- DJANGO_SETTINGS_MODULE=cntmanage.settings_prod
- DJANGO_SUPERUSER_USERNAME=admin
- DJANGO_SUPERUSER_EMAIL=emanuele.trabattoni@gmail.com
- DJANGO_SUPERUSER_PASSWORD=CantorAir2k25
- SECRET_KEY=6WIjA!+mI+ZOWHaJm6v^8F4o,@-gliDtwkp*QFvpkFe"Oo0quq
- DB_NAME=flightslot_db
- DB_USER=flightslot
- DB_PASSWORD=flightslot
- DB_HOST=postgresql
- DB_PORT=5432
volumes:
postgresql_data:
name: postgress_data
networks:
cntnet:

27
cntmanage/docker/entrypoint.sh Executable file
View File

@@ -0,0 +1,27 @@
#!/bin/sh
set -e
echo "📦 Starting Django deploy script..."
echo "🔧 Running migrations..."
django-admin migrate --noinput
echo "📁 Collecting static files..."
django-admin collectstatic --noinput
echo "📁 Manually copying static files..."
cp -v /app/static/* /var/www/static/
echo "📁 Manually copying template files..."
cp -rv /app/templates/ /var/www/templates/
echo "👁️ Creating superuser..."
if django-admin createsuperuser --noinput --email ${DJANGO_SUPERUSER_EMAIL} --username ${DJANGO_SUPERUSER_USERNAME}; then
echo "👁️ Superuser ${DJANGO_SUPERUSER_USERNAME} already created ..."
else
echo "👁️ Superuser ${DJANGO_SUPERUSER_USERNAME} created successfully ..."
fi
echo "🚀 Launching Flightslot..."
exec "$@"

View File

@@ -0,0 +1,41 @@
### STAGE 1 - Builder image ###
# Builder container
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
COPY . ./
# Run poetry update to download dependencies
RUN poetry update --no-interaction --no-ansi
# Build project
RUN poetry build
### STAGE 2 — Final image
FROM python:3.12-slim AS deploy
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 ./
ENTRYPOINT ["/app/entrypoint.sh"]
# Command to be executed after entry point
CMD ["gunicorn", "cntmanage.wsgi:application", "--bind", "0.0.0.0:8000"]

View File

@@ -10,7 +10,7 @@ from typing import List
from ..models.weekpref import WeekPreference from ..models.weekpref import WeekPreference
from ..models.missions import Training from ..models.missions import Training
from ..models.hourbuildings import HourBuilding,HourBuildingLeg from ..models.hourbuildings import HourBuilding, HourBuildingLegFlight, HourBuildingLegStop, HourBuildingLegBase
def export_selected(request: HttpRequest, queryset: QuerySet[WeekPreference]) -> HttpResponse: def export_selected(request: HttpRequest, queryset: QuerySet[WeekPreference]) -> HttpResponse:
@@ -41,7 +41,7 @@ def export_selected(request: HttpRequest, queryset: QuerySet[WeekPreference]) ->
# Header titles # Header titles
days = [f"{datetime.strptime(f"{year} {week} {x}", "%G %V %u").strftime("%A")} {datetime.strptime(f"{year} {week} {x}", "%G %V %u").day}" for x in range(1,8)] days = [f"{datetime.strptime(f"{year} {week} {x}", "%G %V %u").strftime("%A")} {datetime.strptime(f"{year} {week} {x}", "%G %V %u").day}" for x in range(1,8)]
headers = ["Week", "Student", "Course", *days, "Cell.", "Mail", "Notes"] headers = ["Week", "Student", "Course", *days, "Notes", "Cell.", "Mail"]
# Header fields positions # Header fields positions
week_index: int = headers.index("Week") + 1 week_index: int = headers.index("Week") + 1
@@ -59,7 +59,12 @@ def export_selected(request: HttpRequest, queryset: QuerySet[WeekPreference]) ->
# Cell styles # Cell styles
border_thick: Side = Side(style='thick', color='000000') border_thick: Side = Side(style='thick', color='000000')
border_thin: Side = Side(style='thin', color='000000', border_style='dashed')
border_bottom: Border = Border(bottom=border_thick) border_bottom: Border = Border(bottom=border_thick)
border_bottom_thin: Border = Border(bottom=border_thin)
border_left: Border = Border(left=border_thick)
border_right: Border = Border(right=border_thick)
border_right_thin: Border = Border(right=border_thin)
border_all: Border = Border(bottom=border_thick, top=border_thick, left=border_thick, right=None) border_all: Border = Border(bottom=border_thick, top=border_thick, left=border_thick, right=None)
# Scrittura header # Scrittura header
@@ -102,15 +107,14 @@ def export_selected(request: HttpRequest, queryset: QuerySet[WeekPreference]) ->
mission_name if t.sunday else "" mission_name if t.sunday else ""
] ]
mission_notes = t.notes if t.notes else "--" mission_notes = t.notes if t.notes else "--"
mission_data.append([str(q.week), *student_data, *mission_days, student_phone, student_email, mission_notes]) mission_data.append([str(q.week), *student_data, *mission_days, mission_notes, student_phone, student_email, ])
# Fill HourBuilding rows # Fill HourBuilding rows
hb_name: str hb_name: str
hb_days: List[str] hb_days: List[str]
hb_notes: str
hb_data: List[List[str]] = [] hb_data: List[List[str]] = []
for h in HourBuilding.objects.filter(weekpref = q.id): for h in HourBuilding.objects.filter(weekpref = q.id):
hb_name = f"HB-{h.aircraft}\nVedi Note ->" hb_name = f"HB - {h.aircraft}\nVedi Note ->"
hb_days = [ hb_days = [
hb_name if h.monday else "", hb_name if h.monday else "",
hb_name if h.tuesday else "", hb_name if h.tuesday else "",
@@ -120,53 +124,71 @@ def export_selected(request: HttpRequest, queryset: QuerySet[WeekPreference]) ->
hb_name if h.saturday else "", hb_name if h.saturday else "",
hb_name if h.sunday else "" hb_name if h.sunday else ""
] ]
hb_notes = f"{h.notes}\n----\n" if h.notes else "" hb_notes: List[str] = [f"{h.notes}", "---"] if h.notes else []
hb_legs = HourBuildingLeg.objects.filter(hb_id = h.id) hb_legs_all = HourBuildingLegBase.objects.filter(hb_id = h.id)
for hh in hb_legs: for hh in hb_legs_all:
hb_notes += f"{hh.departure} -> {hh.destination} [{hh.time}]\n" if not hh.stop else f"STOP at {hh.departure} [{hh.time}]\n" time_str: str = ':'.join(str(hh.time).split(':')[:2]) # keep only hours and minutes
hb_notes.strip('\n') if isinstance(hh, HourBuildingLegFlight):
hb_data.append([str(q.week), *student_data, *hb_days, str(q.student.phone), q.student.email, hb_notes]) hb_notes.append(f"{hh.departure} -> {hh.destination} [{time_str}]{f' / PAX: {hh.pax.capitalize()}' if hh.pax else ''}")
elif isinstance(hh, HourBuildingLegStop):
hb_notes.append(f"STOP [{time_str}] {"Refuel" if hh.refuel else ""}" )
hb_data.append([str(q.week), *student_data, *hb_days, "\n".join(hb_notes), str(q.student.phone), q.student.email])
# Build rows for table # Build rows for table
all_data: List[List[str]] = mission_data + hb_data all_data: List[List[str]] = mission_data + hb_data
student_start: int = row + row_offset student_start: int = row + row_offset
for r in all_data: for row_content in all_data:
for j, c in enumerate(r, start=1): for c, cell_content in enumerate(row_content, start=1):
cell = ws.cell(row = row + row_offset, column = j, value = c) cell = ws.cell(row = row + row_offset, column = c, value = cell_content)
cell.alignment = center cell.alignment = center
# Format Student Name # Format Student Name
if j == student_index: if c == student_index:
cell.font = bold_black cell.font = bold_black
# Format Course Column # Format Course Column with color
if j == course_index and q.student.course: elif c == course_index and q.student.course:
cell.font = bold_black cell.font = bold_black
cell.fill = PatternFill("solid", fgColor=str(q.student.course.color).lstrip('#').lower()) cell.fill = PatternFill("solid", fgColor=str(q.student.course.color).lstrip('#').lower())
# Add internal borders between mix cells and notes
prev_cell_val: str = r[0] elif c > course_index and c <= note_index:
cell.border = border_bottom_thin + border_right_thin
# Fill mix cells if the cell is not empty
if c > course_index and c < note_index:
if len(cell_content):
cell.fill = PatternFill('solid', fgColor="f0f0f0")
prev_cell_val: str = row_content[0]
merge_start: bool = False merge_start: bool = False
merge_col_start: int = 1 merge_col_start: int = 1
for c, v in enumerate(r, start=1): for c, cell_content in enumerate(row_content, start=1):
# Merge cells in the row # Merge cells in the row
if v == prev_cell_val and not merge_start: if cell_content == prev_cell_val and not merge_start:
merge_start = True merge_start = True
merge_col_start = c-1 # start merge from previous column merge_col_start = c-1 # start merge from previous column
elif v != prev_cell_val and merge_start: elif cell_content != prev_cell_val and merge_start:
merge_start = False merge_start = False
ws.merge_cells(start_row=row+row_offset, ws.merge_cells(start_row=row+row_offset,
end_row=row+row_offset, end_row=row+row_offset,
start_column=max(merge_col_start,1), start_column=max(merge_col_start,1),
end_column=max(c-1,1)) # end merge to previous column end_column=max(c-1,1)) # end merge to previous column
prev_cell_val = v prev_cell_val = cell_content
# Incement row counter # Incement row counter
row_offset += 1 row_offset += 1
# End week preferences for this student # End week preferences for this student
student_end: int = row + row_offset -1 student_end: int = row + row_offset - 1
# Add thick border to the last cell row of this student
for c in range(course_index, mail_index + 1):
ws.cell(row=student_end, column=c).border = Border(bottom=border_thick, right=border_thin)
# And for last column also a vertical border all student high
if c == mail_index:
for row_content in range(student_start, student_end + 1):
ws.cell(row=row_content, column=c).border += border_right
# Merge Week, thick border # Merge Week, thick border
# ws.cell(row=student_start, column=week_index).border = border_all ws.cell(row=student_start, column=week_index).border = border_all
# ws.merge_cells(start_row=student_start, end_row=student_end, start_column=week_index, end_column=week_index) ws.merge_cells(start_row=student_start, end_row=student_end, start_column=week_index, end_column=week_index)
# Merge Name, thick border # Merge Name, thick border
ws.cell(row=student_start, column=student_index).border = border_all ws.cell(row=student_start, column=student_index).border = border_all
ws.merge_cells(start_row=student_start, end_row=student_end, start_column=student_index, end_column=student_index) ws.merge_cells(start_row=student_start, end_row=student_end, start_column=student_index, end_column=student_index)
@@ -177,13 +199,15 @@ def export_selected(request: HttpRequest, queryset: QuerySet[WeekPreference]) ->
# Merge Mail # Merge Mail
ws.merge_cells(start_row=student_start, end_row=student_end, start_column=mail_index, end_column=mail_index) ws.merge_cells(start_row=student_start, end_row=student_end, start_column=mail_index, end_column=mail_index)
# Add thick border to the last cell row of this student
for i in range(course_index, len(all_data[0])+1):
ws.cell(row=student_end, column=i).border = border_bottom
# Keep the largest column # Keep the largest column
max_len: List[int] = []
for column_cells in ws.columns: for column_cells in ws.columns:
length: int = max(len(str(cell.value)) for cell in column_cells) for cell in column_cells:
cell_lines = str(cell.value).splitlines()
if len(cell_lines) == 0:
continue
max_len.append(max([len(ll) for ll in cell_lines]))
length: int = max(max_len)
col_letter: str = "A" col_letter: str = "A"
if column_cells[0].column: if column_cells[0].column:
col_letter = get_column_letter(column_cells[0].column) col_letter = get_column_letter(column_cells[0].column)
@@ -191,8 +215,7 @@ def export_selected(request: HttpRequest, queryset: QuerySet[WeekPreference]) ->
### End of Student Loop ### ### End of Student Loop ###
# Save document in HttpResponse # Save document in HttpResponse
wb.save(response) wb.save(response)
return response return response

View File

@@ -1,4 +1,5 @@
from django.contrib import admin from django.contrib import admin
from django.http import HttpRequest
from .models.courses import Course from .models.courses import Course
from .models.students import Student from .models.students import Student
@@ -9,6 +10,31 @@ from .admins.course_adm import CourseAdmin
from .admins.student_adm import StudentAdmin from .admins.student_adm import StudentAdmin
from .admins.mission_adm import MissionProfileAdmin from .admins.mission_adm import MissionProfileAdmin
from .admins.weekpred_adm import WeekPreferenceAdmin from .admins.weekpred_adm import WeekPreferenceAdmin
#from .admins.hourbuilding_adm import HourBuilding, HourBuildingInLine
from django.contrib.admin import AdminSite
class FlightSlotUserSite(AdminSite):
site_header = "Flight Scheduler 🛫"
site_title = "Flight Scheduler 🛫"
index_title = "Welcome to CantorAir Flight Scheduler 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
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 🛫"
admin.site.index_title = "Welcome to CantorAir Flight Scheduler Administrator Portal"
admin.site.register(Course, CourseAdmin) admin.site.register(Course, CourseAdmin)
admin.site.register(MissionProfile, MissionProfileAdmin) admin.site.register(MissionProfile, MissionProfileAdmin)

View File

@@ -7,36 +7,63 @@ from django.http import HttpRequest
from durationwidget.widgets import TimeDurationWidget from durationwidget.widgets import TimeDurationWidget
from ..models.hourbuildings import HourBuilding, HourBuildingLeg from ..models.hourbuildings import HourBuilding, HourBuildingLegBase, HourBuildingLegFlight, HourBuildingLegStop
from ..models.weekpref import WeekPreference from ..models.weekpref import WeekPreference
from datetime import date from datetime import date
class HourBuildingLegForm(forms.ModelForm): class HourBuildingLegFlightForm(forms.ModelForm):
class Meta: class Meta:
model = HourBuildingLeg model = HourBuildingLegFlight
fields = '__all__' fields = "__all__"
widgets = { widgets = {
'time': TimeDurationWidget(show_days=False, "time": TimeDurationWidget(show_days=False,
show_seconds=False show_seconds=False,
attrs={
"style": (
"margin-right:5px; margin-left:5px; width:40px; min:0; max:5"
) )
})
}
class HourBuildingLegStopForm(forms.ModelForm):
class Meta:
model = HourBuildingLegStop
fields = "__all__"
widgets = {
"time": TimeDurationWidget(show_days=False,
show_seconds=False,
attrs={
"style": (
"margin-right:5px; margin-left:5px; width:40px;"
)
})
} }
# Register your models here. # Register your models here.
class HourBuildingLegInline(nested_admin.NestedTabularInline): class HourBuildingLegBaseInLine(nested_admin.NestedStackedPolymorphicInline):
model = HourBuildingLeg model = HourBuildingLegBase
form = HourBuildingLegForm fk_name = "hb"
extra = 0 verbose_name_plural = "Hour Building Legs"
fk_name = 'hb'
max_num = 5 class HourBuildingLegFlightInLine(nested_admin.NestedStackedPolymorphicInline.Child):
formfield_overrides = { model = HourBuildingLegFlight
models.CharField: {'widget': TextInput(attrs={'size':'20'})}, form = HourBuildingLegFlightForm
models.TextField: {'widget': Textarea(attrs={'rows':4, 'cols':35})}, fk_name = "hourbuildinglegbase_ptr"
} fields = ("departure", "time", "destination", "pax", )
hide_title = True
class HourBuildingLegStopInLine(nested_admin.NestedStackedPolymorphicInline.Child):
model = HourBuildingLegStop
form = HourBuildingLegFlightForm
fk_name = "hourbuildinglegbase_ptr"
fields = ("time", "refuel", )
child_inlines = (HourBuildingLegFlightInLine, HourBuildingLegStopInLine, )
# If user is a student deny edit permission for week past the current one # 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): def has_change_permission(self, request: HttpRequest, obj: HourBuilding | None = None):
if hasattr(request.user, 'student') and obj: if hasattr(request.user, "student") and obj:
current_week = date.today().isocalendar().week current_week = date.today().isocalendar().week
if not obj.DoesNotExist and current_week > obj.weekpref.week: if not obj.DoesNotExist and current_week > obj.weekpref.week:
return False return False
@@ -45,26 +72,16 @@ class HourBuildingLegInline(nested_admin.NestedTabularInline):
def has_delete_permission(self, request: HttpRequest, obj: HourBuilding | None = None): def has_delete_permission(self, request: HttpRequest, obj: HourBuilding | None = None):
return self.has_change_permission(request=request, obj=obj) return self.has_change_permission(request=request, obj=obj)
class HourBuildingInLine(nested_admin.NestedTabularInline): class HourBuildingInLine(nested_admin.NestedTabularInline):
model = HourBuilding model = HourBuilding
inlines = (HourBuildingLegBaseInLine,)
extra = 0 extra = 0
inlines = [HourBuildingLegInline]
fk_name = 'weekpref'
verbose_name_plural = "Hour Building"
max_num = 7 max_num = 7
fk_name = "weekpref"
verbose_name_plural = "Hour Buildings"
formfield_overrides = { formfield_overrides = {
models.CharField: {'widget': TextInput(attrs={'size':'20'})}, models.CharField: {"widget": TextInput(attrs={"size":"20"})},
models.TextField: {'widget': Textarea(attrs={'rows':4, 'cols':35})}, 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):
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):
return self.has_change_permission(request=request, obj=obj)

View File

@@ -58,7 +58,7 @@ class StudentAdmin(ImportMixin, AdminActionFormsMixin, admin.ModelAdmin):
def password(self, obj: Student) -> SafeText: def password(self, obj: Student) -> SafeText:
return SafeText(obj.default_password()) return SafeText(obj.default_password())
@admin.action(description="Disable Students") @admin.action(description="Deactivate Students")
def disable_students(self, request: HttpRequest, queryset: QuerySet[Student]): def disable_students(self, request: HttpRequest, queryset: QuerySet[Student]):
for q in queryset.all(): for q in queryset.all():
if q.user: if q.user:

View File

@@ -18,8 +18,8 @@ from ..actions.exportweek import export_selected
from datetime import date from datetime import date
class WeekPreferenceAdmin(nested_admin.NestedModelAdmin): class WeekPreferenceAdmin(nested_admin.NestedPolymorphicModelAdmin):
inlines = (TrainingInLIne, HourBuildingInLine,) inlines = (TrainingInLIne, HourBuildingInLine, )
list_display = ("week", "student__surname","student__name", "student__course", "course_color", "student_brief_mix",) 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", "student",)
actions = ("export",) actions = ("export",)

View File

@@ -0,0 +1,18 @@
from django.shortcuts import redirect
from django.urls import reverse
from django.http import HttpRequest
class RedirectNonSuperuserFromAdminMiddleware:
def __init__(self, get_response):
self.get_response = get_response
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')
):
return redirect("/user/") # redirect automatico
return self.get_response(request)

View File

@@ -0,0 +1,58 @@
# Generated by Django 5.2.8 on 2025-11-21 11:20
import datetime
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('contenttypes', '0002_remove_content_type_name'),
('flightslot', '0017_alter_missionprofile_mtype_alter_weekpreference_week'),
]
operations = [
migrations.CreateModel(
name='HourBuildingLegBase',
fields=[
('id', models.BigAutoField(primary_key=True, serialize=False)),
('time', models.DurationField(default=datetime.timedelta(seconds=3600))),
('hb', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='flightslot.hourbuilding')),
('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')),
],
options={
'abstract': False,
'base_manager_name': 'objects',
},
),
migrations.CreateModel(
name='HourBuildingLegFlight',
fields=[
('hourbuildinglegbase_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='flightslot.hourbuildinglegbase')),
('departure', models.CharField(default='LILV', max_length=4)),
('destination', models.CharField(default='LILV', max_length=4)),
],
options={
'abstract': False,
'base_manager_name': 'objects',
},
bases=('flightslot.hourbuildinglegbase',),
),
migrations.CreateModel(
name='HourBuildingLegStop',
fields=[
('hourbuildinglegbase_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='flightslot.hourbuildinglegbase')),
('location', models.CharField(default='LILV', max_length=4)),
('refuel', models.BooleanField(default=False)),
],
options={
'abstract': False,
'base_manager_name': 'objects',
},
bases=('flightslot.hourbuildinglegbase',),
),
migrations.DeleteModel(
name='HourBuildingLeg',
),
]

View File

@@ -0,0 +1,37 @@
# Generated by Django 5.2.8 on 2025-11-21 16:27
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('flightslot', '0018_hourbuildinglegbase_hourbuildinglegflight_and_more'),
]
operations = [
migrations.RemoveField(
model_name='hourbuildinglegstop',
name='location',
),
migrations.AddField(
model_name='hourbuildinglegflight',
name='pax',
field=models.CharField(max_length=16, null=True),
),
migrations.AlterField(
model_name='hourbuildinglegbase',
name='time',
field=models.DurationField(),
),
migrations.AlterField(
model_name='hourbuildinglegflight',
name='departure',
field=models.CharField(max_length=4),
),
migrations.AlterField(
model_name='hourbuildinglegflight',
name='destination',
field=models.CharField(max_length=4),
),
]

View File

@@ -1,11 +1,11 @@
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.db import models from django.db import models
from datetime import timedelta
from polymorphic.models import PolymorphicModel
from ..models.weekpref import WeekPreference from ..models.weekpref import WeekPreference
from ..models.aircrafts import AircraftTypes from ..models.aircrafts import AircraftTypes
class HourBuilding(models.Model): class HourBuilding(models.Model):
id = models.BigAutoField( id = models.BigAutoField(
primary_key=True primary_key=True
@@ -63,7 +63,10 @@ class HourBuilding(models.Model):
blank=True blank=True
) )
class HourBuildingLeg(models.Model): def __str__(self):
return f"Hour Building: {self.aircraft}"
class HourBuildingLegBase(PolymorphicModel):
id = models.BigAutoField( id = models.BigAutoField(
primary_key=True primary_key=True
) )
@@ -73,31 +76,63 @@ class HourBuildingLeg(models.Model):
on_delete=models.CASCADE on_delete=models.CASCADE
) )
time = models.DurationField(
null=False,
blank=False
)
# Change displayed name in the inline form
class Meta(PolymorphicModel.Meta):
verbose_name = "Flight Leg or Stop"
verbose_name_plural = "Flight Legs or Stops"
def __str__(self):
return f"Hour Building Leg"
class HourBuildingLegFlight(HourBuildingLegBase):
departure = models.CharField( departure = models.CharField(
null=False, null=False,
blank=False, blank=False,
default="LILV",
max_length=4 max_length=4
) )
destination = models.CharField( destination = models.CharField(
null=False, null=False,
blank=False, blank=False,
default="LILV",
max_length=4 max_length=4
) )
time = models.DurationField( pax = models.CharField(
null=False, null=True,
default = timedelta(hours=1) blank=True,
max_length=16,
verbose_name="Pax (optional)"
) )
stop = models.BooleanField( # Change displayed name in the inline form
class Meta(HourBuildingLegBase.Meta):
verbose_name = "Flight leg"
verbose_name_plural = "Flight legs"
def save(self, *args, **kwargs):
self.departure = self.departure.upper().strip()
self.destination = self.destination.upper().strip()
super().save(*args, **kwargs)
def __str__(self):
return f"{self.departure} -> {self.destination}"
class HourBuildingLegStop(HourBuildingLegBase):
refuel = models.BooleanField(
default=False default=False
) )
# Change displayed name in the inline form
class Meta(HourBuildingLegBase.Meta):
verbose_name = "Stop"
verbose_name_plural = "Stops"
def __str__(self): def __str__(self):
if self.stop: return f"Refuel" if self.refuel else f"No Refuel"
return "Refuelling Stop"
else:
return f"Flight Leg: {self.departure} -> {self.destination}"

View File

@@ -51,24 +51,24 @@ class Student(models.Model):
# Override save method to add user for login upon Student creation # Override save method to add user for login upon Student creation
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
creating = self.pk is None creating: bool = self.pk is None
super().save(*args, **kwargs) super().save(*args, **kwargs)
if creating and not self.user: if creating and not self.user:
username = f"{self.name.lower()}.{self.surname.lower()}" username: str = f"{self.name.lower()}.{self.surname.lower()}"
# Avoid username conflict with progressive number # Avoid username conflict with progressive number
base_username = username base_username = username
counter = 1 counter: int = 1
while User.objects.filter(username=username).exists(): while User.objects.filter(username=username).exists():
username = f"{base_username}{counter}" username = f"{base_username}{counter}"
counter += 1 counter += 1
# Create user # Create user
user = User.objects.create_user( user: User = User.objects.create_user(
first_name=self.name, first_name=self.name,
last_name=self.surname, last_name=self.surname,
username=username, username=username,
email=self.email, email=self.email,
password=self.default_password(), password=self.default_password(),
is_staff=True is_staff=True # allows access to admin page
) )
student_group, _ = Group.objects.get_or_create(name="StudentGroup") student_group, _ = Group.objects.get_or_create(name="StudentGroup")

23
cntmanage/poetry.lock generated
View File

@@ -2,14 +2,14 @@
[[package]] [[package]]
name = "asgiref" name = "asgiref"
version = "3.10.0" version = "3.11.0"
description = "ASGI specs, helper code, and adapters" description = "ASGI specs, helper code, and adapters"
optional = false optional = false
python-versions = ">=3.9" python-versions = ">=3.9"
groups = ["main"] groups = ["main"]
files = [ files = [
{file = "asgiref-3.10.0-py3-none-any.whl", hash = "sha256:aef8a81283a34d0ab31630c9b7dfe70c812c95eba78171367ca8745e88124734"}, {file = "asgiref-3.11.0-py3-none-any.whl", hash = "sha256:1db9021efadb0d9512ce8ffaf72fcef601c7b73a8807a1bb2ef143dc6b14846d"},
{file = "asgiref-3.10.0.tar.gz", hash = "sha256:d89f2d8cd8b56dada7d52fa7dc8075baa08fb836560710d38c292a7a3f78c04e"}, {file = "asgiref-3.11.0.tar.gz", hash = "sha256:13acff32519542a1736223fb79a715acdebe24286d98e8b164a73085f40da2c4"},
] ]
[package.extras] [package.extras]
@@ -143,6 +143,21 @@ python-monkey-business = ">=1.0.0"
dev = ["Pillow", "black", "dj-database-url", "django-selenosis", "flake8", "pytest", "pytest-cov", "pytest-django", "pytest-xdist", "selenium"] 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"] test = ["Pillow", "dj-database-url", "django-selenosis", "pytest", "pytest-cov", "pytest-django", "pytest-xdist", "selenium"]
[[package]]
name = "django-polymorphic"
version = "4.1.0"
description = "Seamless polymorphic inheritance for Django models"
optional = false
python-versions = "*"
groups = ["main"]
files = [
{file = "django_polymorphic-4.1.0-py3-none-any.whl", hash = "sha256:0ce3984999e103a0d1a434a5c5617f2c7f990dc3d5fb3585ce0fadadf9ff90ea"},
{file = "django_polymorphic-4.1.0.tar.gz", hash = "sha256:4438d95a0aef6c4307cd6c83ead387e1142ce80b65188a931ec2f0dbdd9bfc51"},
]
[package.dependencies]
Django = ">=3.2"
[[package]] [[package]]
name = "et-xmlfile" name = "et-xmlfile"
version = "2.0.0" version = "2.0.0"
@@ -421,4 +436,4 @@ files = [
[metadata] [metadata]
lock-version = "2.1" lock-version = "2.1"
python-versions = "^3.12" python-versions = "^3.12"
content-hash = "6bf43236f441d8b6bf8d1928910d169d3b29cfa499bb7d09d97ea227f8115658" content-hash = "e932d0af75c888d83fecefaaad1d018c508881a3bfde2ea640a82790e3567855"

View File

@@ -1,11 +1,12 @@
[tool.poetry] [tool.poetry]
name = "cntmanage" name = "cntmanage"
version = "0.1.0" version = "0.1.0"
packages = [{include = "flightslot"}] packages = [{include = "flightslot"}, {include = "cntmanage"}]
description = "CantorAir Flight Scheduler" description = "CantorAir Flight Scheduler"
authors = ["Emanuele <ema.trabattoni@gmail.com>"] authors = ["Emanuele <ema.trabattoni@gmail.com>"]
readme = "README.md" readme = "README.md"
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = "^3.12" python = "^3.12"
django = "^5.1.2" django = "^5.1.2"
@@ -16,6 +17,7 @@ django-import-export = "^4.3.13"
django-colorfield = "^0.14.0" django-colorfield = "^0.14.0"
openpyxl = "^3.1.5" openpyxl = "^3.1.5"
django-admin-action-forms = "^2.2.1" django-admin-action-forms = "^2.2.1"
django-polymorphic = "^4.1.0"
[build-system] [build-system]

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

View File

@@ -7,12 +7,19 @@
{% block branding %} {% block branding %}
<h1 id="site-name"> <h1 id="site-name">
<a href="{% url 'admin:index' %}"> <a href="{% url 'admin:index' %}" style="color: #0b1728;">
<img src="{% static 'cantorair.jpg' %}" height="60px" /> <img src="{% static 'cantorair_blue.jpg' %}"
height="60px"
style="margin-right: 20px;"/>
</a> </a>
</h1> </h1>
<div id="site-name"><a href="{% url 'admin:index' %}">{{ site_header|default:_('Django administration') }}</a></div> <div id="site-name" style="align-self: center; text-align: center;">
<a href="{% url 'admin:index' %}" style="color: #0b1728;">
{{ site_header | default:_('Django administration') }}
</a>
</div>
{% if user.is_anonymous %} {% if user.is_anonymous %}
{% include "admin/color_theme_toggle.html" %} {% include "admin/color_theme_toggle.html" %}
{% endif %} {% endif %}