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:
parent
8a19b90efa
commit
901c2fc573
17
tailbone/templates/master/edit_row.mako
Normal file
17
tailbone/templates/master/edit_row.mako
Normal 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()}
|
|
@ -4,7 +4,7 @@
|
|||
<%def name="title()">${model_title}</%def>
|
||||
|
||||
<%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)):
|
||||
<li>${h.link_to("Edit this {}".format(model_title), action_url('edit', instance))}</li>
|
||||
% endif
|
||||
|
@ -16,4 +16,10 @@
|
|||
% endif
|
||||
</%def>
|
||||
|
||||
${parent.body()}
|
||||
<ul id="context-menu">
|
||||
${self.context_menu_items()}
|
||||
</ul>
|
||||
|
||||
<div class="form-wrapper">
|
||||
${form.render()|n}
|
||||
</div><!-- form-wrapper -->
|
|
@ -42,7 +42,7 @@
|
|||
% if master.listing:
|
||||
<span class="global">${model_title_plural}</span>
|
||||
% 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:
|
||||
${grid_index_nav()}
|
||||
% endif
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -73,11 +73,16 @@ class HandheldBatchView(FileBatchMasterView):
|
|||
"""
|
||||
model_class = model.HandheldBatch
|
||||
model_title_plural = "Handheld Batches"
|
||||
batch_row_class = model.HandheldBatchRow
|
||||
batch_handler_class = HandheldBatchHandler
|
||||
route_prefix = 'batch.handheld'
|
||||
url_prefix = '/batch/handheld'
|
||||
execution_options_schema = ExecutionOptions
|
||||
editable = False
|
||||
refreshable = False
|
||||
|
||||
model_row_class = model.HandheldBatchRow
|
||||
rows_creatable = False
|
||||
rows_editable = True
|
||||
|
||||
def configure_grid(self, g):
|
||||
g.configure(
|
||||
|
@ -105,7 +110,7 @@ class HandheldBatchView(FileBatchMasterView):
|
|||
])
|
||||
if self.creating:
|
||||
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))
|
||||
|
||||
def configure_row_grid(self, g):
|
||||
|
@ -128,6 +133,27 @@ class HandheldBatchView(FileBatchMasterView):
|
|||
attrs['class_'] = 'warning'
|
||||
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):
|
||||
kwargs['ACTION_OPTIONS'] = list(ACTION_OPTIONS.iteritems())
|
||||
return kwargs
|
||||
|
|
|
@ -26,6 +26,8 @@ Views for inventory batches
|
|||
|
||||
from __future__ import unicode_literals, absolute_import
|
||||
|
||||
from rattail import enum
|
||||
from rattail.db import model
|
||||
from rattail.db.batch.inventory.handler import InventoryBatchHandler
|
||||
|
||||
import formalchemy as fa
|
||||
|
@ -34,8 +36,6 @@ from webhelpers.html import tags
|
|||
from tailbone import forms
|
||||
from tailbone.views.batch import BatchMasterView
|
||||
|
||||
from dtail.db import model
|
||||
|
||||
|
||||
class HandheldBatchFieldRenderer(fa.FieldRenderer):
|
||||
"""
|
||||
|
@ -57,11 +57,30 @@ class InventoryBatchView(BatchMasterView):
|
|||
"""
|
||||
model_class = model.InventoryBatch
|
||||
model_title_plural = "Inventory Batches"
|
||||
batch_row_class = model.InventoryBatchRow
|
||||
batch_handler_class = InventoryBatchHandler
|
||||
route_prefix = 'batch.inventory'
|
||||
url_prefix = '/batch/inventory'
|
||||
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):
|
||||
fs.configure(
|
||||
|
@ -69,24 +88,29 @@ class InventoryBatchView(BatchMasterView):
|
|||
fs.id,
|
||||
fs.created,
|
||||
fs.created_by,
|
||||
fs.handheld_batch.with_renderer(HandheldBatchFieldRenderer),
|
||||
fs.handheld_batch,
|
||||
fs.mode,
|
||||
fs.executed,
|
||||
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):
|
||||
g.configure(
|
||||
include=[
|
||||
g.sequence,
|
||||
g.upc.label("UPC"),
|
||||
g.brand_name.label("Brand"),
|
||||
g.upc,
|
||||
g.brand_name,
|
||||
g.description,
|
||||
g.size,
|
||||
g.cases,
|
||||
g.units,
|
||||
g.status_code.label("Status"),
|
||||
g.status_code,
|
||||
],
|
||||
readonly=True)
|
||||
|
||||
|
@ -96,6 +120,27 @@ class InventoryBatchView(BatchMasterView):
|
|||
attrs['class_'] = 'warning'
|
||||
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
|
||||
def defaults(cls, config):
|
||||
|
||||
|
|
|
@ -71,6 +71,10 @@ class MasterView(View):
|
|||
|
||||
has_rows = False
|
||||
model_row_class = None
|
||||
rows_viewable = True
|
||||
rows_creatable = False
|
||||
rows_editable = False
|
||||
rows_deletable = False
|
||||
|
||||
@property
|
||||
def Session(self):
|
||||
|
@ -146,12 +150,16 @@ class MasterView(View):
|
|||
form = self.make_form(instance)
|
||||
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
|
||||
# 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))
|
||||
|
||||
grid = self.make_row_grid(instance=instance)
|
||||
if self.request.params.get('partial'):
|
||||
self.request.response.content_type = b'text/html'
|
||||
self.request.response.text = grid.render_grid()
|
||||
|
@ -176,16 +184,29 @@ class MasterView(View):
|
|||
"""
|
||||
Make and return a new (configured) rows grid instance.
|
||||
"""
|
||||
instance = kwargs.pop('instance', self.get_instance())
|
||||
data = self.get_row_data(instance)
|
||||
parent = kwargs.pop('instance', self.get_instance())
|
||||
data = self.get_row_data(parent)
|
||||
kwargs['instance'] = parent
|
||||
kwargs = self.make_row_grid_kwargs(**kwargs)
|
||||
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.configure_row_grid(grid)
|
||||
grid.load_settings()
|
||||
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):
|
||||
pass
|
||||
|
||||
|
@ -206,6 +227,14 @@ class MasterView(View):
|
|||
"""
|
||||
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
|
||||
def get_row_permission_prefix(cls):
|
||||
"""
|
||||
|
@ -232,17 +261,44 @@ class MasterView(View):
|
|||
'permission_prefix': permission_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)
|
||||
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):
|
||||
return {}
|
||||
|
||||
def get_row_model_title(self):
|
||||
return "{} Row".format(self.get_model_title())
|
||||
@classmethod
|
||||
def get_row_model_title(cls):
|
||||
return "{} Row".format(cls.get_model_title())
|
||||
|
||||
def get_row_model_title_plural(self):
|
||||
return "{} Rows".format(self.get_model_title())
|
||||
@classmethod
|
||||
def get_row_model_title_plural(cls):
|
||||
return "{} Rows".format(cls.get_model_title())
|
||||
|
||||
def view_index(self):
|
||||
"""
|
||||
|
@ -459,8 +515,17 @@ class MasterView(View):
|
|||
'action_url': self.get_action_url,
|
||||
'grid_index': self.grid_index,
|
||||
}
|
||||
|
||||
if self.grid_index:
|
||||
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(self.template_kwargs(**context))
|
||||
if hasattr(self, 'template_kwargs_{}'.format(template)):
|
||||
|
@ -870,6 +935,155 @@ class MasterView(View):
|
|||
return self.after_delete_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
|
||||
##############################
|
||||
|
@ -892,6 +1106,10 @@ class MasterView(View):
|
|||
model_key = cls.get_model_key()
|
||||
model_title = cls.get_model_title()
|
||||
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)
|
||||
|
||||
|
@ -940,3 +1158,23 @@ class MasterView(View):
|
|||
permission='{0}.delete'.format(permission_prefix))
|
||||
config.add_tailbone_permission(permission_prefix, '{0}.delete'.format(permission_prefix),
|
||||
"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))
|
||||
|
|
2
tailbone/views/vendors/catalogs.py
vendored
2
tailbone/views/vendors/catalogs.py
vendored
|
@ -47,7 +47,7 @@ class VendorCatalogsView(FileBatchMasterView):
|
|||
Master view for vendor catalog batches.
|
||||
"""
|
||||
model_class = model.VendorCatalog
|
||||
batch_row_class = model.VendorCatalogRow
|
||||
model_row_class = model.VendorCatalogRow
|
||||
batch_handler_class = VendorCatalogHandler
|
||||
url_prefix = '/vendors/catalogs'
|
||||
|
||||
|
|
2
tailbone/views/vendors/invoices.py
vendored
2
tailbone/views/vendors/invoices.py
vendored
|
@ -41,7 +41,7 @@ class VendorInvoicesView(FileBatchMasterView):
|
|||
Master view for vendor invoice batches.
|
||||
"""
|
||||
model_class = model.VendorInvoice
|
||||
batch_row_class = model.VendorInvoiceRow
|
||||
model_row_class = model.VendorInvoiceRow
|
||||
batch_handler_class = VendorInvoiceHandler
|
||||
url_prefix = '/vendors/invoices'
|
||||
|
||||
|
|
Loading…
Reference in a new issue