applications/app/controllers/CrosswordsController.scala (324 lines of code) (raw):

package controllers 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, CrosswordPageWithContent, CrosswordPageWithSvg, CrosswordSearchPageNoResult, CrosswordSearchPageWithResults, CrosswordSvg, } import html.HtmlPageHelpers.ContentCSSFile import model.Cached.{RevalidatableResult, WithoutRevalidationResult} import model._ import model.dotcomrendering.pageElements.EditionsCrosswordRenderingDataModel import model.dotcomrendering.pageElements.EditionsCrosswordRenderingDataModel.toJson import model.dotcomrendering.{DotcomRenderingDataModel, PageType} import org.joda.time.{DateTime, LocalDate} import pages.{CrosswordHtmlPage, IndexHtmlPage, PrintableCrosswordHtmlPage} import play.api.data.Forms._ import play.api.data._ import play.api.libs.json.JsValue import play.api.libs.ws.WSClient import play.api.mvc._ import renderers.DotcomRenderingService import services.dotcomrendering.{CrosswordsPicker, RemoteRender} import services.{IndexPage, IndexPageItem} import scala.collection.immutable import scala.concurrent.Future import scala.concurrent.duration._ trait CrosswordController extends BaseController with GuLogging with ImplicitControllerExecutionContext { 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] = { contentApiClient.getResponse( contentApiClient.item(s"crosswords/$crosswordType/$id", Edition(request)).showFields("all"), ) } def withCrossword(crosswordType: String, id: Int)( f: (Crossword, ApiContent) => Future[Result], )(implicit request: RequestHeader): Future[Result] = { getCrossword(crosswordType, id).flatMap { response => val maybeCrossword = for { content <- response.content crossword <- content.crossword } yield f(crossword, content) maybeCrossword getOrElse Future.successful(noResults()) } recover { case t: Throwable => logErrorWithRequestId(s"Error retrieving $crosswordType crossword id $id from API", t) noResults() } } def renderCrosswordPage(crosswordType: String, id: Int)(implicit request: RequestHeader, context: ApplicationContext, ): Future[Result] = { withCrossword(crosswordType, id) { (crossword, content) => 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(CacheTime.Crosswords)( RevalidatableResult.Ok( CrosswordHtmlPage.html(page), ), ), ) } } } class CrosswordPageController( val contentApiClient: ContentApiClient, val controllerComponents: ControllerComponents, val wsClient: WSClient, )(implicit context: ApplicationContext, ) extends CrosswordController { def noResults()(implicit request: RequestHeader): Result = Cached(CacheTime.NotFound)(WithoutRevalidationResult(NotFound)) def crossword(crosswordType: String, id: Int): Action[AnyContent] = Action.async { implicit request => 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, ): JsValue = DotcomRenderingDataModel.toJson( DotcomRenderingDataModel.forCrossword(crosswordPage, request, pageType), ) def accessibleCrossword(crosswordType: String, id: Int): Action[AnyContent] = Action.async { implicit request => withCrossword(crosswordType, id) { (crossword, content) => val retitledFields = for { fields <- content.fields headline <- fields.headline } yield fields.copy(headline = Some(s"Accessible version of $headline")) val retitledContent = content.copy( fields = retitledFields, webTitle = s"Accessible version of ${content.webTitle}", ) Future.successful( Cached(60.seconds)( RevalidatableResult.Ok( CrosswordHtmlPage.html( AccessibleCrosswordPage( CrosswordContent.make( CrosswordData.fromCrossword(crossword, retitledContent), retitledContent, ), AccessibleCrosswordRows(crossword), ), ), ), ), ) } } def printableCrossword(crosswordType: String, id: Int): Action[AnyContent] = Action.async { implicit request => withCrossword(crosswordType, id) { (crossword, content) => Future.successful( Cached(60.seconds)( RevalidatableResult.Ok( PrintableCrosswordHtmlPage.html( CrosswordPageWithSvg( CrosswordContent.make(CrosswordData.fromCrossword(crossword, content), content), CrosswordSvg(crossword, None, None, false), ), ), ), ), ) } } def thumbnail(crosswordType: String, id: Int): Action[AnyContent] = Action.async { implicit request => withCrossword(crosswordType, id) { (crossword, _) => val xml = CrosswordSvg(crossword, Some("100%"), Some("100%"), trim = true) val globalStylesheet = Static(s"stylesheets/$ContentCSSFile.css") Future.successful( Cached(60.seconds) { val body = s"""$xml""" RevalidatableResult( Cors { Ok(body).as("image/svg+xml") }, body, ) }, ) } } } class CrosswordSearchController( val contentApiClient: ContentApiClient, val controllerComponents: ControllerComponents, val wsClient: WSClient, )(implicit context: ApplicationContext) extends CrosswordController { val searchForm = Form( mapping( "crossword_type" -> nonEmptyText, "month" -> number, "year" -> number, "setter" -> optional(text), )(CrosswordSearch.apply)(CrosswordSearch.unapply), ) val lookupForm = Form( mapping( "crossword_type" -> nonEmptyText, "id" -> number, )(CrosswordLookup.apply)(CrosswordLookup.unapply), ) def noResults()(implicit request: RequestHeader): Result = Cached(7.days)( RevalidatableResult.Ok( CrosswordHtmlPage.html(new CrosswordSearchPageNoResult), ), ) def search(): Action[AnyContent] = Action.async { implicit request => searchForm .bindFromRequest() .fold( empty => Future.successful( Cached(7.days)( RevalidatableResult.Ok( CrosswordHtmlPage.html(new CrosswordSearchPageWithResults), ), ), ), params => { val withoutSetter = contentApiClient .item(s"crosswords/series/${params.crosswordType}") .stringParam("from-date", params.fromDate.toString("yyyy-MM-dd")) .stringParam("to-date", params.toDate.toString("yyyy-MM-dd")) .pageSize(50) val maybeSetter = params.setter.fold(withoutSetter) { setter => withoutSetter.stringParam("tag", s"profile/${setter.toLowerCase}") } contentApiClient.getResponse(maybeSetter.showFields("all")).map { response => response.results.getOrElse(Seq.empty).toList match { case Nil => noResults() case results => val section = Section.make( ApiSection( "crosswords", "Crosswords search results", "http://www.theguardian.com/crosswords/search", "", Nil, ), ) val page = IndexPage( page = section, contents = results.map(IndexPageItem(_)), tags = Tags(Nil), date = DateTime.now, tzOverride = None, ) Cached(15.minutes)(RevalidatableResult.Ok(IndexHtmlPage.html(page))) } } }, ) } def lookup(): Action[AnyContent] = Action.async { implicit request => lookupForm .bindFromRequest() .fold( formWithErrors => Future.successful(noResults()), lookUpData => renderCrosswordPage(lookUpData.crosswordType, lookUpData.id), ) } case class CrosswordSearch(crosswordType: String, month: Int, year: Int, setter: Option[String]) { val fromDate = new LocalDate(year, month, 1) val toDate = fromDate.dayOfMonth.withMaximumValue.minusDays(1) } case class CrosswordLookup(crosswordType: String, id: Int) } class CrosswordEditionsController( val contentApiClient: ContentApiClient, val controllerComponents: ControllerComponents, val remoteRenderer: DotcomRenderingService = DotcomRenderingService(), val wsClient: WSClient, ) extends BaseController with GuLogging with ImplicitControllerExecutionContext { def digitalEdition: Action[AnyContent] = Action.async { implicit request => getCrosswords .map(parseCrosswords) .flatMap { crosswords => remoteRenderer.getEditionsCrossword(wsClient, crosswords) } } def digitalEditionJson: Action[AnyContent] = Action.async { implicit request => getCrosswords .map(parseCrosswords) .map { crosswords => Cached(CacheTime.Default)(RevalidatableResult.Ok(toJson(crosswords))).as("application/json") } } 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() .contentType("crossword") .tag(crosswordTags) .useDate("newspaper-edition") .pageSize(75) private lazy val crosswordTags = Seq( "crosswords/series/quick", "crosswords/series/cryptic", "crosswords/series/prize", "crosswords/series/weekend-crossword", "crosswords/series/sunday-quick", "crosswords/series/quick-cryptic", "crosswords/series/everyman", "crosswords/series/speedy", "crosswords/series/quiptic", ).mkString("|") private def parseCrosswords(response: SearchResponse): EditionsCrosswordRenderingDataModel = { val originalCapiCrosswords: Seq[Crossword] = response.results.flatMap(_.crossword).toList val collectedItems = response.results.collect { case content if content.crossword.isDefined => CrosswordData.fromCrossword(content.crossword.get, content) } val crosswordDataItems: immutable.Seq[CrosswordData] = collectedItems.toList EditionsCrosswordRenderingDataModel( crosswords = originalCapiCrosswords, newCrosswords = crosswordDataItems, ) } }