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

@ -0,0 +1,17 @@
## -*- coding: utf-8 -*-
<%inherit file="/master/edit.mako" />
<%def name="context_menu_items()">
<li>${h.link_to("Back to {}".format(model_title), index_url)}</li>
% if master.rows_viewable and request.has_perm('{}.view'.format(row_permission_prefix)):
<li>${h.link_to("View this {}".format(row_model_title), row_action_url('view', instance))}</li>
% endif
% if master.rows_deletable and instance_deletable and request.has_perm('{}.delete'.format(row_permission_prefix)):
<li>${h.link_to("Delete this {}".format(row_model_title), row_action_url('delete', instance))}</li>
% endif
% if master.rows_creatable and request.has_perm('{}.create'.format(row_permission_prefix)):
<li>${h.link_to("Create a new {}".format(row_model_title), url('{}.create'.format(row_route_prefix)))}</li>
% endif
</%def>
${parent.body()}

View file

@ -4,7 +4,7 @@
<%def name="title()">${model_title}</%def> <%def name="title()">${model_title}</%def>
<%def name="context_menu_items()"> <%def name="context_menu_items()">
<li>${h.link_to("Back to {}".format(batch_model_title), index_url)}</li> <li>${h.link_to("Back to {}".format(parent_model_title), index_url)}</li>
% if master.rows_editable and instance_editable and request.has_perm('{}.edit'.format(permission_prefix)): % if master.rows_editable and instance_editable and request.has_perm('{}.edit'.format(permission_prefix)):
<li>${h.link_to("Edit this {}".format(model_title), action_url('edit', instance))}</li> <li>${h.link_to("Edit this {}".format(model_title), action_url('edit', instance))}</li>
% endif % endif
@ -16,4 +16,10 @@
% endif % endif
</%def> </%def>
${parent.body()} <ul id="context-menu">
${self.context_menu_items()}
</ul>
<div class="form-wrapper">
${form.render()|n}
</div><!-- form-wrapper -->

View file

@ -42,7 +42,7 @@
% if master.listing: % if master.listing:
<span class="global">${model_title_plural}</span> <span class="global">${model_title_plural}</span>
% else: % else:
${h.link_to(model_title_plural, index_url, class_='global')} ${h.link_to(index_title, index_url, class_='global')}
% if master.viewing and grid_index: % if master.viewing and grid_index:
${grid_index_nav()} ${grid_index_nav()}
% endif % endif

View file

