diff --git a/applications/app/controllers/CrosswordsController.scala b/applications/app/controllers/CrosswordsController.scala index b2561b234f34..f99846afaf03 100644 --- a/applications/app/controllers/CrosswordsController.scala +++ b/applications/app/controllers/CrosswordsController.scala @@ -1,13 +1,14 @@ package controllers import com.gu.contentapi.client.model.v1.{Crossword, ItemResponse, Content => ApiContent, Section => ApiSection} -import common.{Edition, ImplicitControllerExecutionContext, GuLogging} +import common.{Edition, GuLogging, ImplicitControllerExecutionContext} import conf.Static import contentapi.ContentApiClient import pages.{CrosswordHtmlPage, IndexHtmlPage, PrintableCrosswordHtmlPage} import crosswords.{ AccessibleCrosswordPage, AccessibleCrosswordRows, + CrosswordPageWithContent, CrosswordPageWithSvg, CrosswordSearchPageNoResult, CrosswordSearchPageWithResults, @@ -21,6 +22,10 @@ import play.api.data._ import play.api.mvc.{Action, RequestHeader, Result, _} import services.{IndexPage, IndexPageItem} import html.HtmlPageHelpers.ContentCSSFile +import model.dotcomrendering.{DotcomRenderingDataModel, PageType} +import play.api.libs.ws.WSClient +import renderers.DotcomRenderingService +import services.dotcomrendering.{CrosswordsPicker, RemoteRender} import scala.concurrent.Future import scala.concurrent.duration._ @@ -29,6 +34,9 @@ trait CrosswordController extends BaseController with GuLogging with ImplicitCon def contentApiClient: ContentApiClient + val remoteRenderer: DotcomRenderingService = DotcomRenderingService() + val wsClient: WSClient + def noResults()(implicit request: RequestHeader): Result def getCrossword(crosswordType: String, id: Int)(implicit request: RequestHeader): Future[ItemResponse] = { @@ -38,14 +46,14 @@ trait CrosswordController extends BaseController with GuLogging with ImplicitCon } def withCrossword(crosswordType: String, id: Int)( - f: (Crossword, ApiContent) => Result, + f: (Crossword, ApiContent) => Future[Result], )(implicit request: RequestHeader): Future[Result] = { - getCrossword(crosswordType, id).map { response => + getCrossword(crosswordType, id).flatMap { response => val maybeCrossword = for { content <- response.content crossword <- content.crossword } yield f(crossword, content) - maybeCrossword getOrElse noResults() + maybeCrossword getOrElse Future.successful(noResults()) } recover { case t: Throwable => log.error(s"Error retrieving $crosswordType crossword id $id from API", t) @@ -58,22 +66,31 @@ trait CrosswordController extends BaseController with GuLogging with ImplicitCon context: ApplicationContext, ): Future[Result] = { withCrossword(crosswordType, id) { (crossword, content) => - Cached(60.seconds)( - RevalidatableResult.Ok( - CrosswordHtmlPage.html( - CrosswordPageWithSvg( - CrosswordContent.make(CrosswordData.fromCrossword(crossword, content), content), - CrosswordSvg(crossword, None, None, false), + val page = CrosswordPageWithSvg( + CrosswordContent.make(CrosswordData.fromCrossword(crossword, content), content), + CrosswordSvg(crossword, None, None, false), + ) + + if (CrosswordsPicker.getTier(page) == RemoteRender) + remoteRenderer.getCrossword(wsClient, page, PageType(page, request, context)) + else + Future.successful( + Cached(60.seconds)( + RevalidatableResult.Ok( + CrosswordHtmlPage.html(page), ), ), - ), - ) + ) } } } -class CrosswordPageController(val contentApiClient: ContentApiClient, val controllerComponents: ControllerComponents)( - implicit context: ApplicationContext, +class CrosswordPageController( + val contentApiClient: ContentApiClient, + val controllerComponents: ControllerComponents, + val wsClient: WSClient, +)(implicit + context: ApplicationContext, ) extends CrosswordController { def noResults()(implicit request: RequestHeader): Result = @@ -84,19 +101,41 @@ class CrosswordPageController(val contentApiClient: ContentApiClient, val contro renderCrosswordPage(crosswordType, id) } + def renderJson(crosswordType: String, id: Int): Action[AnyContent] = { + Action.async { implicit request => + withCrossword(crosswordType, id) { (crossword, content) => + val crosswordContent = CrosswordContent.make(CrosswordData.fromCrossword(crossword, content), content) + val crosswordPage = new CrosswordPageWithContent(crosswordContent) + + val pageType = PageType(crosswordPage, request, context) + Future.successful( + common.renderJson(getDCRJson(crosswordPage, pageType), crosswordPage).as("application/json"), + ) + } + } + } + private def getDCRJson(crosswordPage: CrosswordPageWithContent, pageType: PageType)(implicit + request: RequestHeader, + ): String = + DotcomRenderingDataModel.toJson( + DotcomRenderingDataModel.forCrossword(crosswordPage, request, pageType), + ) + def accessibleCrossword(crosswordType: String, id: Int): Action[AnyContent] = Action.async { implicit request => withCrossword(crosswordType, id) { (crossword, content) => - Cached(60.seconds)( - RevalidatableResult.Ok( - CrosswordHtmlPage.html( - AccessibleCrosswordPage( - CrosswordContent.make( - CrosswordData - .fromCrossword(crossword.copy(name = s"Accessible version of ${crossword.name}"), content), - content, + Future.successful( + Cached(60.seconds)( + RevalidatableResult.Ok( + CrosswordHtmlPage.html( + AccessibleCrosswordPage( + CrosswordContent.make( + CrosswordData + .fromCrossword(crossword.copy(name = s"Accessible version of ${crossword.name}"), content), + content, + ), + AccessibleCrosswordRows(crossword), ), - AccessibleCrosswordRows(crossword), ), ), ), @@ -107,12 +146,14 @@ class CrosswordPageController(val contentApiClient: ContentApiClient, val contro def printableCrossword(crosswordType: String, id: Int): Action[AnyContent] = Action.async { implicit request => withCrossword(crosswordType, id) { (crossword, content) => - Cached(60.seconds)( - RevalidatableResult.Ok( - PrintableCrosswordHtmlPage.html( - CrosswordPageWithSvg( - CrosswordContent.make(CrosswordData.fromCrossword(crossword, content), content), - CrosswordSvg(crossword, None, None, false), + Future.successful( + Cached(60.seconds)( + RevalidatableResult.Ok( + PrintableCrosswordHtmlPage.html( + CrosswordPageWithSvg( + CrosswordContent.make(CrosswordData.fromCrossword(crossword, content), content), + CrosswordSvg(crossword, None, None, false), + ), ), ), ), @@ -127,15 +168,17 @@ class CrosswordPageController(val contentApiClient: ContentApiClient, val contro val globalStylesheet = Static(s"stylesheets/$ContentCSSFile.css") - Cached(60.seconds) { - val body = s"""$xml""" - RevalidatableResult( - Cors { - Ok(body).as("image/svg+xml") - }, - body, - ) - } + Future.successful( + Cached(60.seconds) { + val body = s"""$xml""" + RevalidatableResult( + Cors { + Ok(body).as("image/svg+xml") + }, + body, + ) + }, + ) } } } @@ -143,6 +186,7 @@ class CrosswordPageController(val contentApiClient: ContentApiClient, val contro class CrosswordSearchController( val contentApiClient: ContentApiClient, val controllerComponents: ControllerComponents, + val wsClient: WSClient, )(implicit context: ApplicationContext) extends CrosswordController { val searchForm = Form( diff --git a/applications/app/services/dotcomrendering/CrosswordsPicker.scala b/applications/app/services/dotcomrendering/CrosswordsPicker.scala new file mode 100644 index 000000000000..605ab333fd8d --- /dev/null +++ b/applications/app/services/dotcomrendering/CrosswordsPicker.scala @@ -0,0 +1,51 @@ +package services.dotcomrendering + +import common.GuLogging +import crosswords.CrosswordPageWithContent +import model.Cors.RichRequestHeader +import play.api.mvc.RequestHeader +import utils.DotcomponentsLogger + +object CrosswordsPicker extends GuLogging { + + /** + * + * Add to this function any logic for including/excluding + * a crossword page from being rendered with DCR + * + * Currently defaulting to false until we implement crosswords in DCR + * + * */ + private def dcrCouldRender(crosswordPageWithContent: CrosswordPageWithContent): Boolean = { + false + } + + def getTier( + crosswordPageWithContent: CrosswordPageWithContent, + )(implicit + request: RequestHeader, + ): RenderType = { + + val participatingInTest = false // until we create a test for this content type + val dcrCanRender = dcrCouldRender(crosswordPageWithContent) + + val tier = { + if (request.forceDCROff) LocalRender + else if (request.forceDCR) RemoteRender + else if (dcrCanRender && participatingInTest) RemoteRender + else LocalRender + } + + if (tier == RemoteRender) { + DotcomponentsLogger.logger.logRequest( + s"path executing in dotcomponents", + Map.empty, + crosswordPageWithContent.item, + ) + } else { + DotcomponentsLogger.logger.logRequest(s"path executing in web", Map.empty, crosswordPageWithContent.item) + } + + tier + } +} diff --git a/applications/conf/routes b/applications/conf/routes index b9e9d7638c9f..6754d70706fd 100644 --- a/applications/conf/routes +++ b/applications/conf/routes @@ -18,6 +18,7 @@ GET /survey/thankyou # NOTE: Leave this as it is, otherwise we don't render /crosswords/series/prize, for example. GET /crosswords/$crosswordType/:id.svg controllers.CrosswordPageController.thumbnail(crosswordType: String, id: Int) +GET /crosswords/$crosswordType/:id.json controllers.CrosswordPageController.renderJson(crosswordType: String, id: Int) GET /crosswords/$crosswordType/:id controllers.CrosswordPageController.crossword(crosswordType: String, id: Int) GET /crosswords/$crosswordType/:id/print controllers.CrosswordPageController.printableCrossword(crosswordType: String, id: Int) GET /crosswords/accessible/$crosswordType/:id controllers.CrosswordPageController.accessibleCrossword(crosswordType: String, id: Int) diff --git a/applications/test/CrosswordDataTest.scala b/applications/test/CrosswordDataTest.scala index 56061d47b63b..88c990058c87 100644 --- a/applications/test/CrosswordDataTest.scala +++ b/applications/test/CrosswordDataTest.scala @@ -23,7 +23,7 @@ import org.scalatest.time.{Millis, Span} "CrosswordData" - { lazy val crosswordPageController = - new CrosswordPageController(testContentApiClient, play.api.test.Helpers.stubControllerComponents()) + new CrosswordPageController(testContentApiClient, play.api.test.Helpers.stubControllerComponents(), wsClient) "fromCrossword should normalize separators for grouped entries" in { diff --git a/applications/test/CrosswordPageMetaDataTest.scala b/applications/test/CrosswordPageMetaDataTest.scala index f589c4197b5d..4ae93edac97a 100644 --- a/applications/test/CrosswordPageMetaDataTest.scala +++ b/applications/test/CrosswordPageMetaDataTest.scala @@ -18,7 +18,7 @@ import org.scalatest.{BeforeAndAfterAll, DoNotDiscover} val crosswordUrl = "crosswords/cryptic/26697" lazy val crosswordPageController = - new CrosswordPageController(testContentApiClient, play.api.test.Helpers.stubControllerComponents()) + new CrosswordPageController(testContentApiClient, play.api.test.Helpers.stubControllerComponents(), wsClient) it should "not include the ios deep link" in { val result = crosswordPageController.crossword("cryptic", 26697)(TestRequest(crosswordUrl)) diff --git a/applications/app/crosswords/AccessibleCrosswordRows.scala b/common/app/crosswords/AccessibleCrosswordRows.scala similarity index 94% rename from applications/app/crosswords/AccessibleCrosswordRows.scala rename to common/app/crosswords/AccessibleCrosswordRows.scala index 2715ea17b4e7..629b3f14b3d5 100644 --- a/applications/app/crosswords/AccessibleCrosswordRows.scala +++ b/common/app/crosswords/AccessibleCrosswordRows.scala @@ -1,6 +1,6 @@ package crosswords -import com.gu.contentapi.client.model.v1.{CrosswordDimensions, Crossword} +import com.gu.contentapi.client.model.v1.{Crossword, CrosswordDimensions} import model.CrosswordPosition case class AccessibleCrosswordRow(rowNumber: Int, blankColumns: List[Char]) diff --git a/applications/app/crosswords/CrosswordPage.scala b/common/app/crosswords/CrosswordPage.scala similarity index 98% rename from applications/app/crosswords/CrosswordPage.scala rename to common/app/crosswords/CrosswordPage.scala index c42d48efffd8..7dd5f80ae6d9 100644 --- a/applications/app/crosswords/CrosswordPage.scala +++ b/common/app/crosswords/CrosswordPage.scala @@ -1,8 +1,9 @@ package crosswords +import crosswords.CrosswordSvg.{BorderSize, CellSize} import model._ -import java.time.LocalDateTime +import java.time.LocalDateTime import scala.xml.Elem sealed trait CrosswordPage extends Page @@ -27,8 +28,6 @@ class CrosswordPageWithContent(content: CrosswordContent) extends ContentPage { override lazy val item = content val crossword = content.crossword - import crosswords.CrosswordSvg.{BorderSize, CellSize} - case class SvgDimensions(width: Int, height: Int) { def styleString: String = s"width: $width; height: $height" } diff --git a/applications/app/crosswords/CrosswordSvg.scala b/common/app/crosswords/CrosswordSvg.scala similarity index 100% rename from applications/app/crosswords/CrosswordSvg.scala rename to common/app/crosswords/CrosswordSvg.scala diff --git a/common/app/model/dotcomrendering/DotcomRenderingDataModel.scala b/common/app/model/dotcomrendering/DotcomRenderingDataModel.scala index 12c16d07d974..4b1f36f8336c 100644 --- a/common/app/model/dotcomrendering/DotcomRenderingDataModel.scala +++ b/common/app/model/dotcomrendering/DotcomRenderingDataModel.scala @@ -7,6 +7,7 @@ import common.Maps.RichMap import common.commercial.EditionCommercialProperties import common.{CanonicalLink, Chronos, Edition, Localisation, RichRequestHeader} import conf.Configuration +import crosswords.CrosswordPageWithContent import experiments.ActiveExperiments import model.dotcomrendering.DotcomRenderingUtils._ import model.dotcomrendering.pageElements.{PageElement, TextCleaner} @@ -16,6 +17,7 @@ import model.{ CanonicalLiveBlog, ContentFormat, ContentPage, + CrosswordData, GUDateTimeFormatNew, GalleryPage, ImageContentPage, @@ -108,6 +110,7 @@ case class DotcomRenderingDataModel( showTableOfContents: Boolean, lang: Option[String], isRightToLeftLang: Boolean, + crossword: Option[CrosswordData], ) object DotcomRenderingDataModel { @@ -187,6 +190,7 @@ object DotcomRenderingDataModel { "showTableOfContents" -> model.showTableOfContents, "lang" -> model.lang, "isRightToLeftLang" -> model.isRightToLeftLang, + "crossword" -> model.crossword, ) ElementsEnhancer.enhanceDcrObject(obj) @@ -338,6 +342,28 @@ object DotcomRenderingDataModel { ) } + def forCrossword( + crosswordPage: CrosswordPageWithContent, + request: RequestHeader, + pageType: PageType, + ): DotcomRenderingDataModel = { + val linkedData = LinkedData.forArticle( + article = crosswordPage.item, + baseURL = Configuration.dotcom.baseUrl, + fallbackLogo = Configuration.images.fallbackLogo, + ) + + apply( + page = crosswordPage, + request = request, + pageType = pageType, + linkedData = linkedData, + mainBlock = None, + bodyBlocks = Seq.empty, + crossword = Some(crosswordPage.crossword), + ) + } + def keyEventsFallback( blocks: APIBlocks, ): Seq[APIBlock] = { @@ -442,6 +468,7 @@ object DotcomRenderingDataModel { filterKeyEvents: Boolean = false, mostRecentBlockId: Option[String] = None, forceLive: Boolean = false, + crossword: Option[CrosswordData] = None, ): DotcomRenderingDataModel = { val edition = Edition.edition(request) @@ -635,6 +662,7 @@ object DotcomRenderingDataModel { showTableOfContents = content.fields.showTableOfContents.getOrElse(false), lang = content.fields.lang, isRightToLeftLang = content.fields.isRightToLeftLang, + crossword = crossword, ) } } diff --git a/common/app/renderers/DotcomRenderingService.scala b/common/app/renderers/DotcomRenderingService.scala index 7650cabefca6..a0b10f0b978a 100644 --- a/common/app/renderers/DotcomRenderingService.scala +++ b/common/app/renderers/DotcomRenderingService.scala @@ -6,6 +6,7 @@ import common.{DCRMetrics, GuLogging} import concurrent.CircuitBreakerRegistry import conf.Configuration import conf.switches.Switches.CircuitBreakerSwitch +import crosswords.CrosswordPageWithContent import http.{HttpPreconnections, ResultWithPreconnectPreload} import model.Cached.{RevalidatableResult, WithoutRevalidationResult} import model.dotcomrendering._ @@ -392,6 +393,16 @@ class DotcomRenderingService extends GuLogging with ResultWithPreconnectPreload val json = DotcomRenderingDataModel.toJson(dataModel) post(ws, json, Configuration.rendering.articleBaseURL + "/Article", CacheTime.Facia) } + + def getCrossword( + ws: WSClient, + crosswordPage: CrosswordPageWithContent, + pageType: PageType, + )(implicit request: RequestHeader): Future[Result] = { + val dataModel = DotcomRenderingDataModel.forCrossword(crosswordPage, request, pageType) + val json = DotcomRenderingDataModel.toJson(dataModel) + post(ws, json, Configuration.rendering.articleBaseURL + "/Article", CacheTime.Facia) + } } object DotcomRenderingService {