diff --git a/atst/forms/task_order.py b/atst/forms/task_order.py
index 4935702f..f1bf67c9 100644
--- a/atst/forms/task_order.py
+++ b/atst/forms/task_order.py
@@ -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(
diff --git a/atst/forms/validators.py b/atst/forms/validators.py
index 3485c210..d8b59d97 100644
--- a/atst/forms/validators.py
+++ b/atst/forms/validators.py
@@ -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
diff --git a/atst/routes/task_orders/new.py b/atst/routes/task_orders/new.py
index b7c4ce2b..b021ec39 100644
--- a/atst/routes/task_orders/new.py
+++ b/atst/routes/task_orders/new.py
@@ -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):
diff --git a/js/components/forms/cor.js b/js/components/forms/cor.js
new file mode 100644
index 00000000..11e8e578
--- /dev/null
+++ b/js/components/forms/cor.js
@@ -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
+ }
+ }
+}
diff --git a/js/index.js b/js/index.js
index a3c7a390..9c79fcd3 100644
--- a/js/index.js
+++ b/js/index.js
@@ -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,
diff --git a/templates/task_orders/new/oversight.html b/templates/task_orders/new/oversight.html
index 7b5181be..050c7bc3 100644
--- a/templates/task_orders/new/oversight.html
+++ b/templates/task_orders/new/oversight.html
@@ -21,17 +21,16 @@
{{ "task_orders.new.oversight.cor_info_title" | translate }}
{{ "task_orders.new.oversight.cor_info_paragraph" | translate }}
-
-
-
-{{ 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')}}
+
+
+ {{ CheckboxInput(form.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')}}
+
+
+
diff --git a/tests/conftest.py b/tests/conftest.py
index c4e20d6d..781fd515 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -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()
diff --git a/tests/forms/test_validators.py b/tests/forms/test_validators.py
index f6eda1d9..d01ec7a8 100644
--- a/tests/forms/test_validators.py
+++ b/tests/forms/test_validators.py
@@ -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)
diff --git a/tests/routes/task_orders/test_new_task_order.py b/tests/routes/task_orders/test_new_task_order.py
index 1bdf97d5..53374ed6 100644
--- a/tests/routes/task_orders/test_new_task_order.py
+++ b/tests/routes/task_orders/test_new_task_order.py
@@ -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(),
diff --git a/translations.yaml b/translations.yaml
index 249ab7e3..4f6c8752 100644
--- a/translations.yaml
+++ b/translations.yaml
@@ -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: