Remove all old-style batch CRUD views

This commit is contained in:
Lance Edgar 2017-07-06 15:44:37 -05:00
parent 62fa0f9fcb
commit 53d69acbcc
3 changed files with 0 additions and 763 deletions

View file

@ -1,17 +1,5 @@
.. -*- coding: utf-8 -*-
``tailbone.views.batch``
========================
.. automodule:: tailbone.views.batch
.. autoclass:: BatchCrud
:members:
.. autoclass:: FileBatchCrud
:members:
.. autoclass:: BatchRowCrud
:members:
.. autofunction:: defaults

View file

@ -27,7 +27,3 @@ Views for batches
from __future__ import unicode_literals, absolute_import
from .core import BatchMasterView, FileBatchMasterView
# TODO: deprecate / remove this
from .core import (BaseCrud, BatchCrud, FileBatchCrud,
StatusRenderer, BatchRowCrud, defaults)

View file

@ -1136,594 +1136,6 @@ class FileBatchMasterView(BatchMasterView):
"Download existing {} data file".format(model_title))
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 batch_row_class(self):
raise NotImplementedError
@property
def mapped_class(self):
return self.batch_class
@classmethod
def get_route_prefix(cls):
return cls.route_prefix
@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(readonly=True)
fs.created_by.set(label="Created by", renderer=forms.renderers.UserFieldRenderer,
readonly=True)
fs.cognized_by.set(label="Cognized by", renderer=forms.renderers.UserFieldRenderer)
fs.executed_by.set(label="Executed by", renderer=forms.renderers.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
else:
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.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:
raise httpexceptions.HTTPNotFound()
if batch.executed:
raise httpexceptions.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.")
raise httpexceptions.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 self.render_progress(kwargs)
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 = session.merge(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 = RattailSession()
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):
"""
Execute a batch. Starts a separate thread for the execution, and
displays a progress indicator page.
"""
batch = self.current_batch()
key = '{0}.execute'.format(self.batch_class.__tablename__)
progress = SessionProgress(self.request, key)
thread = Thread(target=self.execute_thread, args=(batch.uuid, progress))
thread.start()
kwargs = {
'key': key,
'cancel_url': self.view_url(batch.uuid),
'cancel_msg': "Batch execution was canceled.",
}
return self.render_progress(kwargs)
def execute_thread(self, batch_uuid, progress=None):
"""
Thread target for executing a batch with progress indicator.
"""
# Execute 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 = RattailSession()
batch = session.query(self.batch_class).get(batch_uuid)
try:
result = self.handler.execute(batch, progress=progress)
# If anything goes wrong, rollback and log the error etc.
except Exception as error:
session.rollback()
log.exception("execution failed for batch: {0}".format(batch))
session.close()
if progress:
progress.session.load()
progress.session['error'] = True
progress.session['error_msg'] = "Batch execution failed: {0}".format(error)
progress.session.save()
# If no error, check result flag (false means user canceled).
else:
if result:
batch.executed = datetime.datetime.utcnow()
batch.executed_by = session.merge(self.request.user)
session.commit()
else:
session.rollback()
session.refresh(batch)
session.close()
if progress:
progress.session.load()
progress.session['complete'] = True
progress.session['success_url'] = self.view_url(batch.uuid)
progress.session.save()
def csv(self):
"""
Download batch data as CSV.
"""
batch = self.current_batch()
fields = self.get_csv_fields()
data = StringIO()
writer = UnicodeDictWriter(data, fields)
writer.writeheader()
for row in batch.data_rows:
if not row.removed:
writer.writerow(self.get_csv_row(row, fields))
response = self.request.response
response.text = data.getvalue().decode('utf_8')
data.close()
response.content_length = len(response.text)
response.content_type = b'text/csv'
response.content_disposition = b'attachment; filename=batch.csv'
return response
def get_csv_fields(self):
"""
Return the list of fields to be written to CSV download.
"""
fields = []
mapper = orm.class_mapper(self.batch_row_class)
for prop in mapper.iterate_properties:
if isinstance(prop, orm.ColumnProperty):
if prop.key != 'removed' and not prop.key.endswith('uuid'):
fields.append(prop.key)
return fields
def get_csv_row(self, row, fields):
"""
Return a dict for use when writing the row's data to CSV download.
"""
csvrow = {}
for field in fields:
value = getattr(row, field)
csvrow[field] = '' if value is None else unicode(value)
return csvrow
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", readonly=True)
fs.created_by.set(label="Uploaded by", renderer=forms.renderers.UserFieldRenderer, readonly=True)
fs.cognized_by.set(label="Cognized by", renderer=forms.renderers.UserFieldRenderer)
fs.executed_by.set(label="Executed by", renderer=forms.renderers.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:
raise httpexceptions.HTTPFound(location=self.request.route_url(
'{}.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:
raise httpexceptions.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(forms.renderers.EnumFieldRenderer):
"""
Custom renderer for ``status_code`` fields. Adds ``status_text`` value as
@ -1739,162 +1151,3 @@ class StatusRenderer(forms.renderers.EnumFieldRenderer):
if row.status_text:
return HTML.tag('span', title=row.status_text, c=status_code_text)
return status_code_text
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 fieldset(self, row):
"""
Creates the fieldset for the view. Derived classes should *not*
override this, but :meth:`configure_fieldset()` instead.
"""
fs = self.make_fieldset(row)
self.configure_fieldset(fs)
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.
"""
fieldset.configure()
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:
raise httpexceptions.HTTPNotFound()
row.removed = True
return httpexceptions.HTTPFound(location=self.request.route_url('{}.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 row data as CSV
config.add_route('{0}.csv'.format(route_prefix), '{0}{{uuid}}/csv'.format(url_prefix))
config.add_view(batch_crud, attr='csv', route_name='{0}.csv'.format(route_prefix),
permission='{0}.csv'.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))