Renaming date properties to contain UTC

closes TryGhost/Ghost#6985

- renames all date properties to end with `UTC`:
	- `publishedAt` in `post` model
	- `createdAt` in `post`, `role`, `subscriber`, `tag` and `user` model
	- `updatedAt` in `post`, `role`, `subscriber`, `tag` and `user` model
	- `unsubscribedAt` in `subscriber` model
	- `lastLogin` in `user` model
- adds `attrs` transforms in matching serializers `post`, `tag` and `user`
- new serializers files for `subscribers` and `role` to add `attr` transforms
- adds unit tests for all serializers
- use two variables in `post-settings-menu` controller to handle blog-timezone adjusted date as well as UTC date
This commit is contained in:
Aileen Nowak 2016-06-14 10:11:59 +02:00
parent 0028be64aa
commit d90ed28940
27 changed files with 235 additions and 54 deletions

View File

@ -24,9 +24,9 @@ export default Component.extend({
return htmlSafe(`background-image: url(${url})`);
}),
lastLogin: computed('user.lastLogin', function () {
let lastLogin = this.get('user.lastLogin');
lastLoginUTC: computed('user.lastLoginUTC', function () {
let lastLoginUTC = this.get('user.lastLoginUTC');
return lastLogin ? moment(lastLogin).fromNow() : '(Never)';
return lastLoginUTC ? moment(lastLoginUTC).fromNow() : '(Never)';
})
});

View File

