diff --git a/common/app/common/configuration.scala b/common/app/common/configuration.scala
index 59d8e7f32ce..32ea1bc1da5 100644
--- a/common/app/common/configuration.scala
+++ b/common/app/common/configuration.scala
@@ -159,10 +159,6 @@ class GuardianConfiguration extends GuLogging {
lazy val url = configuration.getMandatoryStringProperty("contributionsService.url")
}
- object weather {
- lazy val apiKey = configuration.getStringProperty("weather.api.key")
- }
-
object indexes {
lazy val tagIndexesBucket =
configuration.getMandatoryStringProperty("tag_indexes.bucket")
diff --git a/common/app/conf/switches/FeatureSwitches.scala b/common/app/conf/switches/FeatureSwitches.scala
index 7b3a2d5c49c..659b5bef03e 100644
--- a/common/app/conf/switches/FeatureSwitches.scala
+++ b/common/app/conf/switches/FeatureSwitches.scala
@@ -173,17 +173,6 @@ trait FeatureSwitches {
highImpact = false,
)
- val WeatherSwitch = Switch(
- SwitchGroup.Feature,
- "weather",
- "If this is switched on then the weather component is displayed",
- owners = Seq(Owner.withGithub("johnduffell")),
- safeState = Off,
- sellByDate = never,
- exposeClientSide = true,
- highImpact = false,
- )
-
val HistoryTags = Switch(
SwitchGroup.Feature,
"history-tags",
diff --git a/dev-build/conf/routes b/dev-build/conf/routes
index 8329b037133..19279330f1a 100644
--- a/dev-build/conf/routes
+++ b/dev-build/conf/routes
@@ -54,21 +54,6 @@ OPTIONS /email
# Business data
GET /business-data/stocks.json controllers.StocksController.stocks
-# Weather - EVEN OLDER
-GET /weather/city/:id.json weather.controllers.WeatherController.forCity(id)
-GET /weather/city.json weather.controllers.LocationsController.whatIsMyCity()
-GET /weather/locations weather.controllers.LocationsController.findCity(query: String)
-GET /weather/forecast/:id.json weather.controllers.WeatherController.forecastForCityId(id)
-
-# Weather - OLD
-GET /weatherapi/city/:id.json weather.controllers.WeatherController.forCity(id)
-GET /weatherapi/city.json weather.controllers.LocationsController.whatIsMyCity()
-GET /weatherapi/locations weather.controllers.LocationsController.findCity(query: String)
-GET /weatherapi/forecast/:id.json weather.controllers.WeatherController.forecastForCityId(id)
-
-#Weather - DCR
-GET /weather weather.controllers.WeatherController.theWeather()
-
# Analytics
GET /analytics/abtests controllers.admin.AnalyticsController.abtests()
GET /analytics/confidence controllers.admin.AnalyticsConfidenceController.renderConfidence()
diff --git a/onward/app/AppLoader.scala b/onward/app/AppLoader.scala
index df9509c43f8..032a30eac3a 100644
--- a/onward/app/AppLoader.scala
+++ b/onward/app/AppLoader.scala
@@ -19,7 +19,6 @@ import play.api.routing.Router
import play.api.libs.ws.WSClient
import router.Routes
import services.{OphanApi, PopularInTagService}
-import weather.WeatherApi
import _root_.commercial.targeting.TargetingLifecycle
import scala.concurrent.ExecutionContext
@@ -41,7 +40,6 @@ trait OnwardServices {
lazy val contentApiClient = wire[ContentApiClient]
lazy val ophanApi = wire[OphanApi]
lazy val stocksData = wire[StocksData]
- lazy val weatherApi = wire[WeatherApi]
lazy val geoMostPopularAgent = wire[GeoMostPopularAgent]
lazy val dayMostPopularAgent = wire[DayMostPopularAgent]
lazy val mostPopularAgent = wire[MostPopularAgent]
diff --git a/onward/app/controllers/OnwardControllers.scala b/onward/app/controllers/OnwardControllers.scala
index 4919dc62639..8a7eb0c2e1c 100644
--- a/onward/app/controllers/OnwardControllers.scala
+++ b/onward/app/controllers/OnwardControllers.scala
@@ -2,14 +2,12 @@ package controllers
import org.apache.pekko.actor.{ActorSystem => PekkoActorSystem}
import com.softwaremill.macwire._
-import weather.controllers.{LocationsController, WeatherController}
import business.StocksData
import contentapi.ContentApiClient
import feed._
import model.ApplicationContext
import play.api.libs.ws.WSClient
import play.api.mvc.ControllerComponents
-import weather.WeatherApi
import agents.DeeplyReadAgent
import renderers.DotcomRenderingService
import services.PopularInTagService
@@ -20,7 +18,6 @@ trait OnwardControllers {
def wsClient: WSClient
def contentApiClient: ContentApiClient
def stocksData: StocksData
- def weatherApi: WeatherApi
def geoMostPopularAgent: GeoMostPopularAgent
def dayMostPopularAgent: DayMostPopularAgent
def mostPopularAgent: MostPopularAgent
@@ -36,8 +33,6 @@ trait OnwardControllers {
def popularInTagService: PopularInTagService
lazy val navigationController = wire[NavigationController]
- lazy val weatherController = wire[WeatherController]
- lazy val locationsController = wire[LocationsController]
lazy val mostViewedSocialController = wire[MostViewedSocialController]
lazy val mostPopularController = wire[MostPopularController]
lazy val topStoriesController = wire[TopStoriesController]
diff --git a/onward/app/views/weatherFragments/cityForecast.scala.html b/onward/app/views/weatherFragments/cityForecast.scala.html
deleted file mode 100644
index f2fe95d1b5e..00000000000
--- a/onward/app/views/weatherFragments/cityForecast.scala.html
+++ /dev/null
@@ -1,18 +0,0 @@
-@(forecasts: Seq[weather.models.ForecastResponse])(implicit request: RequestHeader)
-
-@import common.Edition
-
-
-@for((forecast, index) <- forecasts.drop(1).zipWithIndex) {
- @defining(forecast.temperatureForEdition(Edition(request))) { temp =>
-
-
-
-
- @fragments.inlineSvg(s"weather-${forecast.weatherIcon}", "weather", Seq("weather__icon", "js-weather-icon"), Some(s"${forecast.hourString}: ${temp}, ${forecast.weatherText.toLowerCase()}."))
-
- @temp
-
-
- }
-}
diff --git a/onward/app/views/weatherFragments/cityWeather.scala.html b/onward/app/views/weatherFragments/cityWeather.scala.html
deleted file mode 100644
index d6cbcb8ede6..00000000000
--- a/onward/app/views/weatherFragments/cityWeather.scala.html
+++ /dev/null
@@ -1,44 +0,0 @@
-@(weatherResponse: weather.models.accuweather.WeatherResponse)(implicit request: RequestHeader)
-
-@import common.Edition
-
-@defining(weatherResponse.temperatureForEdition(Edition(request))) { temp =>
-
-}
diff --git a/onward/app/weather/WeatherApi.scala b/onward/app/weather/WeatherApi.scala
deleted file mode 100644
index 5bc3a496d11..00000000000
--- a/onward/app/weather/WeatherApi.scala
+++ /dev/null
@@ -1,140 +0,0 @@
-package weather
-
-import java.net.{URI, URLEncoder}
-import java.util.concurrent.TimeoutException
-import org.apache.pekko.actor.{ActorSystem => PekkoActorSystem, Scheduler}
-import common.{GuLogging, ResourcesHelper}
-import conf.Configuration
-import play.api.libs.json.{JsValue, Json}
-import play.api.libs.ws.WSClient
-import weather.geo.LatitudeLongitude
-import weather.models.CityId
-import weather.models.accuweather.{ForecastResponse, LocationResponse, WeatherResponse}
-import model.ApplicationContext
-
-import scala.concurrent.duration._
-import play.api.{MarkerContext, Mode}
-import net.logstash.logback.marker.Markers.append
-import org.apache.pekko.pattern.after
-
-import scala.concurrent.{ExecutionContext, Future}
-import scala.util.control.NonFatal
-
-class WeatherApi(wsClient: WSClient, context: ApplicationContext, pekkoActorSystem: PekkoActorSystem)(implicit
- ec: ExecutionContext,
-) extends ResourcesHelper
- with GuLogging {
-
- // NOTE: If you change the API Key, you must also update the weatherapi fastly configuration, as it is enforced there
- lazy val weatherApiKey: String = Configuration.weather.apiKey.getOrElse(
- throw new RuntimeException("Weather API Key not set"),
- )
-
- val requestTimeout: FiniteDuration = 300.milliseconds
- val requestRetryMax: Int = 3
- val requestRetryDelay: FiniteDuration = 100.milliseconds
-
- val accuWeatherApiUri = "https://weather.guardianapis.com"
-
- private def autocompleteUrl(query: String): String =
- s"$accuWeatherApiUri/locations/v1/cities/autocomplete?apikey=$weatherApiKey&q=${URLEncoder.encode(query, "utf-8")}"
-
- private def cityLookUp(cityId: CityId): String =
- s"$accuWeatherApiUri/currentconditions/v1/${cityId.id}.json?apikey=$weatherApiKey"
-
- private def forecastLookUp(cityId: CityId): String =
- s"$accuWeatherApiUri/forecasts/v1/hourly/24hour/${cityId.id}.json?details=true&apikey=$weatherApiKey"
-
- private def latitudeLongitudeUrl(latitudeLongitude: LatitudeLongitude): String = {
- s"$accuWeatherApiUri/locations/v1/cities/geoposition/search.json?q=$latitudeLongitude&apikey=$weatherApiKey"
- }
-
- private def searchForCityUrl(countryCode: String, city: String): String = {
- s"$accuWeatherApiUri/locations/v1/cities/$countryCode/search.json?q=$city&alias=Always&apikey=$weatherApiKey"
- }
-
- private def getJson(url: String): Future[JsValue] = {
- if (context.environment.mode == Mode.Test) {
- Future(Json.parse(slurpOrDie(new URI(url).getPath.stripPrefix("/"))))
- } else {
- getJsonWithRetry(url)
- }
- }
-
- private def getJsonWithRetry(url: String): Future[JsValue] = {
- val weatherLogsMarkerContext: MarkerContext = MarkerContext(append("weatherRequestPath", url))
- val weatherApiResponse: Future[JsValue] = WeatherApi.retryWeatherRequest(
- () => getJsonRequest(url),
- requestRetryDelay,
- pekkoActorSystem.scheduler,
- requestRetryMax,
- )
- weatherApiResponse.failed.foreach {
- case error: TimeoutException =>
- log.warn(
- s"Request to weather api ($url) timed out (this is expected, especially at 0 and 30 mins past the hour due to" +
- s" a problem with accuweather).",
- error,
- )(weatherLogsMarkerContext)
- case error: Throwable =>
- log.error("Weather API request failed", error)(weatherLogsMarkerContext)
- }
- weatherApiResponse
- }
-
- private def getJsonRequest(url: String): Future[JsValue] = {
- wsClient
- .url(url)
- .withRequestTimeout(requestTimeout)
- .get()
- .map { response =>
- if (response.status == 200) response.json else throw new RuntimeException(s"Weather API response: $response")
- }
- }
-
- def searchForLocations(query: String): Future[Seq[LocationResponse]] =
- getJson(autocompleteUrl(query)).map({ r =>
- Json.fromJson[Seq[LocationResponse]](r).get
- })
-
- def searchForCity(countryCode: String, city: String) =
- getJson(searchForCityUrl(countryCode, city)).map({ r =>
- Json.fromJson[Seq[LocationResponse]](r).get
- })
-
- def getNearestCity(latitudeLongitude: LatitudeLongitude): Future[LocationResponse] =
- getJson(latitudeLongitudeUrl(latitudeLongitude)).map({ r =>
- Json.fromJson[LocationResponse](r).get
- })
-
- def getWeatherForCityId(cityId: CityId): Future[WeatherResponse] =
- getJson(cityLookUp(cityId)).map({ r =>
- Json.fromJson[Seq[WeatherResponse]](r).get.headOption getOrElse {
- throw new RuntimeException(s"Empty weather response for $cityId")
- }
- })
-
- def getForecastForCityId(cityId: CityId): Future[Seq[ForecastResponse]] =
- getJson(forecastLookUp(cityId)).map({ r =>
- Json.fromJson[Seq[ForecastResponse]](r).get
- })
-}
-
-object WeatherApi extends GuLogging {
-
- def retryWeatherRequest(
- request: () => Future[JsValue],
- retryDelay: FiniteDuration,
- scheduler: Scheduler,
- attempts: Int,
- )(implicit ec: ExecutionContext): Future[JsValue] = {
- def loop(attemptsRemaining: Int): Future[JsValue] = {
- request().recoverWith { case NonFatal(error) =>
- if (attemptsRemaining <= 1) Future.failed(error)
- else after(retryDelay, scheduler)(loop(attemptsRemaining - 1))
- }
- }
- loop(attempts)
- }
-
-}
diff --git a/onward/app/weather/controllers/LocationsController.scala b/onward/app/weather/controllers/LocationsController.scala
deleted file mode 100644
index cd317e80607..00000000000
--- a/onward/app/weather/controllers/LocationsController.scala
+++ /dev/null
@@ -1,75 +0,0 @@
-package weather.controllers
-
-import common._
-import model.{CacheTime, Cached}
-import weather.models.CityResponse
-import play.api.mvc.{Action, AnyContent, BaseController, ControllerComponents}
-import weather.WeatherApi
-
-import scala.language.postfixOps
-import scala.concurrent.duration._
-import scala.concurrent.Future
-
-class LocationsController(weatherApi: WeatherApi, val controllerComponents: ControllerComponents)
- extends BaseController
- with ImplicitControllerExecutionContext
- with GuLogging {
-
- def findCity(query: String): Action[AnyContent] =
- Action.async { implicit request =>
- weatherApi.searchForLocations(query) map { locations =>
- Cached(7.days)(JsonComponent.fromWritable(CityResponse.fromLocationResponses(locations.toList)))
- }
- }
-
- val CityHeader: String = "X-GU-GeoCity"
- val RegionHeader: String = "X-GU-GeoRegion"
- val CountryHeader: String = "X-GU-GeoCountry"
-
- def whatIsMyCity(): Action[AnyContent] =
- Action.async { implicit request =>
- def cityFromRequestEdition = CityResponse.fromEdition(Edition(request))
-
- def getEncodedHeader(key: String) =
- request.headers.get(key).map(java.net.URLDecoder.decode(_, "latin1"))
-
- val maybeCountry = getEncodedHeader(CountryHeader).filter(_.nonEmpty)
- val maybeCity = getEncodedHeader(CityHeader).filter(_.nonEmpty)
- val maybeRegion = getEncodedHeader(RegionHeader).filter(_.nonEmpty)
-
- (maybeCountry, maybeCity) match {
- case (Some(countryCode), Some(city)) =>
- weatherApi.searchForCity(countryCode, city) map { locations =>
- val cities = CityResponse.fromLocationResponses(
- locations
- .filter(_.Country.ID == countryCode)
- .sortBy(_.AdministrativeArea.ID match {
- // We want to get cities within a matching region to come up first
- case region if maybeRegion.contains(region) => 0
- case _ => 1
- })
- .toList,
- )
- cities.headOption.fold {
- log.warn(s"Could not find $countryCode, $city, $maybeRegion")
- Cached(CacheTime.NotFound)(JsonNotFound())
- } { weatherCity =>
- log.info(s"Matched $countryCode, $city, $maybeRegion to ${weatherCity.id}")
- // We do this as AccuWeather writes "New York, New York" if no region is specified, where as we
- // just get "New York" from Fastly.
- val weatherCityWithoutRegion = weatherCity.copy(city = city)
- Cached(1 hour)(JsonComponent.fromWritable(weatherCityWithoutRegion))
- }
- }
-
- case (_, _) =>
- Future.successful(
- cityFromRequestEdition.fold {
- Cached(CacheTime.NotFound)(JsonNotFound())
- } { city =>
- Cached(1 hour)(JsonComponent.fromWritable(city))
- },
- )
- }
- }
-}
diff --git a/onward/app/weather/controllers/WeatherController.scala b/onward/app/weather/controllers/WeatherController.scala
deleted file mode 100644
index e9ad4f99edf..00000000000
--- a/onward/app/weather/controllers/WeatherController.scala
+++ /dev/null
@@ -1,110 +0,0 @@
-package weather.controllers
-
-import common.JsonComponent.resultFor
-import common.Seqs.RichSeq
-import common.{GuLogging, ImplicitControllerExecutionContext, JsonComponent, JsonNotFound}
-import model.{CacheTime, Cached}
-import play.api.libs.json.Json.{stringify, toJson}
-import play.api.mvc._
-import weather.WeatherApi
-import weather.models.{CityId, CityResponse, Weather, WeatherResponse}
-
-import scala.concurrent.Future
-import scala.concurrent.duration._
-
-class WeatherController(weatherApi: WeatherApi, val controllerComponents: ControllerComponents)
- extends BaseController
- with ImplicitControllerExecutionContext
- with GuLogging {
-
- def theWeather(): Action[AnyContent] =
- Action.async { implicit request =>
- val (country, city, region) = readLocationHeaders
-
- val weatherFuture: Future[Result] = for {
- location <- whatIsMyCity(country, city, region)
- currentWeather <- weatherApi.getWeatherForCityId(CityId(location.id))
- forecasts <- weatherApi.getForecastForCityId(CityId(location.id))
- } yield {
- val weather = Weather(
- location = location,
- weather = WeatherResponse.fromAccuweather(currentWeather),
- forecast = forecasts.map(WeatherResponse.fromAccuweather),
- )
- val weatherJson = stringify(toJson(weather))
- Cached(10.minutes)(resultFor(request, weatherJson))
- }
-
- weatherFuture.recover({
- case _: CityNotFoundException =>
- Cached(CacheTime.NotFound)(JsonNotFound())
-
- case error: Throwable =>
- InternalServerError(s"An error occurred: ${error.getMessage}")
- })
- }
-
- def forCity(cityId: String): Action[AnyContent] =
- Action.async { implicit request =>
- weatherApi.getWeatherForCityId(CityId(cityId)).map { weather =>
- Cached(10.minutes)(JsonComponent(views.html.weatherFragments.cityWeather(weather)))
- }
- }
-
- def forecastForCityId(cityId: String): Action[AnyContent] =
- Action.async { implicit request =>
- weatherApi
- .getForecastForCityId(CityId(cityId))
- .map({ forecastDays =>
- val response =
- forecastDays.map(weather.models.ForecastResponse.fromAccuweather).filterByIndex(_ % 3 == 0).take(5)
-
- Cached(10.minutes)(JsonComponent(views.html.weatherFragments.cityForecast(response)))
- })
- }
-
- private def readLocationHeaders(implicit request: Request[AnyContent]) = {
- val CityHeader = "X-GU-GeoCity"
- val RegionHeader = "X-GU-GeoRegion"
- val CountryHeader = "X-GU-GeoCountry"
-
- def getEncodedHeader(key: String)(implicit request: Request[AnyContent]) =
- request.headers.get(key).map(java.net.URLDecoder.decode(_, "latin1"))
-
- val country = getEncodedHeader(CountryHeader)
- val city = getEncodedHeader(CityHeader)
- val region = getEncodedHeader(RegionHeader)
-
- (country, city, region)
- }
-
- private def whatIsMyCity(maybeCountry: Option[String], maybeCity: Option[String], maybeRegion: Option[String])(
- implicit request: Request[AnyContent],
- ): Future[CityResponse] = {
- (maybeCountry, maybeCity) match {
- case (Some(countryCode), Some(city)) if countryCode.nonEmpty && city.nonEmpty =>
- weatherApi.searchForCity(countryCode, city) flatMap { locations =>
- locations
- .filter(_.Country.ID == countryCode)
- .sortBy(_.AdministrativeArea.ID match {
- // We want to get cities within a matching region to come up first
- case region if maybeRegion.contains(region) => 0
- case _ => 1
- })
- .headOption match {
- case Some(location) => Future.successful(CityResponse.fromLocationResponse(location))
- case None =>
- log.warn(
- s"Could not match country [$maybeCountry], " +
- s"city [$maybeCity] and region [$maybeRegion]" +
- s"to a valid location.",
- )
- Future.failed(CityNotFoundException())
- }
- }
- case (_, _) => Future.failed(CityNotFoundException())
- }
- }
-}
-
-case class CityNotFoundException() extends Exception("City not found")
diff --git a/onward/app/weather/geo/LatitudeLongitude.scala b/onward/app/weather/geo/LatitudeLongitude.scala
deleted file mode 100644
index fffda9acebc..00000000000
--- a/onward/app/weather/geo/LatitudeLongitude.scala
+++ /dev/null
@@ -1,5 +0,0 @@
-package weather.geo
-
-case class LatitudeLongitude(latitude: Double, longitude: Double) {
- override def toString: String = s"$latitude,$longitude"
-}
diff --git a/onward/app/weather/models/CityId.scala b/onward/app/weather/models/CityId.scala
deleted file mode 100644
index 871f8f33815..00000000000
--- a/onward/app/weather/models/CityId.scala
+++ /dev/null
@@ -1,5 +0,0 @@
-package weather.models
-
-case class City(name: String) extends AnyVal
-
-case class CityId(id: String) extends AnyVal
diff --git a/onward/app/weather/models/CityResponse.scala b/onward/app/weather/models/CityResponse.scala
deleted file mode 100644
index 7d4baa55de1..00000000000
--- a/onward/app/weather/models/CityResponse.scala
+++ /dev/null
@@ -1,87 +0,0 @@
-package weather.models
-
-import common.Edition
-import common.editions.{Au, Uk, Us}
-import play.api.libs.json.{Json, Reads, Writes}
-import weather.models.accuweather.LocationResponse
-
-object CityResponse {
- implicit val jsonReads: Reads[CityResponse] = Json.reads[CityResponse]
-
- implicit val writes: Writes[CityResponse] = new Writes[CityResponse] {
- def writes(model: CityResponse) = {
- Json.obj(
- "id" -> model.id,
- "city" -> model.city,
- "country" -> model.country,
- )
- }
- }
-
- def fromLocationResponses(locations: List[LocationResponse]): Seq[CityResponse] = {
- def cityAndCountry(location: LocationResponse): (String, String) =
- (location.LocalizedName, location.Country.LocalizedName)
-
- val citiesWithSameNameByCountry = locations.foldLeft(Map.empty[(String, String), Int]) { (accumulation, location) =>
- val key = cityAndCountry(location)
- accumulation + (key -> (accumulation.getOrElse(key, 0) + 1))
- }
-
- locations.map { location =>
- val needsDisambiguating = citiesWithSameNameByCountry.get(cityAndCountry(location)).exists(_ > 1)
-
- val cityName =
- if (needsDisambiguating)
- s"${location.LocalizedName}, ${location.AdministrativeArea.LocalizedName}"
- else
- location.LocalizedName
-
- CityResponse(
- location.Key,
- cityName,
- location.Country.LocalizedName,
- )
- }
- }
-
- def fromLocationResponse(location: LocationResponse): CityResponse = {
- CityResponse(
- location.Key,
- location.LocalizedName,
- location.Country.LocalizedName,
- )
- }
-
- val London = CityResponse(
- id = "328328",
- city = "London",
- country = "England",
- )
-
- val NewYork = CityResponse(
- id = "349727",
- city = "New York",
- country = "US",
- )
-
- val Sydney = CityResponse(
- id = "22889",
- city = "Sydney",
- country = "Australia",
- )
-
- def fromEdition(edition: Edition): Option[CityResponse] = {
- edition match {
- case Uk => Some(London)
- case Us => Some(NewYork)
- case Au => Some(Sydney)
- case _ => None
- }
- }
-}
-
-case class CityResponse(
- id: String,
- city: String,
- country: String,
-)
diff --git a/onward/app/weather/models/ForecastResponse.scala b/onward/app/weather/models/ForecastResponse.scala
deleted file mode 100644
index 3d9477b3d9a..00000000000
--- a/onward/app/weather/models/ForecastResponse.scala
+++ /dev/null
@@ -1,50 +0,0 @@
-package weather.models
-
-import common.Edition
-import common.editions.Us
-import org.joda.time.DateTime
-import org.joda.time.format.ISODateTimeFormat
-
-import scala.util.Try
-
-object ForecastResponse {
-
- def fromAccuweather(forecastResponse: accuweather.ForecastResponse): ForecastResponse = {
- val dateTime = Try(
- ISODateTimeFormat
- .dateTimeNoMillis()
- .withOffsetParsed()
- .parseDateTime(forecastResponse.DateTime),
- ).toOption
- ForecastResponse(
- dateTime,
- forecastResponse.WeatherIcon,
- forecastResponse.IconPhrase,
- forecastResponse.Temperature.Unit match {
- case "C" => Temperatures.fromCelsius(forecastResponse.Temperature.Value)
- case "F" => Temperatures.fromFahrenheit(forecastResponse.Temperature.Value)
- case _ =>
- throw new RuntimeException(
- "Temperature of neither celsius nor fahrenheit from " +
- s"Accuweather! $forecastResponse",
- )
- },
- )
- }
-}
-
-case class ForecastResponse(
- dateTime: Option[DateTime],
- weatherIcon: Int,
- weatherText: String,
- temperature: Temperatures,
-) {
- def temperatureForEdition(edition: Edition): String = {
- edition match {
- case Us => s"${temperature.imperial}°F"
- case _ => s"${temperature.metric}°C"
- }
- }
-
- def hourString: String = dateTime.map(_.toString("HH:00")).getOrElse("")
-}
diff --git a/onward/app/weather/models/Temperatures.scala b/onward/app/weather/models/Temperatures.scala
deleted file mode 100644
index 71a98a02266..00000000000
--- a/onward/app/weather/models/Temperatures.scala
+++ /dev/null
@@ -1,34 +0,0 @@
-package weather.models
-
-import play.api.libs.json.{JsValue, Json, Reads, Writes}
-
-object Temperatures {
- implicit val jsonReads: Reads[Temperatures] = Json.reads[Temperatures]
-
- implicit val jsonWrites: Writes[Temperatures] = new Writes[Temperatures] {
- override def writes(o: Temperatures): JsValue = {
-
- Json.obj(
- "metric" -> o.metric,
- "imperial" -> o.imperial,
- )
- }
- }
-
- def fromCelsius(celsius: Double): Temperatures =
- Temperatures(
- metric = celsius.round,
- imperial = ((celsius * 9d / 5) + 32).round,
- )
-
- def fromFahrenheit(fahrenheit: Double): Temperatures =
- Temperatures(
- metric = ((fahrenheit - 32) * 5d / 9).round,
- imperial = fahrenheit.round,
- )
-}
-
-case class Temperatures(
- metric: Long,
- imperial: Long,
-)
diff --git a/onward/app/weather/models/Weather.scala b/onward/app/weather/models/Weather.scala
deleted file mode 100644
index a6a5472a0df..00000000000
--- a/onward/app/weather/models/Weather.scala
+++ /dev/null
@@ -1,23 +0,0 @@
-package weather.models
-
-import play.api.libs.json.{Json, Reads, Writes}
-
-object Weather {
- implicit val jsonReads: Reads[Weather] = Json.reads[Weather]
-
- implicit val writes: Writes[Weather] = new Writes[Weather] {
- def writes(model: Weather) = {
- Json.obj(
- "location" -> model.location,
- "weather" -> model.weather,
- "forecast" -> model.forecast,
- )
- }
- }
-}
-
-case class Weather(
- location: CityResponse,
- weather: WeatherResponse,
- forecast: Seq[WeatherResponse],
-)
diff --git a/onward/app/weather/models/WeatherResponse.scala b/onward/app/weather/models/WeatherResponse.scala
deleted file mode 100644
index 28017fa3423..00000000000
--- a/onward/app/weather/models/WeatherResponse.scala
+++ /dev/null
@@ -1,45 +0,0 @@
-package weather.models
-
-import play.api.libs.json.{Json, Reads, Writes}
-
-object WeatherResponse {
- implicit val jsonReads: Reads[WeatherResponse] = Json.reads[WeatherResponse]
-
- implicit val writes: Writes[WeatherResponse] = new Writes[WeatherResponse] {
- def writes(model: WeatherResponse) = {
- Json.obj(
- "description" -> model.weatherText,
- "icon" -> model.weatherIcon,
- "link" -> model.weatherLink,
- "temperature" -> model.temperature,
- "dateTime" -> model.dateTime,
- )
- }
- }
-
- def fromAccuweather(weatherResponse: accuweather.WeatherResponse): WeatherResponse =
- WeatherResponse(
- weatherText = weatherResponse.WeatherText,
- weatherIcon = weatherResponse.WeatherIcon,
- weatherLink = Some(weatherResponse.Link),
- temperature = Temperatures.fromCelsius(weatherResponse.Temperature("Metric").Value),
- dateTime = None,
- )
-
- def fromAccuweather(forecastResponse: accuweather.ForecastResponse): WeatherResponse =
- WeatherResponse(
- weatherText = forecastResponse.IconPhrase,
- weatherIcon = forecastResponse.WeatherIcon,
- weatherLink = None,
- temperature = Temperatures.fromFahrenheit(forecastResponse.Temperature.Value),
- dateTime = Some(forecastResponse.DateTime),
- )
-}
-
-case class WeatherResponse(
- weatherText: String,
- weatherIcon: Int,
- weatherLink: Option[String],
- temperature: Temperatures,
- dateTime: Option[String],
-)
diff --git a/onward/app/weather/models/accuweather/ForecastResponse.scala b/onward/app/weather/models/accuweather/ForecastResponse.scala
deleted file mode 100644
index e540cbef1b7..00000000000
--- a/onward/app/weather/models/accuweather/ForecastResponse.scala
+++ /dev/null
@@ -1,25 +0,0 @@
-package weather.models.accuweather
-
-import play.api.libs.json.{Json, OFormat}
-
-/** Not all the fields AccuWeather provides, but the ones we want */
-
-object Temperature {
- implicit val jsonFormat: OFormat[Temperature] = Json.format[Temperature]
-}
-
-case class Temperature(
- Value: Double,
- Unit: String,
-)
-
-object ForecastResponse {
- implicit val jsonFormat: OFormat[ForecastResponse] = Json.format[ForecastResponse]
-}
-
-case class ForecastResponse(
- DateTime: String,
- WeatherIcon: Int,
- IconPhrase: String,
- Temperature: Temperature,
-)
diff --git a/onward/app/weather/models/accuweather/LocationResponse.scala b/onward/app/weather/models/accuweather/LocationResponse.scala
deleted file mode 100644
index 630c6a0ba6e..00000000000
--- a/onward/app/weather/models/accuweather/LocationResponse.scala
+++ /dev/null
@@ -1,26 +0,0 @@
-package weather.models.accuweather
-
-import play.api.libs.json.{Json, Reads}
-
-/* Not all the fields the AccuWeather API returns, but the ones we care about */
-
-object LocationName {
- implicit val jsonReads: Reads[LocationName] = Json.reads[LocationName]
-}
-
-case class LocationName(
- ID: String,
- LocalizedName: String,
-)
-
-object LocationResponse {
- implicit val jsonReads: Reads[LocationResponse] = Json.reads[LocationResponse]
-}
-
-case class LocationResponse(
- Key: String,
- LocalizedName: String,
- Country: LocationName,
- AdministrativeArea: LocationName,
- Type: String,
-)
diff --git a/onward/app/weather/models/accuweather/WeatherResponse.scala b/onward/app/weather/models/accuweather/WeatherResponse.scala
deleted file mode 100644
index a642e0cdb8d..00000000000
--- a/onward/app/weather/models/accuweather/WeatherResponse.scala
+++ /dev/null
@@ -1,25 +0,0 @@
-package weather.models.accuweather
-
-import common.Edition
-import common.editions.Us
-import model.dotcomrendering.{DotcomRenderingDataModel, ElementsEnhancer}
-import play.api.libs.json.{Json, Reads, Writes}
-
-/** Not all the fields AccuWeather supplies, just the ones we care about */
-
-object WeatherResponse {
- implicit val jsonReads: Reads[WeatherResponse] = Json.reads[WeatherResponse]
-}
-
-case class WeatherResponse(
- WeatherText: String,
- WeatherIcon: Int,
- Link: String,
- Temperature: Map[String, Temperature],
-) {
- def temperatureForEdition(edition: Edition): Temperature =
- edition match {
- case Us => Temperature("Imperial")
- case _ => Temperature("Metric")
- }
-}
diff --git a/onward/conf/routes b/onward/conf/routes
index 51ab64bab1c..423b7f2d538 100644
--- a/onward/conf/routes
+++ b/onward/conf/routes
@@ -7,22 +7,6 @@ GET /assets/*path dev.DevAssetsController.at
GET /_healthcheck controllers.HealthCheck.healthCheck()
-# Onward public endpoints
-
-# Weather
-GET /weather/city/:id.json weather.controllers.WeatherController.forCity(id)
-GET /weather/city.json weather.controllers.LocationsController.whatIsMyCity()
-GET /weather/locations weather.controllers.LocationsController.findCity(query: String)
-GET /weather/forecast/:id.json weather.controllers.WeatherController.forecastForCityId(id)
-
-GET /weatherapi/city/:id.json weather.controllers.WeatherController.forCity(id)
-GET /weatherapi/city.json weather.controllers.LocationsController.whatIsMyCity()
-GET /weatherapi/locations weather.controllers.LocationsController.findCity(query: String)
-GET /weatherapi/forecast/:id.json weather.controllers.WeatherController.forecastForCityId(id)
-
-# Weather - DCR
-GET /weather.json weather.controllers.WeatherController.theWeather()
-
# Most Read
GET /most-read-facebook.json controllers.MostViewedSocialController.renderMostViewed(socialContext: String = "facebook")
GET /most-read-twitter.json controllers.MostViewedSocialController.renderMostViewed(socialContext: String = "twitter")
diff --git a/onward/test/weather/WeatherApiTest.scala b/onward/test/weather/WeatherApiTest.scala
deleted file mode 100644
index 24f9b8d544f..00000000000
--- a/onward/test/weather/WeatherApiTest.scala
+++ /dev/null
@@ -1,29 +0,0 @@
-package weather
-
-import org.apache.pekko.actor.{ActorSystem => PekkoActorSystem}
-import org.scalatest.concurrent.ScalaFutures
-import play.api.libs.json.{JsString, JsValue}
-import org.mockito.Mockito._
-import org.scalatest.flatspec.AnyFlatSpec
-import org.scalatest.matchers.should.Matchers
-import org.scalatestplus.mockito.MockitoSugar
-
-import scala.concurrent.ExecutionContext.Implicits.global
-import scala.concurrent.duration._
-import scala.concurrent.Future
-import scala.language.postfixOps
-
-class WeatherApiTest extends AnyFlatSpec with ScalaFutures with Matchers with MockitoSugar {
- val actorSystem = PekkoActorSystem()
-
- "retryWeatherRequest" should "return for a successful future" in {
- val jsValue = JsString("Test")
-
- val funMock = mock[() => Future[JsValue]]
- when(funMock.apply()) thenReturn Future.successful(jsValue)
-
- whenReady(WeatherApi.retryWeatherRequest(funMock, 100 milli, actorSystem.scheduler, 5))(_ shouldBe jsValue)
- verify(funMock).apply()
- }
-
-}
diff --git a/preview/conf/routes b/preview/conf/routes
index 8b238b70dc9..bf1265d279c 100644
--- a/preview/conf/routes
+++ b/preview/conf/routes
@@ -184,16 +184,6 @@ POST /email/footer
POST /email controllers.EmailSignupController.submit()
OPTIONS /email controllers.EmailSignupController.options()
-# Weather - OLD
-GET /weather/city/:id.json weather.controllers.WeatherController.forCity(id)
-GET /weather/city.json weather.controllers.LocationsController.whatIsMyCity()
-GET /weather/locations weather.controllers.LocationsController.findCity(query: String)
-
-# Weather
-GET /weatherapi/city/:id.json weather.controllers.WeatherController.forCity(id)
-GET /weatherapi/city.json weather.controllers.LocationsController.whatIsMyCity()
-GET /weatherapi/locations weather.controllers.LocationsController.findCity(query: String)
-
# Articles
GET /$path<[^/]+/([^/]+/)?live/.*>.json controllers.LiveBlogController.renderJson(path, page: Option[String], lastUpdate: Option[String], rendered: Option[Boolean], isLivePage: Option[Boolean], filterKeyEvents: Option[Boolean])
GET /$path<[^/]+/([^/]+/)?live/.*>/email controllers.LiveBlogController.renderEmail(path)
diff --git a/static/src/javascripts/bootstraps/enhanced/facia.js b/static/src/javascripts/bootstraps/enhanced/facia.js
index 30deb13cc46..d5eef86abbd 100644
--- a/static/src/javascripts/bootstraps/enhanced/facia.js
+++ b/static/src/javascripts/bootstraps/enhanced/facia.js
@@ -13,7 +13,6 @@ import { lazyLoadContainers } from 'facia/modules/ui/lazy-load-containers';
import { showUpdatesFromLiveBlog } from 'facia/modules/ui/live-blog-updates';
import { init as initSnaps } from 'facia/modules/ui/snaps';
import { init as initRegionSelector } from 'facia/modules/ui/au-region-selector'
-import { Weather } from 'facia/modules/onwards/weather';
import partial from 'lodash/partial';
import { videoContainerInit } from 'common/modules/video/video-container';
import { addContributionsBanner } from 'journalism/modules/audio-series-add-contributions';
@@ -60,14 +59,6 @@ const upgradeMostPopularToGeo = () => {
}
};
-const showWeather = () => {
- if (config.get('switches.weather')) {
- mediator.on('page:front:ready', () => {
- Weather.init();
- });
- }
-};
-
const showLiveblogUpdates = () => {
if (
isBreakpoint({
@@ -147,7 +138,6 @@ const init = () => {
['f-geo-most-popular', upgradeMostPopularToGeo],
['f-lazy-load-containers', lazyLoadContainers],
['f-stocks', stocks],
- ['f-weather', showWeather],
['f-live-blog-updates', showLiveblogUpdates],
['f-video-playlists', upgradeVideoPlaylists],
['f-audio-flagship-contributions', addContributionBannerToAudioSeries],
diff --git a/static/src/javascripts/projects/facia/modules/onwards/weather.js b/static/src/javascripts/projects/facia/modules/onwards/weather.js
deleted file mode 100644
index e8f35d8f8b0..00000000000
--- a/static/src/javascripts/projects/facia/modules/onwards/weather.js
+++ /dev/null
@@ -1,195 +0,0 @@
-/**
- "WEATHER"
-
- Whether the weather be fine,
- Or whether the weather be not,
- Whether the weather be cold,
- Or whether the weather be hot,
- We'll weather the weather
- Whatever the weather,
- Whether we like it or not!
-
- Author: Anonymous British
- */
-
-import bean from 'bean';
-import { reportError } from 'lib/report-error';
-import $ from 'lib/$';
-import config from 'lib/config';
-import { fetchJson } from 'lib/fetch-json';
-import { mediator } from 'lib/mediator';
-import userPrefs from 'common/modules/user-prefs';
-import { SearchTool } from 'facia/modules/onwards/search-tool';
-
-let $holder = null;
-let searchTool = null;
-let eventsBound = false;
-const prefName = 'weather-location';
-
-
-
-const isNetworkFront = () =>
- ['uk', 'us', 'au', 'international'].includes(config.get('page.pageId'));
-
-export const Weather = {
- init() {
- if (!config.get('switches.weather', false) || !isNetworkFront()) {
- return false;
- }
-
- this.getDefaultLocation();
- },
-
- /**
- * Check if user has data in local storage.
- * If yes return data from local storage else return default location data.
- */
- getUserLocation() {
- const prefs = userPrefs.get(prefName);
-
- if (prefs && prefs.id) {
- return prefs;
- }
- },
-
- getWeatherData(url) {
- return fetchJson(url, {
- mode: 'cors',
- });
- },
-
- /**
- * Save user location into localStorage
- */
- saveUserLocation(location) {
- userPrefs.set(prefName, {
- id: location.id,
- city: location.city,
- });
- },
-
- getDefaultLocation() {
- const location = this.getUserLocation();
-
- if (location) {
- return this.fetchWeatherData(location);
- }
- return this.getWeatherData(`${config.get('page.weatherapiurl')}.json`)
- .then(response => {
- this.fetchWeatherData(response);
- })
- .catch(err => {
- reportError(err, {
- feature: 'weather',
- });
- });
- },
-
- fetchWeatherData(location) {
- const weatherApiBase = config.get('page.weatherapiurl');
- const edition = config.get('page.edition');
- if (!location) return;
- return this.getWeatherData(
- `${weatherApiBase}/${
- location.id
- }.json?_edition=${edition.toLowerCase()}`
- )
- .then(response => {
- this.render(response, location.city);
- this.fetchForecastData(location);
- })
- .catch(err => {
- reportError(err, {
- feature: 'weather',
- });
- });
- },
-
- clearLocation() {
- userPrefs.remove(prefName);
- if (searchTool !== null) {
- searchTool.setInputValue();
- }
- },
-
- fetchForecastData(location) {
- return this.getWeatherData(
- `${config.get('page.forecastsapiurl')}/${
- location.id
- }.json?_edition=${config.get('page.edition').toLowerCase()}`
- )
- .then(response => {
- this.renderForecast(response);
- })
- .catch(err => {
- reportError(err, {
- feature: 'weather',
- });
- });
- },
-
- saveDeleteLocalStorage(response) {
- if (response.store === 'set') {
- // After user interaction we want to store the location in localStorage
- this.saveUserLocation(response);
- this.fetchWeatherData(response).then(() => this.toggleForecast());
- } else if (response.store === 'remove') {
- // After user sent empty data we want to remove location and get the default location
- this.clearLocation();
- this.getDefaultLocation();
- }
- },
-
- bindEvents() {
- bean.on(document.body, 'click', '.js-toggle-forecast', e => {
- e.preventDefault();
- this.toggleForecast();
- });
-
- mediator.on(
- 'autocomplete:fetch',
- this.saveDeleteLocalStorage.bind(this)
- );
- },
-
- toggleForecast() {
- $('.weather').toggleClass('is-expanded');
- },
-
- addSearch() {
- searchTool = new SearchTool({
- container: $('.js-search-tool'),
- apiUrl: config.get('page.locationapiurl'),
- });
- },
-
- render(weatherData, city) {
- if (!weatherData) return;
- this.attachToDOM(weatherData.html, city);
-
- if (!eventsBound) {
- this.bindEvents();
- eventsBound = true;
- }
-
- if (searchTool === null) {
- this.addSearch();
- } else {
- searchTool.bindElements($('.js-search-tool'));
- }
- },
-
- attachToDOM(tmpl, city) {
- $holder = $('#headlines .js-container__header');
- $('.js-weather', $holder).remove();
- $holder.append(tmpl.replace(new RegExp('<%=city%>', 'g'), city));
- },
-
- renderForecast(forecastData) {
- if (!forecastData) return;
- const $forecastHolder = $('.js-weather-forecast');
- const tmpl = forecastData.html;
-
- $forecastHolder.empty().html(tmpl);
- },
-};
diff --git a/static/src/javascripts/projects/facia/modules/onwards/weather.spec.js b/static/src/javascripts/projects/facia/modules/onwards/weather.spec.js
deleted file mode 100644
index 9accbc43042..00000000000
--- a/static/src/javascripts/projects/facia/modules/onwards/weather.spec.js
+++ /dev/null
@@ -1,234 +0,0 @@
-/* eslint-disable guardian-frontend/no-direct-access-config */
-import config from 'lib/config';
-import { fetchJson } from 'lib/fetch-json';
-import userPrefs from 'common/modules/user-prefs';
-import { Weather } from 'facia/modules/onwards/weather';
-
-jest.mock('lib/raven');
-jest.mock('lib/config');
-jest.mock('common/modules/user-prefs');
-jest.mock('lib/fetch-json', () => ({ fetchJson: jest.fn() }));
-
-const fetchJsonMock = (fetchJson);
-
-describe('Weather component', () => {
- beforeEach(() => {
- if (document.body) {
- document.body.innerHTML = `
-
- `;
- }
- fetchJsonMock.mockImplementation(() => Promise.resolve());
- });
- afterEach(() => {
- config.page = null;
- config.switches = {
- weather: true,
- };
- userPrefs.remove('weather-location');
- fetchJsonMock.mockReset();
- });
-
- describe('initialisation', () => {
- it('should be behind a switch', () => {
- config.page = {
- pageId: 'uk',
- edition: 'uk',
- };
- config.switches = {
- weather: false,
- };
-
- expect(Weather.init()).toEqual(false);
-
- config.switches = null;
- expect(Weather.init()).toEqual(false);
-
- config.switches = {
- weather: true,
- };
- expect(Weather.init()).not.toEqual(false);
- });
-
- it('should initialize only if on front page', () => {
- config.page = {
- pageId: '/social',
- };
- expect(Weather.init()).toEqual(false);
-
- config.page.pageId = 'uk';
- expect(Weather.init()).not.toEqual(false);
- });
-
- it('should return false when the page is not network front', () => {
- config.page = {
- pageId: 'uk',
- };
- expect(Weather.init()).not.toEqual(false);
-
- config.page.pageId = 'us';
- expect(Weather.init()).not.toEqual(false);
-
- config.page.pageId = 'au';
- expect(Weather.init()).not.toEqual(false);
-
- config.page.pageId = 'social';
- expect(Weather.init()).toEqual(false);
- });
- });
-
- it('should get location from user prefs', () => {
- const result = {
- id: 'qux',
- city: 'doo',
- };
- expect(typeof Weather.getUserLocation()).toEqual('undefined');
-
- Weather.saveUserLocation(result);
- expect(Weather.getUserLocation()).toEqual(result);
- });
-
- it('should get the default location', () => {
- config.page = {
- weatherapiurl: 'foo',
- edition: 'bar',
- };
-
- fetchJsonMock.mockImplementationOnce(() =>
- Promise.resolve({
- id: 'qux',
- })
- );
-
- return Weather.getDefaultLocation().then(() => {
- expect(fetchJsonMock.mock.calls[0][0]).toEqual('foo.json');
- expect(fetchJsonMock.mock.calls[1][0]).toEqual(
- 'foo/qux.json?_edition=bar'
- );
- });
- });
-
- it('should set data in userprefs and fetchWeatherData if user searches', () => {
- config.page = {
- weatherapiurl: 'foo',
- edition: 'bar',
- };
-
- const cityPreference = {
- store: 'set',
- id: 'qux',
- city: 'doo',
- };
-
- Weather.saveDeleteLocalStorage(cityPreference);
-
- expect(userPrefs.get('weather-location')).toEqual({
- id: 'qux',
- city: 'doo',
- });
- expect(fetchJsonMock.mock.calls[0][0]).toEqual(
- 'foo/qux.json?_edition=bar'
- );
- });
-
- it('should remove data from userprefs and getDefaultLocation if user removes data', () => {
- config.page = {
- weatherapiurl: 'foo',
- edition: 'bar',
- };
-
- const cityPreference = {
- store: 'remove',
- id: 'qux',
- city: 'doo',
- };
-
- Weather.saveDeleteLocalStorage(cityPreference);
-
- expect(userPrefs.get('weather-location')).toBeUndefined();
- expect(fetchJsonMock.mock.calls[0][0]).toEqual('foo.json');
- });
-
- it('should fetch the data', () => {
- config.page = {
- weatherapiurl: 'foo',
- edition: 'bar',
- };
-
- Weather.fetchWeatherData({
- id: 'qux',
- city: 'doo',
- });
- expect(fetchJsonMock.mock.calls[0][0]).toEqual(
- 'foo/qux.json?_edition=bar'
- );
- });
-
- it('should call render after fetching the weather data', () => {
- config.page = {
- weatherapiurl: 'foo',
- edition: 'bar',
- };
-
- fetchJsonMock.mockImplementationOnce(() =>
- Promise.resolve({
- html: `
-
-
-
4°C
-
`,
- })
- );
-
- return Weather.fetchWeatherData({
- id: 'qux',
- city: 'doo',
- }).then(() => {
- if (document.body) {
- expect(document.body.innerHTML).toContain('value="doo"');
- }
- });
- });
-
- it('should fetch the forecast data', () => {
- config.page = {
- forecastsapiurl: 'foo',
- edition: 'bar',
- };
-
- Weather.fetchForecastData({
- id: 'qux',
- city: 'doo',
- });
- expect(fetchJsonMock.mock.calls[0][0]).toEqual(
- 'foo/qux.json?_edition=bar'
- );
- });
-
- it('should call render after fetching the forecast data', () => {
- config.page = {
- forecastsapiurl: 'foo',
- edition: 'bar',
- };
-
- fetchJsonMock.mockImplementationOnce(() =>
- Promise.resolve({
- html: '
',
- })
- );
-
- return Weather.fetchForecastData({
- id: 'qux',
- city: 'doo',
- }).then(() => {
- if (document.body) {
- expect(document.body.innerHTML).toContain(
- '
'
- );
- }
- });
- });
-});