Update Point of Contact

This commit is contained in:
George Drummond 2019-03-26 11:36:58 -04:00
parent cf2273d47c
commit 090e13f083
No known key found for this signature in database
GPG Key ID: 296DD6077123BF17
11 changed files with 326 additions and 15 deletions

View File

@ -121,6 +121,28 @@ class PortfolioRoles(object):
)
return PermissionSets.get_many(perms_set_names)
@classmethod
def make_ppoc(cls, portfolio_role):
portfolio = portfolio_role.portfolio
original_owner_role = PortfolioRoles.get(
portfolio_id=portfolio.id, user_id=portfolio.owner.id
)
PortfolioRoles.revoke_ppoc_permissions(portfolio_role=original_owner_role)
PortfolioRoles.add(
user=portfolio_role.user,
portfolio_id=portfolio.id,
permission_sets=PortfolioRoles.PORTFOLIO_PERMISSION_SETS,
)
@classmethod
def revoke_ppoc_permissions(cls, portfolio_role):
permission_sets = [
permission_set.name
for permission_set in portfolio_role.permission_sets
if permission_set.name != PermissionSets.PORTFOLIO_POC
]
PortfolioRoles.update(portfolio_role=portfolio_role, set_names=permission_sets)
@classmethod
def disable(cls, portfolio_role):
portfolio_role.status = PortfolioRoleStatus.DISABLED

View File

@ -80,3 +80,11 @@ class NewForm(PermissionsForm):
translate("forms.new_member.dod_id_label"),
validators=[Required(), Length(min=10), IsNumber()],
)
class AssignPPOCForm(PermissionsForm):
user_id = SelectField(
label=translate("forms.assign_ppoc.dod_id"),
validators=[Required()],
choices=[("", "- Select -")],
)

View File

@ -6,12 +6,14 @@ from . import portfolios_bp
from atst.domain.reports import Reports
from atst.domain.portfolios import Portfolios
from atst.domain.portfolio_roles import PortfolioRoles
from atst.domain.permission_sets import PermissionSets
from atst.domain.users import Users
from atst.domain.audit_log import AuditLog
from atst.domain.common import Paginator
from atst.domain.exceptions import NotFoundError
from atst.forms.portfolio import PortfolioForm
import atst.forms.portfolio_member as member_forms
from atst.models.permissions import Permissions
from atst.domain.permission_sets import PermissionSets
from atst.domain.authz.decorator import user_can_access_decorator as user_can
from atst.utils.flash import formatted_flash as flash
from atst.domain.exceptions import UnauthorizedError
@ -70,12 +72,19 @@ def render_admin_page(portfolio, form=None):
member_perms_form = member_forms.MembersPermissionsForm(
data={"members_permissions": members_data}
)
assign_ppoc_form = member_forms.AssignPPOCForm()
assign_ppoc_form.user_id.choices += [
(user.id, user.full_name) for user in portfolio.users
]
return render_template(
"portfolios/admin.html",
form=form,
portfolio_form=portfolio_form,
member_perms_form=member_perms_form,
member_form=member_forms.NewForm(),
assign_ppoc_form=assign_ppoc_form,
portfolio=portfolio,
audit_events=audit_events,
user=g.current_user,
@ -117,6 +126,32 @@ def edit_portfolio_members(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):
user_id = http_request.form.get("user_id")
portfolio = Portfolios.get(g.current_user, portfolio_id)
new_ppoc = Users.get(user_id)
if new_ppoc not in portfolio.users:
raise NotFoundError("user not in portfolio")
portfolio_role = PortfolioRoles.get(portfolio_id=portfolio_id, user_id=user_id)
PortfolioRoles.make_ppoc(portfolio_role=portfolio_role)
flash("primary_point_of_contact_changed", ppoc_name=new_ppoc.full_name)
return redirect(
url_for(
"portfolios.portfolio_admin",
portfolio_id=portfolio.id,
fragment="primary-point-of-contact",
_anchor="primary-point-of-contact",
)
)
@portfolios_bp.route("/portfolios/<portfolio_id>/edit", methods=["POST"])
@user_can(Permissions.EDIT_PORTFOLIO_NAME, message="edit portfolio")
def edit_portfolio(portfolio_id):

View File

@ -2,6 +2,11 @@ from flask import flash, render_template_string
from atst.utils.localization import translate
MESSAGES = {
"primary_point_of_contact_changed": {
"title_template": "Primary Point of Contact Changed",
"message_template": "You have successfully added {{ ppoc_name }} as Point of Contact. You are no longer the PoC.",
"category": "success",
},
"invitation_resent": {
"title_template": "Invitation resent",
"message_template": "The {{ officer_type }} has been resent instructions to join this portfolio.",

View File

@ -25,13 +25,16 @@
</div>
{% endmacro %}
{% macro MultiStepModalForm(name, form, form_action, steps, button_text="", dismissable=False) -%}
{% macro MultiStepModalForm(name, form, form_action, steps, button_icon="", button_text="", link_classes="icon-link modal-link", dismissable=False) -%}
{% set step_count = steps|length %}
<multi-step-modal-form inline-template :steps={{ step_count }}>
<div>
<a class='icon-link modal-link' v-on:click="openModal('{{ name }}')">
<a class='{{ link_classes }}' v-on:click="openModal('{{ name }}')">
{{ button_text }}
{{ Icon('plus-circle-solid') }}
{% if button_icon != "" %}
{{ Icon(button_icon) }}
{% endif %}
</a>
{% call Modal(name=name, dismissable=dismissable, classes="wide") %}
<form id="{{ name }}" action="{{ form_action }}" method="POST" v-on:submit="handleSubmit">

View File

@ -80,5 +80,6 @@
member_form,
url_for("portfolios.create_member", portfolio_id=portfolio.id),
[step_one, step_two],
button_text="portfolios.admin.add_new_member" | translate)
}}
button_text=("portfolios.admin.add_new_member" | translate),
button_icon="plus-circle-solid",
) }}

