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) }} + +
+

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: