Merge branch 'staging' into topbar-styling

This commit is contained in:
Hannah Brinkman 2020-01-17 13:02:20 -05:00 committed by GitHub
commit 0f52e75a4e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 436 additions and 321 deletions

View File

@ -152,7 +152,7 @@
"hashed_secret": "e4f14805dfd1e6af030359090c535e149e6b4207",
"is_secret": false,
"is_verified": false,
"line_number": 665,
"line_number": 649,
"type": "Hex High Entropy String"
}
]

View File

@ -19,9 +19,6 @@ from atst.domain.exceptions import UnauthorizedError
def filter_perm_sets_data(member):
perm_sets_data = {
"perms_portfolio_mgmt": bool(
member.has_permission_set(PermissionSets.EDIT_PORTFOLIO_ADMIN)
),
"perms_app_mgmt": bool(
member.has_permission_set(
PermissionSets.EDIT_PORTFOLIO_APPLICATION_MANAGEMENT
@ -33,24 +30,43 @@ def filter_perm_sets_data(member):
"perms_reporting": bool(
member.has_permission_set(PermissionSets.EDIT_PORTFOLIO_REPORTS)
),
"perms_portfolio_mgmt": bool(
member.has_permission_set(PermissionSets.EDIT_PORTFOLIO_ADMIN)
),
}
return perm_sets_data
def filter_members_data(members_list, portfolio):
def filter_members_data(members_list):
members_data = []
for member in members_list:
members_data.append(
{
"role_id": member.id,
"user_name": member.user_name,
"permission_sets": filter_perm_sets_data(member),
"status": member.display_status,
"ppoc": PermissionSets.PORTFOLIO_POC in member.permission_sets,
# add in stuff here for forms
}
permission_sets = filter_perm_sets_data(member)
ppoc = (
PermissionSets.get(PermissionSets.PORTFOLIO_POC) in member.permission_sets
)
member_data = {
"role_id": member.id,
"user_name": member.user_name,
"permission_sets": filter_perm_sets_data(member),
"status": member.display_status,
"ppoc": ppoc,
"form": member_forms.PermissionsForm(permission_sets),
}
if not ppoc:
member_data["update_invite_form"] = (
member_forms.NewForm(user_data=member.latest_invitation)
if member.latest_invitation and member.latest_invitation.can_resend
else member_forms.NewForm()
)
member_data["invite_token"] = (
member.latest_invitation.token
if member.latest_invitation and member.latest_invitation.can_resend
else None
)
members_data.append(member_data)
return sorted(members_data, key=lambda member: member["user_name"])
@ -75,7 +91,7 @@ def render_admin_page(portfolio, form=None):
"portfolios/admin.html",
form=form,
portfolio_form=portfolio_form,
members=filter_members_data(member_list, portfolio),
members=filter_members_data(member_list),
new_manager_form=member_forms.NewForm(),
assign_ppoc_form=assign_ppoc_form,
portfolio=portfolio,
@ -93,26 +109,27 @@ def admin(portfolio_id):
return render_admin_page(portfolio)
@portfolios_bp.route("/portfolios/<portfolio_id>/update_ppoc", methods=["POST"])
@user_can(Permissions.EDIT_PORTFOLIO_POC, message="update portfolio ppoc")
def update_ppoc(portfolio_id):
role_id = http_request.form.get("role_id")
portfolio = Portfolios.get(g.current_user, portfolio_id)
new_ppoc_role = PortfolioRoles.get_by_id(role_id)
PortfolioRoles.make_ppoc(portfolio_role=new_ppoc_role)
flash("primary_point_of_contact_changed", ppoc_name=new_ppoc_role.full_name)
return redirect(
url_for(
"portfolios.admin",
portfolio_id=portfolio.id,
fragment="primary-point-of-contact",
_anchor="primary-point-of-contact",
)
)
# Updating PPoC is a post-MVP feature
# @portfolios_bp.route("/portfolios/<portfolio_id>/update_ppoc", methods=["POST"])
# @user_can(Permissions.EDIT_PORTFOLIO_POC, message="update portfolio ppoc")
# def update_ppoc(portfolio_id): # pragma: no cover
# role_id = http_request.form.get("role_id")
#
# portfolio = Portfolios.get(g.current_user, portfolio_id)
# new_ppoc_role = PortfolioRoles.get_by_id(role_id)
#
# PortfolioRoles.make_ppoc(portfolio_role=new_ppoc_role)
#
# flash("primary_point_of_contact_changed", ppoc_name=new_ppoc_role.full_name)
#
# return redirect(
# url_for(
# "portfolios.admin",
# portfolio_id=portfolio.id,
# fragment="primary-point-of-contact",
# _anchor="primary-point-of-contact",
# )
# )
@portfolios_bp.route("/portfolios/<portfolio_id>/edit", methods=["POST"])
@ -166,3 +183,30 @@ def remove_member(portfolio_id, portfolio_role_id):
fragment="portfolio-members",
)
)
@portfolios_bp.route(
"/portfolios/<portfolio_id>/members/<portfolio_role_id>", methods=["POST"]
)
@user_can(Permissions.EDIT_PORTFOLIO_USERS, message="update portfolio members")
def update_member(portfolio_id, portfolio_role_id):
form_data = http_request.form
form = member_forms.PermissionsForm(formdata=form_data)
portfolio_role = PortfolioRoles.get_by_id(portfolio_role_id)
portfolio = Portfolios.get(user=g.current_user, portfolio_id=portfolio_id)
if form.validate() and portfolio.owner_role != portfolio_role:
PortfolioRoles.update(portfolio_role, form.data["permission_sets"])
flash("update_portfolio_member", member_name=portfolio_role.full_name)
return redirect(
url_for(
"portfolios.admin",
portfolio_id=portfolio_id,
_anchor="portfolio-members",
fragment="portfolio-members",
)
)
else:
flash("update_portfolio_member_error", member_name=portfolio_role.full_name)
return (render_admin_page(portfolio), 400)

View File

@ -54,13 +54,22 @@ def revoke_invitation(portfolio_id, portfolio_token):
)
@user_can(Permissions.EDIT_PORTFOLIO_USERS, message="resend invitation")
def resend_invitation(portfolio_id, portfolio_token):
invite = PortfolioInvitations.resend(g.current_user, portfolio_token)
send_portfolio_invitation(
invitee_email=invite.email,
inviter_name=g.current_user.full_name,
token=invite.token,
)
flash("resend_portfolio_invitation", user_name=invite.user_name)
form = member_forms.NewForm(http_request.form)
if form.validate():
invite = PortfolioInvitations.resend(
g.current_user, portfolio_token, form.data["user_data"]
)
send_portfolio_invitation(
invitee_email=invite.email,
inviter_name=g.current_user.full_name,
token=invite.token,
)
flash("resend_portfolio_invitation", user_name=invite.user_name)
else:
user_name = f"{form['user_data']['first_name'].data} {form['user_data']['last_name'].data}"
flash("resend_portfolio_invitation_error", user_name=user_name)
return redirect(
url_for(
"portfolios.admin",

View File

@ -128,6 +128,11 @@ MESSAGES = {
"message": "flash.portfolio_invite.resent.message",
"category": "success",
},
"resend_portfolio_invitation_error": {
"title": "flash.portfolio_invite.error.title",
"message": "flash.portfolio_invite.error.message",
"category": "error",
},
"revoked_portfolio_access": {
"title": "flash.portfolio_member.revoked.title",
"message": "flash.portfolio_member.revoked.message",
@ -153,6 +158,16 @@ MESSAGES = {
"message": "flash.task_order.submitted.message",
"category": "success",
},
"update_portfolio_member": {
"title": "flash.portfolio_member.update.title",
"message": "flash.portfolio_member.update.message",
"category": "success",
},
"update_portfolio_member_error": {
"title": "flash.portfolio_member.update_error.title",
"message": "flash.portfolio_member.update_error.message",
"category": "error",
},
"updated_application_team_settings": {
"title": "flash.success",
"message": "flash.updated_application_team_settings",

View File

@ -39,6 +39,7 @@
@import "components/sticky_cta.scss";
@import "components/error_page.scss";
@import "components/member_form.scss";
@import "components/toggle_menu.scss";
@import "sections/login";
@import "sections/home";

View File

@ -130,10 +130,6 @@
&--th {
width: 50%;
}
&--td {
position: relative;
}
}
.row {
@ -154,55 +150,6 @@
margin-right: $gap * 6;
}
}
.app-member-menu {
position: absolute;
top: $gap;
right: $gap * 2;
.accordion-table__item__toggler {
padding: $gap / 3;
border: 1px solid $color-gray-lighter;
border-radius: 3px;
cursor: pointer;
&:hover,
&--active {
background-color: $color-aqua-lightest;
}
.icon {
margin: $gap / 2;
}
}
&__toggle {
position: absolute;
right: 0;
top: 30px;
background-color: $color-white;
border: 1px solid $color-gray-light;
z-index: 1;
margin-top: 0;
a {
display: block;
padding: $gap;
border-bottom: 1px solid $color-gray-lighter;
text-decoration: none;
color: $color-black;
cursor: pointer;
&:last-child {
border-bottom: 0;
}
&:hover {
background-color: $color-aqua-lightest;
}
}
}
}
}
#add-new-env {

View File

@ -0,0 +1,58 @@
.toggle-menu {
position: absolute;
top: $gap;
right: $gap * 2;
&__container {
position: relative;
}
.accordion-table__item__toggler {
padding: $gap / 3;
border: 1px solid $color-gray-lighter;
border-radius: 3px;
cursor: pointer;
&:hover,
&--active {
background-color: $color-aqua-lightest;
}
.icon {
margin: $gap / 2;
}
}
&__toggle {
position: absolute;
right: 0;
top: 30px;
background-color: $color-white;
border: 1px solid $color-gray-light;
z-index: 1;
margin-top: 0;
a {
display: block;
padding: $gap;
border-bottom: 1px solid $color-gray-lighter;
text-decoration: none;
color: $color-black;
cursor: pointer;
white-space: nowrap;
&:last-child {
border-bottom: 0;
}
&:hover {
background-color: $color-aqua-lightest;
}
&.disabled {
color: $color-gray;
pointer-events: none;
}
}
}
}

View File

@ -5,6 +5,7 @@
{% from "components/modal.html" import Modal %}
{% from "components/multi_step_modal_form.html" import MultiStepModalForm %}
{% from "components/save_button.html" import SaveButton %}
{% from "components/toggle_menu.html" import ToggleMenu %}
{% macro MemberManagementTemplate(
application,
@ -38,16 +39,17 @@
{% call Modal(modal_name, classes="form-content--app-mem") %}
<div class="modal__form--header">
<h1>{{ Icon('avatar') }} {{ "portfolios.applications.members.form.edit_access_header" | translate({ "user": member.user_name }) }}</h1>
<hr class="full-width">
</div>
<base-form inline-template>
<form id='{{ modal_name }}' method="POST" action="{{ url_for(action_update, application_id=application.id, application_role_id=member.role_id,) }}">
{{ member.form.csrf_token }}
{{ member_fields.PermsFields(form=member.form, member_role_id=member.role_id) }}
<div class="action-group">
{{ SaveButton(text='Update', element='input', additional_classes='action-group__action') }}
<a class='action-group__action usa-button usa-button-secondary' v-on:click="closeModal('{{ modal_name }}')">{{ "common.cancel" | translate }}</a>
</div>
{{ member_form.SubmitStep(
name=modal_name,
form=member_fields.PermsFields(form=member.form, member_role_id=member.role_id),
submit_text="Update",
previous=False,
modal=modal_name,
) }}
</form>
</base-form>
{% endcall %}
@ -57,16 +59,17 @@
{% call Modal(resend_invite_modal, classes="form-content--app-mem") %}
<div class="modal__form--header">
<h1>{{ "portfolios.applications.members.new.verify" | translate }}</h1>
<hr class="full-width">
</div>
<base-form inline-template :enable-save="true">
<form id='{{ resend_invite_modal }}' method="POST" action="{{ url_for('applications.resend_invite', application_id=application.id, application_role_id=member.role_id) }}">
{{ member.update_invite_form.csrf_token }}
{{ member_fields.InfoFields(member.update_invite_form) }}
<div class="action-group">
{{ SaveButton(text="Resend Invite")}}
<a class='action-group__action' v-on:click="closeModal('{{ resend_invite_modal }}')">{{ "common.cancel" | translate }}</a>
</div>
{{ member_form.SubmitStep(
name=resend_invite_modal,
form=member_fields.InfoFields(member.update_invite_form),
submit_text="Resend Invite",
previous=False,
modal=resend_invite_modal,
) }}
</form>
</base-form>
{% endcall %}
@ -119,7 +122,7 @@
</div>
{% endfor %}
</td>
<td class="env_role--td">
<td class="toggle-menu__container">
{% for env in member.environment_roles %}
<div class="row">
<span class="env-role__environment">
@ -131,32 +134,21 @@
</div>
{% endfor %}
{% if user_can(permissions.EDIT_APPLICATION_MEMBER) -%}
<toggle-menu inline-template v-cloak>
<div class="app-member-menu">
<span v-if="isVisible" class="accordion-table__item__toggler accordion-table__item__toggler--active">
{{ Icon('ellipsis')}}
</span>
<span v-else class="accordion-table__item__toggler">
{{ Icon('ellipsis')}}
</span>
<div v-show="isVisible" class="accordion-table__item-toggle-content app-member-menu__toggle">
<a v-on:click="openModal('{{ perms_modal }}')">
{{ "portfolios.applications.members.menu.edit" | translate }}
</a>
{% if invite_pending or invite_expired -%}
{% set revoke_invite_modal = "revoke_invite_{}".format(member.role_id) %}
{% set resend_invite_modal = "resend_invite-{}".format(member.role_id) %}
<a v-on:click='openModal("{{ resend_invite_modal }}")'>
{{ "portfolios.applications.members.menu.resend" | translate }}
</a>
{% if user_can(permissions.DELETE_APPLICATION_MEMBER) -%}
<a v-on:click='openModal("{{ revoke_invite_modal }}")'>{{ 'invites.revoke' | translate }}</a>
{%- endif %}
{%- endif %}
</div>
</div>
</toggle-menu>
{% call ToggleMenu() %}
<a v-on:click="openModal('{{ perms_modal }}')">
{{ "portfolios.applications.members.menu.edit" | translate }}
</a>
{% if invite_pending or invite_expired -%}
{% set revoke_invite_modal = "revoke_invite_{}".format(member.role_id) %}
{% set resend_invite_modal = "resend_invite-{}".format(member.role_id) %}
<a v-on:click='openModal("{{ resend_invite_modal }}")'>
{{ "portfolios.applications.members.menu.resend" | translate }}
</a>
{% if user_can(permissions.DELETE_APPLICATION_MEMBER) -%}
<a v-on:click='openModal("{{ revoke_invite_modal }}")'>{{ 'invites.revoke' | translate }}</a>
{%- endif %}
{%- endif %}
{% endcall %}
{%- endif %}
</td>
</tr>

View File

@ -0,0 +1,17 @@
{% from "components/icon.html" import Icon %}
{% macro ToggleMenu() %}
<toggle-menu inline-template v-cloak>
<div class="toggle-menu">
<span v-if="isVisible" class="accordion-table__item__toggler accordion-table__item__toggler--active">
{{ Icon('ellipsis')}}
</span>
<span v-else class="accordion-table__item__toggler">
{{ Icon('ellipsis')}}
</span>
<div v-show="isVisible" class="accordion-table__item-toggle-content toggle-menu__toggle">
{{ caller() }}
</div>
</div>
</toggle-menu>
{% endmacro %}

View File

@ -59,10 +59,6 @@
<hr>
{% if user_can(permissions.VIEW_PORTFOLIO_POC) %}
{% include "portfolios/fragments/primary_point_of_contact.html" %}
{% endif %}
{% if user_can(permissions.VIEW_PORTFOLIO_USERS) %}
{% include "portfolios/fragments/portfolio_members.html" %}
{% endif %}

View File

@ -1,80 +0,0 @@
{% from "components/icon.html" import Icon %}
{% from "components/text_input.html" import TextInput %}
{% from "components/multi_step_modal_form.html" import MultiStepModalForm %}
{% from "components/alert.html" import Alert %}
{% from "components/options_input.html" import OptionsInput %}
{% set step_one %}
<hr class="full-width">
<h1>{{ "fragments.ppoc.update_ppoc_title" | translate }}</h1>
{{
Alert(
level="warning",
title=("fragments.ppoc.alert.title" | translate),
message=("fragments.ppoc.alert.message" | translate),
)
}}
<div class='form-row'>
<div class='form-col form-col--half'>
{{
OptionsInput(
assign_ppoc_form.role_id,
optional=False
)
}}
</div>
<div class='form-col form-col--half'>
</div>
</div>
<div class='action-group'>
<input
type='button'
v-on:click="next()"
v-bind:disabled="!canSave"
class='action-group__action usa-button'
value='{{ "fragments.ppoc.assign_user_button_text" | translate }}'>
<a class='action-group__action icon-link icon-link--default' v-on:click="closeModal('change-ppoc-form')">
{{ "common.cancel" | translate }}
</a>
</div>
{% endset %}
{% set step_two %}
<hr class="full-width">
<h1>{{ "fragments.ppoc.update_ppoc_confirmation_title" | translate }}</h1>
{{
Alert(
level="info",
title=("fragments.ppoc.confirm_alert.title" | translate),
)
}}
<div class='action-group'>
<input
type="submit"
class='action-group__action usa-button'
form="change-ppoc-form"
value='{{ "common.confirm" | translate }}'>
<a class='action-group__action icon-link icon-link--default' v-on:click="closeModal('change-ppoc-form')">
{{ "common.cancel" | translate }}
</a>
</div>
{% endset %}
<div class="flex-reverse-row">
{% set disable_ppoc_button = 1 == portfolio.members |length %}
<button type="button" class="usa-button usa-button-primary" v-on:click="openModal('change-ppoc-form')" {% if disable_ppoc_button %}disabled{% endif %}>
{{ "fragments.ppoc.update_btn" | translate }}
</button>
{{
MultiStepModalForm(
'change-ppoc-form',
assign_ppoc_form,
form_action=url_for("portfolios.update_ppoc", portfolio_id=portfolio.id),
steps=[step_one, step_two],
)
}}
</div>

View File

@ -5,6 +5,92 @@
{% from "components/multi_step_modal_form.html" import MultiStepModalForm %}
{% from 'components/save_button.html' import SaveButton %}
{% import "portfolios/fragments/member_form_fields.html" as member_form_fields %}
{% from "components/toggle_menu.html" import ToggleMenu %}
{% if user_can(permissions.EDIT_PORTFOLIO_USERS) -%}
{% for member in members -%}
{% if not member.ppoc -%}
{% set invite_pending = member.status == 'invite_pending' %}
{% set invite_expired = member.status == 'invite_expired' %}
{% set modal_name = "edit_member-{}".format(loop.index) %}
{% call Modal(modal_name, classes="form-content--app-mem") %}
<div class="modal__form--header">
<h1>{{ Icon('avatar') }} {{ "portfolios.applications.members.form.edit_access_header" | translate({ "user": member.user_name }) }}</h1>
</div>
<base-form inline-template>
<form id='{{ modal_name }}' method="POST" action="{{ url_for('portfolios.update_member', portfolio_id=portfolio.id, portfolio_role_id=member.role_id) }}">
{{ member.form.csrf_token }}
{{ member_form.SubmitStep(
name=modal_name,
form=member_form_fields.PermsFields(member.form, member_role_id=member.role_id),
submit_text="Save Changes",
previous=False,
modal=modal_name,
) }}
</form>
</base-form>
{% endcall %}
{% if invite_pending or invite_expired -%}
{% set resend_invite_modal = "resend_invite-{}".format(member.role_id) %}
{% call Modal(resend_invite_modal, classes="form-content--app-mem") %}
<div class="modal__form--header">
<h1>{{ "portfolios.applications.members.new.verify" | translate }}</h1>
</div>
<base-form inline-template :enable-save="true">
<form id='{{ resend_invite_modal }}' method="POST" action="{{ url_for('portfolios.resend_invitation', portfolio_id=portfolio.id, portfolio_token=member.invite_token) }}">
{{ member.update_invite_form.csrf_token }}
{{ member_form.SubmitStep(
name=resend_invite_modal,
form=member_form_fields.InfoFields(member.update_invite_form.user_data),
submit_text="Resend Invite",
previous=False,
modal=resend_invite_modal
) }}
</form>
</base-form>
{% endcall %}
{% set revoke_invite_modal = "revoke_invite-{}".format(member.role_id) %}
{% call Modal(name=revoke_invite_modal) %}
<form method="post" action="{{ url_for('portfolios.revoke_invitation', portfolio_id=portfolio.id, portfolio_token=member.invite_token) }}">
{{ member.form.csrf_token }}
<h1>{{ "invites.revoke" | translate }}</h1>
<hr class="full-width">
{{ "invites.revoke_modal_text" | translate({"application": portfolio.name}) }}
<div class="action-group">
<button class="action-group__action usa-button usa-button-primary" type="submit">{{ "invites.revoke" | translate }}</button>
<button class='action-group__action usa-button usa-button-secondary' v-on:click='closeModal("{{revoke_invite_modal}}")' type="button">{{ "common.cancel" | translate }}</button>
</div>
</form>
{% endcall %}
{% else %}
{% set remove_manager_modal = "remove_manager-{}".format(member.role_id) %}
{% call Modal(name=remove_manager_modal, dismissable=False) %}
<h1>{{ "portfolios.admin.alert_header" | translate }}</h1>
<hr class="full-width">
{{
Alert(
title="portfolios.admin.alert_title" | translate,
message="portfolios.admin.alert_message" | translate,
level="warning"
)
}}
<div class="action-group">
<form method="POST" action="{{ url_for('portfolios.remove_member', portfolio_id=portfolio.id, portfolio_role_id=member.role_id)}}">
{{ member.form.csrf_token }}
<button class="usa-button usa-button-danger">
{{ "portfolios.members.archive_button" | translate }}
</button>
</form>
<a v-on:click="closeModal('{{ modal_id }}')" class="action-group__action icon-link icon-link--default">{{ "common.cancel" | translate }}</a>
</div>
{% endcall %}
{%- endif %}
{%- endif %}
{%- endfor %}
{%- endif %}
<h3>Portfolio Managers</h3>
<div class="panel">
@ -19,6 +105,14 @@
</thead>
<tbody>
{% for member in members -%}
{% set invite_pending = member.status == 'invite_pending' %}
{% set invite_expired = member.status == 'invite_expired' %}
{% set current_user = current_member_id == member.role_id %}
{% set perms_modal = "edit_member-{}".format(loop.index) %}
{% set resend_invite_modal = "resend_invite-{}".format(member.role_id) %}
{% set revoke_invite_modal = "revoke_invite-{}".format(member.role_id) %}
{% set remove_manager_modal = "remove_manager-{}".format(member.role_id) %}
<tr>
<td>
<strong>{{ member.user_name }}{% if member.role_id == current_member_id %} (You){% endif %}</strong>
@ -28,14 +122,33 @@
{% endif %}
{{ Label(type=member.status, classes='label--below')}}
</td>
<td>
<td class="toggle-menu__container">
{% for perm, value in member.permission_sets.items() -%}
<div>
{% if value -%}
{% if value -%}
<div>
{{ ("portfolios.admin.members.{}.{}".format(perm, value)) | translate }}
{%- endif %}
</div>
</div>
{%- endif %}
{%-endfor %}
{% if user_can(permissions.EDIT_PORTFOLIO_USERS) -%}
{% call ToggleMenu() %}
<a
{% if not member.ppoc %}v-on:click="openModal('{{ perms_modal }}')"{% endif %}
class="{% if member.ppoc %}disabled{% endif %}">
Edit Permissions
</a>
{% if invite_pending or invite_expired -%}
<a v-on:click="openModal('{{ resend_invite_modal }}')">Resend Invite</a>
<a v-on:click="openModal('{{ revoke_invite_modal }}')">Revoke Invite</a>
{% else %}
<a
{% if not current_user %}v-on:click="openModal('{{ remove_manager_modal }}')"{% endif %}
class="{% if current_user %}disabled{% endif %}">
Remove Manager
</a>
{%- endif %}
{% endcall %}
{%- endif %}
</td>
</tr>
{%- endfor %}
@ -60,13 +173,13 @@
form=member_form_fields.InfoFields(new_manager_form.user_data),
next_button_text="Next: Permissions",
previous=False,
modal=new_manager_modal_name,
modal=new_manager_modal,
),
member_form.SubmitStep(
name=new_manager_modal,
form=member_form_fields.PermsFields(new_manager_form),
submit_text="Add Mananger",
modal=new_manager_modal_name,
modal=new_manager_modal,
)
],
) }}

