fedspendingtransparency/usaspending-api

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

Summary

Maintainability
A
0 mins
Test Coverage
A
98%
from copy import deepcopy

from django.db.models import F
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.views import APIView

from usaspending_api.awards.models import TransactionNormalized
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.validator import (
    customize_pagination_with_sort_columns,
    get_internal_or_generated_award_id_model,
    TinyShield,
    update_model_in_list,
)


class TransactionViewSet(APIView):
    """
    This route sends a request to the backend to retrieve transactions related to
    a specific parent award.
    """

    endpoint_doc = "usaspending_api/api_contracts/contracts/v2/transactions.md"

    transaction_lookup = {
        # "Display Name": "database_column"
        "id": "transaction_unique_id",
        "type": "type",
        "type_description": "type_description",
        "action_date": "action_date",
        "action_type": "action_type",
        "action_type_description": "action_type_description",
        "modification_number": "modification_number",
        "description": "description",
        "federal_action_obligation": "federal_action_obligation",
        "face_value_loan_guarantee": "face_value_loan_guarantee",
        "original_loan_subsidy_cost": "original_loan_subsidy_cost",
        "is_fpds": "is_fpds",
        "cfda_number": "assistance_data__cfda_number",
    }

    def __init__(self):
        models = customize_pagination_with_sort_columns(
            list(TransactionViewSet.transaction_lookup.keys()), "action_date"
        )
        models.extend(
            [
                get_internal_or_generated_award_id_model(),
                {"key": "idv", "name": "idv", "type": "boolean", "default": True, "optional": True},
            ]
        )

        self._tiny_shield_models = update_model_in_list(model_list=models, model_name="limit", new_dict={"max": 5000})
        super(TransactionViewSet, self).__init__()

    def _parse_and_validate_request(self, request_dict: dict) -> dict:
        return TinyShield(deepcopy(self._tiny_shield_models)).block(request_dict)

    def _business_logic(self, 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 = "award_id" if type(award_id) is int else "award__generated_unique_award_id"
        filter = {award_id_column: award_id}
        if request_data["sort"] == "cfda_number":
            request_data["sort"] = "assistance_data__cfda_number"
        lower_limit = (request_data["page"] - 1) * request_data["limit"]
        upper_limit = request_data["page"] * request_data["limit"]

        queryset = (
            TransactionNormalized.objects.all()
            .filter(**filter)
            .select_related("assistance_data")
            .values(*list(self.transaction_lookup.values()))
        )
        if request_data["order"] == "desc":
            queryset = queryset.order_by(F(request_data["sort"]).desc(nulls_last=True))
        else:
            queryset = queryset.order_by(F(request_data["sort"]).asc(nulls_first=True))

        rows = list(queryset[lower_limit : upper_limit + 1])
        return self._format_results(rows)

    def _format_results(self, rows):
        results = []
        for row in rows:
            unique_prefix = "ASST_TX"
            result = {k: row.get(v) for k, v in self.transaction_lookup.items() if k != "award_id"}
            if result["is_fpds"]:
                unique_prefix = "CONT_TX"
                del result["cfda_number"]
            result["id"] = f"{unique_prefix}_{result['id']}"
            del result["is_fpds"]
            results.append(result)
        return results

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

        response = {"page_metadata": page_metadata, "results": results[: request_data["limit"]]}

        return Response(response)