mirror of
https://github.com/pretix/pretix.git
synced 2026-05-04 15:04:03 +00:00
257 lines
9.5 KiB
Python
257 lines
9.5 KiB
Python
import io
|
||
import tempfile
|
||
from collections import OrderedDict
|
||
from decimal import Decimal
|
||
from typing import Tuple
|
||
|
||
from defusedcsv import csv
|
||
from django import forms
|
||
from django.utils.formats import localize
|
||
from django.utils.translation import ugettext, ugettext_lazy as _
|
||
from openpyxl import Workbook
|
||
from openpyxl.cell.cell import KNOWN_TYPES
|
||
|
||
|
||
class BaseExporter:
|
||
"""
|
||
This is the base class for all data exporters
|
||
"""
|
||
|
||
def __init__(self, event):
|
||
self.event = event
|
||
|
||
def __str__(self):
|
||
return self.identifier
|
||
|
||
@property
|
||
def verbose_name(self) -> str:
|
||
"""
|
||
A human-readable name for this exporter. This should be short but
|
||
self-explaining. Good examples include 'JSON' or 'Microsoft Excel'.
|
||
"""
|
||
raise NotImplementedError() # NOQA
|
||
|
||
@property
|
||
def identifier(self) -> str:
|
||
"""
|
||
A short and unique identifier for this exporter.
|
||
This should only contain lowercase letters and in most
|
||
cases will be the same as your package name.
|
||
"""
|
||
raise NotImplementedError() # NOQA
|
||
|
||
@property
|
||
def export_form_fields(self) -> dict:
|
||
"""
|
||
When the event's administrator visits the export page, this method
|
||
is called to return the configuration fields available.
|
||
|
||
It should therefore return a dictionary where the keys should be field names and
|
||
the values should be corresponding Django form fields.
|
||
|
||
We suggest that you return an ``OrderedDict`` object instead of a dictionary.
|
||
Your implementation could look like this::
|
||
|
||
@property
|
||
def export_form_fields(self):
|
||
return OrderedDict(
|
||
[
|
||
('tab_width',
|
||
forms.IntegerField(
|
||
label=_('Tab width'),
|
||
default=4
|
||
))
|
||
]
|
||
)
|
||
"""
|
||
return {}
|
||
|
||
def render(self, form_data: dict) -> Tuple[str, str, bytes]:
|
||
"""
|
||
Render the exported file and return a tuple consisting of a filename, a file type
|
||
and file content.
|
||
|
||
:type form_data: dict
|
||
:param form_data: The form data of the export details form
|
||
:param output_file: You can optionally accept a parameter that will be given a file handle to write the
|
||
output to. In this case, you can return None instead of the file content.
|
||
|
||
Note: If you use a ``ModelChoiceField`` (or a ``ModelMultipleChoiceField``), the
|
||
``form_data`` will not contain the model instance but only it's primary key (or
|
||
a list of primary keys) for reasons of internal serialization when using background
|
||
tasks.
|
||
"""
|
||
raise NotImplementedError() # NOQA
|
||
|
||
|
||
class ListExporter(BaseExporter):
|
||
|
||
@property
|
||
def export_form_fields(self) -> dict:
|
||
ff = OrderedDict(
|
||
[
|
||
('_format',
|
||
forms.ChoiceField(
|
||
label=_('Export format'),
|
||
choices=(
|
||
('xlsx', _('Excel (.xlsx)')),
|
||
('default', _('CSV (with commas)')),
|
||
('csv-excel', _('CSV (Excel-style)')),
|
||
('semicolon', _('CSV (with semicolons)')),
|
||
),
|
||
)),
|
||
]
|
||
)
|
||
ff.update(self.additional_form_fields)
|
||
return ff
|
||
|
||
@property
|
||
def additional_form_fields(self) -> dict:
|
||
return {}
|
||
|
||
def iterate_list(self, form_data):
|
||
raise NotImplementedError() # noqa
|
||
|
||
def get_filename(self):
|
||
return 'export'
|
||
|
||
def _render_csv(self, form_data, output_file=None, **kwargs):
|
||
if output_file:
|
||
writer = csv.writer(output_file, **kwargs)
|
||
for line in self.iterate_list(form_data):
|
||
line = [
|
||
localize(f) if isinstance(f, Decimal) else f
|
||
for f in line
|
||
]
|
||
writer.writerow(line)
|
||
return self.get_filename() + '.csv', 'text/csv', None
|
||
else:
|
||
output = io.StringIO()
|
||
writer = csv.writer(output, **kwargs)
|
||
for line in self.iterate_list(form_data):
|
||
line = [
|
||
localize(f) if isinstance(f, Decimal) else f
|
||
for f in line
|
||
]
|
||
writer.writerow(line)
|
||
return self.get_filename() + '.csv', 'text/csv', output.getvalue().encode("utf-8")
|
||
|
||
def _render_xlsx(self, form_data, output_file=None):
|
||
wb = Workbook()
|
||
ws = wb.active
|
||
try:
|
||
ws.title = str(self.verbose_name)
|
||
except:
|
||
pass
|
||
for i, line in enumerate(self.iterate_list(form_data)):
|
||
for j, val in enumerate(line):
|
||
ws.cell(row=i + 1, column=j + 1).value = str(val) if not isinstance(val, KNOWN_TYPES) else val
|
||
|
||
if output_file:
|
||
wb.save(output_file)
|
||
return self.get_filename() + '.xlsx', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', None
|
||
else:
|
||
with tempfile.NamedTemporaryFile(suffix='.xlsx') as f:
|
||
wb.save(f.name)
|
||
f.seek(0)
|
||
return self.get_filename() + '.xlsx', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', f.read()
|
||
|
||
def render(self, form_data: dict, output_file=None) -> Tuple[str, str, bytes]:
|
||
if form_data.get('_format') == 'xlsx':
|
||
return self._render_xlsx(form_data, output_file=output_file)
|
||
elif form_data.get('_format') == 'default':
|
||
return self._render_csv(form_data, quoting=csv.QUOTE_NONNUMERIC, delimiter=',', output_file=output_file)
|
||
elif form_data.get('_format') == 'csv-excel':
|
||
return self._render_csv(form_data, dialect='excel', output_file=output_file)
|
||
elif form_data.get('_format') == 'semicolon':
|
||
return self._render_csv(form_data, dialect='excel', delimiter=';', output_file=output_file)
|
||
|
||
|
||
class MultiSheetListExporter(ListExporter):
|
||
|
||
@property
|
||
def sheets(self):
|
||
raise NotImplementedError()
|
||
|
||
@property
|
||
def export_form_fields(self) -> dict:
|
||
choices = [
|
||
('xlsx', _('Combined Excel (.xlsx)')),
|
||
]
|
||
for s, l in self.sheets:
|
||
choices += [
|
||
(s + ':default', str(l) + ' – ' + ugettext('CSV (with commas)')),
|
||
(s + ':excel', str(l) + ' – ' + ugettext('CSV (Excel-style)')),
|
||
(s + ':semicolon', str(l) + ' – ' + ugettext('CSV (with semicolons)')),
|
||
]
|
||
ff = OrderedDict(
|
||
[
|
||
('_format',
|
||
forms.ChoiceField(
|
||
label=_('Export format'),
|
||
choices=choices,
|
||
)),
|
||
]
|
||
)
|
||
ff.update(self.additional_form_fields)
|
||
return ff
|
||
|
||
def iterate_list(self, form_data):
|
||
pass
|
||
|
||
def iterate_sheet(self, form_data, sheet):
|
||
raise NotImplementedError() # noqa
|
||
|
||
def _render_sheet_csv(self, form_data, sheet, output_file=None, **kwargs):
|
||
if output_file:
|
||
writer = csv.writer(output_file, **kwargs)
|
||
for line in self.iterate_sheet(form_data, sheet):
|
||
line = [
|
||
localize(f) if isinstance(f, Decimal) else f
|
||
for f in line
|
||
]
|
||
writer.writerow(line)
|
||
return self.get_filename() + '.csv', 'text/csv', None
|
||
else:
|
||
output = io.StringIO()
|
||
writer = csv.writer(output, **kwargs)
|
||
for line in self.iterate_sheet(form_data, sheet):
|
||
line = [
|
||
localize(f) if isinstance(f, Decimal) else f
|
||
for f in line
|
||
]
|
||
writer.writerow(line)
|
||
return self.get_filename() + '.csv', 'text/csv', output.getvalue().encode("utf-8")
|
||
|
||
def _render_xlsx(self, form_data, output_file=None):
|
||
wb = Workbook()
|
||
ws = wb.active
|
||
wb.remove(ws)
|
||
for s, l in self.sheets:
|
||
ws = wb.create_sheet(str(l))
|
||
for i, line in enumerate(self.iterate_sheet(form_data, sheet=s)):
|
||
for j, val in enumerate(line):
|
||
ws.cell(row=i + 1, column=j + 1).value = str(val) if not isinstance(val, KNOWN_TYPES) else val
|
||
|
||
if output_file:
|
||
wb.save(output_file)
|
||
return self.get_filename() + '.xlsx', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', None
|
||
else:
|
||
with tempfile.NamedTemporaryFile(suffix='.xlsx') as f:
|
||
wb.save(f.name)
|
||
f.seek(0)
|
||
return self.get_filename() + '.xlsx', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', f.read()
|
||
|
||
def render(self, form_data: dict, output_file=None) -> Tuple[str, str, bytes]:
|
||
if form_data.get('_format') == 'xlsx':
|
||
return self._render_xlsx(form_data, output_file=output_file)
|
||
elif ':' in form_data.get('_format'):
|
||
sheet, f = form_data.get('_format').split(':')
|
||
if f == 'default':
|
||
return self._render_sheet_csv(form_data, sheet, quoting=csv.QUOTE_NONNUMERIC, delimiter=',',
|
||
output_file=output_file)
|
||
elif f == 'excel':
|
||
return self._render_sheet_csv(form_data, sheet, dialect='excel', output_file=output_file)
|
||
elif f == 'semicolon':
|
||
return self._render_sheet_csv(form_data, sheet, dialect='excel', delimiter=';', output_file=output_file)
|