Add MasterView.has_rows concept and related logic

Now the `BatchMasterView` no longer provides most of these goodies.

Also tweak some custom batch views to reflect changes etc.
This commit is contained in:
Lance Edgar 2016-08-23 13:11:13 -05:00
parent 8a19b90efa
commit 901c2fc573
9 changed files with 410 additions and 226 deletions

View file

@ -63,11 +63,9 @@ class BatchMasterView(MasterView):
"""
Base class for all "batch master" views.
"""
refreshable = True
rows_viewable = True
rows_creatable = False
rows_editable = False
has_rows = True
rows_deletable = True
refreshable = True
def __init__(self, request):
super(BatchMasterView, self).__init__(request)
@ -96,45 +94,17 @@ class BatchMasterView(MasterView):
return load_object(spec)(self.rattail_config)
return self.batch_handler_class(self.rattail_config)
def view(self):
"""
View for viewing details of an existing model record.
"""
self.viewing = True
batch = self.get_instance()
form = self.make_form(batch)
grid = self.make_row_grid(batch=batch)
def template_kwargs_view(self, **kwargs):
batch = kwargs['instance']
kwargs['batch'] = batch
kwargs['handler'] = self.handler
kwargs['execute_title'] = self.get_execute_title(batch)
kwargs['execute_enabled'] = self.executable(batch)
if kwargs['execute_enabled'] and self.has_execution_options:
kwargs['rendered_execution_options'] = self.render_execution_options(batch)
return kwargs
# If user just refreshed the page with a reset instruction, issue a
# redirect in order to clear out the query string.
if self.request.GET.get('reset-to-default-filters') == 'true':
return self.redirect(self.request.current_route_url(_query=None))
if self.request.params.get('partial'):
self.request.response.content_type = b'text/html'
self.request.response.text = grid.render_grid()
return self.request.response
context = {
'handler': self.handler,
'instance': batch,
'instance_title': self.get_instance_title(batch),
'instance_editable': self.editable_instance(batch),
'instance_deletable': self.deletable_instance(batch),
'form': form,
'batch': batch,
'execute_title': self.get_execute_title(batch),
'execute_enabled': self.executable(batch),
'rows_grid': grid.render_complete(allow_save_defaults=False),
}
if context['execute_enabled'] and self.has_execution_options:
context['rendered_execution_options'] = self.render_execution_options()
return self.render_to_response('view', context)
def render_execution_options(self):
batch = self.get_instance()
def render_execution_options(self, batch):
form = self.make_execution_options_form(batch)
kwargs = {
'batch': batch,
@ -302,6 +272,7 @@ class BatchMasterView(MasterView):
"""
return True
# TODO: some of this at least can go to master now right?
def edit(self):
"""
Don't allow editing a batch which has already been executed.
@ -343,7 +314,7 @@ class BatchMasterView(MasterView):
}
if context['execute_enabled'] and self.has_execution_options:
context['rendered_execution_options'] = self.render_execution_options()
context['rendered_execution_options'] = self.render_execution_options(batch)
return self.render_to_response('edit', context)
@ -500,175 +471,80 @@ class BatchMasterView(MasterView):
# batch rows
########################################
def get_row_model_title(self):
return "{} Row".format(self.get_model_title())
def get_row_model_title_plural(self):
return "{} Rows".format(self.get_model_title())
@classmethod
def get_row_permission_prefix(cls):
"""
Permission prefix specific to the row-level data for this batch type,
e.g. ``'vendorcatalogs.rows'``.
"""
return "{}.rows".format(cls.get_permission_prefix())
@classmethod
def get_row_route_prefix(cls):
"""
Route prefix specific to the row-level views for a batch, e.g.
``'vendorcatalogs.rows'``.
"""
return "{}.rows".format(cls.get_route_prefix())
def make_row_grid(self, **kwargs):
"""
Make and return a new (configured) batch rows grid instance.
"""
batch = kwargs.pop('batch', self.get_instance())
key = '{}.{}'.format(self.get_grid_key(), batch.uuid)
data = self.get_row_data(batch)
kwargs = self.make_row_grid_kwargs(**kwargs)
grid = grids.AlchemyGrid(key, self.request, data=data, model_class=self.batch_row_class, **kwargs)
self._preconfigure_row_grid(grid)
self.configure_row_grid(grid)
grid.load_settings()
return grid
def get_row_instance_title(self, row):
return "Row {}".format(row.sequence)
def _preconfigure_row_grid(self, g):
g.filters['status_code'].label = "Status"
g.filters['status_code'].set_value_renderer(grids.filters.EnumValueRenderer(self.batch_row_class.STATUS))
g.filters['status_code'].set_value_renderer(grids.filters.EnumValueRenderer(self.model_row_class.STATUS))
g.default_sortkey = 'sequence'
g.sequence.set(label="Seq.")
g.status_code.set(label="Status",
renderer=StatusRenderer(self.batch_row_class.STATUS))
def configure_row_grid(self, grid):
grid.configure()
renderer=StatusRenderer(self.model_row_class.STATUS))
def get_row_data(self, batch):
"""
Generate the base data set for a rows grid.
"""
session = orm.object_session(batch)
return session.query(self.batch_row_class)\
.filter(self.batch_row_class.batch == batch)\
.filter(self.batch_row_class.removed == False)
return self.Session.query(self.model_row_class)\
.filter(self.model_row_class.batch == batch)\
.filter(self.model_row_class.removed == False)
def make_row_grid_kwargs(self, **kwargs):
def row_editable(self, row):
"""
Return a dict of kwargs to be used when constructing a new rows grid.
Batch rows are editable only until batch has been executed.
"""
route_prefix = self.get_row_route_prefix()
permission_prefix = self.get_row_permission_prefix()
return self.rows_editable and not row.batch.executed
defaults = {
'width': 'full',
'filterable': True,
'sortable': True,
'pageable': True,
'row_attrs': self.row_grid_row_attrs,
'model_title': self.get_row_model_title(),
'model_title_plural': self.get_row_model_title_plural(),
'permission_prefix': permission_prefix,
'route_prefix': route_prefix,
}
def row_edit_action_url(self, row, i):
if self.row_editable(row):
return self.get_row_action_url('edit', row)
if 'main_actions' not in defaults:
actions = []
# view action
if self.rows_viewable:
view = lambda r, i: self.request.route_url('{}.view'.format(route_prefix),
uuid=r.uuid)
actions.append(grids.GridAction('view', icon='zoomin', url=view))
# delete action
batch = self.get_instance()
if self.editing and self.rows_deletable and not batch.executed:
delete = lambda r, i: self.request.route_url('{}.delete'.format(route_prefix),
uuid=r.uuid)
actions.append(grids.GridAction('delete', icon='trash', url=delete))
defaults['main_actions'] = actions
defaults.update(kwargs)
return defaults
def row_grid_row_attrs(self, row, i):
return {}
def view_row(self):
def row_deletable(self, row):
"""
View for viewing details of a single batch row.
Batch rows are deletable only until batch has been executed.
"""
self.viewing = True
row = self.get_row_instance()
form = self.make_row_form(row)
return self.render_to_response('view_row', {
'instance': row,
'instance_title': self.get_row_instance_title(row),
'instance_editable': False,
'instance_deletable': False,
'model_title': self.get_row_model_title(),
'model_title_plural': self.get_row_model_title_plural(),
'batch_model_title': self.get_model_title(),
'index_url': self.get_action_url('view', row.batch),
'index_title': '{} {}'.format(
self.get_model_title(),
self.get_instance_title(row.batch)),
'form': form})
return self.rows_deletable and not row.batch.executed
def get_row_instance(self):
key = self.request.matchdict[self.get_model_key()]
instance = self.Session.query(self.batch_row_class).get(key)
if not instance:
raise httpexceptions.HTTPNotFound()
return instance
def row_delete_action_url(self, row, i):
if self.row_deletable(row):
return self.get_row_action_url('delete', row)
def get_row_instance_title(self, instance):
return self.get_row_model_title()
def make_row_form(self, instance, **kwargs):
"""
Make a FormAlchemy form for use with CRUD views for a batch *row*.
"""
# TODO: Some hacky stuff here, to accommodate old form cruft. Probably
# should refactor forms soon too, but trying to avoid it for the moment.
kwargs.setdefault('creating', self.creating)
kwargs.setdefault('editing', self.editing)
fieldset = self.make_fieldset(instance)
self.configure_row_fieldset(fieldset)
kwargs.setdefault('action_url', self.request.current_route_url(_query=None))
if self.creating:
kwargs.setdefault('cancel_url', self.get_action_url('view', instance.batch))
else:
kwargs.setdefault('cancel_url', self.request.route_url('{}.view'.format(self.get_row_route_prefix()),
uuid=instance.uuid))
form = forms.AlchemyForm(self.request, fieldset, **kwargs)
form.readonly = self.viewing
return form
def _preconfigure_row_fieldset(self, fs):
fs.sequence.set(readonly=True)
fs.status_code.set(renderer=StatusRenderer(self.model_row_class.STATUS),
label="Status", readonly=True)
fs.status_text.set(readonly=True)
fs.removed.set(readonly=True)
try:
fs.product.set(readonly=True)
except AttributeError:
pass
def configure_row_fieldset(self, fs):
fs.configure()
del fs.batch
def template_kwargs_view_row(self, **kwargs):
kwargs['batch_model_title'] = kwargs['parent_model_title']
return kwargs
def get_parent(self, row):
return row.batch
def delete_row(self):
"""
"Delete" a row from the batch. This sets the ``removed`` flag on the
row but does not truly delete it.
"""
row = self.Session.query(self.batch_row_class).get(self.request.matchdict['uuid'])
row = self.Session.query(self.model_row_class).get(self.request.matchdict['uuid'])
if not row:
raise httpexceptions.HTTPNotFound()
row.removed = True
return self.redirect(self.get_action_url('edit', row.batch))
return self.redirect(self.get_action_url('view', self.get_parent(row)))
def bulk_delete_rows(self):
"""
@ -679,17 +555,6 @@ class BatchMasterView(MasterView):
query.update({'removed': True}, synchronize_session=False)
return self.redirect(self.get_action_url('view', self.get_instance()))
def get_effective_row_query(self):
"""
Convenience method which returns the "effective" query for the master
grid, filtered and sorted to match what would show on the UI, but not
paged etc.
"""
batch = self.get_instance()
grid = self.make_row_grid(batch=batch, sortable=False, pageable=False,
main_actions=[])
return grid._fa_grid.rows
def execute(self):
"""
Execute a batch. Starts a separate thread for the execution, and
@ -795,7 +660,7 @@ class BatchMasterView(MasterView):
Return the list of fields to be written to CSV download.
"""
fields = []
mapper = orm.class_mapper(self.batch_row_class)
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'):
@ -824,24 +689,11 @@ class BatchMasterView(MasterView):
permission_prefix = cls.get_permission_prefix()
model_title = cls.get_model_title()
# view row
if cls.rows_viewable:
config.add_route('{}.rows.view'.format(route_prefix), '{}/rows/{{uuid}}'.format(url_prefix))
config.add_view(cls, attr='view_row', route_name='{}.rows.view'.format(route_prefix),
permission='{}.rows.view'.format(permission_prefix))
config.add_tailbone_permission(permission_prefix, '{}.rows.view'.format(permission_prefix),
"View {} Row Details".format(model_title))
# refresh rows data
config.add_route('{}.refresh'.format(route_prefix), '{}/{{uuid}}/refresh'.format(url_prefix))
config.add_view(cls, attr='refresh', route_name='{}.refresh'.format(route_prefix),
permission='{}.create'.format(permission_prefix))
# delete row
config.add_route('{}.rows.delete'.format(route_prefix), '{}/delete-row/{{uuid}}'.format(url_prefix))
config.add_view(cls, attr='delete_row', route_name='{}.rows.delete'.format(route_prefix),
permission='{}.edit'.format(permission_prefix))
# bulk delete rows
config.add_route('{}.rows.bulk_delete'.format(route_prefix), '{}/{{uuid}}/rows/delete'.format(url_prefix))
config.add_view(cls, attr='bulk_delete_rows', route_name='{}.rows.bulk_delete'.format(route_prefix),