Merge pull request #730 from dod-ccpo/archive-portfolio-member

Remove Portfolio User
This commit is contained in:
George Drummond 2019-04-03 13:06:41 -04:00 committed by GitHub
commit 48d9506f96
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 153 additions and 5 deletions

View File

@ -1,6 +1,7 @@
from atst.utils import first_or_none from atst.utils import first_or_none
from atst.models.permissions import Permissions from atst.models.permissions import Permissions
from atst.domain.exceptions import UnauthorizedError from atst.domain.exceptions import UnauthorizedError
from atst.models.portfolio_role import Status as PortfolioRoleStatus
class Authorization(object): class Authorization(object):
@ -9,7 +10,7 @@ class Authorization(object):
port_role = first_or_none( port_role = first_or_none(
lambda pr: pr.portfolio == portfolio, user.portfolio_roles lambda pr: pr.portfolio == portfolio, user.portfolio_roles
) )
if port_role: if port_role and port_role.status is not PortfolioRoleStatus.DISABLED:
return permission in port_role.permissions return permission in port_role.permissions
else: else:
return False return False

View File

@ -121,6 +121,15 @@ class PortfolioRoles(object):
) )
return PermissionSets.get_many(perms_set_names) return PermissionSets.get_many(perms_set_names)
@classmethod
def disable(cls, portfolio_role):
portfolio_role.status = PortfolioRoleStatus.DISABLED
db.session.add(portfolio_role)
db.session.commit()
return portfolio_role
@classmethod @classmethod
def update(cls, portfolio_role, set_names): def update(cls, portfolio_role, set_names):
new_permission_sets = PortfolioRoles._permission_sets_for_names(set_names) new_permission_sets = PortfolioRoles._permission_sets_for_names(set_names)

View File

@ -2,8 +2,6 @@ from datetime import date, timedelta
from flask import render_template, request as http_request, g, redirect, url_for from flask import render_template, request as http_request, g, redirect, url_for
from atst.utils.flash import formatted_flash as flash
from . import portfolios_bp from . import portfolios_bp
from atst.domain.reports import Reports from atst.domain.reports import Reports
from atst.domain.portfolios import Portfolios from atst.domain.portfolios import Portfolios
@ -15,6 +13,8 @@ import atst.forms.portfolio_member as member_forms
from atst.models.permissions import Permissions from atst.models.permissions import Permissions
from atst.domain.permission_sets import PermissionSets from atst.domain.permission_sets import PermissionSets
from atst.domain.authz.decorator import user_can_access_decorator as user_can 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
@portfolios_bp.route("/portfolios") @portfolios_bp.route("/portfolios")
@ -174,3 +174,28 @@ def portfolio_reports(portfolio_id):
expiration_date=expiration_date, expiration_date=expiration_date,
remaining_days=remaining_days, remaining_days=remaining_days,
) )
@portfolios_bp.route(
"/portfolios/<portfolio_id>/members/<user_id>/delete", methods=["POST"]
)
@user_can(Permissions.EDIT_PORTFOLIO_USERS, message="update portfolio members")
def remove_member(portfolio_id, user_id):
if str(g.current_user.id) == user_id:
raise UnauthorizedError(
g.current_user, "you cant remove yourself from the portfolio"
)
portfolio_role = PortfolioRoles.get(portfolio_id=portfolio_id, user_id=user_id)
PortfolioRoles.disable(portfolio_role=portfolio_role)
flash("portfolio_member_removed", member_name=portfolio_role.user.full_name)
return redirect(
url_for(
"portfolios.portfolio_admin",
portfolio_id=portfolio_id,
_anchor="portfolio-members",
fragment="portfolio-members",
)
)

View File

@ -138,6 +138,11 @@ MESSAGES = {
""", """,
"category": "error", "category": "error",
}, },
"portfolio_member_removed": {
"title_template": "Portfolio Member Removed",
"message_template": "You have successfully removed {{ member_name }} from the portfolio.",
"category": "success",
},
} }

