Refactor batch views / templates per rattail framework overhaul

This commit is contained in:
Lance Edgar 2016-11-19 18:09:14 -06:00
parent a5184e416a
commit 203f0242fb
12 changed files with 225 additions and 182 deletions

View file

@ -50,11 +50,12 @@
</ul>
<div class="form-wrapper">
% if master.edit_with_rows:
${form.render(buttons=capture(buttons))|n}
% else:
## TODO: clean this up or fix etc..?
## % if master.edit_with_rows:
## ${form.render(buttons=capture(buttons))|n}
## % else:
${form.render()|n}
% endif
## % endif
</div>
% if master.edit_with_rows:

View file

@ -43,19 +43,11 @@
</div>
</%def>
<%def name="leading_buttons()">
</%def>
<%def name="leading_buttons()"></%def>
<%def name="refresh_button()">
## TODO: the refreshable thing still seems confusing...
% if master.refreshable:
% if form.readonly:
% if not batch.executed:
<button type="button" id="refresh-data">Refresh Data</button>
% endif
% elif batch.refreshable:
${h.submit('save-refresh', "Save & Refresh Data")}
% endif
% if master.viewing and master.batch_refreshable(batch):
<button type="button" id="refresh-data">Refresh data</button>
% endif
</%def>
@ -75,12 +67,14 @@
${rows_grid|n}
<div id="execution-options-dialog" style="display: none;">
% if not batch.executed:
<div id="execution-options-dialog" style="display: none;">
${h.form(url('{}.execute'.format(route_prefix), uuid=batch.uuid), name='batch-execution')}
% if master.has_execution_options:
${rendered_execution_options|n}
% endif
${h.end_form()}
${h.form(url('{}.execute'.format(route_prefix), uuid=batch.uuid), name='batch-execution')}
% if master.has_execution_options:
${rendered_execution_options|n}
% endif
${h.end_form()}
</div>
</div>
% endif

View file

