Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ The format is based on [Keep a Changelog] and this project adheres to [Semantic
- PR #166: Replaced esdoc, which has had no release since 2018, with jsdoc and clean-jsdoc-theme. The documentation at <https://www.phyloref.org/phyx.js/> is now served from clean URLs, so links into the old pages need updating, and `PhylogenyWrapper` is published for the first time — esdoc and jsdoc alike had been skipping it as undocumented.
- PR #179: Stopped committing generated documentation. The JSON-LD contexts and JSON Schemas moved from `docs/context/` to `context/` at the repository root, changing their path within the published NPM package; they continue to be served from <https://www.phyloref.org/phyx.js/context/>.
- PR #195: Documented every method of `PhylogenyWrapper`, `PhylorefWrapper`, `PhyxWrapper` and `TaxonNameWrapper` that jsdoc had been skipping because its description was a `//` or `/* */` comment, so they now appear at <https://www.phyloref.org/phyx.js/>. The test suite fails if a class member has no `/**` comment above it.
- PR #198: Removed `PhylorefWrapper.getCurrentStatus()`, `getStatusChanges()`, `setStatus()` and `getStatusCURIEsInEnglish()`, which tracked a phyloreference's publication status (draft, under review, published, and so on) as `pso:holdsStatusInTime` entries. That was more machinery than the problem needed: curators can keep that information in curator notes or by filing Phyx files in directories per status, and Klados has already dropped its status editor. The `pso`, `tvc` and `timeinterval` prefixes stay in every JSON-LD context so that older Phyx files still expand, and the `moment` dependency is gone.

## [1.2.1] - 2025-07-28
- PR #152: Upraded packages using `npm upgrade`.
Expand Down
10 changes: 0 additions & 10 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@
"dependencies": {
"jsonld": "^9.0.0",
"lodash": "^4.17.23",
"moment": "^2.31.0",
"newick-js": "^1.2.4",
"retus": "^1.1.2",
"yargs": "^18.2.0"
Expand Down
182 changes: 1 addition & 181 deletions src/wrappers/PhylorefWrapper.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
// Used to parse timestamps for phyloref statuses.
const moment = require('moment');
const { has, cloneDeep, uniq } = require('lodash');

const owlterms = require('../utils/owlterms');
Expand All @@ -9,7 +7,7 @@ const { PhylogenyWrapper } = require('./PhylogenyWrapper');
const { CitationWrapper } = require('./CitationWrapper');

/**
* Wraps a phyloreference in a Phyx model, with multiple specifiers and statuses. Includes code for generating the
* Wraps a phyloreference in a Phyx model, with multiple specifiers. Includes code for generating the
* components of a phyloreference expression in OWL.
* @category Wrappers
*/
Expand Down Expand Up @@ -235,184 +233,6 @@ class PhylorefWrapper {
return Array.from(nodeLabels).sort();
}

/**
* Return a dictionary of all phyloref statuses in English, keyed by their
* CURIE in the Publishing Status Ontology (e.g. `pso:draft`).
*
* @return {Object<string, string>} Status CURIE to English label.
*/
static getStatusCURIEsInEnglish() {
return {
'pso:draft': 'Draft',
'pso:final-draft': 'Final draft',
'pso:under-review': 'Under review',
'pso:submitted': 'Tested',
'pso:published': 'Published',
'pso:retracted-from-publication': 'Retracted',
};
}

/**
* Return the current status of this phyloreference, i.e. the last of its
* `pso:holdsStatusInTime` entries. A phyloreference without any is a draft.
*
* The result object contains:
* - statusCURIE: phyloreference status as a short URI (CURIE)
* - statusInEnglish: an English representation of the phyloref status
* - intervalStart: the start of the interval, if recorded
* - intervalEnd: the end of the interval, if recorded
*
* @return {Object} The result object described above.
*/
getCurrentStatus() {
if (
has(this.phyloref, 'pso:holdsStatusInTime') &&
Array.isArray(this.phyloref['pso:holdsStatusInTime']) &&
this.phyloref['pso:holdsStatusInTime'].length > 0
) {
// If we have any pso:holdsStatusInTime entries, pick the last one, which
// is the current status, and extract the CURIE and time interval
// information from it.
const lastStatusInTime =
this.phyloref['pso:holdsStatusInTime'][
this.phyloref['pso:holdsStatusInTime'].length - 1
];
const statusCURIE = lastStatusInTime['pso:withStatus']['@id'];

// Look for time interval information
let intervalStart;
let intervalEnd;

if (has(lastStatusInTime, 'tvc:atTime')) {
const atTime = lastStatusInTime['tvc:atTime'];
if (has(atTime, 'timeinterval:hasIntervalStartDate'))
intervalStart = atTime['timeinterval:hasIntervalStartDate'];
if (has(atTime, 'timeinterval:hasIntervalEndDate'))
intervalEnd = atTime['timeinterval:hasIntervalEndDate'];
}

// Return result object
return {
statusCURIE,
statusInEnglish:
PhylorefWrapper.getStatusCURIEsInEnglish()[statusCURIE],
intervalStart,
intervalEnd,
};
}

// If we couldn't figure out a status for this phyloref, assume it's a draft.
return {
statusCURIE: 'pso:draft',
statusInEnglish: PhylorefWrapper.getStatusCURIEsInEnglish()['pso:draft'],
};
}

/**
* Return a list of status changes for this phyloreference, one per
* `pso:holdsStatusInTime` entry, in the order they were recorded.
*
* @return {Object[]} Each entry may carry `statusCURIE`, `statusInEnglish`,
* `intervalStart`, `intervalEnd`, and the latter two formatted for display
* as `intervalStartAsCalendar` and `intervalEndAsCalendar`.
*/
getStatusChanges() {
if (has(this.phyloref, 'pso:holdsStatusInTime')) {
return this.phyloref['pso:holdsStatusInTime'].map(entry => {
const result = {};

// Create a statusCURIE convenience field.
if (has(entry, 'pso:withStatus')) {
result.statusCURIE = entry['pso:withStatus']['@id'];
result.statusInEnglish =
PhylorefWrapper.getStatusCURIEsInEnglish()[result.statusCURIE];
}

// Create intervalStart/intervalEnd convenient fields
if (has(entry, 'tvc:atTime')) {
const atTime = entry['tvc:atTime'];
if (has(atTime, 'timeinterval:hasIntervalStartDate')) {
result.intervalStart = atTime['timeinterval:hasIntervalStartDate'];
result.intervalStartAsCalendar = moment(
result.intervalStart,
).calendar();
}

if (has(atTime, 'timeinterval:hasIntervalEndDate')) {
result.intervalEnd = atTime['timeinterval:hasIntervalEndDate'];
result.intervalEndAsCalendar = moment(
result.intervalEnd,
).calendar();
}
}

return result;
});
}

// No changes? Return an empty list.
return [];
}

/**
* Set the status of this phyloreference. If the previous status's time interval
* is still open, it is ended now, and a new `pso:holdsStatusInTime` entry starting
* now is appended.
*
* @param {string} status - A status CURIE; one of the keys of
* {@link PhylorefWrapper.getStatusCURIEsInEnglish}.
* @throws {TypeError} If `status` is not a known status CURIE.
*/
setStatus(status) {
// Check whether we have a valid status CURIE.
if (!has(PhylorefWrapper.getStatusCURIEsInEnglish(), status)) {
throw new TypeError(
`setStatus() called with invalid status CURIE '${status}'`,
);
}

// See if we can end the previous interval.
const currentTime = new Date(Date.now()).toISOString();

if (!has(this.phyloref, 'pso:holdsStatusInTime')) {
// Vue.set(this.phyloref, 'pso:holdsStatusInTime', []);
this.phyloref['pso:holdsStatusInTime'] = [];
}

// Check to see if there's a previous time interval we should end.
if (
Array.isArray(this.phyloref['pso:holdsStatusInTime']) &&
this.phyloref['pso:holdsStatusInTime'].length > 0
) {
const lastStatusInTime =
this.phyloref['pso:holdsStatusInTime'][
this.phyloref['pso:holdsStatusInTime'].length - 1
];

// if (!has(lastStatusInTime, 'tvc:atTime'))
// Vue.set(lastStatusInTime, 'tvc:atTime', {});
if (!has(lastStatusInTime, 'tvc:atTime')) {
lastStatusInTime['tvc:atTime'] = {};
}
if (
!has(lastStatusInTime['tvc:atTime'], 'timeinterval:hasIntervalEndDate')
) {
// If the last time entry doesn't already have an interval end date, set it to now.
lastStatusInTime['tvc:atTime']['timeinterval:hasIntervalEndDate'] =
currentTime;
}
}

// Create new entry.
this.phyloref['pso:holdsStatusInTime'].push({
'@type': 'http://purl.org/spar/pso/StatusInTime',
'pso:withStatus': { '@id': status },
'tvc:atTime': {
'timeinterval:hasIntervalStartDate': currentTime,
},
});
}

/**
* Return a list of all the unique nomenclatural codes used by this phyloreference.
* The default nomenclatural code used in creating the PhylorefWrapper will be used
Expand Down
51 changes: 1 addition & 50 deletions test/phylorefs.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,12 @@ const owlterms = require('../src/utils/owlterms');
const expect = chai.expect;

/*
* Phyloref tests cover three aspects of phyloreferences:
* Phyloref tests cover two aspects of phyloreferences:
* - Whether we can create a phyloref with a particular set of specifiers,
* and whether we can correctly change the type of a specifer (from 'External'
* to 'Internal'), delete specifiers, and retrieve specifier labels.
* - Whether we can determine to which node a phyloref is expected to resolve to
* by using additionalNodeProperties.
* - Whether we can update the phyloref's status several times and retrieve the
* full history of its status changes.
*/

describe('PhylorefWrapper', function () {
Expand Down Expand Up @@ -201,53 +199,6 @@ describe('PhylorefWrapper', function () {
});
});

describe('given an empty phyloreference', function () {
const wrapper = new phyx.PhylorefWrapper({});

describe('#getCurrentStatus', function () {
it('should return "pso:draft" as the default initial status', function () {
// Initially, an empty phyloref should report a status of 'pso:draft'.
expect(wrapper.getCurrentStatus().statusCURIE).to.equal('pso:draft');
});
});

describe('#setStatus', function () {
it('should throw an error if given a mistyped status', function () {
expect(function () { wrapper.setStatus('pso:retracted-from_publication'); })
.to.throw(
TypeError,
'setStatus() called with invalid status CURIE \'pso:retracted-from_publication\'',
'PhylorefWrapper throws TypeError on a mistyped status'
);
});
});

describe('#getStatusChanges', function () {
it('should return the empty list', function () {
expect(wrapper.getStatusChanges()).to.be.empty;
});

describe('when modified by using .setStatus', function () {
it('should return the updated list', function () {
wrapper.setStatus('pso:final-draft');
wrapper.setStatus('pso:under-review');
wrapper.setStatus('pso:submitted');
wrapper.setStatus('pso:published');
wrapper.setStatus('pso:retracted-from-publication');

// And see if we get the statuses back in the correct order.
const statusChanges = wrapper.getStatusChanges();
expect(statusChanges.length, 'number of status changes should be 5').to.equal(5);
expect(statusChanges[0].statusCURIE, 'first status change should be "pso:final-draft"').to.equal('pso:final-draft');
expect(statusChanges[1].statusCURIE, 'second status change should be "pso:under-review"').to.equal('pso:under-review');
expect(statusChanges[2].statusCURIE, 'third status change should be a "pso:submitted"').to.equal('pso:submitted');
expect(statusChanges[3].statusCURIE, 'fourth status change should be a "pso:published"').to.equal('pso:published');
expect(statusChanges[4].statusCURIE, 'fifth status change should be a "pso:retracted-from-publication"').to.equal('pso:retracted-from-publication');
});
});
});
});

describe('#asJSONLD', function () {
it('should preserve an existing @id on input phylorefs', function () {
const jsonld = new phyx.PhylorefWrapper({
Expand Down
Loading