Merge pull request #673 from dod-ccpo/eradicate-requests

Remove no-longer-used requests code
This commit is contained in:
patricksmithdds 2019-02-26 10:35:43 -05:00 committed by GitHub
commit 99d79cef79
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
93 changed files with 395 additions and 6623 deletions

View File

@ -0,0 +1,46 @@
"""Add indexes
Revision ID: 3777e9e39644
Revises: fa3ba4049218
Create Date: 2019-02-20 15:57:44.531311
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '3777e9e39644'
down_revision = 'fa3ba4049218'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_index(op.f('ix_audit_events_portfolio_id'), 'audit_events', ['portfolio_id'], unique=False)
op.drop_index('ix_audit_events_workspace_id', table_name='audit_events')
op.create_index(op.f('ix_invitations_portfolio_role_id'), 'invitations', ['portfolio_role_id'], unique=False)
op.drop_index('ix_invitations_workspace_role_id', table_name='invitations')
op.create_index(op.f('ix_portfolio_roles_portfolio_id'), 'portfolio_roles', ['portfolio_id'], unique=False)
op.create_index(op.f('ix_portfolio_roles_user_id'), 'portfolio_roles', ['user_id'], unique=False)
op.create_index('portfolio_role_user_portfolio', 'portfolio_roles', ['user_id', 'portfolio_id'], unique=True)
op.drop_index('ix_workspace_roles_user_id', table_name='portfolio_roles')
op.drop_index('ix_workspace_roles_workspace_id', table_name='portfolio_roles')
op.drop_index('workspace_role_user_workspace', table_name='portfolio_roles')
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_index('workspace_role_user_workspace', 'portfolio_roles', ['user_id', 'portfolio_id'], unique=True)
op.create_index('ix_workspace_roles_workspace_id', 'portfolio_roles', ['portfolio_id'], unique=False)
op.create_index('ix_workspace_roles_user_id', 'portfolio_roles', ['user_id'], unique=False)
op.drop_index('portfolio_role_user_portfolio', table_name='portfolio_roles')
op.drop_index(op.f('ix_portfolio_roles_user_id'), table_name='portfolio_roles')
op.drop_index(op.f('ix_portfolio_roles_portfolio_id'), table_name='portfolio_roles')
op.create_index('ix_invitations_workspace_role_id', 'invitations', ['portfolio_role_id'], unique=False)
op.drop_index(op.f('ix_invitations_portfolio_role_id'), table_name='invitations')
op.create_index('ix_audit_events_workspace_id', 'audit_events', ['portfolio_id'], unique=False)
op.drop_index(op.f('ix_audit_events_portfolio_id'), table_name='audit_events')
# ### end Alembic commands ###

View File

@ -0,0 +1,32 @@
"""Remove PE number model
Revision ID: 978bf56e21b6
Revises: c92cec2f32d4
Create Date: 2019-02-20 18:24:37.970323
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '978bf56e21b6'
down_revision = 'c92cec2f32d4'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('pe_numbers')
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('pe_numbers',
sa.Column('number', sa.VARCHAR(), autoincrement=False, nullable=False),
sa.Column('description', sa.VARCHAR(), autoincrement=False, nullable=True),
sa.PrimaryKeyConstraint('number', name='pe_numbers_pkey')
)
# ### end Alembic commands ###

View File

@ -0,0 +1,150 @@
"""Remove request related models
Revision ID: c92cec2f32d4
Revises: 3777e9e39644
Create Date: 2019-02-20 17:37:33.992269
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = 'c92cec2f32d4'
down_revision = '3777e9e39644'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('request_status_events')
op.drop_table('request_reviews')
op.drop_table('request_internal_comments')
op.drop_table('request_revisions')
op.drop_index('ix_audit_events_request_id', table_name='audit_events')
op.drop_constraint('audit_events_request_id_fkey', 'audit_events', type_='foreignkey')
op.drop_column('audit_events', 'request_id')
op.drop_constraint('workspaces_request_id_fkey', 'portfolios', type_='foreignkey')
op.drop_table('requests')
op.drop_column('portfolios', 'request_id')
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('portfolios', sa.Column('request_id', postgresql.UUID(), autoincrement=False, nullable=True))
op.add_column('audit_events', sa.Column('request_id', postgresql.UUID(), autoincrement=False, nullable=True))
op.create_table('requests',
sa.Column('time_updated', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), autoincrement=False, nullable=False),
sa.Column('id', postgresql.UUID(), server_default=sa.text('uuid_generate_v4()'), autoincrement=False, nullable=False),
sa.Column('time_created', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), autoincrement=False, nullable=True),
sa.Column('user_id', postgresql.UUID(), autoincrement=False, nullable=False),
sa.Column('legacy_task_order_id', postgresql.UUID(), autoincrement=False, nullable=True),
sa.ForeignKeyConstraint(['legacy_task_order_id'], ['legacy_task_orders.id'], name='requests_legacy_task_order_fkey'),
sa.ForeignKeyConstraint(['user_id'], ['users.id'], name='requests_user_id_fkey'),
sa.PrimaryKeyConstraint('id', name='requests_pkey'),
postgresql_ignore_search_path=False
)
op.create_index('ix_audit_events_request_id', 'audit_events', ['request_id'], unique=False)
op.create_table('request_revisions',
sa.Column('time_created', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), autoincrement=False, nullable=False),
sa.Column('time_updated', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), autoincrement=False, nullable=False),
sa.Column('id', postgresql.UUID(), server_default=sa.text('uuid_generate_v4()'), autoincrement=False, nullable=False),
sa.Column('request_id', postgresql.UUID(), autoincrement=False, nullable=False),
sa.Column('sequence', sa.BIGINT(), autoincrement=False, nullable=False),
sa.Column('am_poc', sa.BOOLEAN(), autoincrement=False, nullable=True),
sa.Column('dodid_poc', sa.VARCHAR(), autoincrement=False, nullable=True),
sa.Column('email_poc', sa.VARCHAR(), autoincrement=False, nullable=True),
sa.Column('fname_poc', sa.VARCHAR(), autoincrement=False, nullable=True),
sa.Column('lname_poc', sa.VARCHAR(), autoincrement=False, nullable=True),
sa.Column('jedi_usage', sa.VARCHAR(), autoincrement=False, nullable=True),
sa.Column('start_date', sa.DATE(), autoincrement=False, nullable=True),
sa.Column('cloud_native', sa.VARCHAR(), autoincrement=False, nullable=True),
sa.Column('dollar_value', sa.INTEGER(), autoincrement=False, nullable=True),
sa.Column('dod_component', sa.VARCHAR(), autoincrement=False, nullable=True),
sa.Column('data_transfers', sa.VARCHAR(), autoincrement=False, nullable=True),
sa.Column('expected_completion_date', sa.VARCHAR(), autoincrement=False, nullable=True),
sa.Column('jedi_migration', sa.VARCHAR(), autoincrement=False, nullable=True),
sa.Column('num_software_systems', sa.INTEGER(), autoincrement=False, nullable=True),
sa.Column('number_user_sessions', sa.INTEGER(), autoincrement=False, nullable=True),
sa.Column('average_daily_traffic', sa.INTEGER(), autoincrement=False, nullable=True),
sa.Column('engineering_assessment', sa.VARCHAR(), autoincrement=False, nullable=True),
sa.Column('technical_support_team', sa.VARCHAR(), autoincrement=False, nullable=True),
sa.Column('estimated_monthly_spend', sa.INTEGER(), autoincrement=False, nullable=True),
sa.Column('average_daily_traffic_gb', sa.INTEGER(), autoincrement=False, nullable=True),
sa.Column('rationalization_software_systems', sa.VARCHAR(), autoincrement=False, nullable=True),
sa.Column('organization_providing_assistance', sa.VARCHAR(), autoincrement=False, nullable=True),
sa.Column('name', sa.VARCHAR(), autoincrement=False, nullable=True),
sa.Column('citizenship', sa.VARCHAR(), autoincrement=False, nullable=True),
sa.Column('designation', sa.VARCHAR(), autoincrement=False, nullable=True),
sa.Column('phone_number', sa.VARCHAR(), autoincrement=False, nullable=True),
sa.Column('email_request', sa.VARCHAR(), autoincrement=False, nullable=True),
sa.Column('fname_request', sa.VARCHAR(), autoincrement=False, nullable=True),
sa.Column('lname_request', sa.VARCHAR(), autoincrement=False, nullable=True),
sa.Column('service_branch', sa.VARCHAR(), autoincrement=False, nullable=True),
sa.Column('date_latest_training', sa.DATE(), autoincrement=False, nullable=True),
sa.Column('pe_id', sa.VARCHAR(), autoincrement=False, nullable=True),
sa.Column('task_order_number', sa.VARCHAR(), autoincrement=False, nullable=True),
sa.Column('fname_co', sa.VARCHAR(), autoincrement=False, nullable=True),
sa.Column('lname_co', sa.VARCHAR(), autoincrement=False, nullable=True),
sa.Column('email_co', sa.VARCHAR(), autoincrement=False, nullable=True),
sa.Column('office_co', sa.VARCHAR(), autoincrement=False, nullable=True),
sa.Column('fname_cor', sa.VARCHAR(), autoincrement=False, nullable=True),
sa.Column('lname_cor', sa.VARCHAR(), autoincrement=False, nullable=True),
sa.Column('email_cor', sa.VARCHAR(), autoincrement=False, nullable=True),
sa.Column('office_cor', sa.VARCHAR(), autoincrement=False, nullable=True),
sa.Column('uii_ids', postgresql.ARRAY(sa.VARCHAR()), autoincrement=False, nullable=True),
sa.Column('treasury_code', sa.VARCHAR(), autoincrement=False, nullable=True),
sa.Column('ba_code', sa.VARCHAR(), autoincrement=False, nullable=True),
sa.Column('phone_ext', sa.VARCHAR(), autoincrement=False, nullable=True),
sa.ForeignKeyConstraint(['request_id'], ['requests.id'], name='request_revisions_request_id_fkey'),
sa.PrimaryKeyConstraint('id', name='request_revisions_pkey'),
postgresql_ignore_search_path=False
)
op.create_table('request_internal_comments',
sa.Column('time_created', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), autoincrement=False, nullable=False),
sa.Column('time_updated', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), autoincrement=False, nullable=False),
sa.Column('id', postgresql.UUID(), server_default=sa.text('uuid_generate_v4()'), autoincrement=False, nullable=False),
sa.Column('text', sa.VARCHAR(), autoincrement=False, nullable=False),
sa.Column('user_id', postgresql.UUID(), autoincrement=False, nullable=False),
sa.Column('request_id', postgresql.UUID(), autoincrement=False, nullable=False),
sa.ForeignKeyConstraint(['request_id'], ['requests.id'], name='request_internal_comments_request_id_fkey', ondelete='CASCADE'),
sa.ForeignKeyConstraint(['user_id'], ['users.id'], name='request_internal_comments_user_id_fkey'),
sa.PrimaryKeyConstraint('id', name='request_internal_comments_pkey')
)
op.create_table('request_reviews',
sa.Column('time_created', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), autoincrement=False, nullable=False),
sa.Column('time_updated', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), autoincrement=False, nullable=False),
sa.Column('id', postgresql.UUID(), server_default=sa.text('uuid_generate_v4()'), autoincrement=False, nullable=False),
sa.Column('user_id', postgresql.UUID(), autoincrement=False, nullable=True),
sa.Column('comment', sa.VARCHAR(), autoincrement=False, nullable=True),
sa.Column('fname_mao', sa.VARCHAR(), autoincrement=False, nullable=True),
sa.Column('lname_mao', sa.VARCHAR(), autoincrement=False, nullable=True),
sa.Column('email_mao', sa.VARCHAR(), autoincrement=False, nullable=True),
sa.Column('phone_mao', sa.VARCHAR(), autoincrement=False, nullable=True),
sa.Column('fname_ccpo', sa.VARCHAR(), autoincrement=False, nullable=True),
sa.Column('lname_ccpo', sa.VARCHAR(), autoincrement=False, nullable=True),
sa.Column('phone_ext_mao', sa.VARCHAR(), autoincrement=False, nullable=True),
sa.ForeignKeyConstraint(['user_id'], ['users.id'], name='request_reviews_user_id_fkey'),
sa.PrimaryKeyConstraint('id', name='request_reviews_pkey'),
postgresql_ignore_search_path=False
)
op.create_table('request_status_events',
sa.Column('time_updated', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), autoincrement=False, nullable=False),
sa.Column('id', postgresql.UUID(), server_default=sa.text('uuid_generate_v4()'), autoincrement=False, nullable=False),
sa.Column('new_status', sa.VARCHAR(length=30), autoincrement=False, nullable=True),
sa.Column('time_created', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), autoincrement=False, nullable=False),
sa.Column('request_id', postgresql.UUID(), autoincrement=False, nullable=False),
sa.Column('sequence', sa.BIGINT(), autoincrement=False, nullable=False),
sa.Column('request_revision_id', postgresql.UUID(), autoincrement=False, nullable=False),
sa.Column('request_review_id', postgresql.UUID(), autoincrement=False, nullable=True),
sa.CheckConstraint("(new_status)::text = ANY ((ARRAY['STARTED'::character varying, 'SUBMITTED'::character varying, 'PENDING_FINANCIAL_VERIFICATION'::character varying, 'PENDING_CCPO_ACCEPTANCE'::character varying, 'PENDING_CCPO_APPROVAL'::character varying, 'CHANGES_REQUESTED'::character varying, 'CHANGES_REQUESTED_TO_FINVER'::character varying, 'APPROVED'::character varying, 'EXPIRED'::character varying, 'DELETED'::character varying])::text[])", name='requeststatus'),
sa.ForeignKeyConstraint(['request_id'], ['requests.id'], name='request_status_events_request_id_fkey', ondelete='CASCADE'),
sa.ForeignKeyConstraint(['request_review_id'], ['request_reviews.id'], name='request_status_events_request_review_id_fkey'),
sa.ForeignKeyConstraint(['request_revision_id'], ['request_revisions.id'], name='request_status_events_request_revision_id_fkey'),
sa.PrimaryKeyConstraint('id', name='request_status_events_pkey')
)
op.create_foreign_key('workspaces_request_id_fkey', 'portfolios', 'requests', ['request_id'], ['id'])
op.create_foreign_key('audit_events_request_id_fkey', 'audit_events', 'requests', ['request_id'], ['id'])
# ### end Alembic commands ###

View File

@ -0,0 +1,49 @@
"""Remove legacy task order table
Revision ID: fb22e47972a3
Revises: 978bf56e21b6
Create Date: 2019-02-20 18:28:56.386152
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = 'fb22e47972a3'
down_revision = '978bf56e21b6'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('legacy_task_orders')
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('legacy_task_orders',
sa.Column('time_created', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), autoincrement=False, nullable=False),
sa.Column('time_updated', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), autoincrement=False, nullable=False),
sa.Column('id', postgresql.UUID(), server_default=sa.text('uuid_generate_v4()'), autoincrement=False, nullable=False),
sa.Column('number', sa.VARCHAR(), autoincrement=False, nullable=True),
sa.Column('source', sa.VARCHAR(length=6), autoincrement=False, nullable=True),
sa.Column('funding_type', sa.VARCHAR(length=5), autoincrement=False, nullable=True),
sa.Column('funding_type_other', sa.VARCHAR(), autoincrement=False, nullable=True),
sa.Column('clin_0001', sa.INTEGER(), autoincrement=False, nullable=True),
sa.Column('clin_0003', sa.INTEGER(), autoincrement=False, nullable=True),
sa.Column('clin_1001', sa.INTEGER(), autoincrement=False, nullable=True),
sa.Column('clin_1003', sa.INTEGER(), autoincrement=False, nullable=True),
sa.Column('clin_2001', sa.INTEGER(), autoincrement=False, nullable=True),
sa.Column('clin_2003', sa.INTEGER(), autoincrement=False, nullable=True),
sa.Column('expiration_date', sa.DATE(), autoincrement=False, nullable=True),
sa.Column('attachment_id', postgresql.UUID(), autoincrement=False, nullable=True),
sa.CheckConstraint("(funding_type)::text = ANY ((ARRAY['RDTE'::character varying, 'OM'::character varying, 'PROC'::character varying, 'OTHER'::character varying])::text[])", name='fundingtype'),
sa.CheckConstraint("(source)::text = ANY ((ARRAY['MANUAL'::character varying, 'EDA'::character varying])::text[])", name='source'),
sa.ForeignKeyConstraint(['attachment_id'], ['attachments.id'], name='task_orders_attachment_id_fkey'),
sa.PrimaryKeyConstraint('id', name='task_orders_pkey'),
sa.UniqueConstraint('number', name='task_orders_number_key')
)
# ### end Alembic commands ###

View File

@ -13,7 +13,6 @@ from atst.assets import environment as assets_environment
from atst.filters import register_filters from atst.filters import register_filters
from atst.routes import bp from atst.routes import bp
from atst.routes.portfolios import portfolios_bp as portfolio_routes from atst.routes.portfolios import portfolios_bp as portfolio_routes
from atst.routes.requests import requests_bp
from atst.routes.task_orders import task_orders_bp from atst.routes.task_orders import task_orders_bp
from atst.routes.dev import bp as dev_routes from atst.routes.dev import bp as dev_routes
from atst.routes.users import bp as user_routes from atst.routes.users import bp as user_routes
@ -68,7 +67,6 @@ def make_app(config):
app.register_blueprint(portfolio_routes) app.register_blueprint(portfolio_routes)
app.register_blueprint(task_orders_bp) app.register_blueprint(task_orders_bp)
app.register_blueprint(user_routes) app.register_blueprint(user_routes)
app.register_blueprint(requests_bp)
if ENV != "prod": if ENV != "prod":
app.register_blueprint(dev_routes) app.register_blueprint(dev_routes)

View File

@ -1,4 +1,3 @@
import datetime
from itertools import groupby from itertools import groupby
from collections import OrderedDict from collections import OrderedDict
import pendulum import pendulum
@ -33,10 +32,10 @@ class MockApplication:
def generate_sample_dates(_max=8): def generate_sample_dates(_max=8):
current = datetime.datetime.today() current = pendulum.now()
sample_dates = [] sample_dates = []
for _i in range(_max): for _i in range(_max):
current = current - datetime.timedelta(days=29) current = current.subtract(months=1)
sample_dates.append(current.strftime("%m/%Y")) sample_dates.append(current.strftime("%m/%Y"))
reversed(sample_dates) reversed(sample_dates)
@ -225,8 +224,6 @@ class MockReportingProvider(ReportingInterface):
def get_budget(self, portfolio): def get_budget(self, portfolio):
if portfolio.name in self.REPORT_FIXTURE_MAP: if portfolio.name in self.REPORT_FIXTURE_MAP:
return self.REPORT_FIXTURE_MAP[portfolio.name]["budget"] return self.REPORT_FIXTURE_MAP[portfolio.name]["budget"]
elif portfolio.request and portfolio.legacy_task_order:
return portfolio.legacy_task_order.budget
return 0 return 0
def get_total_spending(self, portfolio): def get_total_spending(self, portfolio):

View File

@ -1,60 +0,0 @@
from sqlalchemy.orm.exc import NoResultFound
from flask import current_app as app
from atst.database import db
from atst.models.legacy_task_order import LegacyTaskOrder, Source, FundingType
from .exceptions import NotFoundError
from atst.utils import update_obj
class LegacyTaskOrders(object):
TASK_ORDER_DATA = [
col.name for col in LegacyTaskOrder.__table__.c if col.name != "id"
]
@classmethod
def get(cls, order_number):
try:
legacy_task_order = (
db.session.query(LegacyTaskOrder).filter_by(number=order_number).one()
)
except NoResultFound:
if LegacyTaskOrders._client():
legacy_task_order = LegacyTaskOrders.get_from_eda(order_number)
else:
raise NotFoundError("legacy_task_order")
return legacy_task_order
@classmethod
def get_from_eda(cls, order_number):
to_data = LegacyTaskOrders._client().get_contract(order_number, status="y")
if to_data:
# TODO: we need to determine exactly what we're getting and storing from the EDA client
return LegacyTaskOrders.create(
source=Source.EDA, funding_type=FundingType.PROC, **to_data
)
else:
raise NotFoundError("legacy_task_order")
@classmethod
def create(cls, source=Source.MANUAL, **kwargs):
to_data = {k: v for k, v in kwargs.items() if v not in ["", None]}
legacy_task_order = LegacyTaskOrder(source=source, **to_data)
db.session.add(legacy_task_order)
db.session.commit()
return legacy_task_order
@classmethod
def _client(cls):
return app.eda_client
@classmethod
def update(cls, legacy_task_order, dct):
updated = update_obj(legacy_task_order, dct, ignore_vals=["", None])
db.session.add(updated)
db.session.commit()
return updated

View File

@ -1,24 +0,0 @@
from sqlalchemy.dialects.postgresql import insert
from atst.database import db
from atst.models.pe_number import PENumber
from .exceptions import NotFoundError
class PENumbers(object):
@classmethod
def get(cls, number):
pe_number = db.session.query(PENumber).get(number)
if not pe_number:
raise NotFoundError("pe_number")
return pe_number
@classmethod
def create_many(cls, list_of_pe_numbers):
stmt = insert(PENumber).values(list_of_pe_numbers)
do_update = stmt.on_conflict_do_update(
index_elements=["number"], set_=dict(description=stmt.excluded.description)
)
db.session.execute(do_update)
db.session.commit()

View File

@ -24,16 +24,6 @@ class Portfolios(object):
PortfoliosQuery.add_and_commit(portfolio) PortfoliosQuery.add_and_commit(portfolio)
return portfolio return portfolio
@classmethod
def create_from_request(cls, request, name=None):
name = name or request.displayname
portfolio = PortfoliosQuery.create(request=request, name=name)
Portfolios._create_portfolio_role(
request.creator, portfolio, "owner", status=PortfolioRoleStatus.ACTIVE
)
PortfoliosQuery.add_and_commit(portfolio)
return portfolio
@classmethod @classmethod
def get(cls, user, portfolio_id): def get(cls, user, portfolio_id):
portfolio = PortfoliosQuery.get(portfolio_id) portfolio = PortfoliosQuery.get(portfolio_id)
@ -76,10 +66,6 @@ class Portfolios(object):
return portfolio return portfolio
@classmethod
def get_by_request(cls, request):
return PortfoliosQuery.get_by_request(request)
@classmethod @classmethod
def get_with_members(cls, user, portfolio_id): def get_with_members(cls, user, portfolio_id):
portfolio = PortfoliosQuery.get(portfolio_id) portfolio = PortfoliosQuery.get(portfolio_id)

View File

