Skip to content

Commit

Permalink
Remote render email newsletters (#25876)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
dblatcher authored Feb 21, 2023
1 parent 3e1f1df commit a0a3cdc
Show file tree
Hide file tree
Showing 6 changed files with 246 additions and 15 deletions.
110 changes: 95 additions & 15 deletions applications/app/controllers/SignupPageController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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()
}
}
}
}
1 change: 1 addition & 0 deletions applications/conf/routes
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
)
}
}
15 changes: 15 additions & 0 deletions common/app/renderers/DotcomRenderingService.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 {
Expand Down
17 changes: 17 additions & 0 deletions common/app/staticpages/StaticPages.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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,
),
)
}
1 change: 1 addition & 0 deletions dev-build/conf/routes
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down

0 comments on commit a0a3cdc

Please sign in to comment.