From 6820ac093e36f7fe83a9079f8da8a572094c75cf Mon Sep 17 00:00:00 2001 From: Jamie B <53781962+JamieB-gu@users.noreply.github.com> Date: Mon, 25 Nov 2024 17:16:45 +0000 Subject: [PATCH] Change editions crosswords query We'd like to send a list of crosswords to DCAR, so this changes the structure of the data model. It also applies some of the solutions logic used in the similar `CrosswordData` model, widens the types of crosswords requested, and switches to a more specific CAPI search query. Note that it uses the CAPI client's `SearchQuery` directly rather than frontend's `contentApiClient.search`, as that includes a number of fields we don't need here. --- .../controllers/CrosswordsController.scala | 50 +++++++--- .../EditionsCrosswordRenderingDataModel.scala | 23 ++++- ...tionsCrosswordRenderingDataModelTest.scala | 96 +++++++++++++++++++ 3 files changed, 149 insertions(+), 20 deletions(-) create mode 100644 common/test/model/dotcomrendering/EditionsCrosswordRenderingDataModelTest.scala diff --git a/applications/app/controllers/CrosswordsController.scala b/applications/app/controllers/CrosswordsController.scala index 48c16256bec..8a7c5b6f904 100644 --- a/applications/app/controllers/CrosswordsController.scala +++ b/applications/app/controllers/CrosswordsController.scala @@ -1,10 +1,16 @@ package controllers -import com.gu.contentapi.client.model.v1.CrosswordType.{Cryptic, Quick} -import com.gu.contentapi.client.model.v1.{Crossword, ItemResponse, Content => ApiContent, Section => ApiSection} +import com.gu.contentapi.client.model.v1.{ + Crossword, + ItemResponse, + SearchResponse, + Content => ApiContent, + Section => ApiSection, +} import common.{Edition, GuLogging, ImplicitControllerExecutionContext} import conf.Static import contentapi.ContentApiClient +import com.gu.contentapi.client.model.SearchQuery import crosswords.{ AccessibleCrosswordPage, AccessibleCrosswordRows, @@ -304,21 +310,35 @@ class CrosswordEditionsController( def digitalEdition: Action[AnyContent] = Action.async { implicit request => getCrosswords .map(parseCrosswords) - .flatMap { - case Some(crosswordPage) => - remoteRenderer.getEditionsCrossword(wsClient, crosswordPage) - case None => Future.successful(NotFound) + .flatMap { crosswords => + remoteRenderer.getEditionsCrossword(wsClient, crosswords) } } - private lazy val crosswordsQuery = contentApiClient.item("crosswords") - - private def getCrosswords: Future[ItemResponse] = contentApiClient.getResponse(crosswordsQuery) + private def getCrosswords: Future[SearchResponse] = + contentApiClient.getResponse(crosswordsQuery) + + /** Search for playable crosswords sorted by print publication date. This will exclude older, originally print-only + * crosswords that happen to have been re-published in a digital format recently. + */ + private lazy val crosswordsQuery = + SearchQuery() + .tag(crosswordTags) + .useDate("newspaper-edition") + .pageSize(25) + + private lazy val crosswordTags = Seq( + "crosswords/series/quick", + "crosswords/series/cryptic", + "crosswords/series/prize", + "crosswords/series/weekend-crossword", + "crosswords/series/quick-cryptic", + "crosswords/series/everyman", + "crosswords/series/speedy", + "crosswords/series/quiptic", + ).mkString("|") + + private def parseCrosswords(response: SearchResponse): EditionsCrosswordRenderingDataModel = + EditionsCrosswordRenderingDataModel(response.results.flatMap(_.crossword)) - private def parseCrosswords(response: ItemResponse): Option[EditionsCrosswordRenderingDataModel] = - for { - results <- response.results - quick <- results.find(_.crossword.exists(_.`type` == Quick)).flatMap(_.crossword) - cryptic <- results.find(_.crossword.exists(_.`type` == Cryptic)).flatMap(_.crossword) - } yield EditionsCrosswordRenderingDataModel(quick, cryptic) } diff --git a/common/app/model/dotcomrendering/pageElements/EditionsCrosswordRenderingDataModel.scala b/common/app/model/dotcomrendering/pageElements/EditionsCrosswordRenderingDataModel.scala index 92cfff9ec8a..180e91803ac 100644 --- a/common/app/model/dotcomrendering/pageElements/EditionsCrosswordRenderingDataModel.scala +++ b/common/app/model/dotcomrendering/pageElements/EditionsCrosswordRenderingDataModel.scala @@ -4,17 +4,30 @@ import com.gu.contentapi.client.model.v1.Crossword import com.gu.contentapi.json.CirceEncoders._ import io.circe.JsonObject import io.circe.syntax._ +import implicits.Dates.CapiRichDateTime case class EditionsCrosswordRenderingDataModel( - quick: Crossword, - cryptic: Crossword, + crosswords: Iterable[Crossword], ) object EditionsCrosswordRenderingDataModel { + def apply(crosswords: Iterable[Crossword]): EditionsCrosswordRenderingDataModel = + new EditionsCrosswordRenderingDataModel(crosswords.map(crossword => { + val shipSolutions = + crossword.dateSolutionAvailable + .map(_.toJoda.isBeforeNow) + .getOrElse(crossword.solutionAvailable) + + if (shipSolutions) { + crossword + } else { + crossword.copy(entries = crossword.entries.map(_.copy(solution = None))) + } + })) + def toJson(model: EditionsCrosswordRenderingDataModel): String = { JsonObject( - "quick" -> model.quick.asJson.dropNullValues, - "cryptic" -> model.cryptic.asJson.dropNullValues, - ).asJson.dropNullValues.noSpaces + "crosswords" -> model.crosswords.asJson.deepDropNullValues, + ).asJson.noSpaces } } diff --git a/common/test/model/dotcomrendering/EditionsCrosswordRenderingDataModelTest.scala b/common/test/model/dotcomrendering/EditionsCrosswordRenderingDataModelTest.scala new file mode 100644 index 00000000000..1b3b97841e6 --- /dev/null +++ b/common/test/model/dotcomrendering/EditionsCrosswordRenderingDataModelTest.scala @@ -0,0 +1,96 @@ +package model.dotcomrendering + +import com.gu.contentapi.client.model.v1.{CapiDateTime, Crossword, CrosswordType, CrosswordDimensions, CrosswordEntry} +import model.dotcomrendering.pageElements.EditionsCrosswordRenderingDataModel +import org.mockito.Mockito.when +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers +import org.scalatestplus.mockito.MockitoSugar +import org.joda.time.DateTime + +class EditionsCrosswordRenderingDataModelTest extends AnyFlatSpec with Matchers with MockitoSugar { + val mockEntry = CrosswordEntry( + id = "mockId", + solution = Some("Mock solution"), + ) + + val mockCrossword = Crossword( + name = "Mock name", + `type` = CrosswordType.Quick, + number = 1, + date = CapiDateTime(DateTime.now().getMillis(), "date"), + dimensions = CrosswordDimensions(1, 1), + entries = Seq(mockEntry, mockEntry), + solutionAvailable = true, + hasNumbers = false, + randomCluesOrdering = false, + ) + + "apply" should "provide solutions when 'dateSolutionAvailable' is in the past" in { + val crossword = mockCrossword.copy( + solutionAvailable = true, + dateSolutionAvailable = Some(CapiDateTime(DateTime.now().minusDays(1).getMillis(), "date")), + ) + + val crosswords = + EditionsCrosswordRenderingDataModel(Seq(crossword, crossword)) + .crosswords + .toSeq + + crosswords(0).entries(0).solution shouldBe Some("Mock solution") + crosswords(0).entries(1).solution shouldBe Some("Mock solution") + crosswords(1).entries(0).solution shouldBe Some("Mock solution") + crosswords(1).entries(1).solution shouldBe Some("Mock solution") + } + + "apply" should "provide solutions when 'dateSolutionAvailable' is 'None' and solutionAvailable is 'true'" in { + val crossword = mockCrossword.copy( + solutionAvailable = true, + dateSolutionAvailable = None, + ) + + val crosswords = + EditionsCrosswordRenderingDataModel(Seq(crossword, crossword)) + .crosswords + .toSeq + + crosswords(0).entries(0).solution shouldBe Some("Mock solution") + crosswords(0).entries(1).solution shouldBe Some("Mock solution") + crosswords(1).entries(0).solution shouldBe Some("Mock solution") + crosswords(1).entries(1).solution shouldBe Some("Mock solution") + } + + "apply" should "not provide solutions when 'dateSolutionAvailable' is in the future" in { + val crossword = mockCrossword.copy( + solutionAvailable = true, + dateSolutionAvailable = Some(CapiDateTime(DateTime.now().plusDays(1).getMillis(), "date")), + ) + + val crosswords = + EditionsCrosswordRenderingDataModel(Seq(crossword, crossword)) + .crosswords + .toSeq + + crosswords(0).entries(0).solution shouldBe None + crosswords(0).entries(1).solution shouldBe None + crosswords(1).entries(0).solution shouldBe None + crosswords(1).entries(1).solution shouldBe None + } + + "apply" should "not provide solutions when 'dateSolutionAvailable' is 'None' and solutionAvailable is 'false'" in { + val crossword = mockCrossword.copy( + solutionAvailable = false, + dateSolutionAvailable = None, + ) + + val crosswords = + EditionsCrosswordRenderingDataModel(Seq(crossword, crossword)) + .crosswords + .toSeq + + crosswords(0).entries(0).solution shouldBe None + crosswords(0).entries(1).solution shouldBe None + crosswords(1).entries(0).solution shouldBe None + crosswords(1).entries(1).solution shouldBe None + } +}