@ -1,8 +1,5 @@
from sqlalchemy.orm.exc import NoResultFound
from atst.database import db from atst.database import db
from atst.domain.common import Query from atst.domain.common import Query
from atst.domain.exceptions import NotFoundError
from atst.models.portfolio import Portfolio from atst.models.portfolio import Portfolio
from atst.models.portfolio_role import PortfolioRole, Status as PortfolioRoleStatus from atst.models.portfolio_role import PortfolioRole, Status as PortfolioRoleStatus
@ -10,15 +7,6 @@ from atst.models.portfolio_role import PortfolioRole, Status as PortfolioRoleSta
class PortfoliosQuery(Query): class PortfoliosQuery(Query):
model = Portfolio model = Portfolio
@classmethod
def get_by_request(cls, request):
try:
portfolio = db.session.query(Portfolio).filter_by(request=request).one()
except NoResultFound:
raise NotFoundError("portfolio")
return portfolio
@classmethod @classmethod
def get_for_user(cls, user): def get_for_user(cls, user):
return ( return (

View File

@ -1 +0,0 @@
from .requests import Requests, create_revision_from_request_body

View File

@ -1,29 +0,0 @@
from atst.models.permissions import Permissions
from atst.domain.authz import Authorization
from atst.domain.exceptions import UnauthorizedError
class RequestsAuthorization(object):
def __init__(self, user, request):
self.user = user
self.request = request
@property
def can_view(self):
return (
Authorization.has_atat_permission(
self.user, Permissions.REVIEW_AND_APPROVE_JEDI_PORTFOLIO_REQUEST
)
or self.request.creator == self.user
)
def check_can_view(self, message):
if not self.can_view:
raise UnauthorizedError(self.user, message)
def check_can_approve(self):
return Authorization.check_atat_permission(
self.user,
Permissions.REVIEW_AND_APPROVE_JEDI_PORTFOLIO_REQUEST,
"cannot review and approve requests",
)

View File

@ -1,74 +0,0 @@
import re
from atst.domain.legacy_task_orders import LegacyTaskOrders
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:
LegacyTaskOrders.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,73 +0,0 @@
from sqlalchemy import exists, and_, exc, text
from sqlalchemy.orm.exc import NoResultFound
from atst.database import db
from atst.domain.common import Query
from atst.models.request import Request
from atst.domain.exceptions import NotFoundError
class RequestsQuery(Query):
model = Request
@classmethod
def exists(cls, request_id, creator):
try:
return db.session.query(
exists().where(
and_(Request.id == request_id, Request.creator == creator)
)
).scalar()
except exc.DataError:
return False
@classmethod
def get_many(cls, creator=None):
filters = []
if creator:
filters.append(Request.creator == creator)
requests = (
db.session.query(Request)
.filter(*filters)
.order_by(Request.time_created.desc())
.all()
)
return requests
@classmethod
def get_with_lock(cls, request_id):
try:
# Query for request matching id, acquiring a row-level write lock.
# https://www.postgresql.org/docs/10/static/sql-select.html#SQL-FOR-UPDATE-SHARE
return (
db.session.query(Request)
.filter_by(id=request_id)
.with_for_update(of=Request)
.one()
)
except NoResultFound:
raise NotFoundError("requests")
@classmethod
def status_count(cls, status, creator=None):
bindings = {"status": status.name}
raw = """
SELECT count(requests_with_status.id)
FROM (
SELECT DISTINCT ON (rse.request_id) r.*, rse.new_status as status
FROM request_status_events rse JOIN requests r ON r.id = rse.request_id
ORDER BY rse.request_id, rse.sequence DESC
) as requests_with_status
WHERE requests_with_status.status = :status
"""
if creator:
raw += " AND requests_with_status.user_id = :user_id"
bindings["user_id"] = creator.id
results = db.session.execute(text(raw), bindings).fetchone()
(count,) = results
return count

View File

@ -1,239 +0,0 @@
import dateutil
from atst.domain.portfolios import Portfolios
from atst.models.request_revision import RequestRevision
from atst.models.request_status_event import RequestStatusEvent, RequestStatus
from atst.models.request_review import RequestReview
from atst.models.request_internal_comment import RequestInternalComment
from atst.utils import deep_merge
from atst.queue import queue
from atst.filters import dollars
from .query import RequestsQuery
from .authorization import RequestsAuthorization
from .status_event_handler import RequestStatusEventHandler
def create_revision_from_request_body(body):
body = {k: v for p in body.values() for k, v in p.items()}
DATES = ["start_date", "date_latest_training"]
coerced_timestamps = {
k: dateutil.parser.parse(v)
for k, v in body.items()
if k in DATES and isinstance(v, str)
}
body = {**body, **coerced_timestamps}
return RequestRevision(**body)
class Requests(object):
AUTO_ACCEPT_THRESHOLD = 1_000_000
ANNUAL_SPEND_THRESHOLD = 1_000_000
@classmethod
def create(cls, creator, body):
revision = create_revision_from_request_body(body)
request = RequestsQuery.create(creator=creator, revisions=[revision])
request = Requests.set_status(request, RequestStatus.STARTED)
request = RequestsQuery.add_and_commit(request)
return request
@classmethod
def exists(cls, request_id, creator):
return RequestsQuery.exists(request_id, creator)
@classmethod
def get(cls, user, request_id):
request = RequestsQuery.get(request_id)
RequestsAuthorization(user, request).check_can_view("get request")
return request
@classmethod
def get_for_approval(cls, user, request_id):
request = RequestsQuery.get(request_id)
RequestsAuthorization(user, request).check_can_approve()
return request
@classmethod
def get_many(cls, creator=None):
return RequestsQuery.get_many(creator)
@classmethod
def submit(cls, request):
request = Requests.set_status(request, RequestStatus.SUBMITTED)
if Requests.should_auto_accept(request):
request = Requests.set_status(
request, RequestStatus.PENDING_FINANCIAL_VERIFICATION
)
Requests._add_review(
user=None,
request=request,
review_data={
"comment": "Auto-acceptance for dollar value below {}".format(
dollars(Requests.AUTO_ACCEPT_THRESHOLD)
)
},
)
else:
request = Requests.set_status(
request, RequestStatus.PENDING_CCPO_ACCEPTANCE
)
request = RequestsQuery.add_and_commit(request)
return request
@classmethod
def update(cls, request_id, request_delta):
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)
revision = create_revision_from_request_body(new_body)
request.revisions.append(revision)
return RequestsQuery.add_and_commit(request)
@classmethod
def approve_and_create_portfolio(cls, request):
approved_request = Requests.set_status(request, RequestStatus.APPROVED)
portfolio = Portfolios.create_from_request(approved_request)
RequestsQuery.add_and_commit(approved_request)
return portfolio
@classmethod
def auto_approve_and_create_portfolio(
cls,
request,
reason="Financial verification information found in Electronic Document Access API",
):
portfolio = Requests.approve_and_create_portfolio(request)
Requests._add_review(
user=None, request=request, review_data={"comment": reason}
)
return portfolio
@classmethod
def set_status(cls, request, status: RequestStatus):
old_status = request.status
status_event = RequestStatusEvent(
new_status=status, revision=request.latest_revision
)
request.status_events.append(status_event)
updated_request = RequestsQuery.add_and_commit(request)
RequestStatusEventHandler(queue).handle_status_change(
updated_request, old_status, status
)
return updated_request
@classmethod
def should_auto_accept(cls, request):
try:
dollar_value = request.body["details_of_use"]["dollar_value"]
except KeyError:
return False
return dollar_value < cls.AUTO_ACCEPT_THRESHOLD
_VALID_SUBMISSION_STATUSES = [
RequestStatus.STARTED,
RequestStatus.CHANGES_REQUESTED,
]
@classmethod
def should_allow_submission(cls, request):
all_request_sections = [
"details_of_use",
"information_about_you",
"primary_poc",
]
existing_request_sections = request.body.keys()
return request.status in Requests._VALID_SUBMISSION_STATUSES and all(
section in existing_request_sections for section in all_request_sections
)
@classmethod
def status_count(cls, status, creator=None):
return RequestsQuery.status_count(status, creator)
@classmethod
def in_progress_count(cls):
return sum(
[
Requests.status_count(RequestStatus.STARTED),
Requests.status_count(RequestStatus.PENDING_FINANCIAL_VERIFICATION),
Requests.status_count(RequestStatus.CHANGES_REQUESTED),
]
)
@classmethod
def pending_ccpo_count(cls):
return sum(
[
Requests.status_count(RequestStatus.PENDING_CCPO_ACCEPTANCE),
Requests.status_count(RequestStatus.PENDING_CCPO_APPROVAL),
]
)
@classmethod
def completed_count(cls):
return Requests.status_count(RequestStatus.APPROVED)
@classmethod
def update_financial_verification(
cls, request_id, financial_data, legacy_task_order=None
):
request = RequestsQuery.get_with_lock(request_id)
if legacy_task_order:
request.legacy_task_order = legacy_task_order
request = Requests._update(request, {"financial_verification": financial_data})
return request
@classmethod
def submit_financial_verification(cls, request):
request = Requests.set_status(request, RequestStatus.PENDING_CCPO_APPROVAL)
request = RequestsQuery.add_and_commit(request)
return request
@classmethod
def _add_review(cls, user=None, request=None, review_data=None):
request.latest_status.review = RequestReview(reviewer=user, **review_data)
request = RequestsQuery.add_and_commit(request)
return request
@classmethod
def advance(cls, user, request, review_data):
if request.status == RequestStatus.PENDING_CCPO_ACCEPTANCE:
Requests.set_status(request, RequestStatus.PENDING_FINANCIAL_VERIFICATION)
elif request.status == RequestStatus.PENDING_CCPO_APPROVAL:
Requests.approve_and_create_portfolio(request)
return Requests._add_review(user=user, request=request, review_data=review_data)
@classmethod
def request_changes(cls, user, request, review_data):
if request.status == RequestStatus.PENDING_CCPO_ACCEPTANCE:
Requests.set_status(request, RequestStatus.CHANGES_REQUESTED)
elif request.status == RequestStatus.PENDING_CCPO_APPROVAL:
Requests.set_status(request, RequestStatus.CHANGES_REQUESTED_TO_FINVER)
return Requests._add_review(user=user, request=request, review_data=review_data)
@classmethod
def add_internal_comment(cls, user, request, comment_text):
RequestsAuthorization(user, request).check_can_approve()
comment = RequestInternalComment(request=request, text=comment_text, user=user)
RequestsQuery.add_and_commit(comment)
return request
@classmethod
def possible_statuses(cls):
return [s[1].value for s in RequestStatus.__members__.items()]

View File

@ -1,35 +0,0 @@
from flask import render_template
from atst.models.request_status_event import RequestStatus
class RequestStatusEventHandler(object):
STATUS_TRANSITIONS = set(
[
(
RequestStatus.PENDING_CCPO_ACCEPTANCE,
RequestStatus.PENDING_FINANCIAL_VERIFICATION,
),
(RequestStatus.PENDING_CCPO_ACCEPTANCE, RequestStatus.CHANGES_REQUESTED),
(
RequestStatus.PENDING_CCPO_APPROVAL,
RequestStatus.CHANGES_REQUESTED_TO_FINVER,
),
(RequestStatus.PENDING_CCPO_APPROVAL, RequestStatus.APPROVED),
]
)
def __init__(self, queue):
self.queue = queue
def handle_status_change(self, request, old_status, new_status):
if (old_status, new_status) in self.STATUS_TRANSITIONS:
self._send_email(request)
def _send_email(self, request):
email_body = render_template(
"emails/request_status_change.txt", request=request
)
self.queue.send_mail(
[request.creator.email], "Your JEDI request status has changed", email_body
)

View File

@ -36,23 +36,6 @@ def usPhone(number):
return "+1 ({}) {} - {}".format(phone[0:3], phone[3:6], phone[6:]) return "+1 ({}) {} - {}".format(phone[0:3], phone[3:6], phone[6:])
def readableInteger(value):
try:
numberValue = int(value)
except ValueError:
numberValue = 0
return "{:,}".format(numberValue)
def getOptionLabel(value, options):
if hasattr(value, "value"):
value = value.name
try:
return next(tup[1] for tup in options if tup[0] == value) # pragma: no branch
except StopIteration:
return
def findFilter(value, filter_name, filter_args=[]): def findFilter(value, filter_name, filter_args=[]):
if not filter_name: if not filter_name:
return value return value
@ -62,10 +45,6 @@ def findFilter(value, filter_name, filter_args=[]):
raise ValueError("filter name {} not found".format(filter_name)) raise ValueError("filter name {} not found".format(filter_name))
def renderList(value):
return app.jinja_env.filters["safe"]("<br>".join(value))
def formattedDate(value, formatter="%m/%d/%Y"): def formattedDate(value, formatter="%m/%d/%Y"):
if value: if value:
return value.strftime(formatter) return value.strftime(formatter)
@ -95,11 +74,6 @@ def renderAuditEvent(event):
return render_template("audit_log/events/default.html", event=event) return render_template("audit_log/events/default.html", event=event)
def removeHtml(text):
html_tags = re.compile("<.*?>")
return re.sub(html_tags, "", text)
def normalizeOrder(title): def normalizeOrder(title):
# reorders titles from "Army, Department of the" to "Department of the Army" # reorders titles from "Army, Department of the" to "Department of the Army"
text = title.split(", ") text = title.split(", ")
@ -114,15 +88,11 @@ def register_filters(app):
app.jinja_env.filters["justDollars"] = justDollars app.jinja_env.filters["justDollars"] = justDollars
app.jinja_env.filters["justCents"] = justCents app.jinja_env.filters["justCents"] = justCents
app.jinja_env.filters["usPhone"] = usPhone app.jinja_env.filters["usPhone"] = usPhone
app.jinja_env.filters["readableInteger"] = readableInteger
app.jinja_env.filters["getOptionLabel"] = getOptionLabel
app.jinja_env.filters["findFilter"] = findFilter app.jinja_env.filters["findFilter"] = findFilter
app.jinja_env.filters["renderList"] = renderList
app.jinja_env.filters["formattedDate"] = formattedDate app.jinja_env.filters["formattedDate"] = formattedDate
app.jinja_env.filters["dateFromString"] = dateFromString app.jinja_env.filters["dateFromString"] = dateFromString
app.jinja_env.filters["pageWindow"] = pageWindow app.jinja_env.filters["pageWindow"] = pageWindow
app.jinja_env.filters["renderAuditEvent"] = renderAuditEvent app.jinja_env.filters["renderAuditEvent"] = renderAuditEvent
app.jinja_env.filters["removeHtml"] = removeHtml
app.jinja_env.filters["normalizeOrder"] = normalizeOrder app.jinja_env.filters["normalizeOrder"] = normalizeOrder
app.jinja_env.filters["translateDuration"] = translate_duration app.jinja_env.filters["translateDuration"] = translate_duration

View File

@ -1,29 +1,4 @@
from wtforms.fields import Field, FormField, StringField, SelectField as SelectField_ from wtforms.fields import FormField, SelectField as SelectField_
from wtforms.widgets import TextArea
class NewlineListField(Field):
widget = TextArea()
def _value(self):
if isinstance(self.data, list):
return "\n".join(self.data)
elif self.data:
return self.data
else:
return ""
def process_formdata(self, valuelist):
if valuelist:
self.data = [l.strip() for l in valuelist[0].split("\n") if l]
else:
self.data = []
def process_data(self, value):
if isinstance(value, list):
self.data = "\n".join(value)
else:
self.data = value
class SelectField(SelectField_): class SelectField(SelectField_):
@ -33,14 +8,6 @@ class SelectField(SelectField_):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
class NumberStringField(StringField):
def process_data(self, value):
if isinstance(value, int):
self.data = str(value)
else:
self.data = value
class FormFieldWrapper(FormField): class FormFieldWrapper(FormField):
def has_changes(self): def has_changes(self):
if not self.object_data: if not self.object_data:

View File

@ -1,248 +0,0 @@
import re
import pendulum
from wtforms.fields.html5 import DateField, EmailField
from wtforms.fields import StringField, FileField, FormField
from wtforms.validators import InputRequired, Email, Regexp, Optional
from flask_wtf.file import FileAllowed
from werkzeug.datastructures import FileStorage
from .fields import NewlineListField, SelectField, NumberStringField
from atst.forms.forms import CacheableForm
from atst.utils.localization import translate
from .data import FUNDING_TYPES
from .validators import DateRange
TREASURY_CODE_REGEX = re.compile(r"^0*([1-9]{4}|[1-9]{6})$")
BA_CODE_REGEX = re.compile(r"[0-9]{2}\w?$")
def number_to_int(num):
if num:
return int(num)
def coerce_choice(val):
if val is None:
return None
elif isinstance(val, str):
return val
else:
return val.value
class TaskOrderForm(CacheableForm):
def do_validate_number(self):
for field in self:
if field.name != "legacy_task_order-number":
field.validators.insert(0, Optional())
valid = super().validate()
for field in self:
if field.name != "legacy_task_order-number":
field.validators.pop(0)
return valid
number = StringField(
translate("forms.financial.number_label"),
description=translate("forms.financial.number_description"),
validators=[InputRequired()],
)
funding_type = SelectField(
description=translate("forms.financial.funding_type_description"),
choices=FUNDING_TYPES,
validators=[InputRequired()],
coerce=coerce_choice,
render_kw={"required": False},
)
funding_type_other = StringField(
translate("forms.financial.funding_type_other_label")
)
expiration_date = DateField(
translate("forms.financial.expiration_date_label"),
description=translate("forms.financial.expiration_date_description"),
validators=[
InputRequired(),
DateRange(
lower_bound=pendulum.duration(days=0),
upper_bound=pendulum.duration(years=100),
message="Must be a date in the future.",
),
],
format="%m/%d/%Y",
)
clin_0001 = NumberStringField(
translate("forms.financial.clin_0001_label"),
validators=[InputRequired()],
description=translate("forms.financial.clin_0001_description"),
filters=[number_to_int],
)
clin_0003 = NumberStringField(
translate("forms.financial.clin_0003_label"),
validators=[InputRequired()],
description=translate("forms.financial.clin_0003_description"),
filters=[number_to_int],
)
clin_1001 = NumberStringField(
translate("forms.financial.clin_1001_label"),
validators=[InputRequired()],
description=translate("forms.financial.clin_1001_description"),
filters=[number_to_int],
)
clin_1003 = NumberStringField(
translate("forms.financial.clin_1003_label"),
validators=[InputRequired()],
description=translate("forms.financial.clin_1003_description"),
filters=[number_to_int],
)
clin_2001 = NumberStringField(
translate("forms.financial.clin_2001_label"),
validators=[InputRequired()],
description=translate("forms.financial.clin_2001_description"),
filters=[number_to_int],
)
clin_2003 = NumberStringField(
translate("forms.financial.clin_2003_label"),
validators=[InputRequired()],
description=translate("forms.financial.clin_2003_description"),
filters=[number_to_int],
)
pdf = FileField(
translate("forms.financial.pdf_label"),
validators=[
FileAllowed(["pdf"], translate("forms.financial.pdf_allowed_description")),
InputRequired(),
],
render_kw={"required": False},
)
class RequestFinancialVerificationForm(CacheableForm):
uii_ids = NewlineListField(
translate("forms.financial.uii_ids_label"),
description=translate("forms.financial.uii_ids_description"),
)
pe_id = StringField(
translate("forms.financial.pe_id_label"),
description=translate("forms.financial.pe_id_description"),
validators=[InputRequired()],
)
treasury_code = StringField(
translate("forms.financial.treasury_code_label"),
description=translate("forms.financial.treasury_code_description"),
validators=[InputRequired(), Regexp(TREASURY_CODE_REGEX)],
)
ba_code = StringField(
translate("forms.financial.ba_code_label"),
description=translate("forms.financial.ba_code_description"),
validators=[InputRequired(), Regexp(BA_CODE_REGEX)],
)
fname_co = StringField(
translate("forms.financial.fname_co_label"), validators=[InputRequired()]
)
lname_co = StringField(
translate("forms.financial.lname_co_label"), validators=[InputRequired()]
)
email_co = EmailField(
translate("forms.financial.email_co_label"),
validators=[InputRequired(), Email()],
)
office_co = StringField(
translate("forms.financial.office_co_label"), validators=[InputRequired()]
)
fname_cor = StringField(
translate("forms.financial.fname_cor_label"), validators=[InputRequired()]
)
lname_cor = StringField(
translate("forms.financial.lname_cor_label"), validators=[InputRequired()]
)
email_cor = EmailField(
translate("forms.financial.email_cor_label"),
validators=[InputRequired(), Email()],
)
office_cor = StringField(
translate("forms.financial.office_cor_label"), 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(CacheableForm):
legacy_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.legacy_task_order.funding_type.data == "OTHER":
self.legacy_task_order.funding_type_other.validators.append(InputRequired())
to_pdf_validators = None
if kwargs.get("has_attachment"):
to_pdf_validators = list(self.legacy_task_order.pdf.validators)
self.legacy_task_order.pdf.validators = []
valid = super().validate()
if to_pdf_validators:
self.legacy_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.legacy_task_order.do_validate_number()
return request_valid and task_order_valid
def reset(self):
self.request.reset()
@property
def pe_id(self):
return self.request.pe_id
@property
def has_pdf_upload(self):
return isinstance(self.legacy_task_order.pdf.data, FileStorage)
@property
def is_missing_task_order_number(self):
return "number" in self.errors.get("legacy_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,222 +0,0 @@
import pendulum
from wtforms.fields.html5 import DateField, EmailField, IntegerField
from wtforms.fields import BooleanField, RadioField, StringField, TextAreaField
from wtforms.validators import Email, Length, Optional, InputRequired, DataRequired
from .fields import SelectField
from .forms import CacheableForm
from .edit_user import USER_FIELDS, inherit_field
from .data import (
SERVICE_BRANCHES,
ASSISTANCE_ORG_TYPES,
DATA_TRANSFER_AMOUNTS,
COMPLETION_DATE_RANGES,
)
from .validators import DateRange, IsNumber
from atst.domain.requests import Requests
from atst.utils.localization import translate
class DetailsOfUseForm(CacheableForm):
def validate(self, *args, **kwargs):
if self.jedi_migration.data == "no":
self.rationalization_software_systems.validators.append(Optional())
self.technical_support_team.validators.append(Optional())
self.organization_providing_assistance.validators.append(Optional())
self.engineering_assessment.validators.append(Optional())
self.data_transfers.validators.append(Optional())
self.expected_completion_date.validators.append(Optional())
elif self.jedi_migration.data == "yes":
if self.technical_support_team.data == "no":
self.organization_providing_assistance.validators.append(Optional())
self.cloud_native.validators.append(Optional())
try:
annual_spend = int(self.estimated_monthly_spend.data or 0) * 12
except ValueError:
annual_spend = 0
if annual_spend > Requests.ANNUAL_SPEND_THRESHOLD:
self.number_user_sessions.validators.append(InputRequired())
self.average_daily_traffic.validators.append(InputRequired())
return super(DetailsOfUseForm, self).validate(*args, **kwargs)
# Details of Use: General
dod_component = SelectField(
translate("forms.new_request.dod_component_label"),
description=translate("forms.new_request.dod_component_description"),
choices=SERVICE_BRANCHES,
validators=[InputRequired()],
)
jedi_usage = TextAreaField(
translate("forms.new_request.jedi_usage_label"),
description=translate("forms.new_request.jedi_usage_description"),
validators=[InputRequired()],
)
# Details of Use: Cloud Readiness
num_software_systems = IntegerField(
translate("forms.new_request.num_software_systems_label"),
description=translate("forms.new_request.num_software_systems_description"),
)
jedi_migration = RadioField(
translate("forms.new_request.jedi_migration_label"),
description=translate("forms.new_request.jedi_migration_description"),
choices=[("yes", "Yes"), ("no", "No")],
default="",
)
rationalization_software_systems = RadioField(
description=translate(
"forms.new_request.rationalization_software_systems_description"
),
choices=[("yes", "Yes"), ("no", "No"), ("In Progress", "In Progress")],
default="",
)
technical_support_team = RadioField(
description=translate("forms.new_request.technical_support_team_description"),
choices=[("yes", "Yes"), ("no", "No")],
default="",
)
organization_providing_assistance = RadioField( # this needs to be updated to use checkboxes instead of radio
description=translate(
"forms.new_request.organization_providing_assistance_description"
),
choices=ASSISTANCE_ORG_TYPES,
default="",
)
engineering_assessment = RadioField(
description=translate("forms.new_request.engineering_assessment_description"),
choices=[("yes", "Yes"), ("no", "No"), ("In Progress", "In Progress")],
default="",
)
data_transfers = SelectField(
description=translate("forms.new_request.data_transfers_description"),
choices=DATA_TRANSFER_AMOUNTS,
validators=[DataRequired()],
)
expected_completion_date = SelectField(
description=translate("forms.new_request.expected_completion_date_description"),
choices=COMPLETION_DATE_RANGES,
validators=[DataRequired()],
)
cloud_native = RadioField(
description=translate("forms.new_request.cloud_native_description"),
choices=[("yes", "Yes"), ("no", "No")],
default="",
)
# Details of Use: Financial Usage
estimated_monthly_spend = IntegerField(
translate("forms.new_request.estimated_monthly_spend_label"),
description=translate("forms.new_request.estimated_monthly_spend_description"),
)
dollar_value = IntegerField(
translate("forms.new_request.dollar_value_label"),
description=translate("forms.new_request.dollar_value_description"),
)
number_user_sessions = IntegerField(
description=translate("forms.new_request.number_user_sessions_description")
)
average_daily_traffic = IntegerField(
translate("forms.new_request.average_daily_traffic_label"),
description=translate("forms.new_request.average_daily_traffic_description"),
)
average_daily_traffic_gb = IntegerField(
translate("forms.new_request.average_daily_traffic_gb_label"),
description=translate("forms.new_request.average_daily_traffic_gb_description"),
)
start_date = DateField(
description=translate("forms.new_request.start_date_label"),
validators=[
InputRequired(),
DateRange(
lower_bound=pendulum.duration(days=1),
upper_bound=None,
message=translate(
"forms.new_request.start_date_date_range_validation_message"
),
),
],
format="%m/%d/%Y",
)
name = StringField(
translate("forms.new_request.name_label"),
description=translate("forms.new_request.name_description"),
validators=[
InputRequired(),
Length(
min=4,
max=100,
message=translate("forms.new_request.name_length_validation_message"),
),
],
)
class InformationAboutYouForm(CacheableForm):
fname_request = inherit_field(USER_FIELDS["first_name"])
lname_request = inherit_field(USER_FIELDS["last_name"])
email_request = inherit_field(USER_FIELDS["email"])
phone_number = inherit_field(USER_FIELDS["phone_number"])
phone_ext = inherit_field(USER_FIELDS["phone_ext"], required=False)
service_branch = inherit_field(USER_FIELDS["service_branch"])
citizenship = inherit_field(USER_FIELDS["citizenship"])
designation = inherit_field(USER_FIELDS["designation"])
date_latest_training = inherit_field(USER_FIELDS["date_latest_training"])
class PortfolioOwnerForm(CacheableForm):
def validate(self, *args, **kwargs):
if self.am_poc.data:
# Prepend Optional validators so that the validation chain
# halts if no data exists.
self.fname_poc.validators.insert(0, Optional())
self.lname_poc.validators.insert(0, Optional())
self.email_poc.validators.insert(0, Optional())
self.dodid_poc.validators.insert(0, Optional())
return super().validate(*args, **kwargs)
am_poc = BooleanField(
translate("forms.new_request.am_poc_label"),
default=False,
false_values=(False, "false", "False", "no", ""),
)
fname_poc = StringField(
translate("forms.new_request.fname_poc_label"), validators=[InputRequired()]
)
lname_poc = StringField(
translate("forms.new_request.lname_poc_label"), validators=[InputRequired()]
)
email_poc = EmailField(
translate("forms.new_request.email_poc_label"),
validators=[InputRequired(), Email()],
)
dodid_poc = StringField(
translate("forms.new_request.dodid_poc_label"),
validators=[InputRequired(), Length(min=10), IsNumber()],
)
class ReviewAndSubmitForm(CacheableForm):
reviewed = BooleanField(translate("forms.new_request.reviewed_label"))

View File

@ -2,21 +2,14 @@ from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base() Base = declarative_base()
from .request import Request
from .request_status_event import RequestStatusEvent
from .permissions import Permissions from .permissions import Permissions
from .role import Role from .role import Role
from .user import User from .user import User
from .portfolio_role import PortfolioRole from .portfolio_role import PortfolioRole
from .pe_number import PENumber
from .legacy_task_order import LegacyTaskOrder
from .portfolio import Portfolio from .portfolio import Portfolio
from .application import Application from .application import Application
from .environment import Environment from .environment import Environment
from .attachment import Attachment from .attachment import Attachment
from .request_revision import RequestRevision
from .request_review import RequestReview
from .request_internal_comment import RequestInternalComment
from .audit_event import AuditEvent from .audit_event import AuditEvent
from .invitation import Invitation from .invitation import Invitation
from .task_order import TaskOrder from .task_order import TaskOrder

View File

@ -17,9 +17,6 @@ class AuditEvent(Base, TimestampsMixin):
portfolio_id = Column(UUID(as_uuid=True), ForeignKey("portfolios.id"), index=True) portfolio_id = Column(UUID(as_uuid=True), ForeignKey("portfolios.id"), index=True)
portfolio = relationship("Portfolio", backref="audit_events") portfolio = relationship("Portfolio", backref="audit_events")
request_id = Column(UUID(as_uuid=True), ForeignKey("requests.id"), index=True)
request = relationship("Request", backref="audit_events")
changed_state = Column(JSONB()) changed_state = Column(JSONB())
event_details = Column(JSONB()) event_details = Column(JSONB())

View File

@ -1,75 +0,0 @@
from enum import Enum
from sqlalchemy import Column, Integer, String, ForeignKey, Enum as SQLAEnum, Date
from sqlalchemy.orm import relationship
from atst.models import Base, types, mixins
class Source(Enum):
MANUAL = "Manual"
EDA = "EDA"
class FundingType(Enum):
RDTE = "RDTE"
OM = "OM"
PROC = "PROC"
OTHER = "OTHER"
class LegacyTaskOrder(Base, mixins.TimestampsMixin):
__tablename__ = "legacy_task_orders"
id = types.Id()
number = Column(String, unique=True)
source = Column(SQLAEnum(Source, native_enum=False))
funding_type = Column(SQLAEnum(FundingType, native_enum=False))
funding_type_other = Column(String)
clin_0001 = Column(Integer)
clin_0003 = Column(Integer)
clin_1001 = Column(Integer)
clin_1003 = Column(Integer)
clin_2001 = Column(Integer)
clin_2003 = Column(Integer)
expiration_date = Column(Date)
attachment_id = Column(ForeignKey("attachments.id"))
pdf = relationship("Attachment")
@property
def verified(self):
return self.source == Source.EDA
def to_dictionary(self):
return {
c.name: getattr(self, c.name)
for c in self.__table__.columns
if c.name not in ["id", "attachment_id"]
}
@property
def budget(self):
return sum(
filter(
None,
[
self.clin_0001,
self.clin_0003,
self.clin_1001,
self.clin_1003,
self.clin_2001,
self.clin_2003,
],
)
)
def __repr__(self): # pragma: no cover
return "<LegacyTaskOrder(number='{}', verified='{}', budget='{}', expiration_date='{}', pdf='{}', id='{}')>".format(
self.number,
self.verified,
self.budget,
self.expiration_date,
self.pdf,
self.id,
)

View File

@ -14,7 +14,6 @@ class AuditableMixin(object):
def create_audit_event(connection, resource, action): def create_audit_event(connection, resource, action):
user_id = getattr_path(g, "current_user.id") user_id = getattr_path(g, "current_user.id")
portfolio_id = resource.portfolio_id portfolio_id = resource.portfolio_id
request_id = resource.request_id
resource_type = resource.resource_type resource_type = resource.resource_type
display_name = resource.displayname display_name = resource.displayname
event_details = resource.event_details event_details = resource.event_details
@ -24,7 +23,6 @@ class AuditableMixin(object):
audit_event = AuditEvent( audit_event = AuditEvent(
user_id=user_id, user_id=user_id,
portfolio_id=portfolio_id, portfolio_id=portfolio_id,
request_id=request_id,
resource_type=resource_type, resource_type=resource_type,
resource_id=resource.id, resource_id=resource.id,
display_name=display_name, display_name=display_name,
@ -91,10 +89,6 @@ class AuditableMixin(object):
def portfolio_id(self): def portfolio_id(self):
return None return None
@property
def request_id(self):
return None
@property @property
def displayname(self): def displayname(self):
return None return None

View File

@ -1,15 +0,0 @@
from sqlalchemy import String, Column
from atst.models import Base
class PENumber(Base):
__tablename__ = "pe_numbers"
number = Column(String, primary_key=True)
description = Column(String)
def __repr__(self): # pragma: no cover
return "<PENumber(number='{}', description='{}')>".format(
self.number, self.description
)

View File

@ -1,4 +1,4 @@
from sqlalchemy import Column, ForeignKey, String from sqlalchemy import Column, String
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from itertools import chain from itertools import chain
@ -13,7 +13,6 @@ class Portfolio(Base, mixins.TimestampsMixin, mixins.AuditableMixin):
id = types.Id() id = types.Id()
name = Column(String) name = Column(String)
request_id = Column(ForeignKey("requests.id"), nullable=True)
applications = relationship("Application", back_populates="portfolio") applications = relationship("Application", back_populates="portfolio")
roles = relationship("PortfolioRole") roles = relationship("PortfolioRole")
@ -35,10 +34,6 @@ class Portfolio(Base, mixins.TimestampsMixin, mixins.AuditableMixin):
def user_count(self): def user_count(self):
return len(self.members) return len(self.members)
@property
def legacy_task_order(self):
return self.request.legacy_task_order if self.request else None
@property @property
def members(self): def members(self):
return ( return (
@ -60,6 +55,6 @@ class Portfolio(Base, mixins.TimestampsMixin, mixins.AuditableMixin):
return self.id return self.id
def __repr__(self): def __repr__(self):
return "<Portfolio(name='{}', request='{}', user_count='{}', id='{}')>".format( return "<Portfolio(name='{}', user_count='{}', id='{}')>".format(
self.name, self.request_id, self.user_count, self.id self.name, self.user_count, self.id
) )

View File

@ -1,256 +0,0 @@
from sqlalchemy import Column, func, ForeignKey
from sqlalchemy.types import DateTime
from sqlalchemy.orm import relationship
from atst.models import Base, types, mixins
from atst.models.request_status_event import RequestStatus
from atst.utils import first_or_none
from atst.models.request_revision import RequestRevision
from atst.models.legacy_task_order import Source as TaskOrderSource
def map_properties_to_dict(properties, instance):
return {
field: getattr(instance, field)
for field in properties
if getattr(instance, field) is not None
}
def update_dict_with_properties(instance, body, top_level_key, properties):
new_properties = map_properties_to_dict(properties, instance)
if new_properties:
body[top_level_key] = new_properties
return body
class Request(Base, mixins.TimestampsMixin, mixins.AuditableMixin):
__tablename__ = "requests"
id = types.Id()
time_created = Column(DateTime(timezone=True), server_default=func.now())
status_events = relationship(
"RequestStatusEvent", backref="request", order_by="RequestStatusEvent.sequence"
)
portfolio = relationship("Portfolio", uselist=False, backref="request")
user_id = Column(ForeignKey("users.id"), nullable=False)
creator = relationship("User", backref="owned_requests")
legacy_task_order_id = Column(ForeignKey("legacy_task_orders.id"))
legacy_task_order = relationship("LegacyTaskOrder")
revisions = relationship(
"RequestRevision", back_populates="request", order_by="RequestRevision.sequence"
)
internal_comments = relationship("RequestInternalComment")
@property
def latest_revision(self):
if self.revisions:
return self.revisions[-1]
else:
return RequestRevision(request=self)
PRIMARY_POC_FIELDS = ["am_poc", "dodid_poc", "email_poc", "fname_poc", "lname_poc"]
DETAILS_OF_USE_FIELDS = [
"jedi_usage",
"start_date",
"cloud_native",
"dollar_value",
"dod_component",
"data_transfers",
"expected_completion_date",
"jedi_migration",
"num_software_systems",
"number_user_sessions",
"average_daily_traffic",
"engineering_assessment",
"technical_support_team",
"estimated_monthly_spend",
"average_daily_traffic_gb",
"rationalization_software_systems",
"organization_providing_assistance",
"name",
]
INFORMATION_ABOUT_YOU_FIELDS = [
"citizenship",
"designation",
"phone_number",
"phone_ext",
"email_request",
"fname_request",
"lname_request",
"service_branch",
"date_latest_training",
]
FINANCIAL_VERIFICATION_FIELDS = [
"pe_id",
"task_order_number",
"fname_co",
"lname_co",
"email_co",
"office_co",
"fname_cor",
"lname_cor",
"email_cor",
"office_cor",
"uii_ids",
"treasury_code",
"ba_code",
]
@property
def body(self):
current = self.latest_revision
body = {}
for top_level_key, properties in [
("primary_poc", Request.PRIMARY_POC_FIELDS),
("details_of_use", Request.DETAILS_OF_USE_FIELDS),
("information_about_you", Request.INFORMATION_ABOUT_YOU_FIELDS),
("financial_verification", Request.FINANCIAL_VERIFICATION_FIELDS),
]:
body = update_dict_with_properties(current, body, top_level_key, properties)
return body
@property
def latest_status(self):
return self.status_events[-1] if self.status_events else None
@property
def status(self):
return self.latest_status.new_status if self.latest_status else None
@property
def status_displayname(self):
return self.latest_status.displayname
@property
def annual_spend(self):
monthly = self.latest_revision.estimated_monthly_spend or 0
return monthly * 12
@property
def financial_verification(self):
return self.body.get("financial_verification", {})
@property
def is_financially_verified(self):
if self.legacy_task_order:
return self.legacy_task_order.verified
return False
@property
def last_submission_timestamp(self):
def _is_submission(status_event):
return status_event.new_status == RequestStatus.SUBMITTED
last_submission = first_or_none(_is_submission, reversed(self.status_events))
if last_submission:
return last_submission.time_created
return None
@property
def action_required_by(self):
return {
RequestStatus.PENDING_FINANCIAL_VERIFICATION: "mission_owner",
RequestStatus.CHANGES_REQUESTED: "mission_owner",
RequestStatus.CHANGES_REQUESTED_TO_FINVER: "mission_owner",
RequestStatus.PENDING_CCPO_APPROVAL: "ccpo",
RequestStatus.PENDING_CCPO_ACCEPTANCE: "ccpo",
}.get(self.status)
@property
def reviews(self):
return [status.review for status in self.status_events if status.review]
@property
def is_pending_financial_verification(self):
return self.status == RequestStatus.PENDING_FINANCIAL_VERIFICATION
@property
def is_pending_financial_verification_changes(self):
return self.status == RequestStatus.CHANGES_REQUESTED_TO_FINVER
@property
def is_pending_ccpo_acceptance(self):
return self.status == RequestStatus.PENDING_CCPO_ACCEPTANCE
@property
def is_pending_ccpo_approval(self):
return self.status == RequestStatus.PENDING_CCPO_APPROVAL
@property
def is_pending_ccpo_action(self):
return self.is_pending_ccpo_acceptance or self.is_pending_ccpo_approval
@property
def is_approved(self):
return self.status == RequestStatus.APPROVED
@property
def review_comment(self):
if (
self.status == RequestStatus.CHANGES_REQUESTED
or self.status == RequestStatus.CHANGES_REQUESTED_TO_FINVER
):
review = self.latest_status.review
if review: # pragma: no branch
return review.comment
@property
def has_financial_data(self):
return (
self.is_pending_ccpo_approval
or self.is_pending_financial_verification_changes
or self.is_approved
) and self.legacy_task_order
@property
def displayname(self):
return self.latest_revision.name or self.id
@property
def contracting_officer_full_name(self):
if self.latest_revision.fname_co:
return "{} {}".format(
self.latest_revision.fname_co, self.latest_revision.lname_co
)
@property
def contracting_officer_email(self):
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.legacy_task_order.source == TaskOrderSource.MANUAL
if self.legacy_task_order is not None
else None
)
@property
def last_finver_draft_saved_at(self):
if self.latest_revision.any_finver_fields_saved:
return self.latest_revision.time_updated
else:
return None
def __repr__(self):
return "<Request(status='{}', name='{}', creator='{}', is_approved='{}', time_created='{}', id='{}')>".format(
self.status_displayname,
self.displayname,
self.creator.full_name,
self.is_approved,
self.time_created,
self.id,
)

View File

@ -1,22 +0,0 @@
from sqlalchemy import Column, String, ForeignKey
from sqlalchemy.orm import relationship
from atst.models import Base, types, mixins
class RequestInternalComment(Base, mixins.TimestampsMixin):
__tablename__ = "request_internal_comments"
id = types.Id()
text = Column(String(), nullable=False)
user_id = Column(ForeignKey("users.id"), nullable=False)
user = relationship("User")
request_id = Column(ForeignKey("requests.id", ondelete="CASCADE"), nullable=False)
request = relationship("Request")
def __repr__(self): # pragma: no cover
return "<RequestInternalComment(text='{}', user='{}', request='{}', id='{}')>".format(
self.text, self.user.full_name, self.request_id, self.id
)

View File

@ -1,43 +0,0 @@
from sqlalchemy import Column, String, ForeignKey
from sqlalchemy.orm import relationship
from atst.models import Base, mixins, types
class RequestReview(Base, mixins.TimestampsMixin, mixins.AuditableMixin):
__tablename__ = "request_reviews"
id = types.Id()
status = relationship("RequestStatusEvent", uselist=False, back_populates="review")
user_id = Column(ForeignKey("users.id"))
reviewer = relationship("User")
comment = Column(String)
fname_mao = Column(String)
lname_mao = Column(String)
email_mao = Column(String)
phone_mao = Column(String)
phone_ext_mao = Column(String)
fname_ccpo = Column(String)
lname_ccpo = Column(String)
@property
def full_name_reviewer(self):
if self.reviewer:
return self.reviewer.full_name
else:
return "System"
@property
def full_name_mao(self):
return "{} {}".format(self.fname_mao, self.lname_mao)
@property
def full_name_ccpo(self):
return "{} {}".format(self.fname_ccpo, self.lname_ccpo)
def __repr__(self):
return "<RequestReview(status='{}', comment='{}', reviewer='{}', id='{}')>".format(
self.status.log_name, self.comment, self.full_name_reviewer, self.id
)

View File

@ -1,106 +0,0 @@
from sqlalchemy import (
Column,
ForeignKey,
String,
Boolean,
Integer,
Date,
BigInteger,
Sequence,
)
from sqlalchemy.orm import relationship
from sqlalchemy.dialects.postgresql import ARRAY
from atst.models import Base
from atst.models import mixins
from atst.models.types import Id
class RequestRevision(Base, mixins.TimestampsMixin, mixins.AuditableMixin):
__tablename__ = "request_revisions"
id = Id()
request_id = Column(ForeignKey("requests.id"), nullable=False)
request = relationship("Request", back_populates="revisions")
sequence = Column(
BigInteger, Sequence("request_revisions_sequence_seq"), nullable=False
)
# primary_poc
am_poc = Column(Boolean)
dodid_poc = Column(String)
email_poc = Column(String)
fname_poc = Column(String)
lname_poc = Column(String)
# details_of_use
jedi_usage = Column(String)
start_date = Column(Date)
cloud_native = Column(String)
dollar_value = Column(Integer)
dod_component = Column(String)
data_transfers = Column(String)
expected_completion_date = Column(String)
jedi_migration = Column(String)
num_software_systems = Column(Integer)
number_user_sessions = Column(Integer)
average_daily_traffic = Column(Integer)
engineering_assessment = Column(String)
technical_support_team = Column(String)
estimated_monthly_spend = Column(Integer)
average_daily_traffic_gb = Column(Integer)
rationalization_software_systems = Column(String)
organization_providing_assistance = Column(String)
name = Column(String)
# information_about_you
citizenship = Column(String)
designation = Column(String)
phone_number = Column(String)
phone_ext = Column(String)
email_request = Column(String)
fname_request = Column(String)
lname_request = Column(String)
service_branch = Column(String)
date_latest_training = Column(Date)
# financial_verification
pe_id = Column(String)
task_order_number = Column(String)
fname_co = Column(String)
lname_co = Column(String)
email_co = Column(String)
office_co = Column(String)
fname_cor = Column(String)
lname_cor = Column(String)
email_cor = Column(String)
office_cor = Column(String)
uii_ids = Column(ARRAY(String))
treasury_code = Column(String)
ba_code = Column(String)
def __repr__(self): # pragma: no cover
return "<RequestRevision(request='{}', id='{}')>".format(
self.request_id, self.id
)
@property
def any_finver_fields_saved(self):
return any(
getattr(self, n, None)
for n in [
"pe_id",
"task_order_number",
"fname_co",
"lname_co",
"email_co",
"office_co",
"fname_cor",
"lname_cor",
"email_cor",
"office_cor",
"uii_ids",
"treasury_code",
"ba_code",
]
)

View File

@ -1,62 +0,0 @@
from enum import Enum
from sqlalchemy import Column, ForeignKey, Enum as SQLAEnum
from sqlalchemy.orm import relationship
from sqlalchemy.types import BigInteger
from sqlalchemy.schema import Sequence
from sqlalchemy.dialects.postgresql import UUID
from atst.models import Base, mixins
from atst.models.types import Id
class RequestStatus(Enum):
STARTED = "Started"
SUBMITTED = "Submitted"
PENDING_FINANCIAL_VERIFICATION = "Pending Financial Verification"
PENDING_CCPO_ACCEPTANCE = "Pending CCPO Acceptance"
PENDING_CCPO_APPROVAL = "Pending CCPO Approval"
CHANGES_REQUESTED = "Changes Requested"
CHANGES_REQUESTED_TO_FINVER = "Change Requested to Financial Verification"
APPROVED = "Approved"
EXPIRED = "Expired"
DELETED = "Deleted"
class RequestStatusEvent(Base, mixins.TimestampsMixin, mixins.AuditableMixin):
__tablename__ = "request_status_events"
id = Id()
new_status = Column(SQLAEnum(RequestStatus, native_enum=False))
request_id = Column(
UUID(as_uuid=True),
ForeignKey("requests.id", ondelete="CASCADE"),
nullable=False,
)
sequence = Column(
BigInteger, Sequence("request_status_events_sequence_seq"), nullable=False
)
request_revision_id = Column(ForeignKey("request_revisions.id"), nullable=False)
revision = relationship("RequestRevision")
request_review_id = Column(ForeignKey("request_reviews.id"), nullable=True)
review = relationship("RequestReview", back_populates="status")
@property
def displayname(self):
return self.new_status.value if self.new_status else None
@property
def log_name(self):
if self.new_status == RequestStatus.CHANGES_REQUESTED:
return "Denied"
if self.new_status == RequestStatus.CHANGES_REQUESTED_TO_FINVER:
return "Denied"
elif self.new_status == RequestStatus.PENDING_FINANCIAL_VERIFICATION:
return "Accepted"
else:
return self.displayname
def __repr__(self):
return "<RequestStatusEvent(log_name='{}', request='{}', id='{}')>".format(
self.log_name, self.request_id, self.id
)

View File

@ -16,7 +16,6 @@ import pendulum
import os import os
from werkzeug.exceptions import NotFound from werkzeug.exceptions import NotFound
from atst.domain.requests import Requests
from atst.domain.users import Users from atst.domain.users import Users
from atst.domain.authnid import AuthenticationContext from atst.domain.authnid import AuthenticationContext
from atst.domain.audit_log import AuditLog from atst.domain.audit_log import AuditLog
@ -58,10 +57,6 @@ def helpdocs(doc=None):
@bp.route("/home") @bp.route("/home")
def home(): def home():
user = g.current_user user = g.current_user
if user.atat_role_name == "ccpo":
return redirect(url_for("requests.requests_index"))
num_portfolios = len([role for role in user.portfolio_roles if role.is_active]) num_portfolios = len([role for role in user.portfolio_roles if role.is_active])
if num_portfolios == 0: if num_portfolios == 0:

View File

@ -98,19 +98,3 @@ def portfolio_reports(portfolio_id):
expiration_date=expiration_date, expiration_date=expiration_date,
remaining_days=remaining_days, remaining_days=remaining_days,
) )
@portfolios_bp.route("/portfolios/<portfolio_id>/activity")
def portfolio_activity(portfolio_id):
portfolio = Portfolios.get(g.current_user, portfolio_id)
pagination_opts = Paginator.get_pagination_opts(http_request)
audit_events = AuditLog.get_portfolio_events(
g.current_user, portfolio, pagination_opts
)
return render_template(
"portfolios/activity/index.html",
portfolio_name=portfolio.name,
portfolio_id=portfolio_id,
audit_events=audit_events,
)

View File

@ -1,15 +0,0 @@
from flask import Blueprint
from atst.domain.requests import Requests
requests_bp = Blueprint("requests", __name__)
from . import index
from . import requests_form
from . import financial_verification
from . import approval
@requests_bp.context_processor
def annual_spend_threshold():
return {"annual_spend_threshold": Requests.ANNUAL_SPEND_THRESHOLD}

View File

@ -1,97 +0,0 @@
from flask import (
render_template,
g,
Response,
request as http_request,
redirect,
url_for,
)
from flask import current_app as app
from . import requests_bp
from atst.domain.requests import Requests
from atst.domain.exceptions import NotFoundError
from atst.forms.ccpo_review import CCPOReviewForm
from atst.forms.internal_comment import InternalCommentForm
def map_ccpo_authorizing(user):
return {"fname_ccpo": user.first_name, "lname_ccpo": user.last_name}
def render_approval(request, form=None, internal_comment_form=None):
data = request.body
if request.has_financial_data:
data["legacy_task_order"] = request.legacy_task_order.to_dictionary()
if not form:
mo_data = map_ccpo_authorizing(g.current_user)
form = CCPOReviewForm(data=mo_data)
if not internal_comment_form:
internal_comment_form = InternalCommentForm()
return render_template(
"requests/approval.html",
data=data,
reviews=list(reversed(request.reviews)),
jedi_request=request,
current_status=request.status.value,
review_form=form or CCPOReviewForm(),
internal_comment_form=internal_comment_form,
comments=request.internal_comments,
)
@requests_bp.route("/requests/approval/<string:request_id>", methods=["GET"])
def approval(request_id):
request = Requests.get_for_approval(g.current_user, request_id)
return render_approval(request)
@requests_bp.route("/requests/submit_approval/<string:request_id>", methods=["POST"])
def submit_approval(request_id):
request = Requests.get_for_approval(g.current_user, request_id)
form = CCPOReviewForm(http_request.form)
if form.validate():
if http_request.form.get("review") == "approving":
Requests.advance(g.current_user, request, form.data)
else:
Requests.request_changes(g.current_user, request, form.data)
return redirect(url_for("requests.requests_index"))
else:
return render_approval(request, form)
@requests_bp.route("/requests/task_order_download/<string:request_id>", methods=["GET"])
def task_order_pdf_download(request_id):
request = Requests.get(g.current_user, request_id)
if request.legacy_task_order and request.legacy_task_order.pdf:
pdf = request.legacy_task_order.pdf
generator = app.csp.files.download(pdf.object_name)
return Response(
generator,
headers={
"Content-Disposition": "attachment; filename={}".format(pdf.filename)
},
mimetype="application/pdf",
)
else:
raise NotFoundError("legacy_task_order pdf")
@requests_bp.route("/requests/internal_comments/<string:request_id>", methods=["POST"])
def create_internal_comment(request_id):
form = InternalCommentForm(http_request.form)
request = Requests.get(g.current_user, request_id)
if form.validate():
Requests.add_internal_comment(g.current_user, request, form.data.get("text"))
return redirect(
url_for("requests.approval", request_id=request_id, _anchor="ccpo-notes")
)
else:
return render_approval(request, internal_comment_form=form)

View File

@ -1,291 +0,0 @@
from flask import g, render_template, redirect, url_for
from flask import request as http_request
from werkzeug.datastructures import ImmutableMultiDict, FileStorage
from . import requests_bp
from atst.domain.requests import Requests
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.legacy_task_orders import LegacyTaskOrders
from atst.utils.flash import formatted_flash as flash
def fv_extended(_http_request):
return _http_request.args.get("extended", "false").lower() in ["true", "t"]
class FinancialVerification(object):
def __init__(self, request):
self.request = request.latest_revision
self.legacy_task_order = request.legacy_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 not form.has_pdf_upload:
if isinstance(form.legacy_task_order.pdf.data, Attachment):
form.legacy_task_order.pdf.data = (
form.legacy_task_order.pdf.data.filename
)
else:
try:
attachment = Attachment.get_for_resource(
"legacy_task_order", self.request.id
)
form.legacy_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.legacy_task_order.pdf.data, FileStorage):
Attachment.delete_for_resource("legacy_task_order", self.request.id)
attachment = Attachment.attach(
form.legacy_task_order.pdf.data,
"legacy_task_order",
self.request.id,
)
elif isinstance(form.legacy_task_order.pdf.data, str):
try:
attachment = Attachment.get_for_resource(
"legacy_task_order", self.request.id
)
except NotFoundError:
pass
if attachment:
form.legacy_task_order.pdf.data = attachment.filename
return attachment
def _try_create_task_order(self, form, attachment, is_extended):
task_order_number = form.legacy_task_order.number.data
if not task_order_number:
return None
task_order_data = form.legacy_task_order.data
if attachment:
task_order_data["pdf"] = attachment
try:
legacy_task_order = LegacyTaskOrders.get(task_order_number)
legacy_task_order = LegacyTaskOrders.update(
legacy_task_order, task_order_data
)
return legacy_task_order
except NotFoundError:
pass
try:
return LegacyTaskOrders.get_from_eda(task_order_number)
except NotFoundError:
pass
return LegacyTaskOrders.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.is_extended = is_extended
def execute(self):
form = self._get_form(self.request, self.is_extended)
form.reset()
return form
class UpdateFinancialVerification(FinancialVerificationBase):
def __init__(
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
def execute(self):
form = self._get_form(self.request, self.is_extended, self.fv_data)
should_update = True
should_submit = True
updated_request = None
attachment = self._process_attachment(self.is_extended, form)
if not form.validate(is_extended=self.is_extended, has_attachment=attachment):
should_update = False
if not self.pe_validator.validate(self.request, form.pe_id):
should_submit = False
if not self.is_extended and not self.task_order_validator.validate(
form.legacy_task_order.number
):
should_submit = False
if should_update:
legacy_task_order = self._try_create_task_order(
form, attachment, self.is_extended
)
updated_request = Requests.update_financial_verification(
self.request.id, form.request.data, legacy_task_order=legacy_task_order
)
if should_submit:
return Requests.submit_financial_verification(updated_request)
self._raise(form)
class SaveFinancialVerificationDraft(FinancialVerificationBase):
def __init__(
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
def execute(self):
form = self._get_form(self.request, self.is_extended, self.fv_data)
attachment = self._process_attachment(self.is_extended, form)
legacy_task_order = self._try_create_task_order(
form, attachment, self.is_extended
)
updated_request = Requests.update_financial_verification(
self.request.id, form.request.data, legacy_task_order=legacy_task_order
)
return updated_request
@requests_bp.route("/requests/verify/<string:request_id>/draft", methods=["GET"])
@requests_bp.route("/requests/verify/<string:request_id>", methods=["GET"])
def financial_verification(request_id):
request = Requests.get(g.current_user, request_id)
is_extended = fv_extended(http_request)
saved_draft = http_request.args.get("saved_draft", False)
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()
if request.review_comment:
flash("request_review_comment", comment=request.review_comment)
return render_template(
"requests/financial_verification.html",
f=form,
jedi_request=request,
extended=is_extended,
saved_draft=saved_draft,
)
@requests_bp.route("/requests/verify/<string:request_id>", methods=["POST"])
def update_financial_verification(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:
updated_request = UpdateFinancialVerification(
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,
)
if updated_request.legacy_task_order.verified:
portfolio = Requests.auto_approve_and_create_portfolio(updated_request)
flash("new_portfolio")
return redirect(
url_for("portfolios.new_application", portfolio_id=portfolio.id)
)
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):
user = g.current_user
request = Requests.get(user, request_id)
fv_data = {**http_request.form, **http_request.files}
is_extended = fv_extended(http_request)
try:
updated_request = SaveFinancialVerificationDraft(
PENumberValidator(),
TaskOrderNumberValidator(),
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.financial_verification",
request_id=updated_request.id,
is_extended=is_extended,
saved_draft=True,
)
)

View File

@ -1,107 +0,0 @@
import pendulum
from flask import render_template, g, url_for
from . import requests_bp
from atst.domain.requests import Requests
from atst.models.permissions import Permissions
from atst.forms.data import SERVICE_BRANCHES
from atst.utils.flash import formatted_flash as flash
class RequestsIndex(object):
def __init__(self, user):
self.user = user
def execute(self):
if (
Permissions.REVIEW_AND_APPROVE_JEDI_PORTFOLIO_REQUEST
in self.user.atat_permissions
):
context = self._ccpo_view(self.user)
else:
context = self._non_ccpo_view(self.user)
return {
**context,
"possible_statuses": Requests.possible_statuses(),
"possible_dod_components": [b[0] for b in SERVICE_BRANCHES[1:]],
}
def _ccpo_view(self, user):
requests = Requests.get_many()
mapped_requests = [self._map_request(r, "ccpo") for r in requests]
num_action_required = len(
[r for r in mapped_requests if r.get("action_required")]
)
return {
"requests": mapped_requests,
"pending_financial_verification": False,
"pending_ccpo_acceptance": False,
"extended_view": True,
"kpi_inprogress": Requests.in_progress_count(),
"kpi_pending": Requests.pending_ccpo_count(),
"kpi_completed": Requests.completed_count(),
"num_action_required": num_action_required,
}
def _non_ccpo_view(self, user):
requests = Requests.get_many(creator=user)
mapped_requests = [self._map_request(r, "mission_owner") for r in requests]
num_action_required = len(
[r for r in mapped_requests if r.get("action_required")]
)
pending_fv = any(r.is_pending_financial_verification for r in requests)
pending_ccpo = any(r.is_pending_ccpo_acceptance for r in requests)
return {
"requests": mapped_requests,
"pending_financial_verification": pending_fv,
"pending_ccpo_acceptance": pending_ccpo,
"num_action_required": num_action_required,
"extended_view": False,
}
def _portfolio_link_for_request(self, request):
if request.is_approved:
return url_for(
"portfolios.portfolio_applications", portfolio_id=request.portfolio.id
)
else:
return None
def _map_request(self, request, viewing_role):
time_created = pendulum.instance(request.time_created)
is_new = time_created.add(days=1) > pendulum.now()
app_count = request.body.get("details_of_use", {}).get(
"num_software_systems", 0
)
annual_usage = request.annual_spend
return {
"portfolio_id": request.portfolio.id if request.portfolio else None,
"name": request.displayname,
"is_new": is_new,
"is_approved": request.is_approved,
"status": request.status_displayname,
"app_count": app_count,
"last_submission_timestamp": request.last_submission_timestamp,
"last_edited_timestamp": request.latest_revision.time_updated,
"full_name": request.creator.full_name,
"annual_usage": annual_usage,
"edit_link": url_for("requests.edit", request_id=request.id),
"action_required": request.action_required_by == viewing_role,
"dod_component": request.latest_revision.dod_component,
"portfolio_link": self._portfolio_link_for_request(request),
}
@requests_bp.route("/requests", methods=["GET"])
def requests_index():
context = RequestsIndex(g.current_user).execute()
if context.get("num_action_required"):
flash("requests_action_required", count=context.get("num_action_required"))
return render_template("requests/index.html", **context)

View File

@ -1,162 +0,0 @@
from collections import defaultdict
from atst.domain.requests import Requests
import atst.forms.new_request as request_forms
class JEDIRequestFlow(object):
def __init__(
self,
current_step,
current_user=None,
request=None,
post_data=None,
request_id=None,
existing_request=None,
):
self.current_step = current_step
self.current_user = current_user
self.request = request
self.post_data = post_data
self.is_post = self.post_data is not None
self.request_id = request_id
self.form = self._form()
self.existing_request = existing_request
def _form(self):
if self.is_post:
return self.form_class()(self.post_data)
else:
return self.form_class()(data=self.current_step_data)
def validate(self):
return self.form.validate()
def validate_warnings(self):
existing_request_data = (
self.existing_request and self.existing_request.body.get(self.form_section)
) or None
valid = self.form.perform_extra_validation(existing_request_data)
return valid
@property
def current_screen(self):
return self.screens[self.current_step - 1]
@property
def form_section(self):
return self.current_screen["section"]
def form_class(self):
return self.current_screen["form"]
# maps user data to fields in InformationAboutYouForm; this should be moved
# into the request initialization process when we have a request schema, or
# we just shouldn't record this data on the request
def map_user_data(self, user):
return {
"fname_request": user.first_name,
"lname_request": user.last_name,
"email_request": user.email,
"phone_number": user.phone_number,
"phone_ext": user.phone_ext,
"service_branch": user.service_branch,
"designation": user.designation,
"citizenship": user.citizenship,
"date_latest_training": user.date_latest_training,
}
@property
def current_step_data(self):
data = {}
if self.is_post:
data = self.post_data
if self.request:
if self.form_section == "review_submit":
data = self.request.body
elif self.form_section == "information_about_you":
form_data = self.request.body.get(self.form_section, {})
data = {**self.map_user_data(self.request.creator), **form_data}
else:
data = self.request.body.get(self.form_section, {})
elif self.form_section == "information_about_you":
data = self.map_user_data(self.current_user)
return defaultdict(lambda: defaultdict(lambda: None), data)
@property
def can_submit(self):
return self.request and Requests.should_allow_submission(self.request)
@property
def next_screen(self):
return self.current_step + 1
@property
def screens(self):
return [
{
"title": "Details of Use",
"section": "details_of_use",
"form": request_forms.DetailsOfUseForm,
},
{
"title": "Information About You",
"section": "information_about_you",
"form": request_forms.InformationAboutYouForm,
},
{
"title": "Portfolio Owner",
"section": "primary_poc",
"form": request_forms.PortfolioOwnerForm,
},
{
"title": "Review & Submit",
"section": "review_submit",
"form": request_forms.ReviewAndSubmitForm,
},
]
@property
def is_review_screen(self):
return self.screens[-1] == self.current_screen
def create_or_update_request(self):
request_data = self.map_request_data(self.form_section, self.form.data)
if self.request_id:
Requests.update(self.request_id, request_data)
else:
request = Requests.create(self.current_user, request_data)
self.request_id = request.id
def map_request_data(self, section, data):
if section == "primary_poc":
if data.get("am_poc", False):
try:
request_user_info = self.existing_request.body.get(
"information_about_you", {}
)
except AttributeError:
request_user_info = {}
data = {
**data,
"dodid_poc": self.current_user.dod_id,
"fname_poc": request_user_info.get(
"fname_request", self.current_user.first_name
),
"lname_poc": request_user_info.get(
"lname_request", self.current_user.last_name
),
"email_poc": request_user_info.get(
"email_request", self.current_user.email
),
}
return {section: data}

View File

@ -1,187 +0,0 @@
from flask import g, redirect, render_template, url_for, request as http_request
from . import requests_bp
from atst.domain.requests import Requests
from atst.domain.authz import Authorization
from atst.routes.requests.jedi_request_flow import JEDIRequestFlow
from atst.models.request_status_event import RequestStatus
from atst.forms.data import (
SERVICE_BRANCHES,
ASSISTANCE_ORG_TYPES,
DATA_TRANSFER_AMOUNTS,
COMPLETION_DATE_RANGES,
FUNDING_TYPES,
TASK_ORDER_SOURCES,
)
from atst.utils.flash import formatted_flash as flash
@requests_bp.context_processor
def option_data():
return {
"service_branches": SERVICE_BRANCHES,
"assistance_org_types": ASSISTANCE_ORG_TYPES,
"data_transfer_amounts": DATA_TRANSFER_AMOUNTS,
"completion_date_ranges": COMPLETION_DATE_RANGES,
"funding_types": FUNDING_TYPES,
"task_order_sources": TASK_ORDER_SOURCES,
}
@requests_bp.route("/requests/new/<int:screen>", methods=["GET"])
def requests_form_new(screen):
jedi_flow = JEDIRequestFlow(screen, request=None, current_user=g.current_user)
if jedi_flow.is_review_screen and not jedi_flow.can_submit:
flash("request_incomplete")
return render_template(
"requests/screen-%d.html" % int(screen),
f=jedi_flow.form,
data=jedi_flow.current_step_data,
screens=jedi_flow.screens,
current=screen,
next_screen=screen + 1,
can_submit=jedi_flow.can_submit,
)
@requests_bp.route(
"/requests/new/<int:screen>", methods=["GET"], defaults={"request_id": None}
)
@requests_bp.route("/requests/new/<int:screen>/<string:request_id>", methods=["GET"])
def requests_form_update(screen=1, request_id=None):
request = (
Requests.get(g.current_user, request_id) if request_id is not None else None
)
jedi_flow = JEDIRequestFlow(
screen, request=request, request_id=request_id, current_user=g.current_user
)
if jedi_flow.is_review_screen and not jedi_flow.can_submit:
flash("request_incomplete")
if request.review_comment:
flash("request_review_comment", comment=request.review_comment)
return render_template(
"requests/screen-%d.html" % int(screen),
f=jedi_flow.form,
data=jedi_flow.current_step_data,
screens=jedi_flow.screens,
current=screen,
next_screen=screen + 1,
request_id=request_id,
jedi_request=jedi_flow.request,
can_submit=jedi_flow.can_submit,
)
@requests_bp.route(
"/requests/new/<int:screen>", methods=["POST"], defaults={"request_id": None}
)
@requests_bp.route("/requests/new/<int:screen>/<string:request_id>", methods=["POST"])
def requests_update(screen=1, request_id=None):
screen = int(screen)
post_data = http_request.form
current_user = g.current_user
existing_request = (
Requests.get(g.current_user, request_id) if request_id is not None else None
)
jedi_flow = JEDIRequestFlow(
screen,
post_data=post_data,
request_id=request_id,
current_user=current_user,
existing_request=existing_request,
)
has_next_screen = jedi_flow.next_screen <= len(jedi_flow.screens)
valid = jedi_flow.validate() and jedi_flow.validate_warnings()
if valid:
jedi_flow.create_or_update_request()
if has_next_screen:
where = url_for(
"requests.requests_form_update",
screen=jedi_flow.next_screen,
request_id=jedi_flow.request_id,
)
else:
where = "/requests"
return redirect(where)
else:
rerender_args = dict(
f=jedi_flow.form,
data=post_data,
screens=jedi_flow.screens,
current=screen,
next_screen=jedi_flow.next_screen,
request_id=jedi_flow.request_id,
)
return render_template("requests/screen-%d.html" % int(screen), **rerender_args)
@requests_bp.route("/requests/submit/<string:request_id>", methods=["POST"])
def requests_submit(request_id=None):
request = Requests.get(g.current_user, request_id)
Requests.submit(request)
if request.status == RequestStatus.PENDING_FINANCIAL_VERIFICATION:
modal = "pendingFinancialVerification"
else:
modal = "pendingCCPOAcceptance"
return redirect(url_for("requests.requests_index", modal=modal))
@requests_bp.route("/requests/details/<string:request_id>", methods=["GET"])
def view_request_details(request_id=None):
request = Requests.get(g.current_user, request_id)
requires_fv_action = (
request.is_pending_financial_verification
or request.is_pending_financial_verification_changes
)
data = request.body
if request.has_financial_data:
data["legacy_task_order"] = request.legacy_task_order.to_dictionary()
return render_template(
"requests/details.html",
data=data,
jedi_request=request,
requires_fv_action=requires_fv_action,
)
@requests_bp.route("/requests/edit/<string:request_id>")
def edit(request_id):
user = g.current_user
request = Requests.get(user, request_id)
is_ccpo = Authorization.is_ccpo(user)
redirect_url = ""
if request.creator == user:
if request.is_pending_financial_verification:
redirect_url = url_for(
"requests.financial_verification", request_id=request.id
)
elif request.is_pending_financial_verification_changes:
redirect_url = url_for(
"requests.financial_verification", request_id=request.id, extended=True
)
elif request.is_approved:
redirect_url = url_for(
"requests.view_request_details", request_id=request.id
)
else:
redirect_url = url_for(
"requests.requests_form_update", screen=1, request_id=request.id
)
elif is_ccpo:
redirect_url = url_for("requests.approval", request_id=request.id)
return redirect(redirect_url)

View File

@ -5,24 +5,6 @@ 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)
def deep_merge(source, destination: dict):
"""
Merge source dict into destination dict recursively.
"""
def _deep_merge(a, b):
for key, value in a.items():
if isinstance(value, dict):
node = b.setdefault(key, {})
_deep_merge(value, node)
else:
b[key] = value
return b
return _deep_merge(source, dict(destination))
def getattr_path(obj, path, default=None): def getattr_path(obj, path, default=None):
_obj = obj _obj = obj
for item in path.split("."): for item in path.split("."):
@ -33,23 +15,11 @@ def getattr_path(obj, path, default=None):
return _obj return _obj
def update_obj(obj, dct, ignore_vals=[None]):
for k, v in dct.items():
if hasattr(obj, k) and v not in ignore_vals:
setattr(obj, k, v)
return obj
def camel_to_snake(camel_cased): def camel_to_snake(camel_cased):
s1 = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", 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() 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): def pick(keys, dct):
_keys = set(keys) _keys = set(keys)
return {k: v for (k, v) in dct.items() if k in _keys} return {k: v for (k, v) in dct.items() if k in _keys}

View File

@ -88,29 +88,6 @@ MESSAGES = {
"message_template": "", "message_template": "",
"category": "success", "category": "success",
}, },
"request_incomplete": {
"title_template": "Please complete all sections",
"message_template": """
<p>In order to submit your JEDI Cloud request, you'll need to complete all required sections of this form without error. Missing or invalid fields are noted below.</p>
""",
"category": "error",
},
"requests_action_required": {
"title_template": "Action required on {{ count }} requests.",
"message_template": "",
"category": "info",
},
"request_review_comment": {
"title_template": "Changes Requested",
"message_template": """
<p>CCPO has requested changes to your submission with the following notes:
<br>
{{ comment }}
<br>
Please contact info@jedi.cloud or 123-123-4567 for further discussion.</p>
""",
"category": "warning",
},
"environment_access_changed": { "environment_access_changed": {
"title_template": "User access successfully changed.", "title_template": "User access successfully changed.",
"message_template": "", "message_template": "",

View File

@ -1,86 +0,0 @@
import { shallowMount } from '@vue/test-utils'
import RequestsList from '../requests_list'
describe('RequestsList', () => {
describe('isExtended', () => {
it('should disallow sorting if not extended', () => {
const wrapper = shallowMount(RequestsList, {
propsData: { isExtended: false },
})
expect(wrapper.vm.sort.columnName).toEqual('')
wrapper.vm.updateSortValue('full_name')
expect(wrapper.vm.sort.columnName).toEqual('')
})
it('should allow sorting when in extended mode', () => {
const wrapper = shallowMount(RequestsList, {
propsData: { isExtended: true },
})
expect(wrapper.vm.sort.columnName).toEqual('last_submission_timestamp')
wrapper.vm.updateSortValue('full_name')
expect(wrapper.vm.sort.columnName).toEqual('full_name')
})
})
describe('sorting', () => {
const requests = [
{
name: 'X Wing',
last_edited_timestamp: 'Mon, 2 Jan 2017 12:34:56 GMT',
last_submission_timestamp: 'Mon, 2 Jan 2017 12:34:56 GMT',
full_name: 'Luke Skywalker',
annual_usage: '80000',
status: 'Approved',
dod_component: 'Rebels',
},
{
name: 'TIE Fighter',
last_edited_timestamp: 'Mon, 12 Nov 2018 12:34:56 GMT',
last_submission_timestamp: 'Mon, 12 Nov 2018 12:34:56 GMT',
full_name: 'Darth Vader',
annual_usage: '999999',
status: 'Approved',
dod_component: 'Empire',
},
]
const mountWrapper = () =>
shallowMount(RequestsList, { propsData: { requests, isExtended: true } })
it('should default to sorting by submission recency', () => {
const wrapper = mountWrapper()
const displayedRequests = wrapper.vm.filteredRequests
const requestNames = displayedRequests.map(req => req.name)
expect(requestNames).toEqual(['TIE Fighter', 'X Wing'])
})
it('should reverse sort by submission time when selected', () => {
const wrapper = mountWrapper()
wrapper.vm.updateSortValue('last_submission_timestamp')
const displayedRequests = wrapper.vm.filteredRequests
const requestNames = displayedRequests.map(req => req.name)
expect(requestNames).toEqual(['X Wing', 'TIE Fighter'])
})
it('handles sorting with un-submitted requests', () => {
const unsubmittedRequest = {
name: 'Death Star',
status: 'Started',
last_submission_timestamp: null,
}
const wrapper = shallowMount(RequestsList, {
propsData: {
requests: [unsubmittedRequest, ...requests],
isExtended: true,
},
})
const displayedRequests = wrapper.vm.filteredRequests
expect(displayedRequests).toEqual([
requests[1],
requests[0],
unsubmittedRequest,
])
})
})
})

View File

@ -1,34 +0,0 @@
import textinput from '../text_input'
import LocalDatetime from '../local_datetime'
export default {
name: 'ccpo-approval',
components: {
textinput,
LocalDatetime,
},
props: {
initialState: String,
},
data: function() {
return {
approving: this.initialState === 'approving',
denying: this.initialState === 'denying',
}
},
methods: {
setReview: function(e) {
if (e.target.value === 'approving') {
this.approving = true
this.denying = false
} else {
this.approving = false
this.denying = true
}
},
},
}

View File

@ -1,64 +0,0 @@
import createNumberMask from 'text-mask-addons/dist/createNumberMask'
import { conformToMask } from 'vue-text-mask'
import FormMixin from '../../mixins/form'
import textinput from '../text_input'
import optionsinput from '../options_input'
export default {
name: 'details-of-use',
mixins: [FormMixin],
components: {
textinput,
optionsinput,
},
props: {
initialData: {
type: Object,
default: () => ({}),
},
},
data: function() {
const {
estimated_monthly_spend = 0,
jedi_migration = '',
technical_support_team = '',
} = this.initialData
return {
estimated_monthly_spend,
jedi_migration,
technical_support_team,
}
},
computed: {
annualSpend: function() {
const monthlySpend = this.estimated_monthly_spend || 0
return monthlySpend * 12
},
annualSpendStr: function() {
return this.formatDollars(this.annualSpend)
},
jediMigrationOptionSelected: function() {
return this.jedi_migration !== ''
},
isJediMigration: function() {
return this.jedi_migration === 'yes'
},
hasTechnicalSupportTeam: function() {
return this.technical_support_team === 'yes'
},
},
methods: {
formatDollars: function(intValue) {
const mask = createNumberMask({ prefix: '$', allowDecimal: true })
return conformToMask(intValue.toString(), mask).conformedValue
},
},
}

View File

@ -1,48 +0,0 @@
import FormMixin from '../../mixins/form'
import optionsinput from '../options_input'
import textinput from '../text_input'
import localdatetime from '../local_datetime'
export default {
name: 'financial',
mixins: [FormMixin],
components: {
optionsinput,
textinput,
localdatetime,
},
props: {
initialData: {
type: Object,
default: () => ({}),
},
},
data: function() {
const { funding_type = '' } = this.initialData
return {
funding_type,
shouldForceShowTaskOrder: false,
}
},
computed: {
showTaskOrderUpload: function() {
return (
!this.initialData.legacy_task_order.pdf || this.shouldForceShowTaskOrder
)
},
},
methods: {
forceShowTaskOrderUpload: function(e) {
console.log('forceShowTaskOrder', e)
e.preventDefault()
this.shouldForceShowTaskOrder = true
},
},
}

View File

@ -1,161 +0,0 @@
import LocalDatetime from '../components/local_datetime'
import { formatDollars } from '../lib/dollars'
import { parse } from 'date-fns'
import {
compose,
partial,
indexBy,
prop,
propOr,
sortBy,
reverse,
pipe,
} from 'ramda'
export default {
name: 'requests-list',
components: {
LocalDatetime,
},
props: {
requests: {
type: Array,
default: () => [],
},
isExtended: {
type: Boolean,
default: false,
},
statuses: {
type: Array,
default: () => [],
},
dodComponents: {
type: Array,
default: () => [],
},
},
data: function() {
const defaultSort = (sort, requests) =>
sortBy(prop(sort.columnName), requests)
const dateSort = (sort, requests) => {
const parseDate = compose(
partial(parse),
propOr(sort.columnName, '')
)
return sortBy(parseDate, requests)
}
const columnList = [
{
displayName: 'JEDI Cloud Request Name',
attr: 'name',
sortFunc: defaultSort,
},
{
displayName: 'Date Request Submitted',
attr: 'last_submission_timestamp',
sortFunc: dateSort,
},
{
displayName: 'Date Request Last Edited',
attr: 'last_edited_timestamp',
extendedOnly: true,
sortFunc: dateSort,
},
{
displayName: 'Requester',
attr: 'full_name',
extendedOnly: true,
sortFunc: defaultSort,
},
{
displayName: 'Applicationed Annual Usage ($)',
attr: 'annual_usage',
sortFunc: defaultSort,
},
{
displayName: 'Request Status',
attr: 'status',
sortFunc: defaultSort,
},
{
displayName: 'DOD Component',
attr: 'dod_component',
extendedOnly: true,
sortFunc: defaultSort,
},
]
const defaultSortColumn = this.isExtended ? 'last_submission_timestamp' : ''
return {
searchValue: '',
statusValue: '',
dodComponentValue: '',
sort: {
columnName: defaultSortColumn,
isAscending: false,
},
columns: indexBy(prop('attr'), columnList),
}
},
computed: {
filteredRequests: function() {
return pipe(
partial(this.applySearch, [this.searchValue]),
partial(this.applyFilters, [this.statusValue, this.dodComponentValue]),
partial(this.applySort, [this.sort])
)(this.requests)
},
},
methods: {
getColumns: function() {
return Object.values(this.columns).filter(
column => !column.extendedOnly || this.isExtended
)
},
applySearch: (query, requests) => {
return requests.filter(request =>
query !== ''
? request.name.toLowerCase().includes(query.toLowerCase())
: true
)
},
applyFilters: (status, dodComponent, requests) => {
return requests
.filter(request => (status !== '' ? request.status === status : true))
.filter(request =>
dodComponent !== '' ? request.dod_component === dodComponent : true
)
},
applySort: function(sort, requests) {
if (sort.columnName === '') {
return requests
} else {
const { sortFunc } = this.columns[sort.columnName]
const sorted = sortFunc(sort, requests)
return sort.isAscending ? sorted : reverse(sorted)
}
},
dollars: value => formatDollars(value, false),
updateSortValue: function(columnName) {
if (!this.isExtended) {
return
}
// toggle ascending / descending if column is clicked twice
if (columnName === this.sort.columnName) {
this.sort.isAscending = !this.sort.isAscending
}
this.sort.columnName = columnName
},
},
template: '<div></div>',
}

View File

@ -11,11 +11,9 @@ import optionsinput from './components/options_input'
import multicheckboxinput from './components/multi_checkbox_input' import multicheckboxinput from './components/multi_checkbox_input'
import textinput from './components/text_input' import textinput from './components/text_input'
import checkboxinput from './components/checkbox_input' import checkboxinput from './components/checkbox_input'
import DetailsOfUse from './components/forms/details_of_use'
import EditOfficerForm from './components/forms/edit_officer_form' import EditOfficerForm from './components/forms/edit_officer_form'
import poc from './components/forms/poc' import poc from './components/forms/poc'
import oversight from './components/forms/oversight' import oversight from './components/forms/oversight'
import financial from './components/forms/financial'
import toggler from './components/toggler' import toggler from './components/toggler'
import NewApplication from './components/forms/new_application' import NewApplication from './components/forms/new_application'
import EditEnvironmentRole from './components/forms/edit_environment_role' import EditEnvironmentRole from './components/forms/edit_environment_role'
@ -27,10 +25,8 @@ import selector from './components/selector'
import BudgetChart from './components/charts/budget_chart' import BudgetChart from './components/charts/budget_chart'
import SpendTable from './components/tables/spend_table' import SpendTable from './components/tables/spend_table'
import TaskOrderList from './components/tables/task_order_list.js' import TaskOrderList from './components/tables/task_order_list.js'
import CcpoApproval from './components/forms/ccpo_approval'
import MembersList from './components/members_list' import MembersList from './components/members_list'
import LocalDatetime from './components/local_datetime' import LocalDatetime from './components/local_datetime'
import RequestsList from './components/requests_list'
import ConfirmationPopover from './components/confirmation_popover' import ConfirmationPopover from './components/confirmation_popover'
import { isNotInVerticalViewport } from './lib/viewport' import { isNotInVerticalViewport } from './lib/viewport'
import DateSelector from './components/date_selector' import DateSelector from './components/date_selector'
@ -51,21 +47,17 @@ const app = new Vue({
multicheckboxinput, multicheckboxinput,
textinput, textinput,
checkboxinput, checkboxinput,
DetailsOfUse,
poc, poc,
oversight, oversight,
financial,
NewApplication, NewApplication,
selector, selector,
BudgetChart, BudgetChart,
SpendTable, SpendTable,
TaskOrderList, TaskOrderList,
CcpoApproval,
MembersList, MembersList,
LocalDatetime, LocalDatetime,
EditEnvironmentRole, EditEnvironmentRole,
EditApplicationRoles, EditApplicationRoles,
RequestsList,
ConfirmationPopover, ConfirmationPopover,
funding, funding,
uploadinput, uploadinput,

@ -1 +1 @@
Subproject commit 78c51d8dd29b47fd42570896daaded5e2181e923 Subproject commit eb9ea572e4c5157c8e7ba6105ac4efd1df39392e

View File

@ -1,30 +0,0 @@
from urllib.request import urlopen
import csv
# Add root project dir to the python path
import os
import sys
parent_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
sys.path.append(parent_dir)
from atst.app import make_app, make_config
from atst.domain.pe_numbers import PENumbers
def get_pe_numbers(url):
response = urlopen(url)
t = response.read().decode("utf-8")
return list(csv.reader(t.split("\r\n")))
if __name__ == "__main__":
config = make_config({"DISABLE_CRL_CHECK": True})
url = config["PE_NUMBER_CSV_URL"]
print("Fetching PE numbers from {}".format(url))
pe_numbers = get_pe_numbers(url)
app = make_app(config)
with app.app_context():
print("Inserting {} PE numbers".format(len(pe_numbers)))
PENumbers.create_many(pe_numbers)

View File

@ -1,5 +0,0 @@
Your JEDI request status has changed
The status of your JEDI Cloud request - {{ request.displayname }} - was recently updated. Log in to see whether this change requires an action or response from you.
{{ url_for('requests.edit', request_id=request.id, _external=True) }}

View File

@ -1,11 +0,0 @@
{% extends "portfolios/base.html" %}
{% from "components/pagination.html" import Pagination %}
{% set secondary_breadcrumb = "navigation.portfolio_navigation.breadcrumbs.admin" | translate %}
{% block portfolio_content %}
<div v-cloak>
{% include "fragments/audit_events_log.html" %}
{{ Pagination(audit_events, 'portfolios.portfolio_activity', portfolio_id=portfolio_id) }}
</div>
{% endblock %}

View File

@ -6,7 +6,6 @@
<thead> <thead>
<tr> <tr>
<th>Portfolio Name</th> <th>Portfolio Name</th>
<th>Task Order</th>
<th>Users</th> <th>Users</th>
</tr> </tr>
</thead> </thead>
@ -16,9 +15,6 @@
<td> <td>
<a class='icon-link icon-link--large' href="/portfolios/{{ portfolio.id }}/applications">{{ portfolio.name }}</a><br> <a class='icon-link icon-link--large' href="/portfolios/{{ portfolio.id }}/applications">{{ portfolio.name }}</a><br>
</td> </td>
<td>
#{{ portfolio.legacy_task_order.number }}
</td>
<td> <td>
<span class="label">{{ portfolio.user_count }}</span><span class='h6'>Users</span> <span class="label">{{ portfolio.user_count }}</span><span class='h6'>Users</span>
</td> </td>

View File

@ -1,49 +0,0 @@
{% extends "base.html" %}
{% block content %}
<div class="col">
{% include 'requests/menu.html' %}
{% include "fragments/flash.html" %}
{% block form_action %}
{% if request_id %}
<form method='POST' action="{{ url_for('requests.requests_form_update', screen=current, request_id=request_id) }}" autocomplete="off">
{% else %}
<form method='POST' action="{{ url_for('requests.requests_form_update', screen=current) }}" autocomplete="off">
{% endif %}
{% endblock %}
<div class="panel">
<div class="panel__heading">
<h1>{% block heading %}{% endblock %}</h1>
<div class="subtitle h2">{{ "requests._new.new_request" | translate }}</div>
</div>
<div class="panel__content">
{{ f.csrf_token }}
{% block form %}
form goes here
{% endblock %}
</div>
</div>
{% block next %}
<div class='action-group'>
<input type='submit' class='usa-button usa-button-primary' value='{{ "common.save_and_continue" | translate }}' />
</div>
{% endblock %}
</form>
</div>
{% endblock %}

View File

@ -1,215 +0,0 @@
{% macro RequiredLabel() -%}
<span class='label label--error'>Response Required</span>
{%- endmacro %}
{% macro DefinitionReviewField(title, section, item_name, filter=None, filter_args=[]) -%}
<div>
<dt>{{ title | safe }}</dt>
<dd>
{% set value = data.get(section, {}).get(item_name) %}
{% if value is not none %}
{{ value | findFilter(filter, filter_args) }}
{% else %}
{{ RequiredLabel() }}
{% endif %}
</dd>
</div>
{% endmacro %}
{% macro EditLink(screen) %}
{% if request_id %}
{{ url_for('requests.requests_form_update', screen=screen, request_id=request_id)}}
{% else %}
{{ url_for('requests.requests_form_update', screen=screen, request_id=None) }}
{% endif %}
{% endmacro %}
<h2>
Details of Use
{% if editable %}
<a href="{{ EditLink(1) }}" class="icon-link">
{{ Icon('edit') }}
<span>Edit this section</span>
</a>
{% endif %}
</h2>
<dl>
{{ DefinitionReviewField("DoD Component", "details_of_use", "dod_component", filter="getOptionLabel", filter_args=[service_branches]) }}
{{ DefinitionReviewField("JEDI Cloud Usage", "details_of_use", "jedi_usage") }}
{{ DefinitionReviewField("Number of software systems", "details_of_use", "num_software_systems", filter="readableInteger") }}
{{ DefinitionReviewField("JEDI Cloud Migration", "details_of_use", "jedi_migration") }}
{% if data['details_of_use']['jedi_migration'] == 'yes' %}
{{ DefinitionReviewField("Rationalization of Software Systems", "details_of_use", "rationalization_software_systems") }}
{{ DefinitionReviewField("Technical Support Team", "details_of_use", "technical_support_team") }}
{% if data['details_of_use']['technical_support_team'] == 'yes' %}
{{ DefinitionReviewField("Organization Providing Assistance", "details_of_use", "organization_providing_assistance", filter="getOptionLabel", filter_args=[assistance_org_types]) }}
{% endif %}
{{ DefinitionReviewField("Engineering Assessment", "details_of_use", "engineering_assessment") }}
{{ DefinitionReviewField("Data Transfers", "details_of_use", "data_transfers", filter="getOptionLabel", filter_args=[data_transfer_amounts]) }}
{{ DefinitionReviewField("Expected Completion Date", "details_of_use", "expected_completion_date", filter="getOptionLabel", filter_args=[completion_date_ranges]) }}
{% else %}
{{ DefinitionReviewField("Cloud Native", "details_of_use", "cloud_native") }}
{% endif %}
{{ DefinitionReviewField("Estimated Monthly Spend", "details_of_use", "estimated_monthly_spend", filter="dollars") }}
{% if jedi_request and jedi_request.annual_spend > annual_spend_threshold %}
{{ DefinitionReviewField("Number of User Sessions", "details_of_use", "number_user_sessions", filter="readableInteger") }}
{{ DefinitionReviewField("Average Daily Traffic (Number of Requests)", "details_of_use", "average_daily_traffic", filter="readableInteger") }}
{{ DefinitionReviewField("Average Daily Traffic (GB)", "details_of_use", "average_daily_traffic_gb", filter="readableInteger") }}
{% endif %}
{{ DefinitionReviewField("Total Spend", "details_of_use", "dollar_value", filter="dollars") }}
{{ DefinitionReviewField("Start Date", "details_of_use", "start_date") }}
{{ DefinitionReviewField("Request Name", "details_of_use", "name") }}
</dl>
<hr>
<h2>
Information About You
{% if editable %}
<a href="{{ EditLink(2) }}" class="icon-link">
{{ Icon('edit') }}
<span>Edit this section</span>
</a>
{% endif %}
</h2>
<dl>
{{ DefinitionReviewField("First Name", "information_about_you", "fname_request") }}
{{ DefinitionReviewField("Last Name", "information_about_you", "lname_request") }}
{{ DefinitionReviewField("Email Address", "information_about_you", "email_request") }}
<div>
<dt>Phone Number</dt>
<dd>
{% if data.information_about_you.phone_number is not none %}
{{ data.information_about_you.phone_number }}
{% else %}
{{ RequiredLabel() }}
{% endif %}
{% if data.information_about_you.phone_ext %}
ext. {{ data.information_about_you.phone_ext }}
{% endif %}
</dd>
</div>
{{ DefinitionReviewField("Service Branch or Agency", "information_about_you", "service_branch", filter="getOptionLabel", filter_args=[service_branches]) }}
{{ DefinitionReviewField("Citizenship", "information_about_you", "citizenship") }}
{{ DefinitionReviewField("Designation of Person", "information_about_you", "designation", filter="capitalize") }}
{{ DefinitionReviewField("Latest Information Assurance (IA) Training completion date", "information_about_you", "date_latest_training") }}
</dl>
<hr>
<h2>
Portfolio Owner
{% if editable %}
<a href="{{ EditLink(3) }}" class="icon-link">
{{ Icon('edit') }}
<span>Edit this section</span>
</a>
{% endif %}
</h2>
<dl>
{{ DefinitionReviewField("POC First Name", "primary_poc", "fname_poc") }}
{{ DefinitionReviewField("POC Last Name", "primary_poc", "lname_poc") }}
{{ DefinitionReviewField("POC Email Address", "primary_poc", "email_poc") }}
{{ DefinitionReviewField("DoD ID", "primary_poc", "dodid_poc") }}
</dl>
{% if jedi_request.has_financial_data %}
<hr>
<h2>
Financial Verification
</h2>
<div>
{% if jedi_request.legacy_task_order.pdf %}
<a href="{{ url_for("requests.task_order_pdf_download", request_id=request_id)}}" download>
Download the Task Order PDF
</a>
{% else %}
<p>No Task Order PDF attached.</p>
{% endif %}
</div>
<dl>
{{ DefinitionReviewField("Task Order Information Source", "legacy_task_order", "source", filter="getOptionLabel", filter_args=[task_order_sources]) }}
{{ DefinitionReviewField("Task Order Number", "legacy_task_order", "number") }}
{{ DefinitionReviewField("What is the source of funding?", "legacy_task_order", "funding_type", filter="getOptionLabel", filter_args=[funding_types]) }}
{% if data["legacy_task_order"] and data["legacy_task_order"]["funding_type"].value == "OTHER" %}
{{ DefinitionReviewField("If other, please specify", "legacy_task_order", "funding_type_other") }}
{% endif %}
{{ DefinitionReviewField("Task Order Expiration Date", "legacy_task_order", "expiration_date") }}
{{ DefinitionReviewField("<dl><dt>CLIN 0001</dt> - <dd>Unclassified IaaS and PaaS Amount</dd></dl>", "legacy_task_order", "clin_0001", filter="dollars") }}
{{ DefinitionReviewField("<dl><dt>CLIN 0003</dt> - <dd>Unclassified Cloud Support Package</dd></dl>", "legacy_task_order", "clin_0003", filter="dollars") }}
{{ DefinitionReviewField("<dl><dt>CLIN 1001</dt> - <dd>Unclassified IaaS and PaaS Amount <br> OPTION PERIOD 1</dd></dl>", "legacy_task_order", "clin_1001", filter="dollars") }}
{{ DefinitionReviewField("<dl><dt>CLIN 1003</dt> - <dd>Unclassified Cloud Support Package <br> OPTION PERIOD 1</dd></dl>", "legacy_task_order", "clin_1003", filter="dollars") }}
{{ DefinitionReviewField("<dl><dt>CLIN 2001</dt> - <dd>Unclassified IaaS and PaaS Amount <br> OPTION PERIOD 2</dd></dl>", "legacy_task_order", "clin_2001", filter="dollars") }}
{{ DefinitionReviewField("<dl><dt>CLIN 2003</dt> - <dd>Unclassified Cloud Support Package <br> OPTION PERIOD 2</dd></dl>", "legacy_task_order", "clin_2003", filter="dollars") }}
{{ DefinitionReviewField("Unique Item Identifier (UII)s related to your application(s) if you already have them", "financial_verification", "uii_ids", filter="renderList") }}
{{ DefinitionReviewField("Program Element (PE) Number related to your request", "financial_verification", "pe_id") }}
{{ DefinitionReviewField("Program Treasury Code", "financial_verification", "treasury_code") }}
{{ DefinitionReviewField("Program Budget Activity (BA) Code", "financial_verification", "ba_code") }}
{{ DefinitionReviewField("Contracting Officer First Name", "financial_verification", "fname_co") }}
{{ DefinitionReviewField("Contracting Officer Last Name", "financial_verification", "lname_co") }}
{{ DefinitionReviewField("Contracting Officer Email", "financial_verification", "email_co") }}
{{ DefinitionReviewField("Contracting Officer Office", "financial_verification", "office_co") }}
{{ DefinitionReviewField("Contracting Officer Representative (COR) First Name", "financial_verification", "fname_cor") }}
{{ DefinitionReviewField("Contracting Officer Representative (COR) Last Name", "financial_verification", "lname_cor") }}
{{ DefinitionReviewField("Contracting Officer Representative (COR) Email", "financial_verification", "email_cor") }}
{{ DefinitionReviewField("Contracting Officer Representative (COR) Office", "financial_verification", "office_cor") }}
</dl>
{% endif %}

View File

@ -1,271 +0,0 @@
{% extends "base.html" %}
{% from "components/icon.html" import Icon %}
{% from "components/text_input.html" import TextInput %}
{% from "components/phone_input.html" import PhoneInput %}
{% block content %}
<article class='col col--grow request-approval'>
{% include "fragments/flash.html" %}
<section class='panel'>
<header class='panel__heading panel__heading--divider request-approval__heading'>
<h1 class='h2'>
{{ "requests.approval.request_title" | translate({ "displayname": jedi_request.displayname }) }}
</h1>
<span class='label label--info'>{{ current_status }}</span>
</header>
<div class='panel__content'>
{% with data=data, request_id=jedi_request.id %}
{% include "requests/_review.html" %}
{% endwith %}
</div>
</section>
<section class='internal-notes' id='ccpo-notes'>
<form method="POST" action="{{ url_for('requests.create_internal_comment', request_id=jedi_request.id) }}">
<div class='panel'>
<div class='panel__heading panel__heading--divider'>
<h2>{{ "requests.approval.ccpo_internal_comments" | translate }}</h2>
</div>
<div class='comment-log'>
{% if comments %}
<ol>
{% for comment in comments %}
<li>
<article class='comment-log__log-item'>
<div>
<h3 class='comment-log__log-item__header'>{{ comment.user.full_name }}</h3>
<p>{{ comment.text }}</p>
</div>
{% set timestamp=comment.time_created | formattedDate("%Y-%m-%d %H:%M:%S %Z") %}
<footer class='comment-log__log-item__timestamp'>
<local-datetime timestamp='{{ timestamp }}'></local-datetime>
</footer>
</article>
</li>
{% endfor %}
</ol>
{% else %}
<div class='panel__content'>
<p class='h4'>
{{ "requests.approval.no_ccpo_comments" | translate }}
</p>
</div>
{% endif %}
</div>
<div class='panel__heading internal-notes__add-comment__heading'>
<h3 class='h3'>
{{ "requests.approval.add_comment" | translate }}
</h3>
</div>
<div class='panel__content'>
{{ internal_comment_form.csrf_token }}
{{ TextInput(internal_comment_form.text, paragraph=True, noMaxWidth=True) }}
</div>
</div>
<div class='action-group action-group--tight'>
<button class='usa-button' type="submit">
{{ "requests.approval.save_notes" | translate }}
</button>
</div>
</form>
</section>
<section class='request-approval__review'>
<form method="POST" action="{{ url_for("requests.submit_approval", request_id=jedi_request.id) }}" autocomplete="off">
{{ review_form.csrf_token }}
{% set initialState = 'approving' if review_form.errors else '' %}
<ccpo-approval inline-template initial-state="{{ initialState }}">
<div>
<div class='panel'>
<header class='panel__heading panel__heading--divider'>
<h2 class='request-approval__columns__heading'>
{{ "requests.approval.ccpo_review_activity" | translate }}
</h2>
</header>
<div class='approval-log'>
{% if reviews %}
<ol>
{% for review in reviews %}
<li>
<article class='approval-log__log-item'>
<div>
<h3 class='approval-log__log-item__header'>{{ review.status.log_name }} by {{ review.full_name_reviewer }}</h3>
{% if review.comment %}
<p>{{ review.comment }}</p>
{% endif %}
<div class='approval-log__behalfs'>
{% if review.lname_mao %}
<div class='approval-log__behalf'>
<h3 class='approval-log__log-item__header'>
{{ "requests.approval.mission_owner_approval_on_behalf_of" | translate }}
</h3>
<span>{{ review.full_name_mao }}</span>
<span>{{ review.email_mao }}</span>
<span>
{{ review.phone_mao }}
{% if review.phone_ext_mao %}
ext. {{ review.phone_ext_mao }}
{% endif %}
</span>
</div>
{% endif %}
{% if review.lname_ccpo %}
<div class='approval-log__behalf'>
<h3 class='approval-log__log-item__header'>
{{ "requests.approval.ccpo_approval_on_behalf_of" | translate }}
</h3>
<span>{{ review.full_name_ccpo }}</span>
</div>
{% endif %}
</div>
</div>
{% set timestamp=review.status.time_created | formattedDate("%Y-%m-%d %H:%M:%S %Z") %}
<footer class='approval-log__log-item__timestamp'>
<local-datetime timestamp='{{ timestamp }}'></local-datetime>
</footer>
</article>
</li>
{% endfor %}
</ol>
{% else %}
<div class='panel__content'>
<p class='h4'>
{{ "requests.approval.no_ccpo_approval_request_changes" | translate }}
</p>
</div>
{% endif %}
</div>
{% if jedi_request.is_pending_ccpo_action %}
<div class='panel__heading request-approval__review__heading'>
<h3 class='h3'>
{{ "requests.approval.review_request" | translate }}
</h3>
</div>
<div class='panel__content'>
<div class='usa-input'>
<fieldset class='usa-input__choices usa-input__choices--inline'>
<input v-on:change='setReview' type='radio' name='review' id='review-approving' value='approving' {{ 'checked' if initialState == 'approving' }}/>
<label for='review-approving'>
{{ "requests.approval.ready_for_approval" | translate }}
</label>
<input v-on:change='setReview' type='radio' name='review' id='review-denying' value='denying'/>
<label for='review-denying'>
{{ "requests.approval.request_revisions" | translate }}
</label>
</fieldset>
</div>
<div v-if='approving || denying' class='form__sub-fields' v-cloak>
<h3>Message to Requestor <span class='subtitle'>(optional)</span></h3>
<div v-if='approving' key='approving' v-cloak>
{{ TextInput(
review_form.comment,
label=("requests.approval.approve_comments_or_notes_label" | translate),
description=("requests.approval.approve_comments_or_notes_description" | translate),
paragraph=True,
noMaxWidth=True
) }}
</div>
<div v-else key='denying' v-cloak>
{{ TextInput(
review_form.comment,
label=("requests.approval.revision_instructions_or_notes_label" | translate),
paragraph=True,
noMaxWidth=True
) }}
</div>
</div>
<div v-if='approving' class='form__sub-fields' v-cloak>
<h3>
{{ "requests.approval.authorizing_officials_title" | translate }}
<span class='subtitle'>(optional)</span>
</h3>
<p>
{{ "requests.approval.authorizing_officials_paragraph" | translate }}
</p>
<hr />
<h4>
{{ "requests.approval.mission_authorizing_official_title" | translate }}
</h4>
<div class='form-row'>
<div class='form-col form-col--half'>
{{ TextInput(review_form.fname_mao, placeholder="First name of mission authorizing official") }}
</div>
<div class='form-col form-col--half'>
{{ TextInput(review_form.lname_mao, placeholder="Last name of mission authorizing official") }}
</div>
</div>
{{ TextInput(review_form.email_mao, placeholder="name@mail.mil", validation='email') }}
{{ PhoneInput(review_form.phone_mao, review_form.phone_ext_mao) }}
<hr />
<h4>
{{ "requests.approval.ccpo_authorizing_official_title" | translate }}
</h4>
<div class='form-row'>
<div class='form-col form-col--half'>
{{ TextInput(review_form.fname_ccpo, placeholder="First name of CCPO authorizing official") }}
</div>
<div class='form-col form-col--half'>
{{ TextInput(review_form.lname_ccpo, placeholder="Last name of CCPO authorizing official") }}
</div>
</div>
</div>
</div>
{% endif %}
</div>
{% if jedi_request.is_pending_ccpo_action %}
<div v-if='approving || denying' class='action-group' v-cloak>
<button v-if='approving' type="submit" name="approved" class='usa-button usa-button-big'>Approve Request</button>
<button v-if='denying' type="submit" name="denied" class='usa-button usa-button-big'>Request Revisions</button>
<a href='{{ url_for("requests.requests_index") }}' class='icon-link'>
{{ Icon('x') }}
<span>Cancel</span>
</a>
</div>
{% endif %}
</div>
</ccpo-approval>
</form>
</section>
</article>
{% endblock %}

View File

@ -1,30 +0,0 @@
{% extends "base.html" %}
{% from "components/alert.html" import Alert %}
{% block content %}
<div class="col">
{% if jedi_request.is_pending_ccpo_acceptance %}
{{ Alert('Request submitted. Approval pending.', fragment="fragments/pending_ccpo_acceptance_alert.html") }}
{% elif jedi_request.is_pending_ccpo_approval %}
{{ Alert('Request submitted. Approval pending.', fragment="fragments/pending_ccpo_approval_modal.html") }}
{% elif requires_fv_action %}
{% include 'requests/review_menu.html' %}
{{ Alert('Pending Financial Verification', fragment="fragments/pending_financial_verification.html") }}
{% endif %}
<div class="panel">
<div class="panel__heading">
<h1>Request Details</h1>
<div class="subtitle"><h2>Request: {{ jedi_request.displayname }}</h2><span class="label label--info">{{ jedi_request.status_displayname }}</span></div>
</div>
<div class="panel__content">
{% include "requests/_review.html" %}
</div>
</div>
</div>
{% endblock %}

View File

@ -1,220 +0,0 @@
{% extends "base.html" %}
{% from "components/alert.html" import Alert %}
{% from "components/text_input.html" import TextInput %}
{% from "components/options_input.html" import OptionsInput %}
{% from "components/date_input.html" import DateInput %}
{% block content %}
{% include 'requests/review_menu.html' %}
{% include "fragments/flash.html" %}
{% if saved_draft %}
{% call Alert(("requests.financial_verification.draft_saved" | translate), level='success') %}
{% endcall %}
{% endif %}
{% if jedi_request.is_pending_financial_verification and not f.errors and not extended %}
{{ Alert(("requests.financial_verification.pending_financial_verification" | translate), fragment="fragments/pending_financial_verification.html") }}
{% endif %}
<financial inline-template v-bind:initial-data='{{ f.data|tojson }}'>
<div class="col">
{% if extended %}
{{ Alert(("requests.financial_verification.manually_enter_task_information_label" | translate),
message=("requests.financial_verification.manually_enter_task_information_description" | translate),
level='warning',
actions=[
{
'href': url_for('atst.helpdocs'),
'label': ("requests.financial_verification.manually_enter_task_information_help_label" | translate),
'icon': 'help'
}
]
) }}
{% endif %}
{% if f.is_missing_task_order_number %}
{% set extended_url = url_for('requests.financial_verification', request_id=jedi_request.id, extended=True) %}
{% call Alert(("requests.financial_verification.task_order_not_found_eda_label"), level='warning') %}
{{ "requsts.financial_verification.task_order_not_found_eda_description" | translate }}
<br>
<a class="usa-button" href="{{ extended_url }}">
{{ "requests.financial_verification.enter_task_order_manually_link_text" | translate }}
</a>
{% endcall %}
{% endif %}
<form autocomplete="off" enctype="multipart/form-data">
{{ f.csrf_token }}
{% block form %}
{% autoescape false %}
{% if f.errors and not f.is_only_missing_task_order_number %}
{{ Alert(("requests.financial_verification.some_errors_label" | translate),
message="<p>Please see below.</p>",
level='error'
) }}
{% endif %}
<div class="panel">
<div class="panel__heading">
<h1>{{ "requests.financial_verification.financial_verification_title" | translate }}</h1>
<div class="subtitle" id="financial-verification">
<h2>
{{ "requests.financial_verification.request_title" | translate({ "displayname" : jedi_request.displayname }) }}
</h2>
</div>
</div>
<div class="panel__content">
<p>
{{ "requests.financial_verification.permissions_paragraph" | translate }}
</p>
{% if extended %}
<fieldset class="form__sub-fields form__sub-fields--warning">
{{ OptionsInput(f.legacy_task_order.funding_type) }}
<template v-if="funding_type == 'OTHER'" v-cloak>
{{ TextInput(f.legacy_task_order.funding_type_other) }}
</template>
{{
DateInput(
f.legacy_task_order.expiration_date,
placeholder='MM / DD / YYYY',
validation='date',
tooltip=("requests.financial_verification.expiration_date_placeholder" | translate)
)
}}
{{ TextInput(
f.legacy_task_order.clin_0001,
validation='dollars'
) }}
{{ TextInput(
f.legacy_task_order.clin_0003,
validation='dollars'
) }}
{{ TextInput(
f.legacy_task_order.clin_1001,
validation='dollars'
) }}
{{ TextInput(
f.legacy_task_order.clin_1003,
validation='dollars'
) }}
{{ TextInput(
f.legacy_task_order.clin_2001,
validation='dollars'
) }}
{{ TextInput(
f.legacy_task_order.clin_2003,
validation='dollars'
) }}
<template v-if="showTaskOrderUpload">
<div class="usa-input {% if f.legacy_task_order.pdf.errors %} usa-input--error {% endif %}">
{{ f.legacy_task_order.pdf.label }}
{{ f.legacy_task_order.pdf }}
{% for error in f.legacy_task_order.pdf.errors %}
<span class="usa-input__message">{{error}}</span>
{% endfor %}
</div>
</template>
<template v-else>
<p>Uploaded {{ f.legacy_task_order.pdf.data }}.</p>
<div>
<button v-on:click="forceShowTaskOrderUpload($event)">Change</button>
</div>
</template>
</fieldset>
{% endif %}
{{ TextInput(
f.legacy_task_order.number,
placeholder="e.g.: 1234567899C0001",
tooltip=("requests.financial_verification.number_placeholder" | translate),
validation="requiredField"
) }}
{{ TextInput(f.request.uii_ids,
paragraph=True,
placeholder="examples: \nDI 0CVA5786950 \nUN1945326361234786950",
tooltip=("requests.financial_verification.uui_ids_placeholder" | translate)
) }}
{{ TextInput(f.request.pe_id,
placeholder="e.g.: 0105688F",
validation="peNumber"
) }}
{{ TextInput(f.request.treasury_code,placeholder="e.g.: 00123456",validation="treasuryCode") }}
{{ TextInput(f.request.ba_code,placeholder="e.g.: 02A",validation="baCode") }}
<hr />
<h3>
{{ "requests.financial_verification.contracting_officer_information_title" | translate }}
</h3>
<div class='form-row'>
<div class='form-col form-col--half '>{{ TextInput(f.request.fname_co, validation="requiredField") }}</div>
<div class='form-col form-col--half '>{{ TextInput(f.request.lname_co, validation="requiredField") }}</div>
</div>
<div class='form-row'>
<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.request.office_co, validation="requiredField", placeholder="e.g.: WHS") }}</div>
</div>
<hr />
<h3>
{{ "requests.financial_verification.contracting_officer_representative_information_title" | translate }}
</h3>
<div class='form-row'>
<div class='form-col form-col--half '>{{ TextInput(f.request.fname_cor, validation="requiredField") }}</div>
<div class='form-col form-col--half '>{{ TextInput(f.request.lname_cor, validation="requiredField") }}</div>
</div>
<div class='form-row'>
<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.request.office_cor, validation="requiredField", placeholder="e.g.: WHS") }}</div>
</div>
{% endautoescape %}
</div>
</div>
{% endblock form %}
{% block next %}
<div class='action-group'>
<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' />
{% if jedi_request.last_finver_draft_saved_at %}
<em>Draft saved at <localdatetime :timestamp="'{{ jedi_request.last_finver_draft_saved_at.isoformat() }}'"></localdatetime></em>
{% endif %}
</div>
{% endblock %}
</form>
</div>
</financial>
{% endblock %}

