Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(62) Show air quality alerts and 3-day forecast for selected zone #29

Merged
merged 39 commits into from
Oct 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
d8eda46
Add list of zones as static json
patrickjfl Sep 30, 2024
310a07e
Add HTTParty gem
patrickjfl Oct 4, 2024
e9844da
Add client for fetching forecast data from CERC API
patrickjfl Oct 4, 2024
b26164a
Move from forecasts#index -> forecasts#show method
patrickjfl Oct 4, 2024
49380f5
Update forecast page with live data and zone form
patrickjfl Oct 4, 2024
2f959a6
Turn off fingerprinting of assets in test env
edavey Oct 2, 2024
e951d8d
Add WebMock gem to stub CERC API
edavey Oct 2, 2024
ba5eabf
Describe 'View Air Quality Alerts' journey in int. test
edavey Oct 1, 2024
cfeb99f
Fix indentation on forecasts view
edavey Oct 2, 2024
2c64f82
Confirm 'No air quality alerts' when 0 alerts found
edavey Oct 2, 2024
dd53af8
Fix up controller spec
edavey Oct 2, 2024
0bfcaaf
Stub CERC API for 'View forecasts' test
edavey Oct 2, 2024
ef2b0a7
Move existing helper methods to ForecastSteps module
edavey Oct 2, 2024
2a7d2e8
Add markup to aid semantics and testability
edavey Oct 2, 2024
00c5aa6
Describe air pollution predictions for each of 3 days forecast
edavey Oct 2, 2024
9b3c4c9
Turn off deprecation warning in Rspec
edavey Oct 2, 2024
f4dded0
Implement a ForecastFactory to build Forecast entities from CERC's API
edavey Oct 2, 2024
90946ba
Add #inspect methods to forecast classes to aid debugging
edavey Oct 2, 2024
da08d6c
Present `AirPollutionPrediction#overall_label` in sentence case
edavey Oct 3, 2024
48885e9
Use 'Forecast' entities in forecasts view
edavey Oct 3, 2024
cec5ae8
Exclude #inspect definitions from test coverage
edavey Oct 3, 2024
cd38e0b
Describe presentation of ForecastZone#type
edavey Oct 3, 2024
f0b43a1
Integration test for presence of all forecast predictions
patrickjfl Oct 3, 2024
49e7576
Move CERC API forecast fixtures into own namespace
edavey Oct 3, 2024
24108e2
Ask forecast for #alerts
edavey Oct 3, 2024
539da60
Add factories to create test fixtures for domain forecasts
edavey Oct 3, 2024
628a046
Give Forecast responsibility for making AirQualityAlert
edavey Oct 3, 2024
11c42a7
Describe interface of AirQualityAlert
edavey Oct 3, 2024
cc7b917
Add AirQualityAlert#tag_colour
edavey Oct 3, 2024
deb404e
Show air quality alerts when present in forecast
edavey Oct 3, 2024
b850e03
Test CERC API client forecasts_for method
patrickjfl Oct 4, 2024
cd831ba
Create UV prediction model
patrickjfl Oct 4, 2024
54e56dd
Enable UV Prediction to describe itself w/ #inspect
edavey Oct 7, 2024
d860cc7
Add UV prediction model to ForecastFactory
patrickjfl Oct 4, 2024
e164907
Create UV prediction factory and add to forecasts factory definition
patrickjfl Oct 4, 2024
9b013e0
Add UV label and guidance to forecast table
patrickjfl Oct 4, 2024
7a691e3
Set pollen value in API fixures for forecasts
edavey Oct 7, 2024
b50a88d
Set temperature in API fixtures for forecasts
edavey Oct 7, 2024
dcc4815
Set UV in API Forecast fixture
edavey Oct 7, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,6 @@ CANONICAL_HOSTNAME=example.com
# app to respond to and redirect to the canonical hostname, or delete
# this line completely
ADDITIONAL_HOSTNAMES=

CERC_API_HOST_URL=https://cerc.example.com
CERC_API_KEY=SECRET-API-KEY
2 changes: 2 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ gem "turbolinks", "~> 5"
gem "tzinfo-data", platforms: %i[mingw mswin x64_mingw jruby]
gem "terser"
gem "govuk-components"
gem "httparty"

