Compare commits
8 Commits
email
...
flightslot
| Author | SHA1 | Date | |
|---|---|---|---|
| 46b5068c7b | |||
| 6a356e6ea9 | |||
| d79f30e96c | |||
| da8c8db0d2 | |||
| 99d24583d6 | |||
| aa8e4e761f | |||
| 46e6749fd5 | |||
| 1e17a8fe2d |
@@ -1,5 +1,5 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
git pull
|
git pull
|
||||||
./build
|
./build.sh
|
||||||
docker compose up -d
|
docker compose up -d
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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", )
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -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),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -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)),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -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)),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -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),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -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}"
|
||||||
|
|||||||
@@ -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]}."
|
||||||
|
|||||||
Reference in New Issue
Block a user