View File

@ -1,18 +0,0 @@
{% extends "base.html" %}
{% block content %}
<div class="col">
<div class="panel">
<div class="panel__content">
<div class="panel__heading">
<h2 id="financial-verification">Submitted</h2>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -1,178 +0,0 @@
{% extends "base.html" %}
{% from "components/modal.html" import Modal %}
{% from "components/empty_state.html" import EmptyState %}
{% from "components/icon.html" import Icon %}
{% block content %}
{% call Modal(name='pendingFinancialVerification', dismissable=True) %}
<h1>{{ "requests.index.request_submitted_title" | translate }}</h1>
{% include 'fragments/pending_financial_verification.html' %}
<div class='action-group'>
<button autofocus type=button v-on:click="closeModal('pendingFinancialVerification')" class='action-group__action usa-button'>Close</button>
</div>
{% endcall %}
{% call Modal(name='pendingCCPOApproval', dismissable=True) %}
<h1>{{ "requests.index.financial_verification_submitted_title" | translate }}</h1>
{% include 'fragments/pending_ccpo_approval_modal.html' %}
<div class='action-group'>
<button autofocus type='button' v-on:click="closeModal('pendingCCPOApproval')" class='action-group__action usa-button'>Close</button>
</div>
{% endcall %}
{% call Modal(name='pendingCCPOAcceptance', dismissable=True) %}
<h1>{{ "requests.index.request_submitted_title" | translate }}</h1>
{% include 'fragments/pending_ccpo_acceptance_alert.html' %}
<div class='action-group'>
<button autofocus type='button' v-on:click="closeModal('pendingCCPOAcceptance')" class='action-group__action usa-button'>Close</button>
</div>
{% endcall %}
<requests-list
inline-template
v-bind:requests='{{ requests | tojson }}'
v-bind:is-extended='{{ extended_view | tojson }}'
v-bind:statuses='{{ possible_statuses | tojson }}'
v-bind:dod-components='{{ possible_dod_components | tojson }}'
>
<div>
{% include "fragments/flash.html" %}
{% if not requests %}
{{ EmptyState(
("requests.index.no_portfolios_label" | translate),
sub_message=("requests.index.no_portfolios_sub_message" | translate),
action_label=("requests.index.no_portfolios_action_label" | translate),
action_href=url_for('requests.requests_form_new', screen=1),
icon='document'
) }}
{% else %}
{% if extended_view %}
<div class="row kpi">
<div class="kpi__item col col--grow">
<div class="kpi__item__value">{{ kpi_inprogress }}</div>
<div class="kpi__item__description">{{ "requests.index.requests_in_progress" | translate }}</div>
</div>
<div class="kpi__item col col--grow">
<div class="kpi__item__value">{{ kpi_pending }}</div>
<div class="kpi__item__description">{{ "requests.index.pending_ccpo_action" | translate }}</div>
</div>
<div class="kpi__item col col--grow">
<div class="kpi__item__value">{{ kpi_completed }}</div>
<div class="kpi__item__description">{{ "requests.index.approved_requests" | translate }}</div>
</div>
</div>
{% endif %}
<div v-cloak class="col col--grow">
{% if extended_view %}
<form @submit.prevent class='search-bar'>
<div class='usa-input search-input'>
<label for='requests-search'>{{ "requests.index.search_by_name" | translate }}</label>
<input v-model='searchValue' type='search' id='requests-search' name='requests-search' placeholder="Search by name"/>
<button>
<span class="hide">{{ "requests.index.search_button_text" | translate }}</span>
</button>
</div>
<div class="search-bar__filters">
<div class='usa-input'>
<label for='filter-status'>{{ "requests.index.filter_requests_by_status_label" | translate }}</label>
<select v-model="statusValue" id="filter-status" name="filter-status">
<option value="" selected disabled>{{ "requests.index.filter_by_status" | translate }}</option>
<option value="">{{ "requests.index.all_filter" | translate }}</option>
<option v-for="status in statuses" :value="status">!{ status }</option>
</select>
</div>
<div class='usa-input'>
<label for='filter-dod-component'>{{ "requests.index.filter_requests_by_dod_component" | translate }}</label>
<select v-model="dodComponentValue" id="filter-dod-component" name="filter-dod-component">
<option value="" selected disabled>{{ "requests.index.filter_by_dod_component" | translate }}</option>
<option value="">{{ "requests.index.all_filter" | translate }}</option>
<option v-for="dodComponent in dodComponents" :value="dodComponent">!{ dodComponent }</option>
</select>
</div>
</div>
</form>
{% endif %}
<div v-cloak class='responsive-table-wrapper'>
<table v-if="filteredRequests.length">
<thead>
<tr>
<th @click.prevent="updateSortValue(column.attr)" v-for="column in getColumns()"scope="col">
!{ column.displayName }
<span class="sorting-direction" v-if="column.attr === sort.columnName && sort.isAscending">
{{ Icon("caret_down") }}
</span>
<span class="sorting-direction" v-else-if="column.attr === sort.columnName && !sort.isAscending">
{{ Icon("caret_up") }}
</span>
</th>
</tr>
</thead>
<tbody v-for="r in filteredRequests">
<tr>
<th scope="row">
<a class='icon-link icon-link--large' :href="r.edit_link">!{ r.name }</a>
<span v-if="r.action_required" class="label label--info">
{{ "requests.index.action_required" | translate }}
</span>
</th>
<td>
<local-datetime
v-if="r.last_submission_timestamp"
:timestamp="r.last_submission_timestamp"
format="M/D/YYYY">
</local-datetime>
<span v-else><span>
</td>
{% if extended_view %}
<td><local-datetime :timestamp="r.last_edited_timestamp" format="M/D/YYYY"></td>
<td>!{ r.full_name }</td>
{% endif %}
<td>!{ dollars(r.annual_usage) }</td>
<td>
<a v-if="r.is_approved" class="icon-link icon-link--large" :href="r.portfolio_link">
!{ r.status }
</a>
<span v-else>
!{ r.status }
</span>
</td>
{% if extended_view %}
<td>!{ r.dod_component }</td>
{% endif %}
</tr>
</tbody>
</table>
<div v-else>
{{ EmptyState(
("requests.index.no_requests_found" | translate),
action_label=None,
action_href=None,
sub_message=("requests.index.try_different_search" | translate),
icon=None
) }}
</div>
</div>
</div>
{% endif %}
</div>
</requests-list>
{% endblock %}

