diff --git a/alembic/versions/f50596c5ffbb_make_application_description_optional.py b/alembic/versions/f50596c5ffbb_make_application_description_optional.py new file mode 100644 index 00000000..fb2852fb --- /dev/null +++ b/alembic/versions/f50596c5ffbb_make_application_description_optional.py @@ -0,0 +1,31 @@ +"""Make application description optional + +Revision ID: f50596c5ffbb +Revises: e3d93f9caba7 +Create Date: 2019-10-08 09:41:11.835664 + +""" +from alembic import op +import sqlalchemy as sa + +# revision identifiers, used by Alembic. +revision = "f50596c5ffbb" # pragma: allowlist secret +down_revision = "e3d93f9caba7" # pragma: allowlist secret +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column( + "applications", "description", existing_type=sa.VARCHAR(), nullable=True + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column( + "applications", "description", existing_type=sa.VARCHAR(), nullable=False + ) + # ### end Alembic commands ### diff --git a/atst/forms/application.py b/atst/forms/application.py index f9c5ca0e..16db3827 100644 --- a/atst/forms/application.py +++ b/atst/forms/application.py @@ -1,6 +1,6 @@ from .forms import BaseForm from wtforms.fields import StringField, TextAreaField, FieldList -from wtforms.validators import Required +from wtforms.validators import Required, Optional from atst.forms.validators import ListItemRequired, ListItemsUnique from atst.utils.localization import translate @@ -16,7 +16,9 @@ class NameAndDescriptionForm(BaseForm): label=translate("forms.application.name_label"), validators=[Required()] ) description = TextAreaField( - label=translate("forms.application.description_label"), validators=[Required()] + label=translate("forms.application.description_label"), + validators=[Optional()], + filters=[lambda x: x or None], ) diff --git a/atst/models/application.py b/atst/models/application.py index 0f177a22..f79812c6 100644 --- a/atst/models/application.py +++ b/atst/models/application.py @@ -15,7 +15,7 @@ class Application( id = Id() name = Column(String, nullable=False) - description = Column(String, nullable=False) + description = Column(String) portfolio_id = Column(ForeignKey("portfolios.id"), nullable=False) portfolio = relationship("Portfolio") diff --git a/atst/routes/applications/new.py b/atst/routes/applications/new.py index fe160d65..0eecf778 100644 --- a/atst/routes/applications/new.py +++ b/atst/routes/applications/new.py @@ -37,11 +37,9 @@ def render_new_application_form( @applications_bp.route("/portfolios//applications/new") -@applications_bp.route( - "/portfolios//applications//step_1" -) +@applications_bp.route("/applications//new/step_1") @user_can(Permissions.CREATE_APPLICATION, message="view create new application form") -def view_new_application_step_1(portfolio_id, application_id=None): +def view_new_application_step_1(portfolio_id=None, application_id=None): return render_new_application_form( "applications/new/step_1.html", NameAndDescriptionForm, @@ -56,13 +54,12 @@ def view_new_application_step_1(portfolio_id, application_id=None): methods=["POST"], ) @applications_bp.route( - "/portfolios//applications//step_1", + "/applications//new/step_1", endpoint="update_new_application_step_1", methods=["POST"], ) @user_can(Permissions.CREATE_APPLICATION, message="view create new application form") -def create_or_update_new_application_step_1(portfolio_id, application_id=None): - portfolio = Portfolios.get_for_update(portfolio_id) +def create_or_update_new_application_step_1(portfolio_id=None, application_id=None): form = get_new_application_form( {**http_request.form}, NameAndDescriptionForm, application_id ) @@ -72,12 +69,14 @@ def create_or_update_new_application_step_1(portfolio_id, application_id=None): if application_id: application = Applications.get(application_id) application = Applications.update(application, form.data) + flash("application_updated", application_name=application.name) else: + portfolio = Portfolios.get_for_update(portfolio_id) application = Applications.create(g.current_user, portfolio, **form.data) + flash("application_created", application_name=application.name) return redirect( url_for( "applications.update_new_application_step_2", - portfolio_id=portfolio_id, application_id=application.id, ) ) @@ -94,11 +93,9 @@ def create_or_update_new_application_step_1(portfolio_id, application_id=None): ) -@applications_bp.route( - "/portfolios//applications//step_2" -) +@applications_bp.route("/applications//new/step_2") @user_can(Permissions.CREATE_APPLICATION, message="view create new application form") -def view_new_application_step_2(portfolio_id, application_id): +def view_new_application_step_2(application_id): application = Applications.get(application_id) render_args = { "form": EnvironmentsForm( @@ -114,11 +111,9 @@ def view_new_application_step_2(portfolio_id, application_id): return render_template("applications/new/step_2.html", **render_args) -@applications_bp.route( - "/portfolios//applications//step_2", methods=["POST"] -) +@applications_bp.route("/applications//new/step_2", methods=["POST"]) @user_can(Permissions.CREATE_APPLICATION, message="view create new application form") -def update_new_application_step_2(portfolio_id, application_id): +def update_new_application_step_2(application_id): form = get_new_application_form( {**http_request.form}, EnvironmentsForm, application_id ) @@ -137,15 +132,14 @@ def update_new_application_step_2(portfolio_id, application_id): render_new_application_form( "applications/new/step_2.html", EnvironmentsForm, - portfolio_id, - application_id, - form, + application_id=application_id, + form=form, ), 400, ) -@applications_bp.route("/applications//step_3") +@applications_bp.route("/applications//new/step_3") @user_can(Permissions.CREATE_APPLICATION, message="view create new application form") def view_new_application_step_3(application_id): application = Applications.get(application_id) @@ -161,7 +155,7 @@ def view_new_application_step_3(application_id): ) -@applications_bp.route("/applications//step_3", methods=["POST"]) +@applications_bp.route("/applications//new/step_3", methods=["POST"]) @user_can(Permissions.CREATE_APPLICATION, message="view create new application form") def update_new_application_step_3(application_id): diff --git a/atst/utils/flash.py b/atst/utils/flash.py index 3472f1bd..613b55bf 100644 --- a/atst/utils/flash.py +++ b/atst/utils/flash.py @@ -14,6 +14,13 @@ MESSAGES = { """, "category": "success", }, + "application_updated": { + "title_template": translate("flash.success"), + "message_template": """ + {{ "flash.application.updated" | translate({"application_name": application_name}) }} + """, + "category": "success", + }, "application_deleted": { "title_template": translate("flash.success"), "message_template": """ diff --git a/styles/components/_portfolio_layout.scss b/styles/components/_portfolio_layout.scss index 240c3c35..9d4f131d 100644 --- a/styles/components/_portfolio_layout.scss +++ b/styles/components/_portfolio_layout.scss @@ -138,85 +138,22 @@ color: $color-gray; } + .responsive-table-wrapper { + padding-bottom: $gap * 3; + margin-bottom: 0; + } + table { - margin: 0; - width: 100%; + min-width: 100%; + margin-top: 0; + } - thead { - th:first-child { - padding-left: 3 * $gap; - } - - tr:first-child { - padding: 0 2 * $gap 0 5 * $gap; - } - - td { - font-weight: bold; - font-size: 1.4rem; - border-top: 0; - } - } - - th { - background-color: $color-gray-lightest; - padding: $gap 2 * $gap; - border-top: none; - border-bottom: none; - color: $color-gray; - } - - td:first-child { - padding: 2 * $gap 2 * $gap 2 * $gap 5 * $gap; - } - - tbody { - td { - border-bottom: 1px solid $color-gray-lightest; - font-size: 1.6rem; - border-top: 0; - padding: 3 * $gap 2 * $gap; - - .usa-button-disabled { - color: $color-gray-medium; - background-color: $color-gray-lightest; - box-shadow: inset 0 0 0 1px $color-gray-medium; - } - - button { - padding: 0; - margin: 0; - font-size: 1.5rem; - width: 11rem; - height: 3rem; - } - } - - .name { - font-weight: bold; - - .you { - font-size: 1.2rem; - } - } - - .usa-input.usa-input--success { - margin: 0; - } - - select { - border: none; - } - } - - .add-member-link { - text-align: right; - } - - .usa-button-primary .usa-button { - padding: 2 * $gap; - float: right; - } + a.create-member { + display: inherit; + margin-left: auto; + margin-right: auto; + max-width: 50%; + margin-top: 3rem; } input.usa-button.usa-button-primary { diff --git a/styles/components/_sticky_cta.scss b/styles/components/_sticky_cta.scss index 0656c660..c5946951 100644 --- a/styles/components/_sticky_cta.scss +++ b/styles/components/_sticky_cta.scss @@ -2,6 +2,11 @@ margin-left: -$gap * 4; margin-right: -$gap * 5; z-index: 10; + background-color: $color-gray-lightest; + border-top: 1px solid $color-gray-lighter; + border-bottom: 1px solid $color-gray-lighter; + padding: 0 $gap * 5 0 $gap * 5; + box-shadow: $box-shadow; @include media($medium-screen) { margin-right: -$gap * 5; @@ -16,12 +21,6 @@ display: flex; align-items: center; - background-color: $color-gray-lightest; - border-top: 1px solid $color-gray-lighter; - border-bottom: 1px solid $color-gray-lighter; - padding: 0 $gap * 5 0 $gap * 5; - box-shadow: $box-shadow; - .usa-button { margin: $gap $gap * 1.5 $gap 0; width: 20rem; @@ -32,6 +31,12 @@ &-text { flex-grow: 1; + display: flex; + align-items: baseline; + } + &-context { + color: $color-gray; + margin-left: $gap; } &-buttons { diff --git a/styles/elements/_panels.scss b/styles/elements/_panels.scss index 87435b44..97353795 100644 --- a/styles/elements/_panels.scss +++ b/styles/elements/_panels.scss @@ -120,9 +120,10 @@ @include panel-actions; } - hr { + hr, + &__break { border: 0; - border-bottom: 1px dashed $color-gray-light; + border-bottom: 1px solid $color-gray-light; margin: ($gap * 4) ($site-margins * -4); } } diff --git a/styles/elements/_tables.scss b/styles/elements/_tables.scss index 766d67de..89469512 100644 --- a/styles/elements/_tables.scss +++ b/styles/elements/_tables.scss @@ -66,7 +66,7 @@ table.atat-table { display: table-cell; white-space: nowrap; - border-bottom-style: dashed; + border-bottom-style: solid; border-top: none; &:last-child { @@ -88,18 +88,14 @@ table.atat-table { th, td { @include block-list-header; - display: table-cell; white-space: nowrap; + color: black; } } } @at-root .panel #{&} { - tr:last-child td { - border-bottom: 0; - } - &:last-child { margin-bottom: 0; } @@ -138,10 +134,6 @@ table.atat-table { } @at-root .panel #{&} { - tr:last-child td { - border-bottom: 0; - } - &:last-child { margin-bottom: 0; } diff --git a/templates/applications/new/step_1.html b/templates/applications/new/step_1.html index 4c2e0fa3..faf6656d 100644 --- a/templates/applications/new/step_1.html +++ b/templates/applications/new/step_1.html @@ -7,14 +7,14 @@ {% set secondary_breadcrumb = 'portfolios.applications.new_application_title' | translate %} {% if application_id %} - {% set action = url_for('applications.update_new_application_step_1', portfolio_id=portfolio.id, application_id=application_id) %} + {% set action = url_for('applications.update_new_application_step_1', application_id=application_id) %} {% else %} {% set action = url_for('applications.create_new_application_step_1', portfolio_id=portfolio.id, application_id=application_id) %} {% endif %} {% block portfolio_header %} {% include "portfolios/header.html" %} - {{ StickyCTA(text="Name and Describe New Application") }} + {{ StickyCTA(text=('portfolios.applications.new.step_1_header' | translate | safe), context="Step 1 of 3") }} {% endblock %} {% block application_content %} @@ -23,30 +23,32 @@
-
{{ form.csrf_token }} -

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

