poobrains/poobrains/formapp.py

330 lines
9.8 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

"""
Abstractions for complex, session-based form applications.
"""
from poobrains import app, flash, redirect, session, new_session
import poobrains.form
import poobrains.auth
@new_session
def app_sessions(session):
session['apps'] = {}
for cls in FormApp.class_children():
session['apps'][cls.__name__.lower()] = {}
class FormApp(poobrains.auth.ProtectedForm):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.session = session['apps'][self.__class__.__name__.lower()]
self.session_is_new = session == {}
def session_delete(self):
session['apps'][self.__class__.__name__.lower()] = {}
class BoundFormApp(FormApp):
def __new__(cls, model_or_instance, mode=None, prefix=None, name=None, title=None, method=None, action=None):
""" Copy of poobrains.auth.BoundForm.__new__ """
f = super(BoundFormApp, cls).__new__(cls, prefix=prefix, name=name, title=title, method=method, action=action)
if isinstance(model_or_instance, type(poobrains.auth.Administerable)): # hacky
f.model = model_or_instance
f.instance = f.model()
else:
f.instance = model_or_instance
f.model = f.instance.__class__
if hasattr(f.instance, 'menu_actions'):
f.menu_actions = f.instance.menu_actions
if hasattr(f.instance, 'menu_related'):
f.menu_related = f.instance.menu_related
return f
def __init__(self, model_or_instance, **kwargs):
super().__init__(**kwargs)
self.session_is_new = False
if not self.instance.handle_string in self.session:
self.session[self.instance.handle_string] = {}
self.session_is_new = True
flash(f"Created new {self.__class__.__name__} session for '{self.instance.handle_string}'.")
self.session = self.session[self.instance.handle_string]
def session_delete(self):
del(session['apps'][self.__class__.__name__.lower()][self.instance.handle_string])
class Component(poobrains.form.Fieldset):
def __init__(self, parent, **kwargs):
"""
`parent` must be `FormApp` (or subclass thereof) or `Component` (or subclass thereof).
"""
super().__init__(**kwargs)
self.parent = parent
if isinstance(parent, FormApp):
self.formapp = parent
else:
self.formapp = self.parent.formapp
self.session_is_new = parent.session_is_new
if not '_children' in parent.session:
parent.session['_children'] = {}
if not self.name in parent.session['_children']:
parent.session['_children'][self.name] = {}
self.session = parent.session['_children'][self.name]
def __setattr__(self, name, value):
if name == 'parent':
# get around putting parent (in case it's Toolbox or MultistateFieldset)
# into self.fields, avoiding looping references leading to infinite recursion
super(poobrains.form.BaseForm, self).__setattr__(name, value)
elif name == 'name' and value != self.name and hasattr(self, 'session'):
if value in self.parent.session['_children']:
# adopt any data already existing in session
# a change of "name" is mainly expected when doing something like "self.foo = SomeComponent()"
# because the name is automatically set to 'foo' by super().__setattr_
self.session = self.parent.session['_children'][value]
else:
# move own session data to new location
self.parent.session['_children'][value] = self.session
del(self.parent.session['_children'][self.name])
super().__setattr__(name, value)
else:
super().__setattr__(name, value)
def session_delete(self):
del(self.parent.session['_children'][self.name])
def process_submitting_fieldset(self, submit):
fieldset = self.find_submitting_fieldset(submit)
child_submit = self.child_submit(submit)
if isinstance(fieldset, Component):
pre_return = fieldset.preprocess(child_submit) # handles 'cancel' buttons and other potential generic UI elements
if pre_return:
return pre_return
return fieldset.process(child_submit)
class Toolbox(Component):
"""
tools = {
# format of
# <name>: (<label>, <form_class>)
'foo': ('Le Foo': FooForm),
'bar': ('Le Bar': BarForm)
}
"""
tools = {}
def __init__(self, parent, **kwargs):
super().__init__(parent, **kwargs)
if self.session_is_new:
#self.tool_active = next(iter(self.tools)) # first key of self.tools
self.tool_active = None
self.tools_enabled = set(self.tools.keys()) # enable all tools by default
self.build_toolbar()
self.build_tool()
@property
def tool_active(self):
return self.session.get('tool_active', None)
@tool_active.setter
def tool_active(self, value):
self.session['tool_active'] = value
def tool_enable(self, name):
if name not in self.tools:
raise KeyError(f"Unknown tool to enable: '{name}'.")
self.tools_enabled.add(name)
def tool_disable(self, name):
if name not in self.tools:
raise KeyError(f"Unknown tool to disable: '{name}'.")
self.tools_enabled.remove(name)
def build_toolbar(self):
self.toolbar = poobrains.form.Fieldset(title=f'{self.title} toolbar')
for name, (label, form_class) in self.tools.items():
disabled = not name in self.tools_enabled
if name == self.tool_active:
setattr(self.toolbar, name, poobrains.form.Button(type='submit', label=label, value=name, css_class=f'{name} active', disabled=True))
elif self.tool_active is None:
setattr(self.toolbar, name, poobrains.form.Button(type='submit', label=label, value=name, css_class=name, disabled=disabled))
else:
setattr(self.toolbar, name, poobrains.form.DoubleOptInButton('You are currently in the middle of an unfinished operation override?', type='submit', label=label, value=name, css_class=name, disabled=disabled))
def build_tool(self):
if not self.tool_active is None:
form_class = self.tools[self.tool_active][1]
self.tool = form_class(self)#, name='tool')
else:
self.tool = poobrains.form.fields.Message(value="No tool selected.")
def reset(self):
#self.session['tool_active'] = None
self.session.clear()
def render(self, mode='full'):
self.build_toolbar()
self.build_tool()
return super().render(mode=mode)
def process(self, submit):
if submit.startswith('toolbar.'):
tool = submit.split('.')[1]
self.session['tool_active'] = tool
return redirect('#_')
else:
return self.process_submitting_fieldset(submit)
class MultistateFieldset(Component):
@property
def state(self):
return self.session.get('state', 'start')
@state.setter
def state(self, state):
self.session['state'] = state
def build(self):
if not isinstance(self.parent, FormApp): # FIXME: This condition makes no sense!
self.cancel = poobrains.form.Button('submit', label='Cancel')
def render(self, mode='full'):
self.build()
return super().render(mode=mode)
def bind(self, values, files):
self.build()
super().bind(values, files)
def preprocess(self, submit):
if submit == 'cancel':
if self.state == 'start':
if isinstance(self.parent, Toolbox):
self.parent.tool_active = None
self.session_delete()
else:
self.session.clear()
flash('Cancelled operation.')
else:
self.state = 'start'
flash('Went back.')
return redirect('#_')
class FnordForm(MultistateFieldset):
def build(self):
super().build()
if self.state == 'next_thing':
self.do_next_thing = poobrains.form.Button('submit', label='Do next thing!')
elif self.state == 'last_thing':
self.do_last_thing = poobrains.form.Button('submit', label='Do last thing!')
else:
self.do_thing = poobrains.form.Button('submit', label='Do thing!')
def process(self, submit):
if submit == 'do_thing':
self.state = 'next_thing'
elif submit == 'do_next_thing':
flash("A thing did the happen!")
self.state = 'last_thing'
elif submit == 'do_last_thing':
self.parent.tool_active = None
self.session_delete()
flash("Did last thing.")
return redirect('#_')
class EschatonForm(MultistateFieldset):
def build(self):
super().build()
if self.state == 'start':
self.title = 'The eschaton is upon us!'
self.immanentize = poobrains.form.Button('submit', label='Immanentize!')
else:
self.title = 'The eschaton has been immanentized!'
self.unimmanentized = poobrains.form.Button('submit', label='OH GODDESS MAKE IT STOP!1!!')
def process(self, submit):
if submit == 'immanentize':
self.state = 'immanentization'
else:
self.state = 'start'
return redirect('#_')
class FoolBox(Toolbox):
tools = {
'fnord': ('Fnord', FnordForm),
'eschaton': ('Immanentize the Eschaton', EschatonForm)
}
class FoolApp(FormApp):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.foolbox = FoolBox(self)
app.admin.add_view(FoolApp, '/fool/')