article/app/controllers/ArticleController.scala (156 lines of code) (raw):

package controllers import com.gu.contentapi.client.model.v1.{Blocks, ItemResponse, Content => ApiContent} import common._ import contentapi.ContentApiClient import implicits.{AmpFormat, AppsFormat, EmailFormat, HtmlFormat, JsonFormat} import model.Cached.{RevalidatableResult, WithoutRevalidationResult} import model.dotcomrendering.{DotcomRenderingDataModel, PageType} import model._ import pages.{ArticleEmailHtmlPage, ArticleHtmlPage} import play.api.libs.json.{Json, JsValue} import play.api.libs.ws.WSClient import play.api.mvc._ import renderers.DotcomRenderingService import services.{CAPILookup, NewsletterService} import services.dotcomrendering.{ArticlePicker, PressedArticle, RemoteRender} import views.support._ import scala.concurrent.Future class ArticleController( contentApiClient: ContentApiClient, val controllerComponents: ControllerComponents, ws: WSClient, remoteRenderer: renderers.DotcomRenderingService = DotcomRenderingService(), newsletterService: NewsletterService, )(implicit context: ApplicationContext) extends BaseController with RendersItemResponse with GuLogging with ImplicitControllerExecutionContext { val capiLookup: CAPILookup = new CAPILookup(contentApiClient) private def isSupported(c: ApiContent) = c.isArticle || c.isLiveBlog || c.isSudoku override def canRender(i: ItemResponse): Boolean = i.content.exists(isSupported) override def renderItem(path: String)(implicit request: RequestHeader): Future[Result] = mapModel(path, GenericFallback)((article, blocks) => render(path, article, blocks)) def renderJson(path: String): Action[AnyContent] = { Action.async { implicit request => mapModel(path, ArticleBlocks) { (article, blocks) => render(path, article, blocks) } } } def renderArticle(path: String): Action[AnyContent] = { Action.async { implicit request => mapModel(path, ArticleBlocks) { (article, blocks) => render(path, article, blocks) } } } def renderEmail(path: String): Action[AnyContent] = { Action.async { implicit request => mapModel(path, ArticleBlocks) { (article, blocks) => render(path, article, blocks) } } } def renderHeadline(path: String): Action[AnyContent] = Action.async { implicit request => def responseFromOptionalString(headline: Option[String]) = { headline .map(s => Cached(CacheTime.Default)(RevalidatableResult.Ok(s))) .getOrElse { logWarnWithRequestId(s"headline not found for $path") Cached(10)(WithoutRevalidationResult(NotFound)) } } capiLookup .lookup(path, Some(ArticleBlocks)) .map(_.content.map(_.webTitle)) .map(responseFromOptionalString) } private def getJson(article: ArticlePage)(implicit request: RequestHeader): List[(String, Object)] = { val contentFieldsJson = if (request.forceDCR) List( "contentFields" -> Json.toJson(ContentFields(article.article)), "tags" -> Json.toJson(article.article.tags), ) else List() List(("html", views.html.fragments.articleBody(article))) ++ contentFieldsJson } /** Returns a JSON representation of the payload that's sent to DCR when rendering the Article. */ private def getDCRJson(article: ArticlePage, blocks: Blocks)(implicit request: RequestHeader): JsValue = { val pageType: PageType = PageType(article, request, context) val newsletter = newsletterService.getNewsletterForArticle(article) DotcomRenderingDataModel.toJson( DotcomRenderingDataModel .forArticle(article, blocks, request, pageType, newsletter), ) } private def render(path: String, article: ArticlePage, blocks: Blocks)(implicit request: RequestHeader, ): Future[Result] = { val newsletter = newsletterService.getNewsletterForArticle(article) val tier = ArticlePicker.getTier(article, path) val isAmpSupported = article.article.content.shouldAmplify val pageType: PageType = PageType(article, request, context) request.getRequestFormat match { case JsonFormat if request.forceDCR => Future.successful(common.renderJson(getDCRJson(article, blocks), article).as("application/json")) case JsonFormat => Future.successful(common.renderJson(getJson(article), article)) case EmailFormat => Future.successful(common.renderEmail(ArticleEmailHtmlPage.html(article), article)) case HtmlFormat | AmpFormat if tier == PressedArticle => servePressedPage(path) case AmpFormat if isAmpSupported => remoteRenderer.getAMPArticle(ws, article, blocks, pageType, newsletter) case HtmlFormat | AmpFormat if tier == RemoteRender => remoteRenderer.getArticle( ws, article, blocks, pageType, newsletter, ) case HtmlFormat | AmpFormat => Future.successful(common.renderHtml(ArticleHtmlPage.html(article), article)) case AppsFormat => remoteRenderer.getAppsArticle( ws, article, blocks, pageType, newsletter, ) } } private def mapModel(path: String, range: BlockRange)( render: (ArticlePage, Blocks) => Future[Result], )(implicit request: RequestHeader): Future[Result] = { capiLookup .lookup(path, Some(range)) .map(responseToModelOrResult) .recover(convertApiExceptions) .flatMap { case Right((model, blocks)) => render(model, blocks) case Left(other) => Future.successful(RenderOtherStatus(other)) } } private def responseToModelOrResult( response: ItemResponse, )(implicit request: RequestHeader): Either[Result, (ArticlePage, Blocks)] = { val supportedContent: Option[ContentType] = response.content.filter(isSupported).map(Content(_)) val blocks = response.content.flatMap(_.blocks).getOrElse(Blocks()) ModelOrResult(supportedContent, response) match { case Right(article: Article) => Right((ArticlePage(article, StoryPackages(article.metadata.id, response)), blocks)) case Left(r) => Left(r) case _ => Left(NotFound) } } def servePressedPage(path: String)(implicit request: RequestHeader): Future[Result] = { val cacheable = WithoutRevalidationResult( Ok.withHeaders("X-Accel-Redirect" -> s"/s3-archive/www.theguardian.com/$path"), ) Future.successful(Cached(CacheTime.ArchiveRedirect)(cacheable)) } }