Skip to content

Commit

Permalink
Merge pull request #71 from dxw/72-and-73-cache-forecasts-w-cerc-fore…
Browse files Browse the repository at this point in the history
…cast-service

72 and 73 cache forecasts using CercForecastService
  • Loading branch information
edavey authored Oct 31, 2024
2 parents 3bb83be + be2de2b commit 2cd00bd
Show file tree
Hide file tree
Showing 35 changed files with 922 additions and 184 deletions.
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,4 @@ ADDITIONAL_HOSTNAMES=

CERC_API_HOST_URL=https://cerc.example.com
CERC_API_KEY=SECRET-API-KEY
CERC_API_CACHE_LIMIT_MINS=30
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ gem "terser"
gem "govuk-components"
gem "httparty"
gem "view_component"
gem "seed-fu"

group :development do
gem "better_errors"
Expand Down
4 changes: 4 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,9 @@ GEM
sprockets-rails
tilt
securerandom (0.3.1)
seed-fu (2.3.9)
activerecord (>= 3.1)
activesupport (>= 3.1)
selenium-webdriver (4.25.0)
base64 (~> 0.2)
logger (~> 1.4)
Expand Down Expand Up @@ -494,6 +497,7 @@ DEPENDENCIES
rollbar
rspec-rails
sass-rails (~> 6.0)
seed-fu
selenium-webdriver
simplecov
solargraph
Expand Down
2 changes: 2 additions & 0 deletions Procfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
web: bundle exec puma -C config/puma.rb
release: rake db:migrate && rake db:seed_fu
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ To manage sensitive environment variables:

- `CERC_API_HOST_URL`: find the URL of the CERC API host in the 1Password vault
- `CERC_API_KEY`: find the API key in the 1Password vault
- `CERC_API_CACHE_LIMIT_MINS`: how often we expire our cached forecasts

## Access

Expand Down
13 changes: 10 additions & 3 deletions app/controllers/forecasts_controller.rb
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
class ForecastsController < ApplicationController
def show
@forecasts = CercApiClient
.forecasts_for(params.has_key?("zone") ? params["zone"] : "Southwark")
@zones = JSON.parse(File.read("#{Rails.root}/config/list-of-zones.json"))
@forecasts = CercForecastService.latest_forecasts_for(zone).data
@zones = Zone.order(name: :asc).pluck(:name, :cerc_id)
@air_quality_alerts = @forecasts.map(&:alerts).flatten
end

private

def zone
return Zone.default unless params[:zone]

Zone.find_by(cerc_id: params[:zone])
end
end
14 changes: 10 additions & 4 deletions app/controllers/styled_forecasts_controller.rb
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
class StyledForecastsController < ApplicationController
layout "tailwind_layout"
def show
@forecasts = CercApiClient
.forecasts_for(params.fetch("zone", "Southwark"))
@forecasts = CercForecastService.latest_forecasts_for(zone).data
end

def update
forecasts = CercApiClient
.forecasts_for(params.fetch("zone", "Southwark"))
forecasts = CercForecastService.latest_forecasts_for(zone).data

day_forecast = forecast_for_day(params.fetch("day"), forecasts)

Expand All @@ -18,6 +16,8 @@ def update
)
end

private

def forecast_for_day(day, forecasts)
case day
when "today"
Expand All @@ -30,4 +30,10 @@ def forecast_for_day(day, forecasts)
raise ArgumentError, "Invalid day: #{day}"
end
end

def zone
return Zone.default unless params[:zone]

Zone.find_by(cerc_id: params[:zone])
end
end
14 changes: 9 additions & 5 deletions app/lib/forecast_factory.rb
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
class ForecastFactory
def self.build(forecast_representation)
obtained_at = Time.zone.parse(forecast_representation.fetch("forecastdate"))
def self.build(cerc_forecasts:, zone_id: nil)
obtained_at = Time.zone.parse(cerc_forecasts.fetch("forecastdate"))

zone = forecast_representation
.fetch("zones")
.first
zones = cerc_forecasts.fetch("zones")
zone = if zone_id
fail_if_not_found = proc { raise "Forecast for zone ID '#{zone_id}' not found" }
zones.find(fail_if_not_found) { |z| zone_id == z["zone_id"] }
else
zones.first
end

