Compare commits
24 Commits
airplane-c
...
email
| Author | SHA1 | Date | |
|---|---|---|---|
| a9587776e8 | |||
| ec7ae4a48e | |||
| c93171dbc3 | |||
| 42417927c9 | |||
| 7c7d0e1e62 | |||
| e41eea8527 | |||
| 369c3b5e19 | |||
| aeb3aa30ce | |||
| 84cf41535c | |||
| cdf7e7c677 | |||
| b8f4331d3b | |||
| 303359c921 | |||
| ec8373877b | |||
| e7e47152ed | |||
| 1eb11f33fc | |||
| e417268991 | |||
| 4b5319f557 | |||
| 3ee2269d70 | |||
| 5d1686f24b | |||
| 99a8cfe482 | |||
| 2b1042d3a8 | |||
| f06f269568 | |||
| a31798d0b0 | |||
| af62bf843c |
@@ -31,6 +31,7 @@ ALLOWED_HOSTS = []
|
||||
# Application definition
|
||||
|
||||
INSTALLED_APPS = [
|
||||
'admin_confirm',
|
||||
'django.contrib.admin',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
@@ -43,7 +44,8 @@ INSTALLED_APPS = [
|
||||
'colorfield',
|
||||
'import_export',
|
||||
'django_admin_action_forms',
|
||||
'polymorphic'
|
||||
'polymorphic',
|
||||
"phonenumber_field",
|
||||
]
|
||||
|
||||
# Import Export plugin settings
|
||||
@@ -142,3 +144,14 @@ STATIC_URL = 'static/'
|
||||
# https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field
|
||||
|
||||
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
|
||||
|
||||
@@ -35,6 +35,7 @@ CSRF_TRUSTED_ORIGINS = ["http://localhost:8000", "http://127.0.0.1:8000", "http:
|
||||
|
||||
# Application definition
|
||||
INSTALLED_APPS = [
|
||||
'admin_confirm',
|
||||
'django.contrib.admin',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
@@ -47,7 +48,8 @@ INSTALLED_APPS = [
|
||||
'colorfield',
|
||||
'import_export',
|
||||
'django_admin_action_forms',
|
||||
'polymorphic'
|
||||
'polymorphic',
|
||||
"phonenumber_field",
|
||||
]
|
||||
|
||||
# Import Export plugin settings
|
||||
@@ -73,7 +75,7 @@ ROOT_URLCONF = 'cntmanage.urls'
|
||||
TEMPLATES = [
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
'DIRS': ['/var/www/templates'],
|
||||
'DIRS': ['/var/www/templates', '/app/templates'],
|
||||
'APP_DIRS': True,
|
||||
'OPTIONS': {
|
||||
'context_processors': [
|
||||
@@ -137,9 +139,23 @@ USE_TZ = True
|
||||
# https://docs.djangoproject.com/en/5.1/howto/static-files/
|
||||
STATIC_URL = "static/"
|
||||
STATIC_ROOT = "/var/www/static/"
|
||||
STATICFILES_DIRS = [
|
||||
"/app/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'
|
||||
|
||||
#### 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"
|
||||
|
||||
@@ -2,10 +2,12 @@ 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
|
||||
]
|
||||
|
||||
5
cntmanage/docker/deploy.sh
Executable file
5
cntmanage/docker/deploy.sh
Executable file
@@ -0,0 +1,5 @@
|
||||
#!/bin/bash
|
||||
|
||||
git pull
|
||||
./build
|
||||
docker compose up -d
|
||||
@@ -19,11 +19,12 @@ FROM python:3.12-slim AS deploy
|
||||
WORKDIR /app
|
||||
# Copy application custom static files
|
||||
RUN mkdir -p static
|
||||
COPY ./static/cantorair.jpg ./static
|
||||
COPY ./static/cantorair_blue.jpg ./static
|
||||
COPY ./static/* ./static
|
||||
# Copy application custom templates for admin page
|
||||
RUN mkdir -p /templates/admin
|
||||
RUN mkdir -p /templates/email
|
||||
COPY ./templates/admin/* ./templates/admin/
|
||||
COPY ./templates/email/* ./templates/email/
|
||||
# Copy and install application wheel package
|
||||
COPY --from=builder /build/dist/*.whl ./
|
||||
RUN pip install --no-cache-dir *.whl
|
||||
|
||||
23
cntmanage/flightslot/actions/assign_aircraft.py
Normal file
23
cntmanage/flightslot/actions/assign_aircraft.py
Normal 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]
|
||||
20
cntmanage/flightslot/actions/assign_profile.py
Normal file
20
cntmanage/flightslot/actions/assign_profile.py
Normal 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]
|
||||
@@ -5,6 +5,7 @@ from openpyxl import Workbook
|
||||
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
|
||||
from openpyxl.utils import get_column_letter
|
||||
|
||||
from ..models.courses import CourseTypes
|
||||
from ..models.missions import Training
|
||||
from ..models.weekpref import WeekPreference
|
||||
from ..models.hourbuildings import HourBuilding, HourBuildingLegFlight, HourBuildingLegStop, HourBuildingLegBase
|
||||
@@ -12,6 +13,19 @@ from ..models.hourbuildings import HourBuilding, HourBuildingLegFlight, HourBuil
|
||||
from datetime import date, datetime
|
||||
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:
|
||||
|
||||
if not queryset.first():
|
||||
@@ -57,7 +71,7 @@ def export_selected(request: HttpRequest, queryset: QuerySet[WeekPreference]) ->
|
||||
center = Alignment(horizontal="center", vertical="center", wrapText=True)
|
||||
|
||||
# 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_bottom: Border = Border(bottom=border_thick)
|
||||
border_bottom_thin: Border = Border(bottom=border_thin)
|
||||
@@ -78,74 +92,78 @@ def export_selected(request: HttpRequest, queryset: QuerySet[WeekPreference]) ->
|
||||
# Each of this iterations fills the table for a student
|
||||
row: int = 2
|
||||
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_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_course_type: 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:
|
||||
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}",
|
||||
"\n".join([f"{q.student.surname} {q.student.name}", student_course_ac]),
|
||||
f"{student_course_type}-{student_course_number}"
|
||||
]
|
||||
else:
|
||||
student_data = [f"{q.student.surname} {q.student.name}", f"No Course Assigned"]
|
||||
|
||||
# Fill Training mission rows
|
||||
mission_name: str
|
||||
mission_name: List[str]
|
||||
mission_days: List[str]
|
||||
mission_notes: str
|
||||
mission_data: List[List[str]] = []
|
||||
for t in Training.objects.filter(weekpref = q.id):
|
||||
if not t.mission:
|
||||
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_name if t.monday else "",
|
||||
mission_name if t.tuesday else "",
|
||||
mission_name if t.wednesday else "",
|
||||
mission_name if t.thursday else "",
|
||||
mission_name if t.friday else "",
|
||||
mission_name if t.saturday else "",
|
||||
mission_name if t.sunday else ""
|
||||
mission_name_joined if t.monday else "",
|
||||
mission_name_joined if t.tuesday else "",
|
||||
mission_name_joined if t.wednesday else "",
|
||||
mission_name_joined if t.thursday else "",
|
||||
mission_name_joined if t.friday else "",
|
||||
mission_name_joined if t.saturday 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, ])
|
||||
|
||||
# Fill HourBuilding rows
|
||||
hb_name: str
|
||||
hb_name: List[str]
|
||||
hb_days: List[str]
|
||||
hb_data: List[List[str]] = []
|
||||
for h in HourBuilding.objects.filter(weekpref = q.id):
|
||||
hb_name = f"HB - {h.aircraft}\nVedi Note ->"
|
||||
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_name = ["HB", f"({h.aircraft})"]
|
||||
hb_legs_all = HourBuildingLegBase.objects.filter(hb_id = h.id)
|
||||
for hh in hb_legs_all:
|
||||
time_str: str = ':'.join(str(hh.time).split(':')[:2]) # keep only hours and minutes
|
||||
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):
|
||||
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])
|
||||
hb_name.append(f"STOP [{time_str}] {"Refuel" if hh.refuel else ""}" )
|
||||
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
|
||||
all_data: List[List[str]] = mission_data + hb_data
|
||||
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):
|
||||
cell = ws.cell(row = row + row_offset, column = c, value = cell_content)
|
||||
cell.alignment = center
|
||||
@@ -160,25 +178,25 @@ def export_selected(request: HttpRequest, queryset: QuerySet[WeekPreference]) ->
|
||||
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 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_col_start: int = 1
|
||||
for c, cell_content in enumerate(row_content, start=1):
|
||||
# Merge cells in the row
|
||||
if cell_content == prev_cell_val and not merge_start:
|
||||
merge_start = True
|
||||
merge_col_start = c-1 # start merge from previous column
|
||||
elif cell_content != prev_cell_val and merge_start:
|
||||
merge_start = False
|
||||
ws.merge_cells(start_row=row+row_offset,
|
||||
end_row=row+row_offset,
|
||||
start_column=max(merge_col_start,1),
|
||||
end_column=max(c-1,1)) # end merge to previous column
|
||||
prev_cell_val = cell_content
|
||||
cell.fill = PatternFill('solid', fgColor=PALETTE[ri % len(PALETTE)].lstrip("#").lower())
|
||||
if MERGE:
|
||||
prev_cell_val: str = row_content[0]
|
||||
merge_start: bool = False
|
||||
merge_col_start: int = 1
|
||||
for c, cell_content in enumerate(row_content, start=1):
|
||||
# Merge cells in the row
|
||||
if cell_content == prev_cell_val and not merge_start:
|
||||
merge_start = True
|
||||
merge_col_start = c-1 # start merge from previous column
|
||||
elif cell_content != prev_cell_val and merge_start:
|
||||
merge_start = False
|
||||
ws.merge_cells(start_row=row+row_offset,
|
||||
end_row=row+row_offset,
|
||||
start_column=max(merge_col_start,1),
|
||||
end_column=max(c-1,1)) # end merge to previous column
|
||||
prev_cell_val = cell_content
|
||||
|
||||
# Incement row counter
|
||||
row_offset += 1
|
||||
@@ -191,8 +209,7 @@ def export_selected(request: HttpRequest, queryset: QuerySet[WeekPreference]) ->
|
||||
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
|
||||
ws.cell(row=student_end, column=c).border = Border(bottom=border_thick, right=border_thick)
|
||||
|
||||
# Merge Week, thick border
|
||||
ws.cell(row=student_start, column=week_index).border = border_all
|
||||
@@ -209,6 +226,7 @@ def export_selected(request: HttpRequest, queryset: QuerySet[WeekPreference]) ->
|
||||
|
||||
# Keep the largest column
|
||||
max_len: List[int] = []
|
||||
col_letter: str = "A"
|
||||
for column_cells in ws.columns:
|
||||
for cell in column_cells:
|
||||
cell_lines = str(cell.value).splitlines()
|
||||
@@ -216,11 +234,9 @@ def export_selected(request: HttpRequest, queryset: QuerySet[WeekPreference]) ->
|
||||
continue
|
||||
max_len.append(max([len(ll) for ll in cell_lines]))
|
||||
length: int = max(max_len)
|
||||
col_letter: str = "A"
|
||||
if column_cells[0].column:
|
||||
col_letter = get_column_letter(column_cells[0].column)
|
||||
ws.column_dimensions[col_letter].width = length + 2
|
||||
|
||||
### End of Student Loop ###
|
||||
|
||||
# Save document in HttpResponse
|
||||
|
||||
73
cntmanage/flightslot/actions/send_email.py
Normal file
73
cntmanage/flightslot/actions/send_email.py
Normal file
@@ -0,0 +1,73 @@
|
||||
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
|
||||
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)
|
||||
except Exception as e:
|
||||
messages.error(request=request, message=f"General Error: {e}")
|
||||
|
||||
# Open only one conenction and send mass email
|
||||
try:
|
||||
with get_connection() as conn:
|
||||
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 {len(mails)} messages")
|
||||
|
||||
return
|
||||
@@ -6,22 +6,28 @@ 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
|
||||
##################################
|
||||
# User website under /user/ URL #
|
||||
##################################
|
||||
class FlightSlotUserSite(AdminSite):
|
||||
site_header = "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):
|
||||
app_list = super().get_app_list(request)
|
||||
@@ -35,6 +41,27 @@ class FlightSlotUserSite(AdminSite):
|
||||
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(MissionProfile, MissionProfileAdmin)
|
||||
flightslot_staff.register(Availability, AvailabilityAdmin)
|
||||
flightslot_staff.register(Instructor, InstructorAdmin)
|
||||
|
||||
# Get version for debug purposes
|
||||
ver: str = environ.get("VERSION", "dev")
|
||||
@@ -48,3 +75,5 @@ 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)
|
||||
|
||||
66
cntmanage/flightslot/admins/availability_adm.py
Normal file
66
cntmanage/flightslot/admins/availability_adm.py
Normal 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)
|
||||
|
||||
@@ -1,12 +1,24 @@
|
||||
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", "year")
|
||||
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")
|
||||
|
||||
@@ -2,15 +2,15 @@ import nested_admin
|
||||
|
||||
from django import forms
|
||||
from django.db import models
|
||||
from django.forms import TextInput, Textarea
|
||||
from django.http import HttpRequest
|
||||
from django.forms import TextInput, Textarea
|
||||
|
||||
from durationwidget.widgets import TimeDurationWidget
|
||||
|
||||
from ..models.hourbuildings import HourBuilding, HourBuildingLegBase, HourBuildingLegFlight, HourBuildingLegStop
|
||||
from ..models.weekpref import WeekPreference
|
||||
|
||||
from datetime import date
|
||||
from ..custom.student_permissions import has_edit_permission
|
||||
|
||||
class HourBuildingLegFlightForm(forms.ModelForm):
|
||||
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
|
||||
def has_change_permission(self, request: HttpRequest, obj: WeekPreference | None = None) -> bool:
|
||||
if hasattr(request.user, 'student') and obj:
|
||||
current_week: int = date.today().isocalendar().week
|
||||
if current_week > obj.week:
|
||||
return False
|
||||
return True
|
||||
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)
|
||||
|
||||
95
cntmanage/flightslot/admins/instructor_admin.py
Normal file
95
cntmanage/flightslot/admins/instructor_admin.py
Normal 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}")
|
||||
@@ -1,7 +1,7 @@
|
||||
from django.forms import ModelMultipleChoiceField
|
||||
from django.forms import TypedMultipleChoiceField
|
||||
from django.contrib import admin, messages
|
||||
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_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.resources import ModelResource
|
||||
|
||||
from ..models.aircrafts import Aircraft
|
||||
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
|
||||
from typing import Any, Dict, List
|
||||
|
||||
# Resource Class for Student data import
|
||||
class MissionProfileResource(ModelResource):
|
||||
@@ -40,30 +42,25 @@ class MissionProfileResource(ModelResource):
|
||||
|
||||
# Form class to assing aircrafts to students
|
||||
class ChangeAircraftForm(AdminActionForm):
|
||||
aircrafts = ModelMultipleChoiceField(queryset=Aircraft.objects.distinct('type').all())
|
||||
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
|
||||
|
||||
@action_with_form(ChangeAircraftForm, description="Assign Aircraft")
|
||||
def assign_aircraft(self, request: HttpRequest, queryset: QuerySet[MissionProfile], data: Dict[str, QuerySet[Aircraft]]):
|
||||
ac_types = [t.type for t in data["aircrafts"]]
|
||||
ac_query: Q = Q() # Build an or query to select all aircrafts of the specified types
|
||||
for a in ac_types:
|
||||
ac_query |= Q(type=a)
|
||||
aircrafts: QuerySet[Aircraft] = Aircraft.objects.filter(ac_query).all() # Execute query
|
||||
i: int = 0
|
||||
for mix in queryset:
|
||||
mix.aircrafts.clear()
|
||||
for ac in aircrafts:
|
||||
mix.aircrafts.add(ac)
|
||||
mix.save()
|
||||
i += 1
|
||||
messages.success(request, f"{i} Students updated to {ac_types}")
|
||||
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:
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from django.forms import ModelChoiceField, TypedMultipleChoiceField, ModelMultipleChoiceField
|
||||
from django.db.models.query import QuerySet, Q
|
||||
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
|
||||
@@ -10,15 +10,19 @@ 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 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.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 typing import Any, Dict
|
||||
from typing import Any, Dict, List
|
||||
|
||||
# Custom import form to select a course for student input
|
||||
class StudentCustomConfirmImportForm(ConfirmImportForm):
|
||||
@@ -56,23 +60,25 @@ class StudentResource(ModelResource):
|
||||
|
||||
# Form Class for Student course change
|
||||
class ChangeCourseForm(AdminActionForm):
|
||||
course = TypedMultipleChoiceField(choices=AircraftTypes)
|
||||
course = ModelChoiceField(queryset=Course.objects.all().order_by("ctype", "-cnumber"))
|
||||
|
||||
# Form class to assing aircrafts to students
|
||||
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
|
||||
list_display = ("surname", "name", "course", "course_color", "email", "phone", "username", "password", "active", )
|
||||
list_filter = ("course", "active", )
|
||||
search_fields = ("surname", "name", "phone", "email", )
|
||||
actions = ("change_course", "disable_students", "change_aircraft", )
|
||||
actions = ("change_course", "deactivate_students", "change_aircraft", "send_mail", )
|
||||
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:
|
||||
@@ -88,7 +94,7 @@ class StudentAdmin(ImportMixin, AdminActionFormsMixin, admin.ModelAdmin):
|
||||
return SafeText(obj.default_username())
|
||||
|
||||
@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():
|
||||
if q.user:
|
||||
q.user.is_staff = False
|
||||
@@ -102,22 +108,18 @@ class StudentAdmin(ImportMixin, AdminActionFormsMixin, admin.ModelAdmin):
|
||||
count: int = queryset.update(course=course)
|
||||
messages.success(request, f"{count} students updated to {course}")
|
||||
|
||||
@action_with_form(ChangeAircraftForm, description="Assign Aircraft")
|
||||
def change_aircraft(self, request: HttpRequest, queryset: QuerySet[Student], data: Dict[str, QuerySet[Aircraft]]):
|
||||
ac_types = [t.type for t in data["aircrafts"]]
|
||||
ac_query: Q = Q() # Build an or query to select all aircrafts of the specified types
|
||||
for a in ac_types:
|
||||
ac_query |= Q(type=a)
|
||||
aircrafts: QuerySet[Aircraft] = Aircraft.objects.filter(ac_query).all() # Execute query
|
||||
i: int = 0
|
||||
for student in queryset:
|
||||
student.aircrafts.clear()
|
||||
for ac in aircrafts:
|
||||
student.aircrafts.add(ac)
|
||||
student.save()
|
||||
i += 1
|
||||
@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}")
|
||||
|
||||
@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
|
||||
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)
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
import nested_admin
|
||||
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.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 datetime import date
|
||||
from ..custom.student_permissions import has_edit_permission
|
||||
|
||||
class TrainingForm(forms.ModelForm):
|
||||
model=Training
|
||||
@@ -21,17 +25,35 @@ class TrainingInLIne(nested_admin.NestedTabularInline):
|
||||
max_num = 7
|
||||
|
||||
formfield_overrides = {
|
||||
models.CharField: {'widget': TextInput(attrs={'size':'20'})},
|
||||
models.TextField: {'widget': Textarea(attrs={'rows':4, 'cols':35})},
|
||||
CharField: {'widget': TextInput(attrs={'size':'20'})},
|
||||
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
|
||||
def has_change_permission(self, request: HttpRequest, obj: WeekPreference | None = None) -> bool:
|
||||
if hasattr(request.user, 'student') and obj:
|
||||
current_week: int = date.today().isocalendar().week
|
||||
if current_week > obj.week:
|
||||
return False
|
||||
return True
|
||||
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)
|
||||
|
||||
@@ -2,12 +2,13 @@ import nested_admin
|
||||
|
||||
from django.forms import Form
|
||||
from django.db.models.query import QuerySet
|
||||
from django.contrib.auth.models import User
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.contrib import admin, messages
|
||||
from django.utils.translation import ngettext
|
||||
from django.utils.safestring import SafeText
|
||||
|
||||
from ..models.courses import CourseTypes
|
||||
from ..models.students import Student
|
||||
from ..models.missions import Training
|
||||
from ..models.weekpref import WeekPreference
|
||||
|
||||
@@ -15,17 +16,17 @@ from .training_adm import TrainingInLIne
|
||||
from .hourbuilding_adm import HourBuildingInLine
|
||||
|
||||
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 datetime import date
|
||||
from typing import Dict, List, Any
|
||||
|
||||
class WeekPreferenceAdmin(nested_admin.NestedPolymorphicModelAdmin):
|
||||
inlines = (TrainingInLIne, HourBuildingInLine, )
|
||||
list_display = ("week", "student__surname","student__name", "student__course", "course_color", "student_brief_mix",)
|
||||
list_filter = ("week", "student__course",)
|
||||
search_fields = ("student__surname","student__name",)
|
||||
actions = ("export",)
|
||||
list_display = ("week", "student__surname", "student__name", "student__course", "course_color", "student_brief_mix", )
|
||||
list_filter = ("week", "student__course", )
|
||||
search_fields = ("student__surname","student__name", )
|
||||
actions = ("export", )
|
||||
|
||||
@admin.action(description="Export Selected Preferences")
|
||||
def export(self, request: HttpRequest, queryset: QuerySet[WeekPreference]) -> HttpResponse | None:
|
||||
@@ -53,6 +54,24 @@ class WeekPreferenceAdmin(nested_admin.NestedPolymorphicModelAdmin):
|
||||
return []
|
||||
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
|
||||
def get_actions(self, request: HttpRequest) -> Dict[str, Any]:
|
||||
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
|
||||
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"):
|
||||
return qs.filter(student=request.user.student)
|
||||
# If admin show everything
|
||||
@@ -79,7 +98,7 @@ class WeekPreferenceAdmin(nested_admin.NestedPolymorphicModelAdmin):
|
||||
|
||||
# If student is current user making request
|
||||
if hasattr(request.user, "student"):
|
||||
student = request.user.student
|
||||
student: Student = request.user.student
|
||||
if "student" in form.base_fields:
|
||||
form.base_fields["student"].initial = student
|
||||
form.base_fields["student"].disabled = True
|
||||
@@ -88,33 +107,28 @@ class WeekPreferenceAdmin(nested_admin.NestedPolymorphicModelAdmin):
|
||||
|
||||
# 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:
|
||||
if hasattr(request.user, "student") and obj:
|
||||
current_week = date.today().isocalendar().week
|
||||
if current_week > obj.week:
|
||||
return False
|
||||
return True
|
||||
return has_edit_permission(request=request, obj=obj)
|
||||
|
||||
# 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:
|
||||
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
|
||||
def has_delete_permission(self, request: HttpRequest, obj: WeekPreference | None = None)-> bool:
|
||||
return self.has_change_permission(request, obj)
|
||||
|
||||
def changeform_view(self, request: HttpRequest, object_id: int | None = None, form_url: str = "", extra_context=None):
|
||||
def changeform_view(self, request: HttpRequest, object_id: int | None = None, form_url: str = "", extra_context = None):
|
||||
extra_context = extra_context or {}
|
||||
if hasattr(request.user, "student") and object_id:
|
||||
current_week = date.today().isocalendar().week
|
||||
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_and_continue"] = False
|
||||
extra_context["show_save_and_add_another"] = False
|
||||
extra_context["show_delete"] = False
|
||||
return super().changeform_view(request, object_id, form_url, extra_context)
|
||||
|
||||
def save_model(self, request: 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
|
||||
if hasattr(request.user, "student") and not obj.student_id:
|
||||
obj.student = request.user.student
|
||||
|
||||
21
cntmanage/flightslot/custom/student_permissions.py
Normal file
21
cntmanage/flightslot/custom/student_permissions.py
Normal 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
|
||||
@@ -7,8 +7,10 @@ class RedirectNonSuperuserFromAdminMiddleware:
|
||||
self.get_response = get_response
|
||||
|
||||
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 "/admin/" in request.path:
|
||||
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)
|
||||
|
||||
@@ -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'),
|
||||
),
|
||||
]
|
||||
@@ -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)),
|
||||
],
|
||||
),
|
||||
]
|
||||
@@ -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')),
|
||||
],
|
||||
),
|
||||
]
|
||||
@@ -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'},
|
||||
),
|
||||
]
|
||||
18
cntmanage/flightslot/migrations/0029_alter_course_ctype.py
Normal file
18
cntmanage/flightslot/migrations/0029_alter_course_ctype.py
Normal 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'),
|
||||
),
|
||||
]
|
||||
@@ -8,6 +8,7 @@ class AircraftTypes(models.TextChoices):
|
||||
PA34 = "PA34", _("Piper PA34")
|
||||
C182 = "C182", _("Cessna 182Q")
|
||||
P210 = "TWEN", _("Tecnam P2010")
|
||||
CP10 = "CP10", _("Cap 10")
|
||||
ALX40 = "FSTD", _("Alsim ALX40")
|
||||
|
||||
class Aircraft(models.Model):
|
||||
|
||||
79
cntmanage/flightslot/models/availabilities.py
Normal file
79
cntmanage/flightslot/models/availabilities.py
Normal 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]}."
|
||||
|
||||
@@ -8,6 +8,7 @@ class CourseTypes(models.TextChoices):
|
||||
PPL = "PPL", _("PPL")
|
||||
ATPL = "ATPL", _("ATPL")
|
||||
DISTANCE = "DL", _("DISTANCE")
|
||||
DISTANCE_VOLO = "DL_VOLO", _("DISTANCE_VOLO")
|
||||
OTHER = "OTHER",_("OTHER")
|
||||
|
||||
class Course(models.Model):
|
||||
|
||||
@@ -117,6 +117,8 @@ class HourBuildingLegFlight(HourBuildingLegBase):
|
||||
def save(self, *args, **kwargs):
|
||||
self.departure = self.departure.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)
|
||||
|
||||
def __str__(self):
|
||||
|
||||
99
cntmanage/flightslot/models/instructors.py
Normal file
99
cntmanage/flightslot/models/instructors.py
Normal 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"
|
||||
@@ -5,7 +5,7 @@ from datetime import timedelta
|
||||
from ..models.weekpref import WeekPreference
|
||||
from ..models.aircrafts import Aircraft
|
||||
|
||||
class MissionType(models.TextChoices):
|
||||
class MissionTypes(models.TextChoices):
|
||||
OTHER = "OTHER", _("OTHER")
|
||||
CHK = "CHK", _("CHK_6M")
|
||||
PPL = "PPL", _("PPL")
|
||||
@@ -24,8 +24,8 @@ class MissionProfile(models.Model):
|
||||
|
||||
mtype = models.CharField(
|
||||
null=False,
|
||||
default=MissionType.PPL,
|
||||
choices=MissionType,
|
||||
default=MissionTypes.PPL,
|
||||
choices=MissionTypes,
|
||||
verbose_name="Mission Type"
|
||||
)
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
from django.db import models
|
||||
from django.contrib.auth.models import User, Group
|
||||
|
||||
from phonenumber_field import modelfields
|
||||
|
||||
from ..models.courses import Course
|
||||
from ..models.aircrafts import Aircraft
|
||||
|
||||
@@ -11,12 +13,13 @@ class Student(models.Model):
|
||||
|
||||
email = models.EmailField(
|
||||
null=False,
|
||||
db_index=True
|
||||
db_index=True,
|
||||
unique=True
|
||||
)
|
||||
|
||||
phone = models.CharField(
|
||||
phone = modelfields.PhoneNumberField(
|
||||
null=True,
|
||||
max_length=16
|
||||
unique=True
|
||||
)
|
||||
|
||||
name = models.CharField(
|
||||
@@ -52,7 +55,10 @@ class Student(models.Model):
|
||||
)
|
||||
|
||||
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:
|
||||
if self.pk and self.user:
|
||||
@@ -74,8 +80,8 @@ class Student(models.Model):
|
||||
counter += 1
|
||||
# Create user
|
||||
user: User = User.objects.create_user(
|
||||
first_name=self.name,
|
||||
last_name=self.surname,
|
||||
first_name=self.name.capitalize(),
|
||||
last_name=self.surname.capitalize(),
|
||||
username=username,
|
||||
email=self.email,
|
||||
password=self.default_password(),
|
||||
|
||||
@@ -20,7 +20,7 @@ class WeekPreference(models.Model):
|
||||
Student,
|
||||
null=False,
|
||||
db_index=True,
|
||||
on_delete=models.DO_NOTHING,
|
||||
on_delete=models.CASCADE,
|
||||
verbose_name="Student Selection"
|
||||
)
|
||||
|
||||
|
||||
49
cntmanage/poetry.lock
generated
49
cntmanage/poetry.lock
generated
@@ -66,6 +66,21 @@ files = [
|
||||
[package.dependencies]
|
||||
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]]
|
||||
name = "django-colorfield"
|
||||
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"]
|
||||
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"
|
||||
@@ -185,6 +220,18 @@ files = [
|
||||
[package.dependencies]
|
||||
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]]
|
||||
name = "pillow"
|
||||
version = "12.0.0"
|
||||
@@ -436,4 +483,4 @@ files = [
|
||||
[metadata]
|
||||
lock-version = "2.1"
|
||||
python-versions = "^3.12"
|
||||
content-hash = "e932d0af75c888d83fecefaaad1d018c508881a3bfde2ea640a82790e3567855"
|
||||
content-hash = "b45301c627836abac1ef9628e67fc63189b03e7857a7a003854aa1fb30f2a4a3"
|
||||
|
||||
@@ -18,6 +18,8 @@ django-colorfield = "^0.14.0"
|
||||
openpyxl = "^3.1.5"
|
||||
django-admin-action-forms = "^2.2.1"
|
||||
django-polymorphic = "^4.1.0"
|
||||
django-phonenumber-field = {extras = ["phonenumberslite"], version = "^8.4.0"}
|
||||
django-admin-confirm = "^1.0.1"
|
||||
|
||||
|
||||
[build-system]
|
||||
|
||||
BIN
cntmanage/static/cantorair.png
Normal file
BIN
cntmanage/static/cantorair.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.6 KiB |
@@ -1 +0,0 @@
|
||||
admin: CantorAdmin2k25
|
||||
@@ -8,8 +8,8 @@
|
||||
|
||||
<h1 id="site-name">
|
||||
<a href="{% url 'admin:index' %}" style="color: #0b1728;">
|
||||
<img src="{% static 'cantorair_blue.jpg' %}"
|
||||
height="60px"
|
||||
<img src="{% static 'cantorair.png' %}"
|
||||
height="70px"
|
||||
style="margin-right: 20px;"/>
|
||||
</a>
|
||||
</h1>
|
||||
|
||||
408
cntmanage/templates/email/mail.html
Normal file
408
cntmanage/templates/email/mail.html
Normal 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> 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
8
note.txt
Normal 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
|
||||
Reference in New Issue
Block a user