Add "download row results as CSV" feature to master view

This commit is contained in:
Lance Edgar 2017-10-14 14:14:24 -07:00
parent f338a03c97
commit c95e2dbb06
5 changed files with 67 additions and 53 deletions

View file

@ -40,9 +40,6 @@
<%def name="context_menu_items()"> <%def name="context_menu_items()">
${parent.context_menu_items()} ${parent.context_menu_items()}
% if master.rows_downloadable and request.has_perm('{}.csv'.format(permission_prefix)):
<li>${h.link_to("Download row data as CSV", url('{}.csv'.format(route_prefix), uuid=batch.uuid))}</li>
% endif
% if master.cloneable and request.has_perm('{}.clone'.format(permission_prefix)): % if master.cloneable and request.has_perm('{}.clone'.format(permission_prefix)):
<li>${h.link_to("Clone as new batch", url('{}.clone'.format(route_prefix), uuid=batch.uuid))}</li> <li>${h.link_to("Clone as new batch", url('{}.clone'.format(route_prefix), uuid=batch.uuid))}</li>
% endif % endif

View file

@ -61,6 +61,9 @@
% if master.cloneable and request.has_perm('{}.clone'.format(permission_prefix)): % if master.cloneable and request.has_perm('{}.clone'.format(permission_prefix)):
<li>${h.link_to("Clone this as new {}".format(model_title), url('{}.clone'.format(route_prefix), uuid=instance.uuid))}</li> <li>${h.link_to("Clone this as new {}".format(model_title), url('{}.clone'.format(route_prefix), uuid=instance.uuid))}</li>
% endif % endif
% if master.rows_downloadable_csv and request.has_perm('{}.row_results_csv'.format(permission_prefix)):
<li>${h.link_to("Download row results as CSV", url('{}.row_results_csv'.format(route_prefix), uuid=instance.uuid))}</li>
% endif
</%def> </%def>
<ul id="context-menu"> <ul id="context-menu">

View file

