extract date-time-moment-parser
refs FOO-1891 this is the start of a series to break apart the inter-dependencies between timezone and moment.js the formatting that is done by moment now stands alone in packages/date-time-moment-parser and is used by ui/shared/timezone. The formatter depends on phrases coming from the locale files, which are available to ui/* but not to packages/*, so in an initializer we inject our custom formats into the package, allowing it to make use of them. there should be no breaking API changes, even though several APIs were dropped, as they were all used in test but not in the app code. To minimize the changes, a specHelpers.js file is now provided by the timezone package that maintains backwards compat to a reasonable extent. TEST PLAN ==== ==== although the tests should be covering this, it wouldn't hurt to manually exercise any of the date picker widgets Change-Id: I0c59ad2df8f7392425debb6ec448ec1b4fb029c6 Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/265313 Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com> Reviewed-by: Charley Kline <ckline@instructure.com> Product-Review: Charley Kline <ckline@instructure.com> QA-Review: Charley Kline <ckline@instructure.com>
This commit is contained in:
parent
1b0b4b035d
commit
83e4f6d4ab
|
@ -33,7 +33,7 @@ module.exports = {
|
|||
// mock the tinymce-react Editor react component
|
||||
'@tinymce/tinymce-react': '<rootDir>/packages/canvas-rce/src/rce/__mocks__/tinymceReact.js'
|
||||
},
|
||||
roots: ['ui', 'gems/plugins', 'public/javascripts'],
|
||||
roots: ['<rootDir>/ui', 'gems/plugins', 'public/javascripts'],
|
||||
moduleDirectories: ['ui/shims', 'public/javascripts', 'node_modules'],
|
||||
reporters: [
|
||||
'default',
|
||||
|
|
|
@ -19,6 +19,9 @@
|
|||
import Enzyme from 'enzyme'
|
||||
import Adapter from 'enzyme-adapter-react-16'
|
||||
import {filterUselessConsoleMessages} from '@instructure/js-utils'
|
||||
import {
|
||||
up as configureDateTimeMomentParser
|
||||
} from '../ui/boot/initializers/configureDateTimeMomentParser'
|
||||
|
||||
filterUselessConsoleMessages(console)
|
||||
|
||||
|
@ -34,6 +37,8 @@ Enzyme.configure({adapter: new Adapter()})
|
|||
// because InstUI themeable components need an explicit "dir" attribute on the <html> element
|
||||
document.documentElement.setAttribute('dir', 'ltr')
|
||||
|
||||
configureDateTimeMomentParser()
|
||||
|
||||
// because everyone implements `flat()` and `flatMap()` except JSDOM 🤦🏼♂️
|
||||
if (!Array.prototype.flat) {
|
||||
// eslint-disable-next-line no-extend-native
|
||||
|
|
|
@ -0,0 +1,106 @@
|
|||
/*
|
||||
* Copyright (C) 2021 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import moment from 'moment'
|
||||
|
||||
export default function getFormats({ customI18nFormats }) {
|
||||
const i18nFormatsForCurrentLocale = customI18nFormats.map(x => x()).filter(x => !!x)
|
||||
const momentCompatibleI18nFormats = specifyMinutesImplicitly(
|
||||
i18nFormatsForCurrentLocale
|
||||
).map(convertI18nFormatToMomentFormat)
|
||||
|
||||
return union(momentStockFormats, momentCompatibleI18nFormats)
|
||||
}
|
||||
|
||||
const union = (a, b) => b.reduce((acc, x) => {
|
||||
if (!acc.includes(x)) {
|
||||
acc.push(x)
|
||||
}
|
||||
|
||||
return acc
|
||||
}, [].concat(a))
|
||||
|
||||
const i18nToMomentTokenMapping = {
|
||||
'%A': 'dddd',
|
||||
'%B': 'MMMM',
|
||||
'%H': 'HH',
|
||||
'%M': 'mm',
|
||||
'%S': 'ss',
|
||||
'%P': 'a',
|
||||
'%Y': 'YYYY',
|
||||
'%a': 'ddd',
|
||||
'%b': 'MMM',
|
||||
'%m': 'M',
|
||||
'%d': 'D',
|
||||
'%k': 'H',
|
||||
'%l': 'h',
|
||||
'%z': 'Z',
|
||||
|
||||
'%-H': 'H',
|
||||
'%-M': 'm',
|
||||
'%-S': 's',
|
||||
'%-m': 'M',
|
||||
'%-d': 'D',
|
||||
'%-k': 'H',
|
||||
'%-l': 'h'
|
||||
}
|
||||
|
||||
const momentStockFormats = [
|
||||
moment.ISO_8601,
|
||||
'YYYY',
|
||||
'LT',
|
||||
'LTS',
|
||||
'L',
|
||||
'l',
|
||||
'LL',
|
||||
'll',
|
||||
'LLL',
|
||||
'lll',
|
||||
'LLLL',
|
||||
'llll',
|
||||
'D MMM YYYY',
|
||||
'H:mm'
|
||||
]
|
||||
|
||||
// expand every i18n format that specifies minutes (%M or %-M) into two: one
|
||||
// that specifies the minutes and another that doesn't
|
||||
//
|
||||
// why? don't ask me
|
||||
const specifyMinutesImplicitly = (formats) => formats.map(format =>
|
||||
format.match(/:%-?M/) ?
|
||||
[format, format.replace(/:%-?M/, '')] :
|
||||
[format]
|
||||
).flat(1)
|
||||
|
||||
const convertI18nFormatToMomentFormat = i18nFormat => {
|
||||
const escapeNonI18nTokens = (string) => (
|
||||
string.split(' ').map(escapeUnlessIsI18nToken).join(' ')
|
||||
)
|
||||
|
||||
const escapeUnlessIsI18nToken = (string) => {
|
||||
const isKey = Object.keys(i18nToMomentTokenMapping).find(k => string.indexOf(k) > -1)
|
||||
|
||||
return isKey ? string : `[${string}]`
|
||||
}
|
||||
|
||||
const escapedI18nFormat = escapeNonI18nTokens(i18nFormat)
|
||||
|
||||
return Object.keys(i18nToMomentTokenMapping).reduce((acc, i18nToken) => (
|
||||
acc.replace(i18nToken, i18nToMomentTokenMapping[i18nToken])
|
||||
), escapedI18nFormat)
|
||||
}
|
|
@ -0,0 +1,86 @@
|
|||
/*
|
||||
* Copyright (C) 2021 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import moment from 'moment'
|
||||
import getFormats from './formats'
|
||||
|
||||
const config = { customI18nFormats: [] }
|
||||
|
||||
// Parse a DateTime string according to any of the pre-defined formats. The
|
||||
// formats may come from Moment itself, or from the locale dictionary for
|
||||
// date and time.
|
||||
//
|
||||
// See ./formats.js for the possible formats.
|
||||
//
|
||||
// @param {Object.<{
|
||||
// t: (String) -> String,
|
||||
// lookup: (String) -> String
|
||||
// }>}
|
||||
// An object that can query the locale dictionary for datetime formats.
|
||||
//
|
||||
// @param {String}
|
||||
// The string to parse.
|
||||
//
|
||||
// @return {Moment?}
|
||||
// A moment instance in case the string could be parsed.
|
||||
export default function parseDateTime(input, locale) {
|
||||
const formats = getFormats(config)
|
||||
const momentInstance = createDateTimeMoment(input, formats, locale)
|
||||
|
||||
return momentInstance.isValid() ? momentInstance : null
|
||||
}
|
||||
|
||||
export function useI18nFormats(customI18nFormats) {
|
||||
config.customI18nFormats = customI18nFormats
|
||||
}
|
||||
|
||||
// Check a moment instance to see if its format (and input value) explicitly
|
||||
// specify a timezone. This query is useful to know whether the date needs to
|
||||
// be unfudged in case it does NOT specify a timezone (e.g. using tz-parse).
|
||||
export function specifiesTimezone(m) {
|
||||
return !!(m._f.match(/Z/) && m._pf.unusedTokens.indexOf('Z') === -1)
|
||||
}
|
||||
|
||||
export function toRFC3339WithoutTZ(m) {
|
||||
return moment(m).locale('en').format('YYYY-MM-DD[T]HH:mm:ss')
|
||||
}
|
||||
|
||||
// wrap's moment() for parsing datetime strings. assumes the string to be
|
||||
// parsed is in the profile timezone unless if contains an offset string
|
||||
// *and* a format token to parse it, and unfudges the result.
|
||||
export function createDateTimeMoment(input, format, locale) {
|
||||
// ensure first argument is a string and second is a format or an array
|
||||
// of formats
|
||||
if (typeof input !== 'string' || !(typeof format === 'string' || Array.isArray(format))) {
|
||||
throw new Error(
|
||||
'createDateTimeMoment only works on string+format(s). just use moment() directly for any other signature'
|
||||
)
|
||||
}
|
||||
|
||||
let m = moment.apply(null, [input, format, locale])
|
||||
|
||||
if (m._pf.unusedTokens.length > 0) {
|
||||
// we didn't use strict at first, because we want to accept when
|
||||
// there's unused input as long as we're using all tokens. but if the
|
||||
// best non-strict match has unused tokens, reparse with strict
|
||||
return moment.apply(null, [input, format, locale, true])
|
||||
}
|
||||
else {
|
||||
return m
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"name": "date-time-moment-parser",
|
||||
"version": "1.0.0",
|
||||
"description": "parse DateTime strings into Date objects using moment.js",
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
"jest": {
|
||||
"transform": {}
|
||||
},
|
||||
"scripts": {
|
||||
"test": "NODE_OPTIONS=--experimental-vm-modules jest"
|
||||
},
|
||||
"author": "Ahmad Amireh <ahmad@instructure.com>",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"jest": "^26.6.3"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,139 @@
|
|||
/*
|
||||
* Copyright (C) 2021 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import parseDateTime, { createDateTimeMoment, specifiesTimezone, toRFC3339WithoutTZ } from './index'
|
||||
import tz from 'timezone'
|
||||
import moment from 'moment'
|
||||
import detroit from 'timezone/America/Detroit'
|
||||
|
||||
const moonwalk = new Date(Date.UTC(1969, 6, 21, 2, 56))
|
||||
const tzDetroit = tz(detroit, 'America/Detroit')
|
||||
const I18nStub = {
|
||||
lookup: () => null,
|
||||
t: () => null
|
||||
}
|
||||
|
||||
let originalMomentLocale
|
||||
|
||||
beforeEach(() => {
|
||||
originalMomentLocale = moment.locale()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
moment.locale(originalMomentLocale)
|
||||
originalMomentLocale = null
|
||||
})
|
||||
|
||||
test('moment(one-arg) complains', () => {
|
||||
expect(() => {
|
||||
createDateTimeMoment('June 24 at 10:00pm')
|
||||
}).toThrowError(/ only works on /)
|
||||
})
|
||||
|
||||
test('moment(non-string, fmt-string) complains', () => {
|
||||
expect(() => {
|
||||
createDateTimeMoment(moonwalk, 'MMMM D h:mmA')
|
||||
}).toThrowError(/ only works on /)
|
||||
})
|
||||
|
||||
test('moment(date-string, non-string) complains', () => {
|
||||
expect(() => {
|
||||
createDateTimeMoment('June 24 at 10:00pm', 123)
|
||||
}).toThrowError(/ only works on /)
|
||||
})
|
||||
|
||||
test('moment(date-string, fmt-string) works', () =>
|
||||
expect(createDateTimeMoment('June 24 at 10:00pm', 'MMMM D h:mmA')).toBeTruthy()
|
||||
)
|
||||
|
||||
test('moment(date-string, [fmt-strings]) works', () =>
|
||||
expect(createDateTimeMoment('June 24 at 10:00pm', ['MMMM D h:mmA', 'L'])).toBeTruthy()
|
||||
)
|
||||
|
||||
test('moment passes through invalid results', () => {
|
||||
const m = createDateTimeMoment('not a valid date', 'L')
|
||||
expect(m.isValid()).toEqual(false)
|
||||
})
|
||||
|
||||
test('moment accepts excess input, but all format used', () => {
|
||||
const m = createDateTimeMoment('12pm and more', 'ha')
|
||||
expect(m.isValid()).toEqual(true)
|
||||
})
|
||||
|
||||
test('moment rejects excess format', () => {
|
||||
const m = createDateTimeMoment('12pm', 'h:mma')
|
||||
expect(m.isValid()).toEqual(false)
|
||||
})
|
||||
|
||||
test('moment returns moment for valid results', () => {
|
||||
const m = createDateTimeMoment('June 24, 2015 at 10:00pm -04:00', 'MMMM D, YYYY h:mmA Z')
|
||||
expect(m.isValid()).toEqual(true)
|
||||
})
|
||||
|
||||
test('moment sans-timezone info parses according to profile timezone', () => {
|
||||
const expected = new Date(1435197600000) // 10pm EDT on June 24, 2015
|
||||
const m = createDateTimeMoment('June 24, 2015 at 10:00pm', 'MMMM D, YYYY h:mmA')
|
||||
expect(specifiesTimezone(m)).toEqual(false)
|
||||
expect(tzDetroit(toRFC3339WithoutTZ(m))).toEqual(+expected)
|
||||
})
|
||||
|
||||
test('moment with-timezone info parses according to that timezone', () => {
|
||||
const expected = new Date(1435204800000) // 10pm MDT on June 24, 2015
|
||||
const m = createDateTimeMoment('June 24, 2015 at 10:00pm -06:00', 'MMMM D, YYYY h:mmA Z')
|
||||
expect(specifiesTimezone(m)).toEqual(true)
|
||||
expect(+m.toDate()).toEqual(+expected)
|
||||
expect(tzDetroit(m.format())).toEqual(+expected)
|
||||
})
|
||||
|
||||
test('moment can change locales with single arity', () => {
|
||||
moment.locale('en')
|
||||
const m1 = createDateTimeMoment('mercredi 1 juillet 2015 15:00', 'LLLL')
|
||||
expect(m1._locale._abbr.match(/fr/)).toBeFalsy()
|
||||
expect(m1.isValid()).toBeFalsy()
|
||||
|
||||
moment.locale('fr')
|
||||
const m2 = createDateTimeMoment('mercredi 1 juillet 2015 15:00', 'LLLL')
|
||||
expect(m2._locale._abbr.match(/fr/)).toBeTruthy()
|
||||
expect(m2.isValid()).toBeTruthy()
|
||||
})
|
||||
|
||||
describe('specifiesTimezone', () => {
|
||||
it('is true when format contains Z and input contains a timezone', () => {
|
||||
expect(
|
||||
specifiesTimezone(
|
||||
createDateTimeMoment('June 24, 2015 at 10:00pm -06:00', 'MMMM D, YYYY h:mmA Z')
|
||||
)
|
||||
).toEqual(true)
|
||||
})
|
||||
|
||||
it('is false when format contains Z but input has no timezone', () => {
|
||||
expect(
|
||||
specifiesTimezone(
|
||||
createDateTimeMoment('June 24, 2015 at 10:00pm', 'MMMM D, YYYY h:mmA Z')
|
||||
)
|
||||
).toEqual(false)
|
||||
})
|
||||
|
||||
it('is false when format does not contain Z even if the input contains a timezone', () => {
|
||||
expect(
|
||||
specifiesTimezone(
|
||||
createDateTimeMoment('June 24, 2015 at 10:00pm -06:00', 'MMMM D, YYYY h:mmA')
|
||||
)
|
||||
).toEqual(false)
|
||||
})
|
||||
})
|
|
@ -18,7 +18,9 @@
|
|||
|
||||
import $ from 'jquery'
|
||||
import {isArray, isObject, uniq} from 'lodash'
|
||||
import tz from '@canvas/timezone'
|
||||
import tzInTest from '@canvas/timezone/specHelpers'
|
||||
import { configure as configureTimezone } from '@canvas/timezone'
|
||||
import timezone from 'timezone'
|
||||
import fcUtil from '@canvas/calendar/jquery/fcUtil.coffee'
|
||||
import denver from 'timezone/America/Denver'
|
||||
import juneau from 'timezone/America/Juneau'
|
||||
|
@ -67,16 +69,21 @@ QUnit.module('AgendaView', {
|
|||
fcUtil.addMinuteDelta(this.startDate, -60 * 24 * 15)
|
||||
this.dataSource = new EventDataSource(this.contexts)
|
||||
this.server = sinon.fakeServer.create()
|
||||
this.snapshot = tz.snapshot()
|
||||
tz.changeZone(denver, 'America/Denver')
|
||||
tzInTest.configureAndRestoreLater({
|
||||
tz: timezone(denver, 'America/Denver'),
|
||||
tzData: {
|
||||
'America/Denver': denver
|
||||
},
|
||||
momentLocale: 'en'
|
||||
})
|
||||
I18nStubber.pushFrame()
|
||||
fakeENV.setup({CALENDAR: {}})
|
||||
},
|
||||
teardown() {
|
||||
this.container.remove()
|
||||
this.server.restore()
|
||||
tz.restore(this.snapshot)
|
||||
I18nStubber.popFrame()
|
||||
tzInTest.restore()
|
||||
I18nStubber.clear()
|
||||
fakeENV.teardown()
|
||||
}
|
||||
})
|
||||
|
@ -188,7 +195,14 @@ test('should only include days on page breaks once', function() {
|
|||
})
|
||||
|
||||
test('renders non-assignment events with locale-appropriate format string', function() {
|
||||
tz.changeLocale(french, 'fr_FR', 'fr')
|
||||
tzInTest.configureAndRestoreLater({
|
||||
tz: timezone(denver, 'America/Denver', french, 'fr_FR'),
|
||||
tzData: {
|
||||
'America/Denver': denver,
|
||||
},
|
||||
momentLocale: 'fr'
|
||||
})
|
||||
// tz.changeLocale(french, 'fr_FR', 'fr')
|
||||
I18nStubber.setLocale('fr_FR')
|
||||
I18nStubber.stub('fr_FR', {'time.formats.tiny': '%k:%M'})
|
||||
const view = new AgendaView({
|
||||
|
@ -210,7 +224,13 @@ test('renders non-assignment events with locale-appropriate format string', func
|
|||
})
|
||||
|
||||
test('renders assignment events with locale-appropriate format string', function() {
|
||||
tz.changeLocale(french, 'fr_FR', 'fr')
|
||||
tzInTest.configureAndRestoreLater({
|
||||
tz: timezone(denver, 'America/Denver', french, 'fr_FR'),
|
||||
tzData: {
|
||||
'America/Denver': denver,
|
||||
},
|
||||
momentLocale: 'fr'
|
||||
})
|
||||
I18nStubber.setLocale('fr_FR')
|
||||
I18nStubber.stub('fr_FR', {'time.formats.tiny': '%k:%M'})
|
||||
const view = new AgendaView({
|
||||
|
@ -232,7 +252,13 @@ test('renders assignment events with locale-appropriate format string', function
|
|||
})
|
||||
|
||||
test('renders non-assignment events in appropriate timezone', function() {
|
||||
tz.changeZone(juneau, 'America/Juneau')
|
||||
tzInTest.configureAndRestoreLater({
|
||||
tz: timezone(juneau, 'America/Juneau'),
|
||||
tzData: {
|
||||
'America/Juneau': juneau
|
||||
}
|
||||
})
|
||||
|
||||
I18nStubber.stub('en', {
|
||||
'time.formats.tiny': '%l:%M%P',
|
||||
date: {}
|
||||
|
@ -256,7 +282,12 @@ test('renders non-assignment events in appropriate timezone', function() {
|
|||
})
|
||||
|
||||
test('renders assignment events in appropriate timezone', function() {
|
||||
tz.changeZone(juneau, 'America/Juneau')
|
||||
tzInTest.configureAndRestoreLater({
|
||||
tz: timezone(juneau, 'America/Juneau'),
|
||||
tzData: {
|
||||
'America/Juneau': juneau
|
||||
}
|
||||
})
|
||||
I18nStubber.stub('en', {
|
||||
'time.formats.tiny': '%l:%M%P',
|
||||
date: {}
|
||||
|
|
|
@ -22,6 +22,8 @@ import I18n from 'i18n!calendar'
|
|||
import fcUtil from '@canvas/calendar/jquery/fcUtil.coffee'
|
||||
import moment from 'moment'
|
||||
import tz from '@canvas/timezone'
|
||||
import tzInTest from '@canvas/timezone/specHelpers'
|
||||
import timezone from 'timezone'
|
||||
import denver from 'timezone/America/Denver'
|
||||
import fixtures from 'helpers/fixtures'
|
||||
import $ from 'jquery'
|
||||
|
@ -30,14 +32,19 @@ import fakeENV from 'helpers/fakeENV'
|
|||
|
||||
QUnit.module('Calendar', {
|
||||
setup() {
|
||||
this.snapshot = tz.snapshot()
|
||||
tz.changeZone(denver, 'America/Denver')
|
||||
tzInTest.configureAndRestoreLater({
|
||||
tz: timezone(denver, 'America/Denver'),
|
||||
tzData: {
|
||||
'America/Denver': denver
|
||||
}
|
||||
})
|
||||
|
||||
fixtures.setup()
|
||||
sinon.stub($, 'getJSON')
|
||||
fakeENV.setup()
|
||||
},
|
||||
teardown() {
|
||||
tz.restore(this.snapshot)
|
||||
tzInTest.restore()
|
||||
const calendar = $('#fixtures .calendar').data('fullCalendar')
|
||||
if (calendar) {
|
||||
calendar.destroy()
|
||||
|
|
|
@ -71,6 +71,7 @@ QUnit.module('EditAssignmentDetails', {
|
|||
document.getElementById('fixtures').innerHTML = ''
|
||||
fakeENV.teardown()
|
||||
tz.restore(this.snapshot)
|
||||
I18nStubber.clear()
|
||||
}
|
||||
})
|
||||
const createView = function(model, event) {
|
||||
|
@ -135,7 +136,6 @@ test('should localize start date', function() {
|
|||
})
|
||||
const view = createView(commonEvent(), this.event)
|
||||
equal(view.$('.datetime_field').val(), 'ven. 7 août 2015 17:00')
|
||||
I18nStubber.popFrame()
|
||||
})
|
||||
|
||||
test('requires name to save assignment event', function() {
|
||||
|
|
|
@ -49,6 +49,7 @@ QUnit.module('EditAssignmentDetails', {
|
|||
document.getElementById('fixtures').innerHTML = ''
|
||||
fakeENV.teardown()
|
||||
tz.restore(this.snapshot)
|
||||
I18nStubber.clear()
|
||||
}
|
||||
})
|
||||
const createView = function(event = note) {
|
||||
|
@ -74,7 +75,6 @@ test('should localize start date', () => {
|
|||
})
|
||||
const view = createView(commonEvent())
|
||||
equal(view.$('.date_field').val(), '22 juil. 2017')
|
||||
I18nStubber.popFrame()
|
||||
})
|
||||
|
||||
test('requires name to save assignment note', () => {
|
||||
|
|
|
@ -18,15 +18,21 @@
|
|||
|
||||
import fcUtil from '@canvas/calendar/jquery/fcUtil.coffee'
|
||||
import tz from '@canvas/timezone'
|
||||
import tzInTest from '@canvas/timezone/specHelpers'
|
||||
import timezone from 'timezone'
|
||||
import denver from 'timezone/America/Denver'
|
||||
|
||||
QUnit.module('Calendar', {
|
||||
setup() {
|
||||
this.snapshot = tz.snapshot()
|
||||
tz.changeZone(denver, 'America/Denver')
|
||||
tzInTest.configureAndRestoreLater({
|
||||
tz: timezone(denver, 'America/Denver'),
|
||||
tzData: {
|
||||
'America/Denver': denver
|
||||
}
|
||||
})
|
||||
},
|
||||
teardown() {
|
||||
tz.restore(this.snapshot)
|
||||
tzInTest.restore()
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
@ -23,6 +23,8 @@ import assertions from 'helpers/assertions'
|
|||
import fakeENV from 'helpers/fakeENV'
|
||||
import numberFormat from '@canvas/i18n/numberFormat'
|
||||
import tz from '@canvas/timezone'
|
||||
import tzInTest from '@canvas/timezone/specHelpers'
|
||||
import timezone from 'timezone'
|
||||
import detroit from 'timezone/America/Detroit'
|
||||
import chicago from 'timezone/America/Chicago'
|
||||
import newYork from 'timezone/America/New_York'
|
||||
|
@ -125,12 +127,11 @@ test('supports truncation left', () => {
|
|||
|
||||
QUnit.module('friendlyDatetime', {
|
||||
setup() {
|
||||
this.snapshot = tz.snapshot()
|
||||
return tz.changeZone(detroit, 'America/Detroit')
|
||||
},
|
||||
|
||||
teardown() {
|
||||
tz.restore(this.snapshot)
|
||||
tzInTest.restore()
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -166,16 +167,20 @@ test('includes a visible version', () =>
|
|||
|
||||
QUnit.module('contextSensitive FriendlyDatetime', {
|
||||
setup() {
|
||||
this.snapshot = tz.snapshot()
|
||||
fakeENV.setup()
|
||||
ENV.CONTEXT_TIMEZONE = 'America/Chicago'
|
||||
tz.changeZone(detroit, 'America/Detroit')
|
||||
return tz.preload('America/Chicago', chicago)
|
||||
tzInTest.configureAndRestoreLater({
|
||||
tz: timezone(detroit, 'America/Detroit'),
|
||||
tzData: {
|
||||
'America/Chicago': chicago,
|
||||
'America/Detroit': detroit,
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
teardown() {
|
||||
fakeENV.teardown()
|
||||
tz.restore(this.snapshot)
|
||||
tzInTest.restore()
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -216,14 +221,19 @@ QUnit.module('contextSensitiveDatetimeTitle', {
|
|||
this.snapshot = tz.snapshot()
|
||||
fakeENV.setup()
|
||||
ENV.CONTEXT_TIMEZONE = 'America/Chicago'
|
||||
tz.changeZone(detroit, 'America/Detroit')
|
||||
tz.preload('America/Chicago', chicago)
|
||||
return tz.preload('America/New_York', newYork)
|
||||
tzInTest.configureAndRestoreLater({
|
||||
tz: timezone(detroit, 'America/Detroit'),
|
||||
tzData: {
|
||||
'America/Chicago': chicago,
|
||||
'America/Detroit': detroit,
|
||||
'America/New_York': newYork
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
teardown() {
|
||||
fakeENV.teardown()
|
||||
tz.restore(this.snapshot)
|
||||
tzInTest.restore()
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -244,7 +254,13 @@ test('splits title text to both zones', () => {
|
|||
|
||||
test('properly spans day boundaries', () => {
|
||||
ENV.TIMEZONE = 'America/Chicago'
|
||||
tz.changeZone(chicago, 'America/Chicago')
|
||||
tzInTest.configureAndRestoreLater({
|
||||
tz: timezone(chicago, 'America/Chicago'),
|
||||
tzData: {
|
||||
'America/Chicago': chicago,
|
||||
'America/New_York': newYork
|
||||
}
|
||||
})
|
||||
ENV.CONTEXT_TIMEZONE = 'America/New_York'
|
||||
const titleText = helpers.contextSensitiveDatetimeTitle('1970-01-01 05:30:00Z', {
|
||||
hash: {justText: true}
|
||||
|
|
|
@ -17,8 +17,10 @@
|
|||
*/
|
||||
|
||||
import I18n from '@canvas/i18n'
|
||||
import 'translations/_core_en'
|
||||
|
||||
const frames = []
|
||||
const enTranslations = I18n.translations.en
|
||||
|
||||
export default {
|
||||
pushFrame() {
|
||||
|
@ -40,6 +42,11 @@ export default {
|
|||
clear() {
|
||||
while (frames.length > 0) this.popFrame()
|
||||
},
|
||||
useInitialTranslations() {
|
||||
this.pushFrame()
|
||||
I18n.locale = 'en'
|
||||
I18n.translations = { en: enTranslations }
|
||||
},
|
||||
stub(locale, translations, cb) {
|
||||
if (cb) {
|
||||
return this.withFrame(() => this.stub(locale, translations), cb)
|
||||
|
|
|
@ -34,7 +34,7 @@ QUnit.module('I18n', {
|
|||
},
|
||||
|
||||
teardown() {
|
||||
return I18nStubber.popFrame()
|
||||
return I18nStubber.clear()
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -159,7 +159,7 @@ QUnit.module('I18n localize number', {
|
|||
},
|
||||
|
||||
teardown() {
|
||||
return I18nStubber.popFrame()
|
||||
return I18nStubber.clear()
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
@ -18,6 +18,8 @@
|
|||
|
||||
import $ from 'jquery'
|
||||
import tz from '@canvas/timezone'
|
||||
import tzInTest from '@canvas/timezone/specHelpers'
|
||||
import timezone from 'timezone'
|
||||
import detroit from 'timezone/America/Detroit'
|
||||
import juneau from 'timezone/America/Juneau'
|
||||
import kolkata from 'timezone/Asia/Kolkata'
|
||||
|
@ -27,11 +29,10 @@ import '@canvas/datetime'
|
|||
|
||||
QUnit.module('fudgeDateForProfileTimezone', {
|
||||
setup() {
|
||||
this.snapshot = tz.snapshot()
|
||||
this.original = new Date(Date.UTC(2013, 8, 1))
|
||||
},
|
||||
teardown() {
|
||||
tz.restore(this.snapshot)
|
||||
tzInTest.restore()
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -42,7 +43,17 @@ test('should produce a date that formats via toString same as the original forma
|
|||
|
||||
test('should parse dates before the year 1000', () => {
|
||||
// using specific string (and specific timezone to guarantee it) since tz.format has a bug pre-1000
|
||||
tz.changeZone(detroit, 'America Detroit')
|
||||
//
|
||||
// TODO: in 2021, this appears to be bogus as it's never actually specifying
|
||||
// the timezone as the comment above states because "America Detroit" doesn't
|
||||
// resolve to one (America/Detroit does) and tz just ends up using UTC
|
||||
tzInTest.configureAndRestoreLater({
|
||||
tz: timezone(detroit, 'America Detroit'),
|
||||
tzData: {
|
||||
'America Detroit': detroit,
|
||||
}
|
||||
})
|
||||
|
||||
const oldDate = new Date(Date.UTC(900, 1, 1, 0, 0, 0))
|
||||
const oldFudgeDate = $.fudgeDateForProfileTimezone(oldDate)
|
||||
equal(oldFudgeDate.toString('yyyy-MM-dd HH:mm:ss'), '0900-02-01 00:00:00')
|
||||
|
@ -65,21 +76,30 @@ test('should not return treat 0 as invalid', () =>
|
|||
equal(+$.fudgeDateForProfileTimezone(0), +$.fudgeDateForProfileTimezone(new Date(0))))
|
||||
|
||||
test('should be sensitive to profile time zone', function () {
|
||||
tz.changeZone(detroit, 'America/Detroit')
|
||||
tzInTest.configureAndRestoreLater({
|
||||
tz: timezone(detroit, 'America/Detroit'),
|
||||
tzData: {
|
||||
'America/Detroit': detroit,
|
||||
}
|
||||
})
|
||||
let fudged = $.fudgeDateForProfileTimezone(this.original)
|
||||
equal(fudged.toString('yyyy-MM-dd HH:mm:ss'), tz.format(this.original, '%F %T'))
|
||||
tz.changeZone(juneau, 'America/Juneau')
|
||||
tzInTest.configureAndRestoreLater({
|
||||
tz: timezone(juneau, 'America/Juneau'),
|
||||
tzData: {
|
||||
'America/Juneau': juneau,
|
||||
}
|
||||
})
|
||||
fudged = $.fudgeDateForProfileTimezone(this.original)
|
||||
equal(fudged.toString('yyyy-MM-dd HH:mm:ss'), tz.format(this.original, '%F %T'))
|
||||
})
|
||||
|
||||
QUnit.module('unfudgeDateForProfileTimezone', {
|
||||
setup() {
|
||||
this.snapshot = tz.snapshot()
|
||||
this.original = new Date(Date.UTC(2013, 8, 1))
|
||||
},
|
||||
teardown() {
|
||||
tz.restore(this.snapshot)
|
||||
tzInTest.restore()
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -105,20 +125,28 @@ test('should not return treat 0 as invalid', () =>
|
|||
equal(+$.unfudgeDateForProfileTimezone(0), +$.unfudgeDateForProfileTimezone(new Date(0))))
|
||||
|
||||
test('should be sensitive to profile time zone', function () {
|
||||
tz.changeZone(detroit, 'America/Detroit')
|
||||
tzInTest.configureAndRestoreLater({
|
||||
tz: timezone(detroit, 'America/Detroit'),
|
||||
tzData: {
|
||||
'America/Detroit': detroit,
|
||||
}
|
||||
})
|
||||
|
||||
let unfudged = $.unfudgeDateForProfileTimezone(this.original)
|
||||
equal(tz.format(unfudged, '%F %T'), this.original.toString('yyyy-MM-dd HH:mm:ss'))
|
||||
tz.changeZone(juneau, 'America/Juneau')
|
||||
tzInTest.configureAndRestoreLater({
|
||||
tz: timezone(juneau, 'America/Juneau'),
|
||||
tzData: {
|
||||
'America/Juneau': juneau,
|
||||
}
|
||||
})
|
||||
unfudged = $.unfudgeDateForProfileTimezone(this.original)
|
||||
equal(tz.format(unfudged, '%F %T'), this.original.toString('yyyy-MM-dd HH:mm:ss'))
|
||||
})
|
||||
|
||||
QUnit.module('sameYear', {
|
||||
setup() {
|
||||
this.snapshot = tz.snapshot()
|
||||
},
|
||||
teardown() {
|
||||
tz.restore(this.snapshot)
|
||||
tzInTest.restore()
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -131,7 +159,12 @@ test('should return true iff both dates from same year', () => {
|
|||
})
|
||||
|
||||
test('should compare relative to profile timezone', () => {
|
||||
tz.changeZone(detroit, 'America/Detroit')
|
||||
tzInTest.configureAndRestoreLater({
|
||||
tz: timezone(detroit, 'America/Detroit'),
|
||||
tzData: {
|
||||
'America/Detroit': detroit,
|
||||
}
|
||||
})
|
||||
const date1 = new Date(5 * 3600000) // 5am UTC = 12am EST
|
||||
const date2 = new Date(+date1 + 1000) // Jan 1, 1970 at 11:59:59pm EST
|
||||
const date3 = new Date(+date1 - 1000) // Jan 2, 1970 at 00:00:01am EST
|
||||
|
@ -140,11 +173,8 @@ test('should compare relative to profile timezone', () => {
|
|||
})
|
||||
|
||||
QUnit.module('sameDate', {
|
||||
setup() {
|
||||
this.snapshot = tz.snapshot()
|
||||
},
|
||||
teardown() {
|
||||
tz.restore(this.snapshot)
|
||||
tzInTest.restore()
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -157,7 +187,12 @@ test('should return true iff both times from same day', () => {
|
|||
})
|
||||
|
||||
test('should compare relative to profile timezone', () => {
|
||||
tz.changeZone(detroit, 'America/Detroit')
|
||||
tzInTest.configureAndRestoreLater({
|
||||
tz: timezone(detroit, 'America/Detroit'),
|
||||
tzData: {
|
||||
'America/Detroit': detroit,
|
||||
}
|
||||
})
|
||||
const date1 = new Date(86400000 + 5 * 3600000)
|
||||
const date2 = new Date(+date1 + 1000)
|
||||
const date3 = new Date(+date1 - 1000)
|
||||
|
@ -167,35 +202,43 @@ test('should compare relative to profile timezone', () => {
|
|||
|
||||
QUnit.module('dateString', {
|
||||
setup() {
|
||||
this.snapshot = tz.snapshot()
|
||||
I18nStubber.pushFrame()
|
||||
},
|
||||
teardown() {
|
||||
tz.restore(this.snapshot)
|
||||
I18nStubber.popFrame()
|
||||
tzInTest.restore()
|
||||
I18nStubber.clear()
|
||||
}
|
||||
})
|
||||
|
||||
test('should format in profile timezone', () => {
|
||||
I18nStubber.stub('en', {'date.formats.medium': '%b %-d, %Y'})
|
||||
tz.changeZone(detroit, 'America/Detroit')
|
||||
tzInTest.configureAndRestoreLater({
|
||||
tz: timezone(detroit, 'America/Detroit'),
|
||||
tzData: {
|
||||
'America/Detroit': detroit,
|
||||
}
|
||||
})
|
||||
equal($.dateString(new Date(0)), 'Dec 31, 1969')
|
||||
})
|
||||
|
||||
QUnit.module('timeString', {
|
||||
setup() {
|
||||
this.snapshot = tz.snapshot()
|
||||
I18nStubber.pushFrame()
|
||||
},
|
||||
teardown() {
|
||||
tz.restore(this.snapshot)
|
||||
I18nStubber.popFrame()
|
||||
tzInTest.restore()
|
||||
I18nStubber.clear()
|
||||
}
|
||||
})
|
||||
|
||||
test('should format in profile timezone', () => {
|
||||
I18nStubber.stub('en', {'time.formats.tiny': '%l:%M%P'})
|
||||
tz.changeZone(detroit, 'America/Detroit')
|
||||
tzInTest.configureAndRestoreLater({
|
||||
tz: timezone(detroit, 'America/Detroit'),
|
||||
tzData: {
|
||||
'America/Detroit': detroit,
|
||||
}
|
||||
})
|
||||
equal($.timeString(new Date(60000)), '7:01pm')
|
||||
})
|
||||
|
||||
|
@ -207,7 +250,12 @@ test('should format according to profile locale', () => {
|
|||
|
||||
test('should use the tiny_on_the_hour format on the hour', () => {
|
||||
I18nStubber.stub('en', {'time.formats.tiny_on_the_hour': '%l%P'})
|
||||
tz.changeZone(detroit, 'America/Detroit')
|
||||
tzInTest.configureAndRestoreLater({
|
||||
tz: timezone(detroit, 'America/Detroit'),
|
||||
tzData: {
|
||||
'America/Detroit': detroit,
|
||||
}
|
||||
})
|
||||
equal($.timeString(new Date(0)), '7pm')
|
||||
})
|
||||
|
||||
|
@ -215,23 +263,33 @@ test('should use the tiny format on the hour, when timezone difference is not in
|
|||
I18nStubber.stub('en', {'time.formats.tiny': '%l:%M%P'})
|
||||
I18nStubber.stub('en', {'time.formats.tiny_on_the_hour': '%l%P'})
|
||||
// kolkata: +05:30
|
||||
tz.changeZone(kolkata, 'Asia/Kolkata')
|
||||
tzInTest.configureAndRestoreLater({
|
||||
tz: timezone(kolkata, 'Asia/Kolkata'),
|
||||
tzData: {
|
||||
'Asia/Kolkata': kolkata,
|
||||
'America/Detroit': detroit
|
||||
}
|
||||
})
|
||||
equal($.timeString(new Date(30 * 60 * 1000), {timezone: 'America/Detroit'}), '7:30pm')
|
||||
})
|
||||
|
||||
QUnit.module('datetimeString', {
|
||||
setup() {
|
||||
this.snapshot = tz.snapshot()
|
||||
I18nStubber.pushFrame()
|
||||
},
|
||||
teardown() {
|
||||
tz.restore(this.snapshot)
|
||||
I18nStubber.popFrame()
|
||||
tzInTest.restore()
|
||||
I18nStubber.clear()
|
||||
}
|
||||
})
|
||||
|
||||
test('should format in profile timezone', () => {
|
||||
tz.changeZone(detroit, 'America/Detroit')
|
||||
tzInTest.configureAndRestoreLater({
|
||||
tz: timezone(detroit, 'America/Detroit'),
|
||||
tzData: {
|
||||
'America/Detroit': detroit,
|
||||
}
|
||||
})
|
||||
I18nStubber.stub('en', {
|
||||
'date.formats.medium': '%b %-d, %Y',
|
||||
'time.formats.tiny': '%l:%M%P',
|
||||
|
@ -241,7 +299,11 @@ test('should format in profile timezone', () => {
|
|||
})
|
||||
|
||||
test('should translate into the profile locale', () => {
|
||||
tz.changeLocale(portuguese, 'pt_PT', 'pt')
|
||||
tzInTest.configureAndRestoreLater({
|
||||
tz: timezone(portuguese, 'pt_PT'),
|
||||
momentLocale: 'pt'
|
||||
})
|
||||
|
||||
I18nStubber.setLocale('pt')
|
||||
I18nStubber.stub('pt', {
|
||||
'date.formats.medium': '%-d %b %Y',
|
||||
|
@ -253,18 +315,23 @@ test('should translate into the profile locale', () => {
|
|||
|
||||
QUnit.module('$.datepicker.parseDate', {
|
||||
setup() {
|
||||
this.snapshot = tz.snapshot()
|
||||
I18nStubber.pushFrame()
|
||||
},
|
||||
teardown() {
|
||||
tz.restore(this.snapshot)
|
||||
I18nStubber.popFrame()
|
||||
tzInTest.restore()
|
||||
I18nStubber.clear()
|
||||
}
|
||||
})
|
||||
|
||||
test('should accept localized strings and return them fudged', () => {
|
||||
tz.changeZone(detroit, 'America/Detroit')
|
||||
tz.changeLocale(portuguese, 'pt_PT', 'pt')
|
||||
tzInTest.configureAndRestoreLater({
|
||||
tz: timezone(detroit, 'America/Detroit', portuguese, 'pt_PT'),
|
||||
tzData: {
|
||||
'America/Detroit': detroit
|
||||
},
|
||||
momentLocale: 'pt'
|
||||
})
|
||||
|
||||
I18nStubber.setLocale('pt')
|
||||
I18nStubber.stub('pt', {
|
||||
// this isn't the real format, but we want the %Y in here to make it
|
||||
|
|
|
@ -21,6 +21,7 @@ import {mount} from 'enzyme'
|
|||
import chicago from 'timezone/America/Chicago'
|
||||
import DueDateCalendarPicker from '@canvas/due-dates/react/DueDateCalendarPicker'
|
||||
import tz from '@canvas/timezone'
|
||||
import tzInTest from '@canvas/timezone/specHelpers'
|
||||
import french from 'timezone/fr_FR'
|
||||
import I18nStubber from 'helpers/I18nStubber'
|
||||
import fakeENV from 'helpers/fakeENV'
|
||||
|
@ -52,6 +53,7 @@ QUnit.module('DueDateCalendarPicker', suiteHooks => {
|
|||
wrapper.unmount()
|
||||
clock.restore()
|
||||
fakeENV.teardown()
|
||||
tzInTest.restore()
|
||||
})
|
||||
|
||||
function mountComponent() {
|
||||
|
@ -79,10 +81,8 @@ QUnit.module('DueDateCalendarPicker', suiteHooks => {
|
|||
test('converts to fancy midnight in the timezone of the user', () => {
|
||||
props.isFancyMidnight = true
|
||||
mountComponent()
|
||||
const snapshot = tz.snapshot()
|
||||
tz.changeZone(chicago, 'America/Chicago')
|
||||
tzInTest.changeZone(chicago, 'America/Chicago')
|
||||
simulateChange('2015-08-31T00:00:00')
|
||||
tz.restore(snapshot)
|
||||
equal(getEnteredDate().toUTCString(), 'Tue, 01 Sep 2015 04:59:59 GMT')
|
||||
})
|
||||
|
||||
|
@ -99,8 +99,7 @@ QUnit.module('DueDateCalendarPicker', suiteHooks => {
|
|||
|
||||
test('#formattedDate() returns a localized Date', () => {
|
||||
mountComponent()
|
||||
const snapshot = tz.snapshot()
|
||||
tz.changeLocale(french, 'fr_FR', 'fr')
|
||||
tzInTest.changeLocale(french, 'fr_FR', 'fr')
|
||||
I18nStubber.pushFrame()
|
||||
I18nStubber.setLocale('fr_FR')
|
||||
I18nStubber.stub('fr_FR', {
|
||||
|
@ -108,8 +107,7 @@ QUnit.module('DueDateCalendarPicker', suiteHooks => {
|
|||
'time.formats.tiny': '%-k:%M'
|
||||
})
|
||||
equal(wrapper.instance().formattedDate(), '1 févr. 2012 7:01')
|
||||
I18nStubber.popFrame()
|
||||
tz.restore(snapshot)
|
||||
I18nStubber.clear()
|
||||
})
|
||||
|
||||
test('call the update prop when changed', () => {
|
||||
|
|
|
@ -19,6 +19,8 @@
|
|||
import DateHelper from '@canvas/datetime/dateHelper'
|
||||
import {isDate, isNull, isUndefined} from 'lodash'
|
||||
import tz from '@canvas/timezone'
|
||||
import tzInTest from '@canvas/timezone/specHelpers'
|
||||
import timezone from 'timezone'
|
||||
import detroit from 'timezone/America/Detroit'
|
||||
import juneau from 'timezone/America/Juneau'
|
||||
|
||||
|
@ -59,19 +61,28 @@ test('gracefully handles undefined values', () => {
|
|||
|
||||
QUnit.module('DateHelper#formatDatetimeForDisplay', {
|
||||
setup() {
|
||||
this.snapshot = tz.snapshot()
|
||||
},
|
||||
teardown() {
|
||||
tz.restore(this.snapshot)
|
||||
tzInTest.restore()
|
||||
}
|
||||
})
|
||||
|
||||
test('formats the date for display, adjusted for the timezone', () => {
|
||||
const assignment = defaultAssignment()
|
||||
tz.changeZone(detroit, 'America/Detroit')
|
||||
tzInTest.configureAndRestoreLater({
|
||||
tz: timezone(detroit, 'America/Detroit'),
|
||||
tzData: {
|
||||
'America/Detroit': detroit
|
||||
}
|
||||
})
|
||||
let formattedDate = DateHelper.formatDatetimeForDisplay(assignment.due_at)
|
||||
equal(formattedDate, 'Jul 14, 2015 at 2:35pm')
|
||||
tz.changeZone(juneau, 'America/Juneau')
|
||||
tzInTest.configureAndRestoreLater({
|
||||
tz: timezone(juneau, 'America/Juneau'),
|
||||
tzData: {
|
||||
'America/Juneau': juneau
|
||||
}
|
||||
})
|
||||
formattedDate = DateHelper.formatDatetimeForDisplay(assignment.due_at)
|
||||
equal(formattedDate, 'Jul 14, 2015 at 10:35am')
|
||||
})
|
||||
|
@ -93,37 +104,51 @@ test("can specify 'short' format which excludes the year if it matches the curre
|
|||
})
|
||||
|
||||
QUnit.module('DateHelper#formatDateForDisplay', {
|
||||
setup() {
|
||||
this.snapshot = tz.snapshot()
|
||||
},
|
||||
teardown() {
|
||||
tz.restore(this.snapshot)
|
||||
tzInTest.restore()
|
||||
}
|
||||
})
|
||||
|
||||
test('formats the date for display, adjusted for the timezone, excluding the time', () => {
|
||||
const assignment = defaultAssignment()
|
||||
tz.changeZone(detroit, 'America/Detroit')
|
||||
tzInTest.configureAndRestoreLater({
|
||||
tz: timezone(detroit, 'America/Detroit'),
|
||||
tzData: {
|
||||
'America/Detroit': detroit
|
||||
}
|
||||
})
|
||||
let formattedDate = DateHelper.formatDateForDisplay(assignment.due_at)
|
||||
equal(formattedDate, 'Jul 14, 2015')
|
||||
tz.changeZone(juneau, 'America/Juneau')
|
||||
tzInTest.configureAndRestoreLater({
|
||||
tz: timezone(juneau, 'America/Juneau'),
|
||||
tzData: {
|
||||
'America/Juneau': juneau
|
||||
}
|
||||
})
|
||||
formattedDate = DateHelper.formatDateForDisplay(assignment.due_at)
|
||||
equal(formattedDate, 'Jul 14, 2015')
|
||||
})
|
||||
|
||||
QUnit.module('DateHelper#isMidnight', {
|
||||
setup() {
|
||||
this.snapshot = tz.snapshot()
|
||||
},
|
||||
teardown() {
|
||||
tz.restore(this.snapshot)
|
||||
tzInTest.restore()
|
||||
}
|
||||
})
|
||||
|
||||
test('returns true if the time is midnight, adjusted for the timezone', () => {
|
||||
const date = '2015-07-14T04:00:00Z'
|
||||
tz.changeZone(detroit, 'America/Detroit')
|
||||
tzInTest.configureAndRestoreLater({
|
||||
tz: timezone(detroit, 'America/Detroit'),
|
||||
tzData: {
|
||||
'America/Detroit': detroit
|
||||
}
|
||||
})
|
||||
ok(DateHelper.isMidnight(date))
|
||||
tz.changeZone(juneau, 'America/Juneau')
|
||||
tzInTest.configureAndRestoreLater({
|
||||
tz: timezone(juneau, 'America/Juneau'),
|
||||
tzData: {
|
||||
'America/Juneau': juneau
|
||||
}
|
||||
})
|
||||
notOk(DateHelper.isMidnight(date))
|
||||
})
|
||||
|
|
|
@ -19,6 +19,8 @@
|
|||
import $ from 'jquery'
|
||||
import _ from 'lodash'
|
||||
import tz from '@canvas/timezone'
|
||||
import tzInTest from '@canvas/timezone/specHelpers'
|
||||
import timezone from 'timezone'
|
||||
import denver from 'timezone/America/Denver'
|
||||
import newYork from 'timezone/America/New_York'
|
||||
import SyllabusBehaviors from '@canvas/syllabus/backbone/behaviors/SyllabusBehaviors'
|
||||
|
@ -119,9 +121,13 @@ QUnit.module('Syllabus', {
|
|||
// Setup stubs/mocks
|
||||
this.server = setupServerResponses()
|
||||
|
||||
this.tzSnapshot = tz.snapshot()
|
||||
tz.changeZone(denver, 'America/Denver')
|
||||
tz.preload('America/New_York', newYork)
|
||||
tzInTest.configureAndRestoreLater({
|
||||
tz: timezone(denver, 'America/Denver'),
|
||||
tzData: {
|
||||
'America/Denver': denver,
|
||||
'America/New_York': newYork
|
||||
}
|
||||
})
|
||||
|
||||
this.clock = sinon.useFakeTimers(new Date(2012, 0, 23, 15, 30).getTime())
|
||||
|
||||
|
@ -190,7 +196,7 @@ QUnit.module('Syllabus', {
|
|||
this.miniMonth.remove()
|
||||
this.jumpToToday.remove()
|
||||
this.clock.restore()
|
||||
tz.restore(this.tzSnapshot)
|
||||
tzInTest.restore()
|
||||
this.server.restore()
|
||||
document.getElementById('fixtures').innerHTML = ''
|
||||
},
|
||||
|
|
|
@ -23,7 +23,8 @@ import Assignment from '@canvas/assignments/backbone/models/Assignment.coffee'
|
|||
import Submission from '@canvas/assignments/backbone/models/Submission'
|
||||
import AssignmentListItemView from 'ui/features/assignment_index/backbone/views/AssignmentListItemView.coffee'
|
||||
import $ from 'jquery'
|
||||
import tz from '@canvas/timezone'
|
||||
import tzInTest from '@canvas/timezone/specHelpers'
|
||||
import timezone from 'timezone'
|
||||
import juneau from 'timezone/America/Juneau'
|
||||
import french from 'timezone/fr_FR'
|
||||
import I18nStubber from 'helpers/I18nStubber'
|
||||
|
@ -169,14 +170,13 @@ QUnit.module('AssignmentListItemViewSpec', {
|
|||
URLS: {assignment_sort_base_url: 'test'}
|
||||
})
|
||||
genSetup.call(this)
|
||||
this.snapshot = tz.snapshot()
|
||||
return I18nStubber.pushFrame()
|
||||
},
|
||||
teardown() {
|
||||
fakeENV.teardown()
|
||||
genTeardown.call(this)
|
||||
tz.restore(this.snapshot)
|
||||
return I18nStubber.popFrame()
|
||||
tzInTest.restore()
|
||||
return I18nStubber.clear()
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -542,7 +542,10 @@ test('does not render score template without permission', function () {
|
|||
})
|
||||
|
||||
test('renders lockAt/unlockAt with locale-appropriate format string', function () {
|
||||
tz.changeLocale(french, 'fr_FR', 'fr')
|
||||
tzInTest.configureAndRestoreLater({
|
||||
tz: timezone(french, 'fr_FR'),
|
||||
momentLocale: 'fr'
|
||||
})
|
||||
I18nStubber.setLocale('fr_FR')
|
||||
I18nStubber.stub('fr_FR', {
|
||||
'date.formats.short': '%-d %b',
|
||||
|
@ -570,7 +573,12 @@ test('renders lockAt/unlockAt with locale-appropriate format string', function (
|
|||
})
|
||||
|
||||
test('renders lockAt/unlockAt in appropriate time zone', function () {
|
||||
tz.changeZone(juneau, 'America/Juneau')
|
||||
tzInTest.configureAndRestoreLater({
|
||||
tz: timezone(juneau, 'America/Juneau'),
|
||||
tzData: {
|
||||
'America/Juneau': juneau
|
||||
}
|
||||
})
|
||||
I18nStubber.stub('en', {
|
||||
'date.formats.short': '%b %-d',
|
||||
'date.formats.date_at_time': '%b %-d at %l:%M%P',
|
||||
|
@ -646,7 +654,10 @@ test('does not render lockAt/unlockAt when not locking in future', () => {
|
|||
})
|
||||
|
||||
test('renders due date column with locale-appropriate format string', function () {
|
||||
tz.changeLocale(french, 'fr_FR', 'fr')
|
||||
tzInTest.configureAndRestoreLater({
|
||||
tz: timezone(french, 'fr_FR'),
|
||||
momentLocale: 'fr'
|
||||
})
|
||||
I18nStubber.setLocale('fr_FR')
|
||||
I18nStubber.stub('fr_FR', {
|
||||
'date.formats.short': '%-d %b',
|
||||
|
@ -660,7 +671,12 @@ test('renders due date column with locale-appropriate format string', function (
|
|||
})
|
||||
|
||||
test('renders due date column in appropriate time zone', function () {
|
||||
tz.changeZone(juneau, 'America/Juneau')
|
||||
tzInTest.configureAndRestoreLater({
|
||||
tz: timezone(juneau, 'America/Juneau'),
|
||||
tzData: {
|
||||
'America/Juneau': juneau
|
||||
}
|
||||
})
|
||||
I18nStubber.stub('en', {
|
||||
'date.formats.short': '%b %-d',
|
||||
'date.abbr_month_names.8': 'Aug'
|
||||
|
|
|
@ -23,6 +23,8 @@ import CreateAssignmentView from 'ui/features/assignment_index/backbone/views/Cr
|
|||
import DialogFormView from '@canvas/forms/backbone/views/DialogFormView.coffee'
|
||||
import $ from 'jquery'
|
||||
import tz from '@canvas/timezone'
|
||||
import tzInTest from '@canvas/timezone/specHelpers'
|
||||
import timezone from 'timezone'
|
||||
import juneau from 'timezone/America/Juneau'
|
||||
import french from 'timezone/fr_FR'
|
||||
import I18nStubber from 'helpers/I18nStubber'
|
||||
|
@ -142,14 +144,13 @@ QUnit.module('CreateAssignmentView', {
|
|||
this.assignment4 = new Assignment(buildAssignment4())
|
||||
this.assignment5 = new Assignment(buildAssignment5())
|
||||
this.group = assignmentGroup()
|
||||
this.snapshot = tz.snapshot()
|
||||
I18nStubber.pushFrame()
|
||||
fakeENV.setup()
|
||||
},
|
||||
teardown() {
|
||||
fakeENV.teardown()
|
||||
tz.restore(this.snapshot)
|
||||
I18nStubber.popFrame()
|
||||
tzInTest.restore()
|
||||
I18nStubber.clear()
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -325,12 +326,7 @@ test('openAgain adds datetime picker', function() {
|
|||
|
||||
test('adjust datetime to the end of a day for midnight time', function() {
|
||||
sandbox.stub(DialogFormView.prototype, 'openAgain')
|
||||
I18nStubber.setLocale('fr_FR')
|
||||
// tz use the key/values to get formats for different locale
|
||||
I18nStubber.stub('fr_FR', {
|
||||
'date.formats.short': '%b %-d',
|
||||
'time.formats.tiny': '%l:%M%P'
|
||||
})
|
||||
I18nStubber.useInitialTranslations()
|
||||
const tmp = $.screenReaderFlashMessageExclusive
|
||||
$.screenReaderFlashMessageExclusive = sinon.spy()
|
||||
const view = createView(this.assignment2)
|
||||
|
@ -345,12 +341,7 @@ test('adjust datetime to the end of a day for midnight time', function() {
|
|||
|
||||
test('it does not adjust datetime for other date time', function() {
|
||||
sandbox.stub(DialogFormView.prototype, 'openAgain')
|
||||
I18nStubber.setLocale('fr_FR')
|
||||
// tz use the key/values to get formats for different locale
|
||||
I18nStubber.stub('fr_FR', {
|
||||
'date.formats.short': '%b %-d',
|
||||
'time.formats.tiny': '%l:%M%P'
|
||||
})
|
||||
I18nStubber.useInitialTranslations()
|
||||
const tmp = $.screenReaderFlashMessageExclusive
|
||||
$.screenReaderFlashMessageExclusive = sinon.spy()
|
||||
const view = createView(this.assignment2)
|
||||
|
@ -553,7 +544,10 @@ test('validates due date for lock and unlock', function() {
|
|||
})
|
||||
|
||||
test('renders due dates with locale-appropriate format string', function() {
|
||||
tz.changeLocale(french, 'fr_FR', 'fr')
|
||||
tzInTest.configureAndRestoreLater({
|
||||
tz: timezone(french, 'fr_FR'),
|
||||
momentLocale: 'fr'
|
||||
})
|
||||
I18nStubber.setLocale('fr_FR')
|
||||
I18nStubber.stub('fr_FR', {
|
||||
'date.formats.short': '%-d %b',
|
||||
|
@ -571,7 +565,13 @@ test('renders due dates with locale-appropriate format string', function() {
|
|||
})
|
||||
|
||||
test('renders due dates in appropriate time zone', function() {
|
||||
tz.changeZone(juneau, 'America/Juneau')
|
||||
tzInTest.configureAndRestoreLater({
|
||||
tz: timezone(juneau, 'America/Juneau'),
|
||||
tzData: {
|
||||
'America/Juneau': juneau
|
||||
}
|
||||
})
|
||||
|
||||
I18nStubber.stub('en', {
|
||||
'date.formats.short': '%b %-d',
|
||||
'date.abbr_month_names.8': 'Aug'
|
||||
|
|
|
@ -667,7 +667,7 @@ test('validates i18n mastery points', function() {
|
|||
view.$('input[name="mastery_points"]').val('1 234,5')
|
||||
ok(view.isValid())
|
||||
view.remove()
|
||||
I18nStubber.popFrame()
|
||||
I18nStubber.clear()
|
||||
})
|
||||
|
||||
test('getModifiedFields returns false for all fields when not modified', () => {
|
||||
|
|
|
@ -19,6 +19,8 @@
|
|||
import DatetimeField from '@canvas/datetime/jquery/DatetimeField'
|
||||
import $ from 'jquery'
|
||||
import tz from '@canvas/timezone'
|
||||
import tzInTest from '@canvas/timezone/specHelpers'
|
||||
import timezone from 'timezone'
|
||||
import detroit from 'timezone/America/Detroit'
|
||||
import juneau from 'timezone/America/Juneau'
|
||||
import portuguese from 'timezone/pt_PT'
|
||||
|
@ -253,13 +255,12 @@ test('should set suggest text', function() {
|
|||
|
||||
QUnit.module('parseValue', {
|
||||
setup() {
|
||||
this.snapshot = tz.snapshot()
|
||||
this.$field = $('<input type="text" name="due_at">')
|
||||
this.field = new DatetimeField(this.$field, {})
|
||||
},
|
||||
|
||||
teardown() {
|
||||
tz.restore(this.snapshot)
|
||||
tzInTest.restore()
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -355,7 +356,6 @@ test('interprets time-only fields as occurring on implicit date if set', functio
|
|||
|
||||
QUnit.module('updateData', {
|
||||
setup() {
|
||||
this.snapshot = tz.snapshot()
|
||||
tz.changeZone(detroit, 'America/Detroit')
|
||||
this.$field = $('<input type="text" name="due_at">')
|
||||
this.$field.val('Jan 1, 1970 at 12:01am')
|
||||
|
@ -365,7 +365,7 @@ QUnit.module('updateData', {
|
|||
},
|
||||
|
||||
teardown() {
|
||||
tz.restore(this.snapshot)
|
||||
tzInTest.restore()
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -403,7 +403,13 @@ test('sets time-* to fudged, 12-hour values', function() {
|
|||
})
|
||||
|
||||
test('sets time-* to fudged, 24-hour values', function() {
|
||||
tz.changeLocale(portuguese, 'pt_PT', 'pt')
|
||||
tzInTest.configureAndRestoreLater({
|
||||
tz: timezone(detroit, 'America/Detroit', portuguese, 'pt_PT'),
|
||||
tzData: {
|
||||
'America/Detroit': detroit,
|
||||
},
|
||||
momentLocale: 'pt'
|
||||
})
|
||||
this.field.updateData()
|
||||
equal(this.$field.data('time-hour'), '21')
|
||||
equal(this.$field.data('time-minute'), '56')
|
||||
|
@ -573,19 +579,29 @@ test('returns time only if @showDate false', function() {
|
|||
})
|
||||
|
||||
test('localizes formatting of dates and times', function() {
|
||||
tz.changeLocale(portuguese, 'pt_PT', 'pt')
|
||||
tzInTest.configureAndRestoreLater({
|
||||
tz: timezone(detroit, 'America/Detroit', portuguese, 'pt_PT'),
|
||||
tzData: {
|
||||
'America/Detroit': detroit,
|
||||
},
|
||||
momentLocale: 'pt'
|
||||
})
|
||||
I18nStubber.pushFrame()
|
||||
I18nStubber.setLocale('pt_PT')
|
||||
I18nStubber.stub('pt_PT', {'date.formats.full_with_weekday': '%a, %-d %b %Y %k:%M'})
|
||||
equal(this.field.formatSuggest(), 'Dom, 20 Jul 1969 21:56')
|
||||
I18nStubber.popFrame()
|
||||
I18nStubber.clear()
|
||||
})
|
||||
|
||||
QUnit.module('formatSuggestCourse', {
|
||||
setup() {
|
||||
this.snapshot = tz.snapshot()
|
||||
tz.changeZone(detroit, 'America/Detroit')
|
||||
tz.preload('America/Juneau', juneau)
|
||||
tzInTest.configureAndRestoreLater({
|
||||
tz: timezone(detroit, 'America/Detroit'),
|
||||
tzData: {
|
||||
'America/Detroit': detroit,
|
||||
'America/Juneau': juneau,
|
||||
}
|
||||
})
|
||||
fakeENV.setup({TIMEZONE: 'America/Detroit', CONTEXT_TIMEZONE: 'America/Juneau'})
|
||||
this.$field = $('<input type="text" name="due_at">')
|
||||
this.$field.val('Jul 20, 1969 at 9:56pm')
|
||||
|
@ -594,7 +610,7 @@ QUnit.module('formatSuggestCourse', {
|
|||
|
||||
teardown() {
|
||||
fakeENV.teardown()
|
||||
tz.restore(this.snapshot)
|
||||
tzInTest.restore()
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -719,13 +735,19 @@ test('formats value into val() according to format parameter', function() {
|
|||
})
|
||||
|
||||
test('localizes value', function() {
|
||||
tz.changeLocale(portuguese, 'pt_PT', 'pt')
|
||||
tzInTest.configureAndRestoreLater({
|
||||
tz: timezone(detroit, 'America/Detroit', portuguese, 'pt_PT'),
|
||||
tzData: {
|
||||
'America/Detroit': detroit,
|
||||
},
|
||||
momentLocale: 'pt'
|
||||
})
|
||||
I18nStubber.pushFrame()
|
||||
I18nStubber.setLocale('pt_PT')
|
||||
I18nStubber.stub('pt_PT', {'date.formats.full': '%-d %b %Y %-k:%M'})
|
||||
this.field.setFormattedDatetime(moonwalk, 'date.formats.full')
|
||||
equal(this.$field.val(), '20 Jul 1969 21:56')
|
||||
I18nStubber.popFrame()
|
||||
I18nStubber.clear()
|
||||
})
|
||||
|
||||
QUnit.module('setDate/setTime/setDatetime', {
|
||||
|
|
|
@ -37,6 +37,7 @@ QUnit.module('EditConferenceView', {
|
|||
this.view.$el.remove()
|
||||
fakeENV.teardown()
|
||||
tz.restore(this.snapshot)
|
||||
I18nStubber.clear()
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -49,7 +50,6 @@ test('updateConferenceUserSettingDetailsForConference localizes values for datep
|
|||
const conferenceData = {user_settings: {datepickerSetting: '2015-08-07T17:00:00Z'}}
|
||||
this.view.updateConferenceUserSettingDetailsForConference(conferenceData)
|
||||
equal(this.datepickerSetting.value, 'ven. 7 août, 2015 17:00')
|
||||
I18nStubber.popFrame()
|
||||
})
|
||||
|
||||
test('#show sets the proper title for new conferences', function() {
|
||||
|
|
|
@ -22,6 +22,8 @@ import {mount} from 'enzyme'
|
|||
import GradingPeriodForm from 'ui/features/account_grading_standards/react/GradingPeriodForm.js'
|
||||
import chicago from 'timezone/America/Chicago'
|
||||
import tz from '@canvas/timezone'
|
||||
import tzInTest from '@canvas/timezone/specHelpers'
|
||||
import timezone from 'timezone'
|
||||
import fakeENV from 'helpers/fakeENV'
|
||||
|
||||
QUnit.module('GradingPeriodForm', suiteHooks => {
|
||||
|
@ -123,10 +125,17 @@ QUnit.module('GradingPeriodForm', suiteHooks => {
|
|||
QUnit.module('when local and server time are different', hooks => {
|
||||
hooks.beforeEach(() => {
|
||||
Object.assign(ENV, {CONTEXT_TIMEZONE: 'America/Chicago'})
|
||||
tz.preload('America/Chicago', chicago)
|
||||
tzInTest.configureAndRestoreLater({
|
||||
tz: timezone('UTC'),
|
||||
tzData: {
|
||||
'America/Chicago': chicago
|
||||
}
|
||||
})
|
||||
mountComponent()
|
||||
})
|
||||
|
||||
hooks.afterEach(tzInTest.restore)
|
||||
|
||||
test('shows both local and context time suggestions for start date', () => {
|
||||
strictEqual(getDateTimeSuggestions('Start Date').length, 2)
|
||||
})
|
||||
|
@ -168,10 +177,17 @@ QUnit.module('GradingPeriodForm', suiteHooks => {
|
|||
QUnit.module('when local and server time are different', hooks => {
|
||||
hooks.beforeEach(() => {
|
||||
Object.assign(ENV, {CONTEXT_TIMEZONE: 'America/Chicago'})
|
||||
tz.preload('America/Chicago', chicago)
|
||||
tzInTest.configureAndRestoreLater({
|
||||
tz: timezone('UTC'),
|
||||
tzData: {
|
||||
'America/Chicago': chicago
|
||||
}
|
||||
})
|
||||
mountComponent()
|
||||
})
|
||||
|
||||
hooks.afterEach(tzInTest.restore)
|
||||
|
||||
test('shows both local and context time suggestions for end date', () => {
|
||||
strictEqual(getDateTimeSuggestions('End Date').length, 2)
|
||||
})
|
||||
|
@ -213,10 +229,17 @@ QUnit.module('GradingPeriodForm', suiteHooks => {
|
|||
QUnit.module('when local and server time are different', hooks => {
|
||||
hooks.beforeEach(() => {
|
||||
Object.assign(ENV, {CONTEXT_TIMEZONE: 'America/Chicago'})
|
||||
tz.preload('America/Chicago', chicago)
|
||||
tzInTest.configureAndRestoreLater({
|
||||
tz: timezone('UTC'),
|
||||
tzData: {
|
||||
'America/Chicago': chicago
|
||||
}
|
||||
})
|
||||
mountComponent()
|
||||
})
|
||||
|
||||
hooks.afterEach(tzInTest.restore)
|
||||
|
||||
test('shows both local and context time suggestions for close date', () => {
|
||||
strictEqual(getDateTimeSuggestions('Close Date').length, 2)
|
||||
})
|
||||
|
|
|
@ -1,188 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2017 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import sinon from 'sinon'
|
||||
import localeConfig from 'json-loader!yaml-loader!../../../config/locales/locales.yml'
|
||||
import tz from 'timezone_core'
|
||||
import DatetimeField from 'compiled/widget/DatetimeField'
|
||||
import $ from 'jquery'
|
||||
import I18n from 'i18nObj'
|
||||
import 'translations/_core'
|
||||
import 'translations/_core_en'
|
||||
|
||||
import bigeasyLocales from 'timezone/locales'
|
||||
import bigeasyLocale_ca_ES from 'custom_timezone_locales/ca_ES'
|
||||
import bigeasyLocale_cy_GB from 'custom_timezone_locales/cy_GB'
|
||||
import bigeasyLocale_de_DE from 'custom_timezone_locales/de_DE'
|
||||
import bigeasyLocale_fr_FR from 'custom_timezone_locales/fr_FR'
|
||||
import bigeasyLocale_fr_CA from 'custom_timezone_locales/fr_CA'
|
||||
import bigeasyLocale_he_IL from 'custom_timezone_locales/he_IL'
|
||||
import bigeasyLocale_pl_PL from 'custom_timezone_locales/pl_PL'
|
||||
import bigeasyLocale_is_IS from 'custom_timezone_locales/is_IS'
|
||||
import bigeasyLocale_ar_SA from 'custom_timezone_locales/ar_SA'
|
||||
import bigeasyLocale_da_DK from 'custom_timezone_locales/da_DK'
|
||||
import bigeasyLocale_fa_IR from 'custom_timezone_locales/fa_IR'
|
||||
import bigeasyLocale_ht_HT from 'custom_timezone_locales/ht_HT'
|
||||
import bigeasyLocale_hy_AM from 'custom_timezone_locales/hy_AM'
|
||||
import bigeasyLocale_mi_NZ from 'custom_timezone_locales/mi_NZ'
|
||||
import bigeasyLocale_nn_NO from 'custom_timezone_locales/nn_NO'
|
||||
import bigeasyLocale_tr_TR from 'custom_timezone_locales/tr_TR'
|
||||
import bigeasyLocale_uk_UA from 'custom_timezone_locales/uk_UA'
|
||||
import bigeasyLocale_el_GR from 'custom_timezone_locales/el_GR'
|
||||
|
||||
import 'custom_moment_locales/ca'
|
||||
import 'custom_moment_locales/de'
|
||||
import 'custom_moment_locales/he'
|
||||
import 'custom_moment_locales/pl'
|
||||
import 'custom_moment_locales/fa'
|
||||
import 'custom_moment_locales/fr'
|
||||
import 'custom_moment_locales/fr_ca'
|
||||
import 'custom_moment_locales/ht_ht'
|
||||
import 'custom_moment_locales/mi_nz'
|
||||
import 'custom_moment_locales/hy_am'
|
||||
import 'custom_moment_locales/sl'
|
||||
|
||||
let originalLocale
|
||||
let originalFallbacksMap
|
||||
|
||||
const bigeasyLocalesWithCustom = [
|
||||
...bigeasyLocales,
|
||||
bigeasyLocale_ca_ES,
|
||||
bigeasyLocale_cy_GB,
|
||||
bigeasyLocale_de_DE,
|
||||
bigeasyLocale_fr_FR,
|
||||
bigeasyLocale_fr_CA,
|
||||
bigeasyLocale_he_IL,
|
||||
bigeasyLocale_pl_PL,
|
||||
bigeasyLocale_is_IS,
|
||||
bigeasyLocale_ar_SA,
|
||||
bigeasyLocale_da_DK,
|
||||
bigeasyLocale_fa_IR,
|
||||
bigeasyLocale_ht_HT,
|
||||
bigeasyLocale_hy_AM,
|
||||
bigeasyLocale_mi_NZ,
|
||||
bigeasyLocale_nn_NO,
|
||||
bigeasyLocale_tr_TR,
|
||||
bigeasyLocale_uk_UA,
|
||||
bigeasyLocale_el_GR
|
||||
]
|
||||
|
||||
const preloadedData = bigeasyLocalesWithCustom.reduce((memo, locale) => {
|
||||
memo[locale.name] = locale
|
||||
return memo
|
||||
}, {})
|
||||
|
||||
QUnit.module('Parsing locale formatted dates', {
|
||||
setup() {
|
||||
originalLocale = I18n.locale
|
||||
sinon.stub(tz, 'preload').callsFake(name => preloadedData[name])
|
||||
originalFallbacksMap = I18n.fallbacksMap
|
||||
I18n.fallbacksMap = null
|
||||
},
|
||||
|
||||
teardown() {
|
||||
I18n.locale = originalLocale
|
||||
I18n.fallbacksMap = originalFallbacksMap
|
||||
tz.preload.restore()
|
||||
}
|
||||
})
|
||||
|
||||
const locales = Object.keys(localeConfig)
|
||||
.map(key => {
|
||||
const locale = localeConfig[key]
|
||||
const base = key.split('-')[0]
|
||||
return {
|
||||
key,
|
||||
moment: locale.moment_locale || key.toLowerCase(),
|
||||
bigeasy: locale.bigeasy_locale || localeConfig[base].bigeasy_locale
|
||||
}
|
||||
})
|
||||
.filter(l => l.key)
|
||||
|
||||
const dates = []
|
||||
const currentYear = parseInt(tz.format(new Date(), '%Y'), 10)
|
||||
const otherYear = currentYear + 4
|
||||
for (let i = 0; i < 12; ++i) {
|
||||
dates.push(new Date(Date.UTC(currentYear, i, 1, 23, 59)))
|
||||
dates.push(new Date(Date.UTC(currentYear, i, 28, 23, 59)))
|
||||
dates.push(new Date(Date.UTC(otherYear, i, 7, 23, 59)))
|
||||
dates.push(new Date(Date.UTC(otherYear, i, 15, 23, 59)))
|
||||
}
|
||||
|
||||
function assertFormattedParsesToDate(formatted, date) {
|
||||
const parsed = tz.parse(formatted)
|
||||
const formattedDate = tz.format(parsed, 'date.formats.medium')
|
||||
const formattedTime = tz.format(parsed, 'time.formats.tiny')
|
||||
const formattedParsed = `${formattedDate} ${formattedTime}`
|
||||
equal(date.getTime(), parsed.getTime(), `${formatted} incorrectly parsed as ${formattedParsed}`)
|
||||
}
|
||||
|
||||
locales.forEach(locale => {
|
||||
test(`timezone -> moment for ${locale.key}`, () => {
|
||||
I18n.locale = locale.key
|
||||
try {
|
||||
tz.changeLocale(locale.bigeasy, locale.moment)
|
||||
dates.forEach(date => {
|
||||
const formattedDate = $.dateString(date)
|
||||
const formattedTime = tz.format(date, 'time.formats.tiny')
|
||||
const formatted = `${formattedDate} ${formattedTime}`
|
||||
assertFormattedParsesToDate(formatted, date)
|
||||
})
|
||||
} catch (err) {
|
||||
ok(false, err.message)
|
||||
}
|
||||
})
|
||||
|
||||
test(`datepicker -> moment for ${locale.key}`, () => {
|
||||
I18n.locale = locale.key
|
||||
const config = DatetimeField.prototype.datepickerDefaults()
|
||||
try {
|
||||
tz.changeLocale(locale.bigeasy, locale.moment)
|
||||
dates.forEach(date => {
|
||||
const formattedDate = $.datepicker.formatDate(config.dateFormat, date, config)
|
||||
const formattedTime = $.timeString(date)
|
||||
|
||||
const formatted = `${formattedDate} ${formattedTime}`
|
||||
assertFormattedParsesToDate(formatted, date)
|
||||
})
|
||||
} catch (err) {
|
||||
ok(false, err.message)
|
||||
}
|
||||
})
|
||||
|
||||
test(`hour format matches timezone locale for ${locale.key}`, () => {
|
||||
I18n.locale = locale.key
|
||||
tz.changeLocale(locale.bigeasy, locale.moment)
|
||||
const formats = [
|
||||
'date.formats.date_at_time',
|
||||
'date.formats.full',
|
||||
'date.formats.full_with_weekday',
|
||||
'time.formats.tiny',
|
||||
'time.formats.tiny_on_the_hour'
|
||||
]
|
||||
const invalid = key => {
|
||||
const format = I18n.lookup(key)
|
||||
// ok(/%p/i.test(format) === tz.hasMeridian(), `format: ${format}, hasMeridian: ${tz.hasMeridian()}`)
|
||||
ok(
|
||||
tz.hasMeridian() || !/%p/i.test(format),
|
||||
`format: ${format}, hasMeridian: ${tz.hasMeridian()}`
|
||||
)
|
||||
}
|
||||
ok(!formats.forEach(invalid))
|
||||
})
|
||||
})
|
|
@ -16,8 +16,10 @@
|
|||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import moment_formats from '@canvas/timezone/moment_formats'
|
||||
import I18nStubber from 'helpers/I18nStubber'
|
||||
import {
|
||||
prepareFormats
|
||||
} from '../../../ui/boot/initializers/configureDateTimeMomentParser'
|
||||
|
||||
QUnit.module('Moment formats', {
|
||||
setup() {
|
||||
|
@ -32,18 +34,18 @@ QUnit.module('Moment formats', {
|
|||
})
|
||||
},
|
||||
teardown() {
|
||||
I18nStubber.popFrame()
|
||||
I18nStubber.clear()
|
||||
}
|
||||
})
|
||||
|
||||
test('formatsForLocale include formats matching datepicker', () => {
|
||||
const formats = moment_formats.formatsForLocale()
|
||||
const formats = prepareFormats().map(x => x())
|
||||
ok(formats.includes('%b %-d, %Y %l:%M%P'))
|
||||
ok(formats.includes('%b %-d, %Y %l%P'))
|
||||
})
|
||||
|
||||
test('formatsForLocale includes all event formats', () => {
|
||||
const formats = moment_formats.formatsForLocale()
|
||||
const formats = prepareFormats().map(x => x())
|
||||
ok(formats.includes('%b %-d, %Y event %l:%M%P'))
|
||||
ok(formats.includes('%b %-d event %l:%M%P'))
|
||||
ok(formats.includes('%b %-d, %Y event %l%P'))
|
||||
|
|
|
@ -45,7 +45,7 @@ QUnit.module('Number Helper Parse and Validate', {
|
|||
},
|
||||
|
||||
teardown() {
|
||||
I18nStubber.popFrame()
|
||||
I18nStubber.clear()
|
||||
if (numberHelper._parseNumber.restore) {
|
||||
numberHelper._parseNumber.restore()
|
||||
}
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* Copyright (C) 2021 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import tz from '@canvas/timezone'
|
||||
import tzInTest from '@canvas/timezone/specHelpers'
|
||||
|
||||
tz.snapshot = () => {}
|
||||
tz.restore = tzInTest.restore
|
||||
tz.changeZone = tzInTest.changeZone
|
||||
tz.changeLocale = tzInTest.changeLocale
|
|
@ -1,651 +0,0 @@
|
|||
//
|
||||
// Copyright (C) 2013 - present Instructure, Inc.
|
||||
//
|
||||
// This file is part of Canvas.
|
||||
//
|
||||
// Canvas is free software: you can redistribute it and/or modify it under
|
||||
// the terms of the GNU Affero General Public License as published by the Free
|
||||
// Software Foundation, version 3 of the License.
|
||||
//
|
||||
// Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
// A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
// details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License along
|
||||
// with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import tz from '@canvas/timezone'
|
||||
import i18nObj from '@canvas/i18n'
|
||||
import detroit from 'timezone/America/Detroit'
|
||||
import french from 'timezone/fr_FR'
|
||||
import portuguese from 'timezone/pt_PT'
|
||||
import chinese from 'timezone/zh_CN'
|
||||
import I18nStubber from 'helpers/I18nStubber'
|
||||
import trans from 'translations/_core_en'
|
||||
import en_US from 'timezone/en_US'
|
||||
import MockDate from 'mockdate'
|
||||
|
||||
QUnit.module('timezone', {
|
||||
setup() {
|
||||
this.snapshot = tz.snapshot()
|
||||
I18nStubber.pushFrame()
|
||||
},
|
||||
|
||||
teardown() {
|
||||
tz.restore(this.snapshot)
|
||||
I18nStubber.popFrame()
|
||||
}
|
||||
})
|
||||
|
||||
const moonwalk = new Date(Date.UTC(1969, 6, 21, 2, 56))
|
||||
const epoch = new Date(Date.UTC(1970, 0, 1, 0, 0))
|
||||
|
||||
test('moment(one-arg) complains', () => {
|
||||
let err = null
|
||||
try {
|
||||
tz.moment('June 24 at 10:00pm')
|
||||
} catch (error) {
|
||||
err = error
|
||||
}
|
||||
ok(err.toString().match(/^Error: tz.moment only works on /))
|
||||
})
|
||||
|
||||
test('moment(non-string, fmt-string) complains', () => {
|
||||
let err = null
|
||||
try {
|
||||
tz.moment(moonwalk, 'MMMM D h:mmA')
|
||||
} catch (error) {
|
||||
err = error
|
||||
}
|
||||
ok(err.toString().match(/^Error: tz.moment only works on /))
|
||||
})
|
||||
|
||||
test('moment(date-string, non-string) complains', () => {
|
||||
let err = null
|
||||
try {
|
||||
tz.moment('June 24 at 10:00pm', 123)
|
||||
} catch (error) {
|
||||
err = error
|
||||
}
|
||||
ok(err.toString().match(/^Error: tz.moment only works on /))
|
||||
})
|
||||
|
||||
test('moment(date-string, fmt-string) works', () =>
|
||||
ok(tz.moment('June 24 at 10:00pm', 'MMMM D h:mmA')))
|
||||
|
||||
test('moment(date-string, [fmt-strings]) works', () =>
|
||||
ok(tz.moment('June 24 at 10:00pm', ['MMMM D h:mmA', 'L'])))
|
||||
|
||||
test('moment passes through invalid results', () => {
|
||||
const m = tz.moment('not a valid date', 'L')
|
||||
ok(!m.isValid())
|
||||
})
|
||||
|
||||
test('moment accepts excess input, but all format used', () => {
|
||||
const m = tz.moment('12pm and more', 'ha')
|
||||
ok(m.isValid())
|
||||
})
|
||||
|
||||
test('moment rejects excess format', () => {
|
||||
const m = tz.moment('12pm', 'h:mma')
|
||||
ok(!m.isValid())
|
||||
})
|
||||
|
||||
test('moment returns moment for valid results', () => {
|
||||
const m = tz.moment('June 24, 2015 at 10:00pm -04:00', 'MMMM D, YYYY h:mmA Z')
|
||||
ok(m.isValid())
|
||||
})
|
||||
|
||||
test('moment sans-timezone info parses according to profile timezone', () => {
|
||||
tz.changeZone(detroit, 'America/Detroit')
|
||||
const expected = new Date(1435197600000) // 10pm EDT on June 24, 2015
|
||||
const m = tz.moment('June 24, 2015 at 10:00pm', 'MMMM D, YYYY h:mmA')
|
||||
equal(+m.toDate(), +expected)
|
||||
})
|
||||
|
||||
test('moment with-timezone info parses according to that timezone', () => {
|
||||
tz.changeZone(detroit, 'America/Detroit')
|
||||
const expected = new Date(1435204800000) // 10pm MDT on June 24, 2015
|
||||
const m = tz.moment('June 24, 2015 at 10:00pm -06:00', 'MMMM D, YYYY h:mmA Z')
|
||||
equal(+m.toDate(), +expected)
|
||||
})
|
||||
|
||||
test('moment can change locales with single arity', () => {
|
||||
tz.changeLocale('en_US', 'en')
|
||||
const m1 = tz.moment('mercredi 1 juillet 2015 15:00', 'LLLL')
|
||||
ok(!m1._locale._abbr.match(/fr/))
|
||||
ok(!m1.isValid())
|
||||
|
||||
tz.changeLocale('fr_FR', 'fr')
|
||||
const m2 = tz.moment('mercredi 1 juillet 2015 15:00', 'LLLL')
|
||||
ok(m2._locale._abbr.match(/fr/))
|
||||
ok(m2.isValid())
|
||||
})
|
||||
|
||||
test('moment can change locales with multiple arity', () => {
|
||||
tz.changeLocale('en_US', 'en')
|
||||
const m1 = tz.moment('mercredi 1 juillet 2015 15:00', 'LLLL')
|
||||
ok(!m1._locale._abbr.match(/fr/))
|
||||
ok(!m1.isValid())
|
||||
|
||||
tz.changeLocale(french, 'fr_FR', 'fr')
|
||||
const m2 = tz.moment('mercredi 1 juillet 2015 15:00', 'LLLL')
|
||||
ok(m2._locale._abbr.match(/fr/))
|
||||
ok(m2.isValid())
|
||||
})
|
||||
|
||||
test('parse(valid datetime string)', () => {
|
||||
equal(+tz.parse(moonwalk.toISOString()), +moonwalk)
|
||||
})
|
||||
|
||||
test('parse(timestamp integer)', () => {
|
||||
equal(+tz.parse(+moonwalk), +moonwalk)
|
||||
})
|
||||
|
||||
test('parse(Date object)', () => {
|
||||
equal(+tz.parse(moonwalk), +moonwalk)
|
||||
})
|
||||
|
||||
test('parse(date array)', () => {
|
||||
equal(+tz.parse([1969, 7, 21, 2, 56]), +moonwalk)
|
||||
})
|
||||
|
||||
test('parse() should return null on failure', () => equal(tz.parse('bogus'), null))
|
||||
|
||||
test('parse() should return a date on success', () => equal(typeof tz.parse(+moonwalk), 'object'))
|
||||
|
||||
test('parse("") should fail', () => equal(tz.parse(''), null))
|
||||
|
||||
test('parse(null) should fail', () => equal(tz.parse(null), null))
|
||||
|
||||
test('parse(integer) should be ms since epoch', () => equal(+tz.parse(2016), +tz.raw_parse(2016)))
|
||||
|
||||
test('parse("looks like integer") should be a year', () =>
|
||||
equal(+tz.parse('2016'), +tz.parse('2016-01-01')))
|
||||
|
||||
test('parse() should parse relative to UTC by default', () =>
|
||||
equal(+tz.parse('1969-07-21 02:56'), +moonwalk))
|
||||
|
||||
test('format() should format relative to UTC by default', () =>
|
||||
equal(tz.format(moonwalk, '%F %T%:z'), '1969-07-21 02:56:00+00:00'))
|
||||
|
||||
test('format() should format in en_US by default', () =>
|
||||
equal(tz.format(moonwalk, '%c'), 'Mon 21 Jul 1969 02:56:00 AM UTC'))
|
||||
|
||||
test('format() should parse the value if necessary', () =>
|
||||
equal(tz.format('1969-07-21 02:56', '%F %T%:z'), '1969-07-21 02:56:00+00:00'))
|
||||
|
||||
test('format() should return null if the parse fails', () =>
|
||||
equal(tz.format('bogus', '%F %T%:z'), null))
|
||||
|
||||
test('format() should return null if the format string is unrecognized', () =>
|
||||
equal(tz.format(moonwalk, 'bogus'), null))
|
||||
|
||||
test('format() should preserve 12-hour+am/pm if the locale does define am/pm', () => {
|
||||
const time = tz.parse('1969-07-21 15:00:00')
|
||||
equal(tz.format(time, '%-l%P'), '3pm')
|
||||
equal(tz.format(time, '%I%P'), '03pm')
|
||||
equal(tz.format(time, '%r'), '03:00:00 PM')
|
||||
})
|
||||
|
||||
test("format() should promote 12-hour+am/pm into 24-hour if the locale doesn't define am/pm", () => {
|
||||
const time = tz.parse('1969-07-21 15:00:00')
|
||||
tz.changeLocale(french, 'fr_FR', 'fr')
|
||||
equal(tz.format(time, '%-l%P'), '15')
|
||||
equal(tz.format(time, '%I%P'), '15')
|
||||
equal(tz.format(time, '%r'), '15:00:00')
|
||||
})
|
||||
|
||||
test('format() should recognize date.formats.*', () => {
|
||||
I18nStubber.stub('en', {'date.formats.short': '%b %-d'})
|
||||
equal(tz.format(moonwalk, 'date.formats.short'), 'Jul 21')
|
||||
})
|
||||
|
||||
test('format() should recognize time.formats.*', () => {
|
||||
I18nStubber.stub('en', {'time.formats.tiny': '%-l:%M%P'})
|
||||
equal(tz.format(epoch, 'time.formats.tiny'), '12:00am')
|
||||
})
|
||||
|
||||
test('format() should localize when given a localization key', () => {
|
||||
tz.changeLocale(french, 'fr_FR', 'fr')
|
||||
I18nStubber.setLocale('fr_FR')
|
||||
I18nStubber.stub('fr_FR', {'date.formats.full': '%-d %b %Y %-l:%M%P'})
|
||||
equal(tz.format(moonwalk, 'date.formats.full'), '21 juil. 1969 2:56')
|
||||
})
|
||||
|
||||
test('format() should automatically convert %l to %-l when given a localization key', () => {
|
||||
I18nStubber.stub('en', {'time.formats.tiny': '%l:%M%P'})
|
||||
equal(tz.format(moonwalk, 'time.formats.tiny'), '2:56am')
|
||||
})
|
||||
|
||||
test('format() should automatically convert %k to %-k when given a localization key', () => {
|
||||
I18nStubber.stub('en', {'time.formats.tiny': '%k:%M'})
|
||||
equal(tz.format(moonwalk, 'time.formats.tiny'), '2:56')
|
||||
})
|
||||
|
||||
test('format() should automatically convert %e to %-e when given a localization key', () => {
|
||||
I18nStubber.stub('en', {'date.formats.short': '%b %e'})
|
||||
equal(tz.format(epoch, 'date.formats.short'), 'Jan 1')
|
||||
})
|
||||
|
||||
test('shift() should adjust the date as appropriate', () =>
|
||||
equal(+tz.shift(moonwalk, '-1 day'), moonwalk - 86400000))
|
||||
|
||||
test('shift() should apply multiple directives', () =>
|
||||
equal(+tz.shift(moonwalk, '-1 day', '-1 hour'), moonwalk - 86400000 - 3600000))
|
||||
|
||||
test('shift() should parse the value if necessary', () =>
|
||||
equal(+tz.shift('1969-07-21 02:56', '-1 day'), moonwalk - 86400000))
|
||||
|
||||
test('shift() should return null if the parse fails', () =>
|
||||
equal(tz.shift('bogus', '-1 day'), null))
|
||||
|
||||
test('shift() should return null if the directives includes a format string', () =>
|
||||
equal(tz.shift('bogus', '-1 day', '%F %T%:z'), null))
|
||||
|
||||
test('extendConfiguration() should curry the options into tz', () => {
|
||||
tz.extendConfiguration(detroit, 'America/Detroit')
|
||||
equal(+tz.parse('1969-07-20 21:56'), +moonwalk)
|
||||
equal(tz.format(moonwalk, '%c'), 'Sun 20 Jul 1969 09:56:00 PM EST')
|
||||
})
|
||||
|
||||
test('snapshotting should let you restore tz to a previous un-curried state', () => {
|
||||
const snapshot = tz.snapshot()
|
||||
tz.extendConfiguration(detroit, 'America/Detroit')
|
||||
tz.restore(snapshot)
|
||||
equal(+tz.parse('1969-07-21 02:56'), +moonwalk)
|
||||
equal(tz.format(moonwalk, '%c'), 'Mon 21 Jul 1969 02:56:00 AM UTC')
|
||||
})
|
||||
|
||||
test('changeZone(...) should synchronously curry in a loaded zone', () => {
|
||||
tz.changeZone(detroit, 'America/Detroit')
|
||||
equal(+tz.parse('1969-07-20 21:56'), +moonwalk)
|
||||
equal(tz.format(moonwalk, '%c'), 'Sun 20 Jul 1969 09:56:00 PM EST')
|
||||
})
|
||||
|
||||
test('changeZone(...) should asynchronously curry in a zone by name', assert => {
|
||||
const done = assert.async()
|
||||
assert.expect(2)
|
||||
tz.changeZone('America/Detroit').then(() => {
|
||||
equal(+tz.parse('1969-07-20 21:56'), +moonwalk)
|
||||
equal(tz.format(moonwalk, '%c'), 'Sun 20 Jul 1969 09:56:00 PM EST')
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
test('changeLocale(...) should synchronously curry in a loaded locale', () => {
|
||||
tz.changeLocale(french, 'fr_FR', 'fr')
|
||||
equal(tz.format(moonwalk, '%c'), 'lun. 21 juil. 1969 02:56:00 UTC')
|
||||
})
|
||||
|
||||
test('changeLocale(...) should asynchronously curry in a locale by name', assert => {
|
||||
const done = assert.async()
|
||||
assert.expect(1)
|
||||
tz.changeLocale('fr_FR', 'fr').then(() => {
|
||||
equal(tz.format(moonwalk, '%c'), 'lun. 21 juil. 1969 02:56:00 UTC')
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
test('changeZone(...) should synchronously curry if pre-loaded', () => {
|
||||
tz.preload('America/Detroit', detroit)
|
||||
tz.changeZone('America/Detroit')
|
||||
equal(tz.format(moonwalk, '%c'), 'Sun 20 Jul 1969 09:56:00 PM EST')
|
||||
})
|
||||
|
||||
test('hasMeridian() true if locale defines am/pm', () => ok(tz.hasMeridian()))
|
||||
|
||||
test("hasMeridian() false if locale doesn't define am/pm", () => {
|
||||
tz.changeLocale(french, 'fr_FR', 'fr')
|
||||
ok(!tz.hasMeridian())
|
||||
})
|
||||
|
||||
test('useMeridian() true if locale defines am/pm and uses 12-hour format', () => {
|
||||
I18nStubber.stub('en', {'time.formats.tiny': '%l:%M%P'})
|
||||
ok(tz.hasMeridian())
|
||||
ok(tz.useMeridian())
|
||||
})
|
||||
|
||||
test('useMeridian() false if locale defines am/pm but uses 24-hour format', () => {
|
||||
I18nStubber.stub('en', {'time.formats.tiny': '%k:%M'})
|
||||
ok(tz.hasMeridian())
|
||||
ok(!tz.useMeridian())
|
||||
})
|
||||
|
||||
test("useMeridian() false if locale doesn't define am/pm and instead uses 24-hour format", () => {
|
||||
tz.changeLocale(french, 'fr_FR', 'fr')
|
||||
I18nStubber.setLocale('fr_FR')
|
||||
I18nStubber.stub('fr_FR', {'time.formats.tiny': '%-k:%M'})
|
||||
ok(!tz.hasMeridian())
|
||||
ok(!tz.useMeridian())
|
||||
})
|
||||
|
||||
test("useMeridian() false if locale doesn't define am/pm but still uses 12-hour format (format will be corrected)", () => {
|
||||
tz.changeLocale(french, 'fr_FR', 'fr')
|
||||
I18nStubber.setLocale('fr_FR')
|
||||
I18nStubber.stub('fr_FR', {'time.formats.tiny': '%-l:%M%P'})
|
||||
ok(!tz.hasMeridian())
|
||||
ok(!tz.useMeridian())
|
||||
})
|
||||
|
||||
test('isMidnight() is false when no argument given.', () => ok(!tz.isMidnight()))
|
||||
|
||||
test('isMidnight() is false when invalid date is given.', () => {
|
||||
const date = new Date('invalid date')
|
||||
ok(!tz.isMidnight(date))
|
||||
})
|
||||
|
||||
test('isMidnight() is true when date given is at midnight.', () => ok(tz.isMidnight(epoch)))
|
||||
|
||||
test("isMidnight() is false when date given isn't at midnight.", () => ok(!tz.isMidnight(moonwalk)))
|
||||
|
||||
test('isMidnight() is false when date is midnight in a different zone.', () => {
|
||||
tz.changeZone(detroit, 'America/Detroit')
|
||||
ok(!tz.isMidnight(epoch))
|
||||
})
|
||||
|
||||
test('changeToTheSecondBeforeMidnight() returns null when no argument given.', () =>
|
||||
equal(tz.changeToTheSecondBeforeMidnight(), null))
|
||||
|
||||
test('changeToTheSecondBeforeMidnight() returns null when invalid date is given.', () => {
|
||||
const date = new Date('invalid date')
|
||||
equal(tz.changeToTheSecondBeforeMidnight(date), null)
|
||||
})
|
||||
|
||||
test('changeToTheSecondBeforeMidnight() returns fancy midnight when a valid date is given.', () => {
|
||||
const fancyMidnight = tz.changeToTheSecondBeforeMidnight(epoch)
|
||||
equal(fancyMidnight.toGMTString(), 'Thu, 01 Jan 1970 23:59:59 GMT')
|
||||
})
|
||||
|
||||
test('mergeTimeAndDate() finds the given time of day on the given date.', () =>
|
||||
equal(+tz.mergeTimeAndDate(moonwalk, epoch), +new Date(Date.UTC(1970, 0, 1, 2, 56))))
|
||||
|
||||
QUnit.module('english tz', {
|
||||
setup() {
|
||||
MockDate.set('2015-02-01', 'UTC')
|
||||
this.snapshot = tz.snapshot()
|
||||
I18nStubber.pushFrame()
|
||||
I18nStubber.setLocale('en_US')
|
||||
I18nStubber.stub('en_US', {
|
||||
'date.formats.date_at_time': '%b %-d at %l:%M%P',
|
||||
'date.formats.default': '%Y-%m-%d',
|
||||
'date.formats.full': '%b %-d, %Y %-l:%M%P',
|
||||
'date.formats.full_with_weekday': '%a %b %-d, %Y %-l:%M%P',
|
||||
'date.formats.long': '%B %-d, %Y',
|
||||
'date.formats.long_with_weekday': '%A, %B %-d',
|
||||
'date.formats.medium': '%b %-d, %Y',
|
||||
'date.formats.medium_month': '%b %Y',
|
||||
'date.formats.medium_with_weekday': '%a %b %-d, %Y',
|
||||
'date.formats.short': '%b %-d',
|
||||
'date.formats.short_month': '%b',
|
||||
'date.formats.short_weekday': '%a',
|
||||
'date.formats.short_with_weekday': '%a, %b %-d',
|
||||
'date.formats.weekday': '%A',
|
||||
'time.formats.default': '%a, %d %b %Y %H:%M:%S %z',
|
||||
'time.formats.long': '%B %d, %Y %H:%M',
|
||||
'time.formats.short': '%d %b %H:%M',
|
||||
'time.formats.tiny': '%l:%M%P',
|
||||
'time.formats.tiny_on_the_hour': '%l%P'
|
||||
})
|
||||
tz.changeLocale('en_US', 'en')
|
||||
},
|
||||
|
||||
teardown() {
|
||||
tz.restore(this.snapshot)
|
||||
I18nStubber.popFrame()
|
||||
MockDate.reset()
|
||||
}
|
||||
})
|
||||
|
||||
test('parses english dates', () => {
|
||||
const engDates = [
|
||||
'08/03/2015',
|
||||
'8/3/2015',
|
||||
'August 3, 2015',
|
||||
'Aug 3, 2015',
|
||||
'3 Aug 2015',
|
||||
'2015-08-03',
|
||||
'2015 08 03',
|
||||
'August 3, 2015',
|
||||
'Monday, August 3',
|
||||
'Mon Aug 3, 2015',
|
||||
'Mon, Aug 3',
|
||||
'Aug 3'
|
||||
]
|
||||
|
||||
engDates.forEach(date => {
|
||||
const d = tz.parse(date)
|
||||
equal(tz.format(d, '%d'), '03', `this works: ${date}`)
|
||||
})
|
||||
})
|
||||
|
||||
test('parses english times', () => {
|
||||
const engTimes = ['6:06 PM', '6:06:22 PM', '6:06pm', '6pm']
|
||||
|
||||
engTimes.forEach(time => {
|
||||
const d = tz.parse(time)
|
||||
equal(tz.format(d, '%H'), '18', `this works: ${time}`)
|
||||
})
|
||||
})
|
||||
|
||||
test('parses english date times', () => {
|
||||
const engDateTimes = [
|
||||
'2015-08-03 18:06:22',
|
||||
'August 3, 2015 6:06 PM',
|
||||
'Aug 3, 2015 6:06 PM',
|
||||
'Aug 3, 2015 6pm',
|
||||
'Monday, August 3, 2015 6:06 PM',
|
||||
'Mon, Aug 3, 2015 6:06 PM',
|
||||
'Aug 3 at 6:06pm',
|
||||
'Aug 3, 2015 6:06pm',
|
||||
'Mon Aug 3, 2015 6:06pm'
|
||||
]
|
||||
|
||||
engDateTimes.forEach(dateTime => {
|
||||
const d = tz.parse(dateTime)
|
||||
equal(tz.format(d, '%d %H'), '03 18', `this works: ${dateTime}`)
|
||||
})
|
||||
})
|
||||
|
||||
test('parses 24hr times even if the locale lacks them', () => {
|
||||
const d = tz.parse('18:06')
|
||||
equal(tz.format(d, '%H:%M'), '18:06')
|
||||
})
|
||||
|
||||
QUnit.module('french tz', {
|
||||
setup() {
|
||||
MockDate.set('2015-02-01', 'UTC')
|
||||
this.snapshot = tz.snapshot()
|
||||
I18nStubber.pushFrame()
|
||||
I18nStubber.setLocale('fr_FR')
|
||||
I18nStubber.stub('fr_FR', {
|
||||
'date.formats.date_at_time': '%-d %b à %k:%M',
|
||||
'date.formats.default': '%d/%m/%Y',
|
||||
'date.formats.full': '%b %-d, %Y %-k:%M',
|
||||
'date.formats.full_with_weekday': '%a %-d %b, %Y %-k:%M',
|
||||
'date.formats.long': 'le %-d %B %Y',
|
||||
'date.formats.long_with_weekday': '%A, %-d %B',
|
||||
'date.formats.medium': '%-d %b %Y',
|
||||
'date.formats.medium_month': '%b %Y',
|
||||
'date.formats.medium_with_weekday': '%a %-d %b %Y',
|
||||
'date.formats.short': '%-d %b',
|
||||
'date.formats.short_month': '%b',
|
||||
'date.formats.short_weekday': '%a',
|
||||
'date.formats.short_with_weekday': '%a, %-d %b',
|
||||
'date.formats.weekday': '%A',
|
||||
'time.formats.default': '%a, %d %b %Y %H:%M:%S %z',
|
||||
'time.formats.long': ' %d %B, %Y %H:%M',
|
||||
'time.formats.short': '%d %b %H:%M',
|
||||
'time.formats.tiny': '%k:%M',
|
||||
'time.formats.tiny_on_the_hour': '%k:%M'
|
||||
})
|
||||
tz.changeLocale(french, 'fr_FR', 'fr')
|
||||
},
|
||||
|
||||
teardown() {
|
||||
tz.restore(this.snapshot)
|
||||
I18nStubber.popFrame()
|
||||
MockDate.reset()
|
||||
}
|
||||
})
|
||||
|
||||
test('parses french dates', () => {
|
||||
const frenchDates = [
|
||||
'03/08/2015',
|
||||
'3/8/2015',
|
||||
'3 août 2015',
|
||||
'2015-08-03',
|
||||
'le 3 août 2015',
|
||||
'lundi, 3 août',
|
||||
'lun. 3 août 2015',
|
||||
'3 août',
|
||||
'lun., 3 août',
|
||||
'3 août 2015',
|
||||
'3 août'
|
||||
]
|
||||
|
||||
frenchDates.forEach(date => {
|
||||
const d = tz.parse(date)
|
||||
equal(tz.format(d, '%d'), '03', `this works: ${date}`)
|
||||
})
|
||||
})
|
||||
|
||||
test('parses french times', () => {
|
||||
const frenchTimes = ['18:06', '18:06:22']
|
||||
|
||||
frenchTimes.forEach(time => {
|
||||
const d = tz.parse(time)
|
||||
equal(tz.format(d, '%H'), '18', `this works: ${time}`)
|
||||
})
|
||||
})
|
||||
|
||||
test('parses french date times', () => {
|
||||
const frenchDateTimes = [
|
||||
'2015-08-03 18:06:22',
|
||||
'3 août 2015 18:06',
|
||||
'lundi 3 août 2015 18:06',
|
||||
'lun. 3 août 2015 18:06',
|
||||
'3 août à 18:06',
|
||||
'août 3, 2015 18:06',
|
||||
'lun. 3 août, 2015 18:06'
|
||||
]
|
||||
|
||||
frenchDateTimes.forEach(dateTime => {
|
||||
const d = tz.parse(dateTime)
|
||||
equal(tz.format(d, '%d %H'), '03 18', `this works: ${dateTime}`)
|
||||
})
|
||||
})
|
||||
|
||||
QUnit.module('chinese tz', {
|
||||
setup() {
|
||||
MockDate.set('2015-02-01', 'UTC')
|
||||
this.snapshot = tz.snapshot()
|
||||
I18nStubber.pushFrame()
|
||||
I18nStubber.setLocale('zh_CN')
|
||||
I18nStubber.stub('zh_CN', {
|
||||
'date.formats.date_at_time': '%b %-d 于 %H:%M',
|
||||
'date.formats.default': '%Y-%m-%d',
|
||||
'date.formats.full': '%b %-d, %Y %-l:%M%P',
|
||||
'date.formats.full_with_weekday': '%a %b %-d, %Y %-l:%M%P',
|
||||
'date.formats.long': '%Y %B %-d',
|
||||
'date.formats.long_with_weekday': '%A, %B %-d',
|
||||
'date.formats.medium': '%Y %b %-d',
|
||||
'date.formats.medium_month': '%Y %b',
|
||||
'date.formats.medium_with_weekday': '%a %Y %b %-d',
|
||||
'date.formats.short': '%b %-d',
|
||||
'date.formats.short_month': '%b',
|
||||
'date.formats.short_weekday': '%a',
|
||||
'date.formats.short_with_weekday': '%a, %b %-d',
|
||||
'date.formats.weekday': '%A',
|
||||
'time.formats.default': '%a, %Y %b %d %H:%M:%S %z',
|
||||
'time.formats.long': '%Y %B %d %H:%M',
|
||||
'time.formats.short': '%b %d %H:%M',
|
||||
'time.formats.tiny': '%H:%M',
|
||||
'time.formats.tiny_on_the_hour': '%k:%M'
|
||||
})
|
||||
tz.changeLocale(chinese, 'zh_CN', 'zh-cn')
|
||||
},
|
||||
|
||||
teardown() {
|
||||
tz.restore(this.snapshot)
|
||||
I18nStubber.popFrame()
|
||||
MockDate.reset()
|
||||
}
|
||||
})
|
||||
|
||||
test('parses chinese dates', () => {
|
||||
const chineseDates = [
|
||||
'2015-08-03',
|
||||
'2015年8月3日',
|
||||
'2015 八月 3',
|
||||
'2015 8月 3',
|
||||
'星期一, 八月 3',
|
||||
'一 2015 8月 3',
|
||||
'一, 8月 3',
|
||||
'8月 3'
|
||||
]
|
||||
|
||||
chineseDates.forEach(date => {
|
||||
const d = tz.parse(date)
|
||||
equal(tz.format(d, '%d'), '03', `this works: ${date}`)
|
||||
})
|
||||
})
|
||||
|
||||
test('parses chinese PM times', () => {
|
||||
// 晚上 means evening. moment doesn't seem to be handling that correctly,
|
||||
// though I don't believe this will cause any problems in canvas.
|
||||
const chineseTimes = [
|
||||
// '晚上6点06分',
|
||||
// '晚上6点6分22秒',
|
||||
'18:06'
|
||||
]
|
||||
chineseTimes.forEach(time => {
|
||||
const d = tz.parse(time)
|
||||
equal(tz.format(d, '%H'), '18', `this works: ${time}`)
|
||||
})
|
||||
})
|
||||
|
||||
// the 2 chinese AM specs pass in isolation, but when run as part of the whole js test suite
|
||||
// with COVERAGE=1, which serializes the tests, it fails. I suspect it's due to
|
||||
// pollution from another test, but cannot find it.
|
||||
// Skipping for now so the master build completes w/o error.
|
||||
QUnit.skip('parses chinese AM times', () => {
|
||||
const chineseTimes = ['6点06分', '6点6分22秒', '06:06']
|
||||
chineseTimes.forEach(time => {
|
||||
const d = tz.parse(time)
|
||||
equal(tz.format(d, '%H'), '06', `this works: ${time}`)
|
||||
})
|
||||
})
|
||||
|
||||
QUnit.skip('parses chinese date AM times', () => {
|
||||
const chineseDateTimes = [
|
||||
'2015-08-03 06:06:22',
|
||||
'2015年8月3日6点06分',
|
||||
'2015年8月3日星期一6点06分',
|
||||
'8月 3日 于 6:06',
|
||||
'20158月3日, 6:06',
|
||||
'一 20158月3日, 6:06' // this is incorrectly parsing as "Fri, 20 Mar 1908 06:06:00 GMT"
|
||||
]
|
||||
|
||||
chineseDateTimes.forEach(dateTime => {
|
||||
const d = tz.parse(dateTime)
|
||||
equal(tz.format(d, '%d %H'), '03 06', `this works: ${dateTime}`)
|
||||
})
|
||||
})
|
||||
|
||||
test('parses chinese date PM times', () => {
|
||||
const chineseDateTimes = [
|
||||
'2015-08-03 18:06:22',
|
||||
'2015年8月3日晚上6点06分',
|
||||
// '2015年8月3日星期一晚上6点06分', // parsing as "Mon, 03 Aug 2015 06:06:00 GMT"
|
||||
'8月 3日, 于 18:06'
|
||||
// '2015 8月 3日, 6:06下午', // doesn't recognize 下午 as implying PM
|
||||
// '一 2015 8月 3日, 6:06下午'
|
||||
]
|
||||
|
||||
chineseDateTimes.forEach(dateTime => {
|
||||
const d = tz.parse(dateTime)
|
||||
equal(tz.format(d, '%d %H'), '03 18', `this works: ${dateTime}`)
|
||||
})
|
||||
})
|
|
@ -22,8 +22,14 @@ import en_US from 'timezone/en_US'
|
|||
import './jsx/spec-support/specProtection'
|
||||
import setupRavenConsoleLoggingPlugin from '../../ui/boot/initializers/setupRavenConsoleLoggingPlugin.js'
|
||||
import {filterUselessConsoleMessages} from '@instructure/js-utils'
|
||||
import './jsx/spec-support/timezoneBackwardsCompatLayer'
|
||||
import {
|
||||
up as configureDateTimeMomentParser
|
||||
} from '../../ui/boot/initializers/configureDateTimeMomentParser'
|
||||
|
||||
|
||||
filterUselessConsoleMessages(console)
|
||||
configureDateTimeMomentParser()
|
||||
|
||||
Enzyme.configure({adapter: new Adapter()})
|
||||
|
||||
|
|
|
@ -21,18 +21,19 @@ import canvasHighContrastTheme from '@instructure/canvas-high-contrast-theme'
|
|||
import moment from 'moment'
|
||||
import tz from '@canvas/timezone'
|
||||
import './initializers/fakeRequireJSFallback.js'
|
||||
import {
|
||||
up as configureDateTimeMomentParser
|
||||
} from './initializers/configureDateTimeMomentParser'
|
||||
import {
|
||||
up as configureTimezone
|
||||
} from './initializers/configureTimezone'
|
||||
|
||||
// we already put a <script> tag for the locale corresponding ENV.MOMENT_LOCALE
|
||||
// on the page from rails, so this should not cause a new network request.
|
||||
moment().locale(ENV.MOMENT_LOCALE)
|
||||
|
||||
// These timezones and locales should already be put on the page as <script>
|
||||
// tags from rails. this block should not create any network requests.
|
||||
if (typeof ENV !== 'undefined') {
|
||||
if (ENV.TIMEZONE) tz.changeZone(ENV.TIMEZONE)
|
||||
if (ENV.CONTEXT_TIMEZONE) tz.preload(ENV.CONTEXT_TIMEZONE)
|
||||
if (ENV.BIGEASY_LOCALE) tz.changeLocale(ENV.BIGEASY_LOCALE, ENV.MOMENT_LOCALE)
|
||||
}
|
||||
configureDateTimeMomentParser()
|
||||
configureTimezone()
|
||||
|
||||
// This will inject and set up sentry for deprecation reporting. It should be
|
||||
// stripped out and be a no-op in production.
|
||||
|
|
|
@ -0,0 +1,79 @@
|
|||
/*
|
||||
* Copyright (C) 2021 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import I18n from 'i18n!instructure'
|
||||
import { useI18nFormats } from 'date-time-moment-parser'
|
||||
|
||||
const dateFormat = key => () => (
|
||||
I18n.lookup(`date.formats.${key}`)
|
||||
)
|
||||
|
||||
const eventFormat = ({ date, time }) => () => (
|
||||
I18n.t('#time.event', '%{date} at %{time}', {
|
||||
date: I18n.lookup(`date.formats.${date}`),
|
||||
time: I18n.lookup(`time.formats.${time}`)
|
||||
})
|
||||
)
|
||||
|
||||
const timeFormat = key => () => (
|
||||
I18n.lookup(`time.formats.${key}`)
|
||||
)
|
||||
|
||||
const joinFormats = (separator, formats) => () => (
|
||||
formats.map(key => I18n.lookup(key)).join(separator)
|
||||
)
|
||||
|
||||
export function prepareFormats() {
|
||||
// examples are from en_US. order is significant since if an input matches
|
||||
// multiple formats, the format earlier in the list will be preferred
|
||||
return [
|
||||
timeFormat('default'), // %a, %d %b %Y %H:%M:%S %z
|
||||
dateFormat('full_with_weekday'), // %a %b %-d, %Y %-l:%M%P
|
||||
dateFormat('full'), // %b %-d, %Y %-l:%M%P
|
||||
dateFormat('date_at_time'), // %b %-d at %l:%M%P
|
||||
dateFormat('long_with_weekday'), // %A, %B %-d
|
||||
dateFormat('medium_with_weekday'), // %a %b %-d, %Y
|
||||
dateFormat('short_with_weekday'), // %a, %b %-d
|
||||
timeFormat('long'), // %B %d, %Y %H:%M
|
||||
dateFormat('long'), // %B %-d, %Y
|
||||
eventFormat({ date: 'medium', time: 'tiny' }),
|
||||
eventFormat({ date: 'medium', time: 'tiny_on_the_hour' }),
|
||||
eventFormat({ date: 'short', time: 'tiny' }),
|
||||
eventFormat({ date: 'short', time: 'tiny_on_the_hour' }),
|
||||
joinFormats(' ', ['date.formats.medium', 'time.formats.tiny']),
|
||||
joinFormats(' ', ['date.formats.medium', 'time.formats.tiny_on_the_hour']),
|
||||
dateFormat('medium'), // %b %-d, %Y
|
||||
timeFormat('short'), // %d %b %H:%M
|
||||
joinFormats(' ', ['date.formats.short', 'time.formats.tiny']),
|
||||
joinFormats(' ', ['date.formats.short', 'time.formats.tiny_on_the_hour']),
|
||||
dateFormat('short'), // %b %-d
|
||||
dateFormat('default'), // %Y-%m-%d
|
||||
timeFormat('tiny'), // %l:%M%P
|
||||
timeFormat('tiny_on_the_hour'), // %l%P
|
||||
dateFormat('weekday'), // %A
|
||||
dateFormat('short_weekday') // %a
|
||||
]
|
||||
}
|
||||
|
||||
export function up() {
|
||||
useI18nFormats(prepareFormats())
|
||||
}
|
||||
|
||||
export function down() {
|
||||
useI18nFormats([])
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* Copyright (C) 2021 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { configure } from '@canvas/timezone'
|
||||
import timezone from 'timezone'
|
||||
import en_US from 'timezone/en_US'
|
||||
|
||||
export function up() {
|
||||
const tzData = window.__PRELOADED_TIMEZONE_DATA__ || {}
|
||||
|
||||
let userTZ = timezone(en_US, 'en_US', 'UTC')
|
||||
|
||||
// These timezones and locales should already be put on the page as <script>
|
||||
// tags from rails. this block should not create any network requests.
|
||||
if (window.ENV && ENV.TIMEZONE && tzData[ENV.TIMEZONE]) {
|
||||
userTZ = userTZ(tzData[ENV.TIMEZONE], ENV.TIMEZONE)
|
||||
}
|
||||
|
||||
if (window.ENV && ENV.BIGEASY_LOCALE && tzData[ENV.BIGEASY_LOCALE]) {
|
||||
userTZ = userTZ(tzData[ENV.BIGEASY_LOCALE], ENV.BIGEASY_LOCALE)
|
||||
}
|
||||
|
||||
configure({
|
||||
tz: userTZ,
|
||||
tzData,
|
||||
momentLocale: window.ENV && ENV.MOMENT_LOCALE || 'en',
|
||||
})
|
||||
}
|
||||
|
||||
export function down() {
|
||||
configure({})
|
||||
}
|
|
@ -18,7 +18,9 @@
|
|||
|
||||
import React from 'react'
|
||||
import {render, fireEvent, act} from '@testing-library/react'
|
||||
import timezone from '@canvas/timezone'
|
||||
import tz from 'timezone'
|
||||
import timezone, { configure as configureTimezone } from '@canvas/timezone'
|
||||
import tzInTest from '@canvas/timezone/specHelpers'
|
||||
import tokyo from 'timezone/Asia/Tokyo'
|
||||
import anchorage from 'timezone/America/Anchorage'
|
||||
import moment from 'moment-timezone'
|
||||
|
@ -129,21 +131,24 @@ beforeEach(() => {
|
|||
|
||||
describe('Assignment Bulk Edit Dates', () => {
|
||||
let oldEnv
|
||||
let timezoneSnapshot
|
||||
beforeEach(() => {
|
||||
oldEnv = window.ENV
|
||||
window.ENV = {
|
||||
TIMEZONE: 'Asia/Tokyo',
|
||||
FEATURES: {}
|
||||
}
|
||||
timezoneSnapshot = timezone.snapshot()
|
||||
timezone.changeZone(tokyo, 'Asia/Tokyo')
|
||||
tzInTest.configureAndRestoreLater({
|
||||
tz: tz(tokyo, 'Asia/Tokyo'),
|
||||
tzData: {
|
||||
'Asia/Tokyo': tokyo
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await flushPromises()
|
||||
window.ENV = oldEnv
|
||||
timezone.restore(timezoneSnapshot)
|
||||
tzInTest.restore()
|
||||
})
|
||||
|
||||
it('shows a spinner while loading', async () => {
|
||||
|
@ -1020,21 +1025,25 @@ describe('Assignment Bulk Edit Dates', () => {
|
|||
|
||||
describe('in a timezone that does DST', () => {
|
||||
let oldEnv
|
||||
let timezoneSnapshot
|
||||
beforeEach(() => {
|
||||
tzInTest.configureAndRestoreLater({
|
||||
tz: tz(anchorage, 'America/Anchorage'),
|
||||
tzData: {
|
||||
'America/Anchorage': anchorage
|
||||
}
|
||||
})
|
||||
|
||||
oldEnv = window.ENV
|
||||
window.ENV = {
|
||||
TIMEZONE: 'America/Anchorage',
|
||||
FEATURES: {}
|
||||
}
|
||||
timezoneSnapshot = timezone.snapshot()
|
||||
timezone.changeZone(anchorage, 'America/Anchorage')
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await flushPromises()
|
||||
window.ENV = oldEnv
|
||||
timezone.restore(timezoneSnapshot)
|
||||
tzInTest.restore()
|
||||
})
|
||||
|
||||
it('preserves the time when shifting to a DST transition day', async () => {
|
||||
|
|
|
@ -16,7 +16,8 @@
|
|||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import timezone from '@canvas/timezone'
|
||||
import tz from 'timezone'
|
||||
import timezone, { configure as configureTimezone } from '@canvas/timezone'
|
||||
import newYork from 'timezone/America/New_York'
|
||||
|
||||
import {auditEventStudentAnonymityStates, overallAnonymityStates} from '../AuditTrailHelpers'
|
||||
|
@ -42,8 +43,12 @@ describe('AssessmentAuditTray buildAuditTrail()', () => {
|
|||
const quiz = {id: '123', name: 'Unicorns', role: 'grader'}
|
||||
|
||||
beforeEach(() => {
|
||||
timezoneSnapshot = timezone.snapshot()
|
||||
timezone.changeZone(newYork, 'America/New_York')
|
||||
timezoneSnapshot = configureTimezone({
|
||||
tz: tz(newYork, 'America/New_York'),
|
||||
tzData: {
|
||||
'America/New_York': newYork
|
||||
}
|
||||
})
|
||||
|
||||
auditEvents = []
|
||||
users = [firstUser, secondUser]
|
||||
|
@ -53,7 +58,7 @@ describe('AssessmentAuditTray buildAuditTrail()', () => {
|
|||
})
|
||||
|
||||
afterEach(() => {
|
||||
timezone.restore(timezoneSnapshot)
|
||||
configureTimezone(timezoneSnapshot)
|
||||
})
|
||||
|
||||
function buildCreateEvent(payloadValues) {
|
||||
|
|
|
@ -20,6 +20,18 @@ import mergeI18nTranslations from '../mergeI18nTranslations'
|
|||
import i18nObj from '../i18nObj'
|
||||
|
||||
describe('mergeI18nTranslations', () => {
|
||||
let originalTranslations
|
||||
|
||||
beforeEach(() => {
|
||||
originalTranslations = i18nObj.translations
|
||||
i18nObj.translations = {en: {}}
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
i18nObj.translations = originalTranslations
|
||||
originalTranslations = null
|
||||
})
|
||||
|
||||
it('merges onto i18n.translations', () => {
|
||||
const newStrings = {
|
||||
ar: {someKey: 'arabic value'},
|
||||
|
|
|
@ -126,7 +126,7 @@ describe('Shared > Network > RequestDispatch', () => {
|
|||
})
|
||||
|
||||
it('resolves when flooded with requests', async () => {
|
||||
const requests = rangeOfLength(4).map(getJSON)
|
||||
const requests = rangeOfLength(4).map(() => getJSON())
|
||||
for await (const [index] of requests.entries()) {
|
||||
// Get the next request
|
||||
const request = network.getRequests()[index]
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
* Copyright (C) 2021 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import defaultLocales from 'timezone/locales'
|
||||
import ar_SA from '../../../ext/custom_timezone_locales/ar_SA'
|
||||
import ca_ES from '../../../ext/custom_timezone_locales/ca_ES'
|
||||
import cy_GB from '../../../ext/custom_timezone_locales/cy_GB'
|
||||
import da_DK from '../../../ext/custom_timezone_locales/da_DK'
|
||||
import de_DE from '../../../ext/custom_timezone_locales/de_DE'
|
||||
import el_GR from '../../../ext/custom_timezone_locales/el_GR'
|
||||
import fa_IR from '../../../ext/custom_timezone_locales/fa_IR'
|
||||
import fr_CA from '../../../ext/custom_timezone_locales/fr_CA'
|
||||
import fr_FR from '../../../ext/custom_timezone_locales/fr_FR'
|
||||
import he_IL from '../../../ext/custom_timezone_locales/he_IL'
|
||||
import ht_HT from '../../../ext/custom_timezone_locales/ht_HT'
|
||||
import hy_AM from '../../../ext/custom_timezone_locales/hy_AM'
|
||||
import is_IS from '../../../ext/custom_timezone_locales/is_IS'
|
||||
import mi_NZ from '../../../ext/custom_timezone_locales/mi_NZ'
|
||||
import nn_NO from '../../../ext/custom_timezone_locales/nn_NO'
|
||||
import pl_PL from '../../../ext/custom_timezone_locales/pl_PL'
|
||||
import tr_TR from '../../../ext/custom_timezone_locales/tr_TR'
|
||||
import uk_UA from '../../../ext/custom_timezone_locales/uk_UA'
|
||||
|
||||
export default [
|
||||
...defaultLocales,
|
||||
ar_SA,
|
||||
ca_ES,
|
||||
cy_GB,
|
||||
da_DK,
|
||||
de_DE,
|
||||
el_GR,
|
||||
fa_IR,
|
||||
fr_CA,
|
||||
fr_FR,
|
||||
he_IL,
|
||||
ht_HT,
|
||||
hy_AM,
|
||||
is_IS,
|
||||
mi_NZ,
|
||||
nn_NO,
|
||||
pl_PL,
|
||||
tr_TR,
|
||||
uk_UA,
|
||||
]
|
|
@ -0,0 +1,111 @@
|
|||
/*
|
||||
* Copyright (C) 2021 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import tz, { configure } from '../'
|
||||
import timezone from 'timezone/index'
|
||||
import french from 'timezone/fr_FR'
|
||||
import AmericaDenver from 'timezone/America/Denver'
|
||||
import AmericaChicago from 'timezone/America/Chicago'
|
||||
import { setup, I18nStubber, moonwalk, epoch, equal } from './helpers'
|
||||
|
||||
setup(this)
|
||||
|
||||
test('format() should format relative to UTC by default', () =>
|
||||
equal(tz.format(moonwalk, '%F %T%:z'), '1969-07-21 02:56:00+00:00'))
|
||||
|
||||
test('format() should format in en_US by default', () =>
|
||||
equal(tz.format(moonwalk, '%c'), 'Mon 21 Jul 1969 02:56:00 AM UTC'))
|
||||
|
||||
test('format() should parse the value if necessary', () =>
|
||||
equal(tz.format('1969-07-21 02:56', '%F %T%:z'), '1969-07-21 02:56:00+00:00'))
|
||||
|
||||
test('format() should return null if the parse fails', () =>
|
||||
equal(tz.format('bogus', '%F %T%:z'), null))
|
||||
|
||||
test('format() should return null if the format string is unrecognized', () =>
|
||||
equal(tz.format(moonwalk, 'bogus'), null))
|
||||
|
||||
test('format() should preserve 12-hour+am/pm if the locale does define am/pm', () => {
|
||||
const time = tz.parse('1969-07-21 15:00:00')
|
||||
equal(tz.format(time, '%-l%P'), '3pm')
|
||||
equal(tz.format(time, '%I%P'), '03pm')
|
||||
equal(tz.format(time, '%r'), '03:00:00 PM')
|
||||
})
|
||||
|
||||
test("format() should promote 12-hour+am/pm into 24-hour if the locale doesn't define am/pm", () => {
|
||||
configure({
|
||||
tz: timezone(french, 'fr_FR'),
|
||||
momentLocale: 'fr',
|
||||
})
|
||||
|
||||
const time = tz.parse('1969-07-21 15:00:00')
|
||||
|
||||
equal(tz.format(time, '%-l%P'), '15')
|
||||
equal(tz.format(time, '%I%P'), '15')
|
||||
equal(tz.format(time, '%r'), '15:00:00')
|
||||
})
|
||||
|
||||
test("format() should use a specific timezone when asked", () => {
|
||||
configure({
|
||||
tz: timezone,
|
||||
tzData: {
|
||||
'America/Denver': AmericaDenver,
|
||||
'America/Chicago': AmericaChicago,
|
||||
}
|
||||
})
|
||||
|
||||
const time = tz.parse('1969-07-21 15:00:00')
|
||||
equal(tz.format(time, '%-l%P', 'America/Denver'), '9am')
|
||||
equal(tz.format(time, '%-l%P', 'America/Chicago'), '10am')
|
||||
})
|
||||
|
||||
test('format() should recognize date.formats.*', () => {
|
||||
I18nStubber.stub('en', {'date.formats.short': '%b %-d'})
|
||||
equal(tz.format(moonwalk, 'date.formats.short'), 'Jul 21')
|
||||
})
|
||||
|
||||
test('format() should recognize time.formats.*', () => {
|
||||
I18nStubber.stub('en', {'time.formats.tiny': '%-l:%M%P'})
|
||||
equal(tz.format(epoch, 'time.formats.tiny'), '12:00am')
|
||||
})
|
||||
|
||||
test('format() should localize when given a localization key', () => {
|
||||
configure({
|
||||
tz: timezone('fr_FR', french),
|
||||
momentLocale: 'fr'
|
||||
})
|
||||
|
||||
I18nStubber.setLocale('fr_FR')
|
||||
I18nStubber.stub('fr_FR', {'date.formats.full': '%-d %b %Y %-l:%M%P'})
|
||||
equal(tz.format(moonwalk, 'date.formats.full'), '21 juil. 1969 2:56')
|
||||
})
|
||||
|
||||
test('format() should automatically convert %l to %-l when given a localization key', () => {
|
||||
I18nStubber.stub('en', {'time.formats.tiny': '%l:%M%P'})
|
||||
equal(tz.format(moonwalk, 'time.formats.tiny'), '2:56am')
|
||||
})
|
||||
|
||||
test('format() should automatically convert %k to %-k when given a localization key', () => {
|
||||
I18nStubber.stub('en', {'time.formats.tiny': '%k:%M'})
|
||||
equal(tz.format(moonwalk, 'time.formats.tiny'), '2:56')
|
||||
})
|
||||
|
||||
test('format() should automatically convert %e to %-e when given a localization key', () => {
|
||||
I18nStubber.stub('en', {'date.formats.short': '%b %e'})
|
||||
equal(tz.format(epoch, 'date.formats.short'), 'Jan 1')
|
||||
})
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* Copyright (C) 2021 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import 'translations/_core_en'
|
||||
import I18nStubber from '../../../../spec/coffeescripts/helpers/I18nStubber'
|
||||
import tz from 'timezone/index'
|
||||
import { configure } from '../'
|
||||
|
||||
export function setup() {
|
||||
beforeEach(() => {
|
||||
configure({ tz, tzData: {} })
|
||||
I18nStubber.pushFrame()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
I18nStubber.popFrame()
|
||||
configure({})
|
||||
})
|
||||
}
|
||||
|
||||
export const moonwalk = new Date(Date.UTC(1969, 6, 21, 2, 56))
|
||||
export const epoch = new Date(Date.UTC(1970, 0, 1, 0, 0))
|
||||
export const equal = (a, b) => expect(a).toEqual(b)
|
||||
export const ok = a => expect(a).toBeTruthy()
|
||||
export { I18nStubber }
|
|
@ -0,0 +1,305 @@
|
|||
/*
|
||||
* Copyright (C) 2021 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import tz, { configure } from '../'
|
||||
import timezone from 'timezone'
|
||||
import french from 'timezone/fr_FR'
|
||||
import chinese from 'timezone/zh_CN'
|
||||
import en_US from 'timezone/en_US'
|
||||
import MockDate from 'mockdate'
|
||||
import { I18nStubber, equal, epoch, ok, moonwalk } from './helpers'
|
||||
import {
|
||||
up as configureDateTimeMomentParser,
|
||||
down as resetDateTimeMomentParser
|
||||
} from '../../../../ui/boot/initializers/configureDateTimeMomentParser'
|
||||
|
||||
describe('english tz', () => {
|
||||
beforeAll(() => {
|
||||
MockDate.set('2015-02-01', 'UTC')
|
||||
|
||||
I18nStubber.pushFrame()
|
||||
I18nStubber.setLocale('en_US')
|
||||
I18nStubber.stub('en_US', {
|
||||
'date.formats.date_at_time': '%b %-d at %l:%M%P',
|
||||
'date.formats.default': '%Y-%m-%d',
|
||||
'date.formats.full': '%b %-d, %Y %-l:%M%P',
|
||||
'date.formats.full_with_weekday': '%a %b %-d, %Y %-l:%M%P',
|
||||
'date.formats.long': '%B %-d, %Y',
|
||||
'date.formats.long_with_weekday': '%A, %B %-d',
|
||||
'date.formats.medium': '%b %-d, %Y',
|
||||
'date.formats.medium_month': '%b %Y',
|
||||
'date.formats.medium_with_weekday': '%a %b %-d, %Y',
|
||||
'date.formats.short': '%b %-d',
|
||||
'date.formats.short_month': '%b',
|
||||
'date.formats.short_weekday': '%a',
|
||||
'date.formats.short_with_weekday': '%a, %b %-d',
|
||||
'date.formats.weekday': '%A',
|
||||
'time.formats.default': '%a, %d %b %Y %H:%M:%S %z',
|
||||
'time.formats.long': '%B %d, %Y %H:%M',
|
||||
'time.formats.short': '%d %b %H:%M',
|
||||
'time.formats.tiny': '%l:%M%P',
|
||||
'time.formats.tiny_on_the_hour': '%l%P'
|
||||
})
|
||||
|
||||
configure({
|
||||
tz: timezone('en_US', en_US),
|
||||
momentLocale: 'en',
|
||||
})
|
||||
|
||||
configureDateTimeMomentParser()
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
resetDateTimeMomentParser()
|
||||
MockDate.reset()
|
||||
I18nStubber.clear()
|
||||
})
|
||||
|
||||
test('parses english dates', () => {
|
||||
const engDates = [
|
||||
'08/03/2015',
|
||||
'8/3/2015',
|
||||
'August 3, 2015',
|
||||
'Aug 3, 2015',
|
||||
'3 Aug 2015',
|
||||
'2015-08-03',
|
||||
'2015 08 03',
|
||||
'August 3, 2015',
|
||||
'Monday, August 3',
|
||||
'Mon Aug 3, 2015',
|
||||
'Mon, Aug 3',
|
||||
'Aug 3'
|
||||
]
|
||||
|
||||
engDates.forEach(date => {
|
||||
const d = tz.parse(date)
|
||||
equal(tz.format(d, '%d'), '03', `this works: ${date}`)
|
||||
})
|
||||
})
|
||||
|
||||
test('parses english times', () => {
|
||||
const engTimes = ['6:06 PM', '6:06:22 PM', '6:06pm', '6pm']
|
||||
|
||||
engTimes.forEach(time => {
|
||||
const d = tz.parse(time)
|
||||
equal(tz.format(d, '%H'), '18', `this works: ${time}`)
|
||||
})
|
||||
})
|
||||
|
||||
test('parses english date times', () => {
|
||||
const engDateTimes = [
|
||||
'2015-08-03 18:06:22',
|
||||
'August 3, 2015 6:06 PM',
|
||||
'Aug 3, 2015 6:06 PM',
|
||||
'Aug 3, 2015 6pm',
|
||||
'Monday, August 3, 2015 6:06 PM',
|
||||
'Mon, Aug 3, 2015 6:06 PM',
|
||||
'Aug 3 at 6:06pm',
|
||||
'Aug 3, 2015 6:06pm',
|
||||
'Mon Aug 3, 2015 6:06pm'
|
||||
]
|
||||
|
||||
engDateTimes.forEach(dateTime => {
|
||||
const d = tz.parse(dateTime)
|
||||
equal(tz.format(d, '%d %H'), '03 18', `this works: ${dateTime}`)
|
||||
})
|
||||
})
|
||||
|
||||
test('parses 24hr times even if the locale lacks them', () => {
|
||||
const d = tz.parse('18:06')
|
||||
equal(tz.format(d, '%H:%M'), '18:06')
|
||||
})
|
||||
})
|
||||
|
||||
describe('french tz', () => {
|
||||
beforeAll(() => {
|
||||
MockDate.set('2015-02-01', 'UTC')
|
||||
|
||||
I18nStubber.pushFrame()
|
||||
I18nStubber.setLocale('fr_FR')
|
||||
I18nStubber.stub('fr_FR', {
|
||||
'date.formats.date_at_time': '%-d %b à %k:%M',
|
||||
'date.formats.default': '%d/%m/%Y',
|
||||
'date.formats.full': '%b %-d, %Y %-k:%M',
|
||||
'date.formats.full_with_weekday': '%a %-d %b, %Y %-k:%M',
|
||||
'date.formats.long': 'le %-d %B %Y',
|
||||
'date.formats.long_with_weekday': '%A, %-d %B',
|
||||
'date.formats.medium': '%-d %b %Y',
|
||||
'date.formats.medium_month': '%b %Y',
|
||||
'date.formats.medium_with_weekday': '%a %-d %b %Y',
|
||||
'date.formats.short': '%-d %b',
|
||||
'date.formats.short_month': '%b',
|
||||
'date.formats.short_weekday': '%a',
|
||||
'date.formats.short_with_weekday': '%a, %-d %b',
|
||||
'date.formats.weekday': '%A',
|
||||
'time.formats.default': '%a, %d %b %Y %H:%M:%S %z',
|
||||
'time.formats.long': ' %d %B, %Y %H:%M',
|
||||
'time.formats.short': '%d %b %H:%M',
|
||||
'time.formats.tiny': '%k:%M',
|
||||
'time.formats.tiny_on_the_hour': '%k:%M'
|
||||
})
|
||||
|
||||
configure({
|
||||
tz: timezone('fr_FR', french),
|
||||
momentLocale: 'fr'
|
||||
})
|
||||
|
||||
configureDateTimeMomentParser()
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
resetDateTimeMomentParser()
|
||||
MockDate.reset()
|
||||
I18nStubber.clear()
|
||||
})
|
||||
|
||||
const frenchDates = [
|
||||
'03/08/2015',
|
||||
'3/8/2015',
|
||||
'3 août 2015',
|
||||
'2015-08-03',
|
||||
'le 3 août 2015',
|
||||
'lundi, 3 août',
|
||||
'lun. 3 août 2015',
|
||||
'3 août',
|
||||
'lun., 3 août',
|
||||
'3 août 2015',
|
||||
'3 août'
|
||||
]
|
||||
|
||||
for (const date of frenchDates) {
|
||||
test(`parses french date "${date}"`, () => {
|
||||
equal(tz.format(tz.parse(date), '%d'), '03')
|
||||
})
|
||||
}
|
||||
|
||||
const frenchTimes = ['18:06', '18:06:22']
|
||||
|
||||
for (const time of frenchTimes) {
|
||||
test(`parses french time "${time}"`, () => {
|
||||
equal(tz.format(tz.parse(time), '%H'), '18')
|
||||
})
|
||||
}
|
||||
|
||||
const frenchDateTimes = [
|
||||
'2015-08-03 18:06:22',
|
||||
'3 août 2015 18:06',
|
||||
'lundi 3 août 2015 18:06',
|
||||
'lun. 3 août 2015 18:06',
|
||||
'3 août à 18:06',
|
||||
'août 3, 2015 18:06',
|
||||
'lun. 3 août, 2015 18:06'
|
||||
]
|
||||
|
||||
for (const dateTime of frenchDateTimes) {
|
||||
test(`parses french date time "${dateTime}"`, () => {
|
||||
equal(tz.format(tz.parse(dateTime), '%d %H'), '03 18')
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
describe('chinese tz', () => {
|
||||
beforeAll(() => {
|
||||
MockDate.set('2015-02-01', 'UTC')
|
||||
|
||||
I18nStubber.pushFrame()
|
||||
I18nStubber.setLocale('zh_CN')
|
||||
I18nStubber.stub('zh_CN', {
|
||||
'date.formats.date_at_time': '%b %-d 于 %H:%M',
|
||||
'date.formats.default': '%Y-%m-%d',
|
||||
'date.formats.full': '%b %-d, %Y %-l:%M%P',
|
||||
'date.formats.full_with_weekday': '%a %b %-d, %Y %-l:%M%P',
|
||||
'date.formats.long': '%Y %B %-d',
|
||||
'date.formats.long_with_weekday': '%A, %B %-d',
|
||||
'date.formats.medium': '%Y %b %-d',
|
||||
'date.formats.medium_month': '%Y %b',
|
||||
'date.formats.medium_with_weekday': '%a %Y %b %-d',
|
||||
'date.formats.short': '%b %-d',
|
||||
'date.formats.short_month': '%b',
|
||||
'date.formats.short_weekday': '%a',
|
||||
'date.formats.short_with_weekday': '%a, %b %-d',
|
||||
'date.formats.weekday': '%A',
|
||||
'time.formats.default': '%a, %Y %b %d %H:%M:%S %z',
|
||||
'time.formats.long': '%Y %B %d %H:%M',
|
||||
'time.formats.short': '%b %d %H:%M',
|
||||
'time.formats.tiny': '%H:%M',
|
||||
'time.formats.tiny_on_the_hour': '%k:%M'
|
||||
})
|
||||
|
||||
configure({
|
||||
tz: timezone(chinese, 'zh_CN'),
|
||||
momentLocale: 'zh-cn'
|
||||
})
|
||||
|
||||
configureDateTimeMomentParser()
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
resetDateTimeMomentParser()
|
||||
MockDate.reset()
|
||||
I18nStubber.clear()
|
||||
})
|
||||
|
||||
test('parses chinese dates', () => {
|
||||
const chineseDates = [
|
||||
'2015-08-03',
|
||||
'2015年8月3日',
|
||||
'2015 八月 3',
|
||||
'2015 8月 3',
|
||||
'星期一, 八月 3',
|
||||
'一 2015 8月 3',
|
||||
'一, 8月 3',
|
||||
'8月 3'
|
||||
]
|
||||
|
||||
chineseDates.forEach(date => {
|
||||
expect(tz.format(tz.parse(date), '%d')).toEqual('03')
|
||||
})
|
||||
})
|
||||
|
||||
test('parses chinese date AM times', () => {
|
||||
const chineseDateTimes = [
|
||||
'2015-08-03 06:06:22',
|
||||
'2015年8月3日6点06分',
|
||||
'2015年8月3日星期一6点06分',
|
||||
'8月 3日 于 6:06',
|
||||
'20158月3日, 6:06',
|
||||
'一 20158月3日, 6:06' // this is incorrectly parsing as "Fri, 20 Mar 1908 06:06:00 GMT"
|
||||
]
|
||||
|
||||
chineseDateTimes.forEach(dateTime => {
|
||||
equal(tz.format(tz.parse(dateTime), '%d %H'), '03 06')
|
||||
})
|
||||
})
|
||||
|
||||
test('parses chinese date PM times', () => {
|
||||
const chineseDateTimes = [
|
||||
'2015-08-03 18:06:22',
|
||||
'2015年8月3日晚上6点06分',
|
||||
// '2015年8月3日星期一晚上6点06分', // parsing as "Mon, 03 Aug 2015 06:06:00 GMT"
|
||||
'8月 3日, 于 18:06',
|
||||
// '2015 8月 3日, 6:06下午', // doesn't recognize 下午 as implying PM
|
||||
// '一 2015 8月 3日, 6:06下午'
|
||||
]
|
||||
|
||||
chineseDateTimes.forEach(dateTime => {
|
||||
equal(tz.format(tz.parse(dateTime), '%d %H'), '03 18')
|
||||
})
|
||||
})
|
||||
})
|
|
@ -0,0 +1,161 @@
|
|||
/*
|
||||
* Copyright (C) 2021 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
//
|
||||
// this is only meant for use by maintainers and is not actually exercised in
|
||||
// the build mainly because the locale files aren't available to the runner
|
||||
//
|
||||
// use this only if you're doing something substantial to tz/moment/i18n
|
||||
//
|
||||
|
||||
import 'translations/_core'
|
||||
import 'translations/_core_en'
|
||||
|
||||
import tz, { configure } from '../'
|
||||
import timezone from 'timezone'
|
||||
import $ from '@canvas/datetime'
|
||||
import I18n from '@canvas/i18n'
|
||||
import {
|
||||
up as configureDateTimeMomentParser,
|
||||
down as resetDateTimeMomentParser
|
||||
} from '../../../boot/initializers/configureDateTimeMomentParser'
|
||||
import tzLocales from './bigeasyLocales'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import YAML from 'yaml'
|
||||
|
||||
import '../../../ext/custom_moment_locales/ca'
|
||||
import '../../../ext/custom_moment_locales/de'
|
||||
import '../../../ext/custom_moment_locales/he'
|
||||
import '../../../ext/custom_moment_locales/pl'
|
||||
import '../../../ext/custom_moment_locales/fa'
|
||||
import '../../../ext/custom_moment_locales/fr'
|
||||
import '../../../ext/custom_moment_locales/fr_ca'
|
||||
import '../../../ext/custom_moment_locales/ht_ht'
|
||||
import '../../../ext/custom_moment_locales/mi_nz'
|
||||
import '../../../ext/custom_moment_locales/hy_am'
|
||||
import '../../../ext/custom_moment_locales/sl'
|
||||
|
||||
const locales = loadAvailableLocales()
|
||||
const tzLocaleData = tzLocales.reduce((acc, locale) => {
|
||||
acc[locale.name] = locale
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
describe('english tz', () => {
|
||||
const dates = createDateSamples()
|
||||
|
||||
beforeEach(configureDateTimeMomentParser)
|
||||
|
||||
afterEach(resetDateTimeMomentParser)
|
||||
|
||||
for (const locale of locales) {
|
||||
test(`timezone -> moment for ${locale.key}`, () => {
|
||||
I18n.locale = locale.key
|
||||
|
||||
configure({
|
||||
tz: timezone(locale.bigeasy, tzLocaleData[locale.bigeasy]),
|
||||
momentLocale: locale.moment
|
||||
})
|
||||
|
||||
for (const date of dates) {
|
||||
const formattedDate = $.dateString(date)
|
||||
const formattedTime = tz.format(date, 'time.formats.tiny')
|
||||
const formatted = `${formattedDate} ${formattedTime}`
|
||||
|
||||
expect(
|
||||
tz.parse(formatted).getTime()
|
||||
).toEqual(
|
||||
date.getTime()
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
test(`hour format matches timezone locale for ${locale.key}`, () => {
|
||||
if (locale.key === 'ca') {
|
||||
pending("it's broken for ca, needs investigation")
|
||||
}
|
||||
|
||||
I18n.locale = locale.key
|
||||
|
||||
configure({
|
||||
tz: timezone(locale.bigeasy, tzLocaleData[locale.bigeasy]),
|
||||
momentLocale: locale.moment
|
||||
})
|
||||
|
||||
const formats = [
|
||||
'date.formats.date_at_time',
|
||||
'date.formats.full',
|
||||
'date.formats.full_with_weekday',
|
||||
'time.formats.tiny',
|
||||
'time.formats.tiny_on_the_hour'
|
||||
]
|
||||
|
||||
for (const format of formats) {
|
||||
// expect(tz.hasMeridian()).toEqual(/%p/i.test(I18n.lookup(format)))
|
||||
expect(tz.hasMeridian() || !/%p/i.test(I18n.lookup(format))).toBeTruthy()
|
||||
}
|
||||
// const invalid = key => {
|
||||
// const format = I18n.lookup(key)
|
||||
// // ok(/%p/i.test(format) === tz.hasMeridian(), `format: ${format}, hasMeridian: ${tz.hasMeridian()}`)
|
||||
// ok(
|
||||
// tz.hasMeridian() || !/%p/i.test(format),
|
||||
// `format: ${format}, hasMeridian: ${tz.hasMeridian()}`
|
||||
// )
|
||||
// }
|
||||
// ok(!formats.forEach(invalid))
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
function createDateSamples() {
|
||||
const dates = []
|
||||
const currentYear = (new Date()).getFullYear()
|
||||
const otherYear = currentYear + 4
|
||||
|
||||
for (let i = 0; i < 12; ++i) {
|
||||
dates.push(new Date(Date.UTC(currentYear, i, 1, 23, 59)))
|
||||
dates.push(new Date(Date.UTC(currentYear, i, 28, 23, 59)))
|
||||
dates.push(new Date(Date.UTC(otherYear, i, 7, 23, 59)))
|
||||
dates.push(new Date(Date.UTC(otherYear, i, 15, 23, 59)))
|
||||
}
|
||||
|
||||
return dates
|
||||
}
|
||||
|
||||
function loadAvailableLocales() {
|
||||
const manifest = (
|
||||
YAML.parse(
|
||||
fs.readFileSync(
|
||||
path.resolve(__dirname, '../../../../config/locales/locales.yml'),
|
||||
'utf8'
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
return Object.keys(manifest).map(key => {
|
||||
const locale = manifest[key]
|
||||
const base = key.split('-')[0]
|
||||
|
||||
return {
|
||||
key,
|
||||
moment: locale.moment_locale || key.toLowerCase(),
|
||||
bigeasy: locale.bigeasy_locale || manifest[base].bigeasy_locale
|
||||
}
|
||||
}).filter(x => x.key)
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* Copyright (C) 2021 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import tz from '../'
|
||||
import { equal, moonwalk, epoch, setup } from './helpers'
|
||||
|
||||
setup(this)
|
||||
|
||||
test('mergeTimeAndDate() finds the given time of day on the given date.', () =>
|
||||
equal(+tz.mergeTimeAndDate(moonwalk, epoch), +new Date(Date.UTC(1970, 0, 1, 2, 56))))
|
|
@ -0,0 +1,102 @@
|
|||
/*
|
||||
* Copyright (C) 2021 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import tz, { configure } from '../'
|
||||
import timezone from 'timezone'
|
||||
import detroit from 'timezone/America/Detroit'
|
||||
import french from 'timezone/fr_FR'
|
||||
import { setup, I18nStubber, equal, epoch, ok, moonwalk, preload } from './helpers'
|
||||
|
||||
setup(this)
|
||||
|
||||
test('hasMeridian() true if locale defines am/pm', () => ok(tz.hasMeridian()))
|
||||
|
||||
test("hasMeridian() false if locale doesn't define am/pm", () => {
|
||||
configure({
|
||||
tz: timezone('fr_FR', french),
|
||||
momentLocale: 'fr'
|
||||
})
|
||||
|
||||
ok(!tz.hasMeridian())
|
||||
})
|
||||
|
||||
test('useMeridian() true if locale defines am/pm and uses 12-hour format', () => {
|
||||
I18nStubber.stub('en', {'time.formats.tiny': '%l:%M%P'})
|
||||
ok(tz.hasMeridian())
|
||||
ok(tz.useMeridian())
|
||||
})
|
||||
|
||||
test('useMeridian() false if locale defines am/pm but uses 24-hour format', () => {
|
||||
I18nStubber.stub('en', {'time.formats.tiny': '%k:%M'})
|
||||
ok(tz.hasMeridian())
|
||||
ok(!tz.useMeridian())
|
||||
})
|
||||
|
||||
test("useMeridian() false if locale doesn't define am/pm and instead uses 24-hour format", () => {
|
||||
configure({
|
||||
tz: timezone(french, 'fr_FR'),
|
||||
momentLocale: 'fr'
|
||||
})
|
||||
|
||||
I18nStubber.setLocale('fr_FR')
|
||||
I18nStubber.stub('fr_FR', {'time.formats.tiny': '%-k:%M'})
|
||||
ok(!tz.hasMeridian())
|
||||
ok(!tz.useMeridian())
|
||||
})
|
||||
|
||||
test("useMeridian() false if locale doesn't define am/pm but still uses 12-hour format (format will be corrected)", () => {
|
||||
configure({
|
||||
tz: timezone(french, 'fr_FR'),
|
||||
momentLocale: 'fr'
|
||||
})
|
||||
|
||||
I18nStubber.setLocale('fr_FR')
|
||||
I18nStubber.stub('fr_FR', {'time.formats.tiny': '%-l:%M%P'})
|
||||
ok(!tz.hasMeridian())
|
||||
ok(!tz.useMeridian())
|
||||
})
|
||||
|
||||
test('isMidnight() is false when no argument given.', () => ok(!tz.isMidnight()))
|
||||
|
||||
test('isMidnight() is false when invalid date is given.', () => {
|
||||
const date = new Date('invalid date')
|
||||
ok(!tz.isMidnight(date))
|
||||
})
|
||||
|
||||
test('isMidnight() is true when date given is at midnight.', () => ok(tz.isMidnight(epoch)))
|
||||
|
||||
test("isMidnight() is false when date given isn't at midnight.", () => ok(!tz.isMidnight(moonwalk)))
|
||||
|
||||
test('isMidnight() is false when date is midnight in a different zone.', () => {
|
||||
configure({ tz: timezone(detroit, 'America/Detroit') })
|
||||
|
||||
ok(!tz.isMidnight(epoch))
|
||||
})
|
||||
|
||||
test('changeToTheSecondBeforeMidnight() returns null when no argument given.', () =>
|
||||
equal(tz.changeToTheSecondBeforeMidnight(), null))
|
||||
|
||||
test('changeToTheSecondBeforeMidnight() returns null when invalid date is given.', () => {
|
||||
const date = new Date('invalid date')
|
||||
equal(tz.changeToTheSecondBeforeMidnight(date), null)
|
||||
})
|
||||
|
||||
test('changeToTheSecondBeforeMidnight() returns fancy midnight when a valid date is given.', () => {
|
||||
const fancyMidnight = tz.changeToTheSecondBeforeMidnight(epoch)
|
||||
equal(fancyMidnight.toGMTString(), 'Thu, 01 Jan 1970 23:59:59 GMT')
|
||||
})
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* Copyright (C) 2021 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import tz, { configure } from '../'
|
||||
import timezone from 'timezone'
|
||||
import { moonwalk, epoch, equal, setup } from './helpers'
|
||||
|
||||
setup(this)
|
||||
|
||||
test('parse(valid datetime string)', () => {
|
||||
equal(+tz.parse(moonwalk.toISOString()), +moonwalk)
|
||||
})
|
||||
|
||||
test('parse(timestamp integer)', () => {
|
||||
equal(+tz.parse(+moonwalk), +moonwalk)
|
||||
})
|
||||
|
||||
test('parse(Date object)', () => {
|
||||
equal(+tz.parse(moonwalk), +moonwalk)
|
||||
})
|
||||
|
||||
test('parse(date array)', () => {
|
||||
equal(+tz.parse([1969, 7, 21, 2, 56]), +moonwalk)
|
||||
})
|
||||
|
||||
test('parse() should return null on failure', () => equal(tz.parse('bogus'), null))
|
||||
|
||||
test('parse() should return a date on success', () => equal(typeof tz.parse(+moonwalk), 'object'))
|
||||
|
||||
test('parse("") should fail', () => equal(tz.parse(''), null))
|
||||
|
||||
test('parse(null) should fail', () => equal(tz.parse(null), null))
|
||||
|
||||
test('parse(integer) should be ms since epoch', () => equal(+tz.parse(2016), +timezone(2016)))
|
||||
|
||||
test('parse("looks like integer") should be a year', () =>
|
||||
equal(+tz.parse('2016'), +tz.parse('2016-01-01')))
|
||||
|
||||
test('parse() should parse relative to UTC by default', () =>
|
||||
equal(+tz.parse('1969-07-21 02:56'), +moonwalk))
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* Copyright (C) 2021 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import tz from '../'
|
||||
import { equal, moonwalk, setup } from './helpers'
|
||||
|
||||
setup(this)
|
||||
|
||||
test('shift() should adjust the date as appropriate', () =>
|
||||
equal(+tz.shift(moonwalk, '-1 day'), moonwalk - 86400000))
|
||||
|
||||
test('shift() should apply multiple directives', () =>
|
||||
equal(+tz.shift(moonwalk, '-1 day', '-1 hour'), moonwalk - 86400000 - 3600000))
|
||||
|
||||
test('shift() should parse the value if necessary', () =>
|
||||
equal(+tz.shift('1969-07-21 02:56', '-1 day'), moonwalk - 86400000))
|
||||
|
||||
test('shift() should return null if the parse fails', () =>
|
||||
equal(tz.shift('bogus', '-1 day'), null))
|
||||
|
||||
test('shift() should return null if the directives includes a format string', () =>
|
||||
equal(tz.shift('bogus', '-1 day', '%F %T%:z'), null))
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (C) 2013 - present Instructure, Inc.
|
||||
* Copyright (C) 2021 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
|
@ -16,338 +16,244 @@
|
|||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import _ from 'underscore'
|
||||
import __tz from 'timezone/index'
|
||||
import I18n from '@canvas/i18n'
|
||||
import moment from 'moment'
|
||||
import MomentFormats from './moment_formats'
|
||||
import timezone from 'timezone'
|
||||
import en_US from 'timezone/en_US'
|
||||
import parseDateTimeWithMoment, {
|
||||
specifiesTimezone,
|
||||
toRFC3339WithoutTZ,
|
||||
} from 'date-time-moment-parser'
|
||||
|
||||
// start with the bare vendor-provided tz() function
|
||||
let _tz = __tz
|
||||
let currentLocale = 'en_US' // default to US locale
|
||||
let momentLocale = 'en'
|
||||
const _preloadedData =
|
||||
window.__PRELOADED_TIMEZONE_DATA__ || (window.__PRELOADED_TIMEZONE_DATA__ = {})
|
||||
const initialState = Object.freeze({
|
||||
// a timezone instance configured for the user's current timezone and locale
|
||||
//
|
||||
// import timezone from 'timezone'
|
||||
// import Denver from 'timezone/America/Denver'
|
||||
// import french from 'timezone/fr_FR'
|
||||
//
|
||||
// const tz = timezone('America/Denver', Denver, 'fr_FR', french)
|
||||
//
|
||||
// tz: timezone(en_US, 'en_US', 'UTC'),
|
||||
tz: timezone(en_US, 'en_US', 'UTC'),
|
||||
|
||||
// wrap it up in a set of methods that will always call the most up-to-date
|
||||
// version. each method is intended to act as a subset of bigeasy's generic
|
||||
// tz() functionality.
|
||||
const tz = {
|
||||
_preloadedData,
|
||||
// a mapping of timezones (e.g. America/Denver) to their timezone data (e.g.
|
||||
// import Denver from "timezone/America/Denver") that is used when formatting
|
||||
// to a timezone different than the default
|
||||
tzData: {},
|
||||
|
||||
// wrap's moment() for parsing datetime strings. assumes the string to be
|
||||
// parsed is in the profile timezone unless if contains an offset string
|
||||
// *and* a format token to parse it, and unfudges the result.
|
||||
moment(input, format) {
|
||||
// ensure first argument is a string and second is a format or an array
|
||||
// of formats
|
||||
if (!_.isString(input) || !(_.isString(format) || _.isArray(format))) {
|
||||
throw new Error(
|
||||
'tz.moment only works on string+format(s). just use moment() directly for any other signature'
|
||||
)
|
||||
}
|
||||
// used for parsing datetimes when timezone is unable to parse a value
|
||||
momentLocale: 'en',
|
||||
})
|
||||
|
||||
// call out to moment, leaving the result alone if invalid
|
||||
let m = moment.apply(null, [input, format, momentLocale])
|
||||
if (m._pf.unusedTokens.length > 0) {
|
||||
// we didn't use strict at first, because we want to accept when
|
||||
// there's unused input as long as we're using all tokens. but if the
|
||||
// best non-strict match has unused tokens, reparse with strict
|
||||
m = moment.apply(null, [input, format, momentLocale, true])
|
||||
}
|
||||
if (!m.isValid()) return m
|
||||
const state = Object.assign({}, initialState)
|
||||
|
||||
// unfudge the result unless an offset was both specified and used in the
|
||||
// parsed string.
|
||||
//
|
||||
// using moment internals here because I can't come up with any better
|
||||
// reliable way to test for this :( fortunately, both _f and
|
||||
// _pf.unusedTokens are always set as long as format is explicitly
|
||||
// specified as either a string or array (which we've already checked
|
||||
// for).
|
||||
//
|
||||
// _f lacking a 'Z' indicates that no offset token was specified in the
|
||||
// format string used in parsing. we check this instead of just format in
|
||||
// case format is an array, of which one contains a Z and the other
|
||||
// doesn't, and we don't know until after parsing which format would best
|
||||
// match the input.
|
||||
//
|
||||
// _pf.unusedTokens having a 'Z' token indicates that even though the
|
||||
// format used contained a 'Z' token (since the first condition wasn't
|
||||
// false), that token was not used during parsing; i.e. the input string
|
||||
// didn't provide a value for it.
|
||||
//
|
||||
if (!m._f.match(/Z/) || m._pf.unusedTokens.indexOf('Z') >= 0) {
|
||||
const l = m.locale()
|
||||
m = moment(tz.raw_parse(m.locale('en').format('YYYY-MM-DD HH:mm:ss')))
|
||||
m.locale(l)
|
||||
}
|
||||
export function configure({ tz, tzData, momentLocale }) {
|
||||
const previousState = Object.assign({}, state)
|
||||
|
||||
return m
|
||||
},
|
||||
state.tz = tz || initialState.tz
|
||||
state.tzData = tzData || initialState.tzData
|
||||
state.momentLocale = momentLocale || initialState.momentLocale
|
||||
|
||||
// interprets a date value (string, integer, Date, date array, etc. -- see
|
||||
// bigeasy's tz() docs) according to _tz. returns null on parse failure.
|
||||
// otherwise returns a Date (rather than _tz()'s timestamp integer)
|
||||
// because, when treated correctly, they are interchangeable but the Date
|
||||
// is more convenient.
|
||||
raw_parse(value) {
|
||||
const timestamp = _tz(value)
|
||||
if (typeof timestamp === 'number') {
|
||||
return new Date(timestamp)
|
||||
}
|
||||
return null
|
||||
},
|
||||
|
||||
// parses a date value but more robustly. returns null on parse failure. if
|
||||
// the value is a string but does not look like an ISO8601 string
|
||||
// (loosely), or otherwise fails to be interpreted by raw_parse(), then
|
||||
// parsing will be attempted with tz.moment() according to the formats
|
||||
// defined in MomentFormats.getFormats(). also note that raw_parse('') will
|
||||
// return the epoch, but parse('') will return null.
|
||||
parse(value) {
|
||||
// hard code '' and null as unparseable
|
||||
if (value === '' || value == null) return null
|
||||
|
||||
if (!_.isString(value)) {
|
||||
// try and understand the value through _tz. if it doesn't work, we
|
||||
// don't know what else to do with it as a non-string
|
||||
return tz.raw_parse(value)
|
||||
}
|
||||
|
||||
// only try _tz with strings looking loosely like an ISO8601 value. in
|
||||
// particular, we want to avoid parsing e.g. '2016' as 2,016 milliseconds
|
||||
// since the epoch
|
||||
if (value.match(/[-:]/)) {
|
||||
const result = tz.raw_parse(value)
|
||||
if (result) return result
|
||||
}
|
||||
|
||||
// _tz parsing failed or skipped. try moment parsing
|
||||
const formats = MomentFormats.getFormats()
|
||||
const m = tz.moment(value, formats)
|
||||
return m.isValid() ? m.toDate() : null
|
||||
},
|
||||
|
||||
// format a date value (parsing it if necessary). returns null for parse
|
||||
// failure on the value or an unrecognized format string.
|
||||
format(value, format, otherZone) {
|
||||
let localTz = _tz
|
||||
const usingOtherZone = arguments.length === 3 && otherZone
|
||||
if (usingOtherZone) {
|
||||
if (!(otherZone in _preloadedData)) return null
|
||||
localTz = _tz(_preloadedData[otherZone])
|
||||
}
|
||||
// make sure we have a good value first
|
||||
const datetime = tz.parse(value)
|
||||
if (datetime == null) return null
|
||||
|
||||
format = tz.adjustFormat(format)
|
||||
|
||||
// try and apply the format string to the datetime. if it succeeds, we'll
|
||||
// get a string; otherwise we'll get the (non-string) date back.
|
||||
let formatted = null
|
||||
if (usingOtherZone) {
|
||||
formatted = localTz(datetime, format, otherZone)
|
||||
} else {
|
||||
formatted = localTz(datetime, format)
|
||||
}
|
||||
|
||||
if (typeof formatted !== 'string') return null
|
||||
return formatted
|
||||
},
|
||||
|
||||
adjustFormat(format) {
|
||||
// translate recognized 'date.formats.*' and 'time.formats.*' to
|
||||
// appropriate format strings according to locale.
|
||||
if (format.match(/^(date|time)\.formats\./)) {
|
||||
const locale_format = I18n.lookup(format)
|
||||
if (locale_format) {
|
||||
// in the process, turn %l, %k, and %e into %-l, %-k, and %-e
|
||||
// (respectively) to avoid extra unnecessary space characters
|
||||
//
|
||||
// javascript doesn't have lookbehind, so do the fixing on the reversed
|
||||
// string so we can use lookahead instead. the funky '(%%)*(?!%)' pattern
|
||||
// in all the regexes is to make sure we match (once unreversed), e.g.,
|
||||
// both %l and %%%l (literal-% + %l) but not %%l (literal-% + l).
|
||||
format = locale_format
|
||||
.split('')
|
||||
.reverse()
|
||||
.join('')
|
||||
.replace(/([lke])(?=%(%%)*(?!%))/, '$1-')
|
||||
.split('')
|
||||
.reverse()
|
||||
.join('')
|
||||
}
|
||||
}
|
||||
|
||||
// some locales may not (according to bigeasy's localization files) use
|
||||
// an am/pm distinction, but could then be incorrectly used with 12-hour
|
||||
// format strings (e.g. %l:%M%P), whether by erroneous format strings in
|
||||
// canvas' localization files or by unlocalized format strings. as a
|
||||
// result, you might get 3am and 3pm both formatting to the same value.
|
||||
// to prevent this, 12-hour indicators with an am/pm indicator should be
|
||||
// promoted to the equivalent 24-hour indicator when the locale defines
|
||||
// %P as an empty string. ("reverse, look-ahead, reverse" pattern for
|
||||
// same reason as above)
|
||||
format = format
|
||||
.split('')
|
||||
.reverse()
|
||||
.join('')
|
||||
if (
|
||||
!tz.hasMeridian() &&
|
||||
((format.match(/[lI][-_]?%(%%)*(?!%)/) && format.match(/p%(%%)*(?!%)/i)) ||
|
||||
format.match(/r[-_]?%(%%)*(?!%)/))
|
||||
) {
|
||||
format = format.replace(/l(?=[-_]?%(%%)*(?!%))/, 'k')
|
||||
format = format.replace(/I(?=[-_]?%(%%)*(?!%))/, 'H')
|
||||
format = format.replace(/r(?=[-_]?%(%%)*(?!%))/, 'T')
|
||||
}
|
||||
format = format
|
||||
.split('')
|
||||
.reverse()
|
||||
.join('')
|
||||
|
||||
return format
|
||||
},
|
||||
|
||||
hasMeridian() {
|
||||
return _tz(new Date(), '%P') !== ''
|
||||
},
|
||||
|
||||
useMeridian() {
|
||||
if (!this.hasMeridian()) return false
|
||||
const tiny = I18n.lookup('time.formats.tiny')
|
||||
return tiny && tiny.match(/%-?l/)
|
||||
},
|
||||
|
||||
// apply any number of non-format directives to the value (parsing it if
|
||||
// necessary). return null for parse failure on the value or if one of the
|
||||
// directives was mistakenly a format string. returns the modified Date
|
||||
// otherwise. typical directives will be for date math, e.g. '-3 days'.
|
||||
// non-format unrecognized directives are ignored.
|
||||
shift(value) {
|
||||
// make sure we have a good value first
|
||||
const datetime = tz.parse(value)
|
||||
if (datetime == null) return null
|
||||
|
||||
// no application strings given? just regurgitate the input (though
|
||||
// parsed now).
|
||||
if (arguments.length == 1) return datetime
|
||||
|
||||
// try and apply the directives to the datetime. if one was a format
|
||||
// string (unacceptable) we'll get a (non-integer) string back.
|
||||
// otherwise, we'll get a new timestamp integer back (even if some
|
||||
// unrecognized non-format applications were ignored).
|
||||
const args = [datetime].concat([].slice.apply(arguments, [1]))
|
||||
const timestamp = _tz(...args)
|
||||
if (typeof timestamp !== 'number') return null
|
||||
return new Date(timestamp)
|
||||
},
|
||||
|
||||
// allow snapshotting and restoration, and extending through the
|
||||
// vendor-provided tz()'s functional composition
|
||||
snapshot() {
|
||||
return [_tz, currentLocale, momentLocale]
|
||||
},
|
||||
|
||||
restore(snapshot) {
|
||||
// we can't actually check that the snapshot has appropriate values, but
|
||||
// we can at least verify the shape of [function, string, string]
|
||||
if (!_.isArray(snapshot)) throw new Error('invalid tz() snapshot')
|
||||
if (typeof snapshot[0] !== 'function') throw new Error('invalid tz() snapshot')
|
||||
if (!_.isString(snapshot[1])) throw new Error('invalid tz() snapshot')
|
||||
if (!_.isString(snapshot[2])) throw new Error('invalid tz() snapshot')
|
||||
_tz = snapshot[0]
|
||||
currentLocale = snapshot[1]
|
||||
momentLocale = snapshot[2]
|
||||
},
|
||||
|
||||
extendConfiguration() {
|
||||
const extended = _tz(...arguments)
|
||||
if (typeof extended !== 'function') throw new Error('invalid tz() extension')
|
||||
_tz = extended
|
||||
},
|
||||
|
||||
// apply a "feature" to tz (NOTE: persistent and shared). the provided
|
||||
// feature can be a chunk of previously loaded data, which is applied
|
||||
// immediately, or the name of a data file to load and then apply
|
||||
// asynchronously.
|
||||
applyFeature(data, name) {
|
||||
function extendConfig(preloadedData) {
|
||||
tz.extendConfiguration(preloadedData, name)
|
||||
return Promise.resolve()
|
||||
}
|
||||
if (arguments.length > 1) {
|
||||
this.preload(name, data)
|
||||
return extendConfig(data)
|
||||
}
|
||||
|
||||
name = data
|
||||
const preloadedData = this.preload(name)
|
||||
if (preloadedData instanceof Promise) {
|
||||
return preloadedData.then(extendConfig)
|
||||
}
|
||||
return extendConfig(preloadedData)
|
||||
},
|
||||
|
||||
// preload a specific data file without having to actually
|
||||
// change the timezone to do it. Future "applyFeature" calls
|
||||
// will apply synchronously if their data is already preloaded.
|
||||
preload(name, data) {
|
||||
if (arguments.length > 1) {
|
||||
_preloadedData[name] = data
|
||||
return _preloadedData[name]
|
||||
} else if (_preloadedData[name]) {
|
||||
return _preloadedData[name]
|
||||
} else {
|
||||
return new Promise((resolve, reject) =>
|
||||
reject(
|
||||
new Error(
|
||||
`In webpack, loading timezones on-demand is not supported. ${name}" should already be script-tagged onto the page from Rails.`
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
},
|
||||
|
||||
changeLocale() {
|
||||
if (arguments.length > 2) {
|
||||
currentLocale = arguments[1]
|
||||
momentLocale = arguments[2]
|
||||
} else {
|
||||
currentLocale = arguments[0]
|
||||
momentLocale = arguments[1]
|
||||
}
|
||||
// take off the momentLocale before passing up the chain
|
||||
const args = [].slice.apply(arguments).slice(0, arguments.length - 1)
|
||||
return this.applyFeature.apply(this, args)
|
||||
},
|
||||
|
||||
isMidnight(date) {
|
||||
if (date == null) {
|
||||
return false
|
||||
}
|
||||
return tz.format(date, '%R') === '00:00'
|
||||
},
|
||||
|
||||
changeToTheSecondBeforeMidnight(date) {
|
||||
return tz.parse(tz.format(date, '%F 23:59:59'))
|
||||
},
|
||||
|
||||
setToEndOfMinute(date) {
|
||||
return tz.parse(tz.format(date, '%F %R:59'))
|
||||
},
|
||||
|
||||
// finds the given time of day on the given date ignoring dst conversion and such.
|
||||
// e.g. if time is 2016-05-20 14:00:00 and date is 2016-03-17 23:59:59, the result will
|
||||
// be 2016-03-17 14:00:00
|
||||
mergeTimeAndDate(time, date) {
|
||||
return tz.parse(tz.format(date, '%F ') + tz.format(time, '%T'))
|
||||
}
|
||||
return previousState
|
||||
}
|
||||
|
||||
// changing zone and locale are just aliases for applying a feature
|
||||
tz.changeZone = tz.applyFeature
|
||||
export function parse(value) {
|
||||
const { tz, momentLocale } = state
|
||||
|
||||
export default tz
|
||||
// hard code '' and null as unparseable
|
||||
if (value === '' || value === null || value === undefined) {
|
||||
return null
|
||||
}
|
||||
|
||||
// we don't want to use tz for parsing any string that doesn't look like a
|
||||
// datetime string
|
||||
if (typeof value !== 'string' || value.match(/[-:]/)) {
|
||||
const epoch = tz(value)
|
||||
|
||||
if (typeof epoch === 'number') {
|
||||
return new Date(epoch)
|
||||
}
|
||||
}
|
||||
|
||||
// try with moment
|
||||
if (typeof value === 'string') {
|
||||
let m = parseDateTimeWithMoment(value, momentLocale)
|
||||
|
||||
if (m && !specifiesTimezone(m)) {
|
||||
const fudged = tz(toRFC3339WithoutTZ(m))
|
||||
|
||||
m = moment(new Date(fudged))
|
||||
m.locale(momentLocale)
|
||||
}
|
||||
|
||||
if (m) {
|
||||
return m.toDate()
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
// format a date value (parsing it if necessary). returns null for parse
|
||||
// failure on the value or an unrecognized format string.
|
||||
export function format(value, inputFormat, otherZone) {
|
||||
const { tzData } = state
|
||||
const usingOtherZone = arguments.length === 3 && otherZone
|
||||
|
||||
let tz = state.tz
|
||||
if (usingOtherZone) {
|
||||
if (!(otherZone in tzData)) {
|
||||
console.warn(
|
||||
`You are asking to format DateTime into a timezone that is not supported -- ${otherZone}`
|
||||
)
|
||||
return null
|
||||
}
|
||||
tz = tz(tzData[otherZone])
|
||||
}
|
||||
// make sure we have a good value first
|
||||
const datetime = parse(value)
|
||||
if (datetime === null) return null
|
||||
|
||||
const localizedFormat = adjustFormat(inputFormat)
|
||||
|
||||
// try and apply the format string to the datetime. if it succeeds, we'll
|
||||
// get a string; otherwise we'll get the (non-string) date back.
|
||||
let formatted = null
|
||||
|
||||
if (usingOtherZone) {
|
||||
formatted = tz(datetime, localizedFormat, otherZone)
|
||||
} else {
|
||||
formatted = tz(datetime, localizedFormat)
|
||||
}
|
||||
|
||||
if (typeof formatted !== 'string') return null
|
||||
return formatted
|
||||
}
|
||||
|
||||
export function adjustFormat(format) {
|
||||
// translate recognized 'date.formats.*' and 'time.formats.*' to
|
||||
// appropriate format strings according to locale.
|
||||
if (format.match(/^(date|time)\.formats\./)) {
|
||||
const localeFormat = I18n.lookup(format)
|
||||
if (localeFormat) {
|
||||
// in the process, turn %l, %k, and %e into %-l, %-k, and %-e
|
||||
// (respectively) to avoid extra unnecessary space characters
|
||||
//
|
||||
// javascript doesn't have lookbehind, so do the fixing on the reversed
|
||||
// string so we can use lookahead instead. the funky '(%%)*(?!%)' pattern
|
||||
// in all the regexes is to make sure we match (once unreversed), e.g.,
|
||||
// both %l and %%%l (literal-% + %l) but not %%l (literal-% + l).
|
||||
format = localeFormat
|
||||
.split('')
|
||||
.reverse()
|
||||
.join('')
|
||||
.replace(/([lke])(?=%(%%)*(?!%))/, '$1-')
|
||||
.split('')
|
||||
.reverse()
|
||||
.join('')
|
||||
}
|
||||
}
|
||||
|
||||
// some locales may not (according to bigeasy's localization files) use
|
||||
// an am/pm distinction, but could then be incorrectly used with 12-hour
|
||||
// format strings (e.g. %l:%M%P), whether by erroneous format strings in
|
||||
// canvas' localization files or by unlocalized format strings. as a
|
||||
// result, you might get 3am and 3pm both formatting to the same value.
|
||||
// to prevent this, 12-hour indicators with an am/pm indicator should be
|
||||
// promoted to the equivalent 24-hour indicator when the locale defines
|
||||
// %P as an empty string. ("reverse, look-ahead, reverse" pattern for
|
||||
// same reason as above)
|
||||
format = format
|
||||
.split('')
|
||||
.reverse()
|
||||
.join('')
|
||||
if (
|
||||
!hasMeridian() &&
|
||||
((format.match(/[lI][-_]?%(%%)*(?!%)/) && format.match(/p%(%%)*(?!%)/i)) ||
|
||||
format.match(/r[-_]?%(%%)*(?!%)/))
|
||||
) {
|
||||
format = format.replace(/l(?=[-_]?%(%%)*(?!%))/, 'k')
|
||||
format = format.replace(/I(?=[-_]?%(%%)*(?!%))/, 'H')
|
||||
format = format.replace(/r(?=[-_]?%(%%)*(?!%))/, 'T')
|
||||
}
|
||||
format = format
|
||||
.split('')
|
||||
.reverse()
|
||||
.join('')
|
||||
|
||||
return format
|
||||
}
|
||||
|
||||
export function hasMeridian() {
|
||||
const { tz } = state
|
||||
|
||||
return tz(new Date(), '%P') !== ''
|
||||
}
|
||||
|
||||
export function useMeridian() {
|
||||
if (!hasMeridian()) return false
|
||||
const tiny = I18n.lookup('time.formats.tiny')
|
||||
return tiny && tiny.match(/%-?l/)
|
||||
}
|
||||
|
||||
// apply any number of non-format directives to the value (parsing it if
|
||||
// necessary). return null for parse failure on the value or if one of the
|
||||
// directives was mistakenly a format string. returns the modified Date
|
||||
// otherwise. typical directives will be for date math, e.g. '-3 days'.
|
||||
// non-format unrecognized directives are ignored.
|
||||
export function shift(value) {
|
||||
const { tz } = state
|
||||
|
||||
// make sure we have a good value first
|
||||
const datetime = parse(value)
|
||||
if (datetime === null) return null
|
||||
|
||||
// no application strings given? just regurgitate the input (though
|
||||
// parsed now).
|
||||
if (arguments.length === 1) return datetime
|
||||
|
||||
// try and apply the directives to the datetime. if one was a format
|
||||
// string (unacceptable) we'll get a (non-integer) string back.
|
||||
// otherwise, we'll get a new timestamp integer back (even if some
|
||||
// unrecognized non-format applications were ignored).
|
||||
const args = [datetime].concat([].slice.apply(arguments, [1]))
|
||||
const timestamp = tz(...args)
|
||||
if (typeof timestamp !== 'number') return null
|
||||
return new Date(timestamp)
|
||||
}
|
||||
|
||||
export function isMidnight(date) {
|
||||
if (date === null) {
|
||||
return false
|
||||
}
|
||||
return format(date, '%R') === '00:00'
|
||||
}
|
||||
|
||||
export function changeToTheSecondBeforeMidnight(date) {
|
||||
return parse(format(date, '%F 23:59:59'))
|
||||
}
|
||||
|
||||
export function setToEndOfMinute(date) {
|
||||
return parse(format(date, '%F %R:59'))
|
||||
}
|
||||
|
||||
// finds the given time of day on the given date ignoring dst conversion and such.
|
||||
// e.g. if time is 2016-05-20 14:00:00 and date is 2016-03-17 23:59:59, the result will
|
||||
// be 2016-03-17 14:00:00
|
||||
export function mergeTimeAndDate(time, date) {
|
||||
return parse(format(date, '%F ') + format(time, '%T'))
|
||||
}
|
||||
|
||||
export default {
|
||||
adjustFormat,
|
||||
changeToTheSecondBeforeMidnight,
|
||||
format,
|
||||
hasMeridian,
|
||||
isMidnight,
|
||||
mergeTimeAndDate,
|
||||
parse,
|
||||
setToEndOfMinute,
|
||||
shift,
|
||||
useMeridian,
|
||||
}
|
||||
|
|
|
@ -1,168 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2015 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import _ from 'underscore'
|
||||
import I18n from 'i18n!instructure'
|
||||
import moment from 'moment'
|
||||
|
||||
function eventTimes(dateFormats, timeFormats) {
|
||||
const formats = []
|
||||
dateFormats.forEach(df => {
|
||||
timeFormats.forEach(tf => {
|
||||
formats.push(() =>
|
||||
I18n.t('#time.event', '%{date} at %{time}', {
|
||||
date: I18n.lookup(`date.formats.${df}`),
|
||||
time: I18n.lookup(`time.formats.${tf}`)
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
return formats
|
||||
}
|
||||
|
||||
function dateFormat(key) {
|
||||
return () => I18n.lookup(`date.formats.${key}`)
|
||||
}
|
||||
|
||||
function timeFormat(key) {
|
||||
return () => I18n.lookup(`time.formats.${key}`)
|
||||
}
|
||||
|
||||
function joinFormats(separator, ...formats) {
|
||||
return () => formats.map(key => I18n.lookup(key)).join(separator)
|
||||
}
|
||||
|
||||
const moment_formats = {
|
||||
i18nToMomentHash: {
|
||||
'%A': 'dddd',
|
||||
'%B': 'MMMM',
|
||||
'%H': 'HH',
|
||||
'%M': 'mm',
|
||||
'%S': 'ss',
|
||||
'%P': 'a',
|
||||
'%Y': 'YYYY',
|
||||
'%a': 'ddd',
|
||||
'%b': 'MMM',
|
||||
'%m': 'M',
|
||||
'%d': 'D',
|
||||
'%k': 'H',
|
||||
'%l': 'h',
|
||||
'%z': 'Z',
|
||||
|
||||
'%-H': 'H',
|
||||
'%-M': 'm',
|
||||
'%-S': 's',
|
||||
'%-m': 'M',
|
||||
'%-d': 'D',
|
||||
'%-k': 'H',
|
||||
'%-l': 'h'
|
||||
},
|
||||
|
||||
basicMomentFormats: [
|
||||
moment.ISO_8601,
|
||||
'YYYY',
|
||||
'LT',
|
||||
'LTS',
|
||||
'L',
|
||||
'l',
|
||||
'LL',
|
||||
'll',
|
||||
'LLL',
|
||||
'lll',
|
||||
'LLLL',
|
||||
'llll',
|
||||
'D MMM YYYY',
|
||||
'H:mm'
|
||||
],
|
||||
|
||||
getFormats() {
|
||||
let formatsToTransform = moment_formats.formatsForLocale()
|
||||
formatsToTransform = moment_formats.formatsIncludingImplicitMinutes(formatsToTransform)
|
||||
return this.transformFormats(formatsToTransform)
|
||||
},
|
||||
|
||||
formatsIncludingImplicitMinutes(formats) {
|
||||
const arrayOfArrays = _.map(formats, format =>
|
||||
format.match(/:%-?M/) ? [format, format.replace(/:%-?M/, '')] : [format]
|
||||
)
|
||||
return _.flatten(arrayOfArrays)
|
||||
},
|
||||
|
||||
transformFormats: _.memoize(formats => {
|
||||
const localeSpecificFormats = _.map(formats, moment_formats.i18nToMomentFormat)
|
||||
return _.union(moment_formats.basicMomentFormats, localeSpecificFormats)
|
||||
}),
|
||||
|
||||
// examples are from en_US. order is significant since if an input matches
|
||||
// multiple formats, the format earlier in the list will be preferred
|
||||
orderedFormats: [
|
||||
timeFormat('default'), // %a, %d %b %Y %H:%M:%S %z
|
||||
dateFormat('full_with_weekday'), // %a %b %-d, %Y %-l:%M%P
|
||||
dateFormat('full'), // %b %-d, %Y %-l:%M%P
|
||||
dateFormat('date_at_time'), // %b %-d at %l:%M%P
|
||||
dateFormat('long_with_weekday'), // %A, %B %-d
|
||||
dateFormat('medium_with_weekday'), // %a %b %-d, %Y
|
||||
dateFormat('short_with_weekday'), // %a, %b %-d
|
||||
timeFormat('long'), // %B %d, %Y %H:%M
|
||||
dateFormat('long'), // %B %-d, %Y
|
||||
...eventTimes(['medium', 'short'], ['tiny', 'tiny_on_the_hour']),
|
||||
joinFormats(' ', 'date.formats.medium', 'time.formats.tiny'),
|
||||
joinFormats(' ', 'date.formats.medium', 'time.formats.tiny_on_the_hour'),
|
||||
dateFormat('medium'), // %b %-d, %Y
|
||||
timeFormat('short'), // %d %b %H:%M
|
||||
joinFormats(' ', 'date.formats.short', 'time.formats.tiny'),
|
||||
joinFormats(' ', 'date.formats.short', 'time.formats.tiny_on_the_hour'),
|
||||
dateFormat('short'), // %b %-d
|
||||
dateFormat('default'), // %Y-%m-%d
|
||||
timeFormat('tiny'), // %l:%M%P
|
||||
timeFormat('tiny_on_the_hour'), // %l%P
|
||||
dateFormat('weekday'), // %A
|
||||
dateFormat('short_weekday') // %a
|
||||
],
|
||||
|
||||
formatsForLocale() {
|
||||
return _.compact(moment_formats.orderedFormats.map(fn => fn()))
|
||||
},
|
||||
|
||||
i18nToMomentFormat(fullString) {
|
||||
const withEscapes = moment_formats.escapeSubStrings(fullString)
|
||||
return moment_formats.replaceDateKeys(withEscapes)
|
||||
},
|
||||
|
||||
escapeSubStrings(formatString) {
|
||||
const substrings = formatString.split(' ')
|
||||
const escapedSubs = _.map(substrings, moment_formats.escapedUnlessi18nKey)
|
||||
return escapedSubs.join(' ')
|
||||
},
|
||||
|
||||
escapedUnlessi18nKey(string) {
|
||||
const isKey = _.find(_.keys(moment_formats.i18nToMomentHash), k => string.indexOf(k) > -1)
|
||||
|
||||
return isKey ? string : `[${string}]`
|
||||
},
|
||||
|
||||
replaceDateKeys(formatString) {
|
||||
return _.reduce(
|
||||
moment_formats.i18nToMomentHash,
|
||||
(string, forMoment, forBase) => string.replace(forBase, forMoment),
|
||||
formatString
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default moment_formats
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* Copyright (C) 2021 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { configure } from './'
|
||||
import timezone from 'timezone'
|
||||
|
||||
const snapshots = []
|
||||
|
||||
export const configureAndRestoreLater = config => snapshots.push(configure(config))
|
||||
export const restore = () => snapshots.splice(0).reverse().forEach(configure)
|
||||
export const changeZone = (zoneData, zoneName) => configureAndRestoreLater({
|
||||
tz: timezone(zoneData, zoneName),
|
||||
tzData: {
|
||||
[zoneName]: zoneData
|
||||
}
|
||||
})
|
||||
|
||||
export const changeLocale = (localeData, bigeasyLocale, momentLocale) => (
|
||||
configureAndRestoreLater({
|
||||
tz: timezone(localeData, bigeasyLocale),
|
||||
momentLocale
|
||||
})
|
||||
)
|
||||
|
||||
export default {
|
||||
configureAndRestoreLater,
|
||||
changeLocale,
|
||||
changeZone,
|
||||
restore
|
||||
}
|
Loading…
Reference in New Issue