Positions API (#7)
Co-authored-by: Max Erenberg <merenber@csclub.uwaterloo.ca> Co-authored-by: Rio Liu <r345liu@csclub.uwaterloo.ca> Co-authored-by: Rio6 <rio.liu@r26.me> Reviewed-on: #7 Co-authored-by: Rio <r345liu@localhost> Co-committed-by: Rio <r345liu@localhost>pull/13/head
parent
0974a7471b
commit
ad937eebeb
@ -0,0 +1,45 @@ |
||||
from flask import Blueprint, request |
||||
from zope import component |
||||
|
||||
from .utils import authz_restrict_to_syscom, create_streaming_response |
||||
from ceo_common.interfaces import ILDAPService, IConfig |
||||
from ceod.transactions.members import UpdateMemberPositionsTransaction |
||||
|
||||
bp = Blueprint('positions', __name__) |
||||
|
||||
|
||||
@bp.route('/', methods=['GET'], strict_slashes=False) |
||||
def get_positions(): |
||||
ldap_srv = component.getUtility(ILDAPService) |
||||
|
||||
positions = {} |
||||
for user in ldap_srv.get_users_with_positions(): |
||||
for position in user.positions: |
||||
positions[position] = user.uid |
||||
|
||||
return positions |
||||
|
||||
|
||||
@bp.route('/', methods=['POST'], strict_slashes=False) |
||||
@authz_restrict_to_syscom |
||||
def update_positions(): |
||||
cfg = component.getUtility(IConfig) |
||||
body = request.get_json(force=True) |
||||
|
||||
required = cfg.get('positions_required') |
||||
available = cfg.get('positions_available') |
||||
|
||||
for position in body.keys(): |
||||
if position not in available: |
||||
return { |
||||
'error': f'unknown position: {position}' |
||||
}, 400 |
||||
|
||||
for position in required: |
||||
if position not in body: |
||||
return { |
||||
'error': f'missing required position: {position}' |
||||
}, 400 |
||||
|
||||
txn = UpdateMemberPositionsTransaction(body) |
||||
return create_streaming_response(txn) |
@ -0,0 +1,109 @@ |
||||
from collections import defaultdict |
||||
from typing import Dict |
||||
|
||||
from zope import component |
||||
|
||||
from ..AbstractTransaction import AbstractTransaction |
||||
from ceo_common.interfaces import ILDAPService, IConfig, IUser |
||||
from ceo_common.errors import UserAlreadySubscribedError, UserNotSubscribedError |
||||
from ceo_common.logger_factory import logger_factory |
||||
|
||||
logger = logger_factory(__name__) |
||||
|
||||
|
||||
class UpdateMemberPositionsTransaction(AbstractTransaction): |
||||
"""Transaction to update the CSC's executive positions.""" |
||||
|
||||
operations = [ |
||||
'update_positions_ldap', |
||||
'update_exec_group_ldap', |
||||
'subscribe_to_mailing_lists', |
||||
] |
||||
|
||||
def __init__(self, positions_reversed: Dict[str, str]): |
||||
# positions_reversed is position -> username |
||||
super().__init__() |
||||
self.ldap_srv = component.getUtility(ILDAPService) |
||||
|
||||
# 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) |
||||
|
||||
# a cached Dict of the Users who need to be modified (username -> User) |
||||
self.users: Dict[str, IUser] = {} |
||||
|
||||
# for rollback purposes |
||||
self.old_positions = {} # username -> positions |
||||
self.old_execs = [] |
||||
|
||||
def child_execute_iter(self): |
||||
cfg = component.getUtility(IConfig) |
||||
mailing_lists = cfg.get('auxiliary mailing lists_exec') |
||||
|
||||
# position -> username |
||||
new_positions_reversed = {} # For returning result |
||||
|
||||
# retrieve User objects and cache them |
||||
for username in self.positions: |
||||
user = self.ldap_srv.get_user(username) |
||||
self.users[user.uid] = user |
||||
|
||||
# Remove positions for old users |
||||
for user in self.ldap_srv.get_users_with_positions(): |
||||
if user.uid not in self.positions: |
||||
self.positions[user.uid] = [] |
||||
self.users[user.uid] = user |
||||
|
||||
# Update positions in LDAP |
||||
for username, new_positions in self.positions.items(): |
||||
user = self.users[username] |
||||
old_positions = user.positions[:] |
||||
|
||||
user.set_positions(new_positions) |
||||
|
||||
self.old_positions[username] = old_positions |
||||
for position in new_positions: |
||||
new_positions_reversed[position] = username |
||||
yield 'update_positions_ldap' |
||||
|
||||
# update exec group in LDAP |
||||
exec_group = self.ldap_srv.get_group('exec') |
||||
self.old_execs = exec_group.members[:] |
||||
new_execs = [ |
||||
username for username, new_positions in self.positions.items() |
||||
if len(new_positions) > 0 |
||||
] |
||||
exec_group.set_members(new_execs) |
||||
yield 'update_exec_group_ldap' |
||||
|
||||
# Update mailing list subscriptions |
||||
subscription_failed = False |
||||
for username, new_positions in self.positions.items(): |
||||
user = self.users[username] |
||||
for mailing_list in mailing_lists: |
||||
try: |
||||
if len(new_positions) > 0: |
||||
user.subscribe_to_mailing_list(mailing_list) |
||||
else: |
||||
user.unsubscribe_from_mailing_list(mailing_list) |
||||
except (UserAlreadySubscribedError, UserNotSubscribedError): |
||||
pass |
||||
except Exception: |
||||
logger.warning(f'Failed to update mailing list for {user.uid}') |
||||
subscription_failed = True |
||||
if subscription_failed: |
||||
yield 'failed_to_subscribe_to_mailing_lists' |
||||
else: |
||||
yield 'subscribe_to_mailing_lists' |
||||
|
||||
self.finish(new_positions_reversed) |
||||
|
||||
def rollback(self): |
||||
if 'update_exec_group_ldap' in self.finished_operations: |
||||
exec_group = self.ldap_srv.get_group('exec') |
||||
exec_group.set_members(self.old_execs) |
||||
|
||||
for username, positions in self.old_positions.items(): |
||||
user = self.users[username] |
||||
user.set_positions(positions) |
@ -0,0 +1,93 @@ |
||||
from ceod.model import User, Group |
||||
|
||||
|
||||
def test_get_positions(client, ldap_user, g_admin_ctx): |
||||
with g_admin_ctx(): |
||||
ldap_user.set_positions(['president', 'treasurer']) |
||||
status, data = client.get('/api/positions') |
||||
assert status == 200 |
||||
expected = { |
||||
'president': ldap_user.uid, |
||||
'treasurer': ldap_user.uid, |
||||
} |
||||
assert data == expected |
||||
|
||||
|
||||
def test_set_positions(cfg, client, g_admin_ctx, mock_mailman_server): |
||||
mock_mailman_server.clear() |
||||
mailing_lists = cfg.get('auxiliary mailing lists_exec') |
||||
base_domain = cfg.get('base_domain') |
||||
|
||||
users = [] |
||||
with g_admin_ctx(): |
||||
for uid in ['test_1', 'test_2', 'test_3', 'test_4']: |
||||
user = User(uid=uid, cn='Some Name', terms=['s2021']) |
||||
user.add_to_ldap() |
||||
users.append(user) |
||||
exec_group = Group(cn='exec', gid_number=10013) |
||||
exec_group.add_to_ldap() |
||||
|
||||
try: |
||||
# missing required position |
||||
status, _ = client.post('/api/positions', json={ |
||||
'vice-president': 'test_1', |
||||
}) |
||||
assert status == 400 |
||||
|
||||
# non-existent position |
||||
status, _ = client.post('/api/positions', json={ |
||||
'president': 'test_1', |
||||
'vice-president': 'test_2', |
||||
'sysadmin': 'test_3', |
||||
'no-such-position': 'test_3', |
||||
}) |
||||
assert status == 400 |
||||
|
||||
status, data = client.post('/api/positions', json={ |
||||
'president': 'test_1', |
||||
'vice-president': 'test_2', |
||||
'sysadmin': 'test_3', |
||||
}) |
||||
assert status == 200 |
||||
expected = [ |
||||
{"status": "in progress", "operation": "update_positions_ldap"}, |
||||
{"status": "in progress", "operation": "update_exec_group_ldap"}, |
||||
{"status": "in progress", "operation": "subscribe_to_mailing_lists"}, |
||||
{"status": "completed", "result": { |
||||
"president": "test_1", |
||||
"vice-president": "test_2", |
||||
"sysadmin": "test_3", |
||||
}}, |
||||
] |
||||
assert data == expected |
||||
# make sure execs were added to exec group |
||||
status, data = client.get('/api/groups/exec') |
||||
assert status == 200 |
||||
expected = ['test_1', 'test_2', 'test_3'] |
||||
assert sorted([item['uid'] for item in data['members']]) == expected |
||||
# make sure execs were subscribed to mailing lists |
||||
addresses = [f'{uid}@{base_domain}' for uid in expected] |
||||
for mailing_list in mailing_lists: |
||||
assert sorted(mock_mailman_server.subscriptions[mailing_list]) == addresses |
||||
|
||||
_, data = client.post('/api/positions', json={ |
||||
'president': 'test_1', |
||||
'vice-president': 'test_2', |
||||
'sysadmin': 'test_2', |
||||
'treasurer': 'test_4', |
||||
}) |
||||
assert data[-1]['status'] == 'completed' |
||||
# make sure old exec was removed from group |
||||
expected = ['test_1', 'test_2', 'test_4'] |
||||
_, data = client.get('/api/groups/exec') |
||||
assert sorted([item['uid'] for item in data['members']]) == expected |
||||
# make sure old exec was removed from mailing lists |
||||
addresses = [f'{uid}@{base_domain}' for uid in expected] |
||||
for mailing_list in mailing_lists: |
||||
assert sorted(mock_mailman_server.subscriptions[mailing_list]) == addresses |
||||
finally: |
||||
with g_admin_ctx(): |
||||
for user in users: |
||||
user.remove_from_ldap() |
||||
exec_group.remove_from_ldap() |
||||
mock_mailman_server.clear() |
Loading…
Reference in new issue