Skip to content

Commit

Permalink
Facebook and Twitter input fields in gen. settings
Browse files Browse the repository at this point in the history
closes TryGhost#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}}:
	- '<meta property="article:publisher" content="https://www.facebook.com/page" />' and
	- '<meta name="twitter:site" content="@user"/>'
- 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
  • Loading branch information
aileen committed May 5, 2016
1 parent 3f50f11 commit ef6fd1d
Show file tree
Hide file tree
Showing 13 changed files with 258 additions and 18 deletions.
71 changes: 71 additions & 0 deletions core/client/app/controllers/settings/general.js
Expand Up @@ -76,6 +76,14 @@ export default Controller.extend(SettingsSaveMixin, {
}
}),

facebookUrl: computed('model.facebook', function () {
return this.get('model.facebook');
}),

twitterUrl: computed('model.twitter', function () {
return this.get('model.twitter');
}),

save() {
let notifications = this.get('notifications');
let config = this.get('config');
Expand Down Expand Up @@ -110,6 +118,69 @@ export default Controller.extend(SettingsSaveMixin, {

toggleUploadLogoModal() {
this.toggleProperty('showUploadLogoModal');
},

validateFacebookUrl(userInput) {
let newUrl = userInput.trim();
let oldUrl = this.get('model.facebook');
let errMessage = '';

if (!newUrl) {
// Clear out the Facebook url
this.set('model.facebook', '');
return;
}

if (!newUrl.match(/(^https:\/\/www\.facebook\.com\/)(\S+)/g)) {
errMessage = 'The URL must be in a format like ' +
'https://www.facebook.com/yourUsername';
this.get('model.errors').add('facebook', errMessage);
return;
}

// If new url didn't change, exit
if (newUrl === oldUrl) {
return;
}

// Validation complete
this.set('model.facebook', newUrl);

this.get('model').save().catch((errors) => {
this.showErrors(errors);
this.get('model').rollbackAttributes();
});
},

validateTwitterUrl(userInput) {
let newUrl = userInput.trim();
let oldUrl = this.get('model.twitter');
let errMessage = '';

if (!newUrl) {
// Clear out the Twitter url
this.set('model.twitter', '');
return;
}

if (!newUrl.match(/(^https:\/\/twitter\.com\/)(\S+)/g)) {
errMessage = 'The URL must be in a format like ' +
'https://twitter.com/yourUsername';
this.get('model.errors').add('twitter', errMessage);
return;
}

// If new url didn't change, exit
if (newUrl === oldUrl) {
return;
}

this.set('model.twitter', newUrl);

this.get('model').save().catch((errors) => {
this.showErrors(errors);
this.get('model').rollbackAttributes();
});
}
}
});
22 changes: 22 additions & 0 deletions core/client/app/mirage/fixtures/settings.js
Expand Up @@ -168,6 +168,28 @@ export default [
uuid: 'f8e8cbda-d079-11e5-ab30-625662870761',
value: ''
},
{
created_at: '2015-09-11T09:44:30.810Z',
created_by: 1,
id: 16,
key: 'facebook',
type: 'blog',
updated_at: '2015-09-23T13:32:49.868Z',
updated_by: 1,
uuid: 'f8e8cbda-d079-11e5-ab30-625662870761',
value: ''
},
{
created_at: '2015-09-11T09:44:30.810Z',
created_by: 1,
id: 17,
key: 'twitter',
type: 'blog',
updated_at: '2015-09-23T13:32:49.868Z',
updated_by: 1,
uuid: 'f8e8cbda-d079-11e5-ab30-625662870761',
value: ''
},
{
key: 'availableThemes',
value: [
Expand Down
2 changes: 2 additions & 0 deletions core/client/app/models/setting.js
Expand Up @@ -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'),
Expand Down
22 changes: 22 additions & 0 deletions core/client/app/templates/settings/general.hbs
Expand Up @@ -114,6 +114,28 @@
{{/gh-form-group}}
{{/if}}
</fieldset>

<fieldset>
{{#gh-form-group errors=model.errors hasValidated=model.hasValidated property="facebook"}}
<label for="facebook">Facebook</label>
{{gh-input class="gh-input" id="facebook" value=facebookUrl name="general[facebook]" focus-out="validateFacebookUrl" placeholder="https://www.facebook.com/<your username>" autocorrect="off"}}
{{#unless model.errors.facebook}}
<p>The URL of your Facebook account</p>
{{else}}
{{gh-error-message errors=model.errors property="facebook"}}
{{/unless}}
{{/gh-form-group}}

{{#gh-form-group errors=model.errors hasValidated=model.hasValidated property="twitter"}}
<label for="twitter">Twitter</label>
{{gh-input class="gh-input" id="twitter" value=twitterUrl name="general[twitter]" focus-out="validateTwitterUrl" placeholder="https://twitter.com/<your username>" autocorrect="off"}}
{{#unless model.errors.twitter}}
<p>The URL of your Twitter account</p>
{{else}}
{{gh-error-message errors=model.errors property="twitter"}}
{{/unless}}
{{/gh-form-group}}
</fieldset>
</form>
</section>
</section>
27 changes: 27 additions & 0 deletions core/client/app/transforms/twitter-url-user.js
@@ -0,0 +1,27 @@
import Transform from 'ember-data/transform';

export default Transform.extend({
deserialize(serialized) {
if (serialized) {
let url = 'https://twitter.com/';
let modelVal = serialized;
let [ , user ] = modelVal.match(/@?([^\/]*)/);

url = modelVal ? url + user : modelVal;

return url;
}
return serialized;
},

serialize(deserialized) {
if (deserialized) {
let username = '@';
let [ , user] = deserialized.match(/(?:https:\/\/)(?:twitter\.com)\/(?:#!\/)?@?([^\/]*)/);
username = username + user;

return username;
}
return deserialized;
}
});
57 changes: 45 additions & 12 deletions core/client/tests/acceptance/settings/general-test.js
Expand Up @@ -119,25 +119,15 @@ describe('Acceptance: Settings - General', function () {
andThen(() => {
expect(find('.fullscreen-modal').length).to.equal(0);
});
});

it('renders theme selector correctly', function () {
visit('/settings/general');

// renders theme selector correctly
andThen(() => {
expect(currentURL(), 'currentURL').to.equal('/settings/general');

expect(find('#activeTheme select option').length, 'available themes').to.equal(1);
expect(find('#activeTheme select option').text().trim()).to.equal('Blog - 1.0');
});
});

it('handles private blog settings correctly', function () {
visit('/settings/general');

// handles private blog settings correctly
andThen(() => {
expect(currentURL(), 'currentURL').to.equal('/settings/general');

expect(find('input#isPrivate').prop('checked'), 'isPrivate checkbox').to.be.false;
});

Expand All @@ -156,6 +146,49 @@ describe('Acceptance: Settings - General', function () {
expect(find('#settings-general .error .response').text().trim(), 'inline validation response')
.to.equal('Password must be supplied');
});

fillIn('#settings-general input[name="general[password]"]', 'asdfg');
click('.view-header .view-actions .btn-blue');

andThen(() => {
expect(find('#settings-general .error .response').text().trim(), 'inline validation response')
.to.equal('');
});

// validates a facebook url correctly
fillIn('#settings-general input[name="general[facebook]"]', 'http://facebook.com/username');
triggerEvent('#settings-general input[name="general[facebook]"]', 'blur');

andThen(() => {
expect(find('#settings-general .response').text().trim(), 'inline validation response')
.to.equal('The URL must be in a format like https://www.facebook.com/yourUsername');
});

fillIn('#settings-general input[name="general[facebook]"]', 'https://www.facebook.com/username');
triggerEvent('#settings-general input[name="general[facebook]"]', 'blur');

andThen(() => {
expect(find('#settings-general .error .response').text().trim(), 'inline validation response')
.to.equal('');
});

// validates a twitter url correctly
fillIn('#settings-general input[name="general[twitter]"]', 'http://twitter.com/username');
triggerEvent('#settings-general input[name="general[twitter]"]', 'blur');

andThen(() => {
expect(find('#settings-general .response').text().trim(), 'inline validation response')
.to.equal('The URL must be in a format like https://twitter.com/yourUsername');
});

fillIn('#settings-general input[name="general[twitter]"]', 'https://twitter.com/username');
triggerEvent('#settings-general input[name="general[twitter]"]', 'blur');

andThen(() => {
expect(find('#settings-general .response').text().trim(), 'inline validation response')
.to.equal('');
});
});

});
});
12 changes: 12 additions & 0 deletions core/client/tests/unit/controllers/settings/general-test.js
Expand Up @@ -89,5 +89,17 @@ describeModule(
expect(themes.objectAt(1).active).to.not.be.ok;
expect(themes.objectAt(1).label).to.equal('Rasper - 1.0.0');
});

it('renders twitter username with the matching url', function () {
let controller = this.subject({
model: Ember.Object.create({
twitter: 'https://twitter.com/testuser'
})
});

run(function () {
expect(controller.get('twitterUrl')).to.equal('https://twitter.com/testuser');
});
});
}
);
32 changes: 32 additions & 0 deletions core/client/tests/unit/transforms/twitter-url-user-test.js
@@ -0,0 +1,32 @@
/* jshint expr:true */
import { expect } from 'chai';
import { describeModule, it } from 'ember-mocha';
import Ember from 'ember';

const emberA = Ember.A;

describeModule(
'transform:twitter-url-user',
'Unit: Transform: twitter-url-user',
{
// Specify the other units that are required for this test.
// needs: ['transform:foo']
},
function() {
it('deserializes twitter url', function () {
let transform = this.subject();
let serialized = '@testuser';
let result = transform.deserialize(serialized);

expect(result).to.equal('https://twitter.com/testuser');
});

it('serializes url to twitter username', function () {
let transform = this.subject();
let deserialized = 'https://twitter.com/testuser';
let result = transform.serialize(deserialized);

expect(result).to.equal('@testuser');
});
}
);
4 changes: 3 additions & 1 deletion core/server/api/settings.js
Expand Up @@ -58,7 +58,9 @@ updateConfigCache = function () {
cover: (settingsCache.cover && settingsCache.cover.value) || '',
navigation: (settingsCache.navigation && JSON.parse(settingsCache.navigation.value)) || [],
postsPerPage: (settingsCache.postsPerPage && settingsCache.postsPerPage.value) || 5,
permalinks: (settingsCache.permalinks && settingsCache.permalinks.value) || '/:slug/'
permalinks: (settingsCache.permalinks && settingsCache.permalinks.value) || '/:slug/',
twitter: (settingsCache.twitter && settingsCache.twitter.value) || '',
facebook: (settingsCache.facebook && settingsCache.facebook.value) || ''
},
labs: labsValue
});
Expand Down
4 changes: 3 additions & 1 deletion core/server/data/meta/structured_data.js
Expand Up @@ -16,6 +16,7 @@ function getStructuredData(metaData) {
'article:published_time': metaData.publishedDate,
'article:modified_time': metaData.modifiedDate,
'article:tag': metaData.keywords,
'article:publisher': metaData.blog.facebook || undefined,
'twitter:card': card,
'twitter:title': metaData.metaTitle,
'twitter:description': metaData.metaDescription || metaData.excerpt,
Expand All @@ -24,7 +25,8 @@ function getStructuredData(metaData) {
'twitter:label1': metaData.authorName ? 'Written by' : undefined,
'twitter:data1': metaData.authorName,
'twitter:label2': metaData.keywords ? 'Filed under' : undefined,
'twitter:data2': metaData.keywords ? metaData.keywords.join(', ') : undefined
'twitter:data2': metaData.keywords ? metaData.keywords.join(', ') : undefined,
'twitter:site': metaData.blog.twitter || undefined
};

// return structured data removing null or undefined keys
Expand Down
6 changes: 6 additions & 0 deletions core/server/data/schema/default-settings.json
Expand Up @@ -61,6 +61,12 @@
"ghost_foot": {
"defaultValue" : ""
},
"facebook": {
"defaultValue" : ""
},
"twitter": {
"defaultValue" : ""
},
"labs": {
"defaultValue": "{}"
},
Expand Down
1 change: 1 addition & 0 deletions core/server/helpers/ghost_head.js
Expand Up @@ -75,6 +75,7 @@ function ghost_head(options) {
if (this.statusCode >= 400) {
return;
}

var metaData = getMetaData(this, options.data.root),
head = [],
context = this.context ? this.context[0] : null,
Expand Down

0 comments on commit ef6fd1d

Please sign in to comment.