Skip to content

Commit 4439236

Browse files
committed
Merge branch 'develop': release v0.3.0 beta 2
2 parents 1f849f1 + 649c682 commit 4439236

File tree

4 files changed

+134
-83
lines changed

4 files changed

+134
-83
lines changed

timezone_field/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# modeled after django's versioning scheme, PEP 386
2-
VERSION = (0, 3, 0, 'beta', 1)
2+
VERSION = (0, 3, 0, 'beta', 2)
33

44
# adapted from django's get_version()
55
# see django.git/django/__init__.py

timezone_field/fields.py

+42-45
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,44 @@
11
import pytz
22

33
from django.core.exceptions import ValidationError
4-
from django.db import models, IntegrityError
4+
from django.db import models
55
from django.utils.encoding import smart_unicode
66

7+
from timezone_field.validators import TzMaxLengthValidator
78

8-
class TimeZoneField(models.CharField):
9+
10+
class TimeZoneField(models.Field):
911
"""
10-
A TimeZoneField stores pytz DstTzInfo objects to the database.
12+
Stores pytz timezone objects to the database.
13+
14+
Valid inputs:
15+
* any instance of pytz.tzinfo.DstTzInfo or pytz.tzinfo.StaticTzInfo
16+
* any string that validates against pytz.all_timezones. pytz will
17+
be used to build a timezone object from the string.
18+
* None and the empty string both represent 'no timezone'
19+
20+
Outputs:
21+
* None
22+
* instances of pytz.tzinfo.DstTzInfo and pytz.tzinfo.StaticTzInfo
1123
12-
Examples of valid inputs:
13-
'' # if blank == True
14-
'America/Los_Angles' # validated against pytz.all_timezones
15-
None # if blank == True
16-
pytz.tzinfo.DstTzInfo # an instance of
24+
Note that blank values ('' and None) are stored as an empty string
25+
in the db. Specifiying null=True makes your db column not have a NOT
26+
NULL constraint, but from the perspective of this field, has no effect.
1727
1828
If you choose to add validators at runtime, they need to accept
19-
pytz.tzinfo objects as input.
29+
pytz.tzinfo.DstTzInfo and pytz.tzinfo.StaticTzInfo objects as input.
30+
31+
If you choose to override the 'choices' kwarg argument, and you specify
32+
choices that can't be consumed by pytz.timezone(unicode(YOUR_NEW_CHOICE)),
33+
wierdness will ensue. Don't do this. It's okay to further limit CHOICES,
34+
but not expand it.
2035
"""
2136

22-
description = "A pytz.tzinfo.DstTzInfo object"
37+
description = "A pytz timezone object"
2338

2439
__metaclass__ = models.SubfieldBase
2540

26-
CHOICES = tuple(zip(pytz.all_timezones, pytz.all_timezones))
41+
CHOICES = [(pytz.timezone(tz), tz) for tz in pytz.all_timezones]
2742
MAX_LENGTH = 63
2843

2944
def __init__(self, validators=[], **kwargs):
@@ -32,56 +47,38 @@ def __init__(self, validators=[], **kwargs):
3247
'choices': TimeZoneField.CHOICES,
3348
}
3449
defaults.update(kwargs)
35-
3650
super(TimeZoneField, self).__init__(**defaults)
51+
self.validators.append(TzMaxLengthValidator(self.max_length))
3752

38-
# validators our parent (CharField) register aren't going to
39-
# work right out of the box. They expect a string, while our
40-
# python type is a pytz.tzinfo
41-
# So, we'll wrap them in a small conversion object.
42-
43-
class ValidateTimeZoneAsString(object):
44-
def __init__(self, org_validator):
45-
self.org_validator = org_validator
46-
def __call__(self, timezone):
47-
self.org_validator(smart_unicode(timezone))
48-
49-
self.validators = [
50-
ValidateTimeZoneAsString(validator)
51-
for validator in self.validators
52-
]
53-
54-
self.validators += validators
53+
def get_internal_type(self):
54+
return 'CharField'
5555

5656
def validate(self, value, model_instance):
57-
# ensure we can consume the value
5857
value = self.to_python(value)
59-
# parent validation works on strings
60-
str_value = smart_unicode(value)
61-
return super(TimeZoneField, self).validate(str_value, model_instance)
58+
return super(TimeZoneField, self).validate(value, model_instance)
6259

6360
def to_python(self, value):
64-
"Returns pytz.tzinfo objects"
61+
"Convert to pytz timezone object"
6562
# inspriation from django's Datetime field
6663
if value is None or value == '':
6764
return None
68-
if isinstance(value, pytz.tzinfo.DstTzInfo):
65+
if isinstance(value, pytz.tzinfo.BaseTzInfo):
6966
return value
70-
try:
71-
return pytz.timezone(smart_unicode(value))
72-
except pytz.UnknownTimeZoneError:
73-
raise ValidationError("Invalid timezone '{}'".format(value))
67+
if isinstance(value, basestring):
68+
try:
69+
return pytz.timezone(value)
70+
except pytz.UnknownTimeZoneError:
71+
pass
72+
raise ValidationError("Invalid timezone '{}'".format(value))
7473

