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

View File

@ -222,7 +222,7 @@ def make_config(direct_config=None):
config.read_dict({"default": direct_config}) config.read_dict({"default": direct_config})
# Assemble DATABASE_URI value # 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", "PGUSER"),
config.get("default", "PGPASSWORD"), config.get("default", "PGPASSWORD"),
config.get("default", "PGHOST"), config.get("default", "PGHOST"),

View File

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

View File

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

View File

@ -2,12 +2,12 @@ from flask_wtf import FlaskForm
from wtforms.validators import Required, Length from wtforms.validators import Required, Length
from wtforms.fields import StringField from wtforms.fields import StringField
from atst.forms.validators import IsNumber from atst.forms.validators import Number
from atst.utils.localization import translate from atst.utils.localization import translate
class CCPOUserForm(FlaskForm): class CCPOUserForm(FlaskForm):
dod_id = StringField( dod_id = StringField(
translate("forms.new_member.dod_id_label"), 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 .data import SERVICE_BRANCHES
from atst.models.user import User from atst.models.user import User
from atst.utils.localization import translate from atst.utils.localization import translate
from wtforms.validators import Length
from atst.forms.validators import Number
from .validators import Name, DateRange, PhoneNumber from .validators import Name, DateRange, PhoneNumber
USER_FIELDS = { USER_FIELDS = {
"first_name": StringField( "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( "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()]), "email": EmailField(translate("forms.edit_user.email_label"), validators=[Email()]),
"phone_number": TelField( "phone_number": TelField(
translate("forms.edit_user.phone_number_label"), validators=[PhoneNumber()] 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( "service_branch": SelectField(
translate("forms.edit_user.service_branch_label"), choices=SERVICE_BRANCHES 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.validators import Required, Email, Length, Optional
from wtforms.fields import StringField 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 from atst.utils.localization import translate
class NewForm(FlaskForm): class NewForm(FlaskForm):
first_name = StringField( 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( 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( email = EmailField(
translate("forms.new_member.email_label"), validators=[Required(), Email()] translate("forms.new_member.email_label"), validators=[Required(), Email()]
@ -21,8 +23,8 @@ class NewForm(FlaskForm):
translate("forms.new_member.phone_number_label"), translate("forms.new_member.phone_number_label"),
validators=[Optional(), PhoneNumber()], validators=[Optional(), PhoneNumber()],
) )
phone_ext = StringField("Extension") phone_ext = StringField("Extension", validators=[Number(), Length(max=10)])
dod_id = StringField( dod_id = StringField(
translate("forms.new_member.dod_id_label"), 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, TextAreaField,
) )
from wtforms.validators import Length, InputRequired from wtforms.validators import Length, InputRequired
from atst.forms.validators import Name
from wtforms.widgets import ListWidget, CheckboxInput from wtforms.widgets import ListWidget, CheckboxInput
from .forms import BaseForm from .forms import BaseForm
@ -20,10 +21,13 @@ class PortfolioForm(BaseForm):
min=4, min=4,
max=100, max=100,
message=translate("forms.portfolio.name.length_validation_message"), 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): class PortfolioCreationForm(PortfolioForm):

View File

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

View File

@ -2,15 +2,12 @@ from datetime import datetime
import re import re
from werkzeug.datastructures import FileStorage from werkzeug.datastructures import FileStorage
from wtforms.validators import ValidationError from wtforms.validators import ValidationError, Regexp
import pendulum import pendulum
from atst.utils.localization import translate from atst.utils.localization import translate
REGEX_ALPHA_NUMERIC = "^[A-Za-z0-9\-_ \.]*$"
def DateRange(lower_bound=None, upper_bound=None, message=None): def DateRange(lower_bound=None, upper_bound=None, message=None):
def _date_range(form, field): def _date_range(form, field):
if field.data is None: if field.data is None:
@ -34,8 +31,9 @@ def DateRange(lower_bound=None, upper_bound=None, message=None):
return _date_range 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): def _is_number(form, field):
if field.data:
try: try:
int(field.data) int(field.data)
except (ValueError, TypeError): except (ValueError, TypeError):
@ -100,3 +98,7 @@ def FileLength(max_length=50000000, message=None):
field.data.seek(0) field.data.seek(0)
return _file_length 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: [], unmask: [],
validationError: 'Please enter a response', validationError: 'Please enter a response',
}, },
clinNumber: {
mask: false,
match: /^\d{4}$/,
unmask: [],
validationError: 'Please enter a 4-digit CLIN number',
},
date: { date: {
mask: [/\d/, /\d/, '/', /\d/, /\d/, '/', /\d/, /\d/, /\d/, /\d/], 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/, 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: ['$', ','], unmask: ['$', ','],
validationError: 'Please enter a dollar amount', 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: { clinDollars: {
mask: createNumberMask({ prefix: '$', allowDecimal: true }), mask: createNumberMask({ prefix: '$', allowDecimal: true }),
match: /^-?\d+\.?\d*$/, match: /^-?\d+\.?\d*$/,
@ -53,6 +73,13 @@ export default {
unmask: [','], unmask: [','],
validationError: 'Please enter a number', validationError: 'Please enter a number',
}, },
name: {
mask: false,
match: /.{1,100}/,
unmask: [],
validationError:
'This field accepts letters, numbers, commas, apostrophes, hyphens, and periods.',
},
phoneExt: { phoneExt: {
mask: createNumberMask({ mask: createNumberMask({
prefix: '', prefix: '',
@ -71,7 +98,7 @@ export default {
unmask: [], unmask: [],
validationError: 'Portfolio names can be between 4-100 characters', validationError: 'Portfolio names can be between 4-100 characters',
}, },
requiredField: { required: {
mask: false, mask: false,
match: /.+/, match: /.+/,
unmask: [], unmask: [],

View File

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

View File

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

View File

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

View File

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

View File

@ -26,14 +26,14 @@
{{ form.csrf_token }} {{ form.csrf_token }}
<div class="form-row"> <div class="form-row">
<div class="form-col"> <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) }} {{ ('portfolios.applications.new.step_1_form_help_text.name' | translate | safe) }}
</div> </div>
</div> </div>
<hr> <hr>
<div class="form-row"> <div class="form-row">
<div class="form-col form-col--two-thirds"> <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) }} {{ ('portfolios.applications.new.step_1_form_help_text.description' | translate | safe) }}
</div> </div>
</div> </div>

View File

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

View File

@ -41,7 +41,7 @@
<div class="form-row"> <div class="form-row">
<div class="form-col"> <div class="form-col">
{% if fields %} {% if fields %}
{{ TextInput(fields.number, optional=False) }} {{ TextInput(fields.number, validation='clinNumber', optional=False) }}
{% else %} {% else %}
<textinput :name="'clins-' + clinIndex + '-number'" inline-template> <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 }]"> <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='panel__content'>
<div class='form-row'> <div class='form-row'>
<div class='form-col form-col--half'> <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>
<div class='form-col form-col--half'> <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>
</div> </div>

View File

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

View File

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

View File

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

View File

@ -37,6 +37,10 @@ def random_task_order_number():
return "".join(random.choices(string.digits, k=10)) 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): def random_past_date(year_min=1, year_max=5):
return _random_date(year_min, year_max, operator.sub) return _random_date(year_min, year_max, operator.sub)
@ -327,7 +331,7 @@ class CLINFactory(Base):
model = CLIN model = CLIN
task_order = factory.SubFactory(TaskOrderFactory) task_order = factory.SubFactory(TaskOrderFactory)
number = factory.LazyFunction(random_task_order_number) number = factory.LazyFunction(random_clin_number)
start_date = datetime.date.today() start_date = datetime.date.today()
end_date = factory.LazyFunction(random_future_date) end_date = factory.LazyFunction(random_future_date)
total_amount = factory.LazyFunction(lambda *args: random.randint(50000, 999999)) 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
from atst.forms.data import ENV_ROLES, ENV_ROLE_NO_ACCESS as NO_ACCESS
from atst.forms.application_member import * from atst.forms.application_member import *
def test_environment_form(): def test_environment_form():
form_data = { form_data = {
"environment_id": 123, "environment_id": str(uuid.uuid4()),
"environment_name": "testing", "environment_name": "testing",
"role": ENV_ROLES[0][0], "role": ENV_ROLES[0][0],
"disabled": True, "disabled": True,
@ -17,12 +16,13 @@ def test_environment_form():
def test_environment_form_default_no_access(): 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) form = EnvironmentForm(data=form_data)
assert form.validate() assert form.validate()
assert form.data == { assert form.data == {
"environment_id": 123, "environment_id": env_id,
"environment_name": "testing", "environment_name": "testing",
"role": None, "role": None,
"disabled": False, "disabled": False,
@ -31,7 +31,7 @@ def test_environment_form_default_no_access():
def test_environment_form_invalid(): def test_environment_form_invalid():
form_data = { form_data = {
"environment_id": 123, "environment_id": str(uuid.uuid4()),
"environment_name": "testing", "environment_name": "testing",
"role": "not a real choice", "role": "not a real choice",
} }

View File

@ -4,16 +4,16 @@ import pytest
from atst.forms.validators import * from atst.forms.validators import *
class TestIsNumber: class TestNumber:
@pytest.mark.parametrize("valid", ["0", "12", "-12"]) @pytest.mark.parametrize("valid", ["0", "12", "-12"])
def test_IsNumber_accepts_integers(self, valid, dummy_form, dummy_field): def test_Number_accepts_integers(self, valid, dummy_form, dummy_field):
validator = IsNumber() validator = Number()
dummy_field.data = valid dummy_field.data = valid
validator(dummy_form, dummy_field) validator(dummy_form, dummy_field)
@pytest.mark.parametrize("invalid", ["12.1", "two", "", None]) @pytest.mark.parametrize("invalid", ["12.1", "two"])
def test_IsNumber_rejects_anything_else(self, invalid, dummy_form, dummy_field): def test_Number_rejects_anything_else(self, invalid, dummy_form, dummy_field):
validator = IsNumber() validator = Number()
dummy_field.data = invalid dummy_field.data = invalid
with pytest.raises(ValidationError): with pytest.raises(ValidationError):
validator(dummy_form, dummy_field) 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-last_name": user.last_name,
"user_data-dod_id": user.dod_id, "user_data-dod_id": user.dod_id,
"user_data-email": user.email, "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-role": "ADMIN",
"environment_roles-0-environment_name": env.name, "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-role": NO_ACCESS,
"environment_roles-1-environment_name": env_1.name, "environment_roles-1-environment_name": env_1.name,
"perms_env_mgmt": True, "perms_env_mgmt": True,
@ -527,13 +527,13 @@ def test_update_member(client, user_session, session):
application_role_id=app_role.id, application_role_id=app_role.id,
), ),
data={ data={
"environment_roles-0-environment_id": env.id, "environment_roles-0-environment_id": str(env.id),
"environment_roles-0-role": "CONTRIBUTOR", "environment_roles-0-role": "CONTRIBUTOR",
"environment_roles-0-environment_name": env.name, "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-environment_name": env_1.name,
"environment_roles-1-disabled": "True", "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-role": "BILLING_READ",
"environment_roles-2-environment_name": env_2.name, "environment_roles-2-environment_name": env_2.name,
"perms_env_mgmt": True, "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-last_name": user.last_name,
"user_data-dod_id": user.dod_id, "user_data-dod_id": user.dod_id,
"user_data-email": user.email, "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-role": "ADMIN",
"environment_roles-0-environment_name": env.name, "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-role": NO_ACCESS,
"environment_roles-1-environment_name": env_1.name, "environment_roles-1-environment_name": env_1.name,
"perms_env_mgmt": True, "perms_env_mgmt": True,
@ -731,10 +731,10 @@ def test_handle_update_member_success(set_g):
form_data = ImmutableMultiDict( 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-role": "ADMIN",
"environment_roles-0-environment_name": env.name, "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-role": NO_ACCESS,
"environment_roles-1-environment_name": env_1.name, "environment_roles-1-environment_name": env_1.name,
"perms_env_mgmt": True, "perms_env_mgmt": True,
@ -742,6 +742,7 @@ def test_handle_update_member_success(set_g):
"perms_del_env": True, "perms_del_env": True,
} }
) )
handle_update_member(application.id, app_role.id, form_data) handle_update_member(application.id, app_role.id, form_data)
assert len(application.roles) == 1 assert len(application.roles) == 1
@ -771,10 +772,10 @@ def test_handle_update_member_with_error(set_g, monkeypatch, mock_logger):
form_data = ImmutableMultiDict( 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-role": "ADMIN",
"environment_roles-0-environment_name": env.name, "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-role": NO_ACCESS,
"environment_roles-1-environment_name": env_1.name, "environment_roles-1-environment_name": env_1.name,
"perms_env_mgmt": True, "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) user_session(task_order.portfolio.owner)
form_data = { form_data = {
"clins-0-jedi_clin_type": "JEDI_CLIN_1", "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-start_date": "01/01/2020",
"clins-0-end_date": "01/01/2021", "clins-0-end_date": "01/01/2021",
"clins-0-obligated_amount": "5000", "clins-0-obligated_amount": "5000",
"clins-0-total_amount": "10000", "clins-0-total_amount": "10000",
"clins-1-jedi_clin_type": "JEDI_CLIN_1", "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-start_date": "01/01/2020",
"clins-1-end_date": "01/01/2021", "clins-1-end_date": "01/01/2021",
"clins-1-obligated_amount": "5000", "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) user_session(task_order.portfolio.owner)
form_data = { form_data = {
"clins-0-jedi_clin_type": "JEDI_CLIN_1", "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-start_date": "01/01/2020",
"clins-0-end_date": "01/01/2021", "clins-0-end_date": "01/01/2021",
"clins-0-obligated_amount": "5000", "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", "task_orders.submit_form_step_three_add_clins",
{ {
"clins-0-jedi_clin_type": "JEDI_CLIN_1", "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-start_date": "01/01/2020",
"clins-0-end_date": "01/01/2021", "clins-0-end_date": "01/01/2021",
"clins-0-obligated_amount": "5000", "clins-0-obligated_amount": "5000",

View File

@ -1,17 +1,17 @@
# How to use text containing html tags in .html files: # How to use text containing html tags in .html files:
# In the template add the `safe` filter when referencing the string # In the template add the `safe` filter when referencing the string
# from the template file. ie: # from the template file. ie:
# login: # login:
# title: A title with <a href="#">a link</a>! # title: A title with <a href="#">a link</a>!
# `{{ "login.title" | translate | safe }}` # `{{ "login.title" | translate | safe }}`
audit_log: audit_log:
events: events:
default: default:
change: '{from} to {to}' change: "{from} to {to}"
changes: 'Changes:' changes: "Changes:"
details: 'Details:' details: "Details:"
base_public: base_public:
login: Log in login: Log in
title_tag: JEDI Cloud title_tag: JEDI Cloud
@ -43,7 +43,7 @@ common:
confirm: Confirm confirm: Confirm
continue: Continue continue: Continue
delete: Delete delete: Delete
delete_confirm: 'Please type the word {word} to confirm:' delete_confirm: "Please type the word {word} to confirm:"
dod_id: DoD ID dod_id: DoD ID
disable: Disable disable: Disable
email: Email email: Email
@ -87,12 +87,12 @@ flash:
application: application:
created: created:
title: Application Saved 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.' 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.' updated: "You have successfully updated the {application_name} application."
name_error: 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: 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: application_invite:
error: error:
title: Application invitation error title: Application invitation error
@ -117,7 +117,7 @@ flash:
message: You have successfully given {user_name} CCPO permissions. message: You have successfully given {user_name} CCPO permissions.
removed: removed:
message: You have successfully removed {user_name}'s CCPO permissions. 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 deleted_member: Portfolio member deleted
environment_added: 'The environment "{environment_name}" has been added to the application.' environment_added: 'The environment "{environment_name}" has been added to the application.'
environment: environment:
@ -139,7 +139,7 @@ flash:
new_portfolio_member: new_portfolio_member:
title: "{user_name}'s invitation has been sent" 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." 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 new_ppoc_title: Primary point of contact updated
portfolio_member: portfolio_member:
revoked: revoked:
@ -162,7 +162,7 @@ flash:
message: Your session expired due to inactivity. Please log in again to continue. message: Your session expired due to inactivity. Please log in again to continue.
success: Success! success: Success!
task_order_number_error: 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: task_order:
insufficient_funds: insufficient_funds:
title: Insufficient Funds title: Insufficient Funds
@ -172,7 +172,7 @@ flash:
new_application_member: new_application_member:
title: "{user_name}'s invitation has been sent" 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." 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: user:
complete_profile: complete_profile:
title: You must complete your profile title: You must complete your profile
@ -180,7 +180,7 @@ flash:
updated: updated:
title: User information updated. title: User information updated.
footer: footer:
login: 'Last login:' login: "Last login:"
forms: forms:
application: application:
description_label: Application Description description_label: Application Description
@ -189,7 +189,7 @@ forms:
environment_names_unique_validation_message: Environment names must be unique. environment_names_unique_validation_message: Environment names must be unique.
name_label: Application Name name_label: Application Name
assign_ppoc: assign_ppoc:
dod_id: 'Select new primary point of contact:' dod_id: "Select new primary point of contact:"
environments: environments:
name_label: Environment Name name_label: Environment Name
edit_user: edit_user:
@ -293,8 +293,9 @@ forms:
is_number_message: Please enter a valid number. is_number_message: Please enter a valid number.
list_item_required_message: Please provide at least one. list_item_required_message: Please provide at least one.
list_items_unique_message: Items must be unique 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. phone_number_message: Please enter a valid 5 or 10 digit phone number.
alpha_numeric_message: This field may only contain alphanumeric characters.
fragments: fragments:
edit_application_form: 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. 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! title: Warning!
assign_user_button_text: Assign member assign_user_button_text: Assign member
confirm_alert: 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. 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) title: Primary point of contact (PPoC)
update_btn: Update update_btn: Update
@ -321,7 +322,7 @@ login:
ccpo_logo_alt_text: Cloud Computing Program Office Logo ccpo_logo_alt_text: Cloud Computing Program Office Logo
certificate_selection: certificate_selection:
learn_more: Learn more 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 title: Certificate Selection
h1_title: Access the JEDI cloud h1_title: Access the JEDI cloud
login_button: Sign in with CAC login_button: Sign in with CAC
@ -340,23 +341,23 @@ portfolios:
admin: admin:
activity_log_title: Activity log activity_log_title: Activity log
alert_header: Are you sure you want to delete this member? 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. alert_title: Warning! You are about to delete a member from the portfolio.
defense_component_label: Department of Defense Component defense_component_label: Department of Defense Component
portfolio_name: Portfolio name portfolio_name: Portfolio name
members: members:
perms_portfolio_mgmt: perms_portfolio_mgmt:
'False': View Portfolio "False": View Portfolio
'True': Edit Portfolio "True": Edit Portfolio
perms_app_mgmt: perms_app_mgmt:
'False': View Applications "False": View Applications
'True': Edit Applications "True": Edit Applications
perms_funding: perms_funding:
'False': View Funding "False": View Funding
'True': Edit Funding "True": Edit Funding
perms_reporting: perms_reporting:
'False': View Reporting "False": View Reporting
'True': Edit Reporting "True": Edit Reporting
applications: applications:
add_environment: Add an Environment add_environment: Add an Environment
add_member: Add Team Member add_member: Add Team Member
@ -406,8 +407,8 @@ portfolios:
csp_link: Cloud Service Provider Link csp_link: Cloud Service Provider Link
enter_env_name: "Enter environment name:" enter_env_name: "Enter environment name:"
environments_heading: Application Environments environments_heading: Application Environments
existing_application_title: '{application_name} Application Settings' existing_application_title: "{application_name} Application Settings"
member_count: '{count} members' member_count: "{count} members"
new_application_title: New Application new_application_title: New Application
settings: settings:
name_description: Application name and description name_description: Application name and description
@ -444,14 +445,14 @@ portfolios:
new: new:
verify: Verify Member Information verify: Verify Member Information
perms_team_mgmt: perms_team_mgmt:
'False': View Team "False": View Team
'True': Edit Team "True": Edit Team
perms_env_mgmt: perms_env_mgmt:
'False': View Environments "False": View Environments
'True': Edit Environments "True": Edit Environments
perms_del_env: perms_del_env:
'False': "" "False": ""
'True': Delete Application "True": Delete Application
roles: roles:
ADMIN: Admin ADMIN: Admin
BILLING_READ: Billing Read-only BILLING_READ: Billing Read-only
@ -460,7 +461,7 @@ portfolios:
new: new:
title: New Portfolio title: New Portfolio
cta_step_1: Name and Describe 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 save: Save Portfolio
members: members:
archive_button: Delete member archive_button: Delete member
@ -477,7 +478,7 @@ portfolios:
sub_message: 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. 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. 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: total_value:
header: Total Portfolio Value header: Total Portfolio Value
tooltip: Total portfolio value is all obligated and projected funds for all task orders in this portfolio. 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 total_amount: CLIN Value
obligated: Amount Obligated obligated: Amount Obligated
JEDICLINType: JEDICLINType:
JEDI_CLIN_1: 'Unclassified IaaS and PaaS (IDIQ CLIN 0001)' JEDI_CLIN_1: "Unclassified IaaS and PaaS (IDIQ CLIN 0001)"
JEDI_CLIN_2: 'Classified IaaS and PaaS (IDIQ CLIN 0002)' JEDI_CLIN_2: "Classified IaaS and PaaS (IDIQ CLIN 0002)"
JEDI_CLIN_3: 'Unclassified Cloud Support Package (IDIQ CLIN 0003)' JEDI_CLIN_3: "Unclassified Cloud Support Package (IDIQ CLIN 0003)"
JEDI_CLIN_4: 'Classified Cloud Support Package (IDIQ CLIN 0004)' JEDI_CLIN_4: "Classified Cloud Support Package (IDIQ CLIN 0004)"
tooltip: tooltip:
obligated_funds: Funds committed to fund your portfolio. This may represent 100% of your total Task Order value, or a portion of it. 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. 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_funding: CLIN Funding
clin_number_label: CLIN clin_number_label: CLIN
clin_type_label: Corresponding IDIQ 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_confirm: Yes, remove CLIN
clin_remove_cancel: No, go back clin_remove_cancel: No, go back
draft_alert_title: Your information has been saved draft_alert_title: Your information has been saved
@ -527,18 +528,18 @@ task_orders:
title: Upload your approved Task Order (TO) 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. 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: step_3:
next_button: 'Next: Review Task Order' next_button: "Next: Review Task Order"
percent_obligated: '% of Funds Obligated' percent_obligated: "% of Funds Obligated"
step_4: step_4:
documents: Documents documents: Documents
clins: CLIN Summary clins: CLIN Summary
step_5: step_5:
cta_text: Verify Your Information 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. 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' next_button: "Submit Task Order"
sticky_header_text: 'Add a Task Order' sticky_header_text: "Add a Task Order"
sticky_header_review_text: Review Changes sticky_header_review_text: Review Changes
sticky_header_context: 'Step {step} of 5' sticky_header_context: "Step {step} of 5"
empty_state: empty_state:
header: Add approved task orders 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. 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: acknowledge:
title: Acknowledge Statement 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. 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_empty_state: "This Portfolio has no {status} Task Orders."
status_list_title: '{status} Task Orders' status_list_title: "{status} Task Orders"
summary: summary:
obligated: Total Obligated obligated: Total Obligated
total: Total Value total: Total Value
expended: Total Expended expended: Total Expended
JEDICLINType: JEDICLINType:
JEDI_CLIN_1: 'IDIQ CLIN 0001 Unclassified IaaS/PaaS' JEDI_CLIN_1: "IDIQ CLIN 0001 Unclassified IaaS/PaaS"
JEDI_CLIN_2: 'IDIQ CLIN 0002 Classified IaaS/PaaS' JEDI_CLIN_2: "IDIQ CLIN 0002 Classified IaaS/PaaS"
JEDI_CLIN_3: 'IDIQ CLIN 0003 Unclassified Cloud Support Package' JEDI_CLIN_3: "IDIQ CLIN 0003 Unclassified Cloud Support Package"
JEDI_CLIN_4: 'IDIQ CLIN 0004 Classified Cloud Support Package' JEDI_CLIN_4: "IDIQ CLIN 0004 Classified Cloud Support Package"
testing: testing:
example_string: Hello World example_string: Hello World
example_with_variables: 'Hello, {name}!' example_with_variables: "Hello, {name}!"
nested: nested:
example: Hello nested example example: Hello nested example