-
Notifications
You must be signed in to change notification settings - Fork 16
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add Format
to the Scala CAPI client
#312
Changes from 9 commits
4a3e4a2
4f3d909
8f64102
daeb03c
6647d62
9df2764
d6938e2
fa4b130
5597e9e
b8dc1c4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,44 +1,50 @@ | ||
package com.gu.contentapi.client.utils | ||
|
||
import com.gu.contentapi.client.model.v1._ | ||
import com.gu.contentapi.client.utils.format._ | ||
|
||
import java.time.OffsetDateTime | ||
import java.time.format.DateTimeFormatter | ||
|
||
object CapiModelEnrichment { | ||
|
||
implicit class RichCapiDateTime(val cdt: CapiDateTime) extends AnyVal { | ||
def toOffsetDateTime: OffsetDateTime = OffsetDateTime.parse(cdt.iso8601) | ||
} | ||
type ContentFilter = Content => Boolean | ||
|
||
implicit class RichOffsetDateTime(val dt: OffsetDateTime) extends AnyVal { | ||
def toCapiDateTime: CapiDateTime = CapiDateTime.apply(dt.toInstant.toEpochMilli, DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(dt)) | ||
} | ||
def getFromPredicate[T](content: Content, predicates: List[(ContentFilter, T)]): Option[T] = | ||
predicates.collectFirst { case (predicate, t) if predicate(content) => t } | ||
|
||
implicit class RichContent(val content: Content) extends AnyVal { | ||
def tagExistsWithId(tagId: String): ContentFilter = content => content.tags.exists(tag => tag.id == tagId) | ||
|
||
def designType: DesignType = { | ||
def displayHintExistsWithName(displayHintName: String): ContentFilter = content => content.fields.flatMap(_.displayHint).contains(displayHintName) | ||
|
||
val defaultDesignType = Article | ||
def isLiveBloggingNow: ContentFilter = content => content.fields.flatMap(_.liveBloggingNow).contains(true) | ||
|
||
type ContentFilter = Content => Boolean | ||
val isImmersive: ContentFilter = content => displayHintExistsWithName("immersive")(content) | ||
|
||
val isImmersive: ContentFilter = c => c.fields.flatMap(_.displayHint).contains("immersive") | ||
val isMedia: ContentFilter = content => tagExistsWithId("type/audio")(content) || tagExistsWithId("type/video")(content) || tagExistsWithId("type/gallery")(content) | ||
|
||
def tagExistsWithId(tagId: String): ContentFilter = c => c.tags.exists(tag => tag.id == tagId) | ||
val isReview: ContentFilter = content => tagExistsWithId("tone/reviews")(content) || tagExistsWithId("tone/livereview")(content) || tagExistsWithId("tone/albumreview")(content) | ||
|
||
val isMedia: ContentFilter = c => tagExistsWithId("type/audio")(c) || tagExistsWithId("type/video")(c) || tagExistsWithId("type/gallery")(c) | ||
val isLiveBlog: ContentFilter = content => isLiveBloggingNow(content) && tagExistsWithId("tone/minutebyminute")(content) | ||
|
||
val isReview: ContentFilter = c => tagExistsWithId("tone/reviews")(c) || tagExistsWithId("tone/livereview")(c) || tagExistsWithId("tone/albumreview")(c) | ||
val isDeadBlog: ContentFilter = content => !isLiveBloggingNow(content) && tagExistsWithId("tone/minutebyminute")(content) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We'll need guardian/types#83 to match up with Types |
||
|
||
def isComment: ContentFilter = c => tagExistsWithId("tone/comment")(c) || tagExistsWithId("tone/letters")(c) | ||
implicit class RichCapiDateTime(val cdt: CapiDateTime) extends AnyVal { | ||
def toOffsetDateTime: OffsetDateTime = OffsetDateTime.parse(cdt.iso8601) | ||
} | ||
|
||
implicit class RichOffsetDateTime(val dt: OffsetDateTime) extends AnyVal { | ||
def toCapiDateTime: CapiDateTime = CapiDateTime.apply(dt.toInstant.toEpochMilli, DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(dt)) | ||
} | ||
|
||
def liveBloggingNow: Boolean = content.fields.flatMap(_.liveBloggingNow).contains(true) | ||
implicit class RichContent(val content: Content) extends AnyVal { | ||
|
||
val liveBlog: ContentFilter = liveBloggingNow && tagExistsWithId("tone/minutebyminute")(_) | ||
def designType: DesignType = { | ||
|
||
val deadBlog: ContentFilter = !liveBloggingNow && tagExistsWithId("tone/minutebyminute")(_) | ||
val isComment: ContentFilter = content => tagExistsWithId("tone/comment")(content) || tagExistsWithId("tone/letters")(content) | ||
val defaultDesignType: DesignType = Article | ||
|
||
val predicates: List[(ContentFilter, DesignType)] = List ( | ||
val predicates: List[(ContentFilter, DesignType)] = List( | ||
tagExistsWithId("tone/advertisement-features") -> AdvertisementFeature, | ||
tagExistsWithId("tone/matchreports") -> MatchReport, | ||
tagExistsWithId("tone/quizzes") -> Quiz, | ||
|
@@ -51,12 +57,146 @@ object CapiModelEnrichment { | |
tagExistsWithId("tone/analysis") -> Analysis, | ||
isComment -> Comment, | ||
tagExistsWithId("tone/features") -> Feature, | ||
liveBlog -> Live, | ||
deadBlog -> Article | ||
isLiveBlog -> Live, | ||
isDeadBlog -> Article | ||
buck06191 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
) | ||
|
||
val result = predicates.collectFirst { case (predicate, design) if predicate(content) => design } | ||
val result = getFromPredicate(content, predicates) | ||
result.getOrElse(defaultDesignType) | ||
} | ||
} | ||
|
||
implicit class RenderingFormat(val content: Content) extends AnyVal { | ||
|
||
|
||
def design: Design = { | ||
|
||
val defaultDesign: Design = ArticleDesign | ||
|
||
val isPhotoEssay: ContentFilter = content => content.fields.flatMap(_.displayHint).contains("photoessay") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Did you want to reuse your val isPhotoEssay: ContentFilter = content => displayHintExistsWithName("photoessay")(content) |
||
|
||
val isInteractive: ContentFilter = content => content.`type` == ContentType.Interactive | ||
|
||
val predicates: List[(ContentFilter, Design)] = List( | ||
tagExistsWithId("artanddesign/series/guardian-print-shop") -> PrintShopDesign, | ||
isMedia -> MediaDesign, | ||
isReview -> ReviewDesign, | ||
tagExistsWithId("tone/analysis") -> AnalysisDesign, | ||
tagExistsWithId("tone/comment") -> CommentDesign, | ||
tagExistsWithId("tone/letters") -> LetterDesign, | ||
tagExistsWithId("tone/features") -> FeatureDesign, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we are missing There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's a good question. We don't currently have There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Letter is a recent addition: guardian/types#69 |
||
tagExistsWithId("tone/recipes") -> RecipeDesign, | ||
tagExistsWithId("tone/matchreports") -> MatchReportDesign, | ||
tagExistsWithId("tone/interview") -> InterviewDesign, | ||
tagExistsWithId("tone/editorials") -> EditorialDesign, | ||
tagExistsWithId("tone/quizzes") -> QuizDesign, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are we also missing There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think we technically support an There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should the be an There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's in the apps-rendering decision functions, so I'll add it in. If it's in here and then removed, it's not a difficult thing to rip out (which is part of the motivation for this work in the first place). I think that the work here should be done with a focus on targeting a specific version of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I'm curious as to how DCR models these kinds of articles if it doesn't use the |
||
isInteractive -> InteractiveDesign, | ||
isPhotoEssay -> PhotoEssayDesign, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @oliverlloyd are we keeping Photo Essays as a separate There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There is still at least one design aspects of Photo Essays that is unique: coloured captions. So unless we lose that it's here to stay |
||
isLiveBlog -> LiveBlogDesign, | ||
isDeadBlog -> DeadBlogDesign | ||
) | ||
|
||
val result = getFromPredicate(content, predicates) | ||
result.getOrElse(defaultDesign) | ||
} | ||
|
||
def theme: Theme = { | ||
val defaultTheme: Theme = NewsPillar | ||
|
||
val specialReportTags: Set[String] = Set( | ||
"business/series/undercover-in-the-chicken-industry", | ||
"business/series/britains-debt-timebomb", | ||
"world/series/this-is-europe", | ||
"environment/series/the-polluters", | ||
"news/series/hsbc-files", | ||
"news/series/panama-papers", | ||
"us-news/homan-square", | ||
"uk-news/series/the-new-world-of-work", | ||
"world/series/the-new-arrivals", | ||
"news/series/nauru-files", | ||
"us-news/series/counted-us-police-killings", | ||
"australia-news/series/healthcare-in-detention", | ||
"society/series/this-is-the-nhs" | ||
) | ||
|
||
def isPillar(pillar: String): ContentFilter = content => content.pillarName.contains(pillar) | ||
|
||
val isSpecialReport: ContentFilter = content => content.tags.exists(t => specialReportTags(t.id)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What happens for special reports assigned in the targeting tool? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm, I did this off the back of different decision functions I found across the different codebases. If there's one missing, then we'll need to implement it in a future version. I imagine the process of integrating this all into the platforms will highlight missing areas and then we can update this to reflect that . |
||
val isOpinion: ContentFilter = content => (tagExistsWithId("tone/comment")(content) && isPillar("News")(content)) || | ||
isPillar("Opinion")(content) | ||
val isCulture: ContentFilter = content => isPillar("Arts")(content) || isPillar("Books")(content) | ||
|
||
val predicates: List[(ContentFilter, Theme)] = List( | ||
isOpinion -> OpinionPillar, | ||
isPillar("Sport") -> SportPillar, | ||
isCulture -> CulturePillar, | ||
isPillar("Lifestyle") -> LifestylePillar, | ||
isSpecialReport -> SpecialReportTheme, | ||
tagExistsWithId("tone/advertisement-features") -> Labs, | ||
) | ||
|
||
val result = getFromPredicate(content, predicates) | ||
result.getOrElse(defaultTheme) | ||
} | ||
|
||
def display: Display = { | ||
|
||
val defaultDisplay = StandardDisplay | ||
|
||
def hasShowcaseImage: ContentFilter = content => { | ||
val hasShowcaseImage = for { | ||
blocks <- content.blocks | ||
main <- blocks.main | ||
mainMedia = main.elements.head | ||
imageTypeData <- mainMedia.imageTypeData | ||
imageRole <- imageTypeData.role | ||
} yield { | ||
imageRole == "showcase" | ||
} | ||
hasShowcaseImage.getOrElse(false) | ||
} | ||
|
||
def hasShowcaseEmbed: ContentFilter = content => { | ||
|
||
def isMainEmbed(elem: Element): Boolean = elem.relation == "main" && elem.`type` == ElementType.Embed | ||
|
||
def hasShowcaseAsset(assets: scala.collection.Seq[Asset]): Boolean = { | ||
val isShowcaseAsset = for { | ||
embedAsset <- assets.find(asset => asset.`type` == AssetType.Embed) | ||
typeData <- embedAsset.typeData | ||
role <- typeData.role | ||
} yield { | ||
role == "showcase" | ||
} | ||
isShowcaseAsset.getOrElse(false) | ||
} | ||
|
||
val hasShowcaseEmbed = for { | ||
elements <- content.elements | ||
mainEmbed <- elements.find(isMainEmbed) | ||
} yield { | ||
hasShowcaseAsset(mainEmbed.assets) | ||
} | ||
|
||
hasShowcaseEmbed.getOrElse(false) | ||
} | ||
|
||
val isShowcase: ContentFilter = content => displayHintExistsWithName("column")(content) || | ||
displayHintExistsWithName("showcase")(content) || | ||
hasShowcaseImage(content) || | ||
hasShowcaseEmbed(content) | ||
|
||
val isNumberedList: ContentFilter = displayHintExistsWithName("numberedList") | ||
|
||
val predicates: List[(ContentFilter, Display)] = List( | ||
isImmersive -> ImmersiveDisplay, | ||
isShowcase -> ShowcaseDisplay, | ||
isNumberedList -> NumberedListDisplay | ||
Comment on lines
+192
to
+194
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A note here that we need to update https://github.com/guardian/types/blob/main/src/format.ts#L37-L43 to remove |
||
) | ||
|
||
val result = getFromPredicate(content, predicates) | ||
result.getOrElse(defaultDisplay) | ||
} | ||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
package com.gu.contentapi.client.utils.format | ||
|
||
sealed trait Design | ||
|
||
case object ArticleDesign extends Design | ||
case object MediaDesign extends Design | ||
case object ReviewDesign extends Design | ||
case object AnalysisDesign extends Design | ||
case object CommentDesign extends Design | ||
case object LetterDesign extends Design | ||
case object FeatureDesign extends Design | ||
case object LiveBlogDesign extends Design | ||
case object DeadBlogDesign extends Design | ||
case object RecipeDesign extends Design | ||
case object MatchReportDesign extends Design | ||
case object InterviewDesign extends Design | ||
case object EditorialDesign extends Design | ||
case object QuizDesign extends Design | ||
case object InteractiveDesign extends Design | ||
case object PhotoEssayDesign extends Design | ||
case object PrintShopDesign extends Design |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
package com.gu.contentapi.client.utils.format | ||
|
||
sealed trait Display | ||
|
||
case object StandardDisplay extends Display | ||
case object ImmersiveDisplay extends Display | ||
case object ShowcaseDisplay extends Display | ||
case object NumberedListDisplay extends Display | ||
case object ColumnDisplay extends Display |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
package com.gu.contentapi.client.utils.format | ||
|
||
sealed trait Theme | ||
|
||
sealed trait Pillar extends Theme | ||
sealed trait Special extends Theme | ||
|
||
case object NewsPillar extends Pillar | ||
case object OpinionPillar extends Pillar | ||
case object SportPillar extends Pillar | ||
case object CulturePillar extends Pillar | ||
case object LifestylePillar extends Pillar | ||
case object SpecialReportTheme extends Special | ||
case object Labs extends Special |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
AR also has
type/picture
in this category, see guardian/apps-rendering#1146.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for this. I'll add it to a card for changes to make on a future version