View File

@ -0,0 +1,82 @@
{% from "components/icon.html" import Icon %}
{% from "components/selector.html" import Selector %}
{% 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 %}
<div class="modal__form--header">
<h1>{{ "fragments.ppoc.update_ppoc_title" | translate }}</h1>
</div>
{{
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.user_id
)
}}
</div>
<div class='form-col form-col--half'>
</div>
</div>
<div class='action-group'>
<input
type='button'
v-on:click="next()"
v-bind:disabled="invalid"
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 %}
<div class="modal__form--padded">
<div class="modal__form--header">
<h1>{{ "fragments.ppoc.update_ppoc_confirmation_title" | translate }}</h1>
</div>
{{
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>
</div>
{% endset %}
<div class="flex-reverse-row">
{{
MultiStepModalForm(
'change-ppoc-form',
assign_ppoc_form,
form_action=url_for("portfolios.update_ppoc", portfolio_id=portfolio.id),
steps=[step_one, step_two],
button_text=("fragments.ppoc.update_btn" | translate),
link_classes="usa-button-primary"
)
}}
</div>

View File

@ -1,5 +1,9 @@
<div class="panel">
<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>
@ -15,11 +19,7 @@
</p>
{% if user_can(permissions.EDIT_PORTFOLIO_POC) %}
<div class="flex-reverse-row">
<a class="usa-button-primary">
{{ "fragments.ppoc.update_btn" | translate }}
</a>
</div>
{% include "fragments/admin/change_ppoc.html" %}
{% endif %}
</div>
</div>
</section>

View File

@ -1,7 +1,8 @@
from atst.domain.permission_sets import PermissionSets
from atst.domain.portfolio_roles import PortfolioRoles
from atst.domain.users import Users
from atst.models.permissions import Permissions
from atst.models.portfolio_role import Status as PortfolioRoleStatus
from atst.domain.permission_sets import PermissionSets
from tests.factories import (
PortfolioFactory,
@ -37,3 +38,34 @@ def test_disable_portfolio_role():
PortfolioRoles.disable(portfolio_role=portfolio_role)
assert portfolio_role.status == PortfolioRoleStatus.DISABLED
def test_revoke_ppoc_permissions():
portfolio = PortfolioFactory.create()
portfolio_role = PortfolioRoles.get(
portfolio_id=portfolio.id, user_id=portfolio.owner.id
)
assert Permissions.EDIT_PORTFOLIO_POC in portfolio_role.permissions
PortfolioRoles.revoke_ppoc_permissions(portfolio_role=portfolio_role)
assert Permissions.EDIT_PORTFOLIO_POC not in portfolio_role.permissions
def test_make_ppoc():
portfolio = PortfolioFactory.create()
original_owner = portfolio.owner
new_owner = UserFactory.create()
new_owner_role = PortfolioRoles.add(user=new_owner, portfolio_id=portfolio.id)
PortfolioRoles.make_ppoc(portfolio_role=new_owner_role)
assert portfolio.owner is new_owner
assert Permissions.EDIT_PORTFOLIO_POC in new_owner_role.permissions
assert (
Permissions.EDIT_PORTFOLIO_POC
not in PortfolioRoles.get(
portfolio_id=portfolio.id, user_id=original_owner.id
).permissions
)

View File

@ -1,9 +1,12 @@
from flask import url_for
import pytest
from flask import url_for
from atst.domain.permission_sets import PermissionSets
from atst.domain.portfolios import Portfolios
from atst.models.permissions import Permissions
from atst.domain.portfolio_roles import PortfolioRoles
from atst.models.portfolio_role import Status as PortfolioRoleStatus
from atst.domain.exceptions import UnauthorizedError
from tests.factories import (
random_future_date,
@ -28,6 +31,114 @@ def test_update_portfolio_name(client, user_session):
assert portfolio.name == "a cool new name"
def updating_ppoc_successfully(client, old_ppoc, new_ppoc, portfolio):
response = client.post(
url_for("portfolios.update_ppoc", portfolio_id=portfolio.id, _external=True),
data={"user_id": new_ppoc.id},
follow_redirects=False,
)
assert response.status_code == 302
assert response.headers["Location"] == url_for(
"portfolios.portfolio_admin",
portfolio_id=portfolio.id,
fragment="primary-point-of-contact",
_anchor="primary-point-of-contact",
_external=True,
)
assert portfolio.owner.id == new_ppoc.id
assert (
Permissions.EDIT_PORTFOLIO_POC
in PortfolioRoles.get(
portfolio_id=portfolio.id, user_id=new_ppoc.id
).permissions
)
assert (
Permissions.EDIT_PORTFOLIO_POC
not in PortfolioRoles.get(portfolio.id, old_ppoc.id).permissions
)
def test_update_ppoc_no_user_id_specified(client, user_session):
portfolio = PortfolioFactory.create()
user_session(portfolio.owner)
response = client.post(
url_for("portfolios.update_ppoc", portfolio_id=portfolio.id, _external=True),
follow_redirects=False,
)
assert response.status_code == 404
def test_update_ppoc_to_member_not_on_portfolio(client, user_session):
portfolio = PortfolioFactory.create()
original_ppoc = portfolio.owner
non_portfolio_member = UserFactory.create()
user_session(original_ppoc)
response = client.post(
url_for("portfolios.update_ppoc", portfolio_id=portfolio.id, _external=True),
data={"user_id": non_portfolio_member.id},
follow_redirects=False,
)
assert response.status_code == 404
assert portfolio.owner.id == original_ppoc.id
def test_update_ppoc_when_ppoc(client, user_session):
portfolio = PortfolioFactory.create()
original_ppoc = portfolio.owner
new_ppoc = UserFactory.create()
Portfolios.add_member(
member=new_ppoc,
portfolio=portfolio,
permission_sets=[PermissionSets.VIEW_PORTFOLIO],
)
user_session(original_ppoc)
updating_ppoc_successfully(
client=client, new_ppoc=new_ppoc, old_ppoc=original_ppoc, portfolio=portfolio
)
def test_update_ppoc_when_cpo(client, user_session):
ccpo = UserFactory.create_ccpo()
portfolio = PortfolioFactory.create()
original_ppoc = portfolio.owner
new_ppoc = UserFactory.create()
Portfolios.add_member(
member=new_ppoc,
portfolio=portfolio,
permission_sets=[PermissionSets.VIEW_PORTFOLIO],
)
user_session(ccpo)
updating_ppoc_successfully(
client=client, new_ppoc=new_ppoc, old_ppoc=original_ppoc, portfolio=portfolio
)
def test_update_ppoc_when_not_ppoc(client, user_session):
portfolio = PortfolioFactory.create()
new_owner = UserFactory.create()
user_session(new_owner)
response = client.post(
url_for("portfolios.update_ppoc", portfolio_id=portfolio.id, _external=True),
data={"dod_id": new_owner.dod_id},
follow_redirects=False,
)
assert response.status_code == 404
def test_portfolio_index_with_existing_portfolios(client, user_session):
portfolio = PortfolioFactory.create()
user_session(portfolio.owner)

View File

@ -28,6 +28,8 @@ flash:
delete_member_success: You have successfully deleted {member_name} from the portfolio.
common:
back: Back
cancel: Cancel
confirm: Confirm
edit: Edit
manage: manage
cancel: Cancel
@ -61,6 +63,8 @@ footer:
browser_support: JEDI Cloud supported on these web browsers
jedi_help_link_text: Questions? Contact your CCPO representative
forms:
assign_ppoc:
dod_id: "Select new primary point of contact:"
ccpo_review:
comment_description: Provide instructions or notes for additional information that is necessary to approve the request here. The requestor may then re-submit the updated request or initiate contact outside of AT-AT if further discussion is required. <strong>This message will be shared with the person making the JEDI request.</strong>.
comment_label: Instructions or comments
@ -319,6 +323,14 @@ fragments:
title: Primary point of contact (PPoC)
subtitle: The PPoC has the ability to edit all aspects of a portfolio and is the only one who can manage the PPoC role.
update_btn: Update
assign_user_button_text: Assign User
update_ppoc_confirmation_title: Confirmation
update_ppoc_title: Update Primary Point of Contact.
confirm_alert:
title: Once you assign a new point of contact, you will no longer be able to request portfolio deactivation or manage the point of contact role.
alert:
title: Warning!
message: Selecting a new primary contact gives that member full access to the portfolio and removes your PoC rights. Please be sure you want to proceed.
login:
ccpo_logo_alt_text: Cloud Computing Program Office Logo
certificate_selection: