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); +});