Skip to content

Commit

Permalink
Change how prep period is set (bcgov#1877)
Browse files Browse the repository at this point in the history
feat(hfi calculator): Change prep period behaviour [bcgov#1634](bcgov#1634)

- Load the most recently saved prep record that overlaps with the current date. (Previous behaviour was to load the most recently modified prep record.)
- Changing the prep period date does not result in a write to the database. Users can change thus change dates without affecting other users. (Previous behaviour was to create/update a prep record when changing the date.)
- When a fire starts are changed or weather stations are toggled, a prep period record is created in the database.
- Changing the prep period end date results in losing fire starts & weather station toggles. (Previous behaviour would retain fire starts & weather station toggles if the start start date was unchanged.)
- Changed warning message from "Any changes made to dates, fire starts or selected stations ..." to "Any changes made to fire starts or selected stations..."
  • Loading branch information
Sybrand authored Apr 12, 2022
1 parent 168942d commit dcf6199
Show file tree
Hide file tree
Showing 12 changed files with 171 additions and 201 deletions.
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
## 2022-04-11 HFI Calc [#1634](https://github.com/bcgov/wps/issues/1634)

### Features

- **hfi calculator:** Load the most recently saved prep record that overlaps with the current date. (Previous behaviour was to load the most recently modified prep record.)

- **hfi calculator:** Changing the prep period date does not result in a write to the database. Users can change thus change dates without affecting other users. (Previous behaviour was to create/update a prep record when changing the date.)
- **hfi calculator:** When a fire starts are changed or weather stations are toggled, a prep period record is created in the database.
- **hfi calculator:** Changing the prep period end date results in losing fire starts & weather station toggles. (Previous behaviour would retain fire starts & weather station toggles if the start date was unchanged.)
- **hfi calculator:** Changed warning message from "Any changes made to dates, fire starts or selected stations ..." to "Any changes made to fire starts or selected stations..."
26 changes: 17 additions & 9 deletions api/app/db/crud/hfi_calc.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@
from datetime import date
from sqlalchemy.engine.cursor import CursorResult
from sqlalchemy.orm import Session
from app.schemas.hfi_calc import HFIResultRequest
from app.schemas.hfi_calc import DateRange, HFIResultRequest
from app.db.models.hfi_calc import (FireCentre, FuelType, PlanningArea, PlanningWeatherStation, HFIRequest,
FireStartRange, FireCentreFireStartRange, FireStartLookup)
from app.utils.time import get_utc_now
from app.utils.time import get_pst_now, get_utc_now


def get_fire_weather_stations(session: Session) -> CursorResult:
Expand Down Expand Up @@ -35,15 +35,23 @@ def get_fire_centre_stations(session, fire_centre_id: int) -> CursorResult:

def get_most_recent_updated_hfi_request(session: Session,
fire_centre_id: int,
prep_start_day: date = None,
prep_end_day: date = None) -> HFIRequest:
date_range: DateRange) -> HFIRequest:
""" Get the most recently updated hfi request for a fire centre """
query = session.query(HFIRequest)\
.filter(HFIRequest.fire_centre_id == fire_centre_id)
if prep_start_day is not None:
query = query.filter(HFIRequest.prep_start_day == prep_start_day)
if prep_end_day is not None:
query = query.filter(HFIRequest.prep_end_day == prep_end_day)
.filter(HFIRequest.fire_centre_id == fire_centre_id)\
.filter(HFIRequest.prep_start_day == date_range.start_date)\
.filter(HFIRequest.prep_end_day == date_range.end_date)
return query.order_by(HFIRequest.create_timestamp.desc()).first()


def get_most_recent_updated_hfi_request_for_current_date(session: Session,
fire_centre_id: int) -> HFIRequest:
""" Get the most recently updated hfi request within some date range, for a fire centre """
now = get_utc_now().date()
query = session.query(HFIRequest)\
.filter(HFIRequest.fire_centre_id == fire_centre_id)\
.filter(HFIRequest.prep_start_day <= now)\
.filter(HFIRequest.prep_end_day >= now)
return query.order_by(HFIRequest.create_timestamp.desc()).first()


Expand Down
142 changes: 55 additions & 87 deletions api/app/routers/hfi_calc.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@
DateRange)
from app.auth import authentication_required, audit
from app.schemas.hfi_calc import HFIWeatherStationsResponse
from app.db.crud.hfi_calc import (get_most_recent_updated_hfi_request, store_hfi_request,
from app.db.crud.hfi_calc import (get_most_recent_updated_hfi_request,
get_most_recent_updated_hfi_request_for_current_date,
store_hfi_request,
get_fire_centre_stations)
from app.db.database import get_read_session_scope, get_write_session_scope

Expand All @@ -40,18 +42,21 @@
def get_prepared_request(
session: Session,
fire_centre_id: int,
start_date: Optional[date],
end_date: Optional[date] = None) -> Tuple[HFIResultRequest, bool, List[FireStartRange]]:
date_range: Optional[DateRange]) -> Tuple[HFIResultRequest, bool, List[FireStartRange]]:
""" Attempt to load the most recent request from the database, failing that creates a new request all
set up with default values.
TODO: give this function a better name.
"""
fire_centre_fire_start_ranges = list(load_fire_start_ranges(session, fire_centre_id))
date_range = DateRange(start_date=start_date, end_date=end_date)
stored_request = get_most_recent_updated_hfi_request(session,
fire_centre_id,
date_range.start_date)
if date_range:
stored_request = get_most_recent_updated_hfi_request(session,
fire_centre_id,
date_range)
# NOTE: We could be real nice here, and look for a prep period that intercepts, and grab data there.
else:
# No date range specified!
stored_request = get_most_recent_updated_hfi_request_for_current_date(session, fire_centre_id)
request_loaded = False
if stored_request:
try:
Expand Down Expand Up @@ -149,26 +154,28 @@ def extract_selected_stations(request: HFIResultRequest) -> List[int]:
return stations_codes


