tailbone/tailbone/views/batch.py
Lance Edgar 6feb488884 Various tweaks to base batch views.
These were made to support a non-file batch; apparently that use case
hadn't seen much attention yet.
2015-08-18 21:17:15 -05:00

1122 lines
40 KiB
Python

# -*- coding: utf-8 -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2015 Lance Edgar
#
# This file is part of Rattail.
#
# Rattail is free software: you can redistribute it and/or modify it under the
# terms of the GNU Affero General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
# more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
Base views for maintaining new-style batches.
.. note::
This is all still very experimental.
"""
from __future__ import unicode_literals
import os
import datetime
import logging
import formalchemy
from pyramid.renderers import render_to_response
from pyramid.response import FileResponse
from pyramid.httpexceptions import HTTPFound, HTTPNotFound
from webhelpers.html.tags import link_to, HTML
from rattail.db import model
from rattail.db import Session as RatSession
from rattail.threads import Thread
from tailbone.db import Session
from tailbone.views import SearchableAlchemyGridView, CrudView
from tailbone.forms import DateTimeFieldRenderer, UserFieldRenderer, EnumFieldRenderer
from tailbone.forms.renderers.batch import FileFieldRenderer
from tailbone.grids.search import BooleanSearchFilter, EnumSearchFilter
from tailbone.progress import SessionProgress
log = logging.getLogger(__name__)
class BaseGrid(SearchableAlchemyGridView):
"""
Base view for batch and batch row grid views. You should not derive from
this class, but :class:`BatchGrid` or :class:`BatchRowGrid` instead.
"""
@property
def config_prefix(self):
"""
Config prefix for the grid view. This is used to keep track of current
filtering and sorting, within the user's session. Derived classes may
override this.
"""
return self.mapped_class.__name__.lower()
@property
def permission_prefix(self):
"""
Permission prefix for the grid view. This is used to automatically
protect certain views common to all batches. Derived classes can
override this.
"""
return self.route_prefix
def join_map_extras(self):
"""
Derived classes can override this. The value returned will be used to
supplement the default join map.
"""
return {}
def filter_map_extras(self):
"""
Derived classes can override this. The value returned will be used to
supplement the default filter map.
"""
return {}
def make_filter_map(self, **kwargs):
"""
Make a filter map by combining kwargs from the base class, with extras
supplied by a derived class.
"""
extras = self.filter_map_extras()
exact = extras.pop('exact', None)
if exact:
kwargs.setdefault('exact', []).extend(exact)
ilike = extras.pop('ilike', None)
if ilike:
kwargs.setdefault('ilike', []).extend(ilike)
kwargs.update(extras)
return super(BaseGrid, self).make_filter_map(**kwargs)
def filter_config_extras(self):
"""
Derived classes can override this. The value returned will be used to
supplement the default filter config.
"""
return {}
def sort_map_extras(self):
"""
Derived classes can override this. The value returned will be used to
supplement the default sort map.
"""
return {}
def _configure_grid(self, grid):
"""
Internal method for configuring the grid. This is meant only for base
classes; derived classes should not need to override it.
"""
def configure_grid(self, grid):
"""
Derived classes can override this. Customizes a grid which has already
been created with defaults by the base class.
"""
class BatchGrid(BaseGrid):
"""
Base grid view for batches, which can be filtered and sorted.
"""
@property
def batch_class(self):
raise NotImplementedError
@property
def mapped_class(self):
return self.batch_class
@property
def batch_display(self):
"""
Singular display text for the batch type, e.g. "Vendor Invoice".
Override this as necessary.
"""
return self.batch_class.__name__
@property
def batch_display_plural(self):
"""
Plural display text for the batch type, e.g. "Vendor Invoices".
Override this as necessary.
"""
return "{0}s".format(self.batch_display)
def join_map(self):
"""
Provides the default join map for batch grid views. Derived classes
should *not* override this, but :meth:`join_map_extras()` instead.
"""
map_ = {
'created_by':
lambda q: q.join(model.User, model.User.uuid == self.batch_class.created_by_uuid),
'executed_by':
lambda q: q.outerjoin(model.User, model.User.uuid == self.batch_class.executed_by_uuid),
}
map_.update(self.join_map_extras())
return map_
def filter_map(self):
"""
Provides the default filter map for batch grid views. Derived classes
should *not* override this, but :meth:`filter_map_extras()` instead.
"""
def executed_is(q, v):
if v == 'True':
return q.filter(self.batch_class.executed != None)
else:
return q.filter(self.batch_class.executed == None)
def executed_nt(q, v):
if v == 'True':
return q.filter(self.batch_class.executed == None)
else:
return q.filter(self.batch_class.executed != None)
return self.make_filter_map(
executed={'is': executed_is, 'nt': executed_nt})
def filter_config(self):
"""
Provides the default filter config for batch grid views. Derived
classes should *not* override this, but :meth:`filter_config_extras()`
instead.
"""
defaults = self.filter_config_extras()
config = self.make_filter_config(
filter_factory_executed=BooleanSearchFilter,
filter_type_executed='is',
executed=False,
include_filter_executed=True)
defaults.update(config)
return defaults
def sort_map(self):
"""
Provides the default sort map for batch grid views. Derived classes
should *not* override this, but :meth:`sort_map_extras()` instead.
"""
map_ = self.make_sort_map(
created_by=self.sorter(model.User.username),
executed_by=self.sorter(model.User.username))
map_.update(self.sort_map_extras())
return map_
def sort_config(self):
"""
Provides the default sort config for batch grid views. Derived classes
may override this.
"""
return self.make_sort_config(sort='created', dir='desc')
def grid(self):
"""
Creates the grid for the view. Derived classes should *not* override
this, but :meth:`configure_grid()` instead.
"""
g = self.make_grid()
g.created.set(renderer=DateTimeFieldRenderer(self.request.rattail_config))
g.created_by.set(renderer=UserFieldRenderer)
g.cognized.set(renderer=DateTimeFieldRenderer(self.request.rattail_config))
g.cognized_by.set(renderer=UserFieldRenderer)
g.executed.set(renderer=DateTimeFieldRenderer(self.request.rattail_config))
g.executed_by.set(renderer=UserFieldRenderer)
self._configure_grid(g)
self.configure_grid(g)
if self.request.has_perm('{0}.view'.format(self.permission_prefix)):
g.viewable = True
g.view_route_name = '{0}.view'.format(self.route_prefix)
if self.request.has_perm('{0}.edit'.format(self.permission_prefix)):
g.editable = True
g.edit_route_name = '{0}.edit'.format(self.route_prefix)
if self.request.has_perm('{0}.delete'.format(self.permission_prefix)):
g.deletable = True
g.delete_route_name = '{0}.delete'.format(self.route_prefix)
return g
def _configure_grid(self, grid):
grid.created_by.set(label="Created by")
grid.executed_by.set(label="Executed by")
def configure_grid(self, grid):
"""
Derived classes can override this. Customizes a grid which has already
been created with defaults by the base class.
"""
g = grid
g.configure(
include=[
g.created,
g.created_by,
g.executed,
g.executed_by,
],
readonly=True)
def render_kwargs(self):
"""
Add some things to the template context: batch type display name, route
and permission prefixes.
"""
return {
'batch_display': self.batch_display,
'batch_display_plural': self.batch_display_plural,
'route_prefix': self.route_prefix,
'permission_prefix': self.permission_prefix,
}
class FileBatchGrid(BatchGrid):
"""
Base grid view for batches, which involve primarily a file upload.
"""
def _configure_grid(self, g):
super(FileBatchGrid, self)._configure_grid(g)
g.created.set(label="Uploaded")
g.created_by.set(label="Uploaded by")
def configure_grid(self, grid):
"""
Derived classes can override this. Customizes a grid which has already
been created with defaults by the base class.
"""
g = grid
g.configure(
include=[
g.created,
g.created_by,
g.filename,
g.executed,
g.executed_by,
],
readonly=True)
class BaseCrud(CrudView):
"""
Base CRUD view for batches and batch rows.
"""
flash = {}
@property
def home_route(self):
"""
The "home" route for the batch type, i.e. its grid view.
"""
return self.route_prefix
@property
def permission_prefix(self):
"""
Permission prefix used to generically protect certain views common to
all batches. Derived classes can override this.
"""
return self.route_prefix
def flash_create(self, model):
if 'create' in self.flash:
self.request.session.flash(self.flash['create'])
else:
super(BaseCrud, self).flash_create(model)
def flash_update(self, model):
if 'update' in self.flash:
self.request.session.flash(self.flash['update'])
else:
super(BaseCrud, self).flash_update(model)
def flash_delete(self, model):
if 'delete' in self.flash:
self.request.session.flash(self.flash['delete'])
else:
super(BaseCrud, self).flash_delete(model)
class BatchCrud(BaseCrud):
"""
Base CRUD view for batches.
"""
refreshable = False
flash = {}
@property
def batch_class(self):
raise NotImplementedError
@property
def mapped_class(self):
return self.batch_class
@property
def permission_prefix(self):
"""
Permission prefix for the grid view. This is used to automatically
protect certain views common to all batches. Derived classes can - and
typically should - override this.
"""
return self.route_prefix
@property
def batch_display_plural(self):
"""
Plural display text for the batch type.
"""
return "{0}s".format(self.batch_display)
def __init__(self, request):
self.request = request
self.handler = self.get_handler()
def get_handler(self):
"""
Returns a `BatchHandler` instance for the view. Derived classes may
override this as needed. The default is to create an instance of
:attr:`batch_handler_class`.
"""
return self.batch_handler_class(self.request.rattail_config)
def fieldset(self, model):
"""
Creates the fieldset for the view. Derived classes should *not*
override this, but :meth:`configure_fieldset()` instead.
"""
fs = self.make_fieldset(model)
fs.created.set(renderer=DateTimeFieldRenderer(self.request.rattail_config))
fs.created_by.set(label="Created by", renderer=UserFieldRenderer)
fs.cognized.set(renderer=DateTimeFieldRenderer(self.request.rattail_config))
fs.cognized_by.set(label="Cognized by", renderer=UserFieldRenderer)
fs.executed.set(renderer=DateTimeFieldRenderer(self.request.rattail_config))
fs.executed_by.set(label="Executed by", renderer=UserFieldRenderer)
self.configure_fieldset(fs)
if self.creating:
del fs.created
del fs.created_by
if 'cognized' in fs.render_fields:
del fs.cognized
if 'cognized_by' in fs.render_fields:
del fs.cognized_by
if 'executed' in fs.render_fields:
del fs.executed
if 'executed_by' in fs.render_fields:
del fs.executed_by
return fs
def configure_fieldset(self, fieldset):
"""
Derived classes can override this. Customizes a fieldset which has
already been created with defaults by the base class.
"""
fs = fieldset
fs.configure(
include=[
fs.created,
fs.created_by,
# fs.cognized,
# fs.cognized_by,
fs.executed,
fs.executed_by,
])
def init_batch(self, batch):
"""
Initialize a new batch. Derived classes can override this to
effectively provide default values for a batch, etc. This method is
invoked after a batch has been fully prepared for insertion to the
database, but before the push to the database occurs.
Note that the return value of this function matters; if it is boolean
false then the batch will not be persisted at all, and the user will be
redirected to the "create batch" page.
"""
return True
def save_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.
"""
# Transfer form data to batch instance.
form.fieldset.sync()
batch = form.fieldset.model
# For new batches, assign current user as creator, etc.
if self.creating:
with Session.no_autoflush:
batch.created_by = self.request.user or self.late_login_user()
# 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()
def update(self):
"""
Don't allow editing a batch which has already been executed.
"""
batch = self.get_model_from_request()
if not batch:
return HTTPNotFound()
if batch.executed:
return HTTPFound(location=self.view_url(batch.uuid))
return self.crud(batch)
def post_create_url(self, form):
"""
Redirect to view batch after creating a batch.
"""
batch = form.fieldset.model
Session.flush()
return self.view_url(batch.uuid)
def post_update_url(self, form):
"""
Redirect back to edit batch page after editing a batch, unless the
refresh flag is set, in which case do that.
"""
if self.request.params.get('refresh') == 'true':
return self.refresh_url()
return self.request.current_route_url()
def template_kwargs(self, form):
"""
Add some things to the template context: current batch model, batch
type display name, route and permission prefixes, batch row grid.
"""
batch = form.fieldset.model
batch.refreshable = self.refreshable
kwargs = {
'batch': batch,
'batch_display': self.batch_display,
'batch_display_plural': self.batch_display_plural,
'execute_title': self.handler.get_execute_title(batch),
'route_prefix': self.route_prefix,
'permission_prefix': self.permission_prefix,
}
if not self.creating:
kwargs['execute_enabled'] = self.executable(batch)
return kwargs
def executable(self, batch):
return self.handler.executable(batch)
def flash_create(self, batch):
if 'create' in self.flash:
self.request.session.flash(self.flash['create'])
else:
super(BatchCrud, self).flash_create(batch)
def flash_delete(self, batch):
if 'delete' in self.flash:
self.request.session.flash(self.flash['delete'])
else:
super(BatchCrud, self).flash_delete(batch)
def current_batch(self):
"""
Return the current batch, based on the UUID within the URL.
"""
return Session.query(self.mapped_class).get(self.request.matchdict['uuid'])
def refresh(self):
"""
View which will attempt to refresh all data for the batch. What
exactly this means will depend on the type of batch etc.
"""
batch = self.current_batch()
# If handler doesn't declare the need for progress indicator, things
# are nice and simple.
if not self.handler.show_progress:
self.refresh_data(Session, batch)
self.request.session.flash("Batch data has been refreshed.")
return HTTPFound(location=self.view_url(batch.uuid))
# Showing progress requires a separate thread; start that first.
key = '{0}.refresh'.format(self.batch_class.__tablename__)
progress = SessionProgress(self.request, key)
thread = Thread(target=self.refresh_thread, args=(batch.uuid, progress))
thread.start()
# Send user to progress page.
kwargs = {
'key': key,
'cancel_url': self.view_url(batch.uuid),
'cancel_msg': "Batch refresh was canceled.",
}
return render_to_response('/progress.mako', kwargs, request=self.request)
def refresh_data(self, session, batch, cognizer=None, progress=None):
"""
Instruct the batch handler to refresh all data for the batch.
"""
self.handler.refresh_data(session, batch, progress=progress)
batch.cognized = datetime.datetime.utcnow()
if cognizer is not None:
batch.cognized_by = cognizer
else:
batch.cognized_by = self.request.user
def refresh_thread(self, batch_uuid, progress=None, cognizer_uuid=None, success_url=None):
"""
Thread target for refreshing batch data with progress indicator.
"""
# Refresh data for the batch, with progress. Note that we must use the
# rattail session here; can't use tailbone because it has web request
# transaction binding etc.
session = RatSession()
batch = session.query(self.batch_class).get(batch_uuid)
cognizer = session.query(model.User).get(cognizer_uuid) if cognizer_uuid else None
try:
self.refresh_data(session, batch, cognizer=cognizer, progress=progress)
except Exception as error:
session.rollback()
log.warning("refreshing data for batch failed: {0}".format(batch), exc_info=True)
session.close()
if progress:
progress.session.load()
progress.session['error'] = True
progress.session['error_msg'] = "Data refresh failed: {0}".format(error)
progress.session.save()
return
session.commit()
session.refresh(batch)
session.close()
# Finalize progress indicator.
if progress:
progress.session.load()
progress.session['complete'] = True
progress.session['success_url'] = success_url or self.view_url(batch.uuid)
progress.session.save()
def view_url(self, uuid=None):
"""
Returns the URL for viewing a batch; defaults to current batch.
"""
if uuid is None:
uuid = self.request.matchdict['uuid']
return self.request.route_url('{0}.view'.format(self.route_prefix), uuid=uuid)
def refresh_url(self, uuid=None):
"""
Returns the URL for refreshing a batch; defaults to current batch.
"""
if uuid is None:
uuid = self.request.matchdict['uuid']
return self.request.route_url('{0}.refresh'.format(self.route_prefix), uuid=uuid)
def execute(self):
batch = self.current_batch()
if self.handler.execute(batch):
batch.executed = datetime.datetime.utcnow()
batch.executed_by = self.request.user
self.request.session.flash("Batch was executed successfully.")
return HTTPFound(location=self.view_url(batch.uuid))
class FileBatchCrud(BatchCrud):
"""
Base CRUD view for batches which involve a file upload as the first step.
"""
refreshable = True
def post_create_url(self, form):
"""
Redirect to refresh batch after creating a batch.
"""
batch = form.fieldset.model
Session.flush()
return self.refresh_url(batch.uuid)
@property
def upload_dir(self):
"""
The path to the root upload folder, to be used as the ``storage_path``
argument for the file field renderer.
"""
uploads = os.path.join(
self.request.rattail_config.require('rattail', 'batch.files'),
'uploads')
uploads = self.request.rattail_config.get(
'tailbone', 'batch.uploads', default=uploads)
if not os.path.exists(uploads):
os.makedirs(uploads)
return uploads
def fieldset(self, model):
"""
Creates the fieldset for the view. Derived classes should *not*
override this, but :meth:`configure_fieldset()` instead.
"""
fs = self.make_fieldset(model)
fs.created.set(label="Uploaded", renderer=DateTimeFieldRenderer(self.request.rattail_config), readonly=True)
fs.created_by.set(label="Uploaded by", renderer=UserFieldRenderer, readonly=True)
fs.cognized.set(renderer=DateTimeFieldRenderer(self.request.rattail_config))
fs.cognized_by.set(label="Cognized by", renderer=UserFieldRenderer)
fs.executed.set(renderer=DateTimeFieldRenderer(self.request.rattail_config))
fs.executed_by.set(label="Executed by", renderer=UserFieldRenderer)
fs.filename.set(renderer=FileFieldRenderer.new(self), label="Data File")
self.configure_fieldset(fs)
if self.creating:
if 'created' in fs.render_fields:
del fs.created
if 'created_by' in fs.render_fields:
del fs.created_by
if 'cognized' in fs.render_fields:
del fs.cognized
if 'cognized_by' in fs.render_fields:
del fs.cognized_by
if 'executed' in fs.render_fields:
del fs.executed
if 'executed_by' in fs.render_fields:
del fs.executed_by
if 'data_rows' in fs.render_fields:
del fs.data_rows
else:
if self.updating and 'filename' in fs.render_fields:
fs.filename.set(readonly=True)
batch = fs.model
if not batch.executed:
if 'executed' in fs.render_fields:
del fs.executed
if 'executed_by' in fs.render_fields:
del fs.executed_by
return fs
def configure_fieldset(self, fieldset):
"""
Derived classes can override this. Customizes a fieldset which has
already been created with defaults by the base class.
"""
fs = fieldset
fs.configure(
include=[
fs.created,
fs.created_by,
fs.filename,
# fs.cognized,
# fs.cognized_by,
fs.executed,
fs.executed_by,
])
def save_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.
"""
# Transfer form data to batch instance.
form.fieldset.sync()
batch = form.fieldset.model
# For new batches, assign current user as creator, save file etc.
if self.creating:
with Session.no_autoflush:
batch.created_by = self.request.user or self.late_login_user()
# 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)
def post_save(self, form):
"""
This checks for failed batch initialization when creating a new batch.
If a failure is detected, the user is redirected to the page for
creating new batches. The assumption here is that the
:meth:`init_batch()` method responsible for indicating the failure will
have set a flash message for the user with more info.
"""
if self.creating and not self.batch_inited:
return HTTPFound(location=self.request.route_url(
'{0}.create'.format(self.route_prefix)))
def pre_delete(self, batch):
"""
Delete all data (files etc.) for the batch.
"""
batch.delete_data(self.request.rattail_config)
del batch.data_rows[:]
def download(self):
"""
View for downloading the data file associated with a batch.
"""
batch = self.current_batch()
if not batch:
return HTTPNotFound()
path = self.handler.data_path(batch)
response = FileResponse(path, request=self.request)
response.headers[b'Content-Length'] = str(os.path.getsize(path))
filename = os.path.basename(batch.filename).encode('ascii', 'replace')
response.headers[b'Content-Disposition'] = b'attachment; filename="{0}"'.format(filename)
return response
class StatusRenderer(EnumFieldRenderer):
"""
Custom renderer for ``status_code`` fields. Adds ``status_text`` value as
title attribute if it exists.
"""
def render_readonly(self, **kwargs):
value = self.raw_value
if value is None:
return ''
status_code_text = self.enumeration.get(value, unicode(value))
row = self.field.parent.model
if row.status_text:
return HTML.tag('span', title=row.status_text, c=status_code_text)
return status_code_text
class BatchRowGrid(BaseGrid):
"""
Base grid view for batch rows, which can be filtered and sorted. Also it
can delete all rows matching the current list view query.
"""
@property
def row_class(self):
raise NotImplementedError
@property
def mapped_class(self):
return self.row_class
@property
def config_prefix(self):
"""
Config prefix for the grid view. This is used to keep track of current
filtering and sorting, within the user's session. Derived classes may
override this.
"""
return '{0}.{1}'.format(self.mapped_class.__name__.lower(),
self.request.matchdict['uuid'])
@property
def batch_class(self):
"""
Model class of the batch to which the rows belong.
"""
return self.row_class.__batch_class__
@property
def batch_display(self):
"""
Singular display text for the batch type, e.g. "Vendor Invoice".
Override this as necessary.
"""
return self.batch_class.__name__
def current_batch(self):
"""
Return the current batch, based on the UUID within the URL.
"""
return Session.query(self.batch_class).get(self.request.matchdict['uuid'])
def modify_query(self, q):
q = super(BatchRowGrid, self).modify_query(q)
q = q.filter(self.row_class.batch == self.current_batch())
q = q.filter(self.row_class.removed == False)
return q
def join_map(self):
"""
Provides the default join map for batch row grid views. Derived
classes should *not* override this, but :meth:`join_map_extras()`
instead.
"""
return self.join_map_extras()
def filter_map(self):
"""
Provides the default filter map for batch row grid views. Derived
classes should *not* override this, but :meth:`filter_map_extras()`
instead.
"""
return self.make_filter_map(exact=['status_code'])
def filter_config(self):
"""
Provides the default filter config for batch grid views. Derived
classes should *not* override this, but :meth:`filter_config_extras()`
instead.
"""
kwargs = {'filter_label_status_code': "Status",
'filter_factory_status_code': EnumSearchFilter(self.row_class.STATUS)}
kwargs.update(self.filter_config_extras())
return self.make_filter_config(**kwargs)
def sort_map(self):
"""
Provides the default sort map for batch grid views. Derived classes
should *not* override this, but :meth:`sort_map_extras()` instead.
"""
map_ = self.make_sort_map()
map_.update(self.sort_map_extras())
return map_
def sort_config(self):
"""
Provides the default sort config for batch grid views. Derived classes
may override this.
"""
return self.make_sort_config(sort='sequence', dir='asc')
def grid(self):
"""
Creates the grid for the view. Derived classes should *not* override
this, but :meth:`configure_grid()` instead.
"""
g = self.make_grid()
g.extra_row_class = self.tr_class
g.sequence.set(label="Seq.")
g.status_code.set(label="Status", renderer=StatusRenderer(self.row_class.STATUS))
self._configure_grid(g)
self.configure_grid(g)
batch = self.current_batch()
g.viewable = True
g.view_route_name = '{0}.row.view'.format(self.route_prefix)
# TODO: Fix this check for edit mode.
edit_mode = self.request.referrer.endswith('/edit')
if edit_mode and not batch.executed and self.request.has_perm('{0}.edit'.format(self.permission_prefix)):
# g.editable = True
# g.edit_route_name = '{0}.rows.edit'.format(self.route_prefix)
g.deletable = True
g.delete_route_name = '{0}.rows.delete'.format(self.route_prefix)
return g
def tr_class(self, row, i):
pass
def render_kwargs(self):
"""
Add the current batch and route prefix to the template context.
"""
return {'batch': self.current_batch(),
'route_prefix': self.route_prefix}
def bulk_delete(self):
"""
"Delete" all rows matching the current row grid view query. This sets
the ``removed`` flag on the rows but does not truly delete them.
"""
self.query().update({'removed': True}, synchronize_session=False)
return HTTPFound(location=self.request.route_url(
'{0}.view'.format(self.route_prefix),
uuid=self.request.matchdict['uuid']))
class ProductBatchRowGrid(BatchRowGrid):
"""
Base grid view for batch rows which deal directly with products.
"""
def filter_map(self):
"""
Provides the default filter map for batch row grid views. Derived
classes should *not* override this, but :meth:`filter_map_extras()`
instead.
"""
return self.make_filter_map(exact=['status_code'],
ilike=['brand_name', 'description', 'size'],
upc=self.filter_gpc(self.row_class.upc))
def filter_config(self):
"""
Provides the default filter config for batch grid views. Derived
classes should *not* override this, but :meth:`filter_config_extras()`
instead.
"""
kwargs = {'filter_label_status_code': "Status",
'filter_factory_status_code': EnumSearchFilter(self.row_class.STATUS),
'filter_label_upc': "UPC",
'filter_label_brand_name': "Brand"}
kwargs.update(self.filter_config_extras())
return self.make_filter_config(**kwargs)
class BatchRowCrud(BaseCrud):
"""
Base CRUD view for batch rows.
"""
@property
def row_class(self):
raise NotImplementedError
@property
def mapped_class(self):
return self.row_class
@property
def batch_class(self):
"""
Model class of the batch to which the rows belong.
"""
return self.row_class.__batch_class__
@property
def batch_display(self):
"""
Singular display text for the batch type, e.g. "Vendor Invoice".
Override this as necessary.
"""
return self.batch_class.__name__
def template_kwargs(self, form):
"""
Add batch row instance etc. to template context.
"""
row = form.fieldset.model
return {
'row': row,
'batch_display': self.batch_display,
'route_prefix': self.route_prefix,
'permission_prefix': self.permission_prefix,
}
def delete(self):
"""
"Delete" a row from the batch. This sets the ``removed`` flag on the
row but does not truly delete it.
"""
row = self.get_model_from_request()
if not row:
return HTTPNotFound()
row.removed = True
return HTTPFound(location=self.request.route_url(
'{0}.view'.format(self.route_prefix), uuid=row.batch_uuid))
def defaults(config, batch_grid, batch_crud, row_grid, row_crud, url_prefix,
route_prefix=None, permission_prefix=None, template_prefix=None):
"""
Apply default configuration to the Pyramid configurator object, for the
given batch grid and CRUD views.
"""
assert batch_grid
assert batch_crud
assert url_prefix
if route_prefix is None:
route_prefix = batch_grid.route_prefix
if permission_prefix is None:
permission_prefix = route_prefix
if template_prefix is None:
template_prefix = url_prefix
template_prefix.rstrip('/')
# Batches grid
config.add_route(route_prefix, url_prefix)
config.add_view(batch_grid, route_name=route_prefix,
renderer='{0}/index.mako'.format(template_prefix),
permission='{0}.view'.format(permission_prefix))
# Create batch
config.add_route('{0}.create'.format(route_prefix), '{0}new'.format(url_prefix))
config.add_view(batch_crud, attr='create', route_name='{0}.create'.format(route_prefix),
renderer='{0}/create.mako'.format(template_prefix),
permission='{0}.create'.format(permission_prefix))
# View batch
config.add_route('{0}.view'.format(route_prefix), '{0}{{uuid}}'.format(url_prefix))
config.add_view(batch_crud, attr='read', route_name='{0}.view'.format(route_prefix),
renderer='{0}/view.mako'.format(template_prefix),
permission='{0}.view'.format(permission_prefix))
# Edit batch
config.add_route('{0}.edit'.format(route_prefix), '{0}{{uuid}}/edit'.format(url_prefix))
config.add_view(batch_crud, attr='update', route_name='{0}.edit'.format(route_prefix),
renderer='{0}/edit.mako'.format(template_prefix),
permission='{0}.edit'.format(permission_prefix))
# Refresh batch row data
config.add_route('{0}.refresh'.format(route_prefix), '{0}{{uuid}}/refresh'.format(url_prefix))
config.add_view(batch_crud, attr='refresh', route_name='{0}.refresh'.format(route_prefix),
permission='{0}.create'.format(permission_prefix))
# Execute batch
config.add_route('{0}.execute'.format(route_prefix), '{0}{{uuid}}/execute'.format(url_prefix))
config.add_view(batch_crud, attr='execute', route_name='{0}.execute'.format(route_prefix),
permission='{0}.execute'.format(permission_prefix))
# Download batch data file
if hasattr(batch_crud, 'download'):
config.add_route('{0}.download'.format(route_prefix), '{0}{{uuid}}/download'.format(url_prefix))
config.add_view(batch_crud, attr='download', route_name='{0}.download'.format(route_prefix),
permission='{0}.download'.format(permission_prefix))
# Delete batch
config.add_route('{0}.delete'.format(route_prefix), '{0}{{uuid}}/delete'.format(url_prefix))
config.add_view(batch_crud, attr='delete', route_name='{0}.delete'.format(route_prefix),
permission='{0}.delete'.format(permission_prefix))
# Batch rows grid
config.add_route('{0}.rows'.format(route_prefix), '{0}{{uuid}}/rows/'.format(url_prefix))
config.add_view(row_grid, route_name='{0}.rows'.format(route_prefix),
renderer='/batch/rows.mako',
permission='{0}.view'.format(permission_prefix))
# view batch row
config.add_route('{0}.row.view'.format(route_prefix), '{0}row/{{uuid}}'.format(url_prefix))
config.add_view(row_crud, attr='read', route_name='{0}.row.view'.format(route_prefix),
renderer='{0}/row.view.mako'.format(template_prefix),
permission='{0}.view'.format(permission_prefix))
# Bulk delete batch rows
config.add_route('{0}.rows.bulk_delete'.format(route_prefix), '{0}{{uuid}}/rows/delete'.format(url_prefix))
config.add_view(row_grid, attr='bulk_delete', route_name='{0}.rows.bulk_delete'.format(route_prefix),
permission='{0}.edit'.format(permission_prefix))
# Delete batch row
config.add_route('{0}.rows.delete'.format(route_prefix), '{0}delete-row/{{uuid}}'.format(url_prefix))
config.add_view(row_crud, attr='delete', route_name='{0}.rows.delete'.format(route_prefix),
permission='{0}.edit'.format(permission_prefix))