save view state in model

This commit is contained in:
Max Erenberg 2021-09-05 22:48:20 +00:00
parent 6f1851fc19
commit cce920d6ba
7 changed files with 146 additions and 62 deletions

View File

@ -1,3 +1,4 @@
import sys
from typing import List, Union
import click
@ -20,6 +21,7 @@ class Abort(click.ClickException):
class CLIStreamResponseHandler(StreamResponseHandler):
def __init__(self, operations: List[str]):
super().__init__()
self.operations = operations
self.idx = 0
@ -36,6 +38,7 @@ class CLIStreamResponseHandler(StreamResponseHandler):
click.echo('The transaction was rolled back.')
click.echo('The error was: ' + err_msg)
click.echo('Please check the ceod logs.')
sys.exit(1)
def handle_completed(self):
click.echo('Transaction successfully completed.')

View File

@ -1,12 +1,35 @@
from copy import deepcopy
class Model:
"""A convenient place to share data beween views."""
"""A convenient place to store View data persistently."""
def __init__(self):
# simple key-value pairs
self.screen = None
self.title = None
self.for_member = True
self.scene_stack = []
self.deferred_req = None
# view-specific data, to be used when e.g. resizing the window
self._initial_viewdata = {
'adduser': {
'uid': '',
'cn': '',
'program': '',
'forwarding_address': '',
'num_terms': '1',
},
'transaction': {
'op_layout': None,
'msg_layout': None,
'labels': {},
'status': 'not started',
},
}
self.viewdata = deepcopy(self._initial_viewdata)
# data which is shared between multiple views
self.for_member = True
self.confirm_lines = None
self.operations = None
self.deferred_req = None
def reset_viewdata(self):
self.viewdata = deepcopy(self._initial_viewdata)

View File

@ -1,6 +1,6 @@
from typing import Dict, Union
from asciimatics.widgets import Label, Button, Layout, Frame
from asciimatics.widgets import Label, Layout
import requests
from .Model import Model
@ -12,30 +12,25 @@ class TUIStreamResponseHandler(StreamResponseHandler):
self,
model: Model,
labels: Dict[str, Label],
next_btn: Button,
msg_layout: Layout,
frame: Frame,
txn_view, # TransactionView
):
super().__init__()
self.screen = model.screen
self.operations = model.operations
self.idx = 0
self.labels = labels
self.next_btn = next_btn
self.msg_layout = msg_layout
self.frame = frame
self.txn_view = txn_view
self.error_messages = []
def _update(self):
# Since we're running in a separate thread, we need to force the
# screen to update. See
# https://github.com/peterbrittain/asciimatics/issues/56
self.frame.fix()
self.txn_view.fix()
self.screen.force_update()
def _enable_next_btn(self):
self.next_btn.disabled = False
self.frame.reset()
def _show_msg(self, msg: str = ''):
for line in msg.splitlines():
self.msg_layout.add_widget(Label(line, align='^'))
@ -43,7 +38,7 @@ class TUIStreamResponseHandler(StreamResponseHandler):
def _abort(self):
for operation in self.operations[self.idx:]:
self.labels[operation].text = 'ABORTED'
self._enable_next_btn()
self.txn_view.enable_next_btn()
def handle_non_200(self, resp: requests.Response):
self._abort()
@ -65,11 +60,10 @@ class TUIStreamResponseHandler(StreamResponseHandler):
def handle_completed(self):
self._show_msg('Transaction successfully completed.')
if len(self.error_messages) > 0:
self._show_msg('There were some errors, please check the '
'ceod logs.')
# we don't have enough space in the TUI to actually
# show the error messages
self._enable_next_btn()
self._show_msg('There were some errors:')
for msg in self.error_messages:
self._show_msg(msg)
self.txn_view.enable_next_btn()
self._update()
def handle_successful_operation(self):

View File

@ -20,9 +20,9 @@ class TransactionView(Frame):
)
self._model = model
# map operation names to label widgets
self._labels = {}
self._labels = model.viewdata['transaction']['labels']
# this is an ugly hack to get around the fact that _on_load()
# will be called again when we reset() in the TUIStreamResponseHandler
# will be called again when we reset() in enable_next_btn.
self._loaded = False
def _add_buttons(self):
@ -33,49 +33,80 @@ class TransactionView(Frame):
layout = Layout([1, 1])
self.add_layout(layout)
self._next_btn = Button('Next', self._next)
# we don't want to disable the button if the txn completed
# and the user just resized the window
if self._model.viewdata['transaction']['status'] != 'completed':
self._next_btn.disabled = True
layout.add_widget(self._next_btn, 1)
def _add_line(self, text: str = ''):
layout = Layout([100])
self.add_layout(layout)
layout.add_widget(Label(text, align='^'))
def _add_blank_line(self):
self._op_layout.add_widget(Label(''), 0)
self._op_layout.add_widget(Label(''), 2)
def _on_load(self):
if self._loaded:
return
self._loaded = True
d = self._model.viewdata['transaction']
first_time = True
if d['op_layout'] is None:
self._op_layout = Layout([10, 1, 10])
self.add_layout(self._op_layout)
# store the layouts so that we can re-use them when the screen
# gets resized
d['op_layout'] = self._op_layout
for _ in range(2):
self._add_line()
self._add_blank_line()
for operation in self._model.operations:
desc = op_desc[operation]
layout = Layout([10, 1, 10])
self.add_layout(layout)
layout.add_widget(Label(desc + '...', align='>'), 0)
self._op_layout.add_widget(Label(desc + '...', align='>'), 0)
desc_label = Label('', align='<')
layout.add_widget(desc_label, 2)
self._op_layout.add_widget(desc_label, 2)
self._labels[operation] = desc_label
self._add_line()
self._add_blank_line()
# this is the where success/failure messages etc. get placed
self._msg_layout = Layout([100])
self.add_layout(self._msg_layout)
d['msg_layout'] = self._msg_layout
else:
# we arrive here when the screen has been resized
first_time = False
# restore the layouts which we saved
self._op_layout = d['op_layout']
self.add_layout(self._op_layout)
self._msg_layout = d['msg_layout']
self.add_layout(self._msg_layout)
# fill up the rest of the space
self.add_layout(Layout([100], fill_frame=True))
self._add_buttons()
self.fix()
# only send the API request the first time we arrive at this
# scene, not when the screen gets resized
if first_time:
Thread(target=self._do_txn).start()
def _do_txn(self):
self._model.viewdata['transaction']['status'] = 'in progress'
resp = self._model.deferred_req()
handler = TUIStreamResponseHandler(
model=self._model,
labels=self._labels,
next_btn=self._next_btn,
msg_layout=self._msg_layout,
frame=self,
txn_view=self,
)
generic_handle_stream_response(resp, self._model.operations, handler)
def enable_next_btn(self):
self._next_btn.disabled = False
# If we don't reset, the button isn't selectable, even though we
# enabled it
self.reset()
# save the fact that the transaction is completed
self._model.viewdata['transaction']['status'] = 'completed'
def _next(self):
self._model.reset_viewdata()
self._model.scene_stack.clear()
raise NextScene('Welcome')

