3
0
Fork 0

feat: add basic master view class for batches

no support for displaying rows yet, just the main batch CRUD
This commit is contained in:
Lance Edgar 2024-12-13 22:20:04 -06:00
parent d72a2a15ec
commit 5006c97b4b
7 changed files with 516 additions and 2 deletions

255
src/wuttaweb/views/batch.py Normal file
View 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()