from django.http import HttpRequest, HttpResponse 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 from ..models.weekpref import WeekPreference from ..models.hourbuildings import HourBuilding, HourBuildingLegFlight, HourBuildingLegStop, HourBuildingLegBase from datetime import date, datetime from typing import List # Enable cell merging for equal mission MERGE: bool = False PALETTE : List[str] = [ "#E6F2FF", # azzurro chiarissimo "#E5FBF8", # verde acqua molto chiaro "#ECFBE1", # verde chiarissimo "#FFFBD1", # giallo molto chiaro "#FFF1D6", # giallo-arancio molto chiaro "#FFE3DD", # rosa pesca molto chiaro "#F3E6FA", # lilla chiarissimo ] def export_selected(request: HttpRequest, queryset: QuerySet[WeekPreference]) -> HttpResponse: if not queryset.first(): raise Exception("Empty queryset") # Init Variables 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}{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}"' # 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, "Notes", "Cell.", "Mail"] # Header fields positions week_index: int = headers.index("Week") + 1 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 note_index: int = headers.index("Notes") + 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", wrapText=True) # Cell styles border_thick: Side = Side(style='medium', color='000000') border_thin: Side = Side(style='thin', color='000000', border_style='dashed') border_bottom: Border = Border(bottom=border_thick) border_bottom_thin: Border = Border(bottom=border_thin) border_left: Border = Border(left=border_thick) border_right: Border = Border(right=border_thick) border_right_thin: Border = Border(right=border_thin) 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 # Each of this iterations fills the table for a student row: int = 2 row_offset: int = 0 for i, q in enumerate(queryset.order_by("-week", "student__surname", "student__name", "student__course"), start=1): student_data: List[str] student_phone: str = str(q.student.phone) if q.student.phone else "" student_email: str = q.student.email student_course_type: str student_course_number: str student_course_ac: str = f"({'/'.join(t.type for t in q.student.aircrafts.distinct("type").all())})" if q.student.course: student_course_type = q.student.course.ctype student_course_number = str(q.student.course.cnumber) student_data = [ "\n".join([f"{q.student.surname} {q.student.name}", student_course_ac]), f"{student_course_type}-{student_course_number}" ] else: student_data = [f"{q.student.surname} {q.student.name}", f"No Course Assigned"] # Fill Training mission rows mission_name: List[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_notes = t.notes.strip().capitalize() if t.notes else "" mission_name = [f"{t.mission.mtype}-{t.mission.mnum}"] if q.student.course and q.student.course.ctype == CourseTypes.PPL: # add course aircraft only for PPL students mission_name.append(student_course_ac) mission_name_joined: str = "\n".join(mission_name) mission_days = [ mission_name_joined if t.monday else "", mission_name_joined if t.tuesday else "", mission_name_joined if t.wednesday else "", mission_name_joined if t.thursday else "", mission_name_joined if t.friday else "", mission_name_joined if t.saturday else "", mission_name_joined if t.sunday else "" ] mission_data.append([str(q.week), *student_data, *mission_days, mission_notes, student_phone, student_email, ]) # Fill HourBuilding rows hb_name: List[str] hb_days: List[str] hb_data: List[List[str]] = [] for h in HourBuilding.objects.filter(weekpref = q.id): hb_name = ["HB", f"({h.aircraft})"] hb_legs_all = HourBuildingLegBase.objects.filter(hb_id = h.id) for hh in hb_legs_all: time_str: str = ':'.join(str(hh.time).split(':')[:2]) # keep only hours and minutes if isinstance(hh, HourBuildingLegFlight): hb_pax: str | None = " ".join(x.capitalize() for x in hh.pax.split()) if hh.pax else None hb_name.append(f"{hh.departure} -> {hh.destination} [{time_str}]{f' / Pax: {hb_pax}' if hb_pax else ''}") elif isinstance(hh, HourBuildingLegStop): hb_name.append(f"STOP [{time_str}] {"Refuel" if hh.refuel else ""}" ) hb_name_joined: str = "\n".join(hb_name) hb_days = [ hb_name_joined if h.monday else "", hb_name_joined if h.tuesday else "", hb_name_joined if h.wednesday else "", hb_name_joined if h.thursday else "", hb_name_joined if h.friday else "", hb_name_joined if h.saturday else "", hb_name_joined if h.sunday else "" ] hb_notes: str = h.notes.strip().capitalize() if h.notes else "" hb_data.append([str(q.week), *student_data, *hb_days, hb_notes, str(q.student.phone), q.student.email]) # Build rows for table all_data: List[List[str]] = mission_data + hb_data student_start: int = row + row_offset for ri, row_content in enumerate(all_data): for c, cell_content in enumerate(row_content, start=1): cell = ws.cell(row = row + row_offset, column = c, value = cell_content) cell.alignment = center # Format Student Name if c == student_index: cell.font = bold_black # Format Course Column with color elif c == course_index and q.student.course: cell.font = bold_black cell.fill = PatternFill("solid", fgColor=str(q.student.course.color).lstrip('#').lower()) # Add internal borders between mix cells and notes elif c > course_index and c <= note_index: cell.border = border_bottom_thin + border_right_thin # Fill mix cells if the cell is not empty 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 merge_col_start: int = 1 for c, cell_content in enumerate(row_content, start=1): # Merge cells in the row if cell_content == prev_cell_val and not merge_start: merge_start = True merge_col_start = c-1 # start merge from previous column elif cell_content != 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 = cell_content # Incement row counter row_offset += 1 # End week preferences for this student student_end: int = row + row_offset - 1 # Add thick border to the last cell row of this student for c in range(course_index, mail_index + 1): ws.cell(row=student_end, column=c).border = Border(bottom=border_thick, right=border_thin) # And for last column also a vertical border all student high if c == mail_index: ws.cell(row=student_end, column=c).border = Border(bottom=border_thick, right=border_thick) # Merge Week, thick border ws.cell(row=student_start, column=week_index).border = border_all ws.merge_cells(start_row=student_start, end_row=student_end, start_column=week_index, end_column=week_index) # Merge Name, thick border ws.cell(row=student_start, column=student_index).border = border_all 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: col_letter: str = "A" max_len: List[int] = [] for cell in column_cells: cell_lines = str(cell.value).splitlines() if len(cell_lines) == 0: continue max_len.append(max([len(ll) for ll in cell_lines])) 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 = 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) return response