diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index fce33c1..0b1594e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -133,9 +133,9 @@ jobs: - name: zip deployment files run: | zip --junk-paths deployment.zip \ + ./deployment/nginx.conf \ ./deployment/docker-compose.yml \ - ./nginx/nginx.conf \ - .env.example \ + ./deployment/env.example \ ./deployment/README.md - name: Upload deployment.zip @@ -184,20 +184,3 @@ jobs: prerelease: ${{inputs.pre_release}} files: deployment.zip - # citation: - # # it needs to be checked on string value - # if: needs.release_tag.outputs.skipped == 'false' - # needs: [ release_tag, deployment_files, release_draft ] - # name: citations - # uses: ./.github/workflows/_cff.yml - # with: - # artifact: citation - # branch: main - # commit_message: "chore(release): update citation file" - # secrets: - # # need to pass PAT using secrets prop to reusable workflow (module) - # # the secrets are not passed automatically to child modules - # # see https://docs.github.com/en/enterprise-cloud@latest/actions/using-workflows/reusing-workflows#passing-inputs-and-secrets-to-a-reusable-workflow - # token: ${{ secrets.PAT_RELEASE }} - - diff --git a/data-generation/Dockerfile b/data-generation/Dockerfile index e157563..5171c64 100644 --- a/data-generation/Dockerfile +++ b/data-generation/Dockerfile @@ -5,10 +5,11 @@ FROM node:21.4.0-bullseye-slim WORKDIR /usr/app -COPY ./package.json /usr/app -RUN npm install -COPY ./img /usr/app/img -COPY ./images.js /usr/app -COPY ./real-data.js /usr/app -COPY ./main.js /usr/app +# copy +COPY package.json package-lock.json ./ +# install +RUN npm install --frozen-lockfile --silent +# copy all files +COPY . . + CMD npx wait-on --timeout 10000 $POSTGREST_URL && node main.js diff --git a/data-generation/accounts.js b/data-generation/accounts.js new file mode 100644 index 0000000..640a3ff --- /dev/null +++ b/data-generation/accounts.js @@ -0,0 +1,82 @@ +import {faker} from '@faker-js/faker'; + +import {postToBackend} from './utils.js' + +export async function generateAccounts(orcids){ + const accounts = await postAccountsToBackend(100); + const ids = accounts.map(a => a.id) + const logins = await postToBackend('/login_for_account', generateLoginForAccount(ids, orcids)) + // console.log('accounts, login_for_accounts done'); + return ids +} + +export async function postAccountsToBackend(amount = 100) { + const accounts = []; + for (let i = 0; i < amount; i++) { + accounts.push({ + public_orcid_profile: !!faker.helpers.maybe(() => true, { + probability: 0.8, + }), + agree_terms: !!faker.helpers.maybe(() => true, {probability: 0.8}), + notice_privacy_statement: !!faker.helpers.maybe(() => true, { + probability: 0.8, + }), + }); + } + + return postToBackend('/account', accounts); +} + +// Generate one login_for_account per given account +export function generateLoginForAccount(accountIds, orcids) { + const homeOrganisations = [null]; + for (let i = 0; i < 10; i++) { + homeOrganisations.push('Organisation for ' + faker.word.noun()); + } + const providers = ['ipd1', 'idp2', 'idp3', 'ip4']; + + let orcidsAdded = 0; + const login_for_accounts = []; + accountIds.forEach(accountId => { + let firstName = faker.person.firstName(); + let givenName = faker.person.lastName(); + + if (orcidsAdded < orcids.length) { + const orcid = orcids[orcidsAdded]; + orcidsAdded += 1; + login_for_accounts.push({ + account: accountId, + name: firstName + ' ' + givenName, + email: faker.internet.email({ + firstName: firstName, + lastName: givenName, + }), + sub: orcid, + provider: 'orcid', + home_organisation: faker.helpers.arrayElement(homeOrganisations), + last_login_date: + faker.helpers.maybe(() => faker.date.past({years: 3}), { + probability: 0.8, + }) ?? null, + }); + } else { + login_for_accounts.push({ + account: accountId, + name: firstName + ' ' + givenName, + email: faker.internet.email({ + firstName: firstName, + lastName: givenName, + }), + sub: faker.string.alphanumeric(30), + provider: faker.helpers.arrayElement(providers), + home_organisation: faker.helpers.arrayElement(homeOrganisations), + last_login_date: + faker.helpers.maybe(() => faker.date.past({years: 3}), { + probability: 0.8, + }) ?? null, + }); + } + }); + return login_for_accounts; +} + diff --git a/data-generation/auth.js b/data-generation/auth.js new file mode 100644 index 0000000..cbba3e2 --- /dev/null +++ b/data-generation/auth.js @@ -0,0 +1,14 @@ +import jwt from 'jsonwebtoken'; + +function createJWT() { + const secret = process.env.PGRST_JWT_SECRET || 'reallyreallyreallyreallyverysafe'; + return jwt.sign({role: 'rsd_admin'}, secret, {expiresIn: '2m'}); +} + +export const token = createJWT(); + +export const headers = { + 'Content-Type': 'application/json', + Authorization: 'bearer ' + token, + Prefer: 'return=representation,resolution=ignore-duplicates', +}; diff --git a/data-generation/community.js b/data-generation/community.js new file mode 100644 index 0000000..ad854bb --- /dev/null +++ b/data-generation/community.js @@ -0,0 +1,103 @@ +import {faker} from '@faker-js/faker'; + +import { + generateRelationsForDifferingEntities, + generateUniqueCaseInsensitiveString, + generateKeywordsForEntity, + postToBackend, + getKeywordIds +} from './utils.js' +import {organisationLogos,getLocalImageIds} from './images.js'; + +export async function generateCommunities({idsSoftware,amount = 500}){ + const localOrganisationLogoIds = await getLocalImageIds(organisationLogos); + const idsKeywords = await getKeywordIds() + // add communities + const communities = await postToBackend('/community', createCommunities(localOrganisationLogoIds,amount)) + const idsCommunities = communities.map(c=>c.id) + // add other data + const comData = await Promise.all([ + postToBackend('/keyword_for_community', generateKeywordsForEntity(idsCommunities, idsKeywords, 'community')), + postToBackend('/software_for_community', generateSoftwareForCommunity(idsSoftware, idsCommunities)), + generateCategories(idsCommunities) + ]) + + return idsCommunities +} + +export function createCommunities(localOrganisationLogoIds,amount = 500) { + const result = []; + + for (let index = 0; index < amount; index++) { + const maxWords = faker.helpers.maybe(() => 5, {probability: 0.8}) ?? 31; + const name = generateUniqueCaseInsensitiveString(() => + ('Community ' + faker.word.words(faker.number.int({max: maxWords, min: 1}))).substring(0, 200), + ); + + result.push({ + slug: faker.helpers.slugify(name).toLowerCase().replaceAll(/-{2,}/g, '-').replaceAll(/-+$/g, ''), // removes double dashes and trailing dashes + name: name, + short_description: faker.helpers.maybe(() => faker.lorem.paragraphs(1, '\n\n'), {probability: 0.8}) ?? null, + description: faker.helpers.maybe(() => faker.lorem.paragraphs(1, '\n\n'), {probability: 0.8}) ?? null, + logo_id: + faker.helpers.maybe(() => localOrganisationLogoIds[index % localOrganisationLogoIds.length], {probability: 0.8}) ?? + null, + }); + } + + return result; +} + +export async function generateCategories(idsCommunities, maxDepth = 3) { + const communityPromises = []; + for (const commId of idsCommunities) { + communityPromises.push(generateAndSaveCategoriesForCommunity(commId, maxDepth)); + } + communityPromises.push(generateAndSaveCategoriesForCommunity(null, maxDepth)); + + return await Promise.all(communityPromises); +} + +export async function generateAndSaveCategoriesForCommunity(idCommunity, maxDepth) { + return new Promise(async res => { + let parentIds = [null]; + for (let level = 1; level <= maxDepth; level++) { + const newParentIds = []; + for (const parent of parentIds) { + let toGenerateCount = faker.number.int(4); + if (idCommunity === null && level === 1) { + toGenerateCount += 1; + } + for (let i = 0; i < toGenerateCount; i++) { + const name = `Parent ${parent}, level ${level}, item ${i + 1}`; + const shortName = `Level ${level}, item ${i + 1}`; + const body = { + community: idCommunity, + parent: parent, + short_name: shortName, + name: name, + }; + const categories = await postToBackend('/category', body) + newParentIds.push(categories[0].id) + } + } + parentIds = newParentIds; + } + res(); + }); +} + +export function generateSoftwareForCommunity(idsSoftware, idsCommunities) { + const result = generateRelationsForDifferingEntities(idsCommunities, idsSoftware, 'community', 'software'); + + const statuses = [ + {weight: 1, value: 'pending'}, + {weight: 8, value: 'approved'}, + {weight: 1, value: 'rejected'}, + ]; + result.forEach(entry => { + entry['status'] = faker.helpers.weightedArrayElement(statuses); + }); + + return result; +} diff --git a/data-generation/images.js b/data-generation/images.js index 53dc4d7..846c610 100644 --- a/data-generation/images.js +++ b/data-generation/images.js @@ -5,6 +5,9 @@ // // SPDX-License-Identifier: Apache-2.0 +import fs from 'fs/promises'; +import {getFromBackend, mimeTypeFromFileName,postToBackend} from "./utils.js" + export const images = [ 'img/213x640-pexels-1262302.png', 'img/426x640-pexels-357573.jpg', @@ -52,3 +55,67 @@ export const softwareLogos = [ 'img/software/Tux.svg', 'img/software/Xenon_logo.svg', ]; + + +// returns the IDs of the images after they have been posted to the database +export async function getLocalImageIds(fileNames) { + const imageAsBase64Promises = []; + + for (let index = 0; index < fileNames.length; index++) { + const fileName = fileNames[index]; + imageAsBase64Promises[index] = fs.readFile(fileName, {encoding: 'base64'}).then(base64 => { + return { + data: base64, + mime_type: mimeTypeFromFileName(fileName), + }; + }); + } + + const imagesAsBase64 = await Promise.all(imageAsBase64Promises); + // create images + let images = await postToBackend('/image?select=id', imagesAsBase64); + // same images posted - no return + if (images.length === 0){ + // get images from backend + images = await getFromBackend('/image?select=id') + } + const ids = images.map(a => a.id); + return ids; +} + + +// returns the IDs of the images after they have been posted to the database +export async function downloadAndGetImages(urlGenerator, amount) { + const imageAsBase64Promises = []; + const timeOuts = []; + for (let index = 0; index < amount; index++) { + const url = urlGenerator(); + imageAsBase64Promises.push( + Promise.race([ + fetch(url) + .then(resp => { + clearTimeout(timeOuts[index]); + return resp.arrayBuffer(); + }) + .then(ab => Buffer.from(ab)) + .then(bf => bf.toString('base64')), + new Promise((res, rej) => (timeOuts[index] = setTimeout(res, 3000))).then(() => { + console.warn('Timeout for ' + url + ', skipping'); + return null; + }), + ]), + ); + } + const imagesAsBase64 = await Promise.all(imageAsBase64Promises); + + const imagesWithoutNulls = imagesAsBase64 + .filter(img => img !== null) + .map(base64 => { + return {data: base64, mime_type: 'image/jpeg'}; + }); + + const resp = await postToBackend('/image?select=id', imagesWithoutNulls); + const idsAsObjects = await resp.json(); + const ids = idsAsObjects.map(idAsObject => idAsObject.id); + return ids; +} diff --git a/data-generation/main.js b/data-generation/main.js index d409eb2..88aabea 100644 --- a/data-generation/main.js +++ b/data-generation/main.js @@ -1,1121 +1,59 @@ -// SPDX-FileCopyrightText: 2022 - 2023 Christian Meeßen (GFZ) -// SPDX-FileCopyrightText: 2022 - 2023 Dusan Mijatovic (dv4all) -// SPDX-FileCopyrightText: 2022 - 2023 Helmholtz Centre Potsdam - GFZ German Research Centre for Geosciences -// SPDX-FileCopyrightText: 2022 - 2023 dv4all -// SPDX-FileCopyrightText: 2022 - 2024 Ewan Cahen (Netherlands eScience Center) -// SPDX-FileCopyrightText: 2022 - 2024 Netherlands eScience Center -// SPDX-FileCopyrightText: 2023 - 2024 Dusan Mijatovic (Netherlands eScience Center) -// -// SPDX-License-Identifier: Apache-2.0 -import {faker} from '@faker-js/faker'; -import jwt from 'jsonwebtoken'; -import {images, organisationLogos, softwareLogos} from './images.js'; -import {conceptDois, dois, packageManagerLinks} from './real-data.js'; -import fs from 'fs/promises'; - -const usedLowerCaseStrings = new Set(); -function generateUniqueCaseInsensitiveString(randomStringGenerator) { - for (let attempt = 0; attempt < 10000; attempt++) { - const nextString = randomStringGenerator(); - if (usedLowerCaseStrings.has(nextString.toLowerCase())) continue; - - usedLowerCaseStrings.add(nextString.toLowerCase()); - return nextString; - } - throw 'Tried to generate a unique (ignoring case) string for 10000 times but failed to do so'; -} - -function generateMentions(amountExtra = 100) { - const mentionTypes = [ - 'blogPost', - 'book', - 'bookSection', - 'computerProgram', - 'conferencePaper', - 'dataset', - 'interview', - 'highlight', - 'journalArticle', - 'magazineArticle', - 'newspaperArticle', - 'poster', - 'presentation', - 'report', - 'thesis', - 'videoRecording', - 'webpage', - 'workshop', - 'other', - ]; - - const result = []; - - // first use up all the DOIs, then generate random mentions without DOI - for (const doi of dois) { - result.push({ - doi: doi, - url: 'https://doi.org/' + doi, - title: faker.music.songName(), - authors: faker.helpers.maybe(() => faker.person.fullName(), 0.8) ?? null, - publisher: faker.helpers.maybe(() => faker.company.name(), 0.8) ?? null, - publication_year: faker.number.int({max: 2026, min: 2000}), - journal: faker.helpers.maybe(() => faker.company.name(), 0.8) ?? null, - page: faker.helpers.maybe(() => faker.number.int({max: 301, min: 0}), 0.1) ?? null, - image_url: null, - mention_type: faker.helpers.arrayElement(mentionTypes), - source: 'faker', - version: faker.helpers.maybe(() => faker.system.semver(), 0.8) ?? null, - note: faker.helpers.maybe(() => faker.company.catchPhrase(), 0.3) ?? null, - }); - } - - for (let index = 0; index < amountExtra; index++) { - result.push({ - doi: null, - url: faker.internet.url(), - title: faker.music.songName(), - authors: faker.helpers.maybe(() => faker.person.fullName(), 0.8) ?? null, - publisher: faker.helpers.maybe(() => faker.company.name(), 0.8) ?? null, - publication_year: faker.number.int({max: 2026, min: 2000}), - journal: faker.helpers.maybe(() => faker.company.name(), 0.8) ?? null, - page: faker.helpers.maybe(() => faker.number.int({max: 301, min: 0}), 0.1) ?? null, - image_url: null, - mention_type: faker.helpers.arrayElement(mentionTypes), - source: 'faker', - version: faker.helpers.maybe(() => faker.system.semver(), 0.8) ?? null, - note: faker.helpers.maybe(() => faker.company.catchPhrase(), 0.3) ?? null, - }); - } - - return result; -} - -function generateSoftware(amount = 500) { - // real software has a real concept DOI - const amountRealSoftware = Math.min(conceptDois.length, amount); - const brandNames = []; - for (let index = 0; index < amountRealSoftware; index++) { - const maxWords = faker.helpers.maybe(() => 5, {probability: 0.8}) ?? 31; - const brandName = generateUniqueCaseInsensitiveString(() => - ('Real software: ' + faker.word.words(faker.number.int({max: maxWords, min: 1}))).substring(0, 200), - ); - brandNames.push(brandName); - } - - const amountFakeSoftware = amount - amountRealSoftware; - for (let index = 0; index < amountFakeSoftware; index++) { - const maxWords = faker.helpers.maybe(() => 5, {probability: 0.8}) ?? 31; - const brandName = generateUniqueCaseInsensitiveString(() => - ('Software: ' + faker.word.words(faker.number.int({max: maxWords, min: 1}))).substring(0, 200), - ); - brandNames.push(brandName); - } - - const result = []; - - for (let index = 0; index < amount; index++) { - result.push({ - slug: faker.helpers - .slugify(brandNames[index]) - .toLowerCase() - .replaceAll(/-{2,}/g, '-') - .replaceAll(/-+$/g, ''), // removes double dashes and trailing dashes - brand_name: brandNames[index], - concept_doi: index < conceptDois.length ? conceptDois[index] : null, - description: faker.lorem.paragraphs(4, '\n\n'), - get_started_url: faker.internet.url(), - image_id: - faker.helpers.maybe(() => localSoftwareLogoIds[index % localSoftwareLogoIds.length], { - probability: 0.8, - }) ?? null, - is_published: !!faker.helpers.maybe(() => true, {probability: 0.8}), - short_statement: faker.commerce.productDescription(), - closed_source: !!faker.helpers.maybe(() => true, { - probability: 0.8, - }), - }); - } - - return result; -} - -function generateTestimonials(ids) { - const result = []; - - for (const id of ids) { - // each software will get 0, 1 or 2 testimonials - const numberOfTestimonials = faker.number.int({max: 3, min: 0}); - for (let index = 0; index < numberOfTestimonials; index++) { - result.push({ - software: id, - message: faker.hacker.phrase(), - source: faker.person.fullName(), - }); - } - } - - return result; -} - -function generateRepositoryUrls(ids) { - const githubUrls = [ - 'https://github.com/research-software-directory/RSD-as-a-service', - 'https://github.com/wadpac/GGIR', - 'https://github.com/ESMValGroup/ESMValTool', - 'https://github.com/ESMValGroup/ESMValCore', - 'https://github.com/benvanwerkhoven/kernel_tuner', - 'https://github.com/NLeSC/pattyvis', - ]; - - const gitlabUrls = [ - 'https://gitlab.com/dwt1/dotfiles', - 'https://gitlab.com/famedly/fluffychat', - 'https://gitlab.com/gitlab-org/gitlab-shell', - 'https://gitlab.com/cerfacs/batman', - 'https://gitlab.com/cyber5k/mistborn', - ]; - - const repoUrls = githubUrls.concat(gitlabUrls); - - const result = []; - - for (let index = 0; index < ids.length; index++) { - if (!!faker.helpers.maybe(() => true, {probability: 0.25})) continue; - - const repoUrl = faker.helpers.arrayElement(repoUrls); - const codePlatform = repoUrl.startsWith('https://github.com') ? 'github' : 'gitlab'; - result.push({ - software: ids[index], - url: repoUrl, - code_platform: codePlatform, - }); - } - - return result; -} - -function generatePackageManagers(softwareIds) { - const result = []; - - for (let index = 0; index < softwareIds.length; index++) { - // first assign each package manager entry to one software, then randomly assing package manager entries to the remaining ids - const packageManagerLink = - index < packageManagerLinks.length - ? packageManagerLinks[index] - : faker.helpers.arrayElement(packageManagerLinks); - - result.push({ - software: softwareIds[index], - url: packageManagerLink.url, - package_manager: packageManagerLink.type, - }); - } - - return result; -} - -function generateLincensesForSoftware(ids) { - const licenses = [ - { - license: 'Apache-2.0', - name: 'Apache License 2.0', - reference: 'https://spdx.org/licenses/Apache-2.0.html', - }, - { - license: 'MIT', - name: 'MIT License', - reference: 'https://spdx.org/licenses/MIT.html', - }, - { - license: 'GPL-2.0-or-later', - name: 'GNU General Public License v2.0 or later', - reference: 'https://spdx.org/licenses/GPL-2.0-or-later.html', - }, - { - license: 'LGPL-2.0-or-later', - name: 'GNU Library General Public License v2 or later', - reference: 'https://spdx.org/licenses/LGPL-2.0-or-later.html', - }, - { - license: 'CC-BY-4.0', - name: 'Creative Commons Attribution 4.0 International', - reference: 'https://spdx.org/licenses/CC-BY-4.0.html', - }, - { - license: 'CC-BY-NC-ND-3.0', - name: 'Creative Commons Attribution Non Commercial No Derivatives 3.0 Unported', - reference: 'https://spdx.org/licenses/CC-BY-NC-ND-3.0.html', - }, - ]; - - const result = []; - - for (const id of ids) { - const nummerOfLicenses = faker.number.int({max: 3, min: 0}); - if (nummerOfLicenses === 0) continue; - - const licensesToAdd = faker.helpers.arrayElements(licenses, nummerOfLicenses); - for (const item of licensesToAdd) { - result.push({ - software: id, - license: item.license, - name: item.name, - reference: item.reference, - }); - } - } - - return result; -} - -function generateKeywordsForEntity(idsEntity, idsKeyword, nameEntity) { - const result = []; - - for (const idEntity of idsEntity) { - const nummerOfKeywords = faker.number.int({max: 3, min: 0}); - if (nummerOfKeywords === 0) continue; - - const keywordIdsToAdd = faker.helpers.arrayElements(idsKeyword, nummerOfKeywords); - for (const keywordId of keywordIdsToAdd) { - result.push({ - [nameEntity]: idEntity, - keyword: keywordId, - }); - } - } - - return result; -} - -async function generateCategories(idsCommunities, maxDepth = 3) { - const communityPromises = []; - for (const commId of idsCommunities) { - communityPromises.push(generateAndSaveCategoriesForCommunity(commId, maxDepth)); - } - communityPromises.push(generateAndSaveCategoriesForCommunity(null, maxDepth)); - - return await Promise.all(communityPromises); -} - -async function generateAndSaveCategoriesForCommunity(idCommunity, maxDepth) { - return new Promise(async res => { - let parentIds = [null]; - for (let level = 1; level <= maxDepth; level++) { - const newParentIds = []; - for (const parent of parentIds) { - let toGenerateCount = faker.number.int(4); - if (idCommunity === null && level === 1) { - toGenerateCount += 1; - } - for (let i = 0; i < toGenerateCount; i++) { - const name = `Parent ${parent}, level ${level}, item ${i + 1}`; - const shortName = `Level ${level}, item ${i + 1}`; - const body = { - community: idCommunity, - parent: parent, - short_name: shortName, - name: name, - }; - await postToBackend('/category', body) - .then(resp => resp.json()) - .then(json => json[0].id) - .then(id => newParentIds.push(id)); - } - } - parentIds = newParentIds; - } - res(); - }); -} - -function generateMentionsForEntity(idsEntity, idsMention, nameEntity) { - const result = []; - - for (const idEntity of idsEntity) { - const nummerOfMentions = faker.number.int({max: 11, min: 0}); - if (nummerOfMentions === 0) continue; - - const mentionIdsToAdd = faker.helpers.arrayElements(idsMention, nummerOfMentions); - for (const mentionId of mentionIdsToAdd) { - result.push({ - [nameEntity]: idEntity, - mention: mentionId, - }); - } - } - - return result; -} - -function generateResearchDomainsForProjects(idsProject, idsResearchDomain) { - const result = []; - - for (const idProject of idsProject) { - const nummerOfKeywords = faker.number.int({max: 3, min: 0}); - if (nummerOfKeywords === 0) continue; - - const researchDomainIdsToAdd = faker.helpers.arrayElements(idsResearchDomain, nummerOfKeywords); - for (const researchDomainId of researchDomainIdsToAdd) { - result.push({ - project: idProject, - research_domain: researchDomainId, - }); - } - } - - return result; -} - -function generateSoftwareForSoftware(ids) { - const result = []; - - for (let index = 0; index < ids.length; index++) { - const numberOfRelatedSoftware = faker.number.int({max: 5, min: 0}); - if (numberOfRelatedSoftware === 0) continue; - - const origin = ids[index]; - const idsWithoutOrigin = ids.filter(id => id !== origin); - const idsRelation = faker.helpers.arrayElements(idsWithoutOrigin, numberOfRelatedSoftware); - for (const relation of idsRelation) { - result.push({ - origin: origin, - relation: relation, - }); - } - } - - return result; -} - -function generateSoftwareHighlights(ids) { - const result = []; - for (let index = 0; index < ids.length; index++) { - const isHighlight = !!faker.helpers.maybe(() => true, { - probability: 0.3, - }); - if (isHighlight === true) result.push({software: ids[index]}); - } - return result; -} - -function generateProjects(amount = 500) { - const result = []; - - const projectStatuses = ['finished', 'running', 'starting']; - - for (let index = 0; index < amount; index++) { - const maxWords = faker.helpers.maybe(() => 5, {probability: 0.8}) ?? 31; - const title = generateUniqueCaseInsensitiveString(() => - ('Project: ' + faker.word.words(faker.number.int({max: maxWords, min: 1}))).substring(0, 200), - ); - - const status = faker.helpers.arrayElement(projectStatuses); - let dateEnd, dateStart; - switch (status) { - case 'finished': - dateEnd = faker.date.past({years: 2}); - dateStart = faker.date.past({years: 2, refDate: dateEnd}); - break; - case 'running': - dateEnd = faker.date.future({years: 2}); - dateStart = faker.date.past({years: 2}); - break; - case 'starting': - dateStart = faker.date.future({years: 2}); - dateEnd = faker.date.future({years: 2, refDate: dateStart}); - break; - } - - result.push({ - slug: faker.helpers.slugify(title).toLowerCase().replaceAll(/-{2,}/g, '-').replaceAll(/-+$/g, ''), // removes double dashes and trailing dashes - title: title, - subtitle: - faker.helpers.maybe(() => faker.commerce.productDescription(), { - probability: 0.9, - }) ?? null, - date_end: faker.helpers.maybe(() => dateEnd, {probability: 0.9}) ?? null, - date_start: faker.helpers.maybe(() => dateStart, {probability: 0.9}) ?? null, - description: faker.lorem.paragraphs(5, '\n\n'), - grant_id: faker.helpers.maybe(() => faker.helpers.replaceSymbols('******'), {probability: 0.8}) ?? null, - image_caption: faker.animal.cat(), - image_contain: !!faker.helpers.maybe(() => true, { - probability: 0.5, - }), - image_id: - faker.helpers.maybe(() => localImageIds[index % localImageIds.length], {probability: 0.8}) ?? null, - is_published: !!faker.helpers.maybe(() => true, {probability: 0.8}), - }); - } - - return result; -} - -function generateOrcids(amount = 50) { - const orcids = new Set(); - - while (orcids.size < amount) { - orcids.add(faker.helpers.replaceSymbolWithNumber('0000-000#-####-####')); - } - - return [...orcids]; -} - -function generatePeopleWithOrcids(orcids, imageIds) { - const result = []; - - for (const orcid of orcids) { - result.push({ - email_address: faker.internet.email(), - family_names: faker.person.lastName(), - given_names: faker.person.firstName(), - orcid: orcid, - avatar_id: faker.helpers.arrayElement(imageIds), - }); - } - - return result; -} - -async function generateContributors(softwareIds, peopleWithOrcids, minPerSoftware = 0, maxPerSoftware = 15) { - const result = []; - - for (const softwareId of softwareIds) { - const amount = faker.number.int({ - max: maxPerSoftware, - min: minPerSoftware, - }); - const amountWithOrcid = faker.number.int({max: amount, min: 0}); - const amountWithoutOrcid = amount - amountWithOrcid; - - for (let i = 0; i < amountWithoutOrcid; i++) { - result.push({ - software: softwareId, - is_contact_person: !!faker.helpers.maybe(() => true, { - probability: 0.2, - }), - email_address: faker.internet.email(), - family_names: faker.person.lastName(), - given_names: faker.person.firstName(), - affiliation: faker.company.name(), - role: faker.person.jobTitle(), - orcid: null, - avatar_id: - faker.helpers.maybe(() => faker.helpers.arrayElement(localImageIds), {probability: 0.8}) ?? null, - }); - } - - const randomPeopleWithOrcdid = faker.helpers.arrayElements(peopleWithOrcids, amountWithOrcid); - - for (const personWithOrcid of randomPeopleWithOrcdid) { - result.push({ - ...personWithOrcid, - software: softwareId, - is_contact_person: !!faker.helpers.maybe(() => true, { - probability: 0.2, - }), - affiliation: faker.company.name(), - role: faker.person.jobTitle(), - }); - } - } - - return result; -} - -async function generateTeamMembers(projectIds, peopleWithOrcids, minPerProject = 0, maxPerProject = 15) { - const result = await generateContributors(projectIds, peopleWithOrcids, minPerProject, maxPerProject); - result.forEach(contributor => { - contributor['project'] = contributor['software']; - delete contributor['software']; - }); - return result; -} - -function generateUrlsForProjects(ids) { - const result = []; - - for (const id of ids) { - // each project will get 0, 1 or 2 URLs - const numberOfUrls = faker.number.int({max: 3, min: 0}); - for (let index = 0; index < numberOfUrls; index++) { - result.push({ - project: id, - title: faker.commerce.product(), - url: faker.internet.url(), - }); - } - } - - return result; -} - -function generateOrganisations(amount = 500) { - const rorIds = [ - 'https://ror.org/000k1q888', - 'https://ror.org/006hf6230', - 'https://ror.org/008pnp284', - 'https://ror.org/00f9tz983', - 'https://ror.org/00x7ekv49', - 'https://ror.org/00za53h95', - 'https://ror.org/012p63287', - 'https://ror.org/01460j859', - 'https://ror.org/014w0fd65', - 'https://ror.org/016xsfp80', - 'https://ror.org/018dfmf50', - 'https://ror.org/01bnjb948', - 'https://ror.org/01deh9c76', - 'https://ror.org/01hcx6992', - 'https://ror.org/01k0v6g02', - 'https://ror.org/01ryk1543', - 'https://ror.org/027bh9e22', - 'https://ror.org/02e2c7k09', - 'https://ror.org/02e7b5302', - 'https://ror.org/02en5vm52', - 'https://ror.org/02jx3x895', - 'https://ror.org/02jz4aj89', - 'https://ror.org/02w4jbg70', - 'https://ror.org/030a5r161', - 'https://ror.org/031m0hs53', - 'https://ror.org/041kmwe10', - 'https://ror.org/04bdffz58', - 'https://ror.org/04dkp9463', - 'https://ror.org/04njjy449', - 'https://ror.org/04qw24q55', - 'https://ror.org/04s2z4291', - 'https://ror.org/04x6kq749', - 'https://ror.org/052578691', - 'https://ror.org/054hq4w78', - 'https://ror.org/055d8gs64', - 'https://ror.org/05dfgh554', - 'https://ror.org/05jxfge78', - 'https://ror.org/05kaxyq51', - 'https://ror.org/05v6zeb66', - 'https://ror.org/05xvt9f17', - ]; - - const names = []; - for (let index = 0; index < amount; index++) { - const maxWords = faker.helpers.maybe(() => 5, {probability: 0.8}) ?? 31; - const name = generateUniqueCaseInsensitiveString(() => - ('Organisation: ' + faker.word.words(faker.number.int({max: maxWords, min: 1}))).substring(0, 200), - ); - names.push(name); - } - - const result = []; - - for (let index = 0; index < amount; index++) { - result.push({ - parent: null, - primary_maintainer: null, - slug: faker.helpers.slugify(names[index]).toLowerCase().replaceAll(/-{2,}/g, '-').replaceAll(/-+$/g, ''), // removes double dashes and trailing dashes - name: names[index], - short_description: - faker.helpers.maybe(() => faker.commerce.productDescription(), { - probability: 0.8, - }) ?? null, - ror_id: index < rorIds.length ? rorIds[index] : null, - website: faker.internet.url(), - is_tenant: !!faker.helpers.maybe(() => true, {probability: 0.05}), - country: - faker.helpers.maybe(() => faker.location.country(), { - probability: 0.8, - }) ?? null, - logo_id: - faker.helpers.maybe(() => localOrganisationLogoIds[index % localOrganisationLogoIds.length], { - probability: 0.8, - }) ?? null, - }); - } - - return result; -} - -function generateCommunities(amount = 50) { - const result = []; - - for (let index = 0; index < amount; index++) { - const maxWords = faker.helpers.maybe(() => 5, {probability: 0.8}) ?? 31; - const name = generateUniqueCaseInsensitiveString(() => - ('Community: ' + faker.word.words(faker.number.int({max: maxWords, min: 1}))).substring(0, 200), - ); - - result.push({ - slug: faker.helpers.slugify(name).toLowerCase().replaceAll(/-{2,}/g, '-').replaceAll(/-+$/g, ''), // removes double dashes and trailing dashes - name: name, - short_description: faker.helpers.maybe(() => faker.lorem.paragraphs(1, '\n\n'), {probability: 0.8}) ?? null, - description: faker.helpers.maybe(() => faker.lorem.paragraphs(1, '\n\n'), {probability: 0.8}) ?? null, - logo_id: - faker.helpers.maybe(() => localOrganisationLogoIds[index % localImageIds.length], {probability: 0.8}) ?? - null, - }); - } - - return result; -} - -function generateMetaPages() { - const result = []; - - const titles = ['About', 'Terms of Service', 'Privacy Statement']; - const slugs = ['about', 'terms-of-service', 'privacy-statement']; - for (let index = 0; index < titles.length; index++) { - result.push({ - title: titles[index], - slug: slugs[index], - description: faker.lorem.paragraphs(10, '\n\n'), - is_published: true, - position: index + 1, - }); - } - - return result; -} - -function generateNews() { - const entries = [ - { - title: 'RSD released', - slug: 'rsd-released', - }, - { - title: 'Some Big News', - slug: 'some-big-news', - }, - { - title: 'You wont believe this!', - slug: 'you-wont-believe-this', - }, - { - title: "The perfect software doesn't exi-", - slug: 'the-prefect-software-doesnt-exi', - }, - { - title: "10 clickbait headlines you didn't know about!", - slug: '10-clickbait-headlines', - }, - { - title: 'You will never use a dependency anymore after you know this...', - slug: 'never-dependency', - }, - { - title: 'Sunsetting the RSD', - slug: 'sunsetting-the-rsd', - }, - { - title: 'The last package you will ever need', - slug: 'last-package', - }, - { - title: 'How to make your project a big success', - slug: 'project-success', - }, - { - title: 'The 5 best dependencies you never heard about', - slug: '5-best-dependencies', - }, - { - title: 'Rewriting the RSD in CrabLang', - slug: 'rewrite-rsd-crablang', - }, - { - title: 'The RSD joins forces with Big Company (tm)', - slug: 'rsd-joins-big-company', - }, - { - title: "3 features you didn't know about", - slug: '3-features', - }, - { - title: 'Interview with RSD founders', - slug: 'interview-rsd-founders', - }, - ]; - - const result = []; - for (const newsItem of entries) { - result.push({ - slug: newsItem.slug, - is_published: !!faker.helpers.maybe(() => true, {probability: 0.8}), - publication_date: faker.date.anytime(), - title: newsItem.title, - author: faker.person.fullName(), - summary: faker.lorem.paragraph(), - description: faker.lorem.paragraphs(faker.number.int({max: 20, min: 3}), '\n\n'), - }); - } - - return result; -} - -function generateImagesForNews(newsIds, imageIds) { - const result = []; - - for (const id of newsIds) { - if (faker.datatype.boolean(0.2)) { - continue; - } - - result.push({ - news: id, - image_id: faker.helpers.arrayElement(imageIds), - }); - } - - return result; -} - -function generateRelationsForDifferingEntities( - idsOrigin, - idsRelation, - nameOrigin, - nameRelation, - maxRelationsPerOrigin = 11, -) { - const result = []; - - for (const idOrigin of idsOrigin) { - const numberOfIdsRelation = faker.number.int({ - max: maxRelationsPerOrigin, - min: 0, - }); - const relationsToAdd = faker.helpers.arrayElements(idsRelation, numberOfIdsRelation); - for (const idRelation of relationsToAdd) { - result.push({ - [nameOrigin]: idOrigin, - [nameRelation]: idRelation, - }); - } - } - - return result; -} - -function generateProjectForOrganisation(idsProjects, idsOrganisations) { - const result = generateRelationsForDifferingEntities(idsProjects, idsOrganisations, 'project', 'organisation'); - - const roles = ['funding', 'hosting', 'participating']; - result.forEach(entry => { - entry['role'] = faker.helpers.arrayElement(roles); - }); - - return result; -} - -function generateSoftwareForCommunity(idsSoftware, idsCommunities) { - const result = generateRelationsForDifferingEntities(idsCommunities, idsSoftware, 'community', 'software'); - - const statuses = [ - {weight: 1, value: 'pending'}, - {weight: 8, value: 'approved'}, - {weight: 1, value: 'rejected'}, - ]; - result.forEach(entry => { - entry['status'] = faker.helpers.weightedArrayElement(statuses); - }); - - return result; -} - -function createJWT() { - const secret = process.env.PGRST_JWT_SECRET; - return jwt.sign({role: 'rsd_admin'}, secret, {expiresIn: '2m'}); -} - -const token = createJWT(); -const headers = { - 'Content-Type': 'application/json', - Authorization: 'bearer ' + token, - Prefer: 'return=representation,resolution=ignore-duplicates', -}; -const backendUrl = process.env.POSTGREST_URL || 'http://localhost/api/v1'; - -async function postToBackend(endpoint, body) { - const response = await fetch(backendUrl + endpoint, { - method: 'POST', - body: JSON.stringify(body), - headers: headers, - }); - if (!response.ok) { - console.warn( - 'Warning: post request to ' + - endpoint + - ' had status code ' + - response.status + - ' and body ' + - (await response.text()), - ); - } - return response; -} - -async function getFromBackend(endpoint) { - const response = await fetch(backendUrl + endpoint, {headers: headers}); - if (!response.ok) { - console.warn( - 'Warning: post request to ' + - endpoint + - ' had status code ' + - response.status + - ' and body ' + - (await response.text()), - ); - } - return response; -} - -// returns the IDs of the images after they have been posted to the database -async function getLocalImageIds(fileNames) { - const imageAsBase64Promises = []; - for (let index = 0; index < fileNames.length; index++) { - const fileName = fileNames[index]; - imageAsBase64Promises[index] = fs.readFile(fileName, {encoding: 'base64'}).then(base64 => { - return { - data: base64, - mime_type: mimeTypeFromFileName(fileName), - }; - }); - } - const imagesAsBase64 = await Promise.all(imageAsBase64Promises); - - const resp = await postToBackend('/image?select=id', imagesAsBase64); - const idsAsObjects = await resp.json(); - const ids = idsAsObjects.map(idAsObject => idAsObject.id); - return ids; -} - -function mimeTypeFromFileName(fileName) { - if (fileName.endsWith('.png')) { - return 'image/png'; - } else if (fileName.endsWith('.jpg') || fileName.endsWith('.jpeg')) { - return 'image/jpg'; - } else if (fileName.endsWith('.svg')) { - return 'image/svg+xml'; - } else return null; -} - -// returns the IDs of the images after they have been posted to the database -async function downloadAndGetImages(urlGenerator, amount) { - const imageAsBase64Promises = []; - const timeOuts = []; - for (let index = 0; index < amount; index++) { - const url = urlGenerator(); - imageAsBase64Promises.push( - Promise.race([ - fetch(url) - .then(resp => { - clearTimeout(timeOuts[index]); - return resp.arrayBuffer(); - }) - .then(ab => Buffer.from(ab)) - .then(bf => bf.toString('base64')), - new Promise((res, rej) => (timeOuts[index] = setTimeout(res, 3000))).then(() => { - console.warn('Timeout for ' + url + ', skipping'); - return null; - }), - ]), - ); - } - const imagesAsBase64 = await Promise.all(imageAsBase64Promises); - - const imagesWithoutNulls = imagesAsBase64 - .filter(img => img !== null) - .map(base64 => { - return {data: base64, mime_type: 'image/jpeg'}; - }); - - const resp = await postToBackend('/image?select=id', imagesWithoutNulls); - const idsAsObjects = await resp.json(); - const ids = idsAsObjects.map(idAsObject => idAsObject.id); - return ids; -} - -async function postAccountsToBackend(amount = 100) { - const accounts = []; - for (let i = 0; i < amount; i++) { - accounts.push({ - public_orcid_profile: !!faker.helpers.maybe(() => true, { - probability: 0.8, - }), - agree_terms: !!faker.helpers.maybe(() => true, {probability: 0.8}), - notice_privacy_statement: !!faker.helpers.maybe(() => true, { - probability: 0.8, - }), - }); - } - - return postToBackend('/account', accounts); -} - -// Generate one login_for_account per given account -function generateLoginForAccount(accountIds, orcids) { - const homeOrganisations = [null]; - for (let i = 0; i < 10; i++) { - homeOrganisations.push('Organisation for ' + faker.word.noun()); - } - const providers = ['ipd1', 'idp2', 'idp3', 'ip4']; - - let orcidsAdded = 0; - const login_for_accounts = []; - accountIds.forEach(accountId => { - let firstName = faker.person.firstName(); - let givenName = faker.person.lastName(); - - if (orcidsAdded < orcids.length) { - const orcid = orcids[orcidsAdded]; - orcidsAdded += 1; - login_for_accounts.push({ - account: accountId, - name: firstName + ' ' + givenName, - email: faker.internet.email({ - firstName: firstName, - lastName: givenName, - }), - sub: orcid, - provider: 'orcid', - home_organisation: faker.helpers.arrayElement(homeOrganisations), - last_login_date: - faker.helpers.maybe(() => faker.date.past({years: 3}), { - probability: 0.8, - }) ?? null, - }); - } else { - login_for_accounts.push({ - account: accountId, - name: firstName + ' ' + givenName, - email: faker.internet.email({ - firstName: firstName, - lastName: givenName, - }), - sub: faker.string.alphanumeric(30), - provider: faker.helpers.arrayElement(providers), - home_organisation: faker.helpers.arrayElement(homeOrganisations), - last_login_date: - faker.helpers.maybe(() => faker.date.past({years: 3}), { - probability: 0.8, - }) ?? null, - }); - } - }); - return login_for_accounts; -} +import {generateOrcids} from './utils.js'; +import {generateAccounts} from './accounts.js' +import {generateMentions} from './mentions.js' +import {generateSoftware} from './software.js' +import {generateProject} from './project.js' +import {generateOrganisation} from './organisations.js' +import {generateCommunities} from './community.js' +import {generateNews,generateMetaPages} from './news.js' // start of running code, main const orcids = generateOrcids(); -const localImageIds = await getLocalImageIds(images); -const peopleWithOrcid = generatePeopleWithOrcids(orcids, localImageIds); - -await postAccountsToBackend(100) - .then(() => getFromBackend('/account')) - .then(res => res.json()) - .then(jsonAccounts => jsonAccounts.map(a => a.id)) - .then(async accountIds => postToBackend('/login_for_account', generateLoginForAccount(accountIds, orcids))) - .then(() => console.log('accounts, login_for_accounts done')); - -const localOrganisationLogoIds = await getLocalImageIds(organisationLogos); -const localSoftwareLogoIds = await getLocalImageIds(softwareLogos); - -let idsMentions, idsKeywords, idsResearchDomains; -const mentionsPromise = postToBackend('/mention', generateMentions()) - .then(() => getFromBackend('/mention?select=id')) - .then(res => res.json()) - .then(jsonMentions => (idsMentions = jsonMentions.map(element => element.id))); -const keywordPromise = getFromBackend('/keyword?select=id') - .then(res => res.json()) - .then(jsonKeywords => (idsKeywords = jsonKeywords.map(element => element.id))); -const researchDomainsPromise = getFromBackend('/research_domain?select=id') - .then(res => res.json()) - .then(jsonResearchDomains => (idsResearchDomains = jsonResearchDomains.map(element => element.id))); - -await Promise.all([mentionsPromise, keywordPromise, researchDomainsPromise]).then(() => - console.log('mentions, keywords, research domains done'), -); - -let idsSoftware, idsFakeSoftware, idsRealSoftware, idsProjects, idsOrganisations, idsCommunities; -const softwarePromise = postToBackend('/software', generateSoftware()) - .then(resp => resp.json()) - .then(async swArray => { - idsSoftware = swArray.map(sw => sw['id']); - idsFakeSoftware = swArray.filter(sw => sw['brand_name'].startsWith('Software')).map(sw => sw['id']); - idsRealSoftware = swArray.filter(sw => sw['brand_name'].startsWith('Real software')).map(sw => sw['id']); - postToBackend('/contributor', await generateContributors(idsSoftware, peopleWithOrcid)); - postToBackend('/testimonial', generateTestimonials(idsSoftware)); - postToBackend('/repository_url', generateRepositoryUrls(idsSoftware)); - postToBackend('/package_manager', generatePackageManagers(idsRealSoftware)); - postToBackend('/license_for_software', generateLincensesForSoftware(idsSoftware)); - postToBackend('/keyword_for_software', generateKeywordsForEntity(idsSoftware, idsKeywords, 'software')); - postToBackend('/mention_for_software', generateMentionsForEntity(idsSoftware, idsMentions, 'software')); - postToBackend('/software_for_software', generateSoftwareForSoftware(idsSoftware)); - postToBackend('/software_highlight', generateSoftwareHighlights(idsSoftware.slice(0, 10))); - }); -const projectPromise = postToBackend('/project', generateProjects()) - .then(resp => resp.json()) - .then(async pjArray => { - idsProjects = pjArray.map(sw => sw['id']); - postToBackend('/team_member', await generateTeamMembers(idsProjects, peopleWithOrcid)); - postToBackend('/url_for_project', generateUrlsForProjects(idsProjects)); - postToBackend('/keyword_for_project', generateKeywordsForEntity(idsProjects, idsKeywords, 'project')); - postToBackend('/output_for_project', generateMentionsForEntity(idsProjects, idsMentions, 'project')); - postToBackend('/impact_for_project', generateMentionsForEntity(idsProjects, idsMentions, 'project')); - postToBackend( - '/research_domain_for_project', - generateResearchDomainsForProjects(idsProjects, idsResearchDomains), - ); - postToBackend('/project_for_project', generateSoftwareForSoftware(idsProjects)); - }); -const organisationPromise = postToBackend('/organisation', generateOrganisations()) - .then(resp => resp.json()) - .then(async orgArray => { - idsOrganisations = orgArray.map(org => org['id']); - }); - -const communityPromise = postToBackend('/community', generateCommunities()) - .then(resp => resp.json()) - .then(async commArray => { - idsCommunities = commArray.map(comm => comm['id']); - postToBackend('/keyword_for_community', generateKeywordsForEntity(idsCommunities, idsKeywords, 'community')); - generateCategories(idsCommunities); - }); - -await postToBackend('/meta_pages', generateMetaPages()).then(() => console.log('meta pages done')); -await postToBackend('/news?select=id', generateNews()) - .then(() => getFromBackend('/news')) - .then(res => res.json()) - .then(jsonNewsIds => jsonNewsIds.map(news => news.id)) - .then(newsIds => postToBackend('/image_for_news', generateImagesForNews(newsIds, localImageIds))) - .then(() => console.log('news done')); - -await Promise.all([softwarePromise, projectPromise, organisationPromise, communityPromise]).then(() => - console.log('sw, pj, org, comm done'), -); - -await postToBackend( - '/software_for_project', - generateRelationsForDifferingEntities(idsSoftware, idsProjects, 'software', 'project'), -).then(() => console.log('sw-pj done')); -await postToBackend( - '/software_for_organisation', - generateRelationsForDifferingEntities(idsSoftware, idsOrganisations, 'software', 'organisation'), -).then(() => console.log('sw-org done')); -await postToBackend('/project_for_organisation', generateProjectForOrganisation(idsProjects, idsOrganisations)).then( - () => console.log('pj-org done'), -); -await postToBackend('/software_for_community', generateSoftwareForCommunity(idsSoftware, idsCommunities)).then(() => - console.log('sw-comm done'), -); -await postToBackend( - '/release', - idsSoftware.map(id => ({software: id})), -) - .then(() => - postToBackend( - '/release_version', - generateRelationsForDifferingEntities(idsFakeSoftware, idsMentions, 'release_id', 'mention_id', 100), - ), - ) - .then(() => console.log('releases done')); +// generate accounts and mentions +const [ + accounts, + idsMentions +] = await Promise.all([ + generateAccounts(orcids), + generateMentions() +]) + +console.log("accounts...", accounts.length) +console.log("mentions...", idsMentions.length) + +// software, projects, news and meta pages +const [ + // idsSoftware, + idsProjects, + // idsNews, + idsMeta +] = await Promise.all([ + // generateSoftware({orcids,idsMentions}), + generateProject({orcids,idsMentions}), + // generateNews(), + generateMetaPages() +]) + +// console.log("software...", idsSoftware.length) +console.log("projects...", idsProjects.length) +// console.log("news...", idsNews.length) +console.log("meta pages...", idsMeta.length) + +// organisations and communities +const [ + idsOrganisations, + // idsCommunities, +] = await Promise.all([ + generateOrganisation({idsSoftware:[],idsProjects,idsMentions}), + // generateCommunities({idsSoftware}), +]) + +console.log("organisations...", idsOrganisations.length) + +// console.log("communities...", idsCommunities.length) console.log('Done'); + // This is unfortunately needed, because when using docker compose, the node process might hang for a long time process.exit(0); diff --git a/data-generation/mentions.js b/data-generation/mentions.js new file mode 100644 index 0000000..9f9ec8b --- /dev/null +++ b/data-generation/mentions.js @@ -0,0 +1,104 @@ +import {faker} from '@faker-js/faker'; + +import {getFromBackend, postToBackend} from './utils.js' +import {dois} from './real-data.js'; + +export async function generateMentions(){ + try{ + const mentions = await postToBackend('/mention', createMentions()) + const ids = mentions.map(a=>a.id) + // return list of added ids + return ids + }catch(e){ + console.log("failed to generate mentions...getting existing ones") + // try to get existing ones + const mentions = await getFromBackend('/mention?select=id') + const ids = mentions.map(a=>a.id) + return ids + } +} + +export function createMentions(amountExtra = 100) { + const mentionTypes = [ + 'blogPost', + 'book', + 'bookSection', + 'computerProgram', + 'conferencePaper', + 'dataset', + 'interview', + 'highlight', + 'journalArticle', + 'magazineArticle', + 'newspaperArticle', + 'poster', + 'presentation', + 'report', + 'thesis', + 'videoRecording', + 'webpage', + 'workshop', + 'other', + ]; + + const result = []; + + // first use up all the DOIs, then generate random mentions without DOI + for (const doi of dois) { + result.push({ + doi: doi, + url: 'https://doi.org/' + doi, + title: faker.music.songName(), + authors: faker.helpers.maybe(() => faker.person.fullName(), 0.8) ?? null, + publisher: faker.helpers.maybe(() => faker.company.name(), 0.8) ?? null, + publication_year: faker.number.int({max: 2026, min: 2000}), + journal: faker.helpers.maybe(() => faker.company.name(), 0.8) ?? null, + page: faker.helpers.maybe(() => faker.number.int({max: 301, min: 0}), 0.1) ?? null, + image_url: null, + mention_type: faker.helpers.arrayElement(mentionTypes), + source: 'faker', + version: faker.helpers.maybe(() => faker.system.semver(), 0.8) ?? null, + note: faker.helpers.maybe(() => faker.company.catchPhrase(), 0.3) ?? null, + }); + } + + for (let index = 0; index < amountExtra; index++) { + result.push({ + doi: null, + url: faker.internet.url(), + title: faker.music.songName(), + authors: faker.helpers.maybe(() => faker.person.fullName(), 0.8) ?? null, + publisher: faker.helpers.maybe(() => faker.company.name(), 0.8) ?? null, + publication_year: faker.number.int({max: 2026, min: 2000}), + journal: faker.helpers.maybe(() => faker.company.name(), 0.8) ?? null, + page: faker.helpers.maybe(() => faker.number.int({max: 301, min: 0}), 0.1) ?? null, + image_url: null, + mention_type: faker.helpers.arrayElement(mentionTypes), + source: 'faker', + version: faker.helpers.maybe(() => faker.system.semver(), 0.8) ?? null, + note: faker.helpers.maybe(() => faker.company.catchPhrase(), 0.3) ?? null, + }); + } + + return result; +} + + +export function generateMentionsForEntity(idsEntity, idsMention, nameEntity) { + const result = []; + + for (const idEntity of idsEntity) { + const nummerOfMentions = faker.number.int({max: 11, min: 0}); + if (nummerOfMentions === 0) continue; + + const mentionIdsToAdd = faker.helpers.arrayElements(idsMention, nummerOfMentions); + for (const mentionId of mentionIdsToAdd) { + result.push({ + [nameEntity]: idEntity, + mention: mentionId, + }); + } + } + + return result; +} diff --git a/data-generation/news.js b/data-generation/news.js new file mode 100644 index 0000000..fd24c6d --- /dev/null +++ b/data-generation/news.js @@ -0,0 +1,135 @@ + +import {faker} from '@faker-js/faker'; + +import {postToBackend} from './utils.js'; +import {images,getLocalImageIds} from './images.js'; + +export async function generateNews(){ + const newsImageIds = await getLocalImageIds(images); + const news = await postToBackend('/news', createNews()) + const newsIds = news.map(n=>n.id) + + const newsData = Promise.all([ + postToBackend('/image_for_news', generateImagesForNews(newsIds, newsImageIds)) + ]) + + return newsIds +} + +function createNews() { + const entries = [ + { + title: 'RSD released', + slug: 'rsd-released', + }, + { + title: 'Some Big News', + slug: 'some-big-news', + }, + { + title: 'You wont believe this!', + slug: 'you-wont-believe-this', + }, + { + title: "The perfect software doesn't exi-", + slug: 'the-prefect-software-doesnt-exi', + }, + { + title: "10 clickbait headlines you didn't know about!", + slug: '10-clickbait-headlines', + }, + { + title: 'You will never use a dependency anymore after you know this...', + slug: 'never-dependency', + }, + { + title: 'Sunsetting the RSD', + slug: 'sunsetting-the-rsd', + }, + { + title: 'The last package you will ever need', + slug: 'last-package', + }, + { + title: 'How to make your project a big success', + slug: 'project-success', + }, + { + title: 'The 5 best dependencies you never heard about', + slug: '5-best-dependencies', + }, + { + title: 'Rewriting the RSD in CrabLang', + slug: 'rewrite-rsd-crablang', + }, + { + title: 'The RSD joins forces with Big Company (tm)', + slug: 'rsd-joins-big-company', + }, + { + title: "3 features you didn't know about", + slug: '3-features', + }, + { + title: 'Interview with RSD founders', + slug: 'interview-rsd-founders', + }, + ]; + + const result = []; + for (const newsItem of entries) { + result.push({ + slug: newsItem.slug, + is_published: !!faker.helpers.maybe(() => true, {probability: 0.8}), + publication_date: faker.date.anytime(), + title: newsItem.title, + author: faker.person.fullName(), + summary: faker.lorem.paragraph(), + description: faker.lorem.paragraphs(faker.number.int({max: 20, min: 3}), '\n\n'), + }); + } + + return result; +} + +function generateImagesForNews(newsIds, imageIds) { + const result = []; + + for (const id of newsIds) { + if (faker.datatype.boolean(0.2)) { + continue; + } + + result.push({ + news: id, + image_id: faker.helpers.arrayElement(imageIds), + }); + } + + return result; +} + + +export async function generateMetaPages(){ + const meta = await postToBackend('/meta_pages', createMetaPages()) + const idsMeta = meta.map(m=>m.id) + return idsMeta +} + +function createMetaPages() { + const result = []; + + const titles = ['About', 'Terms of Service', 'Privacy Statement']; + const slugs = ['about', 'terms-of-service', 'privacy-statement']; + for (let index = 0; index < titles.length; index++) { + result.push({ + title: titles[index], + slug: slugs[index], + description: faker.lorem.paragraphs(10, '\n\n'), + is_published: true, + position: index + 1, + }); + } + + return result; +} \ No newline at end of file diff --git a/data-generation/organisations.js b/data-generation/organisations.js new file mode 100644 index 0000000..ca2f2cb --- /dev/null +++ b/data-generation/organisations.js @@ -0,0 +1,124 @@ +import {faker} from '@faker-js/faker'; +import { + postToBackend, + generateUniqueCaseInsensitiveString, + generateRelationsForDifferingEntities +} from "./utils.js"; +import {organisationLogos,getLocalImageIds} from './images.js'; + +export async function generateOrganisation({idsSoftware,idsProjects,idsMentions,amount=500}){ + const localOrganisationLogoIds = await getLocalImageIds(organisationLogos); + + const data = createOrganisations(localOrganisationLogoIds) + const organisations = await postToBackend('/organisation',data) + const idsOrganisations = organisations.map(o=>o.id) + + const orgData = await Promise.all([ + postToBackend('/software_for_organisation', + generateRelationsForDifferingEntities(idsSoftware, idsOrganisations, 'software', 'organisation'), + ), + postToBackend('/project_for_organisation', + generateProjectForOrganisation(idsProjects, idsOrganisations) + ), + postToBackend('/release', idsSoftware.map(id => ({software: id}))), + postToBackend('/release_version', + generateRelationsForDifferingEntities(idsSoftware, idsMentions, 'release_id', 'mention_id', 100) + ) + ]) + + return idsOrganisations +} + +export function createOrganisations(localOrganisationLogoIds,amount = 500) { + const rorIds = [ + 'https://ror.org/000k1q888', + 'https://ror.org/006hf6230', + 'https://ror.org/008pnp284', + 'https://ror.org/00f9tz983', + 'https://ror.org/00x7ekv49', + 'https://ror.org/00za53h95', + 'https://ror.org/012p63287', + 'https://ror.org/01460j859', + 'https://ror.org/014w0fd65', + 'https://ror.org/016xsfp80', + 'https://ror.org/018dfmf50', + 'https://ror.org/01bnjb948', + 'https://ror.org/01deh9c76', + 'https://ror.org/01hcx6992', + 'https://ror.org/01k0v6g02', + 'https://ror.org/01ryk1543', + 'https://ror.org/027bh9e22', + 'https://ror.org/02e2c7k09', + 'https://ror.org/02e7b5302', + 'https://ror.org/02en5vm52', + 'https://ror.org/02jx3x895', + 'https://ror.org/02jz4aj89', + 'https://ror.org/02w4jbg70', + 'https://ror.org/030a5r161', + 'https://ror.org/031m0hs53', + 'https://ror.org/041kmwe10', + 'https://ror.org/04bdffz58', + 'https://ror.org/04dkp9463', + 'https://ror.org/04njjy449', + 'https://ror.org/04qw24q55', + 'https://ror.org/04s2z4291', + 'https://ror.org/04x6kq749', + 'https://ror.org/052578691', + 'https://ror.org/054hq4w78', + 'https://ror.org/055d8gs64', + 'https://ror.org/05dfgh554', + 'https://ror.org/05jxfge78', + 'https://ror.org/05kaxyq51', + 'https://ror.org/05v6zeb66', + 'https://ror.org/05xvt9f17', + ]; + + const names = []; + for (let index = 0; index < amount; index++) { + const maxWords = faker.helpers.maybe(() => 5, {probability: 0.8}) ?? 31; + const name = generateUniqueCaseInsensitiveString(() => + ('Organisation ' + faker.word.words(faker.number.int({max: maxWords, min: 1}))).substring(0, 200), + ); + names.push(name); + } + + const result = []; + + for (let index = 0; index < amount; index++) { + result.push({ + parent: null, + primary_maintainer: null, + slug: faker.helpers.slugify(names[index]).toLowerCase().replaceAll(/-{2,}/g, '-').replaceAll(/-+$/g, ''), // removes double dashes and trailing dashes + name: names[index], + short_description: + faker.helpers.maybe(() => faker.commerce.productDescription(), { + probability: 0.8, + }) ?? null, + ror_id: index < rorIds.length ? rorIds[index] : null, + website: faker.internet.url(), + is_tenant: !!faker.helpers.maybe(() => true, {probability: 0.05}), + country: + faker.helpers.maybe(() => faker.location.country(), { + probability: 0.8, + }) ?? null, + logo_id: + faker.helpers.maybe(() => localOrganisationLogoIds[index % localOrganisationLogoIds.length], { + probability: 0.8, + }) ?? null, + }); + } + + return result; +} + +export function generateProjectForOrganisation(idsProjects, idsOrganisations) { + const result = generateRelationsForDifferingEntities(idsProjects, idsOrganisations, 'project', 'organisation'); + + const roles = ['funding', 'hosting', 'participating']; + result.forEach(entry => { + entry['role'] = faker.helpers.arrayElement(roles); + }); + + return result; +} + diff --git a/data-generation/project.js b/data-generation/project.js new file mode 100644 index 0000000..670b2d0 --- /dev/null +++ b/data-generation/project.js @@ -0,0 +1,172 @@ +import {faker} from '@faker-js/faker'; + +import { + generateUniqueCaseInsensitiveString,getKeywordIds, + postToBackend,generatePeopleWithOrcids,generateKeywordsForEntity, + getResearchDomainIds +} from './utils.js'; +import {images,getLocalImageIds} from './images.js'; +import {generateMentionsForEntity} from './mentions.js' +import {generateSoftwareForSoftware} from './software.js' + +export async function generateProject({orcids,idsMentions,amount = 500}){ + const projectImageIds = await getLocalImageIds(images); + const peopleWithOrcid = generatePeopleWithOrcids(orcids, projectImageIds); + const idsKeywords = await getKeywordIds() + const idsResearchDomains = await getResearchDomainIds() + + const projects = await postToBackend('/project', createProjects(projectImageIds,amount)) + const idsProjects = projects.map(p=>p.id) + + const projectData = await Promise.all([ + postToBackend('/team_member', await generateTeamMembers(idsProjects,peopleWithOrcid,projectImageIds)), + postToBackend('/url_for_project', generateUrlsForProjects(idsProjects)), + postToBackend('/keyword_for_project', generateKeywordsForEntity(idsProjects, idsKeywords, 'project')), + postToBackend('/output_for_project', generateMentionsForEntity(idsProjects, idsMentions, 'project')), + postToBackend('/impact_for_project', generateMentionsForEntity(idsProjects, idsMentions, 'project')), + postToBackend( + '/research_domain_for_project', + generateResearchDomainsForProjects(idsProjects, idsResearchDomains) + ), + postToBackend('/project_for_project', generateSoftwareForSoftware(idsProjects)) + ]) + + return idsProjects +} + +export function createProjects(projectImageIds,amount = 500) { + const result = []; + + const projectStatuses = ['finished', 'running', 'starting']; + + for (let index = 0; index < amount; index++) { + const maxWords = faker.helpers.maybe(() => 5, {probability: 0.8}) ?? 31; + const title = generateUniqueCaseInsensitiveString(() => + ('Project: ' + faker.word.words(faker.number.int({max: maxWords, min: 1}))).substring(0, 200), + ); + + const status = faker.helpers.arrayElement(projectStatuses); + let dateEnd, dateStart; + switch (status) { + case 'finished': + dateEnd = faker.date.past({years: 2}); + dateStart = faker.date.past({years: 2, refDate: dateEnd}); + break; + case 'running': + dateEnd = faker.date.future({years: 2}); + dateStart = faker.date.past({years: 2}); + break; + case 'starting': + dateStart = faker.date.future({years: 2}); + dateEnd = faker.date.future({years: 2, refDate: dateStart}); + break; + } + + result.push({ + slug: faker.helpers.slugify(title).toLowerCase().replaceAll(/-{2,}/g, '-').replaceAll(/-+$/g, ''), // removes double dashes and trailing dashes + title: title, + subtitle: + faker.helpers.maybe(() => faker.commerce.productDescription(), { + probability: 0.9, + }) ?? null, + date_end: faker.helpers.maybe(() => dateEnd, {probability: 0.9}) ?? null, + date_start: faker.helpers.maybe(() => dateStart, {probability: 0.9}) ?? null, + description: faker.lorem.paragraphs(5, '\n\n'), + grant_id: faker.helpers.maybe(() => faker.helpers.replaceSymbols('******'), {probability: 0.8}) ?? null, + image_caption: faker.animal.cat(), + image_contain: !!faker.helpers.maybe(() => true, { + probability: 0.5, + }), + image_id: + faker.helpers.maybe(() => projectImageIds[index % projectImageIds.length], {probability: 0.8}) ?? null, + is_published: !!faker.helpers.maybe(() => true, {probability: 0.8}), + }); + } + + return result; +} + +export async function generateTeamMembers(projectIds, peopleWithOrcids, contributorImageIds=[],minPerProject = 0, maxPerProject = 15) { + const result = []; + + for (const projectId of projectIds) { + const amount = faker.number.int({ + max: maxPerProject, + min: minPerProject, + }); + const amountWithOrcid = faker.number.int({max: amount, min: 0}); + const amountWithoutOrcid = amount - amountWithOrcid; + + for (let i = 0; i < amountWithoutOrcid; i++) { + result.push({ + project: projectId, + is_contact_person: !!faker.helpers.maybe(() => true, { + probability: 0.2, + }), + email_address: faker.internet.email(), + family_names: faker.person.lastName(), + given_names: faker.person.firstName(), + affiliation: faker.company.name(), + role: faker.person.jobTitle(), + orcid: null, + avatar_id: + faker.helpers.maybe(() => faker.helpers.arrayElement(contributorImageIds), {probability: 0.8}) ?? null, + }); + } + + const randomPeopleWithOrcdid = faker.helpers.arrayElements(peopleWithOrcids, amountWithOrcid); + + for (const personWithOrcid of randomPeopleWithOrcdid) { + result.push({ + ...personWithOrcid, + project: projectId, + is_contact_person: !!faker.helpers.maybe(() => true, { + probability: 0.2, + }), + affiliation: faker.company.name(), + role: faker.person.jobTitle(), + }); + } + } + + return result; +} + + +export function generateUrlsForProjects(ids) { + const result = []; + + for (const id of ids) { + // each project will get 0, 1 or 2 URLs + const numberOfUrls = faker.number.int({max: 3, min: 0}); + for (let index = 0; index < numberOfUrls; index++) { + result.push({ + project: id, + title: faker.commerce.product(), + url: faker.internet.url(), + }); + } + } + + return result; +} + + +export function generateResearchDomainsForProjects(idsProject, idsResearchDomain) { + const result = []; + + for (const idProject of idsProject) { + const nummerOfKeywords = faker.number.int({max: 3, min: 0}); + if (nummerOfKeywords === 0) continue; + + const researchDomainIdsToAdd = faker.helpers.arrayElements(idsResearchDomain, nummerOfKeywords); + for (const researchDomainId of researchDomainIdsToAdd) { + result.push({ + project: idProject, + research_domain: researchDomainId, + }); + } + } + + return result; +} \ No newline at end of file diff --git a/data-generation/software.js b/data-generation/software.js new file mode 100644 index 0000000..2612d5b --- /dev/null +++ b/data-generation/software.js @@ -0,0 +1,314 @@ +import {faker} from '@faker-js/faker'; + +import { + generateUniqueCaseInsensitiveString,generatePeopleWithOrcids, + getKeywordIds,generateKeywordsForEntity,postToBackend +} from './utils.js'; +import {conceptDois, packageManagerLinks} from './real-data.js'; +import {images,softwareLogos,getLocalImageIds} from './images.js'; +import {generateMentionsForEntity} from './mentions.js' + +export async function generateSoftware({orcids,idsMentions,amount = 500}){ + const contributorImageIds = await getLocalImageIds(images); + const softwareLogoIds = await getLocalImageIds(softwareLogos); + const peopleWithOrcid = generatePeopleWithOrcids(orcids, contributorImageIds); + const idsKeywords = await getKeywordIds() + + // log software as group + // console.group("software") + + // post to software + const software = await postToBackend('/software', createSoftware(softwareLogoIds,amount)) + + // extract ids + let idsSoftware=[], idsFakeSoftware=[], idsRealSoftware=[]; + software.forEach(sw=>{ + // all software ids + idsSoftware.push(sw.id) + // real/fake software ids + if (sw['brand_name'].startsWith('Real,')){ + idsRealSoftware.push(sw.id) + }else{ + idsFakeSoftware.push(sw.id) + } + }) + + // add all related software data + const softwareData = await Promise.all([ + postToBackend('/contributor', + generateContributors(idsSoftware, peopleWithOrcid, contributorImageIds) + ), + postToBackend('/testimonial', generateTestimonials(idsSoftware)), + postToBackend('/repository_url', generateRepositoryUrls(idsSoftware)), + postToBackend('/package_manager', generatePackageManagers(idsRealSoftware)), + postToBackend('/license_for_software', generateLicensesForSoftware(idsSoftware)), + postToBackend('/keyword_for_software', generateKeywordsForEntity(idsSoftware, idsKeywords, 'software')), + postToBackend('/mention_for_software', generateMentionsForEntity(idsSoftware, idsMentions, 'software')), + postToBackend('/software_for_software', generateSoftwareForSoftware(idsSoftware)), + postToBackend('/software_highlight', generateSoftwareHighlights(idsSoftware.slice(0, 10))) + ]) + + // console.groupEnd() + return idsSoftware +} + + + +export function createSoftware(softwareLogoIds,amount = 500) { + + // real software has a real concept DOI + const amountRealSoftware = Math.min(conceptDois.length, amount); + const brandNames = []; + for (let index = 0; index < amountRealSoftware; index++) { + const maxWords = faker.helpers.maybe(() => 5, {probability: 0.8}) ?? 31; + const brandName = generateUniqueCaseInsensitiveString(() => + ('Real, ' + faker.word.words(faker.number.int({max: maxWords, min: 1}))).substring(0, 200) + ); + brandNames.push(brandName); + } + + const amountFakeSoftware = amount - amountRealSoftware; + for (let index = 0; index < amountFakeSoftware; index++) { + const maxWords = faker.helpers.maybe(() => 5, {probability: 0.8}) ?? 31; + const brandName = generateUniqueCaseInsensitiveString(() => + ('Software ' + faker.word.words(faker.number.int({max: maxWords, min: 1}))).substring(0, 200) + ); + brandNames.push(brandName); + } + + const result = []; + + for (let index = 0; index < amount; index++) { + result.push({ + slug: faker.helpers + .slugify(brandNames[index]) + .toLowerCase() + .replaceAll(/-{2,}/g, '-') + .replaceAll(/-+$/g, ''), // removes double dashes and trailing dashes + brand_name: brandNames[index], + concept_doi: index < conceptDois.length ? conceptDois[index] : null, + description: faker.lorem.paragraphs(4, '\n\n'), + get_started_url: faker.internet.url(), + image_id: + faker.helpers.maybe(() => softwareLogoIds[index % softwareLogoIds.length], { + probability: 0.8, + }) ?? null, + is_published: !!faker.helpers.maybe(() => true, {probability: 0.8}), + short_statement: faker.commerce.productDescription(), + closed_source: !!faker.helpers.maybe(() => true, { + probability: 0.8, + }), + }); + } + + return result; +} + +export function generateTestimonials(ids) { + const result = []; + + for (const id of ids) { + // each software will get 0, 1 or 2 testimonials + const numberOfTestimonials = faker.number.int({max: 3, min: 0}); + for (let index = 0; index < numberOfTestimonials; index++) { + result.push({ + software: id, + message: faker.hacker.phrase(), + source: faker.person.fullName(), + }); + } + } + + return result; +} + + +export function generateRepositoryUrls(ids) { + const githubUrls = [ + 'https://github.com/research-software-directory/RSD-as-a-service', + 'https://github.com/wadpac/GGIR', + 'https://github.com/ESMValGroup/ESMValTool', + 'https://github.com/ESMValGroup/ESMValCore', + 'https://github.com/benvanwerkhoven/kernel_tuner', + 'https://github.com/NLeSC/pattyvis', + ]; + + const gitlabUrls = [ + 'https://gitlab.com/dwt1/dotfiles', + 'https://gitlab.com/famedly/fluffychat', + 'https://gitlab.com/gitlab-org/gitlab-shell', + 'https://gitlab.com/cerfacs/batman', + 'https://gitlab.com/cyber5k/mistborn', + ]; + + const repoUrls = githubUrls.concat(gitlabUrls); + + const result = []; + + for (let index = 0; index < ids.length; index++) { + if (!!faker.helpers.maybe(() => true, {probability: 0.25})) continue; + + const repoUrl = faker.helpers.arrayElement(repoUrls); + const codePlatform = repoUrl.startsWith('https://github.com') ? 'github' : 'gitlab'; + result.push({ + software: ids[index], + url: repoUrl, + code_platform: codePlatform, + }); + } + + return result; +} + +export function generatePackageManagers(softwareIds) { + const result = []; + + for (let index = 0; index < softwareIds.length; index++) { + // first assign each package manager entry to one software, then randomly assing package manager entries to the remaining ids + const packageManagerLink = + index < packageManagerLinks.length + ? packageManagerLinks[index] + : faker.helpers.arrayElement(packageManagerLinks); + + result.push({ + software: softwareIds[index], + url: packageManagerLink.url, + package_manager: packageManagerLink.type, + }); + } + + return result; +} + +export function generateLicensesForSoftware(ids) { + const licenses = [ + { + license: 'Apache-2.0', + name: 'Apache License 2.0', + reference: 'https://spdx.org/licenses/Apache-2.0.html', + }, + { + license: 'MIT', + name: 'MIT License', + reference: 'https://spdx.org/licenses/MIT.html', + }, + { + license: 'GPL-2.0-or-later', + name: 'GNU General Public License v2.0 or later', + reference: 'https://spdx.org/licenses/GPL-2.0-or-later.html', + }, + { + license: 'LGPL-2.0-or-later', + name: 'GNU Library General Public License v2 or later', + reference: 'https://spdx.org/licenses/LGPL-2.0-or-later.html', + }, + { + license: 'CC-BY-4.0', + name: 'Creative Commons Attribution 4.0 International', + reference: 'https://spdx.org/licenses/CC-BY-4.0.html', + }, + { + license: 'CC-BY-NC-ND-3.0', + name: 'Creative Commons Attribution Non Commercial No Derivatives 3.0 Unported', + reference: 'https://spdx.org/licenses/CC-BY-NC-ND-3.0.html', + }, + ]; + + const result = []; + + for (const id of ids) { + const nummerOfLicenses = faker.number.int({max: 3, min: 0}); + if (nummerOfLicenses === 0) continue; + + const licensesToAdd = faker.helpers.arrayElements(licenses, nummerOfLicenses); + for (const item of licensesToAdd) { + result.push({ + software: id, + license: item.license, + name: item.name, + reference: item.reference, + }); + } + } + + return result; +} + +export function generateSoftwareForSoftware(ids) { + const result = []; + + for (let index = 0; index < ids.length; index++) { + const numberOfRelatedSoftware = faker.number.int({max: 5, min: 0}); + if (numberOfRelatedSoftware === 0) continue; + + const origin = ids[index]; + const idsWithoutOrigin = ids.filter(id => id !== origin); + const idsRelation = faker.helpers.arrayElements(idsWithoutOrigin, numberOfRelatedSoftware); + for (const relation of idsRelation) { + result.push({ + origin: origin, + relation: relation, + }); + } + } + + return result; +} + +export function generateSoftwareHighlights(ids) { + const result = []; + for (let index = 0; index < ids.length; index++) { + const isHighlight = !!faker.helpers.maybe(() => true, { + probability: 0.3, + }); + if (isHighlight === true) result.push({software: ids[index]}); + } + return result; +} + + +export function generateContributors(softwareIds, peopleWithOrcids, contributorImageIds=[], minPerSoftware = 0, maxPerSoftware = 15) { + const result = []; + + for (const softwareId of softwareIds) { + const amount = faker.number.int({ + max: maxPerSoftware, + min: minPerSoftware, + }); + const amountWithOrcid = faker.number.int({max: amount, min: 0}); + const amountWithoutOrcid = amount - amountWithOrcid; + + for (let i = 0; i < amountWithoutOrcid; i++) { + result.push({ + software: softwareId, + is_contact_person: !!faker.helpers.maybe(() => true, { + probability: 0.2, + }), + email_address: faker.internet.email(), + family_names: faker.person.lastName(), + given_names: faker.person.firstName(), + affiliation: faker.company.name(), + role: faker.person.jobTitle(), + orcid: null, + avatar_id: + faker.helpers.maybe(() => faker.helpers.arrayElement(contributorImageIds), {probability: 0.8}) ?? null, + }); + } + + const randomPeopleWithOrcdid = faker.helpers.arrayElements(peopleWithOrcids, amountWithOrcid); + + for (const personWithOrcid of randomPeopleWithOrcdid) { + result.push({ + ...personWithOrcid, + software: softwareId, + is_contact_person: !!faker.helpers.maybe(() => true, { + probability: 0.2, + }), + affiliation: faker.company.name(), + role: faker.person.jobTitle(), + }); + } + } + + return result; +} diff --git a/data-generation/utils.js b/data-generation/utils.js new file mode 100644 index 0000000..656a97c --- /dev/null +++ b/data-generation/utils.js @@ -0,0 +1,155 @@ +import {faker} from '@faker-js/faker'; + +import {headers} from './auth.js'; + +const usedLowerCaseStrings = new Set(); + +// demo url https://ubuntu2204sudo.demo-nlesc.src.surf-hosted.nl/ +const backendUrl = process.env.POSTGREST_URL || 'http://localhost/api/v1'; + +export async function postToBackend(endpoint, body) { + const response = await fetch(backendUrl + endpoint, { + method: 'POST', + body: JSON.stringify(body), + headers: headers, + }); + + if (!response.ok) { + console.warn( + 'Warning: post request to ' + + endpoint + + ' had status code ' + + response.status + + ' and body ' + + (await response.text()), + ); + } + const json = await response.json(); + + // log posts + // console.log(`${endpoint}...${json.length}`) + return json +} + +export async function getFromBackend(endpoint) { + const response = await fetch(backendUrl + endpoint, {headers: headers}); + if (!response.ok) { + console.warn( + 'Warning: post request to ' + + endpoint + + ' had status code ' + + response.status + + ' and body ' + + (await response.text()), + ); + } + const json = await response.json(); + return json +} + + +export function generateUniqueCaseInsensitiveString(randomStringGenerator) { + for (let attempt = 0; attempt < 10000; attempt++) { + const nextString = randomStringGenerator(); + if (usedLowerCaseStrings.has(nextString.toLowerCase())) continue; + + usedLowerCaseStrings.add(nextString.toLowerCase()); + return nextString; + } + throw 'Tried to generate a unique (ignoring case) string for 10000 times but failed to do so'; +} + + +export function generateKeywordsForEntity(idsEntity, idsKeyword, nameEntity) { + const result = []; + + for (const idEntity of idsEntity) { + const nummerOfKeywords = faker.number.int({max: 3, min: 0}); + if (nummerOfKeywords === 0) continue; + + const keywordIdsToAdd = faker.helpers.arrayElements(idsKeyword, nummerOfKeywords); + for (const keywordId of keywordIdsToAdd) { + result.push({ + [nameEntity]: idEntity, + keyword: keywordId, + }); + } + } + + return result; +} + +export function generateOrcids(amount = 50) { + const orcids = new Set(); + + while (orcids.size < amount) { + orcids.add(faker.helpers.replaceSymbolWithNumber('0000-000#-####-####')); + } + + return [...orcids]; +} + + +export function generatePeopleWithOrcids(orcids, imageIds) { + const result = []; + + for (const orcid of orcids) { + result.push({ + email_address: faker.internet.email(), + family_names: faker.person.lastName(), + given_names: faker.person.firstName(), + orcid: orcid, + avatar_id: faker.helpers.arrayElement(imageIds), + }); + } + + return result; +} + +export function generateRelationsForDifferingEntities( + idsOrigin, + idsRelation, + nameOrigin, + nameRelation, + maxRelationsPerOrigin = 11, +) { + const result = []; + + for (const idOrigin of idsOrigin) { + const numberOfIdsRelation = faker.number.int({ + max: maxRelationsPerOrigin, + min: 0, + }); + const relationsToAdd = faker.helpers.arrayElements(idsRelation, numberOfIdsRelation); + for (const idRelation of relationsToAdd) { + result.push({ + [nameOrigin]: idOrigin, + [nameRelation]: idRelation, + }); + } + } + + return result; +} + +export function mimeTypeFromFileName(fileName) { + if (fileName.endsWith('.png')) { + return 'image/png'; + } else if (fileName.endsWith('.jpg') || fileName.endsWith('.jpeg')) { + return 'image/jpg'; + } else if (fileName.endsWith('.svg')) { + return 'image/svg+xml'; + } else return null; +} + +export async function getKeywordIds(){ + const keywords = await getFromBackend('/keyword?select=id') + const ids = keywords.map(k=>k.id) + return ids +} + +export async function getResearchDomainIds(){ + const domains = await getFromBackend('/research_domain?select=id') + const ids = domains.map(k=>k.id) + return ids +} \ No newline at end of file diff --git a/deployment/.env.example b/deployment/.env.example new file mode 100644 index 0000000..18d5f22 --- /dev/null +++ b/deployment/.env.example @@ -0,0 +1,193 @@ +# example env file +# copy to .env + +################ WARNING ################ +# Using special characters in the values (e.g. in passwords or secrets) might corrupt some processes. +# If you experience any problems, remove the special characters from the values or place them in quotes (' or "). +################ WARNING ################ + +# .env is consumed by docker-compose.yml +# currently assigned values are for .env +# You also need to obtain/generate missing secrets + +# ---- DOCKER PROJECT SETTINGS ---------- + +# Define this variable, if you are running different versions of the RSD, in +# order to define the docker project name. If you leave this empty, docker will +# automatically name the containers. +COMPOSE_PROJECT_NAME="kin" + +# ---- PUBLIC ENV VARIABLES ------------- + +# postgresql +# consumed by services: backend +POSTGRES_DB_HOST=database +# consumed by services: backend +POSTGRES_DB_HOST_PORT=5432 +# consumed by services: database, backend +POSTGRES_DB=kin-rpd-db +# consumed by services: database +POSTGRES_USER=rsd + +# backend (postgREST) +# consumed by services: backend +PGRST_DB_ANON_ROLE=rsd_web_anon +PGRST_DB_SCHEMA=public +PGRST_SERVER_PORT=3500 + +# postgREST API +# consumed by services: authentication,frontend,auth-tests, scrapers +# .env.local: http://localhost/api/v1, .env: http://backend:3500 +POSTGREST_URL=http://backend:3500 + +# postgREST API reachable outside of Docker +# consumed by services: swagger +POSTGREST_URL_EXTERNAL=http://localhost/api/v1 + +# RSD Auth module +# consumed by services: frontend (api/fe) +# .env.local: http://localhost/auth, .env: http://auth:7000 +RSD_AUTH_URL=http://auth:7000 + +# consumed by services: authentication +# If set to "dev", the first user to log in will become admin. +# Any other value doesn't activate this feature (and doesn't do anything). +RSD_ENVIRONMENT=prod + +# consumed by services: authentication, frontend (api/fe) +# provide a list of supported OpenID auth providers +# the values should be separated by semicolon (;) +# Allowed values are: SURFCONEXT, HELMHOLTZID, ORCID or LOCAL +# if env value is not provided default provider is set to be SURFCONEXT +# if you add the value "LOCAL", then local accounts are enabled, USE THIS FOR TESTING PURPOSES ONLY +RSD_AUTH_PROVIDERS=SURFCONEXT;LOCAL + +# consumed by services: authentication, frontend (api/fe) +# provide a list of supported OpenID auth providers for coupling with the user's RSD account +# the values should be separated by semicolon (;) +# Allowed values are: ORCID +# RSD_AUTH_COUPLE_PROVIDERS=ORCID + +# Define a semicolon-separated list of user email addresses which are allowed to +# login to the RSD. If the variable is left empty, or is not defined, all users +# will be allowed to login. +# consumed by: authentication +#RSD_AUTH_USER_MAIL_WHITELIST=user@example.com;test@example.com + +# SURFCONEXT - TEST ENVIRONMENT +# consumed by: authentication, frontend/utils/loginHelpers +SURFCONEXT_CLIENT_ID=kin-rpd-demo.com +# consumed by: authentication, frontend/utils/loginHelpers +SURFCONEXT_REDIRECT=https://ubuntu2204sudo.demo-nlesc.src.surf-hosted.nl/auth/login/surfconext +# consumed by: authentication, frontend/utils/loginHelpers +SURFCONEXT_WELL_KNOWN_URL=https://connect.test.surfconext.nl/.well-known/openid-configuration +# consumed by: authentication, frontend/utils/loginHelpers +SURFCONEXT_SCOPES=openid +# consumed by: frontend/utils/loginHelpers +SURFCONEXT_RESPONSE_MODE=form_post + +# ORCID +# consumed by: authentication, frontend/utils/loginHelpers +ORCID_CLIENT_ID= +# consumed by: authentication, frontend/utils/loginHelpers +ORCID_REDIRECT= +# consumed by: authentication, frontend/utils/loginHelpers +ORCID_REDIRECT_COUPLE= +# consumed by: authentication, frontend/utils/loginHelpers +ORCID_WELL_KNOWN_URL=https://orcid.org/.well-known/openid-configuration +# consumed by: authentication, frontend/utils/loginHelpers +ORCID_SCOPES=openid +# consumed by: frontend/utils/loginHelpers +ORCID_RESPONSE_MODE=query + +# AZURE ACTIVE DIRECTORY +# consumed by: authentication, frontend/utils/loginHelpers +AZURE_CLIENT_ID= +# consumed by: authentication, frontend/utils/loginHelpers +AZURE_REDIRECT= +# consumed by: authentication, frontend/utils/loginHelpers +AZURE_WELL_KNOWN_URL= +# consumed by: authentication, frontend/utils/loginHelpers +AZURE_SCOPES=openid+email+profile +# consumed by: authentication, frontend/utils/loginHelpers +AZURE_LOGIN_PROMPT=select_account +# consumed by: frontend +# the name displayed to users when multiple providers are configured +AZURE_DISPLAY_NAME="Microsoft Azure AD" +# consumed by: frontend +# the description text displayed to users when multiple providers are configured +AZURE_DESCRIPTION_HTML="Sign in with your institutional credentials" +# consumed by: authentication +# the organisation recorded for users logged in via this provider +AZURE_ORGANISATION= + +# max requests to the GitHub API per run, runs 10 times per hour +# optional, comment out if not available, a default of 6 will be used +# consumed by: scrapers +MAX_REQUESTS_GITHUB=6 + +# max request to GitLab API per run, runs 10 times per hour +# optional, comment out if not available, a default of 6 will be used +# consumed by: scrapers +MAX_REQUESTS_GITLAB=6 + +# max mentions to scrape per run, runs 10 times per hour +# optional, comment out if not available, a default of 6 will be used +# consumed by: scrapers +MAX_REQUESTS_DOI=6 + +# max organisations to scrape per run, runs 10 times per hour +# optional, comment out if not available, a default of 6 will be used +# consumed by: scrapers +MAX_REQUESTS_ROR=6 + +# ---- SECRETS ------ SECRETS ----------- + +# consumed by services: database +# generate random/strong password +POSTGRES_PASSWORD= + +# consumed by services: database, backend +# generate random/strong password +POSTGRES_AUTHENTICATOR_PASSWORD= + +# POSTGREST JWT SECRET +# consumed by services: authentication, frontend (auth-node), auth-tests, scrapers +# generate random/strong password with at least 32 characters +PGRST_JWT_SECRET= + +# SURFCONEXT +# consumed by services: authentication +# obtain the secret from SURFCONEXT dashboard +AUTH_SURFCONEXT_CLIENT_SECRET= + +# ORCID +# consumed by services: authentication +# obtain the secret from the project team +AUTH_ORCID_CLIENT_SECRET= + +# Azure Active Directory +# consumed by services: authentication +AUTH_AZURE_CLIENT_SECRET= + +# consumed by: scrapers +# optional, comment out if not available, should be of the form username:token +# obtain the secret from GITHUB dashboard +API_CREDENTIALS_GITHUB= + +# consumed by: scrapers +# obtain the secret from ZENODO dashboard +ZENODO_ACCESS_TOKEN= + +# consumed by: scrapers, frontend api (node) +# email address that Crossref can contact you with to comply with their "polite" policy +# leave blank or use a real email address that you will respond to +CROSSREF_CONTACT_EMAIL= + +# consumed by: frontend +# URL (should end with a trailing slash) and ID for Matomo Tracking Code +# MATOMO_URL= +# MATOMO_ID= + +# consumed by: scrapers +# LIBRARIES_IO_ACCESS_TOKEN= diff --git a/deployment/nginx.conf b/deployment/nginx.conf new file mode 100644 index 0000000..9635546 --- /dev/null +++ b/deployment/nginx.conf @@ -0,0 +1,96 @@ +# SPDX-FileCopyrightText: 2021 - 2022 Netherlands eScience Center +# SPDX-FileCopyrightText: 2021 - 2022 dv4all +# +# SPDX-License-Identifier: CC0-1.0 +# +# +# upstream configuration +upstream backend { + server backend:3500; +} +upstream authentication { + server auth:7000; +} + +server { + listen 80; + listen 443 ssl; + server_name 0.0.0.0; + + # managed by Certbot + # this certificate expires on 2024-10-02. + ssl_certificate /etc/letsencrypt/live/ubuntu2204sudo.demo-nlesc.src.surf-hosted.nl/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/ubuntu2204sudo.demo-nlesc.src.surf-hosted.nl/privkey.pem; + include /etc/letsencrypt/options-ssl-nginx.conf; + ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; + + set $frontend http://frontend:3000; + + charset utf-8; + + # enable gzip file compression + gzip on; + gzip_proxied any; + gzip_comp_level 4; + gzip_types text/css application/javascript image/svg+xml; + + + # auth + location /auth/ { + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_pass http://authentication/; + } + + # PostgREST backend API + # Note! NextJS has api/fe for citation files and images + location /api/v1/ { + # needed to increase size for the migration script + client_max_body_size 40M; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + default_type application/json; + proxy_hide_header Content-Location; + add_header Content-Location /api/$upstream_http_content_location; + proxy_set_header Connection ""; + proxy_http_version 1.1; + proxy_pass http://backend/; + } + + # reverse proxy for next fontend server + location / { + # Use Docker resolver + resolver 127.0.0.11 valid=30s; + # Use variable for proxy_pass so nginx doesn't check existence on startup + proxy_pass $frontend; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_cache_bypass $http_upgrade; + # we need to remove this 404 handling + # because next's _next folder has own 404 handling + # try_files $uri $uri/ =404; + } + + # serve postgrest images with binary header + location /image/ { + proxy_set_header "Authorization" "Bearer $cookie_rsd_token"; + rewrite /image/(.+) /$1 break; + proxy_set_header Accept application/octet-stream; + proxy_pass http://backend/; + } + + # location /metadata/codemeta/ { + # proxy_pass http://codemeta:8000/; + # proxy_redirect ~(.*) /metadata/codemeta$1; + # } + + # location /swagger/ { + # proxy_pass http://swagger:8080/; + # } + + location /documentation/ { + proxy_pass http://documentation/; + } +} diff --git a/frontend/__tests__/Home.test.tsx b/frontend/__tests__/Home.test.tsx index 679e2e5..6a3bf9e 100644 --- a/frontend/__tests__/Home.test.tsx +++ b/frontend/__tests__/Home.test.tsx @@ -54,17 +54,10 @@ describe('pages/index.tsx', () => { expect(page).toBeInTheDocument() }) - it('renders counts on KIN Home page', () => { + it('renders Our Programs section on KIN Home page', () => { render(WrappedComponentWithProps(Home,{props})) - // software_cnt - // const software = screen.getByText(`${props.counts.software_cnt} Software`) - // expect(software).toBeInTheDocument() - // project_cnt - const project = screen.getByText(`${props.counts.project_cnt} Projects`) + const project = screen.getByText('Our Programs') expect(project).toBeInTheDocument() - // organisation_cnt - const organisation = screen.getByText(`${props.counts.organisation_cnt} Organisations`) - expect(organisation).toBeInTheDocument() }) // it('renders Helmholtz Home page when host=helmholtz', () => { diff --git a/frontend/components/AppFooter/OrganisationLogo.tsx b/frontend/components/AppFooter/OrganisationLogo.tsx index 6e86265..902029c 100644 --- a/frontend/components/AppFooter/OrganisationLogo.tsx +++ b/frontend/components/AppFooter/OrganisationLogo.tsx @@ -13,8 +13,7 @@ export default function OrganisationLogo({host}: { host: RsdHost }) { const {name,logo_url,website}=host return (
- + - + {/*
*/} -
+
diff --git a/frontend/components/home/kin/StatsSection.tsx b/frontend/components/home/kin/StatsSection.tsx deleted file mode 100644 index c9642df..0000000 --- a/frontend/components/home/kin/StatsSection.tsx +++ /dev/null @@ -1,42 +0,0 @@ -// SPDX-FileCopyrightText: 2024 Dusan Mijatovic (Netherlands eScience Center) -// SPDX-FileCopyrightText: 2024 Netherlands eScience Center -// -// SPDX-License-Identifier: Apache-2.0 - -type StarsSectionProps={ - project_cnt: number, - organisation_cnt: number, - contributor_cnt: number, - software_mention_cnt: number, -} - - -export default function StarsSection({ - project_cnt,organisation_cnt, - contributor_cnt,software_mention_cnt -}:StarsSectionProps) { - return ( -
- -
-
{project_cnt} Projects
-
registered
-
- -
-
{organisation_cnt} Organisations
-
contributed
-
- -
-
{contributor_cnt} Contributors
-
to research software
-
- -
-
{software_mention_cnt} Mentions
-
of research software
-
-
- ) -} diff --git a/frontend/components/home/kin/TopNewsSection.tsx b/frontend/components/home/kin/TopNewsSection.tsx deleted file mode 100644 index 72a33a3..0000000 --- a/frontend/components/home/kin/TopNewsSection.tsx +++ /dev/null @@ -1,72 +0,0 @@ -// SPDX-FileCopyrightText: 2024 Dusan Mijatovic (Netherlands eScience Center) -// SPDX-FileCopyrightText: 2024 Netherlands eScience Center -// -// SPDX-License-Identifier: Apache-2.0 - -import Link from 'next/link' -import {TopNewsProps} from '~/components/news/apiNews' -import GradientBorderButton from './GradientBorderButton' -import {config} from './config' - -const {button} = config - -function TopNewsItem({item}:{item:TopNewsProps}){ - return ( - -
- {/* */} -
-

