2 changed files with 122 additions and 0 deletions
@ -0,0 +1,121 @@ |
|||
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 |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue