You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

121 lines
4.4 KiB

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