From b7bd6d9968b2acb5879f34f763d4276d7b60e143 Mon Sep 17 00:00:00 2001 From: Aileen Nowak Date: Thu, 3 Mar 2016 10:52:27 +0200 Subject: [PATCH] Structured Data 3.0 closes #6534 - new input fields in general settings incl. validation - facebook and twitter as new models in settings.js - adds values for facebook and twitter to default-settings.js - adds blog helpers for facebook and twittter - rather than saving the whole URL, the Twitter username incl. '@' will be extracted from URL and saved in the settings. The User will still input the full URL. After saving the blog setting, the stored Twitter username will be parsed again as the full URL and available in the input field. A custom transform is used for this. - adding meta fields to be rendered in {{ghost_head}}: - '' and - '' - adds facebook and twitter to unit test for structured data - adds unit test for general settings - adds acceptance test for new input fields in general settings - adds a custom transform for twitter model to save only the username to the server - adds unit test for transform --- .../app/controllers/settings/general.js | 106 ++++++++++++++++- core/client/app/controllers/team/user.js | 109 ++++++++++++++++++ core/client/app/mirage/config.js | 10 ++ core/client/app/mirage/fixtures/settings.js | 22 ++++ core/client/app/models/setting.js | 2 + core/client/app/models/user.js | 2 + core/client/app/routes/settings/general.js | 4 +- .../client/app/templates/settings/general.hbs | 20 +++- core/client/app/templates/team/user.hbs | 14 +++ .../client/app/transforms/twitter-url-user.js | 21 ++++ .../tests/acceptance/settings/general-test.js | 95 ++++++++++++--- core/client/tests/acceptance/team-test.js | 72 +++++++++++- .../unit/transforms/twitter-url-user-test.js | 32 +++++ core/server/api/settings.js | 4 +- core/server/data/meta/author_fb_url.js | 15 +++ core/server/data/meta/creator_url.js | 14 +++ core/server/data/meta/index.js | 4 + core/server/data/meta/schema.js | 12 +- core/server/data/meta/structured_data.js | 13 ++- core/server/data/schema/default-settings.json | 6 + core/server/helpers/ghost_head.js | 1 + core/test/unit/metadata/author_fb_url_spec.js | 68 +++++++++++ core/test/unit/metadata/creator_url_spec.js | 68 +++++++++++ core/test/unit/metadata/schema_spec.js | 33 +++++- .../unit/metadata/structured_data_spec.js | 18 ++- .../unit/server_helpers/ghost_head_spec.js | 51 +++++--- 26 files changed, 771 insertions(+), 45 deletions(-) create mode 100644 core/client/app/transforms/twitter-url-user.js create mode 100644 core/client/tests/unit/transforms/twitter-url-user-test.js create mode 100644 core/server/data/meta/author_fb_url.js create mode 100644 core/server/data/meta/creator_url.js create mode 100644 core/test/unit/metadata/author_fb_url_spec.js create mode 100644 core/test/unit/metadata/creator_url_spec.js diff --git a/core/client/app/controllers/settings/general.js b/core/client/app/controllers/settings/general.js index da7a4653c4..23aa66df29 100644 --- a/core/client/app/controllers/settings/general.js +++ b/core/client/app/controllers/settings/general.js @@ -6,7 +6,8 @@ const { Controller, computed, inject: {service}, - observer + observer, + run } = Ember; export default Controller.extend(SettingsSaveMixin, { @@ -16,6 +17,8 @@ export default Controller.extend(SettingsSaveMixin, { notifications: service(), config: service(), + _scratchFacebook: null, + _scratchTwitter: null, selectedTheme: computed('model.activeTheme', 'themes', function () { let activeTheme = this.get('model.activeTheme'); @@ -88,6 +91,7 @@ export default Controller.extend(SettingsSaveMixin, { if (error) { notifications.showAPIError(error, {key: 'settings.save'}); } + throw error; }); }, @@ -110,6 +114,106 @@ export default Controller.extend(SettingsSaveMixin, { toggleUploadLogoModal() { this.toggleProperty('showUploadLogoModal'); + }, + + validateFacebookUrl() { + let newUrl = this.get('_scratchFacebook'); + let oldUrl = this.get('model.facebook'); + let errMessage = ''; + + if (!newUrl) { + // Clear out the Facebook url + this.set('model.facebook', ''); + this.get('model.errors').remove('facebook'); + return; + } + + // If new url didn't change, exit + if (newUrl === oldUrl) { + return; + } + + if (!newUrl.match(/(^https:\/\/www\.facebook\.com\/)(\S+)/g)) { + if (newUrl.match(/(?:facebook\.com\/)(\S+)/) || (!validator.isURL(newUrl) && newUrl.match(/([a-zA-Z0-9\.]+)/))) { + let [ , username] = newUrl.match(/(?:facebook\.com\/)(\S+)/) || newUrl.match(/([a-zA-Z0-9\.]+)/); + newUrl = `https://www.facebook.com/${username}`; + + this.set('model.facebook', newUrl); + + this.get('model.errors').remove('facebook'); + this.get('model.hasValidated').pushObject('facebook'); + + // User input is validated + return this.save().then(() => { + this.set('model.facebook', ''); + run.schedule('afterRender', this, function () { + this.set('model.facebook', newUrl); + }); + }); + } else if (validator.isURL(newUrl)) { + errMessage = 'The URL must be in a format like ' + + 'https://www.facebook.com/yourPage'; + this.get('model.errors').add('facebook', errMessage); + this.get('model.hasValidated').pushObject('facebook'); + return; + } else { + errMessage = 'The URL must be in a format like ' + + 'https://www.facebook.com/yourPage'; + this.get('model.errors').add('facebook', errMessage); + this.get('model.hasValidated').pushObject('facebook'); + return; + } + } + }, + + validateTwitterUrl() { + let newUrl = this.get('_scratchTwitter'); + let oldUrl = this.get('model.twitter'); + let errMessage = ''; + + if (!newUrl) { + // Clear out the Facebook url + this.set('model.twitter', ''); + this.get('model.errors').remove('twitter'); + return; + } + + // If new url didn't change, exit + if (newUrl === oldUrl) { + return; + } + + if (!newUrl.match(/(^https:\/\/twitter\.com\/)(\S+)/g)) { + if (newUrl.match(/(?:twitter\.com\/)(\S+)/) || (!validator.isURL(newUrl) && newUrl.match(/([a-zA-Z0-9\.]+)/))) { + let [ , username] = newUrl.match(/(?:twitter\.com\/)(\S+)/) || newUrl.match(/([a-zA-Z0-9\.]+)/); + newUrl = `https://twitter.com/${username}`; + + this.set('model.twitter', newUrl); + + this.get('model.errors').remove('twitter'); + this.get('model.hasValidated').pushObject('twitter'); + + // User input is validated + return this.save().then(() => { + this.set('model.twitter', ''); + run.schedule('afterRender', this, function () { + this.set('model.twitter', newUrl); + }); + }); + } else if (validator.isURL(newUrl)) { + errMessage = 'The URL must be in a format like ' + + 'https://twitter.com/yourUsername'; + this.get('model.errors').add('twitter', errMessage); + this.get('model.hasValidated').pushObject('twitter'); + return; + } else { + errMessage = 'The URL must be in a format like ' + + 'https://twitter.com/yourUsername'; + this.get('model.errors').add('twitter', errMessage); + this.get('model.hasValidated').pushObject('twitter'); + return; + } + } } } }); diff --git a/core/client/app/controllers/team/user.js b/core/client/app/controllers/team/user.js index e275d2d5cf..6672c3cf0b 100644 --- a/core/client/app/controllers/team/user.js +++ b/core/client/app/controllers/team/user.js @@ -1,12 +1,14 @@ import Ember from 'ember'; import isNumber from 'ghost/utils/isNumber'; import boundOneWay from 'ghost/utils/bound-one-way'; +import { invoke } from 'ember-invoke-action'; const { Controller, RSVP, computed, inject: {service}, + run, isArray } = Ember; const {alias, and, not, or, readOnly} = computed; @@ -18,6 +20,8 @@ export default Controller.extend({ showTransferOwnerModal: false, showUploadCoverModal: false, showUplaodImageModal: false, + _scratchFacebook: null, + _scratchTwitter: null, ajax: service(), dropdown: service(), @@ -148,6 +152,7 @@ export default Controller.extend({ }); this.set('lastPromise', promise); + return promise; }, deleteUser() { @@ -239,6 +244,110 @@ export default Controller.extend({ this.set('lastPromise', promise); }, + validateFacebookUrl() { + let newUrl = this.get('_scratchFacebook'); + let oldUrl = this.get('user.facebook'); + let errMessage = ''; + + if (!newUrl) { + // Clear out the Facebook url + this.set('user.facebook', ''); + this.get('user.errors').remove('facebook'); + return; + } + + // If new url didn't change, exit + if (newUrl === oldUrl) { + return; + } + + // TODO: put the validation here into a validator + if (!newUrl.match(/(^https:\/\/www\.facebook\.com\/)(\S+)/g)) { + if (newUrl.match(/(?:facebook\.com\/)(\S+)/) || (!validator.isURL(newUrl) && newUrl.match(/([a-zA-Z0-9\.]+)/))) { + let [ , username] = newUrl.match(/(?:facebook\.com\/)(\S+)/) || newUrl.match(/([a-zA-Z0-9\.]+)/); + newUrl = `https://www.facebook.com/${username}`; + + this.set('user.facebook', newUrl); + + this.get('user.errors').remove('facebook'); + this.get('user.hasValidated').pushObject('facebook'); + + // User input is validated + invoke(this, 'save').then(() => { + // necessary to update the value in the input field + this.set('user.facebook', ''); + run.schedule('afterRender', this, function () { + this.set('user.facebook', newUrl); + }); + }); + } else if (validator.isURL(newUrl)) { + errMessage = 'The URL must be in a format like ' + + 'https://www.facebook.com/yourUsername'; + this.get('user.errors').add('facebook', errMessage); + this.get('user.hasValidated').pushObject('facebook'); + return; + } else { + errMessage = 'The URL must be in a format like ' + + 'https://www.facebook.com/yourUsername'; + this.get('user.errors').add('facebook', errMessage); + this.get('user.hasValidated').pushObject('facebook'); + return; + } + } + }, + + validateTwitterUrl() { + let newUrl = this.get('_scratchTwitter'); + let oldUrl = this.get('user.twitter'); + let errMessage = ''; + + if (!newUrl) { + // Clear out the Twitter url + this.set('user.twitter', ''); + this.get('user.errors').remove('twitter'); + return; + } + + // If new url didn't change, exit + if (newUrl === oldUrl) { + return; + } + + // TODO: put the validation here into a validator + if (!newUrl.match(/(^https:\/\/twitter\.com\/)(\S+)/g)) { + if (newUrl.match(/(?:twitter\.com\/)(\S+)/) || (!validator.isURL(newUrl) && newUrl.match(/([a-zA-Z0-9\.]+)/))) { + let [ , username] = newUrl.match(/(?:twitter\.com\/)(\S+)/) || newUrl.match(/([a-zA-Z0-9\.]+)/); + newUrl = `https://twitter.com/${username}`; + + this.set('user.twitter', newUrl); + + this.get('user.errors').remove('twitter'); + this.get('user.hasValidated').pushObject('twitter'); + + // User input is validated + invoke(this, 'save').then(() => { + // necessary to update the value in the input field + this.set('user.twitter', ''); + run.schedule('afterRender', this, function () { + this.set('user.twitter', newUrl); + }); + }); + } else if (validator.isURL(newUrl)) { + errMessage = 'The URL must be in a format like ' + + 'https://twitter.com/yourUsername'; + this.get('user.errors').add('twitter', errMessage); + this.get('user.hasValidated').pushObject('twitter'); + return; + } else { + errMessage = 'The URL must be in a format like ' + + 'https://twitter.com/yourUsername'; + this.get('user.errors').add('twitter', errMessage); + this.get('user.hasValidated').pushObject('twitter'); + return; + } + } + }, + transferOwnership() { let user = this.get('user'); let url = this.get('ghostPaths.url').api('users', 'owner'); diff --git a/core/client/app/mirage/config.js b/core/client/app/mirage/config.js index c2d95a899c..e40b99895a 100644 --- a/core/client/app/mirage/config.js +++ b/core/client/app/mirage/config.js @@ -347,4 +347,14 @@ export function testConfig() { users: [db.users.find(request.params.id)] }; }); + + this.put('/users/:id/', function (db, request) { + let {id} = request.params; + let [attrs] = JSON.parse(request.requestBody).users; + let record = db.users.update(id, attrs); + + return { + user: record + }; + }); } diff --git a/core/client/app/mirage/fixtures/settings.js b/core/client/app/mirage/fixtures/settings.js index fb679765b5..6d76bfa5e7 100644 --- a/core/client/app/mirage/fixtures/settings.js +++ b/core/client/app/mirage/fixtures/settings.js @@ -179,6 +179,28 @@ export default [ uuid: 'dd4ebaa8-dedb-40ff-a663-ec64a92d4111', value: '[{"url":""}]' }, + { + created_at: '2016-05-05T15:40:12.133Z', + created_by: 1, + id: 23, + key: 'facebook', + type: 'blog', + updated_at: '2016-05-08T15:20:25.953Z', + updated_by: 1, + uuid: 'd4387e5c-3230-46dd-a89b-0d8a40365c35', + value: '' + }, + { + created_at: '2016-05-05T15:40:12.134Z', + created_by: 1, + id: 24, + key: 'twitter', + type: 'blog', + updated_at: '2016-05-08T15:20:25.954Z', + updated_by: 1, + uuid: '5130441f-e4c7-4750-9692-a22d841ab049', + value: '' + }, { key: 'availableThemes', value: [ diff --git a/core/client/app/models/setting.js b/core/client/app/models/setting.js index ec006210ae..c8b25efb74 100644 --- a/core/client/app/models/setting.js +++ b/core/client/app/models/setting.js @@ -18,6 +18,8 @@ export default Model.extend(ValidationEngine, { availableThemes: attr(), ghost_head: attr('string'), ghost_foot: attr('string'), + facebook: attr('string'), + twitter: attr('twitter-url-user'), labs: attr('string'), navigation: attr('navigation-settings'), isPrivate: attr('boolean'), diff --git a/core/client/app/models/user.js b/core/client/app/models/user.js index 5b6c98995b..225bbb343e 100644 --- a/core/client/app/models/user.js +++ b/core/client/app/models/user.js @@ -38,6 +38,8 @@ export default Model.extend(ValidationEngine, { async: false }), count: attr('raw'), + facebook: attr('string'), + twitter: attr('string'), ghostPaths: service(), ajax: service(), diff --git a/core/client/app/routes/settings/general.js b/core/client/app/routes/settings/general.js index 6d38ac205f..56e5be0fc4 100644 --- a/core/client/app/routes/settings/general.js +++ b/core/client/app/routes/settings/general.js @@ -15,9 +15,7 @@ export default AuthenticatedRoute.extend(styleBody, CurrentUserSettings, { }, model() { - return this.store.query('setting', {type: 'blog,theme,private'}).then((records) => { - return records.get('firstObject'); - }); + return this.store.queryRecord('setting', {type: 'blog,theme,private'}); }, actions: { diff --git a/core/client/app/templates/settings/general.hbs b/core/client/app/templates/settings/general.hbs index 1cc9e290c0..495791a206 100644 --- a/core/client/app/templates/settings/general.hbs +++ b/core/client/app/templates/settings/general.hbs @@ -35,7 +35,7 @@ {{else}} {{/if}} -

Display a sexy logo for your publication

+

Display a logo for your publication

{{#if showUploadLogoModal}} {{gh-fullscreen-modal "upload-image" @@ -96,6 +96,23 @@

Select a theme for your blog

+
+ {{#gh-form-group errors=model.errors hasValidated=model.hasValidated property="facebook"}} + + + {{gh-error-message errors=model.errors property="facebook"}} +

URL of your blog's Facebook Page

+ {{/gh-form-group}} +
+
+ {{#gh-form-group errors=model.errors hasValidated=model.hasValidated property="twitter"}} + + + {{gh-error-message errors=model.errors property="twitter"}} +

URL of your blog's Twitter profile

+ {{/gh-form-group}} +
+