Compare commits

...

6 Commits

Author SHA1 Message Date
Rio Liu 6577fb3ea6 fix flake8 2021-08-30 23:52:37 -04:00
Rio Liu 36def99b28 add unit test for positions and replace provideUtility() with getGlobalSiteManager().registerUtility() in unit tests 2021-08-30 23:42:51 -04:00
Rio Liu 5bae89a9fd handle empty input in print_colon_kv 2021-08-30 17:37:48 -04:00
Rio Liu 8f5a2803a6 fix some pr feedbacks 2021-08-30 16:59:53 -04:00
Rio6 8c9ddd2d27 fix flake 2021-08-30 16:59:53 -04:00
Rio6 a08a28c98f positions cli 2021-08-30 16:59:52 -04:00
8 changed files with 136 additions and 13 deletions

View File

@ -8,6 +8,7 @@ from zope import component
from ..krb_check import krb_check
from .members import members
from .groups import groups
from .positions import positions
from .updateprograms import updateprograms
from ceo_common.interfaces import IConfig, IHTTPClient
from ceo_common.model import Config, HTTPClient
@ -29,10 +30,14 @@ def cli(ctx):
cli.add_command(members)
cli.add_command(groups)
cli.add_command(positions)
cli.add_command(updateprograms)
def register_services():
# Using base component directly so events get triggered
baseComponent = component.getGlobalSiteManager()
# Config
# This is a hack to determine if we're in the dev env or not
if socket.getfqdn().endswith('.csclub.internal'):
@ -41,8 +46,8 @@ def register_services():
else:
config_file = os.environ.get('CEO_CONFIG', '/etc/csc/ceo.ini')
cfg = Config(config_file)
component.provideUtility(cfg, IConfig)
baseComponent.registerUtility(cfg, IConfig)
# HTTPService
http_client = HTTPClient()
component.provideUtility(http_client, IHTTPClient)
baseComponent.registerUtility(http_client, IHTTPClient)

51
ceo/cli/positions.py Normal file
View File

@ -0,0 +1,51 @@
import click
from zope import component
from zope.interface.interfaces import IRegistered, IUtilityRegistration
import zope.component.event # noqa: F401
from ceo_common.interfaces import IConfig
from ceod.transactions.members import UpdateMemberPositionsTransaction
from .utils import handle_sync_response, handle_stream_response, print_colon_kv
from ..utils import http_get, http_post
@click.group(short_help='List or change exec positions')
def positions():
pass
@positions.command(short_help='Get current positions')
def get():
resp = http_get('/api/positions')
result = handle_sync_response(resp)
print_colon_kv(result.items())
@positions.command(short_help='Update positions')
def set(**kwargs):
click.echo('The positions will be updated:')
print_colon_kv(kwargs.items())
click.confirm('Do you want to continue?', abort=True)
resp = http_post('/api/positions', json={k.replace('_', '-'): v for k, v in kwargs.items()})
handle_stream_response(resp, UpdateMemberPositionsTransaction.operations)
# Provides dynamic parameter for update command using config file
@component.provideHandler
@component.adapter(IRegistered)
def _handler(event):
global set
if not (IUtilityRegistration.providedBy(event.object) and IConfig.providedBy(event.object.component)):
return
cfg = event.object.component
avail = cfg.get('positions_available')
required = cfg.get('positions_required')
for pos in avail:
r = pos in required
set = click.option(f'--{pos}', metavar='USERNAME', required=r, prompt=r)(set)

View File