7574
def get_prep_value(self, value):
76-
"Accepts both a pytz.info object or a string representing a timezone"
75+
"Convert to string describing a valid pytz timezone object"
7776
# inspriation from django's Datetime field
7877
value = self.to_python(value)
79-
# doing some validation
80-
if isinstance(value, pytz.tzinfo.DstTzInfo):
78+
if value is None:
79+
return ''
80+
if isinstance(value, pytz.tzinfo.BaseTzInfo):
8181
return smart_unicode(value)
82-
if value is None or value == '':
83-
return None
84-
raise IntegrityError("Invalid timezone '{}'".format(value))
8582

8683
def value_to_string(self, value):
8784
return self.get_prep_value(value)

timezone_field/tests.py

+86-37
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from django import forms
44
from django.core.exceptions import ValidationError
5-
from django.db import models, IntegrityError
5+
from django.db import models
66
from django.test import TestCase
77

88
from timezone_field.fields import TimeZoneField
@@ -14,8 +14,10 @@
1414

1515

1616
class TestModel(models.Model):
17-
tz_not_blank = TimeZoneField()
18-
tz_blank = TimeZoneField(blank=True, null=True)
17+
tz = TimeZoneField()
18+
tz_null = TimeZoneField(null=True)
19+
tz_blank = TimeZoneField(blank=True)
20+
tz_blank_null = TimeZoneField(blank=True, null=True)
1921

2022

2123
class TestModelForm(forms.ModelForm):
@@ -26,95 +28,142 @@ class Meta:
2628
class TimeZoneFieldModelFormTestCase(TestCase):
2729

2830
def test_valid1(self):
29-
form = TestModelForm({'tz_not_blank': PST})
31+
form = TestModelForm({'tz': PST, 'tz_null': EST})
3032
self.assertTrue(form.is_valid())
3133
form.save()
3234
self.assertEqual(TestModel.objects.count(), 1)
3335

3436
def test_valid2(self):
3537
form = TestModelForm({
36-
'tz_not_blank': PST,
38+
'tz': PST,
39+
'tz_null': PST,
3740
'tz_blank': EST,
41+
'tz_blank_null': EST,
3842
})
3943
self.assertTrue(form.is_valid())
4044
form.save()
4145
self.assertEqual(TestModel.objects.count(), 1)
4246

4347
def test_invalid_not_blank(self):
44-
form = TestModelForm()
45-
self.assertFalse(form.is_valid())
46-
47-
def test_invalid_not_blank2(self):
48-
form = TestModelForm({'tz_blank': EST})
49-
self.assertFalse(form.is_valid())
48+
form1 = TestModelForm({'tz': EST})
49+
form2 = TestModelForm({'tz_null': EST})
50+
form3 = TestModelForm()
51+
self.assertFalse(form1.is_valid())
52+
self.assertFalse(form2.is_valid())
53+
self.assertFalse(form3.is_valid())
5054

5155
def test_invalid_invalid_str(self):
52-
form = TestModelForm({'tz_not_blank': INVALID_TZ})
53-
self.assertFalse(form.is_valid())
56+
form1 = TestModelForm({'tz': INVALID_TZ})
57+
form2 = TestModelForm({'tz_blank_null': INVALID_TZ})
58+
self.assertFalse(form1.is_valid())
59+
self.assertFalse(form2.is_valid())
60+
61+
def test_invalid_type(self):
62+
form1 = TestModelForm({'tz': 4})
63+
form2 = TestModelForm({'tz_blank_null': object()})
64+
self.assertFalse(form1.is_valid())
65+
self.assertFalse(form2.is_valid())
5466

5567

5668
class TimeZoneFieldDBTestCase(TestCase):
5769

5870
def test_valid_strings(self):
5971
m = TestModel.objects.create(
60-
tz_not_blank=PST,
72+
tz=PST,
73+
tz_null=PST,
6174
tz_blank=EST,
75+
tz_blank_null=EST,
6276
)
6377
m = TestModel.objects.get(pk=m.pk)
64-
self.assertEqual(m.tz_not_blank, pytz.timezone(PST))
78+
self.assertEqual(m.tz, pytz.timezone(PST))
79+
self.assertEqual(m.tz_null, pytz.timezone(PST))
6580
self.assertEqual(m.tz_blank, pytz.timezone(EST))
81+
self.assertEqual(m.tz_blank_null, pytz.timezone(EST))
6682

6783
def test_valid_tzinfos(self):
6884
m = TestModel.objects.create(
69-
tz_not_blank=pytz.timezone(PST),
70-
tz_blank=pytz.timezone(EST),
85+
tz=pytz.timezone(PST),
86+
tz_null=pytz.timezone(EST),
87+
tz_blank=pytz.timezone(PST),
88+
tz_blank_null=pytz.timezone(EST),
7189
)
7290
m = TestModel.objects.get(pk=m.pk)
73-
self.assertEqual(m.tz_not_blank, pytz.timezone(PST))
74-
self.assertEqual(m.tz_blank, pytz.timezone(EST))
91+
self.assertEqual(m.tz, pytz.timezone(PST))
92+
self.assertEqual(m.tz_null, pytz.timezone(EST))
93+
self.assertEqual(m.tz_blank, pytz.timezone(PST))
94+
self.assertEqual(m.tz_blank_null, pytz.timezone(EST))
7595

