diff --git a/tailbone/templates/master/index.mako b/tailbone/templates/master/index.mako
index f46f2cee..bc219b00 100644
--- a/tailbone/templates/master/index.mako
+++ b/tailbone/templates/master/index.mako
@@ -69,6 +69,9 @@
%def>
<%def name="context_menu_items()">
+ % 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.creatable and request.has_perm('{}.create'.format(permission_prefix)):
${h.link_to("Create a new {}".format(model_title), url('{}.create'.format(route_prefix)))}
% endif
diff --git a/tailbone/views/master.py b/tailbone/views/master.py
index 3f817d4e..b22f9ee4 100644
--- a/tailbone/views/master.py
+++ b/tailbone/views/master.py
@@ -40,6 +40,7 @@ from rattail.db.continuum import model_transaction_query
from rattail.util import prettify
from rattail.time import localtime #, make_utc
from rattail.threads import Thread
+from rattail.csvutil import UnicodeDictWriter
import formalchemy as fa
from pyramid import httpexceptions
@@ -64,6 +65,7 @@ class MasterView(View):
checkboxes = False
listable = True
+ results_downloadable_csv = False
creatable = True
viewable = True
editable = True
@@ -1384,6 +1386,46 @@ class MasterView(View):
"""
return False
+ def results_csv(self):
+ """
+ Download current list results as CSV
+ """
+ results = self.get_effective_data()
+ fields = self.get_csv_fields()
+ data = six.StringIO()
+ writer = UnicodeDictWriter(data, fields)
+ writer.writeheader()
+ for obj in results:
+ writer.writerow(self.get_csv_row(obj, fields))
+ response = self.request.response
+ response.text = data.getvalue().decode('utf_8')
+ data.close()
+ response.content_length = len(response.text)
+ response.content_type = b'text/csv'
+ response.content_disposition = b'attachment; filename={}.csv'.format(self.get_grid_key())
+ return response
+
+ def get_csv_fields(self):
+ """
+ Return the list of fields to be written to CSV 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_csv_row(self, obj, fields):
+ """
+ Return a dict for use when writing the row's data to CSV download.
+ """
+ csvrow = {}
+ for field in fields:
+ value = getattr(obj, field)
+ csvrow[field] = '' if value is None else six.text_type(value)
+ return csvrow
+
##############################
# CRUD Stuff
##############################
@@ -1799,6 +1841,14 @@ class MasterView(View):
config.add_view(cls, attr='mobile_index', route_name='mobile.{}'.format(route_prefix),
permission='{}.list'.format(permission_prefix))
+ if cls.results_downloadable_csv:
+ config.add_tailbone_permission(permission_prefix, '{}.results_csv'.format(permission_prefix),
+ "Download {} as CSV".format(model_title_plural))
+ config.add_route('{}.results_csv'.format(route_prefix), '{}/csv'.format(url_prefix))
+ config.add_view(cls, attr='results_csv', route_name='{}.results_csv'.format(route_prefix),
+ permission='{}.results_csv'.format(permission_prefix))
+
+
# create
if cls.creatable or cls.mobile_creatable:
config.add_tailbone_permission(permission_prefix, '{}.create'.format(permission_prefix),