diff --git a/.gitignore b/.gitignore index 6afb33d1..7f2755cc 100644 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,4 @@ config/dev.ini # CRLs /crl /crl-tmp +*.bk diff --git a/atst/app.py b/atst/app.py index 99279958..4c206878 100644 --- a/atst/app.py +++ b/atst/app.py @@ -18,6 +18,7 @@ from atst.routes.dev import bp as dev_routes from atst.routes.errors import make_error_pages from atst.domain.authnid.crl import CRLCache from atst.domain.auth import apply_authentication +from atst.eda_client import MockEDAClient ENV = os.getenv("FLASK_ENV", "dev") @@ -41,6 +42,7 @@ def make_app(config): make_flask_callbacks(app) make_crl_validator(app) register_filters(app) + make_eda_client(app) db.init_app(app) csrf.init_app(app) @@ -139,3 +141,5 @@ def make_crl_validator(app): crl_locations.append(filename.absolute()) app.crl_cache = CRLCache(app.config["CA_CHAIN"], crl_locations, logger=app.logger) +def make_eda_client(app): + app.eda_client = MockEDAClient() diff --git a/atst/domain/task_orders.py b/atst/domain/task_orders.py index 5b507f53..fbc587a1 100644 --- a/atst/domain/task_orders.py +++ b/atst/domain/task_orders.py @@ -1,4 +1,5 @@ from sqlalchemy.orm.exc import NoResultFound +from flask import current_app as app from atst.database import db from atst.models.task_order import TaskOrder @@ -8,12 +9,36 @@ from .exceptions import NotFoundError class TaskOrders(object): @classmethod - def get(self, order_number): + def get(cls, order_number): try: task_order = ( db.session.query(TaskOrder).filter_by(number=order_number).one() ) except NoResultFound: - raise NotFoundError("task_order") + if TaskOrders._client(): + task_order = TaskOrders._get_from_eda(order_number) + else: + raise NotFoundError("task_order") return task_order + + @classmethod + def _get_from_eda(cls, order_number): + to_data = TaskOrders._client().get_contract(order_number, status="y") + if to_data: + return TaskOrders.create(to_data["contract_no"]) + else: + raise NotFoundError("task_order") + + @classmethod + def create(cls, order_number): + task_order = TaskOrder(number=order_number) + + db.session.add(task_order) + db.session.commit() + + return task_order + + @classmethod + def _client(cls): + return app.eda_client diff --git a/atst/eda_client.py b/atst/eda_client.py index e181f245..82c01217 100644 --- a/atst/eda_client.py +++ b/atst/eda_client.py @@ -71,8 +71,10 @@ class MockEDAClient(EDAClientBase): }, ] + MOCK_CONTRACT_NUMBER = "DCA10096D0052" + def get_contract(self, contract_number, status): - if contract_number == "DCA10096D0052" and status == "y": + if contract_number == self.MOCK_CONTRACT_NUMBER and status == "y": return { "aco_mod": "01", "admin_dodaac": None, diff --git a/atst/forms/financial.py b/atst/forms/financial.py index 42a29d39..e989d822 100644 --- a/atst/forms/financial.py +++ b/atst/forms/financial.py @@ -1,10 +1,11 @@ import re from wtforms.fields.html5 import EmailField from wtforms.fields import StringField -from wtforms.validators import Required, Email, Regexp +from wtforms.validators import Required, Email, Regexp, ValidationError from atst.domain.exceptions import NotFoundError from atst.domain.pe_numbers import PENumbers +from atst.domain.task_orders import TaskOrders from .fields import NewlineListField, SelectField from .forms import ValidatedForm @@ -57,12 +58,7 @@ def validate_pe_id(field, existing_request): return True -class FinancialForm(ValidatedForm): - def validate(self, *args, **kwargs): - if self.funding_type.data == "OTHER": - self.funding_type_other.validators.append(Required()) - return super().validate(*args, **kwargs) - +class BaseFinancialForm(ValidatedForm): def reset(self): """ Reset UII info so that it can be de-parsed rendered properly. @@ -76,7 +72,11 @@ class FinancialForm(ValidatedForm): valid = validate_pe_id(self.pe_id, existing_request) return valid - task_order_id = StringField( + @property + def is_missing_task_order_number(self): + return False + + task_order_number = StringField( "Task Order Number associated with this request", description="Include the original Task Order number (including the 000X at the end). Do not include any modification numbers. Note that there may be a lag between approving a task order and when it becomes available in our system.", validators=[Required()] @@ -117,6 +117,25 @@ class FinancialForm(ValidatedForm): "Contracting Officer Representative (COR) Office", validators=[Required()] ) + +class FinancialForm(BaseFinancialForm): + def validate_task_order_number(form, field): + try: + TaskOrders.get(field.data) + except NotFoundError: + raise ValidationError("Task Order number not found") + + @property + def is_missing_task_order_number(self): + return "task_order_number" in self.errors + + +class ExtendedFinancialForm(BaseFinancialForm): + def validate(self, *args, **kwargs): + if self.funding_type.data == "OTHER": + self.funding_type_other.validators.append(Required()) + return super().validate(*args, **kwargs) + funding_type = SelectField( description="What is the source of funding?", choices=[ diff --git a/atst/routes/requests/financial_verification.py b/atst/routes/requests/financial_verification.py index 966a9680..b7fc4993 100644 --- a/atst/routes/requests/financial_verification.py +++ b/atst/routes/requests/financial_verification.py @@ -3,15 +3,25 @@ from flask import request as http_request from . import requests_bp from atst.domain.requests import Requests -from atst.forms.financial import FinancialForm +from atst.forms.financial import FinancialForm, ExtendedFinancialForm + + +def financial_form(data): + if http_request.args.get("extended"): + return ExtendedFinancialForm(data=data) + else: + return FinancialForm(data=data) @requests_bp.route("/requests/verify/", methods=["GET"]) def financial_verification(request_id=None): request = Requests.get(request_id) - form = FinancialForm(data=request.body.get("financial_verification")) + form = financial_form(request.body.get("financial_verification")) return render_template( - "requests/financial_verification.html", f=form, request_id=request_id + "requests/financial_verification.html", + f=form, + request_id=request_id, + extended=http_request.args.get("extended"), ) @@ -19,9 +29,9 @@ def financial_verification(request_id=None): def update_financial_verification(request_id): post_data = http_request.form existing_request = Requests.get(request_id) - form = FinancialForm(post_data) + form = financial_form(post_data) - rerender_args = dict(request_id=request_id, f=form) + rerender_args = dict(request_id=request_id, f=form, extended=http_request.args.get("extended")) if form.validate(): request_data = {"financial_verification": form.data} @@ -31,11 +41,13 @@ def update_financial_verification(request_id): Requests.update(request_id, request_data) if valid: return redirect(url_for("requests.financial_verification_submitted")) + else: form.reset() return render_template( "requests/financial_verification.html", **rerender_args ) + else: form.reset() return render_template("requests/financial_verification.html", **rerender_args) diff --git a/templates/requests/financial_verification.html b/templates/requests/financial_verification.html index 71057795..2e7e094e 100644 --- a/templates/requests/financial_verification.html +++ b/templates/requests/financial_verification.html @@ -9,6 +9,24 @@
+ {% if extended %} + {{ Alert('Task Order not found in EDA', + message="Since the Task Order (TO) number was not found in our system of record, EDA, please populate the additional fields in the form below.", + level='warning' + ) }} + {% endif %} + + {% if f.is_missing_task_order_number %} + {% set extended_url = url_for('requests.financial_verification', request_id=request_id, extended=True) %} + {{ Alert('Task Order not found in EDA', + message="We could not find your Task Order in our system of record, EDA. + Please confirm that you have entered it correctly. + Otherwise enter TO information manually. + "|format(extended_url), + level='warning' + ) }} + {% endif %} +
@@ -19,7 +37,11 @@
{% block form_action %} -
+ {% if extended %} + + {% else %} + + {% endif %} {% endblock %} {{ f.csrf_token }} @@ -35,8 +57,48 @@