@ -63,12 +63,13 @@ class BatchMasterView(MasterView):
"""
Base class for all "batch master" views.
"""
default_handler_spec = None
has_rows = True
rows_deletable = True
rows_downloadable = True
refreshable = True
refresh_after_create = False
edit_with_rows = True
edit_with_rows = False
def __init__(self, request):
super(BatchMasterView, self).__init__(request)
@ -92,7 +93,8 @@ class BatchMasterView(MasterView):
``batch_key`` attribute of the main batch model class.
"""
key = self.model_class.batch_key
spec = self.rattail_config.get('rattail.batch', '{}.handler'.format(key))
spec = self.rattail_config.get('rattail.batch', '{}.handler'.format(key),
default=self.default_handler_spec)
if spec:
return load_object(spec)(self.rattail_config)
return self.batch_handler_class(self.rattail_config)
@ -233,41 +235,34 @@ class BatchMasterView(MasterView):
delattr(fs, field)
def save_create_form(self, form):
"""
Save the uploaded data file if necessary, etc. If batch initialization
fails, don't persist the batch at all; the user will be sent back to
the "create batch" page in that case.
"""
self.before_create(form)
# Transfer form data to batch instance.
# transfer form data to batch instance
form.fieldset.sync()
batch = form.fieldset.model
# Assign current user as creator.
with Session.no_autoflush:
batch.created_by = self.request.user or self.late_login_user()
# current user is batch creator
batch.created_by = self.request.user or self.late_login_user()
# TODO: Wouldn't this be handled sufficiently by `no_autoflush` ?
# Expunge batch from session to prevent it from being flushed
# during init. This is done as a convenience to views which
# provide an init method. Some batches may have required fields
# which aren't filled in yet, but the view may need to query the
# database to obtain the values. This will cause a session flush,
# and the missing fields will trigger data integrity errors.
# destroy initial batch and re-make using handler
kwargs = self.get_batch_kwargs(batch)
Session.expunge(batch)
# TODO: is no_autoflush necessary?
with Session.no_autoflush:
batch = self.handler.make_batch(Session(), **kwargs)
self.batch_inited = self.init_batch(batch)
if self.batch_inited:
Session.add(batch)
Session.flush()
Session.flush()
else: # batch init failed
# TODO: this needs work yet surely...
# if batch has input data file, let handler properly establish that
filename = getattr(batch, 'filename', None)
if filename:
path = os.path.join(self.upload_dir, filename)
self.handler.set_input_file(batch, path)
os.remove(path)
# Here we assume that the :meth:`init_batch()` method responsible
# for indicating the failure will have set a flash message for the
# user with more info.
raise self.redirect(self.request.current_route_url())
# return this object to replace the original
return batch
def init_batch(self, batch):
"""
@ -283,9 +278,12 @@ class BatchMasterView(MasterView):
return True
def redirect_after_create(self, batch):
if self.refresh_after_create:
if self.handler.requires_prefill(batch):
return self.redirect(self.get_action_url('prefill', batch))
elif self.refresh_after_create:
return self.redirect(self.get_action_url('refresh', batch))
return super(BatchMasterView, self).redirect_after_create(batch)
else:
return self.redirect(self.get_action_url('view', batch))
# TODO: some of this at least can go to master now right?
def edit(self):
@ -398,6 +396,22 @@ class BatchMasterView(MasterView):
def executable(self, batch):
return self.handler.executable(batch)
def batch_refreshable(self, batch):
"""
Return a boolean indicating whether the given batch should allow a
refresh operation.
"""
# TODO: deprecate/remove this?
if not self.refreshable:
return False
# (this is how it should be done i think..)
if callable(self.handler.refreshable):
return self.handler.refreshable(batch)
# TODO: deprecate/remove this
return self.handler.refreshable and not batch.executed
@property
def has_execution_options(self):
return bool(self.execution_options_schema)
@ -419,7 +433,68 @@ class BatchMasterView(MasterView):
defaults=defaults or None)
def get_execute_title(self, batch):
return self.handler.get_execute_title(batch)
if hasattr(self.handler, 'get_execute_title'):
return self.handler.get_execute_title(batch)
return "Execute this batch"
def prefill(self):
"""
View which will attempt to prefill all data for the batch. What
exactly this means will depend on the type of batch etc.
"""
batch = self.get_instance()
route_prefix = self.get_route_prefix()
permission_prefix = self.get_permission_prefix()
# showing progress requires a separate thread; start that first
progress_key = '{}.prefill'.format(route_prefix)
progress = SessionProgress(self.request, progress_key)
thread = Thread(target=self.prefill_thread, args=(batch.uuid, progress))
thread.start()
# Send user to progress page.
kwargs = {
'key': progress_key,
'cancel_url': self.get_action_url('view', batch),
'cancel_msg': "Batch prefill was canceled.",
}
# TODO: This seems hacky...it exists for (only) one specific scenario.
if not self.request.has_perm('{}.view'.format(permission_prefix)):
kwargs['cancel_url'] = self.request.route_url('{}.create'.format(route_prefix))
return self.render_progress(kwargs)
def prefill_thread(self, batch_uuid, progress):
"""
Thread target for prefilling batch data with progress indicator.
"""
# mustn't use tailbone web session here
session = RattailSession()
batch = session.query(self.model_class).get(batch_uuid)
try:
self.handler.make_initial_rows(batch, progress=progress)
except Exception as error:
session.rollback()
log.warning("batch pre-fill failed: {}".format(batch), exc_info=True)
session.close()
if progress:
progress.session.load()
progress.session['error'] = True
progress.session['error_msg'] = "Batch pre-fill failed: {} {}".format(error.__class__.__name__, error)
progress.session.save()
return
session.commit()
session.refresh(batch)
session.close()
# finalize progress
if progress:
progress.session.load()
progress.session['complete'] = True
progress.session['success_url'] = self.get_action_url('view', batch)
progress.session.save()
def refresh(self):
"""
@ -430,14 +505,16 @@ class BatchMasterView(MasterView):
route_prefix = self.get_route_prefix()
permission_prefix = self.get_permission_prefix()
# TODO: deprecate / remove this
cognizer = self.request.user
if not cognizer:
uuid = self.request.session.pop('late_login_user', None)
cognizer = Session.query(model.User).get(uuid) if uuid else None
# TODO: refresh should probably always imply/use progress
# If handler doesn't declare the need for progress indicator, things
# are nice and simple.
if not self.handler.show_progress:
if not getattr(self.handler, 'show_progress', True):
self.refresh_data(Session, batch, cognizer=cognizer)
self.request.session.flash("Batch data has been refreshed.")
@ -479,9 +556,14 @@ class BatchMasterView(MasterView):
"""
Instruct the batch handler to refresh all data for the batch.
"""
self.handler.refresh_data(session, batch, progress=progress)
batch.cognized = datetime.datetime.utcnow()
batch.cognized_by = cognizer or session.merge(self.request.user)
# TODO: deprecate/remove this
if hasattr(self.handler, 'refresh_data'):
self.handler.refresh_data(session, batch, progress=progress)
batch.cognized = datetime.datetime.utcnow()
batch.cognized_by = cognizer or session.merge(self.request.user)
else: # the future
self.handler.refresh(batch, progress=progress)
def refresh_thread(self, batch_uuid, progress=None, cognizer_uuid=None, success_url=None):
"""
@ -744,6 +826,11 @@ class BatchMasterView(MasterView):
# else the perm group label will not display correctly...
config.add_tailbone_permission_group(permission_prefix, model_title_plural, overwrite=False)
# prefill row data
config.add_route('{}.prefill'.format(route_prefix), '{}/{{uuid}}/prefill'.format(url_prefix))
config.add_view(cls, attr='prefill', route_name='{}.prefill'.format(route_prefix),
permission='{}.create'.format(permission_prefix))
# 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),
@ -835,46 +922,15 @@ class FileBatchMasterView(BatchMasterView):
fs.filename,
])
def save_create_form(self, form):
self.before_create(form)
# Transfer form data to batch instance.
form.fieldset.sync()
batch = form.fieldset.model
# Assign current user as creator.
with Session.no_autoflush:
batch.created_by = self.request.user or self.late_login_user()
# TODO: Wouldn't this be handled sufficiently by `no_autoflush` ?
# Expunge batch from session to prevent it from being flushed
# during init. This is done as a convenience to views which
# provide an init method. Some batches may have required fields
# which aren't filled in yet, but the view may need to query the
# database to obtain the values. This will cause a session flush,
# and the missing fields will trigger data integrity errors.
Session.expunge(batch)
self.batch_inited = self.init_batch(batch)
if self.batch_inited:
Session.add(batch)
Session.flush()
# Handler saves a copy of the file and updates the batch filename.
path = os.path.join(self.upload_dir, batch.filename)
self.handler.set_data_file(batch, path)
os.remove(path)
else: # batch init failed
# Here we assume that the :meth:`init_batch()` method responsible
# for indicating the failure will have set a flash message for the
# user with more info.
raise self.redirect(self.request.current_route_url())
def redirect_after_create(self, batch):
return self.redirect(self.get_action_url('refresh', batch))
def get_batch_kwargs(self, batch):
"""
Return a kwargs dict for use with ``self.handler.make_batch()``, using
the given batch as a template.
"""
kwargs = {'created_by': batch.created_by}
if hasattr(batch, 'filename'):
kwargs['filename'] = batch.filename
return kwargs
def download(self):
"""
@ -902,6 +958,10 @@ class FileBatchMasterView(BatchMasterView):
url_prefix = cls.get_url_prefix()
permission_prefix = cls.get_permission_prefix()
model_title = cls.get_model_title()
model_title_plural = cls.get_model_title_plural()
# fix permission group title
config.add_tailbone_permission_group(permission_prefix, model_title_plural)
# download batch data file
config.add_route('{}.download'.format(route_prefix), '{}/{{uuid}}/download'.format(url_prefix))

View file

@ -41,6 +41,7 @@ class View(object):
def __init__(self, request):
self.request = request
self.enum = self.rattail_config.get_enum()
@property
def rattail_config(self):

View file

@ -26,9 +26,10 @@ Views for handheld batches
from __future__ import unicode_literals, absolute_import
import os
from rattail import enum
from rattail.db import model
from rattail.db.batch.handheld.handler import HandheldBatchHandler
from rattail.util import OrderedDict
import formalchemy as fa
@ -36,6 +37,7 @@ import formencode as fe
from webhelpers.html import tags
from tailbone import forms
from tailbone.db import Session
from tailbone.views.batch import FileBatchMasterView
@ -71,47 +73,63 @@ class HandheldBatchView(FileBatchMasterView):
Master view for handheld batches.
"""
model_class = model.HandheldBatch
default_handler_spec = 'rattail.batch.handheld:HandheldBatchHandler'
model_title_plural = "Handheld Batches"
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):
enum = self.rattail_config.get_enum()
g.configure(
include=[
g.id,
g.device_type.with_renderer(forms.renderers.EnumFieldRenderer(enum.HANDHELD_DEVICE_TYPE)),
g.device_name,
g.created,
g.created_by,
g.device_name,
g.executed,
g.executed_by,
],
readonly=True)
def configure_fieldset(self, fs):
fs.configure(
include=[
fs.id,
fs.created,
fs.created_by,
fs.filename,
fs.device_type.with_renderer(forms.renderers.EnumFieldRenderer(enum.HANDHELD_DEVICE_TYPE)),
fs.device_name,
fs.executed,
fs.executed_by,
])
fs.device_type.set(renderer=forms.renderers.EnumFieldRenderer(enum.HANDHELD_DEVICE_TYPE))
if self.creating:
del fs.id
elif self.viewing and fs.model.inventory_batch:
fs.configure(
include=[
fs.filename,
fs.device_type,
fs.device_name,
])
else:
fs.configure(
include=[
fs.id,
fs.device_type,
fs.device_name,
fs.filename,
fs.created,
fs.created_by,
fs.executed,
fs.executed_by,
])
if self.viewing and fs.model.inventory_batch:
fs.append(fa.Field('inventory_batch', value=fs.model.inventory_batch, renderer=InventoryBatchFieldRenderer))
def get_batch_kwargs(self, batch):
kwargs = super(HandheldBatchView, self).get_batch_kwargs(batch)
kwargs['device_type'] = batch.device_type
kwargs['device_name'] = batch.device_name
return kwargs
def configure_row_grid(self, g):
g.configure(
include=[
@ -167,16 +185,6 @@ class HandheldBatchView(FileBatchMasterView):
return self.request.route_url('labels.batch.view', uuid=result.uuid)
return super(HandheldBatchView, self).get_execute_success_url(batch)
@classmethod
def defaults(cls, config):
# fix permission group title
config.add_tailbone_permission_group('batch.handheld', "Handheld Batches")
cls._filebatch_defaults(config)
cls._batch_defaults(config)
cls._defaults(config)
def includeme(config):
HandheldBatchView.defaults(config)

View file

@ -26,9 +26,7 @@ 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
from tailbone import forms
from tailbone.views.batch import BatchMasterView
@ -40,19 +38,18 @@ class InventoryBatchView(BatchMasterView):
"""
model_class = model.InventoryBatch
model_title_plural = "Inventory Batches"
batch_handler_class = InventoryBatchHandler
default_handler_spec = 'rattail.batch.inventory: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),
g.mode.set(renderer=forms.renderers.EnumFieldRenderer(self.enum.INVENTORY_MODE),
label="Count Mode")
def configure_grid(self, g):
@ -62,7 +59,7 @@ class InventoryBatchView(BatchMasterView):
def _preconfigure_fieldset(self, fs):
super(InventoryBatchView, self)._preconfigure_fieldset(fs)
fs.handheld_batch.set(renderer=forms.renderers.HandheldBatchFieldRenderer, readonly=True)
fs.mode.set(renderer=forms.renderers.EnumFieldRenderer(enum.INVENTORY_MODE),
fs.mode.set(renderer=forms.renderers.EnumFieldRenderer(self.enum.INVENTORY_MODE),
label="Count Mode")
def configure_fieldset(self, fs):
@ -124,15 +121,6 @@ class InventoryBatchView(BatchMasterView):
fs.units,
])
@classmethod
def defaults(cls, config):
# fix permission group title
config.add_tailbone_permission_group('batch.inventory', "Inventory Batches")
cls._batch_defaults(config)
cls._defaults(config)
def includeme(config):
InventoryBatchView.defaults(config)

View file

@ -27,7 +27,6 @@ Views for label batches
from __future__ import unicode_literals, absolute_import
from rattail.db import model
from rattail.db.batch.labels.handler import LabelBatchHandler
from tailbone import forms
from tailbone.views.batch import BatchMasterView
@ -39,7 +38,7 @@ class LabelBatchView(BatchMasterView):
"""
model_class = model.LabelBatch
model_row_class = model.LabelBatchRow
batch_handler_class = LabelBatchHandler
default_handler_spec = 'rattail.batch.labels:LabelBatchHandler'
model_title_plural = "Label Batches"
route_prefix = 'labels.batch'
url_prefix = '/labels/batches'
@ -61,6 +60,9 @@ class LabelBatchView(BatchMasterView):
fs.executed,
fs.executed_by,
])
batch = fs.model
if self.viewing and not batch.handheld_batch:
del fs.handheld_batch
def _preconfigure_row_grid(self, g):
super(LabelBatchView, self)._preconfigure_row_grid(g)

