Merge branch 'v1' into no-resizing
continuous-integration/drone/pr Build is passing Details

This commit is contained in:
Max Erenberg 2021-09-26 19:03:08 -04:00
commit 65131337d1
13 changed files with 89 additions and 21 deletions

View File

@ -6,6 +6,7 @@ from .positions import positions
from .updateprograms import updateprograms
from .mysql import mysql
from .postgresql import postgresql
from .mailman import mailman
@click.group()
@ -19,3 +20,4 @@ cli.add_command(positions)
cli.add_command(updateprograms)
cli.add_command(mysql)
cli.add_command(postgresql)
cli.add_command(mailman)

29
ceo/cli/mailman.py Normal file
View File

@ -0,0 +1,29 @@
import click
from ..utils import http_post, http_delete
from .utils import handle_sync_response
@click.group(short_help='Manage mailing list subscriptions')
def mailman():
pass
@mailman.command(short_help='Subscribe a member to a mailing list')
@click.argument('username')
@click.argument('mailing_list')
def subscribe(username, mailing_list):
click.confirm(f'Are you sure you want to subscribe {username} to {mailing_list}?', abort=True)
resp = http_post(f'/api/mailman/{mailing_list}/{username}')
handle_sync_response(resp)
click.echo('Done.')
@mailman.command(short_help='Unsubscribe a member from a mailing list')
@click.argument('username')
@click.argument('mailing_list')
def unsubscribe(username, mailing_list):
click.confirm(f'Are you sure you want to unsubscribe {username} from {mailing_list}?', abort=True)
resp = http_delete(f'/api/mailman/{mailing_list}/{username}')
handle_sync_response(resp)
click.echo('Done.')

View File

