9
9
# Django
10
10
try :
11
11
from django .core import checks
12
- except ImportError :
12
+ except ImportError : # pragma: no cover
13
13
pass
14
14
from django .core .exceptions import ValidationError
15
15
from django .db .models import SubfieldBase
16
16
from django .db .models .fields import DateTimeField , CharField
17
17
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
19
19
from django .utils .translation import ugettext_lazy as _
20
20
21
21
# App
@@ -47,11 +47,15 @@ def __init__(self, *args, **kwargs):
47
47
super (TimeZoneField , self ).__init__ (* args , ** kwargs )
48
48
49
49
def get_prep_value (self , value ):
50
+ """Converts timezone instances to strings for db storage."""
51
+
50
52
if isinstance (value , tzinfo ):
51
53
return value .zone
52
54
return value
53
55
54
56
def to_python (self , value ):
57
+ """Returns a datetime.tzinfo instance for the value."""
58
+
55
59
value = super (TimeZoneField , self ).to_python (value )
56
60
57
61
if not value :
@@ -67,6 +71,8 @@ def to_python(self, value):
67
71
)
68
72
69
73
def formfield (self , ** kwargs ):
74
+ """Returns a custom form field for the TimeZoneField."""
75
+
70
76
defaults = {'form_class' : forms .TimeZoneField }
71
77
defaults .update (** kwargs )
72
78
return super (TimeZoneField , self ).formfield (** defaults )
@@ -75,16 +81,19 @@ def formfield(self, **kwargs):
75
81
# Django >= 1.7 Checks Framework
76
82
# --------------------------------------------------------------------------
77
83
def check (self , ** kwargs ): # pragma: no cover
84
+ """Calls the TimeZoneField's custom checks."""
85
+
78
86
errors = super (TimeZoneField , self ).check (** kwargs )
79
87
errors .extend (self ._check_timezone_max_length_attribute ())
80
88
errors .extend (self ._check_choices_attribute ())
81
89
return errors
82
90
83
91
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
-
87
92
"""
93
+ Checks that the `max_length` attribute covers all possible pytz timezone
94
+ lengths.
95
+ """
96
+
88
97
# Retrieve the maximum possible length for the time zone string
89
98
possible_max_length = max (map (len , pytz .all_timezones ))
90
99
@@ -114,6 +123,8 @@ def _check_timezone_max_length_attribute(self): # pragma: no cover
114
123
return []
115
124
116
125
def _check_choices_attribute (self ): # pragma: no cover
126
+ """Checks to make sure that choices contains valid timezone choices."""
127
+
117
128
if self .choices :
118
129
warning_params = {
119
130
'msg' : (
@@ -171,10 +182,26 @@ class LinkedTZDateTimeField(with_metaclass(SubfieldBase, DateTimeField)):
171
182
def __init__ (self , * args , ** kwargs ):
172
183
self .populate_from = kwargs .pop ('populate_from' , None )
173
184
self .time_override = kwargs .pop ('time_override' , None )
185
+ self .timezone = get_default_timezone ()
174
186
175
187
super (LinkedTZDateTimeField , self ).__init__ (* args , ** kwargs )
176
188
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
+
177
199
def pre_save (self , model_instance , add ):
200
+ """
201
+ Converts the value being saved based on `populate_from` and
202
+ `time_override`
203
+ """
204
+
178
205
# Retrieve the currently entered datetime
179
206
value = super (
180
207
LinkedTZDateTimeField ,
@@ -184,80 +211,109 @@ def pre_save(self, model_instance, add):
184
211
add = add
185
212
)
186
213
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
+ )
244
220
245
221
setattr (model_instance , self .attname , value )
246
- setattr (model_instance , '_timezone' , tz )
247
222
248
223
return value
249
224
250
225
def deconstruct (self ): # pragma: no cover
226
+ """Add our custom keyword arguments for migrations."""
227
+
251
228
name , path , args , kwargs = super (
252
229
LinkedTZDateTimeField ,
253
230
self
254
231
).deconstruct ()
255
232
256
233
# Only include kwarg if it's not the default
257
234
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
258
238
kwargs ['populate_from' ] = self .populate_from
259
239
240
+ # Only include kwarg if it's not the default
260
241
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
262
247
263
248
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