Skip to content

Commit

Permalink
1.7.1dev: merge [17771:17772] from 1.6-stable (fix for #13482)
Browse files Browse the repository at this point in the history
git-svn-id: http://trac.edgewall.org/intertrac/log:/trunk@17773 af82e41b-90c4-0310-8c96-b1721e28e2e2
  • Loading branch information
jomae committed Apr 19, 2024
2 parents 760e2a4 + b3a2d05 commit a30c5f8
Show file tree
Hide file tree
Showing 6 changed files with 163 additions and 57 deletions.
2 changes: 1 addition & 1 deletion .github/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ multipart; python_version>='3.11'
aiosmtpd; python_version>='3.10'
selenium!=4.10.0
pytidylib
Babel<2.10
Babel
Pygments
docutils
textile
Expand Down
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ exclude =
*.tests.*

[options.extras_require]
babel = Babel>=2.2,<2.10
babel = Babel>=2.2
mysql = PyMySQL
postgresql = psycopg2>=2.5
psycopg2 = psycopg2>=2.5
Expand Down
8 changes: 4 additions & 4 deletions trac/ticket/tests/report.py
Original file line number Diff line number Diff line change
Expand Up @@ -422,11 +422,11 @@ def test_timestamp_columns(self):
rendered = self._render_template(req, *rv)
rendered = str(rendered, 'utf-8')
self.assertRegex(rendered,
r'<td class="date">\s*(12:00:42 AM|00:00:42)\s*</td>')
r'<td class="date">\s*(12:00:42\sAM|00:00:42)\s*</td>')
self.assertRegex(rendered,
r'<td class="date">\s*(Jan 3, 1970|01/03/(19)?70)\s*</td>')
self.assertRegex(rendered,
r'<td class="date">\s*(Jan 4, 1970, 12:00:44 AM|'
r'<td class="date">\s*(Jan 4, 1970, 12:00:44\sAM|'
r'01/04/(19)?70 00:00:44)\s*</td>')
self.assertRegex(rendered,
r'<td class="date">\s*<a class="timeline" href="[^"]*" '
Expand All @@ -446,11 +446,11 @@ def test_timestamp_columns(self):
self.assertRegex(rendered,
r'<td class="date">\s*<a class="timeline" href="[^"]*" '
r'title="See timeline [^"]+ ago">on (Sep 13, 2021|09/13/21) '
r'at (12:13:16 PM|12:13:16)</a>\s*</td>')
r'at (12:13:16\sPM|12:13:16)</a>\s*</td>')
self.assertRegex(rendered,
r'<td class="date">\s*<a class="timeline" href="[^"]*" '
r'title="See timeline [^"]+ ago">on (Sep 12, 2021|09/12/21) '
r'at (12:13:15 PM|12:13:15)</a>\s*</td>')
r'at (12:13:15\sPM|12:13:15)</a>\s*</td>')


class ExecuteReportTestCase(unittest.TestCase):
Expand Down
92 changes: 62 additions & 30 deletions trac/util/datefmt.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
from babel import Locale
from babel.core import LOCALE_ALIASES, UnknownLocaleError
from babel.dates import (
DateTimeFormat,
format_datetime as babel_format_datetime,
format_date as babel_format_date,
format_time as babel_format_time,
Expand All @@ -44,8 +45,9 @@
)
# 'context' parameter was added in Babel 2.3.1
if 'context' in inspect.signature(babel_get_period_names).parameters:
def get_period_names(locale=None):
return babel_get_period_names(context='format', locale=locale)
def get_period_names(width='wide', locale=None):
return babel_get_period_names(width=width, context='format',
locale=locale)
else:
get_period_names = babel_get_period_names

Expand Down Expand Up @@ -292,16 +294,40 @@ def _format_datetime(t, format, tzinfo, locale, hint):
hint = _STRFTIME_HINTS[format]
format = 'medium'
if format in ('short', 'medium', 'long', 'full'):
if hint == 'datetime':
return babel_format_datetime(t, format, None, locale)
if hint == 'date':
return babel_format_date(t, format, locale)
if hint == 'time':
return babel_format_time(t, format, None, locale)
return _format_datetime_babel(t, format, locale, hint)

