FIX: improves code, tests and utc handling of local-dates (#6644)

This commit is contained in:
Joffrey JAFFEUX 2018-11-22 17:19:24 +01:00 committed by GitHub
parent 56478166e5
commit 3ff3bb6e2a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 435 additions and 96 deletions

View File

@ -12,12 +12,16 @@
}
var relativeTime;
var dateAndTime = options.date;
if (options.time) {
dateAndTime = dateAndTime + " " + options.time;
}
if (options.timezone) {
relativeTime = moment
.tz(options.date + " " + options.time, options.timezone)
.utc();
relativeTime = moment.tz(dateAndTime, options.timezone).utc();
} else {
relativeTime = moment.utc(options.date + " " + options.time);
relativeTime = moment.utc(dateAndTime);
}
if (relativeTime < moment().utc()) {
@ -35,9 +39,7 @@
}
var previews = options.timezones.split("|").map(function(timezone) {
var dateTime = relativeTime
.tz(timezone)
.format(options.format || "LLL");
var dateTime = relativeTime.tz(timezone).format(options.format);
var timezoneParts = _formatTimezone(timezone);
@ -53,20 +55,23 @@
}
});
var displayTimezone = moment.tz.guess();
var relativeTime = relativeTime.tz(displayTimezone);
var relativeTime = relativeTime.tz(options.displayedZone);
var d = function(key) {
var translated = I18n.t("discourse_local_dates.relative_dates." + key, {
time: "LT"
});
translated = translated
.split("LT")
.map(function(w) {
return "[" + w + "]";
})
.join("LT");
return translated;
if (options.time) {
return translated
.split("LT")
.map(function(w) {
return "[" + w + "]";
})
.join("LT");
} else {
return "[" + translated.replace(" LT", "") + "]";
}
};
var relativeFormat = {
@ -77,7 +82,7 @@
};
if (
options.format !== "YYYY-MM-DD HH:mm:ss" &&
options.calendar &&
relativeTime.isBetween(
moment().subtract(1, "day"),
moment().add(2, "day")
@ -97,7 +102,7 @@
var displayedTime = relativeTime.replace(
"TZ",
_formatTimezone(displayTimezone).join(": ")
_formatTimezone(options.displayedZone).join(": ")
);
$element
@ -119,12 +124,16 @@
var $this = $(this);
var options = {};
options.format = $this.attr("data-format");
options.time = $this.attr("data-time");
options.format =
$this.attr("data-format") || (options.time ? "LLL" : "LL");
options.date = $this.attr("data-date");
options.time = $this.attr("data-time") || "00:00:00";
options.recurring = $this.attr("data-recurring");
options.timezones = $this.attr("data-timezones");
options.timezones = $this.attr("data-timezones") || "Etc/UTC";
options.timezone = $this.attr("data-timezone");
options.calendar = ($this.attr("data-calendar") || "on") === "on";
options.displayedZone =
$this.attr("data-displayed-zone") || moment.tz.guess();
processElement($this, options);
});

View File

