diff --git a/alembic/versions/4c425f17bfe8_add_edit_workspace_information_.py b/alembic/versions/4c425f17bfe8_add_edit_workspace_information_.py new file mode 100644 index 00000000..e27074f2 --- /dev/null +++ b/alembic/versions/4c425f17bfe8_add_edit_workspace_information_.py @@ -0,0 +1,43 @@ +"""add edit workspace information permission + +Revision ID: 4c425f17bfe8 +Revises: 2572be7fb7fc +Create Date: 2018-09-17 13:14:38.781744 + +""" +from alembic import op +from sqlalchemy.orm.session import Session + +from atst.models.role import Role +from atst.models.permissions import Permissions + + +# revision identifiers, used by Alembic. +revision = '4c425f17bfe8' +down_revision = '2572be7fb7fc' +branch_labels = None +depends_on = None + + +def upgrade(): + session = Session(bind=op.get_bind()) + + owner_and_admin = session.query(Role).filter(Role.name.in_(["owner", "admin"])).all() + for role in owner_and_admin: + role.add_permission(Permissions.EDIT_WORKSPACE_INFORMATION) + session.add(role) + + session.flush() + session.commit() + + +def downgrade(): + session = Session(bind=op.get_bind()) + + owner_and_admin = session.query(Role).filter(Role.name.in_(["owner", "admin"])).all() + for role in owner_and_ccpo: + role.remove_permission(Permissions.EDIT_WORKSPACE_INFORMATION) + session.add(role) + + session.flush() + session.commit() diff --git a/atst/domain/workspaces/workspaces.py b/atst/domain/workspaces/workspaces.py index 636e55ee..61996955 100644 --- a/atst/domain/workspaces/workspaces.py +++ b/atst/domain/workspaces/workspaces.py @@ -27,7 +27,7 @@ class Workspaces(object): return ScopedWorkspace(user, workspace) @classmethod - def get_for_update(cls, user, workspace_id): + def get_for_update_projects(cls, user, workspace_id): workspace = WorkspacesQuery.get(workspace_id) Authorization.check_workspace_permission( user, workspace, Permissions.ADD_APPLICATION_IN_WORKSPACE, "add project" @@ -35,6 +35,18 @@ class Workspaces(object): return workspace + @classmethod + def get_for_update_information(cls, user, workspace_id): + workspace = WorkspacesQuery.get(workspace_id) + Authorization.check_workspace_permission( + user, + workspace, + Permissions.EDIT_WORKSPACE_INFORMATION, + "update workspace information", + ) + + return workspace + @classmethod def get_by_request(cls, request): return WorkspacesQuery.get_by_request(request) @@ -98,3 +110,10 @@ class Workspaces(object): workspace_role = WorkspacesQuery.create_workspace_role(user, role, workspace) WorkspacesQuery.add_and_commit(workspace_role) return workspace_role + + @classmethod + def update(cls, workspace, new_data): + if "name" in new_data: + workspace.name = new_data["name"] + + WorkspacesQuery.add_and_commit(workspace) diff --git a/atst/forms/workspace.py b/atst/forms/workspace.py new file mode 100644 index 00000000..76dd6aae --- /dev/null +++ b/atst/forms/workspace.py @@ -0,0 +1,17 @@ +from wtforms.fields import StringField +from wtforms.validators import Length + +from .forms import ValidatedForm + + +class WorkspaceForm(ValidatedForm): + name = StringField( + "Workspace Name", + validators=[ + Length( + min=4, + max=50, + message="Workspace names must be at least 4 and not more than 50 characters", + ) + ], + ) diff --git a/atst/models/permissions.py b/atst/models/permissions.py index c39d5b71..3888fe2b 100644 --- a/atst/models/permissions.py +++ b/atst/models/permissions.py @@ -20,6 +20,7 @@ class Permissions(object): VIEW_ASSIGNED_ATAT_ROLE_CONFIGURATIONS = "view_assigned_atat_role_configurations" VIEW_ASSIGNED_CSP_ROLE_CONFIGURATIONS = "view_assigned_csp_role_configurations" + EDIT_WORKSPACE_INFORMATION = "edit_workspace_information" DEACTIVATE_WORKSPACE = "deactivate_workspace" VIEW_ATAT_PERMISSIONS = "view_atat_permissions" TRANSFER_OWNERSHIP_OF_WORKSPACE = "transfer_ownership_of_workspace" diff --git a/atst/routes/workspaces.py b/atst/routes/workspaces.py index d4465f32..673719a5 100644 --- a/atst/routes/workspaces.py +++ b/atst/routes/workspaces.py @@ -17,6 +17,7 @@ from atst.domain.workspace_users import WorkspaceUsers from atst.forms.new_project import NewProjectForm from atst.forms.new_member import NewMemberForm from atst.forms.edit_member import EditMemberForm +from atst.forms.workspace import WorkspaceForm from atst.domain.authz import Authorization from atst.models.permissions import Permissions @@ -50,12 +51,32 @@ def workspaces(): return render_template("workspaces/index.html", page=5, workspaces=workspaces) +@bp.route("/workspaces//edit") +def workspace(workspace_id): + workspace = Workspaces.get_for_update_information(g.current_user, workspace_id) + form = WorkspaceForm(data={"name": workspace.name}) + return render_template("workspaces/edit.html", form=form, workspace=workspace) + + @bp.route("/workspaces//projects") def workspace_projects(workspace_id): workspace = Workspaces.get(g.current_user, workspace_id) return render_template("workspaces/projects/index.html", workspace=workspace) +@bp.route("/workspaces//edit", methods=["POST"]) +def edit_workspace(workspace_id): + workspace = Workspaces.get_for_update_information(g.current_user, workspace_id) + form = WorkspaceForm(http_request.form) + if form.validate(): + Workspaces.update(workspace, form.data) + return redirect( + url_for("workspaces.workspace_projects", workspace_id=workspace.id) + ) + else: + return render_template("workspaces/edit.html", form=form, workspace=workspace) + + @bp.route("/workspaces/") def show_workspace(workspace_id): return redirect(url_for("workspaces.workspace_projects", workspace_id=workspace_id)) @@ -98,7 +119,7 @@ def workspace_reports(workspace_id): @bp.route("/workspaces//projects/new") def new_project(workspace_id): - workspace = Workspaces.get_for_update(g.current_user, workspace_id) + workspace = Workspaces.get_for_update_projects(g.current_user, workspace_id) form = NewProjectForm() return render_template( "workspaces/projects/new.html", workspace=workspace, form=form @@ -107,7 +128,7 @@ def new_project(workspace_id): @bp.route("/workspaces//projects/new", methods=["POST"]) def create_project(workspace_id): - workspace = Workspaces.get_for_update(g.current_user, workspace_id) + workspace = Workspaces.get_for_update_projects(g.current_user, workspace_id) form = NewProjectForm(http_request.form) if form.validate(): @@ -130,7 +151,7 @@ def create_project(workspace_id): @bp.route("/workspaces//projects//edit") def edit_project(workspace_id, project_id): - workspace = Workspaces.get_for_update(g.current_user, workspace_id) + workspace = Workspaces.get_for_update_projects(g.current_user, workspace_id) project = Projects.get(g.current_user, workspace, project_id) form = NewProjectForm( name=project.name, diff --git a/js/lib/input_validations.js b/js/lib/input_validations.js index d326d0b8..1696facc 100644 --- a/js/lib/input_validations.js +++ b/js/lib/input_validations.js @@ -78,5 +78,11 @@ export default { match: /[0-9]{2}\w?$/, unmask: [], validationError: 'Please enter a valid BA Code. Note that it should be two digits, followed by a letter.' - } + }, + workspaceName: { + mask: false, + match: /^.{4,50}$/, + unmask: [], + validationError: 'Workspace names must be at least 4 and not more than 50 characters' + }, } diff --git a/styles/elements/_inputs.scss b/styles/elements/_inputs.scss index babd6ce5..6cd22272 100644 --- a/styles/elements/_inputs.scss +++ b/styles/elements/_inputs.scss @@ -194,6 +194,7 @@ &--validation { &--anything, + &--workspaceName, &--email { input { max-width: 30em; diff --git a/templates/navigation/workspace_navigation.html b/templates/navigation/workspace_navigation.html index 5ef3ec8c..fd767205 100644 --- a/templates/navigation/workspace_navigation.html +++ b/templates/navigation/workspace_navigation.html @@ -35,5 +35,15 @@ href=url_for("workspaces.workspace_reports", workspace_id=workspace.id), active=request.url_rule.rule.startswith('/workspaces//reports') ) }} + + {% if user_can(permissions.EDIT_WORKSPACE_INFORMATION) %} + {{ SidenavItem( + "Workspace Settings", + href=url_for("workspaces.workspace", workspace_id=workspace.id), + active=request.url_rule.rule.startswith('/workspaces//edit'), + subnav=None + ) }} + {% endif %} + diff --git a/templates/workspaces/edit.html b/templates/workspaces/edit.html new file mode 100644 index 00000000..45e35656 --- /dev/null +++ b/templates/workspaces/edit.html @@ -0,0 +1,43 @@ +{% extends "workspaces/base.html" %} + +{% from "components/icon.html" import Icon %} +{% from "components/alert.html" import Alert %} +{% from "components/text_input.html" import TextInput %} + + +{% block workspace_content %} + +{% if form.errors %} + {{ Alert('There were some errors', + message="

