Skip to content
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import pytest
from model_bakery import baker
from rest_framework import status

url = "/api/v2/federal_accounts/{federal_account_code}/program_activities/{query_params}"


@pytest.fixture
def program_activities_test_data():
federal_account_1 = baker.make("accounts.FederalAccount", federal_account_code="000-0001")
federal_account_2 = baker.make("accounts.FederalAccount", federal_account_code="000-0002")
treasury_account_1 = baker.make("accounts.TreasuryAppropriationAccount", federal_account=federal_account_1)
treasury_account_2 = baker.make("accounts.TreasuryAppropriationAccount", federal_account=federal_account_2)
park_1 = baker.make("references.ProgramActivityPark", code="00000000001", name="PARK 1")
park_2 = baker.make("references.ProgramActivityPark", code="00000000002", name="PARK 2")
park_3 = baker.make("references.ProgramActivityPark", code="00000000003", name="PARK 3")
park_4 = baker.make("references.ProgramActivityPark", code="00000000004", name="PARK 4")
pac_pan_1 = baker.make(
"references.RefProgramActivity", program_activity_code="0001", program_activity_name="PAC/PAN 1"
)
pac_pan_2 = baker.make(
"references.RefProgramActivity", program_activity_code="0002", program_activity_name="PAC/PAN 2"
)
sa = baker.make("submissions.SubmissionAttributes", is_final_balances_for_fy=True)
baker.make(
"financial_activities.FinancialAccountsByProgramActivityObjectClass",
treasury_account=treasury_account_1,
program_activity_reporting_key=park_1,
program_activity=None,
submission=sa,
)
baker.make(
"financial_activities.FinancialAccountsByProgramActivityObjectClass",
treasury_account=treasury_account_1,
program_activity_reporting_key=None,
program_activity=pac_pan_1,
submission=sa,
)
baker.make(
"financial_activities.FinancialAccountsByProgramActivityObjectClass",
treasury_account=treasury_account_1,
program_activity_reporting_key=park_2,
program_activity=pac_pan_2,
submission=sa,
)
baker.make(
"financial_activities.FinancialAccountsByProgramActivityObjectClass",
treasury_account=treasury_account_1,
program_activity_reporting_key=park_3,
program_activity=None,
submission=sa,
)
baker.make(
"financial_activities.FinancialAccountsByProgramActivityObjectClass",
treasury_account=treasury_account_1,
program_activity_reporting_key=park_3,
program_activity=None,
submission=sa,
)
baker.make(
"financial_activities.FinancialAccountsByProgramActivityObjectClass",
treasury_account=treasury_account_2,
program_activity_reporting_key=park_4,
program_activity=None,
submission=sa,
)


@pytest.mark.django_db
def test_success(client, program_activities_test_data):
resp = client.get(url.format(federal_account_code="000-0001", query_params=""))
expected_result = {
"results": [
{"code": "0001", "name": "PAC/PAN 1", "type": "PAC/PAN"},
{"code": "00000000003", "name": "PARK 3", "type": "PARK"},
{"code": "00000000002", "name": "PARK 2", "type": "PARK"},
{"code": "00000000001", "name": "PARK 1", "type": "PARK"},
],
"page_metadata": {
"page": 1,
"total": 4,
"limit": 10,
"next": None,
"previous": None,
"hasNext": False,
"hasPrevious": False,
},
}
assert resp.status_code == status.HTTP_200_OK
assert resp.json() == expected_result

resp = client.get(url.format(federal_account_code="000-0002", query_params=""))
expected_result = {
"results": [
{"code": "00000000004", "name": "PARK 4", "type": "PARK"},
],
"page_metadata": {
"page": 1,
"total": 1,
"limit": 10,
"next": None,
"previous": None,
"hasNext": False,
"hasPrevious": False,
},
}
assert resp.status_code == status.HTTP_200_OK
assert resp.json() == expected_result


@pytest.mark.django_db
def test_pagination(client, program_activities_test_data):
resp = client.get(url.format(federal_account_code="000-0001", query_params="?limit=3&sort=name&order=asc"))
expected_result = {
"results": [
{"code": "0001", "name": "PAC/PAN 1", "type": "PAC/PAN"},
{"code": "00000000001", "name": "PARK 1", "type": "PARK"},
{"code": "00000000002", "name": "PARK 2", "type": "PARK"},
],
"page_metadata": {
"page": 1,
"total": 4,
"limit": 3,
"next": 2,
"previous": None,
"hasNext": True,
"hasPrevious": False,
},
}
assert resp.status_code == status.HTTP_200_OK
assert resp.json() == expected_result

resp = client.get(url.format(federal_account_code="000-0001", query_params="?limit=3&sort=name&order=asc&page=2"))
expected_result = {
"results": [
{"code": "00000000003", "name": "PARK 3", "type": "PARK"},
],
"page_metadata": {
"page": 2,
"total": 4,
"limit": 3,
"next": None,
"previous": 1,
"hasNext": False,
"hasPrevious": True,
},
}
assert resp.status_code == status.HTTP_200_OK
assert resp.json() == expected_result
8 changes: 7 additions & 1 deletion usaspending_api/accounts/urls_federal_accounts_v2.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from django.urls import re_path

