diff --git a/codemeta_generation.js b/codemeta_generation.js index 892db9b..b694b85 100644 --- a/codemeta_generation.js +++ b/codemeta_generation.js @@ -1,220 +1,228 @@ /** * Copyright (C) 2019-2020 The Software Heritage developers * See the AUTHORS file at the top-level directory of this distribution * License: GNU Affero General Public License version 3, or any later version * See top-level LICENSE file for more information */ "use strict"; const SPDX_PREFIX = 'https://spdx.org/licenses/'; function emptyToUndefined(v) { if (v == null || v == "") return undefined; else return v; } function getIfSet(query) { return emptyToUndefined(document.querySelector(query).value); } function setIfDefined(query, value) { if (value !== undefined) { document.querySelector(query).value = value; } } // Names of codemeta properties with a matching HTML field name const directCodemetaFields = [ 'codeRepository', 'contIntegration', 'dateCreated', 'datePublished', 'dateModified', 'downloadUrl', 'issueTracker', 'name', 'version', 'identifier', 'description', 'applicationCategory', 'releaseNotes', 'funding', 'developmentStatus', 'isPartOf', 'referencePublication' ]; const splittedCodemetaFields = [ ['keywords', ','], ['programmingLanguage', ','], ['runtimePlatform', ','], ['operatingSystem', ','], ['softwareRequirements', '\n'], ['relatedLink', '\n'], ] // Names of codemeta properties with a matching HTML field name, // in a Person object const directPersonCodemetaFields = [ 'givenName', 'familyName', 'email', 'affiliation' ]; function generatePerson(idPrefix) { var doc = { "@type": "Person", "@id": getIfSet(`#${idPrefix}_id`), } directPersonCodemetaFields.forEach(function (item, index) { doc[item] = getIfSet(`#${idPrefix}_${item}`); }); var affiliation = getIfSet(`#${idPrefix}_affiliation`); if (affiliation !== undefined) { doc["affiliation"] = { "@type": "Organization", - "@id": affiliation, + } + if (isUrl(affiliation)) { + doc["affiliation"]["@id"] = affiliation; + } + else { + doc["affiliation"]["name"] = affiliation; } } return doc; } function generatePersons(prefix) { var persons = []; var nbPersons = getNbPersons(prefix); for (let personId = 1; personId <= nbPersons; personId++) { persons.push(generatePerson(`${prefix}_${personId}`)); } return persons; } function generateCodemeta() { var inputForm = document.querySelector('#inputForm'); var codemetaText, errorHTML; if (inputForm.checkValidity()) { var doc = { "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", }; var license = getIfSet('#license') if (license !== undefined) { doc["license"] = SPDX_PREFIX + getIfSet('#license'); } // Generate most fields directCodemetaFields.forEach(function (item, index) { doc[item] = getIfSet('#' + item) }); // Generate simple fields parsed simply by splitting splittedCodemetaFields.forEach(function (item, index) { const id = item[0]; const separator = item[1]; const value = getIfSet('#' + id); if (value !== undefined) { doc[id] = value.split(separator).map(trimSpaces); } }); // Generate dynamic fields var authors = generatePersons('author'); if (authors.length > 0) { doc["author"] = authors; } var contributors = generatePersons('contributor'); if (contributors.length > 0) { doc["contributors"] = contributors; } codemetaText = JSON.stringify(doc, null, 4); errorHTML = ""; } else { codemetaText = ""; errorHTML = "invalid input (see error above)"; inputForm.reportValidity(); } document.querySelector('#codemetaText').innerText = codemetaText; setError(errorHTML); if (codemetaText) { // For restoring the form state on page reload sessionStorage.setItem('codemetaText', codemetaText); } } function importPersons(prefix, legend, docs) { if (docs === undefined) { return; } docs.forEach(function (doc, index) { var personId = addPerson(prefix, legend); setIfDefined(`#${prefix}_${personId}_id`, doc['@id']); directPersonCodemetaFields.forEach(function (item, index) { setIfDefined(`#${prefix}_${personId}_${item}`, doc[item]); }); + + // Use @id if set, else use name + setIfDefined(`#${prefix}_${personId}_affiliation`, doc['affiliation']['name']); setIfDefined(`#${prefix}_${personId}_affiliation`, doc['affiliation']['@id']); }) } function importCodemeta() { var inputForm = document.querySelector('#inputForm'); var codemetaText = document.querySelector('#codemetaText').innerText; var doc; try { doc = JSON.parse(codemetaText); } catch (e) { setError(`Could not read codemeta document because it is not valid JSON (${e})`); return; } resetForm(); if (doc['license'] !== undefined && doc['license'].indexOf(SPDX_PREFIX) == 0) { var license = doc['license'].substring(SPDX_PREFIX.length); document.querySelector('#license').value = license; } directCodemetaFields.forEach(function (item, index) { setIfDefined('#' + item, doc[item]); }); // Import simple fields by joining on their separator splittedCodemetaFields.forEach(function (item, index) { const id = item[0]; const separator = item[1]; let value = doc[id]; if (value !== undefined) { if (Array.isArray(value)) { value = value.join(separator); } setIfDefined('#' + id, value); } }); importPersons('author', 'Author', doc['author']) importPersons('contributor', 'Contributor', doc['contributor']) setError(""); } function loadStateFromStorage() { var codemetaText = sessionStorage.getItem('codemetaText') if (codemetaText) { document.querySelector('#codemetaText').innerText = codemetaText; importCodemeta(); } } diff --git a/cypress/integration/persons.js b/cypress/integration/persons.js index da90e67..6abbf45 100644 --- a/cypress/integration/persons.js +++ b/cypress/integration/persons.js @@ -1,155 +1,271 @@ /** * Copyright (C) 2020 The Software Heritage developers * See the AUTHORS file at the top-level directory of this distribution * License: GNU Affero General Public License version 3, or any later version * See top-level LICENSE file for more information */ /* * Tests the author/contributor dynamic fieldsets */ "use strict"; describe('Persons', function() { beforeEach(function() { /* Clear the session storage, as it is used to restore field data; * and we don't want a test to load data from the previous test. */ cy.window().then((win) => { win.sessionStorage.clear() }) cy.visit('./index.html'); }); // No author: it('exports when there are no author', function() { cy.get('#name').type('My Test Software'); cy.get('#generateCodemeta').click(); cy.get('#errorMessage').should('have.text', ''); cy.get('#codemetaText').then((elem) => JSON.parse(elem.text())) .should('deep.equal', { "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", "name": "My Test Software", }); }); it('imports missing author', 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", })) ); cy.get('#importCodemeta').click(); cy.get('#author_nb').should('have.value', '0'); cy.get('#author_0').should('not.exist'); cy.get('#author_1').should('not.exist'); }); it('imports empty author list', 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", "author": [], })) ); cy.get('#importCodemeta').click(); cy.get('#author_nb').should('have.value', '0'); cy.get('#author_0').should('not.exist'); cy.get('#author_1').should('not.exist'); }); // Single author: it('exports single full author', function() { cy.get('#name').type('My Test Software'); cy.get('#author_nb').should('have.value', '0'); cy.get('#author_0').should('not.exist'); cy.get('#author_1').should('not.exist'); cy.get('#author_1_givenName').should('not.exist'); cy.get('#author_add').click(); cy.get('#author_nb').should('have.value', '1'); cy.get('#author_0').should('not.exist'); cy.get('#author_1').should('exist'); cy.get('#author_2').should('not.exist'); cy.get('#author_1_givenName').should('have.value', ''); cy.get('#author_1_familyName').should('have.value', ''); cy.get('#author_1_email').should('have.value', ''); cy.get('#author_1_id').should('have.value', ''); cy.get('#author_1_affiliation').should('have.value', ''); cy.get('#author_1_givenName').type('Jane'); cy.get('#author_1_familyName').type('Doe'); cy.get('#author_1_email').type('jdoe@example.org'); cy.get('#author_1_id').type('http://example.org/~jdoe'); cy.get('#author_1_affiliation').type('http://example.org/'); cy.get('#generateCodemeta').click(); cy.get('#errorMessage').should('have.text', ''); cy.get('#codemetaText').then((elem) => JSON.parse(elem.text())) .should('deep.equal', { "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", "name": "My Test Software", "author": [ { "@type": "Person", "@id": "http://example.org/~jdoe", "givenName": "Jane", "familyName": "Doe", "email": "jdoe@example.org", "affiliation": { "@type": "Organization", "@id": "http://example.org/", } } ], }); }); it('imports single full author', 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", "author": [ { "@type": "Person", "@id": "http://example.org/~jdoe", "givenName": "Jane", "familyName": "Doe", "email": "jdoe@example.org", "affiliation": { "@type": "Organization", "@id": "http://example.org/", } } ], })) ); cy.get('#importCodemeta').click(); cy.get('#author_nb').should('have.value', '1'); cy.get('#author_0').should('not.exist'); cy.get('#author_1').should('exist'); cy.get('#author_2').should('not.exist'); cy.get('#author_1_givenName').should('have.value', 'Jane'); cy.get('#author_1_familyName').should('have.value', 'Doe'); cy.get('#author_1_email').should('have.value', 'jdoe@example.org'); cy.get('#author_1_id').should('have.value', 'http://example.org/~jdoe'); cy.get('#author_1_affiliation').should('have.value', 'http://example.org/'); }); + + it('exports affiliation id', function() { + cy.get('#name').type('My Test Software'); + + cy.get('#author_add').click(); + cy.get('#author_1_givenName').type('Jane'); + cy.get('#author_1_affiliation').type('http://example.org/'); + + cy.get('#generateCodemeta').click(); + + cy.get('#errorMessage').should('have.text', ''); + cy.get('#codemetaText').then((elem) => JSON.parse(elem.text())) + .should('deep.equal', { + "@context": "https://doi.org/10.5063/schema/codemeta-2.0", + "@type": "SoftwareSourceCode", + "name": "My Test Software", + "author": [ + { + "@type": "Person", + "givenName": "Jane", + "affiliation": { + "@type": "Organization", + "@id": "http://example.org/", + } + } + ], + }); + }); + + it('imports affiliation id', 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", + "author": [ + { + "@type": "Person", + "@id": "http://example.org/~jdoe", + "givenName": "Jane", + "familyName": "Doe", + "email": "jdoe@example.org", + "affiliation": { + "@type": "Organization", + "@id": "http://example.org/", + } + } + ], + })) + ); + cy.get('#importCodemeta').click(); + + cy.get('#author_nb').should('have.value', '1'); + cy.get('#author_0').should('not.exist'); + cy.get('#author_1').should('exist'); + cy.get('#author_2').should('not.exist'); + cy.get('#author_1_affiliation').should('have.value', 'http://example.org/'); + }); + + it('exports affiliation name', function() { + cy.get('#name').type('My Test Software'); + + cy.get('#author_add').click(); + cy.get('#author_1_givenName').type('Jane'); + cy.get('#author_1_affiliation').type('Example Org'); + + cy.get('#generateCodemeta').click(); + + cy.get('#errorMessage').should('have.text', ''); + cy.get('#codemetaText').then((elem) => JSON.parse(elem.text())) + .should('deep.equal', { + "@context": "https://doi.org/10.5063/schema/codemeta-2.0", + "@type": "SoftwareSourceCode", + "name": "My Test Software", + "author": [ + { + "@type": "Person", + "givenName": "Jane", + "affiliation": { + "@type": "Organization", + "name": "Example Org", + } + } + ], + }); + }); + + it('imports affiliation name', 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", + "author": [ + { + "@type": "Person", + "@id": "http://example.org/~jdoe", + "givenName": "Jane", + "familyName": "Doe", + "email": "jdoe@example.org", + "affiliation": { + "@type": "Organization", + "name": "Example Org", + } + } + ], + })) + ); + cy.get('#importCodemeta').click(); + + cy.get('#author_nb').should('have.value', '1'); + cy.get('#author_0').should('not.exist'); + cy.get('#author_1').should('exist'); + cy.get('#author_2').should('not.exist'); + cy.get('#author_1_affiliation').should('have.value', 'Example Org'); + }); }); diff --git a/dynamic_form.js b/dynamic_form.js index 315f400..01db5dc 100644 --- a/dynamic_form.js +++ b/dynamic_form.js @@ -1,128 +1,128 @@ /** * Copyright (C) 2019 The Software Heritage developers * See the AUTHORS file at the top-level directory of this distribution * License: GNU Affero General Public License version 3, or any later version * See top-level LICENSE file for more information */ "use strict"; function createPersonFieldset(personPrefix, legend) { // Creates a fieldset containing inputs for informations about a person var fieldset = document.createElement("fieldset") fieldset.classList.add("person"); fieldset.classList.add("leafFieldset"); fieldset.id = personPrefix; fieldset.innerHTML = ` ${legend}