View File

@ -1,21 +0,0 @@
<div class="progress-menu progress-menu--four">
<ul>
{% for s in screens %}
{% if jedi_request and s.section in jedi_request.body %}
{% set step_indicator = 'complete' %}
{% elif loop.index == current %}
{% set step_indicator = 'active' %}
{% else %}
{% set step_indicator = 'incomplete' %}
{% endif %}
<li class="progress-menu__item progress-menu__item--{{ step_indicator }}">
<a href="{{ url_for('requests.requests_form_update', screen=loop.index, request_id=request_id) if request_id else url_for('requests.requests_form_new', screen=loop.index) }}"
{% if g.matchesPath('/requests/new/{{ loop.index + 1 }}') %}class="active"{% endif %}
>
{{ s['title'] }}
</a>
</li>
{% endfor %}
</ul>
</div>

View File

@ -1,21 +0,0 @@
{% set pending_url=url_for('requests.view_request_details', request_id=jedi_request.id) %}
{% set financial_url=url_for('requests.financial_verification', request_id=jedi_request.id) %}
<div class="progress-menu progress-menu--two">
<ul>
<li class="progress-menu__item progress-menu__item--complete">
<a href="{{ pending_url }}">
{{ "requests.review_menu.request_information_link_text" | translate }}
</a>
</li>
{% if g.matchesPath(financial_url) %}
{% set financial_status="active" %}
{% else %}
{% set financial_status="incomplete" %}
{% endif %}
<li class="progress-menu__item progress-menu__item--{{ financial_status }}">
<a href="{{ financial_url }}">
{{ "requests.review_menu.financial_verification_information_link_text" | translate }}
</a>
</li>
</ul>
</div>

