diff --git a/atst/domain/authz.py b/atst/domain/authz.py index 20bd539f..65db9894 100644 --- a/atst/domain/authz.py +++ b/atst/domain/authz.py @@ -1,5 +1,6 @@ from atst.domain.workspace_users import WorkspaceUsers from atst.models.permissions import Permissions +from atst.domain.exceptions import UnauthorizedError class Authorization(object): @@ -23,3 +24,8 @@ class Authorization(object): return True return False + + @classmethod + def check_workspace_permission(cls, user, workspace, permission, message): + if not Authorization.has_workspace_permission(user, workspace, permission): + raise UnauthorizedError(user, message) diff --git a/atst/domain/workspace_users.py b/atst/domain/workspace_users.py index 6fb761c3..d37ec736 100644 --- a/atst/domain/workspace_users.py +++ b/atst/domain/workspace_users.py @@ -30,6 +30,21 @@ class WorkspaceUsers(object): return WorkspaceUser(user, workspace_role) + @classmethod + def _get_workspace_role(cls, user, workspace_id): + try: + existing_workspace_role = ( + db.session.query(WorkspaceRole) + .filter( + WorkspaceRole.user == user, + WorkspaceRole.workspace_id == workspace_id, + ) + .one() + ) + return existing_workspace_role + except NoResultFound: + raise NotFoundError("workspace role") + @classmethod def add(cls, user, workspace_id, role_name): role = Roles.get(role_name) @@ -57,6 +72,16 @@ class WorkspaceUsers(object): return WorkspaceUser(user, new_workspace_role) + @classmethod + def update_role(cls, member, workspace_id, role_name): + new_role = Roles.get(role_name) + workspace_role = WorkspaceUsers._get_workspace_role(member.user, workspace_id) + workspace_role.role = new_role + + db.session.add(workspace_role) + db.session.commit() + return WorkspaceUser(member.user, workspace_role) + @classmethod def add_many(cls, workspace_id, workspace_user_dicts): workspace_users = [] diff --git a/atst/domain/workspaces.py b/atst/domain/workspaces.py index 6570a831..c1af06ea 100644 --- a/atst/domain/workspaces.py +++ b/atst/domain/workspaces.py @@ -38,10 +38,10 @@ class Workspaces(object): @classmethod def get_for_update(cls, user, workspace_id): workspace = Workspaces.get(user, workspace_id) - if not Authorization.has_workspace_permission( - user, workspace, Permissions.ADD_APPLICATION_IN_WORKSPACE - ): - raise UnauthorizedError(user, "add project") + Authorization.check_workspace_permission( + user, workspace, Permissions.ADD_APPLICATION_IN_WORKSPACE, "add project" + ) + return workspace @classmethod @@ -65,10 +65,12 @@ class Workspaces(object): @classmethod def create_member(cls, user, workspace, data): - if not Authorization.has_workspace_permission( - user, workspace, Permissions.ASSIGN_AND_UNASSIGN_ATAT_ROLE - ): - raise UnauthorizedError(user, "create workspace member") + Authorization.check_workspace_permission( + user, + workspace, + Permissions.ASSIGN_AND_UNASSIGN_ATAT_ROLE, + "create workspace member", + ) new_user = Users.get_or_create_by_dod_id( data["dod_id"], @@ -81,6 +83,17 @@ class Workspaces(object): ) return workspace_user + @classmethod + def update_member(cls, user, workspace, member, role_name): + Authorization.check_workspace_permission( + user, + workspace, + Permissions.ASSIGN_AND_UNASSIGN_ATAT_ROLE, + "edit workspace member", + ) + + return WorkspaceUsers.update_role(member, workspace.id, role_name) + @classmethod def _create_workspace_role(cls, user, workspace, role_name): role = Roles.get(role_name) diff --git a/atst/forms/edit_member.py b/atst/forms/edit_member.py new file mode 100644 index 00000000..e76df6d0 --- /dev/null +++ b/atst/forms/edit_member.py @@ -0,0 +1,13 @@ +from flask_wtf import Form +from wtforms.validators import Optional + +from atst.forms.fields import SelectField + +from .data import WORKSPACE_ROLES + + +class EditMemberForm(Form): + + workspace_role = SelectField( + "Workspace Role", choices=WORKSPACE_ROLES, validators=[Optional()] + ) diff --git a/atst/models/workspace.py b/atst/models/workspace.py index aca024a4..edeba529 100644 --- a/atst/models/workspace.py +++ b/atst/models/workspace.py @@ -5,37 +5,7 @@ from atst.models import Base from atst.models.types import Id from atst.models.mixins import TimestampsMixin from atst.utils import first_or_none - - -MOCK_MEMBERS = [ - { - "first_name": "Danny", - "last_name": "Knight", - "email": "dknight@thenavy.mil", - "dod_id": "1257892124", - "workspace_role": "Developer", - "status": "Pending", - "num_projects": "4", - }, - { - "first_name": "Mario", - "last_name": "Hudson", - "email": "mhudson@thearmy.mil", - "dod_id": "4357892125", - "workspace_role": "CCPO", - "status": "Active", - "num_projects": "0", - }, - { - "first_name": "Louise", - "last_name": "Greer", - "email": "lgreer@theairforce.mil", - "dod_id": "7257892125", - "workspace_role": "Admin", - "status": "Pending", - "num_projects": "43", - }, -] +from atst.models.workspace_user import WorkspaceUser class Workspace(Base, TimestampsMixin): @@ -68,4 +38,4 @@ class Workspace(Base, TimestampsMixin): @property def members(self): - return MOCK_MEMBERS + return [WorkspaceUser(role.user, role) for role in self.roles] diff --git a/atst/models/workspace_user.py b/atst/models/workspace_user.py index 5e3ee1ed..3c92707e 100644 --- a/atst/models/workspace_user.py +++ b/atst/models/workspace_user.py @@ -16,3 +16,19 @@ class WorkspaceUser(object): def workspace_id(self): return self.workspace_role.workspace_id + + @property + def user_id(self): + return self.user.id + + @property + def user_name(self): + return self.user.full_name + + @property + def role(self): + return self.workspace_role.role.name + + @property + def status(self): + return "radical" diff --git a/atst/routes/workspaces.py b/atst/routes/workspaces.py index 665d266c..5ded0500 100644 --- a/atst/routes/workspaces.py +++ b/atst/routes/workspaces.py @@ -9,9 +9,11 @@ from flask import ( from atst.domain.exceptions import UnauthorizedError from atst.domain.workspaces import Workspaces +from atst.domain.workspace_users import WorkspaceUsers from atst.domain.projects import Projects from atst.forms.new_project import NewProjectForm from atst.forms.new_member import NewMemberForm +from atst.forms.edit_member import EditMemberForm from atst.domain.authz import Authorization from atst.models.permissions import Permissions @@ -114,8 +116,58 @@ def create_member(workspace_id): url_for( "workspaces.workspace_members", workspace_id=workspace.id, - newMemberName=new_member.user.full_name, + newMemberName=new_member.user_name, ) ) else: return render_template("member_new.html", workspace=workspace, form=form) + + +@bp.route("/workspaces//members//member_edit") +def view_member(workspace_id, member_id): + workspace = Workspaces.get(g.current_user, workspace_id) + Authorization.check_workspace_permission( + g.current_user, + workspace, + Permissions.ASSIGN_AND_UNASSIGN_ATAT_ROLE, + "edit this workspace user", + ) + member = WorkspaceUsers.get(workspace_id, member_id) + form = EditMemberForm(workspace_role=member.role) + return render_template( + "member_edit.html", form=form, workspace=workspace, member=member + ) + + +@bp.route( + "/workspaces//members//member_edit", methods=["POST"] +) +def update_member(workspace_id, member_id): + workspace = Workspaces.get(g.current_user, workspace_id) + Authorization.check_workspace_permission( + g.current_user, + workspace, + Permissions.ASSIGN_AND_UNASSIGN_ATAT_ROLE, + "edit this workspace user", + ) + member = WorkspaceUsers.get(workspace_id, member_id) + form = EditMemberForm(http_request.form) + + if form.validate(): + role = None + if form.data["workspace_role"] != member.role: + role = form.data["workspace_role"] + Workspaces.update_member(g.current_user, workspace, member, role) + + return redirect( + url_for( + "workspaces.workspace_members", + workspace_id=workspace.id, + memberName=member.user_name, + updatedRole=role, + ) + ) + else: + return render_template( + "member_edit.html", form=form, workspace=workspace, member=member + ) diff --git a/styles/components/_selector.scss b/styles/components/_selector.scss index 7b95b325..030c5ea5 100644 --- a/styles/components/_selector.scss +++ b/styles/components/_selector.scss @@ -14,7 +14,7 @@ width: 100%; height: $input-height; margin: 0; - padding: $input-padding-vertical 0.7em; + padding: $input-padding-vertical ($gap*5) $input-padding-vertical 0.7em; line-height: $input-line-height; color: $color-base; font-size: $base-font-size; @@ -42,6 +42,9 @@ label { padding: 0; + dt { + font-weight: $font-bold; + } } } } diff --git a/styles/sections/_member_edit.scss b/styles/sections/_member_edit.scss index fc9415a5..902ece36 100644 --- a/styles/sections/_member_edit.scss +++ b/styles/sections/_member_edit.scss @@ -3,23 +3,6 @@ padding: $gap*2; justify-content: space-between; - dl { - margin: 0; - - > div { - margin-bottom: $gap; - } - } - - dt { - font-weight: normal; - color: $color-gray; - } - - dd { - display: inline; - } - .member-card__header { display: flex; flex-direction: column; @@ -35,11 +18,13 @@ margin: 0; display: flex; margin-top: $gap; + fieldset { + margin: 0; + } - label { + legend { flex: none; margin-top: $gap*1.5; - font-weight: $font-normal; margin-right: $gap*2; } } @@ -51,5 +36,22 @@ .icon-link { margin: 0 -$gap; } + + dl { + margin: 0; + + > div { + margin-bottom: $gap; + } + } + + dt { + font-weight: normal; + color: $color-gray; + } + + dd { + display: inline; + } } -} \ No newline at end of file +} diff --git a/templates/member_edit.html b/templates/member_edit.html index db52347b..ea213976 100644 --- a/templates/member_edit.html +++ b/templates/member_edit.html @@ -2,223 +2,142 @@ {% from "components/icon.html" import Icon %} {% from "components/modal.html" import Modal %} +{% from "components/selector.html" import Selector %} {% block content %} -
-
-