from usaspending_api.accounts.v2.views.federal_account_program_activities import FederalAccountProgramActivities
from usaspending_api.accounts.views import federal_accounts_v2 as views

# bind ViewSets to URLs
Expand All @@ -10,12 +11,17 @@
federal_account = views.FederalAccountViewSet.as_view()
federal_accounts = views.FederalAccountsViewSet.as_view()


urlpatterns = [
re_path(r"(?P<pk>[0-9]+)/available_object_classes/?$", object_class_federal_accounts),
re_path(r"(?P<pk>[0-9]+)/fiscal_year_snapshot/(?P<fy>[0-9]+)/?$", fiscal_year_snapshot_federal_accounts),
re_path(r"(?P<pk>[0-9]+)/fiscal_year_snapshot/?$", fiscal_year_snapshot_federal_accounts),
# re_path(r'(?P<pk>[0-9]+)/spending_over_time$', spending_over_time_federal_accounts),
# re_path(r'(?P<pk>[0-9]+)/spending_by_category$', spending_by_category_federal_accounts),
re_path(r"(?P<federal_account_code>[0-9]{3}[\-][0-9]{4})/$", federal_account),
re_path(
r"(?P<federal_account_code>[0-9]{3}[\-][0-9]{4})/program_activities/?$",
FederalAccountProgramActivities.as_view(),
),
re_path(r"(?P<federal_account_code>[0-9]{3}[\-][0-9]{4})/?$", federal_account),
re_path(r"$", federal_accounts),
]
39 changes: 39 additions & 0 deletions usaspending_api/accounts/v2/views/federal_account_base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
from django.utils.functional import cached_property
from rest_framework.request import Request
from rest_framework.views import APIView

from usaspending_api.common.data_classes import Pagination
from usaspending_api.common.validator import TinyShield, customize_pagination_with_sort_columns


class FederalAccountBase(APIView):

@property
def federal_account_code(self) -> str:
return self.kwargs["federal_account_code"]


class PaginationMixin:
request: Request

default_sort_column: str
sortable_columns: list[str]

@cached_property
def pagination(self):
model = customize_pagination_with_sort_columns(self.sortable_columns, self.default_sort_column)
request_data = TinyShield(model).block(self.request.query_params)

# Use the default sort as a tie-breaker in the case of a different sort provided
primary_sort_key = request_data.get("sort", self.default_sort_column)
secondary_sort_key = self.default_sort_column if primary_sort_key != self.default_sort_column else None

