Skip to content

Commit

Permalink
Change editions crosswords query
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
JamieB-gu committed Nov 25, 2024
1 parent fd198dd commit 6820ac0
Show file tree
Hide file tree
Showing 3 changed files with 149 additions and 20 deletions.
50 changes: 35 additions & 15 deletions applications/app/controllers/CrosswordsController.scala
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Original file line number Diff line number Diff line change
@@ -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
}
}

0 comments on commit 6820ac0

Please sign in to comment.