42 Commits

Author SHA1 Message Date
4b5319f557 Instructor import 2025-12-02 12:21:11 +01:00
3ee2269d70 Added instructor availability model and admin 2025-12-02 12:00:15 +01:00
5d1686f24b Added instructor model, admin and basic actions 2025-12-01 18:41:44 +01:00
99a8cfe482 Generalized aircraft assign function 2025-12-01 15:18:30 +01:00
2b1042d3a8 fix mission import 2025-12-01 14:50:17 +01:00
f06f269568 centralized student permission in one function 2025-12-01 14:39:09 +01:00
a31798d0b0 Improved data ordering in requests and addes ac types CAP10 2025-12-01 14:23:46 +01:00
af62bf843c Merge pull request 'airplane-class' (#2) from airplane-class into flightslot
Reviewed-on: #2
2025-11-28 12:12:11 +01:00
5291956a31 Add assigned aircraft tp output excel 2025-11-28 12:06:28 +01:00
7f908157bf Added git hash as version number 2025-11-28 11:48:29 +01:00
35f773047d Updated version, Updated poetry 2025-11-28 11:19:45 +01:00
65444e786b Added MEPIR and UPRT mix profile types 2025-11-28 11:14:07 +01:00
7a392df8ad Added assigned aircrafts to mission profiles 2025-11-28 11:04:12 +01:00
de39913275 Adde Aircraft class and associate students with aircrafts 2025-11-28 10:16:01 +01:00
33c610dcbc order export by week first 2025-11-27 13:07:08 +01:00
1c0b287666 Max 4 digits for user password 2025-11-27 12:56:46 +01:00
cc833c475f Fix gunicorn timeout and add coruse to student 2025-11-27 12:48:51 +01:00
09584e22fd Added search filed for student in weekpreference, refuel stop non null default false, migrations 2025-11-27 11:59:11 +01:00
97b14ae1d7 Add select course upon student bulk import, search field for students 2025-11-27 11:52:25 +01:00
18d2604121 Fixed user redirect middleware 2025-11-27 10:17:11 +01:00
b32d0fd032 Install polymorphic in prod 2025-11-25 12:26:48 +01:00
bf9f43eed8 Try fix 500 server error 2025-11-25 12:07:21 +01:00
18953e06b7 Added migrations 2025-11-25 12:01:48 +01:00
b79f0c318a Remove add permission if week expired 2025-11-25 11:51:46 +01:00
c91f603a50 Remove add permission if week expired 2025-11-25 11:51:24 +01:00
f7030e8da1 Merge pull request 'polymorphic' (#1) from polymorphic into flightslot
Reviewed-on: #1
2025-11-24 12:22:01 +01:00
95370ed0dc Improved excel formatting 2025-11-24 12:20:42 +01:00
bb634d28ed improved excel formatting 2025-11-23 12:32:12 +01:00
cbdf49adfd Improved class naming for admin 2025-11-21 19:11:21 +01:00
7ad09e21b7 Improved time widget visualization 2025-11-21 18:57:44 +01:00
48ff1d799c Polymorphic model correctly saves, fixed formatting of admin 2025-11-21 17:49:51 +01:00
02990d4b2f Nested polymorphic hour building legs and stops added, needs restyling 2025-11-21 15:25:29 +01:00
bcdc885d66 fix titles for user 2025-11-19 22:07:41 +01:00
674105600e Improved visuals of admin page login 2025-11-19 21:53:40 +01:00
aa24976abb fix hb description 2025-11-19 19:06:30 +01:00
37b1ed4aa1 added custom hide sidebar middleware to prod 2025-11-19 18:55:32 +01:00
5f5c03e479 Autobuild docker imahe for application 2025-11-19 18:48:21 +01:00
78f53cae7d Disable sidebar for students 2025-11-19 18:20:16 +01:00
72891440af improved compose and entry point files 2025-11-19 16:40:42 +01:00
ae86a2e5fa Deploy application in container using compose to bring up db 2025-11-19 13:37:34 +01:00
d5befdd018 Changed settings project name, removed tech db stuff 2025-11-18 23:34:17 +01:00
491afb6257 Renamed Project to cntmanage 2025-11-18 23:28:50 +01:00
111 changed files with 1809 additions and 1213 deletions

2
.vscode/launch.json vendored
View File

@@ -8,7 +8,7 @@
"name": "Python Debugger: Django", "name": "Python Debugger: Django",
"type": "debugpy", "type": "debugpy",
"request": "launch", "request": "launch",
"program": "${workspaceFolder}/techdb/manage.py", "program": "${workspaceFolder}/cntmanage/manage.py",
"args": ["runserver"], "args": ["runserver"],
"console": "integratedTerminal", "console": "integratedTerminal",
"django": true, "django": true,

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', 'techdb.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,8 @@ INSTALLED_APPS = [
'colorfield', 'colorfield',
'import_export', 'import_export',
'django_admin_action_forms', 'django_admin_action_forms',
'polymorphic',
"phonenumber_field",
] ]
# Import Export plugin settings # Import Export plugin settings
@@ -60,9 +61,10 @@ 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 = 'techdb.urls' ROOT_URLCONF = 'cntmanage.urls'
TEMPLATES = [ TEMPLATES = [
{ {
@@ -80,7 +82,7 @@ TEMPLATES = [
}, },
] ]
WSGI_APPLICATION = 'techdb.wsgi.application' WSGI_APPLICATION = 'cntmanage.wsgi.application'
STATICFILES_DIRS = [ STATICFILES_DIRS = [
os.path.join(BASE_DIR, "static"), os.path.join(BASE_DIR, "static"),
@@ -92,9 +94,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 +129,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,146 @@
"""
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',
"phonenumber_field",
]
# 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'

View File

@@ -0,0 +1,13 @@
from django.contrib import admin
from django.urls import path
from django.shortcuts import redirect
from flightslot.admin import flightslot_user
from flightslot.admin import flightslot_staff
urlpatterns = [
#path('', RedirectView.as_view(url='/admin/', permanent=False)),
path('admin/', admin.site.urls),
path('user/', flightslot_user.urls),
path('staff/', flightslot_staff.urls),
path("", lambda r: redirect("/user/")), # la root porta gli utenti nella pagina giusta
]

View File

@@ -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', 'techdb.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
View File

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

View File

@@ -0,0 +1,54 @@
# Use postgres/example user/password credentials
services:
postgresql:
image: postgres:17.0
container_name: tech-postgresql
restart: always
# set shared memory limit when using docker-compose
shm_size: 128mb
volumes:
- postgresql_data:/var/lib/postgresql/data
ports:
- 5432:5432
networks:
- cntnet
environment:
POSTGRES_DB: flightslot_db
POSTGRES_USER: flightslot
POSTGRES_PASSWORD: flightslot
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
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 version ${VERSION} ..."
exec "$@"

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

View File

@@ -0,0 +1,23 @@
from django.db.models.query import QuerySet, Q
from ..models.instructors import Instructor
from ..models.students import Student
from ..models.missions import MissionProfile
from ..models.aircrafts import Aircraft, AircraftTypes
from typing import List, Dict, Tuple
def assign_aircraft(queryset: QuerySet[Student] | QuerySet[MissionProfile] | QuerySet[Instructor], data: Dict[str, List[AircraftTypes]]) -> Tuple[int, List[str]]:
i: int = 0
ac_types: List[AircraftTypes] = data["aircrafts"]
ac_query: Q = Q() # Build an or query to select all aircrafts of the specified types
for a in ac_types:
ac_query |= Q(type=a)
aircrafts: QuerySet[Aircraft] = Aircraft.objects.filter(ac_query).all() # Execute query
for obj in queryset:
obj.aircrafts.clear()
for ac in aircrafts:
obj.aircrafts.add(ac)
obj.save()
i += 1
return i, [a for a in ac_types]

View File

@@ -0,0 +1,20 @@
from django.db.models.query import QuerySet, Q
from ..models.instructors import Instructor
from ..models.missions import MissionProfile, MissionTypes
from typing import List, Dict, Tuple
def assign_profile(queryset: QuerySet[Instructor], data: Dict[str, List[MissionTypes]]) -> Tuple[int, List[str]]:
i: int = 0
mix_types: List[MissionTypes] = data["mission_profiles"]
mix_query: Q = Q() # Build an or query to select all aircrafts of the specified types
for m in mix_types:
mix_query |= Q(mtype=m)
profiles: QuerySet[MissionProfile] = MissionProfile.objects.filter(mix_query).all() # Execute query
for obj in queryset:
obj.missions.clear()
for ac in profiles:
obj.missions.add(ac)
obj.save()
i += 1
return i, [m for m in mix_types]

View File

@@ -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,15 +115,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 +132,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 +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,8 +223,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

@@ -0,0 +1,79 @@
from django.contrib import admin
from django.http import HttpRequest
from .models.aircrafts import Aircraft
from .models.courses import Course
from .models.students import Student
from .models.missions import MissionProfile
from .models.weekpref import WeekPreference
from .models.instructors import Instructor
from .models.availabilities import Availability
from .admins.aircraft_adm import AircraftAdmin
from .admins.course_adm import CourseAdmin
from .admins.student_adm import StudentAdmin
from .admins.mission_adm import MissionProfileAdmin
from .admins.weekpref_adm import WeekPreferenceAdmin
from .admins.instructor_admin import InstructorAdmin
from .admins.availability_adm import AvailabilityAdmin
from django.contrib.admin import AdminSite
from os import environ
##################################
# User website under /user/ URL #
##################################
class FlightSlotUserSite(AdminSite):
site_header = "Flight Scheduler 🛫"
site_title = "Flight Scheduler 🛫"
index_title = "Welcome to CantorAir Flight Scheduler Student Portal"
def get_app_list(self, request: HttpRequest, *args, **kwargs):
app_list = super().get_app_list(request)
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)
##################################
# User website under /staff/ URL #
##################################
class FlightSlotStaffSite(AdminSite):
site_header = "Flight Scheduler Staff 🛫"
site_title = "Flight Scheduler Staff 🛫"
index_title = "Welcome to CantorAir Flight Scheduler Staff Portal"
def get_app_list(self, request: HttpRequest, *args, **kwargs):
app_list = super().get_app_list(request)
if not request.user.is_superuser:
self.enable_nav_sidebar = False
return app_list
# Register only user visible models
flightslot_staff = FlightSlotUserSite(name="staff_site")
flightslot_staff.register(Availability, AvailabilityAdmin)
flightslot_staff.register(MissionProfile, MissionProfileAdmin)
flightslot_staff.register(Instructor, InstructorAdmin)
# Get version for debug purposes
ver: str = environ.get("VERSION", "dev")
# Register all visible models
admin.site.site_header = f"Flight Scheduler Admin 🛫 - ver.{ver}"
admin.site.site_title = f"Flight Scheduler Admin 🛫 - ver.{ver}"
admin.site.index_title = "Welcome to CantorAir Flight Scheduler Administrator Portal"
admin.site.register(Aircraft, AircraftAdmin)
admin.site.register(Course, CourseAdmin)
admin.site.register(MissionProfile, MissionProfileAdmin)
admin.site.register(Student, StudentAdmin)
admin.site.register(WeekPreference, WeekPreferenceAdmin)
admin.site.register(Instructor, InstructorAdmin)
admin.site.register(Availability, AvailabilityAdmin)

View File

@@ -0,0 +1,17 @@
from django.contrib import admin
from django.db.models.query import QuerySet
from django.http import HttpRequest
from ..models.aircrafts import Aircraft
class AircraftAdmin(admin.ModelAdmin):
model = Aircraft
list_display = ("type", "markings", "avail_hours", "complex", )
list_filter = ("type", )
actions = ("reset_maint")
def get_queryset(self, request: HttpRequest) -> QuerySet[Aircraft]:
qs: QuerySet[Aircraft] = super().get_queryset(request)
qs.order_by("type", "markings")
return qs

View File

@@ -0,0 +1,66 @@
from django import forms
from django.db.models.query import QuerySet
from django.http import HttpRequest
from django.contrib import admin
from django.utils.safestring import SafeText
from ..models.instructors import Instructor
from ..models.availabilities import Availability
from datetime import date
from typing import Any, List
class AvailabilityForm(forms.ModelForm):
model=Availability
class AvailabilityAdmin(admin.ModelAdmin):
model = Availability
list_display = ("week", "instructor__surname", "instructor__name", "days_available", "hours")
list_filter = ("week", )
search_fields = ("instructor__surname","instructor__name", )
#actions = ("export", )
@admin.display(description="Days Available")
def days_available(self, obj: Availability) -> SafeText:
if not obj:
return SafeText("")
days: List[str | None] = [
"Mon" if obj.monday else None,
"Tue" if obj.tuesday else None,
"Wed" if obj.wednesday else None,
"Thu" if obj.thursday else None,
"Fri" if obj.friday else None,
"Sat" if obj.saturday else None,
"Sun" if obj.sunday else None,
]
return SafeText("/".join(d if d else "" for d in days))
def get_queryset(self, request: HttpRequest) -> QuerySet:
return super().get_queryset(request).order_by("-week", "instructor__surname", "instructor__name")
def get_form(self, request: HttpRequest, obj: Availability | None = None, change: bool = False, **kwargs: Any) -> AvailabilityForm:
form: AvailabilityForm = super().get_form(request, obj, change, **kwargs)
if change: # if is only a form change do not set default values and return form
return form
# If form contains the week field
current_week = date.today().isocalendar().week
if "week" in form.base_fields:
# Set default value as current week
form.base_fields["week"].initial = current_week
# If student is current user making request
if hasattr(request.user, "instructor"):
instructor: Instructor = request.user.instructor
if "instructor" in form.base_fields:
form.base_fields["instructor"].initial = instructor
form.base_fields["instructor"].disabled = True
return form
# Imposta automaticamente l'istruttore se non è già valorizzato
def save_model(self, request: HttpRequest, obj: Availability, form: AvailabilityForm, change: bool):
if hasattr(request.user, "instructor") and not obj.instructor_id:
obj.instructor = request.user.instructor
super().save_model(request, obj, form, change)

View File

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

View File

@@ -0,0 +1,83 @@
import nested_admin
from django import forms
from django.db import models
from django.forms import TextInput, Textarea
from django.http import HttpRequest
from durationwidget.widgets import TimeDurationWidget
from ..models.hourbuildings import HourBuilding, HourBuildingLegBase, HourBuildingLegFlight, HourBuildingLegStop
from ..models.weekpref import WeekPreference
from ..custom.student_permissions import has_edit_permission
class HourBuildingLegFlightForm(forms.ModelForm):
class Meta:
model = HourBuildingLegFlight
fields = "__all__"
widgets = {
"time": TimeDurationWidget(show_days=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.
class HourBuildingLegBaseInLine(nested_admin.NestedStackedPolymorphicInline):
model = HourBuildingLegBase
fk_name = "hb"
verbose_name_plural = "Hour Building Legs"
class HourBuildingLegFlightInLine(nested_admin.NestedStackedPolymorphicInline.Child):
model = HourBuildingLegFlight
form = HourBuildingLegFlightForm
fk_name = "hourbuildinglegbase_ptr"
fields = ("departure", "time", "destination", "pax", )
class HourBuildingLegStopInLine(nested_admin.NestedStackedPolymorphicInline.Child):
model = HourBuildingLegStop
form = HourBuildingLegFlightForm
fk_name = "hourbuildinglegbase_ptr"
fields = ("time", "refuel", )
child_inlines = (HourBuildingLegFlightInLine, HourBuildingLegStopInLine, )
class HourBuildingInLine(nested_admin.NestedTabularInline):
model = HourBuilding
inlines = (HourBuildingLegBaseInLine,)
extra = 0
max_num = 7
fk_name = "weekpref"
verbose_name_plural = "Hour Buildings"
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
def has_change_permission(self, request: HttpRequest, obj: WeekPreference | None = None) -> bool:
return has_edit_permission(request=request, obj=obj)
def has_delete_permission(self, request: HttpRequest, obj: WeekPreference | None = None) -> bool:
return self.has_change_permission(request=request, obj=obj)
def has_add_permission(self, request: HttpRequest, obj: WeekPreference | None = None) -> bool:
return self.has_change_permission(request=request, obj=obj)

View File

@@ -0,0 +1,95 @@
from django.forms import TypedMultipleChoiceField
from django.db.models.query import QuerySet
from django.http import HttpRequest
from django.contrib import admin, messages
from django.utils.safestring import SafeText
from django_admin_action_forms import AdminActionFormsMixin, AdminActionForm, action_with_form
from import_export import fields
from import_export.admin import ImportMixin
from import_export.tmp_storages import CacheStorage
from import_export.resources import ModelResource
from ..models.instructors import Instructor
from ..models.aircrafts import AircraftTypes
from ..models.missions import MissionTypes
from ..actions.assign_profile import assign_profile
from ..actions.assign_aircraft import assign_aircraft
from typing import Dict, List
# Resource Class for Instructor data import
class InstructorResource(ModelResource):
surname = fields.Field(attribute="surname", column_name="surname")
name = fields.Field(attribute="name", column_name="name")
email = fields.Field(attribute="email", column_name="email")
phone = fields.Field(attribute="phone", column_name="phone")
# Cleanup fields before entering
def before_import_row(self, row: Dict[str, str], **kwargs) -> None:
row["name"] = SafeText("-".join(c.capitalize() for c in row["name"].split(" ")).strip())
row["surname"] = SafeText("-".join(c.capitalize() for c in row["surname"].split(" ")).strip())
row["phone"] = SafeText(row["phone"].replace(" ",""))
row["email"] = SafeText(row["email"].lower().strip())
return super().before_import_row(row, **kwargs)
class Meta:
model = Instructor
skip_unchanged = True
report_skipped = True
fields = ("surname", "name", "email", "phone", )
import_id_fields = ("surname", "name", )
# Form class to assing aircrafts to instructors
class AssignMissionForm(AdminActionForm):
mission_profiles = TypedMultipleChoiceField(choices=MissionTypes)
# Form class to assing aircrafts to instructors
class AssignAircraftForm(AdminActionForm):
aircrafts = TypedMultipleChoiceField(choices=AircraftTypes)
class InstructorAdmin(ImportMixin, AdminActionFormsMixin, admin.ModelAdmin):
model = Instructor
list_display = ("surname", "name", "email", "phone", "assigned_profiles", "assigned_aircrafts", "active", )
search_fields = ("surname", "name", "phone", "email", )
readonly_fields = ("username", "password", )
actions = ("assign_aircraft", "assign_profile", )
resource_classes = [InstructorResource]
tmp_storage_class = CacheStorage
skip_admin_log = True
@admin.display(description="Password")
def password(self, obj: Instructor) -> SafeText:
return SafeText(obj.default_password())
@admin.display(description="Username")
def username(self, obj: Instructor) -> SafeText:
return SafeText(obj.default_username())
@admin.display(description="Assigned Profiles")
def assigned_profiles(self, obj: Instructor) -> SafeText:
if not obj.aircrafts:
return SafeText("")
return SafeText("/".join(mix.mtype for mix in obj.missions.distinct("mtype").order_by("mtype").all()))
@admin.display(description="Assigned Aircrafts")
def assigned_aircrafts(self, obj: Instructor) -> SafeText:
if not obj.aircrafts:
return SafeText("")
return SafeText("/".join(ac.type for ac in obj.aircrafts.distinct("type").order_by("type").all()))
@action_with_form(AssignAircraftForm, description="Assign Aircraft Type")
def assign_aircraft(self, request: HttpRequest, queryset: QuerySet[Instructor], data: Dict[str, List[AircraftTypes]]):
i: int
ac_types: List[str]
i, ac_types = assign_aircraft(queryset=queryset, data=data)
messages.success(request, f"{i} Instructors updated to {ac_types}")
@action_with_form(AssignMissionForm, description="Assign Mission Type")
def assign_profile(self, request: HttpRequest, queryset: QuerySet[Instructor], data: Dict[str, List[MissionTypes]]):
i: int
mix_types: List[str]
i, mix_types = assign_profile(queryset=queryset, data=data)
messages.success(request, f"{i} Instructors updated to {mix_types}")

View File

@@ -0,0 +1,70 @@
from django.forms import TypedMultipleChoiceField
from django.contrib import admin, messages
from django.http import HttpRequest
from django.db.models.query import QuerySet
from django.utils.safestring import SafeText
from django_admin_action_forms import AdminActionFormsMixin, AdminActionForm, action_with_form
from import_export import fields
from import_export.admin import ImportMixin
from import_export.tmp_storages import CacheStorage
from import_export.resources import ModelResource
from ..models.aircrafts import AircraftTypes
from ..models.missions import MissionProfile
from ..actions.assign_aircraft import assign_aircraft
from datetime import timedelta
from typing import Any, Dict, List
# Resource Class for Student data import
class MissionProfileResource(ModelResource):
mtype = fields.Field(attribute="mtype", column_name="mtype")
mnum = fields.Field(attribute="mnum", column_name="mnum")
duration = fields.Field(attribute="duration", column_name="duration")
# Cleanup fields before entering
def before_import_row(self, row: dict[str, str | Any], **kwargs):
row["mtype"] = SafeText(row["mtype"].upper().strip())
row["mnum"] = SafeText(row["mnum"].upper().strip())
h, m, _ = row["duration"].split(":")
row["duration"] = timedelta(hours=float(h), minutes=float(m))
super().before_import_row(row, **kwargs)
class Meta:
model = MissionProfile
skip_unchanged = True
report_skipped = True
fields = ("mtype", "mnum", "duration",)
import_id_fields = ("mtype", "mnum",)
# Form class to assing aircrafts to students
class ChangeAircraftForm(AdminActionForm):
aircrafts = TypedMultipleChoiceField(choices=AircraftTypes)
class MissionProfileAdmin(ImportMixin, AdminActionFormsMixin, admin.ModelAdmin):
list_display = ("mtype", "mnum", "assigned_aircrafts", "duration", "notes", )
list_filter = ("mtype", )
actions = ("assign_aircraft", )
resource_classes = [MissionProfileResource]
tmp_storage_class = CacheStorage
skip_admin_log = True
def get_queryset(self, request: HttpRequest) -> QuerySet[MissionProfile]:
return super().get_queryset(request).order_by("mtype", "mnum")
@action_with_form(ChangeAircraftForm, description="Assign Aircraft Type")
def assign_aircraft(self, request: HttpRequest, queryset: QuerySet[MissionProfile], data: Dict[str, List[AircraftTypes]]):
i: int
ac_types: List[str]
i, ac_types = assign_aircraft(queryset=queryset, data=data)
messages.success(request, f"{i} Missions updated to {ac_types}")
@admin.display(description="Assigned Aircrafts")
def assigned_aircrafts(self, obj: MissionProfile) -> SafeText:
if not obj.aircrafts:
return SafeText("")
return SafeText("/".join(ac.markings for ac in obj.aircrafts.all()))

View File

@@ -0,0 +1,127 @@
from django.forms import ModelChoiceField, TypedMultipleChoiceField
from django.db.models.query import QuerySet
from django.http import HttpRequest
from django.contrib import admin, messages
from django.utils.safestring import SafeText
from import_export import fields
from import_export.admin import ImportMixin
from import_export.tmp_storages import CacheStorage
from import_export.resources import ModelResource
from import_export.forms import ConfirmImportForm, ImportForm
from django_admin_action_forms import AdminActionFormsMixin, AdminActionForm, action_with_form
from ..models.aircrafts import AircraftTypes
from ..models.courses import Course
from ..models.students import Student
from ..actions.assign_aircraft import assign_aircraft
from ..custom.colortag import course_color
from typing import Any, Dict, List
# Custom import form to select a course for student input
class StudentCustomConfirmImportForm(ConfirmImportForm):
course = ModelChoiceField(
queryset=Course.objects.all(),
required=False)
# Resource Class for Student data import
class StudentResource(ModelResource):
surname = fields.Field(attribute="surname", column_name="surname")
name = fields.Field(attribute="name", column_name="name")
email = fields.Field(attribute="email", column_name="email")
phone = fields.Field(attribute="phone", column_name="phone")
# Cleanup fields before entering
def before_import_row(self, row: Dict[str, str], **kwargs) -> None:
row["name"] = SafeText("-".join(c.capitalize() for c in row["name"].split(" ")).strip())
row["surname"] = SafeText("-".join(c.capitalize() for c in row["surname"].split(" ")).strip())
row["phone"] = SafeText(row["phone"].replace(" ",""))
row["email"] = SafeText(row["email"].lower().strip())
return super().before_import_row(row, **kwargs)
# If course was addedd as a form kwasrg add it to the student after creation
def after_init_instance(self, instance: Student, new: bool, row: Dict[str, str], **kwargs: Dict[str, Any | Course]):
course = kwargs.get("course", None)
if course and isinstance(course, Course):
instance.course = course
class Meta:
model = Student
skip_unchanged = True
report_skipped = True
fields = ("surname", "name", "email", "phone",)
import_id_fields = ("email", "phone",)
# Form Class for Student course change
class ChangeCourseForm(AdminActionForm):
course = ModelChoiceField(queryset=Course.objects.all().order_by("ctype", "-cnumber"))
# Form class to assing aircrafts to students
class ChangeAircraftForm(AdminActionForm):
aircrafts = TypedMultipleChoiceField(choices=AircraftTypes)
class StudentAdmin(ImportMixin, AdminActionFormsMixin, admin.ModelAdmin):
model = Student
list_display = ("surname", "name", "course", "course_color", "email", "phone", "username", "password", "active", )
list_filter = ("course", "active", )
search_fields = ("surname", "name", "phone", "email", )
actions = ("change_course", "deactivate_students", "change_aircraft", )
resource_classes = [StudentResource]
confirm_form_class = StudentCustomConfirmImportForm
tmp_storage_class = CacheStorage
skip_admin_log = True
@admin.display(description="Color")
def course_color(self, obj: Student) -> SafeText:
if not obj.course:
return SafeText("")
return course_color(obj.course.color)
@admin.display(description="Password")
def password(self, obj: Student) -> SafeText:
return SafeText(obj.default_password())
@admin.display(description="Username")
def username(self, obj: Student) -> SafeText:
return SafeText(obj.default_username())
@admin.action(description="Deactivate Students")
def deactivate_students(self, request: HttpRequest, queryset: QuerySet[Student]):
for q in queryset.all():
if q.user:
q.user.is_staff = False
q.user.save()
count: int = queryset.update(active = False)
messages.success(request, f"{count} students deactivated")
@action_with_form(ChangeCourseForm, description="Change Student Course")
def change_course(self, request: HttpRequest, queryset: QuerySet[Student], data):
course = data["course"]
count: int = queryset.update(course=course)
messages.success(request, f"{count} students updated to {course}")
@action_with_form(ChangeAircraftForm, description="Assign Aircraft Type")
def change_aircraft(self, request: HttpRequest, queryset: QuerySet[Student], data: Dict[str, List[AircraftTypes]]):
i: int
ac_types: List[str]
i, ac_types = assign_aircraft(queryset=queryset, data=data)
messages.success(request, f"{i} Students updated to {ac_types}")
# Return the initial form for import confirmations, request course to user
def get_confirm_form_initial(self, request: HttpRequest, import_form) -> Dict[str, Any]:
initial: Dict[str, Any] = super().get_confirm_form_initial(request, import_form)
if import_form and hasattr(import_form.cleaned_data, "course"):
course: Course = import_form.cleaned_data["course"]
initial["course"] = course.id
return initial
# Add course to import form kwargs to be used by resource to associate course with all imported students
def get_import_data_kwargs(self, request: HttpRequest, *args, **kwargs) -> Dict[str, Any]:
form: ImportForm | None = kwargs.get("form", None)
if form and hasattr(form, "cleaned_data"):
kwargs["course"] = form.cleaned_data.get("course", None)
return kwargs

View File

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

View File

@@ -1,12 +1,13 @@
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.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
from django.utils.safestring import SafeText from django.utils.safestring import SafeText
from ..models.students import Student
from ..models.missions import Training from ..models.missions import Training
from ..models.weekpref import WeekPreference from ..models.weekpref import WeekPreference
@@ -14,14 +15,17 @@ from .training_adm import TrainingInLIne
from .hourbuilding_adm import HourBuildingInLine from .hourbuilding_adm import HourBuildingInLine
from ..custom.colortag import course_color from ..custom.colortag import course_color
from ..custom.student_permissions import has_edit_permission
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 +48,70 @@ 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).order_by("-week", "-student__course", "student__surname", "student__name")
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: 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: return has_edit_permission(request=request, obj=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_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 not obj and 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
weekpref = WeekPreference.objects.get(id=object_id) weekpref = WeekPreference.objects.get(id=object_id)
if current_week > weekpref.week: if not has_edit_permission(request=request, obj=weekpref):
extra_context['show_save'] = False extra_context["show_save"] = 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)

View File

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

View File

@@ -0,0 +1,16 @@
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/... o qualsiasi altro path
if hasattr(request, "user") and not request.user.is_superuser:
if hasattr(request.user, "student") and not "/user/" in request.path:
return redirect("/user/") # redirect automatico
elif hasattr(request.user, "instructor") and not "/staff/" in request.path:
return redirect("/staff/") # redirect automatico
return self.get_response(request)

View File

@@ -0,0 +1,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

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

View File

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

View File

@@ -0,0 +1,39 @@
# Generated by Django 5.2.8 on 2025-11-27 13:34
import colorfield.fields
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('flightslot', '0021_alter_hourbuildinglegstop_refuel'),
]
operations = [
migrations.CreateModel(
name='Aircraft',
fields=[
('id', models.AutoField(primary_key=True, serialize=False)),
('type', models.CharField(choices=[('C152', 'Cessna 152'), ('P208', 'Tecnam P2008'), ('PA28', 'Piper PA28R'), ('PA34', 'Piper PA34'), ('C182', 'Cessna 182Q'), ('TWEN', 'Tecnam P2010'), ('FSTD', 'Alsim ALX40')], max_length=4)),
('markings', models.CharField(max_length=6)),
('complex', models.BooleanField(default=False)),
('avail_hours', models.DurationField(null=True, verbose_name='Time until maintenance')),
],
),
migrations.AlterField(
model_name='course',
name='color',
field=colorfield.fields.ColorField(default='#FFFFFF', image_field=None, max_length=25, samples=[('#bfbfbf', 'GREY'), ('#ff0000', 'RED'), ('#ffc000', 'ORANGE'), ('#ffff00', 'YELLOW'), ('#92d050', 'GREEN'), ('#00b0f0', 'CYAN'), ('#b1a0c7', 'MAGENTA'), ('#fabcfb', 'PINK'), ('#f27ae4', 'VIOLET')], verbose_name='Binder Color'),
),
migrations.AlterField(
model_name='hourbuilding',
name='aircraft',
field=models.CharField(choices=[('C152', 'Cessna 152'), ('P208', 'Tecnam P2008'), ('PA28', 'Piper PA28R'), ('PA34', 'Piper PA34'), ('C182', 'Cessna 182Q'), ('TWEN', 'Tecnam P2010'), ('FSTD', 'Alsim ALX40')]),
),
migrations.AddField(
model_name='student',
name='aircrafts',
field=models.ManyToManyField(to='flightslot.aircraft'),
),
]

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,41 @@
# Generated by Django 5.2.8 on 2025-12-01 17:27
import django.db.models.deletion
import phonenumber_field.modelfields
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('flightslot', '0025_alter_aircraft_type_alter_hourbuilding_aircraft_and_more'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AlterField(
model_name='student',
name='email',
field=models.EmailField(db_index=True, max_length=254, unique=True),
),
migrations.AlterField(
model_name='student',
name='phone',
field=phonenumber_field.modelfields.PhoneNumberField(max_length=128, null=True, region=None, unique=True),
),
migrations.CreateModel(
name='Instructor',
fields=[
('id', models.AutoField(primary_key=True, serialize=False)),
('email', models.EmailField(db_index=True, max_length=254)),
('phone', phonenumber_field.modelfields.PhoneNumberField(max_length=128, null=True, region=None, unique=True)),
('name', models.CharField(max_length=32)),
('surname', models.CharField(max_length=32)),
('active', models.BooleanField(default=True)),
('aircrafts', models.ManyToManyField(to='flightslot.aircraft')),
('missions', models.ManyToManyField(to='flightslot.missionprofile')),
('user', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
]

View File

@@ -0,0 +1,36 @@
# Generated by Django 5.2.8 on 2025-12-02 10:02
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('flightslot', '0026_alter_student_email_alter_student_phone_instructor'),
]
operations = [
migrations.AlterField(
model_name='weekpreference',
name='student',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='flightslot.student', verbose_name='Student Selection'),
),
migrations.CreateModel(
name='Availability',
fields=[
('week', models.PositiveSmallIntegerField(auto_created=True, db_default=49, db_index=True, verbose_name='Week Number')),
('id', models.BigAutoField(primary_key=True, serialize=False)),
('monday', models.BooleanField(default=True)),
('tuesday', models.BooleanField(default=True)),
('wednesday', models.BooleanField(default=True)),
('thursday', models.BooleanField(default=True)),
('friday', models.BooleanField(default=True)),
('saturday', models.BooleanField(default=True)),
('sunday', models.BooleanField(default=True)),
('hours', models.DurationField(null=True, verbose_name='Available hours')),
('notes', models.TextField(blank=True, max_length=140, null=True)),
('instructor', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='flightslot.instructor', verbose_name='Instructor Selection')),
],
),
]

View File

@@ -0,0 +1,17 @@
# Generated by Django 5.2.8 on 2025-12-02 10:59
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('flightslot', '0027_alter_weekpreference_student_availability'),
]
operations = [
migrations.AlterModelOptions(
name='availability',
options={'verbose_name': 'Instructor Availability', 'verbose_name_plural': 'Instructor Availabilities'},
),
]

View File

@@ -0,0 +1,50 @@
from django.utils.translation import gettext_lazy as _
from django.db import models
class AircraftTypes(models.TextChoices):
C152 = "C152", _("Cessna 152")
P208 = "P208", _("Tecnam P2008")
PA28 = "PA28", _("Piper PA28R")
PA34 = "PA34", _("Piper PA34")
C182 = "C182", _("Cessna 182Q")
P210 = "TWEN", _("Tecnam P2010")
CP10 = "CP10", _("Cap 10")
ALX40 = "FSTD", _("Alsim ALX40")
class Aircraft(models.Model):
id = models.AutoField(
primary_key=True
)
type = models.CharField(
null=False,
blank=False,
max_length=4, # ICAO naming of aircraft,
choices=AircraftTypes
)
markings = models.CharField(
null=False,
blank=False,
max_length=6
)
complex = models.BooleanField(
null=False,
default=False
)
avail_hours = models.DurationField(
null=True,
verbose_name=_("Time until maintenance")
)
def __str__(self) -> str:
return f"{self.type} ({self.markings})"
# Insert dash between first and rest, I-OASM
def save(self, *args, **kwargs):
self.markings = self.markings.upper()
if not "-" in self.markings:
self.markings = self.markings[0] + "-" + self.markings[1:]
super().save(*args, **kwargs)

View File

@@ -0,0 +1,79 @@
from django.db import models
from datetime import date
from ..models.instructors import Instructor
class Availability(models.Model):
id = models.BigAutoField(
primary_key=True
)
week = models.PositiveSmallIntegerField(
null=False,
db_index=True,
db_default=date.today().isocalendar().week,
auto_created=True,
verbose_name="Week Number"
)
instructor = models.ForeignKey(
Instructor,
null=False,
db_index=True,
on_delete=models.CASCADE,
verbose_name="Instructor Selection"
)
monday = models.BooleanField(
default=True,
null=False
)
tuesday = models.BooleanField(
default=True,
null=False
)
wednesday = models.BooleanField(
default=True,
null=False
)
thursday = models.BooleanField(
default=True,
null=False
)
friday = models.BooleanField(
default=True,
null=False
)
saturday = models.BooleanField(
default=True,
null=False
)
sunday = models.BooleanField(
default=True,
null=False
)
hours = models.DurationField(
null=True,
verbose_name="Available hours"
)
notes = models.TextField(
max_length=140,
null=True,
blank=True
)
class Meta():
verbose_name = "Instructor Availability"
verbose_name_plural = "Instructor Availabilities"
def __str__(self):
return f"Week {self.week} - {self.instructor.surname} {self.instructor.name[0]}."

View File

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

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,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):
return f"{self.departure} -> {self.destination}"
class HourBuildingLegStop(HourBuildingLegBase):
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): 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

@@ -0,0 +1,99 @@
from django.db import models
from django.contrib.auth.models import User, Group
from phonenumber_field import modelfields
from ..models.missions import MissionProfile
from ..models.aircrafts import Aircraft
class Instructor(models.Model):
id = models.AutoField(
primary_key=True
)
email = models.EmailField(
null=False,
db_index=True
)
phone = modelfields.PhoneNumberField(
null=True,
unique=True
)
name = models.CharField(
null=False,
blank=False,
max_length=32
)
surname = models.CharField(
null=False,
blank=False,
max_length=32
)
active = models.BooleanField(
null=False,
default=True
)
aircrafts = models.ManyToManyField(
Aircraft
)
missions = models.ManyToManyField(
MissionProfile
)
user = models.OneToOneField(
User,
on_delete=models.CASCADE,
null=True,
blank=True
)
def default_password(self) -> str: # Maximum 4 digits for passowrd
if self.pk:
return f"{self.name.lower()[0]}{self.surname.lower()}{self.id % 10000}"
else:
return ""
def default_username(self) -> str:
if self.pk and self.user:
return self.user.username
else:
return ""
# Override save method to add user for login upon Student creation
def save(self, *args, **kwargs):
creating: bool = self.pk is None
super().save(*args, **kwargs)
if creating and not self.user:
username: str = f"{self.name.lower()}.{self.surname.lower()}"
# Avoid username conflict with progressive number
base_username = username
counter: int = 1
while User.objects.filter(username=username).exists():
username = f"{base_username}{counter}"
counter += 1
# Create user
user: User = User.objects.create_user(
first_name=self.name.capitalize(),
last_name=self.surname.capitalize(),
username=username,
email=self.email,
password=self.default_password(),
is_staff=True # allows access to admin page
)
instructor_group, _ = Group.objects.get_or_create(name="InstructorGroup")
user.groups.add(instructor_group)
self.user = user
self.save()
def __str__(self):
if self.pk:
return f"{self.surname} {self.name[0]}."
else:
return "New Instructor"

View File

@@ -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 MissionTypes(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(
@@ -21,8 +24,8 @@ class MissionProfile(models.Model):
mtype = models.CharField( mtype = models.CharField(
null=False, null=False,
default=MissionType.PPL, default=MissionTypes.PPL,
choices=MissionType, choices=MissionTypes,
verbose_name="Mission Type" verbose_name="Mission Type"
) )
@@ -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,

View File

@@ -1,7 +1,10 @@
from django.db import models from django.db import models
from django.contrib.auth.models import User, Group from django.contrib.auth.models import User, Group
from phonenumber_field import modelfields
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(
@@ -10,12 +13,13 @@ class Student(models.Model):
email = models.EmailField( email = models.EmailField(
null=False, null=False,
db_index=True db_index=True,
unique=True
) )
phone = models.CharField( phone = modelfields.PhoneNumberField(
null=True, null=True,
max_length=16 unique=True
) )
name = models.CharField( name = models.CharField(
@@ -46,29 +50,42 @@ 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
if self.pk:
return f"{self.name.lower()[0]}{self.surname.lower()}{self.id % 10000}"
else:
return ""
def default_username(self) -> str:
if self.pk and self.user:
return self.user.username
else:
return ""
# Override save method to add user for login upon Student creation # 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.capitalize(),
last_name=self.surname, last_name=self.surname.capitalize(),
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")

View File

@@ -20,7 +20,7 @@ class WeekPreference(models.Model):
Student, Student,
null=False, null=False,
db_index=True, db_index=True,
on_delete=models.DO_NOTHING, on_delete=models.CASCADE,
verbose_name="Student Selection" verbose_name="Student Selection"
) )

View File

@@ -6,7 +6,7 @@ import sys
def main(): def main():
"""Run administrative tasks.""" """Run administrative tasks."""
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'techdb.settings') os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'cntmanage.settings')
try: try:
from django.core.management import execute_from_command_line from django.core.management import execute_from_command_line
except ImportError as exc: except ImportError as exc:

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]
@@ -98,14 +98,14 @@ Django = ">=1.11"
[[package]] [[package]]
name = "django-import-export" name = "django-import-export"
version = "4.3.13" version = "4.3.14"
description = "Django application and library for importing and exporting data with included admin integration." description = "Django application and library for importing and exporting data with included admin integration."
optional = false optional = false
python-versions = ">=3.9" python-versions = ">=3.9"
groups = ["main"] groups = ["main"]
files = [ files = [
{file = "django_import_export-4.3.13-py3-none-any.whl", hash = "sha256:197e6a534948f3dcaeb395536e848a3fa267b9620bed5899314736d321fc6fc4"}, {file = "django_import_export-4.3.14-py3-none-any.whl", hash = "sha256:ce6484fa082a1cdb2bf4e0b60276d3e2a7f39f74c20ae663b2f8eebb54141a58"},
{file = "django_import_export-4.3.13.tar.gz", hash = "sha256:a0ebff68b470e578d1c05da83d7cb7545be1c42e1531720f49d2d9ec9a26f5bb"}, {file = "django_import_export-4.3.14.tar.gz", hash = "sha256:224c7d909fec607378bc58271db38b9c6065982306aa644d26a529fcde64869e"},
] ]
[package.dependencies] [package.dependencies]
@@ -143,6 +143,41 @@ 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-phonenumber-field"
version = "8.4.0"
description = "An international phone number field for django models."
optional = false
python-versions = ">=3.10"
groups = ["main"]
files = [
{file = "django_phonenumber_field-8.4.0-py3-none-any.whl", hash = "sha256:7a1cb3a6456edb54d879f11ffa0acb227ded08c93b587035d0f28093f0e46511"},
{file = "django_phonenumber_field-8.4.0.tar.gz", hash = "sha256:2b83e843dac35eec6a69880a166487235b737a71a1e38c9a52e5ad67d6996083"},
]
[package.dependencies]
Django = ">=4.2"
phonenumberslite = {version = ">=7.0.2", optional = true, markers = "extra == \"phonenumberslite\""}
[package.extras]
phonenumbers = ["phonenumbers (>=7.0.2)"]
phonenumberslite = ["phonenumberslite (>=7.0.2)"]
[[package]]
name = "django-polymorphic"
version = "4.1.0"
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"
@@ -170,6 +205,18 @@ files = [
[package.dependencies] [package.dependencies]
et-xmlfile = "*" et-xmlfile = "*"
[[package]]
name = "phonenumberslite"
version = "9.0.19"
description = "Python version of Google's common library for parsing, formatting, storing and validating international phone numbers."
optional = false
python-versions = "*"
groups = ["main"]
files = [
{file = "phonenumberslite-9.0.19-py2.py3-none-any.whl", hash = "sha256:92a2426808e7d40b4acf36c97dcc436747807419c5dbc035330df28c13d41c0f"},
{file = "phonenumberslite-9.0.19.tar.gz", hash = "sha256:3794fcec9d2a6510a806187de750853c73ea5dabaac4ecd7fa36e79f869b3c2e"},
]
[[package]] [[package]]
name = "pillow" name = "pillow"
version = "12.0.0" version = "12.0.0"
@@ -370,18 +417,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 +468,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 = "5147211bd07992aff3915544175c8d95d77511b9d42273d17c4452fbef9299eb"

View File

@@ -1,10 +1,12 @@
[tool.poetry] [tool.poetry]
name = "techdb" name = "cntmanage"
version = "0.1.0" version = "0.2.0"
description = "CantorAir Tech Organized Parts Storage" packages = [{include = "flightslot"}, {include = "cntmanage"}]
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"
@@ -15,6 +17,8 @@ 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"
django-phonenumber-field = {extras = ["phonenumberslite"], version = "^8.4.0"}
[build-system] [build-system]

View File

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

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 %}

View File

@@ -1,21 +0,0 @@
from django.contrib import admin
from .models.box import Box, BoxAdmin
from .models.part import Part, PartAdmin
from .models.vendor import Vendor, VendorAdmin
from .models.formone import FormOne,FormOneAdmin
from .models.customer import Customer, CustomerAdmin
from .models.plane import Plane, PlaneAdmin
from .models.workorder import Workorder, WorkorderAdmin
from .models.operator import Operator, OperatorAdmin
from .models.movimag import Movimag, MovimagAdmin
# Register your models here.
admin.site.register(Box, BoxAdmin)
admin.site.register(Part, PartAdmin)
admin.site.register(Vendor, VendorAdmin)
admin.site.register(FormOne,FormOneAdmin)
admin.site.register(Customer, CustomerAdmin)
admin.site.register(Plane, PlaneAdmin)
admin.site.register(Workorder, WorkorderAdmin)
admin.site.register(Operator, OperatorAdmin)
admin.site.register(Movimag, MovimagAdmin)

View File

@@ -1,6 +0,0 @@
from django.apps import AppConfig
class CatopsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'catops'

View File

@@ -1,20 +0,0 @@
# Generated by Django 5.1.2 on 2024-10-18 09:46
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Parts',
fields=[
('id', models.UUIDField(primary_key=True, serialize=False)),
],
),
]

View File

@@ -1,17 +0,0 @@
# Generated by Django 5.1.2 on 2024-10-18 09:55
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('catops', '0001_initial'),
]
operations = [
migrations.RenameModel(
old_name='Parts',
new_name='Part',
),
]

View File

@@ -1,101 +0,0 @@
# Generated by Django 5.1.2 on 2024-10-18 15:45
import django.db.models.deletion
import django.db.models.functions.datetime
import uuid
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('catops', '0002_rename_parts_part'),
]
operations = [
migrations.CreateModel(
name='FormOne',
fields=[
('reg_date', models.DateTimeField(auto_created=True, db_default=django.db.models.functions.datetime.Now(), editable=False)),
('id', models.BigAutoField(primary_key=True, serialize=False)),
('external_id', models.CharField(db_index=True, null=True)),
('doc_path', models.FilePathField(null=True)),
('active', models.BooleanField(db_default=True)),
],
),
migrations.AddField(
model_name='part',
name='active',
field=models.BooleanField(db_default=True),
),
migrations.AddField(
model_name='part',
name='expiry_time',
field=models.DurationField(null=True),
),
migrations.AddField(
model_name='part',
name='last_stock',
field=models.DateField(auto_created=True, db_default=django.db.models.functions.datetime.Now(), editable=False),
),
migrations.AddField(
model_name='part',
name='min_reorder',
field=models.PositiveIntegerField(null=True),
),
migrations.AddField(
model_name='part',
name='part_number',
field=models.CharField(db_index=True, default='aaa', max_length=64, unique=True),
preserve_default=False,
),
migrations.AddField(
model_name='part',
name='properties',
field=models.JSONField(null=True),
),
migrations.AddField(
model_name='part',
name='quantity',
field=models.PositiveIntegerField(default=0),
),
migrations.AddField(
model_name='part',
name='reg_date',
field=models.DateTimeField(auto_created=True, db_default=django.db.models.functions.datetime.Now(), editable=False),
),
migrations.AddField(
model_name='part',
name='serial_number',
field=models.CharField(db_index=True, max_length=64, null=True, unique=True),
),
migrations.AddField(
model_name='part',
name='unit',
field=models.CharField(choices=[('QTY', 'Quantity'), ('LT', 'Liters'), ('USG', 'US Gallons'), ('QTS', 'US Quarters'), ('KG', 'Kilograms'), ('LBS', 'Pounds')], default='QTY'),
),
migrations.AlterField(
model_name='part',
name='id',
field=models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False),
),
migrations.CreateModel(
name='Box',
fields=[
('reg_date', models.DateTimeField(auto_created=True, db_default=django.db.models.functions.datetime.Now(), editable=False)),
('id', models.UUIDField(db_index=True, default=uuid.uuid1, editable=False, primary_key=True, serialize=False)),
('loc_room', models.CharField(choices=[('ST1', 'Magazzino'), ('ST2', 'Deposito Esterno')], default='ST1', max_length=3)),
('loc_x', models.CharField(max_length=4)),
('loc_y', models.CharField(max_length=4)),
('loc_z', models.CharField(max_length=4)),
('active', models.BooleanField(db_default=True)),
('label_printed', models.BooleanField(db_default=False)),
('part', models.OneToOneField(null=True, on_delete=django.db.models.deletion.CASCADE, to='catops.part')),
],
),
migrations.AddField(
model_name='part',
name='form_one',
field=models.OneToOneField(null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='catops.formone'),
),
]

View File

@@ -1,22 +0,0 @@
# Generated by Django 5.1.2 on 2024-10-18 15:45
import django.db.models.functions.datetime
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('catops', '0003_formone_part_active_part_expiry_time_part_last_stock_and_more'),
]
operations = [
migrations.CreateModel(
name='Vendor',
fields=[
('reg_date', models.DateTimeField(auto_created=True, db_default=django.db.models.functions.datetime.Now(), editable=False)),
('id', models.UUIDField(primary_key=True, serialize=False)),
('active', models.BooleanField(db_default=True)),
],
),
]

View File

@@ -1,26 +0,0 @@
# Generated by Django 5.1.2 on 2024-10-18 15:45
import django.db.models.functions.datetime
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('catops', '0004_vendor'),
]
operations = [
migrations.CreateModel(
name='Customer',
fields=[
('reg_date', models.DateTimeField(auto_created=True, db_default=django.db.models.functions.datetime.Now(), editable=False)),
('id', models.BigAutoField(primary_key=True, serialize=False)),
('name', models.CharField(max_length=32)),
('surname', models.CharField(max_length=32)),
('external_id', models.CharField(db_index=True, null=True)),
('properties', models.JSONField(null=True)),
('active', models.BooleanField(db_default=True)),
],
),
]

View File

@@ -1,30 +0,0 @@
# Generated by Django 5.1.2 on 2024-10-18 15:46
import django.db.models.deletion
import django.db.models.functions.datetime
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('catops', '0005_customer'),
]
operations = [
migrations.CreateModel(
name='Plane',
fields=[
('reg_date', models.DateTimeField(auto_created=True, db_default=django.db.models.functions.datetime.Now(), editable=False)),
('id', models.BigAutoField(primary_key=True, serialize=False)),
('tail', models.CharField(max_length=6)),
('manufacturer', models.CharField(max_length=32)),
('model', models.CharField(max_length=32)),
('chassis_num', models.CharField(max_length=32)),
('external_id', models.CharField(db_index=True, null=True)),
('properties', models.JSONField(null=True)),
('active', models.BooleanField(db_default=True)),
('customer', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, to='catops.customer')),
],
),
]

View File

@@ -1,41 +0,0 @@
# Generated by Django 5.1.2 on 2024-10-18 15:47
import django.db.models.deletion
import django.db.models.functions.datetime
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('catops', '0006_plane'),
]
operations = [
migrations.CreateModel(
name='Operator',
fields=[
('reg_date', models.DateTimeField(auto_created=True, db_default=django.db.models.functions.datetime.Now(), editable=False)),
('id', models.BigAutoField(primary_key=True, serialize=False)),
('name', models.CharField(max_length=32)),
('surname', models.CharField(max_length=32)),
('external_id', models.CharField(db_index=True, null=True)),
('properties', models.JSONField(null=True)),
('active', models.BooleanField(db_default=True)),
],
),
migrations.CreateModel(
name='Workorder',
fields=[
('reg_date', models.DateTimeField(auto_created=True, db_default=django.db.models.functions.datetime.Now(), editable=False)),
('id', models.BigAutoField(primary_key=True, serialize=False)),
('date_begin', models.DateTimeField(db_default=django.db.models.functions.datetime.Now())),
('date_end', models.DateTimeField()),
('external_id', models.CharField(db_index=True, max_length=32, null=True)),
('properties', models.JSONField(null=True)),
('active', models.BooleanField(db_default=True)),
('operator', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, to='catops.operator')),
('plane', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, to='catops.plane')),
],
),
]

View File

@@ -1,26 +0,0 @@
# Generated by Django 5.1.2 on 2024-10-18 15:47
import django.db.models.deletion
import django.db.models.functions.datetime
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('catops', '0007_operator_workorder'),
]
operations = [
migrations.CreateModel(
name='Movimag',
fields=[
('mov_datetime', models.DateTimeField(auto_created=True, db_default=django.db.models.functions.datetime.Now(), editable=False)),
('id', models.BigAutoField(primary_key=True, serialize=False)),
('direction', models.CharField(choices=[('LOAD', 'Load'), ('UNLOAD', 'UnLoad')])),
('operator', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, to='catops.operator')),
('part', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, to='catops.part')),
('workorder', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, to='catops.workorder')),
],
),
]

View File

@@ -1,19 +0,0 @@
from django.db import models
from django.utils.translation import gettext_lazy as _
class RoomLocation(models.TextChoices):
STORAGE_1 = "ST1", _("Magazzino")
STORAGE_2 = "ST2", _("Deposito Esterno")
class Units(models.TextChoices):
NUM = "QTY", _("Quantity")
LT = "LT", _("Liters")
USG = "USG", _("US Gallons")
QTS = "QTS", _("US Quarters")
KG = "KG", _("Kilograms")
LBS = "LBS", _("Pounds")
class MoviDirection(models.TextChoices):
LOAD = "LOAD", _("Load")
UNLOAD = "UNLOAD", _("UnLoad")

View File

@@ -1,69 +0,0 @@
import uuid
from django.db import models
from django.contrib import admin
from django.db.models.functions import Now
from .part import Part
from ..misc.units import RoomLocation
# Box identifies a part storage location, it can be a drawer or a bag or a carbord box.
# It has a location and one or multiple parts inside it.
# Properties to visualize the Box model in the Django admin view
class BoxAdmin(admin.ModelAdmin):
list_display = ("id", "loc_room", "loc_x", "loc_y", "loc_z", "reg_date", "active")
list_filter = ["loc_room"]
class Box(models.Model):
id = models.UUIDField(
primary_key=True,
default=uuid.uuid1,
db_index=True,
editable=False,
serialize=str,
)
reg_date = models.DateTimeField(
auto_created=True,
editable=False,
db_default=Now(),
)
loc_room = models.CharField(
max_length=3,
choices=RoomLocation,
default=RoomLocation.STORAGE_1,
null=False,
)
loc_x = models.CharField(
max_length=4,
)
loc_y = models.CharField(
max_length=4,
)
loc_z = models.CharField(
max_length=4,
)
active = models.BooleanField(
db_default=True
)
label_printed = models.BooleanField(
db_default=False,
)
part = models.OneToOneField(
Part,
unique=True,
db_index=True,
swappable=True,
null=True,
on_delete=models.CASCADE,
)
def __str__(self):
return f"{str(self.id)}"

View File

@@ -1,39 +0,0 @@
from django.db import models
from django.contrib import admin
from django.db.models.functions import Now
class CustomerAdmin(admin.ModelAdmin):
list_display = ()
list_filter = []
class Customer(models.Model):
id = models.BigAutoField(
primary_key=True
)
reg_date = models.DateTimeField(
auto_created=True,
editable=False,
db_default=Now()
)
name = models.CharField(
max_length=32
)
surname = models.CharField(
max_length=32
)
external_id = models.CharField(
db_index=True,
null=True,
)
properties = models.JSONField(
null=True
)
active = models.BooleanField(
db_default=True
)

View File

@@ -1,31 +0,0 @@
from django.db import models
from django.contrib import admin
from django.db.models.functions import Now
class FormOneAdmin(admin.ModelAdmin):
list_display = ()
list_filter = []
class FormOne(models.Model):
id = models.BigAutoField(
primary_key=True
)
reg_date = models.DateTimeField(
auto_created=True,
editable=False,
db_default=Now()
)
external_id = models.CharField(
db_index=True,
null=True,
)
doc_path = models.FilePathField(
null=True
)
active = models.BooleanField(
db_default=True
)

View File

@@ -1,43 +0,0 @@
from django.db import models
from django.contrib import admin
from django.db.models.functions import Now
from .part import Part
from .operator import Operator
from .workorder import Workorder
from ..misc import units
class MovimagAdmin(admin.ModelAdmin):
list_display = ()
list_filter = []
class Movimag(models.Model):
id = models.BigAutoField(
primary_key=True
)
mov_datetime = models.DateTimeField(
auto_created=True,
editable=False,
db_default=Now()
)
part = models.ForeignKey(
Part,
on_delete=models.DO_NOTHING
)
workorder = models.ForeignKey(
Workorder,
on_delete=models.DO_NOTHING
)
operator = models.ForeignKey(
Operator,
on_delete=models.DO_NOTHING
)
direction = models.CharField(
null=False,
choices=units.MoviDirection
)

View File

@@ -1,39 +0,0 @@
from django.db import models
from django.contrib import admin
from django.db.models.functions import Now
class OperatorAdmin(admin.ModelAdmin):
list_display = ()
list_filter = ()
class Operator(models.Model):
id = models.BigAutoField(
primary_key=True
)
reg_date = models.DateTimeField(
auto_created=True,
editable=False,
db_default=Now()
)
name = models.CharField(
max_length=32
)
surname = models.CharField(
max_length=32
)
external_id = models.CharField(
db_index=True,
null=True,
)
properties = models.JSONField(
null=True
)
active = models.BooleanField(
db_default=True
)

View File

@@ -1,87 +0,0 @@
import uuid
from django.db import models
from django.contrib import admin
from django.db.models.functions import Now
from django.utils.translation import gettext_lazy as _
from ..misc.units import Units
from .formone import FormOne
# Part identifies a part with its part number and associated properties:
# such as torage quantity, min reorder quantities and expiry date.
class PartAdmin(admin.ModelAdmin):
list_display = ()
list_filter = ()
class Part(models.Model):
id = models.UUIDField(
primary_key=True,
default=uuid.uuid4,
db_index=True,
editable=False,
serialize=str
)
reg_date = models.DateTimeField(
auto_created=True,
editable=False,
db_default=Now()
)
part_number = models.CharField(
db_index=True,
max_length=64,
null=False,
unique=True
)
serial_number = models.CharField(
db_index=True,
max_length=64,
null=True,
unique=True
)
quantity = models.PositiveIntegerField(
null=False,
default=0
)
unit = models.CharField(
choices=Units,
default=Units.NUM,
null=False
)
last_stock = models.DateField(
null=False,
auto_created=True,
editable=False,
db_default=Now()
)
expiry_time = models.DurationField(
null=True,
)
min_reorder = models.PositiveIntegerField(
null=True
)
properties = models.JSONField(
null=True
)
form_one = models.OneToOneField(
FormOne,
on_delete=models.DO_NOTHING,
db_index=True,
unique=True,
null=True
)
active = models.BooleanField(
db_default=True
)

View File

@@ -1,59 +0,0 @@
from django.db import models
from django.contrib import admin
from django.db.models.functions import Now
from .customer import Customer
class PlaneAdmin(admin.ModelAdmin):
list_display = ()
list_filter = []
class Plane(models.Model):
id = models.BigAutoField(
primary_key=True
)
reg_date = models.DateTimeField(
auto_created=True,
editable=False,
db_default=Now()
)
tail = models.CharField(
max_length=6,
null=False
)
manufacturer = models.CharField(
max_length=32,
null=False
)
model = models.CharField(
max_length=32,
null=False
)
chassis_num = models.CharField(
max_length=32,
null=False
)
external_id = models.CharField(
db_index=True,
null=True,
)
customer = models.ForeignKey(
Customer,
on_delete=models.DO_NOTHING
)
properties = models.JSONField(
null=True
)
active = models.BooleanField(
db_default=True
)

View File

@@ -1,23 +0,0 @@
import uuid
from django.db import models
from django.contrib import admin
from django.db.models.functions import Now
class VendorAdmin(admin.ModelAdmin):
list_display = ()
list_filter = ()
class Vendor(models.Model):
id = models.UUIDField(primary_key=True)
reg_date = models.DateTimeField(
auto_created=True,
editable=False,
db_default=Now()
)
active = models.BooleanField(
db_default=True
)

View File

@@ -1,54 +0,0 @@
from django.db import models
from django.contrib import admin
from django.db.models.functions import Now
from .plane import Plane
from .operator import Operator
class WorkorderAdmin(admin.ModelAdmin):
list_display = ()
list_filter = []
class Workorder(models.Model):
id = models.BigAutoField(
primary_key=True
)
reg_date = models.DateTimeField(
auto_created=True,
editable=False,
db_default=Now()
)
date_begin = models.DateTimeField(
null=False,
db_default=Now()
)
date_end = models.DateTimeField(
null=False
)
external_id = models.CharField(
max_length=32,
db_index=True,
null=True,
)
plane = models.ForeignKey(
Plane,
on_delete=models.DO_NOTHING
)
operator = models.ForeignKey(
Operator,
on_delete=models.DO_NOTHING
)
properties = models.JSONField(
null=True
)
active = models.BooleanField(
db_default=True
)

View File

@@ -1,21 +0,0 @@
# Use postgres/example user/password credentials
version: '3.9'
services:
postgresql:
image: postgres:17.0
container_name: tech-postgresql
restart: always
# set shared memory limit when using docker-compose
shm_size: 128mb
volumes:
- /mnt/data/postgresql:/var/lib/postgresql/data
ports:
- 5432:5432
environment:
POSTGRES_USER: tech
POSTGRES_PASSWORD: tech
POSTGRED_DB: techstorage
PGDATA: /var/lib/postgresql/data

View File

@@ -1,16 +0,0 @@
from django.contrib import admin
from .models.courses import Course
from .models.students import Student
from .models.missions import MissionProfile
from .models.weekpref import WeekPreference
from .admins.course_adm import CourseAdmin
from .admins.student_adm import StudentAdmin
from .admins.mission_adm import MissionProfileAdmin
from .admins.weekpred_adm import WeekPreferenceAdmin
admin.site.register(Course, CourseAdmin)
admin.site.register(MissionProfile, MissionProfileAdmin)
admin.site.register(Student, StudentAdmin)
admin.site.register(WeekPreference, WeekPreferenceAdmin)

Some files were not shown because too many files have changed in this diff Show More