@router.post("/fire_centre/{fire_centre_id}/{start_date}/planning_area/{planning_area_id}"
@router.post("/fire_centre/{fire_centre_id}/{start_date}/{end_date}/planning_area/{planning_area_id}"
"/station/{station_code}/selected/{enable}")
async def select_planning_area_station(
fire_centre_id: int, start_date: date,
async def set_planning_area_station(
fire_centre_id: int, start_date: date, end_date: date,
planning_area_id: int, station_code: int,
enable: bool,
response: Response,
token=Depends(authentication_required)
):
""" Enable / disable a station withing a planning area """
logger.info('/fire_centre/%s/%s/planning_area/%s/station/%s/selected/%s',
fire_centre_id, start_date, planning_area_id, station_code, enable)
logger.info('/fire_centre/%s/%s/%s/planning_area/%s/station/%s/selected/%s',
fire_centre_id, start_date, end_date, planning_area_id, station_code, enable)
response.headers["Cache-Control"] = no_cache

with get_read_session_scope() as session:
# We get an existing request object (it will load from the DB or create it
# from scratch if it doesn't exist).
request, _, fire_centre_fire_start_ranges = get_prepared_request(session,
fire_centre_id,
start_date)
DateRange(
start_date=start_date,
end_date=end_date))

# Add station if it's not there, otherwise remove it.
if enable:
Expand All @@ -187,11 +194,12 @@ async def select_planning_area_station(
return request_response


@router.post("/fire_centre/{fire_centre_id}/{start_date}/planning_area/{planning_area_id}"
@router.post("/fire_centre/{fire_centre_id}/{start_date}/{end_date}/planning_area/{planning_area_id}"
"/station/{station_code}/fuel_type/{fuel_type_id}")
async def set_planning_area_station_fuel_type(
fire_centre_id: int,
start_date: date,
end_date: date,
planning_area_id: int,
station_code: int,
fuel_type_id: int,
Expand All @@ -200,27 +208,29 @@ async def set_planning_area_station_fuel_type(
):
""" Set the fuel type for a station in a planning area. """
# TODO: stub - implement!
logger.info("/fire_centre/%s/%s/planning_area/%s/station/%s/fuel_type/%s",
fire_centre_id, start_date,
logger.info("/fire_centre/%s/%s/%s/planning_area/%s/station/%s/fuel_type/%s",
fire_centre_id, start_date, end_date,
planning_area_id, station_code, fuel_type_id)
response.headers["Cache-Control"] = no_cache
raise NotImplementedError('This function is not implemented yet.')


@router.post("/fire_centre/{fire_centre_id}/{start_date}/planning_area/{planning_area_id}"
@router.post("/fire_centre/{fire_centre_id}/{start_date}/{end_date}/planning_area/{planning_area_id}"
"/fire_starts/{prep_day_date}/fire_start_range/{fire_start_range_id}",
response_model=HFIResultResponse)
async def set_fire_start_range(fire_centre_id: int,
start_date: date,
end_date: date,
planning_area_id: int,
prep_day_date: date,
fire_start_range_id: int,
response: Response,
token=Depends(authentication_required)):
""" Set the fire start range, by id."""
logger.info("/fire_centre/%s/%s/planning_area/%s"
logger.info("/fire_centre/%s/%s/%s/planning_area/%s"
"/fire_starts/%s/fire_start_range/%s",
fire_centre_id, start_date, planning_area_id,
fire_centre_id, start_date, end_date,
planning_area_id,
prep_day_date, fire_start_range_id)
response.headers["Cache-Control"] = no_cache

Expand All @@ -229,7 +239,8 @@ async def set_fire_start_range(fire_centre_id: int,
# from scratch if it doesn't exist).
request, _, fire_centre_fire_start_ranges = get_prepared_request(session,
fire_centre_id,
start_date)
DateRange(start_date=start_date,
end_date=end_date))

# We set the fire start range in the planning area for the provided prep day.
if prep_day_date <= request.date_range.end_date:
Expand All @@ -250,85 +261,39 @@ async def set_fire_start_range(fire_centre_id: int,
return request_response


@router.post("/fire_centre/{fire_centre_id}/{start_date}/{end_date}")
async def set_prep_period(fire_centre_id: int,
start_date: date,
end_date: date,
response: Response,
token=Depends(authentication_required)
):
""" Set the prep period """
logger.info('/fire_centre/%s/%s/%s', fire_centre_id, start_date, end_date)
response.headers["Cache-Control"] = no_cache

persist_request = False
with get_read_session_scope() as session:
request, request_loaded, fire_centre_fire_start_ranges = get_prepared_request(session,
fire_centre_id,
start_date,
end_date)
if request_loaded and request.date_range.end_date != end_date:
# We loaded the request from the database, but the end date in the database doesn't match the
# end date we've been given. That means we have to modify the store request accordingly,
# then save it in the database.
persist_request = True
date_range = DateRange(start_date=start_date, end_date=end_date)
date_range = validate_date_range(date_range)
request.date_range = date_range

num_prep_days = date_range.days_in_range()
lowest_fire_starts = fire_centre_fire_start_ranges[0]

fire_centre_stations = get_fire_centre_stations(session, fire_centre_id)
for station, _ in fire_centre_stations:
initialize_planning_area_fire_starts(
request.planning_area_fire_starts,
station.planning_area_id,
num_prep_days,
lowest_fire_starts
)
elif not request_loaded:
# There is no request in the database, so we create one.
persist_request = True

# Get the response.
request_response = await calculate_and_create_response(
session, request, fire_centre_fire_start_ranges)

if persist_request:
save_request_in_database(request, token.get('preferred_username', None))

return request_response


@router.get("/fire_centre/{fire_centre_id}", response_model=HFIResultResponse)
async def load_hfi_result(fire_centre_id: int,
response: Response,
token=Depends(authentication_required)):
async def get_hfi_result(fire_centre_id: int,
response: Response,
token=Depends(authentication_required)):
""" Given a fire centre id, load the most recent HFIResultRequest.
If there isn't a stored request, one will be created.
"""
logger.info('/hfi-calc/load/%s', fire_centre_id)
response.headers["Cache-Control"] = no_cache
return await load_hfi_result_with_date(fire_centre_id, None, response, token)
return await get_hfi_result_with_date(fire_centre_id, None, None, response, token)


@router.get("/fire_centre/{fire_centre_id}/{start_date}", response_model=HFIResultResponse)
async def load_hfi_result_with_date(fire_centre_id: int,
start_date: Optional[date],
response: Response,
_=Depends(authentication_required)):
@router.get("/fire_centre/{fire_centre_id}/{start_date}/{end_date}", response_model=HFIResultResponse)
async def get_hfi_result_with_date(fire_centre_id: int,
start_date: Optional[date],
end_date: Optional[date],
response: Response,
_=Depends(authentication_required)):
""" Given a fire centre id (and optionally a start date), load the most recent HFIResultRequest.
If there isn't a stored request, one will be created.
"""
try:
logger.info('/hfi-calc/load/{fire_centre_id}/{start_date}')
logger.info('/hfi-calc/load/%s/%s/%s', fire_centre_id, start_date, end_date)
response.headers["Cache-Control"] = no_cache

with get_read_session_scope() as session:
if start_date and end_date:
date_range = DateRange(start_date=start_date, end_date=end_date)
else:
date_range = None
request, _, fire_centre_fire_start_ranges = get_prepared_request(session,
fire_centre_id,
start_date)
date_range)

# Get the response.
request_response = await calculate_and_create_response(
Expand Down Expand Up @@ -360,21 +325,24 @@ async def get_fire_centres(response: Response):
raise


@router.get('/fire_centre/{fire_centre_id}/{start_date}/pdf')
async def download_pdf(
fire_centre_id: int, start_date: date,
@router.get('/fire_centre/{fire_centre_id}/{start_date}/{end_date}/pdf')
async def get_pdf(
fire_centre_id: int,
start_date: date,
end_date: date,
_: Response,
token=Depends(authentication_required)
):
""" Returns a PDF of the HFI results for the supplied fire centre and start date. """
logger.info('/hfi-calc/fire_centre/%s/%s/pdf', fire_centre_id, start_date)
logger.info('/hfi-calc/fire_centre/%s/%s/%s/pdf', fire_centre_id, start_date, end_date)

with get_read_session_scope() as session:
(request,
_,
fire_centre_fire_start_ranges) = get_prepared_request(session,
fire_centre_id,
start_date)
DateRange(start_date=start_date,
end_date=end_date))

# Get the response.
request_response = await calculate_and_create_response(
Expand Down
20 changes: 14 additions & 6 deletions api/app/schemas/hfi_calc.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
""" This module contains pydandict schemas the HFI Calculator.
"""
import logging
from typing import List, Mapping, Optional
from datetime import datetime, date
from pydantic import BaseModel
from app.schemas.shared import FuelType


logger = logging.getLogger(__name__)


class StationDaily(BaseModel):
""" Station Daily metrics for HFI daily table """
code: Optional[int] = None
Expand Down Expand Up @@ -134,17 +138,21 @@ class StationInfo(BaseModel):
fuel_type_id: int


class InvalidDateRangeError(Exception):
""" Exception thrown when an invalid date range is encounted."""


class DateRange(BaseModel):
""" A Pythonic implementation of the DateRange construct we use on the front-end in Typescript. """
start_date: Optional[date]
end_date: Optional[date]
start_date: date
end_date: date

def days_in_range(self) -> Optional[int]:
""" Calculate the number of days (inclusive) in the date range. """
if self.start_date and self.end_date:
# num prep days is inclusive, so we need to add 1
return (self.end_date - self.start_date).days + 1
return None
if self.start_date > self.end_date:
raise InvalidDateRangeError(f"Start date {self.start_date} is after end date {self.end_date}")
# num prep days is inclusive, so we need to add 1
return (self.end_date - self.start_date).days + 1


class HFIResultRequest(BaseModel):
Expand Down
Loading

0 comments on commit dcf6199

Please sign in to comment.