diff --git a/atst/domain/authz/__init__.py b/atst/domain/authz/__init__.py index 6e8cdfea..38e3fa38 100644 --- a/atst/domain/authz/__init__.py +++ b/atst/domain/authz/__init__.py @@ -1,6 +1,7 @@ from atst.utils import first_or_none from atst.models.permissions import Permissions from atst.domain.exceptions import UnauthorizedError +from atst.models.portfolio_role import Status as PortfolioRoleStatus class Authorization(object): @@ -9,7 +10,7 @@ class Authorization(object): port_role = first_or_none( 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 else: return False diff --git a/atst/domain/portfolio_roles.py b/atst/domain/portfolio_roles.py index 153d4707..cb123b4a 100644 --- a/atst/domain/portfolio_roles.py +++ b/atst/domain/portfolio_roles.py @@ -121,6 +121,15 @@ class PortfolioRoles(object): ) 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 def update(cls, portfolio_role, set_names): new_permission_sets = PortfolioRoles._permission_sets_for_names(set_names) diff --git a/atst/routes/portfolios/index.py b/atst/routes/portfolios/index.py index 541a8194..daf768ae 100644 --- a/atst/routes/portfolios/index.py +++ b/atst/routes/portfolios/index.py @@ -2,8 +2,6 @@ from datetime import date, timedelta 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 atst.domain.reports import Reports 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.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 @portfolios_bp.route("/portfolios") @@ -174,3 +174,28 @@ def portfolio_reports(portfolio_id): expiration_date=expiration_date, remaining_days=remaining_days, ) + + +@portfolios_bp.route( + "/portfolios//members//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", + ) + ) diff --git a/atst/utils/flash.py b/atst/utils/flash.py index 89f81132..1fc6a740 100644 --- a/atst/utils/flash.py +++ b/atst/utils/flash.py @@ -138,6 +138,11 @@ MESSAGES = { """, "category": "error", }, + "portfolio_member_removed": { + "title_template": "Portfolio Member Removed", + "message_template": "You have successfully removed {{ member_name }} from the portfolio.", + "category": "success", + }, } diff --git a/styles/components/_portfolio_layout.scss b/styles/components/_portfolio_layout.scss index 539ef47b..7a33345b 100644 --- a/styles/components/_portfolio_layout.scss +++ b/styles/components/_portfolio_layout.scss @@ -291,6 +291,10 @@ height: 4rem; } + .usa-button-danger { + background: $color-red; + } + .members-table-footer { float: right; padding: 3 * $gap; diff --git a/templates/fragments/admin/members_edit.html b/templates/fragments/admin/members_edit.html index 0d873622..2c4df95f 100644 --- a/templates/fragments/admin/members_edit.html +++ b/templates/fragments/admin/members_edit.html @@ -1,4 +1,8 @@ +{% from "components/confirmation_button.html" import ConfirmationButton %} + {% for subform in member_perms_form.members_permissions %} + {% set modal_id = "portfolio_id_{}_user_id_{}".format(portfolio.id, subform.user_id.data) %} + {{ subform.member.data }} {% if subform.member.data == user.full_name %} @@ -14,7 +18,10 @@ {{ OptionsInput(subform.perms_reporting, label=False) }} {{ OptionsInput(subform.perms_portfolio_mgmt, label=False) }} - + + + {{ "portfolios.members.archive_button" | translate }} + {{ subform.user_id() }} diff --git a/templates/fragments/admin/portfolio_members.html b/templates/fragments/admin/portfolio_members.html index 08e856e0..0763aad0 100644 --- a/templates/fragments/admin/portfolio_members.html +++ b/templates/fragments/admin/portfolio_members.html @@ -1,6 +1,9 @@ {% from "components/icon.html" import Icon %} {% from "components/options_input.html" import OptionsInput %} +{% from "components/modal.html" import Modal %} +{% from "components/alert.html" import Alert %} +
{% if g.matchesPath("portfolio-members") %} @@ -51,6 +54,34 @@ {% endif %} + + {% 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) %} +

Are you sure you want to archive this user?

+ + {{ + 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" + ) + }} + +
+
+ {{ member_perms_form.csrf_token }} + +
+ Cancel +
+ {% endcall %} + {% endfor %} + {% endif %} +