-

`; return fieldset; } function addPersonWithId(container, prefix, legend, id) { var fieldset = createPersonFieldset(`${prefix}_${id}`, `${legend} #${id}`); container.appendChild(fieldset); } function addPerson(prefix, legend) { var container = document.querySelector(`#${prefix}_container`); var personId = getNbPersons(prefix) + 1; addPersonWithId(container, prefix, legend, personId); setNbPersons(prefix, personId); return personId; } function removePerson(prefix) { var personId = getNbPersons(prefix); document.querySelector(`#${prefix}_${personId}`).remove(); setNbPersons(prefix, personId-1); } // Initialize a group of persons (authors, contributors) on page load. // Useful if the page is reloaded. function initPersons(prefix, legend) { var nbPersons = getNbPersons(prefix); var personContainer = document.querySelector(`#${prefix}_container`) for (let personId = 1; personId <= nbPersons; personId++) { addPersonWithId(personContainer, prefix, legend, personId); } } function removePersons(prefix) { var nbPersons = getNbPersons(prefix); var personContainer = document.querySelector(`#${prefix}_container`) for (let personId = 1; personId <= nbPersons; personId++) { removePerson(prefix) } } function resetForm() { removePersons('author'); removePersons('contributor'); // Reset the form after deleting elements, so nbPersons doesn't get // reset before it's read. document.querySelector('#inputForm').reset(); } function fieldToLower(event) { event.target.value = event.target.value.toLowerCase(); } function initCallbacks() { document.querySelector('#license') .addEventListener('change', validateLicense); document.querySelector('#generateCodemeta') .addEventListener('click', generateCodemeta); document.querySelector('#resetForm') .addEventListener('click', resetForm); document.querySelector('#importCodemeta') .addEventListener('click', importCodemeta); document.querySelector('#inputForm') .addEventListener('change', generateCodemeta); document.querySelector('#developmentStatus') .addEventListener('change', fieldToLower); initPersons('author', 'Author'); initPersons('contributor', 'Contributor'); } diff --git a/utils.js b/utils.js index 4d0c673..8d203cb 100644 --- a/utils.js +++ b/utils.js @@ -1,26 +1,36 @@ /** * Copyright (C) 2019 The Software Heritage developers * See the AUTHORS file at the top-level directory of this distribution * License: GNU Affero General Public License version 3, or any later version * See top-level LICENSE file for more information */ "use strict"; function getNbPersons(prefix) { var nbField = document.querySelector(`#${prefix}_nb`); return parseInt(nbField.value, 10); } function setNbPersons(prefix, nb) { var nbField = document.querySelector(`#${prefix}_nb`); nbField.value = nb; } function setError(msg) { document.querySelector("#errorMessage").innerHTML = msg; } function trimSpaces(s) { return s.replace(/^\s+|\s+$/g, ''); } + +// From https://stackoverflow.com/a/43467144 +function isUrl(s) { + try { + new URL(s); + return true; + } catch (e) { + return false; + } +}