Merge pull request #387 from dod-ccpo/save-finver-draft

Save Financial Verification Draft
This commit is contained in:
richard-dds 2018-10-29 10:26:30 -04:00 committed by GitHub
commit 09d3f33908
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 1104 additions and 629 deletions

View File

@ -0,0 +1,32 @@
"""resource attachments
Revision ID: 9c24c609878a
Revises: 903d7c66ff1d
Create Date: 2018-10-18 17:14:25.566215
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = '9c24c609878a'
down_revision = 'c99026ab9918'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('attachments', sa.Column('resource', sa.String(), nullable=True))
op.add_column('attachments', sa.Column('resource_id', postgresql.UUID(as_uuid=True), nullable=True))
op.create_index(op.f('ix_attachments_resource_id'), 'attachments', ['resource_id'], unique=False)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index(op.f('ix_attachments_resource_id'), table_name='attachments')
op.drop_column('attachments', 'resource_id')
op.drop_column('attachments', 'resource')
# ### end Alembic commands ###

View File

@ -0,0 +1,74 @@
import re
from atst.domain.task_orders import TaskOrders
from atst.domain.pe_numbers import PENumbers
from atst.domain.exceptions import NotFoundError
class PENumberValidator(object):
PE_REGEX = re.compile(
r"""
(0?\d) # program identifier
(0?\d) # category
(\d) # activity
(\d+) # sponsor element
(.+) # service
""",
re.X,
)
def validate(self, request, field):
if field.errors:
return False
if self._same_as_previous(request, field.data):
return True
try:
PENumbers.get(field.data)
except NotFoundError:
self._apply_error(field)
return False
return True
def suggest_pe_id(self, pe_id):
suggestion = pe_id
match = self.PE_REGEX.match(pe_id)
if match:
(program, category, activity, sponsor, service) = match.groups()
if len(program) < 2:
program = "0" + program
if len(category) < 2:
category = "0" + category
suggestion = "".join((program, category, activity, sponsor, service))
if suggestion != pe_id:
return suggestion
return None
def _same_as_previous(self, request, pe_id):
return request.pe_number == pe_id
def _apply_error(self, field):
suggestion = self.suggest_pe_id(field.data)
error_str = (
"We couldn't find that PE number. {}"
"If you have double checked it you can submit anyway. "
"Your request will need to go through a manual review."
).format('Did you mean "{}"? '.format(suggestion) if suggestion else "")
field.errors += (error_str,)
class TaskOrderNumberValidator(object):
def validate(self, field):
try:
TaskOrders.get(field.data)
except NotFoundError:
self._apply_error(field)
return False
return True
def _apply_error(self, field):
field.errors += ("Task Order number not found",)

View File

@ -1,7 +1,5 @@
from werkzeug.datastructures import FileStorage
import dateutil import dateutil
from atst.domain.task_orders import TaskOrders
from atst.domain.workspaces import Workspaces from atst.domain.workspaces import Workspaces
from atst.models.request_revision import RequestRevision from atst.models.request_revision import RequestRevision
from atst.models.request_status_event import RequestStatusEvent, RequestStatus from atst.models.request_status_event import RequestStatusEvent, RequestStatus
@ -76,14 +74,15 @@ class Requests(object):
@classmethod @classmethod
def update(cls, request_id, request_delta): def update(cls, request_id, request_delta):
request = RequestsQuery.get_with_lock(request_id) request = RequestsQuery.get_with_lock(request_id)
return Requests._update(request, request_delta)
@classmethod
def _update(cls, request, request_delta):
new_body = deep_merge(request_delta, request.body) new_body = deep_merge(request_delta, request.body)
revision = create_revision_from_request_body(new_body) revision = create_revision_from_request_body(new_body)
request.revisions.append(revision) request.revisions.append(revision)
request = RequestsQuery.add_and_commit(request) return RequestsQuery.add_and_commit(request)
return request
@classmethod @classmethod
def approve_and_create_workspace(cls, request): def approve_and_create_workspace(cls, request):
@ -156,35 +155,12 @@ class Requests(object):
return Requests.status_count(RequestStatus.APPROVED) return Requests.status_count(RequestStatus.APPROVED)
@classmethod @classmethod
def update_financial_verification(cls, request_id, financial_data): def update_financial_verification(cls, request_id, financial_data, task_order=None):
request = RequestsQuery.get_with_lock(request_id) request = RequestsQuery.get_with_lock(request_id)
request_data = financial_data.copy()
task_order_data = {
k: request_data.pop(k)
for (k, v) in financial_data.items()
if k in TaskOrders.TASK_ORDER_DATA
}
if task_order_data:
task_order_number = request_data.pop("task_order_number")
else:
task_order_number = request_data.get("task_order_number")
if "task_order" in request_data and isinstance(
request_data["task_order"], FileStorage
):
task_order_data["pdf"] = request_data.pop("task_order")
task_order = TaskOrders.get_or_create_task_order(
task_order_number, task_order_data
)
if task_order: if task_order:
request.task_order = task_order request.task_order = task_order
request = Requests.update(request.id, {"financial_verification": request_data}) request = Requests._update(request, {"financial_verification": financial_data})
return request return request
@classmethod @classmethod

View File

@ -2,9 +2,9 @@ from sqlalchemy.orm.exc import NoResultFound
from flask import current_app as app from flask import current_app as app
from atst.database import db from atst.database import db
from atst.models.task_order import TaskOrder, Source from atst.models.task_order import TaskOrder, Source, FundingType
from atst.models.attachment import Attachment
from .exceptions import NotFoundError from .exceptions import NotFoundError
from atst.utils import update_obj
class TaskOrders(object): class TaskOrders(object):
@ -18,25 +18,28 @@ class TaskOrders(object):
) )
except NoResultFound: except NoResultFound:
if TaskOrders._client(): if TaskOrders._client():
task_order = TaskOrders._get_from_eda(order_number) task_order = TaskOrders.get_from_eda(order_number)
else: else:
raise NotFoundError("task_order") raise NotFoundError("task_order")
return task_order return task_order
@classmethod @classmethod
def _get_from_eda(cls, order_number): def get_from_eda(cls, order_number):
to_data = TaskOrders._client().get_contract(order_number, status="y") to_data = TaskOrders._client().get_contract(order_number, status="y")
if to_data: if to_data:
# TODO: we need to determine exactly what we're getting and storing from the EDA client # TODO: we need to determine exactly what we're getting and storing from the EDA client
return TaskOrders.create(source=Source.EDA, **to_data) return TaskOrders.create(
source=Source.EDA, funding_type=FundingType.PROC, **to_data
)
else: else:
raise NotFoundError("task_order") raise NotFoundError("task_order")
@classmethod @classmethod
def create(cls, **kwargs): def create(cls, source=Source.MANUAL, **kwargs):
task_order = TaskOrder(**kwargs) to_data = {k: v for k, v in kwargs.items() if v not in ["", None]}
task_order = TaskOrder(source=source, **to_data)
db.session.add(task_order) db.session.add(task_order)
db.session.commit() db.session.commit()
@ -48,18 +51,8 @@ class TaskOrders(object):
return app.eda_client return app.eda_client
@classmethod @classmethod
def get_or_create_task_order(cls, number, task_order_data=None): def update(cls, task_order, dct):
try: updated = update_obj(task_order, dct, ignore_vals=lambda v: v in ["", None])
return TaskOrders.get(number) db.session.add(updated)
db.session.commit()
except NotFoundError: return updated
if task_order_data:
pdf_file = task_order_data.pop("pdf")
# should catch the error here
attachment = Attachment.attach(pdf_file)
return TaskOrders.create(
**task_order_data,
number=number,
source=Source.MANUAL,
pdf=attachment,
)

6
atst/forms/exceptions.py Normal file
View File

@ -0,0 +1,6 @@
class FormValidationError(Exception):
message = "Form validation failed."
def __init__(self, form):
self.form = form

View File

@ -1,171 +1,77 @@
import re import re
import pendulum import pendulum
from wtforms.fields.html5 import DateField, EmailField from wtforms.fields.html5 import DateField, EmailField
from wtforms.fields import StringField, FileField from wtforms.fields import StringField, FileField, FormField
from wtforms.validators import InputRequired, Email, Regexp from wtforms.validators import InputRequired, Email, Regexp, Optional
from flask_wtf.file import FileAllowed from flask_wtf.file import FileAllowed
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, NumberStringField from .fields import NewlineListField, SelectField, NumberStringField
from .forms import ValidatedForm from atst.forms.forms import ValidatedForm
from .data import FUNDING_TYPES from .data import FUNDING_TYPES
from .validators import DateRange from .validators import DateRange
PE_REGEX = re.compile(
r"""
(0?\d) # program identifier
(0?\d) # category
(\d) # activity
(\d+) # sponsor element
(.+) # service
""",
re.X,
)
TREASURY_CODE_REGEX = re.compile(r"^0*([1-9]{4}|[1-9]{6})$") TREASURY_CODE_REGEX = re.compile(r"^0*([1-9]{4}|[1-9]{6})$")
BA_CODE_REGEX = re.compile(r"[0-9]{2}\w?$") BA_CODE_REGEX = re.compile(r"[0-9]{2}\w?$")
def suggest_pe_id(pe_id):
suggestion = pe_id
match = PE_REGEX.match(pe_id)
if match:
(program, category, activity, sponsor, service) = match.groups()
if len(program) < 2:
program = "0" + program
if len(category) < 2:
category = "0" + category
suggestion = "".join((program, category, activity, sponsor, service))
if suggestion != pe_id:
return suggestion
return None
def validate_pe_id(field, existing_request):
try:
PENumbers.get(field.data)
except NotFoundError:
suggestion = suggest_pe_id(field.data)
error_str = (
"We couldn't find that PE number. {}"
"If you have double checked it you can submit anyway. "
"Your request will need to go through a manual review."
).format('Did you mean "{}"? '.format(suggestion) if suggestion else "")
field.errors += (error_str,)
return False
return True
def validate_task_order_number(field):
try:
TaskOrders.get(field.data)
except NotFoundError:
field.errors += ("Task Order number not found",)
return False
return True
def number_to_int(num): def number_to_int(num):
if num: if num:
return int(num) return int(num)
class BaseFinancialForm(ValidatedForm): def coerce_choice(val):
def reset(self): if val is None:
""" return None
Reset UII info so that it can be de-parsed rendered properly. elif isinstance(val, str):
This is a stupid workaround, and there's probably a better way. return val
""" else:
self.uii_ids.process_data(self.uii_ids.data) return val.value
class DraftValidateMixin(object):
def validate_draft(self):
"""
Make all fields optional before validation, and then return them to
their previous state.
"""
for field in self:
field.validators.insert(0, Optional())
valid = self.validate()
for field in self:
field.validators.pop(0)
def perform_extra_validation(self, existing_request):
valid = True
if not existing_request or existing_request.get("pe_id") != self.pe_id.data:
valid = validate_pe_id(self.pe_id, existing_request)
return valid return valid
@property
def is_missing_task_order_number(self):
return False
task_order_number = StringField( class TaskOrderForm(ValidatedForm, DraftValidateMixin):
def do_validate_number(self):
for field in self:
if field.name != "task_order-number":
field.validators.insert(0, Optional())
valid = super().validate()
for field in self:
if field.name != "task_order-number":
field.validators.pop(0)
return valid
number = StringField(
"Task Order Number associated with this request", "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.", 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=[InputRequired()], validators=[InputRequired()],
) )
uii_ids = NewlineListField(
"Unique Item Identifier (UII)s related to your application(s) if you already have them.",
description="If you have more than one UII, place each one on a new line.",
)
pe_id = StringField(
"Program Element Number",
description="PE numbers help the Department of Defense identify which offices' budgets are contributing towards this resource use. <br/><em>It should be 7 digits followed by 1-3 letters, and should have a zero as the first and third digits.</em>",
validators=[InputRequired()],
)
treasury_code = StringField(
"Program Treasury Code",
description="Program Treasury Code (or Appropriations Code) identifies resource types. <br/> <em>It should be a four digit or six digit number, optionally prefixed by one or more zeros.</em>",
validators=[InputRequired(), Regexp(TREASURY_CODE_REGEX)],
)
ba_code = StringField(
"Program Budget Activity (BA) Code",
description="BA Code is used to identify the purposes, projects, or types of activities financed by the appropriation fund. <br/><em>It should be two digits, followed by an optional letter.</em>",
validators=[InputRequired(), Regexp(BA_CODE_REGEX)],
)
fname_co = StringField("KO First Name", validators=[InputRequired()])
lname_co = StringField("KO Last Name", validators=[InputRequired()])
email_co = EmailField("KO Email", validators=[InputRequired(), Email()])
office_co = StringField("KO Office", validators=[InputRequired()])
fname_cor = StringField("COR First Name", validators=[InputRequired()])
lname_cor = StringField("COR Last Name", validators=[InputRequired()])
email_cor = EmailField("COR Email", validators=[InputRequired(), Email()])
office_cor = StringField("COR Office", validators=[InputRequired()])
class FinancialForm(BaseFinancialForm):
def perform_extra_validation(self, existing_request):
previous_valid = super().perform_extra_validation(existing_request)
task_order_valid = validate_task_order_number(self.task_order_number)
return previous_valid and task_order_valid
@property
def is_missing_task_order_number(self):
return "task_order_number" in self.errors
@property
def is_only_missing_task_order_number(self):
return "task_order_number" in self.errors and len(self.errors) == 1
class ExtendedFinancialForm(BaseFinancialForm):
def validate(self, *args, **kwargs):
if self.funding_type.data == "OTHER":
self.funding_type_other.validators.append(InputRequired())
return super().validate(*args, **kwargs)
funding_type = SelectField( funding_type = SelectField(
description="What is the source of funding?", description="What is the source of funding?",
choices=FUNDING_TYPES, choices=FUNDING_TYPES,
validators=[InputRequired()], validators=[InputRequired()],
coerce=coerce_choice,
render_kw={"required": False}, render_kw={"required": False},
) )
@ -227,10 +133,114 @@ class ExtendedFinancialForm(BaseFinancialForm):
filters=[number_to_int], filters=[number_to_int],
) )
task_order = FileField( pdf = FileField(
"Upload a copy of your Task Order", "Upload a copy of your Task Order",
validators=[ validators=[
FileAllowed(["pdf"], "Only PDF documents can be uploaded."), FileAllowed(["pdf"], "Only PDF documents can be uploaded."),
InputRequired(), InputRequired(),
], ],
render_kw={"required": False},
) )
class RequestFinancialVerificationForm(ValidatedForm, DraftValidateMixin):
uii_ids = NewlineListField(
"Unique Item Identifier (UII)s related to your application(s) if you already have them.",
description="If you have more than one UII, place each one on a new line.",
)
pe_id = StringField(
"Program Element Number",
description="PE numbers help the Department of Defense identify which offices' budgets are contributing towards this resource use. <br/><em>It should be 7 digits followed by 1-3 letters, and should have a zero as the first and third digits.</em>",
validators=[InputRequired()],
)
treasury_code = StringField(
"Program Treasury Code",
description="Program Treasury Code (or Appropriations Code) identifies resource types. <br/> <em>It should be a four digit or six digit number, optionally prefixed by one or more zeros.</em>",
validators=[InputRequired(), Regexp(TREASURY_CODE_REGEX)],
)
ba_code = StringField(
"Program Budget Activity (BA) Code",
description="BA Code is used to identify the purposes, projects, or types of activities financed by the appropriation fund. <br/><em>It should be two digits, followed by an optional letter.</em>",
validators=[InputRequired(), Regexp(BA_CODE_REGEX)],
)
fname_co = StringField("KO First Name", validators=[InputRequired()])
lname_co = StringField("KO Last Name", validators=[InputRequired()])
email_co = EmailField("KO Email", validators=[InputRequired(), Email()])
office_co = StringField("KO Office", validators=[InputRequired()])
fname_cor = StringField("COR First Name", validators=[InputRequired()])
lname_cor = StringField("COR Last Name", validators=[InputRequired()])
email_cor = EmailField("COR Email", validators=[InputRequired(), Email()])
office_cor = StringField("COR Office", validators=[InputRequired()])
def reset(self):
"""
Reset UII info so that it can be de-parsed rendered properly.
This is a stupid workaround, and there's probably a better way.
"""
self.uii_ids.process_data(self.uii_ids.data)
class FinancialVerificationForm(ValidatedForm):
task_order = FormField(TaskOrderForm)
request = FormField(RequestFinancialVerificationForm)
def validate(self, *args, **kwargs):
if not kwargs.get("is_extended", True):
return self.do_validate_request()
if self.task_order.funding_type.data == "OTHER":
self.task_order.funding_type_other.validators.append(InputRequired())
to_pdf_validators = None
if kwargs.get("has_attachment"):
to_pdf_validators = list(self.task_order.pdf.validators)
self.task_order.pdf.validators = []
valid = super().validate()
if to_pdf_validators:
self.task_order.pdf.validators = to_pdf_validators
return valid
def do_validate_request(self):
"""
Called do_validate_request to avoid being considered an inline
validator by wtforms.
"""
request_valid = self.request.validate(self)
task_order_valid = self.task_order.do_validate_number()
return request_valid and task_order_valid
def validate_draft(self):
return self.task_order.validate_draft() and self.request.validate_draft()
def reset(self):
self.request.reset()
@property
def pe_id(self):
return self.request.pe_id
@property
def task_order_number(self):
return self.task_order.number
@property
def is_missing_task_order_number(self):
return "number" in self.errors.get("task_order", {})
@property
def is_only_missing_task_order_number(self):
return "task_order_number" in self.errors and len(self.errors) == 1

View File

@ -1,9 +1,12 @@
from sqlalchemy import Column, String from sqlalchemy import Column, String
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm.exc import NoResultFound
from flask import current_app as app from flask import current_app as app
from atst.models import Base, types, mixins from atst.models import Base, types, mixins
from atst.database import db from atst.database import db
from atst.uploader import UploadError from atst.uploader import UploadError
from atst.domain.exceptions import NotFoundError
class AttachmentError(Exception): class AttachmentError(Exception):
@ -16,20 +19,56 @@ class Attachment(Base, mixins.TimestampsMixin):
id = types.Id() id = types.Id()
filename = Column(String, nullable=False) filename = Column(String, nullable=False)
object_name = Column(String, unique=True, nullable=False) object_name = Column(String, unique=True, nullable=False)
resource = Column(String)
resource_id = Column(UUID(as_uuid=True), index=True)
@classmethod @classmethod
def attach(cls, fyle): def attach(cls, fyle, resource=None, resource_id=None):
try: try:
filename, object_name = app.uploader.upload(fyle) filename, object_name = app.uploader.upload(fyle)
except UploadError as e: except UploadError as e:
raise AttachmentError("Could not add attachment. " + str(e)) raise AttachmentError("Could not add attachment. " + str(e))
attachment = Attachment(filename=filename, object_name=object_name) attachment = Attachment(
filename=filename,
object_name=object_name,
resource=resource,
resource_id=resource_id,
)
db.session.add(attachment) db.session.add(attachment)
db.session.commit() db.session.commit()
return attachment return attachment
@classmethod
def get(cls, id_):
try:
return db.session.query(Attachment).filter_by(id=id_).one()
except NoResultFound:
raise NotFoundError("attachment")
@classmethod
def get_for_resource(cls, resource, resource_id):
try:
return (
db.session.query(Attachment)
.filter_by(resource=resource, resource_id=resource_id)
.one()
)
except NoResultFound:
raise NotFoundError("attachment")
@classmethod
def delete_for_resource(cls, resource, resource_id):
try:
return (
db.session.query(Attachment)
.filter_by(resource=resource, resource_id=resource_id)
.delete()
)
except NoResultFound:
raise NotFoundError("attachment")
def __repr__(self): def __repr__(self):
return "<Attachment(name='{}', id='{}')>".format(self.filename, self.id) return "<Attachment(name='{}', id='{}')>".format(self.filename, self.id)

View File

@ -1,26 +1,14 @@
from sqlalchemy import event from sqlalchemy import event
from flask import g from flask import g
import re
from atst.models.audit_event import AuditEvent from atst.models.audit_event import AuditEvent
from atst.utils import camel_to_snake, getattr_path
ACTION_CREATE = "create" ACTION_CREATE = "create"
ACTION_UPDATE = "update" ACTION_UPDATE = "update"
ACTION_DELETE = "delete" ACTION_DELETE = "delete"
def getattr_path(obj, path, default=None):
_obj = obj
for item in path.split("."):
_obj = getattr(_obj, item, default)
return _obj
def camel_to_snake(camel_cased):
s1 = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", camel_cased)
return re.sub("([a-z0-9])([A-Z])", r"\1_\2", s1).lower()
class AuditableMixin(object): class AuditableMixin(object):
@staticmethod @staticmethod
def create_audit_event(connection, resource, action): def create_audit_event(connection, resource, action):

View File

@ -6,6 +6,7 @@ from atst.models import Base, types, mixins
from atst.models.request_status_event import RequestStatus from atst.models.request_status_event import RequestStatus
from atst.utils import first_or_none from atst.utils import first_or_none
from atst.models.request_revision import RequestRevision from atst.models.request_revision import RequestRevision
from atst.models.task_order import Source as TaskOrderSource
def map_properties_to_dict(properties, instance): def map_properties_to_dict(properties, instance):
@ -135,7 +136,7 @@ class Request(Base, mixins.TimestampsMixin, mixins.AuditableMixin):
@property @property
def financial_verification(self): def financial_verification(self):
return self.body.get("financial_verification") return self.body.get("financial_verification", {})
@property @property
def is_financially_verified(self): def is_financially_verified(self):
@ -224,6 +225,18 @@ class Request(Base, mixins.TimestampsMixin, mixins.AuditableMixin):
def contracting_officer_email(self): def contracting_officer_email(self):
return self.latest_revision.email_co return self.latest_revision.email_co
@property
def pe_number(self):
return self.body.get("financial_verification", {}).get("pe_id")
@property
def has_manual_task_order(self):
return (
self.task_order.source == TaskOrderSource.MANUAL
if self.task_order is not None
else None
)
def __repr__(self): def __repr__(self):
return "<Request(status='{}', name='{}', creator='{}', is_approved='{}', time_created='{}', id='{}')>".format( return "<Request(status='{}', name='{}', creator='{}', is_approved='{}', time_created='{}', id='{}')>".format(
self.status_displayname, self.status_displayname,

View File

@ -1,134 +1,275 @@
from flask import g, render_template, redirect, url_for from flask import g, render_template, redirect, url_for
from flask import request as http_request from flask import request as http_request
from werkzeug.datastructures import ImmutableMultiDict, FileStorage
from . import requests_bp from . import requests_bp
from atst.domain.requests import Requests from atst.domain.requests import Requests
from atst.forms.financial import FinancialForm, ExtendedFinancialForm from atst.forms.financial import FinancialVerificationForm
from atst.forms.exceptions import FormValidationError
from atst.domain.exceptions import NotFoundError
from atst.domain.requests.financial_verification import (
PENumberValidator,
TaskOrderNumberValidator,
)
from atst.models.attachment import Attachment
from atst.domain.task_orders import TaskOrders
class FinancialVerification: def fv_extended(_http_request):
def __init__(self, request, extended=False, post_data=None): return _http_request.args.get("extended", "false").lower() in ["true", "t"]
class FinancialVerification(object):
def __init__(self, request):
self.request = request.latest_revision
self.task_order = request.task_order
class FinancialVerificationBase(object):
def _get_form(self, request, is_extended, formdata=None):
_formdata = ImmutableMultiDict(formdata) if formdata is not None else None
fv = FinancialVerification(request)
form = FinancialVerificationForm(obj=fv, formdata=_formdata)
if is_extended:
try:
attachment = Attachment.get_for_resource("task_order", self.request.id)
form.task_order.pdf.data = attachment.filename
except NotFoundError:
pass
return form
def _process_attachment(self, is_extended, form):
attachment = None
if is_extended:
attachment = None
if isinstance(form.task_order.pdf.data, FileStorage):
Attachment.delete_for_resource("task_order", self.request.id)
attachment = Attachment.attach(
form.task_order.pdf.data, "task_order", self.request.id
)
elif isinstance(form.task_order.pdf.data, str):
try:
attachment = Attachment.get_for_resource(
"task_order", self.request.id
)
except NotFoundError:
pass
if attachment:
form.task_order.pdf.data = attachment.filename
return attachment
def _try_create_task_order(self, form, attachment, is_extended):
task_order_number = form.task_order.number.data
if not task_order_number:
return None
task_order_data = form.task_order.data
if attachment:
task_order_data["pdf"] = attachment
try:
task_order = TaskOrders.get(task_order_number)
task_order = TaskOrders.update(task_order, task_order_data)
return task_order
except NotFoundError:
pass
try:
return TaskOrders.get_from_eda(task_order_number)
except NotFoundError:
pass
return TaskOrders.create(**task_order_data)
def _raise(self, form):
form.reset()
raise FormValidationError(form)
class GetFinancialVerificationForm(FinancialVerificationBase):
def __init__(self, user, request, is_extended=False):
self.user = user
self.request = request self.request = request
self._extended = extended self.is_extended = is_extended
self._post_data = post_data
self._form = None
self.reset()
def reset(self): def execute(self):
self._updateable = False form = self._get_form(self.request, self.is_extended)
self._valid = False return form
self.workspace = None
if self._form:
self._form.reset()
@property
def is_extended(self):
return self._extended or self.is_pending_changes
@property class UpdateFinancialVerification(FinancialVerificationBase):
def is_pending_changes(self): def __init__(
return self.request.is_pending_financial_verification_changes self,
pe_validator,
task_order_validator,
user,
request,
fv_data,
is_extended=False,
):
self.pe_validator = pe_validator
self.task_order_validator = task_order_validator
self.user = user
self.request = request
self.fv_data = fv_data
self.is_extended = is_extended
@property def execute(self):
def _task_order_data(self): form = self._get_form(self.request, self.is_extended, self.fv_data)
if self.request.task_order:
task_order = self.request.task_order
data = task_order.to_dictionary()
data["task_order_number"] = task_order.number
data["funding_type"] = task_order.funding_type.value
return data
else:
return {}
@property should_update = True
def _form_data(self): should_submit = True
if self._post_data: updated_request = None
return self._post_data
else:
form_data = self.request.body.get("financial_verification", {})
form_data.update(self._task_order_data)
return form_data attachment = self._process_attachment(self.is_extended, form)
@property if not form.validate(is_extended=self.is_extended, has_attachment=attachment):
def form(self): should_update = False
if not self._form:
if self.is_extended:
self._form = ExtendedFinancialForm(data=self._form_data)
else:
self._form = FinancialForm(data=self._form_data)
return self._form if not self.pe_validator.validate(self.request, form.pe_id):
should_submit = False
def validate(self): if not self.task_order_validator.validate(form.task_order.number):
if self.form.validate(): should_submit = False
self._updateable = True
self._valid = self.form.perform_extra_validation( if should_update:
self.request.body.get("financial_verification") task_order = self._try_create_task_order(form, attachment, self.is_extended)
updated_request = Requests.update_financial_verification(
self.request.id, form.request.data, task_order=task_order
) )
else: if should_submit:
self._updateable = False return Requests.submit_financial_verification(updated_request)
self._valid = False
return self._valid self._raise(form)
@property
def pending(self):
return self.request.is_pending_ccpo_approval
def finalize(self): class SaveFinancialVerificationDraft(FinancialVerificationBase):
if self._updateable: def __init__(
self.request = Requests.update_financial_verification( self,
self.request.id, self.form.data pe_validator,
task_order_validator,
user,
request,
fv_data,
is_extended=False,
):
self.pe_validator = pe_validator
self.task_order_validator = task_order_validator
self.user = user
self.request = request
self.fv_data = fv_data
self.is_extended = is_extended
def execute(self):
form = self._get_form(self.request, self.is_extended, self.fv_data)
valid = True
if not form.validate_draft():
self._raise(form)
if not self.pe_validator.validate(self.request, form.pe_id):
valid = False
if form.task_order.number.data and not self.task_order_validator.validate(
form.task_order.number
):
valid = False
attachment = self._process_attachment(self.is_extended, form)
task_order = self._try_create_task_order(form, attachment, self.is_extended)
updated_request = Requests.update_financial_verification(
self.request.id, form.request.data, task_order=task_order
) )
if self._valid: if valid:
self.request = Requests.submit_financial_verification(self.request) return updated_request
else:
if self.request.is_financially_verified: self._raise(form)
self.workspace = Requests.approve_and_create_workspace(self.request)
@requests_bp.route("/requests/verify/<string:request_id>", methods=["GET"]) @requests_bp.route("/requests/verify/<string:request_id>", methods=["GET"])
def financial_verification(request_id): def financial_verification(request_id):
request = Requests.get(g.current_user, request_id) request = Requests.get(g.current_user, request_id)
finver = FinancialVerification(request, extended=http_request.args.get("extended")) is_extended = fv_extended(http_request)
should_be_extended = not is_extended and request.has_manual_task_order
if should_be_extended:
return redirect(
url_for(".financial_verification", request_id=request_id, extended=True)
)
form = GetFinancialVerificationForm(
g.current_user, request, is_extended=is_extended
).execute()
return render_template( return render_template(
"requests/financial_verification.html", "requests/financial_verification.html",
f=finver.form, f=form,
jedi_request=finver.request, jedi_request=request,
review_comment=finver.request.review_comment, review_comment=request.review_comment,
extended=finver.is_extended, extended=is_extended,
) )
@requests_bp.route("/requests/verify/<string:request_id>", methods=["POST"]) @requests_bp.route("/requests/verify/<string:request_id>", methods=["POST"])
def update_financial_verification(request_id): def update_financial_verification(request_id):
request = Requests.get(g.current_user, request_id) request = Requests.get(g.current_user, request_id)
finver = FinancialVerification( fv_data = {**http_request.form, **http_request.files}
request, extended=http_request.args.get("extended"), post_data=http_request.form is_extended = fv_extended(http_request)
)
finver.validate() try:
updated_request = UpdateFinancialVerification(
finver.finalize() PENumberValidator(),
TaskOrderNumberValidator(),
if finver.workspace: g.current_user,
return redirect( request,
url_for( fv_data,
"workspaces.new_project", is_extended=is_extended,
workspace_id=finver.workspace.id, ).execute()
newWorkspace=True, except FormValidationError as e:
)
)
elif finver.pending:
return redirect(url_for("requests.requests_index", modal="pendingCCPOApproval"))
else:
finver.reset()
return render_template( return render_template(
"requests/financial_verification.html", "requests/financial_verification.html",
jedi_request=finver.request, jedi_request=request,
f=finver.form, f=e.form,
extended=finver.is_extended, extended=is_extended,
) )
if updated_request.task_order.verified:
workspace = Requests.approve_and_create_workspace(updated_request)
return redirect(
url_for(
"workspaces.new_project", workspace_id=workspace.id, newWorkspace=True
)
)
else:
return redirect(url_for("requests.requests_index", modal="pendingCCPOApproval"))
@requests_bp.route("/requests/verify/<string:request_id>/draft", methods=["POST"])
def save_financial_verification_draft(request_id):
request = Requests.get(g.current_user, request_id)
fv_data = {**http_request.form, **http_request.files}
is_extended = fv_extended(http_request)
try:
SaveFinancialVerificationDraft(
PENumberValidator(),
TaskOrderNumberValidator(),
g.current_user,
request,
fv_data,
is_extended=is_extended,
).execute()
except FormValidationError as e:
return render_template(
"requests/financial_verification.html",
jedi_request=request,
f=e.form,
extended=is_extended,
)
return redirect(url_for("requests.requests_index"))

View File

@ -1,3 +1,6 @@
import re
def first_or_none(predicate, lst): def first_or_none(predicate, lst):
return next((x for x in lst if predicate(x)), None) return next((x for x in lst if predicate(x)), None)
@ -18,3 +21,35 @@ def deep_merge(source, destination: dict):
return b return b
return _deep_merge(source, dict(destination)) return _deep_merge(source, dict(destination))
def getattr_path(obj, path, default=None):
_obj = obj
for item in path.split("."):
if isinstance(_obj, dict):
_obj = _obj.get(item)
else:
_obj = getattr(_obj, item, default)
return _obj
def update_obj(obj, dct, ignore_vals=lambda v: v is None):
for k, v in dct.items():
if hasattr(obj, k) and not ignore_vals(v):
setattr(obj, k, v)
return obj
def camel_to_snake(camel_cased):
s1 = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", camel_cased)
return re.sub("([a-z0-9])([A-Z])", r"\1_\2", s1).lower()
def drop(keys, dct):
_keys = set(keys)
return {k: v for k, v in dct.items() if k not in _keys}
def pick(keys, dct):
_keys = set(keys)
return {k: v for (k, v) in dct.items() if k in _keys}

View File

@ -25,7 +25,22 @@ export default {
} = this.initialData } = this.initialData
return { return {
funding_type funding_type,
shouldForceShowTaskOrder: false
}
},
computed: {
showTaskOrderUpload: function() {
return !this.initialData.task_order.pdf || this.shouldForceShowTaskOrder
}
},
methods: {
forceShowTaskOrderUpload: function(e) {
console.log("forceShowTaskOrder", e)
e.preventDefault()
this.shouldForceShowTaskOrder = true
} }
} }
} }

View File

@ -18,7 +18,7 @@
{% if paragraph %}paragraph='true'{% endif %} {% if paragraph %}paragraph='true'{% endif %}
{% if noMaxWidth %}no-max-width='true'{% endif %} {% if noMaxWidth %}no-max-width='true'{% endif %}
{% if initial_value or field.data is not none %}initial-value='{{ initial_value or field.data }}'{% endif %} {% if initial_value or field.data is not none %}initial-value='{{ initial_value or field.data }}'{% endif %}
{% if field.errors %}v-bind:initial-errors='{{ field.errors }}'{% endif %} {% if field.errors %}v-bind:initial-errors='{{ field.errors | list }}'{% endif %}
key='{{ field.name }}' key='{{ field.name }}'
inline-template> inline-template>

View File

@ -42,13 +42,7 @@
{% endcall %} {% endcall %}
{% endif %} {% endif %}
{% block form_action %} <form autocomplete="off" enctype="multipart/form-data">
{% if extended %}
<form method='POST' action="{{ url_for('requests.financial_verification', request_id=jedi_request.id, extended=True) }}" autocomplete="off" enctype="multipart/form-data">
{% else %}
<form method='POST' action="{{ url_for('requests.financial_verification', request_id=jedi_request.id) }}" autocomplete="off">
{% endif %}
{% endblock %}
{{ f.csrf_token }} {{ f.csrf_token }}
{% block form %} {% block form %}
@ -74,101 +68,109 @@
{% if extended %} {% if extended %}
<fieldset class="form__sub-fields form__sub-fields--warning"> <fieldset class="form__sub-fields form__sub-fields--warning">
{{ OptionsInput(f.funding_type) }} {{ OptionsInput(f.task_order.funding_type) }}
<template v-if="funding_type == 'OTHER'" v-cloak> <template v-if="funding_type == 'OTHER'" v-cloak>
{{ TextInput(f.funding_type_other) }} {{ TextInput(f.task_order.funding_type_other) }}
</template> </template>
{{ DateInput(f.expiration_date, placeholder='MM / DD / YYYY', validation='date', tooltip='Please enter the expiration date for the task order only and do not include options that you may choose to exercise in the future.') }} {{ DateInput(f.task_order.expiration_date, placeholder='MM / DD / YYYY', validation='date', tooltip='Please enter the expiration date for the task order only and do not include options that you may choose to exercise in the future.') }}
{{ TextInput( {{ TextInput(
f.clin_0001, f.task_order.clin_0001,
validation='dollars' validation='dollars'
) }} ) }}
{{ TextInput( {{ TextInput(
f.clin_0003, f.task_order.clin_0003,
validation='dollars' validation='dollars'
) }} ) }}
{{ TextInput( {{ TextInput(
f.clin_1001, f.task_order.clin_1001,
validation='dollars' validation='dollars'
) }} ) }}
{{ TextInput( {{ TextInput(
f.clin_1003, f.task_order.clin_1003,
validation='dollars' validation='dollars'
) }} ) }}
{{ TextInput( {{ TextInput(
f.clin_2001, f.task_order.clin_2001,
validation='dollars' validation='dollars'
) }} ) }}
{{ TextInput( {{ TextInput(
f.clin_2003, f.task_order.clin_2003,
validation='dollars' validation='dollars'
) }} ) }}
<div class="usa-input usa-input--validation--anything {% if f.task_order.errors %} usa-input--error {% endif %}"> <template v-if="showTaskOrderUpload">
{{ f.task_order.label }} <div class="usa-input {% if f.task_order.pdf.errors %} usa-input--error {% endif %}">
{{ f.task_order }} {{ f.task_order.pdf.label }}
{% for error in f.task_order.errors %} {{ f.task_order.pdf }}
{% for error in f.task_order.pdf.errors %}
<span class="usa-input__message">{{error}}</span> <span class="usa-input__message">{{error}}</span>
{% endfor %} {% endfor %}
</div> </div>
</template>
<template v-else>
<p>Uploaded {{ f.task_order.pdf.data }}.</p>
<div>
<button v-on:click="forceShowTaskOrderUpload($event)">Change</button>
</div>
</template>
</fieldset> </fieldset>
{% endif %} {% endif %}
{{ TextInput( {{ TextInput(
f.task_order_number, f.task_order.number,
placeholder="e.g.: 1234567899C0001", placeholder="e.g.: 1234567899C0001",
tooltip="A Contracting Officer will likely be the best source for this number.", tooltip="A Contracting Officer will likely be the best source for this number.",
validation="anything" validation="anything"
) }} ) }}
{{ TextInput(f.uii_ids, {{ TextInput(f.request.uii_ids,
paragraph=True, paragraph=True,
placeholder="examples: \nDI 0CVA5786950 \nUN1945326361234786950", placeholder="examples: \nDI 0CVA5786950 \nUN1945326361234786950",
tooltip="A Unique Item Identifer is a unique code that helps the Department of Defense track and report on where and how digital assets are stored. <br>Not all applications have an existing UII number assigned." tooltip="A Unique Item Identifer is a unique code that helps the Department of Defense track and report on where and how digital assets are stored. <br>Not all applications have an existing UII number assigned."
) }} ) }}
{{ TextInput(f.pe_id, {{ TextInput(f.request.pe_id,
placeholder="e.g.: 0105688F", placeholder="e.g.: 0105688F",
validation="peNumber" validation="peNumber"
) }} ) }}
{{ TextInput(f.treasury_code,placeholder="e.g.: 00123456",validation="treasuryCode") }} {{ TextInput(f.request.treasury_code,placeholder="e.g.: 00123456",validation="treasuryCode") }}
{{ TextInput(f.ba_code,placeholder="e.g.: 02A",validation="baCode") }} {{ TextInput(f.request.ba_code,placeholder="e.g.: 02A",validation="baCode") }}
<hr /> <hr />
<h3>Contracting Officer (KO) Information</h3> <h3>Contracting Officer (KO) Information</h3>
<div class='form-row'> <div class='form-row'>
<div class='form-col form-col--half '>{{ TextInput(f.fname_co) }}</div> <div class='form-col form-col--half '>{{ TextInput(f.request.fname_co) }}</div>
<div class='form-col form-col--half '>{{ TextInput(f.lname_co) }}</div> <div class='form-col form-col--half '>{{ TextInput(f.request.lname_co) }}</div>
</div> </div>
<div class='form-row'> <div class='form-row'>
<div class='form-col form-col--half'>{{ TextInput(f.email_co,validation='email', placeholder='e.g. jane@mail.mil') }}</div> <div class='form-col form-col--half'>{{ TextInput(f.request.email_co,validation='email', placeholder='e.g. jane@mail.mil') }}</div>
<div class='form-col form-col--half'>{{ TextInput(f.office_co,placeholder="e.g.: WHS") }}</div> <div class='form-col form-col--half'>{{ TextInput(f.request.office_co,placeholder="e.g.: WHS") }}</div>
</div> </div>
<hr /> <hr />
<h3>Contracting Officer Representative (COR) Information</h3> <h3>Contracting Officer Representative (COR) Information</h3>
<div class='form-row'> <div class='form-row'>
<div class='form-col form-col--half '>{{ TextInput(f.fname_cor) }}</div> <div class='form-col form-col--half '>{{ TextInput(f.request.fname_cor) }}</div>
<div class='form-col form-col--half '>{{ TextInput(f.lname_cor) }}</div> <div class='form-col form-col--half '>{{ TextInput(f.request.lname_cor) }}</div>
</div> </div>
<div class='form-row'> <div class='form-row'>
<div class='form-col form-col--half'>{{ TextInput(f.email_cor,validation='email', placeholder='e.g. jane@mail.mil') }}</div> <div class='form-col form-col--half'>{{ TextInput(f.request.email_cor,validation='email', placeholder='e.g. jane@mail.mil') }}</div>
<div class='form-col form-col--half'>{{ TextInput(f.office_cor,placeholder="e.g.: WHS") }}</div> <div class='form-col form-col--half'>{{ TextInput(f.request.office_cor,placeholder="e.g.: WHS") }}</div>
</div> </div>
@ -180,7 +182,8 @@
{% endblock form %} {% endblock form %}
{% block next %} {% block next %}
<div class='action-group'> <div class='action-group'>
<input type='submit' class='usa-button usa-button-primary' value='Save & Continue' /> <input formmethod="post" formaction="{{ url_for('requests.financial_verification', request_id=jedi_request.id, extended=extended) }}" type='submit' class='usa-button usa-button-primary' value='Save & Continue' />
<input formmethod="post" formaction="{{ url_for('requests.save_financial_verification_draft', request_id=jedi_request.id, extended=extended) }}" type='submit' class='usa-button usa-button-primary' value='Save Draft' />
</div> </div>
{% endblock %} {% endblock %}
</form> </form>

View File

@ -143,39 +143,6 @@ request_financial_data = {
} }
def test_update_financial_verification_without_task_order(
extended_financial_verification_data
):
request = RequestFactory.create()
financial_data = {**request_financial_data, **extended_financial_verification_data}
Requests.update_financial_verification(request.id, financial_data)
assert request.task_order
assert request.task_order.clin_0001 == int(
extended_financial_verification_data["clin_0001"]
)
assert request.task_order.source == TaskOrderSource.MANUAL
assert request.task_order.pdf
def test_update_financial_verification_with_task_order():
task_order = TaskOrderFactory.create(source=TaskOrderSource.EDA)
financial_data = {**request_financial_data, "task_order_number": task_order.number}
request = RequestFactory.create()
Requests.update_financial_verification(request.id, financial_data)
assert request.task_order == task_order
def test_update_financial_verification_with_invalid_task_order():
request = RequestFactory.create()
Requests.update_financial_verification(request.id, request_financial_data)
assert not request.task_order
assert "task_order_number" in request.body.get("financial_verification")
assert (
request.body["financial_verification"]["task_order_number"]
== request_financial_data["task_order_number"]
)
def test_set_status_sets_revision(): def test_set_status_sets_revision():
request = RequestFactory.create() request = RequestFactory.create()
Requests.set_status(request, RequestStatus.APPROVED) Requests.set_status(request, RequestStatus.APPROVED)

View File

@ -1,6 +1,5 @@
import pytest import pytest
from atst.models.task_order import Source as TaskOrderSource
from atst.domain.exceptions import NotFoundError from atst.domain.exceptions import NotFoundError
from atst.domain.task_orders import TaskOrders from atst.domain.task_orders import TaskOrders
from atst.eda_client import MockEDAClient from atst.eda_client import MockEDAClient
@ -15,16 +14,6 @@ def test_can_get_task_order():
assert to.id == to.id assert to.id == to.id
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
assert to.source == TaskOrderSource.EDA
def test_nonexistent_task_order_raises_without_client(): def test_nonexistent_task_order_raises_without_client():
with pytest.raises(NotFoundError): with pytest.raises(NotFoundError):
TaskOrders.get("some fake number") TaskOrders.get("some fake number")
@ -36,10 +25,3 @@ def test_nonexistent_task_order_raises_with_client(monkeypatch):
) )
with pytest.raises(NotFoundError): with pytest.raises(NotFoundError):
TaskOrders.get("some other fake numer") TaskOrders.get("some other fake numer")
def test_create_attachment(extended_financial_verification_data):
task_order_data = extended_financial_verification_data.copy()
task_order_data["pdf"] = task_order_data.pop("task_order")
task_order = TaskOrders.get_or_create_task_order("abc123", task_order_data)
assert task_order.pdf

View File

@ -1,8 +1,8 @@
import pytest import pytest
from werkzeug.datastructures import ImmutableMultiDict from werkzeug.datastructures import ImmutableMultiDict
from atst.forms.financial import suggest_pe_id, FinancialForm, ExtendedFinancialForm from atst.forms.financial import FinancialVerificationForm
from atst.eda_client import MockEDAClient from atst.domain.requests.financial_verification import PENumberValidator
@pytest.mark.parametrize( @pytest.mark.parametrize(
@ -16,21 +16,21 @@ from atst.eda_client import MockEDAClient
], ],
) )
def test_suggest_pe_id(input_, expected): def test_suggest_pe_id(input_, expected):
assert suggest_pe_id(input_) == expected assert PENumberValidator().suggest_pe_id(input_) == expected
def test_funding_type_other_not_required_if_funding_type_is_not_other(): def test_funding_type_other_not_required_if_funding_type_is_not_other():
form_data = {"funding_type": "PROC"} form_data = ImmutableMultiDict({"task_order-funding_type": "PROC"})
form = ExtendedFinancialForm(data=form_data) form = FinancialVerificationForm(form_data)
form.validate() form.validate()
assert "funding_type_other" not in form.errors assert "funding_type_other" not in form.errors
def test_funding_type_other_required_if_funding_type_is_other(): def test_funding_type_other_required_if_funding_type_is_other():
form_data = {"funding_type": "OTHER"} form_data = ImmutableMultiDict({"task_order-funding_type": "OTHER"})
form = ExtendedFinancialForm(data=form_data) form = FinancialVerificationForm(form_data)
form.validate() form.validate()
assert "funding_type_other" in form.errors assert "funding_type_other" in form.errors["task_order"]
@pytest.mark.parametrize( @pytest.mark.parametrize(
@ -47,10 +47,10 @@ def test_funding_type_other_required_if_funding_type_is_other():
], ],
) )
def test_treasury_code_validation(input_, expected): def test_treasury_code_validation(input_, expected):
form_data = ImmutableMultiDict([("treasury_code", input_)]) form_data = ImmutableMultiDict([("request-treasury_code", input_)])
form = FinancialForm(form_data) form = FinancialVerificationForm(form_data)
form.validate() form.validate()
is_valid = "treasury_code" not in form.errors is_valid = "treasury_code" not in form.errors["request"]
assert is_valid == expected assert is_valid == expected
@ -74,38 +74,19 @@ def test_treasury_code_validation(input_, expected):
], ],
) )
def test_ba_code_validation(input_, expected): def test_ba_code_validation(input_, expected):
form_data = ImmutableMultiDict([("ba_code", input_)]) form_data = ImmutableMultiDict([("request-ba_code", input_)])
form = FinancialForm(form_data) form = FinancialVerificationForm(form_data)
form.validate() form.validate()
is_valid = "ba_code" not in form.errors is_valid = "ba_code" not in form.errors["request"]
assert is_valid == expected assert is_valid == expected
def test_task_order_number_validation(monkeypatch):
monkeypatch.setattr(
"atst.domain.task_orders.TaskOrders._client", lambda: MockEDAClient()
)
monkeypatch.setattr("atst.forms.financial.validate_pe_id", lambda *args: True)
form_invalid = FinancialForm(data={"task_order_number": "1234"})
form_invalid.perform_extra_validation({})
assert "task_order_number" in form_invalid.errors
form_valid = FinancialForm(
data={"task_order_number": MockEDAClient.MOCK_CONTRACT_NUMBER},
eda_client=MockEDAClient(),
)
form_valid.perform_extra_validation({})
assert "task_order_number" not in form_valid.errors
def test_can_submit_zero_for_clin(): def test_can_submit_zero_for_clin():
form_first = ExtendedFinancialForm() form_first = FinancialVerificationForm()
form_first.validate() form_first.validate()
assert "clin_0001" in form_first.errors assert "clin_0001" in form_first.errors["task_order"]
form_data = ImmutableMultiDict([("clin_0001", "0")]) form_data = ImmutableMultiDict([("task_order-clin_0001", "0")])
form_second = ExtendedFinancialForm(form_data) form_second = FinancialVerificationForm(form_data)
form_second.validate() form_second.validate()
assert "clin_0001" not in form_second.errors assert "clin_0001" not in form_second.errors["task_order"]

View File

@ -6,7 +6,7 @@ MOCK_REQUEST = RequestFactory.build(creator=MOCK_USER)
DOD_SDN_INFO = {"first_name": "ART", "last_name": "GARFUNKEL", "dod_id": "5892460358"} DOD_SDN_INFO = {"first_name": "ART", "last_name": "GARFUNKEL", "dod_id": "5892460358"}
DOD_SDN = f"CN={DOD_SDN_INFO['last_name']}.{DOD_SDN_INFO['first_name']}.G.{DOD_SDN_INFO['dod_id']},OU=OTHER,OU=PKI,OU=DoD,O=U.S. Government,C=US" DOD_SDN = f"CN={DOD_SDN_INFO['last_name']}.{DOD_SDN_INFO['first_name']}.G.{DOD_SDN_INFO['dod_id']},OU=OTHER,OU=PKI,OU=DoD,O=U.S. Government,C=US"
MOCK_VALID_PE_ID = "8675309U" MOCK_VALID_PE_ID = "080675309U"
FIXTURE_EMAIL_ADDRESS = "artgarfunkel@uso.mil" FIXTURE_EMAIL_ADDRESS = "artgarfunkel@uso.mil"

View File

@ -1,219 +1,439 @@
import urllib
import pytest import pytest
from unittest.mock import MagicMock
from flask import url_for from flask import url_for
import datetime
from atst.eda_client import MockEDAClient from atst.eda_client import MockEDAClient
from atst.models.request_status_event import RequestStatus from atst.routes.requests.financial_verification import (
from atst.routes.requests.financial_verification import FinancialVerification GetFinancialVerificationForm,
UpdateFinancialVerification,
from tests.mocks import MOCK_REQUEST, MOCK_USER SaveFinancialVerificationDraft,
from tests.factories import (
PENumberFactory,
RequestFactory,
UserFactory,
RequestStatusEventFactory,
RequestReviewFactory,
) )
from tests.mocks import MOCK_VALID_PE_ID
from tests.factories import RequestFactory, UserFactory
from atst.forms.exceptions import FormValidationError
from atst.domain.requests.financial_verification import (
PENumberValidator,
TaskOrderNumberValidator,
)
from atst.utils import pick
from atst.models.request_status_event import RequestStatus
from atst.domain.requests.query import RequestsQuery
class TestPENumberInForm:
required_data = { @pytest.fixture
"pe_id": "123", def fv_data():
"task_order_number": MockEDAClient.MOCK_CONTRACT_NUMBER, return {
"fname_co": "Contracting", "request-pe_id": "123",
"lname_co": "Officer", "task_order-number": MockEDAClient.MOCK_CONTRACT_NUMBER,
"email_co": "jane@mail.mil", "request-fname_co": "Contracting",
"office_co": "WHS", "request-lname_co": "Officer",
"fname_cor": "Officer", "request-email_co": "jane@mail.mil",
"lname_cor": "Representative", "request-office_co": "WHS",
"email_cor": "jane@mail.mil", "request-fname_cor": "Officer",
"office_cor": "WHS", "request-lname_cor": "Representative",
"uii_ids": "1234", "request-email_cor": "jane@mail.mil",
"treasury_code": "00123456", "request-office_cor": "WHS",
"ba_code": "02A", "request-uii_ids": "1234",
"request-treasury_code": "00123456",
"request-ba_code": "02A",
} }
def _set_monkeypatches(self, monkeypatch):
monkeypatch.setattr(
"atst.forms.financial.FinancialForm.validate", lambda s: True
)
user = UserFactory.create()
monkeypatch.setattr("atst.domain.auth.get_current_user", lambda *args: user)
return user
def submit_data(self, client, user, data, extended=False): @pytest.fixture
def e_fv_data(pdf_upload):
return {
"task_order-funding_type": "RDTE",
"task_order-funding_type_other": "other",
"task_order-expiration_date": "1/1/{}".format(datetime.date.today().year + 1),
"task_order-clin_0001": "50000",
"task_order-clin_0003": "13000",
"task_order-clin_1001": "30000",
"task_order-clin_1003": "7000",
"task_order-clin_2001": "30000",
"task_order-clin_2003": "7000",
"task_order-pdf": pdf_upload,
}
MANUAL_TO_NUMBER = "DCA10096D0051"
TrueValidator = MagicMock()
TrueValidator.validate = MagicMock(return_value=True)
FalseValidator = MagicMock()
FalseValidator.validate = MagicMock(return_value=False)
def test_update_fv(fv_data):
request = RequestFactory.create()
user = UserFactory.create()
data = {**fv_data, "pe_id": MOCK_VALID_PE_ID}
updated_request = UpdateFinancialVerification(
TrueValidator, TrueValidator, user, request, data, is_extended=False
).execute()
assert updated_request.is_pending_ccpo_approval
def test_update_fv_re_enter_pe_number(fv_data):
request = RequestFactory.create()
user = UserFactory.create()
data = {**fv_data, "pe_id": "0101228M"}
update_fv = UpdateFinancialVerification(
PENumberValidator(), TrueValidator, user, request, data, is_extended=False
)
with pytest.raises(FormValidationError):
update_fv.execute()
updated_request = update_fv.execute()
assert updated_request.is_pending_ccpo_approval
def test_update_fv_invalid_task_order_number(fv_data):
request = RequestFactory.create()
user = UserFactory.create()
data = {**fv_data, "task_order-number": MANUAL_TO_NUMBER}
update_fv = UpdateFinancialVerification(
TrueValidator,
TaskOrderNumberValidator(),
user,
request,
data,
is_extended=False,
)
with pytest.raises(FormValidationError):
update_fv.execute()
def test_update_fv_extended(fv_data, e_fv_data):
request = RequestFactory.create()
user = UserFactory.create()
data = {**fv_data, **e_fv_data}
update_fv = UpdateFinancialVerification(
TrueValidator, TaskOrderNumberValidator(), user, request, data, is_extended=True
)
assert update_fv.execute()
def test_update_fv_missing_extended_data(fv_data):
request = RequestFactory.create()
user = UserFactory.create()
update_fv = UpdateFinancialVerification(
TrueValidator,
TaskOrderNumberValidator(),
user,
request,
fv_data,
is_extended=True,
)
with pytest.raises(FormValidationError):
update_fv.execute()
def test_update_fv_submission(fv_data):
request = RequestFactory.create()
user = UserFactory.create()
updated_request = UpdateFinancialVerification(
TrueValidator, TrueValidator, user, request, fv_data
).execute()
assert updated_request
def test_save_empty_draft():
request = RequestFactory.create()
user = UserFactory.create()
save_draft = SaveFinancialVerificationDraft(
TrueValidator, TrueValidator, user, request, {}, is_extended=False
)
assert save_draft.execute()
def test_save_draft_with_ba_code():
request = RequestFactory.create()
user = UserFactory.create()
data = {"ba_code": "02A"}
save_draft = SaveFinancialVerificationDraft(
TrueValidator, TrueValidator, user, request, data, is_extended=False
)
assert save_draft.execute()
def test_save_draft_with_invalid_task_order(fv_data):
request = RequestFactory.create()
user = UserFactory.create()
save_draft = SaveFinancialVerificationDraft(
TrueValidator, FalseValidator, user, request, fv_data, is_extended=False
)
with pytest.raises(FormValidationError):
assert save_draft.execute()
def test_save_draft_with_invalid_pe_number(fv_data):
request = RequestFactory.create()
user = UserFactory.create()
save_draft = SaveFinancialVerificationDraft(
FalseValidator, TrueValidator, user, request, fv_data, is_extended=False
)
with pytest.raises(FormValidationError):
assert save_draft.execute()
def test_save_draft_re_enter_pe_number(fv_data):
request = RequestFactory.create()
user = UserFactory.create()
data = {**fv_data, "pe_id": "0101228M"}
save_fv = SaveFinancialVerificationDraft(
PENumberValidator(), TrueValidator, user, request, data, is_extended=False
)
with pytest.raises(FormValidationError):
save_fv.execute()
save_fv.execute()
def test_save_draft_and_then_submit():
request = RequestFactory.create()
user = UserFactory.create()
data = {"ba_code": "02A"}
updated_request = SaveFinancialVerificationDraft(
TrueValidator, TrueValidator, user, request, data, is_extended=False
).execute()
with pytest.raises(FormValidationError):
UpdateFinancialVerification(
TrueValidator, TrueValidator, user, updated_request, data
).execute()
def test_updated_request_has_pdf(fv_data, e_fv_data):
request = RequestFactory.create()
user = UserFactory.create()
data = {**fv_data, **e_fv_data, "task_order-number": MANUAL_TO_NUMBER}
updated_request = UpdateFinancialVerification(
TrueValidator, TrueValidator, user, request, data, is_extended=True
).execute()
assert updated_request.task_order.pdf
def test_can_save_draft_with_just_pdf(e_fv_data):
request = RequestFactory.create()
user = UserFactory.create()
data = {"task_order-pdf": e_fv_data["task_order-pdf"]}
SaveFinancialVerificationDraft(
TrueValidator, TrueValidator, user, request, data, is_extended=True
).execute()
form = GetFinancialVerificationForm(user, request, is_extended=True).execute()
assert form.task_order.pdf
def test_task_order_info_present_in_extended_form(fv_data, e_fv_data):
request = RequestFactory.create()
user = UserFactory.create()
data = {
"task_order-clin_0001": "1",
"task_order-number": fv_data["task_order-number"],
}
SaveFinancialVerificationDraft(
TrueValidator, TrueValidator, user, request, data, is_extended=True
).execute()
form = GetFinancialVerificationForm(user, request, is_extended=True).execute()
assert form.task_order.clin_0001.data
def test_update_ignores_empty_values(fv_data, e_fv_data):
request = RequestFactory.create()
user = UserFactory.create()
data = {**fv_data, **e_fv_data, "task_order-funding_type": ""}
SaveFinancialVerificationDraft(
TrueValidator, TrueValidator, user, request, data, is_extended=True
).execute()
def test_simple_form_does_not_generate_task_order(fv_data):
request = RequestFactory.create()
user = UserFactory.create()
data = pick(["uii_ids"], fv_data)
updated_request = SaveFinancialVerificationDraft(
TrueValidator, TrueValidator, user, request, data, is_extended=False
).execute()
assert updated_request.task_order is None
def test_can_save_draft_with_funding_type(fv_data, e_fv_data):
request = RequestFactory.create()
user = UserFactory.create()
data = {
"task_order-number": fv_data["task_order-number"],
"task_order-funding_type": e_fv_data["task_order-funding_type"],
}
updated_request = SaveFinancialVerificationDraft(
TrueValidator, TrueValidator, user, request, data, is_extended=False
).execute()
assert updated_request.task_order.funding_type
def test_update_fv_route(client, user_session, fv_data):
user = UserFactory.create()
request = RequestFactory.create(creator=user) request = RequestFactory.create(creator=user)
url_kwargs = {"request_id": request.id} user_session(user)
if extended:
url_kwargs["extended"] = True
response = client.post( response = client.post(
url_for("requests.financial_verification", **url_kwargs), url_for("requests.financial_verification", request_id=request.id),
data=data, data=fv_data,
follow_redirects=False, follow_redirects=False,
) )
return response
def test_submit_request_form_with_invalid_pe_id(self, monkeypatch, client):
user = self._set_monkeypatches(monkeypatch)
response = self.submit_data(client, user, self.required_data)
assert "We couldn&#39;t find that PE number" in response.data.decode()
assert response.status_code == 200 assert response.status_code == 200
def test_submit_request_form_with_unchanged_pe_id(self, monkeypatch, client):
user = self._set_monkeypatches(monkeypatch)
data = dict(self.required_data) def test_save_fv_draft_route(client, user_session, fv_data):
data["pe_id"] = "0101110F"
response = self.submit_data(client, user, data)
assert response.status_code == 302
assert "/workspaces" in response.headers.get("Location")
def test_submit_request_form_with_new_valid_pe_id(self, monkeypatch, client):
user = self._set_monkeypatches(monkeypatch)
pe = PENumberFactory.create(number="8675309U", description="sample PE number")
data = dict(self.required_data)
data["pe_id"] = pe.number
response = self.submit_data(client, user, data)
assert response.status_code == 302
assert "/workspaces" in response.headers.get("Location")
def test_submit_request_form_with_missing_pe_id(self, monkeypatch, client):
user = self._set_monkeypatches(monkeypatch)
data = dict(self.required_data)
data["pe_id"] = ""
response = self.submit_data(client, user, data)
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
):
user = UserFactory.create()
user_session(user)
data = dict(self.required_data)
data["pe_id"] = "0101110F"
data["task_order_number"] = "1234"
response = self.submit_data(client, user, data)
assert "extended=True" in response.data.decode()
def test_submit_financial_form_with_valid_task_order(
self, monkeypatch, user_session, client
):
user = UserFactory.create()
monkeypatch.setattr(
"atst.domain.requests.Requests.get", lambda *args: MOCK_REQUEST
)
user_session(user)
data = dict(self.required_data)
data["pe_id"] = "0101110F"
data["task_order_number"] = MockEDAClient.MOCK_CONTRACT_NUMBER
response = self.submit_data(client, user, data)
assert "enter TO information manually" not in response.data.decode()
def test_submit_extended_financial_form(
self, monkeypatch, user_session, client, extended_financial_verification_data
):
user = UserFactory.create() user = UserFactory.create()
request = RequestFactory.create(creator=user) request = RequestFactory.create(creator=user)
monkeypatch.setattr("atst.domain.requests.Requests.get", lambda *args: request)
monkeypatch.setattr("atst.forms.financial.validate_pe_id", lambda *args: True)
user_session(user) user_session(user)
data = {**self.required_data, **extended_financial_verification_data} response = client.post(
data["task_order_number"] = "1234567" url_for("requests.save_financial_verification_draft", request_id=request.id),
data=fv_data,
response = self.submit_data(client, user, data, extended=True) follow_redirects=False,
)
assert response.status_code == 302
assert "/requests" in response.headers.get("Location")
def test_submit_invalid_extended_financial_form(
self, monkeypatch, user_session, client, extended_financial_verification_data
):
monkeypatch.setattr("atst.forms.financial.validate_pe_id", lambda *args: True)
user = UserFactory.create()
user_session(user)
data = {**self.required_data, **extended_financial_verification_data}
data["task_order_number"] = "1234567"
del (data["clin_0001"])
response = self.submit_data(client, user, data, extended=True)
assert response.status_code == 200 assert response.status_code == 200
def test_displays_ccpo_review_comment(user_session, client): def test_get_fv_form_route(client, user_session, fv_data):
creator = UserFactory.create() user = UserFactory.create()
ccpo = UserFactory.from_atat_role("ccpo") request = RequestFactory.create(creator=user)
user_session(creator) user_session(user)
request = RequestFactory.create(creator=creator) response = client.get(
status = RequestStatusEventFactory.create( url_for("requests.financial_verification", request_id=request.id),
revision=request.latest_revision, data=fv_data,
new_status=RequestStatus.CHANGES_REQUESTED_TO_FINVER, follow_redirects=False,
request=request,
)
review_comment = "add all of the correct info, instead of the incorrect info"
RequestReviewFactory.create(reviewer=ccpo, comment=review_comment, status=status)
response = client.get("/requests/verify/{}".format(request.id))
body = response.data.decode()
assert review_comment in body
class TestFinancialVerification:
def _service_object(self, request=None, extended=False, post_data={}):
if not request:
self.request = RequestFactory.create()
else:
self.request = request
return FinancialVerification(
self.request, extended=extended, post_data=post_data
) )
def test_is_extended(self): assert response.status_code == 200
finver_one = self._service_object()
assert not finver_one.is_extended
finver_two = self._service_object(
request=RequestFactory.create_with_status(
RequestStatus.CHANGES_REQUESTED_TO_FINVER
)
)
assert finver_two.is_extended
finver_three = self._service_object(extended=True)
assert finver_three.is_extended
def test_is_pending_changes(self):
finver_one = self._service_object()
assert not finver_one.is_pending_changes
finver_two = self._service_object(
request=RequestFactory.create_with_status(
RequestStatus.CHANGES_REQUESTED_TO_FINVER
)
)
assert finver_two.is_pending_changes
def test_pending(self): def test_manual_task_order_triggers_extended_form(
finver_one = self._service_object() client, user_session, fv_data, e_fv_data
assert not finver_one.pending ):
finver_two = self._service_object( user = UserFactory.create()
request=RequestFactory.create_with_status( request = RequestFactory.create(creator=user)
RequestStatus.PENDING_CCPO_APPROVAL
data = {**fv_data, **e_fv_data, "task_order-number": MANUAL_TO_NUMBER}
UpdateFinancialVerification(
TrueValidator, TrueValidator, user, request, data, is_extended=True
).execute()
user_session(user)
response = client.get(
url_for("requests.financial_verification", request_id=request.id),
data=fv_data,
follow_redirects=False,
) )
assert "extended" in response.headers["Location"]
def test_manual_to_does_not_trigger_approval(client, user_session, fv_data, e_fv_data):
user = UserFactory.create()
request = RequestFactory.create(creator=user)
data = {
**fv_data,
**e_fv_data,
"task_order-number": MANUAL_TO_NUMBER,
"request-pe_id": "0101228N",
}
user_session(user)
client.post(
url_for(
"requests.financial_verification", request_id=request.id, extended=True
),
data=data,
follow_redirects=True,
) )
assert finver_two.pending
updated_request = RequestsQuery.get(request.id)
assert updated_request.status != RequestStatus.APPROVED
def test_eda_task_order_does_trigger_approval(client, user_session, fv_data, e_fv_data):
user = UserFactory.create()
request = RequestFactory.create(creator=user)
data = {
**fv_data,
**e_fv_data,
"task_order-number": MockEDAClient.MOCK_CONTRACT_NUMBER,
"request-pe_id": "0101228N",
}
user_session(user)
client.post(
url_for(
"requests.financial_verification", request_id=request.id, extended=True
),
data=data,
follow_redirects=True,
)
updated_request = RequestsQuery.get(request.id)
assert updated_request.status == RequestStatus.APPROVED
def test_task_order_number_persists_in_form(fv_data, e_fv_data):
user = UserFactory.create()
request = RequestFactory.create(creator=user)
data = {
**fv_data,
"task_order-number": MANUAL_TO_NUMBER,
"request-pe_id": "0101228N",
}
try:
UpdateFinancialVerification(
TrueValidator, FalseValidator, user, request, data, is_extended=False
).execute()
except FormValidationError:
pass
form = GetFinancialVerificationForm(user, request, is_extended=True).execute()
assert form.task_order.number.data == MANUAL_TO_NUMBER
def test_can_submit_once_to_details_are_entered(fv_data, e_fv_data):
user = UserFactory.create()
request = RequestFactory.create(creator=user)
data = {
**fv_data,
"task_order-number": MANUAL_TO_NUMBER,
"request-pe_id": "0101228N",
}
try:
UpdateFinancialVerification(
TrueValidator, FalseValidator, user, request, data, is_extended=False
).execute()
except FormValidationError:
pass
data = {
**fv_data,
**e_fv_data,
"task_order-number": MANUAL_TO_NUMBER,
"request-pe_id": "0101228N",
}
assert UpdateFinancialVerification(
TrueValidator, TrueValidator, user, request, data, is_extended=True
).execute()