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 new file mode 100644 index 0000000..600ff1c --- /dev/null +++ b/ceo/tui/positions/GetPositionsView.py @@ -0,0 +1,79 @@ +from threading import Thread + +from asciimatics.widgets import Layout, Label +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): + super().__init__( + screen, height, width, model, 'GetPositions', + escape_on_q=True, + on_load=self._on_load) + + cfg = component.getUtility(IConfig) + avail = cfg.get('positions_available') + + 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: + self._position_widgets[pos] = self._add_pair(position_names[pos], '') + + self.add_flash_message_layout() + 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_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): + 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() + + 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 new file mode 100644 index 0000000..bf23cbb --- /dev/null +++ b/ceo/tui/positions/SetPositionsView.py @@ -0,0 +1,49 @@ +from asciimatics.widgets import Layout, Label, Text +from zope import component + +from ...utils import defer, http_post +from ..CeoFrame import CeoFrame +from .GetPositionsView import position_names +from ceo_common.interfaces import IConfig +from ceod.transactions.members.UpdateMemberPositionsTransaction import UpdateMemberPositionsTransaction + + +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( + back_btn=True, + next_scene='Confirm', on_next=self._next) + self.fix() + + def _next(self): + self.save() + body = {pos: username for pos, username in self.data.items() if username} + + 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:", + '', + *self.data.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..1bc086b 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) @@ -38,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 = [ @@ -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