Merge pull request #1294 from dod-ccpo/portfolio-admin-styling__part-2

Portfolio admin styling - Managers table
This commit is contained in:
leigh-mil 2020-01-10 15:46:34 -05:00 committed by GitHub
commit 7de2f440c6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 378 additions and 788 deletions

View File

@ -75,10 +75,10 @@ class Portfolios(object):
permission_sets = PortfolioRoles._permission_sets_for_names(
member_data.get("permission_sets", [])
)
role = PortfolioRole(portfolio_id=portfolio.id, permission_sets=permission_sets)
role = PortfolioRole(portfolio=portfolio, permission_sets=permission_sets)
invitation = PortfolioInvitations.create(
inviter=inviter, role=role, member_data=member_data
inviter=inviter, role=role, member_data=member_data["user_data"]
)
PortfoliosQuery.add_and_commit(role)

View File

@ -1,76 +1,59 @@
from wtforms.validators import Required
from wtforms.fields import StringField, FormField, FieldList, HiddenField
from wtforms.fields import BooleanField, FormField
from atst.domain.permission_sets import PermissionSets
from .forms import BaseForm
from .member import NewForm as BaseNewMemberForm
from atst.domain.permission_sets import PermissionSets
from atst.forms.fields import SelectField
from atst.utils.localization import translate
class PermissionsForm(BaseForm):
member_name = StringField()
member_id = HiddenField()
perms_app_mgmt = SelectField(
translate("forms.new_member.app_mgmt"),
choices=[
(
PermissionSets.VIEW_PORTFOLIO_APPLICATION_MANAGEMENT,
translate("common.view"),
),
(
PermissionSets.EDIT_PORTFOLIO_APPLICATION_MANAGEMENT,
translate("common.edit"),
),
],
perms_app_mgmt = BooleanField(
translate("forms.new_member.app_mgmt.label"),
default=False,
description=translate("forms.new_member.app_mgmt.description"),
)
perms_funding = SelectField(
translate("forms.new_member.funding"),
choices=[
(PermissionSets.VIEW_PORTFOLIO_FUNDING, translate("common.view")),
(PermissionSets.EDIT_PORTFOLIO_FUNDING, translate("common.edit")),
],
perms_funding = BooleanField(
translate("forms.new_member.funding.label"),
default=False,
description=translate("forms.new_member.funding.description"),
)
perms_reporting = SelectField(
translate("forms.new_member.reporting"),
choices=[
(PermissionSets.VIEW_PORTFOLIO_REPORTS, translate("common.view")),
(PermissionSets.EDIT_PORTFOLIO_REPORTS, translate("common.edit")),
],
perms_reporting = BooleanField(
translate("forms.new_member.reporting.label"),
default=False,
description=translate("forms.new_member.reporting.description"),
)
perms_portfolio_mgmt = SelectField(
translate("forms.new_member.portfolio_mgmt"),
choices=[
(PermissionSets.VIEW_PORTFOLIO_ADMIN, translate("common.view")),
(PermissionSets.EDIT_PORTFOLIO_ADMIN, translate("common.edit")),
],
perms_portfolio_mgmt = BooleanField(
translate("forms.new_member.portfolio_mgmt.label"),
default=False,
description=translate("forms.new_member.portfolio_mgmt.description"),
)
@property
def data(self):
_data = super().data
_data["permission_sets"] = []
for field in _data:
if "perms" in field:
_data["permission_sets"].append(_data[field])
_data.pop("csrf_token", None)
perm_sets = []
if _data["perms_app_mgmt"]:
perm_sets.append(PermissionSets.EDIT_PORTFOLIO_APPLICATION_MANAGEMENT)
if _data["perms_funding"]:
perm_sets.append(PermissionSets.EDIT_PORTFOLIO_FUNDING)
if _data["perms_reporting"]:
perm_sets.append(PermissionSets.EDIT_PORTFOLIO_REPORTS)
if _data["perms_portfolio_mgmt"]:
perm_sets.append(PermissionSets.EDIT_PORTFOLIO_ADMIN)
_data["permission_sets"] = perm_sets
return _data
class MembersPermissionsForm(BaseForm):
members_permissions = FieldList(FormField(PermissionsForm))
class NewForm(BaseForm):
class NewForm(PermissionsForm):
user_data = FormField(BaseNewMemberForm)
permission_sets = FormField(PermissionsForm)
@property
def update_data(self):
return {
"permission_sets": self.data.get("permission_sets").get("permission_sets"),
**self.data.get("user_data"),
}
class AssignPPOCForm(PermissionsForm):

View File

@ -12,17 +12,6 @@ from atst.utils import first_or_none
from atst.models.mixins.auditable import record_permission_sets_updates
MEMBER_STATUSES = {
"active": "Active",
"revoked": "Invite revoked",
"expired": "Invite expired",
"error": "Error on invite",
"pending": "Pending",
"unknown": "Unknown errors",
"disabled": "Disabled",
}
class Status(Enum):
ACTIVE = "active"
DISABLED = "disabled"
@ -90,23 +79,23 @@ class PortfolioRole(
@property
def display_status(self):
if self.status == Status.ACTIVE:
return MEMBER_STATUSES["active"]
return "active"
elif self.status == Status.DISABLED:
return MEMBER_STATUSES["disabled"]
return "disabled"
elif self.latest_invitation:
if self.latest_invitation.is_revoked:
return MEMBER_STATUSES["revoked"]
return "invite_revoked"
elif self.latest_invitation.is_rejected_wrong_user:
return MEMBER_STATUSES["error"]
return "invite_error"
elif (
self.latest_invitation.is_rejected_expired
or self.latest_invitation.is_expired
):
return MEMBER_STATUSES["expired"]
return "invite_expired"
else:
return MEMBER_STATUSES["pending"]
return "invite_pending"
else:
return MEMBER_STATUSES["unknown"]
return "unknown"
def has_permission_set(self, perm_set_name):
return first_or_none(

View File

@ -17,63 +17,51 @@ from atst.utils.flash import formatted_flash as flash
from atst.domain.exceptions import UnauthorizedError
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 {
"member_name": member.full_name,
"member_id": member.id,
"perms_app_mgmt": permission_str(
member,
PermissionSets.EDIT_PORTFOLIO_APPLICATION_MANAGEMENT,
PermissionSets.VIEW_PORTFOLIO_APPLICATION_MANAGEMENT,
def filter_perm_sets_data(member):
perm_sets_data = {
"perms_portfolio_mgmt": bool(
member.has_permission_set(PermissionSets.EDIT_PORTFOLIO_ADMIN)
),
"perms_funding": permission_str(
member,
PermissionSets.EDIT_PORTFOLIO_FUNDING,
PermissionSets.VIEW_PORTFOLIO_FUNDING,
"perms_app_mgmt": bool(
member.has_permission_set(
PermissionSets.EDIT_PORTFOLIO_APPLICATION_MANAGEMENT
)
),
"perms_reporting": permission_str(
member,
PermissionSets.EDIT_PORTFOLIO_REPORTS,
PermissionSets.VIEW_PORTFOLIO_REPORTS,
"perms_funding": bool(
member.has_permission_set(PermissionSets.EDIT_PORTFOLIO_FUNDING)
),
"perms_portfolio_mgmt": permission_str(
member,
PermissionSets.EDIT_PORTFOLIO_ADMIN,
PermissionSets.VIEW_PORTFOLIO_ADMIN,
"perms_reporting": bool(
member.has_permission_set(PermissionSets.EDIT_PORTFOLIO_REPORTS)
),
}
return perm_sets_data
def get_members_data(portfolio):
members = sorted(
[serialize_member_form_data(member) for member in portfolio.members],
key=lambda member: member["member_name"],
def filter_members_data(members_list, portfolio):
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
}
)
for member in members:
if member["member_id"] == portfolio.owner_role.id:
ppoc = member
members.remove(member)
members.insert(0, ppoc)
return members
return sorted(members_data, key=lambda member: member["user_name"])
def render_admin_page(portfolio, form=None):
pagination_opts = Paginator.get_pagination_opts(http_request)
audit_events = AuditLog.get_portfolio_events(portfolio, pagination_opts)
members_data = get_members_data(portfolio)
portfolio_form = PortfolioForm(obj=portfolio)
member_perms_form = member_forms.MembersPermissionsForm(
data={"members_permissions": members_data}
)
member_list = portfolio.members
assign_ppoc_form = member_forms.AssignPPOCForm()
for pf_role in portfolio.roles:
if pf_role.user != portfolio.owner and pf_role.is_active:
assign_ppoc_form.role_id.choices += [(pf_role.id, pf_role.full_name)]
@ -87,13 +75,12 @@ def render_admin_page(portfolio, form=None):
"portfolios/admin.html",
form=form,
portfolio_form=portfolio_form,
member_perms_form=member_perms_form,
member_form=member_forms.NewForm(),
members=filter_members_data(member_list, portfolio),
new_manager_form=member_forms.NewForm(),
assign_ppoc_form=assign_ppoc_form,
portfolio=portfolio,
audit_events=audit_events,
user=g.current_user,
ppoc_id=members_data[0].get("member_id"),
current_member_id=current_member_id,
applications_count=len(portfolio.applications),
)
@ -106,34 +93,6 @@ def admin(portfolio_id):
return render_admin_page(portfolio)
@portfolios_bp.route("/portfolios/<portfolio_id>/admin", methods=["POST"])
@user_can(Permissions.EDIT_PORTFOLIO_USERS, message="view portfolio admin page")
def edit_members(portfolio_id):
portfolio = Portfolios.get_for_update(portfolio_id)
member_perms_form = member_forms.MembersPermissionsForm(http_request.form)
if member_perms_form.validate():
for subform in member_perms_form.members_permissions:
member_id = subform.member_id.data
member = PortfolioRoles.get_by_id(member_id)
if member is not portfolio.owner_role:
new_perm_set = subform.data["permission_sets"]
PortfolioRoles.update(member, new_perm_set)
flash("update_portfolio_members", portfolio=portfolio)
return redirect(
url_for(
"portfolios.admin",
portfolio_id=portfolio_id,
fragment="portfolio-members",
_anchor="portfolio-members",
)
)
else:
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):

View File

@ -79,7 +79,7 @@ def invite_member(portfolio_id):
if form.validate():
try:
invite = Portfolios.invite(portfolio, g.current_user, form.update_data)
invite = Portfolios.invite(portfolio, g.current_user, form.data)
send_portfolio_invitation(
invitee_email=invite.email,
inviter_name=g.current_user.full_name,

View File

@ -159,7 +159,7 @@ def get_users():
def add_members_to_portfolio(portfolio):
for user_data in PORTFOLIO_USERS:
invite = Portfolios.invite(portfolio, portfolio.owner, user_data)
invite = Portfolios.invite(portfolio, portfolio.owner, {"user_data": user_data})
profile = {
k: user_data[k] for k in user_data if k not in ["dod_id", "permission_sets"]
}

View File

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

View File

@ -0,0 +1,61 @@
.member-form {
text-align: left;
input[type="checkbox"] + label::before {
margin-left: 0;
}
.input__inline-fields {
text-align: left;
.usa-input__choices label {
font-weight: $font-bold;
}
}
.input__inline-fields {
padding: $gap * 2;
border: 1px solid $color-gray-lighter;
&.checked {
border: 1px solid $color-blue;
}
label {
font-weight: $font-bold;
}
p.usa-input__help {
margin-bottom: 0;
padding-left: 3rem;
}
}
.user-info {
.usa-input {
width: 45rem;
input,
label,
.usa-input__message {
max-width: unset;
}
label .icon-validation {
left: unset;
right: -$gap * 4;
}
&--validation--phoneExt {
width: 18rem;
}
}
}
}
#modal--add-app-mem,
#modal--add-portfolio-manager {
.modal__body {
min-width: 75rem;
}
}

View File

@ -5,13 +5,6 @@
}
margin-left: 2 * $gap;
.line {
box-sizing: border-box;
height: 2px;
width: 100%;
border: 1px solid $color-gray-lightest;
}
}
.portfolio-header {
@ -40,36 +33,6 @@
}
}
&__budget {
font-size: $small-font-size;
align-items: center;
.icon-tooltip {
margin-left: -$gap / 2;
}
button {
margin: 0;
padding: 0;
}
&--dollars {
font-size: $h2-font-size;
font-weight: bold;
}
&--amount {
white-space: nowrap;
}
&--cents {
font-size: 2rem;
margin-top: 0.75rem;
margin-left: -0.7rem;
font-weight: bold;
}
}
.links {
justify-content: center;
font-size: $small-font-size;
@ -109,22 +72,6 @@
}
}
}
.column-left {
width: 12.5rem;
float: left;
}
.column-right {
margin-left: -0.4rem;
}
.unfunded {
color: $color-red;
.icon {
@include icon-color($color-red);
}
}
}
@mixin subheading {
@ -138,6 +85,10 @@
.portfolio-content {
margin: (4 * $gap) $gap 0 $gap;
.panel {
padding-bottom: 2rem;
}
a.add-new-button {
display: inherit;
margin-left: auto;
@ -157,44 +108,6 @@
}
}
input.usa-button.usa-button-primary {
width: 9rem;
height: 4rem;
}
select {
padding-left: 1.2rem;
}
.members-table-ppoc {
select::-ms-expand {
display: none;
color: $color-gray;
}
select {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
display: block;
width: 100%;
float: right;
margin: 5px 0px;
padding: 0px 24px;
background-image: none;
-ms-word-break: normal;
word-break: normal;
padding-right: 3rem;
padding-left: 1.2rem;
color: $color-gray;
}
select:hover {
box-shadow: none;
color: $color-gray;
}
}
a.modal-link.icon-link {
float: right;

View File

@ -12,6 +12,7 @@
.usa-button,
a {
margin: 0 0 0 $gap;
cursor: pointer;
@include media($medium-screen) {
margin: 0 0 0 ($gap * 2);

View File

@ -21,24 +21,8 @@ table.atat-table {
text-align: right;
}
&--align-center {
text-align: center;
}
&--shrink {
width: 1%;
}
&--expand {
width: 100%;
}
&--hide-small {
display: none;
@include media($medium-screen) {
display: table-cell;
}
&--third {
width: 33%;
}
}
}
@ -83,28 +67,6 @@ table.atat-table {
@include panel-margin;
&__header {
@include panel-base;
@include panel-theme-default;
border-top: none;
border-bottom: 0;
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: space-between;
align-items: center;
padding: $gap * 2;
&__title {
@include h4;
font-size: $lead-font-size;
justify-content: space-between;
flex: 2;
}
}
table {
margin-bottom: 0;
}

View File

@ -23,66 +23,6 @@
}
}
#modal--add-app-mem,
.form-content--app-mem {
text-align: left;
.modal__body {
min-width: 75rem;
}
input[type="checkbox"] + label::before {
margin-left: 0;
}
.input__inline-fields {
text-align: left;
.usa-input__choices label {
font-weight: $font-bold;
}
}
.input__inline-fields {
padding: $gap * 2;
border: 1px solid $color-gray-lighter;
&.checked {
border: 1px solid $color-blue;
}
label {
font-weight: $font-bold;
}
p.usa-input__help {
margin-bottom: 0;
padding-left: 3rem;
}
}
.application-member__user-info {
.usa-input {
width: 45rem;
input,
label,
.usa-input__message {
max-width: unset;
}
label .icon-validation {
left: unset;
right: -$gap * 4;
}
&--validation--phoneExt {
width: 18rem;
}
}
}
}
.environment-roles {
padding: 0 ($gap * 3) ($gap * 3);

View File

@ -118,7 +118,7 @@
{% endmacro %}
{% macro InfoFields(member_form) %}
<div class="application-member__user-info">
<div class="user-info">
{{ TextInput(member_form.first_name, validation='requiredField', optional=False) }}
{{ TextInput(member_form.last_name, validation='requiredField', optional=False) }}
{{ TextInput(member_form.email, validation='email', optional=False) }}

View File

@ -1,12 +1,10 @@
{% from "components/alert.html" import Alert %}
{% from "components/icon.html" import Icon %}
{% from "components/label.html" import Label %}
{% import "applications/fragments/new_member_modal_content.html" as member_steps %}
{% import "components/member_form.html" as member_form %}
{% import "applications/fragments/member_form_fields.html" as member_fields %}
{% 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_list.html" import ToggleButton, ToggleSection %}
{% macro MemberManagementTemplate(
application,
@ -179,8 +177,19 @@
form=new_member_form,
form_action=url_for(action_new, application_id=application.id),
steps=[
member_steps.MemberStepOne(new_member_form),
member_steps.MemberStepTwo(new_member_form, application)
member_form.BasicStep(
title="portfolios.applications.members.form.add_member"|translate,
form=member_fields.InfoFields(new_member_form.user_data),
next_button_text="portfolios.applications.members.form.next_button"|translate,
previous=False,
modal=new_member_modal_name,
),
member_form.SubmitStep(
name=new_member_modal_name,
form=member_fields.PermsFields(form=new_member_form, new=True),
submit_text="portfolios.applications.members.form.add_member"|translate,
modal=new_member_modal_name,
)
],
) }}
{% endif %}

View File

@ -1,50 +0,0 @@
{% from "components/icon.html" import Icon %}
{% import "applications/fragments/member_form_fields.html" as member_fields %}
{% macro MemberFormTemplate(title=None, next_button=None, previous=True) %}
<hr class="full-width">
{% if title %} <h1>{{ title }}</h1> {% endif %}
{{ caller() }}
<div class='action-group'>
{{ next_button }}
{% if previous %}
<input
type='button'
v-on:click="previous()"
class='action-group__action usa-button usa-button-secondary'
value='{{ "common.previous" | translate }}'>
{% endif %}
<a class='action-group__action' v-on:click="closeModal('{{ new_port_mem }}')">{{ "common.cancel" | translate }}</a>
</div>
{% endmacro %}
{% macro MemberStepOne(member_form) %}
{% set next_button %}
<input
type='button'
v-on:click="next()"
v-bind:disabled="!canSave"
class='action-group__action usa-button'
value='{{ "portfolios.applications.members.form.next_button" | translate }}'>
{% endset %}
{% call MemberFormTemplate(title="portfolios.applications.members.form.add_member"|translate, next_button=next_button, previous=False) %}
{{ member_fields.InfoFields(member_form.user_data) }}
{% endcall %}
{% endmacro %}
{% macro MemberStepTwo(member_form, application) %}
{% set next_button %}
<input
type="submit"
class='action-group__action usa-button'
form="add-app-mem"
v-bind:disabled="!canSave"
value='{{ "portfolios.applications.members.form.add_member" | translate}}'>
{% endset %}
{% call MemberFormTemplate(next_button=next_button) %}
{{ member_fields.PermsFields(form=member_form, new=True) }}
{% endcall %}
{% endmacro %}

View File

@ -9,11 +9,15 @@
"text": "changes pending",
"color": "default",
},
"ppoc": {"text": "primary point of contact"}
} %}
{% if type -%}
{% if type in label_info.keys() -%}
<span class='label label--{{ label_info[type]["color"] }} {{ classes }}'>
{{ Icon(label_info[type]["icon"]) }} {{ label_info[type]["text"] }}
{% if label_info[type]["icon"] %}
{{ Icon(label_info[type]["icon"]) }}
{% endif %}
{{ label_info[type]["text"] }}
</span>
{%- endif %}
{%- endmacro %}

View File

@ -0,0 +1,65 @@
<!-- Layout macro -->
{% macro MemberForm(title=None, next_button=None, previous=True, modal=modal) %}
<div class="member-form">
<hr class="full-width">
{% if title %} <h2>{{ title }}</h2> {% endif %}
{{ caller() }}
</div>
<div class='action-group'>
{{ next_button }}
{% if previous %}
<input
type='button'
v-on:click="previous()"
class='action-group__action usa-button usa-button-secondary'
value='{{ "common.previous" | translate }}'>
{% endif %}
<a class='action-group__action' v-on:click="closeModal('{{ modal }}')">{{ "common.cancel" | translate }}</a>
</div>
{% endmacro %}
<!-- Step macros to use with MultiStepModalForm -->
{% macro BasicStep(
title=None,
form=form,
next_button_text=next_button_text,
previous=True,
modal=modal
) %}
{% set next_button %}
<input
type='button'
v-on:click="next()"
v-bind:disabled="!canSave"
class='action-group__action usa-button'
value='{{ next_button_text }}'>
{% endset %}
{% call MemberForm(title=title, next_button=next_button, previous=previous, modal=modal) %}
{{ form }}
{% endcall %}
{% endmacro %}
{% macro SubmitStep(
name=name,
title=None,
form=form,
submit_text=submit_text,
previous=True,
modal=modal
) %}
{% set next_button %}
<input
type="submit"
class='action-group__action usa-button'
form="{{ name }}"
v-bind:disabled="!canSave"
value='{{ submit_text }}'>
{% endset %}
{% call MemberForm(title=title, next_button=next_button, previous=previous, modal=modal) %}
{{ form }}
{% endcall %}
{% endmacro %}

View File

@ -1,5 +1,6 @@
{% extends "portfolios/base.html" %}
{% from "components/label.html" import Label %}
{% from "components/pagination.html" import Pagination %}
{% from 'components/save_button.html' import SaveButton %}
{% from 'components/sticky_cta.html' import StickyCTA %}
@ -9,8 +10,8 @@
{{ StickyCTA(text="Settings") }}
<div v-cloak class="portfolio-admin portfolio-content">
<div v-cloak class="portfolio-admin">
{% include "fragments/flash.html" %}
<!-- max width of this section is 460px -->
<section class="form-container__half">
<h3>Portfolio name and component</h3>

View File

@ -1,79 +0,0 @@
{% from "components/icon.html" import Icon %}
{% from "components/text_input.html" import TextInput %}
{% from "components/multi_step_modal_form.html" import MultiStepModalForm %}
{% macro SimpleOptionsInput(field) %}
<div class="usa-input">
<fieldset data-ally-disabled="true" class="usa-input__choices">
<legend>
<div class="usa-input__title-inline">
{{ field.label | striptags}}
</div>
</legend>
{{ field() }}
</fieldset>
</div>
{% endmacro %}
{% set step_one %}
<hr class="full-width">
<h1>Invite new portfolio member</h1>
<div class='form-row'>
<div class='form-col form-col--half'>
{{ TextInput(member_form.user_data.first_name, validation='requiredField', optional=False) }}
</div>
<div class='form-col form-col--half'>
{{ TextInput(member_form.user_data.last_name, validation='requiredField', optional=False) }}
</div>
</div>
<div class='form-row'>
<div class='form-col form-col--half'>
{{ TextInput(member_form.user_data.email, validation='email', optional=False) }}
</div>
<div class='form-col form-col--half'>
{{ TextInput(member_form.user_data.phone_number, validation='usPhone') }}
</div>
</div>
<div class='form-row'>
<div class='form-col form-col--half'>
{{ TextInput(member_form.user_data.dod_id, validation='dodId', 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 usa-button-primary'
value='Next'>
<a class='action-group__action' v-on:click="closeModal('{{ new_port_mem }}')">Cancel</a>
</div>
{% endset %}
{% set step_two %}
<hr class="full-width">
<h1>Assign member permissions</h1>
<a class='icon-link'>
{{ Icon('info') }}
{{ "portfolios.admin.permissions_info" | translate }}
</a>
{{ SimpleOptionsInput(member_form.permission_sets.perms_app_mgmt) }}
{{ SimpleOptionsInput(member_form.permission_sets.perms_funding) }}
{{ SimpleOptionsInput(member_form.permission_sets.perms_reporting) }}
{{ SimpleOptionsInput(member_form.permission_sets.perms_portfolio_mgmt) }}
<div class='action-group'>
<input
type="submit"
class='action-group__action usa-button usa-button-primary'
form="add-port-mem"
value='Invite member'>
<a class='action-group__action' v-on:click="closeModal('{{ new_port_mem }}')">Cancel</a>
</div>
{% endset %}
{{ MultiStepModalForm(
'add-port-mem',
member_form,
url_for("portfolios.invite_member", portfolio_id=portfolio.id),
[step_one, step_two],
) }}

View File

@ -0,0 +1,37 @@
{% from "components/checkbox_input.html" import CheckboxInput %}
{% from "components/icon.html" import Icon %}
{% from "components/phone_input.html" import PhoneInput %}
{% from "components/text_input.html" import TextInput %}
{% macro PermsFields(form, member_role_id=None) %}
<h2>Set Portfolio Permissions</h2>
<div class="portfolio-perms">
{% if new %}
{% set app_mgmt = form.perms_app_mgmt.name %}
{% set funding = form.perms_funding.name %}
{% set reporting = form.perms_reporting.name %}
{% set portfolio_mgmt = form.perms_portfolio_mgmt.name %}
{% else %}
{% set app_mgmt = "perms_app_mgmt-{}".format(member_role_id) %}
{% set funding = "perms_funding-{}".format(member_role_id) %}
{% set reporting = "perms_reporting-{}".format(member_role_id) %}
{% set portfolio_mgmt = "perms_portfolio_mgmt-{}".format(member_role_id) %}
{% endif %}
{{ CheckboxInput(form.perms_app_mgmt, classes="input__inline-fields", key=app_mgmt, id=app_mgmt, optional=True) }}
{{ CheckboxInput(form.perms_funding, classes="input__inline-fields", key=funding, id=funding, optional=True) }}
{{ CheckboxInput(form.perms_reporting, classes="input__inline-fields", key=reporting, id=reporting, optional=True) }}
{{ CheckboxInput(form.perms_portfolio_mgmt, classes="input__inline-fields", key=portfolio_mgmt, id=portfolio_mgmt, optional=True) }}
</div>
{% endmacro %}
{% macro InfoFields(member_form) %}
<div class="user-info">
{{ TextInput(member_form.first_name, validation='requiredField', optional=False) }}
{{ TextInput(member_form.last_name, validation='requiredField', optional=False) }}
{{ TextInput(member_form.email, validation='email', optional=False) }}
{{ PhoneInput(member_form.phone_number, member_form.phone_ext)}}
{{ TextInput(member_form.dod_id, validation='dodId', optional=False) }}
<a href="#">How do I find the DoD ID?</a>
</div>
{% endmacro %}

View File

@ -1,38 +0,0 @@
{% from "components/alert.html" import Alert %}
{% from "components/modal.html" import Modal %}
{% from "components/options_input.html" import OptionsInput %}
{% for subform in member_perms_form.members_permissions %}
{% set modal_id = "portfolio_id_{}_user_id_{}".format(portfolio.id, subform.member_id.data) %}
{% set ppoc = subform.member_id.data == ppoc_id %}
{% set archive_button_class = 'button-danger-outline' %}
<tr {% if ppoc %}class="members-table-ppoc"{% endif %}>
<td class='name'>{{ subform.member_name.data }}
<div>
{% if ppoc %}
{% set archive_button_class = 'usa-button-disabled' %}
<span class='you'>PPoC</span>
{% endif %}
{% if subform.member_id.data == current_member_id %}
{% set archive_button_class = 'usa-button-disabled' %}
<span class='you'>(<span class='green'>you</span>)</span>
{% endif %}
</div>
</td>
<td>{{ OptionsInput(subform.perms_app_mgmt, label=False, disabled=ppoc) }}</td>
<td>{{ OptionsInput(subform.perms_funding, label=False, disabled=ppoc) }}</td>
<td>{{ OptionsInput(subform.perms_reporting, label=False, disabled=ppoc) }}</td>
<td>{{ OptionsInput(subform.perms_portfolio_mgmt, label=False, disabled=ppoc) }}</td>
<td>
<a v-on:click="openModal('{{ modal_id }}')" class='usa-button {{ archive_button_class }}'>
{{ "portfolios.members.archive_button" | translate }}
</a>
{% if not ppoc %}
{{ subform.member_id() }}
{% endif %}
</td>
</tr>
{% endfor %}

View File

@ -1,26 +0,0 @@
{% for subform in member_perms_form.members_permissions %}
{% set ppoc = subform.member_id.data == ppoc_id %}
{% set heading_perms = [subform.perms_app_mgmt, subform.perms_funding, subform.perms_reporting, subform.perms_portfolio_mgmt] %}
<tr>
<td class='name'>{{ subform.member_name.data }}
<div>
{% if ppoc %}
<span class='you'>PPoC</span>
{% endif %}
{% if subform.member_id.data == current_member_id %}
<span class='you'>(<span class='green'>you</span>)</span>
{% endif %}
</div>
</td>
{% for access in heading_perms %}
{% if dict(access.choices).get(access.data) == ('portfolios.members.permissions.edit_access' | translate) %}
<td class='green'>{{ 'portfolios.members.permissions.edit_access' | translate }}</td>
{% else %}
<td>{{ 'common.view' | translate }}</td>
{% endif %}
{% endfor %}
</tr>
{% endfor %}

View File

@ -1,105 +1,74 @@
{% from "components/icon.html" import Icon %}
{% from 'components/save_button.html' import SaveButton %}
{% from "components/modal.html" import Modal %}
{% from "components/alert.html" import Alert %}
{% from "components/icon.html" import Icon %}
{% import "components/member_form.html" as member_form %}
{% from "components/modal.html" import Modal %}
{% 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 %}
<section class="member-list" id="portfolio-members">
<div class='responsive-table-wrapper panel accordion-table'>
{% if g.matchesPath("portfolio-members") %}
{% include "fragments/flash.html" %}
{% endif %}
<base-form inline-template>
<form method='POST' id="member-perms" action='{{ url_for("portfolios.edit_members", portfolio_id=portfolio.id) }}' autocomplete="off" enctype="multipart/form-data">
{{ member_perms_form.csrf_token }}
<div class='application-list-item'>
<header>
<div class='responsive-table-wrapper__header'>
<div class='responsive-table-wrapper__title'>
<div class='h3'>{{ "portfolios.admin.portfolio_members_title" | translate }}</div>
<div class='subheading'>
{{ "portfolios.admin.portfolio_members_subheading" | translate }}
</div>
</div>
<a class='icon-link'>
{{ Icon('info') }}
{{ "portfolios.admin.settings_info" | translate }}
</a>
</div>
</header>
{% if not portfolio.members %}
<p>{{ "portfolios.admin.no_members" | translate }}</p>
{% else %}
<h3>Portfolio Managers</h3>
<div class="panel">
<section class="member-list">
<div class="responsive-table-wrapper">
<table class="atat-table">
<thead>
<tr>
<td>{{ "portfolios.members.permissions.name" | translate }}</td>
<td>{{ "portfolios.members.permissions.app_mgmt" | translate }}</td>
<td>{{ "portfolios.members.permissions.funding" | translate }}</td>
<td>{{ "portfolios.members.permissions.reporting" | translate }}</td>
<td>{{ "portfolios.members.permissions.portfolio_mgmt" | translate }}</td>
<td></td>
<th class="table-cell--third">Name</th>
<th>Portfolio Permissions</th>
</tr>
</thead>
<tbody>
{% if user_can(permissions.EDIT_PORTFOLIO_USERS) %}
{% include "portfolios/fragments/members_edit.html" %}
{% elif user_can(permissions.VIEW_PORTFOLIO_USERS) %}
{% include "portfolios/fragments/members_view.html" %}
{% for member in members -%}
<tr>
<td>
<strong>{{ member.user_name }}{% if member.role_id == current_member_id %} (You){% endif %}</strong>
<br>
{% if member.ppoc %}
{{ Label(type="ppoc", classes='label--below label--purple')}}
{% endif %}
{{ Label(type=member.status, classes='label--below')}}
</td>
<td>
{% for perm, value in member.permission_sets.items() -%}
<div>
{% if value -%}
{{ ("portfolios.admin.members.{}.{}".format(perm, value)) | translate }}
{%- endif %}
</div>
{%-endfor %}
</td>
</tr>
{%- endfor %}
</tbody>
</table>
</div>
{% endif %}
<div class="panel__footer">
<div class="action-group save">
{% if user_can(permissions.EDIT_PORTFOLIO_USERS) %}
{{ SaveButton(text=('common.save' | translate), element="input", form="member-perms") }}
{% endif %}
{% if user_can(permissions.CREATE_PORTFOLIO_USERS) %}
<a class="icon-link modal-link" v-on:click="openModal('add-port-mem')">
{{ "portfolios.admin.add_new_member" | translate }}
{{ Icon("plus") }}
</a>
{% endif %}
</div>
</div>
</form>
</base-form>
{% if user_can(permissions.EDIT_PORTFOLIO_USERS) %}
{% for subform in member_perms_form.members_permissions %}
{% set modal_id = "portfolio_id_{}_user_id_{}".format(portfolio.id, subform.member_id.data) %}
{% call Modal(name=modal_id, dismissable=False) %}
<h1>{{ "portfolios.admin.alert_header" | translate }}</h1>
<hr>
{{
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=subform.member_id.data)}}">
{{ member_perms_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 %}
{% endfor %}
{% endif %}
</div>
{% if user_can(permissions.CREATE_PORTFOLIO_USERS) %}
{% include "portfolios/fragments/add_new_portfolio_member.html" %}
{% endif %}
</section>
{% if user_can(permissions.CREATE_PORTFOLIO_USERS) %}
{% set new_manager_modal = "add-portfolio-manager" %}
<a class="usa-button usa-button-secondary add-new-button" v-on:click="openModal('{{ new_manager_modal }}')">
Add Portfolio Manager
</a>
{{ MultiStepModalForm(
name=new_manager_modal,
form=new_manager_form,
form_action=url_for("portfolios.invite_member", portfolio_id=portfolio.id),
steps=[
member_form.BasicStep(
title="Add Manager",
form=member_form_fields.InfoFields(new_manager_form.user_data),
next_button_text="Next: Permissions",
previous=False,
modal=new_manager_modal_name,
),
member_form.SubmitStep(
name=new_manager_modal,
form=member_form_fields.PermsFields(new_manager_form),
submit_text="Add Mananger",
modal=new_manager_modal_name,
)
],
) }}
{% endif %}
</div>

View File

@ -205,7 +205,7 @@ def test_invite():
inviter = UserFactory.create()
member_data = UserFactory.dictionary()
invitation = Portfolios.invite(portfolio, inviter, member_data)
invitation = Portfolios.invite(portfolio, inviter, {"user_data": member_data})
assert invitation.role
assert invitation.role.portfolio == portfolio

View File

@ -151,12 +151,12 @@ def test_event_details():
def test_status_when_member_is_active():
portfolio_role = PortfolioRoleFactory.create(status=PortfolioRoleStatus.ACTIVE)
assert portfolio_role.display_status == "Active"
assert portfolio_role.display_status == "active"
def test_status_when_member_is_disabled():
portfolio_role = PortfolioRoleFactory.create(status=PortfolioRoleStatus.DISABLED)
assert portfolio_role.display_status == "Disabled"
assert portfolio_role.display_status == "disabled"
def test_status_when_invitation_has_been_rejected_for_expirations():
@ -168,7 +168,7 @@ def test_status_when_invitation_has_been_rejected_for_expirations():
PortfolioInvitationFactory.create(
role=portfolio_role, status=InvitationStatus.REJECTED_EXPIRED
)
assert portfolio_role.display_status == "Invite expired"
assert portfolio_role.display_status == "invite_expired"
def test_status_when_invitation_has_been_rejected_for_wrong_user():
@ -180,7 +180,7 @@ def test_status_when_invitation_has_been_rejected_for_wrong_user():
PortfolioInvitationFactory.create(
role=portfolio_role, status=InvitationStatus.REJECTED_WRONG_USER
)
assert portfolio_role.display_status == "Error on invite"
assert portfolio_role.display_status == "invite_error"
def test_status_when_invitation_has_been_revoked():
@ -192,7 +192,7 @@ def test_status_when_invitation_has_been_revoked():
PortfolioInvitationFactory.create(
role=portfolio_role, status=InvitationStatus.REVOKED
)
assert portfolio_role.display_status == "Invite revoked"
assert portfolio_role.display_status == "invite_revoked"
def test_status_when_invitation_is_expired():
@ -206,7 +206,7 @@ def test_status_when_invitation_is_expired():
status=InvitationStatus.PENDING,
expiration_time=datetime.datetime.now() - datetime.timedelta(seconds=1),
)
assert portfolio_role.display_status == "Invite expired"
assert portfolio_role.display_status == "invite_expired"
def test_can_not_resend_invitation_if_active():

View File

@ -34,138 +34,6 @@ def test_member_table_access(client, user_session):
assert "<select" not in view_resp.data.decode()
def test_update_member_permissions(client, user_session):
portfolio = PortfolioFactory.create()
rando = UserFactory.create()
rando_pf_role = PortfolioRoleFactory.create(
user=rando,
portfolio=portfolio,
permission_sets=[PermissionSets.get(PermissionSets.VIEW_PORTFOLIO_ADMIN)],
)
user = UserFactory.create()
PortfolioRoleFactory.create(
user=user,
portfolio=portfolio,
permission_sets=PermissionSets.get_many(
[PermissionSets.EDIT_PORTFOLIO_ADMIN, PermissionSets.VIEW_PORTFOLIO_ADMIN]
),
)
user_session(user)
form_data = {
"members_permissions-0-member_id": rando_pf_role.id,
"members_permissions-0-perms_app_mgmt": "edit_portfolio_application_management",
"members_permissions-0-perms_funding": "view_portfolio_funding",
"members_permissions-0-perms_reporting": "view_portfolio_reports",
"members_permissions-0-perms_portfolio_mgmt": "view_portfolio_admin",
}
response = client.post(
url_for("portfolios.edit_members", portfolio_id=portfolio.id),
data=form_data,
follow_redirects=True,
)
assert response.status_code == 200
assert rando_pf_role.has_permission_set(
PermissionSets.EDIT_PORTFOLIO_APPLICATION_MANAGEMENT
)
def test_no_update_member_permissions_without_edit_access(client, user_session):
portfolio = PortfolioFactory.create()
rando = UserFactory.create()
rando_pf_role = PortfolioRoleFactory.create(
user=rando,
portfolio=portfolio,
permission_sets=[PermissionSets.get(PermissionSets.VIEW_PORTFOLIO_ADMIN)],
)
user = UserFactory.create()
PortfolioRoleFactory.create(
user=user,
portfolio=portfolio,
permission_sets=[PermissionSets.get(PermissionSets.VIEW_PORTFOLIO_ADMIN)],
)
user_session(user)
form_data = {
"members_permissions-0-member_id": rando_pf_role.id,
"members_permissions-0-perms_app_mgmt": "edit_portfolio_application_management",
"members_permissions-0-perms_funding": "view_portfolio_funding",
"members_permissions-0-perms_reporting": "view_portfolio_reports",
"members_permissions-0-perms_portfolio_mgmt": "view_portfolio_admin",
}
response = client.post(
url_for("portfolios.edit_members", portfolio_id=portfolio.id),
data=form_data,
follow_redirects=True,
)
assert response.status_code == 404
assert not rando_pf_role.has_permission_set(
PermissionSets.EDIT_PORTFOLIO_APPLICATION_MANAGEMENT
)
def test_rerender_admin_page_if_member_perms_form_does_not_validate(
client, user_session, monkeypatch
):
portfolio = PortfolioFactory.create()
user = UserFactory.create()
role = PortfolioRoleFactory.create(
user=user,
portfolio=portfolio,
permission_sets=[PermissionSets.get(PermissionSets.EDIT_PORTFOLIO_ADMIN)],
)
user_session(user)
form_data = {
"members_permissions-0-member_id": role.id,
"members_permissions-0-perms_app_mgmt": "bad input",
"members_permissions-0-perms_funding": "view_portfolio_funding",
"members_permissions-0-perms_reporting": "view_portfolio_reports",
"members_permissions-0-perms_portfolio_mgmt": "view_portfolio_admin",
}
mock_route = MagicMock(return_value=("", 200, {}))
monkeypatch.setattr("atst.routes.portfolios.admin.render_admin_page", mock_route)
client.post(
url_for("portfolios.edit_members", portfolio_id=portfolio.id), data=form_data
)
mock_route.assert_called()
def test_cannot_update_portfolio_ppoc_perms(client, user_session):
portfolio = PortfolioFactory.create()
ppoc = portfolio.owner
ppoc_pf_role = PortfolioRoles.get(portfolio_id=portfolio.id, user_id=ppoc.id)
user = UserFactory.create()
PortfolioRoleFactory.create(portfolio=portfolio, user=user)
user_session(user)
assert ppoc_pf_role.has_permission_set(PermissionSets.PORTFOLIO_POC)
member_perms_data = {
"members_permissions-0-member_id": ppoc_pf_role.id,
"members_permissions-0-perms_app_mgmt": "view_portfolio_application_management",
"members_permissions-0-perms_funding": "view_portfolio_funding",
"members_permissions-0-perms_reporting": "view_portfolio_reports",
"members_permissions-0-perms_portfolio_mgmt": "view_portfolio_admin",
}
response = client.post(
url_for("portfolios.edit_members", portfolio_id=portfolio.id),
data=member_perms_data,
follow_redirects=True,
)
assert response.status_code == 404
assert ppoc_pf_role.has_permission_set(PermissionSets.PORTFOLIO_POC)
def test_update_portfolio_name_and_description(client, user_session):
portfolio = PortfolioFactory.create()
user_session(portfolio.owner)

View File

@ -269,10 +269,10 @@ def test_existing_member_invite_resent_to_email_submitted_in_form(
_DEFAULT_PERMS_FORM_DATA = {
"permission_sets-perms_app_mgmt": PermissionSets.VIEW_PORTFOLIO_APPLICATION_MANAGEMENT,
"permission_sets-perms_funding": PermissionSets.VIEW_PORTFOLIO_FUNDING,
"permission_sets-perms_reporting": PermissionSets.VIEW_PORTFOLIO_REPORTS,
"permission_sets-perms_portfolio_mgmt": PermissionSets.VIEW_PORTFOLIO_ADMIN,
"permission_sets-perms_app_mgmt": False,
"permission_sets-perms_funding": False,
"permission_sets-perms_reporting": False,
"permission_sets-perms_portfolio_mgmt": False,
}

View File

@ -161,15 +161,23 @@ forms:
phone_number_label: Phone number
service_branch_label: Service branch or agency
new_member:
app_mgmt: App management
app_mgmt:
label: Edit Applications
description: Add, remove and edit applications in this Portfolio.
dod_id_label: DoD ID
email_label: Email address
first_name_label: First name
funding: Funding
funding:
label: Edit Funding
description: Add and Modify Task Orders to fund this Portfolio.
last_name_label: Last name
phone_number_label: Phone number
portfolio_mgmt: Portfolio management
reporting: Reporting
portfolio_mgmt:
label: Edit Portfolio
description: "Edit this Portfolio's settings."
reporting:
label: Edit Reporting
description: "View and export reports about this Portfolio's funding."
portfolio:
name:
label: Portfolio Name
@ -317,6 +325,19 @@ portfolios:
portfolio_members_title: Portfolio members
settings_info: Learn more about these settings
portfolio_name: Portfolio name
members:
perms_portfolio_mgmt:
'False': View Portfolio
'True': Edit Portfolio
perms_app_mgmt:
'False': View Applications
'True': Edit Applications
perms_funding:
'False': View Funding
'True': Edit Funding
perms_reporting:
'False': View Reporting
'True': Edit Reporting
applications:
add_application_text: Add a new application
add_environment: Create an Environment