@ -60,10 +60,7 @@ class CeoFrame(Frame):
after some kind of operation was completed.
This is called from Model.reset().
"""
# Reset input fields to ''.
# This is causing an Exception to be raised...
# self.save()
# self.data = {key: '' for key in self.data}
pass
def clear_layouts(self):
self._layouts.clear()

View File

@ -1,10 +1,16 @@
from copy import deepcopy
from zope import component
from ceo_common.interfaces import IConfig
class Model:
"""A convenient place to store View data persistently."""
def __init__(self):
cfg = component.getUtility(IConfig)
self.screen = None
self.views = []
self.title = None
@ -19,6 +25,8 @@ class Model:
'uid': '',
},
}
for pos in cfg.get('positions_available'):
self._initial_viewdata[pos] = ''
self.viewdata = deepcopy(self._initial_viewdata)
# data which is shared between multiple views
self.is_club_rep = False

View File

@ -31,17 +31,6 @@ class GetPositionsView(CeoFrame):
)
self._position_widgets = {}
def _add_blank_line(self):
self._main_layout.add_widget(Label(' ', 0))
self._main_layout.add_widget(Label(' ', 2))
def _add_pair(self, key: str, val: str):
key_widget = Label(key + ':', align='>')
value_widget = Label(val, align='<')
self._main_layout.add_widget(key_widget, 0)
self._main_layout.add_widget(value_widget, 2)
return value_widget
def _on_load(self):
cfg = component.getUtility(IConfig)
avail = cfg.get('positions_available')
@ -73,6 +62,17 @@ class GetPositionsView(CeoFrame):
self.clear_flash_message(force_update=True)
Thread(target=target).start()
def _add_blank_line(self):
self._main_layout.add_widget(Label(' ', 0))
self._main_layout.add_widget(Label(' ', 2))
def _add_pair(self, key: str, val: str):
key_widget = Label(key + ':', align='>')
value_widget = Label(val, align='<')
self._main_layout.add_widget(key_widget, 0)
self._main_layout.add_widget(value_widget, 2)
return value_widget
def _ceoframe_on_reset(self):
super()._ceoframe_on_reset()
# clear the labels

View File

@ -16,6 +16,8 @@ def http_request(method: str, path: str, **kwargs) -> requests.Response:
cfg = component.getUtility(IConfig)
if path.startswith('/api/db'):
host = cfg.get('ceod_database_host')
elif path.startswith('/api/mailman'):
host = cfg.get('ceod_mailman_host')
else:
host = cfg.get('ceod_admin_host')
return client.request(

View File

@ -67,7 +67,7 @@ def authz_restrict_to_groups(f: Callable, allowed_groups: List[str]) -> Callable
def authz_restrict_to_staff(f: Callable) -> Callable:
"""A decorator to restrict an endpoint to staff members."""
allowed_groups = ['office', 'staff', 'adm']
allowed_groups = ['syscom', 'exec', 'office', 'staff', 'adm']
return authz_restrict_to_groups(f, allowed_groups)

View File

@ -69,7 +69,7 @@ class MailService:
if '@' in auth_user:
auth_user = auth_user[:auth_user.index('@')]
if user.is_club():
if user.terms:
prog = 'addclubrep'
desc = 'Club Rep'
else:

View File

@ -23,7 +23,7 @@ class MockHandler:
async def handle_DATA(self, server, session, envelope):
msg = {
'from': envelope.mail_from,
'to': envelope.rcpt_tos[0],
'to': ','.join(envelope.rcpt_tos),
'content': envelope.content.decode(),
}
self.mock_server.messages.append(msg)

View File

@ -0,0 +1,23 @@
from click.testing import CliRunner
from ceo.cli import cli
def test_mailman_subscribe(cli_setup, mock_mailman_server):
mock_mailman_server.clear()
runner = CliRunner()
result = runner.invoke(cli, ['mailman', 'subscribe', 'test_1', 'exec'], input='y\n')
expected = (
"Are you sure you want to subscribe test_1 to exec? [y/N]: y\n"
"Done.\n"
)
assert result.exit_code == 0
assert result.output == expected
result = runner.invoke(cli, ['mailman', 'unsubscribe', 'test_1', 'exec'], input='y\n')
expected = (
"Are you sure you want to unsubscribe test_1 from exec? [y/N]: y\n"
"Done.\n"
)
assert result.exit_code == 0
assert result.output == expected

View File

@ -6,6 +6,7 @@ uw_domain = uwaterloo.internal
# this is the host with the ceod/admin Kerberos key
admin_host = phosphoric-acid
database_host = coffee
mailman_host = mail
use_https = false
port = 9987

View File

@ -12,7 +12,8 @@ def test_api_user_not_found(client):
@pytest.fixture(scope='module')
def create_user_resp(client, mocks_for_create_user):
def create_user_resp(client, mocks_for_create_user, mock_mail_server):
mock_mail_server.messages.clear()
status, data = client.post('/api/members', json={
'uid': 'test_1',
'cn': 'Test One',
@ -35,7 +36,7 @@ def create_user_result(create_user_resp):
return data[-1]['result']
def test_api_create_user(cfg, create_user_resp):
def test_api_create_user(cfg, create_user_resp, mock_mail_server):
_, data = create_user_resp
min_uid = cfg.get('members_min_id')
expected = [
@ -62,6 +63,12 @@ def test_api_create_user(cfg, create_user_resp):
}},
]
assert data == expected
# Two messages should have been sent: a welcome message to the new member,
# and an announcement to the ceo mailing list
assert len(mock_mail_server.messages) == 2
assert mock_mail_server.messages[0]['to'] == 'test_1@csclub.internal'
assert mock_mail_server.messages[1]['to'] == 'ceo@csclub.internal,ctdalek@csclub.internal'
mock_mail_server.messages.clear()
def test_api_next_uid(cfg, client, create_user_result):

View File

@ -208,7 +208,6 @@ def mock_mailman_server():
@pytest.fixture(scope='session')
def mailman_srv(mock_mailman_server, cfg, http_client):
# TODO: test the RemoteMailmanService as well
mailman = MailmanService()
component.getGlobalSiteManager().registerUtility(mailman, IMailmanService)
return mailman