Merge pull request #738 from dod-ccpo/update-ppoc
Update Point of Contact
This commit is contained in:
commit
2ae9f0cab7
@ -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
|
||||
|
@ -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 -")],
|
||||
)
|
||||
|
@ -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):
|
||||
|
@ -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.",
|
||||
|
@ -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">
|
||||
|
@ -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",
|
||||
) }}
|
||||
|
82
templates/fragments/admin/change_ppoc.html
Normal file
82
templates/fragments/admin/change_ppoc.html
Normal 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>
|
@ -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>
|
||||
|
@ -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
|
||||
)
|
||||
|
@ -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)
|
||||
|
@ -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:
|
||||
|
Loading…
x
Reference in New Issue
Block a user