Merge pull request #722 from dod-ccpo/render-edit-buttons-pf-users-table
Render Edit Buttons on Portfolio Members Table
This commit is contained in:
commit
6b59ab800b
@ -1,4 +1,4 @@
|
|||||||
from wtforms.fields import StringField
|
from wtforms.fields import StringField, FormField, FieldList
|
||||||
from wtforms.fields.html5 import EmailField
|
from wtforms.fields.html5 import EmailField
|
||||||
from wtforms.validators import Required, Email, Length
|
from wtforms.validators import Required, Email, Length
|
||||||
|
|
||||||
@ -10,6 +10,7 @@ from atst.utils.localization import translate
|
|||||||
|
|
||||||
|
|
||||||
class PermissionsForm(BaseForm):
|
class PermissionsForm(BaseForm):
|
||||||
|
member = StringField()
|
||||||
perms_app_mgmt = SelectField(
|
perms_app_mgmt = SelectField(
|
||||||
None,
|
None,
|
||||||
choices=[
|
choices=[
|
||||||
@ -50,6 +51,10 @@ class PermissionsForm(BaseForm):
|
|||||||
return _data
|
return _data
|
||||||
|
|
||||||
|
|
||||||
|
class MembersPermissionsForm(BaseForm):
|
||||||
|
members_permissions = FieldList(FormField(PermissionsForm))
|
||||||
|
|
||||||
|
|
||||||
class EditForm(PermissionsForm):
|
class EditForm(PermissionsForm):
|
||||||
# This form also accepts a field for each environment in each application
|
# This form also accepts a field for each environment in each application
|
||||||
# that the user is a member of
|
# that the user is a member of
|
||||||
|
@ -8,9 +8,10 @@ from atst.domain.portfolios import Portfolios
|
|||||||
from atst.domain.audit_log import AuditLog
|
from atst.domain.audit_log import AuditLog
|
||||||
from atst.domain.common import Paginator
|
from atst.domain.common import Paginator
|
||||||
from atst.forms.portfolio import PortfolioForm
|
from atst.forms.portfolio import PortfolioForm
|
||||||
|
from atst.forms.portfolio_member import MembersPermissionsForm
|
||||||
|
from atst.models.permissions import Permissions
|
||||||
from atst.domain.permission_sets import PermissionSets
|
from atst.domain.permission_sets import PermissionSets
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
@portfolios_bp.route("/portfolios")
|
@portfolios_bp.route("/portfolios")
|
||||||
@ -23,27 +24,53 @@ def portfolios():
|
|||||||
return render_template("portfolios/blank_slate.html")
|
return render_template("portfolios/blank_slate.html")
|
||||||
|
|
||||||
|
|
||||||
def serialize_member(member):
|
def permission_str(member, edit_perm_set, view_perm_set):
|
||||||
|
if member.has_permission_set(edit_perm_set):
|
||||||
|
return edit_perm_set
|
||||||
|
else:
|
||||||
|
return view_perm_set
|
||||||
|
|
||||||
|
|
||||||
|
def serialize_member_form_data(member):
|
||||||
return {
|
return {
|
||||||
"member": member,
|
"member": member.user.full_name,
|
||||||
"app_mgmt": member.has_permission_set(
|
"perms_app_mgmt": permission_str(
|
||||||
PermissionSets.EDIT_PORTFOLIO_APPLICATION_MANAGEMENT
|
member,
|
||||||
|
PermissionSets.EDIT_PORTFOLIO_APPLICATION_MANAGEMENT,
|
||||||
|
PermissionSets.VIEW_PORTFOLIO_APPLICATION_MANAGEMENT,
|
||||||
),
|
),
|
||||||
"funding": member.has_permission_set(PermissionSets.EDIT_PORTFOLIO_FUNDING),
|
"perms_funding": permission_str(
|
||||||
"reporting": member.has_permission_set(PermissionSets.EDIT_PORTFOLIO_REPORTS),
|
member,
|
||||||
"portfolio_mgmt": member.has_permission_set(
|
PermissionSets.EDIT_PORTFOLIO_FUNDING,
|
||||||
PermissionSets.EDIT_PORTFOLIO_ADMIN
|
PermissionSets.VIEW_PORTFOLIO_FUNDING,
|
||||||
|
),
|
||||||
|
"perms_reporting": permission_str(
|
||||||
|
member,
|
||||||
|
PermissionSets.EDIT_PORTFOLIO_REPORTS,
|
||||||
|
PermissionSets.VIEW_PORTFOLIO_REPORTS,
|
||||||
|
),
|
||||||
|
"perms_portfolio_mgmt": permission_str(
|
||||||
|
member,
|
||||||
|
PermissionSets.EDIT_PORTFOLIO_ADMIN,
|
||||||
|
PermissionSets.VIEW_PORTFOLIO_ADMIN,
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def render_admin_page(portfolio, form):
|
def render_admin_page(portfolio, form=None):
|
||||||
pagination_opts = Paginator.get_pagination_opts(http_request)
|
pagination_opts = Paginator.get_pagination_opts(http_request)
|
||||||
audit_events = AuditLog.get_portfolio_events(portfolio, pagination_opts)
|
audit_events = AuditLog.get_portfolio_events(portfolio, pagination_opts)
|
||||||
members_data = [serialize_member(member) for member in portfolio.members]
|
members_data = [serialize_member_form_data(member) for member in portfolio.members]
|
||||||
|
|
||||||
|
portfolio_form = PortfolioForm(data={"name": portfolio.name})
|
||||||
|
member_perms_form = MembersPermissionsForm(
|
||||||
|
data={"members_permissions": members_data}
|
||||||
|
)
|
||||||
return render_template(
|
return render_template(
|
||||||
"portfolios/admin.html",
|
"portfolios/admin.html",
|
||||||
form=form,
|
form=form,
|
||||||
|
portfolio_form=portfolio_form,
|
||||||
|
member_perms_form=member_perms_form,
|
||||||
portfolio=portfolio,
|
portfolio=portfolio,
|
||||||
audit_events=audit_events,
|
audit_events=audit_events,
|
||||||
user=g.current_user,
|
user=g.current_user,
|
||||||
@ -55,8 +82,7 @@ def render_admin_page(portfolio, form):
|
|||||||
@user_can(Permissions.VIEW_PORTFOLIO_ADMIN, message="view portfolio admin page")
|
@user_can(Permissions.VIEW_PORTFOLIO_ADMIN, message="view portfolio admin page")
|
||||||
def portfolio_admin(portfolio_id):
|
def portfolio_admin(portfolio_id):
|
||||||
portfolio = Portfolios.get_for_update(portfolio_id)
|
portfolio = Portfolios.get_for_update(portfolio_id)
|
||||||
form = PortfolioForm(data={"name": portfolio.name})
|
return render_admin_page(portfolio)
|
||||||
return render_admin_page(portfolio, form)
|
|
||||||
|
|
||||||
|
|
||||||
@portfolios_bp.route("/portfolios/<portfolio_id>/edit", methods=["POST"])
|
@portfolios_bp.route("/portfolios/<portfolio_id>/edit", methods=["POST"])
|
||||||
|
1
static/icons/plus-circle-solid.svg
Normal file
1
static/icons/plus-circle-solid.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="plus-circle" class="svg-inline--fa fa-plus-circle fa-w-16" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8zm144 276c0 6.6-5.4 12-12 12h-92v92c0 6.6-5.4 12-12 12h-56c-6.6 0-12-5.4-12-12v-92h-92c-6.6 0-12-5.4-12-12v-56c0-6.6 5.4-12 12-12h92v-92c0-6.6 5.4-12 12-12h56c6.6 0 12 5.4 12 12v92h92c6.6 0 12 5.4 12 12v56z"></path></svg>
|
After Width: | Height: | Size: 516 B |
@ -197,8 +197,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
table {
|
table {
|
||||||
box-shadow: 0 6px 18px 0 rgba(144,164,183,0.3);
|
|
||||||
|
|
||||||
thead {
|
thead {
|
||||||
th:first-child {
|
th:first-child {
|
||||||
padding-left: 3 * $gap;
|
padding-left: 3 * $gap;
|
||||||
@ -233,6 +231,26 @@
|
|||||||
font-size: 1.6rem;
|
font-size: 1.6rem;
|
||||||
border-top: 0;
|
border-top: 0;
|
||||||
padding: 3 * $gap 2 * $gap;
|
padding: 3 * $gap 2 * $gap;
|
||||||
|
|
||||||
|
.usa-button-secondary {
|
||||||
|
color: $color-red;
|
||||||
|
background-color: $color-red-lightest;
|
||||||
|
box-shadow: inset 0 0 0 1px $color-red;
|
||||||
|
}
|
||||||
|
|
||||||
|
.usa-button-disabled {
|
||||||
|
color: $color-gray-medium;
|
||||||
|
background-color: $color-gray-lightest;
|
||||||
|
box-shadow: inset 0 0 0 1px $color-gray-medium;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
width: 11rem;
|
||||||
|
height: 3rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.green {
|
.green {
|
||||||
@ -246,11 +264,35 @@
|
|||||||
font-size: 1.2rem;
|
font-size: 1.2rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.usa-input.usa-input--success {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.add-member-link {
|
.add-member-link {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.usa-button-primary .usa-button {
|
||||||
|
padding: 2 * $gap;
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
input.usa-button.usa-button-primary {
|
||||||
|
margin: 0;
|
||||||
|
width: 9rem;
|
||||||
|
height: 4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.members-table-footer {
|
||||||
|
float: right;
|
||||||
|
padding: 3 * $gap;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
{% from "components/icon.html" import Icon %}
|
{% from "components/icon.html" import Icon %}
|
||||||
{% from "components/tooltip.html" import Tooltip %}
|
{% from "components/tooltip.html" import Tooltip %}
|
||||||
|
|
||||||
{% macro OptionsInput(field, tooltip, inline=False) -%}
|
{% macro OptionsInput(field, tooltip, inline=False, label=True) -%}
|
||||||
<optionsinput
|
<optionsinput
|
||||||
name='{{ field.name }}'
|
name='{{ field.name }}'
|
||||||
inline-template
|
inline-template
|
||||||
@ -12,6 +12,7 @@
|
|||||||
v-bind:class="['usa-input', { 'usa-input--error': showError, 'usa-input--success': showValid }]">
|
v-bind:class="['usa-input', { 'usa-input--error': showError, 'usa-input--success': showValid }]">
|
||||||
|
|
||||||
<fieldset data-ally-disabled="true" v-on:change="onInput" class="usa-input__choices {% if inline %}usa-input__choices--inline{% endif %}">
|
<fieldset data-ally-disabled="true" v-on:change="onInput" class="usa-input__choices {% if inline %}usa-input__choices--inline{% endif %}">
|
||||||
|
{% if label %}
|
||||||
<legend>
|
<legend>
|
||||||
<div class="usa-input__title{% if not field.description %}-inline{% endif %}">
|
<div class="usa-input__title{% if not field.description %}-inline{% endif %}">
|
||||||
{{ field.label | striptags}}
|
{{ field.label | striptags}}
|
||||||
@ -25,6 +26,7 @@
|
|||||||
<span v-show='showError'>{{ Icon('alert',classes="icon-validation") }}</span>
|
<span v-show='showError'>{{ Icon('alert',classes="icon-validation") }}</span>
|
||||||
<span v-show='showValid'>{{ Icon('ok',classes="icon-validation") }}</span>
|
<span v-show='showValid'>{{ Icon('ok',classes="icon-validation") }}</span>
|
||||||
</legend>
|
</legend>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{{ field() }}
|
{{ field() }}
|
||||||
|
|
||||||
|
20
templates/fragments/admin/members_edit.html
Normal file
20
templates/fragments/admin/members_edit.html
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
{% for subform in member_perms_form.members_permissions %}
|
||||||
|
<tr>
|
||||||
|
<td class='name'>{{ subform.member.data }}
|
||||||
|
{% if subform.member.data == user.full_name %}
|
||||||
|
<span class='you'>(<span class='green'>you</span>)</span>
|
||||||
|
{% set archive_button_class = 'usa-button-disabled' %}
|
||||||
|
{% else %}
|
||||||
|
{% set archive_button_class = 'usa-button-secondary' %}
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td>{{ OptionsInput(subform.perms_app_mgmt, label=False) }}</td>
|
||||||
|
<td>{{ OptionsInput(subform.perms_funding, label=False) }}</td>
|
||||||
|
<td>{{ OptionsInput(subform.perms_reporting, label=False) }}</td>
|
||||||
|
<td>{{ OptionsInput(subform.perms_portfolio_mgmt, label=False) }}</td>
|
||||||
|
|
||||||
|
<td><button type="button" class='{{ archive_button_class }}'>{{ "portfolios.members.archive_button" | translate }}</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
20
templates/fragments/admin/members_view.html
Normal file
20
templates/fragments/admin/members_view.html
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
{% for subform in member_perms_form.members_permissions %}
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td class='name'>{{ subform.member.data }}
|
||||||
|
{% if subform.member.data == user.full_name %}
|
||||||
|
<span class='you'>(<span class='green'>you</span>)</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
{% set heading_perms = [subform.perms_app_mgmt, subform.perms_funding, subform.perms_reporting, subform.perms_portfolio_mgmt] %}
|
||||||
|
|
||||||
|
{% for access in heading_perms %}
|
||||||
|
{% if dict(access.choices).get(access.data) == 'Edit Access' %}
|
||||||
|
<td class='green'>Edit Access</td>
|
||||||
|
{% else %}
|
||||||
|
<td>View Only</td>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
@ -1,7 +1,10 @@
|
|||||||
{% from "components/icon.html" import Icon %}
|
{% from "components/icon.html" import Icon %}
|
||||||
|
{% from "components/options_input.html" import OptionsInput %}
|
||||||
|
|
||||||
<section class="member-list">
|
<section class="member-list">
|
||||||
<div class='responsive-table-wrapper panel'>
|
<div class='responsive-table-wrapper panel'>
|
||||||
|
<form method='POST' autocomplete="off" enctype="multipart/form-data">
|
||||||
|
|
||||||
<div class='member-list-header'>
|
<div class='member-list-header'>
|
||||||
<div class='left'>
|
<div class='left'>
|
||||||
<div class='h3'>{{ "portfolios.admin.portfolio_members_title" | translate }}</div>
|
<div class='h3'>{{ "portfolios.admin.portfolio_members_title" | translate }}</div>
|
||||||
@ -35,30 +38,28 @@
|
|||||||
</thead>
|
</thead>
|
||||||
|
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for member_data in members_data %}
|
{% if user_can(permissions.EDIT_PORTFOLIO_USERS) %}
|
||||||
<tr>
|
{% include "fragments/admin/members_edit.html" %}
|
||||||
<td class='name'>{{ member_data.member.user_name }}
|
{% elif user_can(permissions.VIEW_PORTFOLIO_USERS) %}
|
||||||
{% if member_data.member.user == user %}
|
{% include "fragments/admin/members_view.html" %}
|
||||||
<span class='you'>(<span class='green'>you</span>)</span>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
|
||||||
{% set heading_perms = [member_data.app_mgmt, member_data.funding, member_data.reporting, member_data.portfolio_mgmt] %}
|
|
||||||
|
|
||||||
{% for has_perm in heading_perms %}
|
|
||||||
{% if has_perm %}
|
|
||||||
<td class='green'>Edit Access</td>
|
|
||||||
{% else %}
|
|
||||||
<td>View Only</td>
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
{% if user_can(permissions.EDIT_PORTFOLIO_USERS) %}
|
||||||
|
<div class="members-table-footer">
|
||||||
|
<a class='icon-link'>
|
||||||
|
{{ "portfolios.admin.add_member" | translate }}
|
||||||
|
{{ Icon('plus-circle-solid') }}
|
||||||
|
</a>
|
||||||
|
<input type='submit' class='usa-button usa-button-primary' value='{{ "Save" }}' />
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
</section>
|
</section>
|
||||||
|
@ -14,12 +14,13 @@
|
|||||||
|
|
||||||
<div class="panel">
|
<div class="panel">
|
||||||
<div class="panel__content">
|
<div class="panel__content">
|
||||||
|
|
||||||
{% if user_can(permissions.VIEW_PORTFOLIO_NAME) %}
|
{% if user_can(permissions.VIEW_PORTFOLIO_NAME) %}
|
||||||
<form method="POST" action="{{ url_for('portfolios.edit_portfolio', portfolio_id=portfolio.id) }}" autocomplete="false">
|
<form method="POST" action="{{ url_for('portfolios.edit_portfolio', portfolio_id=portfolio.id) }}" autocomplete="false">
|
||||||
{{ form.csrf_token }}
|
{{ portfolio_form.csrf_token }}
|
||||||
<div class='form-row'>
|
<div class='form-row'>
|
||||||
<div class='form-col form-col--half'>
|
<div class='form-col form-col--half'>
|
||||||
{{ TextInput(form.name, validation="portfolioName") }}
|
{{ TextInput(portfolio_form.name, validation="portfolioName") }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class='edit-portfolio-name action-group'>
|
<div class='edit-portfolio-name action-group'>
|
||||||
|
28
tests/routes/portfolios/test_admin.py
Normal file
28
tests/routes/portfolios/test_admin.py
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
from flask import url_for
|
||||||
|
|
||||||
|
from atst.domain.permission_sets import PermissionSets
|
||||||
|
|
||||||
|
from tests.factories import PortfolioFactory, PortfolioRoleFactory, UserFactory
|
||||||
|
|
||||||
|
|
||||||
|
def test_member_table_access(client, user_session):
|
||||||
|
admin = UserFactory.create()
|
||||||
|
portfolio = PortfolioFactory.create(owner=admin)
|
||||||
|
rando = UserFactory.create()
|
||||||
|
PortfolioRoleFactory.create(
|
||||||
|
user=rando,
|
||||||
|
portfolio=portfolio,
|
||||||
|
permission_sets=[PermissionSets.get(PermissionSets.VIEW_PORTFOLIO_ADMIN)],
|
||||||
|
)
|
||||||
|
|
||||||
|
url = url_for("portfolios.portfolio_admin", portfolio_id=portfolio.id)
|
||||||
|
|
||||||
|
# editable
|
||||||
|
user_session(admin)
|
||||||
|
edit_resp = client.get(url)
|
||||||
|
assert "<select" in edit_resp.data.decode()
|
||||||
|
|
||||||
|
# not editable
|
||||||
|
user_session(rando)
|
||||||
|
view_resp = client.get(url)
|
||||||
|
assert "<select" not in view_resp.data.decode()
|
@ -138,6 +138,7 @@ forms:
|
|||||||
last_name_label: Last Name
|
last_name_label: Last Name
|
||||||
portfolio_role_description: 'The portfolio role controls whether a member is permitted to organize a portfolio into applications and environments, add members to this portfolio, and view billing information.'
|
portfolio_role_description: 'The portfolio role controls whether a member is permitted to organize a portfolio into applications and environments, add members to this portfolio, and view billing information.'
|
||||||
portfolio_role_label: Portfolio Role
|
portfolio_role_label: Portfolio Role
|
||||||
|
access: Access Level
|
||||||
new_request:
|
new_request:
|
||||||
am_poc_label: I am the Portfolio Owner
|
am_poc_label: I am the Portfolio Owner
|
||||||
average_daily_traffic_description: What is the average daily traffic you expect the systems under this cloud contract to use?
|
average_daily_traffic_description: What is the average daily traffic you expect the systems under this cloud contract to use?
|
||||||
@ -566,14 +567,18 @@ portfolios:
|
|||||||
portfolio_members_title: Portfolio Members
|
portfolio_members_title: Portfolio Members
|
||||||
portfolio_members_subheading: These members have different levels of access to the portfolio.
|
portfolio_members_subheading: These members have different levels of access to the portfolio.
|
||||||
settings_info: Learn more about these settings
|
settings_info: Learn more about these settings
|
||||||
|
add_member: Add a New Member
|
||||||
activity_log_title: Activity Log
|
activity_log_title: Activity Log
|
||||||
members:
|
members:
|
||||||
|
archive_button: Archive User
|
||||||
permissions:
|
permissions:
|
||||||
name: Name
|
name: Name
|
||||||
app_mgmt: App Mgmt
|
app_mgmt: App Mgmt
|
||||||
funding: Funding
|
funding: Funding
|
||||||
reporting: Reporting
|
reporting: Reporting
|
||||||
portfolio_mgmt: Portfolio Mgmt
|
portfolio_mgmt: Portfolio Mgmt
|
||||||
|
view_only: View Only
|
||||||
|
edit_access: Edit Access
|
||||||
testing:
|
testing:
|
||||||
example_string: Hello World
|
example_string: Hello World
|
||||||
example_with_variables: 'Hello, {name}!'
|
example_with_variables: 'Hello, {name}!'
|
||||||
|
Loading…
x
Reference in New Issue
Block a user