8 Commits

12 changed files with 178 additions and 21 deletions

View File

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

View File

@@ -4,6 +4,7 @@ from django.db.models.query import QuerySet
from openpyxl import Workbook from openpyxl import Workbook
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
from openpyxl.utils import get_column_letter from openpyxl.utils import get_column_letter
from openpyxl.worksheet.page import PageMargins
from ..models.courses import CourseTypes from ..models.courses import CourseTypes
from ..models.missions import Training from ..models.missions import Training
@@ -32,12 +33,15 @@ def export_selected(request: HttpRequest, queryset: QuerySet[WeekPreference]) ->
raise Exception("Empty queryset") raise Exception("Empty queryset")
# Init Variables # Init Variables
year = date.today().year today = date.today()
year = today.year
month = today.month
day = today.day
week = queryset.first().week if queryset.first() else date.today().isocalendar().week week = queryset.first().week if queryset.first() else date.today().isocalendar().week
weeks = queryset.order_by("week").distinct("week").all() weeks = queryset.order_by("week").distinct("week").all()
# Prepare export filename and http content # Prepare export filename and http content
filename = f"{year}_week{'+'.join([str(w.week) for w in weeks])}_export.xlsx" filename = f"{year}{month}{day}_week_{'+'.join([str(w.week) for w in weeks])}_export.xlsx"
response = HttpResponse(content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet') response = HttpResponse(content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
response['Content-Disposition'] = f'attachment; filename="{filename}"' response['Content-Disposition'] = f'attachment; filename="{filename}"'
@@ -47,10 +51,6 @@ def export_selected(request: HttpRequest, queryset: QuerySet[WeekPreference]) ->
if not ws: if not ws:
raise Exception("Export: cannot select active workbook") raise Exception("Export: cannot select active workbook")
ws.title = f"Week Preferences" ws.title = f"Week Preferences"
ws.page_setup.orientation = ws.ORIENTATION_LANDSCAPE
ws.page_setup.paperSize = ws.PAPERSIZE_A3
ws.page_setup.fitToHeight = 0
ws.page_setup.fitToWidth = 1
# Header titles # Header titles
days = [f"{datetime.strptime(f"{year} {week} {x}", "%G %V %u").strftime("%A")} {datetime.strptime(f"{year} {week} {x}", "%G %V %u").day}" for x in range(1,8)] days = [f"{datetime.strptime(f"{year} {week} {x}", "%G %V %u").strftime("%A")} {datetime.strptime(f"{year} {week} {x}", "%G %V %u").day}" for x in range(1,8)]
@@ -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) border_all: Border = Border(bottom=border_thick, top=border_thick, left=border_thick, right=None)
# Scrittura header # Scrittura header
head_size: int = len(headers)
for col, h in enumerate(headers, start=1): for col, h in enumerate(headers, start=1):
cell = ws.cell(row=1, column=col, value=h) cell = ws.cell(row=1, column=col, value=h)
cell.fill = header_fill cell.fill = header_fill
cell.font = bold_white cell.font = bold_white
cell.alignment = center cell.alignment = center
match col:
case int(1):
cell.border = Border(top=border_thick, bottom=border_thick, left=border_thick)
case int(head_size):
cell.border = Border(top=border_thick, bottom=border_thick, right=border_thick)
case _:
cell.border = Border(top=border_thick, bottom=border_thick)
### Start of Student Loop ### ### Start of Student Loop ###
# Fill worksheet with EVERY training and hb for every student # Fill worksheet with EVERY training and hb for every student
@@ -181,6 +189,7 @@ def export_selected(request: HttpRequest, queryset: QuerySet[WeekPreference]) ->
if c > course_index and c <= note_index: if c > course_index and c <= note_index:
if len(cell_content): if len(cell_content):
cell.fill = PatternFill('solid', fgColor=PALETTE[ri % len(PALETTE)].lstrip("#").lower()) cell.fill = PatternFill('solid', fgColor=PALETTE[ri % len(PALETTE)].lstrip("#").lower())
if MERGE: if MERGE:
prev_cell_val: str = row_content[0] prev_cell_val: str = row_content[0]
merge_start: bool = False 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) ws.merge_cells(start_row=student_start, end_row=student_end, start_column=mail_index, end_column=mail_index)
# Keep the largest column # Keep the largest column
max_len: List[int] = []
col_letter: str = "A"
for column_cells in ws.columns: for column_cells in ws.columns:
col_letter: str = "A"
max_len: List[int] = []
for cell in column_cells: for cell in column_cells:
cell_lines = str(cell.value).splitlines() cell_lines = str(cell.value).splitlines()
if len(cell_lines) == 0: if len(cell_lines) == 0:
@@ -236,9 +245,23 @@ def export_selected(request: HttpRequest, queryset: QuerySet[WeekPreference]) ->
length: int = max(max_len) length: int = max(max_len)
if column_cells[0].column: if column_cells[0].column:
col_letter = get_column_letter(column_cells[0].column) col_letter = get_column_letter(column_cells[0].column)
ws.column_dimensions[col_letter].width = length + 2 ws.column_dimensions[col_letter].width = min(length + 2, 35)
### End of Student Loop ### ### End of Student Loop ###
# Set paper size and format
ws.page_setup.orientation = ws.ORIENTATION_LANDSCAPE
ws.page_setup.paperSize = ws.PAPERSIZE_A3
ws.page_setup.fitToHeight = 0
ws.page_setup.fitToWidth = 1
ws.print_options.horizontalCentered = True
ws.page_setup.fitToPage = True
ws.page_margins = PageMargins(
left=0.25, right=0.25,
top=0.75, bottom=0.75,
header=0.3, footer=0.3
)
ws.print_area = ws.calculate_dimension()
# Save document in HttpResponse # Save document in HttpResponse
wb.save(response) wb.save(response)