{item.title}

-

{item.summary}

-
- -
- - ) -} -/** - * Top news items including the homepage divider. - * If there are no news items it returns null. - * */ -export default function TopNewsSection({news}:{news:TopNewsProps[]}) { - - // console.group('TopNewsSection') - // console.log('news...',news) - // console.groupEnd() - - if (news?.length > 0){ - return ( - <> -
- -

- Latest news -

- -
- {news.map(item=>{ - return - })} -
- -
-
-
- - ) - } - return null -} diff --git a/frontend/components/home/kin/index.tsx b/frontend/components/home/kin/index.tsx index c15bfbb..4e8d5cc 100644 --- a/frontend/components/home/kin/index.tsx +++ b/frontend/components/home/kin/index.tsx @@ -13,32 +13,14 @@ import 'aos/dist/aos.css' import AppHeader from '~/components/AppHeader' import AppFooter from '~/components/AppFooter' -import {TopNewsProps} from '~/components/news/apiNews' import Arc from '~/components/home/rsd/arc.svg' -import StatsSection from './StatsSection' import JumboBanner from './JumboBanner' -import TopNewsSection from './TopNewsSection' import HomepageDivider from './HomepageDivider' import AboutUsSection from './AboutUsSection' import OurProgramsSection from './OurProgramsSection' - - import ContributeSection from './ContributeSection' -export type RsdHomeProps = { - software_cnt: number, - open_software_cnt: number, - project_cnt: number, - organisation_cnt: number, - contributor_cnt: number, - software_mention_cnt: number, - news: TopNewsProps[] -} - -export default function RsdHome({ - project_cnt, organisation_cnt, - contributor_cnt, software_mention_cnt,news -}: RsdHomeProps) { +export default function RsdHome() { // Initialize AOS library useEffect(() => { AOS.init({offset: 16}) @@ -52,23 +34,9 @@ export default function RsdHome({ {/* Jumbo Banner */} - {/* KIN stats */} - -
{/* Arc separator */} - {/* Get started section */} - {/* */} - {/* Top news items, ONLY if there are some */} - - {/* Divider */} - {/* Our Programs Section */} {/* Divider */} @@ -79,11 +47,6 @@ export default function RsdHome({ {/* About us section */} - {/* Divider */} - {/* */} - {/* Logos */} - {/* */} - {/* Footer */}
diff --git a/frontend/pages/index.tsx b/frontend/pages/index.tsx index 3ff896e..e73e071 100644 --- a/frontend/pages/index.tsx +++ b/frontend/pages/index.tsx @@ -21,7 +21,7 @@ export type HomeProps = { } const pageTitle = `Home | ${app.title}` -const pageDesc = 'The Research Project Directory is designed to show the impact research software has on research and society. We stimulate the reuse of research software and encourage proper citation of research software to ensure researchers and RSEs get credit for their work.' +const pageDesc = 'KIN connects, broadens, deepens, and unlocks knowledge for transitions towards a climate-neutral and climate-resilient society.' export default function Home({counts,news}: HomeProps) { const {host} = useRsdSettings() @@ -42,7 +42,7 @@ export default function Home({counts,news}: HomeProps) { /> {/* canonical url meta tag */} - + ) } diff --git a/frontend/pages/organisations/index.tsx b/frontend/pages/organisations/index.tsx index 2c40f9e..dba1f45 100644 --- a/frontend/pages/organisations/index.tsx +++ b/frontend/pages/organisations/index.tsx @@ -39,7 +39,7 @@ type OrganisationsOverviewPageProps = { } const pageTitle = `Organisations | ${app.title}` -const pageDesc = 'List of organizations involved in the development of research software.' +const pageDesc = 'List of organizations involved in KIN projects.' export default function OrganisationsOverviewPage({ organisations = [], count, page, rows, search diff --git a/frontend/pages/projects/index.tsx b/frontend/pages/projects/index.tsx index 963b392..f5f10f3 100644 --- a/frontend/pages/projects/index.tsx +++ b/frontend/pages/projects/index.tsx @@ -67,7 +67,7 @@ export type ProjectOverviewPageProps = { } const pageTitle = `Projects | ${app.title}` -const pageDesc = 'The list of research projects in the Research Software Directory.' +const pageDesc = 'The list of research projects in the KIN RPD.' export default function ProjectsOverviewPage({ search, order, diff --git a/frontend/public/data/settings.json b/frontend/public/data/settings.json index 947446f..38adb34 100644 --- a/frontend/public/data/settings.json +++ b/frontend/public/data/settings.json @@ -3,7 +3,7 @@ "name": "kin-rpd", "emailHeaders": [], "logo_url": "/images/logo-KIN.svg", - "website": "https://hetkin.nl", + "website": "https://hetkin.nl/en/home-en/", "feedback": { "enabled": true, "url": "rsd@esciencecenter.nl", @@ -31,14 +31,9 @@ "target": "_blank" }, { - "label": "Het KIN website", + "label": "Het KIN", "url": "https://hetkin.nl/", "target": "_blank" - }, - { - "label": "News", - "url": "/news", - "target": "_self" } ], "theme": {