View file

@ -128,12 +128,12 @@ class MasterView(View):
form = self.make_form(self.get_model_class())
if self.request.method == 'POST':
if form.validate():
self.save_create_form(form)
instance = form.fieldset.model
self.after_create(instance)
# let save_create_form() return alternate object if necessary
obj = self.save_create_form(form) or form.fieldset.model
self.after_create(obj)
self.request.session.flash("{} has been created: {}".format(
self.get_model_title(), self.get_instance_title(instance)))
return self.redirect_after_create(instance)
self.get_model_title(), self.get_instance_title(obj)))
return self.redirect_after_create(obj)
return self.render_to_response('create', {'form': form})
def save_create_form(self, form):

View file

@ -38,6 +38,7 @@ from rattail.gpc import GPC
from rattail.threads import Thread
from rattail.exceptions import LabelPrintingError
from rattail.util import load_object
from rattail.batch import get_batch_handler
import formalchemy as fa
from pyramid import httpexceptions
@ -349,15 +350,14 @@ class ProductsView(MasterView):
# okay then, new-style it is
# TODO: make this more configurable surely..?
supported = {
'labels': 'rattail.db.batch.labels.handler:LabelBatchHandler',
'labels': 'rattail.batch.labels:LabelBatchHandler',
}
if self.request.method == 'POST':
batch_key = self.request.POST.get('batch_type')
if batch_key and batch_key in supported:
handler_spec = self.rattail_config.get('rattail.batch', '{}.handler'.format(batch_key),
default=supported[batch_key])
handler = load_object(handler_spec)(self.rattail_config)
handler = get_batch_handler(self.rattail_config, batch_key,
default=supported[batch_key])
progress = SessionProgress(self.request, 'products.batch')
thread = Thread(target=self.make_batch_thread,
@ -372,7 +372,7 @@ class ProductsView(MasterView):
batch_types = []
for key, spec in supported.iteritems():
handler = load_object(spec)(self.rattail_config)
batch_types.append((key, handler.model_title))
batch_types.append((key, handler.get_model_title()))
return {'supported_batches': batch_types}
@ -384,11 +384,9 @@ class ProductsView(MasterView):
user = session.query(model.User).get(user_uuid)
assert user
products = self.get_effective_query(session)
batch = handler.make_batch(session, created_by=user, products=products, progress=progress)
if not batch:
session.rollback()
session.close()
return
batch = handler.make_batch(session, created_by=user)
batch.products = products.all()
handler.make_initial_rows(batch, progress=progress)
session.commit()
session.refresh(batch)

View file

@ -31,7 +31,6 @@ from sqlalchemy import orm
from rattail import enum
from rattail.db import model, api
from rattail.gpc import GPC
from rattail.db.batch.purchase.handler import PurchaseBatchHandler
from rattail.time import localtime
from rattail.core import Object
from rattail.util import OrderedDict
@ -50,7 +49,7 @@ class PurchaseBatchView(BatchMasterView):
model_class = model.PurchaseBatch
model_title_plural = "Purchase Batches"
model_row_class = model.PurchaseBatchRow
batch_handler_class = PurchaseBatchHandler
default_handler_spec = 'rattail.batch.purchase:PurchaseBatchHandler'
route_prefix = 'purchases.batch'
url_prefix = '/purchases/batches'
rows_creatable = True

View file

@ -29,7 +29,6 @@ from __future__ import unicode_literals, absolute_import
import logging
from rattail.db import model, api
from rattail.db.batch.vendorcatalog.handler import VendorCatalogHandler
from rattail.vendors.catalogs import iter_catalog_parsers
import formalchemy
@ -48,7 +47,7 @@ class VendorCatalogsView(FileBatchMasterView):
"""
model_class = model.VendorCatalog
model_row_class = model.VendorCatalogRow
batch_handler_class = VendorCatalogHandler
default_handler_spec = 'rattail.batch.vendorcatalog:VendorCatalogHandler'
url_prefix = '/vendors/catalogs'
editable = False
@ -105,6 +104,15 @@ class VendorCatalogsView(FileBatchMasterView):
fs.executed_by,
])
def get_batch_kwargs(self, batch):
kwargs = super(VendorCatalogsView, self).get_batch_kwargs(batch)
kwargs['parser_key'] = batch.parser_key
if batch.vendor:
kwargs['vendor'] = batch.vendor
elif batch.vendor_uuid:
kwargs['vendor_uuid'] = batch.vendor_uuid
return kwargs
def configure_row_grid(self, g):
g.configure(
include=[
@ -146,16 +154,6 @@ class VendorCatalogsView(FileBatchMasterView):
kwargs['parsers'] = parsers
return kwargs
@classmethod
def defaults(cls, config):
# fix permission group title
config.add_tailbone_permission_group('vendorcatalogs', "Vendor Catalogs")
cls._filebatch_defaults(config)
cls._batch_defaults(config)
cls._defaults(config)
def includeme(config):
VendorCatalogsView.defaults(config)

View file

@ -27,7 +27,6 @@ Views for maintaining vendor invoices
from __future__ import unicode_literals, absolute_import
from rattail.db import model, api
from rattail.db.batch.vendorinvoice.handler import VendorInvoiceHandler
from rattail.vendors.invoices import iter_invoice_parsers, require_invoice_parser
import formalchemy
@ -42,7 +41,7 @@ class VendorInvoicesView(FileBatchMasterView):
"""
model_class = model.VendorInvoice
model_row_class = model.VendorInvoiceRow
batch_handler_class = VendorInvoiceHandler
default_handler_spec = 'rattail.batch.vendorinvoice:VendorInvoiceHandler'
url_prefix = '/vendors/invoices'
def get_instance_title(self, batch):
@ -109,6 +108,11 @@ class VendorInvoicesView(FileBatchMasterView):
except ValueError as error:
raise formalchemy.ValidationError(unicode(error))
def get_batch_kwargs(self, batch):
kwargs = super(VendorInvoicesView, self).get_batch_kwargs(batch)
kwargs['parser_key'] = batch.parser_key
return kwargs
def init_batch(self, batch):
parser = require_invoice_parser(batch.parser_key)
vendor = api.get_vendor(Session(), parser.vendor_key)
@ -148,16 +152,6 @@ class VendorInvoicesView(FileBatchMasterView):
attrs['class_'] = 'warning'
return attrs
@classmethod
def defaults(cls, config):
# fix permission group title
config.add_tailbone_permission_group('vendorinvoices', "Vendor Invoices")
cls._filebatch_defaults(config)
cls._batch_defaults(config)
cls._defaults(config)
def includeme(config):
VendorInvoicesView.defaults(config)