diff --git a/alembic/versions/3777e9e39644_add_indexes.py b/alembic/versions/3777e9e39644_add_indexes.py new file mode 100644 index 00000000..a48bfb4d --- /dev/null +++ b/alembic/versions/3777e9e39644_add_indexes.py @@ -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 ### diff --git a/alembic/versions/978bf56e21b6_remove_pe_number_model.py b/alembic/versions/978bf56e21b6_remove_pe_number_model.py new file mode 100644 index 00000000..17cd2f1b --- /dev/null +++ b/alembic/versions/978bf56e21b6_remove_pe_number_model.py @@ -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 ### diff --git a/alembic/versions/c92cec2f32d4_remove_request_related_models.py b/alembic/versions/c92cec2f32d4_remove_request_related_models.py new file mode 100644 index 00000000..6ca7e507 --- /dev/null +++ b/alembic/versions/c92cec2f32d4_remove_request_related_models.py @@ -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 ### diff --git a/alembic/versions/fb22e47972a3_remove_legacy_task_order_table.py b/alembic/versions/fb22e47972a3_remove_legacy_task_order_table.py new file mode 100644 index 00000000..987cfaba --- /dev/null +++ b/alembic/versions/fb22e47972a3_remove_legacy_task_order_table.py @@ -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 ### diff --git a/atst/app.py b/atst/app.py index 2f27b661..19e09ca2 100644 --- a/atst/app.py +++ b/atst/app.py @@ -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) diff --git a/atst/domain/csp/reports.py b/atst/domain/csp/reports.py index fd498227..704ffa46 100644 --- a/atst/domain/csp/reports.py +++ b/atst/domain/csp/reports.py @@ -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): diff --git a/atst/domain/legacy_task_orders.py b/atst/domain/legacy_task_orders.py deleted file mode 100644 index e7a5c44f..00000000 --- a/atst/domain/legacy_task_orders.py +++ /dev/null @@ -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 diff --git a/atst/domain/pe_numbers.py b/atst/domain/pe_numbers.py deleted file mode 100644 index 756ea026..00000000 --- a/atst/domain/pe_numbers.py +++ /dev/null @@ -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() diff --git a/atst/domain/portfolios/portfolios.py b/atst/domain/portfolios/portfolios.py index 1df66629..fbb40bad 100644 --- a/atst/domain/portfolios/portfolios.py +++ b/atst/domain/portfolios/portfolios.py @@ -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) diff --git a/atst/domain/portfolios/query.py b/atst/domain/portfolios/query.py index 486009eb..df82efbb 100644 --- a/atst/domain/portfolios/query.py +++ b/atst/domain/portfolios/query.py @@ -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 ( diff --git a/atst/domain/requests/__init__.py b/atst/domain/requests/__init__.py deleted file mode 100644 index 88072d60..00000000 --- a/atst/domain/requests/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .requests import Requests, create_revision_from_request_body diff --git a/atst/domain/requests/authorization.py b/atst/domain/requests/authorization.py deleted file mode 100644 index 173c4bfc..00000000 --- a/atst/domain/requests/authorization.py +++ /dev/null @@ -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", - ) diff --git a/atst/domain/requests/financial_verification.py b/atst/domain/requests/financial_verification.py deleted file mode 100644 index 042080b4..00000000 --- a/atst/domain/requests/financial_verification.py +++ /dev/null @@ -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",) diff --git a/atst/domain/requests/query.py b/atst/domain/requests/query.py deleted file mode 100644 index d080f941..00000000 --- a/atst/domain/requests/query.py +++ /dev/null @@ -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 diff --git a/atst/domain/requests/requests.py b/atst/domain/requests/requests.py deleted file mode 100644 index 6b30c366..00000000 --- a/atst/domain/requests/requests.py +++ /dev/null @@ -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()] diff --git a/atst/domain/requests/status_event_handler.py b/atst/domain/requests/status_event_handler.py deleted file mode 100644 index 3c4c9f87..00000000 --- a/atst/domain/requests/status_event_handler.py +++ /dev/null @@ -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 - ) diff --git a/atst/filters.py b/atst/filters.py index 011d930b..02d51c5c 100644 --- a/atst/filters.py +++ b/atst/filters.py @@ -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"]("
".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 diff --git a/atst/forms/fields.py b/atst/forms/fields.py index 584aeca8..84f4cd1e 100644 --- a/atst/forms/fields.py +++ b/atst/forms/fields.py @@ -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: diff --git a/atst/forms/financial.py b/atst/forms/financial.py deleted file mode 100644 index 0d614997..00000000 --- a/atst/forms/financial.py +++ /dev/null @@ -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 diff --git a/atst/forms/new_request.py b/atst/forms/new_request.py deleted file mode 100644 index e160070c..00000000 --- a/atst/forms/new_request.py +++ /dev/null @@ -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")) diff --git a/atst/models/__init__.py b/atst/models/__init__.py index 32f7144a..ee3a7958 100644 --- a/atst/models/__init__.py +++ b/atst/models/__init__.py @@ -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 diff --git a/atst/models/audit_event.py b/atst/models/audit_event.py index c705ab1d..0e46b086 100644 --- a/atst/models/audit_event.py +++ b/atst/models/audit_event.py @@ -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()) diff --git a/atst/models/legacy_task_order.py b/atst/models/legacy_task_order.py deleted file mode 100644 index 72e466b5..00000000 --- a/atst/models/legacy_task_order.py +++ /dev/null @@ -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 "".format( - self.number, - self.verified, - self.budget, - self.expiration_date, - self.pdf, - self.id, - ) diff --git a/atst/models/mixins/auditable.py b/atst/models/mixins/auditable.py index 5424cc03..86ada487 100644 --- a/atst/models/mixins/auditable.py +++ b/atst/models/mixins/auditable.py @@ -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 diff --git a/atst/models/pe_number.py b/atst/models/pe_number.py deleted file mode 100644 index 626d6842..00000000 --- a/atst/models/pe_number.py +++ /dev/null @@ -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 "".format( - self.number, self.description - ) diff --git a/atst/models/portfolio.py b/atst/models/portfolio.py index da7e8301..83043bcd 100644 --- a/atst/models/portfolio.py +++ b/atst/models/portfolio.py @@ -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 "".format( - self.name, self.request_id, self.user_count, self.id + return "".format( + self.name, self.user_count, self.id ) diff --git a/atst/models/request.py b/atst/models/request.py deleted file mode 100644 index 68243468..00000000 --- a/atst/models/request.py +++ /dev/null @@ -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 "".format( - self.status_displayname, - self.displayname, - self.creator.full_name, - self.is_approved, - self.time_created, - self.id, - ) diff --git a/atst/models/request_internal_comment.py b/atst/models/request_internal_comment.py deleted file mode 100644 index 14d9a298..00000000 --- a/atst/models/request_internal_comment.py +++ /dev/null @@ -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 "".format( - self.text, self.user.full_name, self.request_id, self.id - ) diff --git a/atst/models/request_review.py b/atst/models/request_review.py deleted file mode 100644 index f45fdfb7..00000000 --- a/atst/models/request_review.py +++ /dev/null @@ -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 "".format( - self.status.log_name, self.comment, self.full_name_reviewer, self.id - ) diff --git a/atst/models/request_revision.py b/atst/models/request_revision.py deleted file mode 100644 index 29c4e9b3..00000000 --- a/atst/models/request_revision.py +++ /dev/null @@ -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 "".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", - ] - ) diff --git a/atst/models/request_status_event.py b/atst/models/request_status_event.py deleted file mode 100644 index e3367951..00000000 --- a/atst/models/request_status_event.py +++ /dev/null @@ -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 "".format( - self.log_name, self.request_id, self.id - ) diff --git a/atst/routes/__init__.py b/atst/routes/__init__.py index 4f0e91cb..4183d8ac 100644 --- a/atst/routes/__init__.py +++ b/atst/routes/__init__.py @@ -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: diff --git a/atst/routes/portfolios/index.py b/atst/routes/portfolios/index.py index 6be58772..274c26b4 100644 --- a/atst/routes/portfolios/index.py +++ b/atst/routes/portfolios/index.py @@ -98,19 +98,3 @@ def portfolio_reports(portfolio_id): expiration_date=expiration_date, remaining_days=remaining_days, ) - - -@portfolios_bp.route("/portfolios//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, - ) diff --git a/atst/routes/requests/__init__.py b/atst/routes/requests/__init__.py deleted file mode 100644 index d4214f56..00000000 --- a/atst/routes/requests/__init__.py +++ /dev/null @@ -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} diff --git a/atst/routes/requests/approval.py b/atst/routes/requests/approval.py deleted file mode 100644 index 9ec31bc9..00000000 --- a/atst/routes/requests/approval.py +++ /dev/null @@ -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/", 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/", 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/", 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/", 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) diff --git a/atst/routes/requests/financial_verification.py b/atst/routes/requests/financial_verification.py deleted file mode 100644 index 970d85a4..00000000 --- a/atst/routes/requests/financial_verification.py +++ /dev/null @@ -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//draft", methods=["GET"]) -@requests_bp.route("/requests/verify/", 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/", 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//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, - ) - ) diff --git a/atst/routes/requests/index.py b/atst/routes/requests/index.py deleted file mode 100644 index eb0c7204..00000000 --- a/atst/routes/requests/index.py +++ /dev/null @@ -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) diff --git a/atst/routes/requests/jedi_request_flow.py b/atst/routes/requests/jedi_request_flow.py deleted file mode 100644 index 269ff632..00000000 --- a/atst/routes/requests/jedi_request_flow.py +++ /dev/null @@ -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} diff --git a/atst/routes/requests/requests_form.py b/atst/routes/requests/requests_form.py deleted file mode 100644 index 8d8f17a4..00000000 --- a/atst/routes/requests/requests_form.py +++ /dev/null @@ -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/", 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/", methods=["GET"], defaults={"request_id": None} -) -@requests_bp.route("/requests/new//", 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/", methods=["POST"], defaults={"request_id": None} -) -@requests_bp.route("/requests/new//", 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/", 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/", 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/") -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) diff --git a/atst/utils/__init__.py b/atst/utils/__init__.py index 5852ebf4..01988e10 100644 --- a/atst/utils/__init__.py +++ b/atst/utils/__init__.py @@ -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} diff --git a/atst/utils/flash.py b/atst/utils/flash.py index 3614dffb..89261795 100644 --- a/atst/utils/flash.py +++ b/atst/utils/flash.py @@ -88,29 +88,6 @@ MESSAGES = { "message_template": "", "category": "success", }, - "request_incomplete": { - "title_template": "Please complete all sections", - "message_template": """ -

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.

- """, - "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": """ -

CCPO has requested changes to your submission with the following notes: -
- {{ comment }} -
- Please contact info@jedi.cloud or 123-123-4567 for further discussion.

- """, - "category": "warning", - }, "environment_access_changed": { "title_template": "User access successfully changed.", "message_template": "", diff --git a/js/components/__tests__/requests_list.test.js b/js/components/__tests__/requests_list.test.js deleted file mode 100644 index e9fa1e9d..00000000 --- a/js/components/__tests__/requests_list.test.js +++ /dev/null @@ -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, - ]) - }) - }) -}) diff --git a/js/components/forms/ccpo_approval.js b/js/components/forms/ccpo_approval.js deleted file mode 100644 index 20b69908..00000000 --- a/js/components/forms/ccpo_approval.js +++ /dev/null @@ -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 - } - }, - }, -} diff --git a/js/components/forms/details_of_use.js b/js/components/forms/details_of_use.js deleted file mode 100644 index 74d461bd..00000000 --- a/js/components/forms/details_of_use.js +++ /dev/null @@ -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 - }, - }, -} diff --git a/js/components/forms/financial.js b/js/components/forms/financial.js deleted file mode 100644 index 84402a85..00000000 --- a/js/components/forms/financial.js +++ /dev/null @@ -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 - }, - }, -} diff --git a/js/components/requests_list.js b/js/components/requests_list.js deleted file mode 100644 index 6112fd75..00000000 --- a/js/components/requests_list.js +++ /dev/null @@ -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: '
', -} diff --git a/js/index.js b/js/index.js index fbdd5814..dbc72654 100644 --- a/js/index.js +++ b/js/index.js @@ -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, diff --git a/script/include b/script/include index 78c51d8d..eb9ea572 160000 --- a/script/include +++ b/script/include @@ -1 +1 @@ -Subproject commit 78c51d8dd29b47fd42570896daaded5e2181e923 +Subproject commit eb9ea572e4c5157c8e7ba6105ac4efd1df39392e diff --git a/script/ingest_pe_numbers.py b/script/ingest_pe_numbers.py deleted file mode 100644 index 5dd55580..00000000 --- a/script/ingest_pe_numbers.py +++ /dev/null @@ -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) diff --git a/templates/emails/request_status_change.txt b/templates/emails/request_status_change.txt deleted file mode 100644 index 5b48c739..00000000 --- a/templates/emails/request_status_change.txt +++ /dev/null @@ -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) }} diff --git a/templates/portfolios/activity/index.html b/templates/portfolios/activity/index.html deleted file mode 100644 index c28b766c..00000000 --- a/templates/portfolios/activity/index.html +++ /dev/null @@ -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 %} -
- {% include "fragments/audit_events_log.html" %} - {{ Pagination(audit_events, 'portfolios.portfolio_activity', portfolio_id=portfolio_id) }} -
-{% endblock %} diff --git a/templates/portfolios/index.html b/templates/portfolios/index.html index de713653..610658b7 100644 --- a/templates/portfolios/index.html +++ b/templates/portfolios/index.html @@ -6,7 +6,6 @@ Portfolio Name - Task Order Users @@ -16,9 +15,6 @@ {{ portfolio.name }}
- - #{{ portfolio.legacy_task_order.number }} - {{ portfolio.user_count }}Users diff --git a/templates/requests/_new.html b/templates/requests/_new.html deleted file mode 100644 index 59ceeaa8..00000000 --- a/templates/requests/_new.html +++ /dev/null @@ -1,49 +0,0 @@ -{% extends "base.html" %} - -{% block content %} - -
- - {% include 'requests/menu.html' %} - - {% include "fragments/flash.html" %} - - {% block form_action %} - {% if request_id %} -
- {% else %} - - {% endif %} - {% endblock %} - -
- -
-

{% block heading %}{% endblock %}

-
{{ "requests._new.new_request" | translate }}
-
- -
- - {{ f.csrf_token }} - {% block form %} - form goes here - {% endblock %} - -
- -
- - {% block next %} - -
- -
- - {% endblock %} - -
- -
- -{% endblock %} diff --git a/templates/requests/_review.html b/templates/requests/_review.html deleted file mode 100644 index 9ced019f..00000000 --- a/templates/requests/_review.html +++ /dev/null @@ -1,215 +0,0 @@ -{% macro RequiredLabel() -%} - Response Required -{%- endmacro %} - -{% macro DefinitionReviewField(title, section, item_name, filter=None, filter_args=[]) -%} -
-
{{ title | safe }}
-
- {% set value = data.get(section, {}).get(item_name) %} - {% if value is not none %} - {{ value | findFilter(filter, filter_args) }} - {% else %} - {{ RequiredLabel() }} - {% endif %} -
-
-{% 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 %} - -

- Details of Use - {% if editable %} - - {{ Icon('edit') }} - Edit this section - - {% endif %} -

- -
- - {{ 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") }} -
- -
-

- Information About You - {% if editable %} - - {{ Icon('edit') }} - Edit this section - - {% endif %} -

- -
- {{ DefinitionReviewField("First Name", "information_about_you", "fname_request") }} - - {{ DefinitionReviewField("Last Name", "information_about_you", "lname_request") }} - - {{ DefinitionReviewField("Email Address", "information_about_you", "email_request") }} - -
-
Phone Number
-
- {% 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 %} -
-
- - {{ 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") }} -
- -
-

- Portfolio Owner - {% if editable %} - - {{ Icon('edit') }} - Edit this section - - {% endif %} -

- -
- {{ 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") }} -
- -{% if jedi_request.has_financial_data %} -
-

- Financial Verification -

- -
- {% if jedi_request.legacy_task_order.pdf %} - - Download the Task Order PDF - - {% else %} -

No Task Order PDF attached.

- {% endif %} -
- -
- {{ 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("
CLIN 0001
-
Unclassified IaaS and PaaS Amount
", "legacy_task_order", "clin_0001", filter="dollars") }} - - {{ DefinitionReviewField("
CLIN 0003
-
Unclassified Cloud Support Package
", "legacy_task_order", "clin_0003", filter="dollars") }} - - {{ DefinitionReviewField("
CLIN 1001
-
Unclassified IaaS and PaaS Amount
OPTION PERIOD 1
", "legacy_task_order", "clin_1001", filter="dollars") }} - - {{ DefinitionReviewField("
CLIN 1003
-
Unclassified Cloud Support Package
OPTION PERIOD 1
", "legacy_task_order", "clin_1003", filter="dollars") }} - - {{ DefinitionReviewField("
CLIN 2001
-
Unclassified IaaS and PaaS Amount
OPTION PERIOD 2
", "legacy_task_order", "clin_2001", filter="dollars") }} - - {{ DefinitionReviewField("
CLIN 2003
-
Unclassified Cloud Support Package
OPTION PERIOD 2
", "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") }} -
-{% endif %} diff --git a/templates/requests/approval.html b/templates/requests/approval.html deleted file mode 100644 index bc454e37..00000000 --- a/templates/requests/approval.html +++ /dev/null @@ -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 %} - -
- -{% include "fragments/flash.html" %} - -
-
-

- {{ "requests.approval.request_title" | translate({ "displayname": jedi_request.displayname }) }} -

- {{ current_status }} -
- -
- - {% with data=data, request_id=jedi_request.id %} - {% include "requests/_review.html" %} - {% endwith %} - -
- -
- -
-
-
-
-

{{ "requests.approval.ccpo_internal_comments" | translate }}

-
- -
- {% if comments %} -
    - {% for comment in comments %} -
  1. -
    -
    -

    {{ comment.user.full_name }}

    -

    {{ comment.text }}

    -
    - {% set timestamp=comment.time_created | formattedDate("%Y-%m-%d %H:%M:%S %Z") %} -
    - -
    -
    -
  2. - {% endfor %} -
- {% else %} -
-

- {{ "requests.approval.no_ccpo_comments" | translate }} -

-
- {% endif %} - -
- -
-

- {{ "requests.approval.add_comment" | translate }} -

-
- -
- {{ internal_comment_form.csrf_token }} - {{ TextInput(internal_comment_form.text, paragraph=True, noMaxWidth=True) }} -
-
-
- -
-
-
- - -
-
- {{ review_form.csrf_token }} - - {% set initialState = 'approving' if review_form.errors else '' %} - -
-
- -
-

- {{ "requests.approval.ccpo_review_activity" | translate }} -

-
- -
- {% if reviews %} -
    - {% for review in reviews %} -
  1. -
    -
    -

    {{ review.status.log_name }} by {{ review.full_name_reviewer }}

    - {% if review.comment %} -

    {{ review.comment }}

    - {% endif %} - -
    - {% if review.lname_mao %} -
    -

    - {{ "requests.approval.mission_owner_approval_on_behalf_of" | translate }} -

    - {{ review.full_name_mao }} - {{ review.email_mao }} - - {{ review.phone_mao }} - {% if review.phone_ext_mao %} - ext. {{ review.phone_ext_mao }} - {% endif %} - -
    - {% endif %} - - {% if review.lname_ccpo %} -
    -

    - {{ "requests.approval.ccpo_approval_on_behalf_of" | translate }} -

    - {{ review.full_name_ccpo }} -
    - {% endif %} -
    -
    - {% set timestamp=review.status.time_created | formattedDate("%Y-%m-%d %H:%M:%S %Z") %} -
    - -
    -
    -
  2. - {% endfor %} -
- {% else %} -
-

- {{ "requests.approval.no_ccpo_approval_request_changes" | translate }} -

-
- {% endif %} -
- - {% if jedi_request.is_pending_ccpo_action %} -
-

- {{ "requests.approval.review_request" | translate }} -

-
- -
- -
-
- - - - - -
-
- -
-

Message to Requestor (optional)

-
- {{ 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 - ) }} -
- -
- {{ TextInput( - review_form.comment, - label=("requests.approval.revision_instructions_or_notes_label" | translate), - paragraph=True, - noMaxWidth=True - ) }} -
-
- -
- -

- {{ "requests.approval.authorizing_officials_title" | translate }} - (optional) -

- -

- {{ "requests.approval.authorizing_officials_paragraph" | translate }} -

- -
- -

- {{ "requests.approval.mission_authorizing_official_title" | translate }} -

- -
-
- {{ TextInput(review_form.fname_mao, placeholder="First name of mission authorizing official") }} -
- -
- {{ TextInput(review_form.lname_mao, placeholder="Last name of mission authorizing official") }} -
-
- - {{ TextInput(review_form.email_mao, placeholder="name@mail.mil", validation='email') }} - {{ PhoneInput(review_form.phone_mao, review_form.phone_ext_mao) }} - -
- -

- {{ "requests.approval.ccpo_authorizing_official_title" | translate }} -

- -
-
- {{ TextInput(review_form.fname_ccpo, placeholder="First name of CCPO authorizing official") }} -
- -
- {{ TextInput(review_form.lname_ccpo, placeholder="Last name of CCPO authorizing official") }} -
-
-
-
- {% endif %} - -
- - {% if jedi_request.is_pending_ccpo_action %} -
- - - - {{ Icon('x') }} - Cancel - -
- {% endif %} - -
-
-
-
- -
- -{% endblock %} diff --git a/templates/requests/details.html b/templates/requests/details.html deleted file mode 100644 index 98e65dd0..00000000 --- a/templates/requests/details.html +++ /dev/null @@ -1,30 +0,0 @@ -{% extends "base.html" %} - -{% from "components/alert.html" import Alert %} - -{% block content %} -
- - {% 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 %} - -
-
-

Request Details

-

Request: {{ jedi_request.displayname }}

{{ jedi_request.status_displayname }}
-
- -
- - {% include "requests/_review.html" %} - -
-
-
-{% endblock %} diff --git a/templates/requests/financial_verification.html b/templates/requests/financial_verification.html deleted file mode 100644 index b66ab719..00000000 --- a/templates/requests/financial_verification.html +++ /dev/null @@ -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 %} - - -
- {% 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 }} -
- - {{ "requests.financial_verification.enter_task_order_manually_link_text" | translate }} - - {% endcall %} - {% endif %} - -
- - {{ 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="

Please see below.

", - level='error' - ) }} - {% endif %} - -
- -
-

{{ "requests.financial_verification.financial_verification_title" | translate }}

-
-

- {{ "requests.financial_verification.request_title" | translate({ "displayname" : jedi_request.displayname }) }} -

-
-
- -
- -

- {{ "requests.financial_verification.permissions_paragraph" | translate }} -

- - {% if extended %} -
- {{ OptionsInput(f.legacy_task_order.funding_type) }} - - - - {{ - 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' - ) }} - - - -
- {% 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") }} - -
- -

- {{ "requests.financial_verification.contracting_officer_information_title" | translate }} -

- -
-
{{ TextInput(f.request.fname_co, validation="requiredField") }}
-
{{ TextInput(f.request.lname_co, validation="requiredField") }}
-
- -
-
{{ TextInput(f.request.email_co,validation='email', placeholder='e.g. jane@mail.mil') }}
-
{{ TextInput(f.request.office_co, validation="requiredField", placeholder="e.g.: WHS") }}
-
- -
- -

- {{ "requests.financial_verification.contracting_officer_representative_information_title" | translate }} -

-
-
{{ TextInput(f.request.fname_cor, validation="requiredField") }}
-
{{ TextInput(f.request.lname_cor, validation="requiredField") }}
-
- -
-
{{ TextInput(f.request.email_cor,validation='email', placeholder='e.g. jane@mail.mil') }}
-
{{ TextInput(f.request.office_cor, validation="requiredField", placeholder="e.g.: WHS") }}
-
- - - {% endautoescape %} - -
-
- - {% endblock form %} - {% block next %} -
- - - {% if jedi_request.last_finver_draft_saved_at %} - Draft saved at - {% endif %} -
- {% endblock %} -
- -
-
- -{% endblock %} diff --git a/templates/requests/financial_verification_submitted.html b/templates/requests/financial_verification_submitted.html deleted file mode 100644 index 01d6ef89..00000000 --- a/templates/requests/financial_verification_submitted.html +++ /dev/null @@ -1,18 +0,0 @@ -{% extends "base.html" %} - -{% block content %} - -
- -
- -
- -
-

Submitted

-
-
-
-
- -{% endblock %} diff --git a/templates/requests/index.html b/templates/requests/index.html deleted file mode 100644 index b0ac66f2..00000000 --- a/templates/requests/index.html +++ /dev/null @@ -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) %} -

{{ "requests.index.request_submitted_title" | translate }}

- - {% include 'fragments/pending_financial_verification.html' %} - -
- -
- {% endcall %} - - {% call Modal(name='pendingCCPOApproval', dismissable=True) %} -

{{ "requests.index.financial_verification_submitted_title" | translate }}

- - {% include 'fragments/pending_ccpo_approval_modal.html' %} - -
- -
- {% endcall %} - - {% call Modal(name='pendingCCPOAcceptance', dismissable=True) %} -

{{ "requests.index.request_submitted_title" | translate }}

- - {% include 'fragments/pending_ccpo_acceptance_alert.html' %} - -
- -
- {% endcall %} - - -
- -{% 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 %} -
-
-
{{ kpi_inprogress }}
-
{{ "requests.index.requests_in_progress" | translate }}
-
-
-
{{ kpi_pending }}
-
{{ "requests.index.pending_ccpo_action" | translate }}
-
-
-
{{ kpi_completed }}
-
{{ "requests.index.approved_requests" | translate }}
-
-
- {% endif %} - -
- - {% if extended_view %} - - {% endif %} - -
- - - - - - - - - - - {% if extended_view %} - - - {% endif %} - - - {% if extended_view %} - - {% endif %} - - -
- !{ column.displayName } - - {{ Icon("caret_down") }} - - - {{ Icon("caret_up") }} - -
- !{ r.name } - - {{ "requests.index.action_required" | translate }} - - - - - — - !{ r.full_name }!{ dollars(r.annual_usage) } - - !{ r.status } - - - !{ r.status } - - !{ r.dod_component }
-
- {{ EmptyState( - ("requests.index.no_requests_found" | translate), - action_label=None, - action_href=None, - sub_message=("requests.index.try_different_search" | translate), - icon=None - ) }} -
-
-
-{% endif %} - -
-
- -{% endblock %} diff --git a/templates/requests/menu.html b/templates/requests/menu.html deleted file mode 100644 index 40de55d3..00000000 --- a/templates/requests/menu.html +++ /dev/null @@ -1,21 +0,0 @@ -
-
    - {% 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 %} - -
  • - - {{ s['title'] }} - -
  • - {% endfor %} -
-
diff --git a/templates/requests/review_menu.html b/templates/requests/review_menu.html deleted file mode 100644 index 51ae0fdd..00000000 --- a/templates/requests/review_menu.html +++ /dev/null @@ -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) %} -
- -
diff --git a/templates/requests/screen-1.html b/templates/requests/screen-1.html deleted file mode 100644 index 0d8b52ef..00000000 --- a/templates/requests/screen-1.html +++ /dev/null @@ -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" %} - - -
- - {{ "requests.screen-1.form_instructions" | translate }} - -

{{ "requests.screen-1.general_title_text"| translate }}

- {{ OptionsInput(f.dod_component) }} - {{ - TextInput( - f.jedi_usage, - paragraph=True, - placeholder=("requests.screen-1.jedi_usage_placeholder" | translate) - ) - }} - -

{{ "requests.screen-1.cloud_readiness_title_text" | translate }}

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

{{ "requests.screen-1.financial_usage_title" | translate }}

- {{ - TextInput( - f.estimated_monthly_spend, - tooltip=("requests.screen-1.estimated_monthly_spend_tooltip" | translate), - validation="dollars", - placeholder="$0" - ) - }} - -
-
-
- {{ "requests.screen-1.approximate_annual_spend_paragraph" | translate }} -
-
-
- - - - - - {{ - 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') }} - -
-
- -{% endblock %} diff --git a/templates/requests/screen-2.html b/templates/requests/screen-2.html deleted file mode 100644 index 03984dd4..00000000 --- a/templates/requests/screen-2.html +++ /dev/null @@ -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" %} - -

Please tell us more about you.

- -
-
{{ TextInput(f.fname_request) }}
-
{{ TextInput(f.lname_request) }}
-
- -{{ 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') }} - -

We want to collect the following information from you for security auditing and determining priviledged user access.

- -{{ 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?
Information Assurance (IA) training is an important step in cyber awareness.",placeholder="MM / DD / YYYY", validation="date") }} -{% endblock %} diff --git a/templates/requests/screen-3.html b/templates/requests/screen-3.html deleted file mode 100644 index 2ac4e223..00000000 --- a/templates/requests/screen-3.html +++ /dev/null @@ -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" %} - - -
- -

The Portfolio Owner is the primary point of contact and technical administrator of the JEDI Cloud Portfolio and will have the - following responsibilities:

-
    -
  • Organize your cloud-hosted systems into applications and environments
  • -
  • Add users to this portfolio and manage members
  • -
  • Manage access to the JEDI Cloud service provider’s portal
  • -
-

- -

This person must be a DoD employee (not a contractor).

-

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.

- - {{ CheckboxInput(f.am_poc) }} - - - -
-
-{% endblock %} diff --git a/templates/requests/screen-4.html b/templates/requests/screen-4.html deleted file mode 100644 index 4c4443c4..00000000 --- a/templates/requests/screen-4.html +++ /dev/null @@ -1,40 +0,0 @@ -{% macro RequiredLabel() -%} - Response Required -{%- 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 %} -
-{% endblock %} - - {% block form %} - -

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.

- - {% include "fragments/flash.html" %} - - {% with editable=True %} - {% include "requests/_review.html" %} - {% endwith %} - - -{% endblock %} - -{% block next %} - -
- -
- -
- -{% endblock %} diff --git a/templates/requests/sidebar.html b/templates/requests/sidebar.html deleted file mode 100644 index f8516917..00000000 --- a/templates/requests/sidebar.html +++ /dev/null @@ -1,33 +0,0 @@ -
- - - -
diff --git a/tests/domain/test_applications.py b/tests/domain/test_applications.py index 9bc792d9..5ac13cde 100644 --- a/tests/domain/test_applications.py +++ b/tests/domain/test_applications.py @@ -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"] ) diff --git a/tests/domain/test_legacy_task_orders.py b/tests/domain/test_legacy_task_orders.py deleted file mode 100644 index 5defb88e..00000000 --- a/tests/domain/test_legacy_task_orders.py +++ /dev/null @@ -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") diff --git a/tests/domain/test_pe_numbers.py b/tests/domain/test_pe_numbers.py deleted file mode 100644 index 945a6510..00000000 --- a/tests/domain/test_pe_numbers.py +++ /dev/null @@ -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]) diff --git a/tests/domain/test_portfolios.py b/tests/domain/test_portfolios.py index 6c1da209..9972a682 100644 --- a/tests/domain/test_portfolios.py +++ b/tests/domain/test_portfolios.py @@ -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(): diff --git a/tests/domain/test_reports.py b/tests/domain/test_reports.py index 2307abcb..d0e3a24e 100644 --- a/tests/domain/test_reports.py +++ b/tests/domain/test_reports.py @@ -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 diff --git a/tests/domain/test_requests.py b/tests/domain/test_requests.py deleted file mode 100644 index a229ea50..00000000 --- a/tests/domain/test_requests.py +++ /dev/null @@ -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 diff --git a/tests/factories.py b/tests/factories.py index fd081e34..1226d79a 100644 --- a/tests/factories.py +++ b/tests/factories.py @@ -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"), diff --git a/tests/forms/test_fields.py b/tests/forms/test_fields.py index 8e6f4002..bdae6c85 100644 --- a/tests/forms/test_fields.py +++ b/tests/forms/test_fields.py @@ -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): diff --git a/tests/forms/test_financial.py b/tests/forms/test_financial.py deleted file mode 100644 index 39a02a87..00000000 --- a/tests/forms/test_financial.py +++ /dev/null @@ -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"] diff --git a/tests/forms/test_new_request.py b/tests/forms/test_new_request.py deleted file mode 100644 index 91cbf0dd..00000000 --- a/tests/forms/test_new_request.py +++ /dev/null @@ -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"] diff --git a/tests/mocks.py b/tests/mocks.py index f5e9c2d3..8536eec8 100644 --- a/tests/mocks.py +++ b/tests/mocks.py @@ -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"} diff --git a/tests/models/test_environments.py b/tests/models/test_environments.py index cf48f2b1..1e415efa 100644 --- a/tests/models/test_environments.py +++ b/tests/models/test_environments.py @@ -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, diff --git a/tests/models/test_legacy_task_order.py b/tests/models/test_legacy_task_order.py deleted file mode 100644 index 80e02b05..00000000 --- a/tests/models/test_legacy_task_order.py +++ /dev/null @@ -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 diff --git a/tests/models/test_portfolio_role.py b/tests/models/test_portfolio_role.py index 33d8d119..06db96e3 100644 --- a/tests/models/test_portfolio_role.py +++ b/tests/models/test_portfolio_role.py @@ -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" diff --git a/tests/models/test_requests.py b/tests/models/test_requests.py deleted file mode 100644 index f2a82528..00000000 --- a/tests/models/test_requests.py +++ /dev/null @@ -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 diff --git a/tests/routes/portfolios/test_applications.py b/tests/routes/portfolios/test_applications.py index 5fe52ddf..573773ca 100644 --- a/tests/routes/portfolios/test_applications.py +++ b/tests/routes/portfolios/test_applications.py @@ -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( diff --git a/tests/routes/portfolios/test_members.py b/tests/routes/portfolios/test_members.py index 58078f35..0deb6a06 100644 --- a/tests/routes/portfolios/test_members.py +++ b/tests/routes/portfolios/test_members.py @@ -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() diff --git a/tests/routes/portfolios/test_portfolios_index.py b/tests/routes/portfolios/test_portfolios_index.py index 5899f307..49787575 100644 --- a/tests/routes/portfolios/test_portfolios_index.py +++ b/tests/routes/portfolios/test_portfolios_index.py @@ -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() diff --git a/tests/routes/requests/requests_form/test_details.py b/tests/routes/requests/requests_form/test_details.py deleted file mode 100644 index ddeade54..00000000 --- a/tests/routes/requests/requests_form/test_details.py +++ /dev/null @@ -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) diff --git a/tests/routes/requests/requests_form/test_edit.py b/tests/routes/requests/requests_form/test_edit.py deleted file mode 100644 index 37e7b243..00000000 --- a/tests/routes/requests/requests_form/test_edit.py +++ /dev/null @@ -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 diff --git a/tests/routes/requests/requests_form/test_new.py b/tests/routes/requests/requests_form/test_new.py deleted file mode 100644 index bc3c1705..00000000 --- a/tests/routes/requests/requests_form/test_new.py +++ /dev/null @@ -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 diff --git a/tests/routes/requests/requests_form/test_submit.py b/tests/routes/requests/requests_form/test_submit.py deleted file mode 100644 index c824015d..00000000 --- a/tests/routes/requests/requests_form/test_submit.py +++ /dev/null @@ -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"] - ) diff --git a/tests/routes/requests/test_approval.py b/tests/routes/requests/test_approval.py deleted file mode 100644 index 9d507789..00000000 --- a/tests/routes/requests/test_approval.py +++ /dev/null @@ -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 diff --git a/tests/routes/requests/test_financial_verification.py b/tests/routes/requests/test_financial_verification.py deleted file mode 100644 index 734d0864..00000000 --- a/tests/routes/requests/test_financial_verification.py +++ /dev/null @@ -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 diff --git a/tests/routes/requests/test_requests_index.py b/tests/routes/requests/test_requests_index.py deleted file mode 100644 index 6243bd00..00000000 --- a/tests/routes/requests/test_requests_index.py +++ /dev/null @@ -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 diff --git a/tests/routes/test_home.py b/tests/routes/test_home.py index b2dd8d95..a2eeba20 100644 --- a/tests/routes/test_home.py +++ b/tests/routes/test_home.py @@ -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 diff --git a/tests/test_integration.py b/tests/test_integration.py deleted file mode 100644 index 152e960b..00000000 --- a/tests/test_integration.py +++ /dev/null @@ -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