View File

@ -1,25 +0,0 @@
<section id="primary-point-of-contact" class="panel">
<div class="panel__content">
{% if g.matchesPath("primary-point-of-contact") %}
{% include "fragments/flash.html" %}
{% endif %}
<h2>{{ "fragments.ppoc.title" | translate }}</h2>
<p>{{ "fragments.ppoc.subtitle" | translate }}</p>
<p>
<strong>
{{ portfolio.owner.first_name }}
{{ portfolio.owner.last_name }}
</strong>
<br />
{{ portfolio.owner.email }}
<br />
{{ portfolio.owner.phone_number | usPhone }}
</p>
{% if user_can(permissions.EDIT_PORTFOLIO_POC) %}
{% include "portfolios/fragments/change_ppoc.html" %}
{% endif %}
</div>
</section>

View File

@ -1,3 +1,4 @@
import pytest
from flask import url_for
from unittest.mock import MagicMock
@ -11,29 +12,6 @@ from atst.utils.localization import translate
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.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()
def test_update_portfolio_name_and_description(client, user_session):
portfolio = PortfolioFactory.create()
user_session(portfolio.owner)
@ -47,6 +25,7 @@ def test_update_portfolio_name_and_description(client, user_session):
assert portfolio.description == "a portfolio for things"
@pytest.mark.skip(reason="Out of scope for MVP")
def updating_ppoc_successfully(client, old_ppoc, new_ppoc, portfolio):
response = client.post(
url_for("portfolios.update_ppoc", portfolio_id=portfolio.id, _external=True),
@ -67,6 +46,7 @@ def updating_ppoc_successfully(client, old_ppoc, new_ppoc, portfolio):
assert Permissions.EDIT_PORTFOLIO_POC not in old_ppoc.permissions
@pytest.mark.skip(reason="Out of scope for MVP")
def test_update_ppoc_no_user_id_specified(client, user_session):
portfolio = PortfolioFactory.create()
@ -80,6 +60,7 @@ def test_update_ppoc_no_user_id_specified(client, user_session):
assert response.status_code == 404
@pytest.mark.skip(reason="Out of scope for MVP")
def test_update_ppoc_to_member_not_on_portfolio(client, user_session):
portfolio = PortfolioFactory.create()
original_ppoc = portfolio.owner
@ -97,6 +78,7 @@ def test_update_ppoc_to_member_not_on_portfolio(client, user_session):
assert portfolio.owner.id == original_ppoc.id
@pytest.mark.skip(reason="Out of scope for MVP")
def test_update_ppoc_when_ppoc(client, user_session):
portfolio = PortfolioFactory.create()
original_ppoc = portfolio.owner_role
@ -113,7 +95,8 @@ def test_update_ppoc_when_ppoc(client, user_session):
)
def test_update_ppoc_when_cpo(client, user_session):
@pytest.mark.skip(reason="Out of scope for MVP")
def test_update_ppoc_when_ccpo(client, user_session):
ccpo = UserFactory.create_ccpo()
portfolio = PortfolioFactory.create()
original_ppoc = portfolio.owner_role
@ -130,6 +113,7 @@ def test_update_ppoc_when_cpo(client, user_session):
)
@pytest.mark.skip(reason="Out of scope for MVP")
def test_update_ppoc_when_not_ppoc(client, user_session):
portfolio = PortfolioFactory.create()
new_owner = UserFactory.create()
@ -145,15 +129,6 @@ def test_update_ppoc_when_not_ppoc(client, user_session):
assert response.status_code == 404
def test_portfolio_admin_screen_when_ppoc(client, user_session):
portfolio = PortfolioFactory.create()
user_session(portfolio.owner)
response = client.get(url_for("portfolios.admin", portfolio_id=portfolio.id))
assert response.status_code == 200
assert portfolio.name in response.data.decode()
assert translate("fragments.ppoc.update_btn").encode("utf8") in response.data
def test_portfolio_admin_screen_when_not_ppoc(client, user_session):
portfolio = PortfolioFactory.create()
user = UserFactory.create()
@ -254,3 +229,54 @@ def test_remove_portfolio_member_ppoc(client, user_session):
PortfolioRoles.get(portfolio_id=portfolio.id, user_id=portfolio.owner.id).status
== PortfolioRoleStatus.ACTIVE
)
def test_portfolios_update_member(client, user_session):
portfolio = PortfolioFactory.create()
portfolio_role = PortfolioRoleFactory.create(
portfolio=portfolio,
permission_sets=[PermissionSets.get(PermissionSets.EDIT_PORTFOLIO_ADMIN)],
)
form_data = {
"perms_app_mgmt": "y",
}
user_session(portfolio.owner)
response = client.post(
url_for(
"portfolios.update_member",
portfolio_id=portfolio.id,
portfolio_role_id=portfolio_role.id,
),
data=form_data,
follow_redirects=False,
)
assert response.status_code == 302
assert portfolio_role.has_permission_set(
PermissionSets.EDIT_PORTFOLIO_APPLICATION_MANAGEMENT
)
assert not portfolio_role.has_permission_set(PermissionSets.EDIT_PORTFOLIO_ADMIN)
def test_can_not_update_ppoc_permissions(client, user_session):
portfolio = PortfolioFactory.create()
owner = portfolio.owner
form_data = {
"perms_app_mgmt": "y",
}
user_session(owner)
response = client.post(
url_for(
"portfolios.update_member",
portfolio_id=portfolio.id,
portfolio_role_id=portfolio.owner_role.id,
),
data=form_data,
follow_redirects=False,
)
assert response.status_code == 400