View File

@ -291,6 +291,10 @@
height: 4rem; height: 4rem;
} }
.usa-button-danger {
background: $color-red;
}
.members-table-footer { .members-table-footer {
float: right; float: right;
padding: 3 * $gap; padding: 3 * $gap;

View File

@ -1,4 +1,8 @@
{% from "components/confirmation_button.html" import ConfirmationButton %}
{% for subform in member_perms_form.members_permissions %} {% for subform in member_perms_form.members_permissions %}
{% set modal_id = "portfolio_id_{}_user_id_{}".format(portfolio.id, subform.user_id.data) %}
<tr> <tr>
<td class='name'>{{ subform.member.data }} <td class='name'>{{ subform.member.data }}
{% if subform.member.data == user.full_name %} {% if subform.member.data == user.full_name %}
@ -14,7 +18,10 @@
<td>{{ OptionsInput(subform.perms_reporting, label=False) }}</td> <td>{{ OptionsInput(subform.perms_reporting, label=False) }}</td>
<td>{{ OptionsInput(subform.perms_portfolio_mgmt, label=False) }}</td> <td>{{ OptionsInput(subform.perms_portfolio_mgmt, label=False) }}</td>
<td><button type="button" class='{{ archive_button_class }}'>{{ "portfolios.members.archive_button" | translate }}</button> <td>
<a v-on:click="openModal('{{ modal_id }}')" class='usa-button {{ archive_button_class }}'>
{{ "portfolios.members.archive_button" | translate }}
</a>
</td> </td>
{{ subform.user_id() }} {{ subform.user_id() }}
</tr> </tr>

View File

@ -1,6 +1,9 @@
{% from "components/icon.html" import Icon %} {% from "components/icon.html" import Icon %}
{% from "components/options_input.html" import OptionsInput %} {% from "components/options_input.html" import OptionsInput %}
{% from "components/modal.html" import Modal %}
{% from "components/alert.html" import Alert %}
<section class="member-list" id="portfolio-members"> <section class="member-list" id="portfolio-members">
<div class='responsive-table-wrapper panel'> <div class='responsive-table-wrapper panel'>
{% if g.matchesPath("portfolio-members") %} {% if g.matchesPath("portfolio-members") %}
@ -51,6 +54,34 @@
{% endif %} {% endif %}
</form> </form>
{% if user_can(permissions.EDIT_PORTFOLIO_USERS) %}
{% for member in portfolio.members %}
{% set modal_id = "portfolio_id_{}_user_id_{}".format(portfolio.id, member.user_id) %}
{% call Modal(name=modal_id, dismissable=False) %}
<h1>Are you sure you want to archive this user?</h1>
{{
Alert(
title="Warning! You are about to archive a user from the portfolio admin.",
message="User will be removed from the portfolio, but their log history will be retained.",
level="warning"
)
}}
<div class="action-group">
<form method="POST" action="{{ url_for('portfolios.remove_member', portfolio_id=portfolio.id, user_id=member.user_id) }}">
{{ 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">Cancel</a>
</div>
{% endcall %}
{% endfor %}
{% endif %}
<div class="members-table-footer"> <div class="members-table-footer">
<div class="action-group"> <div class="action-group">
{% if user_can(permissions.EDIT_PORTFOLIO_USERS) %} {% if user_can(permissions.EDIT_PORTFOLIO_USERS) %}

View File

@ -11,6 +11,7 @@ from atst.domain.authz.decorator import user_can_access_decorator
from atst.domain.permission_sets import PermissionSets from atst.domain.permission_sets import PermissionSets
from atst.domain.exceptions import UnauthorizedError from atst.domain.exceptions import UnauthorizedError
from atst.models.permissions import Permissions from atst.models.permissions import Permissions
from atst.domain.portfolio_roles import PortfolioRoles
from tests.utils import FakeLogger from tests.utils import FakeLogger
@ -75,7 +76,7 @@ def test_user_can_access():
portfolio = PortfolioFactory.create(owner=edit_admin) portfolio = PortfolioFactory.create(owner=edit_admin)
# factory gives view perms by default # factory gives view perms by default
PortfolioRoleFactory.create(user=view_admin, portfolio=portfolio) view_admin_pr = PortfolioRoleFactory.create(user=view_admin, portfolio=portfolio)
# check a site-wide permission # check a site-wide permission
assert user_can_access(ccpo, Permissions.VIEW_AUDIT_LOG) assert user_can_access(ccpo, Permissions.VIEW_AUDIT_LOG)
@ -101,6 +102,13 @@ def test_user_can_access():
view_admin, Permissions.EDIT_PORTFOLIO_NAME, portfolio=portfolio view_admin, Permissions.EDIT_PORTFOLIO_NAME, portfolio=portfolio
) )
# check when portfolio_role is disabled
PortfolioRoles.disable(portfolio_role=view_admin_pr)
with pytest.raises(UnauthorizedError):
user_can_access(
view_admin, Permissions.EDIT_PORTFOLIO_NAME, portfolio=portfolio
)
@pytest.fixture @pytest.fixture
def set_current_user(request_ctx): def set_current_user(request_ctx):

View File

@ -29,3 +29,11 @@ def test_add_portfolio_role_with_permission_sets():
] ]
actual_names = [prms.name for prms in port_role.permission_sets] actual_names = [prms.name for prms in port_role.permission_sets]
assert expected_names == expected_names assert expected_names == expected_names
def test_disable_portfolio_role():
portfolio_role = PortfolioRoleFactory.create(status=PortfolioRoleStatus.ACTIVE)
assert portfolio_role.status == PortfolioRoleStatus.ACTIVE
PortfolioRoles.disable(portfolio_role=portfolio_role)
assert portfolio_role.status == PortfolioRoleStatus.DISABLED

