30 Commits

Author SHA1 Message Date
d79f30e96c Merge remote-tracking branch 'origin/flightslot' into flightslot 2025-12-10 11:44:34 +01:00
da8c8db0d2 added email_sent field to student model, do not send mail twice if not needed 2025-12-10 11:43:04 +01:00
99d24583d6 week preference insertion date 2025-12-10 11:26:40 +01:00
aa8e4e761f fixed deploy script 2025-12-10 10:09:44 +00:00
46e6749fd5 improved xlsx formatting 2025-12-10 11:05:22 +01:00
1e17a8fe2d Merge pull request 'email' (#4) from email into flightslot
Reviewed-on: #4
2025-12-09 10:48:51 +01:00
a9587776e8 Fixed static and template email files for container 2025-12-09 10:45:25 +01:00
ec7ae4a48e fix message 2025-12-06 19:14:10 +01:00
c93171dbc3 fix endfile spacing 2025-12-06 19:10:46 +01:00
42417927c9 first version of send mail with dummy backend and confirm action 2025-12-06 18:52:03 +01:00
7c7d0e1e62 fake commit 2025-12-06 15:03:11 +01:00
e41eea8527 fix hb inline for admin 2025-12-05 18:07:49 +01:00
369c3b5e19 deploy script 2025-12-05 18:00:27 +01:00
aeb3aa30ce Show Hour building only at ATPL or DL (volo) students 2025-12-05 17:58:51 +01:00
84cf41535c No --- in notes 2025-12-04 15:07:23 +01:00
cdf7e7c677 Show only mission correct for student phase, add only one preference per week 2025-12-04 14:51:52 +01:00
b8f4331d3b Improved xlsx formatting 2025-12-04 13:05:34 +01:00
303359c921 refix merge 2025-12-03 10:27:54 +01:00
ec8373877b fix row merge 2025-12-03 10:21:50 +01:00
e7e47152ed fixed excel formatting 2025-12-03 10:10:00 +01:00
1eb11f33fc fix phone export 2025-12-03 09:23:27 +01:00
e417268991 Merge pull request 'instructor-class' (#3) from instructor-class into flightslot
Reviewed-on: #3
2025-12-02 12:33:41 +01:00
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
42 changed files with 1483 additions and 167 deletions

View File

@@ -31,6 +31,7 @@ ALLOWED_HOSTS = []
# Application definition # Application definition
INSTALLED_APPS = [ INSTALLED_APPS = [
'admin_confirm',
'django.contrib.admin', 'django.contrib.admin',
'django.contrib.auth', 'django.contrib.auth',
'django.contrib.contenttypes', 'django.contrib.contenttypes',
@@ -43,7 +44,8 @@ INSTALLED_APPS = [
'colorfield', 'colorfield',
'import_export', 'import_export',
'django_admin_action_forms', 'django_admin_action_forms',
'polymorphic' 'polymorphic',
"phonenumber_field",
] ]
# Import Export plugin settings # Import Export plugin settings
@@ -142,3 +144,14 @@ STATIC_URL = 'static/'
# https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field # https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
#### Send Email for user password communication ####
# https://docs.djangoproject.com/en/5.2/topics/email/
EMAIL_HOST = "smtp.gmail.com"
EMAIL_HOST_USER = "ema.trabattoni@gmail.com"
EMAIL_HOST_PASSWORD = "okorjsenzptdiwcr"
EMAIL_PORT = 587
EMAIL_USE_TLS = True
EMAIL_BACKEND = "django.core.mail.backends.filebased.EmailBackend"
EMAIL_FILE_PATH = "/mnt/d/Test/flightslot-mail" # change this to a proper location

View File

@@ -35,6 +35,7 @@ CSRF_TRUSTED_ORIGINS = ["http://localhost:8000", "http://127.0.0.1:8000", "http:
# Application definition # Application definition
INSTALLED_APPS = [ INSTALLED_APPS = [
'admin_confirm',
'django.contrib.admin', 'django.contrib.admin',
'django.contrib.auth', 'django.contrib.auth',
'django.contrib.contenttypes', 'django.contrib.contenttypes',
@@ -47,7 +48,8 @@ INSTALLED_APPS = [
'colorfield', 'colorfield',
'import_export', 'import_export',
'django_admin_action_forms', 'django_admin_action_forms',
'polymorphic' 'polymorphic',
"phonenumber_field",
] ]
# Import Export plugin settings # Import Export plugin settings
@@ -73,7 +75,7 @@ ROOT_URLCONF = 'cntmanage.urls'
TEMPLATES = [ TEMPLATES = [
{ {
'BACKEND': 'django.template.backends.django.DjangoTemplates', 'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': ['/var/www/templates'], 'DIRS': ['/var/www/templates', '/app/templates'],
'APP_DIRS': True, 'APP_DIRS': True,
'OPTIONS': { 'OPTIONS': {
'context_processors': [ 'context_processors': [
@@ -137,9 +139,23 @@ USE_TZ = True
# https://docs.djangoproject.com/en/5.1/howto/static-files/ # https://docs.djangoproject.com/en/5.1/howto/static-files/
STATIC_URL = "static/" STATIC_URL = "static/"
STATIC_ROOT = "/var/www/static/" STATIC_ROOT = "/var/www/static/"
STATICFILES_DIRS = [
"/app/static/"
]
STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage" STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"
# Default primary key field type # Default primary key field type
# https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field # https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
#### Send Email for user password communication ####
# https://docs.djangoproject.com/en/5.2/topics/email/
EMAIL_HOST = "smtp.gmail.com"
EMAIL_HOST_USER = "ema.trabattoni@gmail.com"
EMAIL_HOST_PASSWORD = "okorjsenzptdiwcr"
EMAIL_PORT = 587
EMAIL_USE_TLS = True
# Use dummy backed for testing
EMAIL_BACKEND = "django.core.mail.backends.dummy.EmailBackend"

View File

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

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

@@ -0,0 +1,5 @@
#!/bin/bash
git pull
./build.sh
docker compose up -d

View File

@@ -19,11 +19,12 @@ FROM python:3.12-slim AS deploy
WORKDIR /app WORKDIR /app
# Copy application custom static files # Copy application custom static files
RUN mkdir -p static RUN mkdir -p static
COPY ./static/cantorair.jpg ./static COPY ./static/* ./static
COPY ./static/cantorair_blue.jpg ./static
# Copy application custom templates for admin page # Copy application custom templates for admin page
RUN mkdir -p /templates/admin RUN mkdir -p /templates/admin
RUN mkdir -p /templates/email
COPY ./templates/admin/* ./templates/admin/ COPY ./templates/admin/* ./templates/admin/
COPY ./templates/email/* ./templates/email/
# Copy and install application wheel package # Copy and install application wheel package
COPY --from=builder /build/dist/*.whl ./ COPY --from=builder /build/dist/*.whl ./
RUN pip install --no-cache-dir *.whl RUN pip install --no-cache-dir *.whl

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

@@ -4,7 +4,9 @@ from django.db.models.query import QuerySet
from openpyxl import Workbook 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 openpyxl.worksheet.page import PageMargins
from ..models.courses import CourseTypes
from ..models.missions import Training from ..models.missions import Training
from ..models.weekpref import WeekPreference from ..models.weekpref import WeekPreference
from ..models.hourbuildings import HourBuilding, HourBuildingLegFlight, HourBuildingLegStop, HourBuildingLegBase from ..models.hourbuildings import HourBuilding, HourBuildingLegFlight, HourBuildingLegStop, HourBuildingLegBase
@@ -12,18 +14,34 @@ from ..models.hourbuildings import HourBuilding, HourBuildingLegFlight, HourBuil
from datetime import date, datetime from datetime import date, datetime
from typing import List from typing import List
# Enable cell merging for equal mission
MERGE: bool = False
PALETTE : List[str] = [
"#E6F2FF", # azzurro chiarissimo
"#E5FBF8", # verde acqua molto chiaro
"#ECFBE1", # verde chiarissimo
"#FFFBD1", # giallo molto chiaro
"#FFF1D6", # giallo-arancio molto chiaro
"#FFE3DD", # rosa pesca molto chiaro
"#F3E6FA", # lilla chiarissimo
]
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():
raise Exception("Empty queryset") raise Exception("Empty queryset")
# Init Variables # Init Variables
year = date.today().year today = date.today()
year = today.year
month = today.month
day = today.day
week = queryset.first().week if queryset.first() else date.today().isocalendar().week week = queryset.first().week if queryset.first() else date.today().isocalendar().week
weeks = queryset.order_by("week").distinct("week").all() weeks = queryset.order_by("week").distinct("week").all()
# Prepare export filename and http content # Prepare export filename and http content
filename = f"{year}_week{'+'.join([str(w.week) for w in weeks])}_export.xlsx" filename = f"{year}{month}{day}_week_{'+'.join([str(w.week) for w in weeks])}_export.xlsx"
response = HttpResponse(content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet') response = HttpResponse(content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
response['Content-Disposition'] = f'attachment; filename="{filename}"' response['Content-Disposition'] = f'attachment; filename="{filename}"'
@@ -33,10 +51,6 @@ def export_selected(request: HttpRequest, queryset: QuerySet[WeekPreference]) ->
if not ws: if not ws:
raise Exception("Export: cannot select active workbook") raise Exception("Export: cannot select active workbook")
ws.title = f"Week Preferences" ws.title = f"Week Preferences"
ws.page_setup.orientation = ws.ORIENTATION_LANDSCAPE
ws.page_setup.paperSize = ws.PAPERSIZE_A3
ws.page_setup.fitToHeight = 0
ws.page_setup.fitToWidth = 1
# 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)]
@@ -57,7 +71,7 @@ def export_selected(request: HttpRequest, queryset: QuerySet[WeekPreference]) ->
center = Alignment(horizontal="center", vertical="center", wrapText=True) center = Alignment(horizontal="center", vertical="center", wrapText=True)
# Cell styles # Cell styles
border_thick: Side = Side(style='thick', color='000000') border_thick: Side = Side(style='medium', color='000000')
border_thin: Side = Side(style='thin', color='000000', border_style='dashed') 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_bottom_thin: Border = Border(bottom=border_thin)
@@ -67,85 +81,97 @@ def export_selected(request: HttpRequest, queryset: QuerySet[WeekPreference]) ->
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
head_size: int = len(headers)
for col, h in enumerate(headers, start=1): for col, h in enumerate(headers, start=1):
cell = ws.cell(row=1, column=col, value=h) cell = ws.cell(row=1, column=col, value=h)
cell.fill = header_fill cell.fill = header_fill
cell.font = bold_white cell.font = bold_white
cell.alignment = center cell.alignment = center
match col:
case int(1):
cell.border = Border(top=border_thick, bottom=border_thick, left=border_thick)
case int(head_size):
cell.border = Border(top=border_thick, bottom=border_thick, right=border_thick)
case _:
cell.border = Border(top=border_thick, bottom=border_thick)
### Start of Student Loop ### ### Start of Student Loop ###
# Fill worksheet with EVERY training and hb for every student # Fill worksheet with EVERY training and hb for every student
# 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("week", "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 = 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_type: str
student_course_number: str student_course_number: str
student_course_ac: str student_course_ac: str = f"({'/'.join(t.type for t in q.student.aircrafts.distinct("type").all())})"
if q.student.course: if q.student.course:
student_course_type = q.student.course.ctype student_course_type = q.student.course.ctype
student_course_number = str(q.student.course.cnumber) 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 = [ student_data = [
f"{q.student.surname} {q.student.name}\n{student_course_ac}", "\n".join([f"{q.student.surname} {q.student.name}", student_course_ac]),
f"{student_course_type}-{student_course_number}" 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"]
# Fill Training mission rows # Fill Training mission rows
mission_name: str mission_name: List[str]
mission_days: List[str] mission_days: List[str]
mission_notes: str mission_notes: str
mission_data: List[List[str]] = [] mission_data: List[List[str]] = []
for t in Training.objects.filter(weekpref = q.id): for t in Training.objects.filter(weekpref = q.id):
if not t.mission: if not t.mission:
raise Exception("No Training Mission Assigned") raise Exception("No Training Mission Assigned")
mission_name = f"{t.mission.mtype}-{t.mission.mnum}" mission_notes = t.notes.strip().capitalize() if t.notes else ""
mission_name = [f"{t.mission.mtype}-{t.mission.mnum}"]
if q.student.course and q.student.course.ctype == CourseTypes.PPL: # add course aircraft only for PPL students
mission_name.append(student_course_ac)
mission_name_joined: str = "\n".join(mission_name)
mission_days = [ mission_days = [
mission_name if t.monday else "", mission_name_joined if t.monday else "",
mission_name if t.tuesday else "", mission_name_joined if t.tuesday else "",
mission_name if t.wednesday else "", mission_name_joined if t.wednesday else "",
mission_name if t.thursday else "", mission_name_joined if t.thursday else "",
mission_name if t.friday else "", mission_name_joined if t.friday else "",
mission_name if t.saturday else "", mission_name_joined if t.saturday else "",
mission_name if t.sunday else "" mission_name_joined if t.sunday else ""
] ]
mission_notes = t.notes if t.notes else "--"
mission_data.append([str(q.week), *student_data, *mission_days, mission_notes, student_phone, student_email, ]) 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: List[str]
hb_days: List[str] hb_days: List[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 = ["HB", f"({h.aircraft})"]
hb_days = [
hb_name if h.monday else "",
hb_name if h.tuesday else "",
hb_name if h.wednesday else "",
hb_name if h.thursday else "",
hb_name if h.friday else "",
hb_name if h.saturday else "",
hb_name if h.sunday else ""
]
hb_notes: List[str] = [f"{h.notes}", "---"] if h.notes else []
hb_legs_all = HourBuildingLegBase.objects.filter(hb_id = h.id) hb_legs_all = HourBuildingLegBase.objects.filter(hb_id = h.id)
for hh in hb_legs_all: for hh in hb_legs_all:
time_str: str = ':'.join(str(hh.time).split(':')[:2]) # keep only hours and minutes time_str: str = ':'.join(str(hh.time).split(':')[:2]) # keep only hours and minutes
if isinstance(hh, HourBuildingLegFlight): if isinstance(hh, HourBuildingLegFlight):
hb_notes.append(f"{hh.departure} -> {hh.destination} [{time_str}]{f' / PAX: {hh.pax.capitalize()}' if hh.pax else ''}") hb_pax: str | None = " ".join(x.capitalize() for x in hh.pax.split()) if hh.pax else None
hb_name.append(f"{hh.departure} -> {hh.destination} [{time_str}]{f' / Pax: {hb_pax}' if hb_pax else ''}")
elif isinstance(hh, HourBuildingLegStop): elif isinstance(hh, HourBuildingLegStop):
hb_notes.append(f"STOP [{time_str}] {"Refuel" if hh.refuel else ""}" ) hb_name.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]) hb_name_joined: str = "\n".join(hb_name)
hb_days = [
hb_name_joined if h.monday else "",
hb_name_joined if h.tuesday else "",
hb_name_joined if h.wednesday else "",
hb_name_joined if h.thursday else "",
hb_name_joined if h.friday else "",
hb_name_joined if h.saturday else "",
hb_name_joined if h.sunday else ""
]
hb_notes: str = h.notes.strip().capitalize() if h.notes else ""
hb_data.append([str(q.week), *student_data, *hb_days, 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 row_content in all_data: for ri, row_content in enumerate(all_data):
for c, cell_content in enumerate(row_content, start=1): for c, cell_content in enumerate(row_content, start=1):
cell = ws.cell(row = row + row_offset, column = c, value = cell_content) cell = ws.cell(row = row + row_offset, column = c, value = cell_content)
cell.alignment = center cell.alignment = center
@@ -160,25 +186,26 @@ def export_selected(request: HttpRequest, queryset: QuerySet[WeekPreference]) ->
elif c > course_index and c <= note_index: elif c > course_index and c <= note_index:
cell.border = border_bottom_thin + border_right_thin cell.border = border_bottom_thin + border_right_thin
# Fill mix cells if the cell is not empty # Fill mix cells if the cell is not empty
if c > course_index and c < note_index: if c > course_index and c <= note_index:
if len(cell_content): if len(cell_content):
cell.fill = PatternFill('solid', fgColor="f0f0f0") cell.fill = PatternFill('solid', fgColor=PALETTE[ri % len(PALETTE)].lstrip("#").lower())
prev_cell_val: str = row_content[0] if MERGE:
merge_start: bool = False prev_cell_val: str = row_content[0]
merge_col_start: int = 1 merge_start: bool = False
for c, cell_content in enumerate(row_content, start=1): merge_col_start: int = 1
# Merge cells in the row for c, cell_content in enumerate(row_content, start=1):
if cell_content == prev_cell_val and not merge_start: # Merge cells in the row
merge_start = True if cell_content == prev_cell_val and not merge_start:
merge_col_start = c-1 # start merge from previous column merge_start = True
elif cell_content != prev_cell_val and merge_start: merge_col_start = c-1 # start merge from previous column
merge_start = False elif cell_content != prev_cell_val and merge_start:
ws.merge_cells(start_row=row+row_offset, merge_start = False
end_row=row+row_offset, ws.merge_cells(start_row=row+row_offset,
start_column=max(merge_col_start,1), end_row=row+row_offset,
end_column=max(c-1,1)) # end merge to previous column start_column=max(merge_col_start,1),
prev_cell_val = cell_content end_column=max(c-1,1)) # end merge to previous column
prev_cell_val = cell_content
# Incement row counter # Incement row counter
row_offset += 1 row_offset += 1
@@ -191,8 +218,7 @@ def export_selected(request: HttpRequest, queryset: QuerySet[WeekPreference]) ->
ws.cell(row=student_end, column=c).border = Border(bottom=border_thick, right=border_thin) 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 # And for last column also a vertical border all student high
if c == mail_index: if c == mail_index:
for row_content in range(student_start, student_end + 1): ws.cell(row=student_end, column=c).border = Border(bottom=border_thick, right=border_thick)
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
@@ -208,21 +234,34 @@ def export_selected(request: HttpRequest, queryset: QuerySet[WeekPreference]) ->
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)
# Keep the largest column # Keep the largest column
max_len: List[int] = []
for column_cells in ws.columns: for column_cells in ws.columns:
col_letter: str = "A"
max_len: List[int] = []
for cell in column_cells: for cell in column_cells:
cell_lines = str(cell.value).splitlines() cell_lines = str(cell.value).splitlines()
if len(cell_lines) == 0: if len(cell_lines) == 0:
continue continue
max_len.append(max([len(ll) for ll in cell_lines])) max_len.append(max([len(ll) for ll in cell_lines]))
length: int = max(max_len) length: int = max(max_len)
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)
ws.column_dimensions[col_letter].width = length + 2 ws.column_dimensions[col_letter].width = min(length + 2, 35)
### End of Student Loop ### ### End of Student Loop ###
# Set paper size and format
ws.page_setup.orientation = ws.ORIENTATION_LANDSCAPE
ws.page_setup.paperSize = ws.PAPERSIZE_A3
ws.page_setup.fitToHeight = 0
ws.page_setup.fitToWidth = 1
ws.print_options.horizontalCentered = True
ws.page_setup.fitToPage = True
ws.page_margins = PageMargins(
left=0.25, right=0.25,
top=0.75, bottom=0.75,
header=0.3, footer=0.3
)
ws.print_area = ws.calculate_dimension()
# Save document in HttpResponse # Save document in HttpResponse
wb.save(response) wb.save(response)

View File

@@ -0,0 +1,81 @@
from django.contrib.staticfiles import finders
from django.contrib import messages
from django.core.mail import EmailMultiAlternatives, get_connection
from django.http import HttpRequest
from django.db.models.query import QuerySet
from django.utils.safestring import SafeText
from django.template.loader import render_to_string
from ..models.students import Student
from smtplib import SMTPException
from email.mime.image import MIMEImage
from typing import List
def send_mail_password(request: HttpRequest, queryset: QuerySet[Student]) -> None:
img: MIMEImage | None = None
filename: str
candidates = finders.find("cantorair.png")
if not candidates:
messages.error(request=request, message="Cannot Load CantorAir Logo")
return
elif isinstance(candidates, list):
filename = candidates.pop()
else:
filename = candidates
with open(filename, "rb") as f:
img = MIMEImage(f.read())
img.add_header("Content-ID", "logo_image")
img.add_header("Content-Disposition", "inline", filename="cantorair.png")
# build mail list filling template
queryset = queryset.filter(mail_sent=False)
mails: List[EmailMultiAlternatives] = []
for student in queryset:
if not student.user or not student.email: # skip student if has not an associated user
continue
try:
username: str = student.user.username
password: str = student.default_password()
address: str = student.email
text_message: str = f"Cantor Air Flight Scheduler\nUsername:{username}\nPassword:{password}\n"
html_message: SafeText = render_to_string(
template_name="email/mail.html",
context={"username": username, "password": password}
)
mail: EmailMultiAlternatives = EmailMultiAlternatives(
subject="CantorAir Flight Scheduler 🛫",
from_email="ema.trabattoni@gmail.com",
body=text_message,
to = [ address ]
)
mail.attach(filename=img)
mail.attach_alternative(content=html_message, mimetype="text/html")
mails.append(mail)
student.mail_sent = True
student.save()
except Exception as e:
messages.error(request=request, message=f"General Error: {e}")
if len(mails) == 0:
messages.warning(request=request, message="No email will be sent")
return
# Open only one conenction and send mass email
try:
sent: int = 0
with get_connection() as conn:
sent = conn.send_messages(mails)
except SMTPException as e:
messages.error(request=request, message=f"Send Mail SMTP error: {e.strerror}")
except Exception as e:
messages.error(request=request, message=f"Send Mail General error: {e}")
else:
messages.success(request=request, message=f"Successfully sent {sent} messages")
return

View File

@@ -6,22 +6,28 @@ from .models.courses import Course
from .models.students import Student from .models.students import Student
from .models.missions import MissionProfile from .models.missions import MissionProfile
from .models.weekpref import WeekPreference from .models.weekpref import WeekPreference
from .models.instructors import Instructor
from .models.availabilities import Availability
from .admins.aircraft_adm import AircraftAdmin from .admins.aircraft_adm import AircraftAdmin
from .admins.course_adm import CourseAdmin from .admins.course_adm import CourseAdmin
from .admins.student_adm import StudentAdmin from .admins.student_adm import StudentAdmin
from .admins.mission_adm import MissionProfileAdmin from .admins.mission_adm import MissionProfileAdmin
from .admins.weekpref_adm import WeekPreferenceAdmin 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 django.contrib.admin import AdminSite
from os import environ from os import environ
# User website under /user/ URL ##################################
# User website under /user/ URL #
##################################
class FlightSlotUserSite(AdminSite): class FlightSlotUserSite(AdminSite):
site_header = "Flight Scheduler 🛫" site_header = "Flight Scheduler 🛫"
site_title = "Flight Scheduler 🛫" site_title = "Flight Scheduler 🛫"
index_title = "Welcome to CantorAir Flight Scheduler Portal" index_title = "Welcome to CantorAir Flight Scheduler Student Portal"
def get_app_list(self, request: HttpRequest, *args, **kwargs): def get_app_list(self, request: HttpRequest, *args, **kwargs):
app_list = super().get_app_list(request) app_list = super().get_app_list(request)
@@ -35,6 +41,27 @@ class FlightSlotUserSite(AdminSite):
flightslot_user = FlightSlotUserSite(name="user_site") flightslot_user = FlightSlotUserSite(name="user_site")
flightslot_user.register(WeekPreference, WeekPreferenceAdmin) 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(MissionProfile, MissionProfileAdmin)
flightslot_staff.register(Availability, AvailabilityAdmin)
flightslot_staff.register(Instructor, InstructorAdmin)
# Get version for debug purposes # Get version for debug purposes
ver: str = environ.get("VERSION", "dev") ver: str = environ.get("VERSION", "dev")
@@ -48,3 +75,5 @@ admin.site.register(Course, CourseAdmin)
admin.site.register(MissionProfile, MissionProfileAdmin) admin.site.register(MissionProfile, MissionProfileAdmin)
admin.site.register(Student, StudentAdmin) admin.site.register(Student, StudentAdmin)
admin.site.register(WeekPreference, WeekPreferenceAdmin) admin.site.register(WeekPreference, WeekPreferenceAdmin)
#admin.site.register(Instructor, InstructorAdmin)
#admin.site.register(Availability, AvailabilityAdmin)

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

@@ -1,12 +1,24 @@
from django.contrib import admin from django.contrib import admin
from django.db.models.query import QuerySet
from django.http import HttpRequest
from django.utils.safestring import SafeText from django.utils.safestring import SafeText
from ..models.students import Student
from ..models.courses import Course from ..models.courses import Course
from ..custom.colortag import course_color from ..custom.colortag import course_color
class CourseAdmin(admin.ModelAdmin): class CourseAdmin(admin.ModelAdmin):
list_display = ("ctype", "cnumber","color_display", "year") list_display = ("ctype", "cnumber","color_display", "course_students", "year")
list_filter = ("ctype", "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 # Dinamically add color_display property to show a colored dot
@admin.display(description="Color") @admin.display(description="Color")

View File

@@ -2,15 +2,15 @@ import nested_admin
from django import forms from django import forms
from django.db import models from django.db import models
from django.forms import TextInput, Textarea
from django.http import HttpRequest from django.http import HttpRequest
from django.forms import TextInput, Textarea
from durationwidget.widgets import TimeDurationWidget from durationwidget.widgets import TimeDurationWidget
from ..models.hourbuildings import HourBuilding, HourBuildingLegBase, HourBuildingLegFlight, HourBuildingLegStop from ..models.hourbuildings import HourBuilding, HourBuildingLegBase, HourBuildingLegFlight, HourBuildingLegStop
from ..models.weekpref import WeekPreference from ..models.weekpref import WeekPreference
from datetime import date from ..custom.student_permissions import has_edit_permission
class HourBuildingLegFlightForm(forms.ModelForm): class HourBuildingLegFlightForm(forms.ModelForm):
class Meta: class Meta:
@@ -74,11 +74,7 @@ class HourBuildingInLine(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) -> bool: 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) -> bool: def has_delete_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

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

@@ -1,7 +1,7 @@
from django.forms import ModelMultipleChoiceField from django.forms import TypedMultipleChoiceField
from django.contrib import admin, messages from django.contrib import admin, messages
from django.http import HttpRequest from django.http import HttpRequest
from django.db.models.query import QuerySet, Q from django.db.models.query import QuerySet
from django.utils.safestring import SafeText from django.utils.safestring import SafeText
from django_admin_action_forms import AdminActionFormsMixin, AdminActionForm, action_with_form from django_admin_action_forms import AdminActionFormsMixin, AdminActionForm, action_with_form
@@ -11,11 +11,13 @@ from import_export.admin import ImportMixin
from import_export.tmp_storages import CacheStorage from import_export.tmp_storages import CacheStorage
from import_export.resources import ModelResource from import_export.resources import ModelResource
from ..models.aircrafts import Aircraft from ..models.aircrafts import AircraftTypes
from ..models.missions import MissionProfile from ..models.missions import MissionProfile
from ..actions.assign_aircraft import assign_aircraft
from datetime import timedelta from datetime import timedelta
from typing import Any, Dict from typing import Any, Dict, List
# Resource Class for Student data import # Resource Class for Student data import
class MissionProfileResource(ModelResource): class MissionProfileResource(ModelResource):
@@ -40,30 +42,25 @@ class MissionProfileResource(ModelResource):
# Form class to assing aircrafts to students # Form class to assing aircrafts to students
class ChangeAircraftForm(AdminActionForm): class ChangeAircraftForm(AdminActionForm):
aircrafts = ModelMultipleChoiceField(queryset=Aircraft.objects.distinct('type').all()) aircrafts = TypedMultipleChoiceField(choices=AircraftTypes)
class MissionProfileAdmin(ImportMixin, AdminActionFormsMixin, admin.ModelAdmin): class MissionProfileAdmin(ImportMixin, AdminActionFormsMixin, admin.ModelAdmin):
list_display = ("mtype", "mnum", "assigned_aircrafts", "duration", "notes", ) list_display = ("mtype", "mnum", "assigned_aircrafts", "duration", "notes", )
list_filter = ("mtype", ) list_filter = ("mtype", )
actions = ("assign_aircraft", ) actions = ("assign_aircraft", )
resource_classes = [MissionProfileResource]
tmp_storage_class = CacheStorage tmp_storage_class = CacheStorage
skip_admin_log = True skip_admin_log = True
@action_with_form(ChangeAircraftForm, description="Assign Aircraft") def get_queryset(self, request: HttpRequest) -> QuerySet[MissionProfile]:
def assign_aircraft(self, request: HttpRequest, queryset: QuerySet[MissionProfile], data: Dict[str, QuerySet[Aircraft]]): return super().get_queryset(request).order_by("mtype", "mnum")
ac_types = [t.type for t in data["aircrafts"]]
ac_query: Q = Q() # Build an or query to select all aircrafts of the specified types @action_with_form(ChangeAircraftForm, description="Assign Aircraft Type")
for a in ac_types: def assign_aircraft(self, request: HttpRequest, queryset: QuerySet[MissionProfile], data: Dict[str, List[AircraftTypes]]):
ac_query |= Q(type=a) i: int
aircrafts: QuerySet[Aircraft] = Aircraft.objects.filter(ac_query).all() # Execute query ac_types: List[str]
i: int = 0 i, ac_types = assign_aircraft(queryset=queryset, data=data)
for mix in queryset: messages.success(request, f"{i} Missions updated to {ac_types}")
mix.aircrafts.clear()
for ac in aircrafts:
mix.aircrafts.add(ac)
mix.save()
i += 1
messages.success(request, f"{i} Students updated to {ac_types}")
@admin.display(description="Assigned Aircrafts") @admin.display(description="Assigned Aircrafts")
def assigned_aircrafts(self, obj: MissionProfile) -> SafeText: def assigned_aircrafts(self, obj: MissionProfile) -> SafeText:

View File

@@ -1,5 +1,5 @@
from django.forms import ModelChoiceField, TypedMultipleChoiceField, ModelMultipleChoiceField from django.forms import ModelChoiceField, TypedMultipleChoiceField
from django.db.models.query import QuerySet, Q from django.db.models.query import QuerySet
from django.http import HttpRequest from django.http import HttpRequest
from django.contrib import admin, messages from django.contrib import admin, messages
from django.utils.safestring import SafeText from django.utils.safestring import SafeText
@@ -10,15 +10,19 @@ from import_export.tmp_storages import CacheStorage
from import_export.resources import ModelResource from import_export.resources import ModelResource
from import_export.forms import ConfirmImportForm, ImportForm from import_export.forms import ConfirmImportForm, ImportForm
from django_admin_action_forms import AdminActionFormsMixin, AdminActionForm, action_with_form from django_admin_action_forms import AdminActionFormsMixin, AdminActionForm, ActionForm, action_with_form
from admin_confirm import AdminConfirmMixin, confirm_action
from ..models.aircrafts import Aircraft, AircraftTypes from ..models.aircrafts import AircraftTypes
from ..models.courses import Course from ..models.courses import Course
from ..models.students import Student from ..models.students import Student
from ..actions.assign_aircraft import assign_aircraft
from ..actions.send_email import send_mail_password
from ..custom.colortag import course_color from ..custom.colortag import course_color
from typing import Any, Dict from typing import Any, Dict, List
# Custom import form to select a course for student input # Custom import form to select a course for student input
class StudentCustomConfirmImportForm(ConfirmImportForm): class StudentCustomConfirmImportForm(ConfirmImportForm):
@@ -56,23 +60,25 @@ class StudentResource(ModelResource):
# Form Class for Student course change # Form Class for Student course change
class ChangeCourseForm(AdminActionForm): class ChangeCourseForm(AdminActionForm):
course = TypedMultipleChoiceField(choices=AircraftTypes) course = ModelChoiceField(queryset=Course.objects.all().order_by("ctype", "-cnumber"))
# Form class to assing aircrafts to students # Form class to assing aircrafts to students
class ChangeAircraftForm(AdminActionForm): class ChangeAircraftForm(AdminActionForm):
aircrafts = ModelMultipleChoiceField(queryset=Aircraft.objects.distinct('type').all()) aircrafts = TypedMultipleChoiceField(choices=AircraftTypes)
class StudentAdmin(ImportMixin, AdminActionFormsMixin, admin.ModelAdmin):
class StudentAdmin(ImportMixin, AdminConfirmMixin, AdminActionFormsMixin, admin.ModelAdmin):
model = Student model = Student
list_display = ("surname", "name", "course", "course_color", "email", "phone", "username", "password", "active", ) list_display = ("surname", "name", "course", "course_color", "email", "phone", "username", "password", "active", "mail_sent")
list_filter = ("course", "active", ) list_filter = ("course", "active", )
search_fields = ("surname", "name", "phone", "email", ) search_fields = ("surname", "name", "phone", "email", )
actions = ("change_course", "disable_students", "change_aircraft", ) actions = ("change_course", "deactivate_students", "change_aircraft", "send_mail", )
resource_classes = [StudentResource] resource_classes = [StudentResource]
confirm_form_class = StudentCustomConfirmImportForm confirm_form_class = StudentCustomConfirmImportForm
tmp_storage_class = CacheStorage tmp_storage_class = CacheStorage
skip_admin_log = True skip_admin_log = True
@admin.display(description="Color") @admin.display(description="Color")
def course_color(self, obj: Student) -> SafeText: def course_color(self, obj: Student) -> SafeText:
if not obj.course: if not obj.course:
@@ -88,7 +94,7 @@ class StudentAdmin(ImportMixin, AdminActionFormsMixin, admin.ModelAdmin):
return SafeText(obj.default_username()) return SafeText(obj.default_username())
@admin.action(description="Deactivate Students") @admin.action(description="Deactivate Students")
def disable_students(self, request: HttpRequest, queryset: QuerySet[Student]): def deactivate_students(self, request: HttpRequest, queryset: QuerySet[Student]):
for q in queryset.all(): for q in queryset.all():
if q.user: if q.user:
q.user.is_staff = False q.user.is_staff = False
@@ -102,22 +108,18 @@ class StudentAdmin(ImportMixin, AdminActionFormsMixin, admin.ModelAdmin):
count: int = queryset.update(course=course) count: int = queryset.update(course=course)
messages.success(request, f"{count} students updated to {course}") messages.success(request, f"{count} students updated to {course}")
@action_with_form(ChangeAircraftForm, description="Assign Aircraft") @action_with_form(ChangeAircraftForm, description="Assign Aircraft Type")
def change_aircraft(self, request: HttpRequest, queryset: QuerySet[Student], data: Dict[str, QuerySet[Aircraft]]): def change_aircraft(self, request: HttpRequest, queryset: QuerySet[Student], data: Dict[str, List[AircraftTypes]]):
ac_types = [t.type for t in data["aircrafts"]] i: int
ac_query: Q = Q() # Build an or query to select all aircrafts of the specified types ac_types: List[str]
for a in ac_types: i, ac_types = assign_aircraft(queryset=queryset, data=data)
ac_query |= Q(type=a)
aircrafts: QuerySet[Aircraft] = Aircraft.objects.filter(ac_query).all() # Execute query
i: int = 0
for student in queryset:
student.aircrafts.clear()
for ac in aircrafts:
student.aircrafts.add(ac)
student.save()
i += 1
messages.success(request, f"{i} Students updated to {ac_types}") messages.success(request, f"{i} Students updated to {ac_types}")
@confirm_action
@admin.action(description="Send Access Credentials e-mail")
def send_mail(self, request: HttpRequest, queryset: QuerySet[Student], *args: Any) -> None:
send_mail_password(request=request, queryset=queryset)
# Return the initial form for import confirmations, request course to user # Return the initial form for import confirmations, request course to user
def get_confirm_form_initial(self, request: HttpRequest, import_form) -> Dict[str, Any]: 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) initial: Dict[str, Any] = super().get_confirm_form_initial(request, import_form)

View File

@@ -1,13 +1,17 @@
import nested_admin import nested_admin
from django import forms from django import forms
from django.db import models from django.db.models import CharField, TextField
from django.db.models.query_utils import Q
from django.db.models.fields.related import ForeignKey
from django.forms import TextInput, Textarea from django.forms import TextInput, Textarea
from django.http import HttpRequest from django.http import HttpRequest
from ..models.missions import Training from ..models.courses import Course, CourseTypes
from ..models.students import Student
from ..models.missions import Training, MissionTypes, MissionProfile
from ..models.weekpref import WeekPreference from ..models.weekpref import WeekPreference
from datetime import date from ..custom.student_permissions import has_edit_permission
class TrainingForm(forms.ModelForm): class TrainingForm(forms.ModelForm):
model=Training model=Training
@@ -21,17 +25,35 @@ class TrainingInLIne(nested_admin.NestedTabularInline):
max_num = 7 max_num = 7
formfield_overrides = { formfield_overrides = {
models.CharField: {'widget': TextInput(attrs={'size':'20'})}, CharField: {'widget': TextInput(attrs={'size':'20'})},
models.TextField: {'widget': Textarea(attrs={'rows':4, 'cols':35})}, TextField: {'widget': Textarea(attrs={'rows':4, 'cols':35})},
} }
def formfield_for_foreignkey(self, db_field: ForeignKey, request: HttpRequest, **kwargs):
# modify entries for "mission" field, show only types compatible with student course
if not hasattr(request.user, "student") or not hasattr(request.user.student, "course"):
return super().formfield_for_foreignkey(db_field, request, **kwargs)
course: Course = request.user.student.course
if db_field.name == "mission":
match course.ctype:
case CourseTypes.PPL:
kwargs["queryset"] = MissionProfile.objects.filter(mtype=MissionTypes.PPL)
case CourseTypes.ATPL:
q: Q = Q(mtype=MissionTypes.IR) | \
Q(mtype=MissionTypes.MEP) | \
Q(mtype=MissionTypes.MEP_IR) | \
Q(mtype=MissionTypes.CPL) | \
Q(mtype=MissionTypes.CHK)
kwargs["queryset"] = MissionProfile.objects.filter(q).order_by("id")
case CourseTypes.FI:
kwargs["queryset"] = MissionProfile.objects.filter(mtype=MissionTypes.FI).order_by("mnum")
case _:
pass
return super().formfield_for_foreignkey(db_field, request, **kwargs)
# 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) -> bool: 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) -> bool: def has_delete_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

@@ -2,12 +2,13 @@ import nested_admin
from django.forms import Form from django.forms import Form
from django.db.models.query import QuerySet from django.db.models.query import QuerySet
from django.contrib.auth.models import User
from django.http import HttpRequest, HttpResponse from django.http import HttpRequest, HttpResponse
from django.contrib import admin, messages from django.contrib import admin, messages
from django.utils.translation import ngettext from django.utils.translation import ngettext
from django.utils.safestring import SafeText from django.utils.safestring import SafeText
from ..models.courses import CourseTypes
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
@@ -15,17 +16,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, has_week_add_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 from typing import Dict, List, Any
class WeekPreferenceAdmin(nested_admin.NestedPolymorphicModelAdmin): class WeekPreferenceAdmin(nested_admin.NestedPolymorphicModelAdmin):
inlines = (TrainingInLIne, HourBuildingInLine, ) list_display = ("week", "student__surname", "student__name", "student__course", "course_color", "student_brief_mix", "inserted")
list_display = ("week", "student__surname","student__name", "student__course", "course_color", "student_brief_mix",) list_filter = ("week", "student__course", )
list_filter = ("week", "student__course",) search_fields = ("student__surname","student__name", )
search_fields = ("student__surname","student__name",) actions = ("export", )
actions = ("export",)
@admin.action(description="Export Selected Preferences") @admin.action(description="Export Selected Preferences")
def export(self, request: HttpRequest, queryset: QuerySet[WeekPreference]) -> HttpResponse | None: def export(self, request: HttpRequest, queryset: QuerySet[WeekPreference]) -> HttpResponse | None:
@@ -53,6 +54,24 @@ class WeekPreferenceAdmin(nested_admin.NestedPolymorphicModelAdmin):
return [] return []
return list_filter return list_filter
# Get available mission or HB depending on student course
def get_inline_instances(self, request: HttpRequest, obj: WeekPreference | None = None):
if hasattr(request.user, "student"):
student: Student = request.user.student
# Only ATPL students are able to book HourBuilding Missions
if student.course and student.course.ctype in (CourseTypes.ATPL, CourseTypes.DISTANCE):
return (
TrainingInLIne(self.model, self.admin_site),
HourBuildingInLine(self.model, self.admin_site),
)
# All other courses have only training
return (TrainingInLIne(self.model, self.admin_site), )
else:
return (
TrainingInLIne(self.model, self.admin_site),
HourBuildingInLine(self.model, self.admin_site),
)
# 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: HttpRequest) -> Dict[str, Any]: def get_actions(self, request: HttpRequest) -> Dict[str, Any]:
actions = super().get_actions(request) actions = super().get_actions(request)
@@ -62,7 +81,7 @@ class WeekPreferenceAdmin(nested_admin.NestedPolymorphicModelAdmin):
# 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: HttpRequest) -> QuerySet[WeekPreference]: 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
@@ -70,51 +89,50 @@ class WeekPreferenceAdmin(nested_admin.NestedPolymorphicModelAdmin):
def get_form(self, request: HttpRequest, obj: WeekPreference | None = None, **kwargs: Dict[str, Any]) -> Form: def get_form(self, request: HttpRequest, obj: WeekPreference | None = None, **kwargs: Dict[str, Any]) -> Form:
form: Form = super().get_form(request, obj, **kwargs) form: Form = super().get_form(request, obj, **kwargs)
current_week = date.today().isocalendar().week today: date = date.today()
current_week = 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 "inserted" in form.base_fields:
form.base_fields["inserted"].initial = today
# 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
form.base_fields["inserted"].disabled = True # student cannot change insertion date
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: HttpRequest, obj: WeekPreference | None = None) -> bool: 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: HttpRequest, obj: WeekPreference | None = None) -> bool: def has_add_permission(self, request: HttpRequest, obj: WeekPreference | None = None) -> bool:
return self.has_change_permission(request, obj) return has_week_add_permission(request=request) and has_edit_permission(request=request, obj=obj)
# 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: HttpRequest, obj: WeekPreference | None = None)-> bool: def has_delete_permission(self, request: HttpRequest, obj: WeekPreference | None = None)-> bool:
return self.has_change_permission(request, obj) return self.has_change_permission(request, obj)
def changeform_view(self, request: HttpRequest, object_id: int | None = None, form_url: str = "", extra_context=None): def changeform_view(self, request: HttpRequest, object_id: int | None = None, form_url: str = "", extra_context = None):
extra_context = extra_context or {} 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: HttpRequest, obj, form: Form, change: bool): def save_model(self, request: HttpRequest, obj: WeekPreference, 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

View File

@@ -0,0 +1,21 @@
from django.http import HttpRequest
from ..models.students import Student
from ..models.weekpref import WeekPreference
from datetime import date
def has_week_add_permission(request: HttpRequest):
if hasattr(request.user, 'student'):
student: Student = request.user.student
current_week: int = date.today().isocalendar().week
return student.active and not WeekPreference.objects.filter(student_id=student.id, week=current_week).count()
return True
# 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'):
current_week: int = date.today().isocalendar().week
if obj and current_week > obj.week:
return False
return True

View File

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

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,18 @@
# Generated by Django 5.2.8 on 2025-12-05 16:57
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('flightslot', '0028_alter_availability_options'),
]
operations = [
migrations.AlterField(
model_name='course',
name='ctype',
field=models.CharField(choices=[('FI', 'FI'), ('PPL', 'PPL'), ('ATPL', 'ATPL'), ('DL', 'DISTANCE'), ('DL_VOLO', 'DISTANCE_VOLO'), ('OTHER', 'OTHER')], verbose_name='Course Type'),
),
]

View File

@@ -0,0 +1,29 @@
# Generated by Django 5.2.8 on 2025-12-10 10:18
import datetime
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('flightslot', '0029_alter_course_ctype'),
]
operations = [
migrations.AddField(
model_name='weekpreference',
name='inserted',
field=models.DateField(default=datetime.date(2025, 12, 10)),
),
migrations.AlterField(
model_name='availability',
name='week',
field=models.PositiveSmallIntegerField(auto_created=True, db_default=50, db_index=True, verbose_name='Week Number'),
),
migrations.AlterField(
model_name='weekpreference',
name='week',
field=models.PositiveSmallIntegerField(auto_created=True, db_default=50, db_index=True, verbose_name='Week Number'),
),
]

View File

@@ -0,0 +1,24 @@
# Generated by Django 5.2.8 on 2025-12-10 10:28
import phonenumber_field.modelfields
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('flightslot', '0030_weekpreference_inserted_alter_availability_week_and_more'),
]
operations = [
migrations.AddField(
model_name='student',
name='mail_sent',
field=models.BooleanField(default=False),
),
migrations.AlterField(
model_name='student',
name='phone',
field=phonenumber_field.modelfields.PhoneNumberField(db_index=True, max_length=128, null=True, region=None, unique=True),
),
]

View File

@@ -8,6 +8,7 @@ class AircraftTypes(models.TextChoices):
PA34 = "PA34", _("Piper PA34") PA34 = "PA34", _("Piper PA34")
C182 = "C182", _("Cessna 182Q") C182 = "C182", _("Cessna 182Q")
P210 = "TWEN", _("Tecnam P2010") P210 = "TWEN", _("Tecnam P2010")
CP10 = "CP10", _("Cap 10")
ALX40 = "FSTD", _("Alsim ALX40") ALX40 = "FSTD", _("Alsim ALX40")
class Aircraft(models.Model): class Aircraft(models.Model):

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

@@ -8,6 +8,7 @@ class CourseTypes(models.TextChoices):
PPL = "PPL", _("PPL") PPL = "PPL", _("PPL")
ATPL = "ATPL", _("ATPL") ATPL = "ATPL", _("ATPL")
DISTANCE = "DL", _("DISTANCE") DISTANCE = "DL", _("DISTANCE")
DISTANCE_VOLO = "DL_VOLO", _("DISTANCE_VOLO")
OTHER = "OTHER",_("OTHER") OTHER = "OTHER",_("OTHER")
class Course(models.Model): class Course(models.Model):

View File

@@ -117,6 +117,8 @@ class HourBuildingLegFlight(HourBuildingLegBase):
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
self.departure = self.departure.upper().strip() self.departure = self.departure.upper().strip()
self.destination = self.destination.upper().strip() self.destination = self.destination.upper().strip()
if self.pax:
self.pax = " ".join(c.capitalize() for c in self.pax.split())
super().save(*args, **kwargs) super().save(*args, **kwargs)
def __str__(self): def __str__(self):

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

@@ -5,7 +5,7 @@ from datetime import timedelta
from ..models.weekpref import WeekPreference from ..models.weekpref import WeekPreference
from ..models.aircrafts import Aircraft from ..models.aircrafts import Aircraft
class MissionType(models.TextChoices): class MissionTypes(models.TextChoices):
OTHER = "OTHER", _("OTHER") OTHER = "OTHER", _("OTHER")
CHK = "CHK", _("CHK_6M") CHK = "CHK", _("CHK_6M")
PPL = "PPL", _("PPL") PPL = "PPL", _("PPL")
@@ -24,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"
) )

View File

@@ -1,6 +1,8 @@
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 from ..models.aircrafts import Aircraft
@@ -11,12 +13,14 @@ 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 db_index=True,
unique=True
) )
name = models.CharField( name = models.CharField(
@@ -51,8 +55,16 @@ class Student(models.Model):
Aircraft Aircraft
) )
mail_sent = models.BooleanField(
null=False,
default=False
)
def default_password(self) -> str: # Maximum 4 digits for passowrd def default_password(self) -> str: # Maximum 4 digits for passowrd
return f"{self.name.lower()[0]}{self.surname.lower()}{self.id % 10000}" if self.pk:
return f"{self.name.lower()[0]}{self.surname.lower()}{self.id % 10000}"
else:
return ""
def default_username(self) -> str: def default_username(self) -> str:
if self.pk and self.user: if self.pk and self.user:
@@ -74,8 +86,8 @@ class Student(models.Model):
counter += 1 counter += 1
# Create user # Create user
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(),

View File

@@ -20,9 +20,14 @@ 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"
) )
inserted = models.DateField(
null=False,
default=date.today()
)
def __str__(self): def __str__(self):
return f"Week {self.week} - {self.student.surname} {self.student.name[0]}." return f"Week {self.week} - {self.student.surname} {self.student.name[0]}."

49
cntmanage/poetry.lock generated
View File

@@ -66,6 +66,21 @@ files = [
[package.dependencies] [package.dependencies]
django = ">=3.2" django = ">=3.2"
[[package]]
name = "django-admin-confirm"
version = "1.0.1"
description = "Adds confirmation to Django Admin changes, additions and actions"
optional = false
python-versions = ">=3.7"
groups = ["main"]
files = [
{file = "django-admin-confirm-1.0.1.tar.gz", hash = "sha256:fb6a4b7cb9fc6ccd97f92f88275ee8e3912f3ee0ce82da962ad0a2b1b17cd6a0"},
{file = "django_admin_confirm-1.0.1-py3-none-any.whl", hash = "sha256:271a7135e8e5f0cce94a6c06f708dec794d3538ada37e111c1f8d7c8f762b012"},
]
[package.dependencies]
Django = ">=3.2"
[[package]] [[package]]
name = "django-colorfield" name = "django-colorfield"
version = "0.14.0" version = "0.14.0"
@@ -143,6 +158,26 @@ 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]] [[package]]
name = "django-polymorphic" name = "django-polymorphic"
version = "4.1.0" version = "4.1.0"
@@ -185,6 +220,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"
@@ -436,4 +483,4 @@ files = [
[metadata] [metadata]
lock-version = "2.1" lock-version = "2.1"
python-versions = "^3.12" python-versions = "^3.12"
content-hash = "e932d0af75c888d83fecefaaad1d018c508881a3bfde2ea640a82790e3567855" content-hash = "b45301c627836abac1ef9628e67fc63189b03e7857a7a003854aa1fb30f2a4a3"

View File

@@ -18,6 +18,8 @@ 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-polymorphic = "^4.1.0"
django-phonenumber-field = {extras = ["phonenumberslite"], version = "^8.4.0"}
django-admin-confirm = "^1.0.1"
[build-system] [build-system]

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

View File

@@ -1 +0,0 @@
admin: CantorAdmin2k25

View File

@@ -8,8 +8,8 @@
<h1 id="site-name"> <h1 id="site-name">
<a href="{% url 'admin:index' %}" style="color: #0b1728;"> <a href="{% url 'admin:index' %}" style="color: #0b1728;">
<img src="{% static 'cantorair_blue.jpg' %}" <img src="{% static 'cantorair.png' %}"
height="60px" height="70px"
style="margin-right: 20px;"/> style="margin-right: 20px;"/>
</a> </a>
</h1> </h1>

View File

@@ -0,0 +1,408 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional //EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html
xmlns="http://www.w3.org/1999/xhtml"
xmlns:v="urn:schemas-microsoft-com:vml"
xmlns:o="urn:schemas-microsoft-com:office:office"
>
<head>
<!--[if gte mso 9]>
<xml>
<o:OfficeDocumentSettings>
<o:AllowPNG />
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
<![endif]-->
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="x-apple-disable-message-reformatting" />
<!--[if !mso]><!-->
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<!--<![endif]-->
<style type="text/css">
@media only screen and (min-width: 520px) {
.u-row {
width: 500px !important;
}
.u-row .u-col {
vertical-align: top;
}
.u-row .u-col-100 {
width: 500px !important;
}
}
@media only screen and (max-width: 520px) {
.u-row-container {
max-width: 100% !important;
padding-left: 0px !important;
padding-right: 0px !important;
}
.u-row {
width: 100% !important;
}
.u-row .u-col {
display: block !important;
width: 100% !important;
min-width: 320px !important;
max-width: 100% !important;
}
.u-row .u-col > div {
margin: 0 auto;
}
}
body {
margin: 0;
padding: 0;
}
table,
td,
tr {
border-collapse: collapse;
vertical-align: top;
}
.ie-container table,
.mso-container table {
table-layout: fixed;
}
* {
line-height: inherit;
}
a[x-apple-data-detectors="true"] {
color: inherit !important;
text-decoration: none !important;
}
table,
td {
color: #000000;
}
</style>
</head>
<body
class="clean-body u_body"
style="
margin: 0;
padding: 0;
-webkit-text-size-adjust: 100%;
background-color: #f7f8f9;
color: #000000;
"
>
<!--[if IE]><div class="ie-container"><![endif]-->
<!--[if mso]><div class="mso-container"><![endif]-->
<table
role="presentation"
id="u_body"
style="
border-collapse: collapse;
table-layout: fixed;
border-spacing: 0;
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
vertical-align: top;
min-width: 320px;
margin: 0 auto;
background-color: #f7f8f9;
width: 100%;
"
cellpadding="0"
cellspacing="0"
>
<tbody>
<tr style="vertical-align: top">
<td
style="
word-break: break-word;
border-collapse: collapse !important;
vertical-align: top;
"
>
<!--[if (mso)|(IE)]><table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td align="center" style="background-color: #F7F8F9;"><![endif]-->
<div
class="u-row-container"
style="padding: 0px; background-color: transparent"
>
<div
class="u-row"
style="
margin: 0 auto;
min-width: 320px;
max-width: 500px;
overflow-wrap: break-word;
word-wrap: break-word;
word-break: break-word;
background-color: transparent;
"
>
<div
style="
border-collapse: collapse;
display: table;
width: 100%;
height: 100%;
background-color: transparent;
"
>
<!--[if (mso)|(IE)]><table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding: 0px;background-color: transparent;" align="center"><table role="presentation" cellpadding="0" cellspacing="0" border="0" style="width:500px;"><tr style="background-color: transparent;"><![endif]-->
<!--[if (mso)|(IE)]><td align="center" width="500" style="width: 500px;padding: 0px;border-top: 0px solid transparent;border-left: 0px solid transparent;border-right: 0px solid transparent;border-bottom: 0px solid transparent;border-radius: 0px;-webkit-border-radius: 0px; -moz-border-radius: 0px;" valign="top"><![endif]-->
<div
class="u-col u-col-100"
style="
max-width: 320px;
min-width: 500px;
display: table-cell;
vertical-align: top;
"
>
<div
style="
height: 100%;
width: 100% !important;
border-radius: 0px;
-webkit-border-radius: 0px;
-moz-border-radius: 0px;
"
>
<!--[if (!mso)&(!IE)]><!--><div
style="
box-sizing: border-box;
height: 100%;
padding: 0px;
border-top: 0px solid transparent;
border-left: 0px solid transparent;
border-right: 0px solid transparent;
border-bottom: 0px solid transparent;
border-radius: 0px;
-webkit-border-radius: 0px;
-moz-border-radius: 0px;
"
><!--<![endif]-->
<table
style="font-family: arial, helvetica, sans-serif"
role="presentation"
cellpadding="0"
cellspacing="0"
width="100%"
border="0"
>
<tbody>
<tr>
<td
style="
overflow-wrap: break-word;
word-break: break-word;
padding: 10px;
font-family: arial, helvetica, sans-serif;
"
align="left"
>
<table
role="presentation"
width="100%"
cellpadding="0"
cellspacing="0"
border="0"
>
<tr>
<td
style="
padding-right: 0px;
padding-left: 0px;
"
align="center"
>
<a
href="https://www.cantorair.it"
target="_blank"
style="
color: rgb(0, 0, 238);
text-decoration: underline;
line-height: inherit;
"
><img
align="center"
border="0"
src="cid:logo_image"
alt="Cantor Air Logo"
title=""
style="
outline: none;
text-decoration: none;
-ms-interpolation-mode: bicubic;
clear: both;
display: inline-block !important;
border: none;
height: auto;
float: none;
width: 50%;
max-width: 240px;
"
width="240"
/>
</a>
</td>
</tr>
</table>
</td>
</tr>
</tbody>
</table>
<table
style="font-family: arial, helvetica, sans-serif"
role="presentation"
cellpadding="0"
cellspacing="0"
width="100%"
border="0"
>
<tbody>
<tr>
<td
style="
overflow-wrap: break-word;
word-break: break-word;
padding: 10px;
font-family: arial, helvetica, sans-serif;
"
align="left"
>
<!--[if mso]><table role="presentation" width="100%"><tr><td><![endif]-->
<h1
style="
margin: 0px;
line-height: 140%;
text-align: center;
word-wrap: break-word;
font-size: 22px;
font-weight: 400;
"
>
<span
>✈️ Welcome to CantorAir Flight Scheduler
✈️</span
>
</h1>
<!--[if mso]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
<table
style="font-family: arial, helvetica, sans-serif"
role="presentation"
cellpadding="0"
cellspacing="0"
width="100%"
border="0"
>
<tbody>
<tr>
<td
style="
overflow-wrap: break-word;
word-break: break-word;
padding: 10px;
font-family: arial, helvetica, sans-serif;
"
align="left"
>
<div
style="
font-size: 14px;
line-height: 140%;
text-align: left;
word-wrap: break-word;
"
>
<p
style="
text-align: center;
line-height: inherit;
margin: 0px;
"
>
<span
>You can access the flight scheduler
website with this link: </span
><br /><a href="https://cms.etss.it/user"
><span>https://cms.etss.it</span></a
>
</p>
<p style="text-align: center"><br /></p>
<p dir="ltr" style="text-align: center">
<span style="font-weight: bold"
>Your access credentials:</span
>
</p>
<p dir="ltr" style="text-align: center">
<br /><span>Username: {{username}}</span
><br /><span>Password: {{password}}</span>
</p>
<p dir="ltr" style="text-align: center">
<br />
</p>
<p dir="ltr" style="text-align: center">
<span
>Flight Scheduling is available from </span
><br /><span style="font-weight: bold"
>Monday to Tuesday until 15.00</span
>
</p>
<p style="text-align: center">
<br /><span>_______________________</span>
</p>
<p dir="ltr" style="text-align: center">
<span style="font-style: italic">Team</span
><br /><span style="font-weight: bold"
>CANTOR AIR IT.ATO.0004</span
>
</p>
<p dir="ltr" style="text-align: center">
<span>&nbsp;Email: </span
><a
href="mailto:info@cantorair.it"
target="_blank"
style="line-height: 115%"
><span>info@cantorair.it</span></a
><br /><span>Phone: +39 035 520035</span
><br /><span>_______________________</span>
</p>
</div>
</td>
</tr>
</tbody>
</table>
<!--[if (!mso)&(!IE)]><!-->
</div>
<!--<![endif]-->
</div>
</div>
<!--[if (mso)|(IE)]></td><![endif]-->
<!--[if (mso)|(IE)]></tr></table></td></tr></table><![endif]-->
</div>
</div>
</div>
<!--[if (mso)|(IE)]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
<!--[if mso]></div><![endif]-->
<!--[if IE]></div><![endif]-->
</body>
</html>

8
note.txt Normal file
View File

@@ -0,0 +1,8 @@
OK leg delle hb all'interno del riquadro per i giorni
OK aereo assegnato allo studente di fianco al numero della missione per PPL, invece per CPL e IR e HB il tipo va di fiaco al nome dello studente
OK le missioni ripetute su piu' giorni hanno una cella per giorno (non unite)
OK lo studente vede solo le missioni della sua fase PPL->PPL ATPL-> tutto
OK ogni richiesta ha un colore diverso che cicla con delle tinte pastello
password: CantorAir2k25