Skip to content

Commit 2bdf78b

Browse files
Preparation for v0.7. Corrected a bug where datetime.max.time() resulted in incorrect date/time. Changed tests to compare time_override models via string to prevent future regressions. Added choices GROUPED_ALL_TIMEZONES_CHOICES and GROUPED_COMMON_TIMEZONES_CHOICES.
1 parent fd920d6 commit 2bdf78b

File tree

8 files changed

+144
-28
lines changed

8 files changed

+144
-28
lines changed

README.rst

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ welcomed and appreciated.
4040
Documentation
4141
-------------
4242

43-
Documentation for django-timezone-utils is available at `Read the Docs <https://django-timezone-utils.readthedocs.org/>`_
43+
Documentation for django-timezone-utils is available at `Read the Docs <https://django-timezone-utils.readthedocs.org/>`_.
4444

4545
Inspiration
4646
-----------
@@ -107,6 +107,7 @@ Contributors
107107

108108
Changelog
109109
---------
110+
- 0.7 Corrected a bug where datetime.max.time() resulted in incorrect date/time. Changed tests to compare time_override models via string to prevent future regressions. Added choices ``GROUPED_ALL_TIMEZONES_CHOICES`` and ``GROUPED_COMMON_TIMEZONES_CHOICES``.
110111
- 0.6 Added RTD documentation. LinkedTZDateTimeField now returns the datetime object in the overidden timezone and time.
111112
- 0.5 Bug fix: time override on datetime.min.time() failed to set time properly
112113
- 0.4 Removed support for Python 2.5

docs/conf.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,9 @@
5151
# built documents.
5252
#
5353
# The short X.Y version.
54-
version = '0.6'
54+
version = '0.7'
5555
# The full version, including alpha/beta/rc tags.
56-
release = '0.6'
56+
release = '0.7'
5757

5858
# The language for content autogenerated by Sphinx. Refer to documentation
5959
# for a list of supported languages.

tests/models.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,3 +164,20 @@ class ModelWithLocalTZCharField(models.Model):
164164
default=settings.TEST_DATETIME,
165165
populate_from='timezone'
166166
)
167+
168+
169+
class TZTimeFramedModel(models.Model):
170+
other_model = models.ForeignKey(
171+
to='tests.TZWithGoodStringDefault',
172+
related_name='fk_to_tz_too'
173+
)
174+
start = LinkedTZDateTimeField(
175+
default=settings.TEST_DATETIME,
176+
populate_from=get_other_model_timezone,
177+
time_override=datetime.min.time()
178+
)
179+
end = LinkedTZDateTimeField(
180+
default=settings.TEST_DATETIME,
181+
populate_from=get_other_model_timezone,
182+
time_override=datetime.max.time()
183+
)

tests/test_choices.py

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,16 @@
55
from datetime import datetime
66
from operator import itemgetter
77
import pytz
8+
import re
89

910
# Django
1011
from django.test import TestCase
1112

1213
# App
1314
from timezone_utils.choices import (ALL_TIMEZONES_CHOICES,
1415
COMMON_TIMEZONES_CHOICES,
16+
GROUPED_ALL_TIMEZONES_CHOICES,
17+
GROUPED_COMMON_TIMEZONES_CHOICES,
1518
PRETTY_ALL_TIMEZONES_CHOICES,
1619
PRETTY_COMMON_TIMEZONES_CHOICES,
1720
TIMEZONE_OFFSET_REGEX, get_choices)
@@ -113,7 +116,51 @@ def test_PRETTY_COMMON_TIMEZONES_CHOICES_values(self):
113116
)
114117
)
115118

