diff --git a/data/model/permission.py b/data/model/permission.py index f95764c0c..aeeb55c13 100644 --- a/data/model/permission.py +++ b/data/model/permission.py @@ -5,6 +5,17 @@ from data.database import (RepositoryPermission, User, Repository, Visibility, R from data.model import DataModelException, _basequery +def list_team_permissions(team): + return (RepositoryPermission + .select(RepositoryPermission) + .join(Repository) + .join(Visibility) + .switch(RepositoryPermission) + .join(Role) + .switch(RepositoryPermission) + .where(RepositoryPermission.team == team)) + + def list_robot_permissions(robot_name): return (RepositoryPermission .select(RepositoryPermission, User, Repository) diff --git a/data/model/team.py b/data/model/team.py index ce1f782b4..a04b95483 100644 --- a/data/model/team.py +++ b/data/model/team.py @@ -1,7 +1,9 @@ -from data.database import Team, TeamMember, TeamRole, User, TeamMemberInvite +from data.database import Team, TeamMember, TeamRole, User, TeamMemberInvite, RepositoryPermission from data.model import (DataModelException, InvalidTeamException, UserAlreadyInTeam, InvalidTeamMemberException, user, _basequery) from util.validation import validate_username +from peewee import fn, JOIN_LEFT_OUTER +from util.morecollections import AttrDict def create_team(name, org_obj, team_role_name, description=''): @@ -186,7 +188,50 @@ def get_matching_teams(team_prefix, organization): def get_teams_within_org(organization): - return Team.select().where(Team.organization == organization) + """ Returns a AttrDict of team info (id, name, description), its role under the org, + the number of repositories on which it has permission, and the number of members. + """ + query = (Team.select() + .where(Team.organization == organization) + .join(TeamRole)) + + def _team_view(team): + return { + 'id': team.id, + 'name': team.name, + 'description': team.description, + 'role_name': team.role.name, + + 'repo_count': 0, + 'member_count': 0, + } + + teams = {team.id: _team_view(team) for team in query} + if not teams: + # Just in case. Should ideally never happen. + return [] + + # Add repository permissions count. + permission_tuples = (RepositoryPermission.select(RepositoryPermission.team, + fn.Count(RepositoryPermission.id)) + .where(RepositoryPermission.team << teams.keys()) + .group_by(RepositoryPermission.team) + .tuples()) + + for perm_tuple in permission_tuples: + teams[perm_tuple[0]]['repo_count'] = perm_tuple[1] + + # Add the member count. + members_tuples = (TeamMember.select(TeamMember.team, + fn.Count(TeamMember.id)) + .where(TeamMember.team << teams.keys()) + .group_by(TeamMember.team) + .tuples()) + + for member_tuple in members_tuples: + teams[member_tuple[0]]['member_count'] = member_tuple[1] + + return [AttrDict(team_info) for team_info in teams.values()] def get_user_teams_within_org(username, organization): diff --git a/endpoints/api/organization.py b/endpoints/api/organization.py index 5629ce879..685094d87 100644 --- a/endpoints/api/organization.py +++ b/endpoints/api/organization.py @@ -8,13 +8,12 @@ import features from app import billing as stripe, avatar from endpoints.api import (resource, nickname, ApiResource, validate_json_request, request_error, - related_user_resource, internal_only, require_user_admin, log_action, + related_user_resource, internal_only, require_user_admin, log_action, show_if, path_param, require_scope) from endpoints.exception import Unauthorized, NotFound -from endpoints.api.team import team_view from endpoints.api.user import User, PrivateRepositories from auth.permissions import (AdministerOrganizationPermission, OrganizationMemberPermission, - CreateRepositoryPermission) + CreateRepositoryPermission, ViewTeamPermission) from auth.auth_context import get_authenticated_user from auth import scopes from data import model @@ -24,6 +23,18 @@ from data.billing import get_plan logger = logging.getLogger(__name__) +def team_view(orgname, team): + return { + 'name': team.name, + 'description': team.description, + 'role': team.role_name, + 'avatar': avatar.get_data_for_team(team), + 'can_view': ViewTeamPermission(orgname, team.name).can(), + + 'repo_count': team.repo_count, + 'member_count': team.member_count, + } + def org_view(o, teams): is_admin = AdministerOrganizationPermission(o.username).can() diff --git a/endpoints/api/team.py b/endpoints/api/team.py index 051c9bb1a..358887af1 100644 --- a/endpoints/api/team.py +++ b/endpoints/api/team.py @@ -5,7 +5,7 @@ from flask import request import features from endpoints.api import (resource, nickname, ApiResource, validate_json_request, request_error, - log_action, internal_only, require_scope, path_param, query_param, + log_action, internal_only, require_scope, path_param, query_param, truthy_bool, parse_args, require_user_admin, show_if) from endpoints.exception import Unauthorized, NotFound from auth.permissions import AdministerOrganizationPermission, ViewTeamPermission @@ -15,6 +15,15 @@ from data import model from util.useremails import send_org_invite_email from app import avatar +def permission_view(permission): + return { + 'repository': { + 'name': permission.repository.name, + 'is_public': permission.repository.visibility.name == 'public' + }, + 'role': permission.role.name + } + def try_accept_invite(code, user): (team, inviter) = model.team.confirm_team_invite(code, user) @@ -346,6 +355,30 @@ class InviteTeamMember(ApiResource): raise Unauthorized() +@resource('/v1/organization//team//permissions') +@path_param('orgname', 'The name of the organization') +@path_param('teamname', 'The name of the team') +class TeamPermissions(ApiResource): + """ Resource for listing the permissions an org's team has in the system. """ + @nickname('getTeamPermissions') + def get(self, orgname, teamname): + """ Returns the list of repository permissions for the org's team. """ + permission = AdministerOrganizationPermission(orgname) + if permission.can(): + try: + team = model.team.get_organization_team(orgname, teamname) + except model.InvalidTeamException: + raise NotFound() + + permissions = model.permission.list_team_permissions(team) + + return { + 'permissions': [permission_view(permission) for permission in permissions] + } + + raise Unauthorized() + + @resource('/v1/teaminvite/') @internal_only @show_if(features.MAILING) diff --git a/static/css/directives/ui/create-entity-dialog.css b/static/css/directives/ui/create-entity-dialog.css index f7eb3fc9f..59b598b97 100644 --- a/static/css/directives/ui/create-entity-dialog.css +++ b/static/css/directives/ui/create-entity-dialog.css @@ -15,39 +15,3 @@ margin-left: 4px; margin-right: 4px; } - -.create-entity-dialog-element label { - margin-top: 4px; -} - -.create-entity-dialog-element .co-table { - margin-top: 20px; -} - -.create-entity-dialog-element .fa-hdd-o { - margin-right: 4px; - vertical-align: middle; -} - -.create-entity-dialog-element .co-filter-box { - display: block; - float: right; - margin-bottom: 20px; -} - -.create-entity-dialog-element .co-filter-box .filter-message { - left: -180px; - top: 4px; -} - -.create-entity-dialog-element .co-filter-box input { - width: 100%; - padding-top: 2px; - padding-bottom: 2px; - height: 28px; -} - -.create-entity-dialog-element label .avatar { - vertical-align: text-bottom; - margin-left: 4px; -} \ No newline at end of file diff --git a/static/css/directives/ui/set-repo-permissions.css b/static/css/directives/ui/set-repo-permissions.css new file mode 100644 index 000000000..f5d8b0087 --- /dev/null +++ b/static/css/directives/ui/set-repo-permissions.css @@ -0,0 +1,35 @@ +.set-repo-permissions-element label { + margin-top: 4px; +} + +.set-repo-permissions-element .co-table { + margin-top: 20px; +} + +.set-repo-permissions-element .fa-hdd-o { + margin-right: 4px; + vertical-align: middle; +} + +.set-repo-permissions-element .co-filter-box { + display: block; + float: right; + margin-bottom: 20px; +} + +.set-repo-permissions-element .co-filter-box .filter-message { + left: -180px; + top: 4px; +} + +.set-repo-permissions-element .co-filter-box input { + width: 100%; + padding-top: 2px; + padding-bottom: 2px; + height: 28px; +} + +.set-repo-permissions-element label .avatar { + vertical-align: text-bottom; + margin-left: 4px; +} \ No newline at end of file diff --git a/static/css/directives/ui/teams-manager.css b/static/css/directives/ui/teams-manager.css index 5d6f74a04..a93a844ac 100644 --- a/static/css/directives/ui/teams-manager.css +++ b/static/css/directives/ui/teams-manager.css @@ -1,5 +1,19 @@ -.teams-manager .popup-input-button { +.teams-manager .co-filter-box { + display: block; float: right; + margin-bottom: 20px; +} + +.teams-manager.co-filter-box .filter-message { + left: -180px; + top: 4px; +} + +.teams-manager .co-filter-box input { + width: 100%; + padding-top: 2px; + padding-bottom: 2px; + height: 28px; } .teams-manager .manager-header { @@ -20,6 +34,10 @@ color: #ccc; } +.teams-manager .co-table .avatar { + margin-right: 6px; +} + .teams-manager .cor-confirm-dialog .entity-reference .avatar { margin-left: 4px; margin-right: 0px; @@ -36,14 +54,20 @@ } @media (max-width: 767px) { - .teams-manager .control-col { - padding-left: 55px; - padding-bottom: 10px; + .teams-manager .co-filter-box { + display: block; + float: none; } } -.teams-manager .header-col .info-icon { +.teams-manager .info-icon { + margin-left: 4px; +} + +.teams-manager .popover-content { + color: black; font-size: 16px; + text-transform: none; } .teams-manager .header-col .header-text { diff --git a/static/css/quay.css b/static/css/quay.css index fa9fb3d91..0088fda4f 100644 --- a/static/css/quay.css +++ b/static/css/quay.css @@ -634,13 +634,6 @@ i.toggle-icon:hover { padding: 6px; } -.info-icon { - display: inline-block; - float: right; - vertical-align: middle; - font-size: 20px; -} - .accordion-toggle { cursor: pointer; text-decoration: none !important; diff --git a/static/directives/create-entity-dialog.html b/static/directives/create-entity-dialog.html index 90d59f959..a3ef2d966 100644 --- a/static/directives/create-entity-dialog.html +++ b/static/directives/create-entity-dialog.html @@ -8,81 +8,25 @@ Create {{ entityTitle }} - -