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/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-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/controllers/submit.js b/app/controllers/submit.js index c5b6d6305..8a72cd46a 100644 --- a/app/controllers/submit.js +++ b/app/controllers/submit.js @@ -936,7 +936,7 @@ export default Ember.Controller.extend(Analytics, BasicsValidations, NodeActions 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`, + `${this.get('theme.isSubRoute') ? 'provider.' : ''}content`, model ); } 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 f0e1dd539..0e39d6e41 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: { @@ -94,6 +94,7 @@ export default { }, powered_by: `Powered by ${brand}`, search: `{{count}} searchable preprints`, + or: `or`, as_of: `as of`, example: `See an example` }, @@ -122,27 +123,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`, @@ -232,6 +224,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/router.js b/app/router.js index 21caf6a0e..315a0a6c3 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,17 +50,25 @@ 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'); - }); + } 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'}); + this.route('discover'); + this.route('submit'); + }); + this.route('page-not-found', {path: 'preprints/page-not-found'}); + } + + this.route('content', {path: '/:preprint_id'}); this.route('forbidden'); this.route('resource-deleted'); }); diff --git a/app/routes/content.js b/app/routes/content.js index 273f15de8..284049254 100644 --- a/app/routes/content.js +++ b/app/routes/content.js @@ -5,6 +5,9 @@ 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 providers = config.PREPRINTS.providers; // Error handling for API const handlers = new Map([ @@ -67,7 +70,7 @@ export default Ember.Route.extend(Analytics, ResetScrollMixin, SetupSubmitContro }, afterModel(preprint) { const {origin, search} = window.location; - let contributors = Ember.A(); + const contributors = Ember.A(); return preprint.get('provider') .then(provider => { @@ -82,20 +85,30 @@ export default Ember.Route.extend(Analytics, ResetScrollMixin, SetupSubmitContro 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 - ]; + // 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.'); - if (!isOSF) - urlParts.push('preprints', providerId); + const {domain} = configProvider; + const urlParts = []; - urlParts.push(preprint.get('id'), search); + // Provider with a domain + if (this.get('theme.isDomain') || domain) { + urlParts.push(getRedirectUrl(window.location, domain)); + // Provider without a domain + } else { + urlParts.push(origin); - const url = urlParts.join('/'); + if (!isOSF) + urlParts.push('preprints', providerId); + + urlParts.push(preprint.get('id')); + } - window.history.replaceState({}, document.title, url); + urlParts.push(search); + const url = urlParts.join('/').replace(/\/\/$/, '/'); window.location.replace(url); return Promise.reject(); 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/services/theme.js b/app/services/theme.js index 1035d12fa..1abe02d1a 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('isProvider', 'isDomain', 'id', function() { + let pathPrefix = '/'; + + if (!this.get('isDomain') && this.get('isProvider')) { + 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 9980d2da1..2a13b0d10 100644 --- a/app/styles/app.scss +++ b/app/styles/app.scss @@ -450,23 +450,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; diff --git a/app/styles/brands/_brand.scss b/app/styles/brands/_brand.scss index 3e84cd09d..b71f177ba 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 2d5674cc0..202b72045 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}}