View File

@ -219,11 +219,11 @@ def test_resend_invitation_sends_email(monkeypatch, client, user_session):
monkeypatch.setattr("atst.jobs.send_mail.delay", job_mock)
user = UserFactory.create()
portfolio = PortfolioFactory.create()
ws_role = PortfolioRoleFactory.create(
portfolio_role = PortfolioRoleFactory.create(
user=user, portfolio=portfolio, status=PortfolioRoleStatus.PENDING
)
invite = PortfolioInvitationFactory.create(
user_id=user.id, role=ws_role, status=InvitationStatus.PENDING
user_id=user.id, role=portfolio_role, status=InvitationStatus.PENDING
)
user_session(portfolio.owner)
client.post(
@ -231,48 +231,23 @@ def test_resend_invitation_sends_email(monkeypatch, client, user_session):
"portfolios.resend_invitation",
portfolio_id=portfolio.id,
portfolio_token=invite.token,
)
),
data={
"user_data-dod_id": user.dod_id,
"user_data-first_name": user.first_name,
"user_data-last_name": user.last_name,
"user_data-email": user.email,
},
)
assert job_mock.called
def test_existing_member_invite_resent_to_email_submitted_in_form(
monkeypatch, client, user_session
):
job_mock = Mock()
monkeypatch.setattr("atst.jobs.send_mail.delay", job_mock)
portfolio = PortfolioFactory.create()
user = UserFactory.create()
ws_role = PortfolioRoleFactory.create(
user=user, portfolio=portfolio, status=PortfolioRoleStatus.PENDING
)
invite = PortfolioInvitationFactory.create(
user_id=user.id,
role=ws_role,
status=InvitationStatus.PENDING,
email="example@example.com",
)
user_session(portfolio.owner)
client.post(
url_for(
"portfolios.resend_invitation",
portfolio_id=portfolio.id,
portfolio_token=invite.token,
)
)
assert user.email != "example@example.com"
ordered_args, _unordered_args = job_mock.call_args
recipients, _subject, _message = ordered_args
assert recipients[0] == "example@example.com"
_DEFAULT_PERMS_FORM_DATA = {
"permission_sets-perms_app_mgmt": False,
"permission_sets-perms_funding": False,
"permission_sets-perms_reporting": False,
"permission_sets-perms_portfolio_mgmt": False,
"permission_sets-perms_app_mgmt": "n",
"permission_sets-perms_funding": "n",
"permission_sets-perms_reporting": "n",
"permission_sets-perms_portfolio_mgmt": "n",
}

