Skip to content
This repository has been archived by the owner on Sep 18, 2024. It is now read-only.

Commit

Permalink
[Task HHS#2056] Add post_date and close_date filters to search endpoi…
Browse files Browse the repository at this point in the history
…nt schema (navapbc/simpler-grants-govnavapbc/simpler-grants-gov#168)

Fixes HHS#2056

- Added .with_start_date to search_schema builder to allow building a
date field with key of "start_date"
- Added .with_end_date to search_schema builder to allow building a date
field with key of "end_date"
- Added post_date and close_date properties to
OpportunitySearchFilterV1Schema class, which utilize the above to build
schema filters for post_date and close_date which can utilize start_date
and/or end_date fields.
- Added two unit tests in test_opportunity_route_search that will test
the data validation of these new filters. One test is for 200 response
cases and the other test is for 422 (invalid) response cases.

Note: As noted in the AC of Issue #163, this PR does NOT include
implementation of the filters. Currently, these filters do nothing as
they haven't been tied to any sort of query. This PR is just to lay the
ground work.

---------

Co-authored-by: nava-platform-bot <platform-admins@navapbc.com>
  • Loading branch information
2 people authored and acouch committed Sep 18, 2024
1 parent b9c60e8 commit 373b03b
Show file tree
Hide file tree
Showing 6 changed files with 184 additions and 9 deletions.
41 changes: 41 additions & 0 deletions api/openapi.generated.yml
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,11 @@ paths:
one_of:
- forecasted
- posted
post_date:
start_date: '2024-01-01'
end_date: '2024-02-01'
close_date:
start_date: '2024-01-01'
pagination:
order_by: opportunity_id
page_offset: 1
Expand Down Expand Up @@ -826,6 +831,32 @@ components:
type: string
minLength: 2
example: USAID
PostDateFilterV1:
type: object
properties:
start_date:
type:
- string
- 'null'
format: date
end_date:
type:
- string
- 'null'
format: date
CloseDateFilterV1:
type: object
properties:
start_date:
type:
- string
- 'null'
format: date
end_date:
type:
- string
- 'null'
format: date
OpportunitySearchFilterV1:
type: object
properties:
Expand Down Expand Up @@ -854,6 +885,16 @@ components:
- object
allOf:
- $ref: '#/components/schemas/AgencyFilterV1'
post_date:
type:
- object
allOf:
- $ref: '#/components/schemas/PostDateFilterV1'
close_date:
type:
- object
allOf:
- $ref: '#/components/schemas/CloseDateFilterV1'
OpportunityPaginationV1:
type: object
properties:
Expand Down
4 changes: 4 additions & 0 deletions api/src/api/opportunities_v1/opportunity_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@
"funding_category": {"one_of": ["recovery_act", "arts", "natural_resources"]},
"funding_instrument": {"one_of": ["cooperative_agreement", "grant"]},
"opportunity_status": {"one_of": ["forecasted", "posted"]},
"post_date": {"start_date": "2024-01-01", "end_date": "2024-02-01"},
"close_date": {
"start_date": "2024-01-01",
},
},
"pagination": {
"order_by": "opportunity_id",
Expand Down
10 changes: 9 additions & 1 deletion api/src/api/opportunities_v1/opportunity_schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from src.api.schemas.extension import Schema, fields, validators
from src.api.schemas.response_schema import AbstractResponseSchema, PaginationMixinSchema
from src.api.schemas.search_schema import StrSearchSchemaBuilder
from src.api.schemas.search_schema import DateSearchSchemaBuilder, StrSearchSchemaBuilder
from src.constants.lookup_constants import (
ApplicantType,
FundingCategory,
Expand Down Expand Up @@ -321,6 +321,14 @@ class OpportunitySearchFilterV1Schema(Schema):
.build()
)

post_date = fields.Nested(
DateSearchSchemaBuilder("PostDateFilterV1Schema").with_start_date().with_end_date().build()
)

close_date = fields.Nested(
DateSearchSchemaBuilder("CloseDateFilterV1Schema").with_start_date().with_end_date().build()
)


class OpportunityFacetV1Schema(Schema):
opportunity_status = fields.Dict(
Expand Down
67 changes: 59 additions & 8 deletions api/src/api/schemas/search_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,17 @@ def validates_non_empty(self, data: dict, **kwargs: Any) -> None:
)


class StrSearchSchemaBuilder:
class BaseSearchSchemaBuilder:
def __init__(self, schema_class_name: str):
# The schema class name is used on the endpoint
self.schema_fields: dict[str, fields.MixinField] = {}
self.schema_class_name = schema_class_name

def build(self) -> Schema:
return BaseSearchSchema.from_dict(self.schema_fields, name=self.schema_class_name) # type: ignore


class StrSearchSchemaBuilder(BaseSearchSchemaBuilder):
"""
Builder for setting up a filter in a search endpoint schema.
Expand Down Expand Up @@ -70,11 +80,6 @@ class OpportunitySearchFilterSchema(Schema):
)
"""

def __init__(self, schema_class_name: str):
# The schema class name is used on the endpoint
self.schema_fields: dict[str, fields.MixinField] = {}
self.schema_class_name = schema_class_name

def with_one_of(
self,
*,
Expand Down Expand Up @@ -103,5 +108,51 @@ def with_one_of(

return self

def build(self) -> Schema:
return BaseSearchSchema.from_dict(self.schema_fields, name=self.schema_class_name) # type: ignore

class DateSearchSchemaBuilder(BaseSearchSchemaBuilder):
"""
Builder for setting up a filter for a range of dates in the search endpoint schema.
Example of what this might look like:
{
"filters": {
"post_date": {
"start_date": "YYYY-MM-DD",
"end_date": "YYYY-MM-DD"
}
}
}
Support for start_date and
end_date filters have been partially implemented.
Usage::
# In a search request schema, you would use it like so:
example_start_date_field = fields.Nested(
DateSearchSchemaBuilder("ExampleStartDateFieldSchema")
.with_start_date()
.build()
)
example_end_date_field = fields.Nested(
DateSearchSchemaBuilder("ExampleEndDateFieldSchema")
.with_end_date()
.build()
)
example_startend_date_field = fields.Nested(
DateSearchSchemaBuilder("ExampleStartEndDateFieldSchema")
.with_start_date()
.with_end_date()
.build()
)
"""

def with_start_date(self) -> "DateSearchSchemaBuilder":
self.schema_fields["start_date"] = fields.Date(allow_none=True)
return self

def with_end_date(self) -> "DateSearchSchemaBuilder":
self.schema_fields["end_date"] = fields.Date(allow_none=True)
return self
8 changes: 8 additions & 0 deletions api/tests/src/api/opportunities_v1/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ def get_search_request(
applicant_type_one_of: list[ApplicantType] | None = None,
opportunity_status_one_of: list[OpportunityStatus] | None = None,
agency_one_of: list[str] | None = None,
post_date: dict | None = None,
close_date: dict | None = None,
format: str | None = None,
):
req = {
Expand Down Expand Up @@ -65,6 +67,12 @@ def get_search_request(
if agency_one_of is not None:
filters["agency"] = {"one_of": agency_one_of}

if post_date is not None:
filters["post_date"] = post_date

if close_date is not None:
filters["close_date"] = close_date

if len(filters) > 0:
req["filters"] = filters

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -710,6 +710,69 @@ def test_search_filters_200(
):
call_search_and_validate(client, api_auth_token, search_request, expected_results)

@pytest.mark.parametrize(
"search_request",
[
# Post Date
(get_search_request(post_date={"start_date": None})),
(get_search_request(post_date={"end_date": None})),
(get_search_request(post_date={"start_date": "2020-01-01"})),
(get_search_request(post_date={"end_date": "2020-02-01"})),
(get_search_request(post_date={"start_date": None, "end_date": None})),
(get_search_request(post_date={"start_date": "2020-01-01", "end_date": None})),
(get_search_request(post_date={"start_date": None, "end_date": "2020-02-01"})),
(get_search_request(post_date={"start_date": "2020-01-01", "end_date": "2020-02-01"})),
# Close Date
(get_search_request(close_date={"start_date": None})),
(get_search_request(close_date={"end_date": None})),
(get_search_request(close_date={"start_date": "2020-01-01"})),
(get_search_request(close_date={"end_date": "2020-02-01"})),
(get_search_request(close_date={"start_date": None, "end_date": None})),
(get_search_request(close_date={"start_date": "2020-01-01", "end_date": None})),
(get_search_request(close_date={"start_date": None, "end_date": "2020-02-01"})),
(get_search_request(close_date={"start_date": "2020-01-01", "end_date": "2020-02-01"})),
],
)
def test_search_validate_date_filters_200(self, client, api_auth_token, search_request):
resp = client.post(
"/v1/opportunities/search", json=search_request, headers={"X-Auth": api_auth_token}
)
assert resp.status_code == 200

@pytest.mark.parametrize(
"search_request",
[
# Post Date
(get_search_request(post_date={"start_date": "I am not a date"})),
(get_search_request(post_date={"start_date": "123-456-789"})),
(get_search_request(post_date={"start_date": "5"})),
(get_search_request(post_date={"start_date": 5})),
(get_search_request(post_date={"end_date": "I am not a date"})),
(get_search_request(post_date={"end_date": "123-456-789"})),
(get_search_request(post_date={"end_date": "5"})),
(get_search_request(post_date={"end_date": 5})),
# Close Date
(get_search_request(close_date={"start_date": "I am not a date"})),
(get_search_request(close_date={"start_date": "123-456-789"})),
(get_search_request(close_date={"start_date": "5"})),
(get_search_request(close_date={"start_date": 5})),
(get_search_request(close_date={"end_date": "I am not a date"})),
(get_search_request(close_date={"end_date": "123-456-789"})),
(get_search_request(close_date={"end_date": "5"})),
(get_search_request(close_date={"end_date": 5})),
],
)
def test_search_validate_date_filters_422(self, client, api_auth_token, search_request):
resp = client.post(
"/v1/opportunities/search", json=search_request, headers={"X-Auth": api_auth_token}
)
assert resp.status_code == 422

json = resp.get_json()
error = json["errors"][0]
assert json["message"] == "Validation error"
assert error["message"] == "Not a valid date."

@pytest.mark.parametrize(
"search_request, expected_results",
[
Expand Down

0 comments on commit 373b03b

Please sign in to comment.