group :development do
gem "better_errors"
Expand Down Expand Up @@ -52,4 +53,5 @@ group :test do
gem "selenium-webdriver"
gem "simplecov"
gem "climate_control"
gem "webmock"
end
17 changes: 17 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,11 @@ GEM
coderay (1.1.3)
concurrent-ruby (1.3.4)
connection_pool (2.4.1)
crack (1.0.0)
bigdecimal
rexml
crass (1.0.6)
csv (3.3.0)
database_cleaner (2.0.2)
database_cleaner-active_record (>= 2, < 3)
database_cleaner-active_record (2.2.0)
Expand Down Expand Up @@ -144,10 +148,15 @@ GEM
html-attributes-utils (~> 1.0.0, >= 1.0.0)
pagy (>= 6, < 10)
view_component (>= 3.9, < 3.15)
hashdiff (1.1.1)
high_voltage (4.0.0)
html-attributes-utils (1.0.2)
activesupport (>= 6.1.4.4)
htmlbeautifier (1.4.3)
httparty (0.22.0)
csv
mini_mime (>= 1.0.0)
multi_xml (>= 0.5.2)
i18n (1.14.6)
concurrent-ruby (~> 1.0)
io-console (0.7.2)
Expand Down Expand Up @@ -195,6 +204,8 @@ GEM
mini_mime (1.1.5)
minitest (5.25.1)
msgpack (1.7.2)
multi_xml (0.7.1)
bigdecimal (~> 3.1)
net-imap (0.4.16)
date
net-protocol
Expand Down Expand Up @@ -411,6 +422,10 @@ GEM
activemodel (>= 6.0.0)
bindex (>= 0.4.0)
railties (>= 6.0.0)
webmock (3.24.0)
addressable (>= 2.8.0)
crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0)
webrick (1.8.2)
websocket (1.2.11)
websocket-driver (0.7.6)
Expand Down Expand Up @@ -444,6 +459,7 @@ DEPENDENCIES
govuk-components
high_voltage
htmlbeautifier
httparty
jbuilder (~> 2.11)
jquery-rails
launchy
Expand All @@ -469,6 +485,7 @@ DEPENDENCIES
turbolinks (~> 5)
tzinfo-data
web-console (>= 3.3.0)
webmock

RUBY VERSION
ruby 3.3.5p100
Expand Down
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ To manage sensitive environment variables:
which should never be checked into Git. This file will look something like
`ROLLBAR_TOKEN=123456789`

### Required 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

## Access

TODO: Where can people find the service and the different environments?
Expand Down
35 changes: 35 additions & 0 deletions app/assets/stylesheets/application.css.scss
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,38 @@ $govuk-fonts-path: "govuk-frontend/dist/govuk/assets/fonts/";
$govuk-page-width: 1100px;

@import "govuk-frontend/dist/govuk/all";

.air-quality-alerts {
list-style-type: none;
margin-bottom: 2rem;
}

.air-quality-alert {
padding-bottom: 1rem;
}

.air-quality-alert .heading {
background-color: white;
padding: 1rem;
}

.air-quality-alert .body {
padding: 1rem;
}

.air-quality-alert .guidance {
padding-left: 1rem;
padding-right: 1rem;
}

.air-quality-alert.alert-level-high {
background-color: #febfca;
}

.air-quality-alert.alert-level-moderate {
background-color: #f4e9b6;
}

.air-quality-alert.alert-level-very-high {
background-color: #dfb4ed;
}
8 changes: 5 additions & 3 deletions app/controllers/forecasts_controller.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
class ForecastsController < ApplicationController
def index
@forecast = JSON.parse(File.read("#{Rails.root}/public/sample-forecast.json"))["zones"][0]
@dates = @forecast["forecasts"].map { |forecast| forecast["forecast_date"] }
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"))
@air_quality_alerts = @forecasts.map(&:alerts).flatten
end
end
40 changes: 40 additions & 0 deletions app/lib/forecast_factory.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
class ForecastFactory
def self.build(forecast_representation)
obtained_at = Time.zone.parse(forecast_representation.fetch("forecastdate"))

zone = forecast_representation
.fetch("zones")
.first

zone.fetch("forecasts")
.map do |forecast|
Forecast.new({
obtained_at: obtained_at,
forecast_for: Date.parse(forecast.fetch("forecast_date")),

zone: ForecastZone.new(
id: zone.fetch("zone_id"),
name: zone.fetch("zone_name"),
type: zone.fetch("zone_type")
),

air_pollution: AirPollutionPrediction.new(
forecasted_at: Time.zone.parse(forecast.fetch("pollution_version").to_s),
nitrogen_dioxide: forecast.fetch("NO2"),
particulate_matter_10: forecast.fetch("PM10"),
particulate_matter_2_5: forecast.fetch("PM2.5"),
ozone: forecast.fetch("O3"),
overall_score: forecast.fetch("total"),
overall_label: forecast.fetch("total_status")
),

uv: UvPrediction.new(forecast.fetch("uv")),
pollen: forecast.fetch("pollen"),
temperature: TemperaturePrediction.new(
min: forecast.fetch("temp_min"),
max: forecast.fetch("temp_max")
)
})
end
end
end
46 changes: 46 additions & 0 deletions app/models/air_pollution_prediction.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
class AirPollutionPrediction
attr_reader :forecasted_at,
:nitrogen_dioxide,
:particulate_matter_10,
:particulate_matter_2_5,
:ozone,
:overall_score

