Skip to content

Commit

Permalink
Timezones: Always use the timezone of blog setting
Browse files Browse the repository at this point in the history
closes TryGhost/Ghost#6406

follow-up PR of TryGhost#2

- adds a `timeZone` Service to provide the offset (=timezone reg. moment-timezone) of the users blog settings
- `gh-datetime-input` will read the offset of the timezone now and adjust the `publishedAt` date with it. This is the date which will be shown in the PSM 'Publish Date' field. When the user writes a new date/time, the offset is considered and will be deducted again before saving it to the model. This way, we always work with a UTC publish date except for this input field.
- gets `availableTimezones` from `configuration/timezones` API endpoint
- adds a `moment-utc` transform on all `createdAt`, `updatedAt` and `publishedAt` date properties to only work with UTC times on serverside
- when switching the timezone in the select box, the user will be shown the local time of the selected timezone
- `createdAt`-property in `gh-user-invited` returns now `moment(createdAt).fromNow()` as `createdAt` is a moment date already
- added clock service to show actual time ticking below select box
- default timezone is '(GMT) Greenwich Mean Time : Dublin, Edinburgh, London'
- if no timezone is saved in the settings yet, the default value will be used
- shows the local time in 'Publish Date'  in PSM by default, until user overwrites it
- adds dependency `moment-timezone 0.5.4` to `bower.json`

---------

**Tests:**

- sets except for clock service in test env
- adds fixtures to mirage
- adds `service.ajax` and `service:ghostPaths` to navigation-test.js
- adds unit test for `gh-format-timeago` helper
- updates acceptance test `general-setting`
- adds acceptance test for `editor`

---------

**Todos:**

- [ ] Integration tests: ~~`services/config`~~, `services/time-zone`, `components/gh-datetime-input`
- [x] Acceptance test: `editor`
- [ ] Unit tests: `utils/date-formatting`
- [ ] write issue for renaming date properties (e. g. `createdAt` to `createdAtUTC`) and translate those for server side with serializers + use `moment-utc` transform in scheduler model
  • Loading branch information
aileen committed Jun 2, 2016
1 parent 9c72484 commit ea99c23
Show file tree
Hide file tree
Showing 33 changed files with 1,011 additions and 64 deletions.
17 changes: 14 additions & 3 deletions app/components/gh-datetime-input.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@ import boundOneWay from 'ghost/utils/bound-one-way';
import {formatDate} from 'ghost/utils/date-formatting';
import {invokeAction} from 'ember-invoke-action';

const {Component} = Ember;
const {
Component,
RSVP,
inject: {service},
computed
} = Ember;

