diff --git a/tailbone/templates/master/edit_row.mako b/tailbone/templates/master/edit_row.mako
new file mode 100644
index 00000000..c3cfc0af
--- /dev/null
+++ b/tailbone/templates/master/edit_row.mako
@@ -0,0 +1,17 @@
+## -*- coding: utf-8 -*-
+<%inherit file="/master/edit.mako" />
+
+<%def name="context_menu_items()">
+
${h.link_to("Back to {}".format(model_title), index_url)}
+ % if master.rows_viewable and request.has_perm('{}.view'.format(row_permission_prefix)):
+ ${h.link_to("View this {}".format(row_model_title), row_action_url('view', instance))}
+ % endif
+ % if master.rows_deletable and instance_deletable and request.has_perm('{}.delete'.format(row_permission_prefix)):
+ ${h.link_to("Delete this {}".format(row_model_title), row_action_url('delete', instance))}
+ % endif
+ % if master.rows_creatable and request.has_perm('{}.create'.format(row_permission_prefix)):
+ ${h.link_to("Create a new {}".format(row_model_title), url('{}.create'.format(row_route_prefix)))}
+ % endif
+%def>
+
+${parent.body()}
diff --git a/tailbone/templates/newbatch/view_row.mako b/tailbone/templates/master/view_row.mako
similarity index 78%
rename from tailbone/templates/newbatch/view_row.mako
rename to tailbone/templates/master/view_row.mako
index 27be7115..dc5fd532 100644
--- a/tailbone/templates/newbatch/view_row.mako
+++ b/tailbone/templates/master/view_row.mako
@@ -4,7 +4,7 @@
<%def name="title()">${model_title}%def>
<%def name="context_menu_items()">
- ${h.link_to("Back to {}".format(batch_model_title), index_url)}
+ ${h.link_to("Back to {}".format(parent_model_title), index_url)}
% if master.rows_editable and instance_editable and request.has_perm('{}.edit'.format(permission_prefix)):
${h.link_to("Edit this {}".format(model_title), action_url('edit', instance))}
% endif
@@ -16,4 +16,10 @@
% endif
%def>
-${parent.body()}
+
+
+
+ ${form.render()|n}
+
diff --git a/tailbone/templates/themes/better/base.mako b/tailbone/templates/themes/better/base.mako
index 1c5c21bf..57fcc3bc 100644
--- a/tailbone/templates/themes/better/base.mako
+++ b/tailbone/templates/themes/better/base.mako
@@ -42,7 +42,7 @@
% if master.listing:
${model_title_plural}
% 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
diff --git a/tailbone/views/batch.py b/tailbone/views/batch.py
index 87dfec86..b8f56b91 100644
--- a/tailbone/views/batch.py
+++ b/tailbone/views/batch.py
@@ -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),
diff --git a/tailbone/views/handheld.py b/tailbone/views/handheld.py
index 0edb96ea..4f1d7042 100644
--- a/tailbone/views/handheld.py
+++ b/tailbone/views/handheld.py
@@ -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
diff --git a/tailbone/views/inventory.py b/tailbone/views/inventory.py
index 49519a82..ee37db14 100644
--- a/tailbone/views/inventory.py
+++ b/tailbone/views/inventory.py
@@ -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):
diff --git a/tailbone/views/master.py b/tailbone/views/master.py
index acf6e9bf..640da516 100644
--- a/tailbone/views/master.py
+++ b/tailbone/views/master.py
@@ -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))
diff --git a/tailbone/views/vendors/catalogs.py b/tailbone/views/vendors/catalogs.py
index b0ae1249..cc04d067 100644
--- a/tailbone/views/vendors/catalogs.py
+++ b/tailbone/views/vendors/catalogs.py
@@ -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'
diff --git a/tailbone/views/vendors/invoices.py b/tailbone/views/vendors/invoices.py
index c056cea2..4883fb41 100644
--- a/tailbone/views/vendors/invoices.py
+++ b/tailbone/views/vendors/invoices.py
@@ -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'