Skip to content

Commit c31f114

Browse files
LinkedTZDateTimeField.to_python now casts to the populate_from timezone. Code cleanup. PEP 257 conformance. Updated tests.
1 parent 4a95ce8 commit c31f114

File tree

3 files changed

+147
-73
lines changed

3 files changed

+147
-73
lines changed

tests/models.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ class CallableTimeStampedModel(models.Model):
120120
)
121121
end = LinkedTZDateTimeField(
122122
default=settings.TEST_DATETIME,
123-
time_override=datetime.max.time
123+
time_override=datetime.max.time()
124124
)
125125

126126

@@ -135,14 +135,18 @@ class StaticTimeStampedModel(models.Model):
135135
)
136136

137137

138+
def get_other_model_timezone(obj):
139+
return obj.other_model.timezone
140+
141+
138142
class ModelWithForeignKeyToTimeZone(models.Model):
139143
other_model = models.ForeignKey(
140144
to='tests.TZWithGoodStringDefault',
141145
related_name='fk_to_tz'
142146
)
143147
timestamp = LinkedTZDateTimeField(
144148
default=settings.TEST_DATETIME,
145-
populate_from=lambda instance: instance.other_model.timezone
149+
populate_from=get_other_model_timezone,
146150
)
147151

148152

tests/test_valid_tzdatetimefield.py

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,10 @@
1515

1616
# App
1717
from tests.models import TZWithGoodStringDefault
18-
from .models import (ModelWithDateTimeOnly,
19-
CallableTimeStampedModel,
20-
StaticTimeStampedModel,
21-
ModelWithForeignKeyToTimeZone,
22-
NullModelWithDateTimeOnly,
23-
ModelWithLocalTimeZone,
24-
ModelWithLocalTZCharField)
18+
from .models import (ModelWithDateTimeOnly, CallableTimeStampedModel,
19+
StaticTimeStampedModel, ModelWithForeignKeyToTimeZone,
20+
NullModelWithDateTimeOnly, ModelWithLocalTimeZone,
21+
ModelWithLocalTZCharField)
2522

2623

2724
# ==============================================================================
@@ -108,3 +105,20 @@ def test_populate_from_local_timezone_charfield(self):
108105
model_instance.timestamp,
109106
settings.TEST_DATETIME
110107
)
108+
109+
def test_to_python_conversion(self):
110+
model_instance = ModelWithForeignKeyToTimeZone.objects.get()
111+
self.assertEqual(
112+
model_instance.timestamp,
113+
settings.TEST_DATETIME
114+
)
115+
self.assertEqual(
116+
model_instance.timestamp.tzinfo,
117+
pytz.timezone('US/Eastern').normalize(
118+
model_instance.timestamp
119+
).tzinfo
120+
)
121+
self.assertEqual(
122+
str(model_instance.timestamp),
123+
'2013-12-31 19:00:00-05:00'
124+
)

timezone_utils/fields.py

Lines changed: 120 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,13 @@
99
# Django
1010
try:
1111
from django.core import checks
12-
except ImportError:
12+
except ImportError: # pragma: no cover
1313
pass
1414
from django.core.exceptions import ValidationError
1515
from django.db.models import SubfieldBase
1616
from django.db.models.fields import DateTimeField, CharField
1717
from django.utils.six import with_metaclass
18-
from django.utils.timezone import get_default_timezone
18+
from django.utils.timezone import get_default_timezone, is_naive, make_aware
1919
from django.utils.translation import ugettext_lazy as _
2020

2121
# App
@@ -47,11 +47,15 @@ def __init__(self, *args, **kwargs):
4747
super(TimeZoneField, self).__init__(*args, **kwargs)
4848

4949
def get_prep_value(self, value):
50+
"""Converts timezone instances to strings for db storage."""
51+
5052
if isinstance(value, tzinfo):
5153
return value.zone
5254
return value
5355

5456
def to_python(self, value):
57+
"""Returns a datetime.tzinfo instance for the value."""
58+
5559
value = super(TimeZoneField, self).to_python(value)
5660