export default Component.extend(TextInputMixin, {
tagName: 'span',
Expand All @@ -14,15 +19,21 @@ export default Component.extend(TextInputMixin, {
inputClass: null,
inputId: null,
inputName: null,
timeZone: service(),

didReceiveAttrs() {
let datetime = this.get('datetime') || moment();
let promises = {
datetime: RSVP.resolve(this.get('datetime') || moment.utc()),
offset: RSVP.resolve(this.get('timeZone.offset'))
};

if (!this.get('update')) {
throw new Error(`You must provide an \`update\` action to \`{{${this.templateName}}}\`.`);
}

this.set('datetime', formatDate(datetime));
RSVP.hash(promises).then((hash) => {
this.set('datetime', formatDate(hash.datetime || moment.utc(), hash.offset));
});
},

focusOut() {
Expand Down
2 changes: 1 addition & 1 deletion app/components/gh-user-active.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,6 @@ export default Component.extend({
lastLogin: computed('user.lastLogin', function () {
let lastLogin = this.get('user.lastLogin');

return lastLogin ? lastLogin.fromNow() : '(Never)';
return lastLogin ? moment(lastLogin).fromNow() : '(Never)';
})
});
2 changes: 1 addition & 1 deletion app/components/gh-user-invited.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export default Component.extend({
createdAt: computed('user.createdAt', function () {
let createdAt = this.get('user.createdAt');

return createdAt ? createdAt.fromNow() : '';
return createdAt ? moment(createdAt).fromNow() : '';
}),

actions: {
Expand Down
72 changes: 39 additions & 33 deletions app/controllers/post-settings-menu.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export default Controller.extend(SettingsMenuMixin, {
notifications: service(),
session: service(),
slugGenerator: service(),
timeZone: service(),

initializeSelectedAuthor: observer('model', function () {
return this.get('model.author').then((author) => {
Expand Down Expand Up @@ -294,48 +295,53 @@ export default Controller.extend(SettingsMenuMixin, {
if (this.get('model.isDraft')) {
this.set('model.publishedAt', null);
}

return;
}
this.get('timeZone.offset').then((offset) => {
let newPublishedAt = parseDateString(userInput, offset);
let publishedAt = moment.utc(this.get('model.publishedAt'));
let errMessage = '';

// Clear previous errors
this.get('model.errors').remove('post-setting-date');

// Validate new Published date
if (!newPublishedAt.isValid()) {
errMessage = 'Published Date must be a valid date with format: ' +
'DD MMM YY @ HH:mm (e.g. 6 Dec 14 @ 15:00)';
}

let newPublishedAt = parseDateString(userInput);
let publishedAt = moment(this.get('model.publishedAt'));
let errMessage = '';

// Clear previous errors
this.get('model.errors').remove('post-setting-date');
// Date is a valid date, so now make it UTC
newPublishedAt = moment.utc(newPublishedAt);

// Validate new Published date
if (!newPublishedAt.isValid()) {
errMessage = 'Published Date must be a valid date with format: ' +
'DD MMM YY @ HH:mm (e.g. 6 Dec 14 @ 15:00)';
} else if (newPublishedAt.diff(new Date(), 'h') > 0) {
errMessage = 'Published Date cannot currently be in the future.';
}
if (newPublishedAt.diff(moment.utc(new Date()), 'hours', true) > 0) {
errMessage = 'Published Date cannot currently be in the future.';
}

// If errors, notify and exit.
if (errMessage) {
this.get('model.errors').add('post-setting-date', errMessage);
return;
}
// If errors, notify and exit.
if (errMessage) {
this.get('model.errors').add('post-setting-date', errMessage);
return;
}

// Validation complete, update the view
this.set('model.publishedAt', newPublishedAt);
// Do nothing if the user didn't actually change the date
if (publishedAt && publishedAt.isSame(newPublishedAt)) {
return;
}

// Don't save the date if the user didn't actually changed the date
if (publishedAt && publishedAt.isSame(newPublishedAt)) {
return;
}
// Validation complete
this.set('model.publishedAt', newPublishedAt);

// If this is a new post. Don't save the model. Defer the save
// to the user pressing the save button
if (this.get('model.isNew')) {
return;
}
// If this is a new post. Don't save the model. Defer the save
// to the user pressing the save button
if (this.get('model.isNew')) {
return;
}

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

Expand Down
23 changes: 21 additions & 2 deletions app/controllers/settings/general.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,13 @@ export default Controller.extend(SettingsSaveMixin, {
showUploadLogoModal: false,
showUploadCoverModal: false,

availableTimezones: null,

notifications: service(),
config: service(),
_scratchFacebook: null,
_scratchTwitter: null,
clock: service(),

selectedTheme: computed('model.activeTheme', 'themes', function () {
let activeTheme = this.get('model.activeTheme');
Expand All @@ -34,6 +37,15 @@ export default Controller.extend(SettingsSaveMixin, {
return selectedTheme;
}),

selectedTimezone: computed('model.activeTimezone', 'availableTimezones', function () {
let activeTimezone = this.get('model.activeTimezone');
let availableTimezones = this.get('availableTimezones');

return availableTimezones
.filterBy('name', activeTimezone)
.get('firstObject');
}),

logoImageSource: computed('model.logo', function () {
return this.get('model.logo') || '';
}),
Expand All @@ -42,6 +54,12 @@ export default Controller.extend(SettingsSaveMixin, {
return this.get('model.cover') || '';
}),

localTime: computed('selectedTimezone', 'clock.second', function () {
let timezone = this.get('selectedTimezone.name');
this.get('clock.second');
return timezone ? moment().tz(timezone).format('HH:mm:ss') : moment().utc().format('HH:mm:ss');
}),

isDatedPermalinks: computed('model.permalinks', {
set(key, value) {
this.set('model.permalinks', value ? '/:year/:month/:day/:slug/' : '/:slug/');
Expand Down Expand Up @@ -82,7 +100,6 @@ export default Controller.extend(SettingsSaveMixin, {
save() {
let notifications = this.get('notifications');
let config = this.get('config');

return this.get('model').save().then((model) => {
config.set('blogTitle', model.get('title'));

Expand Down Expand Up @@ -111,7 +128,9 @@ export default Controller.extend(SettingsSaveMixin, {
setTheme(theme) {
this.set('model.activeTheme', theme.name);
},

setTimezone(timezone) {
this.set('model.activeTimezone', timezone.name);
},
toggleUploadCoverModal() {
this.toggleProperty('showUploadCoverModal');
},
Expand Down
10 changes: 7 additions & 3 deletions app/helpers/gh-format-timeago.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,18 @@ import Ember from 'ember';

const {Helper} = Ember;

export default Helper.helper(function (params) {
export function timeAgo(params) {
if (!params || !params.length) {
return;
}

let [timeago] = params;
let utc = moment.utc();

return moment(timeago).fromNow();
return moment(timeago).from(utc);
}

export default Helper.helper(function (params) {
return timeAgo(params);
// stefanpenner says cool for small number of timeagos.
// For large numbers moment sucks => single Ember.Object based clock better
// https://github.com/manuelmitasch/ghost-admin-ember-demo/commit/fba3ab0a59238290c85d4fa0d7c6ed1be2a8a82e#commitcomment-5396524
Expand Down
35 changes: 35 additions & 0 deletions app/mirage/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,31 @@ export function testConfig() {
return response;
});

this.get('/posts/:id', function (db, request) {
let {id} = request.params;
let post = db.posts.find(id);

return {
posts: [post]
};
});

this.put('/posts/:id', function (db, request) {
let [attrs] = JSON.parse(request.requestBody).posts;
let post;

if (isBlank(attrs.slug) && !isBlank(attrs.title)) {
attrs.slug = attrs.title.dasherize();
}

// NOTE: this does not use the post factory to fill in blank fields
post = db.posts.insert(attrs);

return {
posts: [post]
};
});

this.del('/posts/:id/', function (db, request) {
db.posts.remove(request.params.id);

Expand Down Expand Up @@ -264,6 +289,16 @@ export function testConfig() {
return {};
});

/* Configuration -------------------------------------------------------- */

this.get('/configuration/timezones/', function (db) {
return {
configuration: [{
timezones: db.timezones
}]
};
});

/* Slugs ---------------------------------------------------------------- */

this.get('/slugs/post/:slug/', function (db, request) {
Expand Down
7 changes: 4 additions & 3 deletions app/mirage/factories/post.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,15 @@ export default Mirage.Factory.extend({
image(i) { return `/content/images/2015/10/post-${i}.jpg`; },
featured() { return false; },
page() { return false; },
status(i) { return `/content/images/2015/10/post-${i}.jpg`; },
status(i) { return faker.list.cycle('draft', 'published')(i); },
meta_description(i) { return `Meta description for post ${i}.`; },
meta_title(i) { return `Meta Title for post ${i}`; },
author_id() { return 1; },
updated_at() { return '2015-10-19T16:25:07.756Z'; },
updated_by() { return 1; },
published_at() { return '2015-10-19T16:25:07.756Z'; },
published_at() { return '2015-12-19T16:25:07.000Z'; },
published_by() { return 1; },
created_at() { return '2015-09-11T09:44:29.871Z'; },
created_by() { return 1; }
created_by() { return 1; },
tags(i) { return []; }
});
11 changes: 11 additions & 0 deletions app/mirage/fixtures/settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,17 @@ export default [
uuid: '5130441f-e4c7-4750-9692-a22d841ab049',
value: '@test'
},
{
created_at: '2015-09-11T09:44:30.810Z',
created_by: 1,
id: 16,
key: 'activeTimezone',
type: 'blog',
updated_at: '2015-09-23T13:32:49.868Z',
updated_by: 1,
uuid: '310c9169-9613-48b0-8bc4-d1e1c9be85b8',
value: 'Europe/Dublin'
},
{
key: 'availableThemes',
value: [
Expand Down
Loading

0 comments on commit ea99c23

Please sign in to comment.