diff --git a/atst/domain/portfolios/portfolios.py b/atst/domain/portfolios/portfolios.py index bb9e7aea..6b8a5c4e 100644 --- a/atst/domain/portfolios/portfolios.py +++ b/atst/domain/portfolios/portfolios.py @@ -75,10 +75,10 @@ class Portfolios(object): permission_sets = PortfolioRoles._permission_sets_for_names( member_data.get("permission_sets", []) ) - role = PortfolioRole(portfolio_id=portfolio.id, permission_sets=permission_sets) + role = PortfolioRole(portfolio=portfolio, permission_sets=permission_sets) invitation = PortfolioInvitations.create( - inviter=inviter, role=role, member_data=member_data + inviter=inviter, role=role, member_data=member_data["user_data"] ) PortfoliosQuery.add_and_commit(role) diff --git a/atst/forms/portfolio_member.py b/atst/forms/portfolio_member.py index 918d1112..9fff59c1 100644 --- a/atst/forms/portfolio_member.py +++ b/atst/forms/portfolio_member.py @@ -1,76 +1,59 @@ from wtforms.validators import Required -from wtforms.fields import StringField, FormField, FieldList, HiddenField +from wtforms.fields import BooleanField, FormField -from atst.domain.permission_sets import PermissionSets from .forms import BaseForm from .member import NewForm as BaseNewMemberForm +from atst.domain.permission_sets import PermissionSets from atst.forms.fields import SelectField from atst.utils.localization import translate class PermissionsForm(BaseForm): - member_name = StringField() - member_id = HiddenField() - perms_app_mgmt = SelectField( - translate("forms.new_member.app_mgmt"), - choices=[ - ( - PermissionSets.VIEW_PORTFOLIO_APPLICATION_MANAGEMENT, - translate("common.view"), - ), - ( - PermissionSets.EDIT_PORTFOLIO_APPLICATION_MANAGEMENT, - translate("common.edit"), - ), - ], + perms_app_mgmt = BooleanField( + translate("forms.new_member.app_mgmt.label"), + default=False, + description=translate("forms.new_member.app_mgmt.description"), ) - perms_funding = SelectField( - translate("forms.new_member.funding"), - choices=[ - (PermissionSets.VIEW_PORTFOLIO_FUNDING, translate("common.view")), - (PermissionSets.EDIT_PORTFOLIO_FUNDING, translate("common.edit")), - ], + perms_funding = BooleanField( + translate("forms.new_member.funding.label"), + default=False, + description=translate("forms.new_member.funding.description"), ) - perms_reporting = SelectField( - translate("forms.new_member.reporting"), - choices=[ - (PermissionSets.VIEW_PORTFOLIO_REPORTS, translate("common.view")), - (PermissionSets.EDIT_PORTFOLIO_REPORTS, translate("common.edit")), - ], + perms_reporting = BooleanField( + translate("forms.new_member.reporting.label"), + default=False, + description=translate("forms.new_member.reporting.description"), ) - perms_portfolio_mgmt = SelectField( - translate("forms.new_member.portfolio_mgmt"), - choices=[ - (PermissionSets.VIEW_PORTFOLIO_ADMIN, translate("common.view")), - (PermissionSets.EDIT_PORTFOLIO_ADMIN, translate("common.edit")), - ], + perms_portfolio_mgmt = BooleanField( + translate("forms.new_member.portfolio_mgmt.label"), + default=False, + description=translate("forms.new_member.portfolio_mgmt.description"), ) @property def data(self): _data = super().data - _data["permission_sets"] = [] - for field in _data: - if "perms" in field: - _data["permission_sets"].append(_data[field]) + _data.pop("csrf_token", None) + perm_sets = [] + if _data["perms_app_mgmt"]: + perm_sets.append(PermissionSets.EDIT_PORTFOLIO_APPLICATION_MANAGEMENT) + + if _data["perms_funding"]: + perm_sets.append(PermissionSets.EDIT_PORTFOLIO_FUNDING) + + if _data["perms_reporting"]: + perm_sets.append(PermissionSets.EDIT_PORTFOLIO_REPORTS) + + if _data["perms_portfolio_mgmt"]: + perm_sets.append(PermissionSets.EDIT_PORTFOLIO_ADMIN) + + _data["permission_sets"] = perm_sets return _data -class MembersPermissionsForm(BaseForm): - members_permissions = FieldList(FormField(PermissionsForm)) - - -class NewForm(BaseForm): +class NewForm(PermissionsForm): user_data = FormField(BaseNewMemberForm) - permission_sets = FormField(PermissionsForm) - - @property - def update_data(self): - return { - "permission_sets": self.data.get("permission_sets").get("permission_sets"), - **self.data.get("user_data"), - } class AssignPPOCForm(PermissionsForm): diff --git a/atst/models/portfolio_role.py b/atst/models/portfolio_role.py index 53204e82..6d34dd97 100644 --- a/atst/models/portfolio_role.py +++ b/atst/models/portfolio_role.py @@ -12,17 +12,6 @@ from atst.utils import first_or_none from atst.models.mixins.auditable import record_permission_sets_updates -MEMBER_STATUSES = { - "active": "Active", - "revoked": "Invite revoked", - "expired": "Invite expired", - "error": "Error on invite", - "pending": "Pending", - "unknown": "Unknown errors", - "disabled": "Disabled", -} - - class Status(Enum): ACTIVE = "active" DISABLED = "disabled" @@ -90,23 +79,23 @@ class PortfolioRole( @property def display_status(self): if self.status == Status.ACTIVE: - return MEMBER_STATUSES["active"] + return "active" elif self.status == Status.DISABLED: - return MEMBER_STATUSES["disabled"] + return "disabled" elif self.latest_invitation: if self.latest_invitation.is_revoked: - return MEMBER_STATUSES["revoked"] + return "invite_revoked" elif self.latest_invitation.is_rejected_wrong_user: - return MEMBER_STATUSES["error"] + return "invite_error" elif ( self.latest_invitation.is_rejected_expired or self.latest_invitation.is_expired ): - return MEMBER_STATUSES["expired"] + return "invite_expired" else: - return MEMBER_STATUSES["pending"] + return "invite_pending" else: - return MEMBER_STATUSES["unknown"] + return "unknown" def has_permission_set(self, perm_set_name): return first_or_none( diff --git a/atst/routes/portfolios/admin.py b/atst/routes/portfolios/admin.py index 1b4ac711..699bdfab 100644 --- a/atst/routes/portfolios/admin.py +++ b/atst/routes/portfolios/admin.py @@ -17,63 +17,51 @@ from atst.utils.flash import formatted_flash as flash from atst.domain.exceptions import UnauthorizedError -def permission_str(member, edit_perm_set, view_perm_set): - if member.has_permission_set(edit_perm_set): - return edit_perm_set - else: - return view_perm_set - - -def serialize_member_form_data(member): - return { - "member_name": member.full_name, - "member_id": member.id, - "perms_app_mgmt": permission_str( - member, - PermissionSets.EDIT_PORTFOLIO_APPLICATION_MANAGEMENT, - PermissionSets.VIEW_PORTFOLIO_APPLICATION_MANAGEMENT, +def filter_perm_sets_data(member): + perm_sets_data = { + "perms_portfolio_mgmt": bool( + member.has_permission_set(PermissionSets.EDIT_PORTFOLIO_ADMIN) ), - "perms_funding": permission_str( - member, - PermissionSets.EDIT_PORTFOLIO_FUNDING, - PermissionSets.VIEW_PORTFOLIO_FUNDING, + "perms_app_mgmt": bool( + member.has_permission_set( + PermissionSets.EDIT_PORTFOLIO_APPLICATION_MANAGEMENT + ) ), - "perms_reporting": permission_str( - member, - PermissionSets.EDIT_PORTFOLIO_REPORTS, - PermissionSets.VIEW_PORTFOLIO_REPORTS, + "perms_funding": bool( + member.has_permission_set(PermissionSets.EDIT_PORTFOLIO_FUNDING) ), - "perms_portfolio_mgmt": permission_str( - member, - PermissionSets.EDIT_PORTFOLIO_ADMIN, - PermissionSets.VIEW_PORTFOLIO_ADMIN, + "perms_reporting": bool( + member.has_permission_set(PermissionSets.EDIT_PORTFOLIO_REPORTS) ), } + return perm_sets_data -def get_members_data(portfolio): - members = sorted( - [serialize_member_form_data(member) for member in portfolio.members], - key=lambda member: member["member_name"], - ) - for member in members: - if member["member_id"] == portfolio.owner_role.id: - ppoc = member - members.remove(member) - members.insert(0, ppoc) - return members + +def filter_members_data(members_list, portfolio): + members_data = [] + for member in members_list: + members_data.append( + { + "role_id": member.id, + "user_name": member.user_name, + "permission_sets": filter_perm_sets_data(member), + "status": member.display_status, + "ppoc": PermissionSets.PORTFOLIO_POC in member.permission_sets, + # add in stuff here for forms + } + ) + + return sorted(members_data, key=lambda member: member["user_name"]) def render_admin_page(portfolio, form=None): pagination_opts = Paginator.get_pagination_opts(http_request) audit_events = AuditLog.get_portfolio_events(portfolio, pagination_opts) - members_data = get_members_data(portfolio) portfolio_form = PortfolioForm(obj=portfolio) - member_perms_form = member_forms.MembersPermissionsForm( - data={"members_permissions": members_data} - ) - + member_list = portfolio.members assign_ppoc_form = member_forms.AssignPPOCForm() + for pf_role in portfolio.roles: if pf_role.user != portfolio.owner and pf_role.is_active: assign_ppoc_form.role_id.choices += [(pf_role.id, pf_role.full_name)] @@ -87,13 +75,12 @@ def render_admin_page(portfolio, form=None): "portfolios/admin.html", form=form, portfolio_form=portfolio_form, - member_perms_form=member_perms_form, - member_form=member_forms.NewForm(), + members=filter_members_data(member_list, portfolio), + new_manager_form=member_forms.NewForm(), assign_ppoc_form=assign_ppoc_form, portfolio=portfolio, audit_events=audit_events, user=g.current_user, - ppoc_id=members_data[0].get("member_id"), current_member_id=current_member_id, applications_count=len(portfolio.applications), ) @@ -106,34 +93,6 @@ def admin(portfolio_id): return render_admin_page(portfolio) -@portfolios_bp.route("/portfolios//admin", methods=["POST"]) -@user_can(Permissions.EDIT_PORTFOLIO_USERS, message="view portfolio admin page") -def edit_members(portfolio_id): - portfolio = Portfolios.get_for_update(portfolio_id) - member_perms_form = member_forms.MembersPermissionsForm(http_request.form) - - if member_perms_form.validate(): - for subform in member_perms_form.members_permissions: - member_id = subform.member_id.data - member = PortfolioRoles.get_by_id(member_id) - if member is not portfolio.owner_role: - new_perm_set = subform.data["permission_sets"] - PortfolioRoles.update(member, new_perm_set) - - flash("update_portfolio_members", portfolio=portfolio) - - return redirect( - url_for( - "portfolios.admin", - portfolio_id=portfolio_id, - fragment="portfolio-members", - _anchor="portfolio-members", - ) - ) - else: - return render_admin_page(portfolio) - - @portfolios_bp.route("/portfolios//update_ppoc", methods=["POST"]) @user_can(Permissions.EDIT_PORTFOLIO_POC, message="update portfolio ppoc") def update_ppoc(portfolio_id): diff --git a/atst/routes/portfolios/invitations.py b/atst/routes/portfolios/invitations.py index 8f2e4701..09a22d1f 100644 --- a/atst/routes/portfolios/invitations.py +++ b/atst/routes/portfolios/invitations.py @@ -79,7 +79,7 @@ def invite_member(portfolio_id): if form.validate(): try: - invite = Portfolios.invite(portfolio, g.current_user, form.update_data) + invite = Portfolios.invite(portfolio, g.current_user, form.data) send_portfolio_invitation( invitee_email=invite.email, inviter_name=g.current_user.full_name, diff --git a/script/seed_sample.py b/script/seed_sample.py index 47b7efb9..72c16c6c 100644 --- a/script/seed_sample.py +++ b/script/seed_sample.py @@ -159,7 +159,7 @@ def get_users(): def add_members_to_portfolio(portfolio): for user_data in PORTFOLIO_USERS: - invite = Portfolios.invite(portfolio, portfolio.owner, user_data) + invite = Portfolios.invite(portfolio, portfolio.owner, {"user_data": user_data}) profile = { k: user_data[k] for k in user_data if k not in ["dod_id", "permission_sets"] } diff --git a/styles/atat.scss b/styles/atat.scss index 4c8aa263..0134dd89 100644 --- a/styles/atat.scss +++ b/styles/atat.scss @@ -38,6 +38,7 @@ @import "components/dod_login_notice.scss"; @import "components/sticky_cta.scss"; @import "components/error_page.scss"; +@import "components/member_form.scss"; @import "sections/login"; @import "sections/home"; diff --git a/styles/components/_member_form.scss b/styles/components/_member_form.scss new file mode 100644 index 00000000..5cbeaf87 --- /dev/null +++ b/styles/components/_member_form.scss @@ -0,0 +1,61 @@ +.member-form { + text-align: left; + + input[type="checkbox"] + label::before { + margin-left: 0; + } + + .input__inline-fields { + text-align: left; + + .usa-input__choices label { + font-weight: $font-bold; + } + } + + .input__inline-fields { + padding: $gap * 2; + border: 1px solid $color-gray-lighter; + + &.checked { + border: 1px solid $color-blue; + } + + label { + font-weight: $font-bold; + } + + p.usa-input__help { + margin-bottom: 0; + padding-left: 3rem; + } + } + + .user-info { + .usa-input { + width: 45rem; + + input, + label, + .usa-input__message { + max-width: unset; + } + + label .icon-validation { + left: unset; + right: -$gap * 4; + } + + &--validation--phoneExt { + width: 18rem; + } + } + } +} + +#modal--add-app-mem, +#modal--add-portfolio-manager { + .modal__body { + min-width: 75rem; + } +} diff --git a/styles/components/_portfolio_layout.scss b/styles/components/_portfolio_layout.scss index d7583b80..fffc468f 100644 --- a/styles/components/_portfolio_layout.scss +++ b/styles/components/_portfolio_layout.scss @@ -5,13 +5,6 @@ } margin-left: 2 * $gap; - - .line { - box-sizing: border-box; - height: 2px; - width: 100%; - border: 1px solid $color-gray-lightest; - } } .portfolio-header { @@ -40,36 +33,6 @@ } } - &__budget { - font-size: $small-font-size; - align-items: center; - - .icon-tooltip { - margin-left: -$gap / 2; - } - - button { - margin: 0; - padding: 0; - } - - &--dollars { - font-size: $h2-font-size; - font-weight: bold; - } - - &--amount { - white-space: nowrap; - } - - &--cents { - font-size: 2rem; - margin-top: 0.75rem; - margin-left: -0.7rem; - font-weight: bold; - } - } - .links { justify-content: center; font-size: $small-font-size; @@ -109,22 +72,6 @@ } } } - - .column-left { - width: 12.5rem; - float: left; - } - - .column-right { - margin-left: -0.4rem; - } - - .unfunded { - color: $color-red; - .icon { - @include icon-color($color-red); - } - } } @mixin subheading { @@ -138,6 +85,10 @@ .portfolio-content { margin: (4 * $gap) $gap 0 $gap; + .panel { + padding-bottom: 2rem; + } + a.add-new-button { display: inherit; margin-left: auto; @@ -157,44 +108,6 @@ } } - input.usa-button.usa-button-primary { - width: 9rem; - height: 4rem; - } - - select { - padding-left: 1.2rem; - } - - .members-table-ppoc { - select::-ms-expand { - display: none; - color: $color-gray; - } - - select { - -webkit-appearance: none; - -moz-appearance: none; - appearance: none; - display: block; - width: 100%; - float: right; - margin: 5px 0px; - padding: 0px 24px; - background-image: none; - -ms-word-break: normal; - word-break: normal; - padding-right: 3rem; - padding-left: 1.2rem; - color: $color-gray; - } - - select:hover { - box-shadow: none; - color: $color-gray; - } - } - a.modal-link.icon-link { float: right; diff --git a/styles/elements/_action_group.scss b/styles/elements/_action_group.scss index 73cad181..fe375f67 100644 --- a/styles/elements/_action_group.scss +++ b/styles/elements/_action_group.scss @@ -12,6 +12,7 @@ .usa-button, a { margin: 0 0 0 $gap; + cursor: pointer; @include media($medium-screen) { margin: 0 0 0 ($gap * 2); diff --git a/styles/elements/_tables.scss b/styles/elements/_tables.scss index 5f65830c..2a742929 100644 --- a/styles/elements/_tables.scss +++ b/styles/elements/_tables.scss @@ -21,24 +21,8 @@ table.atat-table { text-align: right; } - &--align-center { - text-align: center; - } - - &--shrink { - width: 1%; - } - - &--expand { - width: 100%; - } - - &--hide-small { - display: none; - - @include media($medium-screen) { - display: table-cell; - } + &--third { + width: 33%; } } } @@ -83,28 +67,6 @@ table.atat-table { @include panel-margin; - &__header { - @include panel-base; - @include panel-theme-default; - - border-top: none; - border-bottom: 0; - display: flex; - flex-direction: row; - flex-wrap: wrap; - justify-content: space-between; - align-items: center; - padding: $gap * 2; - - &__title { - @include h4; - - font-size: $lead-font-size; - justify-content: space-between; - flex: 2; - } - } - table { margin-bottom: 0; } diff --git a/styles/sections/_application_edit.scss b/styles/sections/_application_edit.scss index 8ff79b7b..8282cb64 100644 --- a/styles/sections/_application_edit.scss +++ b/styles/sections/_application_edit.scss @@ -23,66 +23,6 @@ } } -#modal--add-app-mem, -.form-content--app-mem { - text-align: left; - - .modal__body { - min-width: 75rem; - } - - input[type="checkbox"] + label::before { - margin-left: 0; - } - - .input__inline-fields { - text-align: left; - - .usa-input__choices label { - font-weight: $font-bold; - } - } - - .input__inline-fields { - padding: $gap * 2; - border: 1px solid $color-gray-lighter; - - &.checked { - border: 1px solid $color-blue; - } - - label { - font-weight: $font-bold; - } - - p.usa-input__help { - margin-bottom: 0; - padding-left: 3rem; - } - } - - .application-member__user-info { - .usa-input { - width: 45rem; - - input, - label, - .usa-input__message { - max-width: unset; - } - - label .icon-validation { - left: unset; - right: -$gap * 4; - } - - &--validation--phoneExt { - width: 18rem; - } - } - } -} - .environment-roles { padding: 0 ($gap * 3) ($gap * 3); diff --git a/templates/applications/fragments/member_form_fields.html b/templates/applications/fragments/member_form_fields.html index 3aa97687..707653fa 100644 --- a/templates/applications/fragments/member_form_fields.html +++ b/templates/applications/fragments/member_form_fields.html @@ -118,7 +118,7 @@ {% endmacro %} {% macro InfoFields(member_form) %} -