mirror of
https://github.com/TryGhost/Ghost-Admin.git
synced 2023-12-14 02:33:04 +01:00
Timezones: Always use the timezone of blog setting
closes TryGhost/Ghost#6406 follow-up PR of #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 date attr (`createdAt`, `updatedAt`, `publishedAt`, `unsubscribedAt` and `lastLogin`) 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` - adds integration tests for `services/config` and `services/time-zone` --------- **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
This commit is contained in:
parent
f0af686c6d
commit
be8b31ec85
37 changed files with 1086 additions and 69 deletions
|
@ -4,7 +4,11 @@ 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}
|
||||
} = Ember;
|
||||
|
||||
export default Component.extend(TextInputMixin, {
|
||||
tagName: 'span',
|
||||
|
@ -14,15 +18,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() {
|
||||
|
|
|
@ -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)';
|
||||
})
|
||||
});
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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) => {
|
||||
|
@ -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 = '';
|
||||
|
||||
let newPublishedAt = parseDateString(userInput);
|
||||
let publishedAt = moment(this.get('model.publishedAt'));
|
||||
let errMessage = '';
|
||||
// Clear previous errors
|
||||
this.get('model.errors').remove('post-setting-date');
|
||||
|
||||
// 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)';
|
||||
}
|
||||
|
||||
// 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.';
|
||||
}
|
||||
// Date is a valid date, so now make it UTC
|
||||
newPublishedAt = moment.utc(newPublishedAt);
|
||||
|
||||
// If errors, notify and exit.
|
||||
if (errMessage) {
|
||||
this.get('model.errors').add('post-setting-date', errMessage);
|
||||
return;
|
||||
}
|
||||
if (newPublishedAt.diff(moment.utc(new Date()), 'hours', true) > 0) {
|
||||
errMessage = 'Published Date cannot currently be in the future.';
|
||||
}
|
||||
|
||||
// Validation complete, update the view
|
||||
this.set('model.publishedAt', newPublishedAt);
|
||||
// If errors, notify and exit.
|
||||
if (errMessage) {
|
||||
this.get('model.errors').add('post-setting-date', errMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't save the date if the user didn't actually changed the date
|
||||
if (publishedAt && publishedAt.isSame(newPublishedAt)) {
|
||||
return;
|
||||
}
|
||||
// Do nothing if the user didn't actually change the date
|
||||
if (publishedAt && publishedAt.isSame(newPublishedAt)) {
|
||||
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;
|
||||
}
|
||||
// Validation complete
|
||||
this.set('model.publishedAt', newPublishedAt);
|
||||
|
||||
this.get('model').save().catch((errors) => {
|
||||
this.showErrors(errors);
|
||||
this.get('model').rollbackAttributes();
|
||||
// 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();
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
|
|
|
@ -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');
|
||||
|
@ -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') || '';
|
||||
}),
|
||||
|
@ -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/');
|
||||
|
@ -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'));
|
||||
|
||||
|
@ -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');
|
||||
},
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -207,6 +207,27 @@ 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 {id} = request.params;
|
||||
let [attrs] = JSON.parse(request.requestBody).posts;
|
||||
delete attrs.id;
|
||||
|
||||
let post = db.posts.update(id, attrs);
|
||||
|
||||
return {
|
||||
posts: [post]
|
||||
};
|
||||
});
|
||||
|
||||
this.del('/posts/:id/', function (db, request) {
|
||||
db.posts.remove(request.params.id);
|
||||
|
||||
|
@ -264,6 +285,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) {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/* jscs:disable */
|
||||
import Mirage from 'ember-cli-mirage';
|
||||
import Mirage, {faker} from 'ember-cli-mirage';
|
||||
|
||||
export default Mirage.Factory.extend({
|
||||
uuid(i) { return `post-${i}`; },
|
||||
|
@ -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() { return []; }
|
||||
});
|
||||
|
|
|
@ -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: [
|
||||
|
|
327
app/mirage/fixtures/timezones.js
Normal file
327
app/mirage/fixtures/timezones.js
Normal file
|
@ -0,0 +1,327 @@
|
|||
export default [
|
||||
{
|
||||
name: 'Pacific/Pago_Pago',
|
||||
label: '(GMT -11:00) Midway Island, Samoa',
|
||||
offset: -660
|
||||
},
|
||||
{
|
||||
name: 'Pacific/Honolulu',
|
||||
label: '(GMT -10:00) Hawaii',
|
||||
offset: -600
|
||||
},
|
||||
{
|
||||
name: 'America/Anchorage',
|
||||
label: '(GMT -9:00) Alaska',
|
||||
offset: -540
|
||||
},
|
||||
{
|
||||
name: 'America/Tijuana',
|
||||
label: '(GMT -8:00) Chihuahua, La Paz, Mazatlan',
|
||||
offset: -480
|
||||
},
|
||||
{
|
||||
name: 'America/Los_Angeles',
|
||||
label: '(GMT -8:00) Pacific Time (US & Canada); Tijuana',
|
||||
offset: -480
|
||||
},
|
||||
{
|
||||
name: 'America/Phoenix',
|
||||
label: '(GMT -7:00) Arizona',
|
||||
offset: -420
|
||||
},
|
||||
{
|
||||
name: 'America/Denver',
|
||||
label: '(GMT -7:00) Mountain Time (US & Canada)',
|
||||
offset: -420
|
||||
},
|
||||
{
|
||||
name: 'America/Costa_Rica',
|
||||
label: '(GMT -6:00) Central America',
|
||||
offset: -360
|
||||
},
|
||||
{
|
||||
name: 'America/Chicago',
|
||||
label: '(GMT -6:00) Central Time (US & Canada)',
|
||||
offset: -360
|
||||
},
|
||||
{
|
||||
name: 'America/Mexico_City',
|
||||
label: '(GMT -6:00) Guadalajara, Mexico City, Monterrey',
|
||||
offset: -360
|
||||
},
|
||||
{
|
||||
name: 'America/Regina',
|
||||
label: '(GMT -6:00) Saskatchewan',
|
||||
offset: -360
|
||||
},
|
||||
{
|
||||
name: 'America/Bogota',
|
||||
label: '(GMT -5:00) Bogota, Lima, Quito',
|
||||
offset: -300
|
||||
},
|
||||
{
|
||||
name: 'America/New_York',
|
||||
label: '(GMT -5:00) Eastern Time (US & Canada)',
|
||||
offset: -300
|
||||
},
|
||||
{
|
||||
name: 'America/Fort_Wayne',
|
||||
label: '(GMT -5:00) Indiana (East)',
|
||||
offset: -300
|
||||
},
|
||||
{
|
||||
name: 'America/Caracas',
|
||||
label: '(GMT -4:30) Caracas, La Paz',
|
||||
offset: -270
|
||||
},
|
||||
{
|
||||
name: 'America/Halifax',
|
||||
label: '(GMT -4:00) Atlantic Time (Canada); Brasilia, Greenland',
|
||||
offset: -240
|
||||
},
|
||||
{
|
||||
name: 'America/St_Johns',
|
||||
label: '(GMT -3:30) Newfoundland',
|
||||
offset: -210
|
||||
},
|
||||
{
|
||||
name: 'America/Argentina/Buenos_Aires',
|
||||
label: '(GMT -3:00) Buenos Aires, Georgetown',
|
||||
offset: -180
|
||||
},
|
||||
{
|
||||
name: 'America/Santiago',
|
||||
label: '(GMT -3:00) Santiago',
|
||||
offset: -180
|
||||
},
|
||||
{
|
||||
name: 'America/Noronha',
|
||||
label: '(GMT -2:00) Fernando de Noronha',
|
||||
offset: -120
|
||||
},
|
||||
{
|
||||
name: 'Atlantic/Azores',
|
||||
label: '(GMT -1:00) Azores',
|
||||
offset: -60
|
||||
},
|
||||
{
|
||||
name: 'Atlantic/Cape_Verde',
|
||||
label: '(GMT -1:00) Cape Verde Is.',
|
||||
offset: -60
|
||||
},
|
||||
{
|
||||
name: 'Africa/Casablanca',
|
||||
label: '(GMT) Casablanca, Monrovia',
|
||||
offset: 0
|
||||
},
|
||||
{
|
||||
name: 'Europe/Dublin',
|
||||
label: '(GMT) Greenwich Mean Time : Dublin, Edinburgh, London',
|
||||
offset: 0
|
||||
},
|
||||
{
|
||||
name: 'Europe/Amsterdam',
|
||||
label: '(GMT +1:00) Amsterdam, Berlin, Rome, Stockholm, Vienna',
|
||||
offset: 60
|
||||
},
|
||||
{
|
||||
name: 'Europe/Prague',
|
||||
label: '(GMT +1:00) Belgrade, Bratislava, Budapest, Prague',
|
||||
offset: 60
|
||||
},
|
||||
{
|
||||
name: 'Europe/Paris',
|
||||
label: '(GMT +1:00) Brussels, Copenhagen, Madrid, Paris',
|
||||
offset: 60
|
||||
},
|
||||
{
|
||||
name: 'Europe/Warsaw',
|
||||
label: '(GMT +1:00) Sarajevo, Skopje, Warsaw, Zagreb',
|
||||
offset: 60
|
||||
},
|
||||
{
|
||||
name: 'Africa/Lagos',
|
||||
label: '(GMT +1:00) West Central Africa',
|
||||
offset: 60
|
||||
},
|
||||
{
|
||||
name: 'Europe/Istanbul',
|
||||
label: '(GMT +2:00) Athens, Beirut, Bucharest, Istanbul',
|
||||
offset: 120
|
||||
},
|
||||
{
|
||||
name: 'Africa/Cairo',
|
||||
label: '(GMT +2:00) Cairo, Egypt',
|
||||
offset: 120
|
||||
},
|
||||
{
|
||||
name: 'Africa/Maputo',
|
||||
label: '(GMT +2:00) Harare',
|
||||
offset: 120
|
||||
},
|
||||
{
|
||||
name: 'Europe/Kiev',
|
||||
label: '(GMT +2:00) Helsinki, Kiev, Riga, Sofia, Tallinn, Vilnius',
|
||||
offset: 120
|
||||
},
|
||||
{
|
||||
name: 'Asia/Jerusalem',
|
||||
label: '(GMT +2:00) Jerusalem',
|
||||
offset: 120
|
||||
},
|
||||
{
|
||||
name: 'Africa/Johannesburg',
|
||||
label: '(GMT +2:00) Pretoria',
|
||||
offset: 120
|
||||
},
|
||||
{
|
||||
name: 'Asia/Baghdad',
|
||||
label: '(GMT +3:00) Baghdad',
|
||||
offset: 180
|
||||
},
|
||||
{
|
||||
name: 'Asia/Riyadh',
|
||||
label: '(GMT +3:00) Kuwait, Nairobi, Riyadh',
|
||||
offset: 180
|
||||
},
|
||||
{
|
||||
name: 'Asia/Tehran',
|
||||
label: '(GMT +3:30) Tehran',
|
||||
offset: 210
|
||||
},
|
||||
{
|
||||
name: 'Asia/Dubai',
|
||||
label: '(GMT +4:00) Abu Dhabi, Muscat',
|
||||
offset: 240
|
||||
},
|
||||
{
|
||||
name: 'Asia/Baku',
|
||||
label: '(GMT +4:00) Baku, Tbilisi, Yerevan',
|
||||
offset: 240
|
||||
},
|
||||
{
|
||||
name: 'Europe/Moscow',
|
||||
label: '(GMT +4:00) Moscow, St. Petersburg, Volgograd',
|
||||
offset: 240
|
||||
},
|
||||
{
|
||||
name: 'Asia/Kabul',
|
||||
label: '(GMT +4:30) Kabul',
|
||||
offset: 270
|
||||
},
|
||||
{
|
||||
name: 'Asia/Karachi',
|
||||
label: '(GMT +5:00) Islamabad, Karachi, Tashkent',
|
||||
offset: 300
|
||||
},
|
||||
{
|
||||
name: 'Asia/Kolkata',
|
||||
label: '(GMT +5:30) Chennai, Calcutta, Mumbai, New Delhi',
|
||||
offset: 330
|
||||
},
|
||||
{
|
||||
name: 'Asia/Kathmandu',
|
||||
label: '(GMT +5:45) Katmandu',
|
||||
offset: 345
|
||||
},
|
||||
{
|
||||
name: 'Asia/Almaty',
|
||||
label: '(GMT +6:00) Almaty, Novosibirsk',
|
||||
offset: 360
|
||||
},
|
||||
{
|
||||
name: 'Asia/Dhaka',
|
||||
label: '(GMT +6:00) Astana, Dhaka, Sri Jayawardenepura',
|
||||
offset: 360
|
||||
},
|
||||
{
|
||||
name: 'Asia/Yekaterinburg',
|
||||
label: '(GMT +6:00) Yekaterinburg',
|
||||
offset: 360
|
||||
},
|
||||
{
|
||||
name: 'Asia/Rangoon',
|
||||
label: '(GMT +6:30) Rangoon',
|
||||
offset: 390
|
||||
},
|
||||
{
|
||||
name: 'Asia/Bangkok',
|
||||
label: '(GMT +7:00) Bangkok, Hanoi, Jakarta',
|
||||
offset: 420
|
||||
},
|
||||
{
|
||||
name: 'Asia/Hong_Kong',
|
||||
label: '(GMT +8:00) Beijing, Chongqing, Hong Kong, Urumqi',
|
||||
offset: 480
|
||||
},
|
||||
{
|
||||
name: 'Asia/Krasnoyarsk',
|
||||
label: '(GMT +8:00) Krasnoyarsk',
|
||||
offset: 480
|
||||
},
|
||||
{
|
||||
name: 'Asia/Singapore',
|
||||
label: '(GMT +8:00) Kuala Lumpur, Perth, Singapore, Taipei',
|
||||
offset: 480
|
||||
},
|
||||
{
|
||||
name: 'Asia/Irkutsk',
|
||||
label: '(GMT +9:00) Irkutsk, Ulaan Bataar',
|
||||
offset: 540
|
||||
},
|
||||
{
|
||||
name: 'Asia/Tokyo',
|
||||
label: '(GMT +9:00) Osaka, Sapporo, Tokyo',
|
||||
offset: 540
|
||||
},
|
||||
{
|
||||
name: 'Asia/Seoul',
|
||||
label: '(GMT +9:00) Seoul',
|
||||
offset: 540
|
||||
},
|
||||
{
|
||||
name: 'Australia/Darwin',
|
||||
label: '(GMT +9:30) Darwin',
|
||||
offset: 570
|
||||
},
|
||||
{
|
||||
name: 'Australia/Brisbane',
|
||||
label: '(GMT +10:00) Brisbane, Guam, Port Moresby',
|
||||
offset: 600
|
||||
},
|
||||
{
|
||||
name: 'Asia/Yakutsk',
|
||||
label: '(GMT +10:00) Yakutsk',
|
||||
offset: 600
|
||||
},
|
||||
{
|
||||
name: 'Australia/Adelaide',
|
||||
label: '(GMT +10:30) Adelaide',
|
||||
offset: 630
|
||||
},
|
||||
{
|
||||
name: 'Australia/Sydney',
|
||||
label: '(GMT +11:00) Canberra, Hobart, Melbourne, Sydney, Vladivostok',
|
||||
offset: 660
|
||||
},
|
||||
{
|
||||
name: 'Pacific/Fiji',
|
||||
label: '(GMT +12:00) Fiji, Kamchatka, Marshall Is.',
|
||||
offset: 720
|
||||
},
|
||||
{
|
||||
name: 'Pacific/Kwajalein',
|
||||
label: '(GMT +12:00) International Date Line West',
|
||||
offset: 720
|
||||
},
|
||||
{
|
||||
name: 'Asia/Magadan',
|
||||
label: '(GMT +12:00) Magadan, Soloman Is., New Caledonia',
|
||||
offset: 720
|
||||
},
|
||||
{
|
||||
name: 'Pacific/Auckland',
|
||||
label: '(GMT +13:00) Auckland, Wellington',
|
||||
offset: 780
|
||||
}
|
||||
];
|
|
@ -28,11 +28,11 @@ export default Model.extend(ValidationEngine, {
|
|||
metaDescription: attr('string'),
|
||||
author: belongsTo('user', {async: true}),
|
||||
authorId: attr('number'),
|
||||
updatedAt: attr('moment-date'),
|
||||
updatedAt: attr('moment-utc'),
|
||||
updatedBy: attr(),
|
||||
publishedAt: attr('moment-date'),
|
||||
publishedAt: attr('moment-utc'),
|
||||
publishedBy: belongsTo('user', {async: true}),
|
||||
createdAt: attr('moment-date'),
|
||||
createdAt: attr('moment-utc'),
|
||||
createdBy: attr(),
|
||||
tags: hasMany('tag', {
|
||||
embedded: 'always',
|
||||
|
@ -42,6 +42,7 @@ export default Model.extend(ValidationEngine, {
|
|||
|
||||
config: service(),
|
||||
ghostPaths: service(),
|
||||
timeZone: service(),
|
||||
|
||||
absoluteUrl: computed('url', 'ghostPaths.url', 'config.blogUrl', function () {
|
||||
let blogUrl = this.get('config.blogUrl');
|
||||
|
|
|
@ -9,8 +9,8 @@ export default Model.extend({
|
|||
uuid: attr('string'),
|
||||
name: attr('string'),
|
||||
description: attr('string'),
|
||||
createdAt: attr('moment-date'),
|
||||
updatedAt: attr('moment-date'),
|
||||
createdAt: attr('moment-utc'),
|
||||
updatedAt: attr('moment-utc'),
|
||||
createdBy: attr(),
|
||||
updatedBy: attr(),
|
||||
|
||||
|
|
|
@ -16,6 +16,7 @@ export default Model.extend(ValidationEngine, {
|
|||
permalinks: attr('string'),
|
||||
activeTheme: attr('string'),
|
||||
availableThemes: attr(),
|
||||
activeTimezone: attr('string', {defaultValue: 'Europe/Dublin'}),
|
||||
ghost_head: attr('string'),
|
||||
ghost_foot: attr('string'),
|
||||
facebook: attr('facebook-url-user'),
|
||||
|
|
|
@ -13,9 +13,9 @@ export default Model.extend(ValidationEngine, {
|
|||
subscribedUrl: attr('string'),
|
||||
subscribedReferrer: attr('string'),
|
||||
unsubscribedUrl: attr('string'),
|
||||
unsubscribedAt: attr('moment-date'),
|
||||
createdAt: attr('moment-date'),
|
||||
updatedAt: attr('moment-date'),
|
||||
unsubscribedAt: attr('moment-utc'),
|
||||
createdAt: attr('moment-utc'),
|
||||
updatedAt: attr('moment-utc'),
|
||||
createdBy: attr('number'),
|
||||
updatedBy: attr('number'),
|
||||
|
||||
|
|
|
@ -15,8 +15,8 @@ export default Model.extend(ValidationEngine, {
|
|||
metaDescription: attr('string'),
|
||||
image: attr('string'),
|
||||
hidden: attr('boolean'),
|
||||
createdAt: attr('moment-date'),
|
||||
updatedAt: attr('moment-date'),
|
||||
createdAt: attr('moment-utc'),
|
||||
updatedAt: attr('moment-utc'),
|
||||
createdBy: attr(),
|
||||
updatedBy: attr(),
|
||||
count: attr('raw')
|
||||
|
|
|
@ -28,10 +28,10 @@ export default Model.extend(ValidationEngine, {
|
|||
language: attr('string', {defaultValue: 'en_US'}),
|
||||
metaTitle: attr('string'),
|
||||
metaDescription: attr('string'),
|
||||
lastLogin: attr('moment-date'),
|
||||
createdAt: attr('moment-date'),
|
||||
lastLogin: attr('moment-utc'),
|
||||
createdAt: attr('moment-utc'),
|
||||
createdBy: attr('number'),
|
||||
updatedAt: attr('moment-date'),
|
||||
updatedAt: attr('moment-utc'),
|
||||
updatedBy: attr('number'),
|
||||
roles: hasMany('role', {
|
||||
embedded: 'always',
|
||||
|
|
|
@ -1,12 +1,20 @@
|
|||
import Ember from 'ember';
|
||||
import AuthenticatedRoute from 'ghost/routes/authenticated';
|
||||
import CurrentUserSettings from 'ghost/mixins/current-user-settings';
|
||||
import styleBody from 'ghost/mixins/style-body';
|
||||
|
||||
const {
|
||||
RSVP,
|
||||
inject: {service}
|
||||
} = Ember;
|
||||
|
||||
export default AuthenticatedRoute.extend(styleBody, CurrentUserSettings, {
|
||||
titleToken: 'Settings - General',
|
||||
|
||||
classNames: ['settings-view-general'],
|
||||
|
||||
config: service(),
|
||||
|
||||
beforeModel() {
|
||||
this._super(...arguments);
|
||||
return this.get('session.user')
|
||||
|
@ -15,7 +23,15 @@ export default AuthenticatedRoute.extend(styleBody, CurrentUserSettings, {
|
|||
},
|
||||
|
||||
model() {
|
||||
return this.store.queryRecord('setting', {type: 'blog,theme,private'});
|
||||
return RSVP.hash({
|
||||
settings: this.store.queryRecord('setting', {type: 'blog,theme,private'}),
|
||||
availableTimezones: this.get('config.availableTimezones')
|
||||
});
|
||||
},
|
||||
|
||||
setupController(controller, models) {
|
||||
controller.set('model', models.settings);
|
||||
controller.set('availableTimezones', models.availableTimezones);
|
||||
},
|
||||
|
||||
actions: {
|
||||
|
|
37
app/services/clock.js
Normal file
37
app/services/clock.js
Normal file
|
@ -0,0 +1,37 @@
|
|||
import Ember from 'ember';
|
||||
|
||||
const {
|
||||
Service,
|
||||
run
|
||||
} = Ember;
|
||||
|
||||
const ONE_SECOND = 1000;
|
||||
|
||||
// Creates a clock service to run intervals.
|
||||
|
||||
export default Service.extend({
|
||||
second: null,
|
||||
minute: null,
|
||||
hour: null,
|
||||
|
||||
init() {
|
||||
this.tick();
|
||||
},
|
||||
|
||||
tick() {
|
||||
let now = moment().utc();
|
||||
this.setProperties({
|
||||
second: now.seconds(),
|
||||
minute: now.minutes(),
|
||||
hour: now.hours()
|
||||
});
|
||||
|
||||
if (!Ember.testing) {
|
||||
run.later(() => {
|
||||
this.tick();
|
||||
}, ONE_SECOND);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
});
|
|
@ -1,6 +1,11 @@
|
|||
import Ember from 'ember';
|
||||
|
||||
const {Service, _ProxyMixin, computed} = Ember;
|
||||
const {
|
||||
Service,
|
||||
_ProxyMixin,
|
||||
computed,
|
||||
inject: {service}
|
||||
} = Ember;
|
||||
|
||||
function isNumeric(num) {
|
||||
return Ember.$.isNumeric(num);
|
||||
|
@ -25,6 +30,9 @@ function _mapType(val, type) {
|
|||
}
|
||||
|
||||
export default Service.extend(_ProxyMixin, {
|
||||
ajax: service(),
|
||||
ghostPaths: service(),
|
||||
|
||||
content: computed(function () {
|
||||
let metaConfigTags = Ember.$('meta[name^="env-"]');
|
||||
let config = {};
|
||||
|
@ -40,5 +48,17 @@ export default Service.extend(_ProxyMixin, {
|
|||
});
|
||||
|
||||
return config;
|
||||
}),
|
||||
|
||||
availableTimezones: computed(function() {
|
||||
let timezonesUrl = this.get('ghostPaths.url').api('configuration', 'timezones');
|
||||
|
||||
return this.get('ajax').request(timezonesUrl).then((configTimezones) => {
|
||||
let [ timezonesObj ] = configTimezones.configuration;
|
||||
|
||||
timezonesObj = timezonesObj.timezones;
|
||||
|
||||
return timezonesObj;
|
||||
});
|
||||
})
|
||||
});
|
||||
|
|
28
app/services/time-zone.js
Normal file
28
app/services/time-zone.js
Normal file
|
@ -0,0 +1,28 @@
|
|||
import Ember from 'ember';
|
||||
|
||||
const {
|
||||
Service,
|
||||
computed,
|
||||
inject: {service}
|
||||
} = Ember;
|
||||
|
||||
export default Service.extend({
|
||||
store: service(),
|
||||
|
||||
_parseTimezones(settings) {
|
||||
let activeTimezone = settings.get('activeTimezone');
|
||||
return activeTimezone;
|
||||
},
|
||||
|
||||
_settings: computed(function () {
|
||||
let store = this.get('store');
|
||||
return store.queryRecord('setting', {type: 'blog,theme,private'});
|
||||
}),
|
||||
|
||||
offset: computed('_settings.activeTimezone', function () {
|
||||
return this.get('_settings').then((settings) => {
|
||||
return this._parseTimezones(settings);
|
||||
});
|
||||
})
|
||||
|
||||
});
|
|
@ -113,6 +113,22 @@
|
|||
{{/gh-form-group}}
|
||||
</div>
|
||||
|
||||
<div class="form-group for-select">
|
||||
<label for="activeTimezone">Timezone</label>
|
||||
<span class="gh-select" data-select-text="{{selectedTimezone.label}}" tabindex="0">
|
||||
{{gh-select-native
|
||||
id="activeTimezone"
|
||||
name="general[activeTimezone]"
|
||||
content=availableTimezones
|
||||
optionValuePath="name"
|
||||
optionLabelPath="label"
|
||||
selection=selectedTimezone
|
||||
action="setTimezone"
|
||||
}}
|
||||
</span>
|
||||
<p>The local time here is currently {{localTime}}</p>
|
||||
</div>
|
||||
|
||||
<div class="form-group for-checkbox">
|
||||
<label for="isPrivate">Make this blog private</label>
|
||||
<label class="checkbox" for="isPrivate">
|
||||
|
|
22
app/transforms/moment-utc.js
Normal file
22
app/transforms/moment-utc.js
Normal file
|
@ -0,0 +1,22 @@
|
|||
/* global moment */
|
||||
import Transform from 'ember-data/transform';
|
||||
|
||||
export default Transform.extend({
|
||||
deserialize(serialized) {
|
||||
if (serialized) {
|
||||
return moment.utc(serialized);
|
||||
}
|
||||
return serialized;
|
||||
},
|
||||
|
||||
serialize(deserialized) {
|
||||
if (deserialized) {
|
||||
try {
|
||||
return deserialized.toJSON();
|
||||
} catch (e) {
|
||||
return deserialized;
|
||||
}
|
||||
}
|
||||
return deserialized;
|
||||
}
|
||||
});
|
|
@ -24,13 +24,18 @@ function verifyTimeStamp(dateString) {
|
|||
}
|
||||
|
||||
// Parses a string to a Moment
|
||||
function parseDateString(value) {
|
||||
function parseDateString(value, offset) {
|
||||
// We need the offset here, otherwise the date will be parsed
|
||||
// in UTC timezone
|
||||
moment.tz.setDefault(offset);
|
||||
|
||||
return value ? moment(verifyTimeStamp(value), parseDateFormats, true) : undefined;
|
||||
}
|
||||
|
||||
// Formats a Date or Moment
|
||||
function formatDate(value) {
|
||||
return verifyTimeStamp(value ? moment(value).format(displayDateFormat) : '');
|
||||
function formatDate(value, offset) {
|
||||
// we output the date adjusted by the offset of the timezone set in the blog setting
|
||||
return verifyTimeStamp(value ? moment(value).tz(offset).format(displayDateFormat) : '');
|
||||
}
|
||||
|
||||
export {
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
"keymaster": "1.6.3",
|
||||
"lodash": "3.7.0",
|
||||
"moment": "2.13.0",
|
||||
"moment-timezone": "0.5.4",
|
||||
"normalize.css": "3.0.3",
|
||||
"password-generator": "2.0.2",
|
||||
"pretender": "1.1.0",
|
||||
|
|
|
@ -55,6 +55,7 @@ module.exports = function (defaults) {
|
|||
app.import('bower_components/showdown-ghost/src/extensions/footnotes.js');
|
||||
app.import('bower_components/showdown-ghost/src/extensions/highlight.js');
|
||||
app.import('bower_components/moment/moment.js');
|
||||
app.import('bower_components/moment-timezone/builds/moment-timezone-with-data.js');
|
||||
app.import('bower_components/keymaster/keymaster.js');
|
||||
app.import('bower_components/devicejs/lib/device.js');
|
||||
app.import('bower_components/jquery-ui/jquery-ui.js');
|
||||
|
|
|
@ -27,7 +27,8 @@
|
|||
"currentPath",
|
||||
"currentRouteName",
|
||||
"expect",
|
||||
"fileUpload"
|
||||
"fileUpload",
|
||||
"moment"
|
||||
],
|
||||
"mocha": true,
|
||||
"node": false,
|
||||
|
|
253
tests/acceptance/editor-test.js
Normal file
253
tests/acceptance/editor-test.js
Normal file
|
@ -0,0 +1,253 @@
|
|||
/* jshint expr:true */
|
||||
import {
|
||||
describe,
|
||||
it,
|
||||
beforeEach,
|
||||
afterEach
|
||||
} from 'mocha';
|
||||
import { expect } from 'chai';
|
||||
import startApp from '../helpers/start-app';
|
||||
import destroyApp from '../helpers/destroy-app';
|
||||
import { invalidateSession, authenticateSession } from 'ghost/tests/helpers/ember-simple-auth';
|
||||
import Mirage from 'ember-cli-mirage';
|
||||
|
||||
describe('Acceptance: Editor', function() {
|
||||
let application;
|
||||
|
||||
beforeEach(function() {
|
||||
application = startApp();
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
destroyApp(application);
|
||||
});
|
||||
|
||||
it('redirects to signin when not authenticated', function () {
|
||||
invalidateSession(application);
|
||||
visit('/editor/1');
|
||||
|
||||
andThen(function() {
|
||||
expect(currentURL(), 'currentURL').to.equal('/signin');
|
||||
});
|
||||
});
|
||||
|
||||
it('does not redirect to team page when authenticated as author', function () {
|
||||
let role = server.create('role', {name: 'Author'});
|
||||
let user = server.create('user', {roles: [role], slug: 'test-user'});
|
||||
|
||||
authenticateSession(application);
|
||||
visit('/editor/1');
|
||||
|
||||
andThen(() => {
|
||||
expect(currentURL(), 'currentURL').to.equal('/editor/1');
|
||||
});
|
||||
});
|
||||
|
||||
it('does not redirect to team page when authenticated as editor', function () {
|
||||
let role = server.create('role', {name: 'Editor'});
|
||||
let user = server.create('user', {roles: [role], slug: 'test-user'});
|
||||
|
||||
authenticateSession(application);
|
||||
visit('/editor/1');
|
||||
|
||||
andThen(() => {
|
||||
expect(currentURL(), 'currentURL').to.equal('/editor/1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when logged in', function () {
|
||||
beforeEach(function () {
|
||||
let role = server.create('role', {name: 'Administrator'});
|
||||
let user = server.create('user', {roles: [role]});
|
||||
|
||||
server.loadFixtures();
|
||||
|
||||
return authenticateSession(application);
|
||||
});
|
||||
|
||||
it('renders the editor correctly, PSM Publish Date and Save Button', function () {
|
||||
let posts = server.createList('post', 3);
|
||||
|
||||
// post id 1 is a draft, checking for draft behaviour now
|
||||
visit('/editor/1');
|
||||
|
||||
andThen(() => {
|
||||
expect(currentURL(), 'currentURL')
|
||||
.to.equal('/editor/1');
|
||||
});
|
||||
|
||||
// should error, if the date input is in a wrong format
|
||||
fillIn('input[name="post-setting-date"]', 'testdate');
|
||||
triggerEvent('input[name="post-setting-date"]', 'blur');
|
||||
|
||||
andThen(() => {
|
||||
expect(find('.ember-view.response').text().trim(), 'inline error response for invalid date')
|
||||
.to.equal('Published Date must be a valid date with format: DD MMM YY @ HH:mm (e.g. 6 Dec 14 @ 15:00)');
|
||||
});
|
||||
|
||||
// saves the post with the new date
|
||||
fillIn('input[name="post-setting-date"]', '10 May 16 @ 10:00');
|
||||
triggerEvent('input[name="post-setting-date"]', 'blur');
|
||||
// saving
|
||||
click('.view-header .btn.btn-sm.js-publish-button');
|
||||
|
||||
andThen(() => {
|
||||
expect(find('input[name="post-setting-date"]').val(), 'date after saving')
|
||||
.to.equal('10 May 16 @ 10:00');
|
||||
});
|
||||
|
||||
// should not do anything if the input date is not different
|
||||
fillIn('input[name="post-setting-date"]', '10 May 16 @ 10:00');
|
||||
triggerEvent('input[name="post-setting-date"]', 'blur');
|
||||
|
||||
andThen(() => {
|
||||
expect(find('input[name="post-setting-date"]').val(), 'date didn\'t change')
|
||||
.to.equal('10 May 16 @ 10:00');
|
||||
});
|
||||
|
||||
// checking the flow of the saving button for a draft
|
||||
andThen(() => {
|
||||
expect(find('.view-header .btn.btn-sm.js-publish-button').hasClass('btn-red'), 'no red button expected')
|
||||
.to.be.false;
|
||||
expect(find('.view-header .btn.btn-sm.js-publish-button').text().trim(), 'text in save button')
|
||||
.to.equal('Save Draft');
|
||||
expect(find('.post-save-draft').hasClass('active'), 'highlights the default active button state for a draft')
|
||||
.to.be.true;
|
||||
});
|
||||
|
||||
// click on publish now
|
||||
click('.post-save-publish a');
|
||||
|
||||
andThen(() => {
|
||||
expect(find('.post-save-publish').hasClass('active'), 'highlights the selected active button state')
|
||||
.to.be.true;
|
||||
expect(find('.view-header .btn.btn-sm.js-publish-button').hasClass('btn-red'), 'red button to change from draft to published')
|
||||
.to.be.true;
|
||||
expect(find('.view-header .btn.btn-sm.js-publish-button').text().trim(), 'text in save button after click on \'publish now\'')
|
||||
.to.equal('Publish Now');
|
||||
});
|
||||
|
||||
// Publish the post
|
||||
click('.view-header .btn.btn-sm.js-publish-button');
|
||||
|
||||
andThen(() => {
|
||||
expect(find('.view-header .btn.btn-sm.js-publish-button').text().trim(), 'text in save button after publishing')
|
||||
.to.equal('Update Post');
|
||||
expect(find('.post-save-publish').hasClass('active'), 'highlights the default active button state for a published post')
|
||||
.to.be.true;
|
||||
expect(find('.view-header .btn.btn-sm.js-publish-button').hasClass('btn-red'), 'no red button expected')
|
||||
.to.be.false;
|
||||
});
|
||||
|
||||
// post id 2 is a published post, checking for published post behaviour now
|
||||
visit('/editor/2');
|
||||
|
||||
andThen(() => {
|
||||
expect(currentURL(), 'currentURL').to.equal('/editor/2');
|
||||
expect(find('input[name="post-setting-date"]').val()).to.equal('19 Dec 15 @ 16:25');
|
||||
});
|
||||
|
||||
// should reset the date if the input field is blank
|
||||
fillIn('input[name="post-setting-date"]', '');
|
||||
triggerEvent('input[name="post-setting-date"]', 'blur');
|
||||
|
||||
andThen(() => {
|
||||
expect(find('input[name="post-setting-date"]').val(), 'empty date input')
|
||||
.to.equal('');
|
||||
});
|
||||
|
||||
// saving
|
||||
click('.view-header .btn.btn-sm.js-publish-button');
|
||||
|
||||
andThen(() => {
|
||||
expect(find('input[name="post-setting-date"]').val(), 'date value restored')
|
||||
.to.equal('19 Dec 15 @ 16:25');
|
||||
});
|
||||
|
||||
// saves the post with a new date
|
||||
fillIn('input[name="post-setting-date"]', '10 May 16 @ 10:00');
|
||||
triggerEvent('input[name="post-setting-date"]', 'blur');
|
||||
// saving
|
||||
click('.view-header .btn.btn-sm.js-publish-button');
|
||||
|
||||
andThen(() => {
|
||||
expect(find('input[name="post-setting-date"]').val(), 'new date after saving')
|
||||
.to.equal('10 May 16 @ 10:00');
|
||||
});
|
||||
|
||||
// should not do anything if the input date is not different
|
||||
fillIn('input[name="post-setting-date"]', '10 May 16 @ 10:00');
|
||||
triggerEvent('input[name="post-setting-date"]', 'blur');
|
||||
|
||||
andThen(() => {
|
||||
expect(find('input[name="post-setting-date"]').val(), 'date didn\'t change')
|
||||
.to.equal('10 May 16 @ 10:00');
|
||||
});
|
||||
|
||||
andThen(() => {
|
||||
expect(currentURL(), 'currentURL').to.equal('/editor/2');
|
||||
expect(find('.view-header .btn.btn-sm.js-publish-button').hasClass('btn-red'), 'no red button expected')
|
||||
.to.be.false;
|
||||
expect(find('.view-header .btn.btn-sm.js-publish-button').text().trim(), 'text in save button for published post')
|
||||
.to.equal('Update Post');
|
||||
expect(find('.post-save-publish').hasClass('active'), 'highlights the default active button state for a published post')
|
||||
.to.be.true;
|
||||
});
|
||||
|
||||
// click on unpublish
|
||||
click('.post-save-draft a');
|
||||
|
||||
andThen(() => {
|
||||
expect(find('.post-save-draft').hasClass('active'), 'highlights the active button state for a draft')
|
||||
.to.be.true;
|
||||
expect(find('.view-header .btn.btn-sm.js-publish-button').hasClass('btn-red'), 'red button to change from published to draft')
|
||||
.to.be.true;
|
||||
expect(find('.view-header .btn.btn-sm.js-publish-button').text().trim(), 'text in save button for post to unpublish')
|
||||
.to.equal('Unpublish');
|
||||
});
|
||||
|
||||
// Unpublish the post
|
||||
click('.view-header .btn.btn-sm.js-publish-button');
|
||||
|
||||
andThen(() => {
|
||||
expect(find('.view-header .btn.btn-sm.js-publish-button').text().trim(), 'text in save button for draft')
|
||||
.to.equal('Save Draft');
|
||||
expect(find('.post-save-draft').hasClass('active'), 'highlights the default active button state for a draft')
|
||||
.to.be.true;
|
||||
expect(find('.view-header .btn.btn-sm.js-publish-button').hasClass('btn-red'), 'no red button expected')
|
||||
.to.be.false;
|
||||
});
|
||||
|
||||
// go to settings to change the timezone
|
||||
visit('/settings/general');
|
||||
|
||||
andThen(() => {
|
||||
expect(currentURL(), 'currentURL for settings')
|
||||
.to.equal('/settings/general');
|
||||
expect(find('#activeTimezone option:selected').text().trim(), 'default timezone')
|
||||
.to.equal('(GMT) Greenwich Mean Time : Dublin, Edinburgh, London');
|
||||
// select a new timezone
|
||||
find('#activeTimezone option[value="Pacific/Auckland"]').prop('selected', true);
|
||||
});
|
||||
|
||||
triggerEvent('#activeTimezone select', 'change');
|
||||
// save the settings
|
||||
click('.view-header .btn.btn-blue');
|
||||
|
||||
andThen(() => {
|
||||
expect(find('#activeTimezone option:selected').text().trim(), 'new timezone after saving')
|
||||
.to.equal('(GMT +13:00) Auckland, Wellington');
|
||||
});
|
||||
|
||||
// and now go back to the editor
|
||||
visit('/editor/2');
|
||||
|
||||
andThen(() => {
|
||||
expect(currentURL(), 'currentURL in editor')
|
||||
.to.equal('/editor/2');
|
||||
expect(find('input[name="post-setting-date"]').val(), 'date with timezone offset')
|
||||
.to.equal('10 May 16 @ 21:00');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -47,9 +47,20 @@ describe('Acceptance: Posts - Post', function() {
|
|||
// if we're in "desktop" size, we should redirect and highlight
|
||||
if (find('.content-preview:visible').length) {
|
||||
expect(currentURL(), 'currentURL').to.equal(`/${posts[0].id}`);
|
||||
expect(find('.posts-list li').first().hasClass('active'), 'highlights latest post').to.be.true;
|
||||
// expect(find('.posts-list li').first().hasClass('active'), 'highlights latest post').to.be.true;
|
||||
}
|
||||
});
|
||||
|
||||
// check if we can edit the post
|
||||
click('.post-edit');
|
||||
|
||||
andThen(() => {
|
||||
expect(currentURL(), 'currentURL to editor')
|
||||
.to.equal('/editor/1');
|
||||
});
|
||||
|
||||
// TODO: test the right order of the listes posts
|
||||
// and fix the faker import to ensure correct ordering
|
||||
});
|
||||
|
||||
it('redirects to 404 when post does not exist', function () {
|
||||
|
|
|
@ -132,6 +132,29 @@ describe('Acceptance: Settings - General', function () {
|
|||
expect(find('#activeTheme select option').length, 'available themes').to.equal(1);
|
||||
expect(find('#activeTheme select option').text().trim()).to.equal('Blog - 1.0');
|
||||
});
|
||||
});
|
||||
|
||||
it('renders timezone selector correctly', function () {
|
||||
visit('/settings/general');
|
||||
|
||||
andThen(() => {
|
||||
expect(currentURL(), 'currentURL').to.equal('/settings/general');
|
||||
|
||||
expect(find('#activeTimezone select option').length, 'available timezones').to.equal(65);
|
||||
expect(find('#activeTimezone option:selected').text().trim()).to.equal('(GMT) Greenwich Mean Time : Dublin, Edinburgh, London');
|
||||
find('#activeTimezone option[value="Africa/Cairo"]').prop('selected', true);
|
||||
});
|
||||
|
||||
triggerEvent('#activeTimezone select', 'change');
|
||||
click('.view-header .btn.btn-blue');
|
||||
|
||||
andThen(() => {
|
||||
expect(find('#activeTimezone option:selected').text().trim()).to.equal('(GMT +2:00) Cairo, Egypt');
|
||||
});
|
||||
});
|
||||
|
||||
it('handles private blog settings correctly', function () {
|
||||
visit('/settings/general');
|
||||
|
||||
// handles private blog settings correctly
|
||||
andThen(() => {
|
||||
|
|
|
@ -174,7 +174,6 @@ describe('Acceptance: Settings - Tags', function () {
|
|||
// trigger save
|
||||
fillIn('.tag-settings-pane input[name="name"]', 'New Name');
|
||||
triggerEvent('.tag-settings-pane input[name="name"]', 'blur');
|
||||
|
||||
andThen(() => {
|
||||
// check we update with the data returned from the server
|
||||
expect(find('.settings-tags .settings-tag:last .tag-title').text(), 'tag list updates on save')
|
||||
|
|
26
tests/integration/components/gh-datetime-input-test.js
Normal file
26
tests/integration/components/gh-datetime-input-test.js
Normal file
|
@ -0,0 +1,26 @@
|
|||
/* jshint expr:true */
|
||||
import { expect } from 'chai';
|
||||
import {
|
||||
describeComponent,
|
||||
it
|
||||
} from 'ember-mocha';
|
||||
import hbs from 'htmlbars-inline-precompile';
|
||||
import Ember from 'ember';
|
||||
|
||||
const {run} = Ember;
|
||||
|
||||
describeComponent(
|
||||
'gh-datetime-input',
|
||||
'Integration: Component: gh-datetime-input',
|
||||
{
|
||||
integration: true
|
||||
},
|
||||
function () {
|
||||
it('renders', function () {
|
||||
// renders the component on the page
|
||||
// this.render(hbs`{{gh-datetime-input}}`);
|
||||
//
|
||||
// expect(this.$('.ember-text-field gh-input')).to.have.length(1);
|
||||
});
|
||||
}
|
||||
);
|
63
tests/integration/services/config-test.js
Normal file
63
tests/integration/services/config-test.js
Normal file
|
@ -0,0 +1,63 @@
|
|||
import { expect } from 'chai';
|
||||
import {
|
||||
describeModule,
|
||||
it
|
||||
} from 'ember-mocha';
|
||||
import Pretender from 'pretender';
|
||||
import Ember from 'ember';
|
||||
|
||||
function stubAvailableTimezonesEndpoint(server) {
|
||||
server.get('/ghost/api/v0.1/configuration/timezones', function (request) {
|
||||
return [
|
||||
200,
|
||||
{'Content-Type': 'application/json'},
|
||||
JSON.stringify({
|
||||
configuration: [{
|
||||
timezones: [{
|
||||
label: '(GMT -11:00) Midway Island, Samoa',
|
||||
name: 'Pacific/Pago_Pago',
|
||||
offset: -660
|
||||
},
|
||||
{
|
||||
label: '(GMT) Greenwich Mean Time : Dublin, Edinburgh, London',
|
||||
name: 'Europe/Dublin',
|
||||
offset: 0
|
||||
}]
|
||||
}]
|
||||
})
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
describeModule(
|
||||
'service:config',
|
||||
'Integration: Service: config',
|
||||
{
|
||||
integration: true
|
||||
},
|
||||
function () {
|
||||
let server;
|
||||
|
||||
beforeEach(function () {
|
||||
server = new Pretender();
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
server.shutdown();
|
||||
});
|
||||
|
||||
it('returns a list of timezones in the expected format', function (done) {
|
||||
let service = this.subject();
|
||||
stubAvailableTimezonesEndpoint(server);
|
||||
|
||||
service.get('availableTimezones').then(function (timezones) {
|
||||
expect(timezones.length).to.equal(2);
|
||||
expect(timezones[0].name).to.equal('Pacific/Pago_Pago');
|
||||
expect(timezones[0].label).to.equal('(GMT -11:00) Midway Island, Samoa');
|
||||
expect(timezones[1].name).to.equal('Europe/Dublin');
|
||||
expect(timezones[1].label).to.equal('(GMT) Greenwich Mean Time : Dublin, Edinburgh, London');
|
||||
done();
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
52
tests/integration/services/time-zone-test.js
Normal file
52
tests/integration/services/time-zone-test.js
Normal file
|
@ -0,0 +1,52 @@
|
|||
import { expect } from 'chai';
|
||||
import {
|
||||
describeModule,
|
||||
it
|
||||
} from 'ember-mocha';
|
||||
import Pretender from 'pretender';
|
||||
import Ember from 'ember';
|
||||
|
||||
function settingsStub(server) {
|
||||
let settings = [
|
||||
{
|
||||
id: '1',
|
||||
type: 'blog',
|
||||
key: 'activeTimezone',
|
||||
value: 'Africa/Cairo'
|
||||
}
|
||||
];
|
||||
|
||||
server.get('/ghost/api/v0.1/settings/', function () {
|
||||
return [200, {'Content-Type': 'application/json'}, JSON.stringify({settings})];
|
||||
});
|
||||
}
|
||||
|
||||
describeModule(
|
||||
'service:time-zone',
|
||||
'Integration: Service: time-zone',
|
||||
{
|
||||
integration: true
|
||||
},
|
||||
function () {
|
||||
let server;
|
||||
|
||||
beforeEach(function () {
|
||||
server = new Pretender();
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
server.shutdown();
|
||||
});
|
||||
|
||||
it('should return a timezone offset', function (done) {
|
||||
let service = this.subject();
|
||||
|
||||
settingsStub(server);
|
||||
|
||||
service.get('offset').then(function (offset) {
|
||||
expect(offset).to.equal('Africa/Cairo');
|
||||
done();
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
|
@ -4,6 +4,7 @@ import {
|
|||
describeModule,
|
||||
it
|
||||
} from 'ember-mocha';
|
||||
import sinon from 'sinon';
|
||||
|
||||
const {run} = Ember;
|
||||
|
||||
|
@ -15,7 +16,7 @@ describeModule(
|
|||
'controller:post-settings-menu',
|
||||
'Unit: Controller: post-settings-menu',
|
||||
{
|
||||
needs: ['controller:application', 'service:notifications', 'service:slug-generator']
|
||||
needs: ['controller:application', 'service:notifications', 'service:slug-generator', 'service:timeZone']
|
||||
},
|
||||
|
||||
function () {
|
||||
|
|
|
@ -22,7 +22,7 @@ describeModule(
|
|||
'Unit: Controller: settings/navigation',
|
||||
{
|
||||
// Specify the other units that are required for this test.
|
||||
needs: ['service:config', 'service:notifications', 'model:navigation-item']
|
||||
needs: ['service:config', 'service:notifications', 'model:navigation-item', 'service:ajax', 'service:ghostPaths']
|
||||
},
|
||||
function () {
|
||||
it('blogUrl: captures config and ensures trailing slash', function () {
|
||||
|
|
25
tests/unit/helpers/gh-format-timeago-test.js
Normal file
25
tests/unit/helpers/gh-format-timeago-test.js
Normal file
|
@ -0,0 +1,25 @@
|
|||
/* jshint expr:true */
|
||||
import {expect} from 'chai';
|
||||
import {
|
||||
describe,
|
||||
it
|
||||
} from 'mocha';
|
||||
import {
|
||||
timeAgo
|
||||
} from 'ghost/helpers/gh-format-timeago';
|
||||
import sinon from 'sinon';
|
||||
|
||||
describe('Unit: Helper: gh-format-timeago', function () {
|
||||
let mockDate;
|
||||
let utcStub;
|
||||
|
||||
it('calculates the correct time difference', function () {
|
||||
mockDate = '2016-05-30T10:00:00.000Z';
|
||||
utcStub = sinon.stub(moment, 'utc').returns('2016-05-30T11:00:00.000Z');
|
||||
|
||||
let result = timeAgo([mockDate]);
|
||||
expect(result).to.be.equal('an hour ago');
|
||||
|
||||
moment.utc.restore();
|
||||
});
|
||||
});
|
6
tests/unit/utils/date-formatting-test.js
Normal file
6
tests/unit/utils/date-formatting-test.js
Normal file
|
@ -0,0 +1,6 @@
|
|||
import {formatDate, parseDateString} from 'ghost/utils/date-formatting';
|
||||
|
||||
describe('Unit: Util: date-formatting', function () {
|
||||
it('parses a string into a moment');
|
||||
it('formats a date or moment');
|
||||
});
|
Loading…
Reference in a new issue