330 lines
9.8 KiB
Python
330 lines
9.8 KiB
Python
"""
|
||
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/')
|