fedspendingtransparency/usaspending-api

View on GitHub
usaspending_api/awards/v2/views/funding.py

Summary

Maintainability
A
0 mins
Test Coverage
A
100%
from collections import OrderedDict
from copy import deepcopy
from itertools import chain

from psycopg2.sql import Identifier, Literal, SQL
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.views import APIView

from usaspending_api.common.cache_decorator import cache_response
from usaspending_api.common.helpers.generic_helper import get_simple_pagination_metadata
from usaspending_api.common.helpers.sql_helpers import build_composable_order_by, execute_sql_to_ordered_dictionary
from usaspending_api.common.validator.award import get_internal_or_generated_award_id_model
from usaspending_api.common.validator.pagination import customize_pagination_with_sort_columns
from usaspending_api.common.validator.tinyshield import validate_post_request
from usaspending_api.references.helpers import generate_agency_slugs_for_agency_list

SORTABLE_COLUMNS = {
    "account_title": "fa.account_title",
    "awarding_agency_name": "awarding_agency_name",
    "disaster_emergency_fund_code": "disaster_emergency_fund_code",
    "federal_account": "federal_account",
    "funding_agency_name": "funding_agency_name",
    "gross_outlay_amount": "gross_outlay_amount",
    "object_class": ["oc.object_class", "oc.object_class_name"],
    "program_activity": ["pa.program_activity_code", "pa.program_activity_name"],
    "reporting_fiscal_date": ["sa.reporting_fiscal_year", "sa.reporting_fiscal_period"],
    "transaction_obligated_amount": "transaction_obligated_amount",
}


DEFAULT_SORT_COLUMN = "federal_account"


FUNDING_SQL = SQL(
    """
    with
    gather_financial_accounts_by_awards as (
        select  a.awarding_agency_id,
                a.funding_agency_id,
                faba.submission_id,
                faba.transaction_obligated_amount,
                faba.gross_outlay_amount_by_award_cpe gross_outlay_amount,
                faba.disaster_emergency_fund_code,
                faba.treasury_account_id,
                faba.object_class_id,
                faba.program_activity_id
        from    vw_awards as a
                inner join financial_accounts_by_awards as faba on faba.award_id = a.id
        where   a.{award_id_column} = {award_id}
    )
    select  gfaba.transaction_obligated_amount,
            gfaba.gross_outlay_amount,
            gfaba.disaster_emergency_fund_code,
            fa.federal_account_code                                         federal_account,
            fa.account_title,
            fta.name                                                        funding_agency_name,
            faa.id                                                          funding_agency_id,
            faa.toptier_agency_id                                           funding_toptier_agency_id,
            ata.name                                                        awarding_agency_name,
            aa.id                                                           awarding_agency_id,
            aa.toptier_agency_id                                            awarding_toptier_agency_id,
            oc.object_class                                                 object_class,
            oc.object_class_name                                            object_class_name,
            pa.program_activity_code                                        program_activity_code,
            pa.program_activity_name                                        program_activity_name,
            sa.reporting_fiscal_year,
            sa.reporting_fiscal_quarter,
            sa.reporting_fiscal_period                                      reporting_fiscal_month,
            sa.quarter_format_flag                                          is_quarterly_submission
    from    gather_financial_accounts_by_awards gfaba
            left outer join treasury_appropriation_account taa on
                taa.treasury_account_identifier = gfaba.treasury_account_id
            left outer join federal_account fa on
                fa.id = taa.federal_account_id
            left outer join agency aa on
                aa.id = gfaba.awarding_agency_id
            left outer join toptier_agency ata on
                ata.toptier_agency_id = aa.toptier_agency_id
            left outer join agency faa on faa.id =
                gfaba.funding_agency_id
            left outer join toptier_agency fta on
                fta.toptier_agency_id = faa.toptier_agency_id
            left outer join object_class oc on
                gfaba.object_class_id = oc.id
            left outer join ref_program_activity pa on
                gfaba.program_activity_id = pa.id
            left outer join submission_attributes sa on
                gfaba.submission_id = sa.submission_id
    {order_by}
    limit {limit} offset {offset};
"""
)


TINYSHIELD_MODELS = customize_pagination_with_sort_columns(list(SORTABLE_COLUMNS.keys()), DEFAULT_SORT_COLUMN)
TINYSHIELD_MODELS.append(get_internal_or_generated_award_id_model())


@validate_post_request(deepcopy(TINYSHIELD_MODELS))
class AwardFundingViewSet(APIView):
    """
    These endpoints are used to power USAspending.gov's Award V2 pages federal account funding table
    """

    endpoint_doc = "usaspending_api/api_contracts/contracts/v2/awards/funding.md"

    @staticmethod
    def _business_logic(request_data: dict) -> list:
        # By this point, our award_id has been validated and cleaned up by
        # TinyShield.  We will either have an internal award id that is an
        # integer or a generated award id that is a string.
        award_id = request_data["award_id"]
        award_id_column = "id" if type(award_id) is int else "generated_unique_award_id"

        sql = FUNDING_SQL.format(
            award_id_column=Identifier(award_id_column),
            award_id=Literal(award_id),
            order_by=build_composable_order_by(SORTABLE_COLUMNS[request_data["sort"]], request_data["order"]),
            limit=Literal(request_data["limit"] + 1),
            offset=Literal((request_data["page"] - 1) * request_data["limit"]),
        )
        return execute_sql_to_ordered_dictionary(sql)

    @cache_response()
    def post(self, request: Request) -> Response:
        results = self._business_logic(request.data)
        page_metadata = get_simple_pagination_metadata(len(results), request.data["limit"], request.data["page"])
        limited_results = results[: request.data["limit"]]

        agency_slugs = generate_agency_slugs_for_agency_list(
            list(
                chain.from_iterable(
                    [(res["awarding_toptier_agency_id"], res["funding_toptier_agency_id"]) for res in limited_results]
                )
            )
        )
        for res in limited_results:
            res["awarding_agency_slug"] = agency_slugs.get(res.get("awarding_toptier_agency_id"))
            res["funding_agency_slug"] = agency_slugs.get(res.get("funding_toptier_agency_id"))

        response = OrderedDict((("results", limited_results), ("page_metadata", page_metadata)))

        return Response(response)