Skip to content

Commit

Permalink
feat(author): add latest work
Browse files Browse the repository at this point in the history
  • Loading branch information
mlbrgl committed Feb 28, 2024
1 parent c90404f commit 6c308b6
Show file tree
Hide file tree
Showing 10 changed files with 187 additions and 10 deletions.
30 changes: 30 additions & 0 deletions db/model/Gdoc/GdocAuthor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,19 @@ import {
RawBlockText,
OwidEnrichedGdocBlock,
OwidGdocErrorMessageType,
DbEnrichedLatestWork,
} from "@ourworldindata/utils"
import { GdocBase } from "./GdocBase.js"
import { htmlToEnrichedTextBlock } from "./htmlToEnriched.js"
import { parseSocials } from "./rawToEnriched.js"
import { getLatestWorkByAuthor } from "../Post.js"
import * as db from "../../../db/db.js"

@Entity("posts_gdocs")
export class GdocAuthor extends GdocBase implements OwidGdocAuthorInterface {
@Column({ default: "{}", type: "json" })
content!: OwidGdocAuthorContent
latestWorkLinks?: DbEnrichedLatestWork[]

constructor(id?: string) {
super(id)
Expand All @@ -25,6 +29,32 @@ export class GdocAuthor extends GdocBase implements OwidGdocAuthorInterface {
return gdoc.content.bio ?? []
}

_loadSubclassAttachments = async (): Promise<void> => {
if (!this.content.title) return
const knex = db.knexInstance()

this.latestWorkLinks = await getLatestWorkByAuthor(
knex,
this.content.title
)
if (!this.latestWorkLinks) return

// We want to load additional image filenames from the referenced
// latest work links. We're not relying here on the Link
// infrastructure as we don't yet have a good way of cleaning up
// obsolete links from the latest work section. Usually the links
// originating from a gdoc are cleaned up when updating/deleting it, but
// here the links from the latest section will change independently of a
// manual authoring and admin action, so they'll end up being stale
// until the author page is updated again.
const latestWorkImageFilenames = this.latestWorkLinks
.map((d) => d["featured-image"])
.filter(Boolean)

// Load the image metadata for the latest work images
return super.loadImageMetadata(latestWorkImageFilenames)
}

_enrichSubclassContent = (content: Record<string, any>): void => {
if (content.bio) {
content.bio = content.bio.map((html: RawBlockText) =>
Expand Down
21 changes: 14 additions & 7 deletions db/model/Gdoc/GdocBase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -710,13 +710,20 @@ export class GdocBase extends BaseEntity implements OwidGdocBaseInterface {
this.linkedDocuments = keyBy(linkedDocuments, "id")
}

async loadImageMetadata(): Promise<void> {
if (this.linkedImageFilenames.length) {
await imageStore.fetchImageMetadata(this.linkedImageFilenames)
const images = await imageStore
.syncImagesToS3()
.then(excludeUndefined)
this.imageMetadata = keyBy(images, "filename")
async loadImageMetadata(filenames?: string[]): Promise<void> {
const imagesFilenames = filenames ?? this.linkedImageFilenames

if (!imagesFilenames.length) return

await imageStore.fetchImageMetadata(imagesFilenames)
const images = await imageStore.syncImagesToS3().then(excludeUndefined)

// Merge the new image metadata with the existing image metadata. This
// is used by GdocAuthor to load additional image metadata from the
// latest work section.
this.imageMetadata = {
...this.imageMetadata,
...keyBy(images, "filename"),
}
}

Expand Down
20 changes: 20 additions & 0 deletions db/model/Gdoc/rawToEnriched.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ import {
EnrichedBlockSocials,
EnrichedSocialLink,
SocialLinkType,
RawBlockLatestWork,
} from "@ourworldindata/types"
import {
traverseEnrichedSpan,
Expand Down Expand Up @@ -1652,13 +1653,18 @@ function parseResearchAndWritingBlock(
heading: "",
articles: [],
},
latest: EnrichedBlockResearchAndWritingRow = {
heading: "",
articles: [],
},
rows: EnrichedBlockResearchAndWritingRow[] = []
): EnrichedBlockResearchAndWriting => ({
type: "research-and-writing",
heading,
primary,
secondary,
more,
latest,
rows,
parseErrors: [error],
})
Expand Down Expand Up @@ -1752,8 +1758,21 @@ function parseResearchAndWritingBlock(
return { heading: "", articles: [] }
}

const parseRowLatest = (
rawRow: RawBlockLatestWork
): EnrichedBlockResearchAndWritingRow | undefined => {
if (rawRow.heading && typeof rawRow.heading !== "string") {
parseErrors.push({ message: `"heading" must be a string` })
return
}
return { heading: rawRow.heading || "", articles: [] }
}

const more = raw.value.more ? parseRow(raw.value.more, true) : undefined
const rows = raw.value.rows?.map((row) => parseRow(row)) || []
const latest = raw.value.latest
? parseRowLatest(raw.value.latest)
: undefined

return {
type: "research-and-writing",
Expand All @@ -1762,6 +1781,7 @@ function parseResearchAndWritingBlock(
secondary,
more,
rows,
latest,
parseErrors,
}
}
Expand Down
36 changes: 36 additions & 0 deletions db/model/Post.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ import {
Tag,
DataPageRelatedResearch,
OwidGdocType,
DbRawLatestWork,
DbEnrichedLatestWork,
parseLatestWork,
} from "@ourworldindata/types"
import { uniqBy, sortBy, memoize, orderBy } from "@ourworldindata/utils"
import { Knex } from "knex"
Expand Down Expand Up @@ -549,3 +552,36 @@ export const getRelatedResearchAndWritingForVariable = async (
// here we deduplicate by url. The first item is retained by uniqBy, latter ones are discarded.
return uniqBy(allSortedRelatedResearch, "url")
}

export const getLatestWorkByAuthor = async (
knex: Knex<any, any[]>,
author: string
): Promise<DbEnrichedLatestWork[]> => {
const rawLatestWorkLinks: DbRawLatestWork[] = await db.knexRaw(
`
SELECT
pg.slug,
pg.content->>'$.title' AS title,
pg.content->>'$.authors' AS authors,
pg.content->>'$."featured-image"' AS "featured-image",
pg.publishedAt
FROM
posts_gdocs pg
WHERE
pg.content ->> '$.authors' LIKE ?
AND pg.published = TRUE
AND pg.content->>'$.type' = "${OwidGdocType.Article}"
`,
knex,
[`%${author}%`]
)

// We're sorting in JS because of the "Out of sort memory, consider
// increasing server sort buffer size" error when using ORDER BY. Adding an
// index on the publishedAt column doesn't help.
return sortBy(
rawLatestWorkLinks.map((work) => parseLatestWork(work)),
// Sort by most recent first
(work) => -work.publishedAt! // "!" because we're only selecting published posts, so publishedAt can't be NULL
)
}
24 changes: 24 additions & 0 deletions packages/@ourworldindata/types/src/domainTypes/Author.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
export interface DbRawLatestWork {
slug: string
title: string
authors: string
"featured-image": string
publishedAt: Date | null
}

export interface DbEnrichedLatestWork {
slug: string
title: string
authors: string[]
"featured-image": string
publishedAt: Date | null
}

export const parseLatestWork = (
latestWork: DbRawLatestWork
): DbEnrichedLatestWork => {
return {
...latestWork,
authors: JSON.parse(latestWork.authors),
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -535,6 +535,10 @@ export type RawBlockResearchAndWritingRow = {
articles?: RawBlockResearchAndWritingLink[]
}

export type RawBlockLatestWork = {
heading?: string
}

export type RawBlockResearchAndWriting = {
type: "research-and-writing"
value: {
Expand All @@ -548,6 +552,7 @@ export type RawBlockResearchAndWriting = {
| RawBlockResearchAndWritingLink[]
more?: RawBlockResearchAndWritingRow
rows?: RawBlockResearchAndWritingRow[]
latest?: RawBlockLatestWork
}
}

Expand All @@ -573,6 +578,7 @@ export type EnrichedBlockResearchAndWriting = {
secondary: EnrichedBlockResearchAndWritingLink[]
more?: EnrichedBlockResearchAndWritingRow
rows: EnrichedBlockResearchAndWritingRow[]
latest?: EnrichedBlockResearchAndWritingRow
} & EnrichedBlockWithParseErrors

export type RawBlockSDGToc = {
Expand Down
2 changes: 2 additions & 0 deletions packages/@ourworldindata/types/src/gdocTypes/Gdoc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
RefDictionary,
} from "./ArchieMlComponents.js"
import { DbChartTagJoin } from "../dbTypes/ChartTags.js"
import { DbEnrichedLatestWork } from "../domainTypes/Author.js"

export enum OwidGdocPublicationContext {
unlisted = "unlisted",
Expand Down Expand Up @@ -151,6 +152,7 @@ export interface OwidGdocAuthorContent {

export interface OwidGdocAuthorInterface extends OwidGdocBaseInterface {
content: OwidGdocAuthorContent
latestWorkLinks?: DbEnrichedLatestWork[]
}

export type OwidGdocContent =
Expand Down
7 changes: 7 additions & 0 deletions packages/@ourworldindata/types/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@ export {
type RawBlockRecirc,
type RawBlockResearchAndWriting,
type RawBlockResearchAndWritingLink,
type RawBlockLatestWork,
type RawBlockScroller,
type RawBlockSDGGrid,
type RawBlockSDGToc,
Expand Down Expand Up @@ -630,3 +631,9 @@ export {
} from "./dbTypes/Variables.js"

export { RedirectCode, type DbPlainRedirect } from "./dbTypes/Redirects.js"

export {
type DbRawLatestWork,
type DbEnrichedLatestWork,
parseLatestWork,
} from "./domainTypes/Author.js"
4 changes: 4 additions & 0 deletions site/gdocs/OwidGdoc.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
MinimalDataInsightInterface,
OwidGdocMinimalPostInterface,
OwidGdocHomepageMetadata,
DbEnrichedLatestWork,
} from "@ourworldindata/types"
import { get, getOwidGdocFromJSON } from "@ourworldindata/utils"
import { DebugProvider } from "./DebugContext.js"
Expand All @@ -28,6 +29,7 @@ export const AttachmentsContext = createContext<{
relatedCharts: RelatedChart[]
latestDataInsights?: MinimalDataInsightInterface[]
homepageMetadata?: OwidGdocHomepageMetadata
latestWorkLinks?: DbEnrichedLatestWork[]
}>({
linkedDocuments: {},
imageMetadata: {},
Expand All @@ -36,6 +38,7 @@ export const AttachmentsContext = createContext<{
relatedCharts: [],
latestDataInsights: [],
homepageMetadata: {},
latestWorkLinks: [],
})

export const DocumentContext = createContext<{ isPreviewing: boolean }>({
Expand Down Expand Up @@ -115,6 +118,7 @@ export function OwidGdoc({
relatedCharts: get(props, "relatedCharts", []),
latestDataInsights: get(props, "latestDataInsights", []),
homepageMetadata: get(props, "homepageMetadata", []),
latestWorkLinks: get(props, "latestWorkLinks", []),
}}
>
<DocumentContext.Provider value={{ isPreviewing }}>
Expand Down
47 changes: 44 additions & 3 deletions site/gdocs/components/ResearchAndWriting.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,12 @@ import {
import { useLinkedDocument } from "../utils.js"
import { formatAuthors } from "../../clientFormatting.js"
import Image from "./Image.js"
import { DocumentContext } from "../OwidGdoc.js"
import { RESEARCH_AND_WRITING_DEFAULT_HEADING } from "@ourworldindata/types"
import { AttachmentsContext, DocumentContext } from "../OwidGdoc.js"
import {
DbEnrichedLatestWork,
RESEARCH_AND_WRITING_DEFAULT_HEADING,
} from "@ourworldindata/types"
import { BAKED_BASE_URL } from "../../../settings/clientSettings.js"

type ResearchAndWritingProps = {
className?: string
Expand Down Expand Up @@ -90,11 +94,30 @@ function ResearchAndWritingLinkContainer(
)
}

const parseLatestWorkToResearchAndWritingLink = (
latestWork: DbEnrichedLatestWork
): EnrichedBlockResearchAndWritingLink => {
return {
value: {
...latestWork,
url: `${BAKED_BASE_URL}/${latestWork.slug}`,
filename: latestWork["featured-image"],
},
}
}

export function ResearchAndWriting(props: ResearchAndWritingProps) {
const { heading, primary, secondary, more, rows, className } = props
const { heading, primary, secondary, more, rows, latest, className } = props

const slug = heading ? slugify(heading) : RESEARCH_AND_WRITING_ID

const { latestWorkLinks } = useContext(AttachmentsContext)
if (latest && latestWorkLinks) {
latest.articles = latestWorkLinks.map(
parseLatestWorkToResearchAndWritingLink
)
}

return (
<div className={cx(className, "grid")}>
<h1
Expand Down Expand Up @@ -155,6 +178,24 @@ export function ResearchAndWriting(props: ResearchAndWritingProps) {
</div>
</div>
) : null}
{latest ? (
<div className="span-cols-12 research-and-writing-row">
<h2 className="h2-bold">
{latest.heading || "Latest work"}
</h2>
<div className="grid grid-cols-4 grid-lg-cols-3 grid-md-cols-2 research-and-writing-row__link-container">
{latest.articles.map((link, i) => (
<ResearchAndWritingLinkContainer
shouldHideSubtitle
isSmall
className="span-cols-1"
key={i}
{...link}
/>
))}
</div>
</div>
) : null}
</div>
)
}

0 comments on commit 6c308b6

Please sign in to comment.