- {this.renderUserMenu()}
- {this.numAvailableRowsForSelectedUser > 0 || !this.listMode ? (
-
- {this.renderGraphers()}
- {this.renderControls()}
- {this.renderMeta()}
-
- ) : (
-
-
- ⚠️ 0 pending chart revisions found. All
- suggested chart revisions have already been
- approved, flagged, or rejected.
-
-
- If you wish to see all suggested chart revisions,
- either uncheck the{" "}
- Show "pending" revisions only box in the
- Settings tab or{" "}
-
- click here
- {" "}
- to view a complete list of suggested chart
- revisions.
-
-
- )}
-
- )
- }
-
- @action.bound onCurrentlyActiveUserChange(
- event: React.ChangeEvent
- ) {
- runInAction(() => {
- this.currentlyActiveUserId =
- event.currentTarget.value === "-1"
- ? undefined
- : parseInt(event.currentTarget.value)
-
- this.rowNum = 1
- })
-
- void this.refresh()
- }
-
- renderUserMenu() {
- const userOptions = [
- { userName: "All users", userId: -1 },
- ...this.availableUsers,
- ]
- return (
-
-
-
-
- )
- }
-
- renderGraphers() {
- // Render both charts next to each other
- console.log("renderGraphers")
- const gpt_model_name = this.getGPTModelNameUsed()
- return (
-
-
- {/* Original chart */}
-
- {this.currentSuggestedChartRevision && (
-
-
-
-
- Original
-
-
-
- {`(#${this.currentSuggestedChartRevision.chartId}, v${this.currentSuggestedChartRevision.originalConfig.version})`}
-
-
- Edit{" "}
-
-
-
-
- )}
- {this._isGraphersSet &&
- this.currentSuggestedChartRevision &&
- this.renderGrapher(
- this.currentSuggestedChartRevision
- .originalConfig
- )}
-
- {this.showExistingChart && (
-
- {this.currentSuggestedChartRevision && (
-
-
-
-
- Existing
-
-
-
- {`(#${this.currentSuggestedChartRevision.chartId}, V${this.currentSuggestedChartRevision.existingConfig.version})`}
-
-
- Edit{" "}
-
-
-
- {/*
- This is what the chart looks like right now on the OWID website.
-
*/}
-
- )}
- {this._isGraphersSet &&
- this.currentSuggestedChartRevision &&
- this.renderGrapher(
- this.currentSuggestedChartRevision
- .existingConfig
- )}
-
- )}
- {/* Suggested chart */}
-
- {this.currentSuggestedChartRevision && (
-
-
- {/* Title and link to edit */}
-
-
-
- Suggested
-
-
-
- {/* {`(#${this.currentSuggestedChartRevision.chartId}, V${this.currentSuggestedChartRevision.suggestedConfig.version})`} */}
-
-
- Edit as chart{" "}
- {
- this
- .currentSuggestedChartRevision
- .chartId
- }{" "}
-
-
-
- {/* GPT section */}
-
- {/* */}
-
- {/* */}
-
-
-
-
- )}
- {this._isGraphersSet &&
- this.currentSuggestedChartRevision &&
- this.renderGrapher(
- this.currentSuggestedChartRevision
- .suggestedConfig
- )}
-
-
-
- )
- }
-
- renderGrapher(grapherConfig: any) {
- console.log("renderGrapher")
- return (
-
- {this.previewSvgOrJson === "json" ? (
-
-
-
- {JSON.stringify(grapherConfig, null, 2)}
-
-
-
- ) : (
-
- )}
-
- )
- }
-
- renderControls() {
- // Render controls on how to navigate the approval
- return (
-
- {this.renderControlsNotes()}
- {this.renderControlsButtons()}
- {this.renderControlsNumberOfRevisions()}
-
- )
- }
-
- renderControlsNotes() {
- // Render textarea in the controls block
- return (
-
- )
- }
-
- renderControlsButtons() {
- // Render buttons in controls section
- return (
-
- {this.listMode && (
-
-
-
-
- )}
-
-
-
- {this.listMode && (
-
-
-
-
-
- )}
-
- )
- }
-
- renderControlsNumberOfRevisions() {
- // Render number of revisions block
- return (
- this.listMode && (
-
- Suggested revision
-
-
- of {this.numAvailableRowsForSelectedUser}
- {this.showPendingOnly ? " remaining" : ""} (
- View all)
-
-
- )
- )
- }
-
- renderMeta() {
- // Renders metadata block
- return (
-
-
-
Metadata
-
- -
- Suggested revision ID:{" "}
- {this.currentSuggestedChartRevision
- ? this.currentSuggestedChartRevision.id
- : ""}
-
- -
- Chart ID:{" "}
- {this.currentSuggestedChartRevision
- ? this.currentSuggestedChartRevision.chartId
- : ""}
-
- -
- Suggested revision created:{" "}
- {this.currentSuggestedChartRevision && (
-
- )}
-
-
- -
- Suggested revision last updated:{" "}
- {this.currentSuggestedChartRevision?.updatedAt && (
-
- )}
-
- -
- Reason for suggested revision:{" "}
- {this.currentSuggestedChartRevision &&
- this.currentSuggestedChartRevision.suggestedReason
- ? this.currentSuggestedChartRevision
- .suggestedReason
- : "None provided."}
-
-
-
-
-
References to original chart
-
-
-
-
Indicator changes
{" "}
- {this.currentSuggestedChartRevision &&
- this.currentSuggestedChartRevision.changesInDataSummary ? (
-
- ) : (
- "No summary provided."
- )}
-
-
- )
- }
-
- renderReadme() {
- // Render the readme (instructions on how to use the approval tool)
- return (
-
-
Terminology
-
- -
- Suggested (chart revision). A suggested chart
- revision is simply an amended OWID chart, but where the
- amendments have not yet been applied to the chart in
- question. A suggested chart revision is housed in the{" "}
-
suggested_chart_revisions
table in{" "}
- MySQL
. If the suggested chart revision gets
- approved, then the amendments are applied to the chart
- (which overwrites and republishes the chart).
-
- -
- Original (Original chart). The chart as it
- originally was when the suggested chart revision was
- created.
-
- -
- Existing (Existing chart). The chart as it
- currently exists on the OWID website.
-
-
-
How to use
-
- You are shown one suggested chart revision at a time,
- alongside the corresponding original chart as it was when
- the suggested chart revision was created.
-
-
- For each suggested revision, choose one of the following
- actions:
-
-
- -
- Approve the revision by clicking{" "}
-
- . This approves the suggestion, replacing the original
- chart with the suggested chart (also republishes the
- chart). Note: if a chart has been edited since the
- suggested revision was created, you will not be allowed
- to approve the suggested revision.
-
- -
- Reject the suggested revision by clicking{" "}
-
- . This rejects the suggestion, keeping the original
- chart as it is.
-
- -
- Flag the suggested revision for further
- inspection by clicking{" "}
-
- .
-
- -
- Edit the original chart by clicking{" "}
-
- Edit
-
- . This opens the original chart in the chart editor. If
- you save your changes to the original chart within the
- chart editor, you will no longer have the option to
- approve the suggested revision.
-
- -
-
- Edit the suggested chart revision as the original
- chart
- {" "}
- by clicking{" "}
-
- Edit as chart [chartId]{" "}
-
-
- . This opens the suggested chart revision in the chart
- editor. If you make and save changes to the chart within
- the chart editor,{" "}
-
- your config and data changes will be applied to the
- original chart, equivalent to approving it.
- {" "}
- Currently, the suggestion is not updated and the
- approval is left as pending, but you will no longer be
- able to approve it (since the chart has changed).
- Because it has actually been applied, you can now reject
- it.
-
-
-
Other useful information
-
- -
- When you click the{" "}
- {" "}
- ,{" "}
- {" "}
- or{" "}
- {" "}
- button, anything you write in the "Notes" text field
- will be saved. You can view these saved notes in the
- "Decision reason" column{" "}
- here. If
- you reject or flag a suggested chart revision, it is{" "}
- strongly recommended that you describe your
- reasoning in the "Notes" field.
-
- -
- If a suggested revision has been approved and the chart
- has not changed since the revision was approved, then
- you can undo the revision by clicking the{" "}
- {" "}
- button.
-
- -
- If a suggested revision has been rejected and the chart
- has not changed since the revision was rejected, then
- you can still approve the revision by clicking the{" "}
- {" "}
- button.
-
- -
- If one or more of the{" "}
- {" "}
- ,{" "}
- {" "}
- or{" "}
- {" "}
- buttons are disabled, this is because these actions are
- not allowed for the suggested revision in question. For
- example, if a chart has changed since the suggested
- revision was created, you will not be allowed to approve
- the revision.
-
-
-
- )
- }
-
- renderSettings() {
- console.log("renderSettings")
- // Render settings
- return (
-
- {/* {this.listMode && (
-
-
-
- )} */}
-
-
-
-
-
-
- Preview size (desktop only):
-
-
-
- {this.listMode && (
-
-
- Sort by:{" "}
-
-
-
-
- )}
-
- View SVG or JSON?
-
-
-
- Emulate vision deficiency:{" "}
-
- (this.simulateVisionDeficiency =
- option.deficiency)
- )}
- />
-
-
-
- )
- }
-}
diff --git a/adminSiteClient/SuggestedChartRevisionList.tsx b/adminSiteClient/SuggestedChartRevisionList.tsx
deleted file mode 100644
index a7ddb83e963..00000000000
--- a/adminSiteClient/SuggestedChartRevisionList.tsx
+++ /dev/null
@@ -1,197 +0,0 @@
-import React from "react"
-import { observer } from "mobx-react"
-import * as lodash from "lodash"
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome/index.js"
-import {
- faQuestionCircle,
- faCheckCircle,
- faTimesCircle,
- faExclamationCircle,
-} from "@fortawesome/free-solid-svg-icons"
-
-import { AdminAppContext, AdminAppContextType } from "./AdminAppContext.js"
-import { BAKED_GRAPHER_URL } from "../settings/clientSettings.js"
-import { ChartListItem } from "./ChartList.js"
-import { Link } from "./Link.js"
-import { SuggestedChartRevisionStatus } from "@ourworldindata/utils"
-import { Timeago } from "./Forms.js"
-
-export interface SuggestedChartRevisionListItem {
- id: number
- chartId: number
- createdByFullName: string
- updatedByFullName: string
- status: SuggestedChartRevisionStatus
- decisionReason: string
- suggestedReason: string
- createdAt: Date
- updatedAt: Date
- originalConfig: ChartListItem
-}
-
-@observer
-class SuggestedChartRevisionRow extends React.Component<{
- suggestedChartRevision: SuggestedChartRevisionListItem
- searchHighlight?: (text: string) => any
-}> {
- static contextType = AdminAppContext
- context!: AdminAppContextType
-
- render() {
- const { suggestedChartRevision, searchHighlight } = this.props
-
- const highlight = searchHighlight || lodash.identity
-
- return (
-
-
-
- {suggestedChartRevision.id}
-
- |
-
-
- {suggestedChartRevision.originalConfig.id}
-
- |
-
- {suggestedChartRevision.originalConfig.isPublished ? (
-
- {highlight(
- suggestedChartRevision.originalConfig.title ??
- ""
- )}
-
- ) : (
-
- Draft: {" "}
- {highlight(
- suggestedChartRevision.originalConfig.title ??
- ""
- )}
-
- )}{" "}
- {suggestedChartRevision.originalConfig.variantName ? (
-
- (
- {highlight(
- suggestedChartRevision.originalConfig
- .variantName
- )}
- )
-
- ) : undefined}
- {suggestedChartRevision.originalConfig.internalNotes && (
-
- {highlight(
- suggestedChartRevision.originalConfig
- .internalNotes
- )}
-
- )}
- |
-
- {suggestedChartRevision.suggestedReason
- ? suggestedChartRevision.suggestedReason
- : ""}
- |
-
-
- |
-
- {" "}
- {highlight(
- (
- suggestedChartRevision.status as unknown as string
- ).toUpperCase()
- )}
- {suggestedChartRevision.updatedByFullName && (
-
- )}
- |
-
- {suggestedChartRevision.decisionReason
- ? suggestedChartRevision.decisionReason
- : ""}
- |
-
- )
- }
-}
-
-@observer
-export class SuggestedChartRevisionList extends React.Component<{
- suggestedChartRevisions: SuggestedChartRevisionListItem[]
- searchHighlight?: (text: string) => any
-}> {
- static contextType = AdminAppContext
- context!: AdminAppContextType
-
- render() {
- const { suggestedChartRevisions, searchHighlight } = this.props
- return (
-
-
-
- Suggested revision Id |
- Chart Id |
- Title |
- Reason suggested |
- Revision suggested by |
- Status |
- Decision reason |
-
-
-
- {suggestedChartRevisions.map((suggestedChartRevision) => (
-
- ))}
-
-
- )
- }
-}
-
-@observer
-export class SuggestedChartRevisionStatusIcon extends React.Component<{
- status: SuggestedChartRevisionStatus
- setColor?: boolean
-}> {
- static defaultProps = {
- setColor: true,
- }
- render() {
- const { status, setColor } = this.props
- let color = "#9E9E9E"
- let icon = faQuestionCircle
- if (status === SuggestedChartRevisionStatus.approved) {
- color = "#0275d8"
- icon = faCheckCircle
- } else if (status === SuggestedChartRevisionStatus.rejected) {
- color = "#d9534f"
- icon = faTimesCircle
- } else if (status === SuggestedChartRevisionStatus.flagged) {
- color = "#f0ad4e"
- icon = faExclamationCircle
- }
- return
- }
-}
diff --git a/adminSiteClient/SuggestedChartRevisionListPage.tsx b/adminSiteClient/SuggestedChartRevisionListPage.tsx
deleted file mode 100644
index 92e5a7add60..00000000000
--- a/adminSiteClient/SuggestedChartRevisionListPage.tsx
+++ /dev/null
@@ -1,281 +0,0 @@
-import React from "react"
-import { observer } from "mobx-react"
-import { observable, computed, action } from "mobx"
-import fuzzysort from "fuzzysort"
-import { Link } from "react-router-dom"
-
-import { TextField } from "./Forms.js"
-import { AdminLayout } from "./AdminLayout.js"
-import {
- uniq,
- SortOrder,
- getStylesForTargetHeight,
-} from "@ourworldindata/utils"
-import Select from "react-select"
-import { highlight as fuzzyHighlight } from "@ourworldindata/grapher"
-import {
- SuggestedChartRevisionList,
- SuggestedChartRevisionListItem,
-} from "./SuggestedChartRevisionList.js"
-import { AdminAppContext, AdminAppContextType } from "./AdminAppContext.js"
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome/index.js"
-import {
- faSortAlphaDown,
- faSortAlphaUpAlt,
-} from "@fortawesome/free-solid-svg-icons"
-
-interface Searchable {
- suggestedChartRevision: SuggestedChartRevisionListItem
- term?: Fuzzysort.Prepared
-}
-
-@observer
-export class SuggestedChartRevisionListPage extends React.Component {
- static contextType = AdminAppContext
- context!: AdminAppContextType
-
- @observable searchInput?: string
- @observable maxVisibleCharts = 50
- @observable suggestedChartRevisions: SuggestedChartRevisionListItem[] = []
- @observable numTotalRows?: number
-
- @observable sortBy: string = "updatedAt"
- @observable sortOrder: SortOrder = SortOrder.desc
-
- @computed get searchIndex(): Searchable[] {
- const searchIndex: Searchable[] = []
- for (const suggestedChartRevision of this.suggestedChartRevisions) {
- const originalConfig = suggestedChartRevision.originalConfig
- searchIndex.push({
- suggestedChartRevision: suggestedChartRevision,
- term: fuzzysort.prepare(`
- ${suggestedChartRevision.status || ""}
- ${originalConfig.title}
- ${originalConfig.variantName || ""}
- ${originalConfig.internalNotes || ""}
- `),
- })
- }
-
- return searchIndex
- }
-
- @computed
- get suggestedChartRevisionsToShow(): SuggestedChartRevisionListItem[] {
- const { searchInput, searchIndex, maxVisibleCharts } = this
- if (searchInput) {
- const results = fuzzysort.go(searchInput, searchIndex, {
- limit: 50,
- key: "term",
- })
- return uniq(
- results.map((result: any) => result.obj.suggestedChartRevision)
- )
- } else {
- return this.suggestedChartRevisions.slice(0, maxVisibleCharts)
- }
- }
-
- @action.bound async getData() {
- const { admin } = this.context
- const json = await admin.getJSON("/api/suggested-chart-revisions", {
- sortBy: this.sortBy,
- sortOrder: this.sortOrder,
- })
- this.suggestedChartRevisions = json.suggestedChartRevisions
- this.numTotalRows = json.numTotalRows
- }
-
- @action.bound onSearchInput(input: string) {
- this.searchInput = input
- }
-
- @action.bound onShowMore() {
- this.maxVisibleCharts += 50
- }
-
- @action.bound onSortByChange(selected: any) {
- this.sortBy = selected.value
- void this.getData()
- }
-
- @action.bound onSortOrderChange(value: SortOrder) {
- this.sortOrder = value
- void this.getData()
- }
-
- componentDidMount() {
- void this.getData()
- }
-
- render() {
- const { suggestedChartRevisionsToShow, searchInput, numTotalRows } =
- this
-
- const highlight = (text: string) => {
- if (this.searchInput) {
- const html =
- fuzzyHighlight(fuzzysort.single(this.searchInput, text)) ??
- text
- return
- } else return text
- }
-
- return (
-
-
-
-
-
- Showing {suggestedChartRevisionsToShow.length}{" "}
- of {numTotalRows} suggested revisions
-
-
- Go to approval tool
-
-
- Upload revisions
-
-
-
-
-
-
- Sort by:{" "}
-
-
-
-
-
- {!searchInput && (
-
- )}
-
-
- )
- }
-}
diff --git a/adminSiteClient/admin.scss b/adminSiteClient/admin.scss
index ebfbeafeffc..943d164c532 100644
--- a/adminSiteClient/admin.scss
+++ b/adminSiteClient/admin.scss
@@ -700,8 +700,7 @@ main:not(.ChartEditorPage):not(.GdocsEditPage) {
}
.ChartIndexPage,
-.UsersIndexPage,
-.SuggestedChartRevisionListPage {
+.UsersIndexPage {
.topRow {
display: flex;
align-items: center;
@@ -996,245 +995,6 @@ main:not(.ChartEditorPage):not(.GdocsEditPage) {
}
}
-.SuggestedChartRevisionApproverPage {
- display: flex;
- flex-direction: column;
- flex-grow: 1;
-
- .params {
- display: flex;
- flex-direction: column;
- justify-content: space-evenly;
- background-color: #f9f9f9;
- padding: 1.2em;
- border-radius: 0.4em;
- margin: 0.4em 0;
- // box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
- }
-
- .collapsible {
- padding: 0.2em 0em;
- // background-color: #f9f9f9;
- // padding: 0.5em;
- // border-radius: 0.4em;
- // margin: 0.4em 0;
- }
-
- .readme {
- ul,
- ol {
- margin-left: 1.2em;
- padding-top: 5px;
- }
-
- li {
- padding-bottom: 5px;
- }
- }
- .settings {
- .flex-row {
- display: flex;
- flex-direction: row;
- }
- }
-
- .settings > * {
- margin-bottom: 10px;
- }
-
- .btn-group .btn {
- width: 50px;
- }
-
- .form-group > label {
- margin-bottom: 0;
- }
-
- .meta {
- list-style-type: none;
- }
-
- .charts-view {
- display: flex;
- flex-direction: row;
- flex-grow: 1;
- flex-wrap: wrap;
- align-items: center;
- justify-content: center;
-
- .chart-view {
- padding: 1em;
-
- .header {
- display: flex;
- flex-direction: row;
- flex-wrap: wrap;
- align-items: center;
- justify-content: flex-start;
- margin-bottom: 5px;
-
- h5 {
- margin-bottom: 0;
- margin-right: 5px;
- }
-
- span {
- margin-right: 10px;
- }
- }
-
- .json-view {
- // width: 100%;
- // height: 100%;
- background-color: #eeeeee;
- overflow-y: auto;
- border: 1px solid rgba(0, 0, 0, 0.2);
- border-radius: 0.4em;
-
- pre {
- white-space: pre-wrap;
- }
- }
- }
- }
-
- .controls {
- margin: 0.1em auto 0 auto;
- display: flex;
- flex-direction: column;
- flex-wrap: wrap;
- justify-content: center;
- align-items: center;
- min-width: 400px;
-
- .btn {
- margin-left: 5px;
- margin-right: 5px;
- }
-
- .row-input {
- padding-bottom: 10px;
- display: flex;
- flex-direction: row;
- flex-wrap: wrap;
- align-items: center;
- justify-content: center;
- padding-top: 0.5rem;
-
- .form-group {
- width: 80px;
- padding-top: 0px;
- padding-bottom: 0px;
- padding-left: 10px;
- padding-right: 10px;
- margin: 0px;
- input {
- text-align: center;
- }
- }
- }
-
- .form-group {
- padding: 10px;
- align-self: stretch;
- padding-left: 25%;
- padding-right: 25%;
-
- textarea {
- height: 3rem;
- }
- }
- }
-
- .warning {
- padding: 10px;
- margin: 10px auto;
- text-align: center;
- max-width: 720px;
- }
-
- .references {
- .list-group {
- margin-top: 0;
- margin-bottom: 0.5em;
- }
- }
-
- h3.grapherChart {
- // border-bottom: 1px darkgray dotted;
- text-decoration: underline;
- text-decoration-style: dotted;
- text-decoration-color: darkgray;
- }
-}
-
-.SuggestedChartRevisionListPage {
- .settings {
- display: flex;
- flex-direction: row;
- flex-wrap: wrap;
- }
-
- .settings > div {
- margin-right: 10px;
- }
-}
-
-.SuggestedChartRevisionImportPage {
- section,
- .collapsible {
- background-color: #f9f9f9;
- padding: 1.2em;
- border-radius: 0.4em;
- margin: 0.4em 0;
- box-shadow:
- 0 1px 3px rgba(0, 0, 0, 0.12),
- 0 1px 2px rgba(0, 0, 0, 0.24);
- }
- .import-form {
- input {
- max-width: 1000px;
- }
-
- label {
- font-size: 1.2rem;
- }
- }
-
- .readme {
- ul,
- ol {
- margin-left: 1.2em;
- padding-top: 5px;
- }
-
- li {
- padding-bottom: 5px;
- }
-
- .snippet {
- background-color: #e8e8e8;
- padding: 10px;
- border-radius: 0.4rem;
- }
- }
-
- .message {
- margin: 1rem 0;
- padding: 1rem;
- border-radius: 0.4rem;
- opacity: 0.87;
- color: #fff;
- box-shadow:
- 0 1px 3px rgba(0, 0, 0, 0.12),
- 0 1px 2px rgba(0, 0, 0, 0.24);
- }
-
- .message.bg-warning {
- color: #000;
- }
-}
-
.BulkDownloadPage {
section {
background-color: #f9f9f9;
diff --git a/adminSiteServer/apiRouter.ts b/adminSiteServer/apiRouter.ts
index 58ecda1740d..27c0f0e726c 100644
--- a/adminSiteServer/apiRouter.ts
+++ b/adminSiteServer/apiRouter.ts
@@ -36,7 +36,6 @@ import {
OwidGdocPostInterface,
parseIntOrUndefined,
DbRawPostWithGdocPublishStatus,
- SuggestedChartRevisionStatus,
OwidVariableWithSource,
OwidChartDimensionInterface,
DimensionProperty,
@@ -95,11 +94,6 @@ import {
syncDatasetToGitRepo,
removeDatasetFromGitRepo,
} from "./gitDataExport.js"
-import {
- getQueryEnrichedSuggestedChartRevision,
- getQueryEnrichedSuggestedChartRevisions,
- isValidStatus,
-} from "../db/model/SuggestedChartRevision.js"
import { denormalizeLatestCountryData } from "../baker/countryProfiles.js"
import {
indexIndividualGdocPost,
@@ -765,11 +759,6 @@ deleteRouteWithRWTransaction(
`DELETE FROM chart_slug_redirects WHERE chart_id=?`,
[chart.id]
)
- await db.knexRaw(
- trx,
- `DELETE FROM suggested_chart_revisions WHERE chartId=?`,
- [chart.id]
- )
await db.knexRaw(trx, `DELETE FROM charts WHERE id=?`, [chart.id])
if (chart.isPublished)
@@ -782,216 +771,6 @@ deleteRouteWithRWTransaction(
}
)
-getRouteWithROTransaction(
- apiRouter,
- "/suggested-chart-revisions",
- async (req, res, trx) => {
- const isValidSortBy = (sortBy: string) => {
- return [
- "updatedAt",
- "createdAt",
- "suggestedReason",
- "id",
- "chartId",
- "status",
- "variableId",
- "chartUpdatedAt",
- "chartCreatedAt",
- ].includes(sortBy)
- }
- const isValidSortOrder = (sortOrder: string) => {
- return (
- sortOrder !== undefined &&
- sortOrder !== null &&
- ["ASC", "DESC"].includes(sortOrder.toUpperCase())
- )
- }
- const limit =
- req.query.limit !== undefined ? expectInt(req.query.limit) : 10000
- const offset =
- req.query.offset !== undefined ? expectInt(req.query.offset) : 0
- const sortBy = isValidSortBy(req.query.sortBy as string)
- ? req.query.sortBy
- : "updatedAt"
- const sortOrder = isValidSortOrder(req.query.sortOrder as string)
- ? (req.query.sortOrder as string).toUpperCase()
- : "DESC"
- const status: string | null = isValidStatus(
- req.query.status as SuggestedChartRevisionStatus
- )
- ? (req.query.status as string)
- : null
-
- let orderBy
- if (sortBy === "variableId") {
- orderBy =
- "CAST(scr.suggestedConfig->>'$.dimensions[0].variableId' as SIGNED)"
- } else if (sortBy === "chartUpdatedAt") {
- orderBy = "c.updatedAt"
- } else if (sortBy === "chartCreatedAt") {
- orderBy = "c.createdAt"
- } else {
- orderBy = `scr.${sortBy}`
- }
-
- const numTotalRows = (
- await db.knexRaw<{ count: number }>(
- trx,
- `
- SELECT COUNT(*) as count
- FROM suggested_chart_revisions
- ${status ? "WHERE status = ?" : ""}
- `,
- status ? [status] : []
- )
- )[0].count
-
- const enrichedSuggestedChartRevisions =
- await getQueryEnrichedSuggestedChartRevisions(
- trx,
- orderBy,
- sortOrder,
- status,
- limit,
- offset
- )
-
- return {
- suggestedChartRevisions: enrichedSuggestedChartRevisions,
- numTotalRows: numTotalRows,
- }
- }
-)
-
-getRouteWithROTransaction(
- apiRouter,
- "/suggested-chart-revisions/:suggestedChartRevisionId",
- async (req, res, trx) => {
- const suggestedChartRevisionId = expectInt(
- req.params.suggestedChartRevisionId
- )
-
- const suggestedChartRevision = getQueryEnrichedSuggestedChartRevision(
- trx,
- suggestedChartRevisionId
- )
-
- if (!suggestedChartRevision) {
- throw new JsonError(
- `No suggested chart revision by id '${suggestedChartRevisionId}'`,
- 404
- )
- }
-
- return {
- suggestedChartRevision: suggestedChartRevision,
- }
- }
-)
-
-postRouteWithRWTransaction(
- apiRouter,
- "/suggested-chart-revisions/:suggestedChartRevisionId/update",
- async (req, res, trx) => {
- const suggestedChartRevisionId = expectInt(
- req.params.suggestedChartRevisionId
- )
-
- // Note: there was a suggestedConfig here that was not used - might have been a
- // mistake in a refactoring that wasn't found before?
- const { status, decisionReason } = req.body as {
- status: string
- decisionReason: string
- }
-
- const suggestedChartRevision =
- await getQueryEnrichedSuggestedChartRevision(
- trx,
- suggestedChartRevisionId
- )
-
- if (!suggestedChartRevision) {
- throw new JsonError(
- `No suggested chart revision found for id '${suggestedChartRevisionId}'`,
- 404
- )
- }
-
- const canUpdate =
- (status === "approved" && suggestedChartRevision.canApprove) ||
- (status === "rejected" && suggestedChartRevision.canReject) ||
- (status === "pending" && suggestedChartRevision.canPending) ||
- (status === "flagged" && suggestedChartRevision.canFlag)
- if (!canUpdate) {
- throw new JsonError(
- `Suggest chart revision ${suggestedChartRevisionId} cannot be ` +
- `updated with status="${status}".`,
- 404
- )
- }
-
- await db.knexRaw(
- trx,
- `
- UPDATE suggested_chart_revisions
- SET status=?, decisionReason=?, updatedAt=?, updatedBy=?
- WHERE id = ?
- `,
- [
- status,
- decisionReason,
- new Date(),
- res.locals.user.id,
- suggestedChartRevisionId,
- ]
- )
-
- // Update config ONLY when APPROVE button is clicked
- // Makes sense when the suggested config is a sugegstion by GPT, otherwise is redundant but we are cool with it
- if (status === SuggestedChartRevisionStatus.approved) {
- await db.knexRaw(
- trx,
- `
- UPDATE suggested_chart_revisions
- SET suggestedConfig=?
- WHERE id = ?
- `,
- [
- JSON.stringify(suggestedChartRevision.suggestedConfig),
- suggestedChartRevisionId,
- ]
- )
- }
- // note: the calls to saveGrapher() below will never overwrite a config
- // that has been changed since the suggestedConfig was created, because
- // if the config has been changed since the suggestedConfig was created
- // then canUpdate will be false (so an error would have been raised
- // above).
-
- if (status === "approved" && suggestedChartRevision.canApprove) {
- await saveGrapher(
- trx,
- res.locals.user,
- suggestedChartRevision.suggestedConfig,
- suggestedChartRevision.existingConfig
- )
- } else if (
- status === "rejected" &&
- suggestedChartRevision.canReject &&
- suggestedChartRevision.status === "approved"
- ) {
- await saveGrapher(
- trx,
- res.locals.user,
- suggestedChartRevision.originalConfig,
- suggestedChartRevision.existingConfig
- )
- }
-
- return { success: true }
- }
-)
-
getRouteWithROTransaction(apiRouter, "/users.json", async (req, res, trx) => ({
users: await trx
.select(
diff --git a/db/model/SuggestedChartRevision.ts b/db/model/SuggestedChartRevision.ts
deleted file mode 100644
index 928e8ce4a48..00000000000
--- a/db/model/SuggestedChartRevision.ts
+++ /dev/null
@@ -1,250 +0,0 @@
-import {
- DbEnrichedSuggestedChartRevision,
- DbRawSuggestedChartRevision,
- GrapherInterface,
- SuggestedChartRevisionStatus,
-} from "@ourworldindata/utils"
-import { KnexReadonlyTransaction, knexRaw, knexRawFirst } from "../db.js"
-
-export function isValidStatus(status: SuggestedChartRevisionStatus): boolean {
- return Object.values(SuggestedChartRevisionStatus).includes(status)
-}
-
-type DbRawQuerySuggestedChartRevisions = Pick<
- DbRawSuggestedChartRevision,
- | "id"
- | "chartId"
- | "updatedAt"
- | "createdAt"
- | "suggestedReason"
- | "decisionReason"
- | "status"
- | "suggestedConfig"
- | "originalConfig"
- | "changesInDataSummary"
- | "experimental"
-> & {
- createdById: number
- updatedById: number
- createdByFullName: string
- updatedByFullName: string
- existingConfig: string
- chartUpdatedAt: Date
- chartCreatedAt: Date
-}
-
-type DbSemiEnrichedQuerySuggestedChartRevisions = Pick<
- DbEnrichedSuggestedChartRevision,
- | "id"
- | "chartId"
- | "updatedAt"
- | "createdAt"
- | "suggestedReason"
- | "decisionReason"
- | "status"
- | "suggestedConfig"
- | "originalConfig"
- | "changesInDataSummary"
- | "experimental"
-> & {
- createdById: number
- updatedById: number
- createdByFullName: string
- updatedByFullName: string
- existingConfig: GrapherInterface
- chartUpdatedAt: Date
- chartCreatedAt: Date
-}
-
-type DbEnrichedQuerySuggestedChartRevisions =
- DbSemiEnrichedQuerySuggestedChartRevisions & {
- canApprove: boolean
- canReject: boolean
- canFlag: boolean
- canPending: boolean
- }
-
-function parseQuerySuggestedChartRevision(
- row: DbRawQuerySuggestedChartRevisions
-): DbEnrichedQuerySuggestedChartRevisions {
- const suggestedConfig = JSON.parse(row.suggestedConfig)
- const existingConfig = JSON.parse(row.existingConfig)
- const originalConfig = JSON.parse(row.originalConfig)
- const experimental = row.experimental ? JSON.parse(row.experimental) : null
- const semiEnriched = {
- ...row,
- suggestedConfig,
- existingConfig,
- originalConfig,
- experimental,
- }
- const canApprove = checkCanApprove(semiEnriched)
- const canReject = checkCanReject(semiEnriched)
- const canFlag = checkCanFlag(semiEnriched)
- const canPending = checkCanPending(semiEnriched)
- return {
- ...semiEnriched,
- canApprove,
- canReject,
- canFlag,
- canPending,
- }
-}
-
-const selectFields = `scr.id, scr.chartId, scr.updatedAt, scr.createdAt,
- scr.suggestedReason, scr.decisionReason, scr.status,
- scr.suggestedConfig, scr.originalConfig, scr.changesInDataSummary,
- scr.experimental,
- createdByUser.id as createdById,
- updatedByUser.id as updatedById,
- createdByUser.fullName as createdByFullName,
- updatedByUser.fullName as updatedByFullName,
- c.config as existingConfig, c.updatedAt as chartUpdatedAt,
- c.createdAt as chartCreatedAt
-`
-
-export async function getQueryEnrichedSuggestedChartRevisions(
- trx: KnexReadonlyTransaction,
- orderBy: string,
- sortOrder: string,
- status: string | null,
- limit: number,
- offset: number
-): Promise {
- const rawSuggestedChartRevisions =
- await knexRaw(
- trx,
- `-- sql
- SELECT ${selectFields}
- FROM suggested_chart_revisions as scr
- LEFT JOIN charts c on c.id = scr.chartId
- LEFT JOIN users createdByUser on createdByUser.id = scr.createdBy
- LEFT JOIN users updatedByUser on updatedByUser.id = scr.updatedBy
- ${status ? "WHERE scr.status = ?" : ""}
- ORDER BY ${orderBy} ${sortOrder}
- LIMIT ? OFFSET ?
- `,
- status ? [status, limit, offset] : [limit, offset]
- )
-
- const enrichedSuggestedChartRevisions = rawSuggestedChartRevisions.map(
- parseQuerySuggestedChartRevision
- )
- return enrichedSuggestedChartRevisions
-}
-
-export async function getQueryEnrichedSuggestedChartRevision(
- trx: KnexReadonlyTransaction,
- id: number
-): Promise {
- const rawSuggestedChartRevisions =
- await knexRawFirst(
- trx,
- `-- sql
- SELECT ${selectFields}
- FROM suggested_chart_revisions as scr
- LEFT JOIN charts c on c.id = scr.chartId
- LEFT JOIN users createdByUser on createdByUser.id = scr.createdBy
- LEFT JOIN users updatedByUser on updatedByUser.id = scr.updatedBy
- WHERE scr.id = ?
- `,
- [id]
- )
-
- if (!rawSuggestedChartRevisions) return null
-
- const enrichedSuggestedChartRevisions = parseQuerySuggestedChartRevision(
- rawSuggestedChartRevisions
- )
- return enrichedSuggestedChartRevisions
-}
-
-export function checkCanApprove(
- suggestedChartRevision: DbSemiEnrichedQuerySuggestedChartRevisions
-): boolean {
- // note: a suggestion can be approved if status == "rejected" |
- // "flagged" | "pending" AND the original config version equals
- // the existing config version (i.e. the existing chart has not
- // been changed since the suggestion was created).
- const status = suggestedChartRevision.status
- const originalVersion = suggestedChartRevision.originalConfig?.version
- const existingVersion = suggestedChartRevision.existingConfig?.version
- const originalVersionExists =
- originalVersion !== null && originalVersion !== undefined
- const existingVersionExists =
- existingVersion !== null && existingVersion !== undefined
- if (
- [
- SuggestedChartRevisionStatus.rejected,
- SuggestedChartRevisionStatus.flagged,
- SuggestedChartRevisionStatus.pending,
- ].includes(status as any) &&
- originalVersionExists &&
- existingVersionExists &&
- originalVersion === existingVersion
- ) {
- return true
- }
- return false
-}
-
-export function checkCanReject(
- suggestedChartRevision: DbSemiEnrichedQuerySuggestedChartRevisions
-): boolean {
- // note: a suggestion can be rejected if: (1) status ==
- // "pending" | "flagged"; or (2) status == "approved" and the
- // suggested config version equals the existing chart version
- // (i.e. the existing chart has not changed since the suggestion
- // was approved).
- const status = suggestedChartRevision.status
- const suggestedVersion = suggestedChartRevision.suggestedConfig?.version
- const existingVersion = suggestedChartRevision.existingConfig?.version
- const suggestedVersionExists =
- suggestedVersion !== null && suggestedVersion !== undefined
- const existingVersionExists =
- existingVersion !== null && existingVersion !== undefined
- if (
- [
- SuggestedChartRevisionStatus.flagged,
- SuggestedChartRevisionStatus.pending,
- ].includes(status as any)
- ) {
- return true
- }
- if (
- status === "approved" &&
- suggestedVersionExists &&
- existingVersionExists &&
- suggestedVersion === existingVersion
- ) {
- return true
- }
- return false
-}
-
-export function checkCanFlag(
- suggestedChartRevision: DbSemiEnrichedQuerySuggestedChartRevisions
-): boolean {
- // note: a suggestion can be flagged if status == "pending" or
- // if it is already flagged. Flagging a suggestion that is
- // already flagged is a hack for updating the decisionReason
- // column in the SuggestedChartRevisionApproverPage UI without
- // changing the status column.
- const status = suggestedChartRevision.status
- if (
- [
- SuggestedChartRevisionStatus.flagged,
- SuggestedChartRevisionStatus.pending,
- ].includes(status as any)
- ) {
- return true
- }
- return false
-}
-
-export function checkCanPending(
- _suggestedChartRevision: DbSemiEnrichedQuerySuggestedChartRevisions
-): boolean {
- // note: a suggestion cannot be altered to pending from another status
- return false
-}
diff --git a/packages/@ourworldindata/types/src/dbTypes/SuggestedChartRevisions.ts b/packages/@ourworldindata/types/src/dbTypes/SuggestedChartRevisions.ts
deleted file mode 100644
index 5248fea42c3..00000000000
--- a/packages/@ourworldindata/types/src/dbTypes/SuggestedChartRevisions.ts
+++ /dev/null
@@ -1,82 +0,0 @@
-import { JsonString } from "../domainTypes/Various.js"
-import { GrapherInterface } from "../grapherTypes/GrapherTypes.js"
-import { parseChartConfig, serializeChartConfig } from "./Charts.js"
-
-export interface SuggestedChartRevisionsExperimental {
- gpt: {
- model: string
- suggestions: {
- title: string
- subtitle: string
- }[]
- }
-}
-
-export const SuggestedChartRevisionsTableName = "suggested_chart_revisions"
-export interface DbInsertSuggestedChartRevision {
- changesInDataSummary?: string | null
- chartId: number
- createdAt?: Date
- createdBy: number
- decisionReason?: string | null
- experimental?: JsonString | null
- id?: string
- isPendingOrFlagged?: number | null
- originalConfig: JsonString
- originalVersion: number
- status: string
- suggestedConfig: JsonString
- suggestedReason?: string | null
- suggestedVersion: number
- updatedAt?: Date | null
- updatedBy?: number | null
-}
-export type DbRawSuggestedChartRevision =
- Required
-
-export type DbEnrichedSuggestedChartRevision = Omit<
- DbRawSuggestedChartRevision,
- "originalConfig" | "suggestedConfig" | "experimental"
-> & {
- originalConfig: GrapherInterface
- suggestedConfig: GrapherInterface
- experimental: SuggestedChartRevisionsExperimental | null
-}
-
-export function parseSuggestedChartRevisionsExperimental(
- experimental: JsonString | null
-): SuggestedChartRevisionsExperimental | null {
- return experimental ? JSON.parse(experimental) : null
-}
-
-export function serializeSuggestedChartRevisionsExperimental(
- experimental: SuggestedChartRevisionsExperimental | null
-): JsonString | null {
- return experimental ? JSON.stringify(experimental) : null
-}
-
-export function parseSuggestedChartRevisionsRow(
- row: DbRawSuggestedChartRevision
-): DbEnrichedSuggestedChartRevision {
- return {
- ...row,
- originalConfig: parseChartConfig(row.originalConfig),
- suggestedConfig: parseChartConfig(row.suggestedConfig),
- experimental: parseSuggestedChartRevisionsExperimental(
- row.experimental
- ),
- }
-}
-
-export function serializeSuggestedChartRevisionsRow(
- row: DbEnrichedSuggestedChartRevision
-): DbRawSuggestedChartRevision {
- return {
- ...row,
- originalConfig: serializeChartConfig(row.originalConfig),
- suggestedConfig: serializeChartConfig(row.suggestedConfig),
- experimental: serializeSuggestedChartRevisionsExperimental(
- row.experimental
- ),
- }
-}
diff --git a/packages/@ourworldindata/types/src/domainTypes/Various.ts b/packages/@ourworldindata/types/src/domainTypes/Various.ts
index 0f491eff454..946339baa14 100644
--- a/packages/@ourworldindata/types/src/domainTypes/Various.ts
+++ b/packages/@ourworldindata/types/src/domainTypes/Various.ts
@@ -53,13 +53,6 @@ export enum TaggableType {
export type TopicId = number
-export enum SuggestedChartRevisionStatus {
- pending = "pending",
- approved = "approved",
- rejected = "rejected",
- flagged = "flagged",
-}
-
// Exception format that can be easily given as an API error
export class JsonError extends Error {
status: number
diff --git a/packages/@ourworldindata/types/src/index.ts b/packages/@ourworldindata/types/src/index.ts
index 7dc9ee05c91..0b9a1f9ca41 100644
--- a/packages/@ourworldindata/types/src/index.ts
+++ b/packages/@ourworldindata/types/src/index.ts
@@ -12,7 +12,6 @@ export {
JsonError,
type SerializedGridProgram,
SiteFooterContext,
- SuggestedChartRevisionStatus,
TaggableType,
type TopicId,
type OwidVariableId,
@@ -596,16 +595,6 @@ export {
parseSourcesRow,
serializeSourcesRow,
} from "./dbTypes/Sources.js"
-export {
- type DbInsertSuggestedChartRevision,
- type DbRawSuggestedChartRevision,
- type DbEnrichedSuggestedChartRevision,
- SuggestedChartRevisionsTableName,
- parseSuggestedChartRevisionsExperimental,
- serializeSuggestedChartRevisionsExperimental,
- parseSuggestedChartRevisionsRow,
- serializeSuggestedChartRevisionsRow,
-} from "./dbTypes/SuggestedChartRevisions.js"
export {
type DbInsertTag,
type DbPlainTag,