feat: add basic master view class for batches
no support for displaying rows yet, just the main batch CRUD
This commit is contained in:
parent
d72a2a15ec
commit
5006c97b4b
7 changed files with 516 additions and 2 deletions
255
src/wuttaweb/views/batch.py
Normal file
255
src/wuttaweb/views/batch.py
Normal file
|
@ -0,0 +1,255 @@
|
|||
# -*- coding: utf-8; -*-
|
||||
################################################################################
|
||||
#
|
||||
# wuttaweb -- Web App for Wutta Framework
|
||||
# Copyright © 2024 Lance Edgar
|
||||
#
|
||||
# This file is part of Wutta Framework.
|
||||
#
|
||||
# Wutta Framework is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU General Public License as published by the Free
|
||||
# Software Foundation, either version 3 of the License, or (at your option) any
|
||||
# later version.
|
||||
#
|
||||
# Wutta Framework 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 General Public License for
|
||||
# more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# Wutta Framework. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
"""
|
||||
Base logic for Batch Master views
|
||||
"""
|
||||
|
||||
import logging
|
||||
import threading
|
||||
import time
|
||||
|
||||
from sqlalchemy import orm
|
||||
|
||||
from wuttaweb.views import MasterView
|
||||
from wuttaweb.forms.schema import UserRef
|
||||
from wuttaweb.forms.widgets import BatchIdWidget
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BatchMasterView(MasterView):
|
||||
"""
|
||||
Base class for all "batch master" views.
|
||||
"""
|
||||
|
||||
labels = {
|
||||
'id': "Batch ID",
|
||||
'status_code': "Batch Status",
|
||||
}
|
||||
|
||||
sort_defaults = ('id', 'desc')
|
||||
|
||||
def __init__(self, request, context=None):
|
||||
super().__init__(request, context=context)
|
||||
self.batch_handler = self.get_batch_handler()
|
||||
|
||||
def get_batch_handler(self):
|
||||
"""
|
||||
Must return the :term:`batch handler` for use with this view.
|
||||
|
||||
There is no default logic; subclass must override.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def configure_grid(self, g):
|
||||
""" """
|
||||
super().configure_grid(g)
|
||||
model = self.app.model
|
||||
|
||||
# created_by
|
||||
CreatedBy = orm.aliased(model.User)
|
||||
g.set_joiner('created_by',
|
||||
lambda q: q.join(CreatedBy,
|
||||
CreatedBy.uuid == self.model_class.created_by_uuid))
|
||||
g.set_sorter('created_by', CreatedBy.username)
|
||||
# g.set_filter('created_by', CreatedBy.username, label="Created By Username")
|
||||
|
||||
# id
|
||||
g.set_renderer('id', self.render_batch_id)
|
||||
g.set_link('id')
|
||||
|
||||
# description
|
||||
g.set_link('description')
|
||||
|
||||
def render_batch_id(self, batch, key, value):
|
||||
""" """
|
||||
if value:
|
||||
batch_id = int(value)
|
||||
return f'{batch_id:08d}'
|
||||
|
||||
def get_instance_title(self, batch):
|
||||
""" """
|
||||
if batch.description:
|
||||
return f"{batch.id_str} {batch.description}"
|
||||
return batch.id_str
|
||||
|
||||
def configure_form(self, f):
|
||||
""" """
|
||||
super().configure_form(f)
|
||||
batch = f.model_instance
|
||||
|
||||
# id
|
||||
if self.creating:
|
||||
f.remove('id')
|
||||
else:
|
||||
f.set_readonly('id')
|
||||
f.set_widget('id', BatchIdWidget())
|
||||
|
||||
# notes
|
||||
f.set_widget('notes', 'notes')
|
||||
|
||||
# rows
|
||||
f.remove('rows')
|
||||
if self.creating:
|
||||
f.remove('row_count')
|
||||
else:
|
||||
f.set_readonly('row_count')
|
||||
|
||||
# status
|
||||
f.remove('status_text')
|
||||
if self.creating:
|
||||
f.remove('status_code')
|
||||
else:
|
||||
f.set_readonly('status_code')
|
||||
|
||||
# created
|
||||
if self.creating:
|
||||
f.remove('created')
|
||||
else:
|
||||
f.set_readonly('created')
|
||||
|
||||
# created_by
|
||||
f.remove('created_by_uuid')
|
||||
if self.creating:
|
||||
f.remove('created_by')
|
||||
else:
|
||||
f.set_node('created_by', UserRef(self.request))
|
||||
f.set_readonly('created_by')
|
||||
|
||||
# executed
|
||||
if self.creating or not batch.executed:
|
||||
f.remove('executed')
|
||||
else:
|
||||
f.set_readonly('executed')
|
||||
|
||||
# executed_by
|
||||
f.remove('executed_by_uuid')
|
||||
if self.creating or not batch.executed:
|
||||
f.remove('executed_by')
|
||||
else:
|
||||
f.set_node('executed_by', UserRef(self.request))
|
||||
f.set_readonly('executed_by')
|
||||
|
||||
def objectify(self, form):
|
||||
"""
|
||||
We override the default logic here, to invoke
|
||||
:meth:`~wuttjamaican:wuttjamaican.batch.BatchHandler.make_batch()`
|
||||
on the batch handler - when creating. Parent/default logic is
|
||||
used when updating.
|
||||
"""
|
||||
if self.creating:
|
||||
|
||||
# first get the "normal" objectified batch. this will have
|
||||
# all attributes set correctly per the form data, but will
|
||||
# not yet belong to the db session. we ultimately discard it.
|
||||
schema = form.get_schema()
|
||||
batch = schema.objectify(form.validated, context=form.model_instance)
|
||||
|
||||
# then we collect attributes from the new batch
|
||||
kwargs = dict([(key, getattr(batch, key))
|
||||
for key in form.validated
|
||||
if hasattr(batch, key)])
|
||||
|
||||
# and set attribute for user creating the batch
|
||||
kwargs['created_by'] = self.request.user
|
||||
|
||||
# finally let batch handler make the "real" batch
|
||||
return self.batch_handler.make_batch(self.Session(), **kwargs)
|
||||
|
||||
# when not creating, normal logic is fine
|
||||
return super().objectify(form)
|
||||
|
||||
def redirect_after_create(self, batch):
|
||||
"""
|
||||
If the new batch requires initial population, we launch a
|
||||
thread for that and show the "progress" page.
|
||||
|
||||
Otherwise this will do the normal thing of redirecting to the
|
||||
"view" page for the new batch.
|
||||
"""
|
||||
# just view batch if should not populate
|
||||
if not self.batch_handler.should_populate(batch):
|
||||
return self.redirect(self.get_action_url('view', batch))
|
||||
|
||||
# setup thread to populate batch
|
||||
route_prefix = self.get_route_prefix()
|
||||
key = f'{route_prefix}.populate'
|
||||
progress = self.make_progress(key, success_url=self.get_action_url('view', batch))
|
||||
thread = threading.Thread(target=self.populate_thread,
|
||||
args=(batch.uuid,),
|
||||
kwargs=dict(progress=progress))
|
||||
|
||||
# start thread and show progress page
|
||||
thread.start()
|
||||
return self.render_progress(progress)
|
||||
|
||||
def populate_thread(self, batch_uuid, progress=None):
|
||||
"""
|
||||
Thread target for populating new object with progress indicator.
|
||||
|
||||
When a new batch is created, and the batch handler says it
|
||||
should also be populated, then this thread is launched to do
|
||||
so outside of the main request/response cycle. Progress bar
|
||||
is then shown to the user until it completes.
|
||||
|
||||
This method mostly just calls
|
||||
:meth:`~wuttjamaican:wuttjamaican.batch.BatchHandler.do_populate()`
|
||||
on the :term:`batch handler`.
|
||||
"""
|
||||
# nb. must use our own session in separate thread
|
||||
session = self.app.make_session()
|
||||
|
||||
# nb. main web request which created the batch, must complete
|
||||
# before that session is committed. until that happens we
|
||||
# will not be able to see the new batch. hence this loop,
|
||||
# where we wait for the batch to appear.
|
||||
batch = None
|
||||
tries = 0
|
||||
while not batch:
|
||||
batch = session.get(self.model_class, batch_uuid)
|
||||
tries += 1
|
||||
if tries > 10:
|
||||
raise RuntimeError("can't find the batch")
|
||||
time.sleep(0.1)
|
||||
|
||||
try:
|
||||
# populate the batch
|
||||
self.batch_handler.do_populate(batch, progress=progress)
|
||||
session.flush()
|
||||
|
||||
except Exception as error:
|
||||
session.rollback()
|
||||
log.warning("failed to populate %s: %s",
|
||||
self.get_model_title(), batch,
|
||||
exc_info=True)
|
||||
if progress:
|
||||
progress.handle_error(error)
|
||||
|
||||
else:
|
||||
session.commit()
|
||||
if progress:
|
||||
progress.handle_success()
|
||||
|
||||
finally:
|
||||
session.close()
|
Loading…
Add table
Add a link
Reference in a new issue