diff --git a/atst/models/portfolio_role.py b/atst/models/portfolio_role.py index eb589157..a0c19bc0 100644 --- a/atst/models/portfolio_role.py +++ b/atst/models/portfolio_role.py @@ -7,6 +7,7 @@ from atst.models import Base, mixins from .types import Id from atst.database import db +from atst.utils import first_or_none from atst.models.environment_role import EnvironmentRole from atst.models.application import Application from atst.models.environment import Environment @@ -111,6 +112,11 @@ class PortfolioRole(Base, mixins.TimestampsMixin, mixins.AuditableMixin): else: return MEMBER_STATUSES["unknown"] + def has_permission_set(self, perm_set_name): + return first_or_none( + lambda prms: prms.name == perm_set_name, self.permission_sets + ) + @property def has_dod_id_error(self): return self.latest_invitation and self.latest_invitation.is_rejected_wrong_user diff --git a/atst/routes/portfolios/index.py b/atst/routes/portfolios/index.py index 8bf9aff6..fbbffd27 100644 --- a/atst/routes/portfolios/index.py +++ b/atst/routes/portfolios/index.py @@ -10,6 +10,7 @@ from atst.domain.authz import Authorization from atst.domain.common import Paginator from atst.forms.portfolio import PortfolioForm from atst.models.permissions import Permissions +from atst.domain.permission_sets import PermissionSets @portfolios_bp.route("/portfolios") @@ -22,6 +23,20 @@ def portfolios(): return render_template("portfolios/blank_slate.html") +def serialize_member(member): + return { + "member": member, + "app_mgmt": member.has_permission_set( + PermissionSets.EDIT_PORTFOLIO_APPLICATION_MANAGEMENT + ), + "funding": member.has_permission_set(PermissionSets.EDIT_PORTFOLIO_FUNDING), + "reporting": member.has_permission_set(PermissionSets.EDIT_PORTFOLIO_REPORTS), + "portfolio_mgmt": member.has_permission_set( + PermissionSets.EDIT_PORTFOLIO_ADMIN + ), + } + + @portfolios_bp.route("/portfolios//admin") def portfolio_admin(portfolio_id): portfolio = Portfolios.get_for_update_information(g.current_user, portfolio_id) @@ -30,11 +45,14 @@ def portfolio_admin(portfolio_id): audit_events = AuditLog.get_portfolio_events( g.current_user, portfolio, pagination_opts ) + members_data = [serialize_member(member) for member in portfolio.members] return render_template( "portfolios/admin.html", form=form, portfolio=portfolio, audit_events=audit_events, + user=g.current_user, + members_data=members_data, ) diff --git a/styles/components/_portfolio_layout.scss b/styles/components/_portfolio_layout.scss index fc9911fe..87c080c2 100644 --- a/styles/components/_portfolio_layout.scss +++ b/styles/components/_portfolio_layout.scss @@ -168,14 +168,51 @@ .member-list { .panel { @include shadow-panel; + padding-bottom: 0; + } + + .member-list-header { + margin: 2 * $gap 5 * $gap; + padding: inherit; + overflow: auto; + + .left { + float: left; + padding-bottom: 0.8rem; + } + + .icon-link { + float: right; + margin-top: 0.8rem; + } + + .icon { + + } + } + + .subheading { + font-size: 1.4rem; + color: $color-gray; } table { box-shadow: 0 6px 18px 0 rgba(144,164,183,0.3); + thead { th:first-child { padding-left: 3 * $gap; } + + tr:first-child { + padding: 0 2 * $gap 0 5 * $gap; + } + + td { + font-weight: bold; + font-size: 1.4rem; + border-top: 0; + } } th { @@ -186,8 +223,29 @@ color: $color-gray; } - td { - border-bottom: 1px solid $color-gray-lightest; + td:first-child { + padding: 2 * $gap 2 * $gap 2 * $gap 5 * $gap; + } + + tbody { + td { + border-bottom: 1px solid $color-gray-lightest; + font-size: 1.6rem; + border-top: 0; + padding: 3 * $gap 2 * $gap; + } + + .green { + color: $color-green; + } + + .name { + font-weight: bold; + + .you { + font-size: 1.2rem; + } + } } .add-member-link { diff --git a/templates/fragments/admin/portfolio_members.html b/templates/fragments/admin/portfolio_members.html new file mode 100644 index 00000000..b9301368 --- /dev/null +++ b/templates/fragments/admin/portfolio_members.html @@ -0,0 +1,64 @@ +{% from "components/icon.html" import Icon %} + +
+
+
+
+
{{ "portfolios.admin.portfolio_members_title" | translate }}
+
+ {{ "portfolios.admin.portfolio_members_subheading" | translate }} +
+
+ + {{ Icon('info') }} + {{ "portfolios.admin.settings_info" | translate }} + +
+ + {% if not portfolio.members %} + +

There are currently no members in this Portfolio.

+ + {% else %} + + + + + + + + + + + + + + + + {% for member_data in members_data %} + + + {% set heading_perms = [member_data.app_mgmt, member_data.funding, member_data.reporting, member_data.portfolio_mgmt] %} + + {% for has_perm in heading_perms %} + {% if has_perm %} + + {% else %} + + {% endif %} + {% endfor %} + + + {% endfor %} + + +
{{ "portfolios.members.permissions.name" | translate }}{{ "portfolios.members.permissions.app_mgmt" | translate }}{{ "portfolios.members.permissions.funding" | translate }}{{ "portfolios.members.permissions.reporting" | translate }}{{ "portfolios.members.permissions.portfolio_mgmt" | translate }}
{{ member_data.member.user_name }} + {% if member_data.member.user == user %} + (you) + {% endif %} + Edit AccessView Only
+
+ +{% endif %} + +
diff --git a/templates/fragments/audit_events_log.html b/templates/fragments/audit_events_log.html index 3b72f7f7..773ffd0b 100644 --- a/templates/fragments/audit_events_log.html +++ b/templates/fragments/audit_events_log.html @@ -1,5 +1,3 @@ -{% from "components/pagination.html" import Pagination %} -
{{ "portfolios.admin.activity_log_title" | translate }}