{{ TextInput(form.name, optional=False) }} + {{ ('portfolios.applications.new.step_1_form_help_text.description' | translate | safe) }}
+
- {{ TextInput(form.description, paragraph=True, optional=False) }} + {{ TextInput(form.description, paragraph=True, optional=True) }} + {{ ('portfolios.applications.new.step_1_form_help_text.description' | translate | safe) }}
-
+ - - {% block next_button %} - {{ SaveButton(text=('portfolios.applications.new.step_1_button_text' | translate)) }} - {% endblock %} - + + {% block next_button %} + {{ SaveButton(text=('portfolios.applications.new.step_1_button_text' | translate)) }} + {% endblock %} + + Cancel + +
diff --git a/templates/applications/new/step_2.html b/templates/applications/new/step_2.html index fa0954e7..75b5f280 100644 --- a/templates/applications/new/step_2.html +++ b/templates/applications/new/step_2.html @@ -8,68 +8,74 @@ {% set secondary_breadcrumb = 'portfolios.applications.new_application_title' | translate %} {% block portfolio_header %} - {{ StickyCTA(text=application.name) }} + {% include "portfolios/header.html" %} + {{ StickyCTA(text=('portfolios.applications.new.step_2_header' | translate({"application_name": application.name}) ), context="Step 2 of 3") }} {% endblock %} {% block application_content %} {% set modalName = "newApplicationConfirmation" %} {% include "fragments/flash.html" %} - - -
-
-
- {{ form.csrf_token }} -
{# this extra div prevents this bug: https://www.pivotaltracker.com/story/show/160768940 #} -
- {{ Alert(message=None, level="error", vue_template=True) }} +
+

+ {{ 'portfolios.applications.new.step_2_description' | translate }} +

+
+ + +
{{ 'portfolios.applications.environments_heading' | translate }}
+
+
+ {{ form.csrf_token }} +
{# this extra div prevents this bug: https://www.pivotaltracker.com/story/show/160768940 #} +
+ {{ Alert(message=None, level="error", vue_template=True) }} +
-
- -
-
-

{{ 'portfolios.applications.environments_heading' | translate }}

-

- {{ 'portfolios.applications.environments_description' | translate }} -

-
- -
    -
  • -
    - - -
    -
    - -
    -
  • -
- - -
+ +
+
    +
  • +
    + + +
    +
    + +
    +
  • +
+ + +
-
- - - {% block next_button %} - {{ SaveButton(text=('portfolios.applications.new.step_2_button_text' | translate)) }} - {% endblock %} - - - +
+ + + {% block next_button %} + {{ SaveButton(text=('portfolios.applications.new.step_2_button_text' | translate)) }} + {% endblock %} + + Previous + + + Cancel + + + + +
{% endblock %} diff --git a/templates/applications/new/step_3.html b/templates/applications/new/step_3.html index c963beee..7c1d6b6b 100644 --- a/templates/applications/new/step_3.html +++ b/templates/applications/new/step_3.html @@ -6,21 +6,36 @@ {% block portfolio_header %} {% include "portfolios/header.html" %} - {{ StickyCTA(text=application.name) }} + {{ StickyCTA(text=('portfolios.applications.new.step_3_header' | translate({"application_name": application.name}) ), context="Step 3 of 3") }} {% endblock %} {% block application_content %} + {% include "fragments/flash.html" %} +
+

+ {{ ('portfolios.applications.new.step_3_description' | translate) }} +

+
+ {{ MemberManagementTemplate( - application, - members, - new_member_form, - "applications.update_new_application_step_3", - user_can(permissions.CREATE_APPLICATION_MEMBER)) }} - - - - Return to Application Settings - - + application, + members, + new_member_form, + "applications.update_new_application_step_3", + user_can(permissions.CREATE_APPLICATION_MEMBER)) }} + + + + + Return to Application Settings + + + Previous + + + Cancel + + +
{% endblock %} diff --git a/templates/applications/settings.html b/templates/applications/settings.html index 5efe09bb..c33db395 100644 --- a/templates/applications/settings.html +++ b/templates/applications/settings.html @@ -32,7 +32,7 @@
{{ TextInput(application_form.name, optional=False) }} - {{ TextInput(application_form.description, paragraph=True, optional=False) }} + {{ TextInput(application_form.description, paragraph=True, optional=True) }}
@@ -81,14 +81,9 @@ "applications.create_member", user_can(permissions.CREATE_APPLICATION_MEMBER)) }} -
+
{{ 'common.resource_names.environments' | translate }} - - {% if user_can(permissions.CREATE_ENVIRONMENT) %} - {% include "applications/fragments/add_new_environment.html" %} - {% endif %}
-
{% if g.matchesPath("application-environments") %} {% include "fragments/flash.html" %} @@ -178,8 +173,13 @@
+ {% if user_can(permissions.CREATE_ENVIRONMENT) %} +
+ {% include "applications/fragments/add_new_environment.html" %} +
+ {% endif %}
- +
{% if user_can(permissions.DELETE_APPLICATION) %} diff --git a/templates/components/sticky_cta.html b/templates/components/sticky_cta.html index 4ba268c0..3d66dd6a 100644 --- a/templates/components/sticky_cta.html +++ b/templates/components/sticky_cta.html @@ -1,23 +1,26 @@ {% from 'components/icon.html' import Icon %} -{% macro StickyCTA(text, return_link_url=None, return_link_text=None) -%} -
-
-
- {% if return_link_url and return_link_text %} - - {% endif %} -

