diff --git a/.ember-cli b/.ember-cli index 59bb55fe9..1d7c4c2a8 100644 --- a/.ember-cli +++ b/.ember-cli @@ -5,5 +5,8 @@ Setting `disableAnalytics` to true will prevent any data from being sent. */ - "disableAnalytics": true + "disableAnalytics": true, + "port": 4200, + + "liveReloadPort": 41953 /* Needed for the TouchBar on the new MacBook Pros */ } diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 000000000..94be88a9f --- /dev/null +++ b/.eslintignore @@ -0,0 +1,4 @@ +coverage +dist +docs +tmp diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 000000000..76865be8a --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,16 @@ +module.exports = { + root: true, + parserOptions: { + ecmaVersion: 6, + sourceType: 'module' + }, + extends: 'eslint:recommended', + env: { + browser: true, + es6: true + }, + globals: { + MathJax: true + }, + rules: {} +}; diff --git a/.jshintrc b/.jshintrc deleted file mode 100644 index d7d7a877e..000000000 --- a/.jshintrc +++ /dev/null @@ -1,33 +0,0 @@ -{ - "predef": [ - "server", - "document", - "window", - "Promise" - ], - "browser": true, - "boss": true, - "curly": false, - "debug": false, - "devel": true, - "eqeqeq": true, - "evil": true, - "forin": false, - "immed": false, - "laxbreak": false, - "newcap": true, - "noarg": true, - "noempty": false, - "nonew": false, - "nomen": false, - "onevar": false, - "plusplus": false, - "regexp": false, - "undef": true, - "sub": true, - "strict": false, - "white": false, - "eqnull": true, - "esversion": 6, - "unused": true -} diff --git a/README.md b/README.md index d93e384c9..67dda06f8 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,10 @@ For local development, this is designed to run alongside (and from within) the f to your `website/settings/local.py` file. Uncomment `'/preprints/': 'http://localhost:4200',` and restart your flask app. 4. Visit your app at http://localhost:5000/preprints/ +### Provider Domains +1. Run `sudo ./scripts/add-domains.js`. This will add the domains to your `/etc/hosts`. +2. Visit your app at one of the provider domains with `https://local.:4200` (e.g. `http://local.socarxiv.org:4200`) + If you encounter problems, make sure that your version of ember-osf is up to date. If login fails, try logging in from any other OSF page, then returning to the preprints app. diff --git a/app/components/error-page.js b/app/components/error-page.js new file mode 100644 index 000000000..1701aeb0f --- /dev/null +++ b/app/components/error-page.js @@ -0,0 +1,12 @@ +import Ember from 'ember'; +import Analytics from '../mixins/analytics'; + +export default Ember.Component.extend(Analytics, { + theme: Ember.inject.service(), + classNames: ['preprint-error-page'], + label: '', + translationKey: '', + supportEmail: Ember.computed('theme.isProvider', 'theme.provider.emailSupport', function() { + return this.get('theme.isProvider') ? this.get('theme.provider.emailSupport') : 'support@osf.io'; + }) +}); diff --git a/app/components/preprint-form-authors.js b/app/components/preprint-form-authors.js index ce81f9204..84d55b1d2 100644 --- a/app/components/preprint-form-authors.js +++ b/app/components/preprint-form-authors.js @@ -36,6 +36,7 @@ import Analytics from '../mixins/analytics'; * @class preprint-form-authors */ export default CpPanelBodyComponent.extend(Analytics, { + i18n: Ember.inject.service(), valid: Ember.computed.alias('newContributorId'), authorModification: false, currentPage: 1, @@ -94,9 +95,10 @@ export default CpPanelBodyComponent.extend(Analytics, { this.attrs.addContributor(user.id, 'write', true, this.get('sendEmail'), undefined, undefined, true).then((res) => { this.toggleAuthorModification(); this.get('contributors').pushObject(res); + this.get('toast').success(this.get('i18n').t('submit.preprint_author_added')); this.highlightSuccessOrFailure(res.id, this, 'success'); }, () => { - this.get('toast').error('Could not add contributor.'); + this.get('toast').error(this.get('i18n').t('submit.error_adding_author')); this.highlightSuccessOrFailure(user.id, this, 'error'); user.rollbackAttributes(); }); @@ -142,12 +144,13 @@ export default CpPanelBodyComponent.extend(Analytics, { this.set('addState', 'searchView'); this.set('fullName', ''); this.set('email', ''); + this.get('toast').success(this.get('i18n').t('submit.preprint_unregistered_author_added')); this.highlightSuccessOrFailure(contributor.id, this, 'success'); }, (error) => { if (error.errors[0] && error.errors[0].detail && error.errors[0].detail.indexOf('is already a contributor') > -1) { this.get('toast').error(error.errors[0].detail); } else { - this.get('toast').error('Could not add unregistered contributor.'); + this.get('toast').error(this.get('i18n').t('submit.error_adding_unregistered_author')); } this.highlightSuccessOrFailure('add-unregistered-contributor-form', this, 'error'); }); @@ -178,8 +181,9 @@ export default CpPanelBodyComponent.extend(Analytics, { this.toggleAuthorModification(); this.removedSelfAsAdmin(contrib, contrib.get('permission')); this.get('contributors').removeObject(contrib); + this.get('toast').success(this.get('i18n').t('submit.preprint_author_removed')); }, () => { - this.get('toast').error('Could not remove author'); + this.get('toast').error(this.get('i18n').t('submit.error_adding_author')); this.highlightSuccessOrFailure(contrib.id, this, 'error'); contrib.rollbackAttributes(); }); diff --git a/app/components/preprint-navbar-branded.js b/app/components/preprint-navbar-branded.js index 3d46cc726..3a231b0b2 100644 --- a/app/components/preprint-navbar-branded.js +++ b/app/components/preprint-navbar-branded.js @@ -1,6 +1,8 @@ import Ember from 'ember'; import OSFAgnosticAuthControllerMixin from 'ember-osf/mixins/osf-agnostic-auth-controller'; import Analytics from '../mixins/analytics'; +import config from 'ember-get-config'; + /** * @module ember-preprints * @submodule components @@ -22,5 +24,6 @@ export default Ember.Component.extend(OSFAgnosticAuthControllerMixin, Analytics, session: Ember.inject.service(), theme: Ember.inject.service(), tagName: 'nav', - classNames: ['navbar', 'branded-navbar', 'preprint-navbar'] + classNames: ['navbar', 'branded-navbar', 'preprint-navbar'], + host: config.OSF.url, }); diff --git a/app/components/supplementary-file-browser.js b/app/components/supplementary-file-browser.js index a921a90a3..b65e4adac 100644 --- a/app/components/supplementary-file-browser.js +++ b/app/components/supplementary-file-browser.js @@ -56,9 +56,32 @@ export default Ember.Component.extend(Analytics, { this.set('primaryFile', pf); this.set('selectedFile', this.get('primaryFile')); this.set('files', [this.get('primaryFile')].concat(this.get('files'))); + this.set('indexes', this.get('files').map(each => each.id)); }); }.observes('preprint'), - + _chosenFile: Ember.observer('chosenFile', 'indexes', function() { + let fid = this.get('chosenFile'); + let index = this.get('indexes').indexOf(fid); + if (fid && index !== -1) { + this.set('selectedFile', this.get('files')[index]); + } + }), + _moveIfNeeded: Ember.observer('selectedFile', function() { + let index = this.get('files').indexOf(this.get('selectedFile')); + if (index < 0) { + return; + } + if (index >= this.get('endIndex') || index < this.get('startIndex')) { + let max = this.get('files').length - 6; + if (index > max) { + this.set('startIndex', max); + this.set('endIndex', this.get('files').length); + } else { + this.set('startIndex', index); + this.set('endIndex', index + 6); + } + } + }), fileDownloadURL: Ember.computed('selectedFile', function() { return fileDownloadPath(this.get('selectedFile'), this.get('node')); }), @@ -90,12 +113,17 @@ export default Ember.Component.extend(Analytics, { action: 'click', label: 'Preprints - Content - Prev' }); - - if (this.get('startIndex') <= 0) return; + let start = this.get('startIndex'); + if (start <= 0) return; this.set('scrollAnim', `to${direction}`); - this.set('endIndex', this.get('endIndex') - 5); - this.set('startIndex', this.get('startIndex') - 5); + if ((start - 5) < 0) { + this.set('startIndex', 0); + this.set('endIndex', 6); + } else { + this.set('startIndex', start - 5); + this.set('endIndex', this.get('endIndex') - 5); + } }, changeFile(file) { Ember.get(this, 'metrics') @@ -106,7 +134,6 @@ export default Ember.Component.extend(Analytics, { }); this.set('selectedFile', file); - if (this.attrs.chooseFile) { this.sendAction('chooseFile', file); } diff --git a/app/controllers/content.js b/app/controllers/content/index.js similarity index 96% rename from app/controllers/content.js rename to app/controllers/content/index.js index b5e4f1fe1..7071bcf52 100644 --- a/app/controllers/content.js +++ b/app/controllers/content/index.js @@ -1,9 +1,9 @@ import Ember from 'ember'; import loadAll from 'ember-osf/utils/load-relationship'; import config from 'ember-get-config'; -import Analytics from '../mixins/analytics'; +import Analytics from '../../mixins/analytics'; import permissions from 'ember-osf/const/permissions'; -import fileDownloadPath from '../utils/file-download-path'; +import fileDownloadPath from '../../utils/file-download-path'; /** * Takes an object with query parameter name as the key and value, or [value, maxLength] as the values. @@ -57,6 +57,9 @@ export default Ember.Controller.extend(Analytics, { showLicenseText: false, fileDownloadURL: '', expandedAbstract: navigator.userAgent.includes('Prerender'), + queryParams: { + chosenFile: 'file' + }, isAdmin: Ember.computed('node', function() { // True if the current user has admin permissions for the node that contains the preprint return (this.get('node.currentUserPermissions') || []).includes(permissions.ADMIN); @@ -67,7 +70,6 @@ export default Ember.Controller.extend(Analytics, { text: this.get('node.title'), via: 'OSFramework' }; - return `https://twitter.com/intent/tweet?${queryStringify(queryParams)}`; }), /* TODO: Update this with new Facebook Share Dialog, but an App ID is required @@ -105,6 +107,7 @@ export default Ember.Controller.extend(Analytics, { }), // The currently selected file (defaults to primary) activeFile: null, + chosenFile: null, disciplineReduced: Ember.computed('model.subjects', function() { // Preprint disciplines are displayed in collapsed form on content page @@ -193,6 +196,7 @@ export default Ember.Controller.extend(Analytics, { }, // Metrics are handled in the component chooseFile(fileItem) { + this.set('chosenFile', fileItem.get('id')); this.set('activeFile', fileItem); }, shareLink(href, category, action, label) { diff --git a/app/controllers/discover.js b/app/controllers/discover.js index e22b102b4..466d1638c 100644 --- a/app/controllers/discover.js +++ b/app/controllers/discover.js @@ -50,15 +50,23 @@ export default Ember.Controller.extend(Analytics, { subjectFilter: null, queryBody: {}, providersPassed: false, + + i18n: Ember.inject.service(), + + sortByOptions: Ember.computed('i18n.locale', function() { + const i18n = this.get('i18n'); + return [i18n.t('discover.relevance'), i18n.t('discover.sort_oldest_newest'), i18n.t('discover.sort_newest_oldest')]; + }), + pageNumbers: [], - sortByOptions: ['Relevance', 'Upload date (oldest to newest)', 'Upload date (newest to oldest)'], + sortByOption: '', treeSubjects: Ember.computed('activeFilters', function() { return this.get('activeFilters.subjects').slice(); }), - // chosenOption is always the first element in the list - chosenSortByOption: Ember.computed('sortByOptions', function() { - return this.get('sortByOptions')[0]; + // chosenSortByOption is going to be the last selected element, or if it's a new page then it's the first in the list + chosenSortByOption: Ember.computed('sortByOption', function() { + return this.get('sortByOption') || this.get('sortByOptions')[0]; }), showActiveFilters: true, //should always have a provider, don't want to mix osfProviders and non-osf @@ -211,7 +219,7 @@ export default Ember.Controller.extend(Analytics, { result.hyperLinks.push({url: identifier}); } else { const spl = identifier.split('://'); - const [type, uri, ..._] = spl; // jshint ignore:line + const [type, uri] = spl; result.infoLinks.push({type, uri}); } }); @@ -293,12 +301,13 @@ export default Ember.Controller.extend(Analytics, { }); } - const sortByOption = this.get('chosenSortByOption'); const sort = {}; + const i18n = this.get('i18n'); + const sortByOption = this.get('sortByOption').toString(); - if (sortByOption === 'Upload date (oldest to newest)') { + if (sortByOption === i18n.t('discover.sort_oldest_newest').toString()) { sort.date_updated = 'asc'; - } else if (sortByOption === 'Upload date (newest to oldest)') { + } else if (sortByOption === i18n.t('discover.sort_newest_oldest').toString()) { sort.date_updated = 'desc'; } @@ -330,6 +339,10 @@ export default Ember.Controller.extend(Analytics, { }); }, + _clearQueryString() { + this.set('queryString', ''); + }, + otherProviders: [], actions: { search(val, event) { @@ -384,12 +397,8 @@ export default Ember.Controller.extend(Analytics, { }, sortBySelect(index) { - // Selecting an option just swaps it with whichever option is first - let copy = this.get('sortByOptions').slice(0); - let temp = copy[0]; - copy[0] = copy[index]; - copy[index] = temp; - this.set('sortByOptions', copy); + // sets the variable for the selected option and reloads the page + this.set('sortByOption', this.get('sortByOptions')[index]); this.set('page', 1); this.loadPage(); @@ -397,7 +406,7 @@ export default Ember.Controller.extend(Analytics, { .trackEvent({ category: 'dropdown', action: 'select', - label: `Preprints - Discover - Sort by: ${copy[index]}` + label: `Preprints - Discover - Sort by: ${this.get('sortByOptions')[index]}` }); }, diff --git a/app/controllers/submit.js b/app/controllers/submit.js index c5b6d6305..b5f87db2c 100644 --- a/app/controllers/submit.js +++ b/app/controllers/submit.js @@ -932,14 +932,10 @@ export default Ember.Controller.extend(Analytics, BasicsValidations, NodeActions return model.save() .then(() => node.save()) .then(() => { - if (this.get('editMode')) { - window.location = window.location.pathname; //TODO Ember way to do this? In edit mode, already in content route. - } else { - this.transitionToRoute( - `${this.get('theme.isProvider') ? 'provider.' : ''}content`, - model - ); - } + this.transitionToRoute( + `${this.get('theme.isSubRoute') ? 'provider.' : ''}content`, + model + ); }) .catch(() => { this.toggleProperty('shareButtonDisabled'); diff --git a/app/helpers/route-prefix.js b/app/helpers/route-prefix.js new file mode 100644 index 000000000..6be3cf870 --- /dev/null +++ b/app/helpers/route-prefix.js @@ -0,0 +1,25 @@ +import Ember from 'ember'; + +/** + * @module ember-preprints + * @submodule helpers + */ + +/** + * Needed for link-to for branded routing to get the correct route path + * + * @class route-prefix + */ +export default Ember.Helper.extend({ + theme: Ember.inject.service(), + + onSubRouteChange: Ember.observer('theme.isSubRoute', function() { + this.recompute(); + }), + + compute(params) { + const route = params.join(''); + + return this.get('theme.isSubRoute') ? `provider.${route}` : route; + } +}); diff --git a/app/index.html b/app/index.html index 9e75aa024..3818ccfba 100644 --- a/app/index.html +++ b/app/index.html @@ -9,8 +9,6 @@ {{content-for "head"}} - - {{content-for "head-footer"}} @@ -29,13 +27,10 @@ {{content-for "cdn"}} - - + {{content-for "assets"}} {{content-for "raven"}} - {{content-for "google-analytics"}} - {{content-for "body-footer"}} diff --git a/app/locales/en/translations.js b/app/locales/en/translations.js index 1a9330fab..d2ce8e150 100644 --- a/app/locales/en/translations.js +++ b/app/locales/en/translations.js @@ -35,7 +35,7 @@ export default { license: 'License', }, application: { - // Nothing to translate + separator: ` | ` }, content: { header: { @@ -70,6 +70,10 @@ export default { placeholder: `Search preprints...` }, sort_by: `Sort by`, + sort_newest_oldest: `Modified Date (newest to oldest)`, + sort_oldest_newest: `Modified Date (oldest to newest)`, + modified_on: `Modified on`, + relevance: `Relevance`, main: { active_filters: { heading: `Active Filters`, @@ -96,6 +100,7 @@ export default { }, powered_by: `Powered by ${brand}`, search: `{{count}} searchable preprints`, + or: `or`, as_of: `as of`, example: `See an example` }, @@ -124,27 +129,18 @@ export default { paragraph: `Our advisory group includes leaders in preprints and scholarly communication` } }, - 'page-not-found': { + // Error pages + 'page-not-found': { // 404 heading: `Page not found`, - paragraph: { - line1: `The page you were looking for is not found on the {{brand}} service.`, - line2: `If this should not have occurred and the issue persists, please report it to` - }, - go_to: `Go to {{brand}}` + message: `The page you were looking for is not found on the {{brand}} service.` }, - 'page-forbidden': { + 'page-forbidden': { // 403 heading: `Forbidden`, - paragraph: { - line1: `User has restricted access to this page. If this should not have occurred and the issue persists, please report it to `, - }, - go_to: `Go to {{brand}}` + message: `User has restricted access to this page.` }, - 'resource-deleted': { + 'resource-deleted': { // 410 heading: `Resource deleted`, - paragraph: { - line1: `User has deleted this content. If this should not have occurred and the issue persists, please report it to `, - }, - go_to: `Go to {{brand}}` + message: `User has deleted this content.` }, submit: { add_heading: `Add Preprint`, @@ -199,6 +195,11 @@ export default { could_not_create_component: `Could not create component. Please try again.`, abandoned_preprint_error: `Error with abandoned preprint.`, preprint_file_uploaded: `Preprint file uploaded!`, + preprint_author_added: `Preprint author added!`, + preprint_author_removed: `Preprint author removed!`, + preprint_unregistered_author_added: `Preprint unregistered author added!`, + error_adding_author: `Could not add author. Please try again.`, + error_adding_unregistered_author: `Could not add unregistered author. Please try again.`, error_initiating_preprint: `Could not initiate preprint. Please try again.`, doi_error: `Error saving DOI`, basics_error: `Error saving basics fields.`, @@ -234,6 +235,10 @@ export default { convert_confirmation_details_project: `Changes you make on this page are saved immediately. Create a new component under this project to avoid overwriting its details.`, convert_confirmation_details_component: `Changes you make on this page are saved immediately. Create a new component under this component to avoid overwriting its details.` }, + 'error-page': { + email_message: `If this should not have occurred and the issue persists, please report it to`, + go_to: `Go to {{brand}}` + }, 'file-uploader': { dropzone_message: `Drop preprint file here to upload`, title_placeholder: `Enter preprint title`, diff --git a/app/mixins/analytics.js b/app/mixins/analytics.js index 6242b9f47..82261a79f 100644 --- a/app/mixins/analytics.js +++ b/app/mixins/analytics.js @@ -22,7 +22,8 @@ export default Ember.Mixin.create({ }); // Needed for outbound links, see https://support.google.com/analytics/answer/1136920?hl=en - if (url) + // Also prevents mouse events from being attacehd to the window location + if (url && typeof url !== 'object') window.location.href = url; return true; diff --git a/app/router.js b/app/router.js index 21caf6a0e..76697daf4 100644 --- a/app/router.js +++ b/app/router.js @@ -1,12 +1,37 @@ import Ember from 'ember'; import config from 'ember-get-config'; +const {hostname} = window.location; + +const provider = config + .PREPRINTS + .providers + // Exclude OSF + .slice(1) + // Filter out providers without a domain + .filter(p => p.domain) + .find(p => + // Check if the hostname includes: the domain, the domain with dashes instead of periods, or just the id + hostname.includes(p.domain) || + hostname.includes(p.domain.replace(/\./g, '-')) || + hostname.includes(p.id) + ); + const Router = Ember.Router.extend({ location: config.locationType, rootURL: config.rootURL, metrics: Ember.inject.service(), theme: Ember.inject.service(), + init() { + this._super(...arguments); + + if (provider) { + this.set('theme.id', provider.id); + this.set('theme.isDomain', true); + } + }, + didTransition() { this._super(...arguments); this._trackPage(); @@ -25,19 +50,33 @@ const Router = Ember.Router.extend({ Router.map(function() { this.route('page-not-found', {path: '/*bad_url'}); - this.route('index', {path: 'preprints'}); - this.route('page-not-found', {path: 'preprints/page-not-found'}); - this.route('submit', {path: 'preprints/submit'}); - this.route('discover', {path: 'preprints/discover'}); - this.route('content', {path: '/:preprint_id' }); - this.route('provider', {path: 'preprints/:slug'}, function() { - this.route('content', {path: '/:preprint_id'}); - this.route('discover'); + + if (provider) { + this.route('index', {path: '/'}); this.route('submit'); + this.route('discover'); this.route('page-not-found'); + this.route('forbidden'); + this.route('resource-deleted'); + } else { + this.route('index', {path: 'preprints'}); + this.route('submit', {path: 'preprints/submit'}); + this.route('discover', {path: 'preprints/discover'}); + this.route('provider', {path: 'preprints/:slug'}, function () { + this.route('content', {path: '/:preprint_id'}, function() { + this.route('edit'); + }); + this.route('discover'); + this.route('submit'); + }); + this.route('page-not-found', {path: 'preprints/page-not-found'}); + this.route('forbidden', {path: 'preprints/forbidden'}); + this.route('resource-deleted', {path: 'preprints/resource-deleted'}); + } + + this.route('content', {path: '/:preprint_id'}, function() { + this.route('edit'); }); - this.route('forbidden'); - this.route('resource-deleted'); }); export default Router; diff --git a/app/routes/content.js b/app/routes/content.js index 273f15de8..8eb1c07b3 100644 --- a/app/routes/content.js +++ b/app/routes/content.js @@ -1,10 +1,4 @@ import Ember from 'ember'; -import ResetScrollMixin from '../mixins/reset-scroll'; -import SetupSubmitControllerMixin from '../mixins/setup-submit-controller'; -import Analytics from '../mixins/analytics'; -import config from 'ember-get-config'; -import loadAll from 'ember-osf/utils/load-relationship'; -import permissions from 'ember-osf/const/permissions'; // Error handling for API const handlers = new Map([ @@ -21,233 +15,14 @@ const handlers = new Map([ */ /** - * Fetches current preprint. Redirects to preprint provider route if necessary. * @class Content Route Handler */ -export default Ember.Route.extend(Analytics, ResetScrollMixin, SetupSubmitControllerMixin, { - theme: Ember.inject.service(), - headTagsService: Ember.inject.service('head-tags'), - currentUser: Ember.inject.service('currentUser'), - queryParams: { - edit: { - refreshModel: true // if queryParam, do a full transition - } - }, - setup() { - // Overrides setup method. If query param /?edit is present, uses 'submit' controller instead. - this.set('controllerName', this.get('editMode') ? 'submit' : 'content'); - return this._super(...arguments); - }, - renderTemplate() { - // Overrides renderTemplate method. If query param /?edit is present, uses 'submit' template instead. - this.render(this.get('editMode') ? 'submit' : 'content'); - }, +export default Ember.Route.extend({ model(params) { - if (params.edit) - this.set('editMode', true); - return this .store .findRecord('preprint', params.preprint_id); }, - setupController(controller, model) { - if (this.get('editMode')) { - // Runs setupController for 'submit' - this.setupSubmitController(controller, model); - } else { - // Runs setupController for 'content' - controller.set('activeFile', model.get('primaryFile')); - controller.set('node', this.get('node')); - Ember.run.scheduleOnce('afterRender', this, function() { - MathJax.Hub.Queue(['Typeset', MathJax.Hub, [Ember.$('.abstract')[0], Ember.$('#preprintTitle')[0]]]); // jshint ignore:line - }); - } - - return this._super(...arguments); - }, - afterModel(preprint) { - const {origin, search} = window.location; - let contributors = Ember.A(); - - return preprint.get('provider') - .then(provider => { - const providerId = provider.get('id'); - const themeId = this.get('theme.id'); - const isOSF = providerId === 'osf'; - - // If we're on the proper branded site, stay here. - if ((!themeId && isOSF) || themeId === providerId) - return Promise.all([ - provider, - preprint.get('node') - ]); - - // Otherwise, redirect to the proper branded site. - // Hard redirect instead of transition, in anticipation of Phase 2 where providers will have their own domains. - const urlParts = [ - origin - ]; - - if (!isOSF) - urlParts.push('preprints', providerId); - - urlParts.push(preprint.get('id'), search); - - const url = urlParts.join('/'); - - window.history.replaceState({}, document.title, url); - window.location.replace(url); - - return Promise.reject(); - }) - .then(([provider, node]) => { - this.set('node', node); - - if (this.get('editMode')) { - const userPermissions = this.get('node.currentUserPermissions') || []; - - if (!userPermissions.includes(permissions.ADMIN)) { - this.replaceWith('forbidden'); // Non-admin trying to access edit form. - } - } - - return Promise.all([ - provider, - node, - preprint.get('license'), - preprint.get('primaryFile'), - loadAll(node, 'contributors', contributors) - ]); - }) - .then(([provider, node, license, primaryFile]) => { - const title = node.get('title'); - const description = node.get('description'); - const doi = preprint.get('doi'); - const image = this.get('theme.logoSharing'); - const imageUrl = `${origin.replace(/^https/, 'http')}${image.path}`; - const dateCreated = new Date(preprint.get('dateCreated') || null); - const dateModified = new Date(preprint.get('dateModified') || dateCreated); - if (!preprint.get('datePublished')) - preprint.set('datePublished', dateCreated); - const providerName = provider.get('name'); - const canonicalUrl = preprint.get('links.html'); - - // NOTE: Ordering of meta tags matters for scrapers (Facebook, LinkedIn, Google, etc) - - // Open Graph Protocol - const openGraph = [ - ['fb:app_id', config.FB_APP_ID], - ['og:title', title], - ['og:image', imageUrl], - ['og:image:secure_url', `${origin}${image.path}`], // We should always be on https in staging/prod - ['og:image:width', image.width.toString()], - ['og:image:height', image.height.toString()], - ['og:image:type', image.type], - ['og:url', canonicalUrl], - ['og:description', description], - ['og:site_name', providerName], - ['og:type', 'article'], - ['article:published_time', dateCreated.toISOString()], - ['article:modified_time', dateModified.toISOString()] - ]; - - // Highwire Press - const highwirePress = [ - ['citation_title', title], - ['citation_description', description], - ['citation_public_url', canonicalUrl], - ['citation_publication_date', `${dateCreated.getFullYear()}/${dateCreated.getMonth() + 1}/${dateCreated.getDate()}`], - ['citation_doi', doi] - ]; - - // TODO map Eprints fields - // Eprints - const eprints = []; - - // TODO map BE Press fields - // BE Press - const bePress = []; - - // TODO map PRISM fields - // PRISM - const prism = []; - - // Dublin Core - const dublinCore = [ - ['dc.title', title], - ['dc.abstract', description], - ['dc.identifier', canonicalUrl], - ['dc.identifier', doi] - ]; - - const tags = [ - ...preprint.get('subjects').map(subjectBlock => subjectBlock.map(subject => subject.text)), - ...node.get('tags') - ]; - - for (const tag of tags) { - openGraph.push(['article:tag', tag]); - highwirePress.push(['citation_keywords', tag]); - dublinCore.push(['dc.subject', tag]); - } - - for (const contributor of contributors) { - const givenName = contributor.get('users.givenName'); - const familyName = contributor.get('users.familyName'); - const fullName = contributor.get('users.fullName'); - - openGraph.push( - ['og:type', 'article:author'], - ['profile:first_name', givenName], - ['profile:last_name', familyName] - ); - highwirePress.push(['citation_author', fullName]); - dublinCore.push(['dc.creator', fullName]); - } - - highwirePress.push(['citation_publisher', providerName]); - dublinCore.push( - ['dc.publisher', providerName], - ['dc.license', license ? license.get('name') : 'No license'] - ); - - if (/\.pdf$/.test(primaryFile.get('name'))) { - highwirePress.push(['citation_pdf_url', primaryFile.get('links').download]); - } - - const openGraphTags = openGraph - .map(([property, content]) => ({ - property, - content - })); - - const googleScholarTags = [ - highwirePress, - eprints, - bePress, - prism, - dublinCore - ] - .reduce((a, b) => a.concat(b), []) - .map(([name, content]) => ({ - name, - content - })); - - const headTags = [ - ...openGraphTags, - ...googleScholarTags - ] - .filter(({content}) => content) // Only show tags with content - .map(attrs => ({ - type: 'meta', - attrs - })); - - this.set('headTags', headTags); - this.get('headTagsService').collectHeadTags(); - }); - }, actions: { error(error) { // Handle API Errors diff --git a/app/routes/content/edit.js b/app/routes/content/edit.js new file mode 100644 index 000000000..df9e463cc --- /dev/null +++ b/app/routes/content/edit.js @@ -0,0 +1,93 @@ +import Ember from 'ember'; +import ResetScrollMixin from '../../mixins/reset-scroll'; +import SetupSubmitControllerMixin from '../../mixins/setup-submit-controller'; +import Analytics from '../../mixins/analytics'; +import config from 'ember-get-config'; +import permissions from 'ember-osf/const/permissions'; +import getRedirectUrl from '../../utils/get-redirect-url'; + +const {PREPRINTS: {providers}} = config; + +/** + * @module ember-preprints + * @submodule routes + */ + +/** + * Fetches current preprint. Redirects to preprint provider route if necessary. + * @class Edit Route Handler + */ +export default Ember.Route.extend(Analytics, ResetScrollMixin, SetupSubmitControllerMixin, { + theme: Ember.inject.service(), + headTagsService: Ember.inject.service('head-tags'), + currentUser: Ember.inject.service('currentUser'), + + editMode: true, + + setup() { + // Overrides setup method. If query param /?edit is present, uses 'submit' controller instead. + this.set('controllerName', 'submit'); + return this._super(...arguments); + }, + renderTemplate() { + // Overrides renderTemplate method. If query param /?edit is present, uses 'submit' template instead. + this.render('submit'); + }, + + setupController(controller, model) { + // Runs setupController for 'submit' + this.setupSubmitController(controller, model); + + return this._super(...arguments); + }, + afterModel(preprint) { + const {location: {origin}} = window; + + return preprint.get('provider') + .then(provider => { + const providerId = provider.get('id'); + const themeId = this.get('theme.id'); + const isOSF = providerId === 'osf'; + + // If we're on the proper branded site, stay here. + if ((!themeId && isOSF) || themeId === providerId) + return preprint.get('node'); + + // Otherwise, find the correct provider and redirect + const configProvider = providers.find(p => p.id === providerId); + + if (!configProvider) + throw new Error('Provider is not configured properly. Check the Ember configuration.'); + + const {domain} = configProvider; + const urlParts = []; + + // Provider with a domain + if (this.get('theme.isDomain') || domain) { + urlParts.push(getRedirectUrl(window.location, domain)); + // Provider without a domain + } else { + urlParts.push(origin); + + if (!isOSF) + urlParts.push('preprints', providerId); + + urlParts.push(preprint.get('id')); + } + + const url = urlParts.join('/').replace(/\/\/$/, '/'); + window.location.replace(url); + + return Promise.reject(); + }) + .then(node => { + this.set('node', node); + + const userPermissions = this.get('node.currentUserPermissions') || []; + + if (!userPermissions.includes(permissions.ADMIN)) { + this.replaceWith('forbidden'); // Non-admin trying to access edit form. + } + }); + }, +}); diff --git a/app/routes/content/index.js b/app/routes/content/index.js new file mode 100644 index 000000000..e10bb03cb --- /dev/null +++ b/app/routes/content/index.js @@ -0,0 +1,250 @@ +import Ember from 'ember'; +import ResetScrollMixin from '../../mixins/reset-scroll'; +import SetupSubmitControllerMixin from '../../mixins/setup-submit-controller'; +import Analytics from '../../mixins/analytics'; +import config from 'ember-get-config'; +import loadAll from 'ember-osf/utils/load-relationship'; +import permissions from 'ember-osf/const/permissions'; +import getRedirectUrl from '../../utils/get-redirect-url'; + +const {PREPRINTS: {providers}} = config; + +// Error handling for API +const handlers = new Map([ + // format: ['Message detail', 'page'] + ['Authentication credentials were not provided.', 'page-not-found'], // 401 + ['You do not have permission to perform this action.', 'page-not-found'], // 403 + ['Not found.', 'page-not-found'], // 404 + ['The requested node is no longer available.', 'resource-deleted'] // 410 +]); + +/** + * @module ember-preprints + * @submodule routes + */ + +/** + * Fetches current preprint. Redirects to preprint provider route if necessary. + * @class Content Route Handler + */ +export default Ember.Route.extend(Analytics, ResetScrollMixin, SetupSubmitControllerMixin, { + theme: Ember.inject.service(), + headTagsService: Ember.inject.service('head-tags'), + currentUser: Ember.inject.service('currentUser'), + + setupController(controller, model) { + controller.set('activeFile', model.get('primaryFile')); + controller.set('node', this.get('node')); + Ember.run.scheduleOnce('afterRender', this, function() { + MathJax.Hub.Queue(['Typeset', MathJax.Hub, [Ember.$('.abstract')[0], Ember.$('#preprintTitle')[0]]]); // jshint ignore:line + }); + + return this._super(...arguments); + }, + + afterModel(preprint) { + const {location: {origin}} = window; + let contributors = Ember.A(); + + return preprint.get('provider') + .then(provider => { + const providerId = provider.get('id'); + const themeId = this.get('theme.id'); + const isOSF = providerId === 'osf'; + + // If we're on the proper branded site, stay here. + if ((!themeId && isOSF) || themeId === providerId) + return Promise.all([ + provider, + preprint.get('node') + ]); + + // Otherwise, find the correct provider and redirect + const configProvider = providers.find(p => p.id === providerId); + + if (!configProvider) + throw new Error('Provider is not configured properly. Check the Ember configuration.'); + + const {domain} = configProvider; + const urlParts = []; + + // Provider with a domain + if (this.get('theme.isDomain') || domain) { + urlParts.push(getRedirectUrl(window.location, domain)); + // Provider without a domain + } else { + urlParts.push(origin); + + if (!isOSF) + urlParts.push('preprints', providerId); + + urlParts.push(preprint.get('id')); + } + + const url = urlParts.join('/').replace(/\/\/$/, '/'); + + window.location.replace(url); + + return Promise.reject(); + }) + .then(([provider, node]) => { + this.set('node', node); + + if (this.get('editMode')) { + const userPermissions = this.get('node.currentUserPermissions') || []; + + if (!userPermissions.includes(permissions.ADMIN)) { + this.replaceWith('forbidden'); // Non-admin trying to access edit form. + } + } + + return Promise.all([ + provider, + node, + preprint.get('license'), + preprint.get('primaryFile'), + loadAll(node, 'contributors', contributors) + ]); + }) + .then(([provider, node, license, primaryFile]) => { + const title = node.get('title'); + const description = node.get('description'); + const doi = preprint.get('doi'); + const image = this.get('theme.logoSharing'); + const imageUrl = `${origin.replace(/^https/, 'http')}${image.path}`; + const dateCreated = new Date(preprint.get('dateCreated') || null); + const dateModified = new Date(preprint.get('dateModified') || dateCreated); + if (!preprint.get('datePublished')) + preprint.set('datePublished', dateCreated); + const providerName = provider.get('name'); + const canonicalUrl = preprint.get('links.html'); + + // NOTE: Ordering of meta tags matters for scrapers (Facebook, LinkedIn, Google, etc) + + // Open Graph Protocol + const openGraph = [ + ['fb:app_id', config.FB_APP_ID], + ['og:title', title], + ['og:image', imageUrl], + ['og:image:secure_url', `${origin}${image.path}`], // We should always be on https in staging/prod + ['og:image:width', image.width.toString()], + ['og:image:height', image.height.toString()], + ['og:image:type', image.type], + ['og:url', canonicalUrl], + ['og:description', description], + ['og:site_name', providerName], + ['og:type', 'article'], + ['article:published_time', dateCreated.toISOString()], + ['article:modified_time', dateModified.toISOString()] + ]; + + // Highwire Press + const highwirePress = [ + ['citation_title', title], + ['citation_description', description], + ['citation_public_url', canonicalUrl], + ['citation_publication_date', `${dateCreated.getFullYear()}/${dateCreated.getMonth() + 1}/${dateCreated.getDate()}`], + ['citation_doi', doi] + ]; + + // TODO map Eprints fields + // Eprints + const eprints = []; + + // TODO map BE Press fields + // BE Press + const bePress = []; + + // TODO map PRISM fields + // PRISM + const prism = []; + + // Dublin Core + const dublinCore = [ + ['dc.title', title], + ['dc.abstract', description], + ['dc.identifier', canonicalUrl], + ['dc.identifier', doi] + ]; + + const tags = [ + ...preprint.get('subjects').map(subjectBlock => subjectBlock.map(subject => subject.text)), + ...node.get('tags') + ]; + + for (const tag of tags) { + openGraph.push(['article:tag', tag]); + highwirePress.push(['citation_keywords', tag]); + dublinCore.push(['dc.subject', tag]); + } + + for (const contributor of contributors) { + const givenName = contributor.get('users.givenName'); + const familyName = contributor.get('users.familyName'); + const fullName = contributor.get('users.fullName'); + + openGraph.push( + ['og:type', 'article:author'], + ['profile:first_name', givenName], + ['profile:last_name', familyName] + ); + highwirePress.push(['citation_author', fullName]); + dublinCore.push(['dc.creator', fullName]); + } + + highwirePress.push(['citation_publisher', providerName]); + dublinCore.push( + ['dc.publisher', providerName], + ['dc.license', license ? license.get('name') : 'No license'] + ); + + if (/\.pdf$/.test(primaryFile.get('name'))) { + highwirePress.push(['citation_pdf_url', primaryFile.get('links').download]); + } + + const openGraphTags = openGraph + .map(([property, content]) => ({ + property, + content + })); + + const googleScholarTags = [ + highwirePress, + eprints, + bePress, + prism, + dublinCore + ] + .reduce((a, b) => a.concat(b), []) + .map(([name, content]) => ({ + name, + content + })); + + const headTags = [ + ...openGraphTags, + ...googleScholarTags + ] + .filter(({content}) => content) // Only show tags with content + .map(attrs => ({ + type: 'meta', + attrs + })); + + this.set('headTags', headTags); + this.get('headTagsService').collectHeadTags(); + }); + }, + + actions: { + error(error) { + // Handle API Errors + if (error && error.errors && Ember.isArray(error.errors)) { + const {detail} = error.errors[0]; + const page = handlers.get(detail) || 'page-not-found'; + + return this.intermediateTransitionTo(page); + } + } + } +}); diff --git a/app/routes/discover.js b/app/routes/discover.js index 14e88285e..2f0112fb5 100644 --- a/app/routes/discover.js +++ b/app/routes/discover.js @@ -27,6 +27,7 @@ export default Ember.Route.extend(Analytics, ResetScrollMixin, { willTransition() { let controller = this.controllerFor('discover'); controller._clearFilters(); + controller._clearQueryString(); } } }); diff --git a/app/routes/index.js b/app/routes/index.js index 8782bbb49..4e2040e90 100644 --- a/app/routes/index.js +++ b/app/routes/index.js @@ -40,7 +40,7 @@ export default Ember.Route.extend(Analytics, ResetScrollMixin, { search(q) { let route = 'discover'; - if (this.get('theme.isProvider')) + if (this.get('theme.isSubRoute')) route = `provider.${route}`; this.transitionTo(route, { queryParams: { queryString: q } }); diff --git a/app/routes/provider.js b/app/routes/provider.js index 732fde026..3a892661f 100644 --- a/app/routes/provider.js +++ b/app/routes/provider.js @@ -1,5 +1,9 @@ import Ember from 'ember'; import config from 'ember-get-config'; +import getRedirectUrl from '../utils/get-redirect-url'; + +const providers = config.PREPRINTS.providers.slice(1); +const providerIds = providers.map(p => p.id); /** * @module ember-preprints @@ -12,19 +16,28 @@ import config from 'ember-get-config'; export default Ember.Route.extend({ theme: Ember.inject.service(), - providerIds: config.PREPRINTS.providers - .slice(1) - .map(provider => provider.id), - beforeModel(transition) { const {slug} = transition.params.provider; const slugLower = (slug || '').toLowerCase(); - if (this.get('providerIds').includes(slugLower)) { + if (providerIds.includes(slugLower)) { + const {domain} = providers.find(provider => provider.id === slugLower) || {}; + + // This should be caught by the proxy, but we'll redirect just in case it is not. + if (domain) { + window.location.replace( + getRedirectUrl(window.location, domain, slug) + ); + + return; + } + if (slugLower !== slug) { const {pathname} = window.location; + const pathRegex = new RegExp(`^/preprints/${slug}`); + window.location.pathname = pathname.replace( - new RegExp(`^/preprints/${slug}`), + pathRegex, `/preprints/${slugLower}` ); } diff --git a/app/routes/provider/content/edit.js b/app/routes/provider/content/edit.js new file mode 100644 index 000000000..75ca3ea82 --- /dev/null +++ b/app/routes/provider/content/edit.js @@ -0,0 +1,3 @@ +import route from '../../content/edit'; + +export default route; diff --git a/app/routes/provider/content/index.js b/app/routes/provider/content/index.js new file mode 100644 index 000000000..faefe0082 --- /dev/null +++ b/app/routes/provider/content/index.js @@ -0,0 +1,13 @@ +import route from '../../content/index'; + +route.reopen({ + controllerName: 'content.index', + renderTemplate(controller, model) { + this.render('content.index', { + controller, + model + }); + } +}); + +export default route; diff --git a/app/services/theme.js b/app/services/theme.js index 1035d12fa..f0c5048b6 100644 --- a/app/services/theme.js +++ b/app/services/theme.js @@ -16,10 +16,15 @@ export default Ember.Service.extend({ store: Ember.inject.service(), session: Ember.inject.service(), + // If we're using a provider domain + isDomain: false, + + // The id of the current provider id: config.PREPRINTS.defaultProvider, currentLocation: null, + // The provider object provider: Ember.computed('id', function() { const id = this.get('id'); @@ -31,21 +36,54 @@ export default Ember.Service.extend({ .findRecord('preprint-provider', id); }), + // If we're using a branded provider isProvider: Ember.computed('id', function() { - const id = this.get('id'); - return id && id !== 'osf'; + return this.get('id') !== 'osf'; + }), + + // If we're using a branded provider and not under a branded domain (e.g. /preprints/) + isSubRoute: Ember.computed('isProvider', 'isDomain', function() { + return this.get('isProvider') && !this.get('isDomain'); + }), + + pathPrefix: Ember.computed('isProvider', 'isDomain', 'id', function() { + let pathPrefix = '/'; + + if (!this.get('isDomain')) { + pathPrefix += 'preprints/'; + + if (this.get('isProvider')) { + pathPrefix += `${this.get('id')}/`; + } + } + + return pathPrefix; + }), + + // Needed for the content route + guidPathPrefix: Ember.computed('isSubRoute', 'id', function() { + let pathPrefix = '/'; + + if (this.get('isSubRoute')) { + pathPrefix += `preprints/${this.get('id')}/`; + } + + return pathPrefix; }), + // The URL for the branded stylesheet stylesheet: Ember.computed('id', function() { const id = this.get('id'); if (!id) return; + const prefix = this.get('isDomain') ? '' : '/preprints'; const suffix = config.ASSET_SUFFIX ? `-${config.ASSET_SUFFIX}` : ''; - return `/preprints/assets/css/${id}${suffix}.css`; + return `${prefix}/assets/css/${id}${suffix}.css`; }), + // The logo object for social sharing logoSharing: Ember.computed('id', function() { const id = this.get('id'); @@ -58,6 +96,7 @@ export default Ember.Service.extend({ return logo; }), + // The url to redirect users to sign up to signupUrl: Ember.computed('id', function() { const query = Ember.$.param({ campaign: `${this.get('id')}-preprints`, @@ -71,6 +110,7 @@ export default Ember.Service.extend({ return this.get('currentLocation'); }), + // The translation key for the provider's permission language permissionLanguage: Ember.computed('id', function() { const id = this.get('id'); diff --git a/app/styles/app.scss b/app/styles/app.scss index a3669cd39..2bcb74378 100644 --- a/app/styles/app.scss +++ b/app/styles/app.scss @@ -113,6 +113,25 @@ ul.comma-list { color: black; } +#downloadCount { + display: inline-block; +} + +@media (max-width: 991px) { + .download_project, .social-div { + width: auto; + } +} + +@media (max-width: 768px) { + #providerCarousel { + .carousel-control .icon-next, .carousel-control .icon-prev { + margin-top: -30px; + font-size: 50px; + } + } +} + /* Main header with search bar */ .preprint-header{ background:#214661 url('img/preprints-bg.jpg') top center ; @@ -321,7 +340,10 @@ ul.preprints-block-list { padding-left: 0; } - +.listOption:hover { + cursor: pointer; + color: rgba(255, 255, 255, 0.8) !important; +} /* VIEW page */ .dark-overlay-header-background { @@ -453,23 +475,7 @@ ul.preprints-block-list { background-color: #b9cdd6; } - -/* Not found page */ -.preprint-404 { - padding: 120px 0 120px 0; - background-color: #ecf2f3; - border-bottom: 1px solid #d6dbdc; -} - -/* Forbidden page */ -.preprint-403 { - padding: 120px 0 120px 0; - background-color: #ecf2f3; - border-bottom: 1px solid #d6dbdc; -} - -/* Gone page */ -.preprint-410 { +.preprint-error-page { padding: 120px 0 120px 0; background-color: #ecf2f3; border-bottom: 1px solid #d6dbdc; @@ -698,6 +704,7 @@ hr { } &.cp-is-open { opacity: 1; + padding: 15px; } } // THESE STYLES ARE INTERFERING WITH MY STYLES. @@ -1108,6 +1115,10 @@ label.title-label { background-size: contain; } +.supplemental-downloads { + text-align: right; +} + @media (max-width: 768px) { .edit-button-and-logo { padding-top: 20px; @@ -1181,6 +1192,7 @@ label.title-label { float: none !important; white-space: nowrap; } + .social-div { margin-top: 5px; } diff --git a/app/styles/brands/_brand.scss b/app/styles/brands/_brand.scss index 8109f1fe1..d07284b90 100644 --- a/app/styles/brands/_brand.scss +++ b/app/styles/brands/_brand.scss @@ -130,8 +130,7 @@ $logo-dir: '../img/provider_logos/'; } } - .preprint-403, - .preprint-404, + .preprint-error-page, .preprint-advisory, .preprint-submit-header, .preprint-search-header, @@ -147,8 +146,7 @@ $logo-dir: '../img/provider_logos/'; } } - .preprint-403, - .preprint-404, + .preprint-error-page, .preprint-advisory { min-height: 200px; diff --git a/app/templates/application.hbs b/app/templates/application.hbs index 8ef7a22df..3da7e2460 100644 --- a/app/templates/application.hbs +++ b/app/templates/application.hbs @@ -1,5 +1,6 @@ +{{title (t (if theme.isProvider 'global.provider_brand' 'global.brand') name=theme.provider.name) separator=(t 'application.separator')}} + {{#if theme.isProvider}} - {{title (concat theme.provider.name ' Preprints') separator=' | '}} {{#if theme.stylesheet}}