diff --git a/techdb/flightslot/actions/exportweek.py b/techdb/flightslot/actions/exportweek.py new file mode 100644 index 0000000..3d9035f --- /dev/null +++ b/techdb/flightslot/actions/exportweek.py @@ -0,0 +1,158 @@ +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 datetime import date, datetime, timedelta +from typing import List +import calendar + +from ..models.weekpref import WeekPreference +from ..models.missions import Training +from ..models.hourbuildings import HourBuilding + +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 + + # Prepare export filename and http content + filename = f"{year}-week{week}_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"{year} Week {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 + + # Scrittura dati + row: int = 2 + row_offset: int = 0 + + # Fill worksheet with EVERY training and hb for every student + 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(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}" + 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 = h.notes if h.notes else "--" + hb_data.append([str(week), *student_data, *hb_days, str(q.student.phone), q.student.email, hb_notes]) + + # Build rows for table + student_start: int = row + row_offset + for r in mission_data + hb_data: + prev_cell_val: str | None = None + merge_start: bool = False + merge_col_start: int = 1 + for j, c in enumerate(r, start=1): + if prev_cell_val is None: + prev_cell_val = c + 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()) + # Merge cells in the row + if c == prev_cell_val and not merge_start: + merge_start = True + merge_col_start = max(j-1, 1) + elif c != prev_cell_val and merge_start: + merge_start = False + ws.merge_cells(start_row=row+row_offset, + end_row=row+row_offset, + start_column=merge_col_start, + end_column=max(j-1,1)) + + # Incement row counter + row_offset += 1 + + # End f 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) + + # Save document in HttpResponse + wb.save(response) + + return response \ No newline at end of file diff --git a/techdb/flightslot/admin.py b/techdb/flightslot/admin.py index c98b1a9..b9a5f51 100644 --- a/techdb/flightslot/admin.py +++ b/techdb/flightslot/admin.py @@ -1,13 +1,14 @@ from django import forms from django.db.models.query import QuerySet -from django.contrib import admin 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 durationwidget.widgets import TimeDurationWidget from datetime import date import nested_admin -import csv from .models.courses import Course from .models.hourbuildings import HourBuilding, HourBuildingLeg @@ -18,6 +19,8 @@ from .models.weekpref import WeekPreference from .custom.colortag import course_color from .custom.defpassword import default_password +from .actions.exportweek import export_selected + class TrainingForm(forms.ModelForm): model=Training @@ -56,39 +59,24 @@ class TrainingInLIne(nested_admin.NestedTabularInline): max_num = 7 class WeekPreferenceAdmin(nested_admin.NestedModelAdmin): - inlines = [TrainingInLIne, HourBuildingInLine] - list_display = ("week", "student__name", "student__surname", "student__course", "course_color", "student_brief_mix") - list_filter = ("week", "student__course", "student") - actions = ("export_selected",) + inlines = (TrainingInLIne, HourBuildingInLine,) + list_display = ("week", "student__name", "student__surname", "student__course", "course_color", "student_brief_mix",) + list_filter = ("week", "student__course", "student",) + actions = ("export",) @admin.action(description="Export Selected Preferences") - def export_selected(modeladmin, request: HttpRequest, queryset: QuerySet[WeekPreference]) -> HttpResponse: - filename = "weekpreferences_export.csv" - response = HttpResponse(content_type='text/csv') - response['Content-Disposition'] = f'attachment; filename="{filename}"' - - 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 - + def export(weekpreferenceadmin, request: HttpRequest, queryset: QuerySet[WeekPreference]) -> HttpResponse | None: + if queryset.count() == 0: + return None + weekpreferenceadmin.message_user(request, ngettext("Exporting %d row", "Exporting %d rows", queryset.count()) % queryset.count(), messages.SUCCESS) + return export_selected(request=request, queryset=queryset) + @admin.display(description="Mission Count") def student_brief_mix(self, obj: WeekPreference) -> SafeText: if not obj.student.course: return SafeText("") return SafeText(f"{Training.objects.filter(weekpref = obj.id).count()}") - @admin.display(description="Color") def course_color(self, obj: WeekPreference) -> SafeText: if not obj.student.course: diff --git a/techdb/poetry.lock b/techdb/poetry.lock index d1f695f..b2b4481 100644 --- a/techdb/poetry.lock +++ b/techdb/poetry.lock @@ -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"] 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]] name = "pillow" version = "12.0.0" @@ -379,4 +406,4 @@ files = [ [metadata] lock-version = "2.1" python-versions = "^3.12" -content-hash = "8d8926bda0e1b7cdb8a1e57168dd3acc0f3240e34d4e2faf613b844d989dad30" +content-hash = "e231b5570d8b02b46736a58612eab986373b3231b23437cad90d489ea97ecb5b" diff --git a/techdb/pyproject.toml b/techdb/pyproject.toml index a3fd22f..20a396e 100644 --- a/techdb/pyproject.toml +++ b/techdb/pyproject.toml @@ -13,6 +13,7 @@ django-nested-admin = "^4.1.1" django-durationwidget = "^1.0.5" django-import-export = "^4.3.13" django-colorfield = "^0.14.0" +openpyxl = "^3.1.5" [build-system]