Danny Knight

+
+ {{ form.csrf_token }} + +
+
+

{{ member.user.full_name }}

+ +
+ {{ Selector(form.workspace_role) }} +
-
- -
- +
+
+
+
DOD ID:
+
{{ member.user.dod_id }}
+
+
+
Email:
+
{{ member.user.email }}
+
+
+ edit account details +
-
-
-
-
DOD ID:
-
789
-
-
-
Email:
-
knight@mil.gov
-
-
- edit account details + +
+
+

Manage Access
Grant access to an environment

+
-
-
-
-

Manage Access
Grant access to an environment

+ -
- -
- - - -
- - -{% call Modal(name='rolesModal', dismissable=False) %} -
-
-

- Environment access for Danny Knight -
Project Name - Environment Name
-

-
- -
-
    -
  • - - +
    +
    -{% endcall %} +
    + +
    -
    - -
    +
    + + + {{ Icon('x') }} + Cancel + +
    -
    - -
    - - +
  • diff --git a/templates/workspace_members.html b/templates/workspace_members.html index bf61ea3d..34761341 100644 --- a/templates/workspace_members.html +++ b/templates/workspace_members.html @@ -33,6 +33,19 @@ ) }} {% endif %} +{% set member_name = request.args.get("memberName") %} +{% set updated_role = request.args.get("updatedRole") %} +{% if updated_role %} + {% set message -%} +

    {{ member_name }}'s role was successfully updated to {{ updated_role }}

    + {%- endset %} + + {{ Alert('Workspace role updated successfully', + message=message, + level='success' + ) }} +{% endif %} +