From d3c98e418a7c28826f6ca5328ffd6ad7ff8ad0ec Mon Sep 17 00:00:00 2001 From: Max Erenberg Date: Tue, 7 Sep 2021 02:29:53 +0000 Subject: [PATCH] implement GetUser in TUI --- ceo/tui/CeoFrame.py | 33 ++++++++++++++-- ceo/tui/ConfirmView.py | 3 +- ceo/tui/ErrorView.py | 1 + ceo/tui/Model.py | 12 ++++-- ceo/tui/ResultView.py | 59 +++++++++++++++++----------- ceo/tui/TransactionView.py | 1 + ceo/tui/members/GetUserResultView.py | 11 ++++++ ceo/tui/members/GetUserView.py | 29 ++++++++++++++ ceo/tui/members/RenewUserView.py | 2 +- ceo/tui/start.py | 11 ++++-- 10 files changed, 128 insertions(+), 34 deletions(-) create mode 100644 ceo/tui/members/GetUserResultView.py create mode 100644 ceo/tui/members/GetUserView.py diff --git a/ceo/tui/CeoFrame.py b/ceo/tui/CeoFrame.py index 39f357d..797c25c 100644 --- a/ceo/tui/CeoFrame.py +++ b/ceo/tui/CeoFrame.py @@ -13,6 +13,7 @@ class CeoFrame(Frame): on_load=None, title=None, save_data=False, # whether to save widget state for resizing + has_dynamic_layouts=False, # whether layouts are created on load ): super().__init__( screen, @@ -28,6 +29,7 @@ class CeoFrame(Frame): self._model = model self._name = name self._loaded = False + self._has_dynamic_layouts = has_dynamic_layouts def _ceoframe_on_load(self): # 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: 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: return - # save the input fields' values so that they don't disappear when - # the window gets resized self.save() 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( self, back_btn=False, back_btn_text='Back', next_scene=None, next_btn_text='Next', on_next=None, diff --git a/ceo/tui/ConfirmView.py b/ceo/tui/ConfirmView.py index 34646cd..4dcfec0 100644 --- a/ceo/tui/ConfirmView.py +++ b/ceo/tui/ConfirmView.py @@ -8,6 +8,7 @@ class ConfirmView(CeoFrame): super().__init__( screen, height, width, model, 'Confirm', on_load=self._confirmview_on_load, title='Confirmation', + has_dynamic_layouts=True, ) def _add_line(self, text: str = ''): @@ -48,7 +49,7 @@ class ConfirmView(CeoFrame): def _next(self): self.flash_message('Sending request...', force_update=True) try: - self._model.deferred_req_resp = self._model.deferred_req() + self._model.resp = self._model.deferred_req() finally: self.clear_flash_message() self.go_to_next_scene('Result') diff --git a/ceo/tui/ErrorView.py b/ceo/tui/ErrorView.py index 2615ed5..4fff146 100644 --- a/ceo/tui/ErrorView.py +++ b/ceo/tui/ErrorView.py @@ -9,6 +9,7 @@ class ErrorView(CeoFrame): super().__init__( screen, height, width, model, 'Error', on_load=self._errorview_on_load, title='Error', + has_dynamic_layouts=True, ) def _errorview_on_load(self): diff --git a/ceo/tui/Model.py b/ceo/tui/Model.py index aaf0bd3..b9b2a1b 100644 --- a/ceo/tui/Model.py +++ b/ceo/tui/Model.py @@ -6,6 +6,7 @@ class Model: def __init__(self): self.screen = None + self.views = [] self.title = None self.scene_stack = [] self.error_message = None @@ -28,7 +29,9 @@ class Model: 'labels': {}, 'status': 'not started', }, - 'Result': {}, + 'GetUser': { + 'uid': '', + }, } self.viewdata = deepcopy(self._initial_viewdata) # data which is shared between multiple views @@ -36,7 +39,7 @@ class Model: self.confirm_lines = None self.operations = None self.deferred_req = None - self.deferred_req_resp = None + self.resp = None def reset(self): self.viewdata = deepcopy(self._initial_viewdata) @@ -44,7 +47,10 @@ class Model: self.confirm_lines = None self.operations = None self.deferred_req = None - self.deferred_req_resp = None + self.resp = None self.title = None self.error_message = None self.scene_stack.clear() + for view in self.views: + if hasattr(view, '_ceoframe_on_reset'): + view._ceoframe_on_reset() diff --git a/ceo/tui/ResultView.py b/ceo/tui/ResultView.py index 24bbaeb..e118a56 100644 --- a/ceo/tui/ResultView.py +++ b/ceo/tui/ResultView.py @@ -10,33 +10,46 @@ class ResultView(CeoFrame): super().__init__( screen, height, width, model, '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.fix() def _next(self): self._model.reset() 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() diff --git a/ceo/tui/TransactionView.py b/ceo/tui/TransactionView.py index ed7809d..e8a959b 100644 --- a/ceo/tui/TransactionView.py +++ b/ceo/tui/TransactionView.py @@ -14,6 +14,7 @@ class TransactionView(CeoFrame): super().__init__( screen, height, width, model, 'Transaction', on_load=self._txnview_on_load, title='Running Transaction', + has_dynamic_layouts=True, ) self._model = model # map operation names to label widgets diff --git a/ceo/tui/members/GetUserResultView.py b/ceo/tui/members/GetUserResultView.py new file mode 100644 index 0000000..755e9fb --- /dev/null +++ b/ceo/tui/members/GetUserResultView.py @@ -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) diff --git a/ceo/tui/members/GetUserView.py b/ceo/tui/members/GetUserView.py new file mode 100644 index 0000000..2b520dc --- /dev/null +++ b/ceo/tui/members/GetUserView.py @@ -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() diff --git a/ceo/tui/members/RenewUserView.py b/ceo/tui/members/RenewUserView.py index 95c0775..86e722a 100644 --- a/ceo/tui/members/RenewUserView.py +++ b/ceo/tui/members/RenewUserView.py @@ -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 ...utils import http_post, defer diff --git a/ceo/tui/start.py b/ceo/tui/start.py index 72b5029..170342d 100644 --- a/ceo/tui/start.py +++ b/ceo/tui/start.py @@ -12,6 +12,8 @@ from .ResultView import ResultView from .TransactionView import TransactionView from .WelcomeView import WelcomeView from .members.AddUserView import AddUserView +from .members.GetUserView import GetUserView +from .members.GetUserResultView import GetUserResultView from .members.RenewUserView import RenewUserView @@ -29,11 +31,10 @@ 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() + if hasattr(view, '_on_ceoframe_unload'): + view._on_ceoframe_unload() width = min(screen.width, 90) height = min(screen.height, 24) views = [ @@ -44,10 +45,14 @@ def screen_wrapper(screen, last_scene, model): ('Error', ErrorView(screen, width, height, model)), ('AddUser', AddUserView(screen, width, height, model)), ('RenewUser', RenewUserView(screen, width, height, model)), + ('GetUser', GetUserView(screen, width, height, model)), + ('GetUserResult', GetUserResultView(screen, width, height, model)), ] scenes = [ Scene([view], -1, name=name) for name, view in views ] + model.screen = screen + model.views = [view for name, view in views] screen.play( scenes, stop_on_resize=True, start_scene=last_scene, allow_int=True, unhandled_input=unhandled)