116-
def test_get_choices_values(self):
119+
def test_GROUPED_ALL_TIMEZONES_CHOICES_group_name(self):
120+
group_name_re = re.compile(r'GMT(\+|-)\d{2}:\d{2}')
121+
for group_name in map(lambda x: x[0], GROUPED_ALL_TIMEZONES_CHOICES):
122+
self.assertNotEqual(
123+
group_name_re.match(group_name),
124+
None
125+
)
126+
127+
def test_GROUPED_COMMON_TIMEZONES_CHOICES_group_name(self):
128+
group_name_re = re.compile(r'GMT(\+|-)\d{2}:\d{2}')
129+
for group_name in map(lambda x: x[0], GROUPED_COMMON_TIMEZONES_CHOICES):
130+
self.assertNotEqual(
131+
group_name_re.match(group_name),
132+
None
133+
)
134+
135+
def test_GROUPED_ALL_TIMEZONES_CHOICES_values(self):
136+
for name, display in map(
137+
lambda v: v[0],
138+
map(lambda x: x[1], GROUPED_ALL_TIMEZONES_CHOICES)
139+
):
140+
self.assertIn(
141+
name,
142+
pytz.all_timezones
143+
)
144+
self.assertIn(
145+
display,
146+
pytz.all_timezones
147+
)
148+
149+
def test_GROUPED_COMMON_TIMEZONES_CHOICES_values(self):
150+
for name, display in map(
151+
lambda v: v[0],
152+
map(lambda x: x[1], GROUPED_COMMON_TIMEZONES_CHOICES)
153+
):
154+
self.assertIn(
155+
name,
156+
pytz.common_timezones
157+
)
158+
self.assertIn(
159+
display,
160+
pytz.common_timezones
161+
)
162+
163+
def test_get_choices_values_ungrouped(self):
117164
choices = get_choices(pytz.common_timezones)
118165
values = map(itemgetter(0), choices)
119166
for value in values:

tests/test_valid_tzdatetimefield.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
from .models import (ModelWithDateTimeOnly, CallableTimeStampedModel,
1616
StaticTimeStampedModel, ModelWithForeignKeyToTimeZone,
1717
NullModelWithDateTimeOnly, ModelWithLocalTimeZone,
18-
ModelWithLocalTZCharField)
18+
ModelWithLocalTZCharField, TZTimeFramedModel)
1919

2020

2121
# ==============================================================================
@@ -31,6 +31,11 @@ def setUp(self):
3131

3232
location = TZWithGoodStringDefault.objects.create()
3333
ModelWithForeignKeyToTimeZone.objects.create(other_model=location)
34+
TZTimeFramedModel.objects.create(
35+
start=make_aware(datetime(2014, 1, 1), pytz.timezone('US/Eastern')),
36+
end=make_aware(datetime(2014, 12, 31), pytz.timezone('US/Eastern')),
37+
other_model=location
38+
)
3439