View File

@ -1,143 +0,0 @@
{% extends 'requests/_new.html' %}
{% from "components/text_input.html" import TextInput %}
{% from "components/options_input.html" import OptionsInput %}
{% from "components/date_input.html" import DateInput %}
{% block heading %}
Details of Use
{% endblock %}
{% block form %}
{% include "fragments/flash.html" %}
<details-of-use inline-template v-bind:initial-data='{{ f.data|tojson }}'>
<div>
{{ "requests.screen-1.form_instructions" | translate }}
<h2>{{ "requests.screen-1.general_title_text"| translate }}</h2>
{{ OptionsInput(f.dod_component) }}
{{
TextInput(
f.jedi_usage,
paragraph=True,
placeholder=("requests.screen-1.jedi_usage_placeholder" | translate)
)
}}
<h2>{{ "requests.screen-1.cloud_readiness_title_text" | translate }}</h2>
{{
TextInput(
f.num_software_systems,
validation="integer",
tooltip=("requests.screen-1.num_software_systems_tooltip" | translate),
placeholder="0"
)
}}
{{
OptionsInput(
f.jedi_migration,
tooltip=("requests.screen-1.jedi_migration_tooltip" | translate)
)
}}
<transition name='slide'>
<template v-if="jediMigrationOptionSelected">
<fieldset class='form__sub-fields' v-if='isJediMigration' v-cloak>
<legend class='usa-sr-only'>
{{ "requests.screen-1.questions_title_text" | translate }}
</legend>
{{
OptionsInput(
f.rationalization_software_systems,
tooltip=("requests.screen-1.rationalization_software_systems_tooltip" | translate)
)
}}
{{ OptionsInput(f.technical_support_team) }}
<transition name='slide'>
<template v-if="hasTechnicalSupportTeam">
{{ OptionsInput(f.organization_providing_assistance) }}
</template>
</transition>
{{ OptionsInput(
f.engineering_assessment,
tooltip=("requests.screen-1.engineering_assessment_tooltip" | translate)
)
}}
{{ OptionsInput(f.data_transfers) }}
{{ OptionsInput(f.expected_completion_date) }}
</fieldset>
<template v-if='!isJediMigration' v-cloak>
{{
OptionsInput(
f.cloud_native,
tooltip=("requests.screen-1.cloud_native_tooltip" | translate)
)
}}
</template>
</template>
</transition>
<h2>{{ "requests.screen-1.financial_usage_title" | translate }}</h2>
{{
TextInput(
f.estimated_monthly_spend,
tooltip=("requests.screen-1.estimated_monthly_spend_tooltip" | translate),
validation="dollars",
placeholder="$0"
)
}}
<div class='alert alert-info'>
<div class='alert__content'>
<div class='alert__message'>
{{ "requests.screen-1.approximate_annual_spend_paragraph" | translate }}
</div>
</div>
</div>
<transition name='slide'>
<template v-if="annualSpend > {{ annual_spend_threshold }}">
<fieldset class='form__sub-fields'>
<h3>
{{ "requests.screen-1.additional_plan_details_title" | translate }}
</h3>
{{ TextInput(f.number_user_sessions, validation='integer', placeholder="0") }}
{{
TextInput(
f.average_daily_traffic,
tooltip=("requests.screen-1.average_daily_traffic_tooltip" | translate),
validation="integer",
placeholder="0"
)
}}
{{
TextInput(
f.average_daily_traffic_gb,
tooltip=("requests.screen-1.average_daily_traffic_gb_tooltip" | translate),
validation="gigabytes",
placeholder="0GB"
)
}}
</fieldset>
</template>
</transition>
{{
TextInput(
f.dollar_value,
validation='dollars',
placeholder='$0',
tooltip=("requests.screen-1.dollar_value_tooltip" | translate)
)
}}
{{ DateInput(f.start_date, placeholder='MM / DD / YYYY', validation='date') }}
{{ TextInput(f.name, placeholder='Request Name', validation='portfolioName') }}
</div>
</details-of-use>
{% endblock %}

