From a0a3cdcf4cc1143b567839b1fb586cbe322fd3b6 Mon Sep 17 00:00:00 2001 From: David Blatcher <30567854+dblatcher@users.noreply.github.com> Date: Tue, 21 Feb 2023 15:00:11 +0000 Subject: [PATCH] Remote render email newsletters (#25876) * throw an error if the path requests dcr=true * add stub service module for requesting the page from dcr * RemoteRenderPage can await Futures * can post test json to DCR and get a response * provide newsletterData list to DCR * adjust timing * scalafmt * use capitalised path for Dcr request * provide some metaData * build a Page object, hard code the JSON string properties, add TO DO's * delegate creating the simple page to the StaticPages module * use some page metaData for the JSON * rename renderer object * add edtion to page model * add urls from config * provide the full config object * provide essentials for navMenu data * construct nav menu * build rendering data model for newsletters page * move the data model building to the DCR service * rename method * all dcr service directly from controller * fix casing * tidy imports * provide canonicalUrl * use forceDCR from RichRequestHeader * add a render json method * 404 error for .json without ?dcr --- .../controllers/SignupPageController.scala | 110 +++++++++++++--- applications/conf/routes | 1 + ...comNewslettersPageRenderingDataModel.scala | 117 ++++++++++++++++++ .../renderers/DotcomRenderingService.scala | 15 +++ common/app/staticpages/StaticPages.scala | 17 +++ dev-build/conf/routes | 1 + 6 files changed, 246 insertions(+), 15 deletions(-) create mode 100644 common/app/model/dotcomrendering/DotcomNewslettersPageRenderingDataModel.scala diff --git a/applications/app/controllers/SignupPageController.scala b/applications/app/controllers/SignupPageController.scala index 15dae1440853..dbc7c26215a5 100644 --- a/applications/app/controllers/SignupPageController.scala +++ b/applications/app/controllers/SignupPageController.scala @@ -5,13 +5,20 @@ import model.{ApplicationContext, Cached, NoCache} import model.Cached.RevalidatableResult import pages.NewsletterHtmlPage import play.api.libs.ws.WSClient -import play.api.mvc.{Action, AnyContent, BaseController, ControllerComponents} +import play.api.mvc.{Action, AnyContent, BaseController, ControllerComponents, RequestHeader, Result} import play.filters.csrf.CSRFAddToken +import renderers.DotcomRenderingService import services.newsletters.GroupedNewslettersResponse.GroupedNewslettersResponse import services.newsletters.NewsletterSignupAgent +import services.newsletters.model.NewsletterResponse import staticpages.StaticPages +import implicits.Requests.RichRequestHeader +import scala.concurrent.{Await, ExecutionContext, Future} import scala.concurrent.duration._ +import model.dotcomrendering.DotcomNewslettersPageRenderingDataModel +import model.SimplePage +import model.CacheTime class SignupPageController( wsClient: WSClient, @@ -24,25 +31,98 @@ class SignupPageController( with GuLogging { val defaultCacheDuration: Duration = 15.minutes + val remoteRenderer = DotcomRenderingService() - def renderNewslettersPage(): Action[AnyContent] = + private def localRenderNewslettersPage()(implicit + request: RequestHeader, + ): Result = { + val groupedNewsletters: Either[String, GroupedNewslettersResponse] = + newsletterSignupAgent.getGroupedNewsletters() + groupedNewsletters match { + case Right(groupedNewsletters) => + Cached(defaultCacheDuration)( + RevalidatableResult.Ok( + NewsletterHtmlPage.html(StaticPages.simpleNewslettersPage(request.path, groupedNewsletters)), + ), + ) + case Left(e) => + log.error(s"API call to get newsletters failed: $e") + NoCache(InternalServerError) + } + } + + private def remoteRenderNewslettersPage()(implicit + request: RequestHeader, + ): Result = { + + val newsletters: Either[String, List[NewsletterResponse]] = + newsletterSignupAgent.getNewsletters() + + newsletters match { + case Right(newsletters) => + Await.result( + remoteRenderer.getEmailNewsletters( + ws = wsClient, + newsletters = newsletters, + page = StaticPages.dcrSimpleNewsletterPage(request.path), + ), + 3.seconds, + ) + case Left(e) => + log.error(s"API call to get newsletters failed: $e") + NoCache(InternalServerError) + } + } + + def renderNewslettersPage()(implicit + executionContext: ExecutionContext = this.executionContext, + ): Action[AnyContent] = csrfAddToken { Action { implicit request => - val groupedNewsletters: Either[String, GroupedNewslettersResponse] = - newsletterSignupAgent.getGroupedNewsletters() - - groupedNewsletters match { - case Right(groupedNewsletters) => - Cached(defaultCacheDuration)( - RevalidatableResult.Ok( - NewsletterHtmlPage.html(StaticPages.simpleNewslettersPage(request.path, groupedNewsletters)), - ), - ) - case Left(e) => - log.error(s"API call to get newsletters failed: $e") - NoCache(InternalServerError) + if (request.forceDCR) { + remoteRenderNewslettersPage() + } else { + localRenderNewslettersPage() } } } + private def renderDCRNewslettersJson()(implicit + request: RequestHeader, + ): Result = { + val newsletters: Either[String, List[NewsletterResponse]] = + newsletterSignupAgent.getNewsletters() + + newsletters match { + case Right(newsletters) => { + val page = StaticPages.dcrSimpleNewsletterPage(request.path) + val dataModel = + DotcomNewslettersPageRenderingDataModel.apply(page, newsletters, request) + val dataJson = DotcomNewslettersPageRenderingDataModel.toJson(dataModel) + common.renderJson(dataJson, page).as("application/json") + } + case Left(e) => + log.error(s"API call to get newsletters failed: $e") + throw new RuntimeException() + } + } + + private def renderNewslettersJson()(implicit + request: RequestHeader, + ): Result = { + Cached(CacheTime.NotFound)(Cached.WithoutRevalidationResult(NotFound)) + } + + def renderNewslettersJson()(implicit + executionContext: ExecutionContext = this.executionContext, + ): Action[AnyContent] = + csrfAddToken { + Action { implicit request => + if (request.forceDCR) { + renderDCRNewslettersJson() + } else { + renderNewslettersJson() + } + } + } } diff --git a/applications/conf/routes b/applications/conf/routes index b5496a12aec0..cb19f29ba150 100644 --- a/applications/conf/routes +++ b/applications/conf/routes @@ -11,6 +11,7 @@ GET /sitemaps/news.xml GET /sitemaps/video.xml controllers.SiteMapController.renderVideoSiteMap() GET /email-newsletters controllers.SignupPageController.renderNewslettersPage() +GET /email-newsletters.json controllers.SignupPageController.renderNewslettersJson() GET /survey/:formName/show controllers.SurveyPageController.renderFormStackSurvey(formName) GET /survey/thankyou controllers.SurveyPageController.thankYou() diff --git a/common/app/model/dotcomrendering/DotcomNewslettersPageRenderingDataModel.scala b/common/app/model/dotcomrendering/DotcomNewslettersPageRenderingDataModel.scala new file mode 100644 index 000000000000..fe4dd253fa55 --- /dev/null +++ b/common/app/model/dotcomrendering/DotcomNewslettersPageRenderingDataModel.scala @@ -0,0 +1,117 @@ +package model.dotcomrendering + +import common.{CanonicalLink, Edition} +import common.Maps.RichMap +import common.commercial.EditionCommercialProperties +import conf.Configuration +import com.gu.contentapi.client.model.v1.Content +import experiments.ActiveExperiments +import layout.ContentCard +import model.{SimplePage, RelatedContentItem} +import navigation.{FooterLinks, Nav} +import play.api.libs.json.{JsObject, JsValue, Json} +import play.api.mvc.RequestHeader +import views.support.{CamelCase, JavaScriptPage} +import services.newsletters.model.NewsletterResponse +import services.NewsletterData + +case class DotcomNewslettersPageRenderingDataModel( + newsletters: List[NewsletterData], + id: String, + editionId: String, + editionLongForm: String, + beaconURL: String, + subscribeUrl: String, + contributionsServiceUrl: String, + webTitle: String, + description: Option[String], + config: JsObject, + openGraphData: Map[String, String], + twitterData: Map[String, String], + nav: Nav, + commercialProperties: Map[String, EditionCommercialProperties], + pageFooter: PageFooter, + isAdFreeUser: Boolean, + canonicalUrl: String, +) + +object DotcomNewslettersPageRenderingDataModel { + implicit val writes = Json.writes[DotcomNewslettersPageRenderingDataModel] + + def apply( + page: SimplePage, + newsletters: List[NewsletterResponse], + request: RequestHeader, + ): DotcomNewslettersPageRenderingDataModel = { + val edition = Edition.edition(request) + val nav = Nav(page, edition) + + val switches: Map[String, Boolean] = conf.switches.Switches.all + .filter(_.exposeClientSide) + .foldLeft(Map.empty[String, Boolean])((acc, switch) => { + acc + (CamelCase.fromHyphenated(switch.name) -> switch.isSwitchedOn) + }) + + val config = Config( + switches = switches, + abTests = ActiveExperiments.getJsMap(request), + ampIframeUrl = DotcomRenderingUtils.assetURL("data/vendor/amp-iframe.html"), + googletagUrl = Configuration.googletag.jsLocation, + stage = common.Environment.stage, + frontendAssetsFullURL = Configuration.assets.fullURL(common.Environment.stage), + ) + + val combinedConfig: JsObject = { + val jsPageConfig: Map[String, JsValue] = + JavaScriptPage.getMap(page, Edition(request), isPreview = false, request) + Json.toJsObject(config).deepMerge(JsObject(jsPageConfig)) + } + + val commercialProperties = page.metadata.commercial + .map { _.perEdition.mapKeys(_.id) } + .getOrElse(Map.empty[String, EditionCommercialProperties]) + + val newsletterData = newsletters + .filter((newsletter) => newsletter.cancelled == false && newsletter.paused == false) + .map((newsletter) => convertNewsletterResponseToData(newsletter)) + + DotcomNewslettersPageRenderingDataModel( + newsletters = newsletterData, + id = page.metadata.id, + editionId = edition.id, + editionLongForm = edition.displayName, + beaconURL = Configuration.debug.beaconUrl, + subscribeUrl = Configuration.id.subscribeUrl, + contributionsServiceUrl = Configuration.contributionsService.url, + webTitle = page.metadata.webTitle, + description = page.metadata.description, + config = combinedConfig, + openGraphData = page.getOpenGraphProperties, + twitterData = page.getTwitterProperties, + nav = nav, + commercialProperties = commercialProperties, + pageFooter = PageFooter(FooterLinks.getFooterByEdition(Edition(request))), + isAdFreeUser = views.support.Commercial.isAdFree(request), + canonicalUrl = CanonicalLink(request, page.metadata.webUrl), + ) + } + + def toJson(model: DotcomNewslettersPageRenderingDataModel): String = { + val jsValue = Json.toJson(model) + Json.stringify(DotcomRenderingUtils.withoutNull(jsValue)) + } + + private def convertNewsletterResponseToData(response: NewsletterResponse): NewsletterData = { + NewsletterData( + response.identityName, + response.name, + response.theme, + response.description, + response.frequency, + response.listId, + response.group, + response.emailEmbed.successDescription, + response.regionFocus, + ) + } +} diff --git a/common/app/renderers/DotcomRenderingService.scala b/common/app/renderers/DotcomRenderingService.scala index 267cb608319e..b0de5c99d4a4 100644 --- a/common/app/renderers/DotcomRenderingService.scala +++ b/common/app/renderers/DotcomRenderingService.scala @@ -8,14 +8,18 @@ import conf.Configuration import conf.switches.Switches.CircuitBreakerSwitch import http.{HttpPreconnections, ResultWithPreconnectPreload} import model.Cached.{RevalidatableResult, WithoutRevalidationResult} +import model.{SimplePage} import model.dotcomrendering.{ DotcomBlocksRenderingDataModel, DotcomFrontsRenderingDataModel, + DotcomNewslettersPageRenderingDataModel, DotcomRenderingDataModel, OnwardCollectionResponse, PageType, } import services.NewsletterData +import services.newsletters.model.NewsletterResponse + import model.{ CacheTime, Cached, @@ -256,6 +260,17 @@ class DotcomRenderingService extends GuLogging with ResultWithPreconnectPreload val json = DotcomRenderingDataModel.toJson(dataModel) post(ws, json, Configuration.rendering.baseURL + "/AMPInteractive", page.metadata.cacheTime) } + + def getEmailNewsletters( + ws: WSClient, + newsletters: List[NewsletterResponse], + page: SimplePage, + )(implicit request: RequestHeader): Future[Result] = { + + val dataModel = DotcomNewslettersPageRenderingDataModel.apply(page, newsletters, request) + val json = DotcomNewslettersPageRenderingDataModel.toJson(dataModel) + post(ws, json, Configuration.rendering.baseURL + "/EmailNewsletters", CacheTime.Facia) + } } object DotcomRenderingService { diff --git a/common/app/staticpages/StaticPages.scala b/common/app/staticpages/StaticPages.scala index 02019cbb1664..3a96a63c2442 100644 --- a/common/app/staticpages/StaticPages.scala +++ b/common/app/staticpages/StaticPages.scala @@ -53,4 +53,21 @@ object StaticPages { ), groupedNewsletterResponses, ) + + def dcrSimpleNewsletterPage( + id: String, + ): SimplePage = + SimplePage( + MetaData.make( + id = id, + section = Option(SectionId(value = "newsletter-signup-page")), + webTitle = "Guardian newsletters: Sign up for our free newsletters", + description = Some( + "Scroll less and understand more about the subjects you care about with the Guardian's brilliant email newsletters, free to your inbox.", + ), + contentType = Some(DotcomContentType.Signup), + iosType = None, + shouldGoogleIndex = true, + ), + ) } diff --git a/dev-build/conf/routes b/dev-build/conf/routes index 1f2091260bd6..8599b2554b6c 100644 --- a/dev-build/conf/routes +++ b/dev-build/conf/routes @@ -5,6 +5,7 @@ GET /commercial/test-page commercial.controllers.CreativeTestPage.allComponents(k: List[String]) GET /email-newsletters controllers.SignupPageController.renderNewslettersPage() +GET /email-newsletters.json controllers.SignupPageController.renderNewslettersJson() GET /survey/:formName/show controllers.SurveyPageController.renderFormStackSurvey(formName) GET /survey/thankyou controllers.SurveyPageController.thankYou()