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
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
|