View File

@ -2,6 +2,8 @@ from flask import url_for
from atst.domain.permission_sets import PermissionSets from atst.domain.permission_sets import PermissionSets
from atst.models.permissions import Permissions from atst.models.permissions import Permissions
from atst.domain.portfolio_roles import PortfolioRoles
from atst.models.portfolio_role import Status as PortfolioRoleStatus
from tests.factories import ( from tests.factories import (
random_future_date, random_future_date,
@ -81,6 +83,54 @@ def test_portfolio_admin_screen_when_not_ppoc(client, user_session):
assert translate("fragments.ppoc.update_btn").encode("utf8") not in response.data assert translate("fragments.ppoc.update_btn").encode("utf8") not in response.data
def test_remove_portfolio_member(client, user_session):
portfolio = PortfolioFactory.create()
user = UserFactory.create()
PortfolioRoleFactory.create(portfolio=portfolio, user=user)
user_session(portfolio.owner)
response = client.post(
url_for("portfolios.remove_member", portfolio_id=portfolio.id, user_id=user.id),
follow_redirects=False,
)
assert response.status_code == 302
assert response.headers["Location"] == url_for(
"portfolios.portfolio_admin",
portfolio_id=portfolio.id,
_anchor="portfolio-members",
fragment="portfolio-members",
_external=True,
)
assert (
PortfolioRoles.get(portfolio_id=portfolio.id, user_id=user.id).status
== PortfolioRoleStatus.DISABLED
)
def test_remove_portfolio_member_self(client, user_session):
portfolio = PortfolioFactory.create()
user_session(portfolio.owner)
response = client.post(
url_for(
"portfolios.remove_member",
portfolio_id=portfolio.id,
user_id=portfolio.owner.id,
),
follow_redirects=False,
)
assert response.status_code == 404
assert (
PortfolioRoles.get(portfolio_id=portfolio.id, user_id=portfolio.owner.id).status
== PortfolioRoleStatus.ACTIVE
)
def test_portfolio_reports(client, user_session): def test_portfolio_reports(client, user_session):
portfolio = PortfolioFactory.create( portfolio = PortfolioFactory.create(
applications=[ applications=[