parent
cce920d6ba
commit
ee21873ad7
@ -0,0 +1,38 @@ |
||||
from typing import List |
||||
|
||||
from .utils import http_get |
||||
from ceo_common.model import Term |
||||
import ceo.cli.utils as cli_utils |
||||
import ceo.tui.utils as tui_utils |
||||
|
||||
# Had to put these in a separate file to avoid a circular import. |
||||
|
||||
|
||||
def get_terms_for_new_user(num_terms: int) -> List[str]: |
||||
current_term = Term.current() |
||||
terms = [current_term + i for i in range(num_terms)] |
||||
return list(map(str, terms)) |
||||
|
||||
|
||||
def get_terms_for_renewal( |
||||
username: str, num_terms: int, clubrep: bool, tui_model=None, |
||||
) -> List[str]: |
||||
resp = http_get('/api/members/' + username) |
||||
if tui_model is None: |
||||
result = cli_utils.handle_sync_response(resp) |
||||
else: |
||||
result = tui_utils.handle_sync_response(resp, tui_model) |
||||
max_term = None |
||||
current_term = Term.current() |
||||
if clubrep and 'non_member_terms' in result: |
||||
max_term = max(Term(s) for s in result['non_member_terms']) |
||||
elif not clubrep and 'terms' in result: |
||||
max_term = max(Term(s) for s in result['terms']) |
||||
|
||||
if max_term is not None and max_term >= current_term: |
||||
next_term = max_term + 1 |
||||
else: |
||||
next_term = Term.current() |
||||
|
||||
terms = [next_term + i for i in range(num_terms)] |
||||
return list(map(str, terms)) |
@ -0,0 +1,92 @@ |
||||
from asciimatics.exceptions import NextScene |
||||
from asciimatics.widgets import Frame, Layout, Divider, Button |
||||
|
||||
|
||||
class CeoFrame(Frame): |
||||
def __init__( |
||||
self, |
||||
screen, |
||||
height, |
||||
width, |
||||
model, |
||||
name, # key in model.viewdata |
||||
on_load=None, |
||||
title=None, |
||||
save_data=False, # whether to save widget state for resizing |
||||
): |
||||
super().__init__( |
||||
screen, |
||||
height, |
||||
width, |
||||
name=name, |
||||
can_scroll=False, |
||||
title=title, |
||||
on_load=self._ceoframe_on_load, |
||||
) |
||||
self._save_data = save_data |
||||
self._extra_on_load = on_load |
||||
self._model = model |
||||
self._name = name |
||||
self._loaded = False |
||||
|
||||
def _ceoframe_on_load(self): |
||||
# We usually don't want _on_load() to be called multiple times |
||||
# e.g. when switching back to a scene |
||||
if self._loaded: |
||||
return |
||||
self._loaded = True |
||||
if self._model.title is not None: |
||||
self.title = self._model.title |
||||
self._model.title = None |
||||
if self._save_data: |
||||
# restore the saved input fields' values |
||||
self.data = self._model.viewdata[self._name] |
||||
if self._extra_on_load is not None: |
||||
self._extra_on_load() |
||||
|
||||
def _on_unload(self): |
||||
if not self._save_data: |
||||
return |
||||
# save the input fields' values so that they don't disappear when |
||||
# the window gets resized |
||||
self.save() |
||||
self._model.viewdata[self._name] = self.data |
||||
|
||||
def add_buttons( |
||||
self, back_btn=False, back_btn_text='Back', |
||||
next_scene=None, next_scene_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(): |
||||
raise NextScene(self._model.scene_stack.pop()) |
||||
|
||||
def _next(): |
||||
if on_next_excl is not None: |
||||
on_next_excl() |
||||
return |
||||
if on_next is not None: |
||||
on_next() |
||||
self._model.scene_stack.append(self._name) |
||||
raise NextScene(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_scene_text, _next), 1) |
@ -0,0 +1,29 @@ |
||||
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,35 +1,48 @@ |
||||
from copy import deepcopy |
||||
|
||||
|
||||
class Model: |
||||
"""A convenient place to store View data persistently.""" |
||||
|
||||
def __init__(self): |
||||
# simple key-value pairs |
||||
self.screen = None |
||||
self.title = None |
||||
self.scene_stack = [] |
||||
self.deferred_req = None |
||||
self.error_message = None |
||||
# view-specific data, to be used when e.g. resizing the window |
||||
self._initial_viewdata = { |
||||
'adduser': { |
||||
'AddUser': { |
||||
'uid': '', |
||||
'cn': '', |
||||
'program': '', |
||||
'forwarding_address': '', |
||||
'num_terms': '1', |
||||
}, |
||||
'transaction': { |
||||
'RenewUser': { |
||||
'uid': '', |
||||
'num_terms': '1', |
||||
}, |
||||
'Transaction': { |
||||
'op_layout': None, |
||||
'msg_layout': None, |
||||
'labels': {}, |
||||
'status': 'not started', |
||||
}, |
||||
'Result': {}, |
||||
} |
||||
self.viewdata = deepcopy(self._initial_viewdata) |
||||
# data which is shared between multiple views |
||||
self.for_member = True |
||||
self.is_club_rep = False |
||||
self.confirm_lines = None |
||||
self.operations = None |
||||
self.deferred_req = None |
||||
|
||||
def reset_viewdata(self): |
||||
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.title = None |
||||
self.error_message = None |
||||
self.scene_stack.clear() |
||||
|
@ -0,0 +1,66 @@ |
||||
from threading import Thread |
||||
|
||||
from asciimatics.exceptions import NextScene |
||||
from asciimatics.widgets import Layout, Label, Button, Divider |
||||
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', |
||||
) |
||||
layout = Layout([1, 10]) |
||||
self.add_layout(layout) |
||||
layout.add_widget(Label(''), 1) |
||||
self._status_label = Label('Sending request... ') |
||||
layout.add_widget(self._status_label, 1) |
||||
layout.add_widget(Label(''), 1) |
||||
self._summary_layout = Layout([1, 10], fill_frame=True) |
||||
self.add_layout(self._summary_layout) |
||||
|
||||
self._add_buttons() |
||||
self.fix() |
||||
|
||||
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) |
||||
self._next_btn.disabled = True |
||||
layout.add_widget(self._next_btn, 1) |
||||
|
||||
def _show_msg(self, text: str = ''): |
||||
for line in text.splitlines(): |
||||
self._summary_layout.add_widget(Label(line), 1) |
||||
|
||||
# override this method in child classes if desired |
||||
def show_result(self, resp: requests.Response): |
||||
self._show_msg('The operation was successfully performed.') |
||||
|
||||
def _resultview_on_load(self): |
||||
def target(): |
||||
try: |
||||
resp = self._model.deferred_req() |
||||
self._status_label.text += 'Done.' |
||||
self._next_btn.disabled = False |
||||
if resp.status_code != 200: |
||||
self._show_msg('An error occurred:') |
||||
self._show_msg(resp.text.rstrip()) |
||||
return |
||||
self.show_result(resp) |
||||
finally: |
||||
self.fix() |
||||
self.reset() |
||||
self._model.screen.force_update() |
||||
Thread(target=target).start() |
||||
|
||||
def _next(self): |
||||
self._model.reset() |
||||
raise NextScene('Welcome') |
@ -0,0 +1,64 @@ |
||||
from asciimatics.widgets import Layout, Text, Label |
||||
|
||||
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', |
||||
save_data=True, |
||||
) |
||||
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') |
||||
layout.add_widget(self._num_terms) |
||||
|
||||
layout = Layout([100]) |
||||
self.add_layout(layout) |
||||
self._status_label = Label('') |
||||
layout.add_widget(self._status_label) |
||||
|
||||
self.add_buttons( |
||||
back_btn=True, |
||||
next_scene='Confirm', on_next=self._next) |
||||
self.fix() |
||||
|
||||
def _next(self): |
||||
uid = self._username.value |
||||
self._status_label.text = 'Looking up user...' |
||||
self._model.screen.force_update() |
||||
self._model.screen.draw_next_frame() |
||||
new_terms = get_terms_for_renewal( |
||||
uid, |
||||
int(self._num_terms.value), |
||||
self._model.is_club_rep, |
||||
self._model, |
||||
) |
||||
self._status_label.text = '' |
||||
|
||||
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) |
@ -0,0 +1,10 @@ |
||||
from asciimatics.exceptions import NextScene |
||||
|
||||
import requests |
||||
|
||||
|
||||
def handle_sync_response(resp: requests.Response, model): |
||||
if resp.status_code != 200: |
||||
model.error_message = resp.text.rstrip() |
||||
raise NextScene('Error') |
||||
return resp.json() |
Loading…
Reference in new issue