commit
d0857a4a74
@ -12,7 +12,7 @@ from wtforms.fields.html5 import DateField, TelField
|
||||
from wtforms.widgets import ListWidget, CheckboxInput
|
||||
from wtforms.validators import Required, Length
|
||||
|
||||
from atst.forms.validators import IsNumber, PhoneNumber
|
||||
from atst.forms.validators import IsNumber, PhoneNumber, RequiredIfNot
|
||||
|
||||
from .forms import CacheableForm
|
||||
from .data import (
|
||||
@ -120,17 +120,19 @@ class OversightForm(CacheableForm):
|
||||
validators=[Required(), Length(min=10), IsNumber()],
|
||||
)
|
||||
|
||||
am_cor = BooleanField(translate("forms.task_order.oversight_am_cor_label"))
|
||||
cor_first_name = StringField(
|
||||
translate("forms.task_order.oversight_first_name_label")
|
||||
)
|
||||
cor_last_name = StringField(translate("forms.task_order.oversight_last_name_label"))
|
||||
cor_email = StringField(translate("forms.task_order.oversight_email_label"))
|
||||
cor_phone_number = TelField(
|
||||
translate("forms.task_order.oversight_phone_label"), validators=[PhoneNumber()]
|
||||
translate("forms.task_order.oversight_phone_label"),
|
||||
validators=[RequiredIfNot("am_cor"), PhoneNumber()],
|
||||
)
|
||||
cor_dod_id = StringField(
|
||||
translate("forms.task_order.oversight_dod_id_label"),
|
||||
validators=[Required(), Length(min=10), IsNumber()],
|
||||
validators=[RequiredIfNot("am_cor"), Length(min=10), IsNumber()],
|
||||
)
|
||||
|
||||
so_first_name = StringField(
|
||||
|
@ -1,5 +1,5 @@
|
||||
import re
|
||||
from wtforms.validators import ValidationError
|
||||
from wtforms.validators import ValidationError, StopValidation
|
||||
import pendulum
|
||||
from datetime import datetime
|
||||
from atst.utils.localization import translate
|
||||
@ -78,3 +78,28 @@ def ListItemsUnique(message=translate("forms.validators.list_items_unique_messag
|
||||
raise ValidationError(message)
|
||||
|
||||
return _list_items_unique
|
||||
|
||||
|
||||
def RequiredIfNot(other_field_name, message=translate("forms.validators.is_required")):
|
||||
""" A validator which makes a field required only if another field
|
||||
has a falsy value
|
||||
Args:
|
||||
other_field_name (str): the name of the field we check before
|
||||
determining if this field is required; if this other field is falsy,
|
||||
the field will be required
|
||||
message (str): an optional message to display if the field is
|
||||
required but hasNone value
|
||||
"""
|
||||
|
||||
def _required_if_not(form, field):
|
||||
other_field = form._fields.get(other_field_name)
|
||||
if other_field is None:
|
||||
raise Exception('no field named "%s" in form' % self.other_field_name)
|
||||
|
||||
if not bool(other_field.data):
|
||||
if field.data is None:
|
||||
raise ValidationError(message)
|
||||
else:
|
||||
raise StopValidation()
|
||||
|
||||
return _required_if_not
|
||||
|
@ -78,6 +78,10 @@ class ShowTaskOrderWorkflow:
|
||||
if self._section["section"] == "app_info":
|
||||
self._form.complexity.data = self.task_order.complexity
|
||||
self._form.dev_team.data = self.task_order.dev_team
|
||||
elif self._section["section"] == "oversight":
|
||||
if self.user.dod_id == self.task_order.cor_dod_id:
|
||||
self._form.am_cor.data = True
|
||||
|
||||
else:
|
||||
self._form = self._section[form_type]()
|
||||
|
||||
@ -133,6 +137,16 @@ class UpdateTaskOrderWorkflow(ShowTaskOrderWorkflow):
|
||||
if "dev_team" in to_data and "other" not in to_data["dev_team"]:
|
||||
to_data["dev_team_other"] = None
|
||||
|
||||
if self.form_data.get("am_cor"):
|
||||
cor_data = {
|
||||
"cor_first_name": self.user.first_name,
|
||||
"cor_last_name": self.user.last_name,
|
||||
"cor_email": self.user.email,
|
||||
"cor_phone_number": self.user.phone_number,
|
||||
"cor_dod_id": self.user.dod_id,
|
||||
}
|
||||
to_data = {**to_data, **cor_data}
|
||||
|
||||
return to_data
|
||||
|
||||
def validate(self):
|
||||
|
31
js/components/forms/cor.js
Normal file
31
js/components/forms/cor.js
Normal file
@ -0,0 +1,31 @@
|
||||
import FormMixin from '../../mixins/form'
|
||||
import textinput from '../text_input'
|
||||
import checkboxinput from '../checkbox_input'
|
||||
|
||||
export default {
|
||||
name: 'cor',
|
||||
|
||||
mixins: [FormMixin],
|
||||
|
||||
components: {
|
||||
textinput,
|
||||
checkboxinput,
|
||||
},
|
||||
|
||||
props: {
|
||||
initialData: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
},
|
||||
|
||||
data: function () {
|
||||
const {
|
||||
am_cor = false
|
||||
} = this.initialData
|
||||
|
||||
return {
|
||||
am_cor
|
||||
}
|
||||
}
|
||||
}
|
@ -11,6 +11,7 @@ import textinput from './components/text_input'
|
||||
import checkboxinput from './components/checkbox_input'
|
||||
import DetailsOfUse from './components/forms/details_of_use'
|
||||
import poc from './components/forms/poc'
|
||||
import cor from './components/forms/cor'
|
||||
import financial from './components/forms/financial'
|
||||
import toggler from './components/toggler'
|
||||
import NewApplication from './components/forms/new_application'
|
||||
@ -44,6 +45,7 @@ const app = new Vue({
|
||||
checkboxinput,
|
||||
DetailsOfUse,
|
||||
poc,
|
||||
cor,
|
||||
financial,
|
||||
NewApplication,
|
||||
selector,
|
||||
|
@ -21,17 +21,16 @@
|
||||
|
||||
<h3 class="subheading">{{ "task_orders.new.oversight.cor_info_title" | translate }}</h3>
|
||||
<p>{{ "task_orders.new.oversight.cor_info_paragraph" | translate }}</p>
|
||||
<div class='usa-input'>
|
||||
<fieldset class="usa-input__choices">
|
||||
<legend>
|
||||
<input type="checkbox" name="am_cor" id="am_cor" value="y" />
|
||||
<label for="am_cor">{{ "task_orders.new.oversight.am_cor_label" | translate }}</label>
|
||||
</legend>
|
||||
</fieldset>
|
||||
</div>
|
||||
{{ UserInfo(form.cor_first_name, form.cor_last_name, form.cor_email, form.cor_phone_number) }}
|
||||
{{ CheckboxInput(form.cor_invite) }}
|
||||
{{ TextInput(form.cor_dod_id, placeholder="1234567890", tooltip="Why", tooltip_title='Why', validation='dodId')}}
|
||||
<cor inline-template v-bind:initial-data='{{ form.data|tojson }}'>
|
||||
<div class='usa-input'>
|
||||
{{ CheckboxInput(form.am_cor) }}
|
||||
<template v-if="!am_cor">
|
||||
{{ UserInfo(form.cor_first_name, form.cor_last_name, form.cor_email, form.cor_phone_number) }}
|
||||
{{ CheckboxInput(form.cor_invite) }}
|
||||
{{ TextInput(form.cor_dod_id, placeholder="1234567890", tooltip="Why", tooltip_title='Why', validation='dodId')}}
|
||||
</template>
|
||||
</div>
|
||||
</cor>
|
||||
|
||||
<hr />
|
||||
|
||||
|
@ -6,6 +6,7 @@ import alembic.command
|
||||
from logging.config import dictConfig
|
||||
from werkzeug.datastructures import FileStorage
|
||||
from tempfile import TemporaryDirectory
|
||||
from collections import OrderedDict
|
||||
|
||||
from atst.app import make_app, make_config
|
||||
from atst.database import db as _db
|
||||
@ -84,14 +85,17 @@ def session(db, request):
|
||||
|
||||
|
||||
class DummyForm(dict):
|
||||
pass
|
||||
def __init__(self, data=OrderedDict(), errors=(), raw_data=None):
|
||||
self._fields = data
|
||||
self.errors = list(errors)
|
||||
|
||||
|
||||
class DummyField(object):
|
||||
def __init__(self, data=None, errors=(), raw_data=None):
|
||||
def __init__(self, data=None, errors=(), raw_data=None, name=None):
|
||||
self.data = data
|
||||
self.errors = list(errors)
|
||||
self.raw_data = raw_data
|
||||
self.name = name
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@ -99,6 +103,15 @@ def dummy_form():
|
||||
return DummyForm()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def dummy_form_with_field():
|
||||
def set_field(name, value):
|
||||
data = DummyField(data=value, name=name)
|
||||
return DummyForm(data=OrderedDict({name: data}))
|
||||
|
||||
return set_field
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def dummy_field():
|
||||
return DummyField()
|
||||
|
@ -1,7 +1,13 @@
|
||||
from wtforms.validators import ValidationError
|
||||
from wtforms.validators import ValidationError, StopValidation
|
||||
import pytest
|
||||
|
||||
from atst.forms.validators import Name, IsNumber, PhoneNumber, ListItemsUnique
|
||||
from atst.forms.validators import (
|
||||
Name,
|
||||
IsNumber,
|
||||
PhoneNumber,
|
||||
ListItemsUnique,
|
||||
RequiredIfNot,
|
||||
)
|
||||
|
||||
|
||||
class TestIsNumber:
|
||||
@ -73,3 +79,32 @@ class TestListItemsUnique:
|
||||
dummy_field.data = invalid
|
||||
with pytest.raises(ValidationError):
|
||||
validator(dummy_form, dummy_field)
|
||||
|
||||
|
||||
class TestRequiredIfNot:
|
||||
def test_RequiredIfNot_requires_field_if_arg_is_falsy(
|
||||
self, dummy_form_with_field, dummy_field
|
||||
):
|
||||
form = dummy_form_with_field("arg", False)
|
||||
validator = RequiredIfNot("arg")
|
||||
dummy_field.data = None
|
||||
|
||||
with pytest.raises(ValidationError):
|
||||
validator(form, dummy_field)
|
||||
|
||||
def test_RequiredIfNot_does_not_require_field_if_arg_is_truthy(
|
||||
self, dummy_form_with_field, dummy_field
|
||||
):
|
||||
form = dummy_form_with_field("arg", True)
|
||||
validator = RequiredIfNot("arg")
|
||||
dummy_field.data = None
|
||||
|
||||
with pytest.raises(StopValidation):
|
||||
validator(form, dummy_field)
|
||||
|
||||
def test_RequiredIfNot_arg_is_None_raises_error(self, dummy_form, dummy_field):
|
||||
validator = RequiredIfNot("arg")
|
||||
dummy_field.data = "some data"
|
||||
|
||||
with pytest.raises(Exception):
|
||||
validator(dummy_form, dummy_field)
|
||||
|
@ -203,6 +203,13 @@ def test_other_text_not_saved_if_other_not_checked(task_order):
|
||||
assert not workflow.task_order.complexity_other
|
||||
|
||||
|
||||
def test_cor_data_set_to_user_data_if_am_cor_is_checked(task_order):
|
||||
to_data = {**task_order.to_dictionary(), "am_cor": True}
|
||||
workflow = UpdateTaskOrderWorkflow(task_order.creator, to_data, 3, task_order.id)
|
||||
workflow.update()
|
||||
assert task_order.cor_dod_id == task_order.creator.dod_id
|
||||
|
||||
|
||||
def test_invite_officers_to_task_order(task_order, queue):
|
||||
to_data = {
|
||||
**TaskOrderFactory.dictionary(),
|
||||
|
@ -223,6 +223,7 @@ forms:
|
||||
oversight_email_label: Email
|
||||
oversight_phone_label: Phone Number
|
||||
oversight_dod_id_label: DoD ID
|
||||
oversight_am_cor_label: I am the Contracting Officer Representative (COR) for this Task Order
|
||||
ko_invite_label: Invite KO to Task Order Builder
|
||||
cor_invite_label: Invite COR to Task Order Builder
|
||||
so_invite_label: Invite Security Officer to Task Order Builder
|
||||
@ -234,6 +235,7 @@ forms:
|
||||
list_items_unique_message: Items must be unique
|
||||
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.
|
||||
is_required: This field is required.
|
||||
portfolio:
|
||||
name_label: Portfolio Name
|
||||
name_length_validation_message: Portfolio names must be at least 4 and not more than 50 characters
|
||||
@ -403,7 +405,6 @@ task_orders:
|
||||
skip_ko_label: "Skip for now (We'll remind you to enter one later)"
|
||||
cor_info_title: Contracting Officer Representative (COR) Information
|
||||
cor_info_paragraph: Your COR may assist in submitting the Task Order documents within thier official system of record. They may also be invited to log in an manage the Task Order entry within the JEDI Cloud portal.
|
||||
am_cor_label: I am the Contracting Officer Representative (COR) for this Task Order
|
||||
so_info_title: Security Officer Information
|
||||
so_info_paragraph: Your Security Officer will need to answer some security configuration questions in order to generate a DD-254 document, then electronically sign.
|
||||
review:
|
||||
|
Loading…
x
Reference in New Issue
Block a user