5761
if not value:
@@ -67,6 +71,8 @@ def to_python(self, value):
6771
)
6872

6973
def formfield(self, **kwargs):
74+
"""Returns a custom form field for the TimeZoneField."""
75+
7076
defaults = {'form_class': forms.TimeZoneField}
7177
defaults.update(**kwargs)
7278
return super(TimeZoneField, self).formfield(**defaults)
@@ -75,16 +81,19 @@ def formfield(self, **kwargs):
7581
# Django >= 1.7 Checks Framework
7682
# --------------------------------------------------------------------------
7783
def check(self, **kwargs): # pragma: no cover
84+
"""Calls the TimeZoneField's custom checks."""
85+
7886
errors = super(TimeZoneField, self).check(**kwargs)
7987
errors.extend(self._check_timezone_max_length_attribute())
8088
errors.extend(self._check_choices_attribute())
8189
return errors
8290

8391
def _check_timezone_max_length_attribute(self): # pragma: no cover
84-
"""Custom check() method that verifies that the `max_length` attribute
85-
covers all possible pytz timezone lengths.
86-
8792
"""
93+
Checks that the `max_length` attribute covers all possible pytz timezone
94+
lengths.
95+
"""
96+
8897
# Retrieve the maximum possible length for the time zone string
8998
possible_max_length = max(map(len, pytz.all_timezones))
9099

@@ -114,6 +123,8 @@ def _check_timezone_max_length_attribute(self): # pragma: no cover
114123
return []
115124

116125
def _check_choices_attribute(self): # pragma: no cover
126+
"""Checks to make sure that choices contains valid timezone choices."""
127+
117128
if self.choices:
118129
warning_params = {
119130
'msg': (
@@ -171,10 +182,26 @@ class LinkedTZDateTimeField(with_metaclass(SubfieldBase, DateTimeField)):
171182
def __init__(self, *args, **kwargs):
172183
self.populate_from = kwargs.pop('populate_from', None)
173184
self.time_override = kwargs.pop('time_override', None)
185+
self.timezone = get_default_timezone()
174186

175187
super(LinkedTZDateTimeField, self).__init__(*args, **kwargs)
176188

189+
def to_python(self, value):
190+
"""Convert the value to the appropriate timezone."""
191+
192+
value = super(LinkedTZDateTimeField, self).to_python(value)
193+
194+
if not value:
195+
return value
196+
197+
return value.astimezone(self.timezone)
198+
177199
def pre_save(self, model_instance, add):
200+
"""
201+
Converts the value being saved based on `populate_from` and
202+
`time_override`
203+
"""
204+
178205
# Retrieve the currently entered datetime
179206
value = super(
180207
LinkedTZDateTimeField,
@@ -184,80 +211,109 @@ def pre_save(self, model_instance, add):
184211
add=add
185212
)
186213

187-
if not value:
188-
return value
189-
190-
# Retrieve the default timezone
191-
tz = get_default_timezone()
192-
193-
if self.populate_from:
194-
if hasattr(self.populate_from, '__call__'):
195-
# LinkedTZDateTimeField(
196-
# populate_from=lambda instance: instance.field.timezone
197-
# )
198-
tz = self.populate_from(model_instance)
199-
else:
200-
# LinkedTZDateTimeField(populate_from='field')
201-
from_attr = getattr(model_instance, self.populate_from)
202-
tz = callable(from_attr) and from_attr() or from_attr
203-
204-
try:
205-
tz = pytz.timezone(str(tz))
206-
except pytz.UnknownTimeZoneError:
207-
# It was a valiant effort. Resistance is futile.
208-
raise
209-
210-
# We don't want to double-convert the value. This leads to incorrect
211-
# dates being generated when the overridden time goes back a day.
212-
if self.time_override is None:
213-
datetime_as_timezone = value.astimezone(tz)
214-
value = tz.normalize(
215-
tz.localize(
216-
datetime.combine(
217-
date=datetime_as_timezone.date(),
218-
time=datetime_as_timezone.time()
219-
)
220-
)
221-
)
222-
223-
if self.time_override is not None and not (
224-
self.auto_now or (self.auto_now_add and add)
225-
):
226-
if callable(self.time_override):
227-
time_override = self.time_override()
228-
else:
229-
time_override = self.time_override
230-
231-
if not isinstance(time_override, datetime_time):
232-
raise ValueError(
233-
'Invalid type. Must be a datetime.time instance.'
234-
)
235-
236-
value = tz.normalize(
237-
tz.localize(
238-
datetime.combine(
239-
date=value.date(),
240-
time=time_override,
241-
)
242-
)
243-
)
214+
# Convert the value to the correct time/timezone
215+
value = self._convert_value(
216+
value=value,
217+
model_instance=model_instance,
218+
add=add
219+
)
244220

245221
setattr(model_instance, self.attname, value)
246-
setattr(model_instance, '_timezone', tz)
247222

248223
return value
249224

250225
def deconstruct(self): # pragma: no cover
226+
"""Add our custom keyword arguments for migrations."""
227+
251228
name, path, args, kwargs = super(
252229
LinkedTZDateTimeField,
253230
self
254231
).deconstruct()
255232

256233
# Only include kwarg if it's not the default
257234
if self.populate_from is not None:
235+
# Since populate_from requires a model instance and Django does not,
236+
# allow lambda, we hope that we have been provided a function that
237+
# can be parsed
258238
kwargs['populate_from'] = self.populate_from
259239

240+
# Only include kwarg if it's not the default
260241
if self.time_override is not None:
261-
kwargs['time_override'] = self.time_override
242+
if hasattr(self.time_override, '__call__'):
243+
# Call the callable datetime.time instance
244+
kwargs['time_override'] = self.time_override()
245+
else:
246+
kwargs['time_override'] = self.time_override
262247

263248
return name, path, args, kwargs
249+
250+
def _get_populate_from(self, model_instance):
251+
"""Retrieves the timezone or None from the `populate_from` attribute."""
252+
253+
if hasattr(self.populate_from, '__call__'):
254+
tz = self.populate_from(model_instance)
255+
else:
256+
from_attr = getattr(model_instance, self.populate_from)
257+
tz = callable(from_attr) and from_attr() or from_attr
258+
259+
try:
260+
tz = pytz.timezone(str(tz))
261+
except pytz.UnknownTimeZoneError:
262+
# It was a valiant effort. Resistance is futile.
263+
raise
264+
265+
# If we have a timezone, set the instance's timezone attribute
266+
self.timezone = tz
267+
268+
return tz
269+
270+
def _get_time_override(self):
271+
"""
272+
Retrieves the datetime.time or None from the `time_override` attribute.
273+
"""
274+
275+
if callable(self.time_override):
276+
time_override = self.time_override()
277+
else:
278+
time_override = self.time_override
279+
280+
if not isinstance(time_override, datetime_time):
281+
raise ValueError(
282+
'Invalid type. Must be a datetime.time instance.'
283+
)
284+
285+
return time_override
286+
287+
def _convert_value(self, value, model_instance, add):
288+
"""
289+
Converts the value to the appropriate timezone and time as declared by
290+
the `time_override` and `populate_from` attributes.
291+
"""
292+
293+
if not value:
294+
return value
295+
296+
# Retrieve the default timezone as the default
297+
tz = get_default_timezone()
298+
299+
if self.time_override is not None and not (
300+
self.auto_now or (self.auto_now_add and add)
301+
):
302+
time_override = self._get_time_override()
303+
304+
value = datetime.combine(
305+
date=value.date(),
306+
time=time_override
307+
)
308+
309+
# If populate_from exists, override the default timezone
310+
if self.populate_from:
311+
tz = self._get_populate_from(model_instance)
312+
313+
if is_naive(value):
314+
value = make_aware(value=value, timezone=tz)
315+
316+
if not value.tzinfo == tz:
317+
value = value.astimezone(tz)
318+
319+
return value

0 commit comments

Comments
 (0)