Merge branch master into bees
This commit is contained in:
commit
1d8ec59362
164 changed files with 6048 additions and 1911 deletions
|
@ -2,12 +2,51 @@ from flask import request
|
|||
|
||||
from endpoints.api import (resource, nickname, ApiResource, validate_json_request, request_error,
|
||||
log_action, Unauthorized, NotFound, internal_only, require_scope,
|
||||
path_param)
|
||||
path_param, query_param, truthy_bool, parse_args, require_user_admin,
|
||||
show_if)
|
||||
from auth.permissions import AdministerOrganizationPermission, ViewTeamPermission
|
||||
from auth.auth_context import get_authenticated_user
|
||||
from auth import scopes
|
||||
from data import model
|
||||
from util.useremails import send_org_invite_email
|
||||
from util.gravatar import compute_hash
|
||||
|
||||
import features
|
||||
|
||||
def try_accept_invite(code, user):
|
||||
(team, inviter) = model.confirm_team_invite(code, user)
|
||||
|
||||
model.delete_matching_notifications(user, 'org_team_invite', code=code)
|
||||
|
||||
orgname = team.organization.username
|
||||
log_action('org_team_member_invite_accepted', orgname, {
|
||||
'member': user.username,
|
||||
'team': team.name,
|
||||
'inviter': inviter.username
|
||||
})
|
||||
|
||||
return team
|
||||
|
||||
|
||||
def handle_addinvite_team(inviter, team, user=None, email=None):
|
||||
invite = model.add_or_invite_to_team(inviter, team, user, email,
|
||||
requires_invite = features.MAILING)
|
||||
if not invite:
|
||||
# User was added to the team directly.
|
||||
return
|
||||
|
||||
orgname = team.organization.username
|
||||
if user:
|
||||
model.create_notification('org_team_invite', user, metadata = {
|
||||
'code': invite.invite_token,
|
||||
'inviter': inviter.username,
|
||||
'org': orgname,
|
||||
'team': team.name
|
||||
})
|
||||
|
||||
send_org_invite_email(user.username if user else email, user.email if user else email,
|
||||
orgname, team.name, inviter.username, invite.invite_token)
|
||||
return invite
|
||||
|
||||
def team_view(orgname, team):
|
||||
view_permission = ViewTeamPermission(orgname, team.name)
|
||||
|
@ -20,14 +59,28 @@ def team_view(orgname, team):
|
|||
'role': role
|
||||
}
|
||||
|
||||
def member_view(member):
|
||||
def member_view(member, invited=False):
|
||||
return {
|
||||
'name': member.username,
|
||||
'kind': 'user',
|
||||
'is_robot': member.robot,
|
||||
'gravatar': compute_hash(member.email) if not member.robot else None,
|
||||
'invited': invited,
|
||||
}
|
||||
|
||||
|
||||
def invite_view(invite):
|
||||
if invite.user:
|
||||
return member_view(invite.user, invited=True)
|
||||
else:
|
||||
return {
|
||||
'email': invite.email,
|
||||
'kind': 'invite',
|
||||
'gravatar': compute_hash(invite.email),
|
||||
'invited': True
|
||||
}
|
||||
|
||||
|
||||
@resource('/v1/organization/<orgname>/team/<teamname>')
|
||||
@path_param('orgname', 'The name of the organization')
|
||||
@path_param('teamname', 'The name of the team')
|
||||
|
@ -119,10 +172,11 @@ class OrganizationTeam(ApiResource):
|
|||
@path_param('teamname', 'The name of the team')
|
||||
class TeamMemberList(ApiResource):
|
||||
""" Resource for managing the list of members for a team. """
|
||||
|
||||
@require_scope(scopes.ORG_ADMIN)
|
||||
@parse_args
|
||||
@query_param('includePending', 'Whether to include pending members', type=truthy_bool, default=False)
|
||||
@nickname('getOrganizationTeamMembers')
|
||||
def get(self, orgname, teamname):
|
||||
def get(self, args, orgname, teamname):
|
||||
""" Retrieve the list of members for the specified team. """
|
||||
view_permission = ViewTeamPermission(orgname, teamname)
|
||||
edit_permission = AdministerOrganizationPermission(orgname)
|
||||
|
@ -135,11 +189,18 @@ class TeamMemberList(ApiResource):
|
|||
raise NotFound()
|
||||
|
||||
members = model.get_organization_team_members(team.id)
|
||||
return {
|
||||
'members': {m.username : member_view(m) for m in members},
|
||||
invites = []
|
||||
|
||||
if args['includePending'] and edit_permission.can():
|
||||
invites = model.get_organization_team_member_invites(team.id)
|
||||
|
||||
data = {
|
||||
'members': [member_view(m) for m in members] + [invite_view(i) for i in invites],
|
||||
'can_edit': edit_permission.can()
|
||||
}
|
||||
|
||||
return data
|
||||
|
||||
raise Unauthorized()
|
||||
|
||||
|
||||
|
@ -153,7 +214,7 @@ class TeamMember(ApiResource):
|
|||
@require_scope(scopes.ORG_ADMIN)
|
||||
@nickname('updateOrganizationTeamMember')
|
||||
def put(self, orgname, teamname, membername):
|
||||
""" Add a member to an existing team. """
|
||||
""" Adds or invites a member to an existing team. """
|
||||
permission = AdministerOrganizationPermission(orgname)
|
||||
if permission.can():
|
||||
team = None
|
||||
|
@ -170,23 +231,151 @@ class TeamMember(ApiResource):
|
|||
if not user:
|
||||
raise request_error(message='Unknown user')
|
||||
|
||||
# Add the user to the team.
|
||||
model.add_user_to_team(user, team)
|
||||
log_action('org_add_team_member', orgname, {'member': membername, 'team': teamname})
|
||||
return member_view(user)
|
||||
# Add or invite the user to the team.
|
||||
inviter = get_authenticated_user()
|
||||
invite = handle_addinvite_team(inviter, team, user=user)
|
||||
if not invite:
|
||||
log_action('org_add_team_member', orgname, {'member': membername, 'team': teamname})
|
||||
return member_view(user, invited=False)
|
||||
|
||||
# User was invited.
|
||||
log_action('org_invite_team_member', orgname, {
|
||||
'user': membername,
|
||||
'member': membername,
|
||||
'team': teamname
|
||||
})
|
||||
return member_view(user, invited=True)
|
||||
|
||||
raise Unauthorized()
|
||||
|
||||
@require_scope(scopes.ORG_ADMIN)
|
||||
@nickname('deleteOrganizationTeamMember')
|
||||
def delete(self, orgname, teamname, membername):
|
||||
""" Delete an existing member of a team. """
|
||||
""" Delete a member of a team. If the user is merely invited to join
|
||||
the team, then the invite is removed instead.
|
||||
"""
|
||||
permission = AdministerOrganizationPermission(orgname)
|
||||
if permission.can():
|
||||
# Remote the user from the team.
|
||||
invoking_user = get_authenticated_user().username
|
||||
|
||||
# Find the team.
|
||||
try:
|
||||
team = model.get_organization_team(orgname, teamname)
|
||||
except model.InvalidTeamException:
|
||||
raise NotFound()
|
||||
|
||||
# Find the member.
|
||||
member = model.get_user(membername)
|
||||
if not member:
|
||||
raise NotFound()
|
||||
|
||||
# First attempt to delete an invite for the user to this team. If none found,
|
||||
# then we try to remove the user directly.
|
||||
if model.delete_team_user_invite(team, member):
|
||||
log_action('org_delete_team_member_invite', orgname, {
|
||||
'user': membername,
|
||||
'team': teamname,
|
||||
'member': membername
|
||||
})
|
||||
return 'Deleted', 204
|
||||
|
||||
model.remove_user_from_team(orgname, teamname, membername, invoking_user)
|
||||
log_action('org_remove_team_member', orgname, {'member': membername, 'team': teamname})
|
||||
return 'Deleted', 204
|
||||
|
||||
raise Unauthorized()
|
||||
|
||||
|
||||
@resource('/v1/organization/<orgname>/team/<teamname>/invite/<email>')
|
||||
@show_if(features.MAILING)
|
||||
class InviteTeamMember(ApiResource):
|
||||
""" Resource for inviting a team member via email address. """
|
||||
@require_scope(scopes.ORG_ADMIN)
|
||||
@nickname('inviteTeamMemberEmail')
|
||||
def put(self, orgname, teamname, email):
|
||||
""" Invites an email address to an existing team. """
|
||||
permission = AdministerOrganizationPermission(orgname)
|
||||
if permission.can():
|
||||
team = None
|
||||
|
||||
# Find the team.
|
||||
try:
|
||||
team = model.get_organization_team(orgname, teamname)
|
||||
except model.InvalidTeamException:
|
||||
raise NotFound()
|
||||
|
||||
# Invite the email to the team.
|
||||
inviter = get_authenticated_user()
|
||||
invite = handle_addinvite_team(inviter, team, email=email)
|
||||
log_action('org_invite_team_member', orgname, {
|
||||
'email': email,
|
||||
'team': teamname,
|
||||
'member': email
|
||||
})
|
||||
return invite_view(invite)
|
||||
|
||||
raise Unauthorized()
|
||||
|
||||
@require_scope(scopes.ORG_ADMIN)
|
||||
@nickname('deleteTeamMemberEmailInvite')
|
||||
def delete(self, orgname, teamname, email):
|
||||
""" Delete an invite of an email address to join a team. """
|
||||
permission = AdministerOrganizationPermission(orgname)
|
||||
if permission.can():
|
||||
team = None
|
||||
|
||||
# Find the team.
|
||||
try:
|
||||
team = model.get_organization_team(orgname, teamname)
|
||||
except model.InvalidTeamException:
|
||||
raise NotFound()
|
||||
|
||||
# Delete the invite.
|
||||
model.delete_team_email_invite(team, email)
|
||||
log_action('org_delete_team_member_invite', orgname, {
|
||||
'email': email,
|
||||
'team': teamname,
|
||||
'member': email
|
||||
})
|
||||
return 'Deleted', 204
|
||||
|
||||
raise Unauthorized()
|
||||
|
||||
|
||||
@resource('/v1/teaminvite/<code>')
|
||||
@internal_only
|
||||
@show_if(features.MAILING)
|
||||
class TeamMemberInvite(ApiResource):
|
||||
""" Resource for managing invites to jon a team. """
|
||||
@require_user_admin
|
||||
@nickname('acceptOrganizationTeamInvite')
|
||||
def put(self, code):
|
||||
""" Accepts an invite to join a team in an organization. """
|
||||
# Accept the invite for the current user.
|
||||
team = try_accept_invite(code, get_authenticated_user())
|
||||
if not team:
|
||||
raise NotFound()
|
||||
|
||||
orgname = team.organization.username
|
||||
return {
|
||||
'org': orgname,
|
||||
'team': team.name
|
||||
}
|
||||
|
||||
@nickname('declineOrganizationTeamInvite')
|
||||
@require_user_admin
|
||||
def delete(self, code):
|
||||
""" Delete an existing member of a team. """
|
||||
(team, inviter) = model.delete_team_invite(code, get_authenticated_user())
|
||||
|
||||
model.delete_matching_notifications(get_authenticated_user(), 'org_team_invite', code=code)
|
||||
|
||||
orgname = team.organization.username
|
||||
log_action('org_team_member_invite_declined', orgname, {
|
||||
'member': get_authenticated_user().username,
|
||||
'team': team.name,
|
||||
'inviter': inviter.username
|
||||
})
|
||||
|
||||
return 'Deleted', 204
|
||||
|
|
Reference in a new issue