@ -63,11 +63,9 @@ class BatchMasterView(MasterView):
""" """
Base class for all "batch master" views. Base class for all "batch master" views.
""" """
refreshable = True has_rows = True
rows_viewable = True
rows_creatable = False
rows_editable = False
rows_deletable = True rows_deletable = True
refreshable = True
def __init__(self, request): def __init__(self, request):
super(BatchMasterView, self).__init__(request) super(BatchMasterView, self).__init__(request)
@ -96,45 +94,17 @@ class BatchMasterView(MasterView):
return load_object(spec)(self.rattail_config) return load_object(spec)(self.rattail_config)
return self.batch_handler_class(self.rattail_config) return self.batch_handler_class(self.rattail_config)
def view(self): def template_kwargs_view(self, **kwargs):
""" batch = kwargs['instance']
View for viewing details of an existing model record. kwargs['batch'] = batch
""" kwargs['handler'] = self.handler
self.viewing = True kwargs['execute_title'] = self.get_execute_title(batch)
batch = self.get_instance() kwargs['execute_enabled'] = self.executable(batch)
form = self.make_form(batch) if kwargs['execute_enabled'] and self.has_execution_options:
grid = self.make_row_grid(batch=batch) kwargs['rendered_execution_options'] = self.render_execution_options(batch)
return kwargs
# If user just refreshed the page with a reset instruction, issue a def render_execution_options(self, batch):
# 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()
form = self.make_execution_options_form(batch) form = self.make_execution_options_form(batch)
kwargs = { kwargs = {
'batch': batch, 'batch': batch,
@ -302,6 +272,7 @@ class BatchMasterView(MasterView):
""" """
return True return True
# TODO: some of this at least can go to master now right?
def edit(self): def edit(self):
""" """
Don't allow editing a batch which has already been executed. 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: 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) return self.render_to_response('edit', context)
@ -500,175 +471,80 @@ class BatchMasterView(MasterView):
# batch rows # batch rows
######################################## ########################################
def get_row_model_title(self): def get_row_instance_title(self, row):
return "{} Row".format(self.get_model_title()) return "Row {}".format(row.sequence)
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 _preconfigure_row_grid(self, g): def _preconfigure_row_grid(self, g):
g.filters['status_code'].label = "Status" 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.default_sortkey = 'sequence'
g.sequence.set(label="Seq.") g.sequence.set(label="Seq.")
g.status_code.set(label="Status", g.status_code.set(label="Status",
renderer=StatusRenderer(self.batch_row_class.STATUS)) renderer=StatusRenderer(self.model_row_class.STATUS))
def configure_row_grid(self, grid):
grid.configure()
def get_row_data(self, batch): def get_row_data(self, batch):
""" """
Generate the base data set for a rows grid. Generate the base data set for a rows grid.
""" """
session = orm.object_session(batch) return self.Session.query(self.model_row_class)\
return session.query(self.batch_row_class)\ .filter(self.model_row_class.batch == batch)\
.filter(self.batch_row_class.batch == batch)\ .filter(self.model_row_class.removed == False)
.filter(self.batch_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() return self.rows_editable and not row.batch.executed
permission_prefix = self.get_row_permission_prefix()
defaults = { def row_edit_action_url(self, row, i):
'width': 'full', if self.row_editable(row):
'filterable': True, return self.get_row_action_url('edit', row)
'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,
}
if 'main_actions' not in defaults: def row_deletable(self, row):
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):
""" """
View for viewing details of a single batch row. Batch rows are deletable only until batch has been executed.
""" """
self.viewing = True return self.rows_deletable and not row.batch.executed
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})
def get_row_instance(self): def row_delete_action_url(self, row, i):
key = self.request.matchdict[self.get_model_key()] if self.row_deletable(row):
instance = self.Session.query(self.batch_row_class).get(key) return self.get_row_action_url('delete', row)
if not instance:
raise httpexceptions.HTTPNotFound()
return instance
def get_row_instance_title(self, instance): def _preconfigure_row_fieldset(self, fs):
return self.get_row_model_title() fs.sequence.set(readonly=True)
fs.status_code.set(renderer=StatusRenderer(self.model_row_class.STATUS),
def make_row_form(self, instance, **kwargs): label="Status", readonly=True)
""" fs.status_text.set(readonly=True)
Make a FormAlchemy form for use with CRUD views for a batch *row*. fs.removed.set(readonly=True)
""" try:
# TODO: Some hacky stuff here, to accommodate old form cruft. Probably fs.product.set(readonly=True)
# should refactor forms soon too, but trying to avoid it for the moment. except AttributeError:
pass
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 configure_row_fieldset(self, fs): def configure_row_fieldset(self, fs):
fs.configure() 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): def delete_row(self):
""" """
"Delete" a row from the batch. This sets the ``removed`` flag on the "Delete" a row from the batch. This sets the ``removed`` flag on the
row but does not truly delete it. 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: if not row:
raise httpexceptions.HTTPNotFound() raise httpexceptions.HTTPNotFound()
row.removed = True 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): def bulk_delete_rows(self):
""" """
@ -679,17 +555,6 @@ class BatchMasterView(MasterView):
query.update({'removed': True}, synchronize_session=False) query.update({'removed': True}, synchronize_session=False)
return self.redirect(self.get_action_url('view', self.get_instance())) 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): def execute(self):
""" """
Execute a batch. Starts a separate thread for the execution, and 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. Return the list of fields to be written to CSV download.
""" """
fields = [] fields = []
mapper = orm.class_mapper(self.batch_row_class) mapper = orm.class_mapper(self.model_row_class)
for prop in mapper.iterate_properties: for prop in mapper.iterate_properties:
if isinstance(prop, orm.ColumnProperty): if isinstance(prop, orm.ColumnProperty):
if prop.key != 'removed' and not prop.key.endswith('uuid'): if prop.key != 'removed' and not prop.key.endswith('uuid'):
@ -824,24 +689,11 @@ class BatchMasterView(MasterView):
permission_prefix = cls.get_permission_prefix() permission_prefix = cls.get_permission_prefix()
model_title = cls.get_model_title() 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 # refresh rows data
config.add_route('{}.refresh'.format(route_prefix), '{}/{{uuid}}/refresh'.format(url_prefix)) config.add_route('{}.refresh'.format(route_prefix), '{}/{{uuid}}/refresh'.format(url_prefix))
config.add_view(cls, attr='refresh', route_name='{}.refresh'.format(route_prefix), config.add_view(cls, attr='refresh', route_name='{}.refresh'.format(route_prefix),
permission='{}.create'.format(permission_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 # bulk delete rows
config.add_route('{}.rows.bulk_delete'.format(route_prefix), '{}/{{uuid}}/rows/delete'.format(url_prefix)) 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), config.add_view(cls, attr='bulk_delete_rows', route_name='{}.rows.bulk_delete'.format(route_prefix),

View file

@ -73,11 +73,16 @@ class HandheldBatchView(FileBatchMasterView):
""" """
model_class = model.HandheldBatch model_class = model.HandheldBatch
model_title_plural = "Handheld Batches" model_title_plural = "Handheld Batches"
batch_row_class = model.HandheldBatchRow
batch_handler_class = HandheldBatchHandler batch_handler_class = HandheldBatchHandler
route_prefix = 'batch.handheld' route_prefix = 'batch.handheld'
url_prefix = '/batch/handheld' url_prefix = '/batch/handheld'
execution_options_schema = ExecutionOptions execution_options_schema = ExecutionOptions
editable = False
refreshable = False
model_row_class = model.HandheldBatchRow
rows_creatable = False
rows_editable = True
def configure_grid(self, g): def configure_grid(self, g):
g.configure( g.configure(
@ -105,7 +110,7 @@ class HandheldBatchView(FileBatchMasterView):
]) ])
if self.creating: if self.creating:
del fs.id del fs.id
elif self.viewing: elif self.viewing and fs.model.inventory_batch:
fs.append(fa.Field('inventory_batch', value=fs.model.inventory_batch, renderer=InventoryBatchFieldRenderer)) fs.append(fa.Field('inventory_batch', value=fs.model.inventory_batch, renderer=InventoryBatchFieldRenderer))
def configure_row_grid(self, g): def configure_row_grid(self, g):
@ -128,6 +133,27 @@ class HandheldBatchView(FileBatchMasterView):
attrs['class_'] = 'warning' attrs['class_'] = 'warning'
return attrs return attrs
def _preconfigure_row_fieldset(self, fs):
super(HandheldBatchView, self)._preconfigure_row_fieldset(fs)
fs.upc.set(readonly=True, label="UPC", renderer=forms.renderers.GPCFieldRenderer,
attrs={'link': lambda r: self.request.route_url('products.view', uuid=r.product_uuid)})
fs.brand_name.set(readonly=True)
fs.description.set(readonly=True)
fs.size.set(readonly=True)
def configure_row_fieldset(self, fs):
fs.configure(
include=[
fs.sequence,
fs.upc,
fs.brand_name,
fs.description,
fs.size,
fs.status_code,
fs.cases,
fs.units,
])
def get_exec_options_kwargs(self, **kwargs): def get_exec_options_kwargs(self, **kwargs):
kwargs['ACTION_OPTIONS'] = list(ACTION_OPTIONS.iteritems()) kwargs['ACTION_OPTIONS'] = list(ACTION_OPTIONS.iteritems())
return kwargs return kwargs

View file

@ -26,6 +26,8 @@ Views for inventory batches
from __future__ import unicode_literals, absolute_import from __future__ import unicode_literals, absolute_import
from rattail import enum
from rattail.db import model
from rattail.db.batch.inventory.handler import InventoryBatchHandler from rattail.db.batch.inventory.handler import InventoryBatchHandler
import formalchemy as fa import formalchemy as fa
@ -34,8 +36,6 @@ from webhelpers.html import tags
from tailbone import forms from tailbone import forms
from tailbone.views.batch import BatchMasterView from tailbone.views.batch import BatchMasterView
from dtail.db import model
class HandheldBatchFieldRenderer(fa.FieldRenderer): class HandheldBatchFieldRenderer(fa.FieldRenderer):
""" """
@ -57,11 +57,30 @@ class InventoryBatchView(BatchMasterView):
""" """
model_class = model.InventoryBatch model_class = model.InventoryBatch
model_title_plural = "Inventory Batches" model_title_plural = "Inventory Batches"
batch_row_class = model.InventoryBatchRow
batch_handler_class = InventoryBatchHandler batch_handler_class = InventoryBatchHandler
route_prefix = 'batch.inventory' route_prefix = 'batch.inventory'
url_prefix = '/batch/inventory' url_prefix = '/batch/inventory'
creatable = False creatable = False
editable = False
refreshable = False
model_row_class = model.InventoryBatchRow
rows_editable = True
def _preconfigure_grid(self, g):
super(InventoryBatchView, self)._preconfigure_grid(g)
g.mode.set(renderer=forms.renderers.EnumFieldRenderer(enum.INVENTORY_MODE),
label="Count Mode")
def configure_grid(self, g):
super(InventoryBatchView, self).configure_grid(g)
g.append(g.mode)
def _preconfigure_fieldset(self, fs):
super(InventoryBatchView, self)._preconfigure_fieldset(fs)
fs.handheld_batch.set(renderer=HandheldBatchFieldRenderer, readonly=True)
fs.mode.set(renderer=forms.renderers.EnumFieldRenderer(enum.INVENTORY_MODE),
label="Count Mode")
def configure_fieldset(self, fs): def configure_fieldset(self, fs):
fs.configure( fs.configure(
@ -69,24 +88,29 @@ class InventoryBatchView(BatchMasterView):
fs.id, fs.id,
fs.created, fs.created,
fs.created_by, fs.created_by,
fs.handheld_batch.with_renderer(HandheldBatchFieldRenderer), fs.handheld_batch,
fs.mode,
fs.executed, fs.executed,
fs.executed_by, fs.executed_by,
]) ])
if not self.viewing:
del fs.handheld_batch def _preconfigure_row_grid(self, g):
super(InventoryBatchView, self)._preconfigure_row_grid(g)
g.upc.set(label="UPC")
g.brand_name.set(label="Brand")
g.status_code.set(label="Status")
def configure_row_grid(self, g): def configure_row_grid(self, g):
g.configure( g.configure(
include=[ include=[
g.sequence, g.sequence,
g.upc.label("UPC"), g.upc,
g.brand_name.label("Brand"), g.brand_name,
g.description, g.description,
g.size, g.size,
g.cases, g.cases,
g.units, g.units,
g.status_code.label("Status"), g.status_code,
], ],
readonly=True) readonly=True)
@ -96,6 +120,27 @@ class InventoryBatchView(BatchMasterView):
attrs['class_'] = 'warning' attrs['class_'] = 'warning'
return attrs return attrs
def _preconfigure_row_fieldset(self, fs):
super(InventoryBatchView, self)._preconfigure_row_fieldset(fs)
fs.upc.set(readonly=True, label="UPC", renderer=forms.renderers.GPCFieldRenderer,
attrs={'link': lambda r: self.request.route_url('products.view', uuid=r.product_uuid)})
fs.brand_name.set(readonly=True)
fs.description.set(readonly=True)
fs.size.set(readonly=True)
def configure_row_fieldset(self, fs):
fs.configure(
include=[
fs.sequence,
fs.upc,
fs.brand_name,
fs.description,
fs.size,
fs.status_code,
fs.cases,
fs.units,
])
@classmethod @classmethod
def defaults(cls, config): def defaults(cls, config):

