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:
dandds 2019-03-26 15:43:39 -04:00 committed by GitHub
commit 6b59ab800b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 205 additions and 54 deletions

View File

@ -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

View File

@ -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"])

View 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

View File

@ -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;
} }
} }

View File

@ -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,19 +12,21 @@
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 %}">
<legend> {% if label %}
<div class="usa-input__title{% if not field.description %}-inline{% endif %}"> <legend>
{{ field.label | striptags}} <div class="usa-input__title{% if not field.description %}-inline{% endif %}">
{% if tooltip %}{{ Tooltip(tooltip) }}{% endif %} {{ field.label | striptags}}
</div> {% if tooltip %}{{ Tooltip(tooltip) }}{% endif %}
</div>
{% if field.description %} {% if field.description %}
<span class='usa-input__help'>{{ field.description | safe }}</span> <span class='usa-input__help'>{{ field.description | safe }}</span>
{% endif %} {% endif %}
<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() }}

View 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 %}

View 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 %}

View File

@ -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>

View File

@ -14,13 +14,14 @@
<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'>
<button type="submit" class="usa-button usa-button-big usa-button-primary" tabindex="0">Save</button> <button type="submit" class="usa-button usa-button-big usa-button-primary" tabindex="0">Save</button>

View 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()

View File

@ -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}!'