diff --git a/atst/routes/applications/settings.py b/atst/routes/applications/settings.py index 586e6d3d..b3077b77 100644 --- a/atst/routes/applications/settings.py +++ b/atst/routes/applications/settings.py @@ -85,20 +85,33 @@ def check_users_are_in_application(user_ids, application): return True -@applications_bp.route("/applications//settings") -@user_can(Permissions.VIEW_APPLICATION, message="view application edit form") -def settings(application_id): - application = Applications.get(application_id) - form = ApplicationForm(name=application.name, description=application.description) +def render_settings_page(application, **kwargs): environments_obj = get_environments_obj_for_app(application=application) members_form = AppEnvRolesForm(data=data_for_app_env_roles_form(application)) + new_env_form = EditEnvironmentForm() + + if "application_form" not in kwargs: + kwargs["application_form"] = ApplicationForm( + name=application.name, description=application.description + ) return render_template( "portfolios/applications/settings.html", application=application, - form=form, environments_obj=environments_obj, members_form=members_form, + new_env_form=new_env_form, + **kwargs, + ) + + +@applications_bp.route("/applications//settings") +@user_can(Permissions.VIEW_APPLICATION, message="view application edit form") +def settings(application_id): + application = Applications.get(application_id) + + return render_settings_page( + application=application, active_toggler=http_request.args.get("active_toggler"), active_toggler_section=http_request.args.get("active_toggler_section"), ) @@ -129,16 +142,8 @@ def update_environment(environment_id): ) else: return ( - render_template( - "portfolios/applications/settings.html", + render_settings_page( application=application, - form=ApplicationForm( - name=application.name, description=application.description - ), - environments_obj=get_environments_obj_for_app(application=application), - members_form=AppEnvRolesForm( - data=data_for_app_env_roles_form(application) - ), active_toggler=environment.id, active_toggler_section="edit", ), @@ -146,6 +151,31 @@ def update_environment(environment_id): ) +@applications_bp.route( + "/applications//environments/new", methods=["POST"] +) +@user_can(Permissions.CREATE_ENVIRONMENT, message="create application environment") +def new_environment(application_id): + application = Applications.get(application_id) + env_form = EditEnvironmentForm(formdata=http_request.form) + + if env_form.validate(): + Environments.create(application=application, name=env_form.name.data) + + flash("environment_added", environment_name=env_form.data["name"]) + + return redirect( + url_for( + "applications.settings", + application_id=application.id, + fragment="application-environments", + _anchor="application-environments", + ) + ) + else: + return (render_settings_page(application=application), 400) + + @applications_bp.route("/applications//edit", methods=["POST"]) @user_can(Permissions.EDIT_APPLICATION, message="update application") def update(application_id): @@ -162,12 +192,7 @@ def update(application_id): ) ) else: - return render_template( - "portfolios/applications/settings.html", - application=application, - form=form, - environments_obj=get_environments_obj_for_app(application=application), - ) + return render_settings_page(application=application, application_form=form) @applications_bp.route("/environments//roles", methods=["POST"]) @@ -214,13 +239,8 @@ def update_env_roles(environment_id): ) else: return ( - render_template( - "portfolios/applications/settings.html", + render_settings_page( application=application, - form=ApplicationForm( - name=application.name, description=application.description - ), - environments_obj=get_environments_obj_for_app(application=application), active_toggler=environment.id, active_toggler_section="edit", ), diff --git a/atst/utils/context_processors.py b/atst/utils/context_processors.py index 243de728..f3794899 100644 --- a/atst/utils/context_processors.py +++ b/atst/utils/context_processors.py @@ -5,6 +5,7 @@ from sqlalchemy.orm.exc import NoResultFound from atst.database import db from atst.domain.authz import Authorization +from atst.domain.exceptions import NotFoundError from atst.domain.portfolios.scopes import ScopedPortfolio from atst.models import ( Application, diff --git a/atst/utils/flash.py b/atst/utils/flash.py index eafa6041..a7412fe7 100644 --- a/atst/utils/flash.py +++ b/atst/utils/flash.py @@ -17,6 +17,13 @@ MESSAGES = { "message_template": "Application environment members have been updated", "category": "success", }, + "environment_added": { + "title_template": translate("flash.success"), + "message_template": """ + {{ "flash.environment_added" | translate({ "env_name": environment_name }) }} + """, + "category": "success", + }, "application_environments_updated": { "title_template": "Application environments updated", "message_template": "Application environments have been updated", @@ -29,7 +36,7 @@ MESSAGES = { }, "invitation_resent": { "title_template": "Invitation resent", - "message_template": "The {{ officer_type }} has been resent instructions to join this portfolio.", + "message_template": "The {{ officer_type }} has been resent instructions to join this portfolio.", "category": "success", }, "task_order_draft": { diff --git a/js/components/forms/new_environment.js b/js/components/forms/new_environment.js new file mode 100644 index 00000000..9b075bc0 --- /dev/null +++ b/js/components/forms/new_environment.js @@ -0,0 +1,24 @@ +import FormMixin from '../../mixins/form' +import textinput from '../text_input' + +export default { + name: 'new-environment', + + mixins: [FormMixin], + + components: { + textinput, + }, + + data: function() { + return { + open: false, + } + }, + + methods: { + toggle: function() { + this.open = !this.open + }, + }, +} diff --git a/js/index.js b/js/index.js index 478f9b39..02a9b404 100644 --- a/js/index.js +++ b/js/index.js @@ -38,6 +38,7 @@ import SidenavToggler from './components/sidenav_toggler' import KoReview from './components/forms/ko_review' import BaseForm from './components/forms/base_form' import DeleteConfirmation from './components/delete_confirmation' +import NewEnvironment from './components/forms/new_environment' Vue.config.productionTip = false @@ -78,6 +79,7 @@ const app = new Vue({ BaseForm, DeleteConfirmation, nestedcheckboxinput, + NewEnvironment, }, mounted: function() { diff --git a/styles/components/_accordion_table.scss b/styles/components/_accordion_table.scss index 28cd08de..1bcf3331 100644 --- a/styles/components/_accordion_table.scss +++ b/styles/components/_accordion_table.scss @@ -42,6 +42,19 @@ .usa-input { margin: 0; + + .icon-validation { + left: 135%; + } + } + + #name { + max-width: none; + border-color: black; + } + + .usa-alert { + margin: 2.5rem 0; } select { @@ -50,6 +63,11 @@ } } + .new-env { + margin-top: 5rem; + padding: 0 5rem; + } + .accordion-table__items { margin: 0; diff --git a/templates/components/delete_confirmation.html b/templates/components/delete_confirmation.html index e5e9bfc5..ac2d250b 100644 --- a/templates/components/delete_confirmation.html +++ b/templates/components/delete_confirmation.html @@ -17,7 +17,7 @@ diff --git a/templates/fragments/applications/add_new_environment.html b/templates/fragments/applications/add_new_environment.html new file mode 100644 index 00000000..f1e72e4f --- /dev/null +++ b/templates/fragments/applications/add_new_environment.html @@ -0,0 +1,42 @@ +{% from "components/alert.html" import Alert %} +{% from 'components/save_button.html' import SaveButton %} +{% from "components/text_input.html" import TextInput %} + + +
+
+
+ {{ new_env_form.csrf_token }} + +
+ {{ Alert( + title=("portfolios.applications.create_new_env" | translate), + message=("portfolios.applications.create_new_env_info" | translate ) + ) }} +
{{ "portfolios.applications.enter_env_name" | translate }}
+ {{ TextInput(new_env_form.name, label="", validation="requiredField") }} +
+ +
+
+ + +
+
+ diff --git a/templates/fragments/applications/edit_environments.html b/templates/fragments/applications/edit_environments.html index 21435046..12f48fbd 100644 --- a/templates/fragments/applications/edit_environments.html +++ b/templates/fragments/applications/edit_environments.html @@ -115,10 +115,10 @@ {{ DeleteConfirmation( - modal_id=delete_modal_environment_id, + modal_id=delete_environment_modal_id, delete_text=('portfolios.applications.environments.delete.button' | translate), delete_action= url_for('applications.delete_environment', environment_id=env['id']), - form=form + form=edit_form ) }} {% endcall %} @@ -126,11 +126,3 @@ - diff --git a/templates/fragments/applications/edit_team.html b/templates/fragments/applications/edit_team.html index 81baff38..8999c88a 100644 --- a/templates/fragments/applications/edit_team.html +++ b/templates/fragments/applications/edit_team.html @@ -1,4 +1,5 @@ {% from "components/options_input.html" import OptionsInput %} +{% from "components/toggle_list.html" import ToggleButton, ToggleSection %} {{ team_form.csrf_token }} diff --git a/templates/portfolios/applications/settings.html b/templates/portfolios/applications/settings.html index 5daf6ccd..8fe46732 100644 --- a/templates/portfolios/applications/settings.html +++ b/templates/portfolios/applications/settings.html @@ -16,13 +16,13 @@
- {{ form.csrf_token }} + {{ application_form.csrf_token }}

{{ "fragments.edit_application_form.explain" | translate }}

- {{ TextInput(form.name) }} + {{ TextInput(application_form.name) }}
{% if user_can(permissions.DELETE_APPLICATION) %} @@ -45,7 +45,7 @@
- {{ TextInput(form.description, paragraph=True) }} + {{ TextInput(application_form.description, paragraph=True) }}
  @@ -69,6 +69,10 @@ {% if user_can(permissions.EDIT_APPLICATION) %} {% include "fragments/applications/edit_environments.html" %} + {% if user_can(permissions.CREATE_ENVIRONMENT) %} + {% include "fragments/applications/add_new_environment.html" %} + {% endif %} + {% elif user_can(permissions.VIEW_ENVIRONMENT) %} {% include "fragments/applications/read_only_environments.html" %} {% endif %} @@ -89,10 +93,10 @@ {{ DeleteConfirmation( - modal_id=delete_modal_environment_id, + modal_id="delete_application", delete_text=('portfolios.applications.delete.button' | translate), delete_action= url_for('applications.delete', application_id=application.id), - form=form + form=application_form ) }} {% endcall %} diff --git a/templates/portfolios/applications/team.html b/templates/portfolios/applications/team.html index 2b6648d1..d33992cd 100644 --- a/templates/portfolios/applications/team.html +++ b/templates/portfolios/applications/team.html @@ -1,10 +1,8 @@ {% extends "portfolios/applications/base.html" %} -{% from "components/empty_state.html" import EmptyState %} {% from "components/icon.html" import Icon %} -{% from 'components/save_button.html' import SaveButton %} -{% from "components/toggle_list.html" import ToggleButton, ToggleSection %} {% from "components/multi_step_modal_form.html" import MultiStepModalForm %} +{% from 'components/save_button.html' import SaveButton %} {% import "fragments/applications/new_member_modal_content.html" as member_steps %} {% from "components/alert.html" import Alert %} {% from "components/delete_confirmation.html" import DeleteConfirmation %} diff --git a/tests/routes/applications/test_settings.py b/tests/routes/applications/test_settings.py index 1e5bfb02..a658107a 100644 --- a/tests/routes/applications/test_settings.py +++ b/tests/routes/applications/test_settings.py @@ -54,7 +54,7 @@ def test_updating_application_environments_success(client, user_session): assert environment.name == "new name a" -def test_updating_application_environments_failure(client, user_session): +def test_update_environment_failure(client, user_session): portfolio = PortfolioFactory.create() application = ApplicationFactory.create(portfolio=portfolio) environment = EnvironmentFactory.create( @@ -391,6 +391,38 @@ def test_delete_application(client, user_session): assert len(application.environments) == 0 +def test_new_environment(client, user_session): + user = UserFactory.create() + portfolio = PortfolioFactory(owner=user) + application = ApplicationFactory.create(portfolio=portfolio) + num_envs = len(application.environments) + + user_session(user) + response = client.post( + url_for("applications.new_environment", application_id=application.id), + data={"name": "dabea"}, + ) + + assert response.status_code == 302 + assert len(application.environments) == num_envs + 1 + + +def test_new_environment_with_bad_data(client, user_session): + user = UserFactory.create() + portfolio = PortfolioFactory(owner=user) + application = ApplicationFactory.create(portfolio=portfolio) + num_envs = len(application.environments) + + user_session(user) + response = client.post( + url_for("applications.new_environment", application_id=application.id), + data={"name": None}, + ) + + assert response.status_code == 400 + assert len(application.environments) == num_envs + + def test_delete_environment(client, user_session): user = UserFactory.create() portfolio = PortfolioFactory(owner=user) diff --git a/translations.yaml b/translations.yaml index cc4d04ac..68824de8 100644 --- a/translations.yaml +++ b/translations.yaml @@ -69,6 +69,7 @@ flash: congrats: Congrats! delete_member_success: 'You have successfully deleted {member_name} from the portfolio.' deleted_member: Portfolio member deleted + environment_added: 'The environment "{env_name}" has been added to the application.' login_required_message: After you log in, you will be redirected to your destination page. login_required_title: Log in required new_portfolio: You've created a new JEDI portfolio and jump-started your first task order! @@ -420,6 +421,8 @@ portfolios: add_another_environment: Add another environment app_settings_text: App settings create_button_text: Create + create_new_env: Create a new environment. + create_new_env_info: Creating an environment gives you access to the Cloud Service Provider. This environment will function within the constraints of the task order, and any costs will be billed against the portfolio. csp_console_text: CSP console remove_member: alert: @@ -431,6 +434,7 @@ portfolios: message: You will lose access to this application and all of its reporting and metrics tools. The activity log will be retained. button: Delete application header: Are you sure you want to delete this application? + enter_env_name: "Enter environment name:" environments: name: Name delete: