rewrite TUI using urwid
This commit is contained in:
parent
19496b4568
commit
2bda75d905
|
@ -28,13 +28,16 @@ POSTGRES_DIR=/etc/postgresql/11/main
|
|||
cat <<EOF > $POSTGRES_DIR/pg_hba.conf
|
||||
# TYPE DATABASE USER ADDRESS METHOD
|
||||
local all postgres peer
|
||||
host all postgres localhost md5
|
||||
host all postgres 0.0.0.0/0 md5
|
||||
host all postgres ::/0 md5
|
||||
|
||||
local all all peer
|
||||
host all all localhost md5
|
||||
|
||||
local sameuser all peer
|
||||
host sameuser all 0.0.0.0/0 md5
|
||||
host sameuser all ::/0 md5
|
||||
EOF
|
||||
grep -Eq "^listen_addresses = '*'$" $POSTGRES_DIR/postgresql.conf || \
|
||||
echo "listen_addresses = '*'" >> $POSTGRES_DIR/postgresql.conf
|
||||
|
|
|
@ -15,13 +15,13 @@ def get_terms_for_new_user(num_terms: int) -> List[str]:
|
|||
|
||||
|
||||
def get_terms_for_renewal(
|
||||
username: str, num_terms: int, clubrep: bool, tui_model=None,
|
||||
username: str, num_terms: int, clubrep: bool, tui_controller=None,
|
||||
) -> List[str]:
|
||||
resp = http_get('/api/members/' + username)
|
||||
if tui_model is None:
|
||||
if tui_controller is None:
|
||||
result = cli_utils.handle_sync_response(resp)
|
||||
else:
|
||||
result = tui_utils.handle_sync_response(resp, tui_model)
|
||||
result = tui_utils.handle_sync_response(resp, tui_controller)
|
||||
max_term = None
|
||||
current_term = Term.current()
|
||||
if clubrep and 'non_member_terms' in result:
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
import os
|
||||
from queue import SimpleQueue
|
||||
|
||||
|
||||
class App:
|
||||
REL_WIDTH_PCT = 60
|
||||
REL_HEIGHT_PCT = 60
|
||||
# On a full-screen (1366x768) gnome-terminal window,
|
||||
# I had 168 cols and 36 rows
|
||||
WIDTH = int(0.6 * 168)
|
||||
HEIGHT = int(0.6 * 36)
|
||||
|
||||
def __init__(self, loop, main_widget):
|
||||
self.loop = loop
|
||||
self.main_widget = main_widget
|
||||
self.history = []
|
||||
self.queued_pipe_callbacks = SimpleQueue()
|
||||
self.pipefd = loop.watch_pipe(self._pipe_callback)
|
||||
|
||||
def run_in_main_loop(self, func):
|
||||
self.queued_pipe_callbacks.put(func)
|
||||
os.write(self.pipefd, b'\x00')
|
||||
|
||||
def _pipe_callback(self, data):
|
||||
# We need to clear the whole queue because select()
|
||||
# will only send one "notification" if there are two
|
||||
# consecutive writes
|
||||
while not self.queued_pipe_callbacks.empty():
|
||||
self.queued_pipe_callbacks.get()()
|
||||
return True
|
|
@ -0,0 +1,36 @@
|
|||
from .Controller import Controller
|
||||
from .TransactionController import TransactionController
|
||||
from ceo.tui.models import TransactionModel
|
||||
from ceo.tui.views import AddGroupConfirmationView, TransactionView
|
||||
from ceod.transactions.groups import AddGroupTransaction
|
||||
|
||||
|
||||
class AddGroupController(Controller):
|
||||
def __init__(self, model, app):
|
||||
super().__init__(model, app)
|
||||
|
||||
def on_next_button_pressed(self, button):
|
||||
try:
|
||||
self.model.name = self.get_group_name_from_view()
|
||||
self.model.description = self.view.description_edit.edit_text
|
||||
if not self.model.description:
|
||||
self.view.popup('Description must not be empty')
|
||||
raise Controller.InvalidInput()
|
||||
except Controller.InvalidInput:
|
||||
return
|
||||
view = AddGroupConfirmationView(self.model, self, self.app)
|
||||
self.switch_to_view(view)
|
||||
|
||||
def on_confirmation_button_pressed(self, button):
|
||||
body = {
|
||||
'cn': self.model.name,
|
||||
'description': self.model.description,
|
||||
}
|
||||
model = TransactionModel(
|
||||
AddGroupTransaction.operations,
|
||||
'POST', '/api/groups', json=body
|
||||
)
|
||||
controller = TransactionController(model, self.app)
|
||||
view = TransactionView(model, controller, self.app)
|
||||
controller.view = view
|
||||
self.switch_to_view(view)
|
|
@ -0,0 +1,37 @@
|
|||
from .Controller import Controller
|
||||
from ceod.transactions.groups import AddMemberToGroupTransaction
|
||||
from .TransactionController import TransactionController
|
||||
from ceo.tui.models import TransactionModel
|
||||
from ceo.tui.views import AddMemberToGroupConfirmationView, TransactionView
|
||||
|
||||
|
||||
class AddMemberToGroupController(Controller):
|
||||
def __init__(self, model, app):
|
||||
super().__init__(model, app)
|
||||
|
||||
def on_list_subscribe_checkbox_change(self, checkbox, new_state):
|
||||
self.model.subscribe_to_lists = new_state
|
||||
|
||||
def on_next_button_pressed(self, button):
|
||||
try:
|
||||
self.model.name = self.get_group_name_from_view()
|
||||
self.model.username = self.get_username_from_view()
|
||||
except Controller.InvalidInput:
|
||||
return
|
||||
view = AddMemberToGroupConfirmationView(self.model, self, self.app)
|
||||
self.switch_to_view(view)
|
||||
|
||||
def on_confirmation_button_pressed(self, button):
|
||||
cn = self.model.name
|
||||
uid = self.model.username
|
||||
url = f'/api/groups/{cn}/members/{uid}'
|
||||
if not self.model.subscribe_to_lists:
|
||||
url += '?subscribe_to_lists=false'
|
||||
model = TransactionModel(
|
||||
AddMemberToGroupTransaction.operations,
|
||||
'POST', url
|
||||
)
|
||||
controller = TransactionController(model, self.app)
|
||||
view = TransactionView(model, controller, self.app)
|
||||
controller.view = view
|
||||
self.switch_to_view(view)
|
|
@ -0,0 +1,110 @@
|
|||
from threading import Thread
|
||||
|
||||
from ...utils import http_get
|
||||
from .Controller import Controller
|
||||
from .AddUserTransactionController import AddUserTransactionController
|
||||
import ceo.term_utils as term_utils
|
||||
from ceo.tui.models import TransactionModel
|
||||
from ceo.tui.views import AddUserConfirmationView, TransactionView
|
||||
from ceod.transactions.members import AddMemberTransaction
|
||||
|
||||
|
||||
class AddUserController(Controller):
|
||||
def __init__(self, model, app):
|
||||
super().__init__(model, app)
|
||||
self.right_col_idx = 0
|
||||
self.prev_searched_username = None
|
||||
|
||||
def on_confirmation_button_pressed(self, button):
|
||||
body = {
|
||||
'uid': self.model.username,
|
||||
'cn': self.model.full_name,
|
||||
'given_name': self.model.first_name,
|
||||
'sn': self.model.last_name,
|
||||
}
|
||||
if self.model.program:
|
||||
body['program'] = self.model.program
|
||||
if self.model.forwarding_address:
|
||||
body['forwarding_addresses'] = [self.model.forwarding_address]
|
||||
new_terms = term_utils.get_terms_for_new_user(self.model.num_terms)
|
||||
if self.model.membership_type == 'club_rep':
|
||||
body['non_member_terms'] = new_terms
|
||||
else:
|
||||
body['terms'] = new_terms
|
||||
|
||||
model = TransactionModel(
|
||||
AddMemberTransaction.operations,
|
||||
'POST', '/api/members',
|
||||
json=body
|
||||
)
|
||||
controller = AddUserTransactionController(model, self.app)
|
||||
view = TransactionView(model, controller, self.app)
|
||||
controller.view = view
|
||||
self.switch_to_view(view)
|
||||
|
||||
def on_next_button_pressed(self, button):
|
||||
try:
|
||||
username = self.get_username_from_view()
|
||||
num_terms = self.get_num_terms_from_view()
|
||||
except Controller.InvalidInput:
|
||||
return
|
||||
full_name = self.view.full_name_edit.edit_text
|
||||
# TODO: share validation logic between CLI and TUI
|
||||
if not full_name:
|
||||
self.view.popup('Full name must not be empty')
|
||||
return
|
||||
self.model.username = username
|
||||
self.model.full_name = full_name
|
||||
self.model.first_name = self.view.first_name_edit.edit_text
|
||||
self.model.last_name = self.view.last_name_edit.edit_text
|
||||
self.model.program = self.view.program_edit.edit_text
|
||||
self.model.forwarding_address = self.view.forwarding_address_edit.edit_text
|
||||
self.model.num_terms = num_terms
|
||||
view = AddUserConfirmationView(self.model, self, self.app)
|
||||
self.switch_to_view(view)
|
||||
|
||||
def on_membership_type_changed(self, radio_button, new_state, selected_type):
|
||||
if new_state:
|
||||
self.model.membership_type = selected_type
|
||||
|
||||
def on_row_focus_changed(self):
|
||||
_, idx = self.view.listwalker.get_focus()
|
||||
old_idx = self.right_col_idx
|
||||
self.right_col_idx = idx
|
||||
# The username field is the third row, so when
|
||||
# idx changes from 2 to 3, this means the user
|
||||
# moved from the username field to the next field
|
||||
if old_idx == 2 and idx == 3:
|
||||
Thread(
|
||||
target=self._lookup_user,
|
||||
args=(self.view.username_edit.edit_text,)
|
||||
).start()
|
||||
|
||||
def _set_flash_text(self, *args):
|
||||
self.view.flash_text.set_text('Looking up user...')
|
||||
|
||||
def _clear_flash_text(self):
|
||||
self.view.flash_text.set_text('')
|
||||
|
||||
def _on_lookup_user_success(self):
|
||||
self._clear_flash_text()
|
||||
self.view.update_fields()
|
||||
|
||||
def _lookup_user(self, username):
|
||||
if not username:
|
||||
return
|
||||
if username == self.prev_searched_username:
|
||||
return
|
||||
self.prev_searched_username = username
|
||||
self.app.run_in_main_loop(self._set_flash_text)
|
||||
resp = http_get('/api/uwldap/' + username)
|
||||
if not resp.ok:
|
||||
self.app.run_in_main_loop(self._clear_flash_text)
|
||||
return
|
||||
data = resp.json()
|
||||
self.model.full_name = data.get('cn', '')
|
||||
self.model.first_name = data.get('given_name', '')
|
||||
self.model.last_name = data.get('sn')
|
||||
self.model.program = data.get('program', '')
|
||||
self.model.forwarding_address = data.get('mail_local_addresses', [''])[0]
|
||||
self.app.run_in_main_loop(self._on_lookup_user_success)
|
|
@ -0,0 +1,38 @@
|
|||
from typing import Dict, List
|
||||
|
||||
from ...utils import get_failed_operations
|
||||
from .TransactionController import TransactionController
|
||||
|
||||
|
||||
class AddUserTransactionController(TransactionController):
|
||||
def __init__(self, model, app):
|
||||
super().__init__(model, app)
|
||||
|
||||
def handle_completed(self):
|
||||
# We don't want to write to the message_text yet, but
|
||||
# we still need to enable the Next button.
|
||||
self.app.run_in_main_loop(self.view.enable_next_button)
|
||||
|
||||
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)
|
||||
lines = []
|
||||
if failed_operations:
|
||||
lines.append('Transaction successfully completed with some errors.')
|
||||
else:
|
||||
lines.append('Transaction successfully completed.')
|
||||
lines.append('')
|
||||
lines.append('User password is: ' + result['password'])
|
||||
if 'send_welcome_message' in failed_operations:
|
||||
lines.extend([
|
||||
'',
|
||||
'Since the welcome message was not sent, '
|
||||
'you need to email this password to the user.'
|
||||
])
|
||||
|
||||
def target():
|
||||
self._show_lines(lines)
|
||||
|
||||
self.app.run_in_main_loop(target)
|
|
@ -0,0 +1,79 @@
|
|||
from threading import Thread
|
||||
|
||||
from ...utils import http_get
|
||||
from .Controller import Controller
|
||||
from .TransactionController import TransactionController
|
||||
from ceo.tui.models import TransactionModel
|
||||
from ceo.tui.views import ChangeLoginShellConfirmationView, TransactionView
|
||||
|
||||
|
||||
class ChangeLoginShellController(Controller):
|
||||
def __init__(self, model, app):
|
||||
super().__init__(model, app)
|
||||
self.right_col_idx = 0
|
||||
self.prev_searched_username = None
|
||||
|
||||
def on_next_button_pressed(self, button):
|
||||
try:
|
||||
self.model.username = self.get_username_from_view()
|
||||
self.model.login_shell = self.view.login_shell_edit.edit_text
|
||||
if not self.model.login_shell:
|
||||
self.view.popup('Login shell must not be empty')
|
||||
raise Controller.InvalidInput()
|
||||
except Controller.InvalidInput:
|
||||
return
|
||||
view = ChangeLoginShellConfirmationView(self.model, self, self.app)
|
||||
self.switch_to_view(view)
|
||||
|
||||
def on_confirmation_button_pressed(self, button):
|
||||
body = {'login_shell': self.model.login_shell}
|
||||
model = TransactionModel(
|
||||
['replace_login_shell'],
|
||||
'PATCH', f'/api/members/{self.model.username}',
|
||||
json=body
|
||||
)
|
||||
controller = TransactionController(model, self.app)
|
||||
view = TransactionView(model, controller, self.app)
|
||||
controller.view = view
|
||||
self.switch_to_view(view)
|
||||
|
||||
# TODO: reduce code duplication with AddUserController
|
||||
|
||||
def on_row_focus_changed(self):
|
||||
_, idx = self.view.listwalker.get_focus()
|
||||
old_idx = self.right_col_idx
|
||||
self.right_col_idx = idx
|
||||
# The username field is the first row, so when
|
||||
# idx changes from 0 to 1, this means the user
|
||||
# moved from the username field to the next field
|
||||
if old_idx == 0 and idx == 1:
|
||||
Thread(
|
||||
target=self._lookup_user,
|
||||
args=(self.view.username_edit.edit_text,)
|
||||
).start()
|
||||
|
||||
def _set_flash_text(self, *args):
|
||||
self.view.flash_text.set_text('Looking up user...')
|
||||
|
||||
def _clear_flash_text(self):
|
||||
self.view.flash_text.set_text('')
|
||||
|
||||
def _on_lookup_user_success(self):
|
||||
self._clear_flash_text()
|
||||
self.view.update_fields()
|
||||
|
||||
def _lookup_user(self, username):
|
||||
if not username:
|
||||
return
|
||||
if username == self.prev_searched_username:
|
||||
return
|
||||
self.prev_searched_username = username
|
||||
self.app.run_in_main_loop(self._set_flash_text)
|
||||
resp = http_get('/api/members/' + username)
|
||||
if not resp.ok:
|
||||
self.app.run_in_main_loop(self._clear_flash_text)
|
||||
return
|
||||
data = resp.json()
|
||||
self.model.login_shell = data.get('login_shell', '')
|
||||
|
||||
self.app.run_in_main_loop(self._on_lookup_user_success)
|
|
@ -0,0 +1,78 @@
|
|||
from abc import ABC
|
||||
|
||||
import ceo.tui.utils as utils
|
||||
|
||||
|
||||
# NOTE: one controller can control multiple views,
|
||||
# but each view must have exactly one controller
|
||||
class Controller(ABC):
|
||||
class InvalidInput(Exception):
|
||||
pass
|
||||
|
||||
class RequestFailed(Exception):
|
||||
pass
|
||||
|
||||
def __init__(self, model, app):
|
||||
super().__init__()
|
||||
self.model = model
|
||||
self.app = app
|
||||
# Since the view and the controller both have a reference to each
|
||||
# other, this needs to be initialized in a separate step
|
||||
self.view = None
|
||||
|
||||
def _push_history(self, old_view, new_view):
|
||||
if new_view.model.name == 'Welcome':
|
||||
self.app.history.clear()
|
||||
else:
|
||||
self.app.history.append(old_view)
|
||||
|
||||
def switch_to_view(self, new_view):
|
||||
self._push_history(self.view, new_view)
|
||||
self.view = new_view
|
||||
new_view.activate()
|
||||
|
||||
def go_to_next_menu(self, next_menu_name):
|
||||
_, new_view, _ = utils.get_mvc(self.app, next_menu_name)
|
||||
self._push_history(self.view, new_view)
|
||||
new_view.activate()
|
||||
|
||||
def prev_menu_callback(self, button):
|
||||
prev_view = self.app.history.pop()
|
||||
prev_view.controller.view = prev_view
|
||||
prev_view.activate()
|
||||
|
||||
def next_menu_callback(self, button, next_menu_name):
|
||||
self.go_to_next_menu(next_menu_name)
|
||||
|
||||
def get_next_menu_callback(self, next_menu_name):
|
||||
def callback(button):
|
||||
self.next_menu_callback(button, next_menu_name)
|
||||
return callback
|
||||
|
||||
def get_username_from_view(self):
|
||||
username = self.view.username_edit.edit_text
|
||||
# TODO: share validation logic between CLI and TUI
|
||||
if not username:
|
||||
self.view.popup('Username must not be empty')
|
||||
raise Controller.InvalidInput()
|
||||
return username
|
||||
|
||||
def get_group_name_from_view(self):
|
||||
name = self.view.name_edit.edit_text
|
||||
# TODO: share validation logic between CLI and TUI
|
||||
if not name:
|
||||
self.view.popup('Name must not be empty')
|
||||
raise Controller.InvalidInput()
|
||||
return name
|
||||
|
||||
def get_num_terms_from_view(self):
|
||||
num_terms_str = self.view.num_terms_edit.edit_text
|
||||
if num_terms_str:
|
||||
num_terms = int(num_terms_str)
|
||||
else:
|
||||
num_terms = 0
|
||||
# TODO: share validation logic between CLI and TUI
|
||||
if num_terms <= 0:
|
||||
self.view.popup('Number of terms must be a positive integer')
|
||||
raise Controller.InvalidInput()
|
||||
return num_terms
|
|
@ -0,0 +1,53 @@
|
|||
import os
|
||||
|
||||
from zope import component
|
||||
|
||||
from ...utils import http_get, http_post, write_db_creds
|
||||
from .Controller import Controller
|
||||
from .SyncRequestController import SyncRequestController
|
||||
import ceo.krb_check as krb
|
||||
from ceo.tui.views import CreateDatabaseConfirmationView, CreateDatabaseResponseView
|
||||
from ceo_common.interfaces import IConfig
|
||||
|
||||
|
||||
class CreateDatabaseController(SyncRequestController):
|
||||
def __init__(self, model, app):
|
||||
super().__init__(model, app)
|
||||
|
||||
def on_db_type_changed(self, radio_button, new_state, selected_type):
|
||||
if new_state:
|
||||
self.model.db_type = selected_type
|
||||
|
||||
def on_next_button_pressed(self, button):
|
||||
view = CreateDatabaseConfirmationView(self.model, self, self.app)
|
||||
self.switch_to_view(view)
|
||||
|
||||
def get_resp(self):
|
||||
db_type = self.model.db_type
|
||||
username = krb.get_username()
|
||||
resp = http_get(f'/api/members/{username}')
|
||||
if not resp.ok:
|
||||
return resp
|
||||
self.model.user_dict = resp.json()
|
||||
return http_post(f'/api/db/{db_type}/{username}')
|
||||
|
||||
def get_response_view(self):
|
||||
return CreateDatabaseResponseView(self.model, self, self.app)
|
||||
|
||||
def write_db_creds_to_file(self):
|
||||
password = self.model.resp_json['password']
|
||||
db_type = self.model.db_type
|
||||
db_type_name = 'MySQL' if db_type == 'mysql' else 'PostgreSQL'
|
||||
cfg = component.getUtility(IConfig)
|
||||
db_host = cfg.get(f'{db_type}_host')
|
||||
username = krb.get_username()
|
||||
homedir = self.model.user_dict['home_directory']
|
||||
filename = os.path.join(homedir, f"ceo-{db_type}-info")
|
||||
wrote_to_file = write_db_creds(
|
||||
filename, self.model.user_dict, password, db_type, db_host
|
||||
)
|
||||
|
||||
self.model.password = password
|
||||
self.model.db_host = db_host
|
||||
self.model.filename = filename
|
||||
self.model.wrote_to_file = wrote_to_file
|
|
@ -0,0 +1,22 @@
|
|||
from ...utils import http_get
|
||||
from .Controller import Controller
|
||||
from .SyncRequestController import SyncRequestController
|
||||
from ceo.tui.views import GetGroupResponseView
|
||||
|
||||
|
||||
class GetGroupController(SyncRequestController):
|
||||
def __init__(self, model, app):
|
||||
super().__init__(model, app)
|
||||
|
||||
def get_resp(self):
|
||||
return http_get(f'/api/groups/{self.model.name}')
|
||||
|
||||
def get_response_view(self):
|
||||
return GetGroupResponseView(self.model, self, self.app)
|
||||
|
||||
def on_next_button_pressed(self, button):
|
||||
try:
|
||||
self.model.name = self.get_group_name_from_view()
|
||||
except Controller.InvalidInput:
|
||||
return
|
||||
self.on_confirmation_button_pressed(button)
|
|
@ -0,0 +1,29 @@
|
|||
from threading import Thread
|
||||
|
||||
from ...utils import http_get
|
||||
from .Controller import Controller
|
||||
import ceo.tui.utils as tui_utils
|
||||
|
||||
|
||||
class GetPositionsController(Controller):
|
||||
def __init__(self, model, app):
|
||||
super().__init__(model, app)
|
||||
|
||||
def lookup_positions_async(self):
|
||||
self.view.flash_text.set_text('Looking up positions...')
|
||||
Thread(target=self.lookup_positions_sync).start()
|
||||
|
||||
def lookup_positions_sync(self):
|
||||
resp = http_get('/api/positions')
|
||||
try:
|
||||
positions = tui_utils.handle_sync_response(resp, self)
|
||||
except Controller.RequestFailed:
|
||||
return
|
||||
for pos, username in positions.items():
|
||||
self.model.positions[pos] = username
|
||||
|
||||
def target():
|
||||
self.view.flash_text.set_text('')
|
||||
self.view.update_fields()
|
||||
|
||||
self.app.run_in_main_loop(target)
|
|
@ -0,0 +1,22 @@
|
|||
from ...utils import http_get
|
||||
from .Controller import Controller
|
||||
from .SyncRequestController import SyncRequestController
|
||||
from ceo.tui.views import GetUserResponseView
|
||||
|
||||
|
||||
class GetUserController(SyncRequestController):
|
||||
def __init__(self, model, app):
|
||||
super().__init__(model, app)
|
||||
|
||||
def get_resp(self):
|
||||
return http_get(f'/api/members/{self.model.username}')
|
||||
|
||||
def get_response_view(self):
|
||||
return GetUserResponseView(self.model, self, self.app)
|
||||
|
||||
def on_next_button_pressed(self, button):
|
||||
try:
|
||||
self.model.username = self.get_username_from_view()
|
||||
except Controller.InvalidInput:
|
||||
return
|
||||
self.on_confirmation_button_pressed(button)
|
|
@ -0,0 +1,37 @@
|
|||
from .Controller import Controller
|
||||
from ceod.transactions.groups import RemoveMemberFromGroupTransaction
|
||||
from .TransactionController import TransactionController
|
||||
from ceo.tui.models import TransactionModel
|
||||
from ceo.tui.views import RemoveMemberFromGroupConfirmationView, TransactionView
|
||||
|
||||
|
||||
class RemoveMemberFromGroupController(Controller):
|
||||
def __init__(self, model, app):
|
||||
super().__init__(model, app)
|
||||
|
||||
def on_list_unsubscribe_checkbox_change(self, checkbox, new_state):
|
||||
self.model.unsubscribe_from_lists = new_state
|
||||
|
||||
def on_next_button_pressed(self, button):
|
||||
try:
|
||||
self.model.name = self.get_group_name_from_view()
|
||||
self.model.username = self.get_username_from_view()
|
||||
except Controller.InvalidInput:
|
||||
return
|
||||
view = RemoveMemberFromGroupConfirmationView(self.model, self, self.app)
|
||||
self.switch_to_view(view)
|
||||
|
||||
def on_confirmation_button_pressed(self, button):
|
||||
cn = self.model.name
|
||||
uid = self.model.username
|
||||
url = f'/api/groups/{cn}/members/{uid}'
|
||||
if not self.model.unsubscribe_from_lists:
|
||||
url += '?unsubscribe_from_lists=false'
|
||||
model = TransactionModel(
|
||||
RemoveMemberFromGroupTransaction.operations,
|
||||
'DELETE', url
|
||||
)
|
||||
controller = TransactionController(model, self.app)
|
||||
view = TransactionView(model, controller, self.app)
|
||||
controller.view = view
|
||||
self.switch_to_view(view)
|
|
@ -0,0 +1,54 @@
|
|||
from threading import Thread
|
||||
|
||||
from ...utils import http_post
|
||||
from .Controller import Controller
|
||||
from .SyncRequestController import SyncRequestController
|
||||
import ceo.term_utils as term_utils
|
||||
from ceo.tui.views import RenewUserConfirmationView
|
||||
|
||||
|
||||
class RenewUserController(SyncRequestController):
|
||||
def __init__(self, model, app):
|
||||
super().__init__(model, app)
|
||||
|
||||
def on_membership_type_changed(self, radio_button, new_state, selected_type):
|
||||
if new_state:
|
||||
self.model.membership_type = selected_type
|
||||
|
||||
def on_next_button_pressed(self, button):
|
||||
try:
|
||||
username = self.get_username_from_view()
|
||||
num_terms = self.get_num_terms_from_view()
|
||||
except Controller.InvalidInput:
|
||||
return
|
||||
self.model.username = username
|
||||
self.model.num_terms = num_terms
|
||||
self.view.flash_text.set_text('Looking up user...')
|
||||
Thread(target=self._get_next_terms).start()
|
||||
|
||||
def _get_next_terms(self):
|
||||
try:
|
||||
self.model.new_terms = term_utils.get_terms_for_renewal(
|
||||
self.model.username,
|
||||
self.model.num_terms,
|
||||
self.model.membership_type == 'club_rep',
|
||||
self
|
||||
)
|
||||
except Controller.RequestFailed:
|
||||
return
|
||||
|
||||
def target():
|
||||
self.view.flash_text.set_text('')
|
||||
view = RenewUserConfirmationView(self.model, self, self.app)
|
||||
self.switch_to_view(view)
|
||||
|
||||
self.app.run_in_main_loop(target)
|
||||
|
||||
def get_resp(self):
|
||||
uid = self.model.username
|
||||
body = {'uid': uid}
|
||||
if self.model.membership_type == 'club_rep':
|
||||
body['non_member_terms'] = self.model.new_terms
|
||||
else:
|
||||
body['terms'] = self.model.new_terms
|
||||
return http_post(f'/api/members/{uid}/renew', json=body)
|
|
@ -0,0 +1,52 @@
|
|||
import os
|
||||
|
||||
from zope import component
|
||||
|
||||
from ...utils import http_get, http_post, write_db_creds
|
||||
from .Controller import Controller
|
||||
from .SyncRequestController import SyncRequestController
|
||||
import ceo.krb_check as krb
|
||||
from ceo.tui.views import ResetDatabasePasswordConfirmationView, ResetDatabasePasswordResponseView
|
||||
from ceo_common.interfaces import IConfig
|
||||
|
||||
|
||||
class ResetDatabasePasswordController(SyncRequestController):
|
||||
def __init__(self, model, app):
|
||||
super().__init__(model, app)
|
||||
|
||||
def on_db_type_changed(self, radio_button, new_state, selected_type):
|
||||
if new_state:
|
||||
self.model.db_type = selected_type
|
||||
|
||||
def on_next_button_pressed(self, button):
|
||||
view = ResetDatabasePasswordConfirmationView(self.model, self, self.app)
|
||||
self.switch_to_view(view)
|
||||
|
||||
def get_resp(self):
|
||||
db_type = self.model.db_type
|
||||
username = krb.get_username()
|
||||
resp = http_get(f'/api/members/{username}')
|
||||
if not resp.ok:
|
||||
return resp
|
||||
self.model.user_dict = resp.json()
|
||||
return http_post(f'/api/db/{db_type}/{username}/pwreset')
|
||||
|
||||
def get_response_view(self):
|
||||
return ResetDatabasePasswordResponseView(self.model, self, self.app)
|
||||
|
||||
def write_db_creds_to_file(self):
|
||||
password = self.model.resp_json['password']
|
||||
db_type = self.model.db_type
|
||||
db_type_name = 'MySQL' if db_type == 'mysql' else 'PostgreSQL'
|
||||
cfg = component.getUtility(IConfig)
|
||||
db_host = cfg.get(f'{db_type}_host')
|
||||
username = self.model.user_dict['uid']
|
||||
homedir = self.model.user_dict['home_directory']
|
||||
filename = os.path.join(homedir, f"ceo-{db_type}-info")
|
||||
wrote_to_file = write_db_creds(
|
||||
filename, self.model.user_dict, password, db_type, db_host
|
||||
)
|
||||
|
||||
self.model.password = password
|
||||
self.model.filename = filename
|
||||
self.model.wrote_to_file = wrote_to_file
|
|
@ -0,0 +1,27 @@
|
|||
from ...utils import http_post
|
||||
from .Controller import Controller
|
||||
from .SyncRequestController import SyncRequestController
|
||||
import ceo.krb_check as krb
|
||||
from ceo.tui.views import ResetPasswordUsePasswdView, ResetPasswordConfirmationView, ResetPasswordResponseView
|
||||
|
||||
|
||||
class ResetPasswordController(SyncRequestController):
|
||||
def __init__(self, model, app):
|
||||
super().__init__(model, app)
|
||||
|
||||
def get_resp(self):
|
||||
return http_post(f'/api/members/{self.model.username}/pwreset')
|
||||
|
||||
def get_response_view(self):
|
||||
return ResetPasswordResponseView(self.model, self, self.app)
|
||||
|
||||
def on_next_button_pressed(self, button):
|
||||
try:
|
||||
self.model.username = self.get_username_from_view()
|
||||
except Controller.InvalidInput:
|
||||
return
|
||||
if self.model.username == krb.get_username():
|
||||
view = ResetPasswordUsePasswdView(self.model, self, self.app)
|
||||
else:
|
||||
view = ResetPasswordConfirmationView(self.model, self, self.app)
|
||||
self.switch_to_view(view)
|
|
@ -0,0 +1,47 @@
|
|||
from threading import Thread
|
||||
|
||||
from ...utils import http_get
|
||||
from .Controller import Controller
|
||||
from .TransactionController import TransactionController
|
||||
from ceo.tui.models import TransactionModel
|
||||
import ceo.tui.utils as tui_utils
|
||||
from ceo.tui.views import TransactionView
|
||||
from ceod.transactions.members import UpdateMemberPositionsTransaction
|
||||
|
||||
|
||||
class SetPositionsController(Controller):
|
||||
def __init__(self, model, app):
|
||||
super().__init__(model, app)
|
||||
|
||||
def on_next_button_pressed(self, button):
|
||||
body = {}
|
||||
for pos, field in self.view.position_fields.items():
|
||||
if field.edit_text != '':
|
||||
body[pos] = field.edit_text
|
||||
model = TransactionModel(
|
||||
UpdateMemberPositionsTransaction.operations,
|
||||
'POST', '/api/positions', json=body
|
||||
)
|
||||
controller = TransactionController(model, self.app)
|
||||
view = TransactionView(model, controller, self.app)
|
||||
controller.view = view
|
||||
self.switch_to_view(view)
|
||||
|
||||
def lookup_positions_async(self):
|
||||
self.view.flash_text.set_text('Looking up positions...')
|
||||
Thread(target=self.lookup_positions_sync).start()
|
||||
|
||||
def lookup_positions_sync(self):
|
||||
resp = http_get('/api/positions')
|
||||
try:
|
||||
positions = tui_utils.handle_sync_response(resp, self)
|
||||
except Controller.RequestFailed:
|
||||
return
|
||||
for pos, username in positions.items():
|
||||
self.model.positions[pos] = username
|
||||
|
||||
def target():
|
||||
self.view.flash_text.set_text('')
|
||||
self.view.update_fields()
|
||||
|
||||
self.app.run_in_main_loop(target)
|
|
@ -0,0 +1,39 @@
|
|||
from threading import Thread
|
||||
|
||||
from .Controller import Controller
|
||||
import ceo.tui.utils as tui_utils
|
||||
from ceo.tui.views import SyncResponseView
|
||||
|
||||
|
||||
class SyncRequestController(Controller):
|
||||
def __init__(self, model, app):
|
||||
super().__init__(model, app)
|
||||
self.request_in_progress = False
|
||||
|
||||
def get_resp(self):
|
||||
# To be implemented by child classes
|
||||
raise NotImplementedError()
|
||||
|
||||
def get_response_view(self):
|
||||
return SyncResponseView(self.model, self, self.app)
|
||||
|
||||
def on_confirmation_button_pressed(self, button):
|
||||
if self.request_in_progress:
|
||||
return
|
||||
self.request_in_progress = True
|
||||
self.view.flash_text.set_text('Sending request...')
|
||||
|
||||
def main_loop_target():
|
||||
self.view.flash_text.set_text('')
|
||||
view = self.get_response_view()
|
||||
self.switch_to_view(view)
|
||||
|
||||
def thread_target():
|
||||
resp = self.get_resp()
|
||||
try:
|
||||
self.model.resp_json = tui_utils.handle_sync_response(resp, self)
|
||||
except Controller.RequestFailed:
|
||||
return
|
||||
self.app.run_in_main_loop(main_loop_target)
|
||||
|
||||
Thread(target=thread_target).start()
|
|
@ -0,0 +1,112 @@
|
|||
from threading import Thread
|
||||
from typing import Dict, List
|
||||
|
||||
import urwid
|
||||
|
||||
from ...StreamResponseHandler import StreamResponseHandler
|
||||
from ...utils import http_request, generic_handle_stream_response
|
||||
from .Controller import Controller
|
||||
|
||||
|
||||
class TransactionController(Controller, StreamResponseHandler):
|
||||
def __init__(self, model, app):
|
||||
super().__init__(model, app)
|
||||
self.op_idx = 0
|
||||
self.error_messages = []
|
||||
|
||||
def start(self):
|
||||
Thread(target=self._start_txn).start()
|
||||
|
||||
def _start_txn(self):
|
||||
resp = http_request(
|
||||
self.model.http_verb,
|
||||
self.model.req_path,
|
||||
**self.model.req_kwargs
|
||||
)
|
||||
data = generic_handle_stream_response(resp, self.model.operations, self)
|
||||
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 _show_lines(self, lines):
|
||||
num_lines = len(lines)
|
||||
# Since the message_text is at the bottom of the window,
|
||||
# we want to add sufficient padding to the bottom of the text
|
||||
lines += [''] * max(4 - num_lines, 0)
|
||||
for i, line in enumerate(lines):
|
||||
if type(line) is str:
|
||||
lines[i] = line + '\n'
|
||||
else: # tuple (attr, text)
|
||||
lines[i] = (line[0], line[1] + '\n')
|
||||
self.view.message_text.set_text(lines)
|
||||
|
||||
def _abort(self):
|
||||
for elem in self.view.right_col_elems[self.op_idx:]:
|
||||
elem.set_text(('red', 'ABORTED'))
|
||||
self.view.enable_next_button()
|
||||
|
||||
def begin(self):
|
||||
pass
|
||||
|
||||
def handle_non_200(self, resp):
|
||||
def target():
|
||||
self._abort()
|
||||
lines = ['An error occurred:']
|
||||
if resp.headers.get('content-type') == 'application/json':
|
||||
err_msg = resp.json()['error']
|
||||
else:
|
||||
err_msg = resp.text
|
||||
lines.extend(err_msg.split('\n'))
|
||||
self._show_lines(lines)
|
||||
self.app.run_in_main_loop(target)
|
||||
|
||||
def handle_aborted(self, err_msg):
|
||||
def target():
|
||||
self._abort()
|
||||
lines = [
|
||||
'The transaction was rolled back.',
|
||||
'The error was:',
|
||||
'',
|
||||
*err_msg.split('\n'),
|
||||
]
|
||||
self._show_lines(lines)
|
||||
self.app.run_in_main_loop(target)
|
||||
|
||||
def handle_completed(self):
|
||||
def target():
|
||||
lines = ['Transaction successfully completed.']
|
||||
if len(self.error_messages) > 0:
|
||||
lines.append('There were some errors:')
|
||||
for msg in self.error_messages:
|
||||
lines.extend(msg.split('\n'))
|
||||
self._show_lines(lines)
|
||||
self.view.enable_next_button()
|
||||
self.app.run_in_main_loop(target)
|
||||
|
||||
def handle_successful_operation(self):
|
||||
def target():
|
||||
self.view.right_col_elems[self.op_idx].set_text(('green', 'Done'))
|
||||
self.op_idx += 1
|
||||
self.app.run_in_main_loop(target)
|
||||
|
||||
def handle_failed_operation(self, err_msg):
|
||||
def target():
|
||||
self.view.right_col_elems[self.op_idx].set_text(('red', 'Failed'))
|
||||
self.op_idx += 1
|
||||
if err_msg is not None:
|
||||
self.error_messages.append(err_msg)
|
||||
self.app.run_in_main_loop(target)
|
||||
|
||||
def handle_skipped_operation(self):
|
||||
def target():
|
||||
self.view.right_col_elems[self.op_idx].set_text('Skipped')
|
||||
self.op_idx += 1
|
||||
self.app.run_in_main_loop(target)
|
||||
|
||||
def handle_unrecognized_operation(self, operation):
|
||||
def target():
|
||||
self.error_messages.append('Unrecognized operation: ' + operation)
|
||||
self.op_idx += 1
|
||||
self.app.run_in_main_loop(target)
|
|
@ -0,0 +1,6 @@
|
|||
from .Controller import Controller
|
||||
|
||||
|
||||
class WelcomeController(Controller):
|
||||
def __init__(self, model, app):
|
||||
super().__init__(model, app)
|
|
@ -0,0 +1,18 @@
|
|||
from .Controller import Controller
|
||||
from .WelcomeController import WelcomeController
|
||||
from .AddUserController import AddUserController
|
||||
from .AddUserTransactionController import AddUserTransactionController
|
||||
from .RenewUserController import RenewUserController
|
||||
from .GetUserController import GetUserController
|
||||
from .ResetPasswordController import ResetPasswordController
|
||||
from .ChangeLoginShellController import ChangeLoginShellController
|
||||
from .AddGroupController import AddGroupController
|
||||
from .GetGroupController import GetGroupController
|
||||
from .AddMemberToGroupController import AddMemberToGroupController
|
||||
from .RemoveMemberFromGroupController import RemoveMemberFromGroupController
|
||||
from .CreateDatabaseController import CreateDatabaseController
|
||||
from .ResetDatabasePasswordController import ResetDatabasePasswordController
|
||||
from .GetPositionsController import GetPositionsController
|
||||
from .SetPositionsController import SetPositionsController
|
||||
from .TransactionController import TransactionController
|
||||
from .SyncRequestController import SyncRequestController
|
|
@ -0,0 +1,7 @@
|
|||
class AddGroupModel:
|
||||
name = 'AddGroup'
|
||||
title = 'Add group'
|
||||
|
||||
def __init__(self):
|
||||
self.name = ''
|
||||
self.description = ''
|
|
@ -0,0 +1,8 @@
|
|||
class AddMemberToGroupModel:
|
||||
name = 'AddMemberToGroup'
|
||||
title = 'Add member to group'
|
||||
|
||||
def __init__(self):
|
||||
self.name = ''
|
||||
self.username = ''
|
||||
self.subscribe_to_lists = True
|
|
@ -0,0 +1,13 @@
|
|||
class AddUserModel:
|
||||
name = 'AddUser'
|
||||
title = 'Add user'
|
||||
|
||||
def __init__(self):
|
||||
self.membership_type = 'general_member'
|
||||
self.username = ''
|
||||
self.full_name = ''
|
||||
self.first_name = ''
|
||||
self.last_name = ''
|
||||
self.program = ''
|
||||
self.forwarding_address = ''
|
||||
self.num_terms = 1
|
|
@ -0,0 +1,8 @@
|
|||
class ChangeLoginShellModel:
|
||||
name = 'ChangeLoginShell'
|
||||
title = 'Change login shell'
|
||||
|
||||
def __init__(self):
|
||||
self.username = ''
|
||||
self.login_shell = ''
|
||||
self.resp_json = None
|
|
@ -0,0 +1,12 @@
|
|||
class CreateDatabaseModel:
|
||||
name = 'CreateDatabase'
|
||||
title = 'Create database'
|
||||
|
||||
def __init__(self):
|
||||
self.db_type = 'mysql'
|
||||
self.user_dict = None
|
||||
self.resp_json = None
|
||||
self.password = None
|
||||
self.db_host = None
|
||||
self.filename = None
|
||||
self.wrote_to_file = False
|
|
@ -0,0 +1,7 @@
|
|||
class GetGroupModel:
|
||||
name = 'GetGroup'
|
||||
title = 'Get group members'
|
||||
|
||||
def __init__(self):
|
||||
self.name = ''
|
||||
self.resp_json = None
|
|
@ -0,0 +1,4 @@
|
|||
class GetPositionsModel:
|
||||
name = 'GetPositions'
|
||||
title = 'Get positions'
|
||||
positions = {}
|
|
@ -0,0 +1,7 @@
|
|||
class GetUserModel:
|
||||
name = 'GetUser'
|
||||
title = 'Get user info'
|
||||
|
||||
def __init__(self):
|
||||
self.username = ''
|
||||
self.resp_json = None
|
|
@ -0,0 +1,8 @@
|
|||
class RemoveMemberFromGroupModel:
|
||||
name = 'RemoveMemberFromGroup'
|
||||
title = 'Remove member from group'
|
||||
|
||||
def __init__(self):
|
||||
self.name = ''
|
||||
self.username = ''
|
||||
self.unsubscribe_from_lists = True
|
|
@ -0,0 +1,10 @@
|
|||
class RenewUserModel:
|
||||
name = 'RenewUser'
|
||||
title = 'Renew user'
|
||||
|
||||
def __init__(self):
|
||||
self.membership_type = 'general_member'
|
||||
self.username = ''
|
||||
self.num_terms = 1
|
||||
self.new_terms = None
|
||||
self.resp_json = None
|
|
@ -0,0 +1,11 @@
|
|||
class ResetDatabasePasswordModel:
|
||||
name = 'ResetDatabasePassword'
|
||||
title = 'Reset database password'
|
||||
|
||||
def __init__(self):
|
||||
self.db_type = 'mysql'
|
||||
self.user_dict = None
|
||||
self.resp_json = None
|
||||
self.password = None
|
||||
self.filename = None
|
||||
self.wrote_to_file = False
|
|
@ -0,0 +1,7 @@
|
|||
class ResetPasswordModel:
|
||||
name = 'ResetPassword'
|
||||
title = 'Reset password'
|
||||
|
||||
def __init__(self):
|
||||
self.username = ''
|
||||
self.resp_json = None
|
|
@ -0,0 +1,4 @@
|
|||
class SetPositionsModel:
|
||||
name = 'SetPositions'
|
||||
title = 'Set positions'
|
||||
positions = {}
|
|
@ -0,0 +1,9 @@
|
|||
class TransactionModel:
|
||||
name = 'Transaction'
|
||||
title = 'Running transaction'
|
||||
|
||||
def __init__(self, operations, http_verb, req_path, **req_kwargs):
|
||||
self.operations = operations
|
||||
self.http_verb = http_verb
|
||||
self.req_path = req_path
|
||||
self.req_kwargs = req_kwargs
|
|
@ -0,0 +1,43 @@
|
|||
from .AddUserModel import AddUserModel
|
||||
from .RenewUserModel import RenewUserModel
|
||||
from .GetUserModel import GetUserModel
|
||||
from .ResetPasswordModel import ResetPasswordModel
|
||||
from .ChangeLoginShellModel import ChangeLoginShellModel
|
||||
from .AddGroupModel import AddGroupModel
|
||||
from .GetGroupModel import GetGroupModel
|
||||
from .AddMemberToGroupModel import AddMemberToGroupModel
|
||||
from .RemoveMemberFromGroupModel import RemoveMemberFromGroupModel
|
||||
from .CreateDatabaseModel import CreateDatabaseModel
|
||||
from .ResetDatabasePasswordModel import ResetDatabasePasswordModel
|
||||
from .GetPositionsModel import GetPositionsModel
|
||||
from .SetPositionsModel import SetPositionsModel
|
||||
|
||||
|
||||
class WelcomeModel:
|
||||
name = 'Welcome'
|
||||
title = 'CSC Electronic Office'
|
||||
|
||||
def __init__(self):
|
||||
self.categories = {
|
||||
'Members': [
|
||||
AddUserModel,
|
||||
RenewUserModel,
|
||||
GetUserModel,
|
||||
ResetPasswordModel,
|
||||
ChangeLoginShellModel,
|
||||
],
|
||||
'Groups': [
|
||||
AddGroupModel,
|
||||
GetGroupModel,
|
||||
AddMemberToGroupModel,
|
||||
RemoveMemberFromGroupModel,
|
||||
],
|
||||
'Databases': [
|
||||
CreateDatabaseModel,
|
||||
ResetDatabasePasswordModel,
|
||||
],
|
||||
'Positions': [
|
||||
GetPositionsModel,
|
||||
SetPositionsModel,
|
||||
],
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
from .WelcomeModel import WelcomeModel
|
||||
from .AddUserModel import AddUserModel
|
||||
from .RenewUserModel import RenewUserModel
|
||||
from .GetUserModel import GetUserModel
|
||||
from .ResetPasswordModel import ResetPasswordModel
|
||||
from .ChangeLoginShellModel import ChangeLoginShellModel
|
||||
from .AddGroupModel import AddGroupModel
|
||||
from .GetGroupModel import GetGroupModel
|
||||
from .AddMemberToGroupModel import AddMemberToGroupModel
|
||||
from .RemoveMemberFromGroupModel import RemoveMemberFromGroupModel
|
||||
from .CreateDatabaseModel import CreateDatabaseModel
|
||||
from .ResetDatabasePasswordModel import ResetDatabasePasswordModel
|
||||
from .GetPositionsModel import GetPositionsModel
|
||||
from .SetPositionsModel import SetPositionsModel
|
||||
from .TransactionModel import TransactionModel
|
120
ceo/tui/start.py
120
ceo/tui/start.py
|
@ -1,89 +1,43 @@
|
|||
import os
|
||||
import sys
|
||||
import urwid
|
||||
|
||||
from asciimatics.exceptions import ResizeScreenError
|
||||
from asciimatics.scene import Scene
|
||||
from asciimatics.screen import Screen
|
||||
|
||||
from .ConfirmView import ConfirmView
|
||||
from .ErrorView import ErrorView
|
||||
from .Model import Model
|
||||
from .ResultView import ResultView
|
||||
from .TransactionView import TransactionView
|
||||
from .WelcomeView import WelcomeView
|
||||
from .databases.CreateDatabaseView import CreateDatabaseView
|
||||
from .databases.CreateDatabaseResultView import CreateDatabaseResultView
|
||||
from .databases.ResetDatabasePasswordView import ResetDatabasePasswordView
|
||||
from .databases.ResetDatabasePasswordResultView import ResetDatabasePasswordResultView
|
||||
from .groups.AddGroupView import AddGroupView
|
||||
from .groups.AddMemberToGroupView import AddMemberToGroupView
|
||||
from .groups.GetGroupView import GetGroupView
|
||||
from .groups.GetGroupResultView import GetGroupResultView
|
||||
from .groups.RemoveMemberFromGroupView import RemoveMemberFromGroupView
|
||||
from .members.AddUserTransactionView import AddUserTransactionView
|
||||
from .members.AddUserView import AddUserView
|
||||
from .members.ChangeLoginShellView import ChangeLoginShellView
|
||||
from .members.GetUserView import GetUserView
|
||||
from .members.GetUserResultView import GetUserResultView
|
||||
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
|
||||
from .app import App
|
||||
from .utils import get_mvc
|
||||
|
||||
|
||||
# tuples of (name, view)
|
||||
views = []
|
||||
|
||||
|
||||
def screen_wrapper(screen, last_scene, model):
|
||||
global views
|
||||
width = min(screen.width, 90)
|
||||
height = min(screen.height, 24)
|
||||
views = [
|
||||
('Welcome', WelcomeView(screen, width, height, model)),
|
||||
('Confirm', ConfirmView(screen, width, height, model)),
|
||||
('Transaction', TransactionView(screen, width, height, model)),
|
||||
('Result', ResultView(screen, width, height, model)),
|
||||
('Error', ErrorView(screen, width, height, model)),
|
||||
('AddUser', AddUserView(screen, width, height, model)),
|
||||
('AddUserTransaction', AddUserTransactionView(screen, width, height, model)),
|
||||
('RenewUser', RenewUserView(screen, width, height, model)),
|
||||
('GetUser', GetUserView(screen, width, height, model)),
|
||||
('GetUserResult', GetUserResultView(screen, width, height, model)),
|
||||
('ResetPassword', ResetPasswordView(screen, width, height, model)),
|
||||
('ResetPasswordResult', ResetPasswordResultView(screen, width, height, model)),
|
||||
('ChangeLoginShell', ChangeLoginShellView(screen, width, height, model)),
|
||||
('SetForwardingAddresses', SetForwardingAddressesView(screen, width, height, model)),
|
||||
('AddGroup', AddGroupView(screen, width, height, model)),
|
||||
('GetGroup', GetGroupView(screen, width, height, model)),
|
||||
('GetGroupResult', GetGroupResultView(screen, width, height, model)),
|
||||
('AddMemberToGroup', AddMemberToGroupView(screen, width, height, model)),
|
||||
('RemoveMemberFromGroup', RemoveMemberFromGroupView(screen, width, height, model)),
|
||||
('CreateDatabase', CreateDatabaseView(screen, width, height, 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
|
||||
]
|
||||
model.screen = screen
|
||||
model.views = [view for name, view in views]
|
||||
screen.play(
|
||||
scenes, stop_on_resize=True, start_scene=last_scene, allow_int=True,
|
||||
)
|
||||
def exit_on_special_chars(key):
|
||||
if key in ('q', 'Q', 'esc'):
|
||||
raise urwid.ExitMainLoop()
|
||||
|
||||
|
||||
def main():
|
||||
last_scene = None
|
||||
model = Model()
|
||||
try:
|
||||
Screen.wrapper(screen_wrapper, arguments=[last_scene, model])
|
||||
sys.exit(0)
|
||||
except ResizeScreenError:
|
||||
os.system('reset')
|
||||
print('Unfortunately, ceo does not currently support dynamic resizing.')
|
||||
sys.exit(1)
|
||||
# Just put some empty placeholder in the main widget for now
|
||||
# (will be replaced by the WelcomeView)
|
||||
main_widget = urwid.Padding(urwid.Text(''), left=2, right=2)
|
||||
top = urwid.Overlay(
|
||||
main_widget,
|
||||
urwid.SolidFill('\N{MEDIUM SHADE}'),
|
||||
align='center',
|
||||
width=('relative', App.REL_WIDTH_PCT),
|
||||
valign='middle',
|
||||
height=('relative', App.REL_HEIGHT_PCT),
|
||||
# On a full-screen (1366x768) gnome-terminal window,
|
||||
# I had 168 cols and 36 rows
|
||||
min_width=App.WIDTH,
|
||||
min_height=App.HEIGHT,
|
||||
)
|
||||
loop = urwid.MainLoop(
|
||||
top,
|
||||
palette=[
|
||||
('reversed', 'standout', ''),
|
||||
('bold', 'bold', ''),
|
||||
('green', 'light green', ''),
|
||||
('red', 'light red', ''),
|
||||
],
|
||||
# Disable the mouse (makes it hard to copy text from the screen)
|
||||
handle_mouse=False,
|
||||
unhandled_input=exit_on_special_chars
|
||||
)
|
||||
app = App(loop, main_widget)
|
||||
_, view, _ = get_mvc(app, 'Welcome')
|
||||
view.activate()
|
||||
loop.run()
|
||||
|
|
|
@ -1,10 +1,88 @@
|
|||
from asciimatics.exceptions import NextScene
|
||||
import json
|
||||
|
||||
import requests
|
||||
from ceo.tui.controllers import *
|
||||
from ceo.tui.models import *
|
||||
from ceo.tui.views import *
|
||||
|
||||
|
||||
def handle_sync_response(resp: requests.Response, model):
|
||||
if resp.status_code != 200:
|
||||
model.error_message = resp.text.rstrip()
|
||||
raise NextScene('Error')
|
||||
def handle_sync_response(resp, controller):
|
||||
if resp.ok:
|
||||
if resp.headers.get('content-type') == 'application/json':
|
||||
return resp.json()
|
||||
# streaming response
|
||||
return [json.loads(line) for line in resp.text.splitlines()]
|
||||
|
||||
def target():
|
||||
view = ErrorView(controller.model, controller, controller.app)
|
||||
controller.switch_to_view(view)
|
||||
|
||||
if resp.headers.get('content-type') == 'application/json':
|
||||
err_msg = resp.json()['error']
|
||||
else:
|
||||
err_msg = resp.text.rstrip()
|
||||
controller.model.error_message = err_msg
|
||||
controller.app.run_in_main_loop(target)
|
||||
raise Controller.RequestFailed()
|
||||
|
||||
|
||||
def get_mvc(app, name):
|
||||
if name == WelcomeModel.name:
|
||||
model = WelcomeModel()
|
||||
controller = WelcomeController(model, app)
|
||||
view = WelcomeView(model, controller, app)
|
||||
elif name == AddUserModel.name:
|
||||
model = AddUserModel()
|
||||
controller = AddUserController(model, app)
|
||||
view = AddUserView(model, controller, app)
|
||||
elif name == RenewUserModel.name:
|
||||
model = RenewUserModel()
|
||||
controller = RenewUserController(model, app)
|
||||
view = RenewUserView(model, controller, app)
|
||||
elif name == GetUserModel.name:
|
||||
model = GetUserModel()
|
||||
controller = GetUserController(model, app)
|
||||
view = GetUserView(model, controller, app)
|
||||
elif name == ResetPasswordModel.name:
|
||||
model = ResetPasswordModel()
|
||||
controller = ResetPasswordController(model, app)
|
||||
view = ResetPasswordView(model, controller, app)
|
||||
elif name == ChangeLoginShellModel.name:
|
||||
model = ChangeLoginShellModel()
|
||||
controller = ChangeLoginShellController(model, app)
|
||||
view = ChangeLoginShellView(model, controller, app)
|
||||
elif name == AddGroupModel.name:
|
||||
model = AddGroupModel()
|
||||
controller = AddGroupController(model, app)
|
||||
view = AddGroupView(model, controller, app)
|
||||
elif name == GetGroupModel.name:
|
||||
model = GetGroupModel()
|
||||
controller = GetGroupController(model, app)
|
||||
view = GetGroupView(model, controller, app)
|
||||
elif name == AddMemberToGroupModel.name:
|
||||
model = AddMemberToGroupModel()
|
||||
controller = AddMemberToGroupController(model, app)
|
||||
view = AddMemberToGroupView(model, controller, app)
|
||||
elif name == RemoveMemberFromGroupModel.name:
|
||||
model = RemoveMemberFromGroupModel()
|
||||
controller = RemoveMemberFromGroupController(model, app)
|
||||
view = RemoveMemberFromGroupView(model, controller, app)
|
||||
elif name == CreateDatabaseModel.name:
|
||||
model = CreateDatabaseModel()
|
||||
controller = CreateDatabaseController(model, app)
|
||||
view = CreateDatabaseView(model, controller, app)
|
||||
elif name == ResetDatabasePasswordModel.name:
|
||||
model = ResetDatabasePasswordModel()
|
||||
controller = ResetDatabasePasswordController(model, app)
|
||||
view = ResetDatabasePasswordView(model, controller, app)
|
||||
elif name == GetPositionsModel.name:
|
||||
model = GetPositionsModel()
|
||||
controller = GetPositionsController(model, app)
|
||||
view = GetPositionsView(model, controller, app)
|
||||
elif name == SetPositionsModel.name:
|
||||
model = SetPositionsModel()
|
||||
controller = SetPositionsController(model, app)
|
||||
view = SetPositionsView(model, controller, app)
|
||||
else:
|
||||
raise NotImplementedError()
|
||||
controller.view = view
|
||||
return model, view, controller
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
from .ConfirmationView import ConfirmationView
|
||||
|
||||
|
||||
class AddGroupConfirmationView(ConfirmationView):
|
||||
def __init__(self, model, controller, app):
|
||||
super().__init__(model, controller, app)
|
||||
lines = [
|
||||
f"A new group '{self.model.name}' will be created."
|
||||
]
|
||||
self.set_lines(lines)
|
|
@ -0,0 +1,21 @@
|
|||
import urwid
|
||||
|
||||
from .ColumnView import ColumnView
|
||||
|
||||
|
||||
class AddGroupView(ColumnView):
|
||||
def __init__(self, model, controller, app):
|
||||
super().__init__(model, controller, app)
|
||||
self.name_edit = urwid.Edit()
|
||||
self.description_edit = urwid.Edit()
|
||||
rows = [
|
||||
(
|
||||
urwid.Text('Name:', align='right'),
|
||||
self.name_edit
|
||||
),
|
||||
(
|
||||
urwid.Text('Description:', align='right'),
|
||||
self.description_edit
|
||||
)
|
||||
]
|
||||
self.set_rows(rows)
|
|
@ -0,0 +1,10 @@
|
|||
from .ConfirmationView import ConfirmationView
|
||||
|
||||
|
||||
class AddMemberToGroupConfirmationView(ConfirmationView):
|
||||
def __init__(self, model, controller, app):
|
||||
super().__init__(model, controller, app)
|
||||
lines = [
|
||||
f"User '{self.model.username}' will be added to the group '{self.model.name}'."
|
||||
]
|
||||
self.set_lines(lines)
|
|
@ -0,0 +1,33 @@
|
|||
import urwid
|
||||
|
||||
from .ColumnView import ColumnView
|
||||
|
||||
|
||||
class AddMemberToGroupView(ColumnView):
|
||||
def __init__(self, model, controller, app):
|
||||
super().__init__(model, controller, app)
|
||||
self.name_edit = urwid.Edit()
|
||||
self.username_edit = urwid.Edit()
|
||||
rows = [
|
||||
(
|
||||
urwid.Text('Group name:', align='right'),
|
||||
self.name_edit
|
||||
),
|
||||
(
|
||||
urwid.Text('New group member:', align='right'),
|
||||
self.username_edit
|
||||
)
|
||||
]
|
||||
checkbox = urwid.CheckBox(
|
||||
'Subscribe to auxiliary mailing lists',
|
||||
state=True,
|
||||
on_state_change=self.controller.on_list_subscribe_checkbox_change
|
||||
)
|
||||
# This is necessary to place the checkbox in the center of the page
|
||||
# (urwid.Padding doesn't seem to have an effect on it)
|
||||
checkbox = urwid.Columns([
|
||||
('weight', 1, urwid.Text('')),
|
||||
('weight', 3, checkbox)
|
||||
])
|
||||
extra_widgets = [urwid.Divider(), checkbox]
|
||||
self.set_rows(rows, extra_widgets=extra_widgets)
|
|
@ -0,0 +1,12 @@
|
|||
from .ConfirmationView import ConfirmationView
|
||||
|
||||
|
||||
class AddUserConfirmationView(ConfirmationView):
|
||||
def __init__(self, model, controller, app):
|
||||
super().__init__(model, controller, app)
|
||||
lines = ['Please make sure that:', '']
|
||||
if self.model.membership_type == 'general_member':
|
||||
lines.append(f'\N{BULLET} The new member has paid ${self.model.num_terms * 2} in club fees')
|
||||
lines.append("\N{BULLET} You have verified the name on the new member's WatCard")
|
||||
lines.append("\N{BULLET} The new member has signed the machine usage agreement")
|
||||
self.set_lines(lines, align='left')
|
|
@ -0,0 +1,75 @@
|
|||
import urwid
|
||||
|
||||
from .ColumnView import ColumnView
|
||||
from .utils import wrap_in_frame
|
||||
|
||||
|
||||
class AddUserView(ColumnView):
|
||||
def __init__(self, model, controller, app):
|
||||
super().__init__(model, controller, app)
|
||||
membership_types_group = []
|
||||
self.username_edit = urwid.Edit()
|
||||
self.full_name_edit = urwid.Edit()
|
||||
self.first_name_edit = urwid.Edit()
|
||||
self.last_name_edit = urwid.Edit()
|
||||
self.program_edit = urwid.Edit()
|
||||
self.forwarding_address_edit = urwid.Edit()
|
||||
self.num_terms_edit = urwid.IntEdit(default=1)
|
||||
rows = [
|
||||
(
|
||||
urwid.Text('Membership type:', align='right'),
|
||||
urwid.RadioButton(
|
||||
membership_types_group,
|
||||
'General membership ($2)',
|
||||
on_state_change=self.controller.on_membership_type_changed,
|
||||
user_data='general_member'
|
||||
)
|
||||
),
|
||||
(
|
||||
urwid.Divider(),
|
||||
urwid.RadioButton(
|
||||
membership_types_group,
|
||||
'Club rep (free)',
|
||||
on_state_change=self.controller.on_membership_type_changed,
|
||||
user_data='club_rep'
|
||||
)
|
||||
),
|
||||
(
|
||||
urwid.Text('Username:', align='right'),
|
||||
self.username_edit
|
||||
),
|
||||
(
|
||||
urwid.Text('Full name:', align='right'),
|
||||
self.full_name_edit
|
||||
),
|
||||
(
|
||||
urwid.Text('First name:', align='right'),
|
||||
self.first_name_edit
|
||||
),
|
||||
(
|
||||
urwid.Text('Last name:', align='right'),
|
||||
self.last_name_edit
|
||||
),
|
||||
(
|
||||
urwid.Text('Program:', align='right'),
|
||||
self.program_edit
|
||||
),
|
||||
(
|
||||
urwid.Text('Number of terms:', align='right'),
|
||||
self.num_terms_edit
|
||||
),
|
||||
]
|
||||
self.set_rows(
|
||||
rows,
|
||||
# We want to know when the username field loses focus
|
||||
notify_when_focus_changes=True,
|
||||
right_col_weight=2
|
||||
)
|
||||
|
||||
def update_fields(self):
|
||||
self.full_name_edit.edit_text = self.model.full_name
|
||||
self.first_name_edit.edit_text = self.model.first_name
|
||||
self.last_name_edit.edit_text = self.model.last_name
|
||||
self.program_edit.edit_text = self.model.program
|
||||
self.forwarding_address_edit.edit_text = self.model.forwarding_address
|
||||
self.num_terms_edit.edit_text = str(self.model.num_terms)
|
|
@ -0,0 +1,10 @@
|
|||
from .ConfirmationView import ConfirmationView
|
||||
|
||||
|
||||
class ChangeLoginShellConfirmationView(ConfirmationView):
|
||||
def __init__(self, model, controller, app):
|
||||
super().__init__(model, controller, app)
|
||||
lines = [
|
||||
f"{self.model.username}'s login shell will be set to {self.model.login_shell}."
|
||||
]
|
||||
self.set_lines(lines)
|
|
@ -0,0 +1,27 @@
|
|||
import urwid
|
||||
|
||||
from .ColumnView import ColumnView
|
||||
|
||||
|
||||
class ChangeLoginShellView(ColumnView):
|
||||
def __init__(self, model, controller, app):
|
||||
super().__init__(model, controller, app)
|
||||
self.username_edit = urwid.Edit()
|
||||
self.login_shell_edit = urwid.Edit()
|
||||
rows = [
|
||||
(
|
||||
urwid.Text('Username:', align='right'),
|
||||
self.username_edit
|
||||
),
|
||||
(
|
||||
urwid.Text('Login shell:', align='right'),
|
||||
self.login_shell_edit
|
||||
)
|
||||
]
|
||||
self.set_rows(
|
||||
rows,
|
||||
notify_when_focus_changes=True
|
||||
)
|
||||
|
||||
def update_fields(self):
|
||||
self.login_shell_edit.edit_text = self.model.login_shell
|
|
@ -0,0 +1,29 @@
|
|||
import urwid
|
||||
|
||||
from .ColumnView import ColumnView
|
||||
|
||||
|
||||
class ColumnResponseView(ColumnView):
|
||||
def __init__(self, model, controller, app):
|
||||
super().__init__(model, controller, app)
|
||||
|
||||
def set_pairs(self, pairs, right_col_weight=1):
|
||||
for i, (left, right) in enumerate(pairs):
|
||||
if type(right) is list:
|
||||
pairs[i] = (left, ','.join(map(str, right)))
|
||||
else:
|
||||
pairs[i] = (left, str(right))
|
||||
rows = [
|
||||
(
|
||||
urwid.Text(left + ':', align='right'),
|
||||
urwid.Text(right)
|
||||
)
|
||||
for left, right in pairs
|
||||
]
|
||||
self.set_rows(
|
||||
rows,
|
||||
right_col_weight=right_col_weight,
|
||||
disable_cols=True,
|
||||
no_back_button=True,
|
||||
on_next=self.controller.get_next_menu_callback('Welcome')
|
||||
)
|
|
@ -0,0 +1,59 @@
|
|||
import urwid
|
||||
|
||||
from .View import View
|
||||
from .utils import wrap_in_frame
|
||||
|
||||
|
||||
class ColumnView(View):
|
||||
def __init__(self, model, controller, app):
|
||||
super().__init__(model, controller, app)
|
||||
|
||||
def set_rows(
|
||||
self,
|
||||
rows,
|
||||
right_col_weight=1,
|
||||
notify_when_focus_changes=False,
|
||||
disable_cols=False,
|
||||
extra_widgets=None,
|
||||
no_back_button=False,
|
||||
on_next=None,
|
||||
no_next_button=False,
|
||||
):
|
||||
# Each item in the list is two columns
|
||||
columns_list = [
|
||||
urwid.Columns(
|
||||
[('weight', 1, left), ('weight', right_col_weight, right)],
|
||||
dividechars=3,
|
||||
focus_column=1
|
||||
)
|
||||
for left, right in rows
|
||||
]
|
||||
if extra_widgets is not None:
|
||||
columns_list.extend(extra_widgets)
|
||||
listwalker = urwid.SimpleFocusListWalker(columns_list)
|
||||
if notify_when_focus_changes:
|
||||
# See https://stackoverflow.com/a/43125172
|
||||
urwid.connect_signal(
|
||||
listwalker, 'modified',
|
||||
self.controller.on_row_focus_changed
|
||||
)
|
||||
# Keep a reference for the controller
|
||||
self.listwalker = listwalker
|
||||
cols = urwid.ListBox(listwalker)
|
||||
if disable_cols:
|
||||
cols = urwid.WidgetDisable(cols)
|
||||
self.flash_text = urwid.Text('')
|
||||
if no_back_button:
|
||||
on_back = None
|
||||
else:
|
||||
on_back = self.controller.prev_menu_callback
|
||||
if on_next is None and not no_next_button:
|
||||
on_next = self.controller.on_next_button_pressed
|
||||
body = cols
|
||||
self.original_widget = wrap_in_frame(
|
||||
body,
|
||||
self.model.title,
|
||||
on_back=on_back,
|
||||
on_next=on_next,
|
||||
flash_text=self.flash_text,
|
||||
)
|
|
@ -0,0 +1,18 @@
|
|||
import urwid
|
||||
|
||||
from .PlainTextView import PlainTextView
|
||||
|
||||
|
||||
class ConfirmationView(PlainTextView):
|
||||
def __init__(self, model, controller, app):
|
||||
super().__init__(model, controller, app)
|
||||
self.flash_text = urwid.Text('')
|
||||
|
||||
def set_lines(self, lines, align='center'):
|
||||
super().set_lines(
|
||||
lines,
|
||||
align=align,
|
||||
on_back=self.controller.prev_menu_callback,
|
||||
on_next=self.controller.on_confirmation_button_pressed,
|
||||
flash_text=self.flash_text,
|
||||
)
|
|
@ -0,0 +1,14 @@
|
|||
from .ConfirmationView import ConfirmationView
|
||||
import ceo.krb_check as krb
|
||||
|
||||
|
||||
class CreateDatabaseConfirmationView(ConfirmationView):
|
||||
def __init__(self, model, controller, app):
|
||||
super().__init__(model, controller, app)
|
||||
db_type = self.model.db_type
|
||||
db_type_name = 'MySQL' if db_type == 'mysql' else 'PostgreSQL'
|
||||
username = krb.get_username()
|
||||
lines = [
|
||||
f"A new {db_type_name} database will be created for {username}."
|
||||
]
|
||||
self.set_lines(lines)
|
|
@ -0,0 +1,33 @@
|
|||
from .PlainTextView import PlainTextView
|
||||
|
||||
|
||||
class CreateDatabaseResponseView(PlainTextView):
|
||||
def __init__(self, model, controller, app):
|
||||
super().__init__(model, controller, app)
|
||||
|
||||
def activate(self):
|
||||
self.controller.write_db_creds_to_file()
|
||||
username = self.model.user_dict['uid']
|
||||
password = self.model.password
|
||||
db_host = self.model.db_host
|
||||
filename = self.model.filename
|
||||
wrote_to_file = self.model.wrote_to_file
|
||||
lines = [
|
||||
'Connection information:',
|
||||
'',
|
||||
f'Database: {username}',
|
||||
f'Username: {username}',
|
||||
f'Password: {password}',
|
||||
f'Host: {db_host}',
|
||||
''
|
||||
]
|
||||
if wrote_to_file:
|
||||
lines.append(f"These settings have been written to {filename}.")
|
||||
else:
|
||||
lines.append(f"We were unable to write these settings to {filename}.")
|
||||
self.set_lines(
|
||||
lines,
|
||||
align='left',
|
||||
on_next=self.controller.get_next_menu_callback('Welcome'),
|
||||
)
|
||||
super().activate()
|
|
@ -0,0 +1,31 @@
|
|||
import urwid
|
||||
|
||||
from .ColumnView import ColumnView
|
||||
from .utils import wrap_in_frame
|
||||
|
||||
|
||||
class CreateDatabaseView(ColumnView):
|
||||
def __init__(self, model, controller, app):
|
||||
super().__init__(model, controller, app)
|
||||
db_types_group = []
|
||||
rows = [
|
||||
(
|
||||
urwid.Text('Database type:', align='right'),
|
||||
urwid.RadioButton(
|
||||
db_types_group,
|
||||
'MySQL',
|
||||
on_state_change=self.controller.on_db_type_changed,
|
||||
user_data='mysql'
|
||||
)
|
||||
),
|
||||
(
|
||||
urwid.Divider(),
|
||||
urwid.RadioButton(
|
||||
db_types_group,
|
||||
'PostgreSQL',
|
||||
on_state_change=self.controller.on_db_type_changed,
|
||||
user_data='postgresql'
|
||||
)
|
||||
),
|
||||
]
|
||||
self.set_rows(rows)
|
|
@ -0,0 +1,15 @@
|
|||
from .PlainTextView import PlainTextView
|
||||
|
||||
|
||||
class ErrorView(PlainTextView):
|
||||
def __init__(self, model, controller, app):
|
||||
super().__init__(model, controller, app)
|
||||
lines = [
|
||||
'An error occurred:',
|
||||
'',
|
||||
*model.error_message.split('\n')
|
||||
]
|
||||
self.set_lines(
|
||||
lines,
|
||||
on_next=self.controller.get_next_menu_callback('Welcome'),
|
||||
)
|
|
@ -0,0 +1,23 @@
|
|||
from .PlainTextView import PlainTextView
|
||||
|
||||
|
||||
class GetGroupResponseView(PlainTextView):
|
||||
def __init__(self, model, controller, app):
|
||||
super().__init__(model, controller, app)
|
||||
d = self.model.resp_json
|
||||
if 'description' in d:
|
||||
desc = d['description'] + ' (' + d['cn'] + ')'
|
||||
else:
|
||||
desc = d['cn']
|
||||
lines = [
|
||||
'Members of ' + desc + ':',
|
||||
''
|
||||
]
|
||||
lines.extend([
|
||||
member['cn'] + ' (' + member['uid'] + ')'
|
||||
for member in self.model.resp_json['members']
|
||||
])
|
||||
self.set_lines(
|
||||
lines,
|
||||
on_next=self.controller.get_next_menu_callback('Welcome'),
|
||||
)
|
|
@ -0,0 +1,16 @@
|
|||
import urwid
|
||||
|
||||
from .ColumnView import ColumnView
|
||||
|
||||
|
||||
class GetGroupView(ColumnView):
|
||||
def __init__(self, model, controller, app):
|
||||
super().__init__(model, controller, app)
|
||||
self.name_edit = urwid.Edit()
|
||||
rows = [
|
||||
(
|
||||
urwid.Text('Group name:', align='right'),
|
||||
self.name_edit
|
||||
)
|
||||
]
|
||||
self.set_rows(rows)
|
|
@ -0,0 +1,31 @@
|
|||
from zope import component
|
||||
import urwid
|
||||
|
||||
from .ColumnView import ColumnView
|
||||
from .position_names import position_names
|
||||
from .utils import wrap_in_frame
|
||||
from ceo_common.interfaces import IConfig
|
||||
|
||||
|
||||
class GetPositionsView(ColumnView):
|
||||
def __init__(self, model, controller, app):
|
||||
super().__init__(model, controller, app)
|
||||
self.position_fields = {}
|
||||
cfg = component.getUtility(IConfig)
|
||||
avail = cfg.get('positions_available')
|
||||
rows = []
|
||||
for pos in avail:
|
||||
name = position_names[pos]
|
||||
field = urwid.Text('...')
|
||||
self.position_fields[pos] = field
|
||||
self.model.positions[pos] = ''
|
||||
rows.append((urwid.Text(name, align='right'), field))
|
||||
self.set_rows(rows, disable_cols=True, no_next_button=True)
|
||||
|
||||
def activate(self):
|
||||
self.controller.lookup_positions_async()
|
||||
super().activate()
|
||||
|
||||
def update_fields(self):
|
||||
for pos, field in self.position_fields.items():
|
||||
field.set_text(self.model.positions[pos])
|
|
@ -0,0 +1,31 @@
|
|||
from ...utils import user_dict_kv
|
||||
from .ColumnResponseView import ColumnResponseView
|
||||
|
||||
|
||||
class GetUserResponseView(ColumnResponseView):
|
||||
def __init__(self, model, controller, app):
|
||||
super().__init__(model, controller, app)
|
||||
d = self.model.resp_json.copy()
|
||||
# We don't have a lot of vertical space, so it's best to
|
||||
# omit unnecessary fields
|
||||
cols_to_omit = [
|
||||
'given_name',
|
||||
'sn',
|
||||
'is_club',
|
||||
'home_directory',
|
||||
]
|
||||
for key in cols_to_omit:
|
||||
del d[key]
|
||||
pairs = user_dict_kv(d)
|
||||
|
||||
num_terms = max(map(len, [
|
||||
d.get('terms', []),
|
||||
d.get('non_member_terms', [])
|
||||
]))
|
||||
if num_terms < 6:
|
||||
right_col_weight = 1
|
||||
elif num_terms < 12:
|
||||
right_col_weight = 2
|
||||
else:
|
||||
right_col_weight = 3
|
||||
self.set_pairs(pairs, right_col_weight=right_col_weight)
|
|
@ -0,0 +1,17 @@
|
|||
import urwid
|
||||
|
||||
from .ColumnView import ColumnView
|
||||
from .utils import wrap_in_frame
|
||||
|
||||
|
||||
class GetUserView(ColumnView):
|
||||
def __init__(self, model, controller, app):
|
||||
super().__init__(model, controller, app)
|
||||
self.username_edit = urwid.Edit()
|
||||
rows = [
|
||||
(
|
||||
urwid.Text('Username:', align='right'),
|
||||
self.username_edit
|
||||
)
|
||||
]
|
||||
self.set_rows(rows)
|
|
@ -0,0 +1,39 @@
|
|||
import urwid
|
||||
|
||||
from .View import View
|
||||
from .utils import wrap_in_frame
|
||||
from ceo.tui.app import App
|
||||
|
||||
|
||||
class PlainTextView(View):
|
||||
def __init__(self, model, controller, app):
|
||||
super().__init__(model, controller, app)
|
||||
|
||||
def set_lines(
|
||||
self,
|
||||
lines,
|
||||
align='center',
|
||||
min_width=None,
|
||||
on_back=None,
|
||||
on_next=None,
|
||||
flash_text=None,
|
||||
):
|
||||
if min_width is None:
|
||||
if align == 'center':
|
||||
min_width = App.WIDTH
|
||||
else:
|
||||
min_width = max(map(len, lines))
|
||||
self.original_widget = wrap_in_frame(
|
||||
urwid.Padding(
|
||||
urwid.Filler(
|
||||
urwid.Text('\n'.join(lines), align=align)
|
||||
),
|
||||
align='center',
|
||||
width=('relative', App.REL_WIDTH_PCT),
|
||||
min_width=min_width,
|
||||
),
|
||||
self.model.title,
|
||||
on_back=on_back,
|
||||
on_next=on_next,
|
||||
flash_text=flash_text,
|
||||
)
|
|
@ -0,0 +1,10 @@
|
|||
from .ConfirmationView import ConfirmationView
|
||||
|
||||
|
||||
class RemoveMemberFromGroupConfirmationView(ConfirmationView):
|
||||
def __init__(self, model, controller, app):
|
||||
super().__init__(model, controller, app)
|
||||
lines = [
|
||||
f"User '{self.model.username}' will be removed from the group '{self.model.name}'."
|
||||
]
|
||||
self.set_lines(lines)
|
|
@ -0,0 +1,34 @@
|
|||
import urwid
|
||||
|
||||
from .ColumnView import ColumnView
|
||||
from .utils import wrap_in_frame
|
||||
|
||||
|
||||
class RemoveMemberFromGroupView(ColumnView):
|
||||
def __init__(self, model, controller, app):
|
||||
super().__init__(model, controller, app)
|
||||
self.name_edit = urwid.Edit()
|
||||
self.username_edit = urwid.Edit()
|
||||
rows = [
|
||||
(
|
||||
urwid.Text('Group name:', align='right'),
|
||||
self.name_edit
|
||||
),
|
||||
(
|
||||
urwid.Text('Member to remove:', align='right'),
|
||||
self.username_edit
|
||||
)
|
||||
]
|
||||
checkbox = urwid.CheckBox(
|
||||
'Unsubscribe from auxiliary mailing lists',
|
||||
state=True,
|
||||
on_state_change=self.controller.on_list_unsubscribe_checkbox_change
|
||||
)
|
||||
# This is necessary to place the checkbox in the center of the page
|
||||
# (urwid.Padding doesn't seem to have an effect on it)
|
||||
checkbox = urwid.Columns([
|
||||
('weight', 1, urwid.Text('')),
|
||||
('weight', 3, checkbox)
|
||||
])
|
||||
extra_widgets = [urwid.Divider(), checkbox]
|
||||
self.set_rows(rows, extra_widgets=extra_widgets)
|
|
@ -0,0 +1,18 @@
|
|||
from .ConfirmationView import ConfirmationView
|
||||
|
||||
|
||||
class RenewUserConfirmationView(ConfirmationView):
|
||||
def __init__(self, model, controller, app):
|
||||
super().__init__(model, controller, app)
|
||||
membership_str = 'member'
|
||||
if model.membership_type == 'club_rep':
|
||||
membership_str = 'non-member'
|
||||
lines = [
|
||||
f"{model.username} will be renewed for the following {membership_str} terms:",
|
||||
'',
|
||||
', '.join(self.model.new_terms)
|
||||
]
|
||||
if model.membership_type == 'general_member':
|
||||
lines.append('')
|
||||
lines.append(f'Please make sure they have paid ${model.num_terms * 2} in club fees.')
|
||||
self.set_lines(lines)
|
|
@ -0,0 +1,41 @@
|
|||
import urwid
|
||||
|
||||
from .ColumnView import ColumnView
|
||||
from .utils import wrap_in_frame
|
||||
|
||||
|
||||
class RenewUserView(ColumnView):
|
||||
def __init__(self, model, controller, app):
|
||||
super().__init__(model, controller, app)
|
||||
membership_types_group = []
|
||||
self.username_edit = urwid.Edit()
|
||||
self.num_terms_edit = urwid.IntEdit(default=1)
|
||||
rows = [
|
||||
(
|
||||
urwid.Text('Membership type:', align='right'),
|
||||
urwid.RadioButton(
|
||||
membership_types_group,
|
||||
'General membership ($2)',
|
||||
on_state_change=self.controller.on_membership_type_changed,
|
||||
user_data='general_member'
|
||||
)
|
||||
),
|
||||
(
|
||||
urwid.Divider(),
|
||||
urwid.RadioButton(
|
||||
membership_types_group,
|
||||
'Club rep (free)',
|
||||
on_state_change=self.controller.on_membership_type_changed,
|
||||
user_data='club_rep'
|
||||
)
|
||||
),
|
||||
(
|
||||
urwid.Text('Username:', align='right'),
|
||||
self.username_edit
|
||||
),
|
||||
(
|
||||
urwid.Text('Number of terms:', align='right'),
|
||||
self.num_terms_edit
|
||||
),
|
||||
]
|
||||
self.set_rows(rows, right_col_weight=2)
|
|
@ -0,0 +1,14 @@
|
|||
from .ConfirmationView import ConfirmationView
|
||||
import ceo.krb_check as krb
|
||||
|
||||
|
||||
class ResetDatabasePasswordConfirmationView(ConfirmationView):
|
||||
def __init__(self, model, controller, app):
|
||||
super().__init__(model, controller, app)
|
||||
db_type = self.model.db_type
|
||||
db_type_name = 'MySQL' if db_type == 'mysql' else 'PostgreSQL'
|
||||
username = krb.get_username()
|
||||
lines = [
|
||||
f"The {db_type_name} password for {username} will be reset."
|
||||
]
|
||||
self.set_lines(lines)
|
|
@ -0,0 +1,31 @@
|
|||
from .PlainTextView import PlainTextView
|
||||
|
||||
|
||||
class ResetDatabasePasswordResponseView(PlainTextView):
|
||||
def __init__(self, model, controller, app):
|
||||
super().__init__(model, controller, app)
|
||||
|
||||
def activate(self):
|
||||
self.controller.write_db_creds_to_file()
|
||||
db_type = self.model.db_type
|
||||
db_type_name = 'MySQL' if db_type == 'mysql' else 'PostgreSQL'
|
||||
username = self.model.user_dict['uid']
|
||||
password = self.model.password
|
||||
filename = self.model.filename
|
||||
wrote_to_file = self.model.wrote_to_file
|
||||
lines = [
|
||||
f'The new {db_type_name} password for {username} is:'
|
||||
'',
|
||||
password,
|
||||
''
|
||||
]
|
||||
if wrote_to_file:
|
||||
lines.append(f"The settings in {filename} have been updated.")
|
||||
else:
|
||||
lines.append(f"We were unable to update the settings in {filename}.")
|
||||
self.set_lines(
|
||||
lines,
|
||||
align='left',
|
||||
on_next=self.controller.get_next_menu_callback('Welcome'),
|
||||
)
|
||||
super().activate()
|
|
@ -0,0 +1,31 @@
|
|||
import urwid
|
||||
|
||||
from .ColumnView import ColumnView
|
||||
from .utils import wrap_in_frame
|
||||
|
||||
|
||||
class ResetDatabasePasswordView(ColumnView):
|
||||
def __init__(self, model, controller, app):
|
||||
super().__init__(model, controller, app)
|
||||
db_types_group = []
|
||||
rows = [
|
||||
(
|
||||
urwid.Text('Database type:', align='right'),
|
||||
urwid.RadioButton(
|
||||
db_types_group,
|
||||
'MySQL',
|
||||
on_state_change=self.controller.on_db_type_changed,
|
||||
user_data='mysql'
|
||||
)
|
||||
),
|
||||
(
|
||||
urwid.Divider(),
|
||||
urwid.RadioButton(
|
||||
db_types_group,
|
||||
'PostgreSQL',
|
||||
on_state_change=self.controller.on_db_type_changed,
|
||||
user_data='postgresql'
|
||||
)
|
||||
),
|
||||
]
|
||||
self.set_rows(rows)
|
|
@ -0,0 +1,10 @@
|
|||
from .ConfirmationView import ConfirmationView
|
||||
|
||||
|
||||
class ResetPasswordConfirmationView(ConfirmationView):
|
||||
def __init__(self, model, controller, app):
|
||||
super().__init__(model, controller, app)
|
||||
lines = [
|
||||
f"{self.model.username}'s password will be set to a random value."
|
||||
]
|
||||
self.set_lines(lines)
|
|
@ -0,0 +1,17 @@
|
|||
from .PlainTextView import PlainTextView
|
||||
|
||||
|
||||
class ResetPasswordResponseView(PlainTextView):
|
||||
def __init__(self, model, controller, app):
|
||||
super().__init__(model, controller, app)
|
||||
lines = [
|
||||
f"The new password for {self.model.username} is:",
|
||||
'',
|
||||
self.model.resp_json['password'],
|
||||
'',
|
||||
'YOU MUST NOW EMAIL THIS PASSWORD TO THE USER.'
|
||||
]
|
||||
self.set_lines(
|
||||
lines,
|
||||
on_next=self.controller.get_next_menu_callback('Welcome'),
|
||||
)
|
|
@ -0,0 +1,15 @@
|
|||
from .PlainTextView import PlainTextView
|
||||
|
||||
|
||||
class ResetPasswordUsePasswdView(PlainTextView):
|
||||
def __init__(self, model, controller, app):
|
||||
super().__init__(model, controller, app)
|
||||
lines = [
|
||||
"This is a utility for syscom members to reset other members' passwords.",
|
||||
'',
|
||||
'To reset your own password, just run `passwd`.'
|
||||
]
|
||||
super().set_lines(
|
||||
lines,
|
||||
on_next=self.controller.get_next_menu_callback('Welcome'),
|
||||
)
|
|
@ -0,0 +1,17 @@
|
|||
import urwid
|
||||
|
||||
from .ColumnView import ColumnView
|
||||
from .utils import wrap_in_frame
|
||||
|
||||
|
||||
class ResetPasswordView(ColumnView):
|
||||
def __init__(self, model, controller, app):
|
||||
super().__init__(model, controller, app)
|
||||
self.username_edit = urwid.Edit()
|
||||
rows = [
|
||||
(
|
||||
urwid.Text('Username:', align='right'),
|
||||
self.username_edit
|
||||
)
|
||||
]
|
||||
self.set_rows(rows)
|
|
@ -0,0 +1,42 @@
|
|||
from zope import component
|
||||
|
||||
import urwid
|
||||
|
||||
from .ColumnView import ColumnView
|
||||
from .position_names import position_names
|
||||
from .utils import wrap_in_frame
|
||||
from ceo_common.interfaces import IConfig
|
||||
|
||||
|
||||
class SetPositionsView(ColumnView):
|
||||
def __init__(self, model, controller, app):
|
||||
super().__init__(model, controller, app)
|
||||
self.position_fields = {}
|
||||
cfg = component.getUtility(IConfig)
|
||||
avail = cfg.get('positions_available')
|
||||
required = cfg.get('positions_required')
|
||||
rows = []
|
||||
for pos in avail:
|
||||
name = position_names[pos]
|
||||
if pos in required:
|
||||
name += ' (*)'
|
||||
else:
|
||||
name += ' '
|
||||
field = urwid.Edit()
|
||||
self.position_fields[pos] = field
|
||||
self.model.positions[pos] = ''
|
||||
rows.append((urwid.Text(name, align='right'), field))
|
||||
extra_widgets = [
|
||||
urwid.Divider(),
|
||||
# Note that this appears all the way on the left
|
||||
urwid.Text('(*) Required')
|
||||
]
|
||||
self.set_rows(rows, extra_widgets=extra_widgets)
|
||||
|
||||
def activate(self):
|
||||
self.controller.lookup_positions_async()
|
||||
super().activate()
|
||||
|
||||
def update_fields(self):
|
||||
for pos, field in self.position_fields.items():
|
||||
field.edit_text = self.model.positions[pos]
|
|
@ -0,0 +1,11 @@
|
|||
from .PlainTextView import PlainTextView
|
||||
|
||||
|
||||
class SyncResponseView(PlainTextView):
|
||||
def __init__(self, model, controller, app):
|
||||
super().__init__(model, controller, app)
|
||||
lines = ['Request successfully completed.']
|
||||
self.set_lines(
|
||||
lines,
|
||||
on_next=self.controller.get_next_menu_callback('Welcome'),
|
||||
)
|
|
@ -0,0 +1,54 @@
|
|||
import urwid
|
||||
|
||||
from ...operation_strings import descriptions
|
||||
from .View import View
|
||||
from .utils import wrap_in_frame, CenterButton, decorate_button
|
||||
|
||||
|
||||
class TransactionView(View):
|
||||
def __init__(self, model, controller, app):
|
||||
super().__init__(model, controller, app)
|
||||
left_col_elems = []
|
||||
right_col_elems = []
|
||||
for op in model.operations:
|
||||
left_col_elems.append(urwid.Text(descriptions[op] + '...', align='right'))
|
||||
right_col_elems.append(urwid.Text(''))
|
||||
left_col = urwid.ListBox(urwid.SimpleFocusListWalker(left_col_elems))
|
||||
left_col = urwid.WidgetDisable(left_col)
|
||||
right_col = urwid.ListBox(urwid.SimpleFocusListWalker(right_col_elems))
|
||||
right_col = urwid.WidgetDisable(right_col)
|
||||
cols = urwid.Columns(
|
||||
[left_col, right_col],
|
||||
dividechars=2
|
||||
)
|
||||
self.next_button = decorate_button(
|
||||
CenterButton(
|
||||
'Next',
|
||||
on_press=self.on_next_button_pressed,
|
||||
)
|
||||
)
|
||||
# It doesn't seem to be possible to move focus to a button which
|
||||
# was intially disabled, so we're going to use a flag variable
|
||||
self._next_button_enabled = False
|
||||
# The controller uses this to show status messages
|
||||
self.message_text = urwid.Text('', align='center')
|
||||
self.original_widget = wrap_in_frame(
|
||||
cols,
|
||||
model.title,
|
||||
next_btn=self.next_button,
|
||||
message_text=self.message_text,
|
||||
)
|
||||
# Keep a reference for the controller
|
||||
self.right_col_elems = right_col_elems
|
||||
|
||||
def enable_next_button(self):
|
||||
self._next_button_enabled = True
|
||||
|
||||
def on_next_button_pressed(self, button):
|
||||
if not self._next_button_enabled:
|
||||
return
|
||||
self.controller.next_menu_callback(button, 'Welcome')
|
||||
|
||||
def activate(self):
|
||||
super().activate()
|
||||
self.controller.start()
|
|
@ -0,0 +1,48 @@
|
|||
from abc import ABC
|
||||
|
||||
import urwid
|
||||
|
||||
from .utils import CenterButton, decorate_button
|
||||
|
||||
|
||||
class View(ABC):
|
||||
def __init__(self, model, controller, app):
|
||||
super().__init__()
|
||||
self.model = model
|
||||
self.controller = controller
|
||||
self.app = app
|
||||
self.original_widget = None
|
||||
|
||||
def activate(self):
|
||||
if self.original_widget is None:
|
||||
raise Exception('child class must set self.original_widget')
|
||||
self.app.main_widget.original_widget = self.original_widget
|
||||
|
||||
def popup(self, message):
|
||||
button = CenterButton('OK')
|
||||
body = urwid.Text(message + '\n'*2, align='center')
|
||||
body = urwid.Pile([
|
||||
body,
|
||||
urwid.Columns([
|
||||
('weight', 1, urwid.WidgetDisable(urwid.Text(''))),
|
||||
decorate_button(button),
|
||||
('weight', 1, urwid.WidgetDisable(urwid.Text(''))),
|
||||
])
|
||||
], focus_item=1)
|
||||
body = urwid.Filler(body)
|
||||
body = urwid.LineBox(body)
|
||||
old_original_widget = self.app.main_widget.original_widget
|
||||
|
||||
def on_ok_clicked(*_):
|
||||
self.app.main_widget.original_widget = old_original_widget
|
||||
|
||||
urwid.connect_signal(button, 'click', on_ok_clicked)
|
||||
popup = urwid.Overlay(
|
||||
body,
|
||||
self.app.main_widget.original_widget,
|
||||
align='center',
|
||||
width=('relative', 60),
|
||||
valign='middle',
|
||||
height=('relative', 60),
|
||||
)
|
||||
self.app.main_widget.original_widget = popup
|
|
@ -0,0 +1,35 @@
|
|||
import urwid
|
||||
|
||||
import ceo.tui.utils as utils
|
||||
from .ColumnView import ColumnView
|
||||
from .utils import decorate_button
|
||||
|
||||
class WelcomeView(ColumnView):
|
||||
def __init__(self, model, controller, app):
|
||||
super().__init__(model, controller, app)
|
||||
rows = []
|
||||
for category, model_classes in model.categories.items():
|
||||
if len(rows) > 0:
|
||||
# Place dividers between sections
|
||||
rows.append((urwid.Divider(), urwid.Divider()))
|
||||
for i, model_class in enumerate(model_classes):
|
||||
if i == 0:
|
||||
left_col_elem = urwid.Text(category, align='right')
|
||||
else:
|
||||
left_col_elem = urwid.Divider()
|
||||
button = urwid.Button(
|
||||
model_class.title,
|
||||
on_press=self.controller.get_next_menu_callback(model_class.name),
|
||||
)
|
||||
rows.append((left_col_elem, decorate_button(button)))
|
||||
extra_widgets = [
|
||||
urwid.Divider(),
|
||||
urwid.Text('Press q or Esc to quit')
|
||||
]
|
||||
self.set_rows(
|
||||
rows,
|
||||
right_col_weight=3,
|
||||
extra_widgets=extra_widgets,
|
||||
no_back_button=True,
|
||||
no_next_button=True,
|
||||
)
|
|
@ -0,0 +1,37 @@
|
|||
from .View import View
|
||||
from .WelcomeView import WelcomeView
|
||||
from .AddUserView import AddUserView
|
||||
from .AddUserConfirmationView import AddUserConfirmationView
|
||||
from .RenewUserView import RenewUserView
|
||||
from .RenewUserConfirmationView import RenewUserConfirmationView
|
||||
from .GetUserView import GetUserView
|
||||
from .GetUserResponseView import GetUserResponseView
|
||||
from .ResetPasswordView import ResetPasswordView
|
||||
from .ResetPasswordConfirmationView import ResetPasswordConfirmationView
|
||||
from .ResetPasswordUsePasswdView import ResetPasswordUsePasswdView
|
||||
from .ResetPasswordResponseView import ResetPasswordResponseView
|
||||
from .ChangeLoginShellView import ChangeLoginShellView
|
||||
from .ChangeLoginShellConfirmationView import ChangeLoginShellConfirmationView
|
||||
from .AddGroupView import AddGroupView
|
||||
from .AddGroupConfirmationView import AddGroupConfirmationView
|
||||
from .GetGroupView import GetGroupView
|
||||
from .GetGroupResponseView import GetGroupResponseView
|
||||
from .AddMemberToGroupView import AddMemberToGroupView
|
||||
from .AddMemberToGroupConfirmationView import AddMemberToGroupConfirmationView
|
||||
from .RemoveMemberFromGroupView import RemoveMemberFromGroupView
|
||||
from .RemoveMemberFromGroupConfirmationView import RemoveMemberFromGroupConfirmationView
|
||||
from .CreateDatabaseView import CreateDatabaseView
|
||||
from .CreateDatabaseConfirmationView import CreateDatabaseConfirmationView
|
||||
from .CreateDatabaseResponseView import CreateDatabaseResponseView
|
||||
from .ResetDatabasePasswordView import ResetDatabasePasswordView
|
||||
from .ResetDatabasePasswordConfirmationView import ResetDatabasePasswordConfirmationView
|
||||
from .ResetDatabasePasswordResponseView import ResetDatabasePasswordResponseView
|
||||
from .GetPositionsView import GetPositionsView
|
||||
from .SetPositionsView import SetPositionsView
|
||||
from .TransactionView import TransactionView
|
||||
from .PlainTextView import PlainTextView
|
||||
from .ConfirmationView import ConfirmationView
|
||||
from .ErrorView import ErrorView
|
||||
from .SyncResponseView import SyncResponseView
|
||||
from .ColumnView import ColumnView
|
||||
from .ColumnResponseView import ColumnResponseView
|
|
@ -0,0 +1,12 @@
|
|||
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",
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
import urwid
|
||||
|
||||
|
||||
def replace_column_element(columns, idx, elem):
|
||||
_, options = columns.contents[idx]
|
||||
columns.contents[idx] = elem, options
|
||||
|
||||
|
||||
class CenterButton(urwid.Button):
|
||||
def __init__(self, label, on_press=None, user_data=None):
|
||||
super().__init__('', on_press, user_data)
|
||||
text = urwid.Text(label, align='center')
|
||||
text._selectable = True
|
||||
replace_column_element(self._w, 1, text)
|
||||
|
||||
|
||||
def decorate_button(button):
|
||||
# See the palette in start.py
|
||||
return urwid.AttrMap(button, None, focus_map='reversed')
|
||||
|
||||
|
||||
def wrap_in_frame(
|
||||
widget,
|
||||
title,
|
||||
on_back=None,
|
||||
on_next=None,
|
||||
next_btn=None,
|
||||
flash_text=None,
|
||||
message_text=None,
|
||||
):
|
||||
back_button_wrapper = urwid.WidgetDisable(urwid.Text(''))
|
||||
next_button_wrapper = urwid.WidgetDisable(urwid.Text(''))
|
||||
if on_back is not None:
|
||||
back_button = CenterButton('Back', on_back)
|
||||
back_button_wrapper = decorate_button(back_button)
|
||||
if on_next is not None:
|
||||
next_button = CenterButton('Next', on_next)
|
||||
next_button_wrapper = decorate_button(next_button)
|
||||
elif next_btn is not None:
|
||||
next_button_wrapper = next_btn
|
||||
if on_back is not None or on_next is not None or next_btn is not None:
|
||||
footer = urwid.Columns([
|
||||
urwid.WidgetDisable(urwid.Text('')),
|
||||
back_button_wrapper,
|
||||
urwid.WidgetDisable(urwid.Text('')),
|
||||
next_button_wrapper,
|
||||
urwid.WidgetDisable(urwid.Text('')),
|
||||
])
|
||||
footer_height = 1
|
||||
if flash_text is not None:
|
||||
flash_text = urwid.WidgetDisable(flash_text)
|
||||
footer = urwid.Pile([flash_text, footer])
|
||||
footer_height = 2
|
||||
elif message_text is not None:
|
||||
footer = urwid.Pile([message_text, footer])
|
||||
footer_height = 6 # ???
|
||||
footer_height += 1 # add 1 for the bottom padding
|
||||
footer = urwid.Filler(footer, valign='bottom', bottom=1)
|
||||
body = urwid.Pile([widget, (footer_height, footer)])
|
||||
else:
|
||||
body = widget
|
||||
header = urwid.Pile([
|
||||
urwid.Text(('bold', title), align='center'),
|
||||
urwid.Divider()
|
||||
])
|
||||
return urwid.Frame(body, header)
|
|
@ -8,6 +8,7 @@ from zope import component
|
|||
|
||||
from .StreamResponseHandler import StreamResponseHandler
|
||||
from ceo_common.interfaces import IHTTPClient, IConfig
|
||||
from ceo_common.model import Term
|
||||
from ceod.transactions.members import AddMemberTransaction
|
||||
|
||||
|
||||
|
@ -125,9 +126,12 @@ def user_dict_kv(d: Dict) -> List[Tuple[str]]:
|
|||
else:
|
||||
pairs.append(('forwarding addresses', ''))
|
||||
if 'terms' in d:
|
||||
pairs.append(('member terms', ','.join(d['terms'])))
|
||||
# sort the terms in chronological order for display purposes
|
||||
_terms = map(str, sorted(map(Term, d['terms'])))
|
||||
pairs.append(('member terms', ','.join(_terms)))
|
||||
if 'non_member_terms' in d:
|
||||
pairs.append(('non-member terms', ','.join(d['non_member_terms'])))
|
||||
_terms = map(str, sorted(map(Term, d['non_member_terms'])))
|
||||
pairs.append(('non-member terms', ','.join(_terms)))
|
||||
if 'password' in d:
|
||||
pairs.append(('password', d['password']))
|
||||
return pairs
|
||||
|
|
|
@ -11,33 +11,14 @@ Build-Depends: debhelper (>= 12.1.1),
|
|||
python3-venv (>= 3.7),
|
||||
libkrb5-dev (>= 1.17),
|
||||
libpq-dev (>= 11.13),
|
||||
libfreetype6-dev (>= 2.2.1),
|
||||
libimagequant-dev (>= 2.11.10),
|
||||
libjpeg62-turbo-dev (>= 1.3.1),
|
||||
liblcms2-dev (>= 2.2+git20110628),
|
||||
libtiff5-dev (>= 4.0.3),
|
||||
libwebp-dev (>= 0.5.1),
|
||||
libwebpdemux2 (>= 0.5.1),
|
||||
libwebpmux3 (>= 0.6.1-2),
|
||||
zlib1g-dev (>= 1:1.1.4),
|
||||
scdoc (>= 1.9)
|
||||
|
||||
Package: ceo-common
|
||||
Architecture: amd64
|
||||
Depends: python3 (>= 3.7),
|
||||
krb5-user (>= 1.17),
|
||||
libffi6 | libffi7 | libffi8,
|
||||
libkrb5-3 (>= 1.17),
|
||||
libpq5 (>= 11.13),
|
||||
libfreetype6 (>= 2.2.1),
|
||||
libimagequant0 (>= 2.11.10),
|
||||
libjpeg62-turbo (>= 1.3.1),
|
||||
liblcms2-2 (>= 2.2+git20110628),
|
||||
libtiff5 (>= 4.0.3),
|
||||
libwebp6 (>= 0.5.1),
|
||||
libwebpdemux2 (>= 0.5.1),
|
||||
libwebpmux3 (>= 0.6.1-2),
|
||||
zlib1g (>= 1:1.2),
|
||||
${python3:Depends},
|
||||
${misc:Depends}
|
||||
Description: CSC Electronic Office common files
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
asciimatics==1.13.0
|
||||
click==8.0.1
|
||||
cryptography==35.0.0
|
||||
Flask==2.0.1
|
||||
|
@ -12,3 +11,4 @@ zope.component==5.0.1
|
|||
zope.interface==5.4.0
|
||||
mysql-connector-python==8.0.26
|
||||
psycopg2==2.9.1
|
||||
urwid==2.1.2
|
||||
|
|
Loading…
Reference in New Issue