From 6d1eb03858e3de8584cee9f57c0494d87e05cabf Mon Sep 17 00:00:00 2001 From: Rich Birch Date: Wed, 13 Nov 2024 10:49:32 +0000 Subject: [PATCH 1/2] DRTII-1683 Add requested fields to flights response json --- client/package-lock.json | 2 +- .../application/AirportInfoController.scala | 4 ++-- .../application/RedListsController.scala | 4 ++-- ...Country.scala => AirportInfoService.scala} | 4 +++- .../scala/services/api/v1/FlightExport.scala | 24 ++++++++++++------- .../serialisation/FlightApiJsonProtocol.scala | 12 +++++++--- ...lightsWithSplitsExportWithDiversions.scala | 4 ++-- .../services/graphstages/FlightFilter.scala | 4 ++-- .../serialization/JsonSerializationSpec.scala | 4 ++-- ...s.scala => AirportInfoServiceTests$.scala} | 6 ++--- .../services/exports/FlightExportSpec.scala | 17 ++++++++----- 11 files changed, 52 insertions(+), 33 deletions(-) rename server/src/main/scala/services/{AirportToCountry.scala => AirportInfoService.scala} (89%) rename server/src/test/scala/services/{AirportToCountryTests.scala => AirportInfoServiceTests$.scala} (72%) diff --git a/client/package-lock.json b/client/package-lock.json index e8f4ac845b..8e25444245 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -1,5 +1,5 @@ { - "name": "main", + "name": "test", "lockfileVersion": 3, "requires": true, "packages": { diff --git a/server/src/main/scala/controllers/application/AirportInfoController.scala b/server/src/main/scala/controllers/application/AirportInfoController.scala index 6852366a1a..1e5bd5f53d 100644 --- a/server/src/main/scala/controllers/application/AirportInfoController.scala +++ b/server/src/main/scala/controllers/application/AirportInfoController.scala @@ -3,7 +3,7 @@ package controllers.application import com.google.inject.Inject import drt.shared._ import play.api.mvc.{Action, AnyContent, ControllerComponents} -import services.AirportToCountry +import services.AirportInfoService import uk.gov.homeoffice.drt.auth.Roles.ArrivalsAndSplitsView import uk.gov.homeoffice.drt.crunchsystem.DrtSystemInterface import uk.gov.homeoffice.drt.ports.PortCode @@ -19,7 +19,7 @@ class AirportInfoController @Inject()(cc: ControllerComponents, ctrl: DrtSystemI .flatMap(_.headOption) .map(codes => codes .split(",") - .map(code => (PortCode(code), AirportToCountry.airportInfoByIataPortCode.get(code))) + .map(code => (PortCode(code), AirportInfoService.airportInfoByIataPortCode.get(code))) .collect { case (code, Some(info)) => (code, info) } diff --git a/server/src/main/scala/controllers/application/RedListsController.scala b/server/src/main/scala/controllers/application/RedListsController.scala index b88b6f6a78..f7588934c8 100644 --- a/server/src/main/scala/controllers/application/RedListsController.scala +++ b/server/src/main/scala/controllers/application/RedListsController.scala @@ -4,7 +4,7 @@ import akka.pattern.ask import com.google.inject.Inject import drt.shared._ import play.api.mvc.{Action, AnyContent, ControllerComponents} -import services.AirportToCountry +import services.AirportInfoService import spray.json.{DefaultJsonProtocol, JsArray, JsNumber, JsObject, JsString, JsValue, RootJsonFormat, enrichAny} import uk.gov.homeoffice.drt.actor.commands.Commands.GetState import uk.gov.homeoffice.drt.crunchsystem.DrtSystemInterface @@ -21,7 +21,7 @@ class RedListsController@Inject()(cc: ControllerComponents, ctrl: DrtSystemInter Action.async { _ => ctrl.applicationService.redListUpdatesActor.ask(GetState).mapTo[RedListUpdates].map { redListUpdates => val forDate = SDate(dateString, europeLondonTimeZone).millisSinceEpoch - val redListPorts = AirportToCountry.airportInfoByIataPortCode.values.collect { + val redListPorts = AirportInfoService.airportInfoByIataPortCode.values.collect { case AirportInfo(_, _, country, portCode) if redListUpdates.countryCodesByName(forDate).contains(country) => PortCode(portCode) } diff --git a/server/src/main/scala/services/AirportToCountry.scala b/server/src/main/scala/services/AirportInfoService.scala similarity index 89% rename from server/src/main/scala/services/AirportToCountry.scala rename to server/src/main/scala/services/AirportInfoService.scala index 705d387782..4406895910 100644 --- a/server/src/main/scala/services/AirportToCountry.scala +++ b/server/src/main/scala/services/AirportInfoService.scala @@ -8,7 +8,7 @@ import uk.gov.homeoffice.drt.redlist.RedListUpdates import scala.io.Codec import scala.util.Try -object AirportToCountry { +object AirportInfoService { lazy val airportInfoByIataPortCode: Map[String, AirportInfo] = { val bufferedSource = scala.io.Source.fromURL(getClass.getResource("/airports.dat"))(Codec.UTF8) @@ -24,6 +24,8 @@ object AirportToCountry { }.map(ai => (ai.code, ai)).toMap } + def airportInfo(code: PortCode): Option[AirportInfo] = airportInfoByIataPortCode.get(code.iata) + def stripQuotes(row1: String): String = { row1.substring(1, row1.length - 1) } diff --git a/server/src/main/scala/services/api/v1/FlightExport.scala b/server/src/main/scala/services/api/v1/FlightExport.scala index c39afca800..83bc1f6e0c 100644 --- a/server/src/main/scala/services/api/v1/FlightExport.scala +++ b/server/src/main/scala/services/api/v1/FlightExport.scala @@ -1,8 +1,8 @@ package services.api.v1 -import akka.NotUsed import akka.stream.Materializer import akka.stream.scaladsl.{Sink, Source} +import services.AirportInfoService import uk.gov.homeoffice.drt.arrivals.Arrival import uk.gov.homeoffice.drt.ports.Terminals.Terminal import uk.gov.homeoffice.drt.ports.{FeedSource, PortCode} @@ -15,8 +15,11 @@ import scala.util.Try object FlightExport { case class FlightJson(code: String, - originPort: String, + originPortIata: String, + originPortName: String, scheduledTime: Long, + estimatedLandingTime: Option[Long], + actualChocksTime: Option[Long], estimatedPcpStartTime: Option[Long], estimatedPcpEndTime: Option[Long], estimatedPaxCount: Option[Int], @@ -26,13 +29,16 @@ object FlightExport { object FlightJson { def apply(ar: Arrival) (implicit sourceOrderPreference: List[FeedSource]): FlightJson = FlightJson( - ar.flightCodeString, - ar.Origin.iata, - ar.Scheduled, - Try(ar.pcpRange(sourceOrderPreference).min).toOption, - Try(ar.pcpRange(sourceOrderPreference).max).toOption, - ar.bestPcpPaxEstimate(sourceOrderPreference), - ar.Status.description, + code = ar.flightCodeString, + originPortIata = ar.Origin.iata, + originPortName = AirportInfoService.airportInfo(ar.Origin).map(_.airportName).getOrElse("n/a"), + scheduledTime = ar.Scheduled, + estimatedLandingTime = ar.Estimated, + actualChocksTime = ar.ActualChox, + estimatedPcpStartTime = Try(ar.pcpRange(sourceOrderPreference).min).toOption, + estimatedPcpEndTime = Try(ar.pcpRange(sourceOrderPreference).max).toOption, + estimatedPaxCount = ar.bestPcpPaxEstimate(sourceOrderPreference), + status = ar.displayStatus.description, ) } diff --git a/server/src/main/scala/services/api/v1/serialisation/FlightApiJsonProtocol.scala b/server/src/main/scala/services/api/v1/serialisation/FlightApiJsonProtocol.scala index 0277745419..34648e3deb 100644 --- a/server/src/main/scala/services/api/v1/serialisation/FlightApiJsonProtocol.scala +++ b/server/src/main/scala/services/api/v1/serialisation/FlightApiJsonProtocol.scala @@ -12,8 +12,11 @@ trait FlightApiJsonProtocol extends DefaultJsonProtocol { val maybePax = obj.estimatedPaxCount.filter(_ > 0) JsObject( "code" -> obj.code.toJson, - "originPort" -> obj.originPort.toJson, + "originPortIata" -> obj.originPortIata.toJson, + "originPortName" -> obj.originPortName.toJson, "scheduledTime" -> SDate(obj.scheduledTime).toISOString.toJson, + "estimatedLandingTime" -> obj.estimatedLandingTime.map(SDate(_).toISOString).toJson, + "actualChocksTime" -> obj.actualChocksTime.map(SDate(_).toISOString).toJson, "estimatedPcpStartTime" -> maybePax.flatMap(_ => obj.estimatedPcpStartTime.map(SDate(_).toISOString)).toJson, "estimatedPcpEndTime" -> maybePax.flatMap(_ => obj.estimatedPcpEndTime.map(SDate(_).toISOString)).toJson, "estimatedPcpPaxCount" -> obj.estimatedPaxCount.toJson, @@ -24,8 +27,11 @@ trait FlightApiJsonProtocol extends DefaultJsonProtocol { override def read(json: JsValue): FlightJson = json match { case JsObject(fields) => FlightJson( fields.get("code").map(_.convertTo[String]).getOrElse(""), - fields.get("originPort").map(_.convertTo[String]).getOrElse(""), + fields.get("originPortIata").map(_.convertTo[String]).getOrElse(""), + fields.get("originPortName").map(_.convertTo[String]).getOrElse(""), fields.get("scheduledTime").map(_.convertTo[Long]).getOrElse(0L), + fields.get("estimatedLandingTime").map(_.convertTo[Long]), + fields.get("actualChocksTime").map(_.convertTo[Long]), fields.get("estimatedPcpStartTime").map(_.convertTo[Long]), fields.get("estimatedPcpEndTime").map(_.convertTo[Long]), fields.get("estimatedPcpPaxCount").map(_.convertTo[Int]), @@ -35,7 +41,7 @@ trait FlightApiJsonProtocol extends DefaultJsonProtocol { } } - implicit val flightJsonFormat: RootJsonFormat[FlightJson] = jsonFormat7(FlightJson.apply) + implicit val flightJsonFormat: RootJsonFormat[FlightJson] = jsonFormat10(FlightJson.apply) implicit object TerminalJsonFormat extends RootJsonFormat[Terminal] { override def write(obj: Terminal): JsValue = obj.toString.toJson diff --git a/server/src/main/scala/services/exports/flights/templates/LHRFlightsWithSplitsExportWithDiversions.scala b/server/src/main/scala/services/exports/flights/templates/LHRFlightsWithSplitsExportWithDiversions.scala index 75098a3106..56689d389b 100644 --- a/server/src/main/scala/services/exports/flights/templates/LHRFlightsWithSplitsExportWithDiversions.scala +++ b/server/src/main/scala/services/exports/flights/templates/LHRFlightsWithSplitsExportWithDiversions.scala @@ -3,7 +3,7 @@ package services.exports.flights.templates import actors.PartitionedPortStateActor.{FlightsRequest, GetFlightsForTerminals} import drt.shared._ import drt.shared.redlist.{LhrRedListDatesImpl, LhrTerminalTypes} -import services.AirportToCountry +import services.AirportInfoService import uk.gov.homeoffice.drt.arrivals.ApiFlightWithSplits import uk.gov.homeoffice.drt.ports.FeedSource import uk.gov.homeoffice.drt.ports.Terminals._ @@ -26,7 +26,7 @@ trait LHRFlightsWithSplitsExportWithDiversions extends FlightsExport { val redListUpdates: RedListUpdates val directRedListFilter: LhrFlightDisplayFilter = - LhrFlightDisplayFilter(redListUpdates, AirportToCountry.isRedListed, LhrTerminalTypes(LhrRedListDatesImpl)) + LhrFlightDisplayFilter(redListUpdates, AirportInfoService.isRedListed, LhrTerminalTypes(LhrRedListDatesImpl)) override val flightsFilter: (ApiFlightWithSplits, Terminal) => Boolean = directRedListFilter.filterReflectingDivertedRedListFlights diff --git a/server/src/main/scala/services/graphstages/FlightFilter.scala b/server/src/main/scala/services/graphstages/FlightFilter.scala index cf6afb916f..0ba3427f55 100644 --- a/server/src/main/scala/services/graphstages/FlightFilter.scala +++ b/server/src/main/scala/services/graphstages/FlightFilter.scala @@ -1,7 +1,7 @@ package services.graphstages import drt.shared.redlist.{LhrRedListDatesImpl, LhrTerminalTypes} -import services.AirportToCountry +import services.AirportInfoService import uk.gov.homeoffice.drt.arrivals.ApiFlightWithSplits import uk.gov.homeoffice.drt.ports.Terminals.Terminal import uk.gov.homeoffice.drt.ports.{AirportConfig, PortCode} @@ -26,7 +26,7 @@ object FlightFilter { val lhrRedListFilter: FlightFilter = FlightFilter { (fws, redListUpdates) => val isGreenOnlyTerminal = terminalTypes.lhrNonRedListTerminalsForDate(fws.apiFlight.Scheduled).contains(fws.apiFlight.Terminal) - val isRedListOrigin = AirportToCountry.isRedListed(fws.apiFlight.Origin, fws.apiFlight.Scheduled, redListUpdates) + val isRedListOrigin = AirportInfoService.isRedListed(fws.apiFlight.Origin, fws.apiFlight.Scheduled, redListUpdates) val okToProcess = !isRedListOrigin || !isGreenOnlyTerminal okToProcess } diff --git a/server/src/test/scala/serialization/JsonSerializationSpec.scala b/server/src/test/scala/serialization/JsonSerializationSpec.scala index 7674befe4a..205aabe9ae 100644 --- a/server/src/test/scala/serialization/JsonSerializationSpec.scala +++ b/server/src/test/scala/serialization/JsonSerializationSpec.scala @@ -3,7 +3,7 @@ package serialization import drt.shared.CrunchApi._ import drt.shared._ import org.specs2.mutable.Specification -import services.AirportToCountry +import services.AirportInfoService import uk.gov.homeoffice.drt.Nationality import uk.gov.homeoffice.drt.arrivals.SplitStyle.Percentage import uk.gov.homeoffice.drt.arrivals._ @@ -75,7 +75,7 @@ class JsonSerializationSpec extends Specification { } "AirportInfo" >> { - val info: Map[String, AirportInfo] = AirportToCountry.airportInfoByIataPortCode + val info: Map[String, AirportInfo] = AirportInfoService.airportInfoByIataPortCode val asJson: String = write(info) diff --git a/server/src/test/scala/services/AirportToCountryTests.scala b/server/src/test/scala/services/AirportInfoServiceTests$.scala similarity index 72% rename from server/src/test/scala/services/AirportToCountryTests.scala rename to server/src/test/scala/services/AirportInfoServiceTests$.scala index 31421a3a2c..ff7014f194 100644 --- a/server/src/test/scala/services/AirportToCountryTests.scala +++ b/server/src/test/scala/services/AirportInfoServiceTests$.scala @@ -6,9 +6,9 @@ import uk.gov.homeoffice.drt.ports.PortCode import uk.gov.homeoffice.drt.redlist.{RedListUpdate, RedListUpdates} import uk.gov.homeoffice.drt.time.SDate -object AirportToCountryTests extends SpecificationLike { +object AirportInfoServiceTests$ extends SpecificationLike { "can load csv" >> { - val result = AirportToCountry.airportInfoByIataPortCode.get("GKA") + val result = AirportInfoService.airportInfoByIataPortCode.get("GKA") val expected = Some(AirportInfo("Goroka", "Goroka", "Papua New Guinea", "GKA")) result === expected } @@ -17,7 +17,7 @@ object AirportToCountryTests extends SpecificationLike { "AirportToCountry should tell me it's a red list port" >> { val bulawayoAirport = PortCode("BUQ") val updates = RedListUpdates(Map(0L -> RedListUpdate(0L, Map("Zimbabwe" -> "ZWE"), List()))) - AirportToCountry.isRedListed(bulawayoAirport, SDate("2021-08-01T00:00").millisSinceEpoch, updates) === true + AirportInfoService.isRedListed(bulawayoAirport, SDate("2021-08-01T00:00").millisSinceEpoch, updates) === true } } } diff --git a/server/src/test/scala/services/exports/FlightExportSpec.scala b/server/src/test/scala/services/exports/FlightExportSpec.scala index fc65dc5bbc..c155661a03 100644 --- a/server/src/test/scala/services/exports/FlightExportSpec.scala +++ b/server/src/test/scala/services/exports/FlightExportSpec.scala @@ -26,25 +26,30 @@ class FlightExportSpec extends AnyWordSpec with Matchers { "FlightExport" should { "return a PortFlightsJson with the correct structure and only the flight with passengers in the requested time range" in { + val sched1 = SDate("2024-10-15T12:00") + val sched2 = SDate("2024-10-15T13:55") val source = (_: SDateLike, _: SDateLike, _: Terminal) => { Future.successful(Seq( ArrivalGenerator.arrival(iata = "BA0001", schDt = "2024-10-15T11:00", totalPax = Option(100), transPax = Option(10), feedSource = LiveFeedSource), - ArrivalGenerator.arrival(iata = "BA0002", schDt = "2024-10-15T12:00", totalPax = Option(100), transPax = Option(10), feedSource = LiveFeedSource), - ArrivalGenerator.arrival(iata = "BA0003", schDt = "2024-10-15T13:55", totalPax = Option(200), transPax = Option(10), feedSource = LiveFeedSource), + ArrivalGenerator.arrival(iata = "BA0002", schDt = sched1.toISOString, estDt = sched1.addMinutes(1).toISOString, + actChoxDt = sched1.addMinutes(5).toISOString, totalPax = Option(100), transPax = Option(10), feedSource = LiveFeedSource), + ArrivalGenerator.arrival(iata = "BA0003", schDt = sched2.toISOString, totalPax = Option(200), transPax = Option(10), feedSource = LiveFeedSource), ArrivalGenerator.arrival(iata = "BA0004", schDt = "2024-10-15T15:00", totalPax = Option(200), transPax = Option(10), feedSource = LiveFeedSource), )) } val export = FlightExport(source, Seq(T1), PortCode("LHR")) - val sched1 = SDate("2024-10-15T12:00") - val sched2 = SDate("2024-10-15T13:55") Await.result(export(startMinute, endMinute), 1.second) shouldEqual PortFlightsJson( PortCode("LHR"), List(TerminalFlightsJson( T1, List( - FlightJson("BA0002", "JFK", sched1.millisSinceEpoch, Some(sched1.addMinutes(5).millisSinceEpoch), Some(sched1.addMinutes(9).millisSinceEpoch), Some(90), ""), - FlightJson("BA0003", "JFK", sched2.millisSinceEpoch, Some(sched2.addMinutes(5).millisSinceEpoch), Some(sched2.addMinutes(14).millisSinceEpoch), Some(190), ""), + FlightJson("BA0002", "JFK", "John F Kennedy Intl", sched1.millisSinceEpoch, + Option(sched1.addMinutes(1).millisSinceEpoch), Option(sched1.addMinutes(5).millisSinceEpoch), + Some(sched1.addMinutes(5).millisSinceEpoch), Some(sched1.addMinutes(9).millisSinceEpoch), Some(90), "On Chocks"), + FlightJson("BA0003", "JFK", "John F Kennedy Intl", sched2.millisSinceEpoch, + None, None, + Some(sched2.addMinutes(5).millisSinceEpoch), Some(sched2.addMinutes(14).millisSinceEpoch), Some(190), "Scheduled"), ) ) ) From d6bb7bfdad045fa7b2ca8dd720b8d68f3944fdfa Mon Sep 17 00:00:00 2001 From: Rich Birch Date: Wed, 13 Nov 2024 11:03:15 +0000 Subject: [PATCH 2/2] DRTII-1683 Tighten populate end points to super admin access & remove CSRF check --- server/src/main/resources/routes | 3 +++ .../controllers/application/api/v1/FlightsApiController.scala | 4 ++-- .../controllers/application/api/v1/QueuesApiController.scala | 4 ++-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/server/src/main/resources/routes b/server/src/main/resources/routes index 9824be4c34..8f17c6fe8d 100644 --- a/server/src/main/resources/routes +++ b/server/src/main/resources/routes @@ -130,8 +130,11 @@ GET /api/passengers/:startLocalDate/:endLocalDate/:terminalName PUT /api/passengers/:localDate controllers.application.SummariesController.populatePassengersForDate(localDate: String) GET /api/v1/queues controllers.application.api.v1.QueuesApiController.queues() ++ nocsrf PUT /api/v1/queues/:startDateLocal/:endDateLocal controllers.application.api.v1.QueuesApiController.populateQueues(startDateLocal: String, endDateLocal: String) + GET /api/v1/flights controllers.application.api.v1.FlightsApiController.flights() ++ nocsrf PUT /api/v1/flights/:startDateLocal/:endDateLocal controllers.application.api.v1.FlightsApiController.populateFlights(startDateLocal: String, endDateLocal: String) GET /debug/flights/:persistenceId/:dateString/:messages controllers.application.DebugController.getMessagesForFlightPersistenceIdAtTime(persistenceId: String, dateString: String, messages: Int) diff --git a/server/src/main/scala/controllers/application/api/v1/FlightsApiController.scala b/server/src/main/scala/controllers/application/api/v1/FlightsApiController.scala index 6d568e1189..69caf36dde 100644 --- a/server/src/main/scala/controllers/application/api/v1/FlightsApiController.scala +++ b/server/src/main/scala/controllers/application/api/v1/FlightsApiController.scala @@ -11,7 +11,7 @@ import services.api.v1.FlightExport import services.api.v1.serialisation.FlightApiJsonProtocol import spray.json.enrichAny import uk.gov.homeoffice.drt.arrivals.{Arrival, FlightsWithSplits} -import uk.gov.homeoffice.drt.auth.Roles.{ApiFlightAccess, ApiQueueAccess} +import uk.gov.homeoffice.drt.auth.Roles.{ApiFlightAccess, ApiQueueAccess, SuperAdmin} import uk.gov.homeoffice.drt.crunchsystem.DrtSystemInterface import uk.gov.homeoffice.drt.ports.FeedSource import uk.gov.homeoffice.drt.ports.Terminals.Terminal @@ -63,7 +63,7 @@ class FlightsApiController @Inject()(cc: ControllerComponents, ctrl: DrtSystemIn } def populateFlights(start: String, end: String): Action[AnyContent] = - authByRole(ApiQueueAccess) { + authByRole(SuperAdmin) { Action { val startDate = UtcDate.parse(start).getOrElse(throw new Exception("Invalid start date")) val endDate = UtcDate.parse(end).getOrElse(throw new Exception("Invalid end date")) diff --git a/server/src/main/scala/controllers/application/api/v1/QueuesApiController.scala b/server/src/main/scala/controllers/application/api/v1/QueuesApiController.scala index cd357e70f3..60adaeaa8a 100644 --- a/server/src/main/scala/controllers/application/api/v1/QueuesApiController.scala +++ b/server/src/main/scala/controllers/application/api/v1/QueuesApiController.scala @@ -12,7 +12,7 @@ import providers.MinutesProvider import services.api.v1.QueueExport import services.api.v1.serialisation.QueueApiJsonProtocol import spray.json.enrichAny -import uk.gov.homeoffice.drt.auth.Roles.ApiQueueAccess +import uk.gov.homeoffice.drt.auth.Roles.{ApiQueueAccess, SuperAdmin} import uk.gov.homeoffice.drt.crunchsystem.DrtSystemInterface import uk.gov.homeoffice.drt.model.{CrunchMinute, TQM} import uk.gov.homeoffice.drt.ports.Queues @@ -55,7 +55,7 @@ class QueuesApiController @Inject()(cc: ControllerComponents, ctrl: DrtSystemInt } def populateQueues(start: String, end: String): Action[AnyContent] = - authByRole(ApiQueueAccess) { + authByRole(SuperAdmin) { Action { val startDate = UtcDate.parse(start).getOrElse(throw new Exception("Invalid start date")) val endDate = UtcDate.parse(end).getOrElse(throw new Exception("Invalid end date"))