app/services/editions/EditionsTemplating.scala (274 lines of code) (raw):

package services.editions import logging.Logging import model.editions._ import model.editions.templates.{CuratedPlatformDefinition, TemplatedPlatform} import play.api.mvc.{Result, Results} import services.editions.prefills._ import services.{Capi, Ophan} import java.time.LocalDate import scala.concurrent.Await import scala.concurrent.duration._ import scala.language.postfixOps import scala.util.control.NonFatal case class GenerateEditionTemplateResult( issueSkeleton: EditionsIssueSkeleton, contentPrefillTimeWindow: CapiQueryTimeWindow ) class EditionsTemplating( templates: Map[Edition, CuratedPlatformDefinition with TemplatedPlatform], capi: Capi, ophan: Ophan ) extends Logging { private val collectionsTemplating = CollectionTemplatingHelper(capi, ophan) def generateEditionTemplate( edition: Edition, issueDate: LocalDate ): Either[Result, GenerateEditionTemplateResult] = { templates.get(edition) match { case Some(editionDefinition) => if (editionDefinition.template.availability.isValid(issueDate)) { val issueSkeleton: EditionsIssueSkeleton = getIssueSkeleton( issueDate, editionDefinition.template, edition, editionDefinition.template.ophanQueryPrefillParams ) val contentPrefillTimeWindow = editionDefinition.template.timeWindowConfig.toCapiQueryTimeWindow( issueDate ) Right( GenerateEditionTemplateResult( issueSkeleton, contentPrefillTimeWindow ) ) } else { Left( Results.UnprocessableEntity( s"$issueDate is not a valid date to create an issue of $edition" ) ) } case None => Left(Results.NotFound(s"No editionTemplate registered for $edition")) } } private def getIssueSkeleton( issueDate: LocalDate, editionTemplate: EditionTemplate, edition: Edition, ophanQueryPrefillParams: Option[OphanQueryPrefillParams] ) = { val frontsSkeleton = editionTemplate.fronts .filter { case (_, period) => period.isValid(issueDate) } .map { case (frontTemplate, _) => getFrontsSkeleton( issueDate, editionTemplate, edition, ophanQueryPrefillParams, frontTemplate ) } EditionsIssueSkeleton( issueDate, editionTemplate.zoneId, frontsSkeleton ) } private def getFrontsSkeleton( issueDate: LocalDate, editionTemplate: EditionTemplate, edition: Edition, ophanQueryPrefillParams: Option[OphanQueryPrefillParams], frontTemplate: FrontTemplate ) = { val editionsCollectionSkeletons = collectionsTemplating.generateCollectionTemplates( frontTemplate, edition, issueDate, editionTemplate, ophanQueryPrefillParams ) EditionsFrontSkeleton( frontTemplate.name, collections = editionsCollectionSkeletons, frontTemplate.presentation, frontTemplate.hidden, frontTemplate.isSpecial ) } } object CollectionTemplatingHelper { def apply(capi: Capi, ophan: Ophan): CollectionTemplatingHelper = new CollectionTemplatingHelper(capi, ophan) } class CollectionTemplatingHelper(capi: Capi, ophan: Ophan) extends Logging { def generateCollectionTemplates( frontTemplate: FrontTemplate, edition: Edition, issueDate: LocalDate, editionTemplate: EditionTemplate, ophanQueryPrefillParams: Option[OphanQueryPrefillParams] ): List[EditionsCollectionSkeleton] = { val collections = frontTemplate.collections val maybeOphanPath = frontTemplate.maybeOphanPath collections.map { collectionTemplate => import collectionTemplate.{hidden, name, prefill} val timeWindowConfig = List( collectionTemplate.maybeTimeWindowConfig, frontTemplate.maybeTimeWindowConfig ) .collectFirst { case Some(timeWindowConfig) => timeWindowConfig } .getOrElse(editionTemplate.timeWindowConfig) val capiQueryTimeWindow: CapiQueryTimeWindow = timeWindowConfig.toCapiQueryTimeWindow(issueDate) val capiPrefillTimeParams = CapiPrefillTimeParams( capiQueryTimeWindow, editionTemplate.capiDateQueryParam ) EditionsCollectionSkeleton( name, prefill .map { contentPrefillUrlSegments => val prefillParams = PrefillParamsAdapter( issueDate, contentPrefillUrlSegments, capiPrefillTimeParams, List( collectionTemplate.maybeOphanPath, maybeOphanPath, editionTemplate.maybeOphanPath ).collectFirst { case Some(path) => path }, // Get first non-None match, ophanQueryPrefillParams, edition, Some(collectionTemplate.cardCap), MetadataForLogging( issueDate, collectionId = None, collectionName = Some(name) ) ) getPrefillArticles(prefillParams) } .getOrElse(Nil), prefill, capiQueryTimeWindow, hidden ) } } // this function fetches articles from CAPI with enough data to resolve the defaults private def getPrefillArticles( prefillParams: PrefillParamsAdapter ): List[EditionsCardSkeleton] = { val maybeOphanScoresMap: Option[Map[String, Double]] = getOphanMetricsMap( prefillParams ) val items = getArticleItemsFromCapi(prefillParams) val sortedArticleItems: List[Prefill] = sortArticleItems(items, maybeOphanScoresMap) val cappedItems = capArticleItems(sortedArticleItems, prefillParams) mapToSkeleton(cappedItems) } private def mapToSkeleton( sortedArticleItems: List[Prefill] ): List[EditionsCardSkeleton] = { sortedArticleItems .map { case Prefill( pageCode, _, _, metaData, cutoutImage, _, mediaType, pickedKicker, promotionMetric, _ ) => val cardMetadata = EditionsArticleMetadata.default.copy( showByline = if (metaData.showByline) Some(true) else None, showQuotedHeadline = if (metaData.showQuotedHeadline) Some(true) else None, mediaType = mediaType, cutoutImage = cutoutImage, customKicker = pickedKicker, promotionMetric = promotionMetric ) EditionsCardSkeleton( pageCode.toString, CardType.Article, Some(cardMetadata) ) } } private def getArticleItemsFromCapi( prefillParams: PrefillParamsAdapter ): List[Prefill] = { // TODO: This being a try will hide a litany of failures, some of which we might want to surface try { capi.getUnsortedPrefillArticleItems(prefillParams) } catch { case NonFatal(t) => // At least log this as a warning so we can trace frequency logger.warn( s"Failed to successfully execute CAPI prefill query $prefillParams", t ) Nil } } private def getOphanMetricsMap( prefillParams: PrefillParamsAdapter ): Option[Map[String, Double]] = { val maybeOphanScores = try { Await.result( ophan.getOphanScores( prefillParams.maybeOphanPath, prefillParams.issueDate, prefillParams.maybeOphanQueryPrefillParams ), 30 seconds ) } catch { case NonFatal(t) => // At least log this as a warning so we can trace frequency logger.warn( s"Failed to successfully fetch Ophan scores from ${prefillParams.maybeOphanPath}", t ) None } maybeOphanScores.map(os => os.toList.map(o => o.capiId -> o.promotionScore).toMap ) } protected[editions] def sortArticleItems( articleItems: List[Prefill], maybeOphanScoresMap: Option[Map[String, Double]] ): List[Prefill] = { // If there are no ophan scores OR the request failed, fall back to ordering by newspaperPageNumber // Otherwise, copy on the ophan score, then sort by it, descending, default zero maybeOphanScoresMap match { case Some(scoresMap) => articleItems .map(item => item.copy(promotionMetric = scoresMap.get(item.capiId))) .sortBy(item => item.newspaperPageNumber.getOrElse(999)) .sortBy(item => item.promotionMetric.getOrElse(0d))( Ordering[Double].reverse ) case _ => articleItems.sortBy(item => item.newspaperPageNumber.getOrElse(999)) } } private def capArticleItems( sortedArticleItems: List[Prefill], prefillParams: PrefillParamsAdapter ): List[Prefill] = { prefillParams.maybePrefillItemsCap match { case Some(cap) => sortedArticleItems.take(cap) case _ => sortedArticleItems } } }