diff --git a/tailbone/templates/master/index.mako b/tailbone/templates/master/index.mako
index 014e1f97..21070505 100644
--- a/tailbone/templates/master/index.mako
+++ b/tailbone/templates/master/index.mako
@@ -72,6 +72,9 @@
% if master.results_downloadable_csv and request.has_perm('{}.results_csv'.format(permission_prefix)):
${h.link_to("Download results as CSV", url('{}.results_csv'.format(route_prefix)))}
% endif
+ % if master.results_downloadable_xlsx and request.has_perm('{}.results_xlsx'.format(permission_prefix)):
+ ${h.link_to("Download results as XLSX", url('{}.results_xlsx'.format(route_prefix)))}
+ % endif
% if master.creatable and request.has_perm('{}.create'.format(permission_prefix)):
% if master.creates_multiple:
${h.link_to("Create new {}".format(model_title_plural), url('{}.create'.format(route_prefix)))}
diff --git a/tailbone/views/master.py b/tailbone/views/master.py
index a21188df..bb320612 100644
--- a/tailbone/views/master.py
+++ b/tailbone/views/master.py
@@ -2,7 +2,7 @@
################################################################################
#
# Rattail -- Retail Software Framework
-# Copyright © 2010-2017 Lance Edgar
+# Copyright © 2010-2018 Lance Edgar
#
# This file is part of Rattail.
#
@@ -41,6 +41,8 @@ from rattail.util import prettify
from rattail.time import localtime #, make_utc
from rattail.threads import Thread
from rattail.csvutil import UnicodeDictWriter
+from rattail.files import temp_path
+from rattail.excel import ExcelWriter
import formalchemy as fa
from pyramid import httpexceptions
@@ -66,6 +68,7 @@ class MasterView(View):
listable = True
results_downloadable_csv = False
+ results_downloadable_xlsx = False
creatable = True
viewable = True
editable = True
@@ -1472,6 +1475,57 @@ class MasterView(View):
response.content_disposition = b'attachment; filename={}.csv'.format(self.get_grid_key())
return response
+ def results_xlsx(self):
+ """
+ Download current list results as XLSX.
+ """
+ results = self.get_effective_data()
+ fields = self.get_xlsx_fields()
+ path = temp_path(suffix='.xlsx')
+ writer = ExcelWriter(path, fields, sheet_title=self.get_model_title_plural())
+ writer.write_header()
+
+ rows = []
+ for obj in results:
+ data = self.get_xlsx_row(obj, fields)
+ row = [data[field] for field in fields]
+ rows.append(row)
+
+ writer.write_rows(rows)
+ writer.auto_freeze()
+ writer.auto_filter()
+ writer.save()
+
+ response = self.request.response
+ with open(path, 'rb') as f:
+ response.body = f.read()
+ os.remove(path)
+
+ response.content_length = len(response.body)
+ response.content_type = b'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
+ response.content_disposition = b'attachment; filename={}.xlsx'.format(self.get_grid_key())
+ return response
+
+ def get_xlsx_fields(self):
+ """
+ Return the list of fields to be written to XLSX download.
+ """
+ fields = []
+ mapper = orm.class_mapper(self.model_class)
+ for prop in mapper.iterate_properties:
+ if isinstance(prop, orm.ColumnProperty):
+ fields.append(prop.key)
+ return fields
+
+ def get_xlsx_row(self, obj, fields):
+ """
+ Return a dict for use when writing the row's data to CSV download.
+ """
+ row = {}
+ for field in fields:
+ row[field] = getattr(obj, field, None)
+ return row
+
def row_results_csv(self):
"""
Download current row results data for an object, as CSV
@@ -1965,6 +2019,13 @@ class MasterView(View):
config.add_view(cls, attr='results_csv', route_name='{}.results_csv'.format(route_prefix),
permission='{}.results_csv'.format(permission_prefix))
+ if cls.results_downloadable_xlsx:
+ config.add_tailbone_permission(permission_prefix, '{}.results_xlsx'.format(permission_prefix),
+ "Download {} as XLSX".format(model_title_plural))
+ config.add_route('{}.results_xlsx'.format(route_prefix), '{}/xlsx'.format(url_prefix))
+ config.add_view(cls, attr='results_xlsx', route_name='{}.results_xlsx'.format(route_prefix),
+ permission='{}.results_xlsx'.format(permission_prefix))
+
# create
if cls.creatable or cls.mobile_creatable: