Merge branch 'orgs' of ssh://bitbucket.org/yackob03/quay into orgs
Conflicts: data/model.py endpoints/api.py test/data/test.db
This commit is contained in:
commit
ad4e227aff
9 changed files with 361 additions and 19 deletions
|
@ -115,6 +115,14 @@ def add_user_to_team(user, team):
|
|||
return TeamMember.create(user=user, team=team)
|
||||
|
||||
|
||||
def remove_user_from_team(user, team):
|
||||
try:
|
||||
found = TeamMember.get(user=user, team=team)
|
||||
found.delete_instance()
|
||||
except TeamMember.DoesNotExist:
|
||||
raise InvalidTeamException('User does not belong to team.')
|
||||
|
||||
|
||||
def set_team_org_permission(team, org, team_role_name):
|
||||
new_role = TeamRole.get(TeamRole.name == tean_role_name)
|
||||
team.role = new_role
|
||||
|
@ -122,6 +130,22 @@ def set_team_org_permission(team, org, team_role_name):
|
|||
return team
|
||||
|
||||
|
||||
def set_team_org_permission(team, org, role_name):
|
||||
new_role = Role.get(Role.name == role_name)
|
||||
|
||||
# Fetch any existing permission for this user on the repo
|
||||
try:
|
||||
perm = TeamPermission.get(TeamPermission.team == team,
|
||||
TeamPermission.organization == org)
|
||||
perm.role = new_role
|
||||
perm.save()
|
||||
return perm
|
||||
except TeamPermission.DoesNotExist:
|
||||
new_perm = TeamPermission.create(team=team, organization=org,
|
||||
role=new_role)
|
||||
return new_perm
|
||||
|
||||
|
||||
def create_federated_user(username, email, service_name, service_id):
|
||||
new_user = create_user(username, None, email)
|
||||
new_user.verified = True
|
||||
|
@ -231,11 +255,26 @@ def get_user_organizations(username):
|
|||
|
||||
def get_organization(name):
|
||||
try:
|
||||
return User.get(username=name, organization=True)
|
||||
return User.get(username = name, organization = True)
|
||||
except User.DoesNotExist:
|
||||
raise InvalidOrganizationException('Organization does not exist: %s' %
|
||||
name)
|
||||
|
||||
|
||||
|
||||
def get_organization_team(orgname, teamname):
|
||||
joined = Team.select().join(User)
|
||||
query = joined.where(Team.name == teamname, User.organization == True, User.username == orgname).limit(1)
|
||||
result = list(query)
|
||||
if not result:
|
||||
raise InvalidTeamException('Team does not exist: %s/%s', orgname, teamname)
|
||||
|
||||
return result[0]
|
||||
|
||||
|
||||
def get_organization_team_members(teamid):
|
||||
joined = User.select().join(TeamMember).join(Team)
|
||||
query = joined.where(Team.id == teamid)
|
||||
return query
|
||||
|
||||
def get_user_teams_within_org(username, organization):
|
||||
joined = Team.select().join(TeamMember).join(User)
|
||||
|
|
100
endpoints/api.py
100
endpoints/api.py
|
@ -58,9 +58,11 @@ def plans_list():
|
|||
@app.route('/api/user/', methods=['GET'])
|
||||
def get_logged_in_user():
|
||||
def org_view(o):
|
||||
# TODO: return whether the user is really the admin of the organization
|
||||
return {
|
||||
'name': o.username,
|
||||
'gravatar': compute_hash(o.email),
|
||||
'is_org_admin': True
|
||||
}
|
||||
|
||||
if current_user.is_anonymous():
|
||||
|
@ -257,7 +259,7 @@ def get_organization(orgname):
|
|||
abort(404)
|
||||
|
||||
teams = model.get_user_teams_within_org(user.username, org)
|
||||
return jsonify(org_view(organization, teams))
|
||||
return jsonify(org_view(org, teams))
|
||||
|
||||
|
||||
@app.route('/api/organization/<orgname>/private', methods=['GET'])
|
||||
|
@ -287,18 +289,110 @@ def get_organization_private_allowed(orgname):
|
|||
})
|
||||
|
||||
|
||||
def member_view(m):
|
||||
return {
|
||||
'username': m.username
|
||||
}
|
||||
|
||||
@app.route('/api/organization/<orgname>/team/<teamname>/members', methods=['GET'])
|
||||
def get_organization_team_members(orgname, teamname):
|
||||
if current_user.is_anonymous():
|
||||
abort(404)
|
||||
|
||||
# TODO: determine whether the user has permission to view the team members of this team
|
||||
# (i.e. they are a member of the team [maybe??] OR they are an admin of the org)
|
||||
user = current_user.db_user()
|
||||
team = None
|
||||
|
||||
try:
|
||||
team = model.get_organization_team(orgname, teamname)
|
||||
except:
|
||||
abort(404)
|
||||
|
||||
members = model.get_organization_team_members(team.id)
|
||||
return jsonify({
|
||||
'members': { m.username : member_view(m) for m in members }
|
||||
})
|
||||
|
||||
|
||||
@app.route('/api/organization/<orgname>/team/<teamname>/members/<membername>', methods=['PUT', 'POST'])
|
||||
def update_organization_team_member(orgname, teamname, membername):
|
||||
if current_user.is_anonymous():
|
||||
abort(404)
|
||||
|
||||
# TODO: determine whether the user has permission to put this user as a member of the team.
|
||||
team = None
|
||||
user = None
|
||||
|
||||
# Find the team.
|
||||
try:
|
||||
team = model.get_organization_team(orgname, teamname)
|
||||
except:
|
||||
abort(404)
|
||||
|
||||
# Find the user.
|
||||
user = model.get_user(membername)
|
||||
if not user:
|
||||
abort(400)
|
||||
|
||||
# Add the user to the team.
|
||||
model.add_user_to_team(user, team)
|
||||
|
||||
return jsonify(member_view(user))
|
||||
|
||||
|
||||
@app.route('/api/organization/<orgname>/team/<teamname>/members/<membername>', methods=['DELETE'])
|
||||
def delete_organization_team_member(orgname, teamname, membername):
|
||||
if current_user.is_anonymous():
|
||||
abort(404)
|
||||
|
||||
# TODO: determine whether the user has permission to delete this user as a member of the team.
|
||||
team = None
|
||||
user = None
|
||||
|
||||
# Find the team.
|
||||
try:
|
||||
team = model.get_organization_team(orgname, teamname)
|
||||
except:
|
||||
abort(404)
|
||||
|
||||
# Find the user.
|
||||
user = model.get_user(membername)
|
||||
if not user:
|
||||
abort(400)
|
||||
|
||||
# Remote the user from the team.
|
||||
model.remove_user_from_team(user, team)
|
||||
|
||||
return jsonify({
|
||||
'success': True
|
||||
})
|
||||
|
||||
|
||||
|
||||
@app.route('/api/repository', methods=['POST'])
|
||||
@api_login_required
|
||||
def create_repo_api():
|
||||
owner = current_user.db_user()
|
||||
json = request.get_json()
|
||||
namespace_name = json['namespace'] if 'namespace' in json else owner.username
|
||||
|
||||
permission = CreateRepositoryPermission(json['namespace'])
|
||||
permission = CreateRepositoryPermission(namespace_name)
|
||||
if permission.can():
|
||||
namespace_name = json['namespace'] if 'namespace' in json else owner.username
|
||||
repository_name = json['repository']
|
||||
visibility = json['visibility']
|
||||
|
||||
existing = model.get_repository(namespace_name, repository_name)
|
||||
if existing:
|
||||
return make_response('Repository already exists', 400)
|
||||
|
||||
visibility = request.get_json()['visibility']
|
||||
|
||||
repo = model.create_repository(namespace_name, repository_name, owner,
|
||||
visibility)
|
||||
repo.description = json['description']
|
||||
repo.save()
|
||||
|
||||
repo = model.create_repository(namespace_name, repository_name, owner,
|
||||
visibility)
|
||||
repo.description = json['description']
|
||||
|
|
|
@ -2,6 +2,31 @@
|
|||
font-family: 'Droid Sans', sans-serif;
|
||||
}
|
||||
|
||||
.organization-header-element {
|
||||
padding: 10px;
|
||||
margin-bottom: 20px;
|
||||
border-bottom: 1px solid #eee;
|
||||
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.organization-header-element .organization-name {
|
||||
display: inline-block;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.organization-header-element .divider {
|
||||
color: #aaa;
|
||||
margin-left: 10px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.organization-header-element .organization-name {
|
||||
display: inline-block;
|
||||
font-size: 20px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.namespace-selector-dropdown .namespace {
|
||||
padding: 6px;
|
||||
padding-left: 10px;
|
||||
|
@ -186,10 +211,14 @@
|
|||
color: #444 !important;
|
||||
}
|
||||
|
||||
.new-repo .new-header {
|
||||
.new-repo .new-header span {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.new-repo .new-header .popover {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.new-repo .new-header .repo-circle {
|
||||
margin-right: 14px;
|
||||
}
|
||||
|
@ -373,6 +402,10 @@
|
|||
padding: 20px;
|
||||
}
|
||||
|
||||
.landing .popover {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.landing {
|
||||
color: white;
|
||||
|
||||
|
@ -1120,7 +1153,7 @@ p.editable:hover i {
|
|||
}
|
||||
|
||||
.delete-ui:focus .delete-ui-button {
|
||||
width: 54px;
|
||||
width: 60px;
|
||||
}
|
||||
|
||||
.repo-admin .repo-delete {
|
||||
|
@ -1329,6 +1362,35 @@ p.editable:hover i {
|
|||
min-height: 50px;
|
||||
}
|
||||
|
||||
.team-view .panel {
|
||||
display: inline-block;
|
||||
width: 620px;
|
||||
}
|
||||
|
||||
.team-view .entity {
|
||||
font-size: 1.2em;
|
||||
min-width: 510px;
|
||||
}
|
||||
|
||||
.team-view .entity i {
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.team-view .entity-search {
|
||||
margin-top: 10px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.team-view .delete-ui {
|
||||
display: inline-block;
|
||||
width: 78px;
|
||||
}
|
||||
|
||||
.team-view .delete-ui i {
|
||||
margin-top: 8px;
|
||||
float: right;
|
||||
}
|
||||
|
||||
/* Overrides for typeahead to work with bootstrap 3. */
|
||||
|
||||
.twitter-typeahead .tt-query,
|
||||
|
|
17
static/directives/organization-header.html
Normal file
17
static/directives/organization-header.html
Normal file
|
@ -0,0 +1,17 @@
|
|||
<div class="organization-header-element">
|
||||
<img src="//www.gravatar.com/avatar/{{ organization.gravatar }}?s=24&d=identicon">
|
||||
<span class="organization-name" ng-show="teamName">
|
||||
<a href="/organization/{{ organization.name }}">{{ organization.name }}</a>
|
||||
</span>
|
||||
<span class="organization-name" ng-show="!teamName">
|
||||
{{ organization.name }}
|
||||
</span>
|
||||
|
||||
<span ng-show="teamName">
|
||||
<span class="divider">/</span>
|
||||
<i class="fa fa-group"></i>
|
||||
<span class="team-name">
|
||||
{{ teamName }}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
|
@ -233,6 +233,24 @@ quayApp.directive('repoCircle', function () {
|
|||
});
|
||||
|
||||
|
||||
quayApp.directive('organizationHeader', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/organization-header.html',
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'organization': '=organization',
|
||||
'teamName': '=teamName'
|
||||
},
|
||||
controller: function($scope, $element) {
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
||||
|
||||
|
||||
quayApp.directive('entitySearch', function () {
|
||||
var number = 0;
|
||||
var directiveDefinitionObject = {
|
||||
|
@ -244,7 +262,7 @@ quayApp.directive('entitySearch', function () {
|
|||
scope: {
|
||||
'organization': '=organization',
|
||||
'inputTitle': '=inputTitle',
|
||||
'entitySelected': '=entitySelected'
|
||||
'entitySelected': '=entitySelected'
|
||||
},
|
||||
controller: function($scope, $element) {
|
||||
if (!$scope.entitySelected) { return; }
|
||||
|
|
|
@ -14,6 +14,17 @@ $.fn.clipboardCopy = function() {
|
|||
});
|
||||
};
|
||||
|
||||
function getRestUrl(args) {
|
||||
var url = '';
|
||||
for (var i = 0; i < arguments.length; ++i) {
|
||||
if (i > 0) {
|
||||
url += '/';
|
||||
}
|
||||
url += encodeURI(arguments[i])
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
function getFirstTextLine(commentString) {
|
||||
if (!commentString) { return; }
|
||||
|
||||
|
@ -573,7 +584,7 @@ function RepoAdminCtrl($scope, Restangular, $routeParams, $rootScope) {
|
|||
};
|
||||
|
||||
$scope.deleteRole = function(entityName, kind) {
|
||||
var permissionDelete = Restangular.one('repository/' + namespace + '/' + name + '/permissions/' + kind + '/' + entityName);
|
||||
var permissionDelete = Restangular.one(getRestUrl('repository', namespace, name, 'permissions', kind, entityName));
|
||||
permissionDelete.customDELETE().then(function() {
|
||||
delete $scope.permissions[kind][entityName];
|
||||
}, function(result) {
|
||||
|
@ -591,7 +602,7 @@ function RepoAdminCtrl($scope, Restangular, $routeParams, $rootScope) {
|
|||
'outside_org': !!outside_org
|
||||
};
|
||||
|
||||
var permissionPost = Restangular.one('repository/' + namespace + '/' + name + '/permissions/' + kind + '/' + entityName);
|
||||
var permissionPost = Restangular.one(getRestUrl('repository', namespace, name, 'permissions', kind, entityName));
|
||||
permissionPost.customPOST(permission).then(function() {
|
||||
$scope.permissions[kind][entityName] = permission;
|
||||
}, function(result) {
|
||||
|
@ -604,7 +615,7 @@ function RepoAdminCtrl($scope, Restangular, $routeParams, $rootScope) {
|
|||
var currentRole = permission.role;
|
||||
permission.role = role;
|
||||
|
||||
var permissionPut = Restangular.one('repository/' + namespace + '/' + name + '/permissions/' + kind + '/' + entityName);
|
||||
var permissionPut = Restangular.one(getRestUrl('repository', namespace, name, 'permissions', kind, entityName));
|
||||
permissionPut.customPUT(permission).then(function() {}, function(result) {
|
||||
if (result.status == 409) {
|
||||
permission.role = currentRole;
|
||||
|
@ -953,7 +964,7 @@ function V1Ctrl($scope, $location, UserService) {
|
|||
}, true);
|
||||
}
|
||||
|
||||
function NewRepoCtrl($scope, $location, $http, UserService, Restangular, PlanService) {
|
||||
function NewRepoCtrl($scope, $location, $http, $timeout, UserService, Restangular, PlanService) {
|
||||
$scope.$watch( function () { return UserService.currentUser(); }, function (currentUser) {
|
||||
$scope.user = currentUser;
|
||||
}, true);
|
||||
|
@ -1079,6 +1090,8 @@ function NewRepoCtrl($scope, $location, $http, UserService, Restangular, PlanSer
|
|||
};
|
||||
|
||||
$scope.createNewRepo = function() {
|
||||
$('#repoName').popover('hide');
|
||||
|
||||
var uploader = $('#file-drop')[0];
|
||||
if ($scope.repo.initialize && uploader.files.length < 1) {
|
||||
$('#missingfileModal').modal();
|
||||
|
@ -1107,9 +1120,12 @@ function NewRepoCtrl($scope, $location, $http, UserService, Restangular, PlanSer
|
|||
|
||||
// Otherwise, redirect to the repo page.
|
||||
$location.path('/repository/' + created.namespace + '/' + created.name);
|
||||
}, function() {
|
||||
$('#cannotcreateModal').modal();
|
||||
}, function(result) {
|
||||
$scope.creating = false;
|
||||
$scope.createError = result.data;
|
||||
$timeout(function() {
|
||||
$('#repoName').popover('show');
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -1173,7 +1189,62 @@ function OrgTeamsCtrl($scope, Restangular, $routeParams) {
|
|||
var orgname = $routeParams.orgname;
|
||||
}
|
||||
|
||||
function TeamViewCtrl($scope, Restangular, $routeParams) {
|
||||
function TeamViewCtrl($rootScope, $scope, Restangular, $routeParams) {
|
||||
$('.info-icon').popover({
|
||||
'trigger': 'hover',
|
||||
'html': true
|
||||
});
|
||||
|
||||
var orgname = $routeParams.orgname;
|
||||
var teamname = $routeParams.teamname;
|
||||
|
||||
$rootScope.title = 'Loading...';
|
||||
$scope.loading = true;
|
||||
$scope.teamname = teamname;
|
||||
|
||||
$scope.addNewMember = function(member) {
|
||||
if ($scope.members[member.name]) { return; }
|
||||
|
||||
$scope.$apply(function() {
|
||||
var addMember = Restangular.one(getRestUrl('organization', orgname, 'team', teamname, 'members', member.name));
|
||||
addMember.customPOST().then(function(resp) {
|
||||
$scope.members[member.name] = resp;
|
||||
}, function() {
|
||||
$('#cannotChangeMembersModal').modal({});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
$scope.removeMember = function(username) {
|
||||
var removeMember = Restangular.one(getRestUrl('organization', orgname, 'team', teamname, 'members', username));
|
||||
removeMember.customDELETE().then(function(resp) {
|
||||
delete $scope.members[username];
|
||||
}, function() {
|
||||
$('#cannotChangeMembersModal').modal({});
|
||||
});
|
||||
};
|
||||
|
||||
var loadOrganization = function() {
|
||||
var getOrganization = Restangular.one(getRestUrl('organization', orgname))
|
||||
getOrganization.get().then(function(resp) {
|
||||
$scope.organization = resp;
|
||||
$scope.loading = !$scope.organization || !$scope.members;
|
||||
}, function() {
|
||||
$scope.loading = false;
|
||||
});
|
||||
};
|
||||
|
||||
var loadMembers = function() {
|
||||
var getMembers = Restangular.one(getRestUrl('organization', orgname, 'team', teamname, 'members'));
|
||||
getMembers.get().then(function(resp) {
|
||||
$scope.members = resp.members;
|
||||
$scope.loading = !$scope.organization || !$scope.members;
|
||||
$rootScope.title = teamname + ' (' + orgname + ')';
|
||||
}, function() {
|
||||
$scope.loading = false;
|
||||
});
|
||||
};
|
||||
|
||||
loadOrganization();
|
||||
loadMembers();
|
||||
}
|
|
@ -20,7 +20,7 @@
|
|||
</div>
|
||||
|
||||
<div class="container new-repo" ng-show="!user.anonymous && !creating && !uploading && !building">
|
||||
<form method="post" name="newRepoForm" ng-submit="createNewRepo()">
|
||||
<form method="post" name="newRepoForm" id="newRepoForm" ng-submit="createNewRepo()">
|
||||
|
||||
<!-- Header -->
|
||||
<div class="row">
|
||||
|
@ -32,7 +32,7 @@
|
|||
<span class="namespace-selector" user="user" namespace="repo.namespace"></span>
|
||||
<span style="color: #ccc">/</span>
|
||||
<span class="name-container">
|
||||
<input id="repoName" name="repoName" type="text" class="form-control" placeholder="Repository Name" ng-model="repo.name" required autofocus>
|
||||
<input id="repoName" name="repoName" type="text" class="form-control" placeholder="Repository Name" ng-model="repo.name" required autofocus data-trigger="manual" data-content="{{ createError }}" data-placement="right">
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -39,7 +39,7 @@
|
|||
<tr ng-repeat="(name, permission) in permissions['team']">
|
||||
<td class="team entity">
|
||||
<i class="fa fa-group"></i>
|
||||
<span>{{name}}</span>
|
||||
<span><a href="/organization/{{ repo.namespace }}/teams/{{ name }}">{{name}}</a></span>
|
||||
</td>
|
||||
<td class="user-permissions">
|
||||
<div class="btn-group btn-group-sm">
|
||||
|
@ -72,7 +72,7 @@
|
|||
<td>
|
||||
<span class="delete-ui" tabindex="0" title="Delete Permission">
|
||||
<span class="delete-ui-button" ng-click="deleteRole(name, 'user')"><button class="btn btn-danger">Delete</button></span>
|
||||
<i class="fa fa-remove"></i>
|
||||
<i class="fa fa-times"></i>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
|
|
41
static/partials/team-view.html
Normal file
41
static/partials/team-view.html
Normal file
|
@ -0,0 +1,41 @@
|
|||
<div class="loading" ng-show="loading">
|
||||
<i class="fa fa-spinner fa-spin fa-3x"></i>
|
||||
</div>
|
||||
|
||||
<div class="loading" ng-show="!loading && !organization">
|
||||
No matching team found
|
||||
</div>
|
||||
|
||||
<div class="team-view container" ng-show="!loading && organization">
|
||||
<div class="organization-header" organization="organization" team-name="teamname"></div>
|
||||
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">Team Members
|
||||
<i class="info-icon fa fa-info-circle" data-placement="left" data-content="Users that inherit all permissions delegated to this team"></i>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<table class="permissions">
|
||||
<tr ng-repeat="(name, member) in members">
|
||||
<td class="user entity">
|
||||
<i class="fa fa-user"></i>
|
||||
<span>{{ member.username }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="delete-ui" tabindex="0" title="Remove User">
|
||||
<span class="delete-ui-button" ng-click="removeMember(member.username)"><button class="btn btn-danger">Remove</button></span>
|
||||
<i class="fa fa-times"></i>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<span class="entity-search" organization="''" input-title="'Add a user...'" entity-selected="addNewMember"></span>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
Reference in a new issue