View File

@ -1,32 +0,0 @@
{% extends 'requests/_new.html' %}
{% from "components/text_input.html" import TextInput %}
{% from "components/options_input.html" import OptionsInput %}
{% from "components/date_input.html" import DateInput %}
{% from "components/phone_input.html" import PhoneInput %}
{% block heading %}
Information About You
{% endblock %}
{% block form %}
{% include "fragments/flash.html" %}
<p>Please tell us more about you.</p>
<div class='form-row'>
<div class='form-col form-col--half'>{{ TextInput(f.fname_request) }}</div>
<div class='form-col form-col--half'>{{ TextInput(f.lname_request) }}</div>
</div>
{{ TextInput(f.email_request, placeholder='e.g. jane@mail.mil', validation='email') }}
{{ PhoneInput(f.phone_number, f.phone_ext, placeholder_phone='e.g. (123) 456-7890') }}
<p>We want to collect the following information from you for security auditing and determining priviledged user access.</p>
{{ OptionsInput(f.service_branch) }}
{{ OptionsInput(f.citizenship) }}
{{ OptionsInput(f.designation) }}
{{ DateInput(f.date_latest_training,tooltip="When was the last time you completed the IA training? <br> Information Assurance (IA) training is an important step in cyber awareness.",placeholder="MM / DD / YYYY", validation="date") }}
{% endblock %}

View File

@ -1,48 +0,0 @@
{% extends 'requests/_new.html' %}
{% from "components/text_input.html" import TextInput %}
{% from "components/checkbox_input.html" import CheckboxInput %}
{% block heading %}
Designate a Portfolio Owner
{% endblock %}
{% block form %}
{% include "fragments/flash.html" %}
<poc inline-template v-bind:initial-data='{{ f.data|tojson }}'>
<div>
<p>The Portfolio Owner is the primary point of contact and technical administrator of the JEDI Cloud Portfolio and will have the
following responsibilities:</p>
<ul>
<li>Organize your cloud-hosted systems into applications and environments</li>
<li>Add users to this portfolio and manage members</li>
<li>Manage access to the JEDI Cloud service providers portal</li>
</ul>
</p>
<p>This person must be a DoD employee (not a contractor).</p>
<p>The Portfolio Owner may be you. You will be able to add other administrators later. This person will be invited via email
once your request is approved.</p>
{{ CheckboxInput(f.am_poc) }}
<template v-if="!am_poc" v-cloak>
<div class='form-row'>
<div class='form-col form-col--half'>{{ TextInput(f.fname_poc) }}</div>
<div class='form-col form-col--half'>{{ TextInput(f.lname_poc) }}</div>
</div>
<div class='form-row'>
<div class='form-col form-col--half'>{{ TextInput(f.email_poc, validation='email', placeholder='e.g. jane@mail.mil') }}</div>
<div class='form-col form-col--half'>{{ TextInput(f.dodid_poc, validation='dodId', placeholder='10-digit number on back of CAC') }}</div>
</div>
</template>
</div>
</poc>
{% endblock %}

View File

@ -1,40 +0,0 @@
{% macro RequiredLabel() -%}
<span class='label label--error'>Response Required</span>
{%- endmacro %}
{% extends 'requests/_new.html' %}
{% from "components/text_input.html" import TextInput %}
{% from "components/icon.html" import Icon %}
{% block heading %}
Review &amp; Submit
{% endblock %}
{% block form_action %}
<form method='POST' action="{{ url_for('requests.requests_submit', request_id=request_id) }}" autocomplete="off">
{% endblock %}
{% block form %}
<p>Before you can submit your request, please take a moment to review the information entered in the form. You may make changes by clicking the edit link on each section. When all information looks right, go ahead and submit.</p>
{% include "fragments/flash.html" %}
{% with editable=True %}
{% include "requests/_review.html" %}
{% endwith %}
{% endblock %}
{% block next %}
<div class='action-group'>
<input type='submit' class='usa-button usa-button-primary' value='Submit' {{ "disabled" if not can_submit else "" }} />
</div>
</form>
{% endblock %}

View File

@ -1,33 +0,0 @@
<div class="sidenav">
<ul>
<li>
<a class="sidenav__link" href="/requests">&#8249; All requests</a>
</li>
</ul>
<ul>
{% for i,s in enumerate(screens) %}
{% if s["show"] %}
<li>
{% if i+1==current %}
<a class="sidenav__link sidenav__link--active" href="{{ reverse_url('request_form_update', i+1, request_id) if request_id else reverse_url('request_form_new',i+1) }}">
{{ i+1 }}. {{ s['title'] }}
</a>
{% if s.get('subitems') %}
<ul>
{% for j,t in enumerate(s['subitems']) %}
<li><a class="sidenav__link" href="#{{ t['id'] }}">{{ t['title'] }}</a></li>
{% end %}
</ul>
{% end %}
{% else %}
<a class="sidenav__link" href="{{ reverse_url('request_form_update', i+1, request_id) if request_id else reverse_url('request_form_new',i+1) }}">
{{ i+1 }}. {{ s['title'] }}
</a>
{% end %}
</li>
{% end %}
{% end %}
</ul>
</div>

View File

@ -1,11 +1,10 @@
from atst.domain.applications import Applications from atst.domain.applications import Applications
from tests.factories import RequestFactory, UserFactory, PortfolioFactory from tests.factories import UserFactory, PortfolioFactory
from atst.domain.portfolios import Portfolios from atst.domain.portfolios import Portfolios
def test_create_application_with_multiple_environments(): def test_create_application_with_multiple_environments():
request = RequestFactory.create() portfolio = PortfolioFactory.create()
portfolio = Portfolios.create_from_request(request)
application = Applications.create( application = Applications.create(
portfolio.owner, portfolio, "My Test Application", "Test", ["dev", "prod"] portfolio.owner, portfolio, "My Test Application", "Test", ["dev", "prod"]
) )

View File

@ -1,28 +0,0 @@
import pytest
from atst.domain.exceptions import NotFoundError
from atst.domain.legacy_task_orders import LegacyTaskOrders
from atst.eda_client import MockEDAClient
from tests.factories import LegacyTaskOrderFactory
def test_can_get_task_order():
new_to = LegacyTaskOrderFactory.create(number="0101969F")
to = LegacyTaskOrders.get(new_to.number)
assert to.id == to.id
def test_nonexistent_task_order_raises_without_client():
with pytest.raises(NotFoundError):
LegacyTaskOrders.get("some fake number")
def test_nonexistent_task_order_raises_with_client(monkeypatch):
monkeypatch.setattr(
"atst.domain.legacy_task_orders.LegacyTaskOrders._client",
lambda: MockEDAClient(),
)
with pytest.raises(NotFoundError):
LegacyTaskOrders.get("some other fake numer")

View File

@ -1,28 +0,0 @@
import pytest
from atst.domain.exceptions import NotFoundError
from atst.domain.pe_numbers import PENumbers
from tests.factories import PENumberFactory
def test_can_get_pe_number():
new_pen = PENumberFactory.create(
number="0701367F", description="Combat Support - Offensive"
)
pen = PENumbers.get(new_pen.number)
assert pen.number == new_pen.number
def test_nonexistent_pe_number_raises():
with pytest.raises(NotFoundError):
PENumbers.get("some fake number")
def test_create_many():
pen_list = [["123456", "Land Speeder"], ["7891011", "Lightsaber"]]
PENumbers.create_many(pen_list)
assert PENumbers.get(pen_list[0][0])
assert PENumbers.get(pen_list[1][0])

View File

