fedspendingtransparency/usaspending-api

View on GitHub
usaspending_api/recipient/v2/views/list_recipients.py

Summary

Maintainability
A
0 mins
Test Coverage
A
100%
import copy
import logging

from django.db.models import F, Q
from django.utils.decorators import method_decorator
from rest_framework.response import Response
from rest_framework.views import APIView

from usaspending_api.common.api_versioning import deprecated
from usaspending_api.common.cache_decorator import cache_response
from usaspending_api.common.helpers.generic_helper import get_pagination_metadata
from usaspending_api.common.validator.pagination import PAGINATION
from usaspending_api.common.validator.tinyshield import TinyShield
from usaspending_api.common.validator.utils import update_model_in_list
from usaspending_api.recipient.models import RecipientProfile
from usaspending_api.recipient.v2.lookups import AWARD_TYPES, SPECIAL_CASES

logger = logging.getLogger(__name__)

award_types = list(AWARD_TYPES.keys()) + ["all"]
RECIPIENT_MODELS = [
    {"name": "keyword", "key": "keyword", "type": "text", "text_type": "search"},
    {"name": "award_type", "key": "award_type", "type": "enum", "enum_values": award_types, "default": "all"},
]


def build_recipient_identifier_base_query(filters):
    qs_filter = Q()
    if "keyword" in filters:
        qs_filter |= Q(recipient_name__contains=filters["keyword"].upper())
        qs_filter |= Q(recipient_unique_id__contains=filters["keyword"]) | Q(uei__contains=filters["keyword"])

    if filters["award_type"] != "all":
        qs_filter &= Q(award_types__overlap=[AWARD_TYPES[filters["award_type"]]["filter"]])

    return qs_filter


def get_recipients(filters={}, count=None):
    lower_limit = (filters["page"] - 1) * filters["limit"]
    upper_limit = filters["page"] * filters["limit"]

    amount_column = "last_12_months"
    if filters["award_type"] != "all":
        amount_column = AWARD_TYPES[filters["award_type"]]["amount"]

    qs_filter = build_recipient_identifier_base_query(filters)

    queryset = (
        RecipientProfile.objects.filter(qs_filter)
        .values("recipient_level", "recipient_hash", "recipient_unique_id", "recipient_name", amount_column, "uei")
        .exclude(recipient_name__in=SPECIAL_CASES)
    )
    api_to_db_mapper = {
        "amount": amount_column,
        "duns": "recipient_unique_id",
        "uei": "uei",
        "name": "recipient_name",
    }

    # Nulls Last isn't enabled for the amount sort because it prevents queries sorted by amount columns DESC
    # from using an index on those columns, even though they cannot contain nulls
    nulls_last = filters["sort"] in ["name", "duns"]

    if filters["order"] == "desc":
        queryset = queryset.order_by(F(api_to_db_mapper[filters["sort"]]).desc(nulls_last=nulls_last))
    else:
        queryset = queryset.order_by(F(api_to_db_mapper[filters["sort"]]).asc(nulls_last=nulls_last))

    if count is None:
        count = get_recipient_count(filters=filters)

    page_metadata = get_pagination_metadata(count, filters["limit"], filters["page"])

    results = [
        {
            "id": "{}-{}".format(row["recipient_hash"], row["recipient_level"]),
            "duns": row["recipient_unique_id"],
            "uei": row["uei"],
            "name": row["recipient_name"],
            "recipient_level": row["recipient_level"],
            "amount": row[amount_column],
        }
        for row in queryset[lower_limit:upper_limit]
    ]

    return results, page_metadata


def get_recipient_count(filters={}):
    qs_filter = build_recipient_identifier_base_query(filters)
    return RecipientProfile.objects.filter(qs_filter).exclude(recipient_name__in=SPECIAL_CASES).count()


class RecipientCount(APIView):
    """
    This route takes a single keyword filter and award_type, and returns a count of matching recipients
    """

    endpoint_doc = "usaspending_api/api_contracts/contracts/v2/recipient/count.md"

    cache_key_whitelist = ["keyword", "award_type"]

    @cache_response()
    def post(self, request):
        validated_payload = TinyShield(RECIPIENT_MODELS).block(request.data)
        return Response({"count": get_recipient_count(validated_payload)})


class ListRecipients(APIView):
    """
    This route takes a single keyword filter (and pagination filters), and returns a list of recipients
    """

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

    def request_count(self, filters={}):
        response = RecipientCount.as_view()(request=self.request._request).data
        return response["count"]

    @cache_response()
    def post(self, request):
        models = copy.deepcopy(RECIPIENT_MODELS)
        models.extend(copy.deepcopy(PAGINATION))  # page, limit, sort, order

        # Override pagination default limit of 100
        for model in models:
            if model["name"] == "limit":
                model["max"] = 1000

        new_sort = {"type": "enum", "enum_values": ["name", "uei", "duns", "amount"], "default": "amount"}
        models = update_model_in_list(models, "sort", new_sort)
        models = update_model_in_list(models, "limit", {"default": 50})
        validated_payload = TinyShield(models).block(request.data)

        count = self.request_count(validated_payload)

        results, page_metadata = get_recipients(filters=validated_payload, count=count)
        return Response({"page_metadata": page_metadata, "results": results})


@method_decorator(deprecated, name="post")
class ListRecipientsByDuns(ListRecipients):
    """
    <em>Deprecated: Please see <a href="../">this endpoint</a> instead.</em>

    This route takes a single keyword filter (and pagination filters), and returns a list of recipients
    """

    endpoint_doc = "usaspending_api/api_contracts/contracts/v2/recipient/duns.md"

    def __init__(self):
        super().__init__()