diff --git a/site/gatsby-site/gatsby-config.js b/site/gatsby-site/gatsby-config.js index b61b2df203..d6e994118f 100755 --- a/site/gatsby-site/gatsby-config.js +++ b/site/gatsby-site/gatsby-config.js @@ -86,6 +86,7 @@ const plugins = [ 'classifications', 'reports', 'entities', + 'publications', ], connectionString: config.mongodb.connectionString, extraParams: { diff --git a/site/gatsby-site/gatsby-node.js b/site/gatsby-site/gatsby-node.js index ccf35b4662..2adcf2e2cd 100644 --- a/site/gatsby-site/gatsby-node.js +++ b/site/gatsby-site/gatsby-node.js @@ -290,6 +290,17 @@ exports.createSchemaCustomization = ({ actions }) => { public: Boolean complete_from: completeFrom } + + + type mongodbAiidprodPublicationsHarm_labels { + label: String + labeler: String + } + type mongodbAiidprodPublications implements Node { + domain: String + title: String + harm_labels: [mongodbAiidprodPublicationsHarm_labels] + } `; createTypes(typeDefs); diff --git a/site/gatsby-site/migrations/2023.03.01T23.01.01.add-political-bias-labels.js b/site/gatsby-site/migrations/2023.03.01T23.01.01.add-political-bias-labels.js new file mode 100644 index 0000000000..1aac5262a0 --- /dev/null +++ b/site/gatsby-site/migrations/2023.03.01T23.01.01.add-political-bias-labels.js @@ -0,0 +1,120 @@ +const config = require('../config'); + +const axios = require('axios'); + +const jsdom = require('jsdom'); + +exports.up = async ({ context: { client } }) => { + const { JSDOM } = jsdom; + + const publications = []; + + const addBiasLabel = (publicationTitle, publicationDomain, labeler, label) => { + const existingPublication = publications.find((p) => p.domain == publicationDomain); + + if (existingPublication) { + existingPublication.bias_labels.push({ label, labeler }); + } else { + publications.push({ + title: publicationTitle, + domain: publicationDomain, + bias_labels: [{ label, labeler }], + }); + } + }; + + for (const alignment of [ + 'left', + 'leftcenter', + 'center', + 'right-center', + 'right', + 'fake-news', + 'conspiracy', + ]) { + const mbfcResponse = await axios.get('https://mediabiasfactcheck.com/' + alignment); + + if (mbfcResponse.status == 200) { + const mbfcPage = new JSDOM(mbfcResponse.data); + + for (const tr of [...mbfcPage.window.document.querySelectorAll('#mbfc-table tr')]) { + const deepestChild = getDeepestChild(tr); + + let tokens = deepestChild.textContent.split(' '); + + let lastToken = tokens.pop(); + + let domain; + + if (lastToken[0] == '(') { + domain = new URL( + 'http://' + + lastToken + .slice(1, lastToken.length - 1) // Remove parentheses + .replace(/^(www|m)\./, '') + ).hostname; + } else { + tokens.push(lastToken); + } + const title = tokens.join(' '); + + if (domain) { + addBiasLabel( + title, + domain, + 'mediabiasfactcheck.com', + { + left: 'left', + right: 'right', + + // These occur in most sources, and it's best to normalize them + // to a specific spelling / wording across sources + // so we can check if different sources agree. + leftcenter: 'center-left', + 'right-center': 'center-right', + + // They use "center" in the url but "least biased" in the site text. + // There's a difference between being unbiased + // and being biased towards the center. + // They seem to be trying to capture the former: + // + // > These sources have minimal bias and use very few loaded words + // > (wording that attempts to influence an audience + // > by using appeal to emotion or stereotypes). + // > The reporting is factual and usually sourced. + // > These are the most credible media sources. + // + // Same with "fake-news" and "conspiracy". + center: 'least biased', + 'fake-news': 'questionable', + conspiracy: 'conspiracy/pseudoscience', + }[alignment] + ); + } + } + } + } + await client.connect(); + + const publicationsCollection = await client + .db(config.realm.production_db.db_name) + .createCollection('publications'); + + for (const publication of publications) { + publicationsCollection.insertOne(publication); + } +}; + +exports.down = async ({ context: { client } }) => { + await client.connect(); + + await client.db(config.realm.production_db.db_name).dropCollection('publications'); +}; + +var getDeepestChild = (htmlNode) => { + if (htmlNode.children.length == 0) { + return htmlNode; + } else { + return getDeepestChild(htmlNode.children[0]); + } +}; diff --git a/site/gatsby-site/page-creators/createCitationPages.js b/site/gatsby-site/page-creators/createCitationPages.js index b29e07dddb..e06b1976b1 100644 --- a/site/gatsby-site/page-creators/createCitationPages.js +++ b/site/gatsby-site/page-creators/createCitationPages.js @@ -5,7 +5,7 @@ const { switchLocalizedPath } = require('../i18n'); const createCitationPages = async (graphql, createPage, { languages }) => { const result = await graphql( ` - query IncidentIDs { + query ContextData { allMongodbAiidprodIncidents { nodes { incident_id @@ -32,13 +32,26 @@ const createCitationPages = async (graphql, createPage, { languages }) => { language image_url cloudinary_id + source_domain + } + } + + allMongodbAiidprodPublications { + nodes { + title + domain + bias_labels { + label + labeler + } } } } ` ); - const { allMongodbAiidprodIncidents, allMongodbAiidprodReports } = result.data; + const { allMongodbAiidprodIncidents, allMongodbAiidprodReports, allMongodbAiidprodPublications } = + result.data; // Incident reports list const incidentReportsMap = {}; @@ -80,6 +93,10 @@ const createCitationPages = async (graphql, createPage, { languages }) => { reports: incidentReportsMap[incident_id], })); + const publications = allMongodbAiidprodPublications.nodes.filter((publication) => + incidentReportsMap[incident_id].some((report) => report.source_domain == publication.domain) + ); + pageContexts.push({ incident, incident_id, @@ -89,6 +106,7 @@ const createCitationPages = async (graphql, createPage, { languages }) => { nlp_similar_incidents, editor_similar_incidents, editor_dissimilar_incidents, + publications, }); } diff --git a/site/gatsby-site/src/components/BiasLabels.js b/site/gatsby-site/src/components/BiasLabels.js new file mode 100644 index 0000000000..8c6b611abf --- /dev/null +++ b/site/gatsby-site/src/components/BiasLabels.js @@ -0,0 +1,85 @@ +import React, { useState } from 'react'; +import { Modal } from 'flowbite-react'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { + faArrowCircleLeft, + faArrowCircleRight, + faChevronCircleLeft, + faChevronCircleRight, + faCheckCircle, + faExclamationCircle, +} from '@fortawesome/free-solid-svg-icons'; + +export function BiasIcon({ bias_labels, publicationName, className, style }) { + bias_labels ||= []; + + // The modal causes server-side-rendering problems, + // so we need to disable rendering it + // until an interaction has occurred on the client-side. + const [modalRendered, setModalRendered] = useState(false); + + const [modalVisible, setModalVisible] = useState(false); + + const hasLabelName = (labelName) => bias_labels.some((biasLabel) => biasLabel.label == labelName); + + const possibleLabels = [ + 'least biased', + 'questionable', + 'left', + 'right', + 'center-left', + 'center-right', + ]; + + return ( + <> + + {modalRendered && ( + setModalVisible(false)}> + {publicationName} + +

The bias of this source was assessed as follows:

+ +
+
+ )} + + ); +} diff --git a/site/gatsby-site/src/components/reports/ReportCard.js b/site/gatsby-site/src/components/reports/ReportCard.js index da8fc4a324..df46cd6332 100644 --- a/site/gatsby-site/src/components/reports/ReportCard.js +++ b/site/gatsby-site/src/components/reports/ReportCard.js @@ -13,13 +13,15 @@ import { RESPONSE_TAG } from 'utils/entities'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faQuestionCircle } from '@fortawesome/free-solid-svg-icons'; import { hasVariantData } from 'utils/variants'; +import { BiasIcon } from 'components/BiasLabels'; import { format, fromUnixTime } from 'date-fns'; const ReportCard = ({ - item, - className = '', - alwaysExpanded = false, actions = null, + alwaysExpanded = false, + className = '', + item, + publications, reportTitle = null, }) => { const { t } = useTranslation(); @@ -42,6 +44,8 @@ const ReportCard = ({ } }; + const publication = (publications || []).find((p) => p.domain == item.source_domain); + const toggleReadMoreKeyDown = (e) => { if (e.key === 'Enter') { toggleReadMore(); @@ -157,7 +161,13 @@ const ReportCard = ({ )} -
+
+ JSON.stringify(biasLabel))) + ).map((json) => JSON.parse(json))} + publicationName={publication?.title} + /> {item.source_domain} ·{' '} {item.date_published @@ -166,6 +176,7 @@ const ReportCard = ({ ? format(fromUnixTime(item.epoch_date_published), 'yyyy') : 'Needs publish date'} +
{actions && <>{actions}}
diff --git a/site/gatsby-site/src/templates/cite.js b/site/gatsby-site/src/templates/cite.js index b040611e3e..76de582115 100644 --- a/site/gatsby-site/src/templates/cite.js +++ b/site/gatsby-site/src/templates/cite.js @@ -18,6 +18,7 @@ function CitePage(props) { nlp_similar_incidents, editor_similar_incidents, editor_dissimilar_incidents, + publications, }, data: { allMongodbAiidprodTaxa, @@ -94,7 +95,6 @@ function CitePage(props) { - {isLiveData ? ( ) : ( )}
diff --git a/site/gatsby-site/src/templates/citeDynamicTemplate.js b/site/gatsby-site/src/templates/citeDynamicTemplate.js index 5ca8e984d0..f4e15b4f45 100644 --- a/site/gatsby-site/src/templates/citeDynamicTemplate.js +++ b/site/gatsby-site/src/templates/citeDynamicTemplate.js @@ -20,6 +20,7 @@ function CiteDynamicTemplate({ editor_dissimilar_incidents, locationPathName, setIsLiveData, + publications, }) { const { locale } = useLocalization(); @@ -154,6 +155,7 @@ function CiteDynamicTemplate({ editor_dissimilar_incidents={editor_dissimilar_incidents} liveVersion={true} setIsLiveData={setIsLiveData} + publications={publications} /> ) )} diff --git a/site/gatsby-site/src/templates/citeTemplate.js b/site/gatsby-site/src/templates/citeTemplate.js index 7d4ec43fd2..f8e1f4ed17 100644 --- a/site/gatsby-site/src/templates/citeTemplate.js +++ b/site/gatsby-site/src/templates/citeTemplate.js @@ -47,6 +47,7 @@ function CiteTemplate({ editor_dissimilar_incidents, liveVersion = false, setIsLiveData, + publications, }) { const { loading, isRole, user } = useUserContext(); @@ -429,7 +430,12 @@ function CiteTemplate({ return ( - + ); diff --git a/site/realm/auth/custom_user_data.json b/site/realm/auth/custom_user_data.json index 8e6f1cc923..422aa195f9 100644 --- a/site/realm/auth/custom_user_data.json +++ b/site/realm/auth/custom_user_data.json @@ -1,5 +1,5 @@ { - "enabled": true, + "enabled": false, "mongo_service_name": "mongodb-atlas", "database_name": "customData", "collection_name": "users", diff --git a/site/realm/data_sources/mongodb-atlas/aiidprod/publications/relationships.json b/site/realm/data_sources/mongodb-atlas/aiidprod/publications/relationships.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/site/realm/data_sources/mongodb-atlas/aiidprod/publications/relationships.json @@ -0,0 +1 @@ +{} diff --git a/site/realm/data_sources/mongodb-atlas/aiidprod/publications/rules.json b/site/realm/data_sources/mongodb-atlas/aiidprod/publications/rules.json new file mode 100644 index 0000000000..12d3f4113f --- /dev/null +++ b/site/realm/data_sources/mongodb-atlas/aiidprod/publications/rules.json @@ -0,0 +1,28 @@ +{ + "collection": "publications", + "database": "aiidprod", + "roles": [ + { + "name": "role is admin", + "apply_when": { + "%%user.custom_data.roles": "admin" + }, + "fields": {}, + "write": true, + "insert": true, + "delete": true, + "search": true, + "additional_fields": {} + }, + { + "name": "default", + "apply_when": {}, + "fields": {}, + "read": true, + "insert": false, + "delete": false, + "search": false, + "additional_fields": {} + } + ] +} diff --git a/site/realm/data_sources/mongodb-atlas/aiidprod/publications/schema.json b/site/realm/data_sources/mongodb-atlas/aiidprod/publications/schema.json new file mode 100644 index 0000000000..fc0ef02df5 --- /dev/null +++ b/site/realm/data_sources/mongodb-atlas/aiidprod/publications/schema.json @@ -0,0 +1,18 @@ +{ + "properties": { + "_id": { + "bsonType": "objectId" + }, + "title": { "bsonType": "string" }, + "domain": { "bsonType": "string" }, + "bias_labels": { + "bsonType": "object", + "properties": { + "labeler": { "bsonType": "string" }, + "label": { "bsonType": "string" } + } + } + }, + "required": [], + "title": "publications" +}