@ -8,12 +8,7 @@ from atst.domain.applications import Applications
from atst.domain.environments import Environments from atst.domain.environments import Environments
from atst.models.portfolio_role import Status as PortfolioRoleStatus from atst.models.portfolio_role import Status as PortfolioRoleStatus
from tests.factories import ( from tests.factories import UserFactory, PortfolioRoleFactory, PortfolioFactory
RequestFactory,
UserFactory,
PortfolioRoleFactory,
PortfolioFactory,
)
@pytest.fixture(scope="function") @pytest.fixture(scope="function")
@ -22,39 +17,21 @@ def portfolio_owner():
@pytest.fixture(scope="function") @pytest.fixture(scope="function")
def request_(portfolio_owner): def portfolio(portfolio_owner):
return RequestFactory.create(creator=portfolio_owner) portfolio = PortfolioFactory.create(owner=portfolio_owner)
@pytest.fixture(scope="function")
def portfolio(request_):
portfolio = Portfolios.create_from_request(request_)
return portfolio return portfolio
def test_can_create_portfolio(request_): def test_can_create_portfolio():
portfolio = Portfolios.create_from_request(request_, name="frugal-whale") portfolio = PortfolioFactory.create(name="frugal-whale")
assert portfolio.name == "frugal-whale" assert portfolio.name == "frugal-whale"
def test_request_is_associated_with_portfolio(portfolio, request_):
assert portfolio.request == request_
def test_default_portfolio_name_is_request_name(portfolio, request_):
assert portfolio.name == str(request_.displayname)
def test_get_nonexistent_portfolio_raises(): def test_get_nonexistent_portfolio_raises():
with pytest.raises(NotFoundError): with pytest.raises(NotFoundError):
Portfolios.get(UserFactory.build(), uuid4()) Portfolios.get(UserFactory.build(), uuid4())
def test_can_get_portfolio_by_request(portfolio):
found = Portfolios.get_by_request(portfolio.request)
assert portfolio == found
def test_creating_portfolio_adds_owner(portfolio, portfolio_owner): def test_creating_portfolio_adds_owner(portfolio, portfolio_owner):
assert portfolio.roles[0].user == portfolio_owner assert portfolio.roles[0].user == portfolio_owner
@ -162,10 +139,6 @@ def test_need_permission_to_update_portfolio_role_role(portfolio, portfolio_owne
def test_owner_can_view_portfolio_members(portfolio, portfolio_owner): def test_owner_can_view_portfolio_members(portfolio, portfolio_owner):
portfolio_owner = UserFactory.create()
portfolio = Portfolios.create_from_request(
RequestFactory.create(creator=portfolio_owner)
)
portfolio = Portfolios.get_with_members(portfolio_owner, portfolio.id) portfolio = Portfolios.get_with_members(portfolio_owner, portfolio.id)
assert portfolio assert portfolio
@ -258,7 +231,7 @@ def test_for_user_returns_active_portfolios_for_user(portfolio, portfolio_owner)
PortfolioRoleFactory.create( PortfolioRoleFactory.create(
user=bob, portfolio=portfolio, status=PortfolioRoleStatus.ACTIVE user=bob, portfolio=portfolio, status=PortfolioRoleStatus.ACTIVE
) )
Portfolios.create_from_request(RequestFactory.create()) PortfolioFactory.create()
bobs_portfolios = Portfolios.for_user(bob) bobs_portfolios = Portfolios.for_user(bob)
@ -268,7 +241,7 @@ def test_for_user_returns_active_portfolios_for_user(portfolio, portfolio_owner)
def test_for_user_does_not_return_inactive_portfolios(portfolio, portfolio_owner): def test_for_user_does_not_return_inactive_portfolios(portfolio, portfolio_owner):
bob = UserFactory.from_atat_role("default") bob = UserFactory.from_atat_role("default")
Portfolios.add_member(portfolio, bob, "developer") Portfolios.add_member(portfolio, bob, "developer")
Portfolios.create_from_request(RequestFactory.create()) PortfolioFactory.create()
bobs_portfolios = Portfolios.for_user(bob) bobs_portfolios = Portfolios.for_user(bob)
assert len(bobs_portfolios) == 0 assert len(bobs_portfolios) == 0
@ -276,17 +249,13 @@ def test_for_user_does_not_return_inactive_portfolios(portfolio, portfolio_owner
def test_for_user_returns_all_portfolios_for_ccpo(portfolio, portfolio_owner): def test_for_user_returns_all_portfolios_for_ccpo(portfolio, portfolio_owner):
sam = UserFactory.from_atat_role("ccpo") sam = UserFactory.from_atat_role("ccpo")
Portfolios.create_from_request(RequestFactory.create()) PortfolioFactory.create()
sams_portfolios = Portfolios.for_user(sam) sams_portfolios = Portfolios.for_user(sam)
assert len(sams_portfolios) == 2 assert len(sams_portfolios) == 2
def test_get_for_update_information(): def test_get_for_update_information(portfolio, portfolio_owner):
portfolio_owner = UserFactory.create()
portfolio = Portfolios.create_from_request(
RequestFactory.create(creator=portfolio_owner)
)
owner_ws = Portfolios.get_for_update_information(portfolio_owner, portfolio.id) owner_ws = Portfolios.get_for_update_information(portfolio_owner, portfolio.id)
assert portfolio == owner_ws assert portfolio == owner_ws
@ -307,8 +276,8 @@ def test_get_for_update_information():
def test_can_create_portfolios_with_matching_names(): def test_can_create_portfolios_with_matching_names():
portfolio_name = "Great Portfolio" portfolio_name = "Great Portfolio"
Portfolios.create_from_request(RequestFactory.create(), name=portfolio_name) PortfolioFactory.create(name=portfolio_name)
Portfolios.create_from_request(RequestFactory.create(), name=portfolio_name) PortfolioFactory.create(name=portfolio_name)
def test_able_to_revoke_portfolio_access_for_active_member(): def test_able_to_revoke_portfolio_access_for_active_member():

View File

@ -1,27 +1,17 @@
from atst.domain.reports import Reports from atst.domain.reports import Reports
from tests.factories import RequestFactory, LegacyTaskOrderFactory, PortfolioFactory from tests.factories import PortfolioFactory
CLIN_NUMS = ["0001", "0003", "1001", "1003", "2001", "2003"]
def test_portfolio_totals(): def test_portfolio_totals():
legacy_task_order = LegacyTaskOrderFactory.create() portfolio = PortfolioFactory.create()
for num in CLIN_NUMS:
setattr(legacy_task_order, "clin_{}".format(num), 200)
request = RequestFactory.create(legacy_task_order=legacy_task_order)
portfolio = PortfolioFactory.create(request=request)
report = Reports.portfolio_totals(portfolio) report = Reports.portfolio_totals(portfolio)
total = 200 * len(CLIN_NUMS) assert report == {"budget": 0, "spent": 0}
assert report == {"budget": total, "spent": 0}
# this is sketched in until we do real reporting # this is sketched in until we do real reporting
def test_monthly_totals(): def test_monthly_totals():
request = RequestFactory.create() portfolio = PortfolioFactory.create()
portfolio = PortfolioFactory.create(request=request)
monthly = Reports.monthly_totals(portfolio) monthly = Reports.monthly_totals(portfolio)
assert not monthly["environments"] assert not monthly["environments"]
@ -31,8 +21,7 @@ def test_monthly_totals():
# this is sketched in until we do real reporting # this is sketched in until we do real reporting
def test_cumulative_budget(): def test_cumulative_budget():
request = RequestFactory.create() portfolio = PortfolioFactory.create()
portfolio = PortfolioFactory.create(request=request)
months = Reports.cumulative_budget(portfolio) months = Reports.cumulative_budget(portfolio)
assert len(months["months"]) >= 12 assert len(months["months"]) >= 12

View File

@ -1,273 +0,0 @@
import pytest
from uuid import uuid4
from atst.domain.exceptions import NotFoundError
from atst.domain.requests import Requests
from atst.domain.requests.authorization import RequestsAuthorization
from atst.models.request import Request
from atst.models.request_status_event import RequestStatus
from tests.factories import (
RequestFactory,
UserFactory,
RequestStatusEventFactory,
RequestRevisionFactory,
RequestReviewFactory,
)
@pytest.fixture(scope="function")
def new_request(session):
return RequestFactory.create()
def test_can_get_request():
factory_req = RequestFactory.create()
request = Requests.get(factory_req.creator, factory_req.id)
assert request.id == factory_req.id
def test_nonexistent_request_raises():
a_user = UserFactory.build()
with pytest.raises(NotFoundError):
Requests.get(a_user, uuid4())
def test_new_request_has_started_status():
request = Requests.create(UserFactory.build(), {})
assert request.status == RequestStatus.STARTED
def test_auto_approve_less_than_1m():
new_request = RequestFactory.create(initial_revision={"dollar_value": 999_999})
request = Requests.submit(new_request)
assert request.status == RequestStatus.PENDING_FINANCIAL_VERIFICATION
assert request.reviews
assert request.reviews[0].full_name_reviewer == "System"
def test_dont_auto_approve_if_dollar_value_is_1m_or_above():
new_request = RequestFactory.create(initial_revision={"dollar_value": 1_000_000})
request = Requests.submit(new_request)
assert request.status == RequestStatus.PENDING_CCPO_ACCEPTANCE
def test_dont_auto_approve_if_no_dollar_value_specified():
new_request = RequestFactory.create(initial_revision={})
request = Requests.submit(new_request)
assert request.status == RequestStatus.PENDING_CCPO_ACCEPTANCE
def test_should_allow_submission():
new_request = RequestFactory.create()
assert Requests.should_allow_submission(new_request)
RequestStatusEventFactory.create(
request=new_request,
new_status=RequestStatus.CHANGES_REQUESTED,
revision=new_request.latest_revision,
)
assert Requests.should_allow_submission(new_request)
# new, blank revision
RequestRevisionFactory.create(request=new_request)
assert not Requests.should_allow_submission(new_request)
def test_request_knows_its_last_submission_timestamp(new_request):
submitted_request = Requests.submit(new_request)
assert submitted_request.last_submission_timestamp
def test_request_knows_if_it_has_no_last_submission_timestamp(new_request):
assert new_request.last_submission_timestamp is None
def test_exists(session):
user_allowed = UserFactory.create()
user_denied = UserFactory.create()
request = RequestFactory.create(creator=user_allowed)
assert Requests.exists(request.id, user_allowed)
assert not Requests.exists(request.id, user_denied)
def test_status_count(session):
# make sure table is empty
session.query(Request).delete()
request1 = RequestFactory.create()
request2 = RequestFactory.create()
RequestStatusEventFactory.create(
sequence=2,
request_id=request2.id,
revision=request2.latest_revision,
new_status=RequestStatus.PENDING_FINANCIAL_VERIFICATION,
)
assert Requests.status_count(RequestStatus.PENDING_FINANCIAL_VERIFICATION) == 1
assert Requests.status_count(RequestStatus.STARTED) == 1
assert Requests.in_progress_count() == 2
def test_status_count_scoped_to_creator(session):
# make sure table is empty
session.query(Request).delete()
user = UserFactory.create()
request1 = RequestFactory.create()
request2 = RequestFactory.create(creator=user)
assert Requests.status_count(RequestStatus.STARTED) == 2
assert Requests.status_count(RequestStatus.STARTED, creator=user) == 1
request_financial_data = {
"pe_id": "123",
"task_order_number": "021345",
"fname_co": "Contracting",
"lname_co": "Officer",
"email_co": "jane@mail.mil",
"office_co": "WHS",
"fname_cor": "Officer",
"lname_cor": "Representative",
"email_cor": "jane@mail.mil",
"office_cor": "WHS",
"uii_ids": "1234",
"treasury_code": "00123456",
"ba_code": "024A",
}
def test_set_status_sets_revision():
request = RequestFactory.create()
Requests.set_status(request, RequestStatus.APPROVED)
assert request.latest_revision == request.status_events[-1].revision
def test_advance_to_financial_verification():
request = RequestFactory.create_with_status(
status=RequestStatus.PENDING_CCPO_ACCEPTANCE
)
review_data = RequestReviewFactory.dictionary()
Requests.advance(UserFactory.create(), request, review_data)
assert request.status == RequestStatus.PENDING_FINANCIAL_VERIFICATION
current_review = request.latest_status.review
assert current_review.fname_mao == review_data["fname_mao"]
def test_advance_to_approval():
request = RequestFactory.create_with_status(
status=RequestStatus.PENDING_CCPO_APPROVAL
)
review_data = RequestReviewFactory.dictionary()
Requests.advance(UserFactory.create(), request, review_data)
assert request.status == RequestStatus.APPROVED
def test_request_changes_to_request_application():
request = RequestFactory.create_with_status(
status=RequestStatus.PENDING_CCPO_ACCEPTANCE
)
review_data = RequestReviewFactory.dictionary()
Requests.request_changes(UserFactory.create(), request, review_data)
assert request.status == RequestStatus.CHANGES_REQUESTED
current_review = request.latest_status.review
assert current_review.fname_mao == review_data["fname_mao"]
def test_request_changes_to_financial_verification_info():
request = RequestFactory.create_with_status(
status=RequestStatus.PENDING_CCPO_APPROVAL
)
review_data = RequestReviewFactory.dictionary()
Requests.request_changes(UserFactory.create(), request, review_data)
assert request.status == RequestStatus.CHANGES_REQUESTED_TO_FINVER
current_review = request.latest_status.review
assert current_review.fname_mao == review_data["fname_mao"]
def test_add_internal_comment():
request = RequestFactory.create()
ccpo = UserFactory.from_atat_role("ccpo")
assert len(request.internal_comments) == 0
request = Requests.add_internal_comment(ccpo, request, "this is my comment")
assert len(request.internal_comments) == 1
assert request.internal_comments[0].text == "this is my comment"
def test_creator_can_view_own_request():
creator = UserFactory.create()
request = RequestFactory.create(creator=creator)
assert RequestsAuthorization(creator, request).can_view
def test_ccpo_can_view_request():
ccpo = UserFactory.from_atat_role("ccpo")
request = RequestFactory.create()
assert RequestsAuthorization(ccpo, request).can_view
def test_random_user_cannot_view_request():
user = UserFactory.create()
request = RequestFactory.create()
assert not RequestsAuthorization(user, request).can_view
def test_auto_approve_and_create_portfolio():
request = RequestFactory.create()
portfolio = Requests.auto_approve_and_create_portfolio(request)
assert portfolio
assert request.reviews[0]
assert request.reviews[0].full_name_reviewer == "System"
class TestStatusNotifications(object):
def _assert_job(self, queue, request):
assert len(queue.get_queue()) == 1
job = queue.get_queue().jobs[0]
assert job.func == queue._send_mail
assert job.args[0] == [request.creator.email]
def test_pending_finver_triggers_notification(self, queue):
request = RequestFactory.create()
request = Requests.set_status(request, RequestStatus.PENDING_CCPO_ACCEPTANCE)
request = Requests.set_status(
request, RequestStatus.PENDING_FINANCIAL_VERIFICATION
)
self._assert_job(queue, request)
def test_changes_requested_triggers_notification(self, queue):
request = RequestFactory.create()
request = Requests.set_status(request, RequestStatus.PENDING_CCPO_ACCEPTANCE)
request = Requests.set_status(request, RequestStatus.CHANGES_REQUESTED)
self._assert_job(queue, request)
def test_changes_requested_to_finver_triggers_notification(self, queue):
request = RequestFactory.create()
request = Requests.set_status(request, RequestStatus.PENDING_CCPO_APPROVAL)
request = Requests.set_status(
request, RequestStatus.CHANGES_REQUESTED_TO_FINVER
)
self._assert_job(queue, request)
def test_approval_triggers_notification(self, queue):
request = RequestFactory.create()
request = Requests.set_status(request, RequestStatus.PENDING_CCPO_APPROVAL)
request = Requests.set_status(request, RequestStatus.APPROVED)
self._assert_job(queue, request)
def test_submitted_does_not_trigger_notification(self, queue):
request = RequestFactory.create()
request = Requests.set_status(request, RequestStatus.SUBMITTED)
assert len(queue.get_queue()) == 0

View File

@ -9,13 +9,7 @@ from faker import Faker as _Faker
from atst.forms import data from atst.forms import data
from atst.models.attachment import Attachment from atst.models.attachment import Attachment
from atst.models.environment import Environment from atst.models.environment import Environment
from atst.models.request import Request
from atst.models.request_revision import RequestRevision
from atst.models.request_review import RequestReview
from atst.models.request_status_event import RequestStatusEvent, RequestStatus
from atst.models.pe_number import PENumber
from atst.models.application import Application from atst.models.application import Application
from atst.models.legacy_task_order import LegacyTaskOrder, Source, FundingType
from atst.models.task_order import TaskOrder from atst.models.task_order import TaskOrder
from atst.models.user import User from atst.models.user import User
from atst.models.role import Role from atst.models.role import Role
@ -105,173 +99,11 @@ class UserFactory(Base):
return cls.create(atat_role=role, **kwargs) return cls.create(atat_role=role, **kwargs)
class RequestStatusEventFactory(Base):
class Meta:
model = RequestStatusEvent
id = factory.Sequence(lambda x: uuid4())
sequence = 1
class RequestRevisionFactory(Base):
class Meta:
model = RequestRevision
id = factory.Sequence(lambda x: uuid4())
class RequestReviewFactory(Base):
class Meta:
model = RequestReview
comment = factory.Faker("sentence")
fname_mao = factory.Faker("first_name")
lname_mao = factory.Faker("last_name")
email_mao = factory.Faker("email")
phone_mao = factory.LazyFunction(
lambda: "".join(random.choices(string.digits, k=10))
)
fname_ccpo = factory.Faker("first_name")
lname_ccpo = factory.Faker("last_name")
class RequestFactory(Base):
class Meta:
model = Request
id = factory.Sequence(lambda x: uuid4())
creator = factory.SubFactory(UserFactory)
revisions = factory.LazyAttribute(
lambda r: [RequestFactory.create_initial_revision(r)]
)
status_events = factory.RelatedFactory(
RequestStatusEventFactory,
"request",
new_status=RequestStatus.STARTED,
revision=factory.LazyAttribute(lambda se: se.factory_parent.revisions[-1]),
)
class Params:
initial_revision = None
@classmethod
def _adjust_kwargs(cls, **kwargs):
if kwargs.pop("with_task_order", False) and "legacy_task_order" not in kwargs:
kwargs["legacy_task_order"] = LegacyTaskOrderFactory.build()
return kwargs
@classmethod
def create_initial_status_event(cls, request):
return RequestStatusEventFactory(
request=request,
new_status=RequestStatus.STARTED,
revision=request.revisions,
)
@classmethod
def create_initial_revision(cls, request, dollar_value=1_000_000):
user = request.creator
default_data = dict(
name=factory.Faker("domain_word"),
am_poc=False,
dodid_poc=user.dod_id,
email_poc=user.email,
fname_poc=user.first_name,
lname_poc=user.last_name,
jedi_usage="adf",
start_date=datetime.date(2050, 1, 1),
cloud_native="yes",
dollar_value=dollar_value,
dod_component=random_service_branch(),
data_transfers="Less than 100GB",
expected_completion_date="Less than 1 month",
jedi_migration="yes",
num_software_systems=1,
number_user_sessions=2,
average_daily_traffic=1,
engineering_assessment="yes",
technical_support_team="yes",
estimated_monthly_spend=100,
average_daily_traffic_gb=4,
rationalization_software_systems="yes",
organization_providing_assistance="In-house staff",
citizenship="United States",
designation="military",
phone_number="1234567890",
phone_ext="123",
email_request=user.email,
fname_request=user.first_name,
lname_request=user.last_name,
service_branch=random_service_branch(),
date_latest_training=datetime.date(2018, 8, 6),
)
data = (
request.initial_revision
if request.initial_revision is not None
else default_data
)
return RequestRevisionFactory.build(**data)
@classmethod
def create_with_status(cls, status=RequestStatus.STARTED, **kwargs):
request = RequestFactory(**kwargs)
RequestStatusEventFactory.create(
request=request, revision=request.latest_revision, new_status=status
)
return request
@classmethod
def mock_financial_data(cls):
fake = _Faker()
return {
"pe_id": "0101110F",
"fname_co": fake.first_name(),
"lname_co": fake.last_name(),
"email_co": fake.email(),
"office_co": fake.phone_number(),
"fname_cor": fake.first_name(),
"lname_cor": fake.last_name(),
"email_cor": fake.email(),
"office_cor": fake.phone_number(),
"uii_ids": "123abc",
"treasury_code": "00123456",
"ba_code": "02A",
}
class PENumberFactory(Base):
class Meta:
model = PENumber
class LegacyTaskOrderFactory(Base):
class Meta:
model = LegacyTaskOrder
source = Source.MANUAL
funding_type = FundingType.PROC
funding_type_other = None
number = factory.LazyFunction(
lambda: "".join(random.choices(string.ascii_uppercase + string.digits, k=13))
)
expiration_date = factory.LazyFunction(random_future_date)
clin_0001 = random.randrange(100, 100_000)
clin_0003 = random.randrange(100, 100_000)
clin_1001 = random.randrange(100, 100_000)
clin_1003 = random.randrange(100, 100_000)
clin_2001 = random.randrange(100, 100_000)
clin_2003 = random.randrange(100, 100_000)
class PortfolioFactory(Base): class PortfolioFactory(Base):
class Meta: class Meta:
model = Portfolio model = Portfolio
request = factory.SubFactory(RequestFactory, with_task_order=True) name = factory.Faker("name")
# name it the same as the request ID by default
name = factory.LazyAttribute(lambda w: w.request.id)
@classmethod @classmethod
def _create(cls, model_class, *args, **kwargs): def _create(cls, model_class, *args, **kwargs):
@ -286,7 +118,6 @@ class PortfolioFactory(Base):
for p in with_applications for p in with_applications
] ]
portfolio.request.creator = owner
PortfolioRoleFactory.create( PortfolioRoleFactory.create(
portfolio=portfolio, portfolio=portfolio,
role=Roles.get("owner"), role=Roles.get("owner"),

View File

@ -4,41 +4,7 @@ from wtforms.fields import StringField
import pendulum import pendulum
from werkzeug.datastructures import ImmutableMultiDict from werkzeug.datastructures import ImmutableMultiDict
from atst.forms.fields import NewlineListField, FormFieldWrapper from atst.forms.fields import FormFieldWrapper
class NewlineListForm(Form):
newline_list = NewlineListField()
@pytest.mark.parametrize(
"input_,expected",
[
("", []),
("hello", ["hello"]),
("hello\n", ["hello"]),
("hello\nworld", ["hello", "world"]),
("hello\nworld\n", ["hello", "world"]),
],
)
def test_newline_list_process(input_, expected):
form_data = ImmutableMultiDict({"newline_list": input_})
form = NewlineListForm(form_data)
assert form.validate()
assert form.data == {"newline_list": expected}
@pytest.mark.parametrize(
"input_,expected",
[([], ""), (["hello"], "hello"), (["hello", "world"], "hello\nworld")],
)
def test_newline_list_value(input_, expected):
form_data = {"newline_list": input_}
form = NewlineListForm(data=form_data)
assert form.validate()
assert form.newline_list._value() == expected
class PersonForm(Form): class PersonForm(Form):

View File

@ -1,92 +0,0 @@
import pytest
from werkzeug.datastructures import ImmutableMultiDict
from atst.forms.financial import FinancialVerificationForm
from atst.domain.requests.financial_verification import PENumberValidator
@pytest.mark.parametrize(
"input_,expected",
[
("0603502N", None),
("0603502NZ", None),
("603502N", "0603502N"),
("063502N", "0603502N"),
("63502N", "0603502N"),
],
)
def test_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():
form_data = ImmutableMultiDict({"legacy_task_order-funding_type": "PROC"})
form = FinancialVerificationForm(form_data)
form.validate()
assert "funding_type_other" not in form.errors
def test_funding_type_other_required_if_funding_type_is_other():
form_data = ImmutableMultiDict({"legacy_task_order-funding_type": "OTHER"})
form = FinancialVerificationForm(form_data)
form.validate()
assert "funding_type_other" in form.errors["legacy_task_order"]
@pytest.mark.parametrize(
"input_,expected",
[
("1234", True),
("123456", True),
("0001234", True),
("000123456", True),
("12345", False),
("00012345", False),
("0001234567", False),
("000000", False),
],
)
def test_treasury_code_validation(input_, expected):
form_data = ImmutableMultiDict([("request-treasury_code", input_)])
form = FinancialVerificationForm(form_data)
form.validate()
is_valid = "treasury_code" not in form.errors["request"]
assert is_valid == expected
@pytest.mark.parametrize(
"input_,expected",
[
("1", False),
("12", True),
("01", True),
("0A", False),
("A", False),
("AB", False),
("123", True),
("012", True),
("12A", True),
("02A", True),
("0012", False),
("012A", False),
("2AB", False),
],
)
def test_ba_code_validation(input_, expected):
form_data = ImmutableMultiDict([("request-ba_code", input_)])
form = FinancialVerificationForm(form_data)
form.validate()
is_valid = "ba_code" not in form.errors["request"]
assert is_valid == expected
def test_can_submit_zero_for_clin():
form_first = FinancialVerificationForm()
form_first.validate()
assert "clin_0001" in form_first.errors["legacy_task_order"]
form_data = ImmutableMultiDict([("legacy_task_order-clin_0001", "0")])
form_second = FinancialVerificationForm(form_data)
form_second.validate()
assert "clin_0001" not in form_second.errors["legacy_task_order"]

View File

@ -1,103 +0,0 @@
import pytest
from werkzeug.datastructures import ImmutableMultiDict
from atst.forms.new_request import DetailsOfUseForm
class TestDetailsOfUseForm:
form_data = {
"dod_component": "Army and Air Force Exchange Service",
"jedi_usage": "cloud-ify all the things",
"num_software_systems": "12",
"estimated_monthly_spend": "1000000",
"dollar_value": "42",
"number_user_sessions": "6",
"average_daily_traffic": "0",
"start_date": "12/12/2050",
"name": "blue-beluga",
}
migration_data = {
"jedi_migration": "yes",
"rationalization_software_systems": "yes",
"technical_support_team": "yes",
"organization_providing_assistance": "In-house staff",
"engineering_assessment": "yes",
"data_transfers": "Less than 100GB",
"expected_completion_date": "Less than 1 month",
}
def _make_form(self, data):
form_data = ImmutableMultiDict(data.items())
return DetailsOfUseForm(form_data)
def test_require_cloud_native_when_not_migrating(self):
extra_data = {"jedi_migration": "no"}
request_form = self._make_form({**self.form_data, **extra_data})
assert not request_form.validate()
assert request_form.errors == {"cloud_native": ["Not a valid choice"]}
def test_require_migration_questions_when_migrating(self):
extra_data = {
"jedi_migration": "yes",
"data_transfers": "",
"expected_completion_date": "",
}
request_form = self._make_form({**self.form_data, **extra_data})
assert not request_form.validate()
assert request_form.errors == {
"rationalization_software_systems": ["Not a valid choice"],
"technical_support_team": ["Not a valid choice"],
"organization_providing_assistance": ["Not a valid choice"],
"engineering_assessment": ["Not a valid choice"],
"data_transfers": ["This field is required."],
"expected_completion_date": ["This field is required."],
}
def test_require_organization_when_technical_support_team(self):
data = {**self.form_data, **self.migration_data}
del data["organization_providing_assistance"]
request_form = self._make_form(data)
assert not request_form.validate()
assert request_form.errors == {
"organization_providing_assistance": ["Not a valid choice"]
}
def test_valid_form_data(self):
data = {**self.form_data, **self.migration_data}
data["technical_support_team"] = "no"
del data["organization_providing_assistance"]
request_form = self._make_form(data)
assert request_form.validate()
def test_sessions_required_for_large_applications(self):
data = {**self.form_data, **self.migration_data}
data["estimated_monthly_spend"] = "9999999"
del data["number_user_sessions"]
del data["average_daily_traffic"]
request_form = self._make_form(data)
assert not request_form.validate()
assert request_form.errors == {
"number_user_sessions": ["This field is required."],
"average_daily_traffic": ["This field is required."],
}
def test_sessions_not_required_low_monthly_spend(self):
data = {**self.form_data, **self.migration_data}
data["estimated_monthly_spend"] = "10"
del data["number_user_sessions"]
del data["average_daily_traffic"]
request_form = self._make_form(data)
assert request_form.validate()
def test_start_date_must_be_in_the_future(self):
data = {**self.form_data, **self.migration_data}
data["start_date"] = "01/01/2018"
request_form = self._make_form(data)
assert not request_form.validate()
assert "Must be a date in the future." in request_form.errors["start_date"]

View File

@ -1,4 +1,4 @@
from tests.factories import RequestFactory, UserFactory from tests.factories import UserFactory
DOD_SDN_INFO = {"first_name": "ART", "last_name": "GARFUNKEL", "dod_id": "5892460358"} DOD_SDN_INFO = {"first_name": "ART", "last_name": "GARFUNKEL", "dod_id": "5892460358"}

View File

@ -1,14 +1,13 @@
from atst.domain.environments import Environments from atst.domain.environments import Environments
from atst.domain.portfolios import Portfolios
from atst.domain.applications import Applications from atst.domain.applications import Applications
from tests.factories import RequestFactory, UserFactory from tests.factories import PortfolioFactory, UserFactory
def test_add_user_to_environment(): def test_add_user_to_environment():
owner = UserFactory.create() owner = UserFactory.create()
developer = UserFactory.from_atat_role("developer") developer = UserFactory.from_atat_role("developer")
portfolio = Portfolios.create_from_request(RequestFactory.create(creator=owner)) portfolio = PortfolioFactory.create(owner=owner)
application = Applications.create( application = Applications.create(
owner, owner,
portfolio, portfolio,

View File

@ -1,20 +0,0 @@
from tests.factories import LegacyTaskOrderFactory
from tests.assert_util import dict_contains
def test_as_dictionary():
data = LegacyTaskOrderFactory.dictionary()
real_task_order = LegacyTaskOrderFactory.create(**data)
assert dict_contains(real_task_order.to_dictionary(), data)
def test_budget():
legacy_task_order = LegacyTaskOrderFactory.create(
clin_0001=500,
clin_0003=200,
clin_1001=None,
clin_1003=None,
clin_2001=None,
clin_2003=None,
)
assert legacy_task_order.budget == 700

View File

@ -9,7 +9,6 @@ from atst.models.invitation import Status as InvitationStatus
from atst.models.audit_event import AuditEvent from atst.models.audit_event import AuditEvent
from atst.models.portfolio_role import Status as PortfolioRoleStatus from atst.models.portfolio_role import Status as PortfolioRoleStatus
from tests.factories import ( from tests.factories import (
RequestFactory,
UserFactory, UserFactory,
InvitationFactory, InvitationFactory,
PortfolioRoleFactory, PortfolioRoleFactory,
@ -25,7 +24,7 @@ def test_has_no_ws_role_history(session):
owner = UserFactory.create() owner = UserFactory.create()
user = UserFactory.create() user = UserFactory.create()
portfolio = Portfolios.create_from_request(RequestFactory.create(creator=owner)) portfolio = PortfolioFactory.create(owner=owner)
portfolio_role = PortfolioRoles.add(user, portfolio.id, "developer") portfolio_role = PortfolioRoles.add(user, portfolio.id, "developer")
create_event = ( create_event = (
session.query(AuditEvent) session.query(AuditEvent)
@ -42,7 +41,7 @@ def test_has_ws_role_history(session):
owner = UserFactory.create() owner = UserFactory.create()
user = UserFactory.create() user = UserFactory.create()
portfolio = Portfolios.create_from_request(RequestFactory.create(creator=owner)) portfolio = PortfolioFactory.create(owner=owner)
role = session.query(Role).filter(Role.name == "developer").one() role = session.query(Role).filter(Role.name == "developer").one()
# in order to get the history, we don't want the PortfolioRoleFactory # in order to get the history, we don't want the PortfolioRoleFactory
# to commit after create() # to commit after create()
@ -67,7 +66,7 @@ def test_has_ws_status_history(session):
owner = UserFactory.create() owner = UserFactory.create()
user = UserFactory.create() user = UserFactory.create()
portfolio = Portfolios.create_from_request(RequestFactory.create(creator=owner)) portfolio = PortfolioFactory.create(owner=owner)
# in order to get the history, we don't want the PortfolioRoleFactory # in order to get the history, we don't want the PortfolioRoleFactory
# to commit after create() # to commit after create()
PortfolioRoleFactory._meta.sqlalchemy_session_persistence = "flush" PortfolioRoleFactory._meta.sqlalchemy_session_persistence = "flush"
@ -89,7 +88,7 @@ def test_has_ws_status_history(session):
def test_has_no_env_role_history(session): def test_has_no_env_role_history(session):
owner = UserFactory.create() owner = UserFactory.create()
user = UserFactory.create() user = UserFactory.create()
portfolio = Portfolios.create_from_request(RequestFactory.create(creator=owner)) portfolio = PortfolioFactory.create(owner=owner)
application = ApplicationFactory.create(portfolio=portfolio) application = ApplicationFactory.create(portfolio=portfolio)
environment = EnvironmentFactory.create( environment = EnvironmentFactory.create(
application=application, name="new environment!" application=application, name="new environment!"
@ -110,7 +109,7 @@ def test_has_no_env_role_history(session):
def test_has_env_role_history(session): def test_has_env_role_history(session):
owner = UserFactory.create() owner = UserFactory.create()
user = UserFactory.create() user = UserFactory.create()
portfolio = Portfolios.create_from_request(RequestFactory.create(creator=owner)) portfolio = PortfolioFactory.create(owner=owner)
portfolio_role = PortfolioRoleFactory.create(portfolio=portfolio, user=user) portfolio_role = PortfolioRoleFactory.create(portfolio=portfolio, user=user)
application = ApplicationFactory.create(portfolio=portfolio) application = ApplicationFactory.create(portfolio=portfolio)
environment = EnvironmentFactory.create( environment = EnvironmentFactory.create(
@ -137,7 +136,7 @@ def test_event_details():
owner = UserFactory.create() owner = UserFactory.create()
user = UserFactory.create() user = UserFactory.create()
portfolio = Portfolios.create_from_request(RequestFactory.create(creator=owner)) portfolio = PortfolioFactory.create(owner=owner)
portfolio_role = PortfolioRoles.add(user, portfolio.id, "developer") portfolio_role = PortfolioRoles.add(user, portfolio.id, "developer")
assert portfolio_role.event_details["updated_user_name"] == user.displayname assert portfolio_role.event_details["updated_user_name"] == user.displayname
@ -154,7 +153,7 @@ def test_has_no_environment_roles():
"portfolio_role": "developer", "portfolio_role": "developer",
} }
portfolio = Portfolios.create_from_request(RequestFactory.create(creator=owner)) portfolio = PortfolioFactory.create(owner=owner)
portfolio_role = Portfolios.create_member(owner, portfolio, developer_data) portfolio_role = Portfolios.create_member(owner, portfolio, developer_data)
assert not portfolio_role.has_environment_roles assert not portfolio_role.has_environment_roles
@ -170,7 +169,7 @@ def test_has_environment_roles():
"portfolio_role": "developer", "portfolio_role": "developer",
} }
portfolio = Portfolios.create_from_request(RequestFactory.create(creator=owner)) portfolio = PortfolioFactory.create(owner=owner)
portfolio_role = Portfolios.create_member(owner, portfolio, developer_data) portfolio_role = Portfolios.create_member(owner, portfolio, developer_data)
application = Applications.create( application = Applications.create(
owner, owner,
@ -195,7 +194,7 @@ def test_role_displayname():
"portfolio_role": "developer", "portfolio_role": "developer",
} }
portfolio = Portfolios.create_from_request(RequestFactory.create(creator=owner)) portfolio = PortfolioFactory.create(owner=owner)
portfolio_role = Portfolios.create_member(owner, portfolio, developer_data) portfolio_role = Portfolios.create_member(owner, portfolio, developer_data)
assert portfolio_role.role_displayname == "Developer" assert portfolio_role.role_displayname == "Developer"

View File

@ -1,122 +0,0 @@
from tests.factories import (
RequestFactory,
UserFactory,
RequestStatusEventFactory,
RequestReviewFactory,
RequestRevisionFactory,
)
from atst.domain.requests import Requests
from atst.models.request_status_event import RequestStatus
def test_pending_financial_requires_mo_action():
request = RequestFactory.create()
request = Requests.set_status(request, RequestStatus.PENDING_FINANCIAL_VERIFICATION)
assert request.action_required_by == "mission_owner"
def test_pending_ccpo_approval_requires_ccpo():
request = RequestFactory.create()
request = Requests.set_status(request, RequestStatus.PENDING_CCPO_APPROVAL)
assert request.action_required_by == "ccpo"
def test_request_has_creator():
user = UserFactory.create()
request = RequestFactory.create(creator=user)
assert request.creator == user
def test_request_status_started_displayname():
request = RequestFactory.create()
request = Requests.set_status(request, RequestStatus.STARTED)
assert request.status_displayname == "Started"
def test_request_status_pending_financial_displayname():
request = RequestFactory.create()
request = Requests.set_status(request, RequestStatus.PENDING_FINANCIAL_VERIFICATION)
assert request.status_displayname == "Pending Financial Verification"
def test_request_status_pending_ccpo_displayname():
request = RequestFactory.create()
request = Requests.set_status(request, RequestStatus.PENDING_CCPO_APPROVAL)
assert request.status_displayname == "Pending CCPO Approval"
def test_request_status_pending_approved_displayname():
request = RequestFactory.create()
request = Requests.set_status(request, RequestStatus.APPROVED)
assert request.status_displayname == "Approved"
def test_request_status_pending_expired_displayname():
request = RequestFactory.create()
request = Requests.set_status(request, RequestStatus.EXPIRED)
assert request.status_displayname == "Expired"
def test_request_status_pending_deleted_displayname():
request = RequestFactory.create()
request = Requests.set_status(request, RequestStatus.DELETED)
assert request.status_displayname == "Deleted"
def test_annual_spend():
request = RequestFactory.create()
monthly = request.body.get("details_of_use").get("estimated_monthly_spend")
assert request.annual_spend == monthly * 12
def test_reviews():
request = RequestFactory.create()
ccpo = UserFactory.from_atat_role("ccpo")
RequestStatusEventFactory.create(
request=request,
revision=request.latest_revision,
review=RequestReviewFactory.create(reviewer=ccpo),
),
RequestStatusEventFactory.create(
request=request,
revision=request.latest_revision,
review=RequestReviewFactory.create(reviewer=ccpo),
),
RequestStatusEventFactory.create(request=request, revision=request.latest_revision),
assert len(request.reviews) == 2
def test_review_comment():
request = RequestFactory.create()
ccpo = UserFactory.from_atat_role("ccpo")
RequestStatusEventFactory.create(
request=request,
revision=request.latest_revision,
new_status=RequestStatus.CHANGES_REQUESTED,
review=RequestReviewFactory.create(reviewer=ccpo, comment="do better"),
)
assert request.review_comment == "do better"
RequestStatusEventFactory.create(
request=request,
revision=request.latest_revision,
new_status=RequestStatus.APPROVED,
review=RequestReviewFactory.create(reviewer=ccpo, comment="much better"),
)
assert not request.review_comment
def test_finver_last_saved_at():
request = RequestFactory.create()
RequestRevisionFactory.create(fname_co="Amanda", request=request)
assert request.last_finver_draft_saved_at

View File

@ -90,18 +90,16 @@ def test_user_without_permission_has_no_activity_log_link(client, user_session):
) )
@pytest.mark.skip(reason="Temporarily no add application link")
def test_user_with_permission_has_add_application_link(client, user_session): def test_user_with_permission_has_add_application_link(client, user_session):
portfolio = PortfolioFactory.create() portfolio = PortfolioFactory.create()
user_session(portfolio.owner) user_session(portfolio.owner)
response = client.get("/portfolios/{}/applications".format(portfolio.id)) response = client.get("/portfolios/{}/applications".format(portfolio.id))
assert ( assert (
'href="/portfolios/{}/applications/new"'.format(portfolio.id).encode() "href='/portfolios/{}/applications/new'".format(portfolio.id).encode()
in response.data in response.data
) )
@pytest.mark.skip(reason="Temporarily no add application link")
def test_user_without_permission_has_no_add_application_link(client, user_session): def test_user_without_permission_has_no_add_application_link(client, user_session):
user = UserFactory.create() user = UserFactory.create()
portfolio = PortfolioFactory.create() portfolio = PortfolioFactory.create()
@ -109,11 +107,29 @@ def test_user_without_permission_has_no_add_application_link(client, user_sessio
user_session(user) user_session(user)
response = client.get("/portfolios/{}/applications".format(portfolio.id)) response = client.get("/portfolios/{}/applications".format(portfolio.id))
assert ( assert (
'href="/portfolios/{}/applications/new"'.format(portfolio.id).encode() "href='/portfolios/{}/applications/new'".format(portfolio.id).encode()
not in response.data not in response.data
) )
def test_creating_application(client, user_session):
portfolio = PortfolioFactory.create()
user_session(portfolio.owner)
response = client.post(
url_for("portfolios.create_application", portfolio_id=portfolio.id),
data={
"name": "Test Application",
"description": "This is only a test",
"environment_names-0": "dev",
"environment_names-1": "staging",
"environment_names-2": "prod",
},
)
assert response.status_code == 302
assert len(portfolio.applications) == 1
assert len(portfolio.applications[0].environments) == 3
def test_view_edit_application(client, user_session): def test_view_edit_application(client, user_session):
portfolio = PortfolioFactory.create() portfolio = PortfolioFactory.create()
application = Applications.create( application = Applications.create(

View File

@ -37,7 +37,6 @@ def create_portfolio_and_invite_user(
return portfolio return portfolio
@pytest.mark.skip(reason="Temporarily no add member link")
def test_user_with_permission_has_add_member_link(client, user_session): def test_user_with_permission_has_add_member_link(client, user_session):
portfolio = PortfolioFactory.create() portfolio = PortfolioFactory.create()
user_session(portfolio.owner) user_session(portfolio.owner)
@ -48,7 +47,6 @@ def test_user_with_permission_has_add_member_link(client, user_session):
) )
@pytest.mark.skip(reason="Temporarily no add member link")
def test_user_without_permission_has_no_add_member_link(client, user_session): def test_user_without_permission_has_no_add_member_link(client, user_session):
user = UserFactory.create() user = UserFactory.create()
portfolio = PortfolioFactory.create() portfolio = PortfolioFactory.create()

View File

@ -1,6 +1,12 @@
from flask import url_for from flask import url_for
from tests.factories import PortfolioFactory, UserFactory from tests.factories import (
random_future_date,
random_past_date,
PortfolioFactory,
TaskOrderFactory,
UserFactory,
)
from atst.utils.localization import translate from atst.utils.localization import translate
@ -40,3 +46,46 @@ def test_portfolio_index_without_existing_portfolios(client, user_session):
assert ( assert (
translate("portfolios.index.empty.start_button").encode("utf8") in response.data translate("portfolios.index.empty.start_button").encode("utf8") in response.data
) )
def test_portfolio_admin_screen(client, user_session):
portfolio = PortfolioFactory.create()
user_session(portfolio.owner)
response = client.get(
url_for("portfolios.portfolio_admin", portfolio_id=portfolio.id)
)
assert response.status_code == 200
assert portfolio.name in response.data.decode()
def test_portfolio_reports(client, user_session):
portfolio = PortfolioFactory.create(
applications=[
{"name": "application1", "environments": [{"name": "application1 prod"}]}
]
)
task_order = TaskOrderFactory.create(
number="42",
start_date=random_past_date(),
end_date=random_future_date(),
portfolio=portfolio,
)
user_session(portfolio.owner)
response = client.get(
url_for("portfolios.portfolio_reports", portfolio_id=portfolio.id)
)
assert response.status_code == 200
assert portfolio.name in response.data.decode()
expiration_date = task_order.end_date.strftime("%Y-%m-%d")
assert expiration_date in response.data.decode()
def test_portfolio_reports_with_mock_portfolio(client, user_session):
portfolio = PortfolioFactory.create(name="Aardvark")
user_session(portfolio.owner)
response = client.get(
url_for("portfolios.portfolio_reports", portfolio_id=portfolio.id)
)
assert response.status_code == 200
assert portfolio.name in response.data.decode()
assert "$251,626.00 Total spend to date" in response.data.decode()

View File

@ -1,39 +0,0 @@
import re
from flask import url_for
from atst.models.request_status_event import RequestStatus
from tests.factories import RequestFactory, LegacyTaskOrderFactory, UserFactory
def test_can_show_financial_data(client, user_session):
user = UserFactory.create()
user_session(user)
legacy_task_order = LegacyTaskOrderFactory.create()
request = RequestFactory.create_with_status(
status=RequestStatus.PENDING_CCPO_APPROVAL,
legacy_task_order=legacy_task_order,
creator=user,
)
response = client.get(
url_for("requests.view_request_details", request_id=request.id)
)
body = response.data.decode()
assert re.search(r">\s+Financial Verification\s+<", body)
def test_can_not_show_financial_data(client, user_session):
user = UserFactory.create()
user_session(user)
request = RequestFactory.create_with_status(
status=RequestStatus.PENDING_CCPO_ACCEPTANCE, creator=user
)
response = client.get(
url_for("requests.view_request_details", request_id=request.id)
)
body = response.data.decode()
assert not re.search(r">\s+Financial Verification\s+<", body)

View File

@ -1,52 +0,0 @@
from tests.factories import UserFactory, RequestFactory
from atst.models.request_status_event import RequestStatus
def test_creator_pending_finver(client, user_session):
request = RequestFactory.create_with_status(
RequestStatus.PENDING_FINANCIAL_VERIFICATION
)
user_session(request.creator)
response = client.get(
"/requests/edit/{}".format(request.id), follow_redirects=False
)
assert "verify" in response.location
def test_creator_pending_finver_changes(client, user_session):
request = RequestFactory.create_with_status(
RequestStatus.CHANGES_REQUESTED_TO_FINVER
)
user_session(request.creator)
response = client.get(
"/requests/edit/{}".format(request.id), follow_redirects=False
)
assert "verify" in response.location
def test_creator_approved(client, user_session):
request = RequestFactory.create_with_status(RequestStatus.APPROVED)
user_session(request.creator)
response = client.get(
"/requests/edit/{}".format(request.id), follow_redirects=False
)
assert "details" in response.location
def test_creator_approved(client, user_session):
request = RequestFactory.create_with_status(RequestStatus.STARTED)
user_session(request.creator)
response = client.get(
"/requests/edit/{}".format(request.id), follow_redirects=False
)
assert "new" in response.location
def test_ccpo(client, user_session):
ccpo = UserFactory.from_atat_role("ccpo")
request = RequestFactory.create_with_status(RequestStatus.STARTED)
user_session(ccpo)
response = client.get(
"/requests/edit/{}".format(request.id), follow_redirects=False
)
assert "approval" in response.location

View File

@ -1,249 +0,0 @@
import datetime
import re
import pytest
from tests.factories import (
RequestFactory,
UserFactory,
RequestRevisionFactory,
RequestStatusEventFactory,
RequestReviewFactory,
)
from atst.models.request_status_event import RequestStatus
from atst.domain.roles import Roles
from atst.domain.requests import Requests
from urllib.parse import urlencode
from tests.assert_util import dict_contains
ERROR_CLASS = "alert--error"
def test_submit_invalid_request_form(monkeypatch, client, user_session):
user_session()
response = client.post(
"/requests/new/1",
headers={"Content-Type": "application/x-www-form-urlencoded"},
data="total_ram=5",
)
assert re.search(ERROR_CLASS, response.data.decode())
def test_submit_valid_request_form(monkeypatch, client, user_session):
user_session()
monkeypatch.setattr(
"atst.forms.new_request.DetailsOfUseForm.validate", lambda s: True
)
response = client.post(
"/requests/new/1",
headers={"Content-Type": "application/x-www-form-urlencoded"},
data="meaning=42",
)
assert "/requests/new/2" in response.headers.get("Location")
def test_owner_can_view_request(client, user_session):
user = UserFactory.create()
user_session(user)
request = RequestFactory.create(creator=user)
response = client.get(
"/requests/new/1/{}".format(request.id), follow_redirects=True
)
assert response.status_code == 200
def test_non_owner_cannot_view_request(client, user_session):
user = UserFactory.create()
user_session(user)
request = RequestFactory.create()
response = client.get(
"/requests/new/1/{}".format(request.id), follow_redirects=True
)
assert response.status_code == 404
def test_ccpo_can_view_request(client, user_session):
ccpo = Roles.get("ccpo")
user = UserFactory.create(atat_role=ccpo)
user_session(user)
request = RequestFactory.create()
response = client.get(
"/requests/new/1/{}".format(request.id), follow_redirects=True
)
assert response.status_code == 200
@pytest.mark.skip(reason="create request flow no longer active")
def test_nonexistent_request(client, user_session):
user_session()
response = client.get("/requests/new/1/foo", follow_redirects=True)
assert response.status_code == 404
def test_creator_info_is_autopopulated_for_existing_request(
monkeypatch, client, user_session
):
user = UserFactory.create()
user_session(user)
request = RequestFactory.create(creator=user, initial_revision={})
response = client.get("/requests/new/2/{}".format(request.id))
body = response.data.decode()
prepopulated_values = [
"first_name",
"last_name",
"email",
"phone_number",
"date_latest_training",
]
for attr in prepopulated_values:
value = getattr(user, attr)
if isinstance(value, datetime.date):
value = value.strftime("%m/%d/%Y")
assert "initial-value='{}'".format(value) in body
def test_creator_info_is_autopopulated_for_new_request(
monkeypatch, client, user_session
):
user = UserFactory.create()
user_session(user)
response = client.get("/requests/new/2")
body = response.data.decode()
assert "initial-value='{}'".format(user.first_name) in body
assert "initial-value='{}'".format(user.last_name) in body
assert "initial-value='{}'".format(user.email) in body
def test_non_creator_info_is_not_autopopulated(monkeypatch, client, user_session):
user = UserFactory.create()
creator = UserFactory.create()
user_session(user)
request = RequestFactory.create(creator=creator, initial_revision={})
response = client.get("/requests/new/2/{}".format(request.id))
body = response.data.decode()
assert not user.first_name in body
assert not user.last_name in body
assert not user.email in body
def test_am_poc_causes_poc_to_be_autopopulated(client, user_session):
creator = UserFactory.create()
user_session(creator)
request = RequestFactory.create(creator=creator, initial_revision={})
client.post(
"/requests/new/3/{}".format(request.id),
headers={"Content-Type": "application/x-www-form-urlencoded"},
data="am_poc=yes",
)
request = Requests.get(creator, request.id)
assert request.body["primary_poc"]["dodid_poc"] == creator.dod_id
def test_not_am_poc_requires_poc_info_to_be_completed(client, user_session):
creator = UserFactory.create()
user_session(creator)
request = RequestFactory.create(creator=creator, initial_revision={})
response = client.post(
"/requests/new/3/{}".format(request.id),
headers={"Content-Type": "application/x-www-form-urlencoded"},
data="am_poc=no",
follow_redirects=True,
)
assert ERROR_CLASS in response.data.decode()
def test_not_am_poc_allows_user_to_fill_in_poc_info(client, user_session):
creator = UserFactory.create()
user_session(creator)
request = RequestFactory.create(creator=creator, initial_revision={})
poc_input = {
"am_poc": "no",
"fname_poc": "test",
"lname_poc": "user",
"email_poc": "test.user@mail.com",
"dodid_poc": "1234567890",
}
response = client.post(
"/requests/new/3/{}".format(request.id),
headers={"Content-Type": "application/x-www-form-urlencoded"},
data=urlencode(poc_input),
)
assert ERROR_CLASS not in response.data.decode()
def test_poc_details_can_be_autopopulated_on_new_request(client, user_session):
creator = UserFactory.create()
user_session(creator)
response = client.post(
"/requests/new/3",
headers={"Content-Type": "application/x-www-form-urlencoded"},
data="am_poc=yes",
)
request_id = response.headers["Location"].split("/")[-1]
request = Requests.get(creator, request_id)
assert request.body["primary_poc"]["dodid_poc"] == creator.dod_id
def test_poc_autofill_checks_information_about_you_form_first(client, user_session):
creator = UserFactory.create()
user_session(creator)
request = RequestFactory.create(
creator=creator,
initial_revision=dict(
fname_request="Alice",
lname_request="Adams",
email_request="alice.adams@mail.mil",
),
)
poc_input = {"am_poc": "yes"}
client.post(
"/requests/new/3/{}".format(request.id),
headers={"Content-Type": "application/x-www-form-urlencoded"},
data=urlencode(poc_input),
)
request = Requests.get(creator, request.id)
assert dict_contains(
request.body["primary_poc"],
{
"fname_poc": "Alice",
"lname_poc": "Adams",
"email_poc": "alice.adams@mail.mil",
},
)
def test_can_review_data(user_session, client):
creator = UserFactory.create()
user_session(creator)
request = RequestFactory.create(creator=creator)
response = client.get("/requests/new/4/{}".format(request.id))
body = response.data.decode()
# assert a sampling of the request data is on the review page
assert request.body["primary_poc"]["fname_poc"] in body
assert request.body["information_about_you"]["email_request"] in body
def test_displays_ccpo_review_comment(user_session, client):
creator = UserFactory.create()
ccpo = UserFactory.from_atat_role("ccpo")
user_session(creator)
request = RequestFactory.create(creator=creator)
request = Requests.set_status(request, RequestStatus.CHANGES_REQUESTED)
review_comment = "add all of the correct info, instead of the incorrect info"
RequestReviewFactory.create(
reviewer=ccpo, comment=review_comment, status=request.status_events[-1]
)
response = client.get("/requests/new/1/{}".format(request.id))
body = response.data.decode()
assert review_comment in body

View File

@ -1,42 +0,0 @@
import pytest
from tests.factories import RequestFactory
from atst.models.request_status_event import RequestStatus
def _mock_func(*args, **kwargs):
return RequestFactory.create()
def test_submit_reviewed_request(monkeypatch, client, user_session):
user_session()
monkeypatch.setattr("atst.domain.requests.Requests.get", _mock_func)
monkeypatch.setattr("atst.domain.requests.Requests.submit", _mock_func)
monkeypatch.setattr("atst.models.request.Request.status", "pending")
# this just needs to send a known invalid form value
response = client.post(
"/requests/submit/1",
headers={"Content-Type": "application/x-www-form-urlencoded"},
data="",
follow_redirects=False,
)
assert "/requests" in response.headers["Location"]
assert "modal=pendingCCPOAcceptance" in response.headers["Location"]
def test_submit_autoapproved_reviewed_request(monkeypatch, client, user_session):
user_session()
monkeypatch.setattr("atst.domain.requests.Requests.get", _mock_func)
monkeypatch.setattr("atst.domain.requests.Requests.submit", _mock_func)
monkeypatch.setattr(
"atst.models.request.Request.status",
RequestStatus.PENDING_FINANCIAL_VERIFICATION,
)
response = client.post(
"/requests/submit/1",
headers={"Content-Type": "application/x-www-form-urlencoded"},
data="",
follow_redirects=False,
)
assert (
"/requests?modal=pendingFinancialVerification" in response.headers["Location"]
)

View File

@ -1,201 +0,0 @@
import os
from flask import url_for
from atst.models.attachment import Attachment
from atst.models.request_status_event import RequestStatus
from atst.domain.roles import Roles
from tests.factories import (
RequestFactory,
LegacyTaskOrderFactory,
UserFactory,
RequestReviewFactory,
RequestStatusEventFactory,
)
def test_ccpo_can_view_approval(user_session, client):
ccpo = Roles.get("ccpo")
user = UserFactory.create(atat_role=ccpo)
user_session(user)
request = RequestFactory.create()
response = client.get(url_for("requests.approval", request_id=request.id))
assert response.status_code == 200
def test_ccpo_prepopulated_as_mission_owner(user_session, client):
user = UserFactory.from_atat_role("ccpo")
user_session(user)
request = RequestFactory.create_with_status(RequestStatus.PENDING_CCPO_ACCEPTANCE)
response = client.get(url_for("requests.approval", request_id=request.id))
body = response.data.decode()
assert user.first_name in body
assert user.last_name in body
def test_non_ccpo_cannot_view_approval(user_session, client):
user = UserFactory.create()
user_session(user)
request = RequestFactory.create(creator=user)
response = client.get(url_for("requests.approval", request_id=request.id))
assert response.status_code == 404
def prepare_request_pending_approval(creator, pdf_attachment=None):
legacy_task_order = LegacyTaskOrderFactory.create(
number="abc123", pdf=pdf_attachment
)
return RequestFactory.create_with_status(
status=RequestStatus.PENDING_CCPO_APPROVAL,
legacy_task_order=legacy_task_order,
creator=creator,
)
def test_ccpo_sees_pdf_link(user_session, client, pdf_upload):
ccpo = UserFactory.from_atat_role("ccpo")
user_session(ccpo)
attachment = Attachment.attach(pdf_upload)
request = prepare_request_pending_approval(ccpo, pdf_attachment=attachment)
response = client.get(url_for("requests.approval", request_id=request.id))
download_url = url_for("requests.task_order_pdf_download", request_id=request.id)
body = response.data.decode()
assert download_url in body
def test_ccpo_does_not_see_pdf_link_if_no_pdf(user_session, client, pdf_upload):
ccpo = UserFactory.from_atat_role("ccpo")
user_session(ccpo)
request = prepare_request_pending_approval(ccpo)
response = client.get(url_for("requests.approval", request_id=request.id))
download_url = url_for("requests.task_order_pdf_download", request_id=request.id)
body = response.data.decode()
assert download_url not in body
def test_task_order_download(app, client, user_session, pdf_upload):
user = UserFactory.create()
user_session(user)
attachment = Attachment.attach(pdf_upload)
legacy_task_order = LegacyTaskOrderFactory.create(number="abc123", pdf=attachment)
request = RequestFactory.create(legacy_task_order=legacy_task_order, creator=user)
# ensure that real data for pdf upload has been flushed to disk
pdf_upload.seek(0)
pdf_content = pdf_upload.read()
pdf_upload.close()
full_path = os.path.join(
app.config.get("STORAGE_CONTAINER"), attachment.object_name
)
with open(full_path, "wb") as output_file:
output_file.write(pdf_content)
output_file.flush()
response = client.get(
url_for("requests.task_order_pdf_download", request_id=request.id)
)
assert response.data == pdf_content
def test_task_order_download_does_not_exist(client, user_session):
user = UserFactory.create()
user_session(user)
request = RequestFactory.create(creator=user)
response = client.get(
url_for("requests.task_order_pdf_download", request_id=request.id)
)
assert response.status_code == 404
def test_can_submit_request_approval(client, user_session):
user = UserFactory.from_atat_role("ccpo")
user_session(user)
request = RequestFactory.create_with_status(
status=RequestStatus.PENDING_CCPO_ACCEPTANCE
)
review_data = RequestReviewFactory.dictionary()
review_data["review"] = "approving"
response = client.post(
url_for("requests.submit_approval", request_id=request.id), data=review_data
)
assert response.status_code == 302
assert request.status == RequestStatus.PENDING_FINANCIAL_VERIFICATION
def test_can_submit_request_denial(client, user_session):
user = UserFactory.from_atat_role("ccpo")
user_session(user)
request = RequestFactory.create_with_status(
status=RequestStatus.PENDING_CCPO_ACCEPTANCE
)
review_data = RequestReviewFactory.dictionary()
review_data["review"] = "denying"
response = client.post(
url_for("requests.submit_approval", request_id=request.id), data=review_data
)
assert response.status_code == 302
assert request.status == RequestStatus.CHANGES_REQUESTED
def test_ccpo_user_can_comment_on_request(client, user_session):
user = UserFactory.from_atat_role("ccpo")
user_session(user)
request = RequestFactory.create_with_status(
status=RequestStatus.PENDING_CCPO_ACCEPTANCE
)
assert len(request.internal_comments) == 0
comment_text = "This is the greatest request in the history of requests"
comment_form_data = {"text": comment_text}
response = client.post(
url_for("requests.create_internal_comment", request_id=request.id),
data=comment_form_data,
)
assert response.status_code == 302
assert len(request.internal_comments) == 1
assert request.internal_comments[0].text == comment_text
def test_comment_text_is_required(client, user_session):
user = UserFactory.from_atat_role("ccpo")
user_session(user)
request = RequestFactory.create_with_status(
status=RequestStatus.PENDING_CCPO_ACCEPTANCE
)
assert len(request.internal_comments) == 0
comment_form_data = {"text": ""}
response = client.post(
url_for("requests.create_internal_comment", request_id=request.id),
data=comment_form_data,
)
assert response.status_code == 200
assert len(request.internal_comments) == 0
def test_other_user_cannot_comment_on_request(client, user_session):
user = UserFactory.create()
user_session(user)
request = RequestFactory.create_with_status(
status=RequestStatus.PENDING_CCPO_ACCEPTANCE
)
comment_text = "What is this even"
comment_form_data = {"text": comment_text}
response = client.post(
url_for("requests.create_internal_comment", request_id=request.id),
data=comment_form_data,
)
assert response.status_code == 404

View File

@ -1,543 +0,0 @@
import pytest
from unittest.mock import MagicMock
from flask import url_for
import datetime
from atst.eda_client import MockEDAClient
from atst.routes.requests.financial_verification import (
GetFinancialVerificationForm,
UpdateFinancialVerification,
SaveFinancialVerificationDraft,
)
from tests.mocks import MOCK_VALID_PE_ID
from tests.factories import RequestFactory, UserFactory, LegacyTaskOrderFactory
from atst.forms.exceptions import FormValidationError
from atst.domain.requests.financial_verification import (
PENumberValidator,
TaskOrderNumberValidator,
)
from atst.models.request_status_event import RequestStatus
from atst.models.attachment import Attachment
from atst.domain.requests.query import RequestsQuery
@pytest.fixture
def fv_data():
return {
"request-pe_id": "123",
"legacy_task_order-number": MockEDAClient.MOCK_CONTRACT_NUMBER,
"request-fname_co": "Contracting",
"request-lname_co": "Officer",
"request-email_co": "jane@mail.mil",
"request-office_co": "WHS",
"request-fname_cor": "Officer",
"request-lname_cor": "Representative",
"request-email_cor": "jane@mail.mil",
"request-office_cor": "WHS",
"request-uii_ids": "1234",
"request-treasury_code": "00123456",
"request-ba_code": "02A",
}
@pytest.fixture
def e_fv_data(pdf_upload):
return {
"legacy_task_order-funding_type": "RDTE",
"legacy_task_order-funding_type_other": "other",
"legacy_task_order-expiration_date": "1/1/{}".format(
datetime.date.today().year + 1
),
"legacy_task_order-clin_0001": "50000",
"legacy_task_order-clin_0003": "13000",
"legacy_task_order-clin_1001": "30000",
"legacy_task_order-clin_1003": "7000",
"legacy_task_order-clin_2001": "30000",
"legacy_task_order-clin_2003": "7000",
"legacy_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, "legacy_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_draft_without_pe_id(fv_data):
request = RequestFactory.create()
user = UserFactory.create()
data = {"request-uii_ids": "1234"}
assert SaveFinancialVerificationDraft(
PENumberValidator(),
TaskOrderNumberValidator(),
user,
request,
data,
is_extended=False,
).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_extended_does_not_validate_task_order(fv_data, e_fv_data):
request = RequestFactory.create()
user = UserFactory.create()
data = {**fv_data, **e_fv_data, "legacy_task_order-number": "abc123"}
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_allows_invalid_data():
request = RequestFactory.create()
user = UserFactory.create()
data = {
"legacy_task_order-number": MANUAL_TO_NUMBER,
"request-pe_id": "123",
"request-ba_code": "a",
}
assert SaveFinancialVerificationDraft(
PENumberValidator(),
TaskOrderNumberValidator(),
user,
request,
data,
is_extended=True,
).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, "legacy_task_order-number": MANUAL_TO_NUMBER}
updated_request = UpdateFinancialVerification(
TrueValidator, TrueValidator, user, request, data, is_extended=True
).execute()
assert updated_request.legacy_task_order.pdf
def test_can_save_draft_with_just_pdf(e_fv_data):
request = RequestFactory.create()
user = UserFactory.create()
data = {"legacy_task_order-pdf": e_fv_data["legacy_task_order-pdf"]}
SaveFinancialVerificationDraft(
TrueValidator, TrueValidator, user, request, data, is_extended=True
).execute()
form = GetFinancialVerificationForm(user, request, is_extended=True).execute()
assert form.legacy_task_order.pdf
def test_task_order_info_present_in_extended_form(fv_data, e_fv_data):
request = RequestFactory.create()
user = UserFactory.create()
data = {
"legacy_task_order-clin_0001": "1",
"legacy_task_order-number": fv_data["legacy_task_order-number"],
}
SaveFinancialVerificationDraft(
TrueValidator, TrueValidator, user, request, data, is_extended=True
).execute()
form = GetFinancialVerificationForm(user, request, is_extended=True).execute()
assert form.legacy_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, "legacy_task_order-funding_type": ""}
SaveFinancialVerificationDraft(
TrueValidator, TrueValidator, user, request, data, is_extended=True
).execute()
def test_can_save_draft_with_funding_type(fv_data, e_fv_data):
request = RequestFactory.create()
user = UserFactory.create()
data = {
"legacy_task_order-number": fv_data["legacy_task_order-number"],
"legacy_task_order-funding_type": e_fv_data["legacy_task_order-funding_type"],
}
updated_request = SaveFinancialVerificationDraft(
TrueValidator, TrueValidator, user, request, data, is_extended=False
).execute()
assert updated_request.legacy_task_order.funding_type
def test_update_fv_route(client, user_session, fv_data):
user = UserFactory.create()
request = RequestFactory.create(creator=user)
user_session(user)
response = client.post(
url_for("requests.financial_verification", request_id=request.id),
data=fv_data,
follow_redirects=False,
)
assert response.status_code == 200
def test_save_fv_draft_route(client, user_session, fv_data):
user = UserFactory.create()
request = RequestFactory.create(creator=user)
user_session(user)
response = client.post(
url_for("requests.save_financial_verification_draft", request_id=request.id),
data=fv_data,
follow_redirects=True,
)
assert response.status_code == 200
def test_get_fv_form_route(client, user_session, fv_data):
user = UserFactory.create()
request = RequestFactory.create(creator=user)
user_session(user)
response = client.get(
url_for("requests.financial_verification", request_id=request.id),
data=fv_data,
follow_redirects=False,
)
assert response.status_code == 200
def test_manual_task_order_triggers_extended_form(
client, user_session, fv_data, e_fv_data
):
user = UserFactory.create()
request = RequestFactory.create(creator=user)
data = {**fv_data, **e_fv_data, "legacy_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,
"legacy_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,
)
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,
"legacy_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_attachment_on_non_extended_form(client, user_session, fv_data, e_fv_data):
user = UserFactory.create()
request = RequestFactory.create(creator=user)
data = {
**fv_data,
**e_fv_data,
"legacy_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,
)
response = client.get(
url_for("requests.financial_verification", request_id=request.id)
)
assert response.status_code == 200
def test_task_order_number_persists_in_form(fv_data, e_fv_data):
user = UserFactory.create()
request = RequestFactory.create(creator=user)
data = {
**fv_data,
"legacy_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.legacy_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,
"legacy_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,
"legacy_task_order-number": MANUAL_TO_NUMBER,
"request-pe_id": "0101228N",
}
assert UpdateFinancialVerification(
TrueValidator, TrueValidator, user, request, data, is_extended=True
).execute()
def test_existing_task_order_with_pdf(fv_data, e_fv_data, client, user_session):
# Use finver route to create initial TO #1, complete with PDF
user = UserFactory.create()
request = RequestFactory.create(creator=user)
data = {**fv_data, **e_fv_data, "legacy_task_order-number": MANUAL_TO_NUMBER}
UpdateFinancialVerification(
TrueValidator, TaskOrderNumberValidator(), user, request, data, is_extended=True
).execute()
# Save draft on a new finver form, but with same number as TO #1
user = UserFactory.create()
request = RequestFactory.create(creator=user)
data = {"legacy_task_order-number": MANUAL_TO_NUMBER}
SaveFinancialVerificationDraft(
TrueValidator,
TaskOrderNumberValidator(),
user,
request,
data,
is_extended=False,
).execute()
# Get finver form
user_session(user)
response = client.get(
url_for("requests.financial_verification", request_id=request.id),
follow_redirects=True,
)
assert response.status_code == 200
def test_pdf_clearing(fv_data, e_fv_data, pdf_upload, pdf_upload2):
user = UserFactory.create()
request = RequestFactory.create(creator=user)
data = {**fv_data, **e_fv_data, "legacy_task_order-pdf": pdf_upload}
SaveFinancialVerificationDraft(
TrueValidator, TrueValidator, user, request, data, is_extended=True
).execute()
data = {**data, "legacy_task_order-pdf": pdf_upload2}
UpdateFinancialVerification(
TrueValidator, TrueValidator, user, request, data, is_extended=True
).execute()
form = GetFinancialVerificationForm(user, request, is_extended=True).execute()
assert form.legacy_task_order.pdf.data == pdf_upload2.filename
# TODO: This test manages an edge case for our current non-unique handling of
# task orders. Because two requests can reference the same task order but we
# only record one request ID on the PDF attachment, multiple task
# orders/requests reference the same task order but only one of them is noted
# in the related attachment entity. I have changed the handling in
# FinancialVerificationBase#_get_form to be more generous in how it finds the
# PDF filename and prepopulates the form data with that name.
def test_always_derives_pdf_filename(fv_data, e_fv_data, pdf_upload):
user = UserFactory.create()
request_one = RequestFactory.create(creator=user)
attachment = Attachment.attach(
pdf_upload, resource="legacy_task_order", resource_id=request_one.id
)
legacy_task_order = LegacyTaskOrderFactory.create(pdf=attachment)
request_two = RequestFactory.create(
creator=user, legacy_task_order=legacy_task_order
)
form_one = GetFinancialVerificationForm(
user, request_one, is_extended=True
).execute()
form_two = GetFinancialVerificationForm(
user, request_two, is_extended=True
).execute()
assert form_one.legacy_task_order.pdf.data == attachment.filename
assert form_two.legacy_task_order.pdf.data == attachment.filename

View File

@ -1,28 +0,0 @@
from flask import url_for
from atst.routes.requests.index import RequestsIndex
from tests.factories import RequestFactory, UserFactory
from atst.domain.requests import Requests
def test_action_required_mission_owner():
creator = UserFactory.create()
requests = RequestFactory.create_batch(5, creator=creator)
Requests.submit(requests[0])
Requests.approve_and_create_portfolio(requests[1])
context = RequestsIndex(creator).execute()
assert context["requests"][0]["action_required"] == False
def test_action_required_ccpo():
creator = UserFactory.create()
requests = RequestFactory.create_batch(5, creator=creator)
Requests.submit(requests[0])
Requests.approve_and_create_portfolio(requests[1])
ccpo = UserFactory.from_atat_role("ccpo")
context = RequestsIndex(ccpo).execute()
assert context["num_action_required"] == 1

View File

@ -1,28 +1,27 @@
import pytest import pytest
from tests.factories import UserFactory, PortfolioFactory, RequestFactory from tests.factories import UserFactory, PortfolioFactory
from atst.domain.portfolios import Portfolios from atst.domain.portfolios import Portfolios
from atst.models.portfolio_role import Status as PortfolioRoleStatus from atst.models.portfolio_role import Status as PortfolioRoleStatus
def test_request_owner_with_one_portfolio_redirected_to_reports(client, user_session): def test_portfolio_owner_with_one_portfolio_redirected_to_reports(client, user_session):
request = RequestFactory.create() portfolio = PortfolioFactory.create()
portfolio = Portfolios.create_from_request(request)
user_session(request.creator) user_session(portfolio.owner)
response = client.get("/home", follow_redirects=False) response = client.get("/home", follow_redirects=False)
assert "/portfolios/{}/reports".format(portfolio.id) in response.location assert "/portfolios/{}/reports".format(portfolio.id) in response.location
def test_request_owner_with_more_than_one_portfolio_redirected_to_portfolios( def test_portfolio_owner_with_more_than_one_portfolio_redirected_to_portfolios(
client, user_session client, user_session
): ):
request_creator = UserFactory.create() owner = UserFactory.create()
Portfolios.create_from_request(RequestFactory.create(creator=request_creator)) PortfolioFactory.create(owner=owner)
Portfolios.create_from_request(RequestFactory.create(creator=request_creator)) PortfolioFactory.create(owner=owner)
user_session(request_creator) user_session(owner)
response = client.get("/home", follow_redirects=False) response = client.get("/home", follow_redirects=False)
assert "/portfolios" in response.location assert "/portfolios" in response.location
@ -61,16 +60,3 @@ def test_non_owner_user_with_mulitple_portfolios_redirected_to_portfolios(
alphabetically_first_portfolio = sorted(portfolios, key=lambda p: p.name)[0] alphabetically_first_portfolio = sorted(portfolios, key=lambda p: p.name)[0]
assert "/portfolios" in response.location assert "/portfolios" in response.location
assert str(alphabetically_first_portfolio.id) in response.location assert str(alphabetically_first_portfolio.id) in response.location
@pytest.mark.skip(reason="this may no longer be accurate")
def test_ccpo_user_redirected_to_requests(client, user_session):
user = UserFactory.from_atat_role("ccpo")
for _ in range(3):
portfolio = PortfolioFactory.create()
Portfolios._create_portfolio_role(user, portfolio, "developer")
user_session(user)
response = client.get("/home", follow_redirects=False)
assert "/requests" in response.location

View File

@ -1,84 +0,0 @@
import pytest
from urllib.parse import urlencode
from .factories import UserFactory, RequestFactory
from atst.routes.requests.jedi_request_flow import JEDIRequestFlow
from atst.models.request_status_event import RequestStatus
from atst.domain.requests import Requests
@pytest.fixture
def screens(app):
return JEDIRequestFlow(3).screens
def serialize_dates(data):
if not data:
return data
dates = {
k: v.strftime("%m/%d/%Y") for k, v in data.items() if hasattr(v, "strftime")
}
new_data = data.copy()
new_data.update(dates)
return new_data
def test_stepthrough_request_form(user_session, screens, client):
user = UserFactory.create()
user_session(user)
mock_request = RequestFactory.create()
mock_body = mock_request.body
def post_form(url, redirects=False, data=""):
return client.post(
url,
headers={"Content-Type": "application/x-www-form-urlencoded"},
data=data,
follow_redirects=redirects,
)
def take_a_step(inc, req=None, data=None):
req_url = "/requests/new/{}".format(inc)
if req:
req_url += "/" + req
# we do it twice, with and without redirect, in order to get the
# destination url
prelim_resp = post_form(req_url, data=data)
response = post_form(req_url, True, data=data)
assert prelim_resp.status_code == 302
return (prelim_resp.headers.get("Location"), response)
# GET the initial form
response = client.get("/requests/new/1")
assert screens[0]["title"] in response.data.decode()
# POST to each of the form pages up until review and submit
req_id = None
for i in range(1, len(screens)):
# get appropriate form data to POST for this section
section = screens[i - 1]["section"]
massaged = serialize_dates(mock_body[section])
post_data = urlencode(massaged)
effective_url, resp = take_a_step(i, req=req_id, data=post_data)
req_id = effective_url.split("/")[-1]
screen_title = screens[i]["title"].replace("&", "&amp;")
assert "/requests/new/{}/{}".format(i + 1, req_id) in effective_url
assert screen_title in resp.data.decode()
# at this point, the real request we made and the mock_request bodies
# should be equivalent
assert Requests.get(user, req_id).body == mock_body
# finish the review and submit step
client.post(
"/requests/submit/{}".format(req_id),
headers={"Content-Type": "application/x-www-form-urlencoded"},
)
finished_request = Requests.get(user, req_id)
assert finished_request.status == RequestStatus.PENDING_CCPO_ACCEPTANCE