format = _BABEL_FORMATS[hint].get(format, format)
return _format_datetime_without_babel(t, format)

if babel:
class _DateTimeFormatFixup(DateTimeFormat):
def __getitem__(self, name):
if name.startswith(('b', 'B')):
return self.format_period('a', len(name))
else:
return super().__getitem__(name)

def _format_datetime_babel(t, format, locale, hint):
if hint in ('datetime', 'date'):
datepart = babel_format_date(t, format, locale)
if hint == 'date':
return datepart
if hint in ('datetime', 'time'):
time_format = get_time_format(format, locale)
# Use `a` period instead of `b` and `B` periods because `parse_date`
# and jQuery timepicker addon don't support the periods
if '%(b' in time_format.format or '%(B' in time_format.format:
timepart = time_format.format % _DateTimeFormatFixup(t, locale)
else:
timepart = babel_format_time(t, format, None, locale)
if hint == 'time':
return timepart
if hint == 'datetime':
return get_datetime_format(format, locale=locale) \
.replace("'", '') \
.replace('{0}', timepart) \
.replace('{1}', datepart)

def format_datetime(t=None, format='%x %X', tzinfo=None, locale=None):
"""Format the `datetime` object `t` into a `str` string
Expand Down Expand Up @@ -439,23 +465,28 @@ def get_time_format_jquery_ui(locale):
"""Get the time format for the jQuery UI timepicker addon."""
if locale == 'iso8601':
return 'HH:mm:ssZ'

t = datetime(1999, 10, 29, 23, 59, 58, tzinfo=utc)
if babel and locale:
values = {'h': 'h', 'hh': 'hh', 'H': 'H', 'HH': 'HH',
'm': 'm', 'mm': 'mm', 's': 's', 'ss': 'ss'}
f = get_time_format('medium', locale=locale).format
if '%(a)s' in f:
t = datetime(1999, 10, 29, 23, 59, 58, tzinfo=utc)
# Use `a` period instead of `b` and `B` periods, because jQuery
# timepicker addon doesn't support the periods.
tmpl = babel_format_time(t, tzinfo=utc, locale=locale)
if '23' not in tmpl:
ampm = babel_format_datetime(t, 'a', None, locale)
values['a'] = 'TT' if ampm[0].isupper() else 'tt'
ampm = 'TT' if ampm[0].isupper() else 'tt'
values.update((period * n, ampm) for period in ('a', 'b', 'B')
for n in range(1, 6))
f = get_time_format('medium', locale=locale).format
return f % values

t = datetime(1999, 10, 29, 23, 59, 58, tzinfo=utc)
tmpl = format_time(t, tzinfo=utc)
ampm = format_time(t, '%p', tzinfo=utc)
if ampm:
tmpl = tmpl.replace(ampm, 'TT' if ampm[0].isupper() else 'tt', 1)
return tmpl.replace('23', 'HH', 1).replace('11', 'hh', 1) \
.replace('59', 'mm', 1).replace('58', 'ss', 1)
else:
tmpl = format_time(t, tzinfo=utc)
ampm = format_time(t, '%p', tzinfo=utc)
if ampm:
tmpl = tmpl.replace(ampm, 'TT' if ampm[0].isupper() else 'tt', 1)
return tmpl.replace('23', 'HH', 1).replace('11', 'hh', 1) \
.replace('59', 'mm', 1).replace('58', 'ss', 1)

def get_timezone_list_jquery_ui(t=None):
"""Get timezone list for jQuery timepicker addon"""
Expand Down Expand Up @@ -701,20 +732,21 @@ def _i18n_parse_date_pattern(locale):
if name:
period_names[name.lower()] = period
else:
if formats[0].find('%(MMM)s') != -1:
for width in ('wide', 'abbreviated'):
names = get_month_names(width, locale=locale)
month_names.update((name.lower(), num)
for num, name in names.items())
if formats[0].find('%(a)s') != -1:
names = get_period_names(locale=locale)
for width in ('wide', 'abbreviated'):
names = get_month_names(width=width, locale=locale)
month_names.update((name.lower(), num)
for num, name in names.items())
names = get_period_names(width=width, locale=locale)
period_names.update((name.lower(), period)
for period, name in names.items()
if period in ('am', 'pm'))

regexp = ['[0-9]+']
regexp.extend(re.escape(name) for name in month_names)
regexp.extend(re.escape(name) for name in period_names)
regexp = []
regexp.extend(month_names)
regexp.extend(period_names)
regexp.sort(key=lambda v: len(v), reverse=True)
regexp = list(map(re.escape, regexp))
regexp.append('[0-9]+')

return {
'orders': orders,
Expand Down
84 changes: 68 additions & 16 deletions trac/util/tests/datefmt.py
Original file line number Diff line number Diff line change
Expand Up @@ -1166,15 +1166,18 @@ def test_i18n_format_datetime(self):
self.assertIn(datefmt.format_datetime(t, tzinfo=tz,
locale=locale_en),
('Aug 28, 2010 1:45:56 PM',
'Aug 28, 2010, 1:45:56 PM')) # CLDR 23
'Aug 28, 2010, 1:45:56 PM', # CLDR 23
'Aug 28, 2010, 1:45:56\u202fPM', # CLDR 42
))
en_GB = Locale.parse('en_GB')
self.assertIn(datefmt.format_datetime(t, tzinfo=tz, locale=en_GB),
('28 Aug 2010 13:45:56', # Babel < 2.2.0
'28 Aug 2010, 13:45:56')) # Babel 2.2.0
fr = Locale.parse('fr')
self.assertIn(datefmt.format_datetime(t, tzinfo=tz, locale=fr),
('28 août 2010 13:45:56', # Babel < 2.2.0
'28 ao\xfbt 2010 \xe0 13:45:56')) # Babel 2.2.0
'28 ao\xfbt 2010 \xe0 13:45:56', # Babel 2.2.0
'28 août 2010, 13:45:56')) # Babel 2.10.0
ja = Locale.parse('ja')
self.assertEqual('2010/08/28 13:45:56',
datefmt.format_datetime(t, tzinfo=tz, locale=ja))
Expand All @@ -1185,7 +1188,8 @@ def test_i18n_format_datetime(self):
zh_CN = Locale.parse('zh_CN')
self.assertIn(datefmt.format_datetime(t, tzinfo=tz, locale=zh_CN),
('2010-8-28 下午01:45:56',
'2010年8月28日 下午1:45:56'))
'2010年8月28日 下午1:45:56',
'2010年8月28日 13:45:56')) # Babel 2.10.0

def test_i18n_format_date(self):
tz = datefmt.timezone('GMT +2:00')
Expand Down Expand Up @@ -1219,9 +1223,10 @@ def test_i18n_format_time(self):
vi = Locale.parse('vi')
zh_CN = Locale.parse('zh_CN')

self.assertEqual('1:45:56 PM',
datefmt.format_time(t, tzinfo=tz,
locale=locale_en))
self.assertIn(datefmt.format_time(t, tzinfo=tz, locale=locale_en),
('1:45:56 PM',
'1:45:56\u202fPM', # CLDR 42
))
self.assertEqual('13:45:56',
datefmt.format_time(t, tzinfo=tz, locale=en_GB))
self.assertEqual('13:45:56',
Expand All @@ -1231,7 +1236,8 @@ def test_i18n_format_time(self):
self.assertEqual('13:45:56',
datefmt.format_time(t, tzinfo=tz, locale=vi))
self.assertIn(datefmt.format_time(t, tzinfo=tz, locale=zh_CN),
('下午01:45:56', '下午1:45:56'))
('下午01:45:56', '下午1:45:56',
'13:45:56')) # Babel 2.10.0

def test_i18n_datetime_hint(self):
en_GB = Locale.parse('en_GB')
Expand All @@ -1241,24 +1247,30 @@ def test_i18n_datetime_hint(self):
zh_CN = Locale.parse('zh_CN')

self.assertIn(datefmt.get_datetime_format_hint(locale_en),
('MMM d, yyyy h:mm:ss a', 'MMM d, y h:mm:ss a',
'MMM d, y, h:mm:ss a'))
('MMM d, yyyy h:mm:ss a',
'MMM d, y h:mm:ss a',
'MMM d, y, h:mm:ss a',
'MMM d, y, h:mm:ss\u202fa', # Babel 2.12.0 (CLDR 42)
))
self.assertIn(datefmt.get_datetime_format_hint(en_GB),
('d MMM yyyy HH:mm:ss',
'd MMM y HH:mm:ss',
'd MMM y, HH:mm:ss')) # Babel 2.2.0
self.assertIn(datefmt.get_datetime_format_hint(fr),
('d MMM yyyy HH:mm:ss',
'd MMM y HH:mm:ss',
"d MMM y '\xe0' HH:mm:ss")) # Babel 2.2.0
"d MMM y '\xe0' HH:mm:ss", # Babel 2.2.0
'd MMM y, HH:mm:ss')) # Babel 2.10.0
self.assertIn(datefmt.get_datetime_format_hint(ja),
('yyyy/MM/dd H:mm:ss', 'y/MM/dd H:mm:ss'))
self.assertIn(datefmt.get_datetime_format_hint(vi),
('HH:mm:ss dd-MM-yyyy',
'HH:mm:ss dd-MM-y',
'HH:mm:ss, d MMM, y')) # Babel 2.2.0
self.assertIn(datefmt.get_datetime_format_hint(zh_CN),
('yyyy-M-d ahh:mm:ss', 'y年M月d日 ah:mm:ss'))
('yyyy-M-d ahh:mm:ss',
'y年M月d日 ah:mm:ss',
'y年M月d日 HH:mm:ss')) # Babel 2.10.0

def test_i18n_date_hint(self):
en_GB = Locale.parse('en_GB')
Expand Down Expand Up @@ -1468,16 +1480,56 @@ def test_format_compatibility(self):
# Converting default format to babel's format
self.assertIn(datefmt.format_datetime(t, '%x %X', tz, locale_en),
('Aug 28, 2010 1:45:56 PM',
'Aug 28, 2010, 1:45:56 PM')) # CLDR 23
'Aug 28, 2010, 1:45:56 PM', # CLDR 23
'Aug 28, 2010, 1:45:56\u202fPM', # CLDR 42
))
self.assertEqual('Aug 28, 2010',
datefmt.format_datetime(t, '%x', tz, locale_en))
self.assertEqual('1:45:56 PM',
datefmt.format_datetime(t, '%X', tz, locale_en))
self.assertEqual('Aug 28, 2010',
self.assertEqual(datefmt.format_datetime(t, '%x', tz, locale_en),
datefmt.format_date(t, '%x', tz, locale_en))
self.assertEqual('1:45:56 PM',
self.assertIn(datefmt.format_datetime(t, '%X', tz, locale_en),
('1:45:56 PM',
'1:45:56\u202fPM', # CLDR 42
))
self.assertEqual(datefmt.format_datetime(t, '%X', tz, locale_en),
datefmt.format_time(t, '%X', tz, locale_en))

def test_format_a_period_instead_of_b_periods(self):
zh_TW = Locale.parse('zh_TW')

t = datetime.datetime(2024, 4, 18, 23, 45, 56, 123456, datefmt.utc)
self.assertEqual(
'2024年4月18日',
datefmt.format_date(t, tzinfo=datefmt.utc, locale=zh_TW))
self.assertEqual(
'下午11:45:56',
datefmt.format_time(t, tzinfo=datefmt.utc, locale=zh_TW))
self.assertEqual(
'2024年4月18日 下午11:45:56',
datefmt.format_datetime(t, tzinfo=datefmt.utc, locale=zh_TW))

t = datetime.datetime(2024, 4, 19, 1, 45, 56, 123456, datefmt.utc)
self.assertEqual(
'2024年4月19日',
datefmt.format_date(t, tzinfo=datefmt.utc, locale=zh_TW))
self.assertEqual(
'上午1:45:56',
datefmt.format_time(t, tzinfo=datefmt.utc, locale=zh_TW))
self.assertEqual(
'2024年4月19日 上午1:45:56',
datefmt.format_datetime(t, tzinfo=datefmt.utc, locale=zh_TW))

t = datetime.datetime(2024, 4, 19, 12, 45, 56, 123456, datefmt.utc)
self.assertEqual(
'2024年4月19日',
datefmt.format_date(t, tzinfo=datefmt.utc, locale=zh_TW))
self.assertEqual(
'下午12:45:56',
datefmt.format_time(t, tzinfo=datefmt.utc, locale=zh_TW))
self.assertEqual(
'2024年4月19日 下午12:45:56',
datefmt.format_datetime(t, tzinfo=datefmt.utc, locale=zh_TW))

def test_parse_invalid_date(self):
tz = datefmt.timezone('GMT +2:00')

Expand Down
32 changes: 27 additions & 5 deletions trac/web/tests/chrome.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
from trac.util import create_file
from trac.util.datefmt import pytz, timezone, utc
from trac.util.html import Markup, tag
from trac.util.translation import has_babel
from trac.util.translation import get_available_locales, has_babel
from trac.web.api import IRequestHandler
from trac.web.chrome import (
Chrome, INavigationContributor, add_link, add_meta, add_notice,
Expand Down Expand Up @@ -288,6 +288,28 @@ def test_add_jquery_ui_default_format(self):
data = self._get_jquery_ui_script_data(locale_en)
self.assertIsNone(data['timezone_list'])

@unittest.skipUnless(has_babel, 'Babel unavailable')
def test_add_jquery_ui_time_format(self):
from babel.core import Locale
data = self._get_jquery_ui_script_data(locale_en)
self.assertEqual(True, data['ampm'])
self.assertIn(data['time_format'], ('h:mm:ss TT', 'h:mm:ss\u202fTT'))
locale_ja = Locale.parse('ja')
data = self._get_jquery_ui_script_data(locale_ja)
self.assertEqual(False, data['ampm'])
self.assertEqual('H:mm:ss', data['time_format'])
locale_zh_tw = Locale.parse('zh_TW')
data = self._get_jquery_ui_script_data(locale_zh_tw)
self.assertEqual(True, data['ampm'])
self.assertEqual('tth:mm:ss', data['time_format'])

@unittest.skipUnless(has_babel, 'Babel unavailable')
def test_add_jquery_ui_available_locales(self):
from babel.core import Locale
for locale in get_available_locales():
locale = Locale.parse(locale)
data = self._get_jquery_ui_script_data(locale)

def test_invalid_default_dateinfo_format_raises_exception(self):
self.env.config.set('trac', 'default_dateinfo_format', 'ābšolute')

Expand Down Expand Up @@ -1148,12 +1170,12 @@ def test_pretty_dateinfo(self):
<body>
<ul>
<li></li>
<li><span title="Jul 1, 2007, 12:34:56 PM">[0-9]+ years ago</span></li>
<li><span title="Jul 1, 2007, 12:34:56 PM">[0-9]+ years ago</span></li>
<li><span title="Jul 1, 2007, 12:34:56\\sPM">[0-9]+ years ago</span></li>
<li><span title="Jul 1, 2007, 12:34:56\\sPM">[0-9]+ years ago</span></li>
<li><span title="[0-9]+ years ago">on Jul 1, 2007</span></li>
<li><span title="[0-9]+ years ago">on Jul 1, 2007 at 12:34:56 PM</span></li>
<li><span title="[0-9]+ years ago">on Jul 1, 2007 at 12:34:56\\sPM</span></li>
<li><span title="[0-9]+ years ago">Jul 1, 2007</span></li>
<li><span title="[0-9]+ years ago">Jul 1, 2007, 12:34:56 PM</span></li>
<li><span title="[0-9]+ years ago">Jul 1, 2007, 12:34:56\\sPM</span></li>
</ul>
</body>
</html>"""), content)
Expand Down

0 comments on commit a30c5f8

Please sign in to comment.