@ -14,10 +14,10 @@ export default Component.extend({
notifications: service(),
createdAt: computed('user.createdAt', function () {
let createdAt = this.get('user.createdAt');
createdAtUTC: computed('user.createdAtUTC', function () {
let createdAtUTC = this.get('user.createdAtUTC');
return createdAt ? moment(createdAt).fromNow() : '';
return createdAtUTC ? moment(createdAtUTC).fromNow() : '';
}),
actions: {

View File

@ -290,11 +290,11 @@ export default Controller.extend(SettingsMenuMixin, {
* Action sent by post settings menu view.
* (#1351)
*/
setPublishedAt(userInput) {
setPublishedAtUTC(userInput) {
if (!userInput) {
// Clear out the publishedAt field for a draft
// Clear out the publishedAtUTC field for a draft
if (this.get('model.isDraft')) {
this.set('model.publishedAt', null);
this.set('model.publishedAtUTC', null);
}
return;
}
@ -303,8 +303,9 @@ export default Controller.extend(SettingsMenuMixin, {
// we have to work with the timezone offset which we get from the timeZone Service.
this.get('timeZone.blogTimezone').then((blogTimezone) => {
let newPublishedAt = parseDateString(userInput, blogTimezone);
let publishedAt = moment.utc(this.get('model.publishedAt'));
let publishedAtUTC = moment.utc(this.get('model.publishedAtUTC'));
let errMessage = '';
let newPublishedAtUTC;
// Clear previous errors
this.get('model.errors').remove('post-setting-date');
@ -316,13 +317,13 @@ export default Controller.extend(SettingsMenuMixin, {
}
// Date is a valid date, so now make it UTC
newPublishedAt = moment.utc(newPublishedAt);
newPublishedAtUTC = moment.utc(newPublishedAt);
if (newPublishedAt.diff(moment.utc(new Date()), 'hours', true) > 0) {
if (newPublishedAtUTC.diff(moment.utc(new Date()), 'hours', true) > 0) {
// We have to check that the time from now is not shorter than 2 minutes,
// otherwise we'll have issues with the serverside scheduling procedure
if (newPublishedAt.diff(moment.utc(new Date()), 'minutes', true) < 2) {
if (newPublishedAtUTC.diff(moment.utc(new Date()), 'minutes', true) < 2) {
errMessage = 'Must be at least 2 minutes from now.';
} else {
// in case the post is already published and the user sets the date
@ -351,12 +352,12 @@ export default Controller.extend(SettingsMenuMixin, {
}
// Do nothing if the user didn't actually change the date
if (publishedAt && publishedAt.isSame(newPublishedAt)) {
if (publishedAtUTC && publishedAtUTC.isSame(newPublishedAtUTC)) {
return;
}
// Validation complete
this.set('model.publishedAt', newPublishedAt);
this.set('model.publishedAtUTC', newPublishedAtUTC);
// If this is a new post. Don't save the model. Defer the save
// to the user pressing the save button
@ -440,7 +441,7 @@ export default Controller.extend(SettingsMenuMixin, {
},
resetPubDate() {
this.set('publishedAtValue', '');
this.set('publishedAtUTCValue', '');
},
closeNavMenu() {

View File

@ -10,12 +10,12 @@ const {equal} = computed;
// a custom sort function is needed in order to sort the posts list the same way the server would:
// status: scheduled, draft, published
// publishedAt: DESC
// updatedAt: DESC
// publishedAtUTC: DESC
// updatedAtUTC: DESC
// id: DESC
function comparator(item1, item2) {
let updated1 = item1.get('updatedAt');
let updated2 = item2.get('updatedAt');
let updated1 = item1.get('updatedAtUTC');
let updated2 = item2.get('updatedAtUTC');
let idResult,
publishedAtResult,
statusResult,
@ -85,8 +85,8 @@ function statusCompare(item1, item2) {
}
function publishedAtCompare(item1, item2) {
let published1 = item1.get('publishedAt');
let published2 = item2.get('publishedAt');
let published1 = item1.get('publishedAtUTC');
let published2 = item2.get('publishedAtUTC');
if (!published1 && !published2) {
return 0;
@ -112,7 +112,7 @@ export default Controller.extend({
postListFocused: equal('keyboardFocus', 'postList'),
postContentFocused: equal('keyboardFocus', 'postContent'),
sortedPosts: computed('model.@each.status', 'model.@each.publishedAt', 'model.@each.isNew', 'model.@each.updatedAt', function () {
sortedPosts: computed('model.@each.status', 'model.@each.publishedAtUTC', 'model.@each.isNew', 'model.@each.updatedAtUTC', function () {
let postsArray = this.get('model').toArray();
return postsArray.sort(comparator);

View File

@ -57,7 +57,7 @@ export default Controller.extend(PaginationMixin, {
ascending: direction === 'asc'
}, {
label: 'Subscription Date',
valuePath: 'createdAt',
valuePath: 'createdAtUTC',
format(value) {
return value.format('MMMM DD, YYYY');
},
@ -111,7 +111,7 @@ export default Controller.extend(PaginationMixin, {
if (column.sorted) {
this.setProperties({
order: column.get('valuePath').trim().underscore(),
order: column.get('valuePath').trim().replace(/UTC$/, '').underscore(),
direction: column.ascending ? 'asc' : 'desc'
});
table.setRows([]);

View File

@ -86,9 +86,9 @@ export default Mixin.create({
// countdown timer to show the time left until publish time for a scheduled post
// starts 15 minutes before scheduled time
scheduleCountdown: computed('model.status', 'clock.second', 'model.publishedAt', 'model.timeScheduled', function () {
scheduleCountdown: computed('model.status', 'clock.second', 'model.publishedAtUTC', 'model.timeScheduled', function () {
let status = this.get('model.status');
let publishTime = this.get('model.publishedAt');
let publishTime = this.get('model.publishedAtUTC');
this.get('clock.second');
@ -104,9 +104,9 @@ export default Mixin.create({
// dropdown menu, the save button gets the status 'isDangerous' to turn red and will only have the option to unschedule the post
// 2. when the scheduled time is reached we use a helper 'scheduledWillPublish' to pretend we're already dealing with a published post.
// This will take effect on the save button menu, the workflows and existing conditionals.
statusFreeze: computed('model.status', 'clock.second', 'model.publishedAt', 'model.timeScheduled', function () {
statusFreeze: computed('model.status', 'clock.second', 'model.publishedAtUTC', 'model.timeScheduled', function () {
let status = this.get('model.status');
let publishTime = this.get('model.publishedAt');
let publishTime = this.get('model.publishedAtUTC');
this.get('clock.second');

View File

@ -28,11 +28,11 @@ export default Model.extend(ValidationEngine, {
metaDescription: attr('string'),
author: belongsTo('user', {async: true}),
authorId: attr('number'),
updatedAt: attr('moment-utc'),
updatedAtUTC: attr('moment-utc'),
updatedBy: attr(),
publishedAt: attr('moment-utc'),
publishedAtUTC: attr('moment-utc'),
publishedBy: belongsTo('user', {async: true}),
createdAt: attr('moment-utc'),
createdAtUTC: attr('moment-utc'),
createdBy: attr(),
tags: hasMany('tag', {
embedded: 'always',
@ -74,11 +74,11 @@ export default Model.extend(ValidationEngine, {
// TODO: move this into gh-posts-list-item component
// Checks every second, if we reached the scheduled date
timeScheduled: computed('publishedAt', 'clock.second', function () {
let publishedAt = this.get('publishedAt') || moment.utc(new Date());
timeScheduled: computed('publishedAtUTC', 'clock.second', function () {
let publishedAtUTC = this.get('publishedAtUTC') || moment.utc(new Date());
this.get('clock.second');
return publishedAt.diff(moment.utc(new Date()), 'hours', true) > 0 ? true : false;
return publishedAtUTC.diff(moment.utc(new Date()), 'hours', true) > 0 ? true : false;
}),
// remove client-generated tags, which have `id: null`.

View File

@ -9,8 +9,8 @@ export default Model.extend({
uuid: attr('string'),
name: attr('string'),
description: attr('string'),
createdAt: attr('moment-utc'),
updatedAt: attr('moment-utc'),
createdAtUTC: attr('moment-utc'),
updatedAtUTC: attr('moment-utc'),
createdBy: attr(),
updatedBy: attr(),

View File

@ -13,9 +13,9 @@ export default Model.extend(ValidationEngine, {
subscribedUrl: attr('string'),
subscribedReferrer: attr('string'),
unsubscribedUrl: attr('string'),
unsubscribedAt: attr('moment-utc'),
createdAt: attr('moment-utc'),
updatedAt: attr('moment-utc'),
unsubscribedAtUTC: attr('moment-utc'),
createdAtUTC: attr('moment-utc'),
updatedAtUTC: attr('moment-utc'),
createdBy: attr('number'),
updatedBy: attr('number'),

View File

@ -24,8 +24,8 @@ export default Model.extend(ValidationEngine, {
metaDescription: attr('string'),
image: attr('string'),
visibility: attr('string', {defaultValue: 'public'}),
createdAt: attr('moment-utc'),
updatedAt: attr('moment-utc'),
createdAtUTC: attr('moment-utc'),
updatedAtUTC: attr('moment-utc'),
createdBy: attr(),
updatedBy: attr(),
count: attr('raw'),

View File

@ -28,10 +28,10 @@ export default Model.extend(ValidationEngine, {
language: attr('string', {defaultValue: 'en_US'}),
metaTitle: attr('string'),
metaDescription: attr('string'),
lastLogin: attr('moment-utc'),
createdAt: attr('moment-utc'),
lastLoginUTC: attr('moment-utc'),
createdAtUTC: attr('moment-utc'),
createdBy: attr('number'),
updatedAt: attr('moment-utc'),
updatedAtUTC: attr('moment-utc'),
updatedBy: attr('number'),
roles: hasMany('role', {
embedded: 'always',

View File

@ -10,7 +10,10 @@ const {
export default ApplicationSerializer.extend(EmbeddedRecordsMixin, {
// settings for the EmbeddedRecordsMixin.
attrs: {
tags: {embedded: 'always'}
tags: {embedded: 'always'},
publishedAtUTC: {key: 'published_at'},
createdAtUTC: {key: 'created_at'},
updatedAtUTC: {key: 'updated_at'}
},
normalize(model, hash, prop) {

View File

@ -0,0 +1,8 @@
import ApplicationSerializer from 'ghost-admin/serializers/application';
export default ApplicationSerializer.extend({
attrs: {
createdAtUTC: {key: 'created_at'},
updatedAtUTC: {key: 'updated_at'}
}
});

View File

@ -0,0 +1,9 @@
import ApplicationSerializer from 'ghost-admin/serializers/application';
export default ApplicationSerializer.extend({
attrs: {
unsubscribedAtUTC: {key: 'unsubscribed_at'},
createdAtUTC: {key: 'created_at'},
updatedAtUTC: {key: 'updated_at'}
}
});

View File

@ -7,6 +7,11 @@ const {
} = Ember;
export default ApplicationSerializer.extend({
attrs: {
createdAtUTC: {key: 'created_at'},
updatedAtUTC: {key: 'updated_at'}
},
serializeIntoHash(hash, type, record, options) {
options = options || {};
options.includeId = true;

View File

@ -8,7 +8,10 @@ const {
export default ApplicationSerializer.extend(EmbeddedRecordsMixin, {
attrs: {
roles: {embedded: 'always'}
roles: {embedded: 'always'},
lastLoginUTC: {key: 'last_login'},
createdAtUTC: {key: 'created_at'},
updatedAtUTC: {key: 'updated_at'}
},
extractSingle(store, primaryType, payload) {

View File

@ -7,7 +7,7 @@
{{user.name}}
</span>
<br>
<span class="description">Last seen: {{component.lastLogin}}</span>
<span class="description">Last seen: {{component.lastLoginUTC}}</span>
</div>
<aside class="user-list-item-aside">
{{#unless session.user.isAuthor}}

View File

@ -4,7 +4,7 @@
{{gh-trim-focus-input type="text" id="entry-title" placeholder="Your Post Title" value=model.titleScratch tabindex="1" focus=shouldFocusTitle focus-out="updateTitle" }}
{{/gh-view-title}}
{{#if scheduleCountdown}}
<time datetime="{{post.publishedAt}}" class="gh-notification gh-notification-schedule">
<time datetime="{{post.publishedAtUTC}}" class="gh-notification gh-notification-schedule">
Post will be published {{scheduleCountdown}}.
</time>
{{/if}}

View File

@ -36,8 +36,8 @@
{{else}}
<label for="post-setting-date">Publish Date</label>
{{/if}}
{{gh-datetime-input value=model.publishedAt
update=(action "setPublishedAt")
{{gh-datetime-input value=model.publishedAtUTC
update=(action "setPublishedAtUTC")
inputClass="post-setting-date"
inputId="post-setting-date"
inputName="post-setting-date"}}

View File

@ -24,14 +24,16 @@
{{#if post.page}}
<span class="page">Page</span>
{{else}}
<time datetime="{{post.publishedAt}}" class="date published">{{gh-format-timeago post.publishedAt}}
<time datetime="{{post.publishedAtUTC}}" class="date published">
Published {{gh-format-timeago post.publishedAtUTC}}
</time>
{{/if}}
{{else}}
{{#if component.isScheduled}}
<span class="scheduled">Scheduled</span>
<span>&ndash;
<time datetime="{{post.publishedAt}}" class-="date scheduled">{{gh-format-time-scheduled post.publishedAt component.blogTimezone}}
<time datetime="{{post.publishedAtUTC}}" class-="date scheduled">
{{gh-format-time-scheduled post.publishedAtUTC component.blogTimezone}}
</time>
</span>
{{else}}

View File

@ -38,7 +38,7 @@
</span>
{{else}}
<span class="description">
Invitation sent: {{component.createdAt}}
Invitation sent: {{component.createdAtUTC}}
</span>
{{/if}}
</div>

View File

@ -0,0 +1,23 @@
/* jshint expr:true */
import { expect } from 'chai';
import { describeModel, it } from 'ember-mocha';
describeModel(
'post',
'Unit:Serializer: post',
{
// Specify the other units that are required for this test.
needs: ['transform:moment-utc', 'model:user', 'model:tag']
},
function() {
// Replace this with your real tests.
it('serializes records', function() {
let record = this.subject();
let serializedRecord = record.serialize();
expect(record).to.be.ok;
});
}
);

View File

@ -0,0 +1,23 @@
/* jshint expr:true */
import { expect } from 'chai';
import { describeModel, it } from 'ember-mocha';
describeModel(
'role',
'Unit:Serializer: role',
{
// Specify the other units that are required for this test.
needs: ['transform:moment-utc']
},
function() {
// Replace this with your real tests.
it('serializes records', function() {
let record = this.subject();
let serializedRecord = record.serialize();
expect(record).to.be.ok;
});
}
);

View File

@ -0,0 +1,29 @@
/* jshint expr:true */
import { expect } from 'chai';
import { describeModel, it } from 'ember-mocha';
describeModel(
'setting',
'Unit:Serializer: setting',
{
// Specify the other units that are required for this test.
needs: [
'transform:moment-utc',
'transform:facebook-url-user',
'transform:twitter-url-user',
'transform:navigation-settings',
'transform:slack-settings'
]
},
function() {
// Replace this with your real tests.
it('serializes records', function() {
let record = this.subject();
let serializedRecord = record.serialize();
expect(record).to.be.ok;
});
}
);

View File

@ -0,0 +1,23 @@
/* jshint expr:true */
import { expect } from 'chai';
import { describeModel, it } from 'ember-mocha';
describeModel(
'subscriber',
'Unit:Serializer: subscriber',
{
// Specify the other units that are required for this test.
needs: ['model:post', 'transform:moment-utc']
},
function() {
// Replace this with your real tests.
it('serializes records', function() {
let record = this.subject();
let serializedRecord = record.serialize();
expect(record).to.be.ok;
});
}
);

View File

@ -0,0 +1,23 @@
/* jshint expr:true */
import { expect } from 'chai';
import { describeModel, it } from 'ember-mocha';
describeModel(
'tag',
'Unit:Serializer: tag',
{
// Specify the other units that are required for this test.
needs: ['transform:moment-utc', 'transform:raw']
},
function() {
// Replace this with your real tests.
it('serializes records', function() {
let record = this.subject();
let serializedRecord = record.serialize();
expect(record).to.be.ok;
});
}
);

View File

@ -0,0 +1,29 @@
/* jshint expr:true */
import { expect } from 'chai';
import { describeModel, it } from 'ember-mocha';
describeModel(
'user',
'Unit:Serializer: user',
{
// Specify the other units that are required for this test.
needs: [
'transform:moment-utc',
'transform:raw',
'transform:facebook-url-user',
'transform:twitter-url-user',
'model:role'
]
},
function() {
// Replace this with your real tests.
it('serializes records', function() {
let record = this.subject();
let serializedRecord = record.serialize();
expect(record).to.be.ok;
});
}
);