|
10 | 10 | # This software consists of voluntary contributions made by many
|
11 | 11 | # individuals. For the exact contribution history, see the revision
|
12 | 12 | # history and logs, available at https://trac.edgewall.org/log/.
|
| 13 | +import itertools |
13 | 14 | import textwrap
|
14 | 15 | import unittest
|
15 | 16 | from datetime import datetime, timedelta
|
|
22 | 23 | from trac.timeline.web_ui import TimelineModule
|
23 | 24 | from trac.util.datefmt import (
|
24 | 25 | datetime_now, format_date, format_datetime, format_time,
|
25 |
| - get_date_format_hint, pretty_timedelta, utc, |
| 26 | + get_date_format_hint, get_timezone, pretty_timedelta, pytz, utc, |
26 | 27 | )
|
27 | 28 | from trac.util.html import plaintext, tag
|
28 | 29 | from trac.web.chrome import Chrome
|
@@ -265,13 +266,12 @@ def render(context, field, event):
|
265 | 266 | if field == 'description':
|
266 | 267 | return tag(tag.h1('Title 2nd'), tag.p('body & < >'))
|
267 | 268 |
|
268 |
| - provider = self._get_event_provider('normal') |
269 |
| - provider._events = [ |
| 269 | + self._set_events([ |
270 | 270 | ('test&1', datetime(2018, 4, 27, 12, 34, 56, 123456, utc),
|
271 | 271 | 'jo&hn', Mock(render=render)),
|
272 | 272 | ('test&2', datetime(2018, 3, 19, 23, 56, 12, 987654, utc),
|
273 | 273 | 'Joe <[email protected]>', Mock( render=render)),
|
274 |
| - ] |
| 274 | + ]) |
275 | 275 | req = MockRequest(self.env, path_info='/timeline', tz=utc,
|
276 | 276 | args={'format': 'rss', 'daysback': '90',
|
277 | 277 | 'from': '2018-04-30T00:00:00Z'})
|
@@ -311,12 +311,11 @@ def render(context, field, event):
|
311 | 311 | minidom.parseString(output) # verify valid xml
|
312 | 312 |
|
313 | 313 | def test_daysback(self):
|
314 |
| - provider = self._get_event_provider('normal') |
315 | 314 | base_datetime = datetime(2025, 2, 20, 12, tzinfo=utc)
|
316 |
| - provider._events = [ |
| 315 | + self._set_events([ |
317 | 316 | ('test', base_datetime - timedelta(days=days), 'trac', None)
|
318 | 317 | for days in range(-30, 120)
|
319 |
| - ] |
| 318 | + ]) |
320 | 319 |
|
321 | 320 | def render_and_get_events(daysback):
|
322 | 321 | req = MockRequest(self.env, path_info='/timeline', tz=utc,
|
@@ -348,8 +347,86 @@ def get_datetimes(events):
|
348 | 347 | get_datetimes(events)[-1])
|
349 | 348 | self.assertEqual(91, len(events))
|
350 | 349 |
|
351 |
| - def _get_event_provider(self, name): |
352 |
| - return self.timeline_event_providers[name](self.env) |
| 350 | + @unittest.skipUnless(pytz, 'pytz unavailable') |
| 351 | + def test_forward_across_dst(self): |
| 352 | + tz = get_timezone('Europe/Berlin') # DST start at 2025-03-30 02:00 |
| 353 | + base = datetime(2025, 3, 30, 12, 30, tzinfo=utc) |
| 354 | + self._set_events([ |
| 355 | + ('test', base - timedelta(hours=hours), 'trac', None) |
| 356 | + for hours in range(-48, 48) |
| 357 | + ]) |
| 358 | + req = MockRequest(self.env, path_info='/timeline', tz=tz, |
| 359 | + args={'from': '2025-03-31T12:34:56Z', |
| 360 | + 'daysback': '2'}) |
| 361 | + rv = self._process_request(req) |
| 362 | + events = rv[1]['events'] |
| 363 | + grouped_events = { |
| 364 | + str(key): list(map(str, sorted(e['datetime'] for e in g))) |
| 365 | + for key, g in itertools.groupby(events, key=lambda e: e['date']) |
| 366 | + } |
| 367 | + self.assertEqual( |
| 368 | + ['2025-03-30 00:30:00+01:00', '2025-03-30 01:30:00+01:00', |
| 369 | + '2025-03-30 03:30:00+02:00', '2025-03-30 04:30:00+02:00', |
| 370 | + '2025-03-30 05:30:00+02:00', '2025-03-30 06:30:00+02:00', |
| 371 | + '2025-03-30 07:30:00+02:00', '2025-03-30 08:30:00+02:00', |
| 372 | + '2025-03-30 09:30:00+02:00', '2025-03-30 10:30:00+02:00', |
| 373 | + '2025-03-30 11:30:00+02:00', '2025-03-30 12:30:00+02:00', |
| 374 | + '2025-03-30 13:30:00+02:00', '2025-03-30 14:30:00+02:00', |
| 375 | + '2025-03-30 15:30:00+02:00', '2025-03-30 16:30:00+02:00', |
| 376 | + '2025-03-30 17:30:00+02:00', '2025-03-30 18:30:00+02:00', |
| 377 | + '2025-03-30 19:30:00+02:00', '2025-03-30 20:30:00+02:00', |
| 378 | + '2025-03-30 21:30:00+02:00', '2025-03-30 22:30:00+02:00', |
| 379 | + '2025-03-30 23:30:00+02:00'], |
| 380 | + grouped_events['2025-03-30 00:00:00+01:00']) |
| 381 | + self.assertEqual( |
| 382 | + ['2025-03-29 00:00:00+01:00', '2025-03-30 00:00:00+01:00', |
| 383 | + '2025-03-31 00:00:00+02:00'], sorted(grouped_events)) |
| 384 | + self.assertEqual(24, len(grouped_events['2025-03-29 00:00:00+01:00'])) |
| 385 | + self.assertEqual(23, len(grouped_events['2025-03-30 00:00:00+01:00'])) |
| 386 | + self.assertEqual(24, len(grouped_events['2025-03-31 00:00:00+02:00'])) |
| 387 | + |
| 388 | + @unittest.skipUnless(pytz, 'pytz unavailable') |
| 389 | + def test_back_across_dst(self): |
| 390 | + tz = get_timezone('Europe/Berlin') # DST end at 2024-10-27 03:00 |
| 391 | + base = datetime(2024, 10, 27, 12, 30, tzinfo=utc) |
| 392 | + self._set_events([ |
| 393 | + ('test', base - timedelta(hours=hours), 'trac', None) |
| 394 | + for hours in range(-48, 48) |
| 395 | + ]) |
| 396 | + req = MockRequest(self.env, path_info='/timeline', tz=tz, |
| 397 | + args={'from': '2024-10-28T12:34:56Z', |
| 398 | + 'daysback': '2'}) |
| 399 | + rv = self._process_request(req) |
| 400 | + events = rv[1]['events'] |
| 401 | + grouped_events = { |
| 402 | + str(key): list(map(str, sorted(e['datetime'] for e in g))) |
| 403 | + for key, g in itertools.groupby(events, key=lambda e: e['date']) |
| 404 | + } |
| 405 | + self.assertEqual( |
| 406 | + ['2024-10-27 00:30:00+02:00', '2024-10-27 01:30:00+02:00', |
| 407 | + '2024-10-27 02:30:00+02:00', '2024-10-27 02:30:00+01:00', |
| 408 | + '2024-10-27 03:30:00+01:00', '2024-10-27 04:30:00+01:00', |
| 409 | + '2024-10-27 05:30:00+01:00', '2024-10-27 06:30:00+01:00', |
| 410 | + '2024-10-27 07:30:00+01:00', '2024-10-27 08:30:00+01:00', |
| 411 | + '2024-10-27 09:30:00+01:00', '2024-10-27 10:30:00+01:00', |
| 412 | + '2024-10-27 11:30:00+01:00', '2024-10-27 12:30:00+01:00', |
| 413 | + '2024-10-27 13:30:00+01:00', '2024-10-27 14:30:00+01:00', |
| 414 | + '2024-10-27 15:30:00+01:00', '2024-10-27 16:30:00+01:00', |
| 415 | + '2024-10-27 17:30:00+01:00', '2024-10-27 18:30:00+01:00', |
| 416 | + '2024-10-27 19:30:00+01:00', '2024-10-27 20:30:00+01:00', |
| 417 | + '2024-10-27 21:30:00+01:00', '2024-10-27 22:30:00+01:00', |
| 418 | + '2024-10-27 23:30:00+01:00'], |
| 419 | + grouped_events['2024-10-27 00:00:00+02:00']) |
| 420 | + self.assertEqual( |
| 421 | + ['2024-10-26 00:00:00+02:00', '2024-10-27 00:00:00+02:00', |
| 422 | + '2024-10-28 00:00:00+01:00'], sorted(grouped_events)) |
| 423 | + self.assertEqual(24, len(grouped_events['2024-10-26 00:00:00+02:00'])) |
| 424 | + self.assertEqual(25, len(grouped_events['2024-10-27 00:00:00+02:00'])) |
| 425 | + self.assertEqual(24, len(grouped_events['2024-10-28 00:00:00+01:00'])) |
| 426 | + |
| 427 | + def _set_events(self, events): |
| 428 | + provider = self.timeline_event_providers['normal'](self.env) |
| 429 | + provider._events = tuple(events) |
353 | 430 |
|
354 | 431 | def _process_request(self, req):
|
355 | 432 | mod = TimelineModule(self.env)
|
|
0 commit comments