From 67aa4d005631c555a46e42d9f1bc8bcdd2646577 Mon Sep 17 00:00:00 2001 From: Rio Liu Date: Sat, 25 Sep 2021 14:27:06 -0400 Subject: [PATCH 1/4] positions tui --- ceo/tui/positions/GetPositionsView.py | 65 +++++++++++++++++++++++++++ ceo/tui/positions/SetPositionsView.py | 29 ++++++++++++ ceo/tui/positions/__init__.py | 2 + ceo/tui/start.py | 3 ++ 4 files changed, 99 insertions(+) create mode 100644 ceo/tui/positions/GetPositionsView.py create mode 100644 ceo/tui/positions/SetPositionsView.py create mode 100644 ceo/tui/positions/__init__.py diff --git a/ceo/tui/positions/GetPositionsView.py b/ceo/tui/positions/GetPositionsView.py new file mode 100644 index 0000000..7d15e68 --- /dev/null +++ b/ceo/tui/positions/GetPositionsView.py @@ -0,0 +1,65 @@ +from asciimatics.widgets import Layout, Text, Button +from zope import component + +from ...utils import http_get +from ..CeoFrame import CeoFrame +from ceo_common.interfaces import IConfig + + +position_names = { + 'president': "President", + 'vice-president': "Vice President", + 'treasurer': "Treasurer", + 'secretary': "Secretary", + 'sysadmin': "Sysadmin", + 'cro': "Chief Returning Officer", + 'librarian': "Librarian", + 'imapd': "IMAPd", + 'webmaster': "Web Master", + 'offsck': "Office Manager", +} + + +class GetPositionsView(CeoFrame): + def __init__(self, screen, width, height, model, readonly=True): + super().__init__( + screen, height, width, model, + 'GetPositions', + on_load = self._on_load) + + cfg = component.getUtility(IConfig) + + layout = Layout([100], fill_frame=True) + self.add_layout(layout) + + self._positions = [] + for pos in cfg.get('positions_available'): + widget = Text( + f"{position_names.get(pos, pos)}:", + pos, + readonly=readonly, + ) + self._positions.append(widget) + layout.add_widget(widget) + + self.add_flash_message_layout() + self._add_buttons() + self.fix() + + + def _add_buttons(self): + self.add_buttons( + next_btn_text="Change", + next_scene='SetPositions', + back_btn=True, + ) + + + def _on_load(self): + res = http_get('/api/positions') + if res.status_code != 200: + return + + positions = res.json() + for pos in self._positions: + pos.value = positions.get(pos.name) diff --git a/ceo/tui/positions/SetPositionsView.py b/ceo/tui/positions/SetPositionsView.py new file mode 100644 index 0000000..497edba --- /dev/null +++ b/ceo/tui/positions/SetPositionsView.py @@ -0,0 +1,29 @@ +from ...utils import defer, http_post +from . import GetPositionsView +class SetPositionsView(GetPositionsView): + def __init__(self, screen, widgets, height, model): + super().__init__(screen, widgets, height, model, False) + + + def _add_buttons(self): + self.add_buttons( + next_btn_text="Update", + next_scene='Confirm', + on_next=self._next, + back_btn=True, + ) + + + def _next(self): + positions = {} + for pos in self._positions: + positions[pos.name] = pos.value + + self._model.deferred_req = defer(http_post, f'/api/positions', json=positions) + self._model.confirm_lines = [ + "The positions will be updated as follows", + '', + *positions.items(), + '', + 'Are you sure you want to continue?', + ] diff --git a/ceo/tui/positions/__init__.py b/ceo/tui/positions/__init__.py new file mode 100644 index 0000000..55fa7d1 --- /dev/null +++ b/ceo/tui/positions/__init__.py @@ -0,0 +1,2 @@ +from .GetPositionsView import GetPositionsView +from .SetPositionsView import SetPositionsView diff --git a/ceo/tui/start.py b/ceo/tui/start.py index a7bf5a4..204f0d5 100644 --- a/ceo/tui/start.py +++ b/ceo/tui/start.py @@ -28,6 +28,7 @@ from .members.RenewUserView import RenewUserView from .members.ResetPasswordView import ResetPasswordView from .members.ResetPasswordResultView import ResetPasswordResultView from .members.SetForwardingAddressesView import SetForwardingAddressesView +from .positions import GetPositionsView, SetPositionsView # tuples of (name, view) @@ -66,6 +67,8 @@ def screen_wrapper(screen, last_scene, model): ('CreateDatabaseResult', CreateDatabaseResultView(screen, width, height, model)), ('ResetDatabasePassword', ResetDatabasePasswordView(screen, width, height, model)), ('ResetDatabasePasswordResult', ResetDatabasePasswordResultView(screen, width, height, model)), + ('GetPositions', GetPositionsView(screen, width, height, model)), + ('SetPositions', SetPositionsView(screen, width, height, model)), ] scenes = [ Scene([view], -1, name=name) for name, view in views -- 2.39.2 From 103fdc64a9464ca01a51d7b1fe1b7230e30584fb Mon Sep 17 00:00:00 2001 From: Rio Liu Date: Sat, 25 Sep 2021 14:57:37 -0400 Subject: [PATCH 2/4] Show required positions and make SetPositionsView use transaction streaming --- ceo/tui/positions/GetPositionsView.py | 12 +++++++----- ceo/tui/positions/SetPositionsView.py | 3 +++ 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/ceo/tui/positions/GetPositionsView.py b/ceo/tui/positions/GetPositionsView.py index 7d15e68..21ac830 100644 --- a/ceo/tui/positions/GetPositionsView.py +++ b/ceo/tui/positions/GetPositionsView.py @@ -14,7 +14,7 @@ position_names = { 'sysadmin': "Sysadmin", 'cro': "Chief Returning Officer", 'librarian': "Librarian", - 'imapd': "IMAPd", + 'imapd': "IMAPD", 'webmaster': "Web Master", 'offsck': "Office Manager", } @@ -27,15 +27,17 @@ class GetPositionsView(CeoFrame): 'GetPositions', on_load = self._on_load) - cfg = component.getUtility(IConfig) - layout = Layout([100], fill_frame=True) self.add_layout(layout) + cfg = component.getUtility(IConfig) + avail = cfg.get('positions_available') + required = cfg.get('positions_required') + self._positions = [] - for pos in cfg.get('positions_available'): + for pos in avail: widget = Text( - f"{position_names.get(pos, pos)}:", + f"{'*' if pos in required else ' '}{position_names.get(pos, pos)}:", pos, readonly=readonly, ) diff --git a/ceo/tui/positions/SetPositionsView.py b/ceo/tui/positions/SetPositionsView.py index 497edba..4bdfe9a 100644 --- a/ceo/tui/positions/SetPositionsView.py +++ b/ceo/tui/positions/SetPositionsView.py @@ -1,5 +1,7 @@ from ...utils import defer, http_post from . import GetPositionsView +from ceod.transactions.members.UpdateMemberPositionsTransaction import UpdateMemberPositionsTransaction as PositionsTransactions + class SetPositionsView(GetPositionsView): def __init__(self, screen, widgets, height, model): super().__init__(screen, widgets, height, model, False) @@ -20,6 +22,7 @@ class SetPositionsView(GetPositionsView): positions[pos.name] = pos.value self._model.deferred_req = defer(http_post, f'/api/positions', json=positions) + self._model.operations = PositionsTransactions.operations self._model.confirm_lines = [ "The positions will be updated as follows", '', -- 2.39.2 From faa5c73145419699a8009b494a62e0de0aacc340 Mon Sep 17 00:00:00 2001 From: Max Erenberg <> Date: Sat, 25 Sep 2021 23:49:10 -0400 Subject: [PATCH 3/4] add positions get/set widgets --- ceo/tui/CeoFrame.py | 21 +++++--- ceo/tui/Model.py | 14 ++++- ceo/tui/TransactionView.py | 4 +- ceo/tui/positions/GetPositionsView.py | 74 ++++++++++++++++----------- ceo/tui/positions/SetPositionsView.py | 57 ++++++++++++++------- ceo/tui/start.py | 4 +- 6 files changed, 112 insertions(+), 62 deletions(-) diff --git a/ceo/tui/CeoFrame.py b/ceo/tui/CeoFrame.py index 87c1609..5115e08 100644 --- a/ceo/tui/CeoFrame.py +++ b/ceo/tui/CeoFrame.py @@ -32,7 +32,6 @@ class CeoFrame(Frame): self._extra_on_load = on_load self._model = model self._name = name - self._loaded = False self._has_dynamic_layouts = has_dynamic_layouts self._quit_keys = [Screen.KEY_ESCAPE] if escape_on_q: @@ -40,13 +39,19 @@ class CeoFrame(Frame): # sanity check if save_data: assert name in model.viewdata + # child classes may override this as a last resort + self.do_not_reload = False def _ceoframe_on_load(self): - # We usually don't want _on_load() to be called multiple times - # e.g. when switching back to a scene, or after calling reset() - if self._loaded: + if self.do_not_reload: + self.do_not_reload = False return - self._loaded = True + self.do_not_reload = True + if self._has_dynamic_layouts: + # We arrive here after a user pressed 'Back' then 'Next'. + # The data may have changed, so we need to redraw everything, + # via self._extra_on_load(). + self.clear_layouts() if self._model.title is not None: self.title = self._model.title self._model.title = None @@ -76,7 +81,7 @@ class CeoFrame(Frame): """ # 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 + self.do_not_reload = False if self._has_dynamic_layouts: # We don't want layouts to accumulate. self.clear_layouts() @@ -157,8 +162,8 @@ class CeoFrame(Frame): self._model.screen.force_update() self._model.screen.draw_next_frame() - def clear_flash_message(self): - self.flash_message('') + def clear_flash_message(self, force_update: bool = False): + self.flash_message('', force_update) def process_event(self, event): if not isinstance(event, KeyboardEvent): diff --git a/ceo/tui/Model.py b/ceo/tui/Model.py index 08c9b38..989a5bb 100644 --- a/ceo/tui/Model.py +++ b/ceo/tui/Model.py @@ -1,10 +1,16 @@ from copy import deepcopy +from zope import component + +from ceo_common.interfaces import IConfig + class Model: """A convenient place to store View data persistently.""" def __init__(self): + cfg = component.getUtility(IConfig) + self.screen = None self.views = [] self.title = None @@ -12,7 +18,9 @@ class Model: self.result_view_name = None self.txn_view_name = None self.error_message = None - # view-specific data, to be used when e.g. resizing the window + # View-specific data, to be used when e.g. resizing the window. + # For the views where save_data=True was passed to the CeoFrame + # constructor, the keys correspond to the names of text fields. self._initial_viewdata = { 'AddUser': { 'uid': '', @@ -68,7 +76,11 @@ class Model: 'ResetDatabasePassword': { 'uid': '', }, + # This one needs to be filled in dynamically + 'SetPositions': {}, } + for pos in cfg.get('positions_available'): + self._initial_viewdata[pos] = '' self.viewdata = deepcopy(self._initial_viewdata) # data which is shared between multiple views self.is_club_rep = False diff --git a/ceo/tui/TransactionView.py b/ceo/tui/TransactionView.py index 8aa3c45..b2413c7 100644 --- a/ceo/tui/TransactionView.py +++ b/ceo/tui/TransactionView.py @@ -97,7 +97,9 @@ class TransactionView(CeoFrame): 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 + # enabled it. + # We don't want to reload, though (which reset() will trigger). + self.do_not_reload = True self.reset() # save the fact that the transaction is completed self._model.viewdata['Transaction']['status'] = 'completed' diff --git a/ceo/tui/positions/GetPositionsView.py b/ceo/tui/positions/GetPositionsView.py index 21ac830..600ff1c 100644 --- a/ceo/tui/positions/GetPositionsView.py +++ b/ceo/tui/positions/GetPositionsView.py @@ -1,4 +1,6 @@ -from asciimatics.widgets import Layout, Text, Button +from threading import Thread + +from asciimatics.widgets import Layout, Label from zope import component from ...utils import http_get @@ -21,47 +23,57 @@ position_names = { class GetPositionsView(CeoFrame): - def __init__(self, screen, width, height, model, readonly=True): + def __init__(self, screen, width, height, model): super().__init__( - screen, height, width, model, - 'GetPositions', - on_load = self._on_load) - - layout = Layout([100], fill_frame=True) - self.add_layout(layout) + screen, height, width, model, 'GetPositions', + escape_on_q=True, + on_load=self._on_load) cfg = component.getUtility(IConfig) avail = cfg.get('positions_available') - required = cfg.get('positions_required') - self._positions = [] + layout = Layout([100]) + self.add_layout(layout) + layout.add_widget(Label('')) + + self._main_layout = Layout([10, 1, 10], fill_frame=True) + self.add_layout(self._main_layout) + + self._position_widgets = {} for pos in avail: - widget = Text( - f"{'*' if pos in required else ' '}{position_names.get(pos, pos)}:", - pos, - readonly=readonly, - ) - self._positions.append(widget) - layout.add_widget(widget) + self._position_widgets[pos] = self._add_pair(position_names[pos], '') self.add_flash_message_layout() - self._add_buttons() + self.add_buttons(back_btn=True) self.fix() + def _add_blank_line(self): + self._main_layout.add_widget(Label(' ', 0)) + self._main_layout.add_widget(Label(' ', 2)) - def _add_buttons(self): - self.add_buttons( - next_btn_text="Change", - next_scene='SetPositions', - back_btn=True, - ) - + def _add_pair(self, key: str, val: str): + key_widget = Label(key + ':', align='>') + value_widget = Label(val, align='<') + self._main_layout.add_widget(key_widget, 0) + self._main_layout.add_widget(value_widget, 2) + return value_widget def _on_load(self): - res = http_get('/api/positions') - if res.status_code != 200: - return + def target(): + self.flash_message('Looking up positions...') + try: + resp = http_get('/api/positions') + if not resp.ok: + return + positions = resp.json() + for pos, username in positions.items(): + self._position_widgets[pos].text = username + finally: + self.clear_flash_message(force_update=True) + Thread(target=target).start() - positions = res.json() - for pos in self._positions: - pos.value = positions.get(pos.name) + def _ceoframe_on_reset(self): + super()._ceoframe_on_reset() + # clear the labels + for widget in self._position_widgets.values(): + widget.text = '' diff --git a/ceo/tui/positions/SetPositionsView.py b/ceo/tui/positions/SetPositionsView.py index 4bdfe9a..57bb9da 100644 --- a/ceo/tui/positions/SetPositionsView.py +++ b/ceo/tui/positions/SetPositionsView.py @@ -1,32 +1,51 @@ +from threading import Thread + +from asciimatics.widgets import Layout, Label, Text +from zope import component + from ...utils import defer, http_post -from . import GetPositionsView -from ceod.transactions.members.UpdateMemberPositionsTransaction import UpdateMemberPositionsTransaction as PositionsTransactions - -class SetPositionsView(GetPositionsView): - def __init__(self, screen, widgets, height, model): - super().__init__(screen, widgets, height, model, False) +from ..CeoFrame import CeoFrame +from .GetPositionsView import position_names +from ceo_common.interfaces import IConfig +from ceod.transactions.members.UpdateMemberPositionsTransaction import UpdateMemberPositionsTransaction - def _add_buttons(self): +class SetPositionsView(CeoFrame): + def __init__(self, screen, width, height, model): + super().__init__( + screen, height, width, model, 'SetPositions', + save_data=True) + cfg = component.getUtility(IConfig) + avail = cfg.get('positions_available') + required = cfg.get('positions_required') + + layout = Layout([100], fill_frame=True) + self.add_layout(layout) + for pos in avail: + suffix = ' (*)' if pos in required else '' + widget = Text(position_names[pos] + suffix, pos) + layout.add_widget(widget) + + layout = Layout([100]) + self.add_layout(layout) + layout.add_widget(Label('(*) Required')) + + self.add_flash_message_layout() self.add_buttons( - next_btn_text="Update", - next_scene='Confirm', - on_next=self._next, back_btn=True, - ) - + next_scene='Confirm', on_next=self._next) + self.fix() def _next(self): - positions = {} - for pos in self._positions: - positions[pos.name] = pos.value + self.save() + body = {pos: username for pos, username in self.data.items() if username} - self._model.deferred_req = defer(http_post, f'/api/positions', json=positions) - self._model.operations = PositionsTransactions.operations + self._model.deferred_req = defer(http_post, f'/api/positions', json=body) + self._model.operations = UpdateMemberPositionsTransaction.operations self._model.confirm_lines = [ - "The positions will be updated as follows", + "The positions will be updated as follows:", '', - *positions.items(), + *self.data.items(), '', 'Are you sure you want to continue?', ] diff --git a/ceo/tui/start.py b/ceo/tui/start.py index 204f0d5..1bc086b 100644 --- a/ceo/tui/start.py +++ b/ceo/tui/start.py @@ -39,8 +39,8 @@ def screen_wrapper(screen, last_scene, model): global views # unload the old views for name, view in views: - if hasattr(view, '_on_ceoframe_unload'): - view._on_ceoframe_unload() + if hasattr(view, '_ceoframe_on_unload'): + view._ceoframe_on_unload() width = min(screen.width, 90) height = min(screen.height, 24) views = [ -- 2.39.2 From a6b32c1b0bb6d7c50fc06b2df532864a3c54e544 Mon Sep 17 00:00:00 2001 From: Max Erenberg <> Date: Sun, 26 Sep 2021 14:56:09 -0400 Subject: [PATCH 4/4] fix lint errors --- ceo/tui/positions/SetPositionsView.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/ceo/tui/positions/SetPositionsView.py b/ceo/tui/positions/SetPositionsView.py index 57bb9da..bf23cbb 100644 --- a/ceo/tui/positions/SetPositionsView.py +++ b/ceo/tui/positions/SetPositionsView.py @@ -1,5 +1,3 @@ -from threading import Thread - from asciimatics.widgets import Layout, Label, Text from zope import component @@ -40,7 +38,7 @@ class SetPositionsView(CeoFrame): self.save() body = {pos: username for pos, username in self.data.items() if username} - self._model.deferred_req = defer(http_post, f'/api/positions', json=body) + self._model.deferred_req = defer(http_post, '/api/positions', json=body) self._model.operations = UpdateMemberPositionsTransaction.operations self._model.confirm_lines = [ "The positions will be updated as follows:", -- 2.39.2