View File

@ -373,6 +373,24 @@ def test_portfolios_edit_access(post_url_assert_status):
post_url_assert_status(rando, url, 404)
# portfolios.update_member
def test_portfolios_update_member_access(post_url_assert_status):
ccpo = user_with(PermissionSets.EDIT_PORTFOLIO_ADMIN)
owner = user_with()
rando = user_with()
portfolio = PortfolioFactory.create(owner=owner)
portfolio_role = PortfolioRoleFactory.create(portfolio=portfolio)
url = url_for(
"portfolios.update_member",
portfolio_id=portfolio.id,
portfolio_role_id=portfolio_role.id,
)
post_url_assert_status(ccpo, url, 302)
post_url_assert_status(owner, url, 302)
post_url_assert_status(rando, url, 404)
# applications.new
def test_applications_new_access(get_url_assert_status):
ccpo = user_with(PermissionSets.EDIT_PORTFOLIO_APPLICATION_MANAGEMENT)

View File

@ -169,10 +169,19 @@ flash:
revoked:
title: Removed portfolio access
message: Portfolio access successfully removed from {member_name}.
update:
title: Success!
message: You have successfully updated access permissions for {member_name}.
update_error:
title: Permissions for {member_name} could not be updated
message: An unexpected problem occurred with your request, please try again. If the problem persists, contact an administrator.
portfolio_invite:
resent:
title: Invitation resent
message: Successfully sent a new invitation to {user_name}.
error:
title: Portfolio invitation error
message: There was an error processing the invitation for {user_name}.
session_expired:
title: Session Expired
message: Your session expired due to inactivity. Please log in again to continue.