In order to get you access to the JEDI Cloud, we will need you to enter the details below that will help us verify and account for your Task Order.

+ {% if extended %} +
+ {{ OptionsInput(f.funding_type) }} + + + + {{ TextInput( + f.clin_0001,placeholder="50,000", + validation='integer' + ) }} + + {{ TextInput( + f.clin_0003,placeholder="13,000", + validation='integer' + ) }} + + {{ TextInput( + f.clin_1001,placeholder="30,000", + validation='integer' + ) }} + + {{ TextInput( + f.clin_1003,placeholder="7,000", + validation='integer' + ) }} + + {{ TextInput( + f.clin_2001,placeholder="30,000", + validation='integer' + ) }} + + {{ TextInput( + f.clin_2003,placeholder="7,000", + validation='integer' + ) }} +
+ {% endif %} + {{ TextInput( - f.task_order_id, + f.task_order_number, placeholder="e.g.: 1234567899C0001", tooltip="A Contracting Officer will likely be the best source for this number.", validation="anything" @@ -83,51 +145,6 @@ {{ TextInput(f.office_cor,placeholder="e.g.: WHS") }} -
- - {{ Alert('Task Order not found in EDA', - message="Since the Task Order (TO) number was not found in our system of record, EDA, please populate the additional fields in the form below.", - level='warning' - ) }} - -
- {{ OptionsInput(f.funding_type) }} - - - - {{ TextInput( - f.clin_0001,placeholder="50,000", - validation='integer' - ) }} - - {{ TextInput( - f.clin_0003,placeholder="13,000", - validation='integer' - ) }} - - {{ TextInput( - f.clin_1001,placeholder="30,000", - validation='integer' - ) }} - - {{ TextInput( - f.clin_1003,placeholder="7,000", - validation='integer' - ) }} - - {{ TextInput( - f.clin_2001,placeholder="30,000", - validation='integer' - ) }} - - {{ TextInput( - f.clin_2003,placeholder="7,000", - validation='integer' - ) }} -
- {% endautoescape %} {% endblock form %} diff --git a/templates/requests/screen-5.html b/templates/requests/screen-5.html index 9c5b16ac..435b88cf 100644 --- a/templates/requests/screen-5.html +++ b/templates/requests/screen-5.html @@ -15,9 +15,9 @@

Financial Verification

In order to get you access to the JEDI Cloud, we will need you to enter the details below that will help us verify and account for your Task Order.

-{{ f.task_order_id.label }} -{{ f.task_order_id(placeholder="Example: 1234567899C0001") }} -{% for e in f.task_order_id.errors %} +{{ f.task_order_number.label }} +{{ f.task_order_number(placeholder="Example: 1234567899C0001") }} +{% for e in f.task_order_number.errors %}
{{ e }}
diff --git a/tests/domain/test_task_orders.py b/tests/domain/test_task_orders.py index ba422bc5..27b5d2de 100644 --- a/tests/domain/test_task_orders.py +++ b/tests/domain/test_task_orders.py @@ -2,6 +2,7 @@ import pytest from atst.domain.exceptions import NotFoundError from atst.domain.task_orders import TaskOrders +from atst.eda_client import MockEDAClient from tests.factories import TaskOrderFactory @@ -13,6 +14,19 @@ def test_can_get_task_order(): assert to.id == to.id -def test_nonexistent_task_order_raises(): +def test_can_get_task_order_from_eda(monkeypatch): + monkeypatch.setattr("atst.domain.task_orders.TaskOrders._client", lambda: MockEDAClient()) + to = TaskOrders.get(MockEDAClient.MOCK_CONTRACT_NUMBER) + + assert to.number == MockEDAClient.MOCK_CONTRACT_NUMBER + + +def test_nonexistent_task_order_raises_without_client(): with pytest.raises(NotFoundError): TaskOrders.get("some fake number") + + +def test_nonexistent_task_order_raises_with_client(monkeypatch): + monkeypatch.setattr("atst.domain.task_orders.TaskOrders._client", lambda: MockEDAClient()) + with pytest.raises(NotFoundError): + TaskOrders.get("some other fake numer") diff --git a/tests/forms/test_financial.py b/tests/forms/test_financial.py index 9bc82421..871c7c19 100644 --- a/tests/forms/test_financial.py +++ b/tests/forms/test_financial.py @@ -1,6 +1,7 @@ import pytest -from atst.forms.financial import suggest_pe_id, FinancialForm +from atst.forms.financial import suggest_pe_id, FinancialForm, ExtendedFinancialForm +from atst.eda_client import MockEDAClient @pytest.mark.parametrize("input_,expected", [ @@ -18,7 +19,7 @@ def test_funding_type_other_not_required_if_funding_type_is_not_other(): form_data = { "funding_type": "PROC" } - form = FinancialForm(data=form_data) + form = ExtendedFinancialForm(data=form_data) form.validate() assert "funding_type_other" not in form.errors @@ -27,7 +28,7 @@ def test_funding_type_other_required_if_funding_type_is_other(): form_data = { "funding_type": "OTHER" } - form = FinancialForm(data=form_data) + form = ExtendedFinancialForm(data=form_data) form.validate() assert "funding_type_other" in form.errors @@ -67,3 +68,16 @@ def test_ba_code_validation(input_, expected): is_valid = "ba_code" not in form.errors assert is_valid == expected + +def test_task_order_number_validation(monkeypatch): + monkeypatch.setattr("atst.domain.task_orders.TaskOrders._client", lambda: MockEDAClient()) + form_invalid = FinancialForm(data={"task_order_number": "1234"}) + form_invalid.validate() + + assert "task_order_number" in form_invalid.errors + + form_valid = FinancialForm(data={"task_order_number": MockEDAClient.MOCK_CONTRACT_NUMBER}, eda_client=MockEDAClient()) + form_valid.validate() + + assert "task_order_number" not in form_valid.errors + diff --git a/tests/routes/test_financial_verification.py b/tests/routes/test_financial_verification.py index fca2aa3b..675b15e3 100644 --- a/tests/routes/test_financial_verification.py +++ b/tests/routes/test_financial_verification.py @@ -1,6 +1,10 @@ import re import pytest import urllib +from flask import url_for + +from atst.eda_client import MockEDAClient + from tests.mocks import MOCK_REQUEST, MOCK_USER from tests.factories import PENumberFactory @@ -9,7 +13,7 @@ class TestPENumberInForm: required_data = { "pe_id": "123", - "task_order_id": "1234567899C0001", + "task_order_number": MockEDAClient.MOCK_CONTRACT_NUMBER, "fname_co": "Contracting", "lname_co": "Officer", "email_co": "jane@mail.mil", @@ -18,6 +22,11 @@ class TestPENumberInForm: "lname_cor": "Representative", "email_cor": "jane@mail.mil", "office_cor": "WHS", + "uii_ids": "1234", + "treasury_code": "00123456", + "ba_code": "024A" + } + extended_data = { "funding_type": "RDTE", "funding_type_other": "other", "clin_0001": "50,000", @@ -33,9 +42,13 @@ class TestPENumberInForm: monkeypatch.setattr("atst.domain.requests.Requests.get", lambda i: MOCK_REQUEST) monkeypatch.setattr("atst.domain.auth.get_current_user", lambda *args: MOCK_USER) - def submit_data(self, client, data): + def submit_data(self, client, data, extended=False): + url_kwargs = {"request_id": MOCK_REQUEST.id} + if extended: + url_kwargs["extended"] = True + response = client.post( - "/requests/verify/{}".format(MOCK_REQUEST.id), + url_for("requests.financial_verification", **url_kwargs), headers={"Content-Type": "application/x-www-form-urlencoded"}, data=urllib.parse.urlencode(data), follow_redirects=False, @@ -83,3 +96,40 @@ class TestPENumberInForm: assert "There were some errors" in response.data.decode() assert response.status_code == 200 + + def test_submit_financial_form_with_invalid_task_order(self, monkeypatch, user_session, client): + monkeypatch.setattr("atst.domain.requests.Requests.get", lambda i: MOCK_REQUEST) + user_session() + + data = dict(self.required_data) + data['pe_id'] = MOCK_REQUEST.body['financial_verification']['pe_id'] + data['task_order_number'] = '1234' + + response = self.submit_data(client, data) + + assert "enter TO information manually" in response.data.decode() + + def test_submit_financial_form_with_valid_task_order(self, monkeypatch, user_session, client): + monkeypatch.setattr("atst.domain.requests.Requests.get", lambda i: MOCK_REQUEST) + user_session() + + data = dict(self.required_data) + data['pe_id'] = MOCK_REQUEST.body['financial_verification']['pe_id'] + data['task_order_number'] = MockEDAClient.MOCK_CONTRACT_NUMBER + + response = self.submit_data(client, data) + + assert "enter TO information manually" not in response.data.decode() + + def test_submit_extended_financial_form(self, monkeypatch, user_session, client): + monkeypatch.setattr("atst.domain.requests.Requests.get", lambda i: MOCK_REQUEST) + user_session() + + data = { **self.required_data, **self.extended_data } + data['pe_id'] = MOCK_REQUEST.body['financial_verification']['pe_id'] + data['task_order_number'] = "1234567" + + response = self.submit_data(client, data, extended=True) + + assert response.status_code == 302 + assert "/requests/financial_verification_submitted" in response.headers.get("Location")