@ -37,7 +37,6 @@ from sqlalchemy import orm
from rattail.db import model, Session as RattailSession from rattail.db import model, Session as RattailSession
from rattail.threads import Thread from rattail.threads import Thread
from rattail.csvutil import UnicodeDictWriter
from rattail.util import load_object, prettify from rattail.util import load_object, prettify
import formalchemy as fa import formalchemy as fa
@ -64,7 +63,7 @@ class BatchMasterView(MasterView):
default_handler_spec = None default_handler_spec = None
has_rows = True has_rows = True
rows_deletable = True rows_deletable = True
rows_downloadable = True rows_downloadable_csv = True
refreshable = True refreshable = True
refresh_after_create = False refresh_after_create = False
edit_with_rows = False edit_with_rows = False
@ -905,47 +904,14 @@ class BatchMasterView(MasterView):
def get_execute_results_success_url(self, result, **kwargs): def get_execute_results_success_url(self, result, **kwargs):
return self.get_index_url() return self.get_index_url()
def csv(self): def get_row_csv_fields(self):
""" fields = super(BatchMasterView, self).get_row_csv_fields()
Download batch data as CSV. fields = [field for field in fields
""" if field != 'removed' and not field.endswith('uuid')]
batch = self.get_instance()
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.model_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 return fields
def get_csv_row(self, row, fields): def get_row_results_csv_filename(self, batch):
""" return '{}.{}.csv'.format(self.get_route_prefix(), batch.id_str)
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
def clone(self): def clone(self):
""" """
@ -1021,14 +987,6 @@ class BatchMasterView(MasterView):
config.add_tailbone_permission(permission_prefix, '{}.execute_multiple'.format(permission_prefix), config.add_tailbone_permission(permission_prefix, '{}.execute_multiple'.format(permission_prefix),
"Execute multiple {}".format(model_title_plural)) "Execute multiple {}".format(model_title_plural))
# download rows as CSV
if cls.rows_downloadable:
config.add_route('{}.csv'.format(route_prefix), '{}/{{uuid}}/csv'.format(url_prefix))
config.add_view(cls, attr='csv', route_name='{}.csv'.format(route_prefix),
permission='{}.csv'.format(permission_prefix))
config.add_tailbone_permission(permission_prefix, '{}.csv'.format(permission_prefix),
"Download {} rows as CSV".format(model_title))
class FileBatchMasterView(BatchMasterView): class FileBatchMasterView(BatchMasterView):
""" """

View file

@ -112,6 +112,7 @@ class MasterView(View):
rows_deletable_speedbump = False rows_deletable_speedbump = False
rows_bulk_deletable = False rows_bulk_deletable = False
rows_default_pagesize = 20 rows_default_pagesize = 20
rows_downloadable_csv = False
mobile_rows_creatable = False mobile_rows_creatable = False
mobile_rows_filterable = False mobile_rows_filterable = False
@ -1319,6 +1320,9 @@ class MasterView(View):
# default previously came from cls.get_normalized_model_name() but this is hopefully better # default previously came from cls.get_normalized_model_name() but this is hopefully better
return cls.get_route_prefix() return cls.get_route_prefix()
def get_row_grid_key(self):
return '{}.{}'.format(self.get_grid_key(), self.request.matchdict[self.get_model_key()])
def get_grid_actions(self): def get_grid_actions(self):
main, more = self.get_main_actions(), self.get_more_actions() main, more = self.get_main_actions(), self.get_more_actions()
if len(more) == 1: if len(more) == 1:
@ -1472,6 +1476,29 @@ class MasterView(View):
response.content_disposition = b'attachment; filename={}.csv'.format(self.get_grid_key()) response.content_disposition = b'attachment; filename={}.csv'.format(self.get_grid_key())
return response return response
def row_results_csv(self):
"""
Download current row results data for an object, as CSV
"""
obj = self.get_instance()
fields = self.get_row_csv_fields()
data = six.StringIO()
writer = UnicodeDictWriter(data, fields)
writer.writeheader()
for row in self.get_effective_row_data(sort=True):
writer.writerow(self.get_row_csv_row(row, fields))
response = self.request.response
response.body = data.getvalue()
data.close()
response.content_length = len(response.body)
response.content_type = b'text/csv'
filename = self.get_row_results_csv_filename(obj)
response.content_disposition = b'attachment; filename={}'.format(filename)
return response
def get_row_results_csv_filename(self, instance):
return '{}.csv'.format(self.get_row_grid_key())
def get_csv_fields(self): def get_csv_fields(self):
""" """
Return the list of fields to be written to CSV download. Return the list of fields to be written to CSV download.
@ -1483,6 +1510,17 @@ class MasterView(View):
fields.append(prop.key) fields.append(prop.key)
return fields return fields
def get_row_csv_fields(self):
"""
Return the list of row fields to be written to CSV download.
"""
fields = []
mapper = orm.class_mapper(self.model_row_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): def get_csv_row(self, obj, fields):
""" """
Return a dict for use when writing the row's data to CSV download. Return a dict for use when writing the row's data to CSV download.
@ -1493,6 +1531,16 @@ class MasterView(View):
csvrow[field] = '' if value is None else six.text_type(value) csvrow[field] = '' if value is None else six.text_type(value)
return csvrow return csvrow
def get_row_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 six.text_type(value)
return csvrow
############################## ##############################
# CRUD Stuff # CRUD Stuff
############################## ##############################
@ -2026,6 +2074,14 @@ class MasterView(View):
### sub-rows stuff follows ### sub-rows stuff follows
# download row results as CSV
if cls.has_rows and cls.rows_downloadable_csv:
config.add_tailbone_permission(permission_prefix, '{}.row_results_csv'.format(permission_prefix),
"Download {} results as CSV".format(row_model_title))
config.add_route('{}.row_results_csv'.format(route_prefix), '{}/{{uuid}}/rows-csv'.format(url_prefix))
config.add_view(cls, attr='row_results_csv', route_name='{}.row_results_csv'.format(route_prefix),
permission='{}.row_results_csv'.format(permission_prefix))
# create row # create row
if cls.has_rows: if cls.has_rows:
if cls.rows_creatable or cls.mobile_rows_creatable: if cls.rows_creatable or cls.mobile_rows_creatable:

View file

@ -126,7 +126,7 @@ class MasterView2(MasterView):
if factory is None: if factory is None:
factory = self.get_row_grid_factory() factory = self.get_row_grid_factory()
if key is None: if key is None:
key = '{}.{}'.format(self.get_grid_key(), self.request.matchdict[self.get_model_key()]) key = self.get_row_grid_key()
if data is None: if data is None:
data = self.get_row_data(instance) data = self.get_row_data(instance)
if columns is None: if columns is None: