diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8dbf0c8da..34a0d42c9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,16 +3,29 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
-## [Unreleased]
+
+## [Unreleased]
+
### Added
-- Added `dateWithdrawn` and `withdrawalJustification` to `preprint` model
-- 'My Preprints' link to the Preprint navbar
+- Contributor mixin to share contributor-related methods between nodes and preprints
+- New fields added to `preprint` model for node-preprint divorce
+- Default message in `old-file-browser` if no files found
### Changed
-- Normalize "Add a Preprint" language across screen sizes
+- Node-preprint divorce changes
+- `contributor` model now shared between nodes and preprints. `preprint` relationship added to `contributor` model
+- Modify `contributor` adapter to talk to both node/preprint contributor endpoints
+- `Preprint` adapter has an option to unset a supplemental project from a preprint
+- Fix for building mfrURLs
+
+## [0.21.0] - 2018-09-20
+### Added
+- Added `dateWithdrawn` and `withdrawalJustification` to `preprint` model
+- 'My Preprints' link to the Preprint navbar
### Changed
- `users` model to use urlForQuery to allow searching via `/search/users/`
+- Normalize "Add a Preprint" language across screen sizes
## [0.20.1] - 2018-08-16
### Remove unwanted lineage code
diff --git a/addon/adapters/contributor.js b/addon/adapters/contributor.js
index d2dd7bf30..12061a195 100644
--- a/addon/adapters/contributor.js
+++ b/addon/adapters/contributor.js
@@ -3,16 +3,23 @@ import OsfAdapter from './osf-adapter';
export default OsfAdapter.extend({
buildURL(modelName, id, snapshot, requestType) { // jshint ignore:line
if (requestType === 'createRecord' || requestType === 'findRecord') {
- var nodeId;
- var sendEmail = true;
+ let nodeId;
+ let sendEmail = true;
+ let requestUrl;
+
if (snapshot) {
nodeId = snapshot.record.get('nodeId');
sendEmail = snapshot.record.get('sendEmail');
} else {
nodeId = id.split('-').shift();
}
-
+ let type = 'node';
let node = this.store.peekRecord('node', nodeId);
+ if (!node) {
+ node = this.store.peekRecord('preprint', nodeId);
+ type = 'preprint';
+ }
+
if (node) {
let base = this._buildRelationshipURL(
node._internalModel.createSnapshot(),
@@ -24,7 +31,11 @@ export default OsfAdapter.extend({
}
// Needed for Ember Data to update the inverse record's (the node's) relationship
- var requestUrl = `${base}?embed=node`;
+ if (type == 'preprint') {
+ requestUrl = `${base}?embed=preprint`;
+ } else {
+ requestUrl = `${base}?embed=node`;
+ }
if (!sendEmail) {
requestUrl += `&send_email=false`;
diff --git a/addon/adapters/preprint.js b/addon/adapters/preprint.js
index a48b1c6ef..807ec17a8 100644
--- a/addon/adapters/preprint.js
+++ b/addon/adapters/preprint.js
@@ -2,15 +2,27 @@ import OsfAdapter from './osf-adapter';
export default OsfAdapter.extend({
// Overrides updateRecord on OsfAdapter. Is identical to JSONAPIAdapter > Update Record (parent's parent method).
- // Updates to preprints do not need special handling.
+ // NOTE: With this implementation,
+ // the app cannot remove a `node` relationship and update other attributes/relationship with one .save() call.
updateRecord(store, type, snapshot) {
- var data = {};
- var serializer = store.serializerFor(type.modelName);
+ let data = {};
+ let url = null;
- serializer.serializeIntoHash(data, type, snapshot, { includeId: true });
-
- var id = snapshot.id;
- var url = this.buildURL(type.modelName, id, snapshot, 'updateRecord');
+ if (snapshot.record.get('_dirtyRelationships')['node'] && snapshot.record.get('_dirtyRelationships')['node']['remove'].length && !snapshot.record.get('_dirtyRelationships')['node']['add'].length) {
+ // Supplemental project has been selected for removal.
+ // Send request to relationship link to remove node
+ url = this._buildRelationshipURL(snapshot, 'node');
+ data = {
+ 'data': null
+ };
+ } else {
+ // Preprint attributes and/or relationships have been modified.
+ // Send patch request to preprint detail link
+ const serializer = store.serializerFor(type.modelName);
+ serializer.serializeIntoHash(data, type, snapshot, { includeId: true });
+ const id = snapshot.id;
+ url = this.buildURL(type.modelName, id, snapshot, 'updateRecord');
+ }
return this.ajax(url, 'PATCH', { data: data });
}
diff --git a/addon/components/file-renderer/component.js b/addon/components/file-renderer/component.js
index 2630560d7..023fd21e6 100644
--- a/addon/components/file-renderer/component.js
+++ b/addon/components/file-renderer/component.js
@@ -27,7 +27,12 @@ export default Ember.Component.extend({
allowfullscreen: true,
version: null,
mfrUrl: Ember.computed('download', 'version', function() {
- let download = this.get('download') + '?direct&mode=render';
+ let download = this.get('download');
+ if (download.includes('?')) {
+ download = download + '&mode=render';
+ } else {
+ download = download + '?direct&mode=render';
+ }
if (this.get('version')) {
download += '&version=' + this.get('version');
}
diff --git a/addon/components/old-file-browser/template.hbs b/addon/components/old-file-browser/template.hbs
index 6408b2bf5..9557845c4 100644
--- a/addon/components/old-file-browser/template.hbs
+++ b/addon/components/old-file-browser/template.hbs
@@ -14,19 +14,24 @@
{{/if}}
- {{#ember-collection
- items=items
- cell-layout=(fixed-grid-layout itemWidth itemHeight)
- as |item|
- }}
- {{old-file-browser-item
- item=item
- navigateToItem=(action 'navigateToItem')
- selectItem=(action 'selectItem')
- openItem=(action 'openItem')
- }}
- {{/ember-collection}}
- {{#unless itemsLoaded}}
- {{fa-icon 'spinner' size=3 pulse=true}}
- {{/unless}}
+ {{#if itemsLoaded}}
+ {{#if items}}
+ {{#ember-collection
+ items=items
+ cell-layout=(fixed-grid-layout itemWidth itemHeight)
+ as |item|
+ }}
+ {{old-file-browser-item
+ item=item
+ navigateToItem=(action 'navigateToItem')
+ selectItem=(action 'selectItem')
+ openItem=(action 'openItem')
+ }}
+ {{/ember-collection}}
+ {{else}}
+
No files found
+ {{/if}}
+ {{else}}
+
{{fa-icon 'spinner' size=3 pulse=true}}
+ {{/if}}
diff --git a/addon/locales/en/translations.js b/addon/locales/en/translations.js
index 6414b40c5..ad65f3abe 100644
--- a/addon/locales/en/translations.js
+++ b/addon/locales/en/translations.js
@@ -39,6 +39,12 @@ export default {
singular: 'thesis',
singularCapitalized: 'Thesis',
},
+ supplementalProject: {
+ plural: 'supplemental projects',
+ pluralCapitalized: 'Supplemental Projects',
+ singular: 'supplemental project',
+ singularCapitalized: 'Supplemental Project',
+ }
},
eosf: {
authDropdown: {
diff --git a/addon/mixins/contributor-mixin.js b/addon/mixins/contributor-mixin.js
new file mode 100644
index 000000000..b7b62577b
--- /dev/null
+++ b/addon/mixins/contributor-mixin.js
@@ -0,0 +1,146 @@
+import Ember from 'ember';
+
+export default Ember.Mixin.create({
+ /**
+ * Determine whether the specified user ID is a contributor on this node
+ * @method isContributor
+ * @param {String} userId
+ * @return {boolean} Whether the specified user is a contributor on this node
+ */
+ isContributor(userId) {
+ // Return true if there is at least one matching contributor for this user ID
+ if (!userId) {
+ return new Ember.RSVP.Promise((resolve) => resolve(false));
+ }
+ const contribId = `${this.get('id')}-${userId}`;
+ return this.store.findRecord('contributor', contribId).then(() => true, () => false);
+ },
+
+ save() {
+ // Some duplicate logic from osf-model#save needed to support
+ // contributor edits being saved through the node
+ // Note: order is important here so _dirtyRelationships gets set by the _super call
+ const promise = this._super(...arguments);
+ if (!this.get('_dirtyRelationships.contributors')) {
+ this.set('_dirtyRelationships.contributors', {});
+ }
+
+ const contributors = this.hasMany('contributors').hasManyRelationship;
+ this.set(
+ '_dirtyRelationships.contributors.update',
+ contributors.members.list.filter(m => !m.getRecord().get('isNew') && Object.keys(m.getRecord().changedAttributes()).length > 0)
+ );
+ // Need to included created contributors even in relationship
+ // hasLoaded is false
+ this.set(
+ '_dirtyRelationships.contributors.create',
+ contributors.members.list.filter(m => m.getRecord().get('isNew'))
+ );
+ // Contributors are a 'real' delete, not just a de-reference
+ this.set(
+ '_dirtyRelationships.contributors.delete',
+ this.get('_dirtyRelationships.contributors.remove') || []
+ );
+ this.set('_dirtyRelationships.contributors.remove', []);
+ return promise;
+ },
+ addContributor(userId, permission, isBibliographic, sendEmail, fullName, email) {
+ const contrib = this.store.createRecord('contributor', {
+ permission: permission,
+ bibliographic: isBibliographic,
+ sendEmail: sendEmail,
+ nodeId: this.get('id'),
+ userId: userId,
+ fullName: fullName,
+ email: email
+ });
+
+ return contrib.save();
+ },
+
+ addContributors(contributors, sendEmail) {
+ const payload = contributors.map(contrib => {
+ const contribData = {
+ permission: contrib.permission,
+ bibliographic: contrib.bibliographic,
+ nodeId: this.get('id'),
+ userId: contrib.userId,
+ id: this.get('id') + '-' + contrib.userId,
+ };
+ if (contrib.unregisteredContributor) {
+ contribData.fullName = contrib.unregisteredContributor;
+ }
+ const c = this.store.createRecord('contributor', contribData);
+
+ return c.serialize({
+ includeId: true,
+ includeUser: true
+ }).data;
+ });
+
+ let emailQuery = '';
+ if (!sendEmail) {
+ emailQuery = '?send_email=false';
+ } else if (sendEmail === 'preprint') {
+ emailQuery = '?send_email=preprint';
+ }
+
+ // TODO Get this working properly - should not be an ajax request in the future.
+ return this.store.adapterFor('contributor').ajax(this.get('links.relationships.contributors.links.related.href') + emailQuery, 'POST', {
+ data: {
+ data: payload
+ },
+ isBulk: true
+ }).then(resp => {
+ this.store.pushPayload(resp);
+ const createdContribs = Ember.A();
+ resp.data.map((contrib) => {
+ createdContribs.push(this.store.peekRecord('contributor', contrib.id));
+ });
+ return createdContribs;
+ });
+ },
+
+ removeContributor(contributor) {
+ return contributor.destroyRecord().then(rec => {
+ this.get('store')._removeFromIdMap(rec._internalModel);
+ });
+ },
+
+ updateContributor(contributor, permissions, bibliographic) {
+ if (!Ember.isEmpty(permissions))
+ contributor.set('permission', permissions);
+ if (!Ember.isEmpty(bibliographic))
+ contributor.set('bibliographic', bibliographic);
+ return contributor.save();
+ },
+
+ updateContributors(contributors, permissionsChanges, bibliographicChanges) {
+ let payload = contributors
+ .filter(contrib => contrib.id in permissionsChanges || contrib.id in bibliographicChanges)
+ .map(contrib => {
+ if (contrib.id in permissionsChanges) {
+ contrib.set('permission', permissionsChanges[contrib.id]);
+ }
+
+ if (contrib.id in bibliographicChanges) {
+ contrib.set('bibliographic', bibliographicChanges[contrib.id]);
+ }
+
+ return contrib.serialize({
+ includeId: true,
+ includeUser: false
+ }).data;
+ });
+
+ return this.store.adapterFor('contributor').ajax(this.get('links.relationships.contributors.links.related.href'), 'PATCH', {
+ data: {
+ data: payload
+ },
+ isBulk: true
+ }).then(resp => {
+ this.store.pushPayload(resp);
+ return contributors;
+ });
+ }
+});
diff --git a/addon/models/contributor.js b/addon/models/contributor.js
index d836c28bd..490d17ab5 100644
--- a/addon/models/contributor.js
+++ b/addon/models/contributor.js
@@ -54,5 +54,8 @@ export default OsfModel.extend({
node: DS.belongsTo('node', {
inverse: 'contributors'
+ }),
+ preprint: DS.belongsTo('preprint', {
+ inverse: 'contributors'
})
});
diff --git a/addon/models/node.js b/addon/models/node.js
index a4888c360..382e1ae08 100644
--- a/addon/models/node.js
+++ b/addon/models/node.js
@@ -3,7 +3,7 @@ import DS from 'ember-data';
import OsfModel from './osf-model';
import FileItemMixin from 'ember-osf/mixins/file-item';
-
+import ContributorMixin from 'ember-osf/mixins/contributor-mixin';
/**
* @module ember-osf
* @submodule models
@@ -21,7 +21,7 @@ import FileItemMixin from 'ember-osf/mixins/file-item';
* * https://api.osf.io/v2/docs/#!/v2/User_Nodes_GET
* @class Node
*/
-export default OsfModel.extend(FileItemMixin, {
+export default OsfModel.extend(FileItemMixin, ContributorMixin, {
isNode: true,
title: DS.attr('fixstring'),
@@ -123,50 +123,6 @@ export default OsfModel.extend(FileItemMixin, {
*/
isAnonymous: Ember.computed.bool('meta.anonymous'),
- /**
- * Determine whether the specified user ID is a contributor on this node
- * @method isContributor
- * @param {String} userId
- * @return {boolean} Whether the specified user is a contributor on this node
- */
- isContributor(userId) {
- // Return true if there is at least one matching contributor for this user ID
- if (!userId) {
- return new Ember.RSVP.Promise((resolve) => resolve(false));
- }
- var contribId = `${this.get('id')}-${userId}`;
- return this.store.findRecord('contributor', contribId).then(() => true, () => false);
- },
-
- save() {
- // Some duplicate logic from osf-model#save needed to support
- // contributor edits being saved through the node
- // Note: order is important here so _dirtyRelationships gets set by the _super call
- var promise = this._super(...arguments);
- if (!this.get('_dirtyRelationships.contributors')) {
- this.set('_dirtyRelationships.contributors', {});
- }
-
- var contributors = this.hasMany('contributors').hasManyRelationship;
- this.set(
- '_dirtyRelationships.contributors.update',
- contributors.members.list.filter(m => !m.getRecord().get('isNew') && Object.keys(m.getRecord().changedAttributes()).length > 0)
- );
- // Need to included created contributors even in relationship
- // hasLoaded is false
- this.set(
- '_dirtyRelationships.contributors.create',
- contributors.members.list.filter(m => m.getRecord().get('isNew'))
- );
- // Contributors are a 'real' delete, not just a de-reference
- this.set(
- '_dirtyRelationships.contributors.delete',
- this.get('_dirtyRelationships.contributors.remove') || []
- );
- this.set('_dirtyRelationships.contributors.remove', []);
- return promise;
- },
-
addChild(title, description=null, category='project', isPublic) {
let child = this.store.createRecord('node', {
title,
@@ -178,98 +134,4 @@ export default OsfModel.extend(FileItemMixin, {
return child.save();
},
-
- addContributor(userId, permission, isBibliographic, sendEmail, fullName, email) {
- let contrib = this.store.createRecord('contributor', {
- permission: permission,
- bibliographic: isBibliographic,
- sendEmail: sendEmail,
- nodeId: this.get('id'),
- userId: userId,
- fullName: fullName,
- email: email
- });
-
- return contrib.save();
- },
-
- addContributors(contributors, sendEmail) {
- let payload = contributors.map(contrib => {
- let c = this.store.createRecord('contributor', {
- permission: contrib.permission,
- bibliographic: contrib.bibliographic,
- nodeId: this.get('id'),
- userId: contrib.userId,
- id: this.get('id') + '-' + contrib.userId,
- unregisteredContributor: null
- });
- return c.serialize({
- includeId: true,
- includeUser: true
- }).data;
- });
-
- let emailQuery = '';
- if (!sendEmail) {
- emailQuery = '?send_email=false';
- } else if (sendEmail === 'preprint') {
- emailQuery = '?send_email=preprint';
- }
-
- // TODO Get this working properly - should not be an ajax request in the future.
- return this.store.adapterFor('contributor').ajax(this.get('links.relationships.contributors.links.related.href') + emailQuery, 'POST', {
- data: {
- data: payload
- },
- isBulk: true
- }).then(resp => {
- this.store.pushPayload(resp);
- var createdContribs = Ember.A();
- resp.data.map((contrib) => {
- createdContribs.push(this.store.peekRecord('contributor', contrib.id));
- });
- return createdContribs;
- });
- },
-
- removeContributor(contributor) {
- return contributor.destroyRecord();
- },
-
- updateContributor(contributor, permissions, bibliographic) {
- if (!Ember.isEmpty(permissions))
- contributor.set('permission', permissions);
- if (!Ember.isEmpty(bibliographic))
- contributor.set('bibliographic', bibliographic);
- return contributor.save();
- },
-
- updateContributors(contributors, permissionsChanges, bibliographicChanges) {
- let payload = contributors
- .filter(contrib => contrib.id in permissionsChanges || contrib.id in bibliographicChanges)
- .map(contrib => {
- if (contrib.id in permissionsChanges) {
- contrib.set('permission', permissionsChanges[contrib.id]);
- }
-
- if (contrib.id in bibliographicChanges) {
- contrib.set('bibliographic', bibliographicChanges[contrib.id]);
- }
-
- return contrib.serialize({
- includeId: true,
- includeUser: false
- }).data;
- });
-
- return this.store.adapterFor('contributor').ajax(this.get('links.relationships.contributors.links.related.href'), 'PATCH', {
- data: {
- data: payload
- },
- isBulk: true
- }).then(resp => {
- this.store.pushPayload(resp);
- return contributors;
- });
- }
});
diff --git a/addon/models/preprint.js b/addon/models/preprint.js
index f28c995ba..cd3ddd0f4 100644
--- a/addon/models/preprint.js
+++ b/addon/models/preprint.js
@@ -1,6 +1,7 @@
import Ember from 'ember';
import DS from 'ember-data';
import OsfModel from './osf-model';
+import ContributorMixin from 'ember-osf/mixins/contributor-mixin';
/**
* @module ember-osf
@@ -16,7 +17,7 @@ import OsfModel from './osf-model';
* https://api.osf.io/v2/docs/#!/v2/User_Preprints_GET
* @class Preprint
*/
-export default OsfModel.extend({
+export default OsfModel.extend(ContributorMixin, {
title: DS.attr('fixstring'),
// TODO: May be a relationship in the future pending APIv2 changes
@@ -34,6 +35,9 @@ export default OsfModel.extend({
preprintDoiCreated: DS.attr('date'),
description: DS.attr('fixstring'),
tags: DS.attr(),
+ public: DS.attr('boolean'),
+ // List of strings
+ currentUserPermissions: DS.attr(),
dateWithdrawn: DS.attr('date'),
withdrawalJustification: DS.attr('fixstring'),
@@ -42,9 +46,13 @@ export default OsfModel.extend({
license: DS.belongsTo('license', { inverse: null }),
primaryFile: DS.belongsTo('file', { inverse: null }),
provider: DS.belongsTo('preprint-provider', { inverse: 'preprints', async: true }),
+ files: DS.hasMany('file-provider'),
reviewActions: DS.hasMany('review-action', { inverse: 'target', async: true }),
- contributors: DS.hasMany('contributors', { async: true }),
-
+ contributors: DS.hasMany('contributors', {
+ allowBulkUpdate: true,
+ allowBulkRemove: true,
+ inverse: 'preprint'
+ }),
uniqueSubjects: Ember.computed('subjects', function() {
if (!this.get('subjects')) return [];
return this.get('subjects').reduce((acc, val) => acc.concat(val), []).uniqBy('id');
diff --git a/package.json b/package.json
index c46425b2f..7d0146cc4 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@centerforopenscience/ember-osf",
- "version": "0.20.1",
+ "version": "0.21.0",
"description": "Reusable ember models and components for interacting with the Open Science Framework",
"directories": {
"doc": "docs",
diff --git a/tests/unit/mixins/contributor-mixin-test.js b/tests/unit/mixins/contributor-mixin-test.js
new file mode 100644
index 000000000..b4238efa7
--- /dev/null
+++ b/tests/unit/mixins/contributor-mixin-test.js
@@ -0,0 +1,11 @@
+import Ember from 'ember';
+import ContributorMixinMixin from 'ember-osf/mixins/contributor-mixin';
+import { module, test } from 'qunit';
+
+module('Unit | Mixin | contributor mixin');
+
+test('it works', function(assert) {
+ const ContributorMixinObject = Ember.Object.extend(ContributorMixinMixin);
+ const subject = ContributorMixinObject.create();
+ assert.ok(subject);
+});