@ -102,9 +102,9 @@ export default Ember.Component.extend({
let dateTime;
if (!timeInferred) {
dateTime = moment.tz(`${date} ${time}`, timezone);
dateTime = moment.tz(`${date} ${time}`, timezone).utc();
} else {
dateTime = moment.tz(date, timezone);
dateTime = moment.tz(date, timezone).utc();
}
let toDateTime;
@ -123,8 +123,13 @@ export default Ember.Component.extend({
timezone
};
config.time = dateTime.format(this.timeFormat);
config.toTime = toDateTime.format(this.timeFormat);
if (!timeInferred) {
config.time = dateTime.format(this.timeFormat);
}
if (!toTimeInferred) {
config.toTime = toDateTime.format(this.timeFormat);
}
if (toDate) {
config.toDate = toDateTime.format(this.dateFormat);
@ -163,7 +168,6 @@ export default Ember.Component.extend({
text += `time=${config.time} `;
}
text += `timezone="${config.timezone}" `;
text += `format="${config.format}" `;
text += `timezones="${config.timezones.join("|")}"`;
text += `]`;
@ -176,7 +180,6 @@ export default Ember.Component.extend({
text += `time=${config.toTime} `;
}
text += `timezone="${config.timezone}" `;
text += `format="${config.format}" `;
text += `timezones="${config.timezones.join("|")}"`;
text += `]`;

View File

@ -7,8 +7,9 @@ function addLocalDate(buffer, matches, state) {
date: null,
time: null,
timezone: null,
format: "YYYY-MM-DD HH:mm:ss",
timezones: "Etc/UTC"
format: null,
timezones: null,
displayedZone: null
};
let parsed = parseBBCodeTag(
@ -18,18 +19,18 @@ function addLocalDate(buffer, matches, state) {
);
config.date = parsed.attrs.date;
config.format = parsed.attrs.format;
config.calendar = parsed.attrs.calendar;
config.time = parsed.attrs.time;
config.timezone = parsed.attrs.timezone;
config.recurring = parsed.attrs.recurring;
config.format = parsed.attrs.format || config.format;
config.timezones = parsed.attrs.timezones || config.timezones;
config.timezones = parsed.attrs.timezones;
config.displayedZone = parsed.attrs.displayedZone;
token = new state.Token("span_open", "span", 1);
token.attrs = [
["class", "discourse-local-date"],
["data-date", state.md.utils.escapeHtml(config.date)],
["data-format", state.md.utils.escapeHtml(config.format)],
["data-timezones", state.md.utils.escapeHtml(config.timezones)]
["data-date", state.md.utils.escapeHtml(config.date)]
];
let dateTime = config.date;
@ -38,6 +39,31 @@ function addLocalDate(buffer, matches, state) {
dateTime = `${dateTime} ${config.time}`;
}
if (config.format) {
token.attrs.push(["data-format", state.md.utils.escapeHtml(config.format)]);
}
if (config.calendar) {
token.attrs.push([
"data-calendar",
state.md.utils.escapeHtml(config.calendar)
]);
}
if (config.displayedZone) {
token.attrs.push([
"data-displayed-zone",
state.md.utils.escapeHtml(config.displayedZone)
]);
}
if (config.timezones) {
token.attrs.push([
"data-timezones",
state.md.utils.escapeHtml(config.timezones)
]);
}
if (config.timezone) {
token.attrs.push([
"data-timezone",
@ -54,10 +80,11 @@ function addLocalDate(buffer, matches, state) {
state.md.utils.escapeHtml(config.recurring)
]);
}
buffer.push(token);
let emailPreview;
const emailTimezone = config.timezones.split("|")[0];
const emailTimezone = (config.timezones || "Etc/UTC").split("|")[0];
const formattedDateTime = dateTime.tz(emailTimezone).format(config.format);
const formattedTimezone = emailTimezone.replace("/", ": ").replace("_", " ");
@ -66,7 +93,6 @@ function addLocalDate(buffer, matches, state) {
} else {
emailPreview = `${formattedDateTime} (${formattedTimezone})`;
}
token.attrs.push(["data-email-preview", emailPreview]);
token = new state.Token("text", "", 0);
@ -74,6 +100,7 @@ function addLocalDate(buffer, matches, state) {
buffer.push(token);
token = new state.Token("span_close", "span", -1);
buffer.push(token);
}

View File

@ -52,7 +52,6 @@ after_initialize do
on(:reduce_cooked) do |fragment|
fragment.css(".discourse-local-date").each do |container|
if container.attributes["data-email-preview"]
preview = container.attributes["data-email-preview"].value
container.content = preview

View File

@ -2,13 +2,13 @@ require 'rails_helper'
RSpec.describe "Local Dates" do
before do
freeze_time
freeze_time DateTime.parse('2018-11-10 12:00')
end
it "should work without timezone" do
post = Fabricate(:post, raw: <<~SQL)
post = Fabricate(:post, raw: <<~TXT)
[date=2018-05-08 time=22:00 format="L LTS" timezones="Europe/Paris|America/Los_Angeles"]
SQL
TXT
cooked = post.cooked
@ -26,9 +26,9 @@ RSpec.describe "Local Dates" do
end
it "should work with timezone" do
post = Fabricate(:post, raw: <<~SQL)
post = Fabricate(:post, raw: <<~TXT)
[date=2018-05-08 time=22:00 format="L LTS" timezone="Asia/Calcutta" timezones="Europe/Paris|America/Los_Angeles"]
SQL
TXT
cooked = post.cooked
@ -37,13 +37,44 @@ RSpec.describe "Local Dates" do
end
it 'requires the right attributes to convert to a local date' do
post = Fabricate(:post, raw: <<~SQL)
post = Fabricate(:post, raw: <<~TXT)
[date]
SQL
TXT
cooked = post.cooked
expect(post.cooked).to include("<p>[date]</p>")
expect(cooked).to_not include('data-date=')
end
it 'requires the right attributes to convert to a local date' do
post = Fabricate(:post, raw: <<~TXT)
[date]
TXT
cooked = post.cooked
expect(post.cooked).to include("<p>[date]</p>")
expect(cooked).to_not include('data-date=')
end
it 'it works with only a date and time' do
raw = "[date=2018-11-01 time=12:00]"
cooked = Fabricate(:post, raw: raw).cooked
expect(cooked).to include('data-date="2018-11-01"')
expect(cooked).to include('data-time="12:00"')
end
it 'doesnt include format by default' do
raw = "[date=2018-11-01 time=12:00]"
cooked = Fabricate(:post, raw: raw).cooked
expect(cooked).not_to include('data-format=')
end
it 'doesnt include timezone by default' do
raw = "[date=2018-11-01 time=12:00]"
cooked = Fabricate(:post, raw: raw).cooked
expect(cooked).not_to include("data-timezone=")
end
end

View File

@ -1,24 +1,52 @@
require 'rails_helper'
def generate_html(text, opts = {})
output = "<p><span class=\"discourse-local-date\""
output += " data-date=\"#{opts[:date]}\"" if opts[:date]
output += " data-time=\"#{opts[:time]}\"" if opts[:time]
output += " data-format=\"#{opts[:format]}\"" if opts[:format]
output += " data-email-preview=\"#{opts[:email_preview]}\"" if opts[:email_preview]
output += ">"
output += text
output + "</span></p>"
end
describe PrettyText do
it 'uses a simplified syntax in emails' do
before do
freeze_time
cooked = PrettyText.cook <<~MD
[date=2018-05-08 time=22:00 format=LLL timezones="Europe/Paris|America/Los_Angeles"]
MD
cooked_mail = <<~HTML
<p><span class="discourse-local-date" data-date="2018-05-08" data-format="LLL" data-timezones="Europe/Paris|America/Los_Angeles" data-time="22:00" data-email-preview="May 9, 2018 12:00 AM (Europe: Paris)">May 9, 2018 12:00 AM (Europe: Paris)</span></p>
HTML
end
expect(PrettyText.format_for_email(cooked)).to match_html(cooked_mail)
context 'emails simplified rendering' do
it 'works with default markup' do
cooked = PrettyText.cook("[date=2018-05-08]")
cooked_mail = generate_html("2018-05-08T00:00:00Z (Etc: UTC)",
date: "2018-05-08",
email_preview: "2018-05-08T00:00:00Z (Etc: UTC)"
)
cooked = PrettyText.cook <<~MD
[date=2018-05-08 format=LLL timezone="Europe/Berlin" timezones="Europe/Paris|America/Los_Angeles"]
MD
cooked_mail = <<~HTML
<p><span class="discourse-local-date" data-date="2018-05-08" data-format="LLL" data-timezones="Europe/Paris|America/Los_Angeles" data-timezone="Europe/Berlin" data-email-preview="May 8, 2018 12:00 AM (Europe: Paris)">May 8, 2018 12:00 AM (Europe: Paris)</span></p>
HTML
expect(PrettyText.format_for_email(cooked)).to match_html(cooked_mail)
end
expect(PrettyText.format_for_email(cooked)).to match_html(cooked_mail)
it 'works with format' do
cooked = PrettyText.cook("[date=2018-05-08 format=LLLL]")
cooked_mail = generate_html("Tuesday, May 8, 2018 12:00 AM (Etc: UTC)",
date: "2018-05-08",
email_preview: "Tuesday, May 8, 2018 12:00 AM (Etc: UTC)",
format: "LLLL"
)
expect(PrettyText.format_for_email(cooked)).to match_html(cooked_mail)
end
it 'works with time' do
cooked = PrettyText.cook("[date=2018-05-08 time=20:00:00]")
cooked_mail = generate_html("2018-05-08T20:00:00Z (Etc: UTC)",
date: "2018-05-08",
email_preview: "2018-05-08T20:00:00Z (Etc: UTC)",
time: "20:00:00"
)
expect(PrettyText.format_for_email(cooked)).to match_html(cooked_mail)
end
end
end

View File

@ -0,0 +1,50 @@
import { acceptance } from "helpers/qunit-helpers";
acceptance("Local Dates - composer", {
loggedIn: true,
settings: { discourse_local_dates_enabled: true }
});
test("composer bbcode", async assert => {
const getAttr = attr => {
return find(".d-editor-preview .discourse-local-date.cooked-date").attr(
`data-${attr}`
);
};
await visit("/");
await click("#create-topic");
await fillIn(
".d-editor-input",
'[date=2017-10-23 time=01:30:00 displayedZone="America/Chicago" format="LLLL" calendar="off" recurring="1.weeks" timezone="Asia/Calcutta" timezones="Europe/Paris|America/Los_Angeles"]'
);
assert.equal(getAttr("date"), "2017-10-23", "it has the correct date");
assert.equal(getAttr("time"), "01:30:00", "it has the correct time");
assert.equal(
getAttr("displayed-zone"),
"America/Chicago",
"it has the correct displayed zone"
);
assert.equal(getAttr("format"), "LLLL", "it has the correct format");
assert.equal(
getAttr("timezones"),
"Europe/Paris|America/Los_Angeles",
"it has the correct timezones"
);
assert.equal(getAttr("recurring"), "1.weeks", "it has the correct recurring");
assert.equal(
getAttr("timezone"),
"Asia/Calcutta",
"it has the correct timezone"
);
await fillIn(
".d-editor-input",
'[date=2017-10-24 format="LL" timezone="Asia/Calcutta" timezones="Europe/Paris|America/Los_Angeles"]'
);
assert.equal(getAttr("date"), "2017-10-24", "it has the correct date");
assert.notOk(getAttr("time"), "it doesnt have time");
});

View File

@ -1,62 +1,254 @@
import { acceptance } from "helpers/qunit-helpers";
import { clearPopupMenuOptionsCallback } from "discourse/controllers/composer";
const sandbox = sinon.createSandbox();
acceptance("Local Dates", {
loggedIn: true,
settings: { discourse_local_dates_enabled: true },
beforeEach() {
clearPopupMenuOptionsCallback();
freezeDateAndZone();
},
afterEach() {
sinon.restore();
sandbox.restore();
moment.tz.setDefault();
}
});
test("at removal", assert => {
let now = moment("2018-06-20").valueOf();
let timezone = moment.tz.guess();
const DEFAULT_DATE = "2018-06-20";
const DEFAULT_ZONE = "Europe/Paris";
sinon.useFakeTimers(now);
function advance(count, unit = "days") {
return moment(DEFAULT_DATE)
.add(count, unit)
.format("YYYY-MM-DD");
}
let html = `<span data-timezones="${timezone}" data-timezone="${timezone}" class="discourse-local-date past cooked-date" data-date="DATE" data-format="L LTS" data-time="14:42:26"></span>`;
function rewind(count, unit = "days") {
return moment(DEFAULT_DATE)
.subtract(count, unit)
.format("YYYY-MM-DD");
}
let yesterday = $(html.replace("DATE", "2018-06-19"));
yesterday.applyLocalDates();
function freezeDateAndZone(date, zone, cb) {
date = date || DEFAULT_DATE;
zone = zone || DEFAULT_ZONE;
assert.equal(yesterday.text(), "Yesterday 2:42 PM");
sandbox.restore();
sandbox.stub(moment.tz, "guess");
moment.tz.guess.returns(zone);
let today = $(html.replace("DATE", "2018-06-20"));
today.applyLocalDates();
const now = moment(date).valueOf();
sandbox.useFakeTimers(now);
assert.equal(today.text(), "Today 2:42 PM");
if (cb) {
cb();
let tomorrow = $(html.replace("DATE", "2018-06-21"));
tomorrow.applyLocalDates();
moment.tz.guess.returns(DEFAULT_ZONE);
sandbox.useFakeTimers(moment(DEFAULT_DATE).valueOf());
}
}
assert.equal(tomorrow.text(), "Tomorrow 2:42 PM");
});
function generateHTML(options = {}) {
let output = `<span class="discourse-local-date past cooked-date"`;
test("local dates bbcode", async assert => {
await visit("/");
await click("#create-topic");
output += ` data-date="${options.date || DEFAULT_DATE}"`;
if (options.format) output += ` data-format="${options.format}"`;
if (options.time) output += ` data-time="${options.time}"`;
if (options.calendar) output += ` data-calendar="${options.calendar}"`;
if (options.recurring) output += ` data-recurring="${options.recurring}"`;
if (options.displayedZone)
output += ` data-displayed-zone="${options.displayedZone}"`;
await fillIn(
".d-editor-input",
'[date=2017-10-23 time=01:30:00 format="LL" timezone="Asia/Calcutta" timezones="Europe/Paris|America/Los_Angeles"]'
);
return (output += "></span>");
}
assert.ok(
exists(".d-editor-preview .discourse-local-date.past.cooked-date"),
"it should contain the cooked output for date & time inputs"
);
test("default format - time specified", assert => {
const html = generateHTML({ date: advance(3), time: "00:00" });
const transformed = $(html).applyLocalDates();
await fillIn(
".d-editor-input",
'[date=2017-10-23 format="LL" timezone="Asia/Calcutta" timezones="Europe/Paris|America/Los_Angeles"]'
);
assert.ok(
exists(".d-editor-preview .discourse-local-date.past.cooked-date"),
"it should contain the cooked output for date only input"
assert.equal(
transformed.text(),
"June 23, 2018 2:00 AM",
"it uses moment LLL format"
);
});
test("default format - no time specified", assert => {
const html = generateHTML({ date: advance(3) });
const transformed = $(html).applyLocalDates();
assert.equal(
transformed.text(),
"June 23, 2018",
"it uses moment LL format as default if not time is specified"
);
});
test("today", assert => {
const html = generateHTML({ time: "14:00" });
const transformed = $(html).applyLocalDates();
assert.equal(transformed.text(), "Today 4:00 PM", "it display Today");
});
test("today - no time", assert => {
const html = generateHTML();
const transformed = $(html).applyLocalDates();
assert.equal(transformed.text(), "Today", "it display Today without time");
});
test("yesterday", assert => {
const html = generateHTML({ date: rewind(1), time: "14:00" });
const transformed = $(html).applyLocalDates();
assert.equal(
transformed.text(),
"Yesterday 4:00 PM",
"it displays yesterday"
);
});
QUnit.skip("yesterday - no time", assert => {
const html = generateHTML({ date: rewind(1) });
const transformed = $(html).applyLocalDates();
assert.equal(
transformed.text(),
"Yesterday",
"it displays yesterday without time"
);
});
test("tomorrow", assert => {
const html = generateHTML({ date: advance(1), time: "14:00" });
const transformed = $(html).applyLocalDates();
assert.equal(transformed.text(), "Tomorrow 4:00 PM", "it displays tomorrow");
});
test("tomorrow - no time", assert => {
const html = generateHTML({ date: advance(1) });
const transformed = $(html).applyLocalDates();
assert.equal(
transformed.text(),
"Tomorrow",
"it displays tomorrow without time"
);
});
test("today - no time with different zones", assert => {
const html = generateHTML();
let transformed = $(html).applyLocalDates();
assert.equal(transformed.text(), "Today", "it displays today without time");
freezeDateAndZone(rewind(12, "hours"), "Pacific/Auckland", () => {
transformed = $(html).applyLocalDates();
assert.equal(
transformed.text(),
"Tomorrow",
"it displays Tomorrow without time"
);
});
});
test("calendar off", assert => {
const html = generateHTML({ calendar: "off", time: "14:00" });
const transformed = $(html).applyLocalDates();
assert.equal(
transformed.text(),
"June 20, 2018 4:00 PM",
"it displays the date without Today"
);
});
test("recurring", assert => {
const html = generateHTML({ recurring: "1.week", time: "14:00" });
let transformed = $(html).applyLocalDates();
assert.equal(
transformed.text(),
"Today 4:00 PM",
"it displays the next occurrence"
);
freezeDateAndZone(advance(1), () => {
transformed = $(html).applyLocalDates();
assert.equal(
transformed.text(),
"June 27, 2018 4:00 PM",
"it displays the next occurrence"
);
});
});
test("displayedZone", assert => {
const html = generateHTML({
date: advance(3),
displayedZone: "Etc/UTC",
time: "14:00"
});
const transformed = $(html).applyLocalDates();
assert.equal(
transformed.text(),
"June 23, 2018 2:00 PM",
"it forces display in the given timezone"
);
});
test("format", assert => {
const html = generateHTML({
date: advance(3),
format: "YYYY | MM - DD"
});
const transformed = $(html).applyLocalDates();
assert.equal(
transformed.text(),
"2018 | 06 - 23",
"it uses the given format"
);
});
test("test utils", assert => {
assert.equal(
moment().format("LLLL"),
moment(DEFAULT_DATE).format("LLLL"),
"it has defaults"
);
assert.equal(moment.tz.guess(), DEFAULT_ZONE, "it has defaults");
freezeDateAndZone(advance(1), DEFAULT_ZONE, () => {
assert.equal(
moment().format("LLLL"),
moment(DEFAULT_DATE)
.add(1, "days")
.format("LLLL"),
"it applies new time"
);
assert.equal(moment.tz.guess(), DEFAULT_ZONE);
});
assert.equal(
moment().format("LLLL"),
moment(DEFAULT_DATE).format("LLLL"),
"it restores time"
);
freezeDateAndZone(advance(1), "Pacific/Auckland", () => {
assert.equal(
moment().format("LLLL"),
moment(DEFAULT_DATE)
.add(1, "days")
.format("LLLL")
);
assert.equal(moment.tz.guess(), "Pacific/Auckland", "it applies new zone");
});
assert.equal(moment.tz.guess(), DEFAULT_ZONE, "it restores zone");
});