app/model/editions/EditionsCard.scala (297 lines of code) (raw):

package model.editions import enumeratum.EnumEntry.{Hyphencase, Uncapitalised} import enumeratum.{EnumEntry, PlayEnum} import logging.Logging import model.editions.EditionsArticle.logger import play.api.libs.json.{Json, OFormat, Reads, Writes} import scalikejdbc.WrappedResultSet case class Image( height: Option[Int], width: Option[Int], origin: String, src: String, thumb: Option[String] = None ) { def toPublishedImage: PublishedImage = PublishedImage(height, width, src) } object Image { implicit val format: OFormat[Image] = Json.format[Image] } case class CoverCardImages(mobile: Option[Image], tablet: Option[Image]) object CoverCardImages { implicit val format: OFormat[CoverCardImages] = Json.format[CoverCardImages] } case class Palette(foregroundHex: String, backgroundHex: String) object Palette { implicit val format: OFormat[Palette] = Json.format[Palette] } case class ChefTheme(id: String, palette: Palette) object ChefTheme { implicit val format: OFormat[ChefTheme] = Json.format[ChefTheme] } case class FeastCollectionTheme( id: String, lightPalette: Palette, darkPalette: Palette, imageURL: Option[String] ) object FeastCollectionTheme { implicit val format: OFormat[FeastCollectionTheme] = Json.format[FeastCollectionTheme] } sealed trait EditionsCardMetadata object EditionsCardMetadata { implicit val format: OFormat[EditionsCardMetadata] = Json.format[EditionsCardMetadata] } case class EditionsArticleMetadata( headline: Option[String], customKicker: Option[String], trailText: Option[String], showQuotedHeadline: Option[Boolean], showByline: Option[Boolean], byline: Option[String], sportScore: Option[String], mediaType: Option[MediaType], // keep overrides even if not used so user can switch back w/out needing to re-crop cutoutImage: Option[Image], replaceImage: Option[Image], overrideArticleMainMedia: Option[Boolean], coverCardImages: Option[CoverCardImages], promotionMetric: Option[Double] ) extends EditionsCardMetadata object EditionsArticleMetadata { implicit val format: OFormat[EditionsArticleMetadata] = Json.format[EditionsArticleMetadata] val default = EditionsArticleMetadata( None, None, None, None, None, None, None, None, None, None, None, None, None ) } sealed abstract class CardType extends EnumEntry with Uncapitalised with Hyphencase object CardType extends PlayEnum[CardType] { case object Article extends CardType case object Recipe extends CardType case object Chef extends CardType case object FeastCollection extends CardType override def values = findValues } /** A Card for Editions-based platforms. Analogous to the `Trail` type in * facia-scala-client. * * Distinct from `Trail` because Editions Cards include Feast-specific entities * that are not available in facia-scala-client. */ sealed trait EditionsCard { val id: String val addedOn: Long val cardType: CardType def toSkeleton: EditionsCardSkeleton = this match { case EditionsArticle(id, _, metadata) => EditionsCardSkeleton(id, cardType, metadata) case EditionsRecipe(id, _) => EditionsCardSkeleton(id, cardType) case EditionsChef(id, _, metadata) => EditionsCardSkeleton(id, cardType, metadata) case EditionsFeastCollection(id, _, metadata) => EditionsCardSkeleton(id, cardType, metadata) } } object EditionsCard { implicit val format: OFormat[EditionsCard] = Json.format[EditionsCard] def fromRowOpt( rs: WrappedResultSet, prefix: String = "" ): Option[EditionsCard] = { for { id <- rs.stringOpt(prefix + "id") cardType <- rs .stringOpt(prefix + "card_type") .flatMap(CardType.withNameOption) addedOn <- rs .zonedDateTimeOpt(prefix + "added_on") .map(_.toInstant.toEpochMilli) } yield cardType match { case CardType.Article => EditionsArticle( id, addedOn, metadata = extractMetadata[EditionsArticleMetadata]( rs, prefix, EditionsArticleMetadata.default ) ) case CardType.Chef => EditionsChef( id, addedOn, metadata = extractMetadata[EditionsChefMetadata]( rs, prefix, EditionsChefMetadata() ) ) case CardType.Recipe => EditionsRecipe(id, addedOn) case CardType.FeastCollection => EditionsFeastCollection( id, addedOn, metadata = extractMetadata[EditionsFeastCollectionMetadata]( rs, prefix, EditionsFeastCollectionMetadata() ) ) } } def getMetadataJson(card: EditionsCard): Option[String] = card match { case EditionsArticle(_, _, metadata) => maybeJson(metadata) case EditionsChef(_, _, metadata) => maybeJson(metadata) case EditionsFeastCollection(_, _, metadata) => maybeJson(metadata) case _ => Some("{}") } private def maybeJson[T](maybeModel: Option[T])(implicit writes: Writes[T]) = maybeModel.map(m => Json.toJson(m).toString) private def extractMetadata[T]( rs: WrappedResultSet, prefix: String, default: T )(implicit reads: Reads[T]): Option[T] = rs .stringOpt(prefix + "metadata") .map(s => Json.parse(s).validate[T] match { case result if result.isError => logger.error(s"Unable to parse card from database: \n$s") default case result => result.get } ) } case class EditionsArticle( id: String, addedOn: Long, metadata: Option[EditionsArticleMetadata] ) extends EditionsCard with Logging { val cardType: CardType = CardType.Article } object EditionsArticle extends Logging { implicit val format: OFormat[EditionsArticle] = Json.format[EditionsArticle] def toPublishedArticle(editionsArticle: EditionsArticle): PublishedArticle = { var mediaType: Option[MediaType] = editionsArticle.metadata.flatMap(_.mediaType) val coverCardImages = editionsArticle.metadata.flatMap { meta => meta.mediaType match { case Some(MediaType.CoverCard) => val mobile = meta.coverCardImages.flatMap(_.mobile) val tablet = meta.coverCardImages.flatMap(_.tablet) (mobile, tablet) match { case (Some(m), Some(t)) => Some(PublishedCardImage(m.toPublishedImage, t.toPublishedImage)) case _ => logger.warn( s"Failed to convert card images, must define both mobile and tablet images, falling back to use article trail" ) mediaType = mediaType.map(_ => MediaType.UseArticleTrail) None } case _ => None } } val imageSrcOverride: Option[PublishedImage] = editionsArticle.metadata.flatMap(meta => { mediaType match { case Some(MediaType.Image) => meta.replaceImage.map(_.toPublishedImage) case Some(MediaType.Cutout) => meta.cutoutImage.map(_.toPublishedImage) case _ => None } }) PublishedArticle( editionsArticle.id.toLong, PublishedFurniture( kicker = editionsArticle.metadata.flatMap(_.customKicker), headlineOverride = editionsArticle.metadata.flatMap(_.headline), trailTextOverride = editionsArticle.metadata.flatMap(_.trailText), bylineOverride = editionsArticle.metadata.flatMap(_.byline), showByline = editionsArticle.metadata .flatMap(_.showByline) .getOrElse(PublishedArticle.SHOW_BYLINE_DEFAULT), showQuotedHeadline = editionsArticle.metadata .flatMap(_.showQuotedHeadline) .getOrElse(PublishedArticle.SHOW_QUOTED_HEADLINE_DEFAULT), mediaType = mediaType .map(_.toPublishedMediaType) .getOrElse(PublishedMediaType.UseArticleTrail), imageSrcOverride = imageSrcOverride, sportScore = editionsArticle.metadata.flatMap(_.sportScore), overrideArticleMainMedia = editionsArticle.metadata .flatMap(_.overrideArticleMainMedia) .getOrElse(PublishedArticle.OVERRIDE_ARTICLE_MAIN_MEDIA_DEFAULT), coverCardImages = coverCardImages ) ) } } // Only certain cards are permitted within a FeastCollection. sealed trait EditionsFeastCollectionItem extends EditionsCard object EditionsFeastCollectionItem { implicit val format: OFormat[EditionsFeastCollectionItem] = Json.format[EditionsFeastCollectionItem] } case class EditionsRecipe(id: String, addedOn: Long) extends EditionsCard with EditionsFeastCollectionItem { val cardType: CardType = CardType.Recipe } object EditionsRecipe { implicit val format: OFormat[EditionsRecipe] = Json.format[EditionsRecipe] } case class EditionsChefMetadata( bio: Option[String] = None, theme: Option[ChefTheme] = None, chefImageOverride: Option[Image] = None ) extends EditionsCardMetadata object EditionsChefMetadata { implicit val format: OFormat[EditionsChefMetadata] = Json.format[EditionsChefMetadata] } case class EditionsChef( id: String, addedOn: Long, metadata: Option[EditionsChefMetadata] ) extends EditionsCard with EditionsFeastCollectionItem { val cardType: CardType = CardType.Chef } object EditionsChef { implicit val format: OFormat[EditionsChef] = Json.format[EditionsChef] } case class EditionsFeastCollectionMetadata( title: Option[String] = None, theme: Option[FeastCollectionTheme] = None, collectionItems: List[EditionsFeastCollectionItem] = List.empty ) extends EditionsCardMetadata object EditionsFeastCollectionMetadata { implicit val format: OFormat[EditionsFeastCollectionMetadata] = Json.format[EditionsFeastCollectionMetadata] } case class EditionsFeastCollection( id: String, addedOn: Long, metadata: Option[EditionsFeastCollectionMetadata] ) extends EditionsCard { val cardType: CardType = CardType.FeastCollection } object EditionsFeastCollection { implicit val format: OFormat[EditionsFeastCollection] = Json.format[EditionsFeastCollection] }