Display app users view only table
This commit is contained in:
parent
ec3d4f518f
commit
769867c6a9
@ -5,6 +5,13 @@ from atst.models import Base
|
|||||||
from atst.models.types import Id
|
from atst.models.types import Id
|
||||||
from atst.models import mixins
|
from atst.models import mixins
|
||||||
|
|
||||||
|
from atst.models.application_role import (
|
||||||
|
ApplicationRole,
|
||||||
|
Status as ApplicationRoleStatuses,
|
||||||
|
)
|
||||||
|
|
||||||
|
from atst.database import db
|
||||||
|
|
||||||
|
|
||||||
class Application(
|
class Application(
|
||||||
Base, mixins.TimestampsMixin, mixins.AuditableMixin, mixins.DeletableMixin
|
Base, mixins.TimestampsMixin, mixins.AuditableMixin, mixins.DeletableMixin
|
||||||
@ -28,6 +35,15 @@ class Application(
|
|||||||
def users(self):
|
def users(self):
|
||||||
return set(role.user for role in self.roles)
|
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
|
@property
|
||||||
def num_users(self):
|
def num_users(self):
|
||||||
return len(self.users)
|
return len(self.users)
|
||||||
|
@ -4,6 +4,7 @@ from sqlalchemy.dialects.postgresql import UUID
|
|||||||
from sqlalchemy.orm import relationship
|
from sqlalchemy.orm import relationship
|
||||||
from sqlalchemy.event import listen
|
from sqlalchemy.event import listen
|
||||||
|
|
||||||
|
from atst.utils import first_or_none
|
||||||
from atst.models import Base, mixins
|
from atst.models import Base, mixins
|
||||||
from atst.models.mixins.auditable import record_permission_sets_updates
|
from atst.models.mixins.auditable import record_permission_sets_updates
|
||||||
from .types import Id
|
from .types import Id
|
||||||
@ -59,6 +60,11 @@ class ApplicationRole(
|
|||||||
def history(self):
|
def history(self):
|
||||||
return self.get_changes()
|
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(
|
Index(
|
||||||
"application_role_user_application",
|
"application_role_user_application",
|
||||||
|
@ -9,6 +9,7 @@ from flask import (
|
|||||||
|
|
||||||
from . import portfolios_bp
|
from . import portfolios_bp
|
||||||
from atst.domain.environment_roles import EnvironmentRoles
|
from atst.domain.environment_roles import EnvironmentRoles
|
||||||
|
from atst.domain.environments import Environments
|
||||||
from atst.domain.exceptions import UnauthorizedError
|
from atst.domain.exceptions import UnauthorizedError
|
||||||
from atst.domain.applications import Applications
|
from atst.domain.applications import Applications
|
||||||
from atst.domain.portfolios import Portfolios
|
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.domain.authz.decorator import user_can_access_decorator as user_can
|
||||||
from atst.models.permissions import Permissions
|
from atst.models.permissions import Permissions
|
||||||
from atst.utils.flash import formatted_flash as flash
|
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/<portfolio_id>/applications")
|
@portfolios_bp.route("/portfolios/<portfolio_id>/applications")
|
||||||
@ -139,3 +142,44 @@ def delete_application(portfolio_id, application_id):
|
|||||||
return redirect(
|
return redirect(
|
||||||
url_for("portfolios.portfolio_applications", portfolio_id=portfolio_id)
|
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/<portfolio_id>/applications/<application_id>/team")
|
||||||
|
@user_can(Permissions.VIEW_APPLICATION, message="view portfolio applications")
|
||||||
|
def application_team(portfolio_id, application_id):
|
||||||
|
application = Applications.get(application_id)
|
||||||
|
portfolio = Portfolios.get(g.current_user, 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,
|
||||||
|
portfolio=portfolio,
|
||||||
|
environment_users=environment_users,
|
||||||
|
)
|
||||||
|
@ -117,3 +117,28 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#portfolio-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 {
|
||||||
|
width: 75%;
|
||||||
|
&.icon-link {
|
||||||
|
width: 25%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
display: flex;
|
||||||
|
width: 25%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -49,7 +49,9 @@
|
|||||||
<div class='separator'></div>
|
<div class='separator'></div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if user_can(permissions.VIEW_PORTFOLIO_USERS) %}
|
{% if user_can(permissions.VIEW_PORTFOLIO_USERS) %}
|
||||||
<a class='icon-link'>
|
<a
|
||||||
|
href="{{ url_for('portfolios.application_team', portfolio_id=portfolio.id, application_id=application.id) }}"
|
||||||
|
class='icon-link'>
|
||||||
<span>{{ "portfolios.applications.team_text" | translate }}</span>
|
<span>{{ "portfolios.applications.team_text" | translate }}</span>
|
||||||
<span class='counter'>{{ application.num_users }}</span>
|
<span class='counter'>{{ application.num_users }}</span>
|
||||||
</a>
|
</a>
|
||||||
|
104
templates/portfolios/applications/team.html
Normal file
104
templates/portfolios/applications/team.html
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
{% extends "portfolios/applications/base.html" %}
|
||||||
|
|
||||||
|
{% from "components/empty_state.html" import EmptyState %}
|
||||||
|
{% from "components/icon.html" import Icon %}
|
||||||
|
{% from "components/toggle_list.html" import ToggleList %}
|
||||||
|
|
||||||
|
{% set secondary_breadcrumb = 'portfolios.applications.team_settings.title' | translate({ "application_name": application.name }) %}
|
||||||
|
|
||||||
|
{% block application_content %}
|
||||||
|
{% if not application.members %}
|
||||||
|
{% set user_can_invite = user_can(permissions.CREATE_APPLICATION_MEMBER) %}
|
||||||
|
|
||||||
|
{{ EmptyState(
|
||||||
|
("portfolios.applications.team_settings.blank_slate.title" | translate),
|
||||||
|
action_label=("portfolios.applications.team_settings.blank_slate.action_label" | translate),
|
||||||
|
action_href='#' if user_can_invite else None,
|
||||||
|
sub_message=None if user_can_invite else ("portfolios.team_settings.blank_slate.sub_message" | translate),
|
||||||
|
icon='avatar'
|
||||||
|
) }}
|
||||||
|
|
||||||
|
{% else %}
|
||||||
|
<div class='subheading'>
|
||||||
|
{{ 'portfolios.applications.team_settings.subheading' | translate }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section class="member-list" id="portfolio-members">
|
||||||
|
<div class='responsive-table-wrapper panel'>
|
||||||
|
{% if g.matchesPath("portfolio-members") %}
|
||||||
|
{% include "fragments/flash.html" %}
|
||||||
|
{% endif %}
|
||||||
|
<form>
|
||||||
|
<header>
|
||||||
|
<div class="responsive-table-wrapper__header">
|
||||||
|
<div class="responsive-table-wrapper__title">
|
||||||
|
<div class="h3">
|
||||||
|
{{ "portfolios.applications.team_settings.section.title" | translate({ "application_name": application.name }) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<a class='icon-link'>
|
||||||
|
{{ Icon('info') }}
|
||||||
|
{{ "portfolios.admin.settings_info" | translate }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="accordion-table accordion-table-list">
|
||||||
|
<div class="accordion-table__head">
|
||||||
|
<span>
|
||||||
|
<span>
|
||||||
|
{{ "portfolios.applications.team_settings.user" | translate }}
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
{{ "portfolios.applications.team_settings.section.table.delete_access" | translate }}
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
{{ "portfolios.applications.team_settings.section.table.environment_management" | translate }}
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
{{ "portfolios.applications.team_settings.section.table.team_management" | translate }}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<ul class="accordion-table__items">
|
||||||
|
{% for member in application.members %}
|
||||||
|
{% set user = member.user %}
|
||||||
|
{% set user_info = environment_users[user.id] %}
|
||||||
|
{% set user_permissions = user_info["permissions"] %}
|
||||||
|
|
||||||
|
{% set user_row %}
|
||||||
|
<span>{{ user.full_name }}</span>
|
||||||
|
<span>{{ user_permissions["delete_access"] }}</span>
|
||||||
|
<span>{{ user_permissions["environment_management"] }}</span>
|
||||||
|
<span>{{ user_permissions["team_management"] }}</span>
|
||||||
|
{% endset %}
|
||||||
|
|
||||||
|
{% call ToggleList(
|
||||||
|
item_name=user_row,
|
||||||
|
item_type=("portfolios.applications.team_settings.environments" | translate),
|
||||||
|
length=(user_info["environments"] | length)
|
||||||
|
)
|
||||||
|
%}
|
||||||
|
<ul>
|
||||||
|
{% for environment in user_info["environments"] %}
|
||||||
|
<li class="accordion-table__item__expanded">
|
||||||
|
<div class="accordion-table__item-content">
|
||||||
|
{{ environment.name }}
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endcall %}
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="members-table-footer">
|
||||||
|
<div class="action-group save">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
@ -347,4 +347,40 @@ def test_edit_application_scope(client, user_session):
|
|||||||
application_id=port1.applications[0].id,
|
application_id=port1.applications[0].id,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
|
def test_application_team_with_permissions(client, user_session):
|
||||||
|
portfolio = PortfolioFactory.create()
|
||||||
|
application = ApplicationFactory.create(portfolio=portfolio)
|
||||||
|
|
||||||
|
user_session(portfolio.owner)
|
||||||
|
|
||||||
|
response = client.get(
|
||||||
|
url_for(
|
||||||
|
"portfolios.application_team",
|
||||||
|
portfolio_id=portfolio.id,
|
||||||
|
application_id=application.id,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
|
||||||
|
def test_application_team_without_permissions(client, user_session):
|
||||||
|
random_user = UserFactory.create()
|
||||||
|
portfolio = PortfolioFactory.create()
|
||||||
|
application = ApplicationFactory.create(portfolio=portfolio)
|
||||||
|
|
||||||
|
user_session(random_user)
|
||||||
|
|
||||||
|
response = client.get(
|
||||||
|
url_for(
|
||||||
|
"portfolios.application_team",
|
||||||
|
portfolio_id=portfolio.id,
|
||||||
|
application_id=application.id,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
assert response.status_code == 404
|
assert response.status_code == 404
|
||||||
|
@ -601,6 +601,24 @@ portfolios:
|
|||||||
environments_description: Each environment created within an application is logically separated from one another for easier management and security.
|
environments_description: Each environment created within an application is logically separated from one another for easier management and security.
|
||||||
update_button_text: Save
|
update_button_text: Save
|
||||||
create_button_text: Create
|
create_button_text: Create
|
||||||
|
team_settings:
|
||||||
|
title: '{application_name} Team Settings'
|
||||||
|
subheading: Team Settings
|
||||||
|
user: User
|
||||||
|
environments: Environments
|
||||||
|
blank_slate:
|
||||||
|
action_label: Invite a new team member
|
||||||
|
sub_message: Please contact your JEDI Cloud portfolio administrator to invite new members.
|
||||||
|
title: There are currently no team members for this application.
|
||||||
|
section:
|
||||||
|
title: '{application_name} Team'
|
||||||
|
members_count: 'Members ({members_count})'
|
||||||
|
table:
|
||||||
|
delete_access: Delete Access
|
||||||
|
environment_management: Environment Management
|
||||||
|
environments: Environments
|
||||||
|
name: Name
|
||||||
|
team_management: Team Management
|
||||||
team_management:
|
team_management:
|
||||||
title: '{application_name} Team Management'
|
title: '{application_name} Team Management'
|
||||||
subheading: Team Management
|
subheading: Team Management
|
||||||
|
Loading…
x
Reference in New Issue
Block a user