diff --git a/tailbone/templates/master/view.mako b/tailbone/templates/master/view.mako
index 77e11c58..5e7e8c07 100644
--- a/tailbone/templates/master/view.mako
+++ b/tailbone/templates/master/view.mako
@@ -67,6 +67,9 @@
% if master.has_rows and master.rows_downloadable_csv and request.has_perm('{}.row_results_csv'.format(permission_prefix)):
${h.link_to("Download row results as CSV", url('{}.row_results_csv'.format(route_prefix), uuid=instance.uuid))}
% endif
+ % if master.has_rows and master.rows_downloadable_xlsx and request.has_perm('{}.row_results_xlsx'.format(permission_prefix)):
+ ${h.link_to("Download row results as XLSX", url('{}.row_results_xlsx'.format(route_prefix), uuid=instance.uuid))}
+ % endif
%def>
<%def name="object_helpers()">%def>
diff --git a/tailbone/views/batch/core.py b/tailbone/views/batch/core.py
index b1b20f36..5c2df5f7 100644
--- a/tailbone/views/batch/core.py
+++ b/tailbone/views/batch/core.py
@@ -74,6 +74,7 @@ class BatchMasterView(MasterView):
has_rows = True
rows_deletable = True
rows_downloadable_csv = True
+ rows_downloadable_xlsx = True
refreshable = True
refresh_after_create = False
cloneable = False
@@ -1333,6 +1334,9 @@ class BatchMasterView(MasterView):
def get_row_results_csv_filename(self, batch):
return '{}.{}.csv'.format(self.get_route_prefix(), batch.id_str)
+ def get_row_results_xlsx_filename(self, batch):
+ return '{}.{}.xlsx'.format(self.get_route_prefix(), batch.id_str)
+
def clone(self):
"""
Clone current batch as new batch
diff --git a/tailbone/views/master.py b/tailbone/views/master.py
index 5eca8b63..c524da7a 100644
--- a/tailbone/views/master.py
+++ b/tailbone/views/master.py
@@ -45,6 +45,7 @@ from rattail.threads import Thread
from rattail.csvutil import UnicodeDictWriter
from rattail.files import temp_path
from rattail.excel import ExcelWriter
+from rattail.gpc import GPC
import colander
import deform
@@ -141,6 +142,7 @@ class MasterView(View):
rows_bulk_deletable = False
rows_default_pagesize = 20
rows_downloadable_csv = False
+ rows_downloadable_xlsx = False
mobile_rows_creatable = False
mobile_rows_creatable_via_browse = False
@@ -2354,6 +2356,64 @@ class MasterView(View):
row[field] = getattr(obj, field, None)
return row
+ def row_results_xlsx(self):
+ """
+ Download current *row* results as XLSX.
+ """
+ obj = self.get_instance()
+ results = self.get_effective_row_data(sort=True)
+ fields = self.get_row_xlsx_fields()
+ path = temp_path(suffix='.xlsx')
+ writer = ExcelWriter(path, fields, sheet_title=self.get_row_model_title_plural())
+ writer.write_header()
+
+ rows = []
+ for row_obj in results:
+ data = self.get_row_xlsx_row(row_obj, fields)
+ row = [data[field] for field in fields]
+ rows.append(row)
+
+ writer.write_rows(rows)
+ writer.auto_freeze()
+ writer.auto_filter()
+ writer.auto_resize()
+ 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 = str('application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
+ filename = self.get_row_results_xlsx_filename(obj)
+ response.content_disposition = str('attachment; filename={}'.format(filename))
+ return response
+
+ def get_row_xlsx_fields(self):
+ """
+ Return the list of row fields to be written to XLSX download.
+ """
+ # TODO: should this be shared at all? in a better way?
+ return self.get_row_csv_fields()
+
+ def get_row_xlsx_row(self, row, fields):
+ """
+ Return a dict for use when writing the row's data to XLSX download.
+ """
+ xlrow = {}
+ for field in fields:
+ value = getattr(row, field, None)
+
+ if isinstance(value, GPC):
+ value = six.text_type(value)
+
+ xlrow[field] = value
+ return xlrow
+
+ def get_row_results_xlsx_filename(self, obj):
+ return '{}.xlsx'.format(self.get_row_grid_key())
+
def row_results_csv(self):
"""
Download current row results data for an object, as CSV
@@ -3265,6 +3325,14 @@ class MasterView(View):
config.add_view(cls, attr='row_results_csv', route_name='{}.row_results_csv'.format(route_prefix),
permission='{}.row_results_csv'.format(permission_prefix))
+ # download row results as Excel
+ if cls.has_rows and cls.rows_downloadable_xlsx:
+ config.add_tailbone_permission(permission_prefix, '{}.row_results_xlsx'.format(permission_prefix),
+ "Download {} results as XLSX".format(row_model_title))
+ config.add_route('{}.row_results_xlsx'.format(route_prefix), '{}/{{uuid}}/rows-xlsx'.format(url_prefix))
+ config.add_view(cls, attr='row_results_xlsx', route_name='{}.row_results_xlsx'.format(route_prefix),
+ permission='{}.row_results_xlsx'.format(permission_prefix))
+
# create row
if cls.has_rows:
if cls.rows_creatable or cls.mobile_rows_creatable: