16 Commits

27 changed files with 768 additions and 30 deletions

View File

@@ -31,6 +31,7 @@ ALLOWED_HOSTS = []
# Application definition
INSTALLED_APPS = [
'admin_confirm',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
@@ -143,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

View File

@@ -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',
@@ -74,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': [
@@ -138,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"

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

View File

@@ -4,6 +4,7 @@ from django.db.models.query import QuerySet
from openpyxl import Workbook
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
from openpyxl.utils import get_column_letter
from openpyxl.worksheet.page import PageMargins
from ..models.courses import CourseTypes
from ..models.missions import Training
@@ -32,12 +33,15 @@ def export_selected(request: HttpRequest, queryset: QuerySet[WeekPreference]) ->
raise Exception("Empty queryset")
# 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
weeks = queryset.order_by("week").distinct("week").all()
# 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['Content-Disposition'] = f'attachment; filename="{filename}"'
@@ -47,10 +51,6 @@ def export_selected(request: HttpRequest, queryset: QuerySet[WeekPreference]) ->
if not ws:
raise Exception("Export: cannot select active workbook")
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
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)]
@@ -81,11 +81,19 @@ def export_selected(request: HttpRequest, queryset: QuerySet[WeekPreference]) ->
border_all: Border = Border(bottom=border_thick, top=border_thick, left=border_thick, right=None)
# Scrittura header
head_size: int = len(headers)
for col, h in enumerate(headers, start=1):
cell = ws.cell(row=1, column=col, value=h)
cell.fill = header_fill
cell.font = bold_white
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 ###
# Fill worksheet with EVERY training and hb for every student
@@ -181,6 +189,7 @@ def export_selected(request: HttpRequest, queryset: QuerySet[WeekPreference]) ->
if c > course_index and c <= note_index:
if len(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
@@ -225,9 +234,9 @@ 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)
# Keep the largest column
max_len: List[int] = []
col_letter: str = "A"
for column_cells in ws.columns:
col_letter: str = "A"
max_len: List[int] = []
for cell in column_cells:
cell_lines = str(cell.value).splitlines()
if len(cell_lines) == 0:
@@ -236,9 +245,23 @@ def export_selected(request: HttpRequest, queryset: QuerySet[WeekPreference]) ->
length: int = max(max_len)
if 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 ###
# 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
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

@@ -2,8 +2,8 @@ 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

View File

@@ -10,13 +10,15 @@ 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 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
@@ -64,17 +66,19 @@ class ChangeCourseForm(AdminActionForm):
class ChangeAircraftForm(AdminActionForm):
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_display = ("surname", "name", "course", "course_color", "email", "phone", "username", "password", "active", "mail_sent")
list_filter = ("course", "active", )
search_fields = ("surname", "name", "phone", "email", )
actions = ("change_course", "deactivate_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:
@@ -111,6 +115,11 @@ class StudentAdmin(ImportMixin, AdminActionFormsMixin, admin.ModelAdmin):
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)

View File

@@ -45,6 +45,8 @@ class TrainingInLIne(nested_admin.NestedTabularInline):
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)

View File

@@ -7,6 +7,7 @@ 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
@@ -18,15 +19,15 @@ 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
from datetime import date, datetime
from typing import Dict, List, Tuple, 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", "inserted")
list_filter = ("week", "student__course", )
search_fields = ("student__surname","student__name", )
actions = ("export", )
readonly_fields = ("inserted", )
@admin.action(description="Export Selected Preferences")
def export(self, request: HttpRequest, queryset: QuerySet[WeekPreference]) -> HttpResponse | None:
@@ -47,6 +48,12 @@ class WeekPreferenceAdmin(nested_admin.NestedPolymorphicModelAdmin):
return SafeText("")
return course_color(obj.student.course.color)
# Hide brief mix count and inserted fields for students
def get_list_display(self, request: HttpRequest) -> Tuple:
if hasattr(request.user, "student"):
return ("week", "student__surname", "student__name", "student__course", "course_color", )
return ("week", "student__surname", "student__name", "student__course", "course_color", "student_brief_mix", "inserted")
# If a user is registered as student hide filters
def get_list_filter(self, request: HttpRequest) -> List[str]:
list_filter = super().get_list_filter(request)
@@ -54,6 +61,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)
@@ -72,7 +97,6 @@ class WeekPreferenceAdmin(nested_admin.NestedPolymorphicModelAdmin):
def get_form(self, request: HttpRequest, obj: WeekPreference | None = None, **kwargs: Dict[str, Any]) -> Form:
form: Form = super().get_form(request, obj, **kwargs)
current_week = date.today().isocalendar().week
# If form contains the week field
if "week" in form.base_fields:
# Set default value as current week

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

@@ -0,0 +1,19 @@
# Generated by Django 5.2.8 on 2025-12-10 10:47
import datetime
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('flightslot', '0031_student_mail_sent_alter_student_phone'),
]
operations = [
migrations.AlterField(
model_name='weekpreference',
name='inserted',
field=models.DateTimeField(default=datetime.datetime(2025, 12, 10, 10, 47, 2, 305637)),
),
]

View File

@@ -0,0 +1,19 @@
# Generated by Django 5.2.8 on 2025-12-10 11:02
import datetime
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('flightslot', '0032_alter_weekpreference_inserted'),
]
operations = [
migrations.AlterField(
model_name='weekpreference',
name='inserted',
field=models.DateTimeField(default=datetime.datetime(2025, 12, 10, 11, 2, 58, 110972)),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.2.8 on 2025-12-10 11:20
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('flightslot', '0033_alter_weekpreference_inserted'),
]
operations = [
migrations.AlterField(
model_name='weekpreference',
name='inserted',
field=models.DateTimeField(auto_now_add=True),
),
]

View File

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

View File

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

View File

@@ -19,6 +19,7 @@ class Student(models.Model):
phone = modelfields.PhoneNumberField(
null=True,
db_index=True,
unique=True
)
@@ -54,6 +55,11 @@ class Student(models.Model):
Aircraft
)
mail_sent = models.BooleanField(
null=False,
default=False
)
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}"

View File

@@ -24,5 +24,10 @@ class WeekPreference(models.Model):
verbose_name="Student Selection"
)
inserted = models.DateTimeField(
null=False,
auto_now_add=True
)
def __str__(self):
return f"Week {self.week} - {self.student.surname} {self.student.name[0]}."

17
cntmanage/poetry.lock generated
View File

@@ -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"
@@ -468,4 +483,4 @@ files = [
[metadata]
lock-version = "2.1"
python-versions = "^3.12"
content-hash = "5147211bd07992aff3915544175c8d95d77511b9d42273d17c4452fbef9299eb"
content-hash = "b45301c627836abac1ef9628e67fc63189b03e7857a7a003854aa1fb30f2a4a3"

View File

@@ -19,6 +19,7 @@ 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]

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

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>

View File

@@ -1,6 +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)
lo studente vede solo le missioni della sua fase PPL->PPL ATPL-> tutto
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