diff --git a/atst/models/application.py b/atst/models/application.py index d06f20b7..8512c570 100644 --- a/atst/models/application.py +++ b/atst/models/application.py @@ -5,6 +5,13 @@ from atst.models import Base from atst.models.types import Id from atst.models import mixins +from atst.models.application_role import ( + ApplicationRole, + Status as ApplicationRoleStatuses, +) + +from atst.database import db + class Application( Base, mixins.TimestampsMixin, mixins.AuditableMixin, mixins.DeletableMixin @@ -28,6 +35,15 @@ class Application( def users(self): return set(role.user for role in self.roles) + @property + def members(self): + return ( + db.session.query(ApplicationRole) + .filter(ApplicationRole.application_id == self.id) + .filter(ApplicationRole.status != ApplicationRoleStatuses.DISABLED) + .all() + ) + @property def num_users(self): return len(self.users) diff --git a/atst/models/application_role.py b/atst/models/application_role.py index 9b8fe05d..3ae42845 100644 --- a/atst/models/application_role.py +++ b/atst/models/application_role.py @@ -4,6 +4,7 @@ from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.orm import relationship from sqlalchemy.event import listen +from atst.utils import first_or_none from atst.models import Base, mixins from atst.models.mixins.auditable import record_permission_sets_updates from .types import Id @@ -59,6 +60,11 @@ class ApplicationRole( def history(self): return self.get_changes() + def has_permission_set(self, perm_set_name): + return first_or_none( + lambda prms: prms.name == perm_set_name, self.permission_sets + ) + Index( "application_role_user_application", diff --git a/atst/routes/portfolios/applications.py b/atst/routes/portfolios/applications.py index 6df0f25b..952ec643 100644 --- a/atst/routes/portfolios/applications.py +++ b/atst/routes/portfolios/applications.py @@ -9,6 +9,7 @@ from flask import ( from . import portfolios_bp from atst.domain.environment_roles import EnvironmentRoles +from atst.domain.environments import Environments from atst.domain.exceptions import UnauthorizedError from atst.domain.applications import Applications from atst.domain.portfolios import Portfolios @@ -16,6 +17,8 @@ from atst.forms.application import NewApplicationForm, ApplicationForm from atst.domain.authz.decorator import user_can_access_decorator as user_can from atst.models.permissions import Permissions from atst.utils.flash import formatted_flash as flash +from atst.domain.permission_sets import PermissionSets +from atst.utils.localization import translate @portfolios_bp.route("/portfolios//applications") @@ -139,3 +142,44 @@ def delete_application(portfolio_id, application_id): return redirect( url_for("portfolios.portfolio_applications", portfolio_id=portfolio_id) ) + + +def permission_str(member, edit_perm_set): + if member.has_permission_set(edit_perm_set): + return translate("portfolios.members.permissions.edit_access") + else: + return translate("portfolios.members.permissions.view_only") + + +@portfolios_bp.route("/portfolios//applications//team") +@user_can(Permissions.VIEW_APPLICATION, message="view portfolio applications") +def application_team(portfolio_id, application_id): + application = Applications.get( + resource_id=application_id, portfolio_id=portfolio_id + ) + + environment_users = {} + for member in application.members: + user_id = member.user.id + environment_users[user_id] = { + "permissions": { + "delete_access": permission_str( + member, PermissionSets.DELETE_APPLICATION_ENVIRONMENTS + ), + "environment_management": permission_str( + member, PermissionSets.EDIT_APPLICATION_ENVIRONMENTS + ), + "team_management": permission_str( + member, PermissionSets.EDIT_APPLICATION_TEAM + ), + }, + "environments": Environments.for_user( + user=member.user, application=application + ), + } + + return render_template( + "portfolios/applications/team.html", + application=application, + environment_users=environment_users, + ) diff --git a/styles/components/_accordion_table.scss b/styles/components/_accordion_table.scss index 715bc057..f11d767d 100644 --- a/styles/components/_accordion_table.scss +++ b/styles/components/_accordion_table.scss @@ -117,3 +117,30 @@ } } } + +#application-members { + .accordion-table { + .accordion-table__head { + font-size: $small-font-size; + padding-left: $gap*3; + } + + .accordion-table__item-content, .accordion-table__head { + display: flex; + + & > span { + flex-grow: 3; + display: flex; + flex-basis: 0; + &.icon-link { + flex-grow: 1; + } + } + } + + span { + flex-grow: 1; + flex-basis: 0; + } + } +} diff --git a/templates/components/toggle_list.html b/templates/components/toggle_list.html index 8bce7dc1..af5f5db2 100644 --- a/templates/components/toggle_list.html +++ b/templates/components/toggle_list.html @@ -12,7 +12,9 @@