{{ text }}

-
- {% if caller %} -
- {{ caller() }} -
- {% endif %} -
+{% macro StickyCTA(text, context=None, return_link_url=None, return_link_text=None) -%} +
+ {% if return_link_url and return_link_text %} + + {% endif %} +
+
+

{{ text }}

+ {% if context %} + {{ context }} + {% endif %} +
+ {% if caller %} +
+ {{ caller() }} +
+ {% endif %} +
+
{%- endmacro %} diff --git a/templates/fragments/members.html b/templates/fragments/members.html index 31848326..9f43c93e 100644 --- a/templates/fragments/members.html +++ b/templates/fragments/members.html @@ -15,10 +15,16 @@ ) %} - {% include "fragments/flash.html" %} + {% if g.matchesPath("application-members") %} + {% include "fragments/flash.html" %} + {% endif %} +
+ {{ 'portfolios.applications.settings.team_members' | translate }} +
+
{% if not application.members %} -
+

{{ ("portfolios.applications.team_settings.blank_slate.title" | translate) }}

{{ Icon('avatar') }} @@ -45,21 +51,9 @@
{% else %} -
- {{ 'portfolios.applications.settings.team_members' | translate }} - - {% set new_member_modal_name = "add-app-mem" %} - {% if user_can_create_app_member %} - - {{ Icon("plus") }} - {{ "portfolios.applications.add_member" | translate }} - - {% endif %} -
- -
-
- {% for member in members %} + {% set new_member_modal_name = "add-app-mem" %} + + {% for member in members %} {% set modal_name = "edit_member-{}".format(loop.index) %} {% call Modal(modal_name) %} {% if user_can_create_app_member %} @@ -139,5 +142,6 @@ {% endif %}
{% endif %} +
{% endmacro %} diff --git a/translations.yaml b/translations.yaml index 83cbe0ad..22d3a9ab 100644 --- a/translations.yaml +++ b/translations.yaml @@ -107,6 +107,7 @@ email: flash: application: created: 'You have successfully created the {application_name} application.' + updated: 'You have successfully updated the {application_name} application.' deleted: 'You have successfully deleted the {application_name} application. To view the retained activity log, visit the portfolio administration page.' delete_member_success: 'You have successfully deleted {member_name} from the portfolio.' deleted_member: Portfolio member deleted @@ -308,8 +309,38 @@ portfolios: add_another_environment: Add another environment app_settings_text: App settings new: + step_1_header: Name and Describe New Application step_1_button_text: "Save and Add Environments" + step_1_form_help_text: + name: | +
+

+ The name of your application should be intuitive and easily recognizable for all of your team members. +

+

+ Writer's Block? A naming example includes: +

    +
  • Army Security Infrastructure Application
  • +
+

+
+ description: | +
+

+ Add a brief one to two sentence description of your application. You should be able to reference your TO Description of Work. +

+

+ Writer's Block? A naming example includes: +

    +
  • Build security applications for FOB Clark
  • +
+

+
+ step_2_header: Add Environments to {application_name} + step_2_description: "Production, Staging, Testing, and Development environments are included by default. However, you can add, edit, and delete environments based on the needs of your Application." step_2_button_text: "Save and Add Members" + step_3_header: Invite Members to {application_name} + step_3_description: "To proceed, you will need each member's email address and DOD ID. Within this section, you will also assign application-level permissions and environment-level roles for each member." step_3_button_text: Save Application 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. @@ -333,8 +364,7 @@ portfolios: delete: button: Delete environment edit_name: Edit name - environments_description: Each environment created within an application is logically separated from one another for easier management and security. - environments_heading: Application environments + environments_heading: Application Environments existing_application_title: '{application_name} Application Settings' member_count: '{count} members' new_application_title: New Application