Merge pull request #673 from dod-ccpo/eradicate-requests
Remove no-longer-used requests code
This commit is contained in:
commit
99d79cef79
46
alembic/versions/3777e9e39644_add_indexes.py
Normal file
46
alembic/versions/3777e9e39644_add_indexes.py
Normal 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 ###
|
32
alembic/versions/978bf56e21b6_remove_pe_number_model.py
Normal file
32
alembic/versions/978bf56e21b6_remove_pe_number_model.py
Normal 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 ###
|
150
alembic/versions/c92cec2f32d4_remove_request_related_models.py
Normal file
150
alembic/versions/c92cec2f32d4_remove_request_related_models.py
Normal 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 ###
|
@ -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 ###
|
@ -13,7 +13,6 @@ from atst.assets import environment as assets_environment
|
||||
from atst.filters import register_filters
|
||||
from atst.routes import bp
|
||||
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.dev import bp as dev_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(task_orders_bp)
|
||||
app.register_blueprint(user_routes)
|
||||
app.register_blueprint(requests_bp)
|
||||
|
||||
if ENV != "prod":
|
||||
app.register_blueprint(dev_routes)
|
||||
|
@ -1,4 +1,3 @@
|
||||
import datetime
|
||||
from itertools import groupby
|
||||
from collections import OrderedDict
|
||||
import pendulum
|
||||
@ -33,10 +32,10 @@ class MockApplication:
|
||||
|
||||
|
||||
def generate_sample_dates(_max=8):
|
||||
current = datetime.datetime.today()
|
||||
current = pendulum.now()
|
||||
sample_dates = []
|
||||
for _i in range(_max):
|
||||
current = current - datetime.timedelta(days=29)
|
||||
current = current.subtract(months=1)
|
||||
sample_dates.append(current.strftime("%m/%Y"))
|
||||
|
||||
reversed(sample_dates)
|
||||
@ -225,8 +224,6 @@ class MockReportingProvider(ReportingInterface):
|
||||
def get_budget(self, portfolio):
|
||||
if portfolio.name in self.REPORT_FIXTURE_MAP:
|
||||
return self.REPORT_FIXTURE_MAP[portfolio.name]["budget"]
|
||||
elif portfolio.request and portfolio.legacy_task_order:
|
||||
return portfolio.legacy_task_order.budget
|
||||
return 0
|
||||
|
||||
def get_total_spending(self, portfolio):
|
||||
|
@ -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
|
@ -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()
|
@ -24,16 +24,6 @@ class Portfolios(object):
|
||||
PortfoliosQuery.add_and_commit(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
|
||||
def get(cls, user, portfolio_id):
|
||||
portfolio = PortfoliosQuery.get(portfolio_id)
|
||||
@ -76,10 +66,6 @@ class Portfolios(object):
|
||||
|
||||
return portfolio
|
||||
|
||||
@classmethod
|
||||
def get_by_request(cls, request):
|
||||
return PortfoliosQuery.get_by_request(request)
|
||||
|
||||
@classmethod
|
||||
def get_with_members(cls, user, portfolio_id):
|
||||
portfolio = PortfoliosQuery.get(portfolio_id)
|
||||
|
@ -1,8 +1,5 @@
|
||||
from sqlalchemy.orm.exc import NoResultFound
|
||||
|
||||
from atst.database import db
|
||||
from atst.domain.common import Query
|
||||
from atst.domain.exceptions import NotFoundError
|
||||
from atst.models.portfolio import Portfolio
|
||||
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):
|
||||
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
|
||||
def get_for_user(cls, user):
|
||||
return (
|
||||
|
@ -1 +0,0 @@
|
||||
from .requests import Requests, create_revision_from_request_body
|
@ -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",
|
||||
)
|
@ -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",)
|
@ -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
|
@ -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()]
|
@ -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
|
||||
)
|
@ -36,23 +36,6 @@ def usPhone(number):
|
||||
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=[]):
|
||||
if not filter_name:
|
||||
return value
|
||||
@ -62,10 +45,6 @@ def findFilter(value, filter_name, filter_args=[]):
|
||||
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"):
|
||||
if value:
|
||||
return value.strftime(formatter)
|
||||
@ -95,11 +74,6 @@ def renderAuditEvent(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):
|
||||
# reorders titles from "Army, Department of the" to "Department of the Army"
|
||||
text = title.split(", ")
|
||||
@ -114,15 +88,11 @@ def register_filters(app):
|
||||
app.jinja_env.filters["justDollars"] = justDollars
|
||||
app.jinja_env.filters["justCents"] = justCents
|
||||
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["renderList"] = renderList
|
||||
app.jinja_env.filters["formattedDate"] = formattedDate
|
||||
app.jinja_env.filters["dateFromString"] = dateFromString
|
||||
app.jinja_env.filters["pageWindow"] = pageWindow
|
||||
app.jinja_env.filters["renderAuditEvent"] = renderAuditEvent
|
||||
app.jinja_env.filters["removeHtml"] = removeHtml
|
||||
app.jinja_env.filters["normalizeOrder"] = normalizeOrder
|
||||
app.jinja_env.filters["translateDuration"] = translate_duration
|
||||
|
||||
|
@ -1,29 +1,4 @@
|
||||
from wtforms.fields import Field, FormField, StringField, 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
|
||||
from wtforms.fields import FormField, SelectField as SelectField_
|
||||
|
||||
|
||||
class SelectField(SelectField_):
|
||||
@ -33,14 +8,6 @@ class SelectField(SelectField_):
|
||||
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):
|
||||
def has_changes(self):
|
||||
if not self.object_data:
|
||||
|
@ -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
|
@ -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"))
|
@ -2,21 +2,14 @@ from sqlalchemy.ext.declarative import declarative_base
|
||||
|
||||
Base = declarative_base()
|
||||
|
||||
from .request import Request
|
||||
from .request_status_event import RequestStatusEvent
|
||||
from .permissions import Permissions
|
||||
from .role import Role
|
||||
from .user import User
|
||||
from .portfolio_role import PortfolioRole
|
||||
from .pe_number import PENumber
|
||||
from .legacy_task_order import LegacyTaskOrder
|
||||
from .portfolio import Portfolio
|
||||
from .application import Application
|
||||
from .environment import Environment
|
||||
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 .invitation import Invitation
|
||||
from .task_order import TaskOrder
|
||||
|
@ -17,9 +17,6 @@ class AuditEvent(Base, TimestampsMixin):
|
||||
portfolio_id = Column(UUID(as_uuid=True), ForeignKey("portfolios.id"), index=True)
|
||||
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())
|
||||
event_details = Column(JSONB())
|
||||
|
||||
|
@ -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,
|
||||
)
|
@ -14,7 +14,6 @@ class AuditableMixin(object):
|
||||
def create_audit_event(connection, resource, action):
|
||||
user_id = getattr_path(g, "current_user.id")
|
||||
portfolio_id = resource.portfolio_id
|
||||
request_id = resource.request_id
|
||||
resource_type = resource.resource_type
|
||||
display_name = resource.displayname
|
||||
event_details = resource.event_details
|
||||
@ -24,7 +23,6 @@ class AuditableMixin(object):
|
||||
audit_event = AuditEvent(
|
||||
user_id=user_id,
|
||||
portfolio_id=portfolio_id,
|
||||
request_id=request_id,
|
||||
resource_type=resource_type,
|
||||
resource_id=resource.id,
|
||||
display_name=display_name,
|
||||
@ -91,10 +89,6 @@ class AuditableMixin(object):
|
||||
def portfolio_id(self):
|
||||
return None
|
||||
|
||||
@property
|
||||
def request_id(self):
|
||||
return None
|
||||
|
||||
@property
|
||||
def displayname(self):
|
||||
return None
|
||||
|
@ -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
|
||||
)
|
@ -1,4 +1,4 @@
|
||||
from sqlalchemy import Column, ForeignKey, String
|
||||
from sqlalchemy import Column, String
|
||||
from sqlalchemy.orm import relationship
|
||||
from itertools import chain
|
||||
|
||||
@ -13,7 +13,6 @@ class Portfolio(Base, mixins.TimestampsMixin, mixins.AuditableMixin):
|
||||
|
||||
id = types.Id()
|
||||
name = Column(String)
|
||||
request_id = Column(ForeignKey("requests.id"), nullable=True)
|
||||
applications = relationship("Application", back_populates="portfolio")
|
||||
roles = relationship("PortfolioRole")
|
||||
|
||||
@ -35,10 +34,6 @@ class Portfolio(Base, mixins.TimestampsMixin, mixins.AuditableMixin):
|
||||
def user_count(self):
|
||||
return len(self.members)
|
||||
|
||||
@property
|
||||
def legacy_task_order(self):
|
||||
return self.request.legacy_task_order if self.request else None
|
||||
|
||||
@property
|
||||
def members(self):
|
||||
return (
|
||||
@ -60,6 +55,6 @@ class Portfolio(Base, mixins.TimestampsMixin, mixins.AuditableMixin):
|
||||
return self.id
|
||||
|
||||
def __repr__(self):
|
||||
return "<Portfolio(name='{}', request='{}', user_count='{}', id='{}')>".format(
|
||||
self.name, self.request_id, self.user_count, self.id
|
||||
return "<Portfolio(name='{}', user_count='{}', id='{}')>".format(
|
||||
self.name, self.user_count, self.id
|
||||
)
|
||||
|
@ -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,
|
||||
)
|
@ -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
|
||||
)
|
@ -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
|
||||
)
|
@ -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",
|
||||
]
|
||||
)
|
@ -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
|
||||
)
|
@ -16,7 +16,6 @@ import pendulum
|
||||
import os
|
||||
from werkzeug.exceptions import NotFound
|
||||
|
||||
from atst.domain.requests import Requests
|
||||
from atst.domain.users import Users
|
||||
from atst.domain.authnid import AuthenticationContext
|
||||
from atst.domain.audit_log import AuditLog
|
||||
@ -58,10 +57,6 @@ def helpdocs(doc=None):
|
||||
@bp.route("/home")
|
||||
def home():
|
||||
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])
|
||||
|
||||
if num_portfolios == 0:
|
||||
|
@ -98,19 +98,3 @@ def portfolio_reports(portfolio_id):
|
||||
expiration_date=expiration_date,
|
||||
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,
|
||||
)
|
||||
|
@ -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}
|
@ -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)
|
@ -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,
|
||||
)
|
||||
)
|
@ -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)
|
@ -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}
|
@ -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)
|
@ -5,24 +5,6 @@ def first_or_none(predicate, lst):
|
||||
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):
|
||||
_obj = obj
|
||||
for item in path.split("."):
|
||||
@ -33,23 +15,11 @@ def getattr_path(obj, path, default=None):
|
||||
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):
|
||||
s1 = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", camel_cased)
|
||||
return re.sub("([a-z0-9])([A-Z])", r"\1_\2", s1).lower()
|
||||
|
||||
|
||||
def drop(keys, dct):
|
||||
_keys = set(keys)
|
||||
return {k: v for k, v in dct.items() if k not in _keys}
|
||||
|
||||
|
||||
def pick(keys, dct):
|
||||
_keys = set(keys)
|
||||
return {k: v for (k, v) in dct.items() if k in _keys}
|
||||
|
@ -88,29 +88,6 @@ MESSAGES = {
|
||||
"message_template": "",
|
||||
"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": {
|
||||
"title_template": "User access successfully changed.",
|
||||
"message_template": "",
|
||||
|
@ -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,
|
||||
])
|
||||
})
|
||||
})
|
||||
})
|
@ -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
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
@ -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
|
||||
},
|
||||
},
|
||||
}
|
@ -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
|
||||
},
|
||||
},
|
||||
}
|
@ -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>',
|
||||
}
|
@ -11,11 +11,9 @@ import optionsinput from './components/options_input'
|
||||
import multicheckboxinput from './components/multi_checkbox_input'
|
||||
import textinput from './components/text_input'
|
||||
import checkboxinput from './components/checkbox_input'
|
||||
import DetailsOfUse from './components/forms/details_of_use'
|
||||
import EditOfficerForm from './components/forms/edit_officer_form'
|
||||
import poc from './components/forms/poc'
|
||||
import oversight from './components/forms/oversight'
|
||||
import financial from './components/forms/financial'
|
||||
import toggler from './components/toggler'
|
||||
import NewApplication from './components/forms/new_application'
|
||||
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 SpendTable from './components/tables/spend_table'
|
||||
import TaskOrderList from './components/tables/task_order_list.js'
|
||||
import CcpoApproval from './components/forms/ccpo_approval'
|
||||
import MembersList from './components/members_list'
|
||||
import LocalDatetime from './components/local_datetime'
|
||||
import RequestsList from './components/requests_list'
|
||||
import ConfirmationPopover from './components/confirmation_popover'
|
||||
import { isNotInVerticalViewport } from './lib/viewport'
|
||||
import DateSelector from './components/date_selector'
|
||||
@ -51,21 +47,17 @@ const app = new Vue({
|
||||
multicheckboxinput,
|
||||
textinput,
|
||||
checkboxinput,
|
||||
DetailsOfUse,
|
||||
poc,
|
||||
oversight,
|
||||
financial,
|
||||
NewApplication,
|
||||
selector,
|
||||
BudgetChart,
|
||||
SpendTable,
|
||||
TaskOrderList,
|
||||
CcpoApproval,
|
||||
MembersList,
|
||||
LocalDatetime,
|
||||
EditEnvironmentRole,
|
||||
EditApplicationRoles,
|
||||
RequestsList,
|
||||
ConfirmationPopover,
|
||||
funding,
|
||||
uploadinput,
|
||||
|
@ -1 +1 @@
|
||||
Subproject commit 78c51d8dd29b47fd42570896daaded5e2181e923
|
||||
Subproject commit eb9ea572e4c5157c8e7ba6105ac4efd1df39392e
|
@ -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)
|
@ -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) }}
|
@ -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 %}
|
@ -6,7 +6,6 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Portfolio Name</th>
|
||||
<th>Task Order</th>
|
||||
<th>Users</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@ -16,9 +15,6 @@
|
||||
<td>
|
||||
<a class='icon-link icon-link--large' href="/portfolios/{{ portfolio.id }}/applications">{{ portfolio.name }}</a><br>
|
||||
</td>
|
||||
<td>
|
||||
#{{ portfolio.legacy_task_order.number }}
|
||||
</td>
|
||||
<td>
|
||||
<span class="label">{{ portfolio.user_count }}</span><span class='h6'>Users</span>
|
||||
</td>
|
||||
|
@ -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 %}
|
@ -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 %}
|
@ -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 %}
|
@ -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 %}
|
@ -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 %}
|
@ -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 %}
|
@ -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 %}
|
@ -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>
|
@ -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>
|
@ -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 %}
|
@ -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 %}
|
@ -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 provider’s 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 %}
|
@ -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 & 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 %}
|
@ -1,33 +0,0 @@
|
||||
<div class="sidenav">
|
||||
<ul>
|
||||
<li>
|
||||
<a class="sidenav__link" href="/requests">‹ 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>
|
@ -1,11 +1,10 @@
|
||||
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
|
||||
|
||||
|
||||
def test_create_application_with_multiple_environments():
|
||||
request = RequestFactory.create()
|
||||
portfolio = Portfolios.create_from_request(request)
|
||||
portfolio = PortfolioFactory.create()
|
||||
application = Applications.create(
|
||||
portfolio.owner, portfolio, "My Test Application", "Test", ["dev", "prod"]
|
||||
)
|
||||
|
@ -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")
|
@ -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])
|
@ -8,12 +8,7 @@ from atst.domain.applications import Applications
|
||||
from atst.domain.environments import Environments
|
||||
from atst.models.portfolio_role import Status as PortfolioRoleStatus
|
||||
|
||||
from tests.factories import (
|
||||
RequestFactory,
|
||||
UserFactory,
|
||||
PortfolioRoleFactory,
|
||||
PortfolioFactory,
|
||||
)
|
||||
from tests.factories import UserFactory, PortfolioRoleFactory, PortfolioFactory
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
@ -22,39 +17,21 @@ def portfolio_owner():
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def request_(portfolio_owner):
|
||||
return RequestFactory.create(creator=portfolio_owner)
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def portfolio(request_):
|
||||
portfolio = Portfolios.create_from_request(request_)
|
||||
def portfolio(portfolio_owner):
|
||||
portfolio = PortfolioFactory.create(owner=portfolio_owner)
|
||||
return portfolio
|
||||
|
||||
|
||||
def test_can_create_portfolio(request_):
|
||||
portfolio = Portfolios.create_from_request(request_, name="frugal-whale")
|
||||
def test_can_create_portfolio():
|
||||
portfolio = PortfolioFactory.create(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():
|
||||
with pytest.raises(NotFoundError):
|
||||
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):
|
||||
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):
|
||||
portfolio_owner = UserFactory.create()
|
||||
portfolio = Portfolios.create_from_request(
|
||||
RequestFactory.create(creator=portfolio_owner)
|
||||
)
|
||||
portfolio = Portfolios.get_with_members(portfolio_owner, portfolio.id)
|
||||
|
||||
assert portfolio
|
||||
@ -258,7 +231,7 @@ def test_for_user_returns_active_portfolios_for_user(portfolio, portfolio_owner)
|
||||
PortfolioRoleFactory.create(
|
||||
user=bob, portfolio=portfolio, status=PortfolioRoleStatus.ACTIVE
|
||||
)
|
||||
Portfolios.create_from_request(RequestFactory.create())
|
||||
PortfolioFactory.create()
|
||||
|
||||
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):
|
||||
bob = UserFactory.from_atat_role("default")
|
||||
Portfolios.add_member(portfolio, bob, "developer")
|
||||
Portfolios.create_from_request(RequestFactory.create())
|
||||
PortfolioFactory.create()
|
||||
bobs_portfolios = Portfolios.for_user(bob)
|
||||
|
||||
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):
|
||||
sam = UserFactory.from_atat_role("ccpo")
|
||||
Portfolios.create_from_request(RequestFactory.create())
|
||||
PortfolioFactory.create()
|
||||
|
||||
sams_portfolios = Portfolios.for_user(sam)
|
||||
assert len(sams_portfolios) == 2
|
||||
|
||||
|
||||
def test_get_for_update_information():
|
||||
portfolio_owner = UserFactory.create()
|
||||
portfolio = Portfolios.create_from_request(
|
||||
RequestFactory.create(creator=portfolio_owner)
|
||||
)
|
||||
def test_get_for_update_information(portfolio, portfolio_owner):
|
||||
owner_ws = Portfolios.get_for_update_information(portfolio_owner, portfolio.id)
|
||||
assert portfolio == owner_ws
|
||||
|
||||
@ -307,8 +276,8 @@ def test_get_for_update_information():
|
||||
|
||||
def test_can_create_portfolios_with_matching_names():
|
||||
portfolio_name = "Great Portfolio"
|
||||
Portfolios.create_from_request(RequestFactory.create(), name=portfolio_name)
|
||||
Portfolios.create_from_request(RequestFactory.create(), name=portfolio_name)
|
||||
PortfolioFactory.create(name=portfolio_name)
|
||||
PortfolioFactory.create(name=portfolio_name)
|
||||
|
||||
|
||||
def test_able_to_revoke_portfolio_access_for_active_member():
|
||||
|
@ -1,27 +1,17 @@
|
||||
from atst.domain.reports import Reports
|
||||
|
||||
from tests.factories import RequestFactory, LegacyTaskOrderFactory, PortfolioFactory
|
||||
|
||||
CLIN_NUMS = ["0001", "0003", "1001", "1003", "2001", "2003"]
|
||||
from tests.factories import PortfolioFactory
|
||||
|
||||
|
||||
def test_portfolio_totals():
|
||||
legacy_task_order = LegacyTaskOrderFactory.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)
|
||||
portfolio = PortfolioFactory.create()
|
||||
report = Reports.portfolio_totals(portfolio)
|
||||
total = 200 * len(CLIN_NUMS)
|
||||
assert report == {"budget": total, "spent": 0}
|
||||
assert report == {"budget": 0, "spent": 0}
|
||||
|
||||
|
||||
# this is sketched in until we do real reporting
|
||||
def test_monthly_totals():
|
||||
request = RequestFactory.create()
|
||||
portfolio = PortfolioFactory.create(request=request)
|
||||
portfolio = PortfolioFactory.create()
|
||||
monthly = Reports.monthly_totals(portfolio)
|
||||
|
||||
assert not monthly["environments"]
|
||||
@ -31,8 +21,7 @@ def test_monthly_totals():
|
||||
|
||||
# this is sketched in until we do real reporting
|
||||
def test_cumulative_budget():
|
||||
request = RequestFactory.create()
|
||||
portfolio = PortfolioFactory.create(request=request)
|
||||
portfolio = PortfolioFactory.create()
|
||||
months = Reports.cumulative_budget(portfolio)
|
||||
|
||||
assert len(months["months"]) >= 12
|
||||
|
@ -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
|
@ -9,13 +9,7 @@ from faker import Faker as _Faker
|
||||
from atst.forms import data
|
||||
from atst.models.attachment import Attachment
|
||||
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.legacy_task_order import LegacyTaskOrder, Source, FundingType
|
||||
from atst.models.task_order import TaskOrder
|
||||
from atst.models.user import User
|
||||
from atst.models.role import Role
|
||||
@ -105,173 +99,11 @@ class UserFactory(Base):
|
||||
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 Meta:
|
||||
model = Portfolio
|
||||
|
||||
request = factory.SubFactory(RequestFactory, with_task_order=True)
|
||||
# name it the same as the request ID by default
|
||||
name = factory.LazyAttribute(lambda w: w.request.id)
|
||||
name = factory.Faker("name")
|
||||
|
||||
@classmethod
|
||||
def _create(cls, model_class, *args, **kwargs):
|
||||
@ -286,7 +118,6 @@ class PortfolioFactory(Base):
|
||||
for p in with_applications
|
||||
]
|
||||
|
||||
portfolio.request.creator = owner
|
||||
PortfolioRoleFactory.create(
|
||||
portfolio=portfolio,
|
||||
role=Roles.get("owner"),
|
||||
|
@ -4,41 +4,7 @@ from wtforms.fields import StringField
|
||||
import pendulum
|
||||
from werkzeug.datastructures import ImmutableMultiDict
|
||||
|
||||
from atst.forms.fields import NewlineListField, 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
|
||||
from atst.forms.fields import FormFieldWrapper
|
||||
|
||||
|
||||
class PersonForm(Form):
|
||||
|
@ -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"]
|
@ -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"]
|
@ -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"}
|
||||
|
@ -1,14 +1,13 @@
|
||||
from atst.domain.environments import Environments
|
||||
from atst.domain.portfolios import Portfolios
|
||||
from atst.domain.applications import Applications
|
||||
from tests.factories import RequestFactory, UserFactory
|
||||
from tests.factories import PortfolioFactory, UserFactory
|
||||
|
||||
|
||||
def test_add_user_to_environment():
|
||||
owner = UserFactory.create()
|
||||
developer = UserFactory.from_atat_role("developer")
|
||||
|
||||
portfolio = Portfolios.create_from_request(RequestFactory.create(creator=owner))
|
||||
portfolio = PortfolioFactory.create(owner=owner)
|
||||
application = Applications.create(
|
||||
owner,
|
||||
portfolio,
|
||||
|
@ -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
|
@ -9,7 +9,6 @@ from atst.models.invitation import Status as InvitationStatus
|
||||
from atst.models.audit_event import AuditEvent
|
||||
from atst.models.portfolio_role import Status as PortfolioRoleStatus
|
||||
from tests.factories import (
|
||||
RequestFactory,
|
||||
UserFactory,
|
||||
InvitationFactory,
|
||||
PortfolioRoleFactory,
|
||||
@ -25,7 +24,7 @@ def test_has_no_ws_role_history(session):
|
||||
owner = 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")
|
||||
create_event = (
|
||||
session.query(AuditEvent)
|
||||
@ -42,7 +41,7 @@ def test_has_ws_role_history(session):
|
||||
owner = 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()
|
||||
# in order to get the history, we don't want the PortfolioRoleFactory
|
||||
# to commit after create()
|
||||
@ -67,7 +66,7 @@ def test_has_ws_status_history(session):
|
||||
owner = 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
|
||||
# to commit after create()
|
||||
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):
|
||||
owner = UserFactory.create()
|
||||
user = UserFactory.create()
|
||||
portfolio = Portfolios.create_from_request(RequestFactory.create(creator=owner))
|
||||
portfolio = PortfolioFactory.create(owner=owner)
|
||||
application = ApplicationFactory.create(portfolio=portfolio)
|
||||
environment = EnvironmentFactory.create(
|
||||
application=application, name="new environment!"
|
||||
@ -110,7 +109,7 @@ def test_has_no_env_role_history(session):
|
||||
def test_has_env_role_history(session):
|
||||
owner = 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)
|
||||
application = ApplicationFactory.create(portfolio=portfolio)
|
||||
environment = EnvironmentFactory.create(
|
||||
@ -137,7 +136,7 @@ def test_event_details():
|
||||
owner = 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")
|
||||
|
||||
assert portfolio_role.event_details["updated_user_name"] == user.displayname
|
||||
@ -154,7 +153,7 @@ def test_has_no_environment_roles():
|
||||
"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)
|
||||
|
||||
assert not portfolio_role.has_environment_roles
|
||||
@ -170,7 +169,7 @@ def test_has_environment_roles():
|
||||
"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)
|
||||
application = Applications.create(
|
||||
owner,
|
||||
@ -195,7 +194,7 @@ def test_role_displayname():
|
||||
"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)
|
||||
|
||||
assert portfolio_role.role_displayname == "Developer"
|
||||
|
@ -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
|
@ -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):
|
||||
portfolio = PortfolioFactory.create()
|
||||
user_session(portfolio.owner)
|
||||
response = client.get("/portfolios/{}/applications".format(portfolio.id))
|
||||
assert (
|
||||
'href="/portfolios/{}/applications/new"'.format(portfolio.id).encode()
|
||||
"href='/portfolios/{}/applications/new'".format(portfolio.id).encode()
|
||||
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):
|
||||
user = UserFactory.create()
|
||||
portfolio = PortfolioFactory.create()
|
||||
@ -109,11 +107,29 @@ def test_user_without_permission_has_no_add_application_link(client, user_sessio
|
||||
user_session(user)
|
||||
response = client.get("/portfolios/{}/applications".format(portfolio.id))
|
||||
assert (
|
||||
'href="/portfolios/{}/applications/new"'.format(portfolio.id).encode()
|
||||
"href='/portfolios/{}/applications/new'".format(portfolio.id).encode()
|
||||
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):
|
||||
portfolio = PortfolioFactory.create()
|
||||
application = Applications.create(
|
||||
|
@ -37,7 +37,6 @@ def create_portfolio_and_invite_user(
|
||||
return portfolio
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="Temporarily no add member link")
|
||||
def test_user_with_permission_has_add_member_link(client, user_session):
|
||||
portfolio = PortfolioFactory.create()
|
||||
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):
|
||||
user = UserFactory.create()
|
||||
portfolio = PortfolioFactory.create()
|
||||
|
@ -1,6 +1,12 @@
|
||||
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
|
||||
|
||||
|
||||
@ -40,3 +46,46 @@ def test_portfolio_index_without_existing_portfolios(client, user_session):
|
||||
assert (
|
||||
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()
|
||||
|
@ -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)
|
@ -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
|
@ -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
|
@ -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"]
|
||||
)
|
@ -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
|
@ -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
|
@ -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
|
@ -1,28 +1,27 @@
|
||||
import pytest
|
||||
|
||||
from tests.factories import UserFactory, PortfolioFactory, RequestFactory
|
||||
from tests.factories import UserFactory, PortfolioFactory
|
||||
from atst.domain.portfolios import Portfolios
|
||||
from atst.models.portfolio_role import Status as PortfolioRoleStatus
|
||||
|
||||
|
||||
def test_request_owner_with_one_portfolio_redirected_to_reports(client, user_session):
|
||||
request = RequestFactory.create()
|
||||
portfolio = Portfolios.create_from_request(request)
|
||||
def test_portfolio_owner_with_one_portfolio_redirected_to_reports(client, user_session):
|
||||
portfolio = PortfolioFactory.create()
|
||||
|
||||
user_session(request.creator)
|
||||
user_session(portfolio.owner)
|
||||
response = client.get("/home", follow_redirects=False)
|
||||
|
||||
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
|
||||
):
|
||||
request_creator = UserFactory.create()
|
||||
Portfolios.create_from_request(RequestFactory.create(creator=request_creator))
|
||||
Portfolios.create_from_request(RequestFactory.create(creator=request_creator))
|
||||
owner = UserFactory.create()
|
||||
PortfolioFactory.create(owner=owner)
|
||||
PortfolioFactory.create(owner=owner)
|
||||
|
||||
user_session(request_creator)
|
||||
user_session(owner)
|
||||
response = client.get("/home", follow_redirects=False)
|
||||
|
||||
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]
|
||||
assert "/portfolios" 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
|
||||
|
@ -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("&", "&")
|
||||
|
||||
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
|
Loading…
x
Reference in New Issue
Block a user