def initialize(
forecasted_at:,
nitrogen_dioxide:,
particulate_matter_10:,
particulate_matter_2_5:,
ozone:,
overall_score:,
overall_label:
)
@forecasted_at = forecasted_at
@nitrogen_dioxide = nitrogen_dioxide
@particulate_matter_10 = particulate_matter_10
@particulate_matter_2_5 = particulate_matter_2_5
@ozone = ozone
@overall_score = overall_score
@overall_label = overall_label
end

def overall_label
ActiveSupport::Inflector.upcase_first(@overall_label.downcase)
end

# :nocov:
def inspect
attr_values = [
"@forecasted_at=#{forecasted_at}",
"@nitrogen_dioxide=#{nitrogen_dioxide}>",
"@particulate_matter_10=#{particulate_matter_10}",
"@particulate_matter_2_5=#{particulate_matter_2_5}",
"@ozone=#{ozone}",
"@overall_score=#{overall_score}",
"@overall_label=#{@overall_label}"
]

"#<#{self.class.name} #{attr_values.join(" ")}>"
end
# :nocov:
end
29 changes: 29 additions & 0 deletions app/models/air_quality_alert.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
class AirQualityAlert
def initialize(forecast)
@forecast = forecast
end

TAG_COLOURS = {
moderate: :yellow,
high: :red,
very_high: :purple
}

def date
@forecast.forecast_for
end

def level
@forecast.air_pollution.overall_label
end

def score
@forecast.air_pollution.overall_score
end

def tag_colour
TAG_COLOURS.fetch(
ActiveSupport::Inflector.parameterize(level, separator: "_").to_sym
)
end
end
22 changes: 22 additions & 0 deletions app/models/cerc_api_client.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
require "httparty"

class CercApiClient
include HTTParty

def self.fetch_data(zone)
base_url = ENV.fetch("CERC_API_HOST_URL")

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

HTTParty.get("#{base_url}/getforecast/all", query: query)
end

def self.forecasts_for(zone)
ForecastFactory.build(fetch_data(zone))
end
end
39 changes: 39 additions & 0 deletions app/models/forecast.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
class Forecast
attr_reader :obtained_at, :forecast_for, :zone, :air_pollution, :uv, :pollen, :temperature

def initialize(attrs)
@obtained_at = attrs.fetch(:obtained_at)
@forecast_for = attrs.fetch(:forecast_for)
@zone = attrs.fetch(:zone)
@air_pollution = attrs.fetch(:air_pollution)
@uv = attrs.fetch(:uv)
@pollen = attrs.fetch(:pollen)
@temperature = attrs.fetch(:temperature)
end

def alerts
[air_quality_alert].compact
end

def air_quality_alert
return if air_pollution.overall_label.downcase == "low"

AirQualityAlert.new(self)
end

# :nocov:
def inspect
attr_values = [
"@obtained_at=#{obtained_at}",
"@forecast_for=#{forecast_for}",
"@zone=#{zone.inspect}",
"@air_pollution=#{air_pollution.inspect}",
"@uv=#{uv.inspect}",
"@pollen=#{pollen}",
"@temperature=#{temperature.inspect}"
]

"#<#{self.class.name} #{attr_values.join(" ")}>"
end
# :nocov:
end
21 changes: 21 additions & 0 deletions app/models/forecast_zone.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
class ForecastZone
attr_reader :id, :name

def initialize(id:, name:, type:)
@id = id
@name = name
@type = type
end

def type
return "London Borough" if @type == 1

"Area"
end

# :nocov:
def inspect
"#<#{self.class.name} @id=#{id} @name=#{name} @type=#{@type}>"
end
# :nocov:
end
14 changes: 14 additions & 0 deletions app/models/temperature_prediction.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
class TemperaturePrediction
attr_reader :min, :max

def initialize(min:, max:)
@min = min
@max = max
end

# :nocov:
def inspect
"#<#{self.class.name} @min=#{min} @max=#{max}>"
end
# :nocov:
end
27 changes: 27 additions & 0 deletions app/models/uv_prediction.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
class UvPrediction
attr_reader :level, :label, :guidance

def initialize(level)
@level = level
case level
when 1..3
@label = "Low"
@guidance = "No action required. You can safely stay outside."
when 4..6
@label = "Moderate"
@guidance = "Protection required. Seek shade during midday hours, cover up and wear suncream."
when 7..9
@label = "High"
@guidance = "some high UV guidance"
else
@label = "Very high"
@guidance = "some very high UV guidance"
end
end

# :nocov:
def inspect
"#<#{self.class.name} @level=#{level} @label=#{label} @guidance=#{guidance}>"
end
# :nocov:
end
Loading