This repository has been archived by the owner on Sep 18, 2024. It is now read-only.
forked from HHS/simpler-grants-gov
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Browse files
Browse the repository at this point in the history
Fixes HHS#2079 Adds an endpoint to fetch opportunity versions * Only includes some of the filters that we'll need to include Adds a lot of utilities for setting up opportunities for local development and testing with versions https://docs.google.com/document/d/18oWmjQJKunMKy6cfnfUnyGEX33uu5UDPnRktD_wRFlE/edit#heading=h.4xmkylqq7mnx provides a lot of context for how opportunity versioning works in the existing system - which is to say its very very complex. I'm sure we'll alter that behavior as we go forward, for now I kept the endpoint simple in terms of its filters, just removing obvious cases (eg. the summary record is marked as deleted). I'm also not certain what we want to do with naming. I really don't like my current approach of "forecast" and "non-forecast", but we can address that later as well. -- Beyond understanding what versioning logic we needed to support, the most complex component by far is setting up the data in the first place in an easy manner. I originally tried some ideas using the factory classes themselves, but due to the order of operations necessary to do that, that wasn't possible (in short, to create prior history records, I first need the current record, but that doesn't exist until after everything else in a factory runs). So, I went with a builder process that wraps the factories and sets up some reasonable scenarios for you. Its clunky, but seems to work well enough. --------- Co-authored-by: nava-platform-bot <[email protected]>
- Loading branch information
Showing
12 changed files
with
656 additions
and
52 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,24 +1,99 @@ | ||
from datetime import date | ||
|
||
from sqlalchemy import select | ||
from sqlalchemy.orm import noload, selectinload | ||
|
||
import src.adapters.db as db | ||
import src.util.datetime_util as datetime_util | ||
from src.api.route_utils import raise_flask_error | ||
from src.db.models.opportunity_models import Opportunity | ||
from src.db.models.opportunity_models import Opportunity, OpportunitySummary | ||
|
||
|
||
def get_opportunity(db_session: db.Session, opportunity_id: int) -> Opportunity: | ||
opportunity: Opportunity | None = ( | ||
db_session.execute( | ||
select(Opportunity) | ||
.where(Opportunity.opportunity_id == opportunity_id) | ||
.where(Opportunity.is_draft.is_(False)) | ||
.options(selectinload("*"), noload(Opportunity.all_opportunity_summaries)) | ||
) | ||
.unique() | ||
.scalar_one_or_none() | ||
def _fetch_opportunity( | ||
db_session: db.Session, opportunity_id: int, load_all_opportunity_summaries: bool | ||
) -> Opportunity: | ||
stmt = ( | ||
select(Opportunity) | ||
.where(Opportunity.opportunity_id == opportunity_id) | ||
.where(Opportunity.is_draft.is_(False)) | ||
.options(selectinload("*")) | ||
) | ||
|
||
if not load_all_opportunity_summaries: | ||
stmt = stmt.options(noload(Opportunity.all_opportunity_summaries)) | ||
|
||
opportunity = db_session.execute(stmt).unique().scalar_one_or_none() | ||
|
||
if opportunity is None: | ||
raise_flask_error(404, message=f"Could not find Opportunity with ID {opportunity_id}") | ||
|
||
return opportunity | ||
|
||
|
||
def get_opportunity(db_session: db.Session, opportunity_id: int) -> Opportunity: | ||
return _fetch_opportunity(db_session, opportunity_id, load_all_opportunity_summaries=False) | ||
|
||
|
||
def get_opportunity_versions(db_session: db.Session, opportunity_id: int) -> dict: | ||
opportunity = _fetch_opportunity( | ||
db_session, opportunity_id, load_all_opportunity_summaries=True | ||
) | ||
|
||
now_us_eastern = datetime_util.get_now_us_eastern_date() | ||
|
||
forecasts = _filter_summaries(opportunity.all_forecasts, now_us_eastern) | ||
non_forecasts = _filter_summaries(opportunity.all_non_forecasts, now_us_eastern) | ||
|
||
return {"opportunity": opportunity, "forecasts": forecasts, "non_forecasts": non_forecasts} | ||
|
||
|
||
def _filter_summaries( | ||
summaries: list[OpportunitySummary], current_date: date | ||
) -> list[OpportunitySummary]: | ||
# Find the most recent summary | ||
most_recent_summary: OpportunitySummary | None = None | ||
for summary in summaries: | ||
if summary.revision_number is None: | ||
most_recent_summary = summary | ||
summaries.remove(summary) | ||
break | ||
|
||
# If there is no most recent summary, even if there is any history records | ||
# we have to filter all of the summaries. Effectively this would mean the most recent | ||
# was deleted, and we never show deleted summaries (or anything that comes before them). | ||
if most_recent_summary is None: | ||
return [] | ||
|
||
# If the most recent summary isn't able to be public itself, we can't display any history | ||
# for this type of summary object. | ||
if not most_recent_summary.can_summary_be_public(current_date): | ||
return [] | ||
|
||
summaries_to_keep = [most_recent_summary] | ||
|
||
# We want to process these in reverse order (most recent first) | ||
# as soon as we hit one that we need to filter, we stop adding records. | ||
# | ||
# For example, if a summary is marked as deleted, we won't add that, and | ||
# we also filter out all summaries that came before it (by just breaking the loop) | ||
summaries = sorted(summaries, key=lambda s: s.version_number, reverse=True) # type: ignore | ||
|
||
for summary in summaries: | ||
if summary.is_deleted: | ||
break | ||
|
||
if summary.post_date is None: | ||
break | ||
|
||
# If a historical record was updated (or initially created) before | ||
# its own post date (ie. would have been visible when created) regardless | ||
# of what the current date may be | ||
# TODO - leaving this out of the implementation for the moment | ||
# as we need to investigate why this is being done and if there is a better | ||
# way as this ends up filtering out records we don't want removed | ||
# if summary.updated_at.date() < summary.post_date: | ||
# break | ||
|
||
summaries_to_keep.append(summary) | ||
|
||
return summaries_to_keep |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.