angrybeanie_wagtail/env/lib/python3.12/site-packages/wagtail/admin/views/mixins.py

336 lines
13 KiB
Python
Raw Permalink Normal View History

2025-07-25 21:32:16 +10:00
import csv
import datetime
from collections import OrderedDict
from functools import partial
from io import BytesIO
from django.contrib.admin.utils import label_for_field
from django.core.exceptions import FieldDoesNotExist
from django.http import FileResponse, StreamingHttpResponse
from django.utils import timezone
from django.utils.dateformat import Formatter
from django.utils.encoding import force_str
from django.utils.formats import get_format
from django.utils.functional import cached_property
from django.utils.text import capfirst
from django.utils.translation import gettext as _
from wagtail.admin.widgets.button import Button
from wagtail.coreutils import multigetattr
class Echo:
"""An object that implements just the write method of the file-like interface."""
def write(self, value):
"""Write the value by returning it, instead of storing in a buffer."""
return value.encode("UTF-8")
def list_to_str(value):
return force_str(", ".join(value))
class ExcelDateFormatter(Formatter):
data = None
# From: https://docs.djangoproject.com/en/stable/ref/templates/builtins/#date
# To: https://support.microsoft.com/en-us/office/format-numbers-as-dates-or-times-418bd3fe-0577-47c8-8caa-b4d30c528309#bm2
_formats = {
# Day of the month, 2 digits with leading zeros.
"d": "dd",
# Day of the month without leading zeros.
"j": "d",
# Day of the week, textual, 3 letters.
"D": "ddd",
# Day of the week, textual, full.
"l": "dddd",
# English ordinal suffix for the day of the month, 2 characters.
"S": "", # Not supported in Excel
# Day of the week, digits without leading zeros.
"w": "", # Not supported in Excel
# Day of the year.
"z": "", # Not supported in Excel
# ISO-8601 week number of year, with weeks starting on Monday.
"W": "", # Not supported in Excel
# Month, 2 digits with leading zeros.
"m": "mm",
# Month without leading zeros.
"n": "m",
# Month, textual, 3 letters.
"M": "mmm",
# Month, textual, 3 letters, lowercase. (Not supported in Excel)
"b": "mmm",
# Month, locale specific alternative representation usually used for long date representation.
"E": "mmmm", # Not supported in Excel
# Month, textual, full.
"F": "mmmm",
# Month abbreviation in Associated Press style. Proprietary extension.
"N": "mmm.", # Approximation, wrong for May
# Number of days in the given month.
"t": "", # Not supported in Excel
# Year, 2 digits with leading zeros.
"y": "yy",
# Year, 4 digits with leading zeros.
"Y": "yyyy",
# Whether it's a leap year.
"L": "", # Not supported in Excel
# ISO-8601 week-numbering year.
"o": "yyyy", # Approximation, same as Y
# Hour, 12-hour format without leading zeros.
"g": "h", # Only works when combined with AM/PM, 24-hour format is used otherwise
# Hour, 24-hour format without leading zeros.
"G": "hH",
# Hour, 12-hour format with leading zeros.
"h": "hh", # Only works when combined with AM/PM, 24-hour format is used otherwise
# Hour, 24-hour format with leading zeros.
"H": "hh",
# Minutes.
"i": "mm",
# Seconds.
"s": "ss",
# Microseconds.
"u": ".00", # Only works when combined with ss
# 'a.m.' or 'p.m.'.
"a": "AM/PM", # Approximation, uses AM/PM and only works when combined with h/hh
# AM/PM.
"A": "AM/PM", # Only works when combined with h/hh
# Time, in 12-hour hours and minutes, with minutes left off if theyre zero.
"f": "h:mm", # Approximation, uses 24-hour format and minutes are never left off
# Time, in 12-hour hours, minutes and a.m./p.m., with minutes left off if theyre zero and the special-case strings midnight and noon if appropriate.
"P": "h:mm AM/PM", # Approximation, minutes are never left off, no special case strings
# Timezone name.
"e": "", # Not supported in Excel
# Daylight saving time, whether its in effect or not.
"I": "", # Not supported in Excel
# Difference to Greenwich time in hours.
"O": "", # Not supported in Excel
# Time zone of this machine.
"T": "", # Not supported in Excel
# Timezone offset in seconds.
"Z": "", # Not supported in Excel
# ISO 8601 format.
"c": "yyyy-mm-ddThh:mm:ss.00",
# RFC 5322 formatted date.
"r": "ddd, d mmm yyyy hh:mm:ss",
# Seconds since the Unix epoch.
"U": "", # Not supported in Excel
}
def get(self):
format = get_format("SHORT_DATETIME_FORMAT")
return self.format(format)
def __getattr__(self, name):
if name in self._formats:
return lambda: self._formats[name]
raise AttributeError(
f"'{type(self).__name__}' object has no attribute '{name}'"
)
class SpreadsheetExportMixin:
"""A mixin for views, providing spreadsheet export functionality in csv and xlsx formats"""
FORMAT_XLSX = "xlsx"
FORMAT_CSV = "csv"
FORMATS = (FORMAT_XLSX, FORMAT_CSV)
# A list of fields or callables (without arguments) to export from each item in the queryset (dotted paths allowed)
list_export = []
# A dictionary of custom preprocessing functions by field and format (expected value would be of the form {field_name: {format: function}})
# If a valid field preprocessing function is found, any applicable value preprocessing functions will not be used
custom_field_preprocess = {}
# A dictionary of preprocessing functions by value class and format
custom_value_preprocess = {
datetime.datetime: {
FORMAT_XLSX: lambda value: (
value
if timezone.is_naive(value)
else timezone.make_naive(value, datetime.timezone.utc)
)
},
(datetime.date, datetime.time): {FORMAT_XLSX: None},
list: {FORMAT_CSV: list_to_str, FORMAT_XLSX: list_to_str},
}
# A dictionary of column heading overrides in the format {field: heading}
export_headings = {}
export_buttons_template_name = "wagtailadmin/shared/export_buttons.html"
export_filename = "spreadsheet-export"
def setup(self, request, *args, **kwargs):
super().setup(request, *args, **kwargs)
self.is_export = request.GET.get("export") in self.FORMATS
def get_paginate_by(self, queryset):
if self.is_export:
return None
return super().get_paginate_by(queryset)
def get_filename(self):
"""Gets the base filename for the exported spreadsheet, without extensions"""
return self.export_filename
def to_row_dict(self, item):
"""Returns an OrderedDict (in the order given by list_export) of the exportable information for a model instance"""
row_dict = OrderedDict(
(field, multigetattr(item, field)) for field in self.list_export
)
return row_dict
def get_preprocess_function(self, field, value, export_format):
"""Returns the preprocessing function for a given field name, field value, and export format"""
# Try to find a field specific function and return it
format_dict = self.custom_field_preprocess.get(field, {})
if export_format in format_dict:
return format_dict[export_format]
# Otherwise check for a value class specific function
for value_classes, format_dict in self.custom_value_preprocess.items():
if isinstance(value, value_classes) and export_format in format_dict:
return format_dict[export_format]
# Finally resort to force_str to prevent encoding errors
return partial(force_str, strings_only=True)
def preprocess_field_value(self, field, value, export_format):
"""Preprocesses a field value before writing it to the spreadsheet"""
preprocess_function = self.get_preprocess_function(field, value, export_format)
if preprocess_function is not None:
return preprocess_function(value)
else:
return value
def generate_xlsx_row(self, worksheet, row_dict, date_format=None):
"""Generate cells to append to the worksheet"""
from openpyxl.cell import WriteOnlyCell
for field, value in row_dict.items():
cell = WriteOnlyCell(
worksheet, self.preprocess_field_value(field, value, self.FORMAT_XLSX)
)
if date_format and isinstance(value, datetime.datetime):
cell.number_format = date_format
yield cell
def write_csv_row(self, writer, row_dict):
return writer.writerow(
{
field: self.preprocess_field_value(field, value, self.FORMAT_CSV)
for field, value in row_dict.items()
}
)
def get_heading(self, queryset, field):
"""Get the heading label for a given field for a spreadsheet generated from queryset"""
heading_override = self.export_headings.get(field)
if heading_override:
return force_str(heading_override)
try:
return capfirst(force_str(label_for_field(field, queryset.model)))
except (AttributeError, FieldDoesNotExist):
return force_str(field)
def stream_csv(self, queryset):
"""Generate a csv file line by line from queryset, to be used in a StreamingHTTPResponse"""
writer = csv.DictWriter(Echo(), fieldnames=self.list_export)
yield writer.writerow(
{field: self.get_heading(queryset, field) for field in self.list_export}
)
for item in queryset:
yield self.write_csv_row(writer, self.to_row_dict(item))
def write_xlsx(self, queryset, output):
"""Write an xlsx workbook from a queryset"""
from openpyxl import Workbook
workbook = Workbook(write_only=True, iso_dates=True)
worksheet = workbook.create_sheet(title="Sheet1")
worksheet.append(
self.get_heading(queryset, field) for field in self.list_export
)
date_format = ExcelDateFormatter().get()
for item in queryset:
worksheet.append(
self.generate_xlsx_row(
worksheet, self.to_row_dict(item), date_format=date_format
)
)
workbook.save(output)
def write_xlsx_response(self, queryset):
"""Write an xlsx file from a queryset and return a FileResponse"""
output = BytesIO()
self.write_xlsx(queryset, output)
output.seek(0)
return FileResponse(
output,
as_attachment=True,
content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
filename=f"{self.get_filename()}.xlsx",
)
def write_csv_response(self, queryset):
stream = self.stream_csv(queryset)
response = StreamingHttpResponse(stream, content_type="text/csv")
response["Content-Disposition"] = 'attachment; filename="{}.csv"'.format(
self.get_filename()
)
return response
def as_spreadsheet(self, queryset, spreadsheet_format):
"""Return a response with a spreadsheet representing the exported data from queryset, in the format specified"""
if spreadsheet_format == self.FORMAT_CSV:
return self.write_csv_response(queryset)
elif spreadsheet_format == self.FORMAT_XLSX:
return self.write_xlsx_response(queryset)
def get_export_url(self, format):
params = self.request.GET.copy()
params["export"] = format
return self.request.path + "?" + params.urlencode()
@property
def xlsx_export_url(self):
return self.get_export_url("xlsx")
@property
def csv_export_url(self):
return self.get_export_url("csv")
@cached_property
def show_export_buttons(self):
return bool(self.list_export)
@cached_property
def header_more_buttons(self):
buttons = super().header_more_buttons.copy()
if self.show_export_buttons:
buttons.append(
Button(
_("Download XLSX"),
url=self.xlsx_export_url,
icon_name="download",
priority=90,
)
)
buttons.append(
Button(
_("Download CSV"),
url=self.csv_export_url,
icon_name="download",
priority=100,
)
)
return buttons