return Pagination(
page=request_data["page"],
limit=request_data["limit"],
lower_limit=(request_data["page"] - 1) * request_data["limit"],
upper_limit=(request_data["page"] * request_data["limit"]),
sort_key=primary_sort_key,
sort_order=request_data["order"],
secondary_sort_key=secondary_sort_key,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
from django.db.models import Case, Q, TextField, When, Value
from django.db.models.functions import Coalesce
from rest_framework.request import Request
from rest_framework.response import Response

from usaspending_api.common.helpers.generic_helper import get_pagination_metadata
from usaspending_api.accounts.v2.views.federal_account_base import FederalAccountBase, PaginationMixin
from usaspending_api.financial_activities.models import FinancialAccountsByProgramActivityObjectClass


class FederalAccountProgramActivities(PaginationMixin, FederalAccountBase):
"""
Retrieve a list of all program activities for a federal account.
"""

endpoint_doc = (
"usaspending_api/api_contracts/contracts/v2/federal_accounts/federal_account_code/program_activities.md"
)

def __init__(self, **kwargs):
super().__init__(**kwargs)
self.default_sort_column = "code"
self.sortable_columns = ["code", "name", "type"]

def get(self, request: Request, *args, **kwargs):
results = (
FinancialAccountsByProgramActivityObjectClass.objects.filter(
Q(Q(program_activity_reporting_key__isnull=False) | Q(program_activity__isnull=False)),
treasury_account__federal_account__federal_account_code=self.federal_account_code,
submission__is_final_balances_for_fy=True,
)
.annotate(
code=Coalesce(
"program_activity_reporting_key__code",
"program_activity__program_activity_code",
output_field=TextField(),
),
name=Coalesce(
"program_activity_reporting_key__name",
"program_activity__program_activity_name",
output_field=TextField(),
),
type=Case(
When(program_activity_reporting_key__isnull=False, then=Value("PARK")),
default=Value("PAC/PAN"),
output_field=TextField(),
),
)
.order_by(*self.pagination.robust_order_by_fields)
.values("code", "name", "type")
.distinct()
)
page_metadata = get_pagination_metadata(len(results), self.pagination.limit, self.pagination.page)
return Response(
{
"results": results[self.pagination.lower_limit : self.pagination.upper_limit],
"page_metadata": page_metadata,
}
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
FORMAT: 1A
HOST: https://api.usaspending.gov

# Program Activities [/api/v2/federal_accounts/{federal_account_code}/program_activities/{?limit,page,order,sort}]

This route returns program activities that the specified federal account has allotted money toward.

## GET

+ Parameters
+ `federal_account_code`: `431-0500` (required, string)
Federal account code consisting of the AID and main account code
+ `limit`: 10 (optional, number)
The maximum number of results to return in the response
+ `page`: 1 (optional, number)
The response page to return (the record offset is (`page` - 1) * `limit`).
+ `sort` (optional, enum[string])
+ Default: `code`
+ Members
+ `code`
+ `name`
+ `type`
+ `order` (optional, enum[string])
+ Default: `desc`
+ Members
+ `asc`
+ `desc`

+ Response 200 (application/json)
+ Attributes (object)
+ `results` (required, array[ProgramActivities], fixed-type)
+ `page_metadata` (required, PageMetadata, fixed-type)
Information used for pagination of results.
+ Body

{
"results": [
{
"code": "0000",
"name": "OTHER/UNKNOWN",
"type": "PAC/PAN"
},
{
"code": "0001",
"name": "TECHNICAL AND SCIENTIFIC ACTIVITIES",
"type": "PAC/PAN"
},
{
"code": "0001",
"name": "TECHNICAL AND SCIENTIFIC ACTIVITIES",
"type": "PARK"
}
],
"page_metadata": {
"limit": 10,
"page": 1,
"next": null,
"previous": null,
"hasNext": false,
"hasPrevious": false,
"total": 3
}
}

# Data Structures

## ProgramActivities (object)
+ `code` (required, string)
+ `name` (required, string)
+ `type` (required, enum[string], fixed-type)
Whether the Program Activity values are from the older Program Activity Code / Name (PAC/PAN) or the Program Activity Reporting Key (PARK)
+ Members
+ `PAC/PAN`
+ `PARK`

## PageMetadata (object)
+ `limit` (required, number)
+ `page` (required, number)
+ `next` (required, number, nullable)
+ `previous` (required, number, nullable)
+ `hasNext` (required, boolean)
+ `hasPrevious` (required, boolean)
+ `total` (required, number)
1 change: 1 addition & 0 deletions usaspending_api/api_docs/markdown/endpoints.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ The currently available endpoints are listed in the following table.
|[/api/v2/federal_accounts/<ACCOUNT_CODE\>/available_object_classes/](/api/v2/federal_accounts/4324/available_object_classes/)|GET| Returns financial spending data by object class based on account's internal ID |
|[/api/v2/federal_accounts/<ACCOUNT_CODE\>/fiscal_year_snapshot/<YEAR\>/](/api/v2/federal_accounts/4324/fiscal_year_snapshot/2017/)|GET| Returns budget information for a federal account for the year provided based on account's internal ID |
|[/api/v2/federal_accounts/<ACCOUNT_CODE\>/fiscal_year_snapshot/](/api/v2/federal_accounts/4324/fiscal_year_snapshot/)|GET| Returns budget information for a federal account for the most recent year based on account's internal ID |
|[/api/v2/federal_accounts/<ACCOUNT_CODE\>/program_activities/](/api/v2/federal_accounts/020-0550/program_activities/)|GET| Returns a list of program activities under a federal account based on its federal account code |
|[/api/v2/federal_accounts/](/api/v2/federal_accounts/)|POST| Returns financial spending data by object class |
|[/api/v2/federal_obligations/](/api/v2/federal_obligations/?fiscal_year=2019&funding_agency_id=315&limit=10&page=1)|GET| Returns a paginated list of obligations for the provided agency for the provided year |
|[/api/v2/financial_balances/agencies/](/api/v2/financial_balances/agencies/?fiscal_year=2017&funding_agency_id=4324)|GET| Returns financial balances by agency and the latest quarter for the given fiscal year |
Expand Down
12 changes: 8 additions & 4 deletions usaspending_api/common/data_classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,22 @@ class Pagination:
secondary_sort_key: Optional[str] = None

@property
def _sort_order_field_prefix(self):
def _sort_order_field_prefix(self) -> str:
if self.sort_order == "desc":
return "-"
return ""

@property
def order_by(self):
def order_by(self) -> str:
return f"{self._sort_order_field_prefix}{self.sort_key}"

@property
def robust_order_by_fields(self):
return (self.order_by, f"{self._sort_order_field_prefix}{self.secondary_sort_key}")
def robust_order_by_fields(self) -> tuple[str] | tuple[str, str]:
return (
(self.order_by,)
if self.secondary_sort_key is None
else (self.order_by, f"{self._sort_order_field_prefix}{self.secondary_sort_key}")
)


@dataclass
Expand Down