diff --git a/.editorconfig b/.editorconfig index 9a13d9b1e..3ef6636ee 100644 --- a/.editorconfig +++ b/.editorconfig @@ -20,15 +20,15 @@ indent_size = 4 [*.hbs] insert_final_newline = false indent_style = space -indent_size = 2 +indent_size = 4 [*.css] indent_style = space -indent_size = 2 +indent_size = 4 [*.html] indent_style = space -indent_size = 2 +indent_size = 4 [*.{diff,md}] trim_trailing_whitespace = false diff --git a/addon/adapters/application.js b/addon/adapters/application.js index c16a9618f..cff7dd94b 100644 --- a/addon/adapters/application.js +++ b/addon/adapters/application.js @@ -1,17 +1,22 @@ import Ember from 'ember'; import DS from 'ember-data'; import DataAdapterMixin from 'ember-simple-auth/mixins/data-adapter-mixin'; -import UrlTemplates from "ember-data-url-templates"; import config from 'ember-get-config'; -export default DS.JSONAPIAdapter.extend(UrlTemplates, DataAdapterMixin, { +export default DS.JSONAPIAdapter.extend(DataAdapterMixin, { authorizer: 'authorizer:osf-token', host: config.OSF.apiUrl, - urlTemplate: '{+host}{modelName}{/id}/', - urlSegments: { - modelName (modelName) { - return Ember.String.pluralize(modelName); - } + namespace: config.OSF.apiNamespace, + pathForType: Ember.String.pluralize, + + buildURL(modelName, id, snapshot, requestType, query) { // jshint ignore:line + var url = this._super(...arguments); + // Fix issue where CORS request failed on 301s: Ember does not seem to append trailing + // slash to URLs for single documents, but DRF redirects to force a trailing slash + if (url.lastIndexOf('/') !== 0) { + url += '/'; + } + return url; } }); diff --git a/addon/authenticators/osf-token.js b/addon/authenticators/osf-token.js index 470c8730d..9727ff8a4 100644 --- a/addon/authenticators/osf-token.js +++ b/addon/authenticators/osf-token.js @@ -9,7 +9,7 @@ export default BaseAuthenticator.extend({ _test (accessToken) { return Ember.$.ajax({ method: 'GET', - url: `${config.OSF.apiUrl}users/me/`, + url: `${config.OSF.apiUrl}/${config.OSF.apiNamespace}/users/me/`, dataType: 'json', contentType: 'application/json', xhrFields: {withCredentials: false}, diff --git a/addon/mixins/osf-login-route.js b/addon/mixins/osf-login-route.js index 6fed1cd3f..f8ed29827 100644 --- a/addon/mixins/osf-login-route.js +++ b/addon/mixins/osf-login-route.js @@ -10,8 +10,9 @@ export default Ember.Mixin.create(UnauthenticatedRouteMixin, { if (config.OSF.isLocal) { accessToken = config.OSF.accessToken; } else { - // Acquire an OSF access token, then exchange it for a Jam token + // Acquire an OSF access token, then exchange it for a Jam token // TODO: Jam? var hash = window.location.hash.substring(1).split('&').map(function(str) { + // TODO: Comma expression; check with Sam on intent return this[str.split('=')[0]] = str.split('=')[1], this; }.bind({}))[0]; if (!hash || !hash.access_token) { diff --git a/addon/models/node.js b/addon/models/node.js index 5985e35d4..df7e0778e 100644 --- a/addon/models/node.js +++ b/addon/models/node.js @@ -34,7 +34,7 @@ export default OsfModel.extend({ //forkedFrom: DS.belongsTo('node'), //nodeLinks: DS.hasMany('node-pointers'), //registrations: DS.hasMany('registrations'), - //primaryInistution: DS.belongsTo('institution'), + //primaryInstitution: DS.belongsTo('institution'), root: DS.belongsTo('node') //logs: DS.hasMany('node-logs'), }); diff --git a/addon/serializers/application.js b/addon/serializers/application.js index e1fa924c2..89ab966f3 100644 --- a/addon/serializers/application.js +++ b/addon/serializers/application.js @@ -2,39 +2,41 @@ import Ember from 'ember'; import DS from 'ember-data'; export default DS.JSONAPISerializer.extend({ - _normalizeAttributes(attributes) { - var normalized = {}; - Object.keys(attributes).forEach(function(key) { - normalized[Ember.String.camelize(key)] = attributes[key]; - }); - return normalized; + + // TODO: Pre-1.0, refactor this into a separate OSF serializer, so we can support other microservices such as WB + attrs: { + links: {serialize: false}, + embeds: {serialize: false} }, - _normalizeRecord(record) { - record.attributes = this._normalizeAttributes(record.attributes); - if (record.links) { - record.attributes.links = record.links; - } - if (record.embeds) { - // TODO, actually merge in embedded data? - record.attributes.embeds = record.embeds; - } - return record; - }, - normalizeSingleResponse(_, __, payload) { - payload.data = this._normalizeRecord(payload.data); - return this._super(...arguments); + + _mergeFields(resourceHash) { + // ApiV2 `links` exist outside the attributes field; make them accessible to the data model + if (resourceHash.links) { // TODO: Should also test whether model class defines a links field + resourceHash.attributes.links = resourceHash.links; + } + if (resourceHash.embeds) { + resourceHash.attributes.embeds = resourceHash.embeds; + } + return resourceHash; }, - normalizeArrayResponse(_, __, payload) { - payload.data = payload.data.map(this._normalizeRecord.bind(this)); - return this._super(...arguments); + + extractAttributes(modelClass, resourceHash) { + resourceHash = this._mergeFields(resourceHash); + return this._super(modelClass, resourceHash); }, - keyForAttribute(key) { - return Ember.String.camelize(key); + + keyForAttribute(key, method) { + if (method === 'deserialize') { + return Ember.String.underscore(key); + } else if (method === 'serialize') { + return Ember.String.camelize(key); + } }, - serializeIntoHash(/*hash, typeClass, snapshot, options*/) { - // Don't send links as part of hash - // TODO - return this._super(...arguments); + serialize: function(snapshot, options) { + var serialized = this._super(snapshot, options); + // Don't send relationships to the server; this can lead to 500 errors. + delete serialized.data.relationships; + return serialized; } }); diff --git a/index.js b/index.js index f6aaf9f38..ef70fa2ae 100644 --- a/index.js +++ b/index.js @@ -5,55 +5,46 @@ var config = require('config'); module.exports = { name: 'ember-osf', config: function(environment, ENV) { - let BACKEND = process.env.BACKEND || 'local'; - let SETTINGS = {}; - try { - SETTINGS = config.get(BACKEND); - } - catch (e) { - console.log(`WARNING: you have specified a backend '${BACKEND}' that you have not configured in your config/.yml`); - } + let BACKEND = process.env.BACKEND || 'local'; + let SETTINGS = {}; + try { + SETTINGS = config.get(BACKEND); + } + catch (e) { + console.log(`WARNING: you have specified a backend '${BACKEND}' that you have not configured in your config/.yml`); + } - ENV.OSF = { - clientId: SETTINGS.CLIENT_ID, - scope: SETTINGS.OAUTH_SCOPES + ENV.OSF = { + clientId: SETTINGS.CLIENT_ID, + scope: SETTINGS.OAUTH_SCOPES, + apiNamespace: 'v2' // URL suffix (after host) }; - if (BACKEND === 'local') { - ENV.OSF.url = 'http://localhost:5000/'; - ENV.OSF.apiUrl = 'http://localhost:8000/v2/'; - ENV.OSF.authUrl = 'http://localhost:8080/oauth2/profile'; + if (BACKEND === 'local') { + ENV.OSF.url = 'http://localhost:5000/'; + ENV.OSF.apiUrl = 'http://localhost:8000'; + ENV.OSF.authUrl = 'http://localhost:8080/oauth2/profile'; - ENV.OSF.accessToken = SETTINGS.PERSONAL_ACCESS_TOKEN; - ENV.OSF.isLocal = true; - } - if (BACKEND === 'stage') { - ENV.OSF.url = 'https://staging.osf.io/'; - ENV.OSF.apiUrl = 'https://staging-api.osf.io/v2/'; - ENV.OSF.authUrl = 'https://staging-accounts.osf.io/oauth2/authorize'; - } - if (BACKEND === 'stage2') { - ENV.OSF.url = 'https://staging2.osf.io/'; - ENV.OSF.apiUrl = 'https://staging2-api.osf.io/v2/'; - ENV.OSF.authUrl = 'https://staging2-accounts.osf.io/oauth2/authorize'; - } - if (BACKEND === 'prod') { - ENV.OSF.url = 'https://osf.io/'; - ENV.OSF.apiUrl = 'https://api.osf.io/v2/'; - ENV.OSF.authUrl = 'https://accounts.osf.io/oauth2/authorize'; - } - - ENV['ember-simple-auth'] = { - authorizer: 'authorizer:osf-token' + ENV.OSF.accessToken = SETTINGS.PERSONAL_ACCESS_TOKEN; + ENV.OSF.isLocal = true; + } + if (BACKEND === 'stage') { + ENV.OSF.url = 'https://staging.osf.io/'; + ENV.OSF.apiUrl = 'https://staging-api.osf.io'; + ENV.OSF.authUrl = 'https://staging-accounts.osf.io/oauth2/authorize'; + } + if (BACKEND === 'stage2') { + ENV.OSF.url = 'https://staging2.osf.io/'; + ENV.OSF.apiUrl = 'https://staging2-api.osf.io'; + ENV.OSF.authUrl = 'https://staging2-accounts.osf.io/oauth2/authorize'; + } + if (BACKEND === 'prod') { + ENV.OSF.url = 'https://osf.io/'; + ENV.OSF.apiUrl = 'https://api.osf.io'; + ENV.OSF.authUrl = 'https://accounts.osf.io/oauth2/authorize'; + } + ENV['ember-simple-auth'] = { + authorizer: 'authorizer:osf-token' }; - }, - // A small hack for ember-data-url-templates to work - // TODO: followup based on https://github.com/dfreeman/ember-cli-node-assets/issues/1 - options: { - nodeAssets: { - 'uri-templates': { - import: ['uri-templates.js'] - } - } } }; diff --git a/package.json b/package.json index 158f93502..07ab56a32 100644 --- a/package.json +++ b/package.json @@ -55,12 +55,10 @@ "ember-cli-babel": "^5.1.5", "ember-cli-moment-shim": "1.1.0", "ember-cli-node-assets": "^0.1.3", - "ember-data-url-templates": "^0.1.1", "ember-get-config": "0.0.2", "ember-moment": "6.1.0", "ember-simple-auth": "1.1.0-beta.5", - "js-yaml": "^3.6.0", - "uri-templates": "^0.1.9" + "js-yaml": "^3.6.0" }, "ember-addon": { "configPath": "tests/dummy/config" diff --git a/tests/dummy/app/router.js b/tests/dummy/app/router.js index 5a1c57bf8..dc7d751eb 100644 --- a/tests/dummy/app/router.js +++ b/tests/dummy/app/router.js @@ -8,7 +8,7 @@ const Router = Ember.Router.extend({ Router.map(function() { this.route('index', {path: '/'}); this.route('nodes', function() { - this.route('detail', {path: '/:node_id'}); + this.route('detail', {path: '/:node_id'}); }); this.route('login'); }); diff --git a/tests/dummy/app/routes/nodes.js b/tests/dummy/app/routes/nodes.js index 67d18dcbc..0a3f1c5ef 100644 --- a/tests/dummy/app/routes/nodes.js +++ b/tests/dummy/app/routes/nodes.js @@ -6,11 +6,19 @@ export default Ember.Route.extend(AuthenticatedRouteMixin, { session: Ember.inject.service(), model() { let user = this.modelFor('application'); - if(user) { - return user.get('nodes'); - } - else { - return this.get('store').findRecord('user', 'me').then(user => user.get('nodes')); - } + if (user) { + return user.get('nodes'); // Fetch from `/users/me/nodes/` + } + else { + return this.get('store').findRecord('user', 'me').then(user => user.get('nodes')); + } + }, + actions: { + createNew() { + // TODO: Just hardcode a payload here, tests POST + console.log('button was clicked'); + //var record = this.store.createRecord('node', {}); // TODO write + //record.save(); + } } }); diff --git a/tests/dummy/app/routes/nodes/detail.js b/tests/dummy/app/routes/nodes/detail.js index b711cbe5f..a90314bff 100644 --- a/tests/dummy/app/routes/nodes/detail.js +++ b/tests/dummy/app/routes/nodes/detail.js @@ -2,6 +2,26 @@ import Ember from 'ember'; export default Ember.Route.extend({ model(params) { - return this.store.findRecord('node', params.node_id); + return this.store.findRecord('node', params.node_id); + }, + + setupController(controller, model) { + controller.set('editedTitle', model.get('title')); + this._super(...arguments); + }, + + actions: { + editExisting(value) { + // TODO: Should test PUT or PATCH + console.log('Will edit title from', this.modelFor(this.routeName).get('title'), ' to ', value); + var node = this.modelFor(this.routeName); + if (node.get('currentUserPermissions').indexOf('write') !== -1) { + node.set('title', value); + node.save(); + } else { + console.log('You do not have permissions to edit this node'); + } + } } + }); diff --git a/tests/dummy/app/templates/application.hbs b/tests/dummy/app/templates/application.hbs index 979afbede..5d6d4df6d 100644 --- a/tests/dummy/app/templates/application.hbs +++ b/tests/dummy/app/templates/application.hbs @@ -1,6 +1,6 @@
{{link-to 'Home' 'index' class="btn btn-default"}} -

Welcome to OSF Ember Example App

+

Welcome to OSF Ember Example App

{{outlet}} diff --git a/tests/dummy/app/templates/nodes/detail.hbs b/tests/dummy/app/templates/nodes/detail.hbs index 914145e0f..05bba69c0 100644 --- a/tests/dummy/app/templates/nodes/detail.hbs +++ b/tests/dummy/app/templates/nodes/detail.hbs @@ -1,26 +1,38 @@ {{link-to 'Back to list' 'nodes' class="btn btn-default"}}
-

{{model.title}}

-

{{model.category}}

-

{{moment-format model.dateCreated}}

-

{{moment-format model.dateModified}}

+ +

Data

+

{{model.title}}

+

{{model.category}}

+

{{moment-format model.dateCreated}}

+

{{moment-format model.dateModified}}

View on OSF


-

- -

+ +

Contributors

+ - {{#each model.contributors as |contrib|}} - - {{contrib.bibliographic}} - - {{/each}} + + + + + {{#each model.contributors as |contrib|}} + + + + + {{/each}}
IDAuthor?
{{contrib.id}}{{contrib.bibliographic}}
-

-

+ + + +

Edit this node

+ Title: {{input value=editedTitle}} + +
diff --git a/tests/dummy/app/templates/nodes/index.hbs b/tests/dummy/app/templates/nodes/index.hbs index 6058e0729..3926175a0 100644 --- a/tests/dummy/app/templates/nodes/index.hbs +++ b/tests/dummy/app/templates/nodes/index.hbs @@ -1,16 +1,20 @@

My Nodes

+ + + + {{#each model as |node|}} -
-
-

{{node.title}}

-

{{node.category}}

-

{{moment-format node.dateCreated}}

-

{{moment-format node.dateModified}}

-

- - View on OSF - - {{link-to 'Detail' 'nodes.detail' node.id class="btn btn-primary"}} -

-
+
+
+

{{node.title}}

+

{{node.category}}

+

{{moment-format node.dateCreated}}

+

{{moment-format node.dateModified}}

+

+ + View on OSF + + {{link-to 'Detail' 'nodes.detail' node.id class="btn btn-primary"}} +

+
{{/each}} diff --git a/tests/unit/serializers/application-test.js b/tests/unit/serializers/application-test.js index ae5fc74a9..77ab6a77c 100644 --- a/tests/unit/serializers/application-test.js +++ b/tests/unit/serializers/application-test.js @@ -5,38 +5,35 @@ moduleForModel('base', 'Unit | Serializer | application', { needs: ['serializer:application'] }); -test('#_normalizeRecord adds links to attributes if included in payload', function(assert) { +test('#_mergeFields adds links to attributes if included in payload', function(assert) { let payload = { - id: faker.random.uuid(), - type: 'base', - attributes: { - key: 'value' - }, - links: { - html: faker.internet.url() - } + id: faker.random.uuid(), + type: 'base', + attributes: { + key: 'value' + }, + links: { + html: faker.internet.url() + } }; - let serializer = this.container.lookup('serializer:application'); - let normalized = serializer._normalizeRecord(payload); - + let normalized = serializer._mergeFields(payload); assert.equal(normalized.attributes.links, payload.links); }); -test('#_normalizeRecord adds links to attributes if included in payload', function(assert) { +test('#_mergeFields adds embeds to attributes if included in payload', function(assert) { let payload = { - id: faker.random.uuid(), - attributes: { - key: 'value' - }, - embeds: { - embedded: { - data: [faker.random.arrayElement()] - } - } + id: faker.random.uuid(), + attributes: { + key: 'value' + }, + embeds: { + embedded: { + data: [faker.random.arrayElement()] + } + } }; - let serializer = this.container.lookup('serializer:application'); - let normalized = serializer._normalizeRecord(payload); + let normalized = serializer._mergeFields(payload); assert.equal(normalized.attributes.embeds, payload.embeds); });