From f0e4739df15c45cc6c1c5e0277daeffa8253956c Mon Sep 17 00:00:00 2001 From: Marjan Kalanaki Date: Fri, 6 Dec 2024 19:17:18 +0000 Subject: [PATCH 1/4] add dcr json endpoint for football/live --- .../controllers/MatchListController.scala | 38 +++++--- sport/app/football/model/LiveScores.scala | 90 +++++++++++++++++++ 2 files changed, 116 insertions(+), 12 deletions(-) create mode 100644 sport/app/football/model/LiveScores.scala diff --git a/sport/app/football/controllers/MatchListController.scala b/sport/app/football/controllers/MatchListController.scala index 38f5f714a50..4ef86441361 100644 --- a/sport/app/football/controllers/MatchListController.scala +++ b/sport/app/football/controllers/MatchListController.scala @@ -2,10 +2,10 @@ package football.controllers import common.{Edition, JsonComponent} import feed.Competitions -import football.model.MatchesList +import football.model.{CompetitionMatches, DateCompetitionMatches, MatchesList, LiveScores} import implicits.Requests import model.Cached.RevalidatableResult -import model.{ApplicationContext, Cached, Competition, TeamMap} +import model.{ApplicationContext, CacheTime, Cached, Competition, TeamMap} import java.time.LocalDate import pa.FootballTeam @@ -28,17 +28,31 @@ trait MatchListController extends BaseController with Requests { filters: Map[String, Seq[CompetitionFilter]], atom: Option[InteractiveAtom] = None, )(implicit request: RequestHeader, context: ApplicationContext) = { - Cached(10) { - if (request.isJson) - JsonComponent( - "html" -> football.views.html.matchList.matchesComponent(matchesList), - "next" -> Html(matchesList.nextPage.getOrElse("")), - "previous" -> Html(matchesList.previousPage.getOrElse("")), - "atom" -> atom.isDefined, - ) - else - RevalidatableResult.Ok(football.views.html.matchList.matchesPage(page, matchesList, filters, atom)) + + if (request.isJson && request.forceDCR) { + val model = LiveScores( + pageTitle = matchesList.getPageTitle(Edition(request)), + pageType = matchesList.pageType, + matchesGroupedByDateAndCompetition = matchesList.matchesGroupedByDateAndCompetition.map { item => + DateCompetitionMatches(date = item._1, competitions = item._2.map(a => CompetitionMatches(a._1, a._2))) + }, + nextPage = None, + ) + Cached(CacheTime.Default)(RevalidatableResult.Ok(LiveScores.toJson(model))).as("application/json") + } else { + Cached(10) { + if (request.isJson) + JsonComponent( + "html" -> football.views.html.matchList.matchesComponent(matchesList), + "next" -> Html(matchesList.nextPage.getOrElse("")), + "previous" -> Html(matchesList.previousPage.getOrElse("")), + "atom" -> atom.isDefined, + ) + else + RevalidatableResult.Ok(football.views.html.matchList.matchesPage(page, matchesList, filters, atom)) + } } + } protected def renderMoreMatches( diff --git a/sport/app/football/model/LiveScores.scala b/sport/app/football/model/LiveScores.scala new file mode 100644 index 00000000000..38f344366b9 --- /dev/null +++ b/sport/app/football/model/LiveScores.scala @@ -0,0 +1,90 @@ +package football.model + +import model.Competition +import model.dotcomrendering.DotcomRenderingUtils.withoutNull +import pa.{ + Fixture, + FootballMatch, + LeagueStats, + LeagueTableEntry, + LeagueTeam, + LiveMatch, + MatchDay, + MatchDayTeam, + Official, + Result, + Round, + Stage, + Venue, + Competition => PaCompetition, +} +import play.api.libs.json.{JsObject, JsString, Json, Writes} + +import java.time.LocalDate +import java.time.format.DateTimeFormatter + +case class CompetitionMatches(competition: Competition, matches: List[FootballMatch]) +case class DateCompetitionMatches(date: LocalDate, competitions: List[CompetitionMatches]) + +case class LiveScores( + pageTitle: String, + pageType: String, + matchesGroupedByDateAndCompetition: Seq[DateCompetitionMatches], + nextPage: Option[String], +) + +object LiveScores { + implicit val localDateWrites: Writes[LocalDate] = Writes[LocalDate] { date => + JsString(date.format(DateTimeFormatter.ISO_LOCAL_DATE)) + } + + implicit val stageFormat: Writes[Stage] = Json.writes[Stage] + implicit val roundFormat: Writes[Round] = Json.writes[Round] + implicit val matchDayTeamFormat: Writes[MatchDayTeam] = Json.writes[MatchDayTeam] + implicit val venueFormat: Writes[Venue] = Json.writes[Venue] + implicit val paCompetitionFormat: Writes[PaCompetition] = Json.writes[PaCompetition] + implicit val officialFormat: Writes[Official] = Json.writes[Official] + + // Writes for Fixture with a type discriminator + implicit val fixtureWrites: Writes[Fixture] = Writes { fixture => + Json.writes[Fixture].writes(fixture).as[JsObject] + ("type" -> JsString("Fixture")) + } + + // Writes for MatchDay with a type discriminator + implicit val matchDayWrites: Writes[MatchDay] = Writes { matchDay => + Json.writes[MatchDay].writes(matchDay).as[JsObject] + ("type" -> JsString("MatchDay")) + } + + // Writes for Result with a type discriminator + implicit val resultWrites: Writes[Result] = Writes { result => + Json.writes[Result].writes(result).as[JsObject] + ("type" -> JsString("Result")) + } + + // Writes for LiveMatch with a type discriminator + implicit val liveMatchWrites: Writes[LiveMatch] = Writes { liveMatch => + Json.writes[LiveMatch].writes(liveMatch).as[JsObject] + ("type" -> JsString("LiveMatch")) + } + + implicit val footballMatchWrites: Writes[FootballMatch] = Writes { matchInstance => + matchInstance match { + case f: Fixture => Json.toJson(f)(fixtureWrites) + case m: MatchDay => Json.toJson(m)(matchDayWrites) + case r: Result => Json.toJson(r)(resultWrites) + case l: LiveMatch => Json.toJson(l)(liveMatchWrites) + } + } + + implicit val leagueStatsWrites: Writes[LeagueStats] = Json.writes[LeagueStats] + implicit val leagueTeamWrites: Writes[LeagueTeam] = Json.writes[LeagueTeam] + implicit val leagueTableEntryWrites: Writes[LeagueTableEntry] = Json.writes[LeagueTableEntry] + + implicit val competitionFormat: Writes[Competition] = Json.writes[Competition] + implicit val competitionMatchesFormat: Writes[CompetitionMatches] = Json.writes[CompetitionMatches] + implicit val dateCompetitionMatchesFormat: Writes[DateCompetitionMatches] = Json.writes[DateCompetitionMatches] + implicit val SportsFormat: Writes[LiveScores] = Json.writes[LiveScores] + + def toJson(model: LiveScores): String = { + val jsValue = Json.toJson(model) + Json.stringify(withoutNull(jsValue)) + } +} From 23e9b5960b6183206ba95c908978bb409f4e19e1 Mon Sep 17 00:00:00 2001 From: Marjan Kalanaki Date: Tue, 17 Dec 2024 12:37:09 +0000 Subject: [PATCH 2/4] refactor the data model and summerise the competion in response --- .../controllers/MatchListController.scala | 45 ++++++++--------- ...=> DotcomRenderingFootballDataModel.scala} | 50 ++++++++++++++----- sport/app/football/model/model.scala | 10 +++- 3 files changed, 67 insertions(+), 38 deletions(-) rename sport/app/football/model/{LiveScores.scala => DotcomRenderingFootballDataModel.scala} (61%) diff --git a/sport/app/football/controllers/MatchListController.scala b/sport/app/football/controllers/MatchListController.scala index 4ef86441361..a4e1eeb76f5 100644 --- a/sport/app/football/controllers/MatchListController.scala +++ b/sport/app/football/controllers/MatchListController.scala @@ -2,10 +2,11 @@ package football.controllers import common.{Edition, JsonComponent} import feed.Competitions -import football.model.{CompetitionMatches, DateCompetitionMatches, MatchesList, LiveScores} +import football.model.{DotcomRenderingFootballDataModel, MatchesList} +import football.model.DotcomRenderingFootballDataModelImplicits._ import implicits.Requests import model.Cached.RevalidatableResult -import model.{ApplicationContext, CacheTime, Cached, Competition, TeamMap} +import model.{ApplicationContext, Cached, Competition, TeamMap} import java.time.LocalDate import pa.FootballTeam @@ -28,31 +29,25 @@ trait MatchListController extends BaseController with Requests { filters: Map[String, Seq[CompetitionFilter]], atom: Option[InteractiveAtom] = None, )(implicit request: RequestHeader, context: ApplicationContext) = { + Cached(10) { + if (request.isJson && request.forceDCR) { + val model = DotcomRenderingFootballDataModel( + pageTitle = matchesList.getPageTitle(Edition(request)), + pageType = matchesList.pageType, + matchesList = DotcomRenderingFootballDataModel.getMatchesList(matchesList.matchesGroupedByDateAndCompetition), + ) - if (request.isJson && request.forceDCR) { - val model = LiveScores( - pageTitle = matchesList.getPageTitle(Edition(request)), - pageType = matchesList.pageType, - matchesGroupedByDateAndCompetition = matchesList.matchesGroupedByDateAndCompetition.map { item => - DateCompetitionMatches(date = item._1, competitions = item._2.map(a => CompetitionMatches(a._1, a._2))) - }, - nextPage = None, - ) - Cached(CacheTime.Default)(RevalidatableResult.Ok(LiveScores.toJson(model))).as("application/json") - } else { - Cached(10) { - if (request.isJson) - JsonComponent( - "html" -> football.views.html.matchList.matchesComponent(matchesList), - "next" -> Html(matchesList.nextPage.getOrElse("")), - "previous" -> Html(matchesList.previousPage.getOrElse("")), - "atom" -> atom.isDefined, - ) - else - RevalidatableResult.Ok(football.views.html.matchList.matchesPage(page, matchesList, filters, atom)) - } + JsonComponent.fromWritable(model) + } else if (request.isJson) { + JsonComponent( + "html" -> football.views.html.matchList.matchesComponent(matchesList), + "next" -> Html(matchesList.nextPage.getOrElse("")), + "previous" -> Html(matchesList.previousPage.getOrElse("")), + "atom" -> atom.isDefined, + ) + } else + RevalidatableResult.Ok(football.views.html.matchList.matchesPage(page, matchesList, filters, atom)) } - } protected def renderMoreMatches( diff --git a/sport/app/football/model/LiveScores.scala b/sport/app/football/model/DotcomRenderingFootballDataModel.scala similarity index 61% rename from sport/app/football/model/LiveScores.scala rename to sport/app/football/model/DotcomRenderingFootballDataModel.scala index 38f344366b9..02dcd1f47a6 100644 --- a/sport/app/football/model/LiveScores.scala +++ b/sport/app/football/model/DotcomRenderingFootballDataModel.scala @@ -1,6 +1,6 @@ package football.model -import model.Competition +import model.{Competition, CompetitionSummary} import model.dotcomrendering.DotcomRenderingUtils.withoutNull import pa.{ Fixture, @@ -18,22 +18,39 @@ import pa.{ Venue, Competition => PaCompetition, } -import play.api.libs.json.{JsObject, JsString, Json, Writes} +import play.api.libs.json.{JsObject, JsString, JsValue, Json, Writes} import java.time.LocalDate import java.time.format.DateTimeFormatter -case class CompetitionMatches(competition: Competition, matches: List[FootballMatch]) -case class DateCompetitionMatches(date: LocalDate, competitions: List[CompetitionMatches]) +case class CompetitionMatches(competitionSummary: CompetitionSummary, matches: List[FootballMatch]) +case class MatchesByDateAndCompetition(date: LocalDate, competitionMatches: List[CompetitionMatches]) -case class LiveScores( +case class DotcomRenderingFootballDataModel( pageTitle: String, pageType: String, - matchesGroupedByDateAndCompetition: Seq[DateCompetitionMatches], - nextPage: Option[String], + matchesList: Seq[MatchesByDateAndCompetition], ) -object LiveScores { +object DotcomRenderingFootballDataModel { + def getMatchesList( + matches: Seq[(LocalDate, List[(Competition, List[FootballMatch])])], + ): Seq[MatchesByDateAndCompetition] = { + matches.map { case (date, competitionMatches) => + MatchesByDateAndCompetition( + date = date, + competitionMatches = competitionMatches.map { case (competition, matches) => + CompetitionMatches( + competitionSummary = competition, + matches = matches, + ) + }, + ) + } + } +} + +object DotcomRenderingFootballDataModelImplicits { implicit val localDateWrites: Writes[LocalDate] = Writes[LocalDate] { date => JsString(date.format(DateTimeFormatter.ISO_LOCAL_DATE)) } @@ -78,12 +95,21 @@ object LiveScores { implicit val leagueTeamWrites: Writes[LeagueTeam] = Json.writes[LeagueTeam] implicit val leagueTableEntryWrites: Writes[LeagueTableEntry] = Json.writes[LeagueTableEntry] - implicit val competitionFormat: Writes[Competition] = Json.writes[Competition] + implicit val competitionFormat: Writes[CompetitionSummary] = new Writes[CompetitionSummary] { + def writes(competition: CompetitionSummary): JsValue = Json.obj( + "id" -> competition.id, + "url" -> competition.url, + "fullName" -> competition.fullName, + "shortName" -> competition.shortName, + ) + } implicit val competitionMatchesFormat: Writes[CompetitionMatches] = Json.writes[CompetitionMatches] - implicit val dateCompetitionMatchesFormat: Writes[DateCompetitionMatches] = Json.writes[DateCompetitionMatches] - implicit val SportsFormat: Writes[LiveScores] = Json.writes[LiveScores] + implicit val dateCompetitionMatchesFormat: Writes[MatchesByDateAndCompetition] = + Json.writes[MatchesByDateAndCompetition] + + implicit val SportsFormat: Writes[DotcomRenderingFootballDataModel] = Json.writes[DotcomRenderingFootballDataModel] - def toJson(model: LiveScores): String = { + def toJson(model: DotcomRenderingFootballDataModel): String = { val jsValue = Json.toJson(model) Json.stringify(withoutNull(jsValue)) } diff --git a/sport/app/football/model/model.scala b/sport/app/football/model/model.scala index f17b20b2471..45057f23544 100644 --- a/sport/app/football/model/model.scala +++ b/sport/app/football/model/model.scala @@ -6,6 +6,13 @@ import pa.MatchDayTeam import java.awt.Color import java.time.LocalDate +trait CompetitionSummary { + def id: String + def url: String + def fullName: String + def shortName: String +} + /** @param tableDividers * divides the league table into zones for promotion/relegation, or for qualification to another competition. Only * add a table divider where the boundaries for progression are clear, e.g do not add a divider in Euro group stages @@ -23,7 +30,8 @@ case class Competition( showInTeamsList: Boolean = false, tableDividers: List[Int] = Nil, finalMatchSVG: Option[String] = None, -) extends implicits.Football { +) extends implicits.Football + with CompetitionSummary { lazy val hasMatches = matches.nonEmpty lazy val hasLiveMatches = matches.exists(_.isLive) From 7998a82987711c34291943873ca20e3e0526119c Mon Sep 17 00:00:00 2001 From: Marjan Kalanaki Date: Fri, 20 Dec 2024 09:31:07 +0000 Subject: [PATCH 3/4] add next page to the dcr football data --- sport/app/football/controllers/MatchListController.scala | 1 + .../football/model/DotcomRenderingFootballDataModel.scala | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/sport/app/football/controllers/MatchListController.scala b/sport/app/football/controllers/MatchListController.scala index a4e1eeb76f5..18c890ac456 100644 --- a/sport/app/football/controllers/MatchListController.scala +++ b/sport/app/football/controllers/MatchListController.scala @@ -35,6 +35,7 @@ trait MatchListController extends BaseController with Requests { pageTitle = matchesList.getPageTitle(Edition(request)), pageType = matchesList.pageType, matchesList = DotcomRenderingFootballDataModel.getMatchesList(matchesList.matchesGroupedByDateAndCompetition), + nextPage = matchesList.nextPage, ) JsonComponent.fromWritable(model) diff --git a/sport/app/football/model/DotcomRenderingFootballDataModel.scala b/sport/app/football/model/DotcomRenderingFootballDataModel.scala index 02dcd1f47a6..eb89738bd5a 100644 --- a/sport/app/football/model/DotcomRenderingFootballDataModel.scala +++ b/sport/app/football/model/DotcomRenderingFootballDataModel.scala @@ -1,7 +1,7 @@ package football.model -import model.{Competition, CompetitionSummary} import model.dotcomrendering.DotcomRenderingUtils.withoutNull +import model.{Competition, CompetitionSummary} import pa.{ Fixture, FootballMatch, @@ -18,7 +18,7 @@ import pa.{ Venue, Competition => PaCompetition, } -import play.api.libs.json.{JsObject, JsString, JsValue, Json, Writes} +import play.api.libs.json._ import java.time.LocalDate import java.time.format.DateTimeFormatter @@ -30,6 +30,7 @@ case class DotcomRenderingFootballDataModel( pageTitle: String, pageType: String, matchesList: Seq[MatchesByDateAndCompetition], + nextPage: Option[String], ) object DotcomRenderingFootballDataModel { From 6a7bd3b5a0428610d0275f4789f0b69d9cf24e3b Mon Sep 17 00:00:00 2001 From: Marjan Kalanaki Date: Fri, 20 Dec 2024 09:56:28 +0000 Subject: [PATCH 4/4] add nation to competition summary model --- sport/app/football/controllers/MatchListController.scala | 5 +++-- .../football/model/DotcomRenderingFootballDataModel.scala | 7 ++++--- sport/app/football/model/model.scala | 1 + 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/sport/app/football/controllers/MatchListController.scala b/sport/app/football/controllers/MatchListController.scala index 18c890ac456..1a22fe9a728 100644 --- a/sport/app/football/controllers/MatchListController.scala +++ b/sport/app/football/controllers/MatchListController.scala @@ -36,17 +36,18 @@ trait MatchListController extends BaseController with Requests { pageType = matchesList.pageType, matchesList = DotcomRenderingFootballDataModel.getMatchesList(matchesList.matchesGroupedByDateAndCompetition), nextPage = matchesList.nextPage, + previousPage = matchesList.previousPage, ) JsonComponent.fromWritable(model) - } else if (request.isJson) { + } else if (request.isJson) JsonComponent( "html" -> football.views.html.matchList.matchesComponent(matchesList), "next" -> Html(matchesList.nextPage.getOrElse("")), "previous" -> Html(matchesList.previousPage.getOrElse("")), "atom" -> atom.isDefined, ) - } else + else RevalidatableResult.Ok(football.views.html.matchList.matchesPage(page, matchesList, filters, atom)) } } diff --git a/sport/app/football/model/DotcomRenderingFootballDataModel.scala b/sport/app/football/model/DotcomRenderingFootballDataModel.scala index eb89738bd5a..38b1f401878 100644 --- a/sport/app/football/model/DotcomRenderingFootballDataModel.scala +++ b/sport/app/football/model/DotcomRenderingFootballDataModel.scala @@ -31,6 +31,7 @@ case class DotcomRenderingFootballDataModel( pageType: String, matchesList: Seq[MatchesByDateAndCompetition], nextPage: Option[String], + previousPage: Option[String], ) object DotcomRenderingFootballDataModel { @@ -96,14 +97,14 @@ object DotcomRenderingFootballDataModelImplicits { implicit val leagueTeamWrites: Writes[LeagueTeam] = Json.writes[LeagueTeam] implicit val leagueTableEntryWrites: Writes[LeagueTableEntry] = Json.writes[LeagueTableEntry] - implicit val competitionFormat: Writes[CompetitionSummary] = new Writes[CompetitionSummary] { - def writes(competition: CompetitionSummary): JsValue = Json.obj( + implicit val competitionFormat: Writes[CompetitionSummary] = (competition: CompetitionSummary) => + Json.obj( "id" -> competition.id, "url" -> competition.url, "fullName" -> competition.fullName, "shortName" -> competition.shortName, + "nation" -> competition.nation, ) - } implicit val competitionMatchesFormat: Writes[CompetitionMatches] = Json.writes[CompetitionMatches] implicit val dateCompetitionMatchesFormat: Writes[MatchesByDateAndCompetition] = Json.writes[MatchesByDateAndCompetition] diff --git a/sport/app/football/model/model.scala b/sport/app/football/model/model.scala index 45057f23544..edd5f909991 100644 --- a/sport/app/football/model/model.scala +++ b/sport/app/football/model/model.scala @@ -11,6 +11,7 @@ trait CompetitionSummary { def url: String def fullName: String def shortName: String + def nation: String } /** @param tableDividers