import io from datetime import datetime, date from typing import List, Any from django.http import HttpResponse from django.utils import timezone import openpyxl from openpyxl.styles import Font, PatternFill, Alignment, Border, Side from openpyxl.utils import get_column_letter def export_to_excel_response( filename: str, headers: List[str], rows: List[List[Any]], sheet_title: str = "Export" ) -> HttpResponse: """ Generates a beautifully formatted Excel (.xlsx) file and returns it as a Django HttpResponse. :param filename: Base name of the file (without extension or with .xlsx) :param headers: List of column header names :param rows: List of data rows (each row is a list of cell values) :param sheet_title: Title for the worksheet :return: HttpResponse with .xlsx content type and attachment disposition """ wb = openpyxl.Workbook() ws = wb.active ws.title = sheet_title[:31] # Excel sheet title max 31 chars # Enable gridlines ws.views.sheetView[0].showGridLines = True # 1. Styles Definition # Modern dark indigo / slate header style header_fill = PatternFill(start_color="1E293B", end_color="1E293B", fill_type="solid") header_font = Font(name="Calibri", size=11, bold=True, color="FFFFFF") header_alignment = Alignment(horizontal="center", vertical="center", wrap_text=True) data_font = Font(name="Calibri", size=10, color="0F172A") data_alignment_left = Alignment(horizontal="left", vertical="center") data_alignment_center = Alignment(horizontal="center", vertical="center") thin_border_side = Side(border_style="thin", color="E2E8F0") border_style = Border( left=thin_border_side, right=thin_border_side, top=thin_border_side, bottom=thin_border_side ) zebra_fill = PatternFill(start_color="F8FAFC", end_color="F8FAFC", fill_type="solid") # 2. Write Headers ws.append(headers) header_row = ws[1] ws.row_dimensions[1].height = 28 for col_idx, cell in enumerate(header_row, 1): cell.font = header_font cell.fill = header_fill cell.alignment = header_alignment cell.border = border_style # 3. Write Data Rows for row_idx, row_data in enumerate(rows, start=2): formatted_row = [] for val in row_data: if val is None: formatted_row.append("") elif isinstance(val, (datetime, date)): if isinstance(val, datetime) and timezone.is_aware(val): val = timezone.localtime(val) formatted_row.append(val.strftime("%Y-%m-%d %H:%M")) elif isinstance(val, bool): formatted_row.append("Yes" if val else "No") else: formatted_row.append(str(val)) ws.append(formatted_row) current_row = ws[row_idx] ws.row_dimensions[row_idx].height = 22 is_even = (row_idx % 2 == 0) for col_idx, cell in enumerate(current_row, 1): cell.font = data_font cell.border = border_style if is_even: cell.fill = zebra_fill # Align IDs or numbers center, text left val = row_data[col_idx - 1] if col_idx - 1 < len(row_data) else None if isinstance(val, (int, float, date, datetime)) or (isinstance(val, str) and val.isdigit()): cell.alignment = data_alignment_center else: cell.alignment = data_alignment_left # 4. Auto-fit column widths (with min 12 and max 50) for col in ws.columns: max_len = 0 col_letter = get_column_letter(col[0].column) for cell in col: val_str = str(cell.value or "") if len(val_str) > max_len: max_len = len(val_str) adjusted_width = min(max(max_len + 4, 12), 50) ws.column_dimensions[col_letter].width = adjusted_width # 5. Output to buffer output = io.BytesIO() wb.save(output) output.seek(0) # Ensure .xlsx extension in filename clean_filename = filename if filename.endswith(".xlsx") else f"{filename}.xlsx" response = HttpResponse( output.getvalue(), content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" ) response["Content-Disposition"] = f'attachment; filename="{clean_filename}"' response["Access-Control-Expose-Headers"] = "Content-Disposition" return response