save view state in model
This commit is contained in:
parent
6f1851fc19
commit
cce920d6ba
@ -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.')
|
||||
|
@ -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)
|
||||
|
@ -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):
|
||||
|
@ -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)
|
||||
self._next_btn.disabled = True
|
||||
# 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
|
||||
|
||||
for _ in range(2):
|
||||
self._add_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)
|
||||
desc_label = Label('', align='<')
|
||||
layout.add_widget(desc_label, 2)
|
||||
self._labels[operation] = desc_label
|
||||
self._add_line()
|
||||
self._msg_layout = Layout([100])
|
||||
self.add_layout(self._msg_layout)
|
||||
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_blank_line()
|
||||
for operation in self._model.operations:
|
||||
desc = op_desc[operation]
|
||||
self._op_layout.add_widget(Label(desc + '...', align='>'), 0)
|
||||
desc_label = Label('', align='<')
|
||||
self._op_layout.add_widget(desc_label, 2)
|
||||
self._labels[operation] = desc_label
|
||||
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()
|
||||
Thread(target=self._do_txn).start()
|
||||
# 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')
|
||||
|
@ -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):
|
||||
resp = http_get('/api/uwldap/' + username)
|
||||
if resp.status_code != 200:
|
||||
return
|
||||
data = resp.json()
|
||||
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]
|
||||
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,
|
||||
|
@ -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)
|
||||
|
||||
|
||||
|
@ -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()
|
||||
|
Loading…
x
Reference in New Issue
Block a user