Compare commits
2 Commits
bb9ff3a86c
...
231a3e9861
| Author | SHA1 | Date | |
|---|---|---|---|
| 231a3e9861 | |||
| a91e0cd7bc |
172
techdb/flightslot/actions/exportweek.py
Normal file
172
techdb/flightslot/actions/exportweek.py
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
from django.http import HttpRequest, HttpResponse
|
||||||
|
from django.db.models.query import QuerySet
|
||||||
|
|
||||||
|
from openpyxl import Workbook
|
||||||
|
from openpyxl.styles import Font, PatternFill, Alignment
|
||||||
|
from openpyxl.utils import get_column_letter
|
||||||
|
|
||||||
|
from datetime import date, datetime
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from ..models.weekpref import WeekPreference
|
||||||
|
from ..models.missions import Training
|
||||||
|
from ..models.hourbuildings import HourBuilding,HourBuildingLeg
|
||||||
|
|
||||||
|
|
||||||
|
def export_selected(request: HttpRequest, queryset: QuerySet[WeekPreference]) -> HttpResponse:
|
||||||
|
|
||||||
|
if not queryset.first():
|
||||||
|
raise Exception("Empty queryset")
|
||||||
|
|
||||||
|
# Init Variables
|
||||||
|
year = date.today().year
|
||||||
|
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"
|
||||||
|
response = HttpResponse(content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
|
||||||
|
response['Content-Disposition'] = f'attachment; filename="{filename}"'
|
||||||
|
|
||||||
|
# Create workbook and sheet
|
||||||
|
wb = Workbook()
|
||||||
|
ws = wb.active
|
||||||
|
if not ws:
|
||||||
|
raise Exception("Export: cannot select active workbook")
|
||||||
|
ws.title = f"Week Preferences"
|
||||||
|
|
||||||
|
# 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)]
|
||||||
|
headers = ["Week", "Student", "Course", *days, "Cell.", "Mail", "Notes"]
|
||||||
|
|
||||||
|
# Header fields positions
|
||||||
|
student_index: int = headers.index("Student") + 1
|
||||||
|
course_index: int = headers.index("Course") + 1
|
||||||
|
cell_index: int = headers.index("Cell.") + 1
|
||||||
|
mail_index: int = headers.index("Mail") + 1
|
||||||
|
|
||||||
|
# Stile header
|
||||||
|
header_fill = PatternFill("solid", fgColor="0e005c")
|
||||||
|
bold_white = Font(color="FFFFFF", bold=True)
|
||||||
|
bold_black = Font(color="000000", bold=True)
|
||||||
|
center = Alignment(horizontal="center", vertical="center")
|
||||||
|
|
||||||
|
# Scrittura header
|
||||||
|
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
|
||||||
|
|
||||||
|
# Fill worksheet with EVERY training and hb for every student
|
||||||
|
row: int = 2
|
||||||
|
row_offset: int = 0
|
||||||
|
for i, q in enumerate(queryset.order_by("student__surname", "student__name", "student__course"), start=1):
|
||||||
|
student_data: List[str]
|
||||||
|
student_phone: str = q.student.phone if q.student.phone else ""
|
||||||
|
student_email: str = q.student.email
|
||||||
|
if q.student.course:
|
||||||
|
student_data = [f"{q.student.surname} {q.student.name}", f"{q.student.course.ctype}-{q.student.course.cnumber}"]
|
||||||
|
else:
|
||||||
|
student_data = [f"{q.student.surname} {q.student.name}", f"No Course Assigned"]
|
||||||
|
|
||||||
|
# Fill Training mission rows
|
||||||
|
mission_name: 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_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_notes = t.notes if t.notes else "--"
|
||||||
|
mission_data.append([str(q.week), *student_data, *mission_days, student_phone, student_email, mission_notes])
|
||||||
|
|
||||||
|
# Fill HourBuilding rows
|
||||||
|
hb_name: str
|
||||||
|
hb_days: List[str]
|
||||||
|
hb_notes: 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 = f"{h.notes}\n----\n" if h.notes else ""
|
||||||
|
hb_legs = HourBuildingLeg.objects.filter(hb_id = h.id)
|
||||||
|
for hh in hb_legs:
|
||||||
|
hb_notes += f"{hh.departure} -> {hh.destination} [{hh.time}]\n" if not hh.stop else f"STOP at {hh.departure} [{hh.time}]\n"
|
||||||
|
hb_notes.strip('\r')
|
||||||
|
hb_data.append([str(q.week), *student_data, *hb_days, str(q.student.phone), q.student.email, hb_notes])
|
||||||
|
|
||||||
|
# Build rows for table
|
||||||
|
all_data: List[List[str]] = mission_data + hb_data
|
||||||
|
student_start: int = row + row_offset
|
||||||
|
for r in all_data:
|
||||||
|
for j, c in enumerate(r, start=1):
|
||||||
|
cell = ws.cell(row = row + row_offset, column = j, value = c)
|
||||||
|
cell.alignment = center
|
||||||
|
# Format Student Name
|
||||||
|
if j == student_index:
|
||||||
|
cell.font = bold_black
|
||||||
|
# Format Course Column
|
||||||
|
if j == course_index and q.student.course:
|
||||||
|
cell.fill = PatternFill("solid", fgColor=str(q.student.course.color).lstrip('#').lower())
|
||||||
|
|
||||||
|
prev_cell_val: str = r[0]
|
||||||
|
merge_start: bool = False
|
||||||
|
merge_col_start: int = 1
|
||||||
|
for c, v in enumerate(r, start=1):
|
||||||
|
# Merge cells in the row
|
||||||
|
if v == prev_cell_val and not merge_start:
|
||||||
|
merge_start = True
|
||||||
|
merge_col_start = c-1 # start merge from previous column
|
||||||
|
elif v != 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 = v
|
||||||
|
|
||||||
|
# Incement row counter
|
||||||
|
row_offset += 1
|
||||||
|
|
||||||
|
# End week preferences for this student
|
||||||
|
student_end: int = row + row_offset -1
|
||||||
|
# Merge Name
|
||||||
|
ws.merge_cells(start_row=student_start, end_row=student_end, start_column=student_index, end_column=student_index)
|
||||||
|
# Merge Course
|
||||||
|
ws.merge_cells(start_row=student_start, end_row=student_end, start_column=course_index, end_column=course_index)
|
||||||
|
# Merge Cell
|
||||||
|
ws.merge_cells(start_row=student_start, end_row=student_end, start_column=cell_index, end_column=cell_index)
|
||||||
|
# Merge Mail
|
||||||
|
ws.merge_cells(start_row=student_start, end_row=student_end, start_column=mail_index, end_column=mail_index)
|
||||||
|
|
||||||
|
# Keep the largest column
|
||||||
|
for column_cells in ws.columns:
|
||||||
|
length: int = max(len(str(cell.value)) for cell in column_cells)
|
||||||
|
col_letter: str = "A"
|
||||||
|
if column_cells[0].column:
|
||||||
|
get_column_letter(column_cells[0].column)
|
||||||
|
ws.column_dimensions[col_letter].width = length
|
||||||
|
|
||||||
|
# Save document in HttpResponse
|
||||||
|
wb.save(response)
|
||||||
|
|
||||||
|
return response
|
||||||
@@ -1,13 +1,14 @@
|
|||||||
from django import forms
|
from django import forms
|
||||||
from django.db.models.query import QuerySet
|
from django.db.models.query import QuerySet
|
||||||
from django.contrib import admin
|
|
||||||
from django.http import HttpRequest, HttpResponse
|
from django.http import HttpRequest, HttpResponse
|
||||||
|
from django.contrib import admin
|
||||||
|
from django.contrib import messages
|
||||||
|
from django.utils.translation import ngettext
|
||||||
from django.utils.safestring import SafeText
|
from django.utils.safestring import SafeText
|
||||||
from durationwidget.widgets import TimeDurationWidget
|
from durationwidget.widgets import TimeDurationWidget
|
||||||
from datetime import date
|
from datetime import date
|
||||||
|
|
||||||
import nested_admin
|
import nested_admin
|
||||||
import csv
|
|
||||||
|
|
||||||
from .models.courses import Course
|
from .models.courses import Course
|
||||||
from .models.hourbuildings import HourBuilding, HourBuildingLeg
|
from .models.hourbuildings import HourBuilding, HourBuildingLeg
|
||||||
@@ -18,6 +19,8 @@ from .models.weekpref import WeekPreference
|
|||||||
from .custom.colortag import course_color
|
from .custom.colortag import course_color
|
||||||
from .custom.defpassword import default_password
|
from .custom.defpassword import default_password
|
||||||
|
|
||||||
|
from .actions.exportweek import export_selected
|
||||||
|
|
||||||
class TrainingForm(forms.ModelForm):
|
class TrainingForm(forms.ModelForm):
|
||||||
model=Training
|
model=Training
|
||||||
|
|
||||||
@@ -56,31 +59,17 @@ class TrainingInLIne(nested_admin.NestedTabularInline):
|
|||||||
max_num = 7
|
max_num = 7
|
||||||
|
|
||||||
class WeekPreferenceAdmin(nested_admin.NestedModelAdmin):
|
class WeekPreferenceAdmin(nested_admin.NestedModelAdmin):
|
||||||
inlines = [TrainingInLIne, HourBuildingInLine]
|
inlines = (TrainingInLIne, HourBuildingInLine,)
|
||||||
list_display = ("week", "student__name", "student__surname", "student__course", "course_color", "student_brief_mix")
|
list_display = ("week", "student__name", "student__surname", "student__course", "course_color", "student_brief_mix",)
|
||||||
list_filter = ("week", "student__course", "student")
|
list_filter = ("week", "student__course", "student",)
|
||||||
actions = ("export_selected",)
|
actions = ("export",)
|
||||||
|
|
||||||
@admin.action(description="Export Selected Preferences")
|
@admin.action(description="Export Selected Preferences")
|
||||||
def export_selected(modeladmin, request: HttpRequest, queryset: QuerySet[WeekPreference]) -> HttpResponse:
|
def export(weekpreferenceadmin, request: HttpRequest, queryset: QuerySet[WeekPreference]) -> HttpResponse | None:
|
||||||
filename = "weekpreferences_export.csv"
|
if queryset.count() == 0:
|
||||||
response = HttpResponse(content_type='text/csv')
|
return None
|
||||||
response['Content-Disposition'] = f'attachment; filename="{filename}"'
|
weekpreferenceadmin.message_user(request, ngettext("Exporting %d row", "Exporting %d rows", queryset.count()) % queryset.count(), messages.SUCCESS)
|
||||||
|
return export_selected(request=request, queryset=queryset)
|
||||||
writer = csv.writer(response)
|
|
||||||
# intestazione — scegli i campi che vuoi esportare
|
|
||||||
writer.writerow(['student_name', 'student_surname', 'course', 'course_color', "training", "hourbuilding"])
|
|
||||||
|
|
||||||
for q in queryset:
|
|
||||||
writer.writerow([
|
|
||||||
q.student.name,
|
|
||||||
q.student.surname,
|
|
||||||
q.student.course,
|
|
||||||
q.student.course.color,
|
|
||||||
"+".join([f"{t.mission.mtype}-{t.mission.mnum}" for t in Training.objects.filter(weekpref = q.id)]),
|
|
||||||
"+".join([f"HB_{t.aircraft}" for t in HourBuilding.objects.filter(weekpref = q.id)]),
|
|
||||||
])
|
|
||||||
return response
|
|
||||||
|
|
||||||
@admin.display(description="Mission Count")
|
@admin.display(description="Mission Count")
|
||||||
def student_brief_mix(self, obj: WeekPreference) -> SafeText:
|
def student_brief_mix(self, obj: WeekPreference) -> SafeText:
|
||||||
@@ -88,7 +77,6 @@ class WeekPreferenceAdmin(nested_admin.NestedModelAdmin):
|
|||||||
return SafeText("")
|
return SafeText("")
|
||||||
return SafeText(f"{Training.objects.filter(weekpref = obj.id).count()}")
|
return SafeText(f"{Training.objects.filter(weekpref = obj.id).count()}")
|
||||||
|
|
||||||
|
|
||||||
@admin.display(description="Color")
|
@admin.display(description="Color")
|
||||||
def course_color(self, obj: WeekPreference) -> SafeText:
|
def course_color(self, obj: WeekPreference) -> SafeText:
|
||||||
if not obj.student.course:
|
if not obj.student.course:
|
||||||
|
|||||||
29
techdb/poetry.lock
generated
29
techdb/poetry.lock
generated
@@ -128,6 +128,33 @@ python-monkey-business = ">=1.0.0"
|
|||||||
dev = ["Pillow", "black", "dj-database-url", "django-selenosis", "flake8", "pytest", "pytest-cov", "pytest-django", "pytest-xdist", "selenium"]
|
dev = ["Pillow", "black", "dj-database-url", "django-selenosis", "flake8", "pytest", "pytest-cov", "pytest-django", "pytest-xdist", "selenium"]
|
||||||
test = ["Pillow", "dj-database-url", "django-selenosis", "pytest", "pytest-cov", "pytest-django", "pytest-xdist", "selenium"]
|
test = ["Pillow", "dj-database-url", "django-selenosis", "pytest", "pytest-cov", "pytest-django", "pytest-xdist", "selenium"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "et-xmlfile"
|
||||||
|
version = "2.0.0"
|
||||||
|
description = "An implementation of lxml.xmlfile for the standard library"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.8"
|
||||||
|
groups = ["main"]
|
||||||
|
files = [
|
||||||
|
{file = "et_xmlfile-2.0.0-py3-none-any.whl", hash = "sha256:7a91720bc756843502c3b7504c77b8fe44217c85c537d85037f0f536151b2caa"},
|
||||||
|
{file = "et_xmlfile-2.0.0.tar.gz", hash = "sha256:dab3f4764309081ce75662649be815c4c9081e88f0837825f90fd28317d4da54"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "openpyxl"
|
||||||
|
version = "3.1.5"
|
||||||
|
description = "A Python library to read/write Excel 2010 xlsx/xlsm files"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.8"
|
||||||
|
groups = ["main"]
|
||||||
|
files = [
|
||||||
|
{file = "openpyxl-3.1.5-py2.py3-none-any.whl", hash = "sha256:5282c12b107bffeef825f4617dc029afaf41d0ea60823bbb665ef3079dc79de2"},
|
||||||
|
{file = "openpyxl-3.1.5.tar.gz", hash = "sha256:cf0e3cf56142039133628b5acffe8ef0c12bc902d2aadd3e0fe5878dc08d1050"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
et-xmlfile = "*"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pillow"
|
name = "pillow"
|
||||||
version = "12.0.0"
|
version = "12.0.0"
|
||||||
@@ -379,4 +406,4 @@ files = [
|
|||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.1"
|
lock-version = "2.1"
|
||||||
python-versions = "^3.12"
|
python-versions = "^3.12"
|
||||||
content-hash = "8d8926bda0e1b7cdb8a1e57168dd3acc0f3240e34d4e2faf613b844d989dad30"
|
content-hash = "e231b5570d8b02b46736a58612eab986373b3231b23437cad90d489ea97ecb5b"
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ django-nested-admin = "^4.1.1"
|
|||||||
django-durationwidget = "^1.0.5"
|
django-durationwidget = "^1.0.5"
|
||||||
django-import-export = "^4.3.13"
|
django-import-export = "^4.3.13"
|
||||||
django-colorfield = "^0.14.0"
|
django-colorfield = "^0.14.0"
|
||||||
|
openpyxl = "^3.1.5"
|
||||||
|
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
|
|||||||
1
techdb/static/password
Normal file
1
techdb/static/password
Normal file
@@ -0,0 +1 @@
|
|||||||
|
admin: CantorAdmin2k25
|
||||||
Reference in New Issue
Block a user