implement GetUser in TUI

This commit is contained in:
Max Erenberg 2021-09-07 02:29:53 +00:00
parent af73dd713d
commit d3c98e418a
10 changed files with 128 additions and 34 deletions

View File

@ -13,6 +13,7 @@ class CeoFrame(Frame):
on_load=None, on_load=None,
title=None, title=None,
save_data=False, # whether to save widget state for resizing save_data=False, # whether to save widget state for resizing
has_dynamic_layouts=False, # whether layouts are created on load
): ):
super().__init__( super().__init__(
screen, screen,
@ -28,6 +29,7 @@ class CeoFrame(Frame):
self._model = model self._model = model
self._name = name self._name = name
self._loaded = False self._loaded = False
self._has_dynamic_layouts = has_dynamic_layouts
def _ceoframe_on_load(self): def _ceoframe_on_load(self):
# We usually don't want _on_load() to be called multiple times # We usually don't want _on_load() to be called multiple times
@ -44,14 +46,39 @@ class CeoFrame(Frame):
if self._extra_on_load is not None: if self._extra_on_load is not None:
self._extra_on_load() self._extra_on_load()
def _on_unload(self): def _ceoframe_on_unload(self):
"""
This should be called just after the screen gets resized,
but before the new scenes are constructed.
The idea is to save the user's data in the input fields
so that we can restore them in the new scenes.
"""
if not self._save_data: if not self._save_data:
return return
# save the input fields' values so that they don't disappear when
# the window gets resized
self.save() self.save()
self._model.viewdata[self._name] = self.data self._model.viewdata[self._name] = self.data
def _ceoframe_on_reset(self):
"""
This needs to be called whenever we return to the home screen
after some kind of operation was completed.
Currently this is called from Model.reset().
"""
# We want a fresh slate once we return to the home screen, so we
# want on_load() to be called for the scenes.
self._loaded = False
if self._has_dynamic_layouts:
# We don't want layouts to accumulate.
self.clear_layouts()
def clear_layouts(self):
# OK so this a *really* bad thing to do, since we're reaching
# into the private variables of a third-party library.
# Unfortunately asciimatics doesn't allow us to clear the layouts
# of an existing frame, and we need this to be able to re-use
# frames which create layouts dynamically.
self._layouts.clear()
def add_buttons( def add_buttons(
self, back_btn=False, back_btn_text='Back', self, back_btn=False, back_btn_text='Back',
next_scene=None, next_btn_text='Next', on_next=None, next_scene=None, next_btn_text='Next', on_next=None,

View File

@ -8,6 +8,7 @@ class ConfirmView(CeoFrame):
super().__init__( super().__init__(
screen, height, width, model, 'Confirm', screen, height, width, model, 'Confirm',
on_load=self._confirmview_on_load, title='Confirmation', on_load=self._confirmview_on_load, title='Confirmation',
has_dynamic_layouts=True,
) )
def _add_line(self, text: str = ''): def _add_line(self, text: str = ''):
@ -48,7 +49,7 @@ class ConfirmView(CeoFrame):
def _next(self): def _next(self):
self.flash_message('Sending request...', force_update=True) self.flash_message('Sending request...', force_update=True)
try: try:
self._model.deferred_req_resp = self._model.deferred_req() self._model.resp = self._model.deferred_req()
finally: finally:
self.clear_flash_message() self.clear_flash_message()
self.go_to_next_scene('Result') self.go_to_next_scene('Result')

View File

@ -9,6 +9,7 @@ class ErrorView(CeoFrame):
super().__init__( super().__init__(
screen, height, width, model, 'Error', screen, height, width, model, 'Error',
on_load=self._errorview_on_load, title='Error', on_load=self._errorview_on_load, title='Error',
has_dynamic_layouts=True,
) )
def _errorview_on_load(self): def _errorview_on_load(self):

View File

@ -6,6 +6,7 @@ class Model:
def __init__(self): def __init__(self):
self.screen = None self.screen = None
self.views = []
self.title = None self.title = None
self.scene_stack = [] self.scene_stack = []
self.error_message = None self.error_message = None
@ -28,7 +29,9 @@ class Model:
'labels': {}, 'labels': {},
'status': 'not started', 'status': 'not started',
}, },
'Result': {}, 'GetUser': {
'uid': '',
},
} }
self.viewdata = deepcopy(self._initial_viewdata) self.viewdata = deepcopy(self._initial_viewdata)
# data which is shared between multiple views # data which is shared between multiple views
@ -36,7 +39,7 @@ class Model:
self.confirm_lines = None self.confirm_lines = None
self.operations = None self.operations = None
self.deferred_req = None self.deferred_req = None
self.deferred_req_resp = None self.resp = None
def reset(self): def reset(self):
self.viewdata = deepcopy(self._initial_viewdata) self.viewdata = deepcopy(self._initial_viewdata)
@ -44,7 +47,10 @@ class Model:
self.confirm_lines = None self.confirm_lines = None
self.operations = None self.operations = None
self.deferred_req = None self.deferred_req = None
self.deferred_req_resp = None self.resp = None
self.title = None self.title = None
self.error_message = None self.error_message = None
self.scene_stack.clear() self.scene_stack.clear()
for view in self.views:
if hasattr(view, '_ceoframe_on_reset'):
view._ceoframe_on_reset()

View File

