Merge pull request #1088 from dod-ccpo/make-application-creation-multistep

Make application creation multistep
This commit is contained in:
graham-dds 2019-09-24 13:56:36 -04:00 committed by GitHub
commit 7e340936dc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 419 additions and 134 deletions

View File

@ -3,7 +3,7 @@
"files": "^.secrets.baseline$", "files": "^.secrets.baseline$",
"lines": null "lines": null
}, },
"generated_at": "2019-09-20T19:20:43Z", "generated_at": "2019-09-24T13:51:51Z",
"plugins_used": [ "plugins_used": [
{ {
"base64_limit": 4.5, "base64_limit": 4.5,
@ -199,5 +199,5 @@
} }
] ]
}, },
"version": "0.12.6" "version": "0.12.5"
} }

View File

@ -1,4 +1,5 @@
from . import BaseDomainClass from . import BaseDomainClass
from flask import g
from atst.database import db from atst.database import db
from atst.domain.application_roles import ApplicationRoles from atst.domain.application_roles import ApplicationRoles
from atst.domain.environment_roles import EnvironmentRoles from atst.domain.environment_roles import EnvironmentRoles
@ -19,12 +20,13 @@ class Applications(BaseDomainClass):
resource_name = "application" resource_name = "application"
@classmethod @classmethod
def create(cls, user, portfolio, name, description, environment_names): def create(cls, user, portfolio, name, description, environment_names=None):
application = Application( application = Application(
portfolio=portfolio, name=name, description=description portfolio=portfolio, name=name, description=description
) )
db.session.add(application) db.session.add(application)
if environment_names:
Environments.create_many(user, application, environment_names) Environments.create_many(user, application, environment_names)
db.session.commit() db.session.commit()
@ -48,7 +50,10 @@ class Applications(BaseDomainClass):
application.name = new_data["name"] application.name = new_data["name"]
if "description" in new_data: if "description" in new_data:
application.description = new_data["description"] application.description = new_data["description"]
if "environment_names" in new_data:
Environments.create_many(
g.current_user, application, new_data["environment_names"]
)
db.session.add(application) db.session.add(application)
db.session.commit() db.session.commit()

View File

