diff --git a/atst/domain/projects.py b/atst/domain/projects.py index ce9534bd..913127d0 100644 --- a/atst/domain/projects.py +++ b/atst/domain/projects.py @@ -64,3 +64,15 @@ class Projects(object): raise NotFoundError("projects") return projects + + @classmethod + def update(cls, user, workspace, project, new_data): + if "name" in new_data: + project.name = new_data["name"] + if "description" in new_data: + project.description = new_data["description"] + + db.session.add(project) + db.session.commit() + + return project diff --git a/atst/forms/new_project.py b/atst/forms/project.py similarity index 93% rename from atst/forms/new_project.py rename to atst/forms/project.py index 00b01325..8d858a59 100644 --- a/atst/forms/new_project.py +++ b/atst/forms/project.py @@ -4,12 +4,14 @@ from wtforms.validators import Required from atst.forms.validators import ListItemRequired, ListItemsUnique -class NewProjectForm(FlaskForm): - - EMPTY_ENVIRONMENT_NAMES = ["", None] - +class ProjectForm(FlaskForm): name = StringField(label="Project Name", validators=[Required()]) description = TextAreaField(label="Description", validators=[Required()]) + + +class NewProjectForm(ProjectForm): + EMPTY_ENVIRONMENT_NAMES = ["", None] + environment_names = FieldList( StringField(label="Environment Name"), validators=[ diff --git a/atst/routes/workspaces.py b/atst/routes/workspaces.py index 8f1055ba..af59939b 100644 --- a/atst/routes/workspaces.py +++ b/atst/routes/workspaces.py @@ -17,7 +17,7 @@ from atst.domain.workspaces import Workspaces from atst.domain.workspace_users import WorkspaceUsers from atst.domain.environments import Environments from atst.domain.environment_roles import EnvironmentRoles -from atst.forms.new_project import NewProjectForm +from atst.forms.project import NewProjectForm, ProjectForm from atst.forms.new_member import NewMemberForm from atst.forms.edit_member import EditMemberForm from atst.forms.workspace import WorkspaceForm @@ -181,17 +181,34 @@ def create_project(workspace_id): def edit_project(workspace_id, project_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, - environment_names=[env.name for env in project.environments], - description=project.description, - ) + form = ProjectForm(name=project.name, description=project.description) return render_template( "workspaces/projects/edit.html", workspace=workspace, project=project, form=form ) +@bp.route("/workspaces//projects//edit", methods=["POST"]) +def update_project(workspace_id, project_id): + workspace = Workspaces.get_for_update_projects(g.current_user, workspace_id) + project = Projects.get(g.current_user, workspace, project_id) + form = ProjectForm(http_request.form) + if form.validate(): + project_data = form.data + Projects.update(g.current_user, workspace, project, project_data) + + return redirect( + url_for("workspaces.workspace_projects", workspace_id=workspace.id) + ) + else: + return render_template( + "workspaces/projects/edit.html", + workspace=workspace, + project=project, + form=form, + ) + + @bp.route("/workspaces//members/new") def new_member(workspace_id): workspace = Workspaces.get(g.current_user, workspace_id) diff --git a/styles/elements/_inputs.scss b/styles/elements/_inputs.scss index 6cd22272..2be9f2fe 100644 --- a/styles/elements/_inputs.scss +++ b/styles/elements/_inputs.scss @@ -133,6 +133,10 @@ } } + input:read-only { + color: grey; + } + .usa-input__choices { // checkbox & radio sets legend { padding: 0 0 $gap 0; diff --git a/templates/fragments/edit_project_form.html b/templates/fragments/edit_project_form.html index a5c6d485..42d5f3e7 100644 --- a/templates/fragments/edit_project_form.html +++ b/templates/fragments/edit_project_form.html @@ -1,85 +1,19 @@ -{% from "components/icon.html" import Icon %} -{% from "components/modal.html" import Modal %} {% from "components/text_input.html" import TextInput %} {% from "components/alert.html" import Alert %} - - {% set new_project = project is not defined %} - {% set form_action = url_for('workspaces.create_project', workspace_id=workspace.id) if new_project else url_for('workspaces.edit_project', workspace_id=workspace.id, project_id=project.id) %} - {% set action_text = 'Create' if new_project else 'Update' %} - {% set title_text = 'Add a new project' if new_project else 'Edit {} project'.format(project.name) %} +{% set title_text = 'Edit {} project'.format(project.name) if project else 'Add a new project' %} -
- {% call Modal(name=modalName, dismissable=False) %} -

{{ action_text }} project !{ name }

- -

- When you click {{ action_text }} Project, the environments - - !{environment.name} - - will be created as individual cloud resource groups under !{ name } project. -

- -
- - -
- {% endcall %} - - {{ form.csrf_token }} -
-
-

{{ title_text }}

-
- -
-

- AT-AT allows you to organize your workspace into multiple projects, each of which may have environments. -

- {{ TextInput(form.name) }} - {{ TextInput(form.description, paragraph=True) }} -
-
- -
{# this extra div prevents this bug: https://www.pivotaltracker.com/story/show/160768940 #} -
- {{ Alert(message=None, level="error", vue_template=True) }} -
-
- -
-
-

Project Environments

-

- Each environment created within a project is an enclave of cloud resources that is logically separated from each other for increased security. -

-
- -
    -
  • -
    - - -
    - -
  • -
- - -
- -
- -
- - - -
-
+{{ form.csrf_token }} +
+
+

{{ title_text }}

+
+
+

+ AT-AT allows you to organize your workspace into multiple projects, each of which may have environments. +

+ {{ TextInput(form.name) }} + {{ TextInput(form.description, paragraph=True) }} +
+
diff --git a/templates/workspaces/projects/edit.html b/templates/workspaces/projects/edit.html index 1fa3bc20..c9785f2d 100644 --- a/templates/workspaces/projects/edit.html +++ b/templates/workspaces/projects/edit.html @@ -1,15 +1,36 @@ {% extends "workspaces/base.html" %} -{% from "components/alert.html" import Alert %} -{% from "components/icon.html" import Icon %} +{% from "components/text_input.html" import TextInput %} {% block workspace_content %} -{% if g.dev %} - {{ Alert("In Progress", message="This page is a work in progress. You won't be able to edit environments on this project just yet.", level="warning") }} -{% endif %} +
-{% set modalName = "updateProjectConfirmation" %} -{% include "fragments/edit_project_form.html" %} + {% include "fragments/edit_project_form.html" %} + +
+
+

Project Environments

+

+ Each environment created within a project is an enclave of cloud resources that is logically separated from each other for increased security. +

+
+ +
    + {% for environment in project.environments %} +
  • +
    + + +
    +
  • + {% endfor %} +
+
+ +
+ +
+
{% endblock %} diff --git a/templates/workspaces/projects/new.html b/templates/workspaces/projects/new.html index c140023b..9a83ae40 100644 --- a/templates/workspaces/projects/new.html +++ b/templates/workspaces/projects/new.html @@ -1,19 +1,80 @@ {% extends "workspaces/base.html" %} {% from "components/alert.html" import Alert %} +{% from "components/icon.html" import Icon %} +{% from "components/modal.html" import Modal %} +{% from "components/text_input.html" import TextInput %} {% block workspace_content %} -{% set modalName = "newProjectConfirmation" %} -{% if request.args.get("newWorkspace") %} - {{ Alert('Workspace created!', - message="\ -

You are now ready to create projects and environments within the JEDI Cloud.

- ", - level='success' - ) }} -{% endif %} + {% set modalName = "newProjectConfirmation" %} + {% if request.args.get("newWorkspace") %} + {{ Alert('Workspace created!', + message="\ +

You are now ready to create projects and environments within the JEDI Cloud.

+ ", + level='success' + ) }} + {% endif %} -{% include "fragments/edit_project_form.html" %} + +
+ + {% call Modal(name=modalName, dismissable=False) %} +

Create project !{ name }

+ +

+ When you click Create Project, the environments + + !{environment.name} + + will be created as individual cloud resource groups under !{ name } project. +

+ +
+ + +
+ {% endcall %} + + {% include "fragments/edit_project_form.html" %} + +
{# this extra div prevents this bug: https://www.pivotaltracker.com/story/show/160768940 #} +
+ {{ Alert(message=None, level="error", vue_template=True) }} +
+
+ +
+
+

Project Environments

+

+ Each environment created within a project is an enclave of cloud resources that is logically separated from each other for increased security. +

+
+ +
    +
  • +
    + + +
    + +
  • +
+ + +
+ +
+ +
+
+
{% endblock %} diff --git a/tests/domain/test_projects.py b/tests/domain/test_projects.py index d46b1135..4fa0a7bb 100644 --- a/tests/domain/test_projects.py +++ b/tests/domain/test_projects.py @@ -24,3 +24,34 @@ def test_workspace_owner_can_view_environments(): project = Projects.get(owner, workspace, workspace.projects[0].id) assert len(project.environments) == 2 + + +def test_can_only_update_name_and_description(): + owner = UserFactory.create() + workspace = WorkspaceFactory.create( + owner=owner, + projects=[ + { + "name": "Project 1", + "description": "a project", + "environments": [{"name": "dev"}], + } + ], + ) + project = Projects.get(owner, workspace, workspace.projects[0].id) + env_name = project.environments[0].name + Projects.update( + owner, + workspace, + project, + { + "name": "New Name", + "description": "a new project", + "environment_name": "prod", + }, + ) + + assert project.name == "New Name" + assert project.description == "a new project" + assert len(project.environments) == 1 + assert project.environments[0].name == env_name diff --git a/tests/routes/test_workspaces.py b/tests/routes/test_workspaces.py index beacb529..f345f462 100644 --- a/tests/routes/test_workspaces.py +++ b/tests/routes/test_workspaces.py @@ -115,6 +115,66 @@ def test_view_edit_project(client, user_session): assert response.status_code == 200 +def test_user_with_permission_can_update_project(client, user_session): + owner = UserFactory.create() + workspace = WorkspaceFactory.create( + owner=owner, + projects=[ + { + "name": "Awesome Project", + "description": "It's really awesome!", + "environments": [{"name": "dev"}, {"name": "prod"}], + } + ], + ) + project = workspace.projects[0] + user_session(owner) + response = client.post( + url_for( + "workspaces.update_project", + workspace_id=workspace.id, + project_id=project.id, + ), + data={"name": "Really Cool Project", "description": "A very cool project."}, + follow_redirects=True, + ) + + assert response.status_code == 200 + assert project.name == "Really Cool Project" + assert project.description == "A very cool project." + + +def test_user_without_permission_cannot_update_project(client, user_session): + dev = UserFactory.create() + owner = UserFactory.create() + workspace = WorkspaceFactory.create( + owner=owner, + members=[{"user": dev, "role_name": "developer"}], + projects=[ + { + "name": "Great Project", + "description": "Cool stuff happening here!", + "environments": [{"name": "dev"}, {"name": "prod"}], + } + ], + ) + project = workspace.projects[0] + user_session(dev) + response = client.post( + url_for( + "workspaces.update_project", + workspace_id=workspace.id, + project_id=project.id, + ), + data={"name": "New Name", "description": "A new description."}, + follow_redirects=True, + ) + + assert response.status_code == 404 + assert project.name == "Great Project" + assert project.description == "Cool stuff happening here!" + + def test_create_member(client, user_session): owner = UserFactory.create() user = UserFactory.create()