no-resizing (#21)
continuous-integration/drone/push Build is passing Details

This PR disables resizing the TUI.
Unfortunately this is a regression from the old ceo. But trying to preserve state when destroying and creating new views in asciimatics proved to be very difficult.

Co-authored-by: Max Erenberg <>
Reviewed-on: #21
Co-authored-by: Max Erenberg <merenber@csclub.uwaterloo.ca>
Co-committed-by: Max Erenberg <merenber@csclub.uwaterloo.ca>
This commit is contained in:
Max Erenberg 2021-10-04 20:04:05 -04:00
parent 7edc01e42b
commit 3e1131c4e4
24 changed files with 141 additions and 385 deletions

View File

@ -125,7 +125,7 @@ cn: regular1
gidNumber: 20002 gidNumber: 20002
dn: uid=exec1,ou=People,dc=csclub,dc=internal dn: uid=exec1,ou=People,dc=csclub,dc=internal
cn: Regular One cn: Exec One
userPassword: {SASL}exec1@CSCLUB.INTERNAL userPassword: {SASL}exec1@CSCLUB.INTERNAL
loginShell: /bin/bash loginShell: /bin/bash
homeDirectory: /users/exec1 homeDirectory: /users/exec1

View File

@ -47,201 +47,6 @@ objectClass: top
uid: regular2 uid: regular2
mail: regular2@uwaterloo.internal mail: regular2@uwaterloo.internal
dn: uid=exec1,ou=UWLDAP,dc=csclub,dc=internal
displayName: Exec One
givenName: Exec
sn: One
cn: Exec One
ou: MAT/Mathematics Computer Science
mailLocalAddress: exec1@uwaterloo.internal
objectClass: inetLocalMailRecipient
objectClass: inetOrgPerson
objectClass: organizationalPerson
objectClass: person
objectClass: top
uid: exec1
mail: exec1@uwaterloo.internal
dn: uid=exec2,ou=UWLDAP,dc=csclub,dc=internal
displayName: Exec Two
givenName: Exec
sn: Two
cn: Exec Two
ou: MAT/Mathematics Computer Science
mailLocalAddress: exec2@uwaterloo.internal
objectClass: inetLocalMailRecipient
objectClass: inetOrgPerson
objectClass: organizationalPerson
objectClass: person
objectClass: top
uid: exec2
mail: exec2@uwaterloo.internal
dn: uid=ctdalek,ou=UWLDAP,dc=csclub,dc=internal
displayName: Calum Dalek
givenName: Calum
sn: Dalek
cn: Calum Dalek
ou: MAT/Mathematics Computer Science
mailLocalAddress: ctdalek@uwaterloo.internal
objectClass: inetLocalMailRecipient
objectClass: inetOrgPerson
objectClass: organizationalPerson
objectClass: person
objectClass: top
uid: ctdalek
mail: ctdalek@uwaterloo.internal
dn: uid=regular1,ou=UWLDAP,dc=csclub,dc=internal
displayName: Regular One
givenName: Regular
sn: One
cn: Regular One
ou: MAT/Mathematics Computer Science
mailLocalAddress: regular1@uwaterloo.internal
objectClass: inetLocalMailRecipient
objectClass: inetOrgPerson
objectClass: organizationalPerson
objectClass: person
objectClass: top
uid: regular1
mail: regular1@uwaterloo.internal
dn: uid=regular2,ou=UWLDAP,dc=csclub,dc=internal
displayName: Regular Two
givenName: Regular
sn: Two
cn: Regular Two
ou: MAT/Mathematics Computer Science
mailLocalAddress: regular2@uwaterloo.internal
objectClass: inetLocalMailRecipient
objectClass: inetOrgPerson
objectClass: organizationalPerson
objectClass: person
objectClass: top
uid: regular2
mail: regular2@uwaterloo.internal
dn: uid=exec1,ou=UWLDAP,dc=csclub,dc=internal
displayName: Exec One
givenName: Exec
sn: One
cn: Exec One
ou: MAT/Mathematics Computer Science
mailLocalAddress: exec1@uwaterloo.internal
objectClass: inetLocalMailRecipient
objectClass: inetOrgPerson
objectClass: organizationalPerson
objectClass: person
objectClass: top
uid: exec1
mail: exec1@uwaterloo.internal
dn: uid=ctdalek,ou=UWLDAP,dc=csclub,dc=internal
displayName: Calum Dalek
givenName: Calum
sn: Dalek
cn: Calum Dalek
ou: MAT/Mathematics Computer Science
mailLocalAddress: ctdalek@uwaterloo.internal
objectClass: inetLocalMailRecipient
objectClass: inetOrgPerson
objectClass: organizationalPerson
objectClass: person
objectClass: top
uid: ctdalek
mail: ctdalek@uwaterloo.internal
dn: uid=regular1,ou=UWLDAP,dc=csclub,dc=internal
displayName: Regular One
givenName: Regular
sn: One
cn: Regular One
ou: MAT/Mathematics Computer Science
mailLocalAddress: regular1@uwaterloo.internal
objectClass: inetLocalMailRecipient
objectClass: inetOrgPerson
objectClass: organizationalPerson
objectClass: person
objectClass: top
uid: regular1
mail: regular1@uwaterloo.internal
dn: uid=regular2,ou=UWLDAP,dc=csclub,dc=internal
displayName: Regular Two
givenName: Regular
sn: Two
cn: Regular Two
ou: MAT/Mathematics Computer Science
mailLocalAddress: regular2@uwaterloo.internal
objectClass: inetLocalMailRecipient
objectClass: inetOrgPerson
objectClass: organizationalPerson
objectClass: person
objectClass: top
uid: regular2
mail: regular2@uwaterloo.internal
dn: uid=exec1,ou=UWLDAP,dc=csclub,dc=internal
displayName: Exec One
givenName: Exec
sn: One
cn: Exec One
ou: MAT/Mathematics Computer Science
mailLocalAddress: exec1@uwaterloo.internal
objectClass: inetLocalMailRecipient
objectClass: inetOrgPerson
objectClass: organizationalPerson
objectClass: person
objectClass: top
uid: exec1
mail: exec1@uwaterloo.internal
dn: uid=ctdalek,ou=UWLDAP,dc=csclub,dc=internal
displayName: Calum Dalek
givenName: Calum
sn: Dalek
cn: Calum Dalek
ou: MAT/Mathematics Computer Science
mailLocalAddress: ctdalek@uwaterloo.internal
objectClass: inetLocalMailRecipient
objectClass: inetOrgPerson
objectClass: organizationalPerson
objectClass: person
objectClass: top
uid: ctdalek
mail: ctdalek@uwaterloo.internal
dn: uid=regular1,ou=UWLDAP,dc=csclub,dc=internal
displayName: Regular One
givenName: Regular
sn: One
cn: Regular One
ou: MAT/Mathematics Computer Science
mailLocalAddress: regular1@uwaterloo.internal
objectClass: inetLocalMailRecipient
objectClass: inetOrgPerson
objectClass: organizationalPerson
objectClass: person
objectClass: top
uid: regular1
mail: regular1@uwaterloo.internal
dn: uid=regular2,ou=UWLDAP,dc=csclub,dc=internal
displayName: Regular Two
givenName: Regular
sn: Two
cn: Regular Two
ou: MAT/Mathematics Computer Science
mailLocalAddress: regular2@uwaterloo.internal
objectClass: inetLocalMailRecipient
objectClass: inetOrgPerson
objectClass: organizationalPerson
objectClass: person
objectClass: top
uid: regular2
mail: regular2@uwaterloo.internal
dn: uid=regular3,ou=UWLDAP,dc=csclub,dc=internal dn: uid=regular3,ou=UWLDAP,dc=csclub,dc=internal
displayName: Regular Three displayName: Regular Three
givenName: Regular givenName: Regular

View File

@ -12,11 +12,9 @@ class CeoFrame(Frame):
height, height,
width, width,
model, model,
name, # key in model.viewdata name,
on_load=None, on_load=None,
title=None, title=None,
save_data=False, # whether to save widget state for resizing
has_dynamic_layouts=False, # whether layouts are created on load
escape_on_q=False, # whether to quit when 'q' is pressed escape_on_q=False, # whether to quit when 'q' is pressed
): ):
super().__init__( super().__init__(
@ -28,70 +26,43 @@ class CeoFrame(Frame):
title=title, title=title,
on_load=self._ceoframe_on_load, on_load=self._ceoframe_on_load,
) )
self._save_data = save_data self._custom_on_load = on_load
self._extra_on_load = on_load
self._model = model self._model = model
self._name = name self._name = name
self._has_dynamic_layouts = has_dynamic_layouts # If a view has a custom on_load function, all layouts should
# be created on load (*not* in the constructor)
self._has_dynamic_layouts = on_load is not None
self._quit_keys = [Screen.KEY_ESCAPE] self._quit_keys = [Screen.KEY_ESCAPE]
if escape_on_q: if escape_on_q:
self._quit_keys.append(ord('q')) self._quit_keys.append(ord('q'))
# sanity check
if save_data:
assert name in model.viewdata
# child classes may override this as a last resort # child classes may override this as a last resort
self.do_not_reload = False self.skip_reload = False
def _ceoframe_on_load(self): def _ceoframe_on_load(self):
if self.do_not_reload: if self.skip_reload:
self.do_not_reload = False self.skip_reload = False
return return
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: if self._model.title is not None:
self.title = self._model.title self.title = self._model.title
self._model.title = None self._model.title = None
if self._save_data: if self._has_dynamic_layouts and self._model.nav_direction == 'forward':
# restore the saved input fields' values # We arrive here after a user pressed 'Back' then 'Next',
self.data = self._model.viewdata[self._name] # or after we returned to the Welcome screen.
if self._extra_on_load is not None: # The data may have changed, so we need to redraw everything,
self._extra_on_load() # via self._custom_on_load().
self.clear_layouts()
def _ceoframe_on_unload(self): self._custom_on_load()
"""
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
self.save()
self._model.viewdata[self._name] = self.data
# may be overridden by child classes
def _ceoframe_on_reset(self): def _ceoframe_on_reset(self):
""" """
This needs to be called whenever we return to the home screen This is called whenever we return to the home screen
after some kind of operation was completed. after some kind of operation was completed.
Currently this is called from Model.reset(). This is called from Model.reset().
""" """
# We want a fresh slate once we return to the home screen, so we pass
# want on_load() to be called for the scenes.
self.do_not_reload = False
if self._has_dynamic_layouts:
# We don't want layouts to accumulate.
self.clear_layouts()
def clear_layouts(self): 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() self._layouts.clear()
def force_update(self): def force_update(self):
@ -126,12 +97,14 @@ class CeoFrame(Frame):
layout.add_widget(Divider()) layout.add_widget(Divider())
def _back(): def _back():
self._model.nav_direction = 'backward'
last_scene = self._model.scene_stack.pop() last_scene = self._model.scene_stack.pop()
if last_scene == 'Welcome': if last_scene == 'Welcome':
self._model.reset() self._model.reset()
raise NextScene(last_scene) raise NextScene(last_scene)
def _next(): def _next():
self._model.nav_direction = 'forward'
if on_next_excl is not None: if on_next_excl is not None:
on_next_excl() on_next_excl()
return return

View File

@ -8,7 +8,6 @@ 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,
escape_on_q=True, escape_on_q=True,
) )
@ -46,6 +45,11 @@ class ConfirmView(CeoFrame):
kwargs['on_next_excl'] = self._next kwargs['on_next_excl'] = self._next
self.add_buttons(**kwargs) self.add_buttons(**kwargs)
self.fix() self.fix()
# OK so there's some weird bug somewhere which causes the buttons to be unselectable
# if we add a new user, return to the Welcome screen, then try to renew a user.
# This is a workaround for that.
self.skip_reload = True
self.reset()
def _next(self): def _next(self):
self.flash_message('Sending request...', force_update=True) self.flash_message('Sending request...', force_update=True)

View File

@ -9,7 +9,6 @@ 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

@ -18,66 +18,12 @@ class Model:
self.result_view_name = None self.result_view_name = None
self.txn_view_name = None self.txn_view_name = None
self.error_message = None self.error_message = None
# View-specific data, to be used when e.g. resizing the window. self.nav_direction = 'forward'
# For the views where save_data=True was passed to the CeoFrame # View-specific data
# constructor, the keys correspond to the names of text fields.
self._initial_viewdata = { self._initial_viewdata = {
'AddUser': {
'uid': '',
'cn': '',
'program': '',
'forwarding_address': '',
'num_terms': '1',
},
'RenewUser': {
'uid': '',
'num_terms': '1',
},
'Transaction': {
'op_layout': None,
'msg_layout': None,
'labels': {},
'status': 'not started',
},
'GetUser': {
'uid': '',
},
'ResetPassword': { 'ResetPassword': {
'uid': '', 'uid': '',
}, },
'ChangeLoginShell': {
'uid': '',
'login_shell': '',
},
'SetForwardingAddresses': {
'uid': '',
'forwarding_addresses': [''],
},
'AddGroup': {
'cn': '',
'description': '',
},
'GetGroup': {
'cn': '',
},
'AddMemberToGroup': {
'cn': '',
'uid': '',
'subscribe': True,
},
'RemoveMemberFromGroup': {
'cn': '',
'uid': '',
'unsubscribe': True,
},
'CreateDatabase': {
'uid': '',
},
'ResetDatabasePassword': {
'uid': '',
},
# This one needs to be filled in dynamically
'SetPositions': {},
} }
for pos in cfg.get('positions_available'): for pos in cfg.get('positions_available'):
self._initial_viewdata[pos] = '' self._initial_viewdata[pos] = ''

View File

@ -10,7 +10,6 @@ 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,
escape_on_q=True, escape_on_q=True,
) )

View File

@ -15,10 +15,9 @@ 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,
) )
# map operation names to label widgets # map operation names to label widgets
self._labels = model.viewdata['Transaction']['labels'] self._labels = {}
def _add_buttons(self): def _add_buttons(self):
layout = Layout([100]) layout = Layout([100])
@ -28,10 +27,6 @@ class TransactionView(CeoFrame):
layout = Layout([1, 1]) layout = Layout([1, 1])
self.add_layout(layout) self.add_layout(layout)
self._next_btn = Button('Next', self._next) self._next_btn = Button('Next', self._next)
# we don't want to disable the button if the txn completed
# and the user just resized the window
if self._model.viewdata['Transaction']['status'] != 'completed':
self._next_btn.disabled = True
layout.add_widget(self._next_btn, 1) layout.add_widget(self._next_btn, 1)
def _add_blank_line(self): def _add_blank_line(self):
@ -39,47 +34,30 @@ class TransactionView(CeoFrame):
self._op_layout.add_widget(Label(''), 2) self._op_layout.add_widget(Label(''), 2)
def _txnview_on_load(self): def _txnview_on_load(self):
d = self._model.viewdata['Transaction'] self._op_layout = Layout([12, 1, 10])
if d['op_layout'] is None: self.add_layout(self._op_layout)
first_time = True # store the layouts so that we can re-use them when the screen
self._op_layout = Layout([12, 1, 10]) # gets resized
self.add_layout(self._op_layout) for _ in range(2):
# store the layouts so that we can re-use them when the screen
# gets resized
d['op_layout'] = self._op_layout
for _ in range(2):
self._add_blank_line()
for operation in self._model.operations:
desc = op_desc[operation]
self._op_layout.add_widget(Label(desc + '...', align='>'), 0)
desc_label = Label('', align='<')
self._op_layout.add_widget(desc_label, 2)
self._labels[operation] = desc_label
self._add_blank_line() self._add_blank_line()
# this is the where success/failure messages etc. get placed for operation in self._model.operations:
self._msg_layout = Layout([100]) desc = op_desc[operation]
self.add_layout(self._msg_layout) self._op_layout.add_widget(Label(desc + '...', align='>'), 0)
d['msg_layout'] = self._msg_layout desc_label = Label('', align='<')
else: self._op_layout.add_widget(desc_label, 2)
# we arrive here when the screen has been resized self._labels[operation] = desc_label
first_time = False self._add_blank_line()
# restore the layouts which we saved # this is the where success/failure messages etc. get placed
self._op_layout = d['op_layout'] self._msg_layout = Layout([100])
self.add_layout(self._op_layout) self.add_layout(self._msg_layout)
self._msg_layout = d['msg_layout']
self.add_layout(self._msg_layout)
# fill up the rest of the space # fill up the rest of the space
self.add_layout(Layout([100], fill_frame=True)) self.add_layout(Layout([100], fill_frame=True))
self._add_buttons() self._add_buttons()
self.fix() self.fix()
# only send the API request the first time we arrive at this Thread(target=self._do_txn).start()
# scene, not when the screen gets resized
if first_time:
Thread(target=self._do_txn).start()
def _do_txn(self): def _do_txn(self):
self._model.viewdata['Transaction']['status'] = 'in progress'
resp = self._model.deferred_req() resp = self._model.deferred_req()
handler = TUIStreamResponseHandler( handler = TUIStreamResponseHandler(
model=self._model, model=self._model,
@ -99,10 +77,8 @@ class TransactionView(CeoFrame):
# If we don't reset, the button isn't selectable, even though we # 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). # We don't want to reload, though (which reset() will trigger).
self.do_not_reload = True self.skip_reload = True
self.reset() self.reset()
# save the fact that the transaction is completed
self._model.viewdata['Transaction']['status'] = 'completed'
def _next(self): def _next(self):
self._model.reset() self._model.reset()

View File

@ -8,7 +8,6 @@ class CreateDatabaseView(CeoFrame):
def __init__(self, screen, width, height, model): def __init__(self, screen, width, height, model):
super().__init__( super().__init__(
screen, height, width, model, 'CreateDatabase', screen, height, width, model, 'CreateDatabase',
save_data=True,
) )
layout = Layout([100], fill_frame=True) layout = Layout([100], fill_frame=True)
self.add_layout(layout) self.add_layout(layout)
@ -19,6 +18,10 @@ class CreateDatabaseView(CeoFrame):
on_next=self._next) on_next=self._next)
self.fix() self.fix()
def _ceoframe_on_reset(self):
super()._ceoframe_on_reset()
self._username.value = None
def _target(self): def _target(self):
username = self._username.value username = self._username.value
db_type = self._model.db_type db_type = self._model.db_type

View File

@ -8,7 +8,6 @@ class ResetDatabasePasswordView(CeoFrame):
def __init__(self, screen, width, height, model): def __init__(self, screen, width, height, model):
super().__init__( super().__init__(
screen, height, width, model, 'ResetDatabasePassword', screen, height, width, model, 'ResetDatabasePassword',
save_data=True,
) )
layout = Layout([100], fill_frame=True) layout = Layout([100], fill_frame=True)
self.add_layout(layout) self.add_layout(layout)
@ -19,6 +18,10 @@ class ResetDatabasePasswordView(CeoFrame):
on_next=self._next) on_next=self._next)
self.fix() self.fix()
def _ceoframe_on_reset(self):
super()._ceoframe_on_reset()
self._username.value = None
def _target(self): def _target(self):
username = self._username.value username = self._username.value
db_type = self._model.db_type db_type = self._model.db_type

View File

@ -9,7 +9,6 @@ class AddGroupView(CeoFrame):
def __init__(self, screen, width, height, model): def __init__(self, screen, width, height, model):
super().__init__( super().__init__(
screen, height, width, model, 'AddGroup', screen, height, width, model, 'AddGroup',
save_data=True,
) )
layout = Layout([100], fill_frame=True) layout = Layout([100], fill_frame=True)
self.add_layout(layout) self.add_layout(layout)
@ -23,6 +22,11 @@ class AddGroupView(CeoFrame):
next_scene='Confirm', on_next=self._next) next_scene='Confirm', on_next=self._next)
self.fix() self.fix()
def _ceoframe_on_reset(self):
super()._ceoframe_on_reset()
self._cn.value = None
self._description.value = None
def _next(self): def _next(self):
cn = self._cn.value cn = self._cn.value
description = self._description.value description = self._description.value

View File

@ -9,7 +9,6 @@ class AddMemberToGroupView(CeoFrame):
def __init__(self, screen, width, height, model): def __init__(self, screen, width, height, model):
super().__init__( super().__init__(
screen, height, width, model, 'AddMemberToGroup', screen, height, width, model, 'AddMemberToGroup',
save_data=True,
) )
layout = Layout([100], fill_frame=True) layout = Layout([100], fill_frame=True)
self.add_layout(layout) self.add_layout(layout)
@ -28,6 +27,12 @@ class AddMemberToGroupView(CeoFrame):
next_scene='Confirm', on_next=self._next) next_scene='Confirm', on_next=self._next)
self.fix() self.fix()
def _ceoframe_on_reset(self):
super()._ceoframe_on_reset()
self._cn.value = None
self._username.value = None
self._checkbox.value = True
def _next(self): def _next(self):
cn = self._cn.value cn = self._cn.value
uid = self._username.value uid = self._username.value

View File

@ -8,7 +8,6 @@ class GetGroupView(CeoFrame):
def __init__(self, screen, width, height, model): def __init__(self, screen, width, height, model):
super().__init__( super().__init__(
screen, height, width, model, 'GetGroup', screen, height, width, model, 'GetGroup',
save_data=True,
) )
layout = Layout([100], fill_frame=True) layout = Layout([100], fill_frame=True)
self.add_layout(layout) self.add_layout(layout)
@ -21,9 +20,12 @@ class GetGroupView(CeoFrame):
next_scene='GetGroupResult', on_next=self._next) next_scene='GetGroupResult', on_next=self._next)
self.fix() self.fix()
def _ceoframe_on_reset(self):
super()._ceoframe_on_reset()
self._cn.value = None
def _next(self): def _next(self):
cn = self._cn.value cn = self._cn.value
self._model.viewdata['GetGroup']['cn'] = cn
self.flash_message('Looking up group...', force_update=True) self.flash_message('Looking up group...', force_update=True)
try: try:
self._model.resp = http_get(f'/api/groups/{cn}') self._model.resp = http_get(f'/api/groups/{cn}')

View File

@ -9,7 +9,6 @@ class RemoveMemberFromGroupView(CeoFrame):
def __init__(self, screen, width, height, model): def __init__(self, screen, width, height, model):
super().__init__( super().__init__(
screen, height, width, model, 'RemoveMemberFromGroup', screen, height, width, model, 'RemoveMemberFromGroup',
save_data=True,
) )
layout = Layout([100], fill_frame=True) layout = Layout([100], fill_frame=True)
self.add_layout(layout) self.add_layout(layout)
@ -28,6 +27,11 @@ class RemoveMemberFromGroupView(CeoFrame):
next_scene='Confirm', on_next=self._next) next_scene='Confirm', on_next=self._next)
self.fix() self.fix()
def _ceoframe_on_reset(self):
super()._ceoframe_on_reset()
self._cn.value = None
self._username.value = None
def _next(self): def _next(self):
cn = self._cn.value cn = self._cn.value
uid = self._username.value uid = self._username.value

View File

@ -12,7 +12,6 @@ class AddUserView(CeoFrame):
def __init__(self, screen, width, height, model): def __init__(self, screen, width, height, model):
super().__init__( super().__init__(
screen, height, width, model, 'AddUser', screen, height, width, model, 'AddUser',
save_data=True,
) )
self._username_changed = False self._username_changed = False
@ -33,6 +32,7 @@ class AddUserView(CeoFrame):
self._num_terms = Text( self._num_terms = Text(
"Number of terms:", "num_terms", "Number of terms:", "num_terms",
validator=lambda s: s.isdigit() and s[0] != '0') validator=lambda s: s.isdigit() and s[0] != '0')
self._num_terms.value = '1'
layout.add_widget(self._num_terms) layout.add_widget(self._num_terms)
self.add_flash_message_layout() self.add_flash_message_layout()
@ -41,6 +41,14 @@ class AddUserView(CeoFrame):
next_scene='Confirm', on_next=self._next) next_scene='Confirm', on_next=self._next)
self.fix() self.fix()
def _ceoframe_on_reset(self):
super()._ceoframe_on_reset()
self._username.value = None
self._full_name.value = None
self._program.value = None
self._forwarding_address.value = None
self._num_terms.value = '1'
def _on_username_change(self): def _on_username_change(self):
self._username_changed = True self._username_changed = True

View File

@ -10,7 +10,6 @@ class ChangeLoginShellView(CeoFrame):
def __init__(self, screen, width, height, model): def __init__(self, screen, width, height, model):
super().__init__( super().__init__(
screen, height, width, model, 'ChangeLoginShell', screen, height, width, model, 'ChangeLoginShell',
save_data=True,
) )
self._username_changed = False self._username_changed = False
@ -31,6 +30,11 @@ class ChangeLoginShellView(CeoFrame):
next_scene='Confirm', on_next=self._next) next_scene='Confirm', on_next=self._next)
self.fix() self.fix()
def _ceoframe_on_reset(self):
super()._ceoframe_on_reset()
self._username.value = None
self._login_shell.value = None
# TODO: deduplicate this from AddUserView # TODO: deduplicate this from AddUserView
def _on_username_change(self): def _on_username_change(self):
self._username_changed = True self._username_changed = True

View File

@ -8,7 +8,6 @@ class GetUserView(CeoFrame):
def __init__(self, screen, width, height, model): def __init__(self, screen, width, height, model):
super().__init__( super().__init__(
screen, height, width, model, 'GetUser', screen, height, width, model, 'GetUser',
save_data=True,
) )
layout = Layout([100], fill_frame=True) layout = Layout([100], fill_frame=True)
self.add_layout(layout) self.add_layout(layout)
@ -21,6 +20,10 @@ class GetUserView(CeoFrame):
next_scene='GetUserResult', on_next=self._next) next_scene='GetUserResult', on_next=self._next)
self.fix() self.fix()
def _ceoframe_on_reset(self):
super()._ceoframe_on_reset()
self._username.value = None
def _next(self): def _next(self):
uid = self._username.value uid = self._username.value
self.flash_message('Looking up user...', force_update=True) self.flash_message('Looking up user...', force_update=True)

View File

@ -9,7 +9,6 @@ class RenewUserView(CeoFrame):
def __init__(self, screen, width, height, model): def __init__(self, screen, width, height, model):
super().__init__( super().__init__(
screen, height, width, model, 'RenewUser', screen, height, width, model, 'RenewUser',
save_data=True,
) )
self._model = model self._model = model
@ -20,6 +19,7 @@ class RenewUserView(CeoFrame):
self._num_terms = Text( self._num_terms = Text(
"Number of terms:", "num_terms", "Number of terms:", "num_terms",
validator=lambda s: s.isdigit() and s[0] != '0') validator=lambda s: s.isdigit() and s[0] != '0')
self._num_terms.value = '1'
layout.add_widget(self._num_terms) layout.add_widget(self._num_terms)
self.add_flash_message_layout() self.add_flash_message_layout()
@ -28,6 +28,11 @@ class RenewUserView(CeoFrame):
next_scene='Confirm', on_next=self._next) next_scene='Confirm', on_next=self._next)
self.fix() self.fix()
def _ceoframe_on_reset(self):
super()._ceoframe_on_reset()
self._username.value = None
self._num_terms.value = '1'
def _next(self): def _next(self):
uid = self._username.value uid = self._username.value
self.flash_message('Looking up user...', force_update=True) self.flash_message('Looking up user...', force_update=True)

View File

@ -8,7 +8,6 @@ class ResetPasswordView(CeoFrame):
def __init__(self, screen, width, height, model): def __init__(self, screen, width, height, model):
super().__init__( super().__init__(
screen, height, width, model, 'ResetPassword', screen, height, width, model, 'ResetPassword',
save_data=True,
) )
layout = Layout([100], fill_frame=True) layout = Layout([100], fill_frame=True)
self.add_layout(layout) self.add_layout(layout)
@ -21,6 +20,10 @@ class ResetPasswordView(CeoFrame):
next_scene='Confirm', on_next=self._next) next_scene='Confirm', on_next=self._next)
self.fix() self.fix()
def _ceoframe_on_reset(self):
super()._ceoframe_on_reset()
self._username.value = None
def _next(self): def _next(self):
uid = self._username.value uid = self._username.value
self._model.viewdata['ResetPassword']['uid'] = uid self._model.viewdata['ResetPassword']['uid'] = uid

View File

@ -10,7 +10,6 @@ class SetForwardingAddressesView(CeoFrame):
def __init__(self, screen, width, height, model): def __init__(self, screen, width, height, model):
super().__init__( super().__init__(
screen, height, width, model, 'SetForwardingAddresses', screen, height, width, model, 'SetForwardingAddresses',
save_data=True,
) )
self._username_changed = False self._username_changed = False
@ -34,6 +33,11 @@ class SetForwardingAddressesView(CeoFrame):
next_scene='Confirm', on_next=self._next) next_scene='Confirm', on_next=self._next)
self.fix() self.fix()
def _ceoframe_on_reset(self):
super()._ceoframe_on_reset()
self._username.value = None
self._forwarding_addresses.value = None
# TODO: deduplicate this from AddUserView # TODO: deduplicate this from AddUserView
def _on_username_change(self): def _on_username_change(self):
self._username_changed = True self._username_changed = True

View File

@ -27,8 +27,11 @@ class GetPositionsView(CeoFrame):
super().__init__( super().__init__(
screen, height, width, model, 'GetPositions', screen, height, width, model, 'GetPositions',
escape_on_q=True, escape_on_q=True,
on_load=self._on_load) on_load=self._on_load
)
self._position_widgets = {}
def _on_load(self):
cfg = component.getUtility(IConfig) cfg = component.getUtility(IConfig)
avail = cfg.get('positions_available') avail = cfg.get('positions_available')
@ -39,7 +42,6 @@ class GetPositionsView(CeoFrame):
self._main_layout = Layout([10, 1, 10], fill_frame=True) self._main_layout = Layout([10, 1, 10], fill_frame=True)
self.add_layout(self._main_layout) self.add_layout(self._main_layout)
self._position_widgets = {}
for pos in avail: for pos in avail:
self._position_widgets[pos] = self._add_pair(position_names[pos], '') self._position_widgets[pos] = self._add_pair(position_names[pos], '')
@ -47,18 +49,6 @@ class GetPositionsView(CeoFrame):
self.add_buttons(back_btn=True) self.add_buttons(back_btn=True)
self.fix() 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(): def target():
self.flash_message('Looking up positions...') self.flash_message('Looking up positions...')
try: try:
@ -72,6 +62,17 @@ class GetPositionsView(CeoFrame):
self.clear_flash_message(force_update=True) self.clear_flash_message(force_update=True)
Thread(target=target).start() Thread(target=target).start()
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 _ceoframe_on_reset(self): def _ceoframe_on_reset(self):
super()._ceoframe_on_reset() super()._ceoframe_on_reset()
# clear the labels # clear the labels

View File

@ -12,16 +12,18 @@ class SetPositionsView(CeoFrame):
def __init__(self, screen, width, height, model): def __init__(self, screen, width, height, model):
super().__init__( super().__init__(
screen, height, width, model, 'SetPositions', screen, height, width, model, 'SetPositions',
save_data=True) )
cfg = component.getUtility(IConfig) cfg = component.getUtility(IConfig)
avail = cfg.get('positions_available') avail = cfg.get('positions_available')
required = cfg.get('positions_required') required = cfg.get('positions_required')
layout = Layout([100], fill_frame=True) layout = Layout([100], fill_frame=True)
self.add_layout(layout) self.add_layout(layout)
self._widgets = []
for pos in avail: for pos in avail:
suffix = ' (*)' if pos in required else '' suffix = ' (*)' if pos in required else ''
widget = Text(position_names[pos] + suffix, pos) widget = Text(position_names[pos] + suffix, pos)
self._widgets.append(widget)
layout.add_widget(widget) layout.add_widget(widget)
layout = Layout([100]) layout = Layout([100])
@ -34,6 +36,11 @@ class SetPositionsView(CeoFrame):
next_scene='Confirm', on_next=self._next) next_scene='Confirm', on_next=self._next)
self.fix() self.fix()
def _ceoframe_on_reset(self):
super()._ceoframe_on_reset()
for widget in self._widgets:
widget.value = None
def _next(self): def _next(self):
self.save() self.save()
body = {pos: username for pos, username in self.data.items() if username} body = {pos: username for pos, username in self.data.items() if username}

View File

@ -1,3 +1,4 @@
import os
import sys import sys
from asciimatics.exceptions import ResizeScreenError from asciimatics.exceptions import ResizeScreenError
@ -37,10 +38,6 @@ views = []
def screen_wrapper(screen, last_scene, model): def screen_wrapper(screen, last_scene, model):
global views global views
# unload the old views
for name, view in views:
if hasattr(view, '_ceoframe_on_unload'):
view._ceoframe_on_unload()
width = min(screen.width, 90) width = min(screen.width, 90)
height = min(screen.height, 24) height = min(screen.height, 24)
views = [ views = [
@ -83,9 +80,10 @@ def screen_wrapper(screen, last_scene, model):
def main(): def main():
last_scene = None last_scene = None
model = Model() model = Model()
while True: try:
try: Screen.wrapper(screen_wrapper, arguments=[last_scene, model])
Screen.wrapper(screen_wrapper, arguments=[last_scene, model]) sys.exit(0)
sys.exit(0) except ResizeScreenError:
except ResizeScreenError as e: os.system('reset')
last_scene = e.scene print('Unfortunately, ceo does not currently support dynamic resizing.')
sys.exit(1)

View File

@ -146,7 +146,7 @@ def generic_handle_stream_response(
""" """
if resp.status_code != 200: if resp.status_code != 200:
handler.handle_non_200(resp) handler.handle_non_200(resp)
return return [{'status': 'error'}]
handler.begin() handler.begin()
idx = 0 idx = 0
data = [] data = []
@ -155,7 +155,7 @@ def generic_handle_stream_response(
data.append(d) data.append(d)
if d['status'] == 'aborted': if d['status'] == 'aborted':
handler.handle_aborted(d['error']) handler.handle_aborted(d['error'])
return return data
elif d['status'] == 'completed': elif d['status'] == 'completed':
while idx < len(operations): while idx < len(operations):
handler.handle_skipped_operation() handler.handle_skipped_operation()