Merge branch 'staging' into topbar-styling
This commit is contained in:
commit
0f52e75a4e
@ -152,7 +152,7 @@
|
||||
"hashed_secret": "e4f14805dfd1e6af030359090c535e149e6b4207",
|
||||
"is_secret": false,
|
||||
"is_verified": false,
|
||||
"line_number": 665,
|
||||
"line_number": 649,
|
||||
"type": "Hex High Entropy String"
|
||||
}
|
||||
]
|
||||
|
@ -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)
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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";
|
||||
|
@ -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 {
|
||||
|
58
styles/components/_toggle_menu.scss
Normal file
58
styles/components/_toggle_menu.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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>
|
||||
|
17
templates/components/toggle_menu.html
Normal file
17
templates/components/toggle_menu.html
Normal 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 %}
|
@ -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 %}
|
||||
|
@ -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>
|
@ -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,
|
||||
)
|
||||
],
|
||||
) }}
|
||||
|
@ -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>
|
@ -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
|
||||
|
@ -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",
|
||||
}
|
||||
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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.
|
||||
|
Loading…
x
Reference in New Issue
Block a user