Address comments on code review

This commit is contained in:
Joseph Schorr 2014-09-11 15:45:41 -04:00
parent 7c45aca405
commit 8d3ce44682
12 changed files with 150 additions and 33 deletions

View file

@ -8,7 +8,7 @@ from peewee import *
from data.read_slave import ReadSlaveModel
from sqlalchemy.engine.url import make_url
from urlparse import urlparse
from util.names import urn_generator
logger = logging.getLogger(__name__)
@ -116,7 +116,7 @@ class TeamMemberInvite(BaseModel):
email = CharField(null=True)
team = ForeignKeyField(Team, index=True)
inviter = ForeignKeyField(User, related_name='inviter')
invite_token = CharField(default=uuid_generator)
invite_token = CharField(default=urn_generator(['teaminvite']))
class LoginService(BaseModel):

View file

@ -10,6 +10,24 @@ from data import model
from util.useremails import send_org_invite_email
from util.gravatar import compute_hash
def try_accept_invite(code, user):
try:
(team, inviter) = model.confirm_team_invite(code, user)
except model.DataModelException:
return None
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)
if not invite:
@ -323,20 +341,11 @@ class TeamMemberInvite(ApiResource):
def put(self, code):
""" Accepts an invite to join a team in an organization. """
# Accept the invite for the current user.
try:
(team, inviter) = model.confirm_team_invite(code, get_authenticated_user())
except model.DataModelException:
team = try_accept_invite(code, get_authenticated_user())
if not team:
raise NotFound()
model.delete_matching_notifications(get_authenticated_user(), 'org_team_invite', code=code)
orgname = team.organization.username
log_action('org_team_member_invite_accepted', orgname, {
'member': get_authenticated_user().username,
'team': team.name,
'inviter': inviter.username
})
return {
'org': orgname,
'team': team.name

View file

@ -12,6 +12,8 @@ from endpoints.api import (ApiResource, nickname, resource, validate_json_reques
license_error)
from endpoints.api.subscribe import subscribe
from endpoints.common import common_login
from endpoints.api.team import try_accept_invite
from data import model
from data.billing import get_plan
from auth.permissions import (AdministerOrganizationPermission, CreateRepositoryPermission,
@ -20,6 +22,7 @@ from auth.auth_context import get_authenticated_user
from auth import scopes
from util.gravatar import compute_hash
from util.useremails import (send_confirmation_email, send_recovery_email, send_change_email)
from util.names import parse_single_urn
import features
@ -179,11 +182,15 @@ class User(ApiResource):
return user_view(user)
@nickname('createNewUser')
@parse_args
@query_param('inviteCode', 'Invitation code given for creating the user.', type=str,
default='')
@internal_only
@validate_json_request('NewUser')
def post(self):
def post(self, args):
""" Create a new user. """
user_data = request.get_json()
invite_code = args['inviteCode']
existing_user = model.get_user(user_data['username'])
if existing_user:
@ -194,6 +201,14 @@ class User(ApiResource):
user_data['email'])
code = model.create_confirm_email_code(new_user)
send_confirmation_email(new_user.username, new_user.email, code.code)
# Handle any invite codes.
parsed_invite = parse_single_urn(invite_code)
if parsed_invite is not None:
if parsed_invite[0] == 'teaminvite':
# Add the user to the team.
try_accept_invite(invite_code, new_user)
return 'Created', 201
except model.TooManyUsersException as ex:
raise license_error(exception=ex)

View file

@ -1,5 +1,5 @@
<div class="signup-form-element">
<form class="form-signup" name="signupForm" ng-submit="register()" ngshow="!awaitingConfirmation && !registering">
<form class="form-signup" name="signupForm" ng-submit="register()" ng-show="!awaitingConfirmation && !registering">
<input type="text" class="form-control" placeholder="Create a username" name="username" ng-model="newUser.username" autofocus required ng-pattern="/^[a-z0-9_]{4,30}$/">
<input type="email" class="form-control" placeholder="Email address" ng-model="newUser.email" required>
<input type="password" class="form-control" placeholder="Create a password" ng-model="newUser.password" required

View file

@ -1,4 +1,4 @@
<div class="team-view-add-element">
<div class="team-view-add-element" focusable-popover-content>
<div class="entity-search"
namespace="orgname" placeholder="'Add a registered user or robot...'"
entity-selected="addNewMember(entity)"

View file

@ -24,7 +24,7 @@
</div>
<div id="collapseRegister" class="panel-collapse collapse" ng-class="hasSignedIn() ? 'out' : 'in'">
<div class="panel-body">
<div class="signup-form"></div>
<div class="signup-form" user-registered="handleUserRegistered(username)" invite-code="inviteCode"></div>
</div>
</div>
</div>

View file

@ -1843,7 +1843,7 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
when('/tour/features', {title: title + ' Features', templateUrl: '/static/partials/tour.html', controller: TourCtrl}).
when('/tour/enterprise', {title: 'Enterprise Edition', templateUrl: '/static/partials/tour.html', controller: TourCtrl}).
when('/confirminvite', {title: 'Confirm Team Invite', templateUrl: '/static/partials/confirm-team-invite.html', controller: ConfirmInviteCtrl, reloadOnSearch: false}).
when('/confirminvite', {title: 'Confirm Invite', templateUrl: '/static/partials/confirm-invite.html', controller: ConfirmInviteCtrl, reloadOnSearch: false}).
when('/', {title: 'Hosted Private Docker Registry', templateUrl: '/static/partials/landing.html', controller: LandingCtrl,
pageClass: 'landing-page'}).
@ -2218,6 +2218,36 @@ quayApp.directive('repoBreadcrumb', function () {
return directiveDefinitionObject;
});
quayApp.directive('focusablePopoverContent', ['$timeout', '$popover', function ($timeout, $popover) {
return {
restrict: "A",
link: function (scope, element, attrs) {
$body = $('body');
var hide = function() {
$body.off('click');
scope.$apply(function() {
scope.$hide();
});
};
scope.$on('$destroy', function() {
$body.off('click');
});
$timeout(function() {
$body.on('click', function(evt) {
var target = evt.target;
var isPanelMember = $(element).has(target).length > 0 || target == element;
if (!isPanelMember) {
hide();
}
});
$(element).find('input').focus();
}, 100);
}
};
}]);
quayApp.directive('repoCircle', function () {
var directiveDefinitionObject = {
@ -2276,8 +2306,12 @@ quayApp.directive('userSetup', function () {
restrict: 'C',
scope: {
'redirectUrl': '=redirectUrl',
'inviteCode': '=inviteCode',
'signInStarted': '&signInStarted',
'signedIn': '&signedIn'
'signedIn': '&signedIn',
'userRegistered': '&userRegistered'
},
controller: function($scope, $location, $timeout, ApiService, KeyService, UserService) {
$scope.sendRecovery = function() {
@ -2292,6 +2326,10 @@ quayApp.directive('userSetup', function () {
});
};
$scope.handleUserRegistered = function(username) {
$scope.userRegistered({'username': username});
};
$scope.hasSignedIn = function() {
return UserService.hasEverLoggedIn();
};
@ -2390,7 +2428,9 @@ quayApp.directive('signupForm', function () {
transclude: true,
restrict: 'C',
scope: {
'inviteCode': '=inviteCode',
'userRegistered': '&userRegistered'
},
controller: function($scope, $location, $timeout, ApiService, KeyService, UserService, Config, UIService) {
$('.form-signup').popover();
@ -2411,6 +2451,10 @@ quayApp.directive('signupForm', function () {
UIService.hidePopover('#signupButton');
$scope.registering = true;
if ($scope.inviteCode) {
$scope.newUser['inviteCode'] = $scope.inviteCode;
}
ApiService.createNewUser($scope.newUser).then(function() {
$scope.registering = false;
$scope.awaitingConfirmation = true;
@ -2418,6 +2462,8 @@ quayApp.directive('signupForm', function () {
if (Config.MIXPANEL_KEY) {
mixpanel.alias($scope.newUser.username);
}
$scope.userRegistered({'username': $scope.newUser.username});
}, function(result) {
$scope.registering = false;
UIService.showFormError('#signupButton', result);

View file

@ -2276,7 +2276,7 @@ function OrgAdminCtrl($rootScope, $scope, $timeout, Restangular, $routeParams, U
loadOrganization();
}
function TeamViewCtrl($rootScope, $scope, Restangular, ApiService, $routeParams) {
function TeamViewCtrl($rootScope, $scope, $timeout, Restangular, ApiService, $routeParams) {
var teamname = $routeParams.teamname;
var orgname = $routeParams.orgname;
@ -2755,6 +2755,7 @@ function TourCtrl($scope, $location) {
function ConfirmInviteCtrl($scope, $location, UserService, ApiService, NotificationService) {
// Monitor any user changes and place the current user into the scope.
$scope.loading = false;
$scope.inviteCode = $location.search()['code'] || '';
UserService.updateUserIn($scope, function(user) {
if (!user.anonymous && !$scope.loading) {
@ -2776,6 +2777,6 @@ function ConfirmInviteCtrl($scope, $location, UserService, ApiService, Notificat
});
}
});
$scope.redirectUrl = window.location.href;
}

View file

@ -1,8 +1,10 @@
<div class="confirm-team-invite">
<div class="confirm-invite">
<div class="container signin-container">
<div class="row">
<div class="col-sm-6 col-sm-offset-3">
<div class="user-setup" ng-show="user.anonymous" redirect-url="redirectUrl"></div>
<div class="user-setup" ng-show="user.anonymous" redirect-url="redirectUrl"
invite-code="inviteCode">
</div>
<div class="quay-spinner" ng-show="!user.anonymous && loading"></div>
<div class="alert alert-danger" ng-show="!user.anonymous && invalid">
Invalid confirmation code

View file

@ -3,11 +3,12 @@
<div class="organization-header" organization="organization" team-name="teamname">
<div ng-show="canEditMembers" class="side-controls">
<div class="hidden-sm hidden-xs">
<button class="btn btn-success" data-trigger="click"
<button class="btn btn-success"
id="showAddMember"
data-title="Add Team Member"
data-content-template="/static/directives/team-view-add.html"
data-placement="bottom-right"
bs-popover>
data-placement="bottom-right"
bs-popover="bs-popover">
<i class="fa fa-plus"></i>
Add Team Member
</button>

View file

@ -166,6 +166,13 @@ class ApiTestCase(unittest.TestCase):
parsed = py_json.loads(data)
return parsed
def assertInTeam(self, data, membername):
for memberData in data['members']:
if memberData['name'] == membername:
return
self.fail(membername + ' not found in team: ' + json.dumps(data))
def login(self, username, password='password'):
return self.postJsonResponse(Signin, data=dict(username=username, password=password))
@ -378,6 +385,28 @@ class TestCreateNewUser(ApiTestCase):
expected_code=201)
self.assertEquals('"Created"', data)
def test_createuser_withteaminvite(self):
inviter = model.get_user(ADMIN_ACCESS_USER)
team = model.get_organization_team(ORGANIZATION, 'owners')
invite = model.add_or_invite_to_team(inviter, team, None, 'foo@example.com')
details = {
'inviteCode': invite.invite_token
}
details.update(NEW_USER_DETAILS);
data = self.postResponse(User,
data=details,
expected_code=201)
self.assertEquals('"Created"', data)
# Make sure the user was added to the team.
self.login(ADMIN_ACCESS_USER)
json = self.getJsonResponse(TeamMemberList,
params=dict(orgname=ORGANIZATION,
teamname='owners'))
self.assertInTeam(json, NEW_USER_DETAILS['username'])
class TestSignout(ApiTestCase):
def test_signout(self):
@ -743,13 +772,6 @@ class TestGetOrganizationTeamMembers(ApiTestCase):
class TestUpdateOrganizationTeamMember(ApiTestCase):
def assertInTeam(self, data, membername):
for memberData in data['members']:
if memberData['name'] == membername:
return
self.fail(membername + ' not found in team: ' + json.dumps(data))
def test_addmember_alreadyteammember(self):
self.login(ADMIN_ACCESS_USER)

View file

@ -34,6 +34,27 @@ def parse_robot_username(robot_username):
return robot_username.split('+', 2)
def parse_urn(urn):
""" Parses a URN, returning a pair that contains a list of URN
namespace parts, followed by the URN's unique ID.
"""
if not urn.startswith('urn:'):
return None
parts = urn[len('urn:'):].split(':')
return (parts[0:len(parts) - 1], parts[len(parts) - 1])
def parse_single_urn(urn):
""" Parses a URN, returning a pair that contains the first
namespace part, followed by the URN's unique ID.
"""
result = parse_urn(urn)
if result is None or not len(result[0]):
return None
return (result[0][0], result[1])
uuid_generator = lambda: str(uuid4())