From ecbd1f1ef3aed4afdfc69ef8f1be8f1741ab7856 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Mon, 4 Nov 2013 14:56:54 -0500 Subject: [PATCH] Work in progress: Add the team management page --- data/model.py | 27 ++++++- endpoints/api.py | 87 ++++++++++++++++++++- static/css/quay.css | 41 ++++++++++ static/directives/organization-header.html | 17 ++++ static/js/app.js | 20 ++++- static/js/controllers.js | 35 ++++++++- static/partials/repo-admin.html | 2 +- static/partials/team-view.html | 49 ++++++++++++ test/data/test.db | Bin 99328 -> 99328 bytes 9 files changed, 272 insertions(+), 6 deletions(-) create mode 100644 static/directives/organization-header.html create mode 100644 static/partials/team-view.html diff --git a/data/model.py b/data/model.py index e1ea835bb..9b54ed93f 100644 --- a/data/model.py +++ b/data/model.py @@ -117,6 +117,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: + return + + def set_team_org_permission(team, org, role_name): new_role = Role.get(Role.name == role_name) @@ -242,11 +250,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) diff --git a/endpoints/api.py b/endpoints/api.py index ec8e31ab5..a0fb1e0cf 100644 --- a/endpoints/api.py +++ b/endpoints/api.py @@ -57,9 +57,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(): @@ -256,7 +258,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//private', methods=['GET']) @@ -286,6 +288,89 @@ def get_organization_private_allowed(orgname): }) +@app.route('/api/organization//team//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//team//members/', 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//team//members/', 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(): diff --git a/static/css/quay.css b/static/css/quay.css index fa199192c..d1ea6bac9 100644 --- a/static/css/quay.css +++ b/static/css/quay.css @@ -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; @@ -1329,6 +1354,22 @@ p.editable:hover i { 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. */ .twitter-typeahead .tt-query, diff --git a/static/directives/organization-header.html b/static/directives/organization-header.html new file mode 100644 index 000000000..723ed4d80 --- /dev/null +++ b/static/directives/organization-header.html @@ -0,0 +1,17 @@ +
+ + + {{ organization.name }} + + + {{ organization.name }} + + + + / + + + {{ teamName }} + + +
diff --git a/static/js/app.js b/static/js/app.js index dd5fd151c..c167a33c2 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -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; } diff --git a/static/js/controllers.js b/static/js/controllers.js index 3ab8578dd..a2cbb72af 100644 --- a/static/js/controllers.js +++ b/static/js/controllers.js @@ -1173,7 +1173,40 @@ 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; + + 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(); } \ No newline at end of file diff --git a/static/partials/repo-admin.html b/static/partials/repo-admin.html index cd6e4c4b1..a9a23613b 100644 --- a/static/partials/repo-admin.html +++ b/static/partials/repo-admin.html @@ -39,7 +39,7 @@ - {{name}} + {{name}}
diff --git a/static/partials/team-view.html b/static/partials/team-view.html new file mode 100644 index 000000000..2319fca90 --- /dev/null +++ b/static/partials/team-view.html @@ -0,0 +1,49 @@ +
+ +
+ +
+ No matching team found +
+ +
+
+ +
+
Team Members + +
+
+ + + + + + + + + + + + + + + + + +
Member
+ + {{ member.username }} + + + + + +
+ +
+
+
+ + +
diff --git a/test/data/test.db b/test/data/test.db index 242e6c95aa5262320826917830ed7ce05a62ae35..0e3173b94bc0794441687153d5723c5772a3c38c 100644 GIT binary patch delta 521 zcmZvY!An$86o>D9-+Ryd-YY_wjgw$t%Uo0xE+yB_Kj6x6)I~92rQ2avE{$<=<3e0D zZp3+lib#WtnIS|N2g*>3)Ks!COES=f=c(;lUhen(&iS46keeQIhi~eRSB>pXhicin zJ>5M%6*E?k;se9H#~5mgZ`4A6u8d>jX_H-BxDzuZ6lvBd@|qv?kzi6X9};h{o166i0!`TWcI;L> zAgB1tAv>&7rNSaJ^XwwNWw87F*ak1c7m%mzo2 zQ4;eFfrHir(T;w$dT?L(vXbBOmGg!7VjQu{y6|~?8>Ro@hY?MYQ8Zju$zz I@@z|=17)h36#xJL delta 344 zcmXZXF)RaN7{>9h_iyhavFKo7u(31-?84MdtX4w@tNn6QH*K1j3?@;F=t)`;ak-@; zjW`Xhw1}3HB1IdO)Znc%zrXK!p3jWyW?WB<1ft2&`A}3vYU6v$o4a8{vlJx~tTLl@ zw47$yL%|izwlY>oHz+Hb_Fyhga=}PB?O>{UI1x68DNM>Ztn)^U9eJ~e9Xs9_8y+C7 z_@>1J6+BKjVxP2tHSBbKDJZ~G{Ltl