7696
def test_valid_blank_str(self):
7797
m = TestModel.objects.create(
78-
tz_not_blank=PST,
98+
tz=PST,
99+
tz_null=EST,
79100
tz_blank='',
101+
tz_blank_null='',
80102
)
81103
m = TestModel.objects.get(pk=m.pk)
82-
self.assertEqual(m.tz_not_blank, pytz.timezone(PST))
104+
self.assertEqual(m.tz, pytz.timezone(PST))
105+
self.assertEqual(m.tz_null, pytz.timezone(EST))
83106
self.assertIsNone(m.tz_blank)
107+
self.assertIsNone(m.tz_blank_null)
84108

85109
def test_valid_blank_none(self):
86110
m = TestModel.objects.create(
87-
tz_not_blank=PST,
111+
tz=PST,
88112
tz_blank=None,
113+
tz_blank_null=None,
89114
)
90115
m = TestModel.objects.get(pk=m.pk)
91-
self.assertEqual(m.tz_not_blank, pytz.timezone(PST))
116+
self.assertEqual(m.tz, pytz.timezone(PST))
92117
self.assertIsNone(m.tz_blank)
118+
self.assertIsNone(m.tz_blank_null)
93119

94120
def test_string_value_lookup(self):
95-
TestModel.objects.create(tz_not_blank=EST)
96-
qs = TestModel.objects.filter(tz_not_blank=EST)
121+
TestModel.objects.create(tz=EST)
122+
qs = TestModel.objects.filter(tz=EST)
97123
self.assertEqual(qs.count(), 1)
98124

99125
def test_tz_value_lookup(self):
100-
TestModel.objects.create(tz_not_blank=EST)
101-
qs = TestModel.objects.filter(tz_not_blank=pytz.timezone(EST))
126+
TestModel.objects.create(tz=EST)
127+
qs = TestModel.objects.filter(tz=pytz.timezone(EST))
102128
self.assertEqual(qs.count(), 1)
103129

104130
def test_invalid_blank_str(self):
105-
m = TestModel(tz_not_blank='')
106-
with self.assertRaises(ValidationError):
107-
m.full_clean()
108-
with self.assertRaises(IntegrityError):
109-
m.save()
131+
m1 = TestModel(tz='')
132+
m2 = TestModel(tz_null='')
133+
self.assertRaises(ValidationError, m1.full_clean)
134+
self.assertRaises(ValidationError, m2.full_clean)
110135

111136
def test_invalid_blank_none(self):
112-
m = TestModel(tz_not_blank=None)
137+
m1 = TestModel(tz=None)
138+
m2 = TestModel(tz_null=None)
139+
self.assertRaises(ValidationError, m1.full_clean)
140+
self.assertRaises(ValidationError, m2.full_clean)
141+
142+
def test_invalid_type(self):
113143
with self.assertRaises(ValidationError):
114-
m.full_clean()
115-
with self.assertRaises(IntegrityError):
116-
m.save()
144+
TestModel(tz=4)
145+
with self.assertRaises(ValidationError):
146+
TestModel(tz_null=object())
117147

118148
def test_invalid_string(self):
119149
with self.assertRaises(ValidationError):
120-
TestModel(tz_not_blank=INVALID_TZ)
150+
TestModel(tz=INVALID_TZ)
151+
with self.assertRaises(ValidationError):
152+
TestModel(tz_null=INVALID_TZ)
153+
with self.assertRaises(ValidationError):
154+
TestModel(tz_blank=INVALID_TZ)
155+
with self.assertRaises(ValidationError):
156+
TestModel(tz_blank_null=INVALID_TZ)
157+
158+
def test_invalid_max_length(self):
159+
class TestModelML(models.Model):
160+
tz = TimeZoneField(max_length=4)
161+
m1 = TestModelML(tz=PST)
162+
self.assertRaises(ValidationError, m1.full_clean)
163+
164+
def test_invalid_choice(self):
165+
class TestModelChoice(models.Model):
166+
CHOICES = [(pytz.timezone(tz), tz) for tz in pytz.common_timezones]
167+
tz = TimeZoneField(choices=CHOICES)
168+
m1 = TestModelChoice(tz='Europe/Nicosia')
169+
self.assertRaises(ValidationError, m1.full_clean)

timezone_field/validators.py

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from django.core.validators import MaxLengthValidator
2+
3+
class TzMaxLengthValidator(MaxLengthValidator):
4+
"Validate a timezone's string representation does not exceed max_length"
5+
clean = lambda self, x: len(unicode(x))

0 commit comments

Comments
 (0)