Initial commit
This commit is contained in:
333
env/lib/python3.10/site-packages/wagtail/admin/views/mixins.py
vendored
Normal file
333
env/lib/python3.10/site-packages/wagtail/admin/views/mixins.py
vendored
Normal file
@@ -0,0 +1,333 @@
|
||||
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 openpyxl import Workbook
|
||||
from openpyxl.cell import WriteOnlyCell
|
||||
|
||||
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 they’re 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 they’re 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 it’s 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"""
|
||||
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"""
|
||||
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
|
||||
Reference in New Issue
Block a user