import sys from typing import Dict import click from zope import component from ..utils import http_post, http_get, http_patch, http_delete, get_failed_operations from .utils import handle_stream_response, handle_sync_response, print_colon_kv, \ check_if_in_development from ceo_common.interfaces import IConfig from ceo_common.model import Term from ceod.transactions.members import ( AddMemberTransaction, DeleteMemberTransaction, ) @click.group() def members(): pass @members.command(short_help='Add a new member or club rep') @click.argument('username') @click.option('--cn', help='Full name', prompt='Full name') @click.option('--program', required=False, help='Academic program') @click.option('--terms', 'num_terms', type=click.IntRange(1, 100), help='Number of terms to add', prompt='Number of terms') @click.option('--clubrep', is_flag=True, default=False, help='Add non-member terms instead of member terms') @click.option('--forwarding-address', required=False, help=('Forwarding address to set in ~/.forward. ' 'Default is UW address. ' 'Set to the empty string to disable forwarding.')) def add(username, cn, program, num_terms, clubrep, forwarding_address): cfg = component.getUtility(IConfig) uw_domain = cfg.get('uw_domain') current_term = Term.current() terms = [current_term + i for i in range(num_terms)] terms = list(map(str, terms)) if forwarding_address is None: forwarding_address = username + '@' + uw_domain click.echo("The following user will be created:") lines = [ ('uid', username), ('cn', cn), ] if program is not None: lines.append(('program', program)) if clubrep: lines.append(('non-member terms', ','.join(terms))) else: lines.append(('member terms', ','.join(terms))) if forwarding_address != '': lines.append(('forwarding address', forwarding_address)) print_colon_kv(lines) click.confirm('Do you want to continue?', abort=True) body = { 'uid': username, 'cn': cn, } if program is not None: body['program'] = program if clubrep: body['non_member_terms'] = terms else: body['terms'] = terms if forwarding_address != '': body['forwarding_addresses'] = [forwarding_address] operations = AddMemberTransaction.operations if forwarding_address == '': # don't bother displaying this because it won't be run operations.remove('set_forwarding_addresses') resp = http_post('/api/members', json=body) data = handle_stream_response(resp, operations) result = data[-1]['result'] print_user_lines(result) failed_operations = get_failed_operations(data) if 'send_welcome_message' in failed_operations: click.echo(click.style( 'Warning: welcome message was not sent. You now need to manually ' 'send the user their password.', fg='yellow')) def print_user_lines(result: Dict): """Pretty-print a user JSON response.""" lines = [ ('uid', result['uid']), ('cn', result['cn']), ('program', result.get('program', 'Unknown')), ('UID number', result['uid_number']), ('GID number', result['gid_number']), ('login shell', result['login_shell']), ('home directory', result['home_directory']), ('is a club', result['is_club']), ] if 'forwarding_addresses' in result: lines.append(('forwarding addresses', ','.join(result['forwarding_addresses']))) if 'terms' in result: lines.append(('terms', ','.join(result['terms']))) if 'non_member_terms' in result: lines.append(('non-member terms', ','.join(result['non_member_terms']))) if 'password' in result: lines.append(('password', result['password'])) print_colon_kv(lines) @members.command(short_help='Get info about a user') @click.argument('username') def get(username): resp = http_get('/api/members/' + username) result = handle_sync_response(resp) print_user_lines(result) @members.command(short_help="Replace a user's login shell or forwarding addresses") @click.argument('username') @click.option('--login-shell', required=False, help='Login shell') @click.option('--forwarding-addresses', required=False, help=( 'Comma-separated list of forwarding addresses. ' 'Set to the empty string to disable forwarding.' )) def modify(username, login_shell, forwarding_addresses): if login_shell is None and forwarding_addresses is None: click.echo('Nothing to do.') sys.exit() operations = [] body = {} if login_shell is not None: body['login_shell'] = login_shell operations.append('replace_login_shell') click.echo('Login shell will be set to: ' + login_shell) if forwarding_addresses is not None: if forwarding_addresses == '': forwarding_addresses = [] else: forwarding_addresses = forwarding_addresses.split(',') body['forwarding_addresses'] = forwarding_addresses operations.append('replace_forwarding_addresses') prefix = '~/.forward will be set to: ' if len(forwarding_addresses) > 0: click.echo(prefix + forwarding_addresses[0]) for address in forwarding_addresses[1:]: click.echo((' ' * len(prefix)) + address) else: click.echo(prefix) click.confirm('Do you want to continue?', abort=True) resp = http_patch('/api/members/' + username, json=body) handle_stream_response(resp, operations) @members.command(short_help="Renew a member or club rep's membership") @click.argument('username') @click.option('--terms', 'num_terms', type=click.IntRange(1, 100), help='Number of terms to add', prompt='Number of terms') @click.option('--clubrep', is_flag=True, default=False, help='Add non-member terms instead of member terms') def renew(username, num_terms, clubrep): resp = http_get('/api/members/' + username) result = handle_sync_response(resp) 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)] terms = list(map(str, terms)) if clubrep: body = {'non_member_terms': terms} click.echo('The following non-member terms will be added: ' + ','.join(terms)) else: body = {'terms': terms} click.echo('The following member terms will be added: ' + ','.join(terms)) click.confirm('Do you want to continue?', abort=True) resp = http_post(f'/api/members/{username}/renew', json=body) handle_sync_response(resp) click.echo('Done.') @members.command(short_help="Reset a user's password") @click.argument('username') def pwreset(username): click.confirm(f"Are you sure you want to reset {username}'s password?", abort=True) resp = http_post(f'/api/members/{username}/pwreset') result = handle_sync_response(resp) click.echo('New password: ' + result['password']) @members.command(short_help="Delete a user") @click.argument('username') def delete(username): check_if_in_development() click.confirm(f"Are you sure you want to delete {username}?", abort=True) resp = http_delete(f'/api/members/{username}') handle_stream_response(resp, DeleteMemberTransaction.operations)