Rewrite TUI #52
|
@ -1,160 +0,0 @@
|
|||
from asciimatics.event import KeyboardEvent
|
||||
from asciimatics.exceptions import NextScene, StopApplication
|
||||
from asciimatics.screen import Screen
|
||||
from asciimatics.widgets import Frame, Layout, Divider, Button, Label, \
|
||||
PopUpDialog
|
||||
|
||||
|
||||
class CeoFrame(Frame):
|
||||
def __init__(
|
||||
self,
|
||||
screen,
|
||||
height,
|
||||
width,
|
||||
model,
|
||||
name,
|
||||
on_load=None,
|
||||
title=None,
|
||||
escape_on_q=False, # whether to quit when 'q' is pressed
|
||||
):
|
||||
super().__init__(
|
||||
screen,
|
||||
height,
|
||||
width,
|
||||
name=name,
|
||||
can_scroll=False,
|
||||
title=title,
|
||||
on_load=self._ceoframe_on_load,
|
||||
)
|
||||
self._custom_on_load = on_load
|
||||
self._model = model
|
||||
self._name = name
|
||||
# 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]
|
||||
if escape_on_q:
|
||||
self._quit_keys.append(ord('q'))
|
||||
# child classes may override this as a last resort
|
||||
self.skip_reload = False
|
||||
|
||||
def _ceoframe_on_load(self):
|
||||
if self.skip_reload:
|
||||
self.skip_reload = False
|
||||
return
|
||||
if self._model.title is not None:
|
||||
self.title = self._model.title
|
||||
self._model.title = None
|
||||
if self._has_dynamic_layouts and self._model.nav_direction == 'forward':
|
||||
# We arrive here after a user pressed 'Back' then 'Next',
|
||||
# or after we returned to the Welcome screen.
|
||||
# The data may have changed, so we need to redraw everything,
|
||||
# via self._custom_on_load().
|
||||
self.clear_layouts()
|
||||
self._custom_on_load()
|
||||
|
||||
# may be overridden by child classes
|
||||
def _ceoframe_on_reset(self):
|
||||
"""
|
||||
This is called whenever we return to the home screen
|
||||
after some kind of operation was completed.
|
||||
This is called from Model.reset().
|
||||
"""
|
||||
pass
|
||||
|
||||
def clear_layouts(self):
|
||||
self._layouts.clear()
|
||||
|
||||
def force_update(self):
|
||||
"""
|
||||
This should be called by background threads after they make changes
|
||||
to the UI.
|
||||
"""
|
||||
# Since we're running in a separate thread, we need to force the
|
||||
# screen to update. See
|
||||
# https://github.com/peterbrittain/asciimatics/issues/56
|
||||
self.fix()
|
||||
self._model.screen.force_update()
|
||||
|
||||
def add_buttons(
|
||||
self, back_btn=False, back_btn_text='Back',
|
||||
next_scene=None, next_btn_text='Next', on_next=None,
|
||||
on_next_excl=None,
|
||||
):
|
||||
"""
|
||||
Add a new layout at the bottom of the frame with buttons.
|
||||
If back_btn is True, a Back button is added.
|
||||
If next_scene is set to the name of the next scene, or on_next_excl
|
||||
is set, a Next button will be added.
|
||||
If on_next is set to a function, it will be called when the Next
|
||||
button is pressed, and the screen will switch to the next scene.
|
||||
If on_next_excl is set to a function, it will be called when the Next
|
||||
button is pressed, and the scene will not be switched.
|
||||
If both on_next and on_next_excl are set, on_next will be ignored.
|
||||
"""
|
||||
layout = Layout([100])
|
||||
self.add_layout(layout)
|
||||
layout.add_widget(Divider())
|
||||
|
||||
def _back():
|
||||
self._model.nav_direction = 'backward'
|
||||
last_scene = self._model.scene_stack.pop()
|
||||
if last_scene == 'Welcome':
|
||||
self._model.reset()
|
||||
raise NextScene(last_scene)
|
||||
|
||||
def _next():
|
||||
self._model.nav_direction = 'forward'
|
||||
if on_next_excl is not None:
|
||||
on_next_excl()
|
||||
return
|
||||
if on_next is not None:
|
||||
on_next()
|
||||
self.go_to_next_scene(next_scene)
|
||||
|
||||
layout = Layout([1, 1])
|
||||
self.add_layout(layout)
|
||||
if back_btn:
|
||||
layout.add_widget(Button(back_btn_text, _back), 0)
|
||||
if next_scene is not None or on_next_excl is not None:
|
||||
layout.add_widget(Button(next_btn_text, _next), 1)
|
||||
|
||||
def go_to_next_scene(self, next_scene: str):
|
||||
self._model.scene_stack.append(self._name)
|
||||
raise NextScene(next_scene)
|
||||
|
||||
def add_flash_message_layout(self):
|
||||
layout = Layout([100])
|
||||
self.add_layout(layout)
|
||||
self._status_label = Label('')
|
||||
layout.add_widget(self._status_label)
|
||||
|
||||
def flash_message(self, msg: str, force_update: bool = False):
|
||||
self._status_label.text = msg
|
||||
if force_update:
|
||||
self._model.screen.force_update()
|
||||
self._model.screen.draw_next_frame()
|
||||
|
||||
def clear_flash_message(self, force_update: bool = False):
|
||||
self.flash_message('', force_update)
|
||||
|
||||
def process_event(self, event):
|
||||
if not isinstance(event, KeyboardEvent):
|
||||
return super().process_event(event)
|
||||
c = event.key_code
|
||||
# Stop on 'q' or 'Esc'
|
||||
if c in self._quit_keys:
|
||||
self._scene.add_effect(PopUpDialog(
|
||||
self.screen,
|
||||
'Are you sure you want to quit?',
|
||||
['Yes', 'No'],
|
||||
has_shadow=True,
|
||||
on_close=self._quit_on_yes,
|
||||
))
|
||||
return super().process_event(event)
|
||||
|
||||
@staticmethod
|
||||
def _quit_on_yes(selected):
|
||||
# Yes is the first button
|
||||
if selected == 0:
|
||||
raise StopApplication("User terminated app")
|
|
@ -1,61 +0,0 @@
|
|||
from asciimatics.widgets import Layout, Label
|
||||
|
||||
from .CeoFrame import CeoFrame
|
||||
|
||||
|
||||
class ConfirmView(CeoFrame):
|
||||
def __init__(self, screen, width, height, model):
|
||||
super().__init__(
|
||||
screen, height, width, model, 'Confirm',
|
||||
on_load=self._confirmview_on_load, title='Confirmation',
|
||||
escape_on_q=True,
|
||||
)
|
||||
|
||||
def _add_line(self, text: str = ''):
|
||||
layout = Layout([100])
|
||||
self.add_layout(layout)
|
||||
layout.add_widget(Label(text, align='^'))
|
||||
|
||||
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)
|
||||
|
||||
def _confirmview_on_load(self):
|
||||
for _ in range(2):
|
||||
self._add_line()
|
||||
for line in self._model.confirm_lines:
|
||||
if isinstance(line, str):
|
||||
self._add_line(line)
|
||||
else:
|
||||
# assume tuple
|
||||
key, val = line
|
||||
self._add_pair(key, val)
|
||||
# fill the rest of the space
|
||||
self.add_layout(Layout([100], fill_frame=True))
|
||||
|
||||
kwargs = {
|
||||
'back_btn': True, 'back_btn_text': 'No', 'next_btn_text': 'Yes',
|
||||
}
|
||||
if self._model.operations is not None:
|
||||
kwargs['next_scene'] = self._model.txn_view_name or 'Transaction'
|
||||
else:
|
||||
self.add_flash_message_layout()
|
||||
kwargs['on_next_excl'] = self._next
|
||||
self.add_buttons(**kwargs)
|
||||
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):
|
||||
self.flash_message('Sending request...', force_update=True)
|
||||
try:
|
||||
self._model.resp = self._model.deferred_req()
|
||||
finally:
|
||||
self.clear_flash_message()
|
||||
next_scene = self._model.result_view_name or 'Result'
|
||||
self.go_to_next_scene(next_scene)
|
|
@ -1,29 +0,0 @@
|
|||
from asciimatics.exceptions import NextScene
|
||||
from asciimatics.widgets import Layout, Label
|
||||
|
||||
from .CeoFrame import CeoFrame
|
||||
|
||||
|
||||
class ErrorView(CeoFrame):
|
||||
def __init__(self, screen, width, height, model):
|
||||
super().__init__(
|
||||
screen, height, width, model, 'Error',
|
||||
on_load=self._errorview_on_load, title='Error',
|
||||
)
|
||||
|
||||
def _errorview_on_load(self):
|
||||
layout = Layout([1, 10], fill_frame=True)
|
||||
self.add_layout(layout)
|
||||
for _ in range(2):
|
||||
layout.add_widget(Label(''), 1)
|
||||
layout.add_widget(Label('An error occurred:'), 1)
|
||||
layout.add_widget(Label(''), 1)
|
||||
for line in self._model.error_message.splitlines():
|
||||
layout.add_widget(Label(line), 1)
|
||||
|
||||
self.add_buttons(on_next_excl=self._next)
|
||||
self.fix()
|
||||
|
||||
def _next(self):
|
||||
self._model.reset()
|
||||
raise NextScene('Welcome')
|
|
@ -1,56 +0,0 @@
|
|||
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
|
||||
self.scene_stack = []
|
||||
self.result_view_name = None
|
||||
self.txn_view_name = None
|
||||
self.error_message = None
|
||||
self.nav_direction = 'forward'
|
||||
# View-specific data
|
||||
self._initial_viewdata = {
|
||||
'ResetPassword': {
|
||||
'uid': '',
|
||||
},
|
||||
}
|
||||
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
|
||||
self.confirm_lines = None
|
||||
self.operations = None
|
||||
self.deferred_req = None
|
||||
self.resp = None
|
||||
self.db_type = None
|
||||
self.user_dict = None
|
||||
|
||||
def reset(self):
|
||||
self.viewdata = deepcopy(self._initial_viewdata)
|
||||
self.is_club_rep = False
|
||||
self.confirm_lines = None
|
||||
self.operations = None
|
||||
self.deferred_req = None
|
||||
self.resp = None
|
||||
self.db_type = None
|
||||
self.user_dict = None
|
||||
self.title = None
|
||||
self.error_message = None
|
||||
self.scene_stack.clear()
|
||||
self.result_view_name = None
|
||||
self.txn_view_name = None
|
||||
for view in self.views:
|
||||
if hasattr(view, '_ceoframe_on_reset'):
|
||||
view._ceoframe_on_reset()
|
|
@ -1,62 +0,0 @@
|
|||
from asciimatics.exceptions import NextScene
|
||||
from asciimatics.widgets import Layout, Label
|
||||
import requests
|
||||
|
||||
from .CeoFrame import CeoFrame
|
||||
|
||||
|
||||
class ResultView(CeoFrame):
|
||||
def __init__(self, screen, width, height, model):
|
||||
super().__init__(
|
||||
screen, height, width, model, 'Result',
|
||||
on_load=self._resultview_on_load, title='Result',
|
||||
escape_on_q=True,
|
||||
)
|
||||
|
||||
# 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)
|
||||
if key:
|
||||
layout.add_widget(Label(key + ':', align='>'), 0)
|
||||
else:
|
||||
layout.add_widget(Label(''), 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 not resp.ok:
|
||||
self._add_text('An error occurred:')
|
||||
if resp.headers.get('content-type') == 'application/json':
|
||||
err_msg = resp.json()['error']
|
||||
else:
|
||||
err_msg = resp.text.rstrip()
|
||||
self._add_text(err_msg)
|
||||
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')
|
|
@ -1,93 +0,0 @@
|
|||
from typing import Dict, Union
|
||||
|
||||
from asciimatics.widgets import Label, Layout
|
||||
import requests
|
||||
|
||||
from .Model import Model
|
||||
from ..StreamResponseHandler import StreamResponseHandler
|
||||
|
||||
|
||||
class TUIStreamResponseHandler(StreamResponseHandler):
|
||||
def __init__(
|
||||
self,
|
||||
model: Model,
|
||||
labels: Dict[str, Label],
|
||||
msg_layout: Layout,
|
||||
txn_view, # TransactionView
|
||||
):
|
||||
super().__init__()
|
||||
self.screen = model.screen
|
||||
self.operations = model.operations
|
||||
self.idx = 0
|
||||
self.labels = labels
|
||||
self.msg_layout = msg_layout
|
||||
self.txn_view = txn_view
|
||||
self.error_messages = []
|
||||
|
||||
def _update(self):
|
||||
self.txn_view.force_update()
|
||||
|
||||
def _show_msg(self, msg: str = '\n'):
|
||||
for line in msg.splitlines():
|
||||
self.msg_layout.add_widget(Label(line, align='^'))
|
||||
|
||||
def _abort(self):
|
||||
for operation in self.operations[self.idx:]:
|
||||
self.labels[operation].text = 'ABORTED'
|
||||
self.txn_view.enable_next_btn()
|
||||
|
||||
def handle_non_200(self, resp: requests.Response):
|
||||
self._abort()
|
||||
self._show_msg('An error occurred:')
|
||||
if resp.headers.get('content-type') == 'application/json':
|
||||
err_msg = resp.json()['error']
|
||||
else:
|
||||
err_msg = resp.text
|
||||
self._show_msg(err_msg)
|
||||
self._update()
|
||||
|
||||
def begin(self):
|
||||
pass
|
||||
|
||||
def handle_aborted(self, err_msg: str):
|
||||
self._abort()
|
||||
self._show_msg('The transaction was rolled back.')
|
||||
self._show_msg('The error was:\n')
|
||||
self._show_msg(err_msg)
|
||||
self._show_msg()
|
||||
self._show_msg('Please check the ceod logs.')
|
||||
self._update()
|
||||
|
||||
def handle_completed(self):
|
||||
self._show_msg('Transaction successfully completed.')
|
||||
if len(self.error_messages) > 0:
|
||||
self._show_msg('There were some errors:')
|
||||
for msg in self.error_messages:
|
||||
self._show_msg(msg)
|
||||
self.txn_view.enable_next_btn()
|
||||
self._update()
|
||||
|
||||
def handle_successful_operation(self):
|
||||
operation = self.operations[self.idx]
|
||||
self.labels[operation].text = 'Done'
|
||||
self.idx += 1
|
||||
self._update()
|
||||
|
||||
def handle_failed_operation(self, err_msg: Union[str, None]):
|
||||
operation = self.operations[self.idx]
|
||||
self.labels[operation].text = 'Failed'
|
||||
if err_msg is not None:
|
||||
self.error_messages.append(err_msg)
|
||||
self.idx += 1
|
||||
self._update()
|
||||
|
||||
def handle_skipped_operation(self):
|
||||
operation = self.operations[self.idx]
|
||||
self.labels[operation].text = 'Skipped'
|
||||
self.idx += 1
|
||||
self._update()
|
||||
|
||||
def handle_unrecognized_operation(self, operation: str):
|
||||
self.error_messages.append('Unrecognized operation: ' + operation)
|
||||
self.idx += 1
|
||||
self._update()
|
|
@ -1,85 +0,0 @@
|
|||
from threading import Thread
|
||||
from typing import List, Dict
|
||||
|
||||
from asciimatics.exceptions import NextScene
|
||||
from asciimatics.widgets import Layout, Button, Divider, Label
|
||||
|
||||
from ..operation_strings import descriptions as op_desc
|
||||
from ..utils import generic_handle_stream_response
|
||||
from .CeoFrame import CeoFrame
|
||||
from .TUIStreamResponseHandler import TUIStreamResponseHandler
|
||||
|
||||
|
||||
class TransactionView(CeoFrame):
|
||||
def __init__(self, screen, width, height, model):
|
||||
super().__init__(
|
||||
screen, height, width, model, 'Transaction',
|
||||
on_load=self._txnview_on_load, title='Running Transaction',
|
||||
)
|
||||
# map operation names to label widgets
|
||||
self._labels = {}
|
||||
|
||||
def _add_buttons(self):
|
||||
layout = Layout([100])
|
||||
self.add_layout(layout)
|
||||
layout.add_widget(Divider())
|
||||
|
||||
layout = Layout([1, 1])
|
||||
self.add_layout(layout)
|
||||
self._next_btn = Button('Next', self._next)
|
||||
layout.add_widget(self._next_btn, 1)
|
||||
|
||||
def _add_blank_line(self):
|
||||
self._op_layout.add_widget(Label(''), 0)
|
||||
self._op_layout.add_widget(Label(''), 2)
|
||||
|
||||
def _txnview_on_load(self):
|
||||
self._op_layout = Layout([12, 1, 10])
|
||||
self.add_layout(self._op_layout)
|
||||
# store the layouts so that we can re-use them when the screen
|
||||
# gets resized
|
||||
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()
|
||||
# this is the where success/failure messages etc. get placed
|
||||
self._msg_layout = Layout([100])
|
||||
self.add_layout(self._msg_layout)
|
||||
# fill up the rest of the space
|
||||
self.add_layout(Layout([100], fill_frame=True))
|
||||
|
||||
self._add_buttons()
|
||||
self.fix()
|
||||
Thread(target=self._do_txn).start()
|
||||
|
||||
def _do_txn(self):
|
||||
resp = self._model.deferred_req()
|
||||
handler = TUIStreamResponseHandler(
|
||||
model=self._model,
|
||||
labels=self._labels,
|
||||
msg_layout=self._msg_layout,
|
||||
txn_view=self,
|
||||
)
|
||||
data = generic_handle_stream_response(resp, self._model.operations, handler)
|
||||
self.write_extra_txn_info(data)
|
||||
|
||||
# to be overridden in child classes if desired
|
||||
def write_extra_txn_info(self, data: List[Dict]):
|
||||
pass
|
||||
|
||||
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.
|
||||
# We don't want to reload, though (which reset() will trigger).
|
||||
self.skip_reload = True
|
||||
self.reset()
|
||||
|
||||
def _next(self):
|
||||
self._model.reset()
|
||||
raise NextScene('Welcome')
|
|
@ -1,107 +0,0 @@
|
|||
from asciimatics.widgets import ListBox, Layout, Divider, Button, Label
|
||||
from asciimatics.exceptions import NextScene, StopApplication
|
||||
|
||||
from .CeoFrame import CeoFrame
|
||||
|
||||
|
||||
class WelcomeView(CeoFrame):
|
||||
def __init__(self, screen, width, height, model):
|
||||
super().__init__(
|
||||
screen, height, width, model, 'Welcome',
|
||||
title='CSC Electronic Office',
|
||||
escape_on_q=True,
|
||||
)
|
||||
members_menu_items = [
|
||||
('Add member', 'AddUser'),
|
||||
('Add club rep', 'AddUser'),
|
||||
('Renew member', 'RenewUser'),
|
||||
('Renew club rep', 'RenewUser'),
|
||||
('Get user info', 'GetUser'),
|
||||
('Reset password', 'ResetPassword'),
|
||||
('Change login shell', 'ChangeLoginShell'),
|
||||
('Set forwarding addresses', 'SetForwardingAddresses'),
|
||||
]
|
||||
groups_menu_items = [
|
||||
('Add group', 'AddGroup'),
|
||||
('Get group members', 'GetGroup'),
|
||||
('Add member to group', 'AddMemberToGroup'),
|
||||
('Remove member from group', 'RemoveMemberFromGroup'),
|
||||
]
|
||||
db_menu_items = [
|
||||
('Create MySQL database', 'CreateDatabase'),
|
||||
('Reset MySQL password', 'ResetDatabasePassword'),
|
||||
('Create PostgreSQL database', 'CreateDatabase'),
|
||||
('Reset PostgreSQL password', 'ResetDatabasePassword'),
|
||||
]
|
||||
positions_menu_items = [
|
||||
('Get positions', 'GetPositions'),
|
||||
('Set positions', 'SetPositions'),
|
||||
]
|
||||
self.menu_items = [
|
||||
('members', members_menu_items),
|
||||
('groups', groups_menu_items),
|
||||
('databases', db_menu_items),
|
||||
('positions', positions_menu_items),
|
||||
]
|
||||
self.menu_items_dict = dict(self.menu_items)
|
||||
flat_menu_items = [item for name, items in self.menu_items for item in items]
|
||||
menu = ListBox(
|
||||
len(flat_menu_items),
|
||||
[
|
||||
(desc, i) for i, (desc, view) in
|
||||
enumerate(flat_menu_items)
|
||||
],
|
||||
name='menu',
|
||||
on_select=self._menu_select,
|
||||
)
|
||||
labels = []
|
||||
for name, items in self.menu_items:
|
||||
labels.append(Label(name.capitalize(), align='>'))
|
||||
for _ in range(len(items) - 1):
|
||||
labels.append(Label(''))
|
||||
|
||||
layout = Layout([5, 1, 8], fill_frame=True)
|
||||
self.add_layout(layout)
|
||||
layout.add_widget(menu, 2)
|
||||
for label in labels:
|
||||
layout.add_widget(label, 0)
|
||||
|
||||
layout = Layout([100])
|
||||
self.add_layout(layout)
|
||||
layout.add_widget(Label('Press <TAB> to switch widgets'))
|
||||
layout.add_widget(Divider())
|
||||
|
||||
layout = Layout([1, 1, 1])
|
||||
self.add_layout(layout)
|
||||
layout.add_widget(Button("Quit", self._quit), 2)
|
||||
self.fix()
|
||||
|
||||
def _menu_select(self):
|
||||
self.save()
|
||||
item_id = self.data['menu']
|
||||
# find which submenu the item belongs to
|
||||
counter = 0
|
||||
for name, items in self.menu_items:
|
||||
if item_id < counter + len(items):
|
||||
break
|
||||
counter += len(items)
|
||||
submenu_idx = item_id - counter
|
||||
desc, view = items[submenu_idx]
|
||||
if name == 'members':
|
||||
if desc.endswith('club rep'):
|
||||
self._model.is_club_rep = True
|
||||
elif name == 'databases':
|
||||
if 'MySQL' in desc:
|
||||
self._model.db_type = 'mysql'
|
||||
else:
|
||||
self._model.db_type = 'postgresql'
|
||||
self._welcomeview_go_to_next_scene(desc, view)
|
||||
|
||||
def _welcomeview_go_to_next_scene(self, desc, view):
|
||||
self._model.title = desc
|
||||
self._model.scene_stack.append('Welcome')
|
||||
raise NextScene(view)
|
||||
|
||||
@staticmethod
|
||||
def _quit():
|
||||
raise StopApplication("User pressed quit")
|
|
@ -1,34 +0,0 @@
|
|||
import os
|
||||
|
||||
import requests
|
||||
from zope import component
|
||||
|
||||
from ...utils import write_db_creds
|
||||
from ..ResultView import ResultView
|
||||
from ceo_common.interfaces import IConfig
|
||||
|
||||
|
||||
class CreateDatabaseResultView(ResultView):
|
||||
def show_result(self, resp: requests.Response):
|
||||
password = resp.json()['password']
|
||||
db_type = self._model.db_type
|
||||
db_type_name = 'MySQL' if db_type == 'mysql' else 'PostgreSQL'
|
||||
db_host = component.getUtility(IConfig).get(f'{db_type}_host')
|
||||
user_dict = self._model.user_dict
|
||||
username = user_dict['uid']
|
||||
filename = os.path.join(user_dict['home_directory'], f"ceo-{db_type}-info")
|
||||
wrote_to_file = write_db_creds(
|
||||
filename, user_dict, password, db_type, db_host)
|
||||
self._add_text(f'{db_type_name} database created.', center=True)
|
||||
self._add_text()
|
||||
self._add_text((f'''Connection Information:
|
||||
|
||||
Database: {username}
|
||||
Username: {username}
|
||||
Password: {password}
|
||||
Host: {db_host}'''))
|
||||
self._add_text()
|
||||
if wrote_to_file:
|
||||
self._add_text(f"These settings have been written to {filename}.")
|
||||
else:
|
||||
self._add_text(f"We were unable to write these settings to {filename}.")
|
|
@ -1,47 +0,0 @@
|
|||
from asciimatics.widgets import Layout, Text
|
||||
|
||||
from ...utils import http_post, http_get, defer
|
||||
from ..CeoFrame import CeoFrame
|
||||
|
||||
|
||||
class CreateDatabaseView(CeoFrame):
|
||||
def __init__(self, screen, width, height, model):
|
||||
super().__init__(
|
||||
screen, height, width, model, 'CreateDatabase',
|
||||
)
|
||||
layout = Layout([100], fill_frame=True)
|
||||
self.add_layout(layout)
|
||||
self._username = Text("Username:", "uid")
|
||||
layout.add_widget(self._username)
|
||||
self.add_buttons(
|
||||
back_btn=True, next_scene='Confirm',
|
||||
on_next=self._next)
|
||||
self.fix()
|
||||
|
||||
def _ceoframe_on_reset(self):
|
||||
super()._ceoframe_on_reset()
|
||||
self._username.value = None
|
||||
|
||||
def _target(self):
|
||||
username = self._username.value
|
||||
db_type = self._model.db_type
|
||||
resp = http_get(f'/api/members/{username}')
|
||||
if not resp.ok:
|
||||
return resp
|
||||
user_dict = resp.json()
|
||||
self._model.user_dict = user_dict
|
||||
return http_post(f'/api/db/{db_type}/{username}')
|
||||
|
||||
def _next(self):
|
||||
username = self._username.value
|
||||
if not username:
|
||||
return
|
||||
if self._model.db_type == 'mysql':
|
||||
db_type_name = 'MySQL'
|
||||
else:
|
||||
db_type_name = 'PostgreSQL'
|
||||
self._model.confirm_lines = [
|
||||
f'Are you sure you want to create a {db_type_name} database for {username}?',
|
||||
]
|
||||
self._model.deferred_req = defer(self._target)
|
||||
self._model.result_view_name = 'CreateDatabaseResult'
|
|
@ -1,29 +0,0 @@
|
|||
import os
|
||||
|
||||
import requests
|
||||
from zope import component
|
||||
|
||||
from ...utils import write_db_creds
|
||||
from ..ResultView import ResultView
|
||||
from ceo_common.interfaces import IConfig
|
||||
|
||||
|
||||
class ResetDatabasePasswordResultView(ResultView):
|
||||
def show_result(self, resp: requests.Response):
|
||||
password = resp.json()['password']
|
||||
db_type = self._model.db_type
|
||||
db_type_name = 'MySQL' if db_type == 'mysql' else 'PostgreSQL'
|
||||
db_host = component.getUtility(IConfig).get(f'{db_type}_host')
|
||||
user_dict = self._model.user_dict
|
||||
username = user_dict['uid']
|
||||
filename = os.path.join(user_dict['home_directory'], f"ceo-{db_type}-info")
|
||||
wrote_to_file = write_db_creds(
|
||||
filename, user_dict, password, db_type, db_host)
|
||||
self._add_text(f'The new {db_type_name} password for {username} is:')
|
||||
self._add_text()
|
||||
self._add_text(password)
|
||||
self._add_text()
|
||||
if wrote_to_file:
|
||||
self._add_text(f"The settings in {filename} have been updated.")
|
||||
else:
|
||||
self._add_text(f"We were unable to update the settings in {filename}.")
|
|
@ -1,47 +0,0 @@
|
|||
from asciimatics.widgets import Layout, Text
|
||||
|
||||
from ...utils import http_post, http_get, defer
|
||||
from ..CeoFrame import CeoFrame
|
||||
|
||||
|
||||
class ResetDatabasePasswordView(CeoFrame):
|
||||
def __init__(self, screen, width, height, model):
|
||||
super().__init__(
|
||||
screen, height, width, model, 'ResetDatabasePassword',
|
||||
)
|
||||
layout = Layout([100], fill_frame=True)
|
||||
self.add_layout(layout)
|
||||
self._username = Text("Username:", "uid")
|
||||
layout.add_widget(self._username)
|
||||
self.add_buttons(
|
||||
back_btn=True, next_scene='Confirm',
|
||||
on_next=self._next)
|
||||
self.fix()
|
||||
|
||||
def _ceoframe_on_reset(self):
|
||||
super()._ceoframe_on_reset()
|
||||
self._username.value = None
|
||||
|
||||
def _target(self):
|
||||
username = self._username.value
|
||||
db_type = self._model.db_type
|
||||
resp = http_get(f'/api/members/{username}')
|
||||
if not resp.ok:
|
||||
return resp
|
||||
user_dict = resp.json()
|
||||
self._model.user_dict = user_dict
|
||||
return http_post(f'/api/db/{db_type}/{username}/pwreset')
|
||||
|
||||
def _next(self):
|
||||
username = self._username.value
|
||||
if not username:
|
||||
return
|
||||
if self._model.db_type == 'mysql':
|
||||
db_type_name = 'MySQL'
|
||||
else:
|
||||
db_type_name = 'PostgreSQL'
|
||||
self._model.confirm_lines = [
|
||||
f'Are you sure you want to reset the {db_type_name} password for {username}?',
|
||||
]
|
||||
self._model.deferred_req = defer(self._target)
|
||||
self._model.result_view_name = 'ResetDatabasePasswordResult'
|
|
@ -1 +0,0 @@
|
|||
|
|
@ -1,46 +0,0 @@
|
|||
from asciimatics.widgets import Layout, Text
|
||||
|
||||
from ...utils import defer, http_post
|
||||
from ..CeoFrame import CeoFrame
|
||||
from ceod.transactions.groups import AddGroupTransaction
|
||||
|
||||
|
||||
class AddGroupView(CeoFrame):
|
||||
def __init__(self, screen, width, height, model):
|
||||
super().__init__(
|
||||
screen, height, width, model, 'AddGroup',
|
||||
)
|
||||
layout = Layout([100], fill_frame=True)
|
||||
self.add_layout(layout)
|
||||
self._cn = Text('Name:', 'cn')
|
||||
layout.add_widget(self._cn)
|
||||
self._description = Text('Description:', 'description')
|
||||
layout.add_widget(self._description)
|
||||
|
||||
self.add_buttons(
|
||||
back_btn=True,
|
||||
next_scene='Confirm', on_next=self._next)
|
||||
self.fix()
|
||||
|
||||
def _ceoframe_on_reset(self):
|
||||
super()._ceoframe_on_reset()
|
||||
self._cn.value = None
|
||||
self._description.value = None
|
||||
|
||||
def _next(self):
|
||||
cn = self._cn.value
|
||||
description = self._description.value
|
||||
body = {
|
||||
'cn': cn,
|
||||
'description': description,
|
||||
}
|
||||
self._model.confirm_lines = [
|
||||
'The following group will be created:',
|
||||
'',
|
||||
('cn', cn),
|
||||
('description', description),
|
||||
'',
|
||||
'Are you sure you want to continue?',
|
||||
]
|
||||
self._model.deferred_req = defer(http_post, '/api/groups', json=body)
|
||||
self._model.operations = AddGroupTransaction.operations
|
|
@ -1,49 +0,0 @@
|
|||
from asciimatics.widgets import Layout, Text, CheckBox, Label
|
||||
|
||||
from ...utils import defer, http_post
|
||||
from ..CeoFrame import CeoFrame
|
||||
from ceod.transactions.groups import AddMemberToGroupTransaction
|
||||
|
||||
|
||||
class AddMemberToGroupView(CeoFrame):
|
||||
def __init__(self, screen, width, height, model):
|
||||
super().__init__(
|
||||
screen, height, width, model, 'AddMemberToGroup',
|
||||
)
|
||||
layout = Layout([100], fill_frame=True)
|
||||
self.add_layout(layout)
|
||||
self._cn = Text('Group name:', 'cn')
|
||||
layout.add_widget(self._cn)
|
||||
self._username = Text('Username:', 'uid')
|
||||
layout.add_widget(self._username)
|
||||
layout.add_widget(Label(''))
|
||||
self._checkbox = CheckBox(
|
||||
'subscribe to auxiliary mailing lists', name='subscribe')
|
||||
self._checkbox.value = True
|
||||
layout.add_widget(self._checkbox)
|
||||
|
||||
self.add_buttons(
|
||||
back_btn=True,
|
||||
next_scene='Confirm', on_next=self._next)
|
||||
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):
|
||||
cn = self._cn.value
|
||||
uid = self._username.value
|
||||
self._model.confirm_lines = [
|
||||
f'Are you sure you want to add {uid} to {cn}?',
|
||||
]
|
||||
operations = AddMemberToGroupTransaction.operations
|
||||
url = f'/api/groups/{cn}/members/{uid}'
|
||||
# TODO: deduplicate this logic from the CLI
|
||||
if not self._checkbox.value:
|
||||
url += '?subscribe_to_lists=false'
|
||||
operations.remove('subscribe_user_to_auxiliary_mailing_lists')
|
||||
self._model.deferred_req = defer(http_post, url)
|
||||
self._model.operations = operations
|
|
@ -1,18 +0,0 @@
|
|||
import requests
|
||||
|
||||
from ..ResultView import ResultView
|
||||
|
||||
|
||||
class GetGroupResultView(ResultView):
|
||||
def show_result(self, resp: requests.Response):
|
||||
d = resp.json()
|
||||
if 'description' in d:
|
||||
desc = d['description'] + ' (' + d['cn'] + ')'
|
||||
else:
|
||||
desc = d['cn']
|
||||
self._add_text('Members of ' + desc, center=True)
|
||||
self._add_text()
|
||||
for member in d['members']:
|
||||
self._add_text(
|
||||
member['cn'] + ' (' + member['uid'] + ')',
|
||||
center=True)
|
|
@ -1,33 +0,0 @@
|
|||
from asciimatics.widgets import Layout, Text
|
||||
|
||||
from ...utils import http_get
|
||||
from ..CeoFrame import CeoFrame
|
||||
|
||||
|
||||
class GetGroupView(CeoFrame):
|
||||
def __init__(self, screen, width, height, model):
|
||||
super().__init__(
|
||||
screen, height, width, model, 'GetGroup',
|
||||
)
|
||||
layout = Layout([100], fill_frame=True)
|
||||
self.add_layout(layout)
|
||||
self._cn = Text("Group name", "cn")
|
||||
layout.add_widget(self._cn)
|
||||
|
||||
self.add_flash_message_layout()
|
||||
self.add_buttons(
|
||||
back_btn=True,
|
||||
next_scene='GetGroupResult', on_next=self._next)
|
||||
self.fix()
|
||||
|
||||
def _ceoframe_on_reset(self):
|
||||
super()._ceoframe_on_reset()
|
||||
self._cn.value = None
|
||||
|
||||
def _next(self):
|
||||
cn = self._cn.value
|
||||
self.flash_message('Looking up group...', force_update=True)
|
||||
try:
|
||||
self._model.resp = http_get(f'/api/groups/{cn}')
|
||||
finally:
|
||||
self.clear_flash_message()
|
|
@ -1,48 +0,0 @@
|
|||
from asciimatics.widgets import Layout, Text, CheckBox, Label
|
||||
|
||||
from ...utils import defer, http_delete
|
||||
from ..CeoFrame import CeoFrame
|
||||
from ceod.transactions.groups import RemoveMemberFromGroupTransaction
|
||||
|
||||
|
||||
class RemoveMemberFromGroupView(CeoFrame):
|
||||
def __init__(self, screen, width, height, model):
|
||||
super().__init__(
|
||||
screen, height, width, model, 'RemoveMemberFromGroup',
|
||||
)
|
||||
layout = Layout([100], fill_frame=True)
|
||||
self.add_layout(layout)
|
||||
self._cn = Text('Group name:', 'cn')
|
||||
layout.add_widget(self._cn)
|
||||
self._username = Text('Username:', 'uid')
|
||||
layout.add_widget(self._username)
|
||||
layout.add_widget(Label(''))
|
||||
self._checkbox = CheckBox(
|
||||
'unsubscribe from auxiliary mailing lists', name='unsubscribe')
|
||||
self._checkbox.value = True
|
||||
layout.add_widget(self._checkbox)
|
||||
|
||||
self.add_buttons(
|
||||
back_btn=True,
|
||||
next_scene='Confirm', on_next=self._next)
|
||||
self.fix()
|
||||
|
||||
def _ceoframe_on_reset(self):
|
||||
super()._ceoframe_on_reset()
|
||||
self._cn.value = None
|
||||
self._username.value = None
|
||||
|
||||
def _next(self):
|
||||
cn = self._cn.value
|
||||
uid = self._username.value
|
||||
self._model.confirm_lines = [
|
||||
f'Are you sure you want to remove {uid} from {cn}?',
|
||||
]
|
||||
operations = RemoveMemberFromGroupTransaction.operations
|
||||
url = f'/api/groups/{cn}/members/{uid}'
|
||||
# TODO: deduplicate this logic from the CLI
|
||||
if not self._checkbox.value:
|
||||
url += '?unsubscribe_from_lists=false'
|
||||
operations.remove('unsubscribe_user_from_auxiliary_mailing_lists')
|
||||
self._model.deferred_req = defer(http_delete, url)
|
||||
self._model.operations = operations
|
|
@ -1,25 +0,0 @@
|
|||
from typing import List, Dict
|
||||
|
||||
from asciimatics.widgets import Label
|
||||
|
||||
from ...utils import get_failed_operations
|
||||
from ..TransactionView import TransactionView
|
||||
|
||||
|
||||
class AddUserTransactionView(TransactionView):
|
||||
def _show_msg(self, msg: str = '\n'):
|
||||
for line in msg.splitlines():
|
||||
self._msg_layout.add_widget(Label(line, align='^'))
|
||||
|
||||
def write_extra_txn_info(self, data: List[Dict]):
|
||||
if data[-1]['status'] != 'completed':
|
||||
return
|
||||
result = data[-1]['result']
|
||||
failed_operations = get_failed_operations(data)
|
||||
self._show_msg()
|
||||
self._show_msg('User password is: ' + result['password'])
|
||||
if 'send_welcome_message' in failed_operations:
|
||||
self._show_msg()
|
||||
self._show_msg('Since the welcome message was not sent,')
|
||||
self._show_msg('you need to email this password to the user.')
|
||||
self.force_update()
|
|
@ -1,116 +0,0 @@
|
|||
from threading import Thread
|
||||
|
||||
from asciimatics.widgets import Layout, Text
|
||||
|
||||
from ...term_utils import get_terms_for_new_user
|
||||
from ...utils import http_get, http_post, defer, user_dict_kv, \
|
||||
get_adduser_operations
|
||||
from ..CeoFrame import CeoFrame
|
||||
|
||||
|
||||
class AddUserView(CeoFrame):
|
||||
def __init__(self, screen, width, height, model):
|
||||
super().__init__(
|
||||
screen, height, width, model, 'AddUser',
|
||||
)
|
||||
self._username_changed = False
|
||||
|
||||
layout = Layout([100], fill_frame=True)
|
||||
self.add_layout(layout)
|
||||
self._username = Text(
|
||||
"Username:", "uid",
|
||||
on_change=self._on_username_change,
|
||||
on_blur=self._on_username_blur,
|
||||
)
|
||||
layout.add_widget(self._username)
|
||||
self._full_name = Text("Full name:", "cn")
|
||||
layout.add_widget(self._full_name)
|
||||
self._first_name = Text("First name:", "given_name")
|
||||
layout.add_widget(self._first_name)
|
||||
self._last_name = Text("Last name:", "sn")
|
||||
layout.add_widget(self._last_name)
|
||||
self._program = Text("Program:", "program")
|
||||
layout.add_widget(self._program)
|
||||
self._forwarding_address = Text("Forwarding address:", "forwarding_address")
|
||||
layout.add_widget(self._forwarding_address)
|
||||
self._num_terms = Text(
|
||||
"Number of terms:", "num_terms",
|
||||
validator=lambda s: s.isdigit() and s[0] != '0')
|
||||
self._num_terms.value = '1'
|
||||
layout.add_widget(self._num_terms)
|
||||
|
||||
self.add_flash_message_layout()
|
||||
self.add_buttons(
|
||||
back_btn=True,
|
||||
next_scene='Confirm', on_next=self._next)
|
||||
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):
|
||||
self._username_changed = True
|
||||
|
||||
def _on_username_blur(self):
|
||||
if not self._username_changed:
|
||||
return
|
||||
self._username_changed = False
|
||||
username = self._username.value
|
||||
if username == '':
|
||||
return
|
||||
Thread(target=self._get_uwldap_info, args=[username]).start()
|
||||
|
||||
def _get_uwldap_info(self, username):
|
||||
self.flash_message('Looking up user...')
|
||||
try:
|
||||
resp = http_get('/api/uwldap/' + username)
|
||||
if resp.status_code != 200:
|
||||
return
|
||||
data = resp.json()
|
||||
self._status_label.text = ''
|
||||
if data.get('cn'):
|
||||
self._full_name.value = data['cn']
|
||||
if data.get('given_name'):
|
||||
self._first_name.value = data['given_name']
|
||||
if data.get('sn'):
|
||||
self._last_name.value = data['sn']
|
||||
if data.get('program'):
|
||||
self._program.value = data.get('program', '')
|
||||
if data.get('mail_local_addresses'):
|
||||
self._forwarding_address.value = data['mail_local_addresses'][0]
|
||||
finally:
|
||||
self.clear_flash_message()
|
||||
|
||||
def _next(self):
|
||||
body = {
|
||||
'uid': self._username.value,
|
||||
'cn': self._full_name.value,
|
||||
'given_name': self._first_name.value,
|
||||
'sn': self._last_name.value,
|
||||
}
|
||||
if self._program.value:
|
||||
body['program'] = self._program.value
|
||||
if self._forwarding_address.value:
|
||||
body['forwarding_addresses'] = [self._forwarding_address.value]
|
||||
new_terms = get_terms_for_new_user(int(self._num_terms.value))
|
||||
if self._model.is_club_rep:
|
||||
body['non_member_terms'] = new_terms
|
||||
else:
|
||||
body['terms'] = new_terms
|
||||
pairs = user_dict_kv(body)
|
||||
self._model.confirm_lines = [
|
||||
'The following user will be created:',
|
||||
'',
|
||||
] + pairs + [
|
||||
'',
|
||||
'Are you sure you want to continue?',
|
||||
]
|
||||
|
||||
self._model.deferred_req = defer(http_post, '/api/members', json=body)
|
||||
self._model.operations = get_adduser_operations(body)
|
||||
self._model.txn_view_name = 'AddUserTransaction'
|
|
@ -1,75 +0,0 @@
|
|||
from threading import Thread
|
||||
|
||||
from asciimatics.widgets import Layout, Text
|
||||
|
||||
from ...utils import defer, http_patch, http_get
|
||||
from ..CeoFrame import CeoFrame
|
||||
|
||||
|
||||
class ChangeLoginShellView(CeoFrame):
|
||||
def __init__(self, screen, width, height, model):
|
||||
super().__init__(
|
||||
screen, height, width, model, 'ChangeLoginShell',
|
||||
)
|
||||
self._username_changed = False
|
||||
|
||||
layout = Layout([100], fill_frame=True)
|
||||
self.add_layout(layout)
|
||||
self._username = Text(
|
||||
"Username:", "uid",
|
||||
on_change=self._on_username_change,
|
||||
on_blur=self._on_username_blur,
|
||||
)
|
||||
layout.add_widget(self._username)
|
||||
self._login_shell = Text('Login shell:', 'login_shell')
|
||||
layout.add_widget(self._login_shell)
|
||||
|
||||
self.add_flash_message_layout()
|
||||
self.add_buttons(
|
||||
back_btn=True,
|
||||
next_scene='Confirm', on_next=self._next)
|
||||
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
|
||||
def _on_username_change(self):
|
||||
self._username_changed = True
|
||||
|
||||
def _on_username_blur(self):
|
||||
if not self._username_changed:
|
||||
return
|
||||
self._username_changed = False
|
||||
username = self._username.value
|
||||
if username == '':
|
||||
return
|
||||
Thread(target=self._get_user_info, args=[username]).start()
|
||||
|
||||
def _get_user_info(self, username):
|
||||
self.flash_message('Looking up user...')
|
||||
try:
|
||||
resp = http_get('/api/members/' + username)
|
||||
if resp.status_code != 200:
|
||||
return
|
||||
data = resp.json()
|
||||
self._login_shell.value = data['login_shell']
|
||||
finally:
|
||||
self.clear_flash_message()
|
||||
|
||||
def _next(self):
|
||||
uid = self._username.value
|
||||
login_shell = self._login_shell.value
|
||||
body = {'login_shell': login_shell}
|
||||
self._model.deferred_req = defer(
|
||||
http_patch, f'/api/members/{uid}', json=body)
|
||||
self._model.confirm_lines = [
|
||||
f"{uid}'s login shell will be changed to:",
|
||||
'',
|
||||
login_shell,
|
||||
'',
|
||||
'Are you sure you want to continue?',
|
||||
]
|
||||
self._model.operations = ['replace_login_shell']
|
|
@ -1,11 +0,0 @@
|
|||
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)
|
|
@ -1,33 +0,0 @@
|
|||
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',
|
||||
)
|
||||
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 _ceoframe_on_reset(self):
|
||||
super()._ceoframe_on_reset()
|
||||
self._username.value = None
|
||||
|
||||
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()
|
|
@ -1,65 +0,0 @@
|
|||
from asciimatics.widgets import Layout, Text
|
||||
|
||||
from ...term_utils import get_terms_for_renewal
|
||||
from ...utils import http_post, defer
|
||||
from ..CeoFrame import CeoFrame
|
||||
|
||||
|
||||
class RenewUserView(CeoFrame):
|
||||
def __init__(self, screen, width, height, model):
|
||||
super().__init__(
|
||||
screen, height, width, model, 'RenewUser',
|
||||
)
|
||||
self._model = model
|
||||
|
||||
layout = Layout([100], fill_frame=True)
|
||||
self.add_layout(layout)
|
||||
self._username = Text("Username:", "uid")
|
||||
layout.add_widget(self._username)
|
||||
self._num_terms = Text(
|
||||
"Number of terms:", "num_terms",
|
||||
validator=lambda s: s.isdigit() and s[0] != '0')
|
||||
self._num_terms.value = '1'
|
||||
layout.add_widget(self._num_terms)
|
||||
|
||||
self.add_flash_message_layout()
|
||||
self.add_buttons(
|
||||
back_btn=True,
|
||||
next_scene='Confirm', on_next=self._next)
|
||||
self.fix()
|
||||
|
||||
def _ceoframe_on_reset(self):
|
||||
super()._ceoframe_on_reset()
|
||||
self._username.value = None
|
||||
self._num_terms.value = '1'
|
||||
|
||||
def _next(self):
|
||||
uid = self._username.value
|
||||
self.flash_message('Looking up user...', force_update=True)
|
||||
try:
|
||||
new_terms = get_terms_for_renewal(
|
||||
uid,
|
||||
int(self._num_terms.value),
|
||||
self._model.is_club_rep,
|
||||
self._model,
|
||||
)
|
||||
finally:
|
||||
self.clear_flash_message()
|
||||
|
||||
body = {'uid': uid}
|
||||
if self._model.is_club_rep:
|
||||
body['non_member_terms'] = new_terms
|
||||
terms_str = 'non-member terms'
|
||||
else:
|
||||
body['terms'] = new_terms
|
||||
terms_str = 'member terms'
|
||||
|
||||
self._model.confirm_lines = [
|
||||
'The following ' + terms_str + ' will be added:',
|
||||
'',
|
||||
','.join(new_terms),
|
||||
'',
|
||||
'Are you sure you want to continue?',
|
||||
]
|
||||
self._model.deferred_req = defer(
|
||||
http_post, f'/api/members/{uid}/renew', json=body)
|
|
@ -1,12 +0,0 @@
|
|||
from ..ResultView import ResultView
|
||||
|
||||
import requests
|
||||
|
||||
|
||||
class ResetPasswordResultView(ResultView):
|
||||
def show_result(self, resp: requests.Response):
|
||||
result = resp.json()
|
||||
uid = self._model.viewdata['ResetPassword']['uid']
|
||||
self._add_text(f'The new password for {uid} is:', center=True)
|
||||
self._add_text()
|
||||
self._add_text(result['password'], center=True)
|
|
@ -1,34 +0,0 @@
|
|||
from asciimatics.widgets import Layout, Text, Label
|
||||
|
||||
from ...utils import defer, http_post
|
||||
from ..CeoFrame import CeoFrame
|
||||
|
||||
|
||||
class ResetPasswordView(CeoFrame):
|
||||
def __init__(self, screen, width, height, model):
|
||||
super().__init__(
|
||||
screen, height, width, model, 'ResetPassword',
|
||||
)
|
||||
layout = Layout([100], fill_frame=True)
|
||||
self.add_layout(layout)
|
||||
layout.add_widget(Label('Enter the username of the user whose password will be reset:'))
|
||||
self._username = Text(None, "uid")
|
||||
layout.add_widget(self._username)
|
||||
|
||||
self.add_buttons(
|
||||
back_btn=True,
|
||||
next_scene='Confirm', on_next=self._next)
|
||||
self.fix()
|
||||
|
||||
def _ceoframe_on_reset(self):
|
||||
super()._ceoframe_on_reset()
|
||||
self._username.value = None
|
||||
|
||||
def _next(self):
|
||||
uid = self._username.value
|
||||
self._model.viewdata['ResetPassword']['uid'] = uid
|
||||
self._model.confirm_lines = [
|
||||
f"Are you sure you want to reset {uid}'s password?",
|
||||
]
|
||||
self._model.deferred_req = defer(http_post, f'/api/members/{uid}/pwreset')
|
||||
self._model.result_view_name = 'ResetPasswordResult'
|
|
@ -1,80 +0,0 @@
|
|||
from threading import Thread
|
||||
|
||||
from asciimatics.widgets import Layout, Label, Text, TextBox, Widget
|
||||
|
||||
from ...utils import defer, http_patch, http_get
|
||||
from ..CeoFrame import CeoFrame
|
||||
|
||||
|
||||
class SetForwardingAddressesView(CeoFrame):
|
||||
def __init__(self, screen, width, height, model):
|
||||
super().__init__(
|
||||
screen, height, width, model, 'SetForwardingAddresses',
|
||||
)
|
||||
self._username_changed = False
|
||||
|
||||
layout = Layout([100], fill_frame=True)
|
||||
self.add_layout(layout)
|
||||
self._username = Text(
|
||||
"Username:", "uid",
|
||||
on_change=self._on_username_change,
|
||||
on_blur=self._on_username_blur,
|
||||
)
|
||||
layout.add_widget(self._username)
|
||||
self._forwarding_addresses = TextBox(
|
||||
Widget.FILL_FRAME, 'Forwarding addresses:', 'forwarding_addresses',
|
||||
line_wrap=True)
|
||||
layout.add_widget(self._forwarding_addresses)
|
||||
layout.add_widget(Label('Press <TAB> to switch widgets'))
|
||||
|
||||
self.add_flash_message_layout()
|
||||
self.add_buttons(
|
||||
back_btn=True,
|
||||
next_scene='Confirm', on_next=self._next)
|
||||
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
|
||||
def _on_username_change(self):
|
||||
self._username_changed = True
|
||||
|
||||
def _on_username_blur(self):
|
||||
if not self._username_changed:
|
||||
return
|
||||
self._username_changed = False
|
||||
username = self._username.value
|
||||
if username == '':
|
||||
return
|
||||
Thread(target=self._get_user_info, args=[username]).start()
|
||||
|
||||
def _get_user_info(self, username):
|
||||
self.flash_message('Looking up user...')
|
||||
try:
|
||||
resp = http_get('/api/members/' + username)
|
||||
if resp.status_code != 200:
|
||||
return
|
||||
data = resp.json()
|
||||
if 'forwarding_addresses' not in data:
|
||||
return
|
||||
self._forwarding_addresses.value = data['forwarding_addresses']
|
||||
finally:
|
||||
self.clear_flash_message()
|
||||
|
||||
def _next(self):
|
||||
uid = self._username.value
|
||||
forwarding_addresses = self._forwarding_addresses.value
|
||||
body = {'forwarding_addresses': forwarding_addresses}
|
||||
self._model.deferred_req = defer(
|
||||
http_patch, f'/api/members/{uid}', json=body)
|
||||
self._model.confirm_lines = [
|
||||
f"{uid}'s forwarding addresses will be set to:",
|
||||
'',
|
||||
*forwarding_addresses,
|
||||
'',
|
||||
'Are you sure you want to continue?',
|
||||
]
|
||||
self._model.operations = ['replace_forwarding_addresses']
|
|
@ -1,80 +0,0 @@
|
|||
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
|
||||
)
|
||||
self._position_widgets = {}
|
||||
|
||||
def _on_load(self):
|
||||
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)
|
||||
|
||||
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 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 _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):
|
||||
super()._ceoframe_on_reset()
|
||||
# clear the labels
|
||||
for widget in self._position_widgets.values():
|
||||
widget.text = ''
|
|
@ -1,76 +0,0 @@
|
|||
from threading import Thread
|
||||
|
||||
from asciimatics.widgets import Layout, Label, Text
|
||||
from zope import component
|
||||
|
||||
from ...utils import defer, http_post, http_get
|
||||
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):
|
||||
|
||||
"""
|
||||
Reset the positions to the currently set positions
|
||||
"""
|
||||
def reset_positions(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._widgets[pos].value = username
|
||||
finally:
|
||||
self.clear_flash_message(force_update=True)
|
||||
Thread(target=target).start()
|
||||
|
||||
def __init__(self, screen, width, height, model):
|
||||
super().__init__(
|
||||
screen, height, width, model, 'SetPositions',
|
||||
)
|
||||
cfg = component.getUtility(IConfig)
|
||||
avail = cfg.get('positions_available')
|
||||
required = cfg.get('positions_required')
|
||||
|
||||
layout = Layout([100], fill_frame=True)
|
||||
self.add_layout(layout)
|
||||
self._widgets = {}
|
||||
for pos in avail:
|
||||
suffix = ' (*)' if pos in required else ''
|
||||
widget = Text(position_names[pos] + suffix, pos)
|
||||
self._widgets[pos] = widget
|
||||
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.reset_positions()
|
||||
self.fix()
|
||||
|
||||
def _ceoframe_on_reset(self):
|
||||
super()._ceoframe_on_reset()
|
||||
self.reset_positions()
|
||||
|
||||
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?',
|
||||
]
|
|
@ -1,2 +0,0 @@
|
|||
from .GetPositionsView import GetPositionsView
|
||||
from .SetPositionsView import SetPositionsView
|
Loading…
Reference in New Issue