Remove all old-style batch CRUD views
This commit is contained in:
parent
62fa0f9fcb
commit
53d69acbcc
|
@ -1,17 +1,5 @@
|
||||||
.. -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
``tailbone.views.batch``
|
``tailbone.views.batch``
|
||||||
========================
|
========================
|
||||||
|
|
||||||
.. automodule:: tailbone.views.batch
|
.. automodule:: tailbone.views.batch
|
||||||
|
|
||||||
.. autoclass:: BatchCrud
|
|
||||||
:members:
|
|
||||||
|
|
||||||
.. autoclass:: FileBatchCrud
|
|
||||||
:members:
|
|
||||||
|
|
||||||
.. autoclass:: BatchRowCrud
|
|
||||||
:members:
|
|
||||||
|
|
||||||
.. autofunction:: defaults
|
|
||||||
|
|
|
@ -27,7 +27,3 @@ Views for batches
|
||||||
from __future__ import unicode_literals, absolute_import
|
from __future__ import unicode_literals, absolute_import
|
||||||
|
|
||||||
from .core import BatchMasterView, FileBatchMasterView
|
from .core import BatchMasterView, FileBatchMasterView
|
||||||
|
|
||||||
# TODO: deprecate / remove this
|
|
||||||
from .core import (BaseCrud, BatchCrud, FileBatchCrud,
|
|
||||||
StatusRenderer, BatchRowCrud, defaults)
|
|
||||||
|
|
|
@ -1136,594 +1136,6 @@ class FileBatchMasterView(BatchMasterView):
|
||||||
"Download existing {} data file".format(model_title))
|
"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):
|
class StatusRenderer(forms.renderers.EnumFieldRenderer):
|
||||||
"""
|
"""
|
||||||
Custom renderer for ``status_code`` fields. Adds ``status_text`` value as
|
Custom renderer for ``status_code`` fields. Adds ``status_text`` value as
|
||||||
|
@ -1739,162 +1151,3 @@ class StatusRenderer(forms.renderers.EnumFieldRenderer):
|
||||||
if row.status_text:
|
if row.status_text:
|
||||||
return HTML.tag('span', title=row.status_text, c=status_code_text)
|
return HTML.tag('span', title=row.status_text, c=status_code_text)
|
||||||
return 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))
|
|
||||||
|
|
Loading…
Reference in a new issue