View file

@ -71,6 +71,10 @@ class MasterView(View):
has_rows = False has_rows = False
model_row_class = None model_row_class = None
rows_viewable = True
rows_creatable = False
rows_editable = False
rows_deletable = False
@property @property
def Session(self): def Session(self):
@ -146,12 +150,16 @@ class MasterView(View):
form = self.make_form(instance) form = self.make_form(instance)
if self.has_rows: if self.has_rows:
# must make grid prior to redirecting from filter reset, b/c the
# grid will detect the filter reset request and store defaults in
# the session, that way redirect will then show The Right Thing
grid = self.make_row_grid(instance=instance)
# If user just refreshed the page with a reset instruction, issue a # If user just refreshed the page with a reset instruction, issue a
# redirect in order to clear out the query string. # redirect in order to clear out the query string.
if self.request.GET.get('reset-to-default-filters') == 'true': if self.request.GET.get('reset-to-default-filters') == 'true':
return self.redirect(self.request.current_route_url(_query=None)) return self.redirect(self.request.current_route_url(_query=None))
grid = self.make_row_grid(instance=instance)
if self.request.params.get('partial'): if self.request.params.get('partial'):
self.request.response.content_type = b'text/html' self.request.response.content_type = b'text/html'
self.request.response.text = grid.render_grid() self.request.response.text = grid.render_grid()
@ -176,16 +184,29 @@ class MasterView(View):
""" """
Make and return a new (configured) rows grid instance. Make and return a new (configured) rows grid instance.
""" """
instance = kwargs.pop('instance', self.get_instance()) parent = kwargs.pop('instance', self.get_instance())
data = self.get_row_data(instance) data = self.get_row_data(parent)
kwargs['instance'] = parent
kwargs = self.make_row_grid_kwargs(**kwargs) kwargs = self.make_row_grid_kwargs(**kwargs)
key = '{}.{}'.format(self.get_grid_key(), self.request.matchdict[self.get_model_key()]) key = '{}.{}'.format(self.get_grid_key(), self.request.matchdict[self.get_model_key()])
grid = grids.AlchemyGrid(key, self.request, data=data, model_class=self.model_row_class, **kwargs) factory = self.get_grid_factory()
grid = factory(key, self.request, data=data, model_class=self.model_row_class, **kwargs)
self._preconfigure_row_grid(grid) self._preconfigure_row_grid(grid)
self.configure_row_grid(grid) self.configure_row_grid(grid)
grid.load_settings() grid.load_settings()
return grid return grid
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.
"""
parent = self.get_instance()
grid = self.make_row_grid(instance=parent, sortable=False, pageable=False,
main_actions=[])
return grid._fa_grid.rows
def _preconfigure_row_grid(self, g): def _preconfigure_row_grid(self, g):
pass pass
@ -206,6 +227,14 @@ class MasterView(View):
""" """
return "{}.rows".format(cls.get_route_prefix()) return "{}.rows".format(cls.get_route_prefix())
@classmethod
def get_row_url_prefix(cls):
"""
Returns a prefix which (by default) applies to all URLs provided by the
master view class, for "row" views, e.g. '/products/rows'.
"""
return getattr(cls, 'row_url_prefix', '{}/rows'.format(cls.get_url_prefix()))
@classmethod @classmethod
def get_row_permission_prefix(cls): def get_row_permission_prefix(cls):
""" """
@ -232,17 +261,44 @@ class MasterView(View):
'permission_prefix': permission_prefix, 'permission_prefix': permission_prefix,
'route_prefix': route_prefix, 'route_prefix': route_prefix,
} }
if self.has_rows and 'main_actions' not in defaults:
actions = []
# view action
if self.rows_viewable:
view = lambda r, i: self.get_row_action_url('view', r)
actions.append(grids.GridAction('view', icon='zoomin', url=view))
# edit action
if self.rows_editable:
actions.append(grids.GridAction('edit', icon='pencil', url=self.row_edit_action_url))
# delete action
if self.rows_deletable:
actions.append(grids.GridAction('delete', icon='trash', url=self.row_delete_action_url))
defaults['main_actions'] = actions
defaults.update(kwargs) defaults.update(kwargs)
return defaults return defaults
def row_edit_action_url(self, row, i):
return self.get_row_action_url('edit', row)
def row_delete_action_url(self, row, i):
return self.get_row_action_url('delete', row)
def row_grid_row_attrs(self, row, i): def row_grid_row_attrs(self, row, i):
return {} return {}
def get_row_model_title(self): @classmethod
return "{} Row".format(self.get_model_title()) def get_row_model_title(cls):
return "{} Row".format(cls.get_model_title())
def get_row_model_title_plural(self): @classmethod
return "{} Rows".format(self.get_model_title()) def get_row_model_title_plural(cls):
return "{} Rows".format(cls.get_model_title())
def view_index(self): def view_index(self):
""" """
@ -459,8 +515,17 @@ class MasterView(View):
'action_url': self.get_action_url, 'action_url': self.get_action_url,
'grid_index': self.grid_index, 'grid_index': self.grid_index,
} }
if self.grid_index: if self.grid_index:
context['grid_count'] = self.grid_count context['grid_count'] = self.grid_count
if self.has_rows:
context['row_route_prefix'] = self.get_row_route_prefix()
context['row_permission_prefix'] = self.get_row_permission_prefix()
context['row_model_title'] = self.get_row_model_title()
context['row_model_title_plural'] = self.get_row_model_title_plural()
context['row_action_url'] = self.get_row_action_url
context.update(data) context.update(data)
context.update(self.template_kwargs(**context)) context.update(self.template_kwargs(**context))
if hasattr(self, 'template_kwargs_{}'.format(template)): if hasattr(self, 'template_kwargs_{}'.format(template)):
@ -870,6 +935,155 @@ class MasterView(View):
return self.after_delete_url return self.after_delete_url
return self.get_index_url() return self.get_index_url()
##############################
# Associated Rows Stuff
##############################
def view_row(self):
"""
View for viewing details of a single data row.
"""
self.viewing = True
row = self.get_row_instance()
form = self.make_row_form(row)
parent = self.get_parent(row)
return self.render_to_response('view_row', {
'instance': row,
'instance_title': self.get_row_instance_title(row),
'instance_editable': self.row_editable(row),
'instance_deletable': self.row_deletable(row),
'model_title': self.get_row_model_title(),
'model_title_plural': self.get_row_model_title_plural(),
'parent_model_title': self.get_model_title(),
'index_url': self.get_action_url('view', parent),
'index_title': '{} {}'.format(
self.get_model_title(),
self.get_instance_title(parent)),
'action_url': self.get_row_action_url,
'form': form})
def edit_row(self):
"""
View for editing an existing model record.
"""
self.editing = True
row = self.get_row_instance()
form = self.make_row_form(row)
if self.request.method == 'POST':
if form.validate():
self.save_edit_row_form(form)
return self.redirect_after_edit_row(row)
parent = self.get_parent(row)
return self.render_to_response('edit_row', {
'instance': row,
'instance_title': self.get_row_instance_title(row),
'instance_deletable': self.row_deletable(row),
'index_url': self.get_action_url('view', parent),
'index_title': '{} {}'.format(
self.get_model_title(),
self.get_instance_title(parent)),
'form': form})
def save_edit_row_form(self, form):
self.save_row_form(form)
self.after_edit_row(form.fieldset.model)
def save_row_form(self, form):
form.save()
def after_edit_row(self, row):
"""
Event hook, called just after an existing row object is saved.
"""
def redirect_after_edit_row(self, row):
return self.redirect(self.get_action_url('view', self.get_parent(row)))
def row_editable(self, row):
"""
Returns boolean indicating whether or not the given row can be
considered "editable". Returns ``True`` by default; override as
necessary.
"""
return True
def row_deletable(self, row):
"""
Returns boolean indicating whether or not the given row can be
considered "deletable". Returns ``True`` by default; override as
necessary.
"""
return True
def delete_row(self):
"""
"Delete" a sub-row from the parent.
"""
row = self.Session.query(self.model_row_class).get(self.request.matchdict['uuid'])
if not row:
raise httpexceptions.HTTPNotFound()
self.Session.delete(row)
return self.redirect(self.get_action_url('edit', self.get_parent(row)))
def get_parent(self, row):
raise NotImplementedError
def get_row_instance_title(self, instance):
return self.get_row_model_title()
def get_row_instance(self):
key = self.request.matchdict[self.get_model_key()]
instance = self.Session.query(self.model_row_class).get(key)
if not instance:
raise httpexceptions.HTTPNotFound()
return instance
def make_row_form(self, instance, **kwargs):
"""
Make a FormAlchemy form for use with CRUD views for a data *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._preconfigure_row_fieldset(fieldset)
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', self.get_parent(instance)))
else:
kwargs.setdefault('cancel_url', self.get_row_action_url('view', instance))
form = forms.AlchemyForm(self.request, fieldset, **kwargs)
form.readonly = self.viewing
return form
def _preconfigure_row_fieldset(self, fs):
pass
def configure_row_fieldset(self, fs):
fs.configure()
def get_row_action_url(self, action, row):
"""
Generate a URL for the given action on the given row.
"""
return self.request.route_url('{}.{}'.format(self.get_row_route_prefix(), action),
**self.get_row_action_route_kwargs(row))
def get_row_action_route_kwargs(self, row):
"""
Hopefully generic kwarg generator for basic action routes.
"""
# TODO: make this smarter?
return {'uuid': row.uuid}
############################## ##############################
# Config Stuff # Config Stuff
############################## ##############################
@ -892,6 +1106,10 @@ class MasterView(View):
model_key = cls.get_model_key() model_key = cls.get_model_key()
model_title = cls.get_model_title() model_title = cls.get_model_title()
model_title_plural = cls.get_model_title_plural() model_title_plural = cls.get_model_title_plural()
if cls.has_rows:
row_route_prefix = cls.get_row_route_prefix()
row_url_prefix = cls.get_row_url_prefix()
row_model_title = cls.get_row_model_title()
config.add_tailbone_permission_group(permission_prefix, model_title_plural, overwrite=False) config.add_tailbone_permission_group(permission_prefix, model_title_plural, overwrite=False)
@ -940,3 +1158,23 @@ class MasterView(View):
permission='{0}.delete'.format(permission_prefix)) permission='{0}.delete'.format(permission_prefix))
config.add_tailbone_permission(permission_prefix, '{0}.delete'.format(permission_prefix), config.add_tailbone_permission(permission_prefix, '{0}.delete'.format(permission_prefix),
"Delete {0}".format(model_title)) "Delete {0}".format(model_title))
### sub-rows stuff follows
# view row
if cls.has_rows and cls.rows_viewable:
config.add_route('{}.view'.format(row_route_prefix), '{}/{{uuid}}'.format(row_url_prefix))
config.add_view(cls, attr='view_row', route_name='{}.view'.format(row_route_prefix),
permission='{}.view'.format(permission_prefix))
# edit row
if cls.has_rows and cls.rows_editable:
config.add_route('{}.edit'.format(row_route_prefix), '{}/{{uuid}}/edit'.format(row_url_prefix))
config.add_view(cls, attr='edit_row', route_name='{}.edit'.format(row_route_prefix),
permission='{}.edit'.format(permission_prefix))
# delete row
if cls.has_rows and cls.rows_deletable:
config.add_route('{}.delete'.format(row_route_prefix), '{}/{{uuid}}/delete'.format(row_url_prefix))
config.add_view(cls, attr='delete_row', route_name='{}.delete'.format(row_route_prefix),
permission='{}.edit'.format(permission_prefix))

View file

@ -47,7 +47,7 @@ class VendorCatalogsView(FileBatchMasterView):
Master view for vendor catalog batches. Master view for vendor catalog batches.
""" """
model_class = model.VendorCatalog model_class = model.VendorCatalog
batch_row_class = model.VendorCatalogRow model_row_class = model.VendorCatalogRow
batch_handler_class = VendorCatalogHandler batch_handler_class = VendorCatalogHandler
url_prefix = '/vendors/catalogs' url_prefix = '/vendors/catalogs'

View file

@ -41,7 +41,7 @@ class VendorInvoicesView(FileBatchMasterView):
Master view for vendor invoice batches. Master view for vendor invoice batches.
""" """
model_class = model.VendorInvoice model_class = model.VendorInvoice
batch_row_class = model.VendorInvoiceRow model_row_class = model.VendorInvoiceRow
batch_handler_class = VendorInvoiceHandler batch_handler_class = VendorInvoiceHandler
url_prefix = '/vendors/invoices' url_prefix = '/vendors/invoices'