View File

@@ -31,6 +31,7 @@ def send_mail_password(request: HttpRequest, queryset: QuerySet[Student]) -> Non
img.add_header("Content-Disposition", "inline", filename="cantorair.png") img.add_header("Content-Disposition", "inline", filename="cantorair.png")
# build mail list filling template # build mail list filling template
queryset = queryset.filter(mail_sent=False)
mails: List[EmailMultiAlternatives] = [] mails: List[EmailMultiAlternatives] = []
for student in queryset: for student in queryset:
if not student.user or not student.email: # skip student if has not an associated user if not student.user or not student.email: # skip student if has not an associated user
@@ -56,18 +57,25 @@ def send_mail_password(request: HttpRequest, queryset: QuerySet[Student]) -> Non
mail.attach(filename=img) mail.attach(filename=img)
mail.attach_alternative(content=html_message, mimetype="text/html") mail.attach_alternative(content=html_message, mimetype="text/html")
mails.append(mail) mails.append(mail)
student.mail_sent = True
student.save()
except Exception as e: except Exception as e:
messages.error(request=request, message=f"General Error: {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 # Open only one conenction and send mass email
try: try:
sent: int = 0
with get_connection() as conn: with get_connection() as conn:
conn.send_messages(mails) sent = conn.send_messages(mails)
except SMTPException as e: except SMTPException as e:
messages.error(request=request, message=f"Send Mail SMTP error: {e.strerror}") messages.error(request=request, message=f"Send Mail SMTP error: {e.strerror}")
except Exception as e: except Exception as e:
messages.error(request=request, message=f"Send Mail General error: {e}") messages.error(request=request, message=f"Send Mail General error: {e}")
else: else:
messages.success(request=request, message=f"Successfully sent {len(mails)} messages") messages.success(request=request, message=f"Successfully sent {sent} messages")
return return

View File

@@ -69,7 +69,7 @@ class ChangeAircraftForm(AdminActionForm):
class StudentAdmin(ImportMixin, AdminConfirmMixin, AdminActionFormsMixin, admin.ModelAdmin): class StudentAdmin(ImportMixin, AdminConfirmMixin, AdminActionFormsMixin, admin.ModelAdmin):
model = Student model = Student
list_display = ("surname", "name", "course", "course_color", "email", "phone", "username", "password", "active", ) list_display = ("surname", "name", "course", "course_color", "email", "phone", "username", "password", "active", "mail_sent")
list_filter = ("course", "active", ) list_filter = ("course", "active", )
search_fields = ("surname", "name", "phone", "email", ) search_fields = ("surname", "name", "phone", "email", )
actions = ("change_course", "deactivate_students", "change_aircraft", "send_mail", ) actions = ("change_course", "deactivate_students", "change_aircraft", "send_mail", )

View File

@@ -19,14 +19,15 @@ from ..custom.colortag import course_color
from ..custom.student_permissions import has_edit_permission, has_week_add_permission from ..custom.student_permissions import has_edit_permission, has_week_add_permission
from ..actions.exportweek import export_selected from ..actions.exportweek import export_selected
from datetime import date from datetime import date, datetime
from typing import Dict, List, Any from typing import Dict, List, Tuple, Any
class WeekPreferenceAdmin(nested_admin.NestedPolymorphicModelAdmin): class WeekPreferenceAdmin(nested_admin.NestedPolymorphicModelAdmin):
list_display = ("week", "student__surname", "student__name", "student__course", "course_color", "student_brief_mix", ) #list_display = ("week", "student__surname", "student__name", "student__course", "course_color", "student_brief_mix", "inserted")
list_filter = ("week", "student__course", ) list_filter = ("week", "student__course", )
search_fields = ("student__surname","student__name", ) search_fields = ("student__surname","student__name", )
actions = ("export", ) actions = ("export", )
readonly_fields = ("inserted", )
@admin.action(description="Export Selected Preferences") @admin.action(description="Export Selected Preferences")
def export(self, request: HttpRequest, queryset: QuerySet[WeekPreference]) -> HttpResponse | None: def export(self, request: HttpRequest, queryset: QuerySet[WeekPreference]) -> HttpResponse | None:
@@ -47,6 +48,12 @@ class WeekPreferenceAdmin(nested_admin.NestedPolymorphicModelAdmin):
return SafeText("") return SafeText("")
return course_color(obj.student.course.color) 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 # If a user is registered as student hide filters
def get_list_filter(self, request: HttpRequest) -> List[str]: def get_list_filter(self, request: HttpRequest) -> List[str]:
list_filter = super().get_list_filter(request) list_filter = super().get_list_filter(request)
@@ -68,9 +75,9 @@ class WeekPreferenceAdmin(nested_admin.NestedPolymorphicModelAdmin):
return (TrainingInLIne(self.model, self.admin_site), ) return (TrainingInLIne(self.model, self.admin_site), )
else: else:
return ( return (
TrainingInLIne(self.model, self.admin_site), TrainingInLIne(self.model, self.admin_site),
HourBuildingInLine(self.model, self.admin_site), HourBuildingInLine(self.model, self.admin_site),
) )
# If a user is registered as student do not show actions # If a user is registered as student do not show actions
def get_actions(self, request: HttpRequest) -> Dict[str, Any]: def get_actions(self, request: HttpRequest) -> Dict[str, Any]:
@@ -90,7 +97,6 @@ class WeekPreferenceAdmin(nested_admin.NestedPolymorphicModelAdmin):
def get_form(self, request: HttpRequest, obj: WeekPreference | None = None, **kwargs: Dict[str, Any]) -> Form: def get_form(self, request: HttpRequest, obj: WeekPreference | None = None, **kwargs: Dict[str, Any]) -> Form:
form: Form = super().get_form(request, obj, **kwargs) form: Form = super().get_form(request, obj, **kwargs)
current_week = date.today().isocalendar().week current_week = date.today().isocalendar().week
# If form contains the week field # If form contains the week field
if "week" in form.base_fields: if "week" in form.base_fields:
# Set default value as current week # Set default value as current week

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

@@ -19,6 +19,7 @@ class Student(models.Model):
phone = modelfields.PhoneNumberField( phone = modelfields.PhoneNumberField(
null=True, null=True,
db_index=True,
unique=True unique=True
) )
@@ -54,6 +55,11 @@ class Student(models.Model):
Aircraft Aircraft
) )
mail_sent = models.BooleanField(
null=False,
default=False
)
def default_password(self) -> str: # Maximum 4 digits for passowrd def default_password(self) -> str: # Maximum 4 digits for passowrd
if self.pk: if self.pk:
return f"{self.name.lower()[0]}{self.surname.lower()}{self.id % 10000}" 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" verbose_name="Student Selection"
) )
inserted = models.DateTimeField(
null=False,
auto_now_add=True
)
def __str__(self): def __str__(self):
return f"Week {self.week} - {self.student.surname} {self.student.name[0]}." return f"Week {self.week} - {self.student.surname} {self.student.name[0]}."