Merge pull request #748 from dod-ccpo/display-app-users
Display app users
This commit is contained in:
commit
882998e1d7
@ -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)
|
||||
|
@ -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",
|
||||
|
@ -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/<portfolio_id>/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/<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(
|
||||
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,
|
||||
)
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -12,7 +12,9 @@
|
||||
<li is="toggler" class="accordion-table__item">
|
||||
<template slot-scope="props">
|
||||
<div class="accordion-table__item-content">
|
||||
<span v-on:click="props.toggle">{{ item_name }}</span>
|
||||
<span v-on:click="props.toggle">
|
||||
{{ item_name }}
|
||||
</span>
|
||||
|
||||
<template v-if="props.isVisible">
|
||||
{{
|
||||
|
@ -49,7 +49,9 @@
|
||||
<div class='separator'></div>
|
||||
{% endif %}
|
||||
{% 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 class='counter'>{{ application.num_users }}</span>
|
||||
</a>
|
||||
|
105
templates/portfolios/applications/team.html
Normal file
105
templates/portfolios/applications/team.html
Normal file
@ -0,0 +1,105 @@
|
||||
{% 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="application-members">
|
||||
<div class='responsive-table-wrapper panel'>
|
||||
{% if g.matchesPath("application-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>
|
||||
<span class="icon-link" />
|
||||
</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,22 @@ def test_edit_application_scope(client, user_session):
|
||||
application_id=port1.applications[0].id,
|
||||
)
|
||||
)
|
||||
|
||||
assert response.status_code == 404
|
||||
|
||||
|
||||
def test_application_team(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
|
||||
|
@ -12,6 +12,7 @@ from atst.models.portfolio_role import Status as PortfolioRoleStatus
|
||||
|
||||
from tests.factories import (
|
||||
AttachmentFactory,
|
||||
ApplicationFactory,
|
||||
ApplicationRoleFactory,
|
||||
InvitationFactory,
|
||||
PortfolioFactory,
|
||||
@ -815,3 +816,21 @@ def test_task_orders_update_access(post_url_assert_status):
|
||||
post_url_assert_status(owner, url, 302)
|
||||
post_url_assert_status(ccpo, url, 302)
|
||||
post_url_assert_status(rando, url, 404)
|
||||
|
||||
|
||||
def test_portfolio_application_team_access(get_url_assert_status):
|
||||
ccpo = UserFactory.create_ccpo()
|
||||
rando = UserFactory.create()
|
||||
|
||||
portfolio = PortfolioFactory.create()
|
||||
application = ApplicationFactory.create(portfolio=portfolio)
|
||||
|
||||
url = url_for(
|
||||
"portfolios.application_team",
|
||||
portfolio_id=portfolio.id,
|
||||
application_id=application.id,
|
||||
)
|
||||
|
||||
get_url_assert_status(ccpo, url, 200)
|
||||
get_url_assert_status(portfolio.owner, url, 200)
|
||||
get_url_assert_status(rando, url, 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.
|
||||
update_button_text: Save
|
||||
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:
|
||||
title: '{application_name} Team Management'
|
||||
subheading: Team Management
|
||||
|
Loading…
x
Reference in New Issue
Block a user