Merge pull request #1343 from dod-ccpo/text-input-validation

Form text input validation
This commit is contained in:
graham-dds 2020-01-22 15:39:26 -05:00 committed by GitHub
commit a5684d099e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 203 additions and 143 deletions

View File

@ -3,7 +3,7 @@
"files": "^.secrets.baseline$|^.*pgsslrootcert.yml$",
"lines": null
},
"generated_at": "2020-01-09T16:55:07Z",
"generated_at": "2020-01-16T16:10:27Z",
"plugins_used": [
{
"base64_limit": 4.5,

View File

@ -222,7 +222,7 @@ def make_config(direct_config=None):
config.read_dict({"default": direct_config})
# Assemble DATABASE_URI value
database_uri = "postgres://{}:{}@{}:{}/{}".format( # pragma: allowlist secret
database_uri = "postgresql://{}:{}@{}:{}/{}".format( # pragma: allowlist secret
config.get("default", "PGUSER"),
config.get("default", "PGPASSWORD"),
config.get("default", "PGHOST"),

View File

@ -1,14 +1,14 @@
from .forms import BaseForm, remove_empty_string
from wtforms.fields import StringField, TextAreaField, FieldList
from wtforms.validators import Required, Optional
from atst.forms.validators import ListItemRequired, ListItemsUnique
from wtforms.validators import Required, Optional, Length
from atst.forms.validators import ListItemRequired, ListItemsUnique, Name, AlphaNumeric
from atst.utils.localization import translate
class EditEnvironmentForm(BaseForm):
name = StringField(
label=translate("forms.environments.name_label"),
validators=[Required()],
validators=[Required(), Name(), Length(max=100)],
filters=[remove_empty_string],
)
@ -16,12 +16,12 @@ class EditEnvironmentForm(BaseForm):
class NameAndDescriptionForm(BaseForm):
name = StringField(
label=translate("forms.application.name_label"),
validators=[Required()],
validators=[Required(), Name(), Length(max=100)],
filters=[remove_empty_string],
)
description = TextAreaField(
label=translate("forms.application.description_label"),
validators=[Optional()],
validators=[Optional(), Length(max=1_000)],
filters=[remove_empty_string],
)
@ -31,6 +31,7 @@ class EnvironmentsForm(BaseForm):
StringField(
label=translate("forms.application.environment_names_label"),
filters=[remove_empty_string],
validators=[AlphaNumeric(), Length(max=100)],
),
validators=[
ListItemRequired(

View File

@ -1,5 +1,6 @@
from flask_wtf import FlaskForm
from wtforms.fields import FormField, FieldList, HiddenField, BooleanField
from wtforms.validators import UUID
from wtforms import Form
from .member import NewForm as BaseNewMemberForm
@ -7,11 +8,13 @@ from .data import ENV_ROLES, ENV_ROLE_NO_ACCESS as NO_ACCESS
from atst.forms.fields import SelectField
from atst.domain.permission_sets import PermissionSets
from atst.utils.localization import translate
from atst.forms.validators import AlphaNumeric
from wtforms.validators import Length
class EnvironmentForm(Form):
environment_id = HiddenField()
environment_name = HiddenField()
environment_id = HiddenField(validators=[UUID()])
environment_name = HiddenField(validators=[AlphaNumeric(), Length(max=100)])
role = SelectField(
environment_name,
choices=ENV_ROLES,

View File

@ -2,12 +2,12 @@ from flask_wtf import FlaskForm
from wtforms.validators import Required, Length
from wtforms.fields import StringField
from atst.forms.validators import IsNumber
from atst.forms.validators import Number
from atst.utils.localization import translate
class CCPOUserForm(FlaskForm):
dod_id = StringField(
translate("forms.new_member.dod_id_label"),
validators=[Required(), Length(min=10, max=10), IsNumber()],
validators=[Required(), Length(min=10, max=10), Number()],
)

View File

@ -9,22 +9,26 @@ from .forms import BaseForm
from .data import SERVICE_BRANCHES
from atst.models.user import User
from atst.utils.localization import translate
from wtforms.validators import Length
from atst.forms.validators import Number
from .validators import Name, DateRange, PhoneNumber
USER_FIELDS = {
"first_name": StringField(
translate("forms.edit_user.first_name_label"), validators=[Name()]
translate("forms.edit_user.first_name_label"),
validators=[Name(), Length(max=100)],
),
"last_name": StringField(
translate("forms.edit_user.last_name_label"), validators=[Name()]
translate("forms.edit_user.last_name_label"),
validators=[Name(), Length(max=100)],
),
"email": EmailField(translate("forms.edit_user.email_label"), validators=[Email()]),
"phone_number": TelField(
translate("forms.edit_user.phone_number_label"), validators=[PhoneNumber()]
),
"phone_ext": StringField("Extension"),
"phone_ext": StringField("Extension", validators=[Number(), Length(max=10)]),
"service_branch": SelectField(
translate("forms.edit_user.service_branch_label"), choices=SERVICE_BRANCHES
),

View File

@ -3,16 +3,18 @@ from wtforms.fields.html5 import EmailField, TelField
from wtforms.validators import Required, Email, Length, Optional
from wtforms.fields import StringField
from atst.forms.validators import IsNumber, PhoneNumber
from atst.forms.validators import Number, PhoneNumber, Name
from atst.utils.localization import translate
class NewForm(FlaskForm):
first_name = StringField(
label=translate("forms.new_member.first_name_label"), validators=[Required()]
label=translate("forms.new_member.first_name_label"),
validators=[Required(), Name(), Length(max=100)],
)
last_name = StringField(
label=translate("forms.new_member.last_name_label"), validators=[Required()]
label=translate("forms.new_member.last_name_label"),
validators=[Required(), Name(), Length(max=100)],
)
email = EmailField(
translate("forms.new_member.email_label"), validators=[Required(), Email()]
@ -21,8 +23,8 @@ class NewForm(FlaskForm):
translate("forms.new_member.phone_number_label"),
validators=[Optional(), PhoneNumber()],
)
phone_ext = StringField("Extension")
phone_ext = StringField("Extension", validators=[Number(), Length(max=10)])
dod_id = StringField(
translate("forms.new_member.dod_id_label"),
validators=[Required(), Length(min=10), IsNumber()],
validators=[Required(), Length(min=10), Number()],
)

View File

@ -4,6 +4,7 @@ from wtforms.fields import (
TextAreaField,
)
from wtforms.validators import Length, InputRequired
from atst.forms.validators import Name
from wtforms.widgets import ListWidget, CheckboxInput
from .forms import BaseForm
@ -20,10 +21,13 @@ class PortfolioForm(BaseForm):
min=4,
max=100,
message=translate("forms.portfolio.name.length_validation_message"),
)
),
Name(),
],
)
description = TextAreaField(translate("forms.portfolio.description.label"),)
description = TextAreaField(
translate("forms.portfolio.description.label"), validators=[Length(max=1_000)]
)
class PortfolioCreationForm(PortfolioForm):

View File

@ -7,18 +7,23 @@ from wtforms.fields import (
HiddenField,
)
from wtforms.fields.html5 import DateField
from wtforms.validators import Required, Length, NumberRange, ValidationError, Regexp
from wtforms.validators import (
Required,
Length,
NumberRange,
ValidationError,
)
from flask_wtf import FlaskForm
from numbers import Number
import numbers
from atst.forms.validators import Number, AlphaNumeric
from .data import JEDI_CLIN_TYPES
from .fields import SelectField
from .forms import BaseForm, remove_empty_string
from atst.utils.localization import translate
from .validators import REGEX_ALPHA_NUMERIC
from flask import current_app as app
MAX_CLIN_AMOUNT = 1000000000
MAX_CLIN_AMOUNT = 1_000_000_000
def coerce_enum(enum_inst):
@ -30,8 +35,8 @@ def coerce_enum(enum_inst):
def validate_funding(form, field):
if (
isinstance(form.total_amount.data, Number)
and isinstance(field.data, Number)
isinstance(form.total_amount.data, numbers.Number)
and isinstance(field.data, numbers.Number)
and form.total_amount.data < field.data
):
raise ValidationError(
@ -62,7 +67,10 @@ class CLINForm(FlaskForm):
coerce=coerce_enum,
)
number = StringField(label=translate("task_orders.form.clin_number_label"))
number = StringField(
label=translate("task_orders.form.clin_number_label"),
validators=[Number(), Length(max=4)],
)
start_date = DateField(
translate("task_orders.form.pop_start"),
description=translate("task_orders.form.pop_example"),
@ -120,7 +128,7 @@ class AttachmentForm(BaseForm):
Length(
max=100, message=translate("forms.attachment.filename.length_error")
),
Regexp(regex=REGEX_ALPHA_NUMERIC),
AlphaNumeric(),
],
)
object_name = HiddenField(
@ -129,7 +137,7 @@ class AttachmentForm(BaseForm):
Length(
max=40, message=translate("forms.attachment.object_name.length_error")
),
Regexp(regex=REGEX_ALPHA_NUMERIC),
AlphaNumeric(),
],
)
accept = ".pdf,application/pdf"
@ -142,6 +150,7 @@ class TaskOrderForm(BaseForm):
number = StringField(
label=translate("forms.task_order.number_description"),
filters=[remove_empty_string],
validators=[Number(), Length(max=13)],
)
pdf = FormField(
AttachmentForm,

View File

@ -2,15 +2,12 @@ from datetime import datetime
import re
from werkzeug.datastructures import FileStorage
from wtforms.validators import ValidationError
from wtforms.validators import ValidationError, Regexp
import pendulum
from atst.utils.localization import translate
REGEX_ALPHA_NUMERIC = "^[A-Za-z0-9\-_ \.]*$"
def DateRange(lower_bound=None, upper_bound=None, message=None):
def _date_range(form, field):
if field.data is None:
@ -34,12 +31,13 @@ def DateRange(lower_bound=None, upper_bound=None, message=None):
return _date_range
def IsNumber(message=translate("forms.validators.is_number_message")):
def Number(message=translate("forms.validators.is_number_message")):
def _is_number(form, field):
try:
int(field.data)
except (ValueError, TypeError):
raise ValidationError(message)
if field.data:
try:
int(field.data)
except (ValueError, TypeError):
raise ValidationError(message)
return _is_number
@ -100,3 +98,7 @@ def FileLength(max_length=50000000, message=None):
field.data.seek(0)
return _file_length
def AlphaNumeric(message=translate("forms.validators.alpha_numeric_message")):
return Regexp(regex=r"^[A-Za-z0-9\-_ \.]*$", message=message)

View File

@ -9,6 +9,12 @@ export default {
unmask: [],
validationError: 'Please enter a response',
},
clinNumber: {
mask: false,
match: /^\d{4}$/,
unmask: [],
validationError: 'Please enter a 4-digit CLIN number',
},
date: {
mask: [/\d/, /\d/, '/', /\d/, /\d/, '/', /\d/, /\d/, /\d/, /\d/],
match: /(0[1-9]|1[012])[- \/.](0[1-9]|[12][0-9]|3[01])[- \/.](19|20)\d\d/,
@ -34,6 +40,20 @@ export default {
unmask: ['$', ','],
validationError: 'Please enter a dollar amount',
},
defaultStringField: {
mask: false,
match: /^[A-Za-z0-9\-_ \.]{1,100}$/,
unmask: [],
validationError:
'Please enter a response of no more than 100 alphanumeric characters',
},
defaultTextAreaField: {
mask: false,
match: /^[A-Za-z0-9\-_ \.]{1,1000}$/,
unmask: [],
validationError:
'Please enter a response of no more than 1000 alphanumeric characters',
},
clinDollars: {
mask: createNumberMask({ prefix: '$', allowDecimal: true }),
match: /^-?\d+\.?\d*$/,
@ -53,6 +73,13 @@ export default {
unmask: [','],
validationError: 'Please enter a number',
},
name: {
mask: false,
match: /.{1,100}/,
unmask: [],
validationError:
'This field accepts letters, numbers, commas, apostrophes, hyphens, and periods.',
},
phoneExt: {
mask: createNumberMask({
prefix: '',
@ -71,7 +98,7 @@ export default {
unmask: [],
validationError: 'Portfolio names can be between 4-100 characters',
},
requiredField: {
required: {
mask: false,
match: /.+/,
unmask: [],

View File

@ -230,6 +230,8 @@
&--anything,
&--portfolioName,
&--requiredField,
&--defaultStringField,
&--defaultTextAreaField,
&--taskOrderNumber,
&--email {
input {

View File

@ -13,7 +13,7 @@
) }}
<div class="accordion-table__item-content new-env">
<div class="h4">{{ "portfolios.applications.enter_env_name" | translate }}</div>
{{ TextInput(new_env_form.name, label="", validation="requiredField", optional=False) }}
{{ TextInput(new_env_form.name, label="", validation="defaultStringField", optional=False) }}
<div class="action-group">
{{ SaveButton(text=('common.save' | translate), element="input", form="add-new-env") }}
<a class='action-group__action icon-link icon-link--default' v-on:click="toggle">

View File

@ -81,7 +81,7 @@
<base-form inline-template>
<form action="{{ url_for('applications.update_environment', environment_id=env['id']) }}" method="post" v-on:submit="handleSubmit">
{{ edit_form.csrf_token }}
{{ TextInput(edit_form.name, validation='requiredField', optional=False) }}
{{ TextInput(edit_form.name, validation='defaultStringField', optional=False) }}
{{
SaveButton(
text=("common.save_changes" | translate)

View File

@ -119,8 +119,8 @@
{% macro InfoFields(member_form) %}
<div class="user-info">
{{ TextInput(member_form.first_name, validation='requiredField', optional=False) }}
{{ TextInput(member_form.last_name, validation='requiredField', optional=False) }}
{{ TextInput(member_form.first_name, validation='name', optional=False) }}
{{ TextInput(member_form.last_name, validation='name', optional=False) }}
{{ TextInput(member_form.email, validation='email', optional=False) }}
{{ PhoneInput(member_form.phone_number, member_form.phone_ext)}}
{{ TextInput(member_form.dod_id, validation='dodId', optional=False) }}

View File

@ -26,14 +26,14 @@
{{ form.csrf_token }}
<div class="form-row">
<div class="form-col">
{{ TextInput(form.name, optional=False) }}
{{ TextInput(form.name, validation="name", optional=False) }}
{{ ('portfolios.applications.new.step_1_form_help_text.name' | translate | safe) }}
</div>
</div>
<hr>
<div class="form-row">
<div class="form-col form-col--two-thirds">
{{ TextInput(form.description, paragraph=True, optional=True) }}
{{ TextInput(form.description, validation="defaultTextAreaField", paragraph=True, optional=True) }}
{{ ('portfolios.applications.new.step_1_form_help_text.description' | translate | safe) }}
</div>
</div>

View File

@ -22,8 +22,8 @@
<base-form inline-template>
<form method="POST" action="{{ url_for('applications.update', application_id=application.id) }}" class="col col--half">
{{ application_form.csrf_token }}
{{ TextInput(application_form.name, optional=False) }}
{{ TextInput(application_form.description, paragraph=True, optional=True, showOptional=False) }}
{{ TextInput(application_form.name, validation="name", optional=False) }}
{{ TextInput(application_form.description, validation="defaultTextAreaField", paragraph=True, optional=True, showOptional=False) }}
<div class="action-group action-group--tight">
{{ SaveButton(text='common.save_changes'|translate) }}
</div>

View File

@ -41,7 +41,7 @@
<div class="form-row">
<div class="form-col">
{% if fields %}
{{ TextInput(fields.number, optional=False) }}
{{ TextInput(fields.number, validation='clinNumber', optional=False) }}
{% else %}
<textinput :name="'clins-' + clinIndex + '-number'" inline-template>
<div v-bind:class="['usa-input usa-input--validation--' + validation, { 'usa-input--error': showError, 'usa-input--success': showValid, 'usa-input--validation--paragraph': paragraph, 'no-max-width': noMaxWidth }]">

View File

@ -11,11 +11,11 @@
<div class='panel__content'>
<div class='form-row'>
<div class='form-col form-col--half'>
{{ TextInput(form.first_name, validation='requiredField', optional=False) }}
{{ TextInput(form.first_name, validation='name', optional=False) }}
</div>
<div class='form-col form-col--half'>
{{ TextInput(form.last_name, validation='requiredField', optional=False) }}
{{ TextInput(form.last_name, validation='name', optional=False) }}
</div>
</div>

View File

@ -20,7 +20,7 @@
<form method="POST" action="{{ url_for('portfolios.edit', portfolio_id=portfolio.id) }}" autocomplete="false">
{{ portfolio_form.csrf_token }}
{{ TextInput(portfolio_form.name, validation="portfolioName", optional=False) }}
{{ TextInput(portfolio_form.description, paragraph=True) }}
{{ TextInput(portfolio_form.description, validation="defaultTextAreaField", paragraph=True) }}
<div class='edit-portfolio-name action-group'>
{{ SaveButton(text='Save Changes', additional_classes='usa-button-big') }}
</div>

View File

@ -27,8 +27,8 @@
{% macro InfoFields(member_form) %}
<div class="user-info">
{{ TextInput(member_form.first_name, validation='requiredField', optional=False) }}
{{ TextInput(member_form.last_name, validation='requiredField', optional=False) }}
{{ TextInput(member_form.first_name, validation='name', optional=False) }}
{{ TextInput(member_form.last_name, validation='name', optional=False) }}
{{ TextInput(member_form.email, validation='email', optional=False) }}
{{ PhoneInput(member_form.phone_number, member_form.phone_ext)}}
{{ TextInput(member_form.dod_id, validation='dodId', optional=False) }}

View File

@ -21,13 +21,13 @@
{{ form.csrf_token }}
<div class="form-row form-row--bordered">
<div class="form-col">
{{ TextInput(form.name, optional=False, classes="form-col") }}
{{ TextInput(form.name, validation="name", optional=False, classes="form-col") }}
{{"forms.portfolio.name.help_text" | translate | safe }}
</div>
</div>
<div class="form-row form-row--bordered">
<div class="form-col">
{{ TextInput(form.description, paragraph=True) }}
{{ TextInput(form.description, validation="defaultTextAreaField", paragraph=True) }}
{{"forms.portfolio.description.help_text" | translate | safe }}
</div>
</div>

View File

@ -37,6 +37,10 @@ def random_task_order_number():
return "".join(random.choices(string.digits, k=10))
def random_clin_number():
return "".join(random.choices(string.digits, k=4))
def random_past_date(year_min=1, year_max=5):
return _random_date(year_min, year_max, operator.sub)
@ -327,7 +331,7 @@ class CLINFactory(Base):
model = CLIN
task_order = factory.SubFactory(TaskOrderFactory)
number = factory.LazyFunction(random_task_order_number)
number = factory.LazyFunction(random_clin_number)
start_date = datetime.date.today()
end_date = factory.LazyFunction(random_future_date)
total_amount = factory.LazyFunction(lambda *args: random.randint(50000, 999999))

View File

@ -1,13 +1,12 @@
from wtforms.validators import ValidationError
import uuid
from atst.domain.permission_sets import PermissionSets
from atst.forms.data import ENV_ROLES, ENV_ROLE_NO_ACCESS as NO_ACCESS
from atst.forms.data import ENV_ROLES
from atst.forms.application_member import *
def test_environment_form():
form_data = {
"environment_id": 123,
"environment_id": str(uuid.uuid4()),
"environment_name": "testing",
"role": ENV_ROLES[0][0],
"disabled": True,
@ -17,12 +16,13 @@ def test_environment_form():
def test_environment_form_default_no_access():
form_data = {"environment_id": 123, "environment_name": "testing"}
env_id = str(uuid.uuid4())
form_data = {"environment_id": env_id, "environment_name": "testing"}
form = EnvironmentForm(data=form_data)
assert form.validate()
assert form.data == {
"environment_id": 123,
"environment_id": env_id,
"environment_name": "testing",
"role": None,
"disabled": False,
@ -31,7 +31,7 @@ def test_environment_form_default_no_access():
def test_environment_form_invalid():
form_data = {
"environment_id": 123,
"environment_id": str(uuid.uuid4()),
"environment_name": "testing",
"role": "not a real choice",
}

View File

@ -4,16 +4,16 @@ import pytest
from atst.forms.validators import *
class TestIsNumber:
class TestNumber:
@pytest.mark.parametrize("valid", ["0", "12", "-12"])
def test_IsNumber_accepts_integers(self, valid, dummy_form, dummy_field):
validator = IsNumber()
def test_Number_accepts_integers(self, valid, dummy_form, dummy_field):
validator = Number()
dummy_field.data = valid
validator(dummy_form, dummy_field)
@pytest.mark.parametrize("invalid", ["12.1", "two", "", None])
def test_IsNumber_rejects_anything_else(self, invalid, dummy_form, dummy_field):
validator = IsNumber()
@pytest.mark.parametrize("invalid", ["12.1", "two"])
def test_Number_rejects_anything_else(self, invalid, dummy_form, dummy_field):
validator = Number()
dummy_field.data = invalid
with pytest.raises(ValidationError):
validator(dummy_form, dummy_field)

View File

@ -401,10 +401,10 @@ def test_create_member(monkeypatch, client, user_session, session):
"user_data-last_name": user.last_name,
"user_data-dod_id": user.dod_id,
"user_data-email": user.email,
"environment_roles-0-environment_id": env.id,
"environment_roles-0-environment_id": str(env.id),
"environment_roles-0-role": "ADMIN",
"environment_roles-0-environment_name": env.name,
"environment_roles-1-environment_id": env_1.id,
"environment_roles-1-environment_id": str(env_1.id),
"environment_roles-1-role": NO_ACCESS,
"environment_roles-1-environment_name": env_1.name,
"perms_env_mgmt": True,
@ -527,13 +527,13 @@ def test_update_member(client, user_session, session):
application_role_id=app_role.id,
),
data={
"environment_roles-0-environment_id": env.id,
"environment_roles-0-environment_id": str(env.id),
"environment_roles-0-role": "CONTRIBUTOR",
"environment_roles-0-environment_name": env.name,
"environment_roles-1-environment_id": env_1.id,
"environment_roles-1-environment_id": str(env_1.id),
"environment_roles-1-environment_name": env_1.name,
"environment_roles-1-disabled": "True",
"environment_roles-2-environment_id": env_2.id,
"environment_roles-2-environment_id": str(env_2.id),
"environment_roles-2-role": "BILLING_READ",
"environment_roles-2-environment_name": env_2.name,
"perms_env_mgmt": True,
@ -694,10 +694,10 @@ def test_handle_create_member(monkeypatch, set_g, session):
"user_data-last_name": user.last_name,
"user_data-dod_id": user.dod_id,
"user_data-email": user.email,
"environment_roles-0-environment_id": env.id,
"environment_roles-0-environment_id": str(env.id),
"environment_roles-0-role": "ADMIN",
"environment_roles-0-environment_name": env.name,
"environment_roles-1-environment_id": env_1.id,
"environment_roles-1-environment_id": str(env_1.id),
"environment_roles-1-role": NO_ACCESS,
"environment_roles-1-environment_name": env_1.name,
"perms_env_mgmt": True,
@ -731,10 +731,10 @@ def test_handle_update_member_success(set_g):
form_data = ImmutableMultiDict(
{
"environment_roles-0-environment_id": env.id,
"environment_roles-0-environment_id": str(env.id),
"environment_roles-0-role": "ADMIN",
"environment_roles-0-environment_name": env.name,
"environment_roles-1-environment_id": env_1.id,
"environment_roles-1-environment_id": str(env_1.id),
"environment_roles-1-role": NO_ACCESS,
"environment_roles-1-environment_name": env_1.name,
"perms_env_mgmt": True,
@ -742,6 +742,7 @@ def test_handle_update_member_success(set_g):
"perms_del_env": True,
}
)
handle_update_member(application.id, app_role.id, form_data)
assert len(application.roles) == 1
@ -771,10 +772,10 @@ def test_handle_update_member_with_error(set_g, monkeypatch, mock_logger):
form_data = ImmutableMultiDict(
{
"environment_roles-0-environment_id": env.id,
"environment_roles-0-environment_id": str(env.id),
"environment_roles-0-role": "ADMIN",
"environment_roles-0-environment_name": env.name,
"environment_roles-1-environment_id": env_1.id,
"environment_roles-1-environment_id": str(env_1.id),
"environment_roles-1-role": NO_ACCESS,
"environment_roles-1-environment_name": env_1.name,
"perms_env_mgmt": True,

View File

@ -219,13 +219,13 @@ def test_task_orders_submit_form_step_three_add_clins(client, user_session, task
user_session(task_order.portfolio.owner)
form_data = {
"clins-0-jedi_clin_type": "JEDI_CLIN_1",
"clins-0-clin_number": "12312",
"clins-0-number": "1212",
"clins-0-start_date": "01/01/2020",
"clins-0-end_date": "01/01/2021",
"clins-0-obligated_amount": "5000",
"clins-0-total_amount": "10000",
"clins-1-jedi_clin_type": "JEDI_CLIN_1",
"clins-1-number": "12312",
"clins-1-number": "1212",
"clins-1-start_date": "01/01/2020",
"clins-1-end_date": "01/01/2021",
"clins-1-obligated_amount": "5000",
@ -269,7 +269,7 @@ def test_task_orders_submit_form_step_three_add_clins_existing_to(
user_session(task_order.portfolio.owner)
form_data = {
"clins-0-jedi_clin_type": "JEDI_CLIN_1",
"clins-0-clin_number": "12312",
"clins-0-number": "1212",
"clins-0-start_date": "01/01/2020",
"clins-0-end_date": "01/01/2021",
"clins-0-obligated_amount": "5000",

View File

@ -668,7 +668,7 @@ def test_task_orders_new_post_routes(post_url_assert_status):
"task_orders.submit_form_step_three_add_clins",
{
"clins-0-jedi_clin_type": "JEDI_CLIN_1",
"clins-0-clin_number": "12312",
"clins-0-number": "1212",
"clins-0-start_date": "01/01/2020",
"clins-0-end_date": "01/01/2021",
"clins-0-obligated_amount": "5000",

View File

@ -1,17 +1,17 @@
# How to use text containing html tags in .html files:
# In the template add the `safe` filter when referencing the string
# from the template file. ie:
# In the template add the `safe` filter when referencing the string
# from the template file. ie:
# login:
# title: A title with <a href="#">a link</a>!
# login:
# title: A title with <a href="#">a link</a>!
# `{{ "login.title" | translate | safe }}`
# `{{ "login.title" | translate | safe }}`
audit_log:
events:
default:
change: '{from} to {to}'
changes: 'Changes:'
details: 'Details:'
change: "{from} to {to}"
changes: "Changes:"
details: "Details:"
base_public:
login: Log in
title_tag: JEDI Cloud
@ -43,7 +43,7 @@ common:
confirm: Confirm
continue: Continue
delete: Delete
delete_confirm: 'Please type the word {word} to confirm:'
delete_confirm: "Please type the word {word} to confirm:"
dod_id: DoD ID
disable: Disable
email: Email
@ -87,12 +87,12 @@ flash:
application:
created:
title: Application Saved
message: '{application_name} has been successfully created. You may continue on to provision environments and assign team members now, or come back and complete these tasks at a later time.'
updated: 'You have successfully updated the {application_name} application.'
message: "{application_name} has been successfully created. You may continue on to provision environments and assign team members now, or come back and complete these tasks at a later time."
updated: "You have successfully updated the {application_name} application."
name_error:
message: 'The application name {name} has already been used in this portfolio. Please enter a unique name.'
message: "The application name {name} has already been used in this portfolio. Please enter a unique name."
env_name_error:
message: 'The environment name {name} has already been used in this application. Please enter a unique name.'
message: "The environment name {name} has already been used in this application. Please enter a unique name."
application_invite:
error:
title: Application invitation error
@ -117,7 +117,7 @@ flash:
message: You have successfully given {user_name} CCPO permissions.
removed:
message: You have successfully removed {user_name}'s CCPO permissions.
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
environment_added: 'The environment "{environment_name}" has been added to the application.'
environment:
@ -139,7 +139,7 @@ flash:
new_portfolio_member:
title: "{user_name}'s invitation has been sent"
message: "{user_name}'s access to this Portfolio is pending until they sign in for the first time."
new_ppoc_message: 'You have successfully added {ppoc_name} as the primary point of contact. You are no longer the PPoC.'
new_ppoc_message: "You have successfully added {ppoc_name} as the primary point of contact. You are no longer the PPoC."
new_ppoc_title: Primary point of contact updated
portfolio_member:
revoked:
@ -162,7 +162,7 @@ flash:
message: Your session expired due to inactivity. Please log in again to continue.
success: Success!
task_order_number_error:
message: 'The TO number has already been entered for a JEDI task order #{to_number}. Please double-check the TO number you are entering. If you believe this is in error, please contact support@cloud.mil.'
message: "The TO number has already been entered for a JEDI task order #{to_number}. Please double-check the TO number you are entering. If you believe this is in error, please contact support@cloud.mil."
task_order:
insufficient_funds:
title: Insufficient Funds
@ -172,7 +172,7 @@ flash:
new_application_member:
title: "{user_name}'s invitation has been sent"
message: "{user_name}'s access to this Application is pending until they sign in for the first time."
updated_application_team_settings: 'You have updated the {application_name} team settings.'
updated_application_team_settings: "You have updated the {application_name} team settings."
user:
complete_profile:
title: You must complete your profile
@ -180,7 +180,7 @@ flash:
updated:
title: User information updated.
footer:
login: 'Last login:'
login: "Last login:"
forms:
application:
description_label: Application Description
@ -189,7 +189,7 @@ forms:
environment_names_unique_validation_message: Environment names must be unique.
name_label: Application Name
assign_ppoc:
dod_id: 'Select new primary point of contact:'
dod_id: "Select new primary point of contact:"
environments:
name_label: Environment Name
edit_user:
@ -293,8 +293,9 @@ forms:
is_number_message: Please enter a valid number.
list_item_required_message: Please provide at least one.
list_items_unique_message: Items must be unique
name_message: 'This field accepts letters, numbers, commas, apostrophes, hyphens, and periods.'
name_message: "This field accepts letters, numbers, commas, apostrophes, hyphens, and periods."
phone_number_message: Please enter a valid 5 or 10 digit phone number.
alpha_numeric_message: This field may only contain alphanumeric characters.
fragments:
edit_application_form:
explain: AT-AT allows you to create multiple applications within a portfolio. Each application can then be broken down into its own customizable environments.
@ -308,7 +309,7 @@ fragments:
title: Warning!
assign_user_button_text: Assign member
confirm_alert:
title: 'Once you assign a new PPoC, you will no longer be able to request portfolio deactivation or manage the PPoC role.'
title: "Once you assign a new PPoC, you will no longer be able to request portfolio deactivation or manage the PPoC role."
subtitle: The PPoC has the ability to edit all aspects of a portfolio and is the only one who can manage the PPoC role.
title: Primary point of contact (PPoC)
update_btn: Update
@ -321,7 +322,7 @@ login:
ccpo_logo_alt_text: Cloud Computing Program Office Logo
certificate_selection:
learn_more: Learn more
message: 'When you are prompted to select a certificate, please select Email Certificate from the provided choices.'
message: "When you are prompted to select a certificate, please select Email Certificate from the provided choices."
title: Certificate Selection
h1_title: Access the JEDI cloud
login_button: Sign in with CAC
@ -340,23 +341,23 @@ portfolios:
admin:
activity_log_title: Activity log
alert_header: Are you sure you want to delete this member?
alert_message: 'The member will be removed from the portfolio, but their log history will be retained.'
alert_message: "The member will be removed from the portfolio, but their log history will be retained."
alert_title: Warning! You are about to delete a member from the portfolio.
defense_component_label: Department of Defense Component
portfolio_name: Portfolio name
members:
perms_portfolio_mgmt:
'False': View Portfolio
'True': Edit Portfolio
"False": View Portfolio
"True": Edit Portfolio
perms_app_mgmt:
'False': View Applications
'True': Edit Applications
"False": View Applications
"True": Edit Applications
perms_funding:
'False': View Funding
'True': Edit Funding
"False": View Funding
"True": Edit Funding
perms_reporting:
'False': View Reporting
'True': Edit Reporting
"False": View Reporting
"True": Edit Reporting
applications:
add_environment: Add an Environment
add_member: Add Team Member
@ -406,8 +407,8 @@ portfolios:
csp_link: Cloud Service Provider Link
enter_env_name: "Enter environment name:"
environments_heading: Application Environments
existing_application_title: '{application_name} Application Settings'
member_count: '{count} members'
existing_application_title: "{application_name} Application Settings"
member_count: "{count} members"
new_application_title: New Application
settings:
name_description: Application name and description
@ -444,14 +445,14 @@ portfolios:
new:
verify: Verify Member Information
perms_team_mgmt:
'False': View Team
'True': Edit Team
"False": View Team
"True": Edit Team
perms_env_mgmt:
'False': View Environments
'True': Edit Environments
"False": View Environments
"True": Edit Environments
perms_del_env:
'False': ""
'True': Delete Application
"False": ""
"True": Delete Application
roles:
ADMIN: Admin
BILLING_READ: Billing Read-only
@ -460,7 +461,7 @@ portfolios:
new:
title: New Portfolio
cta_step_1: Name and Describe Portfolio
sticky_header_context: 'Step {step} of 1'
sticky_header_context: "Step {step} of 1"
save: Save Portfolio
members:
archive_button: Delete member
@ -477,7 +478,7 @@ portfolios:
sub_message:
can_create_applications: This portfolio has no cloud environments set up, so there is no spending data to report. Create an application with some cloud environments to get started.
cannot_create_applications: This portfolio has no cloud environments set up, so there is no spending data to report. Contact the portfolio owner to set up some cloud environments.
action_label: 'Add a new application'
action_label: "Add a new application"
total_value:
header: Total Portfolio Value
tooltip: Total portfolio value is all obligated and projected funds for all task orders in this portfolio.
@ -492,10 +493,10 @@ task_orders:
total_amount: CLIN Value
obligated: Amount Obligated
JEDICLINType:
JEDI_CLIN_1: 'Unclassified IaaS and PaaS (IDIQ CLIN 0001)'
JEDI_CLIN_2: 'Classified IaaS and PaaS (IDIQ CLIN 0002)'
JEDI_CLIN_3: 'Unclassified Cloud Support Package (IDIQ CLIN 0003)'
JEDI_CLIN_4: 'Classified Cloud Support Package (IDIQ CLIN 0004)'
JEDI_CLIN_1: "Unclassified IaaS and PaaS (IDIQ CLIN 0001)"
JEDI_CLIN_2: "Classified IaaS and PaaS (IDIQ CLIN 0002)"
JEDI_CLIN_3: "Unclassified Cloud Support Package (IDIQ CLIN 0003)"
JEDI_CLIN_4: "Classified Cloud Support Package (IDIQ CLIN 0004)"
tooltip:
obligated_funds: Funds committed to fund your portfolio. This may represent 100% of your total Task Order value, or a portion of it.
total_value: All obligated and projected funds for the Task Orders Base and Option CLINs.
@ -510,7 +511,7 @@ task_orders:
clin_funding: CLIN Funding
clin_number_label: CLIN
clin_type_label: Corresponding IDIQ CLIN
clin_remove_text: 'Do you want to remove '
clin_remove_text: "Do you want to remove "
clin_remove_confirm: Yes, remove CLIN
clin_remove_cancel: No, go back
draft_alert_title: Your information has been saved
@ -527,18 +528,18 @@ task_orders:
title: Upload your approved Task Order (TO)
description: Upload your approved Task Order here. You are required to confirm you have the appropriate signature. You will have the ability to add additional approved Task Orders with more funding to this Portfolio in the future.
step_3:
next_button: 'Next: Review Task Order'
percent_obligated: '% of Funds Obligated'
next_button: "Next: Review Task Order"
percent_obligated: "% of Funds Obligated"
step_4:
documents: Documents
clins: CLIN Summary
step_5:
cta_text: Verify Your Information
description: Prior to submitting the Task Order, you must acknowledge, by marking the appropriate box below, that the uploaded Task Order is signed by an appropriate, duly warranted Contracting Officer who has the authority to execute the uploaded Task Order on your Agencys behalf and has authorized you to upload the Task Order in accordance with Agency policy and procedures. You must further acknowledge, by marking the appropriate box below, that all information entered herein matches that of the submitted Task Order.
next_button: 'Submit Task Order'
sticky_header_text: 'Add a Task Order'
next_button: "Submit Task Order"
sticky_header_text: "Add a Task Order"
sticky_header_review_text: Review Changes
sticky_header_context: 'Step {step} of 5'
sticky_header_context: "Step {step} of 5"
empty_state:
header: Add approved task orders
message: Upload your approved Task Order here. You are required to confirm you have the appropriate signature. You will have the ability to add additional approved Task Orders with more funding to this Portfolio in the future.
@ -550,19 +551,19 @@ task_orders:
acknowledge:
title: Acknowledge Statement
text: I acknowledge, by executing the confirmation above and submitting this verification, that I am subject to potential penalties that may include fines, imprisonment, or both, under the U.S. law and regulations for any false statement or misrepresentation in association with this Task Order submission or on any accompanying documentation.
status_empty_state: 'This Portfolio has no {status} Task Orders.'
status_list_title: '{status} Task Orders'
status_empty_state: "This Portfolio has no {status} Task Orders."
status_list_title: "{status} Task Orders"
summary:
obligated: Total Obligated
total: Total Value
expended: Total Expended
JEDICLINType:
JEDI_CLIN_1: 'IDIQ CLIN 0001 Unclassified IaaS/PaaS'
JEDI_CLIN_2: 'IDIQ CLIN 0002 Classified IaaS/PaaS'
JEDI_CLIN_3: 'IDIQ CLIN 0003 Unclassified Cloud Support Package'
JEDI_CLIN_4: 'IDIQ CLIN 0004 Classified Cloud Support Package'
JEDI_CLIN_1: "IDIQ CLIN 0001 Unclassified IaaS/PaaS"
JEDI_CLIN_2: "IDIQ CLIN 0002 Classified IaaS/PaaS"
JEDI_CLIN_3: "IDIQ CLIN 0003 Unclassified Cloud Support Package"
JEDI_CLIN_4: "IDIQ CLIN 0004 Classified Cloud Support Package"
testing:
example_string: Hello World
example_with_variables: 'Hello, {name}!'
example_with_variables: "Hello, {name}!"
nested:
example: Hello nested example