@ -11,7 +11,7 @@ class EditEnvironmentForm(BaseForm):
) )
class ApplicationForm(BaseForm): class NameAndDescriptionForm(BaseForm):
name = StringField( name = StringField(
label=translate("forms.application.name_label"), validators=[Required()] label=translate("forms.application.name_label"), validators=[Required()]
) )
@ -20,7 +20,7 @@ class ApplicationForm(BaseForm):
) )
class NewApplicationForm(ApplicationForm): class EnvironmentsForm(BaseForm):
environment_names = FieldList( environment_names = FieldList(
StringField(label=translate("forms.application.environment_names_label")), StringField(label=translate("forms.application.environment_names_label")),
validators=[ validators=[

View File

@ -3,35 +3,130 @@ from flask import redirect, render_template, request as http_request, url_for, g
from . import applications_bp from . import applications_bp
from atst.domain.applications import Applications from atst.domain.applications import Applications
from atst.domain.portfolios import Portfolios from atst.domain.portfolios import Portfolios
from atst.forms.application import NewApplicationForm from atst.forms.application import NameAndDescriptionForm, EnvironmentsForm
from atst.domain.authz.decorator import user_can_access_decorator as user_can from atst.domain.authz.decorator import user_can_access_decorator as user_can
from atst.models.permissions import Permissions from atst.models.permissions import Permissions
from atst.utils.flash import formatted_flash as flash
def get_new_application_form(form_data, form_class, application_id=None):
if application_id:
application = Applications.get(application_id)
return form_class(form_data, obj=application)
else:
return form_class(form_data)
def render_new_application_form(
template, form_class, portfolio_id=None, application_id=None, form=None
):
render_args = {"application_id": application_id}
if application_id:
application = Applications.get(application_id)
render_args["form"] = form or form_class(obj=application)
else:
render_args["form"] = form or form_class()
return render_template(template, **render_args)
@applications_bp.route("/portfolios/<portfolio_id>/applications/new") @applications_bp.route("/portfolios/<portfolio_id>/applications/new")
@applications_bp.route(
"/portfolios/<portfolio_id>/applications/<application_id>/step_1"
)
@user_can(Permissions.CREATE_APPLICATION, message="view create new application form") @user_can(Permissions.CREATE_APPLICATION, message="view create new application form")
def new(portfolio_id): def view_new_application_step_1(portfolio_id, application_id=None):
form = NewApplicationForm() return render_new_application_form(
return render_template("portfolios/applications/new.html", form=form) "portfolios/applications/new/step_1.html",
NameAndDescriptionForm,
portfolio_id=portfolio_id,
application_id=application_id,
)
@applications_bp.route("/portfolios/<portfolio_id>/applications", methods=["POST"]) @applications_bp.route(
@user_can(Permissions.CREATE_APPLICATION, message="create new application") "/portfolios/<portfolio_id>/applications/new",
def create(portfolio_id): endpoint="create_new_application_step_1",
methods=["POST"],
)
@applications_bp.route(
"/portfolios/<portfolio_id>/applications/<application_id>/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) portfolio = Portfolios.get_for_update(portfolio_id)
form = NewApplicationForm(http_request.form) form = get_new_application_form(
{**http_request.form}, NameAndDescriptionForm, application_id
)
if form.validate(): if form.validate():
application_data = form.data application = None
Applications.create( if application_id:
g.current_user, application = Applications.get(application_id)
portfolio, application = Applications.update(application, form.data)
application_data["name"], else:
application_data["description"], application = Applications.create(g.current_user, portfolio, **form.data)
application_data["environment_names"],
)
return redirect( return redirect(
url_for("applications.portfolio_applications", portfolio_id=portfolio_id) url_for(
"applications.update_new_application_step_2",
portfolio_id=portfolio_id,
application_id=application.id,
)
) )
else: else:
return render_template("portfolios/applications/new.html", form=form) return (
render_new_application_form(
"portfolios/applications/new/step_1.html",
NameAndDescriptionForm,
portfolio_id,
application_id,
form,
),
400,
)
@applications_bp.route(
"/portfolios/<portfolio_id>/applications/<application_id>/step_2"
)
@user_can(Permissions.CREATE_APPLICATION, message="view create new application form")
def view_new_application_step_2(portfolio_id, application_id):
return render_new_application_form(
"portfolios/applications/new/step_2.html",
EnvironmentsForm,
portfolio_id=portfolio_id,
application_id=application_id,
)
@applications_bp.route(
"/portfolios/<portfolio_id>/applications/<application_id>/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):
form = get_new_application_form(
{**http_request.form}, EnvironmentsForm, application_id
)
if form.validate():
application = Applications.get(application_id)
application = Applications.update(application, form.data)
flash("application_created", application_name=application.name)
return redirect(
url_for(
"applications.portfolio_applications",
portfolio_id=application.portfolio_id,
)
)
else:
return (
render_new_application_form(
"portfolios/applications/new/step_2.html",
EnvironmentsForm,
portfolio_id,
application_id,
form,
),
400,
)

View File

@ -8,8 +8,8 @@ from atst.domain.application_roles import ApplicationRoles
from atst.domain.audit_log import AuditLog from atst.domain.audit_log import AuditLog
from atst.domain.common import Paginator from atst.domain.common import Paginator
from atst.domain.environment_roles import EnvironmentRoles from atst.domain.environment_roles import EnvironmentRoles
from atst.forms.application import ApplicationForm, EditEnvironmentForm
from atst.forms.application_member import NewForm as NewMemberForm, UpdateMemberForm from atst.forms.application_member import NewForm as NewMemberForm, UpdateMemberForm
from atst.forms.application import NameAndDescriptionForm, EditEnvironmentForm
from atst.forms.data import ENV_ROLE_NO_ACCESS as NO_ACCESS from atst.forms.data import ENV_ROLE_NO_ACCESS as NO_ACCESS
from atst.domain.authz.decorator import user_can_access_decorator as user_can from atst.domain.authz.decorator import user_can_access_decorator as user_can
from atst.models.permissions import Permissions from atst.models.permissions import Permissions
@ -125,7 +125,7 @@ def render_settings_page(application, **kwargs):
members = get_members_data(application) members = get_members_data(application)
if "application_form" not in kwargs: if "application_form" not in kwargs:
kwargs["application_form"] = ApplicationForm( kwargs["application_form"] = NameAndDescriptionForm(
name=application.name, description=application.description name=application.name, description=application.description
) )
@ -229,7 +229,7 @@ def new_environment(application_id):
@user_can(Permissions.EDIT_APPLICATION, message="update application") @user_can(Permissions.EDIT_APPLICATION, message="update application")
def update(application_id): def update(application_id):
application = Applications.get(application_id) application = Applications.get(application_id)
form = ApplicationForm(http_request.form) form = NameAndDescriptionForm(http_request.form)
if form.validate(): if form.validate():
application_data = form.data application_data = form.data
Applications.update(application, application_data) Applications.update(application, application_data)

View File

@ -7,6 +7,13 @@ MESSAGES = {
"message_template": "Portfolio '{{portfolio_name}}' has been deleted", "message_template": "Portfolio '{{portfolio_name}}' has been deleted",
"category": "success", "category": "success",
}, },
"application_created": {
"title_template": translate("flash.success"),
"message_template": """
{{ "flash.application.created" | translate({"application_name": application_name}) }}
""",
"category": "success",
},
"application_deleted": { "application_deleted": {
"title_template": translate("flash.success"), "title_template": translate("flash.success"),
"message_template": """ "message_template": """

View File

@ -1,11 +1,11 @@
import FormMixin from '../../mixins/form' import FormMixin from '../../../mixins/form'
import textinput from '../text_input' import textinput from '../../text_input'
import * as R from 'ramda' import * as R from 'ramda'
const createEnvironment = name => ({ name }) const createEnvironment = name => ({ name })
export default { export default {
name: 'new-application', name: 'application-environments',
mixins: [FormMixin], mixins: [FormMixin],
@ -18,7 +18,6 @@ export default {
type: Object, type: Object,
default: () => ({}), default: () => ({}),
}, },
modalName: String,
}, },
data: function() { data: function() {
@ -47,16 +46,18 @@ export default {
errors: [], errors: [],
environments, environments,
name, name,
readyToSubmit: false, changed: true,
} }
}, },
mounted: function() { mounted: function() {
this.$root.$on('onEnvironmentAdded', this.addEnvironment) this.$root.$on('onEnvironmentAdded', this.addEnvironment)
this.validate()
}, },
methods: { methods: {
addEnvironment: function(event) { addEnvironment: function(_event) {
this.changed = false
this.environments.push(createEnvironment('')) this.environments.push(createEnvironment(''))
}, },
@ -64,6 +65,7 @@ export default {
if (this.environments.length > 1) { if (this.environments.length > 1) {
this.environments.splice(index, 1) this.environments.splice(index, 1)
} }
this.validate()
}, },
validate: function() { validate: function() {
@ -75,6 +77,7 @@ export default {
R.filter(Boolean), R.filter(Boolean),
R.take(1) R.take(1)
)(this.validations) )(this.validations)
this.invalid = this.errors.length > 0
}, },
hasEnvironments: function() { hasEnvironments: function() {
@ -99,34 +102,9 @@ export default {
return names.every((n, index) => names.indexOf(n) === index) return names.every((n, index) => names.indexOf(n) === index)
}, },
handleSubmit: function(event) { onInput: function(e) {
if (!this.readyToSubmit) { this.changed = true
event.preventDefault()
this.validateAndOpenModal()
}
},
handleCancelSubmit: function() {
this.readyToSubmit = false
this.closeModal(this.modalName)
},
validateAndOpenModal: function() {
let isValid = this.$children.reduce((previous, newVal) => {
// display textInput error if it is not valid
if (!newVal.showValid) {
newVal.showError = true
}
return newVal.showValid && previous
}, true)
this.validate() this.validate()
isValid = this.errors.length == 0 && isValid
if (isValid) {
this.readyToSubmit = true
this.openModal(this.modalName)
}
}, },
}, },
} }

View File

@ -0,0 +1,19 @@
import FormMixin from '../../../mixins/form'
import textinput from '../../text_input'
import * as R from 'ramda'
export default {
name: 'application-name-and-description',
mixins: [FormMixin],
components: {
textinput,
},
data: function() {
return {
errors: [],
}
},
}

View File

@ -17,7 +17,8 @@ import EditOfficerForm from './components/forms/edit_officer_form'
import poc from './components/forms/poc' import poc from './components/forms/poc'
import oversight from './components/forms/oversight' import oversight from './components/forms/oversight'
import toggler from './components/toggler' import toggler from './components/toggler'
import NewApplication from './components/forms/new_application' import ApplicationNameAndDescription from './components/forms/new_application/name_and_description'
import ApplicationEnvironments from './components/forms/new_application/environments'
import { EditEnvironmentRole } from './components/forms/edit_environment_role' import { EditEnvironmentRole } from './components/forms/edit_environment_role'
import EditApplicationRoles from './components/forms/edit_application_roles' import EditApplicationRoles from './components/forms/edit_application_roles'
import MultiStepModalForm from './components/forms/multi_step_modal_form' import MultiStepModalForm from './components/forms/multi_step_modal_form'
@ -60,7 +61,8 @@ const app = new Vue({
checkboxinput, checkboxinput,
poc, poc,
oversight, oversight,
NewApplication, ApplicationNameAndDescription,
ApplicationEnvironments,
selector, selector,
BudgetChart, BudgetChart,
SpendTable, SpendTable,

View File

@ -13,7 +13,7 @@
<div class='portfolio-applications__header--title col col--grow'>Applications</div> <div class='portfolio-applications__header--title col col--grow'>Applications</div>
<div class='portfolio-applications__header--actions col'> <div class='portfolio-applications__header--actions col'>
{% if can_create_applications %} {% if can_create_applications %}
<a class='icon-link' href='{{ url_for('applications.new', portfolio_id=portfolio.id) }}'> <a class='icon-link' href='{{ url_for('applications.view_new_application_step_1', portfolio_id=portfolio.id) }}'>
{{ 'portfolios.applications.add_application_text' | translate }} {{ 'portfolios.applications.add_application_text' | translate }}
{{ Icon("plus", classes="sidenav__link-icon") }} {{ Icon("plus", classes="sidenav__link-icon") }}
</a> </a>
@ -26,7 +26,7 @@
{{ EmptyState( {{ EmptyState(
'This portfolio doesnt have any applications', 'This portfolio doesnt have any applications',
action_label='Add a new application' if can_create_applications else None, action_label='Add a new application' if can_create_applications else None,
action_href=url_for('applications.new', portfolio_id=portfolio.id) if can_create_applications else None, action_href=url_for('applications.create_new_application_step_1', portfolio_id=portfolio.id) if can_create_applications else None,
icon='cloud', icon='cloud',
sub_message=None if can_create_applications else 'Please contact your JEDI Cloud portfolio administrator to set up a new application.', sub_message=None if can_create_applications else 'Please contact your JEDI Cloud portfolio administrator to set up a new application.',
add_perms=can_create_applications add_perms=can_create_applications

View File

@ -0,0 +1,51 @@
{% extends "portfolios/applications/base.html" %}
{% from "components/alert.html" import Alert %}
{% from "components/text_input.html" import TextInput %}
{% from 'components/save_button.html' import SaveButton %}
{% 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) %}
{% else %}
{% set action = url_for('applications.create_new_application_step_1', portfolio_id=portfolio.id, application_id=application_id) %}
{% endif %}
{% block application_content %}
{% include "fragments/flash.html" %}
<div class='subheading'>{{ 'portfolios.applications.settings_heading' | translate }}</div>
<application-name-and-description inline-template v-bind:initial-data='{{ form.data|tojson }}'>
<form method="POST" action="{{ action }}" v-on:submit="handleSubmit">
<div class="panel">
<div class="panel__content">
{{ form.csrf_token }}
<p>
{{ "fragments.edit_application_form.explain" | translate }}
</p>
<div class="form-row">
<div class="form-col form-col--two-thirds">
{{ TextInput(form.name, optional=False) }}
</div>
</div>
<div class="form-row">
<div class="form-col form-col--two-thirds">
{{ TextInput(form.description, paragraph=True, optional=False) }}
</div>
</div>
</div>
</div>
<span class="action-group">
{% block next_button %}
{{ SaveButton(text=('portfolios.applications.next_button_text' | translate)) }}
{% endblock %}
</span>
</form>
</application-name-and-description>
{% endblock %}

View File

@ -3,7 +3,6 @@
{% from "components/alert.html" import Alert %} {% from "components/alert.html" import Alert %}
{% from "components/icon.html" import Icon %} {% from "components/icon.html" import Icon %}
{% from "components/modal.html" import Modal %} {% from "components/modal.html" import Modal %}
{% from "components/text_input.html" import TextInput %}
{% from 'components/save_button.html' import SaveButton %} {% from 'components/save_button.html' import SaveButton %}
{% set secondary_breadcrumb = 'portfolios.applications.new_application_title' | translate %} {% set secondary_breadcrumb = 'portfolios.applications.new_application_title' | translate %}
@ -14,50 +13,11 @@
{% include "fragments/flash.html" %} {% include "fragments/flash.html" %}
<div class='subheading'>{{ 'portfolios.applications.settings_heading' | translate }}</div> <div class='subheading'>{{ 'portfolios.applications.settings_heading' | translate }}</div>
<application-environments inline-template v-bind:initial-data='{{ form.data|tojson }}'>
<new-application inline-template v-bind:initial-data='{{ form.data|tojson }}' modal-name='{{ modalName }}'> <form method="POST" action="{{ url_for('applications.view_new_application_step_2', portfolio_id=portfolio.id, application_id=application_id) }}" v-on:submit="handleSubmit">
<form method="POST" action="{{ url_for('applications.create', portfolio_id=portfolio.id) }}" v-on:submit="handleSubmit">
<div class="panel"> <div class="panel">
<div class="panel__content"> <div class="panel__content">
{% call Modal(name=modalName, dismissable=False) %}
<h1>Create application !{ name }</h1>
<p>
When you click <em>{{ 'portfolios.applications.create_button_text' | translate }}</em>, the environments
<span v-for="(environment, index) in environments">
<strong>!{environment.name}</strong><template v-if="index < (environments.length - 1)">, </template>
</span>
will be created as individual cloud resource groups under <strong>!{ name }</strong> application.
</p>
<div class='action-group'>
<button autofocus type='submit' class='action-group__action usa-button' tabindex='0'>{{ 'portfolios.applications.create_button_text' | translate }}</button>
<button type='button' v-on:click="handleCancelSubmit" class='icon-link action-group__action' tabindex='0'>Cancel</button>
</div>
{% endcall %}
{{ form.csrf_token }} {{ form.csrf_token }}
<p>
{{ "fragments.edit_application_form.explain" | translate }}
</p>
<div class="form-row">
<div class="form-col form-col--two-thirds">
{{ TextInput(form.name, optional=False) }}
</div>
<div class="form-col form-col--third">
&nbsp;
</div>
</div>
<div class="form-row">
<div class="form-col form-col--two-thirds">
{{ TextInput(form.description, paragraph=True, optional=False) }}
</div>
<div class="form-col form-col--third">
&nbsp;
</div>
</div>
<div> {# this extra div prevents this bug: https://www.pivotaltracker.com/story/show/160768940 #} <div> {# this extra div prevents this bug: https://www.pivotaltracker.com/story/show/160768940 #}
<div v-cloak v-for="title in errors" :key="title"> <div v-cloak v-for="title in errors" :key="title">
{{ Alert(message=None, level="error", vue_template=True) }} {{ Alert(message=None, level="error", vue_template=True) }}
@ -76,7 +36,7 @@
<li v-for="(environment, i) in environments" class="application-edit__env-list-item"> <li v-for="(environment, i) in environments" class="application-edit__env-list-item">
<div class="usa-input"> <div class="usa-input">
<label :for="'environment_names-' + i">Environment Name</label> <label :for="'environment_names-' + i">Environment Name</label>
<input type="text" :id="'environment_names-' + i" v-model="environment.name" placeholder="e.g. Development, Staging, Production"/> <input type="hidden" :name="'environment_names-' + i" v-model="environment.name"/> <input type="text" :id="'environment_names-' + i" v-model="environment.name" @input="onInput" placeholder="e.g. Development, Staging, Production"/> <input type="hidden" :name="'environment_names-' + i" v-model="environment.name"/>
</div> </div>
<div class="application-edit__env-list-item-block"> <div class="application-edit__env-list-item-block">
<button v-on:click="removeEnvironment(i)" v-if="environments.length > 1" type="button" class="application-edit__env-list-item__remover"> <button v-on:click="removeEnvironment(i)" v-if="environments.length > 1" type="button" class="application-edit__env-list-item__remover">
@ -98,14 +58,15 @@
</button> </button>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="action-group"> <span class="action-group">
{% block next_button %}
{{ SaveButton(text=('portfolios.applications.create_button_text' | translate)) }} {{ SaveButton(text=('portfolios.applications.create_button_text' | translate)) }}
</div> {% endblock %}
</span>
</form> </form>
</new-application> </application-environments>
{% endblock %} {% endblock %}

View File

@ -141,7 +141,7 @@
{{ EmptyState( {{ EmptyState(
'Nothing to report', 'Nothing to report',
action_label='Add a new application' if can_create_applications else None, action_label='Add a new application' if can_create_applications else None,
action_href=url_for('applications.new', portfolio_id=portfolio.id) if can_create_applications else None, action_href=url_for('applications.create_new_application_step_1', portfolio_id=portfolio.id) if can_create_applications else None,
icon='chart', icon='chart',
sub_message=message, sub_message=message,
add_perms=can_create_applications add_perms=can_create_applications

View File

@ -44,7 +44,7 @@ def test_user_with_permission_has_add_application_link(client, user_session):
url_for("applications.portfolio_applications", portfolio_id=portfolio.id) url_for("applications.portfolio_applications", portfolio_id=portfolio.id)
) )
assert ( assert (
url_for("applications.create", portfolio_id=portfolio.id) url_for("applications.create_new_application_step_1", portfolio_id=portfolio.id)
in response.data.decode() in response.data.decode()
) )
@ -58,7 +58,7 @@ def test_user_without_permission_has_no_add_application_link(client, user_sessio
url_for("applications.portfolio_applications", portfolio_id=portfolio.id) url_for("applications.portfolio_applications", portfolio_id=portfolio.id)
) )
assert ( assert (
url_for("applications.create", portfolio_id=portfolio.id) url_for("applications.create_new_application_step_1", portfolio_id=portfolio.id)
not in response.data.decode() not in response.data.decode()
) )

View File

@ -1,21 +1,96 @@
from flask import url_for from flask import url_for
from tests.factories import PortfolioFactory from tests.factories import PortfolioFactory, ApplicationFactory
from atst.domain.applications import Applications
def test_creating_application(client, user_session): def test_get_name_and_description_form(client, user_session):
portfolio = PortfolioFactory.create()
user_session(portfolio.owner)
response = client.get(
url_for("applications.view_new_application_step_1", portfolio_id=portfolio.id)
)
assert response.status_code == 200
def test_get_name_and_description_form_for_update(client, user_session):
name = "My Test Application"
description = "This is the description of the test application."
application = ApplicationFactory.create(name=name, description=description)
user_session(application.portfolio.owner)
response = client.get(
url_for(
"applications.view_new_application_step_1",
portfolio_id=application.portfolio.id,
application_id=application.id,
)
)
assert response.status_code == 200
assert name in response.data.decode()
assert description in response.data.decode()
def test_post_name_and_description(client, user_session):
portfolio = PortfolioFactory.create() portfolio = PortfolioFactory.create()
user_session(portfolio.owner) user_session(portfolio.owner)
response = client.post( response = client.post(
url_for("applications.create", portfolio_id=portfolio.id), url_for(
data={ "applications.create_new_application_step_1", portfolio_id=portfolio.id
"name": "Test Application", ),
"description": "This is only a test", data={"name": "Test Application", "description": "This is only a test"},
"environment_names-0": "dev",
"environment_names-1": "staging",
"environment_names-2": "prod",
},
) )
assert response.status_code == 302 assert response.status_code == 302
assert len(portfolio.applications) == 1 assert len(portfolio.applications) == 1
assert len(portfolio.applications[0].environments) == 3 assert portfolio.applications[0].name == "Test Application"
assert portfolio.applications[0].description == "This is only a test"
def test_post_name_and_description_for_update(client, session, user_session):
application = ApplicationFactory.create()
user_session(application.portfolio.owner)
response = client.post(
url_for(
"applications.update_new_application_step_1",
portfolio_id=application.portfolio.id,
application_id=application.id,
),
data={"name": "Test Application", "description": "This is only a test"},
)
assert response.status_code == 302
session.refresh(application)
assert application.name == "Test Application"
assert application.description == "This is only a test"
def test_get_environments(client, user_session):
application = ApplicationFactory.create()
user_session(application.portfolio.owner)
response = client.get(
url_for(
"applications.view_new_application_step_2",
portfolio_id=application.portfolio.id,
application_id=application.id,
)
)
assert response.status_code == 200
def test_post_environments(client, session, user_session):
application = ApplicationFactory.create(environments=[])
user_session(application.portfolio.owner)
response = client.post(
url_for(
"applications.update_new_application_step_2",
portfolio_id=application.portfolio.id,
application_id=application.id,
),
data={
"environment_names-0": "development",
"environment_names-1": "staging",
"environment_names-2": "production",
},
)
assert response.status_code == 302
session.refresh(application)
assert len(application.environments) == 3

View File

@ -222,17 +222,105 @@ def test_applications_access_environment_access(get_url_assert_status):
get_url_assert_status(ccpo, url, 404) get_url_assert_status(ccpo, url, 404)
# applications.create # applications.view_new_application_step_1
def test_applications_create_access(post_url_assert_status): def test_applications_get_application_step_1(get_url_assert_status):
ccpo = user_with(PermissionSets.EDIT_PORTFOLIO_APPLICATION_MANAGEMENT) ccpo = user_with(PermissionSets.EDIT_PORTFOLIO_APPLICATION_MANAGEMENT)
owner = user_with() owner = user_with()
rando = user_with() rando = user_with()
portfolio = PortfolioFactory.create(owner=owner) portfolio = PortfolioFactory.create(owner=owner)
url = url_for("applications.create", portfolio_id=portfolio.id) url = url_for("applications.view_new_application_step_1", portfolio_id=portfolio.id)
post_url_assert_status(ccpo, url, 200) get_url_assert_status(ccpo, url, 200)
post_url_assert_status(owner, url, 200) get_url_assert_status(owner, url, 200)
post_url_assert_status(rando, url, 404) get_url_assert_status(rando, url, 404)
# applications.view_new_application_step_1
def test_applications_get_application_step_1_update(get_url_assert_status):
ccpo = user_with(PermissionSets.EDIT_PORTFOLIO_APPLICATION_MANAGEMENT)
owner = user_with()
rando = user_with()
portfolio = PortfolioFactory.create(owner=owner)
application = ApplicationFactory.create(portfolio=portfolio)
url = url_for(
"applications.view_new_application_step_1",
portfolio_id=portfolio.id,
application_id=application.id,
)
get_url_assert_status(ccpo, url, 200)
get_url_assert_status(owner, url, 200)
get_url_assert_status(rando, url, 404)
# applications.create_or_update_new_application_step_1
def test_applications_post_application_step_1(post_url_assert_status):
ccpo = user_with(PermissionSets.EDIT_PORTFOLIO_APPLICATION_MANAGEMENT)
owner = user_with()
rando = user_with()
portfolio = PortfolioFactory.create(owner=owner)
application = ApplicationFactory.create(portfolio=portfolio)
step_1_form_data = {
"name": "Test Application",
"description": "This is only a test",
}
url = url_for(
"applications.create_new_application_step_1", portfolio_id=portfolio.id
)
post_url_assert_status(ccpo, url, 302, data=step_1_form_data)
post_url_assert_status(owner, url, 302, data=step_1_form_data)
post_url_assert_status(rando, url, 404, data=step_1_form_data)
url = url_for(
"applications.update_new_application_step_1",
portfolio_id=portfolio.id,
application_id=application.id,
)
post_url_assert_status(ccpo, url, 302, data=step_1_form_data)
post_url_assert_status(owner, url, 302, data=step_1_form_data)
post_url_assert_status(rando, url, 404, data=step_1_form_data)
# applications.view_new_application_step_2
def test_applications_get_application_step_2(get_url_assert_status):
ccpo = user_with(PermissionSets.EDIT_PORTFOLIO_APPLICATION_MANAGEMENT)
owner = user_with()
rando = user_with()
portfolio = PortfolioFactory.create(owner=owner)
application = ApplicationFactory.create(portfolio=portfolio)
url = url_for(
"applications.view_new_application_step_2",
portfolio_id=portfolio.id,
application_id=application.id,
)
get_url_assert_status(ccpo, url, 200)
get_url_assert_status(owner, url, 200)
get_url_assert_status(rando, url, 404)
# applications.update_new_application_step_2
def test_applications_post_application_step_2(post_url_assert_status):
ccpo = user_with(PermissionSets.EDIT_PORTFOLIO_APPLICATION_MANAGEMENT)
owner = user_with()
rando = user_with()
portfolio = PortfolioFactory.create(owner=owner)
application = ApplicationFactory.create(portfolio=portfolio)
step_2_form_data = {
"environment_names-0": "development",
"environment_names-1": "staging",
"environment_names-2": "production",
}
url = url_for(
"applications.update_new_application_step_1",
portfolio_id=portfolio.id,
application_id=application.id,
)
post_url_assert_status(ccpo, url, 302, data=step_2_form_data)
post_url_assert_status(owner, url, 302, data=step_2_form_data)
post_url_assert_status(rando, url, 404, data=step_2_form_data)
# portfolios.invite_member # portfolios.invite_member
@ -319,7 +407,9 @@ def test_applications_new_access(get_url_assert_status):
rando = user_with() rando = user_with()
portfolio = PortfolioFactory.create(owner=owner) portfolio = PortfolioFactory.create(owner=owner)
url = url_for("applications.new", portfolio_id=portfolio.id) url = url_for(
"applications.create_new_application_step_1", portfolio_id=portfolio.id
)
get_url_assert_status(ccpo, url, 200) get_url_assert_status(ccpo, url, 200)
get_url_assert_status(owner, url, 200) get_url_assert_status(owner, url, 200)
get_url_assert_status(rando, url, 404) get_url_assert_status(rando, url, 404)

View File

@ -101,6 +101,7 @@ email:
portfolio_invite: "{inviter_name} has invited you to a JEDI cloud portfolio" portfolio_invite: "{inviter_name} has invited you to a JEDI cloud portfolio"
flash: flash:
application: application:
created: 'You have successfully created the {application_name} application.'
deleted: 'You have successfully deleted the {application_name} application. To view the retained activity log, visit the portfolio administration page.' 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.' delete_member_success: 'You have successfully deleted {member_name} from the portfolio.'
deleted_member: Portfolio member deleted deleted_member: Portfolio member deleted
@ -296,6 +297,7 @@ portfolios:
add_another_environment: Add another environment add_another_environment: Add another environment
app_settings_text: App settings app_settings_text: App settings
create_button_text: Create create_button_text: Create
next_button_text: "Next: Environments"
create_new_env: Create a new environment. 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. 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 csp_console_text: CSP console