Compare commits
32 Commits
d5befdd018
...
airplane-c
| Author | SHA1 | Date | |
|---|---|---|---|
| 5291956a31 | |||
| 7f908157bf | |||
| 35f773047d | |||
| 65444e786b | |||
| 7a392df8ad | |||
| de39913275 | |||
| 33c610dcbc | |||
| 1c0b287666 | |||
| cc833c475f | |||
| 09584e22fd | |||
| 97b14ae1d7 | |||
| 18d2604121 | |||
| b32d0fd032 | |||
| bf9f43eed8 | |||
| 18953e06b7 | |||
| b79f0c318a | |||
| c91f603a50 | |||
| f7030e8da1 | |||
| 95370ed0dc | |||
| bb634d28ed | |||
| cbdf49adfd | |||
| 7ad09e21b7 | |||
| 48ff1d799c | |||
| 02990d4b2f | |||
| bcdc885d66 | |||
| 674105600e | |||
| aa24976abb | |||
| 37b1ed4aa1 | |||
| 5f5c03e479 | |||
| 78f53cae7d | |||
| 72891440af | |||
| ae86a2e5fa |
@@ -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()
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -60,6 +60,7 @@ MIDDLEWARE = [
|
|||||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||||
'django.contrib.messages.middleware.MessageMiddleware',
|
'django.contrib.messages.middleware.MessageMiddleware',
|
||||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||||
|
'flightslot.middleware.RedirectNonSuperuserFromAdminMiddleware', # custom middleware to show "user" page to non superuser
|
||||||
]
|
]
|
||||||
|
|
||||||
ROOT_URLCONF = 'cntmanage.urls'
|
ROOT_URLCONF = 'cntmanage.urls'
|
||||||
@@ -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,7 +128,7 @@ 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
|
||||||
|
|
||||||
|
|||||||
145
cntmanage/cntmanage/settings_prod.py
Normal file
145
cntmanage/cntmanage/settings_prod.py
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
"""
|
||||||
|
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',
|
||||||
|
'polymorphic'
|
||||||
|
]
|
||||||
|
|
||||||
|
# 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 = [
|
||||||
|
'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',
|
||||||
|
'flightslot.middleware.RedirectNonSuperuserFromAdminMiddleware', # custom middleware to show "user" page to non superuser
|
||||||
|
]
|
||||||
|
|
||||||
|
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'
|
||||||
@@ -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"
|
|
||||||
@@ -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()
|
||||||
|
|||||||
5
cntmanage/docker/build.sh
Executable file
5
cntmanage/docker/build.sh
Executable 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}
|
||||||
@@ -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,48 @@ 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
|
||||||
|
args:
|
||||||
|
GIT_HASH:
|
||||||
|
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
27
cntmanage/docker/entrypoint.sh
Executable 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 version ${VERSION} ..."
|
||||||
|
exec "$@"
|
||||||
38
cntmanage/docker/flightslot.Dockerfile
Normal file
38
cntmanage/docker/flightslot.Dockerfile
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
### 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"
|
||||||
|
# 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
|
||||||
|
# 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"]
|
||||||
@@ -5,14 +5,13 @@ from openpyxl import Workbook
|
|||||||
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
|
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
|
||||||
from openpyxl.utils import get_column_letter
|
from openpyxl.utils import get_column_letter
|
||||||
|
|
||||||
|
from ..models.missions import Training
|
||||||
|
from ..models.weekpref import WeekPreference
|
||||||
|
from ..models.hourbuildings import HourBuilding, HourBuildingLegFlight, HourBuildingLegStop, HourBuildingLegBase
|
||||||
|
|
||||||
from datetime import date, datetime
|
from datetime import date, datetime
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
from ..models.weekpref import WeekPreference
|
|
||||||
from ..models.missions import Training
|
|
||||||
from ..models.hourbuildings import HourBuilding,HourBuildingLeg
|
|
||||||
|
|
||||||
|
|
||||||
def export_selected(request: HttpRequest, queryset: QuerySet[WeekPreference]) -> HttpResponse:
|
def export_selected(request: HttpRequest, queryset: QuerySet[WeekPreference]) -> HttpResponse:
|
||||||
|
|
||||||
if not queryset.first():
|
if not queryset.first():
|
||||||
@@ -41,7 +40,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 +58,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
|
||||||
@@ -74,12 +78,21 @@ def export_selected(request: HttpRequest, queryset: QuerySet[WeekPreference]) ->
|
|||||||
# Each of this iterations fills the table for a student
|
# Each of this iterations fills the table for a student
|
||||||
row: int = 2
|
row: int = 2
|
||||||
row_offset: int = 0
|
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_data: List[str]
|
||||||
student_phone: str = q.student.phone if q.student.phone else ""
|
student_phone: str = q.student.phone if q.student.phone else ""
|
||||||
student_email: str = q.student.email
|
student_email: str = q.student.email
|
||||||
|
student_course_type: str
|
||||||
|
student_course_number: str
|
||||||
|
student_course_ac: str
|
||||||
if q.student.course:
|
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:
|
else:
|
||||||
student_data = [f"{q.student.surname} {q.student.name}", f"No Course Assigned"]
|
student_data = [f"{q.student.surname} {q.student.name}", f"No Course Assigned"]
|
||||||
|
|
||||||
@@ -102,12 +115,11 @@ 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 ->"
|
||||||
@@ -120,43 +132,53 @@ 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
|
||||||
|
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 = r[0]
|
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
|
||||||
@@ -164,9 +186,17 @@ def export_selected(request: HttpRequest, queryset: QuerySet[WeekPreference]) ->
|
|||||||
# 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 +207,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,7 +223,6 @@ 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)
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,49 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
from django.http import HttpRequest
|
||||||
|
|
||||||
|
from .models.aircrafts import Aircraft
|
||||||
from .models.courses import Course
|
from .models.courses import Course
|
||||||
from .models.students import Student
|
from .models.students import Student
|
||||||
from .models.missions import MissionProfile
|
from .models.missions import MissionProfile
|
||||||
from .models.weekpref import WeekPreference
|
from .models.weekpref import WeekPreference
|
||||||
|
|
||||||
|
from .admins.aircraft_adm import AircraftAdmin
|
||||||
from .admins.course_adm import CourseAdmin
|
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.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 🛫"
|
||||||
|
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
|
||||||
|
|
||||||
|
# Register only user visible models
|
||||||
|
flightslot_user = FlightSlotUserSite(name="user_site")
|
||||||
|
flightslot_user.register(WeekPreference, WeekPreferenceAdmin)
|
||||||
|
|
||||||
|
|
||||||
|
# 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(Course, CourseAdmin)
|
||||||
admin.site.register(MissionProfile, MissionProfileAdmin)
|
admin.site.register(MissionProfile, MissionProfileAdmin)
|
||||||
admin.site.register(Student, StudentAdmin)
|
admin.site.register(Student, StudentAdmin)
|
||||||
|
|||||||
17
cntmanage/flightslot/admins/aircraft_adm.py
Normal file
17
cntmanage/flightslot/admins/aircraft_adm.py
Normal 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
|
||||||
|
|
||||||
@@ -7,64 +7,81 @@ 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
|
|
||||||
formfield_overrides = {
|
|
||||||
models.CharField: {'widget': TextInput(attrs={'size':'20'})},
|
|
||||||
models.TextField: {'widget': Textarea(attrs={'rows':4, 'cols':35})},
|
|
||||||
}
|
|
||||||
|
|
||||||
# If user is a student deny edit permission for week past the current one
|
class HourBuildingLegFlightInLine(nested_admin.NestedStackedPolymorphicInline.Child):
|
||||||
def has_change_permission(self, request: HttpRequest, obj: HourBuilding | None = None):
|
model = HourBuildingLegFlight
|
||||||
if hasattr(request.user, 'student') and obj:
|
form = HourBuildingLegFlightForm
|
||||||
current_week = date.today().isocalendar().week
|
fk_name = "hourbuildinglegbase_ptr"
|
||||||
if not obj.DoesNotExist and current_week > obj.weekpref.week:
|
fields = ("departure", "time", "destination", "pax", )
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
def has_delete_permission(self, request: HttpRequest, obj: HourBuilding | None = None):
|
class HourBuildingLegStopInLine(nested_admin.NestedStackedPolymorphicInline.Child):
|
||||||
return self.has_change_permission(request=request, obj=obj)
|
model = HourBuildingLegStop
|
||||||
|
form = HourBuildingLegFlightForm
|
||||||
|
fk_name = "hourbuildinglegbase_ptr"
|
||||||
|
fields = ("time", "refuel", )
|
||||||
|
|
||||||
|
child_inlines = (HourBuildingLegFlightInLine, HourBuildingLegStopInLine, )
|
||||||
|
|
||||||
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
|
# 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:
|
if hasattr(request.user, 'student') and obj:
|
||||||
current_week = date.today().isocalendar().week
|
current_week: int = date.today().isocalendar().week
|
||||||
if current_week > obj.week:
|
if current_week > obj.week:
|
||||||
return False
|
return False
|
||||||
return True
|
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)
|
return self.has_change_permission(request=request, obj=obj)
|
||||||
|
|||||||
@@ -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 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 import fields
|
||||||
from import_export.admin import ImportMixin
|
from import_export.admin import ImportMixin
|
||||||
|
from import_export.tmp_storages import CacheStorage
|
||||||
from import_export.resources import ModelResource
|
from import_export.resources import ModelResource
|
||||||
|
|
||||||
|
from ..models.aircrafts import Aircraft
|
||||||
from ..models.missions import MissionProfile
|
from ..models.missions import MissionProfile
|
||||||
|
|
||||||
from django_admin_action_forms import AdminActionFormsMixin
|
|
||||||
|
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
from typing import Any, Dict
|
||||||
|
|
||||||
# Resource Class for Student data import
|
# Resource Class for Student data import
|
||||||
class MissionProfileResource(ModelResource):
|
class MissionProfileResource(ModelResource):
|
||||||
@@ -20,20 +24,50 @@ class MissionProfileResource(ModelResource):
|
|||||||
duration = fields.Field(attribute="duration", column_name="duration")
|
duration = fields.Field(attribute="duration", column_name="duration")
|
||||||
|
|
||||||
# Cleanup fields before entering
|
# 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["mtype"] = SafeText(row["mtype"].upper().strip())
|
||||||
row["mnum"] = SafeText(row["mnum"].upper().strip())
|
row["mnum"] = SafeText(row["mnum"].upper().strip())
|
||||||
h, m, _ = row["duration"].split(":")
|
h, m, _ = row["duration"].split(":")
|
||||||
row["duration"] = timedelta(hours=float(h), minutes=float(m))
|
row["duration"] = timedelta(hours=float(h), minutes=float(m))
|
||||||
return super().before_import_row(row, **kwargs)
|
super().before_import_row(row, **kwargs)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = MissionProfile
|
model = MissionProfile
|
||||||
skip_unchanged = True
|
skip_unchanged = True
|
||||||
report_skipped = True
|
report_skipped = True
|
||||||
fields = ("mtype", "mnum", "duration")
|
fields = ("mtype", "mnum", "duration",)
|
||||||
import_id_fields = ("mtype", "mnum")
|
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):
|
class MissionProfileAdmin(ImportMixin, AdminActionFormsMixin, admin.ModelAdmin):
|
||||||
list_display = ("mtype", "mnum", "notes")
|
list_display = ("mtype", "mnum", "assigned_aircrafts", "duration", "notes", )
|
||||||
list_filter = ("mtype", )
|
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()))
|
||||||
|
|
||||||
@@ -1,21 +1,31 @@
|
|||||||
from django import forms
|
from django.forms import ModelChoiceField, TypedMultipleChoiceField, ModelMultipleChoiceField
|
||||||
from django.db.models.query import QuerySet
|
from django.db.models.query import QuerySet, Q
|
||||||
from django.http import HttpRequest
|
from django.http import HttpRequest
|
||||||
from django.contrib import admin, messages
|
from django.contrib import admin, messages
|
||||||
from django.utils.safestring import SafeText
|
from django.utils.safestring import SafeText
|
||||||
|
|
||||||
from import_export import fields
|
from import_export import fields
|
||||||
from import_export.admin import ImportMixin
|
from import_export.admin import ImportMixin
|
||||||
|
from import_export.tmp_storages import CacheStorage
|
||||||
from import_export.resources import ModelResource
|
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 django_admin_action_forms import AdminActionFormsMixin, AdminActionForm, action_with_form
|
||||||
|
|
||||||
|
from ..models.aircrafts import Aircraft, AircraftTypes
|
||||||
from ..models.courses import Course
|
from ..models.courses import Course
|
||||||
from ..models.students import Student
|
from ..models.students import Student
|
||||||
|
|
||||||
from ..custom.colortag import course_color
|
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
|
# Resource Class for Student data import
|
||||||
class StudentResource(ModelResource):
|
class StudentResource(ModelResource):
|
||||||
surname = fields.Field(attribute="surname", column_name="surname")
|
surname = fields.Field(attribute="surname", column_name="surname")
|
||||||
@@ -24,29 +34,44 @@ class StudentResource(ModelResource):
|
|||||||
phone = fields.Field(attribute="phone", column_name="phone")
|
phone = fields.Field(attribute="phone", column_name="phone")
|
||||||
|
|
||||||
# Cleanup fields before entering
|
# Cleanup fields before entering
|
||||||
def before_import_row(self, row: dict[str, str], **kwargs) -> None:
|
def before_import_row(self, row: Dict[str, str], **kwargs) -> None:
|
||||||
row["name"] = SafeText(row["name"].capitalize().strip())
|
row["name"] = SafeText("-".join(c.capitalize() for c in row["name"].split(" ")).strip())
|
||||||
row["surname"] = SafeText(row["surname"].capitalize().strip())
|
row["surname"] = SafeText("-".join(c.capitalize() for c in row["surname"].split(" ")).strip())
|
||||||
row["phone"] = SafeText(row["phone"].replace(" ",""))
|
row["phone"] = SafeText(row["phone"].replace(" ",""))
|
||||||
row["email"] = SafeText(row["email"].lower().strip())
|
row["email"] = SafeText(row["email"].lower().strip())
|
||||||
return super().before_import_row(row, **kwargs)
|
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:
|
class Meta:
|
||||||
model = Student
|
model = Student
|
||||||
skip_unchanged = True
|
skip_unchanged = True
|
||||||
report_skipped = True
|
report_skipped = True
|
||||||
fields = ("surname", "name", "email", "phone")
|
fields = ("surname", "name", "email", "phone",)
|
||||||
import_id_fields = ("email", "phone")
|
import_id_fields = ("email", "phone",)
|
||||||
|
|
||||||
# Form Class for Student course change
|
# Form Class for Student course change
|
||||||
class ChangeCourseForm(AdminActionForm):
|
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):
|
class StudentAdmin(ImportMixin, AdminActionFormsMixin, admin.ModelAdmin):
|
||||||
list_display = ("surname", "name", "course", "course_color", "email", "phone", "password", "active")
|
model = Student
|
||||||
list_filter = ("course", "active")
|
list_display = ("surname", "name", "course", "course_color", "email", "phone", "username", "password", "active", )
|
||||||
actions = ("change_course", "disable_students")
|
list_filter = ("course", "active", )
|
||||||
|
search_fields = ("surname", "name", "phone", "email", )
|
||||||
|
actions = ("change_course", "disable_students", "change_aircraft", )
|
||||||
resource_classes = [StudentResource]
|
resource_classes = [StudentResource]
|
||||||
|
confirm_form_class = StudentCustomConfirmImportForm
|
||||||
|
tmp_storage_class = CacheStorage
|
||||||
|
skip_admin_log = True
|
||||||
|
|
||||||
@admin.display(description="Color")
|
@admin.display(description="Color")
|
||||||
def course_color(self, obj: Student) -> SafeText:
|
def course_color(self, obj: Student) -> SafeText:
|
||||||
@@ -58,7 +83,11 @@ 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.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]):
|
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:
|
||||||
@@ -66,10 +95,40 @@ class StudentAdmin(ImportMixin, AdminActionFormsMixin, admin.ModelAdmin):
|
|||||||
q.user.save()
|
q.user.save()
|
||||||
count: int = queryset.update(active = False)
|
count: int = queryset.update(active = False)
|
||||||
messages.success(request, f"{count} students deactivated")
|
messages.success(request, f"{count} students deactivated")
|
||||||
pass
|
|
||||||
|
|
||||||
@action_with_form(ChangeCourseForm, description="Change Student Course")
|
@action_with_form(ChangeCourseForm, description="Change Student Course")
|
||||||
def change_course(self, request: HttpRequest, queryset: QuerySet[Student], data):
|
def change_course(self, request: HttpRequest, queryset: QuerySet[Student], data):
|
||||||
course = data["course"]
|
course = data["course"]
|
||||||
count: int = queryset.update(course=course)
|
count: int = queryset.update(course=course)
|
||||||
messages.success(request, f"{count} students updated to {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
|
||||||
|
|||||||
@@ -26,12 +26,15 @@ class TrainingInLIne(nested_admin.NestedTabularInline):
|
|||||||
}
|
}
|
||||||
|
|
||||||
# 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: WeekPreference | None = None):
|
def has_change_permission(self, request: HttpRequest, obj: WeekPreference | None = None) -> bool:
|
||||||
if hasattr(request.user, 'student') and obj:
|
if hasattr(request.user, 'student') and obj:
|
||||||
current_week: int = date.today().isocalendar().week
|
current_week: int = date.today().isocalendar().week
|
||||||
if current_week > obj.week:
|
if current_week > obj.week:
|
||||||
return False
|
return False
|
||||||
return True
|
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)
|
return self.has_change_permission(request=request, obj=obj)
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import nested_admin
|
import nested_admin
|
||||||
|
|
||||||
from django import forms
|
from django.forms import Form
|
||||||
from django.db.models.query import QuerySet
|
from django.db.models.query import QuerySet
|
||||||
|
from django.contrib.auth.models import User
|
||||||
from django.http import HttpRequest, HttpResponse
|
from django.http import HttpRequest, HttpResponse
|
||||||
from django.contrib import admin, messages
|
from django.contrib import admin, messages
|
||||||
from django.utils.translation import ngettext
|
from django.utils.translation import ngettext
|
||||||
@@ -17,11 +18,13 @@ from ..custom.colortag import course_color
|
|||||||
from ..actions.exportweek import export_selected
|
from ..actions.exportweek import export_selected
|
||||||
|
|
||||||
from datetime import date
|
from datetime import date
|
||||||
|
from typing import Dict, List, Any
|
||||||
|
|
||||||
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",)
|
||||||
|
search_fields = ("student__surname","student__name",)
|
||||||
actions = ("export",)
|
actions = ("export",)
|
||||||
|
|
||||||
@admin.action(description="Export Selected Preferences")
|
@admin.action(description="Export Selected Preferences")
|
||||||
@@ -44,83 +47,75 @@ class WeekPreferenceAdmin(nested_admin.NestedModelAdmin):
|
|||||||
return course_color(obj.student.course.color)
|
return course_color(obj.student.course.color)
|
||||||
|
|
||||||
# If a user is registered as student hide filters
|
# 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)
|
list_filter = super().get_list_filter(request)
|
||||||
if hasattr(request.user, 'student'):
|
if hasattr(request.user, "student"):
|
||||||
return []
|
return []
|
||||||
return list_filter
|
return list_filter
|
||||||
|
|
||||||
# If a user is registered as student do not show actions
|
# 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)
|
actions = super().get_actions(request)
|
||||||
if hasattr(request.user, 'student'):
|
if hasattr(request.user, "student"):
|
||||||
return []
|
return {}
|
||||||
return actions
|
return actions
|
||||||
|
|
||||||
# If a user is registered as student show only their preferences
|
# 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)
|
qs = super().get_queryset(request)
|
||||||
if hasattr(request.user, 'student'):
|
if hasattr(request.user, "student"):
|
||||||
return qs.filter(student=request.user.student)
|
return qs.filter(student=request.user.student)
|
||||||
# If admin show everything
|
# If admin show everything
|
||||||
return qs
|
return qs
|
||||||
|
|
||||||
def get_form(self, request, obj=None, **kwargs):
|
def get_form(self, request: HttpRequest, obj: WeekPreference | None = None, **kwargs: Dict[str, Any]) -> Form:
|
||||||
form: forms.Form = super().get_form(request, obj, **kwargs)
|
form: Form = super().get_form(request, obj, **kwargs)
|
||||||
current_week = date.today().isocalendar().week
|
current_week = date.today().isocalendar().week
|
||||||
|
|
||||||
# If form contains the week field
|
# If form contains the week field
|
||||||
if 'week' in form.base_fields:
|
if "week" in form.base_fields:
|
||||||
# Set default value as current week
|
# 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 student is current user making request
|
||||||
if hasattr(request.user, 'student'):
|
if hasattr(request.user, "student"):
|
||||||
student = request.user.student
|
student = request.user.student
|
||||||
if 'student' in form.base_fields:
|
if "student" in form.base_fields:
|
||||||
form.base_fields['student'].initial = student
|
form.base_fields["student"].initial = student
|
||||||
form.base_fields['student'].disabled = True
|
form.base_fields["student"].disabled = True
|
||||||
form.base_fields['week'].disabled = True # student cannot change week
|
form.base_fields["week"].disabled = True # student cannot change week
|
||||||
return form
|
return form
|
||||||
|
|
||||||
# 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, obj: WeekPreference | None = None):
|
def has_change_permission(self, request: HttpRequest, obj: WeekPreference | None = None) -> bool:
|
||||||
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 current_week > obj.week:
|
if current_week > obj.week:
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# 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_add_permission(self, request, obj: WeekPreference | None = None):
|
def has_add_permission(self, request: HttpRequest, obj: WeekPreference | None = None) -> bool:
|
||||||
if hasattr(request.user, 'student') and obj:
|
return self.has_change_permission(request, 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
|
# If user is a student deny edit permission for week past the current one
|
||||||
def has_delete_permission(self, request, obj: WeekPreference | None = None):
|
def has_delete_permission(self, request: HttpRequest, obj: WeekPreference | None = None)-> bool:
|
||||||
if hasattr(request.user, 'student') and obj:
|
return self.has_change_permission(request, obj)
|
||||||
current_week = date.today().isocalendar().week
|
|
||||||
if current_week > obj.week:
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
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 {}
|
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
|
current_week = date.today().isocalendar().week
|
||||||
weekpref = WeekPreference.objects.get(id=object_id)
|
weekpref = WeekPreference.objects.get(id=object_id)
|
||||||
if current_week > weekpref.week:
|
if current_week > weekpref.week:
|
||||||
extra_context['show_save'] = False
|
extra_context["show_save"] = False
|
||||||
extra_context['show_save_and_continue'] = False
|
extra_context["show_save_and_continue"] = False
|
||||||
extra_context['show_save_and_add_another'] = False
|
extra_context["show_save_and_add_another"] = False
|
||||||
extra_context['show_delete'] = False
|
extra_context["show_delete"] = False
|
||||||
return super().changeform_view(request, object_id, form_url, extra_context)
|
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
|
# 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
|
obj.student = request.user.student
|
||||||
super().save_model(request, obj, form, change)
|
super().save_model(request, obj, form, change)
|
||||||
14
cntmanage/flightslot/middleware.py
Normal file
14
cntmanage/flightslot/middleware.py
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
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") and not request.user.is_superuser:
|
||||||
|
if "/admin/" in request.path:
|
||||||
|
return redirect("/user/") # redirect automatico
|
||||||
|
return self.get_response(request)
|
||||||
@@ -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',
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -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),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -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'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -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'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -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'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -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'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -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'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -5,5 +5,45 @@ class AircraftTypes(models.TextChoices):
|
|||||||
C152 = "C152", _("Cessna 152")
|
C152 = "C152", _("Cessna 152")
|
||||||
P208 = "P208", _("Tecnam P2008")
|
P208 = "P208", _("Tecnam P2008")
|
||||||
PA28 = "PA28", _("Piper PA28R")
|
PA28 = "PA28", _("Piper PA28R")
|
||||||
|
PA34 = "PA34", _("Piper PA34")
|
||||||
C182 = "C182", _("Cessna 182Q")
|
C182 = "C182", _("Cessna 182Q")
|
||||||
P210 = "TWEN", _("Tecnam P2010")
|
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)
|
||||||
|
|||||||
@@ -10,14 +10,18 @@ class CourseTypes(models.TextChoices):
|
|||||||
DISTANCE = "DL", _("DISTANCE")
|
DISTANCE = "DL", _("DISTANCE")
|
||||||
OTHER = "OTHER",_("OTHER")
|
OTHER = "OTHER",_("OTHER")
|
||||||
|
|
||||||
|
|
||||||
class Course(models.Model):
|
class Course(models.Model):
|
||||||
# Add colors according to table from Alessia
|
# Add colors according to table from Alessia
|
||||||
COLOR_PALETTE = [
|
COLOR_PALETTE = [
|
||||||
("#ffffff","WHITE"),
|
("#bfbfbf","GREY"),
|
||||||
("#ff0000", "RED"),
|
("#ff0000", "RED"),
|
||||||
("#00ff00", "GREEN"),
|
("#ffc000", "ORANGE"),
|
||||||
("#0000ff", "BLUE")
|
("#ffff00", "YELLOW"),
|
||||||
|
("#92d050", "GREEN"),
|
||||||
|
("#00b0f0", "CYAN"),
|
||||||
|
("#b1a0c7", "MAGENTA"),
|
||||||
|
("#fabcfb", "PINK"),
|
||||||
|
("#f27ae4", "VIOLET"),
|
||||||
]
|
]
|
||||||
|
|
||||||
id = models.AutoField(
|
id = models.AutoField(
|
||||||
@@ -43,8 +47,8 @@ class Course(models.Model):
|
|||||||
)
|
)
|
||||||
|
|
||||||
color = ColorField (
|
color = ColorField (
|
||||||
samples=COLOR_PALETTE,
|
verbose_name=_("Binder Color"),
|
||||||
verbose_name=_("Binder Color")
|
samples=COLOR_PALETTE
|
||||||
)
|
)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
|
|||||||
@@ -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,65 @@ 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
|
||||||
default=False
|
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):
|
def __str__(self):
|
||||||
if self.stop:
|
return f"{self.departure} -> {self.destination}"
|
||||||
return "Refuelling Stop"
|
|
||||||
else:
|
class HourBuildingLegStop(HourBuildingLegBase):
|
||||||
return f"Flight Leg: {self.departure} -> {self.destination}"
|
|
||||||
|
refuel = models.BooleanField (
|
||||||
|
null=False,
|
||||||
|
default=False,
|
||||||
|
verbose_name="Stop for Refuelling"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Change displayed name in the inline form
|
||||||
|
class Meta(HourBuildingLegBase.Meta):
|
||||||
|
verbose_name = "Stop"
|
||||||
|
verbose_name_plural = "Stops"
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"Refuel" if self.refuel else f"No Refuel"
|
||||||
|
|
||||||
@@ -3,16 +3,19 @@ from django.db import models
|
|||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
from ..models.weekpref import WeekPreference
|
from ..models.weekpref import WeekPreference
|
||||||
|
from ..models.aircrafts import Aircraft
|
||||||
|
|
||||||
class MissionType(models.TextChoices):
|
class MissionType(models.TextChoices):
|
||||||
OTHER = "OTHER", _("OTHER")
|
OTHER = "OTHER", _("OTHER")
|
||||||
|
CHK = "CHK", _("CHK_6M")
|
||||||
PPL = "PPL", _("PPL")
|
PPL = "PPL", _("PPL")
|
||||||
IR = "IR", _("IR")
|
IR = "IR", _("IR")
|
||||||
MEP = "MEP", _("MEP")
|
MEP = "MEP", _("MEP")
|
||||||
|
MEP_IR = "MEP_IR", _("MEP_IR")
|
||||||
CPL = "CPL", _("CPL")
|
CPL = "CPL", _("CPL")
|
||||||
|
UPRT = "UPRT", _("UPRT")
|
||||||
FI = "FI", _("FI")
|
FI = "FI", _("FI")
|
||||||
PC = "PC", _("PC")
|
PC = "PC", _("PC")
|
||||||
CHK = "CHK", _("CHK_6M")
|
|
||||||
|
|
||||||
class MissionProfile(models.Model):
|
class MissionProfile(models.Model):
|
||||||
id = models.AutoField(
|
id = models.AutoField(
|
||||||
@@ -37,6 +40,10 @@ class MissionProfile(models.Model):
|
|||||||
default=timedelta(hours=1)
|
default=timedelta(hours=1)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
aircrafts = models.ManyToManyField(
|
||||||
|
Aircraft
|
||||||
|
)
|
||||||
|
|
||||||
notes = models.TextField(
|
notes = models.TextField(
|
||||||
max_length=140,
|
max_length=140,
|
||||||
null=True,
|
null=True,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ from django.db import models
|
|||||||
from django.contrib.auth.models import User, Group
|
from django.contrib.auth.models import User, Group
|
||||||
|
|
||||||
from ..models.courses import Course
|
from ..models.courses import Course
|
||||||
|
from ..models.aircrafts import Aircraft
|
||||||
|
|
||||||
class Student(models.Model):
|
class Student(models.Model):
|
||||||
id = models.AutoField(
|
id = models.AutoField(
|
||||||
@@ -46,29 +47,39 @@ class Student(models.Model):
|
|||||||
blank=True
|
blank=True
|
||||||
)
|
)
|
||||||
|
|
||||||
def default_password(self) -> str:
|
aircrafts = models.ManyToManyField(
|
||||||
return f"{self.name.lower()[0]}{self.surname.lower()}{self.id}"
|
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
|
# 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")
|
||||||
|
|||||||
31
cntmanage/poetry.lock
generated
31
cntmanage/poetry.lock
generated
@@ -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"
|
||||||
@@ -370,18 +385,18 @@ files = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sqlparse"
|
name = "sqlparse"
|
||||||
version = "0.5.3"
|
version = "0.5.4"
|
||||||
description = "A non-validating SQL parser."
|
description = "A non-validating SQL parser."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
groups = ["main"]
|
groups = ["main"]
|
||||||
files = [
|
files = [
|
||||||
{file = "sqlparse-0.5.3-py3-none-any.whl", hash = "sha256:cf2196ed3418f3ba5de6af7e82c694a9fbdbfecccdfc72e281548517081f16ca"},
|
{file = "sqlparse-0.5.4-py3-none-any.whl", hash = "sha256:99a9f0314977b76d776a0fcb8554de91b9bb8a18560631d6bc48721d07023dcb"},
|
||||||
{file = "sqlparse-0.5.3.tar.gz", hash = "sha256:09f67787f56a0b16ecdbde1bfc7f5d9c3371ca683cfeaa8e6ff60b4807ec9272"},
|
{file = "sqlparse-0.5.4.tar.gz", hash = "sha256:4396a7d3cf1cd679c1be976cf3dc6e0a51d0111e87787e7a8d780e7d5a998f9e"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.extras]
|
[package.extras]
|
||||||
dev = ["build", "hatch"]
|
dev = ["build"]
|
||||||
doc = ["sphinx"]
|
doc = ["sphinx"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -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"
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "cntmanage"
|
name = "cntmanage"
|
||||||
version = "0.1.0"
|
version = "0.2.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]
|
||||||
|
|||||||
BIN
cntmanage/static/cantorair_blue.jpg
Normal file
BIN
cntmanage/static/cantorair_blue.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.8 KiB |
@@ -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 %}
|
||||||
|
|||||||
Reference in New Issue
Block a user