Positions TUI #20

Merged
merenber merged 4 commits from positions-tui into v1 2021-09-26 15:23:49 -04:00
7 changed files with 164 additions and 12 deletions

View File

@ -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):

View File

@ -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

View File

@ -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'

View File

@ -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 = ''

View File

@ -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?',
]

View File

@ -0,0 +1,2 @@
from .GetPositionsView import GetPositionsView
from .SetPositionsView import SetPositionsView

View File

@ -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