Work in progress: Add the team management page
This commit is contained in:
parent
100ec563fa
commit
ecbd1f1ef3
9 changed files with 272 additions and 6 deletions
|
@ -117,6 +117,14 @@ def add_user_to_team(user, team):
|
||||||
return TeamMember.create(user=user, team=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:
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
def set_team_org_permission(team, org, role_name):
|
def set_team_org_permission(team, org, role_name):
|
||||||
new_role = Role.get(Role.name == role_name)
|
new_role = Role.get(Role.name == role_name)
|
||||||
|
|
||||||
|
@ -242,12 +250,27 @@ def get_user_organizations(username):
|
||||||
|
|
||||||
def get_organization(name):
|
def get_organization(name):
|
||||||
try:
|
try:
|
||||||
return User.get(username=name, organization=True)
|
return User.get(username = name, organization = True)
|
||||||
except User.DoesNotExist:
|
except User.DoesNotExist:
|
||||||
raise InvalidOrganizationException('Organization does not exist: %s' %
|
raise InvalidOrganizationException('Organization does not exist: %s' %
|
||||||
name)
|
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):
|
def get_user_teams_within_org(username, organization):
|
||||||
joined = Team.select().join(TeamMember).join(User)
|
joined = Team.select().join(TeamMember).join(User)
|
||||||
return joined.where(Team.organization == organization,
|
return joined.where(Team.organization == organization,
|
||||||
|
|
|
@ -57,9 +57,11 @@ def plans_list():
|
||||||
@app.route('/api/user/', methods=['GET'])
|
@app.route('/api/user/', methods=['GET'])
|
||||||
def get_logged_in_user():
|
def get_logged_in_user():
|
||||||
def org_view(o):
|
def org_view(o):
|
||||||
|
# TODO: return whether the user is really the admin of the organization
|
||||||
return {
|
return {
|
||||||
'name': o.username,
|
'name': o.username,
|
||||||
'gravatar': compute_hash(o.email),
|
'gravatar': compute_hash(o.email),
|
||||||
|
'is_org_admin': True
|
||||||
}
|
}
|
||||||
|
|
||||||
if current_user.is_anonymous():
|
if current_user.is_anonymous():
|
||||||
|
@ -256,7 +258,7 @@ def get_organization(orgname):
|
||||||
abort(404)
|
abort(404)
|
||||||
|
|
||||||
teams = model.get_user_teams_within_org(user.username, org)
|
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'])
|
@app.route('/api/organization/<orgname>/private', methods=['GET'])
|
||||||
|
@ -286,6 +288,89 @@ def get_organization_private_allowed(orgname):
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/organization/<orgname>/team/<teamname>/members', methods=['GET'])
|
||||||
|
def get_organization_team_members(orgname, teamname):
|
||||||
|
def member_view(m):
|
||||||
|
return {
|
||||||
|
'username': m.username
|
||||||
|
}
|
||||||
|
|
||||||
|
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': [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({
|
||||||
|
'success': True
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@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'])
|
@app.route('/api/repository', methods=['POST'])
|
||||||
@api_login_required
|
@api_login_required
|
||||||
def create_repo_api():
|
def create_repo_api():
|
||||||
|
|
|
@ -2,6 +2,31 @@
|
||||||
font-family: 'Droid Sans', sans-serif;
|
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 {
|
.namespace-selector-dropdown .namespace {
|
||||||
padding: 6px;
|
padding: 6px;
|
||||||
padding-left: 10px;
|
padding-left: 10px;
|
||||||
|
@ -1329,6 +1354,22 @@ p.editable:hover i {
|
||||||
min-height: 50px;
|
min-height: 50px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.team-view .panel {
|
||||||
|
display: inline-block;
|
||||||
|
width: 620px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-view .entity {
|
||||||
|
font-size: 1.2em;
|
||||||
|
min-width: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-view .entity i {
|
||||||
|
margin-right: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* Overrides for typeahead to work with bootstrap 3. */
|
/* Overrides for typeahead to work with bootstrap 3. */
|
||||||
|
|
||||||
.twitter-typeahead .tt-query,
|
.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 () {
|
quayApp.directive('entitySearch', function () {
|
||||||
var number = 0;
|
var number = 0;
|
||||||
var directiveDefinitionObject = {
|
var directiveDefinitionObject = {
|
||||||
|
|
|
@ -1173,7 +1173,40 @@ function OrgTeamsCtrl($scope, Restangular, $routeParams) {
|
||||||
var orgname = $routeParams.orgname;
|
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 orgname = $routeParams.orgname;
|
||||||
var teamname = $routeParams.teamname;
|
var teamname = $routeParams.teamname;
|
||||||
|
|
||||||
|
$rootScope.title = 'Loading...';
|
||||||
|
$scope.loading = true;
|
||||||
|
$scope.teamname = teamname;
|
||||||
|
|
||||||
|
var loadOrganization = function() {
|
||||||
|
var getOrganization = Restangular.one('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('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();
|
||||||
}
|
}
|
|
@ -39,7 +39,7 @@
|
||||||
<tr ng-repeat="(name, permission) in permissions['team']">
|
<tr ng-repeat="(name, permission) in permissions['team']">
|
||||||
<td class="team entity">
|
<td class="team entity">
|
||||||
<i class="fa fa-group"></i>
|
<i class="fa fa-group"></i>
|
||||||
<span>{{name}}</span>
|
<span><a href="/organization/{{ repo.namespace }}/teams/{{ name }}">{{name}}</a></span>
|
||||||
</td>
|
</td>
|
||||||
<td class="user-permissions">
|
<td class="user-permissions">
|
||||||
<div class="btn-group btn-group-sm">
|
<div class="btn-group btn-group-sm">
|
||||||
|
|
49
static/partials/team-view.html
Normal file
49
static/partials/team-view.html
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
<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">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<td>Member</td>
|
||||||
|
<td></td>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tr ng-repeat="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-remove"></i>
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td colspan="2">
|
||||||
|
<span class="entity-search" input-title="'Add a user...'" entity-selected="addNewMember"></span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
Binary file not shown.
Reference in a new issue