diff --git a/README.md b/README.md index 8093a47..45d1857 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,7 @@ a codemeta.json file This tool was initially prepared for the [FORCE19 Hackathon](https://github.com/force11/force11-rda-scidwg/tree/master/hackathon/FORCE2019). +**NB:** codemeta v2.0 is generated by default, but v3.0 (v2.0 compatible) can be generated via a dedicated button. ## Code contributions. @@ -61,8 +62,7 @@ Chromium/Google Chrome, Edge, Safari). Check [Caniuse](https://caniuse.com/) for availability of features for these browsers. To keep the architecture simple, we serve javascript files directly to -browsers, without a compiler or transpiler; and do not use third-party -dependencies for now. +browsers, without a compiler or transpiler. ### Running local changes diff --git a/cypress/integration/basics.js b/cypress/integration/basics.js index 4e1e575..2cfd994 100644 --- a/cypress/integration/basics.js +++ b/cypress/integration/basics.js @@ -264,4 +264,101 @@ describe('JSON Import', function() { cy.get('#name').should('have.value', 'My Test Software'); }); + it('imports properties introduced in codemeta v3.0', function() { + cy.get('#codemetaText').then((elem) => + elem.text(JSON.stringify({ + "@context": "https://w3id.org/codemeta/3.0", + "type": "SoftwareSourceCode", + "name": "My Test Software", + "continuousIntegration": "https://test-ci.org/my-software", + "isSourceCodeOf": "Bigger Application", + "review": { + "type": "Review", + "reviewAspect": "Some software aspect", + "reviewBody": "Some review" + } + })) + ); + cy.get('#importCodemeta').click(); + + cy.get('#contIntegration').should('have.value', 'https://test-ci.org/my-software'); + cy.get('#isSourceCodeOf').should('have.value', 'Bigger Application'); + cy.get('#reviewAspect').should('have.value', 'Some software aspect'); + cy.get('#reviewBody').should('have.value', 'Some review'); + }); + + it('imports codemeta v2.0 properties from document with v3.0 context', function() { + cy.get('#codemetaText').then((elem) => + elem.text(JSON.stringify({ + "@context": "https://w3id.org/codemeta/3.0", + "type": "SoftwareSourceCode", + "name": "My Test Software", + "codemeta:contIntegration": { + "id": "https://test-ci.org/my-software" + } + })) + ); + cy.get('#importCodemeta').click(); + + cy.get('#contIntegration').should('have.value', 'https://test-ci.org/my-software'); + }); + + it('imports codemeta v3.0 properties from document with v2.0 context', function() { + cy.get('#codemetaText').then((elem) => + elem.text(JSON.stringify({ + "@context": "https://doi.org/10.5063/schema/codemeta-2.0", + "type": "SoftwareSourceCode", + "name": "My Test Software", + "codemeta:continuousIntegration": { + "id": "https://test-ci.org/my-software" + }, + "codemeta:isSourceCodeOf": { + "id": "Bigger Application" + }, + "schema:review": { + "type": "schema:Review", + "schema:reviewAspect": "Some software aspect", + "schema:reviewBody": "Some review" + } + })) + ); + cy.get('#importCodemeta').click(); + + cy.get('#contIntegration').should('have.value', 'https://test-ci.org/my-software'); + cy.get('#isSourceCodeOf').should('have.value', 'Bigger Application'); + cy.get('#reviewAspect').should('have.value', 'Some software aspect'); + cy.get('#reviewBody').should('have.value', 'Some review'); + }); + + it('imports newest version property when it is duplicate in multiple version context', function() { + cy.get('#codemetaText').then((elem) => + elem.text(JSON.stringify({ + "@context": "https://doi.org/10.5063/schema/codemeta-2.0", + "type": "SoftwareSourceCode", + "name": "My Test Software", + "contIntegration": "https://test-ci1.org/my-software", + "codemeta:continuousIntegration": { + "id": "https://test-ci2.org/my-software" + }, + })) + ); + cy.get('#importCodemeta').click(); + + cy.get('#contIntegration').should('have.value', 'https://test-ci2.org/my-software'); + + cy.get('#codemetaText').then((elem) => + elem.text(JSON.stringify({ + "@context": "https://doi.org/10.5063/schema/codemeta-3.0", + "type": "SoftwareSourceCode", + "name": "My Test Software", + "continuousIntegration": "https://test-ci1.org/my-software", + "codemeta:contIntegration": { + "id": "https://test-ci2.org/my-software" + }, + })) + ); + cy.get('#importCodemeta').click(); + + cy.get('#contIntegration').should('have.value', 'https://test-ci1.org/my-software'); + }); }); diff --git a/cypress/integration/persons.js b/cypress/integration/persons.js index 7516167..b3b68ce 100644 --- a/cypress/integration/persons.js +++ b/cypress/integration/persons.js @@ -108,7 +108,7 @@ describe('One full author', function() { }); }); - it('can be imported', function() { + it('can be imported even if there is also a role-less author', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", @@ -578,7 +578,82 @@ describe('One author with a role', function () { }); it('can be imported', function () { - // TODO + cy.get('#codemetaText').then((elem) => + elem.text(JSON.stringify({ + "@context": "https://w3id.org/codemeta/3.0", + "type": "SoftwareSourceCode", + "name": "My Test Software", + "author": [ + { + "type": "Person", + "givenName": "Jane" + }, + { + "type": "Role", + "schema:author": { + "type": "Person", + "givenName": "Jane" + }, + "roleName": "Developer", + "startDate": "2024-03-04", + "endDate": "2024-04-03" + } + ] + })) + ); + cy.get('#importCodemeta').click(); + + cy.get('#author_nb').should('have.value', '1'); + cy.get('#author_1_givenName').should('have.value', 'Jane'); + cy.get('#author_1_roleName_0').should('have.value', 'Developer'); + cy.get('#author_1_startDate_0').should('have.value', '2024-03-04'); + cy.get('#author_1_endDate_0').should('have.value', '2024-04-03'); + }); + + it('can be imported when there is a second one, and they are merged', function () { + cy.get('#codemetaText').then((elem) => + elem.text(JSON.stringify({ + "@context": "https://w3id.org/codemeta/3.0", + "type": "SoftwareSourceCode", + "name": "My Test Software", + "author": [ + { + "type": "Person", + "givenName": "Jane" + }, + { + "type": "Role", + "schema:author": { + "type": "Person", + "givenName": "Jane" + }, + "roleName": "Maintainer", + "startDate": "2024-04-04", + "endDate": "2024-05-05" + }, + { + "type": "Role", + "schema:author": { + "type": "Person", + "givenName": "Jane" + }, + "roleName": "Developer", + "startDate": "2024-03-04", + "endDate": "2024-04-03" + } + ] + })) + ); + cy.get('#importCodemeta').click(); + + cy.get('#author_nb').should('have.value', '1'); + cy.get('#author_1_givenName').should('have.value', 'Jane'); + cy.get('#author_1_roleName_0').should('have.value', 'Maintainer'); + cy.get('#author_1_startDate_0').should('have.value', '2024-04-04'); + cy.get('#author_1_endDate_0').should('have.value', '2024-05-05'); + cy.get('#author_1_roleName_1').should('have.value', 'Developer'); + cy.get('#author_1_startDate_1').should('have.value', '2024-03-04'); + cy.get('#author_1_endDate_1').should('have.value', '2024-04-03'); }); }); @@ -683,4 +758,93 @@ describe('Multiple authors', function () { ] }); }); + + it('who both have roles can be imported', function () { + cy.get('#codemetaText').then((elem) => + elem.text(JSON.stringify({ + "@context": "https://w3id.org/codemeta/3.0", + "type": "SoftwareSourceCode", + "name": "My Test Software", + "author": [ + { + "type": "Person", + "givenName": "Jane" + }, + { + "type": "Role", + "schema:author": { + "type": "Person", + "givenName": "Jane" + }, + "roleName": "Developer", + "startDate": "2024-03-04", + "endDate": "2024-04-03" + }, + { + "type": "Person", + "givenName": "Joe" + }, + { + "type": "Role", + "schema:author": { + "type": "Person", + "givenName": "Joe" + }, + "roleName": "Maintainer", + "startDate": "2024-04-04", + "endDate": "2024-05-05" + } + ] + })) + ); + cy.get('#importCodemeta').click(); + + cy.get('#author_nb').should('have.value', '2'); + cy.get('#author_1_givenName').should('have.value', 'Jane'); + cy.get('#author_1_roleName_0').should('have.value', 'Developer'); + cy.get('#author_1_startDate_0').should('have.value', '2024-03-04'); + cy.get('#author_1_endDate_0').should('have.value', '2024-04-03'); + cy.get('#author_2_givenName').should('have.value', 'Joe'); + cy.get('#author_2_roleName_0').should('have.value', 'Maintainer'); + cy.get('#author_2_startDate_0').should('have.value', '2024-04-04'); + cy.get('#author_2_endDate_0').should('have.value', '2024-05-05'); + }); + + it('whose one has a role and the other not can be imported', function () { + cy.get('#codemetaText').then((elem) => + elem.text(JSON.stringify({ + "@context": "https://w3id.org/codemeta/3.0", + "type": "SoftwareSourceCode", + "name": "My Test Software", + "author": [ + { + "type": "Person", + "givenName": "Jane" + }, + { + "type": "Role", + "schema:author": { + "type": "Person", + "givenName": "Jane" + }, + "roleName": "Developer", + "startDate": "2024-03-04", + "endDate": "2024-04-03" + }, + { + "type": "Person", + "givenName": "Joe" + } + ] + })) + ); + cy.get('#importCodemeta').click(); + + cy.get('#author_nb').should('have.value', '2'); + cy.get('#author_1_givenName').should('have.value', 'Jane'); + cy.get('#author_1_roleName_0').should('have.value', 'Developer'); + cy.get('#author_1_startDate_0').should('have.value', '2024-03-04'); + cy.get('#author_1_endDate_0').should('have.value', '2024-04-03'); + cy.get('#author_2_givenName').should('have.value', 'Joe'); + }); }); diff --git a/index.html b/index.html index 3af9815..cda6dc8 100644 --- a/index.html +++ b/index.html @@ -21,7 +21,7 @@
-

CodeMeta generator v3.0

+

CodeMeta generator v3.0

diff --git a/js/codemeta_generation.js b/js/codemeta_generation.js index 8a603c7..6dd111d 100644 --- a/js/codemeta_generation.js +++ b/js/codemeta_generation.js @@ -127,9 +127,9 @@ const directReviewCodemetaFields = [ 'reviewBody' ]; -const crossedCodemetaFields = { +const crossCodemetaFields = { "contIntegration": ["contIntegration", "continuousIntegration"], - "embargoDate": ["embargoDate", "embargoEndDate"], + // "embargoDate": ["embargoDate", "embargoEndDate"], Not present in the form yet TODO ? }; function generateShortOrg(fieldName) { @@ -260,9 +260,9 @@ async function buildExpandedJson() { doc["contributor"] = contributors; } - for (const [key, values] of Object.entries(crossedCodemetaFields)) { - values.forEach(value => { - doc[value] = doc[key]; + for (const [key, items] of Object.entries(crossCodemetaFields)) { + items.forEach(item => { + doc[item] = doc[key]; }); } return await jsonld.expand(doc); @@ -312,12 +312,59 @@ function importShortOrg(fieldName, doc) { } } +function importReview(doc) { + if (doc !== undefined) { + directReviewCodemetaFields.forEach(item => { + setIfDefined('#' + item, doc[item]); + }); + } +} + +function authorsEqual(author1, author2) { + // TODO should test more properties for equality? + return author1.givenName === author2.givenName + && author1.familyName === author2.familyName + && author1.email === author2.email; +} + +function getSingleAuthorsFromRoles(docs) { + return docs.filter(doc => getDocumentType(doc) === "Role") + .map(doc => doc["schema:author"]) + .reduce((authorSet, currentAuthor) => { + const foundAuthor = authorSet.find(author => authorsEqual(author, currentAuthor)); + if (!foundAuthor) { + return authorSet.concat([currentAuthor]); + } else { + return authorSet; + } + }, []); +} + +function importRoles(personPrefix, roles) { + roles.forEach(role => { + const roleId = addRole(`${personPrefix}`); + directRoleCodemetaFields.forEach(item => { + setIfDefined(`#${personPrefix}_${item}_${roleId}`, role[item]); + }); + }); +} + function importPersons(prefix, legend, docs) { if (docs === undefined) { return; } - docs.forEach(function (doc, index) { + const authors = docs.filter(doc => getDocumentType(doc) === "Person"); + const authorsFromRoles = getSingleAuthorsFromRoles(docs); + const allAuthorDocs = authors.concat(authorsFromRoles) + .reduce((authors, currentAuthor) => { + if (!authors.find(author => authorsEqual(author, currentAuthor))) { + authors.push(currentAuthor); + } + return authors; + }, []); + + allAuthorDocs.forEach(function (doc, index) { var personId = addPerson(prefix, legend); setIfDefined(`#${prefix}_${personId}_id`, getDocumentId(doc)); @@ -325,8 +372,12 @@ function importPersons(prefix, legend, docs) { setIfDefined(`#${prefix}_${personId}_${item}`, doc[item]); }); - importShortOrg(`#${prefix}_${personId}_affiliation`, doc['affiliation']) - }) + importShortOrg(`#${prefix}_${personId}_affiliation`, doc['affiliation']); + + const roles = docs.filter(currentDoc => getDocumentType(currentDoc) === "Role") + .filter(currentDoc => authorsEqual(currentDoc["schema:author"], doc)); + importRoles(`${prefix}_${personId}`, roles); + }); } async function importCodemeta() { @@ -350,6 +401,7 @@ async function importCodemeta() { setIfDefined('#' + item, doc[item]); }); importShortOrg('#funder', doc["funder"]); + importReview(doc["review"]); // Import simple fields by joining on their separator splittedCodemetaFields.forEach(function (item, index) { @@ -364,6 +416,14 @@ async function importCodemeta() { } }); + for (const [key, items] of Object.entries(crossCodemetaFields)) { + let value = ""; + items.forEach(item => { + value = doc[item] || value; + }); + setIfDefined(`#${key}`, value); + } + importPersons('author', 'Author', doc['author']) importPersons('contributor', 'Contributor', doc['contributor']) } diff --git a/js/dynamic_form.js b/js/dynamic_form.js index 7f43bd2..f15aba8 100644 --- a/js/dynamic_form.js +++ b/js/dynamic_form.js @@ -175,6 +175,8 @@ function addRole(personPrefix) { .addEventListener('click', () => removeRole(personPrefix, roleIndex)); roleIndexNode.value = roleIndex + 1; + + return roleIndex; } function removeRole(personPrefix, roleIndex) { diff --git a/js/validation/index.js b/js/validation/index.js index c2ca222..4035f8c 100644 --- a/js/validation/index.js +++ b/js/validation/index.js @@ -89,6 +89,8 @@ async function parseAndValidateCodemeta(showPopup) { } } - doc = await jsonld.compact(parsed, CODEMETA_CONTEXTS["2.0"].url); // Only import codemeta v2.0 for now + parsed["@context"] = LOCAL_CONTEXT_URL; + const expanded = await jsonld.expand(parsed); + doc = await jsonld.compact(expanded, LOCAL_CONTEXT_URL); return doc; }