Please see below.

", + level='error' + ) }} +{% endif %} + +
+ {{ form.csrf_token }} + +
+ +
+

Workspace Settings

+
+ +
+ {{ TextInput(form.name, validation="workspaceName") }} +
+
+ + + + + +
+ +{% endblock %} diff --git a/tests/domain/test_workspaces.py b/tests/domain/test_workspaces.py index c64c34d5..5ff8aa69 100644 --- a/tests/domain/test_workspaces.py +++ b/tests/domain/test_workspaces.py @@ -63,16 +63,16 @@ def test_workspaces_get_ensures_user_is_in_workspace(workspace, workspace_owner) Workspaces.get(outside_user, workspace.id) -def test_get_for_update_allows_owner(workspace, workspace_owner): - Workspaces.get_for_update(workspace_owner, workspace.id) +def test_get_for_update_projects_allows_owner(workspace, workspace_owner): + Workspaces.get_for_update_projects(workspace_owner, workspace.id) -def test_get_for_update_blocks_developer(workspace): +def test_get_for_update_projects_blocks_developer(workspace): developer = UserFactory.create() WorkspaceUsers.add(developer, workspace.id, "developer") with pytest.raises(UnauthorizedError): - Workspaces.get_for_update(developer, workspace.id) + Workspaces.get_for_update_projects(developer, workspace.id) def test_can_create_workspace_user(workspace, workspace_owner): @@ -234,3 +234,19 @@ def test_for_user_returns_all_workspaces_for_ccpo(workspace, workspace_owner): sams_workspaces = Workspaces.for_user(sam) assert len(sams_workspaces) == 2 + + +def test_get_for_update_information(): + workspace_owner = UserFactory.create() + workspace = Workspaces.create(RequestFactory.create(creator=workspace_owner)) + owner_ws = Workspaces.get_for_update_information(workspace_owner, workspace.id) + assert workspace == owner_ws + + admin = UserFactory.create() + Workspaces.add_member(workspace, admin, "admin") + admin_ws = Workspaces.get_for_update_information(admin, workspace.id) + assert workspace == admin_ws + + ccpo = UserFactory.from_atat_role("ccpo") + with pytest.raises(UnauthorizedError): + Workspaces.get_for_update_information(ccpo, workspace.id) diff --git a/tests/routes/test_workspaces.py b/tests/routes/test_workspaces.py index 202a8127..fcab09d2 100644 --- a/tests/routes/test_workspaces.py +++ b/tests/routes/test_workspaces.py @@ -1,3 +1,5 @@ +from flask import url_for + from tests.factories import UserFactory, WorkspaceFactory from atst.domain.workspaces import Workspaces from atst.models.workspace_user import WorkspaceUser @@ -51,3 +53,17 @@ def test_user_without_permission_has_no_add_member_link(client, user_session): 'href="/workspaces/{}/members/new"'.format(workspace.id).encode() not in response.data ) + + +def test_update_workspace_name(client, user_session): + user = UserFactory.create() + workspace = WorkspaceFactory.create() + Workspaces._create_workspace_role(user, workspace, "admin") + user_session(user) + response = client.post( + url_for("workspaces.edit_workspace", workspace_id=workspace.id), + data={"name": "a cool new name"}, + follow_redirects=True, + ) + assert response.status_code == 200 + assert workspace.name == "a cool new name"