View File

@ -1,5 +1,7 @@
from threading import Thread
from asciimatics.exceptions import NextScene
from asciimatics.widgets import Frame, Layout, Text, Button, Divider
from asciimatics.widgets import Frame, Layout, Text, Button, Divider, Label
from ...utils import http_get, http_post, defer, user_dict_kv, \
get_terms_for_new_user, get_adduser_operations
@ -16,6 +18,7 @@ class AddUserView(Frame):
)
self._model = model
self._username_changed = False
layout = Layout([100], fill_frame=True)
self.add_layout(layout)
self._username = Text(
@ -33,9 +36,13 @@ class AddUserView(Frame):
self._num_terms = Text(
"Number of terms:", "num_terms",
validator=lambda s: s.isdigit() and s[0] != '0')
self._num_terms.value = '1'
layout.add_widget(self._num_terms)
layout = Layout([100])
self.add_layout(layout)
self._status_label = Label('')
layout.add_widget(self._status_label)
layout = Layout([100])
self.add_layout(layout)
layout.add_widget(Divider())
@ -48,6 +55,14 @@ class AddUserView(Frame):
def _on_load(self):
self.title = self._model.title
# restore the saved input fields' values
self.data = self._model.viewdata['adduser']
def _on_unload(self):
# save the input fields' values so that they don't disappear when
# the window gets resized
self.save()
self._model.viewdata['adduser'] = self.data
def _on_username_change(self):
self._username_changed = True
@ -59,23 +74,28 @@ class AddUserView(Frame):
username = self._username.value
if username == '':
return
self._get_uwldap_info(username)
Thread(target=self._get_uwldap_info, args=[username]).start()
#self._get_uwldap_info(username)
def _get_uwldap_info(self, username):
self._status_label.text = 'Searching for user...'
try:
resp = http_get('/api/uwldap/' + username)
if resp.status_code != 200:
return
data = resp.json()
self._status_label.text = ''
self._full_name.value = data['cn']
self._program.value = data.get('program', '')
if data.get('mail_local_addresses'):
self._forwarding_address.value = data['mail_local_addresses'][0]
finally:
self._status_label.text = ''
def _back(self):
raise NextScene(self._model.scene_stack.pop())
def _next(self):
self._model.prev_scene = 'AddUser'
body = {
'uid': self._username.value,
'cn': self._full_name.value,

View File

@ -20,18 +20,30 @@ def unhandled(event):
raise StopApplication("User terminated app")
def screen_wrapper(screen, scene, model):
# tuples of (name, view)
views = []
def screen_wrapper(screen, last_scene, model):
global views
model.screen = screen
# unload the old views
for name, view in views:
if hasattr(view, '_on_unload'):
view._on_unload()
width = min(screen.width, 90)
height = min(screen.height, 24)
views = [
('Welcome', WelcomeView(screen, width, height, model)),
('AddUser', AddUserView(screen, width, height, model)),
('Confirm', ConfirmView(screen, width, height, model)),
('Transaction', TransactionView(screen, width, height, model)),
]
scenes = [
Scene([WelcomeView(screen, width, height, model)], -1, name='Welcome'),
Scene([AddUserView(screen, width, height, model)], -1, name='AddUser'),
Scene([ConfirmView(screen, width, height, model)], -1, name='Confirm'),
Scene([TransactionView(screen, width, height, model)], -1, name='Transaction'),
Scene([view], -1, name=name) for name, view in views
]
screen.play(
scenes, stop_on_resize=True, start_scene=scene, allow_int=True,
scenes, stop_on_resize=True, start_scene=last_scene, allow_int=True,
unhandled_input=unhandled)

View File

@ -149,15 +149,16 @@ def generic_handle_stream_response(
"""
if resp.status_code != 200:
handler.handle_non_200(resp)
return
handler.begin()
idx = 0
data = []
for line in resp.iter_lines(decode_unicode=True, chunk_size=8):
for line in resp.iter_lines(decode_unicode=True, chunk_size=1):
d = json.loads(line)
data.append(d)
if d['status'] == 'aborted':
handler.handle_aborted(d['error'])
sys.exit(1)
return
elif d['status'] == 'completed':
while idx < len(operations):
handler.handle_skipped_operation()