zone.fetch("forecasts")
.map do |forecast|
Expand Down
27 changes: 27 additions & 0 deletions app/models/cached_forecast.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
class CachedForecast < ApplicationRecord
self.implicit_order_column = "obtained_at"
belongs_to :zone
serialize :data

scope :latest_for, ->(zone) { where("zone_id = ?", zone.id).last }

def self.stale?
if (latest_record = last)
threshold = Time.current - ENV.fetch("CERC_API_CACHE_LIMIT_MINS").to_i.minutes
return latest_record.obtained_at < threshold
end

true
end

def self.store(built_forecasts)
zone = Zone.find_by(cerc_id: built_forecasts.first.zone.id)
obtained_at = built_forecasts.first.obtained_at

create(
zone: zone,
obtained_at: obtained_at,
data: built_forecasts
)
end
end
14 changes: 13 additions & 1 deletion app/models/cerc_api_client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,18 @@ def self.fetch_data(zone)
end

def self.forecasts_for(zone)
ForecastFactory.build(fetch_data(zone))
ForecastFactory.build(cerc_forecasts: fetch_data(zone))
end

def self.latest_forecasts
base_url = ENV.fetch("CERC_API_HOST_URL")

query = {
"from" => Date.today,
"numdays" => 3,
"key" => ENV.fetch("CERC_API_KEY")
}

HTTParty.get("#{base_url}/getforecast/all", query: query)
end
end
2 changes: 1 addition & 1 deletion app/models/forecast.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ def inspect
"@zone=#{zone.inspect}",
"@air_pollution=#{air_pollution.inspect}",
"@uv=#{uv.inspect}",
"@pollen=#{pollen}",
"@pollen=#{pollen.inspect}",
"@temperature=#{temperature.inspect}"
]

Expand Down
7 changes: 7 additions & 0 deletions app/models/zone.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
class Zone < ApplicationRecord
DEFAULT_ZONE_SOUTHWARK_CERC_ID = 29

def self.default
find_by!(cerc_id: DEFAULT_ZONE_SOUTHWARK_CERC_ID)
end
end
21 changes: 21 additions & 0 deletions app/services/cerc_forecast_service.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
class CercForecastService
class << self
def latest_forecasts_for(zone)
refresh_cache if CachedForecast.stale?

CachedForecast.latest_for(zone)
end

def refresh_cache
cerc_forecasts = CercApiClient.latest_forecasts

cerc_forecasts.fetch("zones").each do |zone|
CachedForecast.store(
ForecastFactory.build(
cerc_forecasts: cerc_forecasts, zone_id: zone.fetch("zone_id")
)
)
end
end
end
end
2 changes: 1 addition & 1 deletion app/views/styled_forecasts/show.html.erb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<%= render partial: "styled_forecasts/location" %>
<%= render partial: "styled_forecasts/forecast_tabs", forecasts: @forecasts %>
<%= render partial: "styled_forecasts/forecast_tabs", cerc_forecasts: @forecasts %>
<%= render partial: "styled_forecasts/predictions", locals: { forecast: @forecasts.first } %>
<%= render partial: "sharing" %>
<%= render partial: "learning" %>
Expand Down
16 changes: 16 additions & 0 deletions config/application.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
module AirText
class Application < Rails::Application
config.generators do |g|
g.orm :active_record, primary_key_type: :uuid
g.test_framework :rspec,
fixtures: true,
view_specs: false,
Expand All @@ -36,5 +37,20 @@ class Application < Rails::Application
# Make sure the `form_with` helper generates local forms, instead of defaulting
# to remote and unobtrusive XHR forms
config.action_view.form_with_generates_remote_forms = false

config.after_initialize do
ActiveRecord.yaml_column_permitted_classes += [
ActiveSupport::TimeWithZone,
ActiveSupport::TimeZone,
Time,
Date,
Forecast,
ForecastZone,
AirPollutionPrediction,
UvPrediction,
PollenPrediction,
TemperaturePrediction
]
end
end
end
Loading

0 comments on commit 2cd00bd

Please sign in to comment.