@ -10,33 +10,46 @@ class ResultView(CeoFrame):
super().__init__( super().__init__(
screen, height, width, model, 'Result', screen, height, width, model, 'Result',
on_load=self._resultview_on_load, title='Result', on_load=self._resultview_on_load, title='Result',
has_dynamic_layouts=True,
) )
self._summary_layout = Layout([1, 10], fill_frame=True)
self.add_layout(self._summary_layout)
self._show_msg()
# TODO: deduplicate this from ConfirmView
def _add_text(self, text: str = '\n', center: bool = False):
if center:
layout = Layout([100])
align = '^'
col = 0
else:
layout = Layout([1, 10])
align = '<'
col = 1
self.add_layout(layout)
for line in text.splitlines():
layout.add_widget(Label(line, align=align), col)
def _add_pair(self, key: str, val: str):
layout = Layout([10, 1, 10])
self.add_layout(layout)
layout.add_widget(Label(key + ':', align='>'), 0)
layout.add_widget(Label(val, align='<'), 2)
# override this method in child classes if desired
def show_result(self, resp: requests.Response):
self._add_text('The operation was successfully performed.', center=True)
def _resultview_on_load(self):
self._add_text()
resp = self._model.resp
if resp.status_code != 200:
self._add_text('An error occurred:')
self._add_text(resp.text.rstrip())
else:
self.show_result(resp)
# fill the rest of the space
self.add_layout(Layout([100], fill_frame=True))
self.add_buttons(on_next_excl=self._next) self.add_buttons(on_next_excl=self._next)
self.fix()
def _next(self): def _next(self):
self._model.reset() self._model.reset()
raise NextScene('Welcome') raise NextScene('Welcome')
def _show_msg(self, text: str = '\n', center=False):
for line in text.splitlines():
align = '^' if center else '<'
self._summary_layout.add_widget(Label(line, align=align), 1)
# override this method in child classes if desired
def show_result(self, resp: requests.Response):
self._show_msg('The operation was successfully performed.', center=True)
def _resultview_on_load(self):
resp = self._model.deferred_req_resp
try:
if resp.status_code != 200:
self._show_msg('An error occurred:')
self._show_msg(resp.text.rstrip())
return
self.show_result(resp)
finally:
self.fix()

View File

@ -14,6 +14,7 @@ class TransactionView(CeoFrame):
super().__init__( super().__init__(
screen, height, width, model, 'Transaction', screen, height, width, model, 'Transaction',
on_load=self._txnview_on_load, title='Running Transaction', on_load=self._txnview_on_load, title='Running Transaction',
has_dynamic_layouts=True,
) )
self._model = model self._model = model
# map operation names to label widgets # map operation names to label widgets

View File

@ -0,0 +1,11 @@
import requests
from ...utils import user_dict_kv
from ..ResultView import ResultView
class GetUserResultView(ResultView):
def show_result(self, resp: requests.Response):
pairs = user_dict_kv(resp.json())
for key, val in pairs:
self._add_pair(key, val)

View File

@ -0,0 +1,29 @@
from asciimatics.widgets import Layout, Text
from ...utils import http_get
from ..CeoFrame import CeoFrame
class GetUserView(CeoFrame):
def __init__(self, screen, width, height, model):
super().__init__(
screen, height, width, model, 'GetUser',
save_data=True,
)
layout = Layout([100], fill_frame=True)
self.add_layout(layout)
self._username = Text("Username:", "uid")
layout.add_widget(self._username)
self.add_flash_message_layout()
self.add_buttons(
back_btn=True,
next_scene='GetUserResult', on_next=self._next)
self.fix()
def _next(self):
uid = self._username.value
self.flash_message('Looking up user...', force_update=True)
try:
self._model.resp = http_get(f'/api/members/{uid}')
finally:
self.clear_flash_message()

View File

@ -1,4 +1,4 @@
from asciimatics.widgets import Layout, Text, Label from asciimatics.widgets import Layout, Text
from ...term_utils import get_terms_for_renewal from ...term_utils import get_terms_for_renewal
from ...utils import http_post, defer from ...utils import http_post, defer

View File

@ -12,6 +12,8 @@ from .ResultView import ResultView
from .TransactionView import TransactionView from .TransactionView import TransactionView
from .WelcomeView import WelcomeView from .WelcomeView import WelcomeView
from .members.AddUserView import AddUserView from .members.AddUserView import AddUserView
from .members.GetUserView import GetUserView
from .members.GetUserResultView import GetUserResultView
from .members.RenewUserView import RenewUserView from .members.RenewUserView import RenewUserView
@ -29,11 +31,10 @@ views = []
def screen_wrapper(screen, last_scene, model): def screen_wrapper(screen, last_scene, model):
global views global views
model.screen = screen
# unload the old views # unload the old views
for name, view in views: for name, view in views:
if hasattr(view, '_on_unload'): if hasattr(view, '_on_ceoframe_unload'):
view._on_unload() view._on_ceoframe_unload()
width = min(screen.width, 90) width = min(screen.width, 90)
height = min(screen.height, 24) height = min(screen.height, 24)
views = [ views = [
@ -44,10 +45,14 @@ def screen_wrapper(screen, last_scene, model):
('Error', ErrorView(screen, width, height, model)), ('Error', ErrorView(screen, width, height, model)),
('AddUser', AddUserView(screen, width, height, model)), ('AddUser', AddUserView(screen, width, height, model)),
('RenewUser', RenewUserView(screen, width, height, model)), ('RenewUser', RenewUserView(screen, width, height, model)),
('GetUser', GetUserView(screen, width, height, model)),
('GetUserResult', GetUserResultView(screen, width, height, model)),
] ]
scenes = [ scenes = [
Scene([view], -1, name=name) for name, view in views Scene([view], -1, name=name) for name, view in views
] ]
model.screen = screen
model.views = [view for name, view in views]
screen.play( screen.play(
scenes, stop_on_resize=True, start_scene=last_scene, allow_int=True, scenes, stop_on_resize=True, start_scene=last_scene, allow_int=True,
unhandled_input=unhandled) unhandled_input=unhandled)