diff --git a/tailbone/templates/batch/crud.mako b/tailbone/templates/batch/crud.mako
index b4f1896d..ecc61021 100644
--- a/tailbone/templates/batch/crud.mako
+++ b/tailbone/templates/batch/crud.mako
@@ -42,21 +42,25 @@
%def>
+<%def name="context_menu_items()">
+
${h.link_to("Back to {0}".format(batch_display_plural), url(route_prefix))}
+ % if not batch.executed:
+ % if form.updating:
+ ${h.link_to("View this {0}".format(batch_display), url('{0}.view'.format(route_prefix), uuid=batch.uuid))}
+ % endif
+ % if form.readonly and request.has_perm('{0}.edit'.format(permission_prefix)):
+ ${h.link_to("Edit this {0}".format(batch_display), url('{0}.edit'.format(route_prefix), uuid=batch.uuid))}
+ % endif
+ % endif
+ % if request.has_perm('{0}.delete'.format(permission_prefix)):
+ ${h.link_to("Delete this {0}".format(batch_display), url('{0}.delete'.format(route_prefix), uuid=batch.uuid))}
+ % endif
+%def>
+
${form.render(form_id='batch-form', buttons=capture(buttons))|n}
diff --git a/tailbone/templates/batch/view.mako b/tailbone/templates/batch/view.mako
index dd8ae649..74cb10d3 100644
--- a/tailbone/templates/batch/view.mako
+++ b/tailbone/templates/batch/view.mako
@@ -13,6 +13,13 @@
%def>
+<%def name="context_menu_items()">
+ ${parent.context_menu_items()}
+ % if request.has_perm('{0}.csv'.format(permission_prefix)):
+
${h.link_to("Download this {0} as CSV".format(batch_display), url('{0}.csv'.format(route_prefix), uuid=batch.uuid))}
+ % endif
+%def>
+
<%def name="buttons()">
% if not form.readonly and batch.refreshable:
diff --git a/tailbone/views/batch.py b/tailbone/views/batch.py
index b824bece..68715341 100644
--- a/tailbone/views/batch.py
+++ b/tailbone/views/batch.py
@@ -32,6 +32,9 @@ from __future__ import unicode_literals
import os
import datetime
import logging
+from cStringIO import StringIO
+
+from sqlalchemy import orm
import formalchemy
from pyramid.renderers import render_to_response
@@ -42,6 +45,7 @@ from webhelpers.html.tags import link_to, HTML
from rattail.db import model
from rattail.db import Session as RatSession
from rattail.threads import Thread
+from rattail.csvutil import UnicodeDictWriter
from tailbone.db import Session
from tailbone.views import SearchableAlchemyGridView, CrudView
@@ -366,6 +370,10 @@ class BatchCrud(BaseCrud):
def batch_class(self):
raise NotImplementedError
+ @property
+ def batch_row_class(self):
+ raise NotImplementedError
+
@property
def mapped_class(self):
return self.batch_class
@@ -705,6 +713,48 @@ class BatchCrud(BaseCrud):
progress.session['success_url'] = self.view_url(batch.uuid)
progress.session.save()
+ def csv(self):
+ """
+ Download batch data as CSV.
+ """
+ batch = self.current_batch()
+ fields = self.get_csv_fields()
+ data = StringIO()
+ writer = UnicodeDictWriter(data, fields)
+ writer.writeheader()
+ for row in batch.data_rows:
+ if not row.removed:
+ writer.writerow(self.get_csv_row(row, 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=batch.csv'
+ return response
+
+ def get_csv_fields(self):
+ """
+ Return the list of fields to be written to CSV download.
+ """
+ fields = []
+ mapper = orm.class_mapper(self.batch_row_class)
+ for prop in mapper.iterate_properties:
+ if isinstance(prop, orm.ColumnProperty):
+ if prop.key != 'removed' and not prop.key.endswith('uuid'):
+ fields.append(prop.key)
+ return fields
+
+ def get_csv_row(self, row, fields):
+ """
+ Return a dict for use when writing the row's data to CSV download.
+ """
+ csvrow = {}
+ for field in fields:
+ value = getattr(row, field)
+ csvrow[field] = '' if value is None else unicode(value)
+ return csvrow
+
class FileBatchCrud(BatchCrud):
"""
@@ -1163,6 +1213,11 @@ def defaults(config, batch_grid, batch_crud, row_grid, row_crud, url_prefix,
config.add_view(batch_crud, attr='execute', route_name='{0}.execute'.format(route_prefix),
permission='{0}.execute'.format(permission_prefix))
+ # Download batch row data as CSV
+ config.add_route('{0}.csv'.format(route_prefix), '{0}{{uuid}}/csv'.format(url_prefix))
+ config.add_view(batch_crud, attr='csv', route_name='{0}.csv'.format(route_prefix),
+ permission='{0}.csv'.format(permission_prefix))
+
# Download batch data file
if hasattr(batch_crud, 'download'):
config.add_route('{0}.download'.format(route_prefix), '{0}{{uuid}}/download'.format(url_prefix))