From c3b10c12bbf9dfc22a05a2b7703b17a86ad88ae9 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Thu, 31 Oct 2013 15:04:07 -0400 Subject: [PATCH 1/3] Add check for existing repo with the same name --- endpoints/api.py | 5 +++++ static/css/quay.css | 10 +++++++++- static/js/controllers.js | 11 ++++++++--- static/partials/new-repo.html | 4 ++-- 4 files changed, 24 insertions(+), 6 deletions(-) diff --git a/endpoints/api.py b/endpoints/api.py index 853c7498f..af08c7b45 100644 --- a/endpoints/api.py +++ b/endpoints/api.py @@ -187,6 +187,11 @@ def create_repo_api(): namespace_name = owner.username repository_name = request.get_json()['repository'] + + 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, diff --git a/static/css/quay.css b/static/css/quay.css index 9f5001fc4..63cbe4fbd 100644 --- a/static/css/quay.css +++ b/static/css/quay.css @@ -179,10 +179,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; } @@ -366,6 +370,10 @@ padding: 20px; } +.landing .popover { + font-size: 14px; +} + .landing { color: white; diff --git a/static/js/controllers.js b/static/js/controllers.js index 0b7a8c3f4..939890838 100644 --- a/static/js/controllers.js +++ b/static/js/controllers.js @@ -953,7 +953,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); @@ -1069,6 +1069,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(); @@ -1096,9 +1098,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'); + }); }); }; diff --git a/static/partials/new-repo.html b/static/partials/new-repo.html index 30cdd2316..02ad53331 100644 --- a/static/partials/new-repo.html +++ b/static/partials/new-repo.html @@ -20,7 +20,7 @@
-
+
@@ -29,7 +29,7 @@
- {{user.username}} / + {{user.username}} /
From ecbd1f1ef3aed4afdfc69ef8f1be8f1741ab7856 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Mon, 4 Nov 2013 14:56:54 -0500 Subject: [PATCH 2/3] 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 Date: Mon, 4 Nov 2013 15:31:38 -0500 Subject: [PATCH 3/3] Add ability to change the members of a team --- endpoints/api.py | 16 ++++++------ static/css/quay.css | 17 +++++++++++-- static/js/controllers.js | 43 +++++++++++++++++++++++++++++---- static/partials/repo-admin.html | 2 +- static/partials/team-view.html | 16 +++--------- 5 files changed, 65 insertions(+), 29 deletions(-) diff --git a/endpoints/api.py b/endpoints/api.py index 55306ee89..ef7d675de 100644 --- a/endpoints/api.py +++ b/endpoints/api.py @@ -288,13 +288,13 @@ def get_organization_private_allowed(orgname): }) +def member_view(m): + return { + 'username': m.username + } + @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) @@ -310,7 +310,7 @@ def get_organization_team_members(orgname, teamname): members = model.get_organization_team_members(team.id) return jsonify({ - 'members': [member_view(m) for m in members] + 'members': { m.username : member_view(m) for m in members } }) @@ -337,9 +337,7 @@ def update_organization_team_member(orgname, teamname, membername): # Add the user to the team. model.add_user_to_team(user, team) - return jsonify({ - 'success': True - }) + return jsonify(member_view(user)) @app.route('/api/organization//team//members/', methods=['DELETE']) diff --git a/static/css/quay.css b/static/css/quay.css index cd9253696..4cedc576e 100644 --- a/static/css/quay.css +++ b/static/css/quay.css @@ -1153,7 +1153,7 @@ p.editable:hover i { } .delete-ui:focus .delete-ui-button { - width: 54px; + width: 60px; } .repo-admin .repo-delete { @@ -1369,14 +1369,27 @@ p.editable:hover i { .team-view .entity { font-size: 1.2em; - min-width: 300px; + 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. */ diff --git a/static/js/controllers.js b/static/js/controllers.js index 06b810ec9..6152bed9e 100644 --- a/static/js/controllers.js +++ b/static/js/controllers.js @@ -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; @@ -1191,8 +1202,30 @@ function TeamViewCtrl($rootScope, $scope, Restangular, $routeParams) { $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('organization/' + orgname); + var getOrganization = Restangular.one(getRestUrl('organization', orgname)) getOrganization.get().then(function(resp) { $scope.organization = resp; $scope.loading = !$scope.organization || !$scope.members; @@ -1202,7 +1235,7 @@ function TeamViewCtrl($rootScope, $scope, Restangular, $routeParams) { }; var loadMembers = function() { - var getMembers = Restangular.one('organization/' + orgname + '/team/' + teamname + '/members'); + 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; diff --git a/static/partials/repo-admin.html b/static/partials/repo-admin.html index a9a23613b..511554a87 100644 --- a/static/partials/repo-admin.html +++ b/static/partials/repo-admin.html @@ -72,7 +72,7 @@ - + diff --git a/static/partials/team-view.html b/static/partials/team-view.html index 2319fca90..317916362 100644 --- a/static/partials/team-view.html +++ b/static/partials/team-view.html @@ -14,16 +14,8 @@
- - - - - - - - - - +
Member
+
{{ member.username }} @@ -31,14 +23,14 @@ - +
- +