3540
def test_that_model_timestamp_is_unaltered(self):
3641
"""Make sure that we aren't modifying the timezone if one is not
@@ -127,3 +132,14 @@ def test_to_python_conversion(self):
127132
str(model_instance.timestamp),
128133
'2013-12-31 19:00:00-05:00'
129134
)
135+
136+
def test_full_overrides(self):
137+
model_instance = TZTimeFramedModel.objects.get()
138+
self.assertEqual(
139+
str(model_instance.start),
140+
'2014-01-01 00:00:00-05:00'
141+
)
142+
self.assertEqual(
143+
str(model_instance.end),
144+
'2014-12-31 23:59:59.999999-05:00'
145+
)

timezone_utils/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
__version__ = (0, 6)
1+
__version__ = (0, 7)
22
VERSION = '.'.join(map(str, __version__))

timezone_utils/choices.py

Lines changed: 51 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,14 @@
22
# IMPORTS
33
# ==============================================================================
44
# Python
5+
from collections import defaultdict, namedtuple
56
from datetime import datetime
7+
from operator import attrgetter
68
import pytz
79
import re
810

911
__all__ = ('get_choices', 'ALL_TIMEZONES_CHOICES', 'COMMON_TIMEZONES_CHOICES',
12+
'GROUPED_ALL_TIMEZONES_CHOICES', 'GROUPED_COMMON_TIMEZONES_CHOICES',
1013
'PRETTY_ALL_TIMEZONES_CHOICES', 'PRETTY_COMMON_TIMEZONES_CHOICES')
1114

1215

@@ -19,9 +22,13 @@
1922
)
2023

2124

22-
def get_choices(timezones):
25+
def get_choices(timezones, grouped=False):
2326
"""Retrieves timezone choices from any iterable (normally pytz)."""
24-
timezone_choices = []
27+
28+
# Created a namedtuple to store the "key" for the choices_dict
29+
TZOffset = namedtuple('TZOffset', 'value offset_string')
30+
31+
choices_dict = defaultdict(list)
2532

2633
# Iterate through the timezones and populate the timezone choices
2734
for tz in iter(timezones):
@@ -31,27 +38,43 @@ def get_choices(timezones):
3138
# Retrieve the timezone offset ("-0500" / "+0500")
3239
offset = now.strftime("%z")
3340

34-
# Format the timezone display string
35-
display_string = '(GMT{plus_minus}{hours}:{minutes}) {tz}'.format(
36-
tz=tz,
41+
# Retrieve the offset string ("GMT-12:00" / "GMT+12:00")
42+
timezone_offset_string = 'GMT{plus_minus}{hours}:{minutes}'.format(
3743
**TIMEZONE_OFFSET_REGEX.match(offset).groupdict()
3844
)
3945

40-
# Append a tuple of the timezone information:
41-
# (-500, 'US/Eastern', '(GMT-05:00) US/Eastern')
42-
timezone_choices.append((int(offset), tz, display_string))
46+
if not grouped:
47+
# Format the timezone display string
48+
display_string = '({timezone_offset_string}) {tz}'.format(
49+
timezone_offset_string=timezone_offset_string,
50+
tz=tz,
51+
)
52+
else:
53+
display_string = tz
54+
55+
choices_dict[
56+
TZOffset(value=int(offset), offset_string=timezone_offset_string)
57+
].append(
58+
(tz, display_string)
59+
)
4360

44-
# Sort the timezone choices by the integer offsets (negative to positive)
45-
timezone_choices.sort()
61+
choices = []
4662

47-
# Iterate through the timezone choices by index, and update the index to
48-
# remove the integer offset, leaving the:
49-
# ('US/Eastern', '(GMT-05:00) US/Eastern')
50-
for i in range(len(timezone_choices)):
51-
timezone_choices[i] = timezone_choices[i][1:]
63+
for tz_offset in sorted(choices_dict, key=attrgetter('value')):
64+
if not grouped:
65+
choices.extend(
66+
tuple(choices_dict[tz_offset])
67+
)
68+
else:
69+
choices.append(
70+
(
71+
tz_offset.offset_string,
72+
tuple(choices_dict[tz_offset])
73+
)
74+
)
5275

5376
# Cast the timezone choices to a tuple and return
54-
return tuple(timezone_choices)
77+
return tuple(choices)
5578

5679

5780
# ==============================================================================
@@ -63,6 +86,16 @@ def get_choices(timezones):
6386
zip(pytz.common_timezones, pytz.common_timezones)
6487
)
6588

89+
# Grouped by timezone offset, with "GMT-05:00" as the group name
90+
GROUPED_ALL_TIMEZONES_CHOICES = get_choices(
91+
timezones=pytz.all_timezones,
92+
grouped=True
93+
)
94+
GROUPED_COMMON_TIMEZONES_CHOICES = get_choices(
95+
timezones=pytz.common_timezones,
96+
grouped=True
97+
)
98+
6699
# Sorted by timezone offset, with "(GMT-05:00) US/Eastern" as the display name
67-
PRETTY_ALL_TIMEZONES_CHOICES = get_choices(pytz.all_timezones)
68-
PRETTY_COMMON_TIMEZONES_CHOICES = get_choices(pytz.common_timezones)
100+
PRETTY_ALL_TIMEZONES_CHOICES = get_choices(timezones=pytz.all_timezones)
101+
PRETTY_COMMON_TIMEZONES_CHOICES = get_choices(timezones=pytz.common_timezones)

timezone_utils/fields.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -308,22 +308,24 @@ def _convert_value(self, value, model_instance, add):
308308
# Retrieve the default timezone as the default
309309
tz = get_default_timezone()
310310

311+
# If populate_from exists, override the default timezone
312+
if self.populate_from is not None:
313+
tz = self._get_populate_from(model_instance)
314+
311315
# Do not convert the time to the time override if auto_now or
312316
# auto_now_add is set
313317
if self.time_override is not None and not (
314318
self.auto_now or (self.auto_now_add and add)
315319
):
320+
# Retrieve the time override
316321
time_override = self._get_time_override()
317322

323+
# Convert the value to the date/time
318324
value = datetime.combine(
319325
date=value.date(),
320326
time=time_override
321327
)
322328

323-
# If populate_from exists, override the default timezone
324-
if self.populate_from is not None:
325-
tz = self._get_populate_from(model_instance)
326-
327329
# If the value is naive (due to the time_override conversion), make it
328330
# aware based on the appropriate timezone
329331
if is_naive(value):

0 commit comments

Comments
 (0)