@ -28,6 +28,9 @@ def print_colon_kv(pairs: List[Tuple[str, str]]):
key1: value1
key1000: value2
"""
if len(pairs) == 0:
return
maxlen = max(len(key) for key, val in pairs)
for key, val in pairs:
if key != '':

View File

@ -24,4 +24,7 @@ descriptions = {
'remove_user_from_auxiliary_groups': 'Remove user from auxiliary groups',
'unsubscribe_user_from_auxiliary_mailing_lists': 'Unsubscribe user from auxiliary mailing lists',
'remove_sudo_role': 'Remove sudo role from LDAP',
'update_positions_ldap': 'Update positions in LDAP',
'update_exec_group_ldap': 'Update executive group in LDAP',
'subscribe_to_mailing_lists': 'Subscribe to mailing lists',
}

View File

@ -28,7 +28,8 @@ class UpdateMemberPositionsTransaction(AbstractTransaction):
# Reverse the dict so it's easier to use (username -> positions)
self.positions = defaultdict(list)
for position, username in positions_reversed.items():
self.positions[username].append(position)
if username is not None:
self.positions[username].append(position)
# a cached Dict of the Users who need to be modified (username -> User)
self.users: Dict[str, IUser] = {}

View File

@ -0,0 +1,55 @@
from click.testing import CliRunner
from ceo.cli import cli
def test_positions(cli_setup):
runner = CliRunner()
# Setup test data
for i in range(5):
runner.invoke(cli, ['members', 'add', f'test_{i}', '--cn', f'Test {i}', '--program', 'Math', '--terms', '1'], input='y\n')
runner.invoke(cli, ['groups', 'add', 'exec', '--description', 'Test Group'], input='y\n')
result = runner.invoke(cli, [
'positions', 'set',
'--president', 'test_0',
'--vice-president', 'test_1',
'--sysadmin', 'test_2',
'--secretary', 'test_3',
'--webmaster', 'test_4',
], input='y\n')
assert result.exit_code == 0
assert result.output == '''
The positions will be updated:
president: test_0
vice_president: test_1
sysadmin: test_2
secretary: test_3
webmaster: test_4
treasurer:
cro:
librarian:
imapd:
offsck:
Do you want to continue? [y/N]: y
Update positions in LDAP... Done
Update executive group in LDAP... Done
Subscribe to mailing lists... Done
Transaction successfully completed.
'''[1:] # noqa: W291
result = runner.invoke(cli, ['positions', 'get'])
assert result.exit_code == 0
assert result.output == '''
president: test_0
secretary: test_3
sysadmin: test_2
vice-president: test_1
webmaster: test_4
'''[1:]
# Cleanup test data
for i in range(5):
runner.invoke(cli, ['members', 'delete', f'test_{i}'], input='y\n')
runner.invoke(cli, ['groups', 'delete', 'exec'], input='y\n')

View File

@ -7,3 +7,8 @@ uw_domain = uwaterloo.internal
admin_host = phosphoric-acid
use_https = false
port = 9987
[positions]
required = president,vice-president,sysadmin
available = president,vice-president,treasurer,secretary,
sysadmin,cro,librarian,imapd,webmaster,offsck

View File

@ -51,7 +51,7 @@ def cfg(_drone_hostname_mock):
with importlib.resources.path('tests', 'ceod_test_local.ini') as p:
config_file = p.__fspath__()
_cfg = Config(config_file)
component.provideUtility(_cfg, IConfig)
component.getGlobalSiteManager().registerUtility(_cfg, IConfig)
return _cfg
@ -75,7 +75,7 @@ def krb_srv(cfg):
else:
principal = 'ceod/' + socket.getfqdn()
krb = KerberosService(principal)
component.provideUtility(krb, IKerberosService)
component.getGlobalSiteManager().registerUtility(krb, IKerberosService)
delete_test_princs(krb)
yield krb
@ -160,7 +160,7 @@ def ldap_srv_session(cfg, krb_srv, ldap_conn):
conn.add(base_dn, 'organizationalUnit')
_ldap_srv = LDAPService()
component.provideUtility(_ldap_srv, ILDAPService)
component.getGlobalSiteManager().registerUtility(_ldap_srv, ILDAPService)
yield _ldap_srv
@ -180,7 +180,7 @@ def ldap_srv(ldap_srv_session, g_admin_ctx):
@pytest.fixture(scope='session')
def file_srv(cfg):
_file_srv = FileService()
component.provideUtility(_file_srv, IFileService)
component.getGlobalSiteManager().registerUtility(_file_srv, IFileService)
members_home = cfg.get('members_home')
clubs_home = cfg.get('clubs_home')
@ -194,7 +194,7 @@ def file_srv(cfg):
@pytest.fixture(scope='session')
def http_client(cfg):
_client = HTTPClient()
component.provideUtility(_client, IHTTPClient)
component.getGlobalSiteManager().registerUtility(_client, IHTTPClient)
return _client
@ -210,7 +210,7 @@ def mock_mailman_server():
def mailman_srv(mock_mailman_server, cfg, http_client):
# TODO: test the RemoteMailmanService as well
mailman = MailmanService()
component.provideUtility(mailman, IMailmanService)
component.getGlobalSiteManager().registerUtility(mailman, IMailmanService)
return mailman
@ -223,7 +223,7 @@ def uwldap_srv(cfg, ldap_conn):
conn.add(base_dn, 'organizationalUnit')
_uwldap_srv = UWLDAPService()
component.provideUtility(_uwldap_srv, IUWLDAPService)
component.getGlobalSiteManager().registerUtility(_uwldap_srv, IUWLDAPService)
yield _uwldap_srv
delete_subtree(conn, base_dn)
@ -240,21 +240,21 @@ def mock_mail_server():
@pytest.fixture(scope='session')
def mail_srv(cfg, mock_mail_server):
_mail_srv = MailService()
component.provideUtility(_mail_srv, IMailService)
component.getGlobalSiteManager().registerUtility(_mail_srv, IMailService)
return _mail_srv
@pytest.fixture(scope='session')
def mysql_srv(cfg):
mysql_srv = MySQLService()
component.provideUtility(mysql_srv, IDatabaseService, 'mysql')
component.getGlobalSiteManager().registerUtility(mysql_srv, IDatabaseService, 'mysql')
return mysql_srv
@pytest.fixture(scope='session')
def postgresql_srv(cfg):
psql_srv = PostgreSQLService()
component.provideUtility(psql_srv, IDatabaseService, 'postgresql')
component.getGlobalSiteManager().registerUtility(psql_srv, IDatabaseService, 'postgresql')
return psql_srv