Skip to content

Commit

Permalink
Merge branch 'develop': release 1.3
Browse files Browse the repository at this point in the history
  • Loading branch information
mfogel committed Oct 13, 2015
2 parents 8be692c + 80ab1ec commit fdc1e0c
Show file tree
Hide file tree
Showing 7 changed files with 140 additions and 130 deletions.
21 changes: 10 additions & 11 deletions README.rst
Expand Up @@ -7,12 +7,6 @@ django-timezone-field
.. image:: https://coveralls.io/repos/mfogel/django-timezone-field/badge.png?branch=develop
:target: https://coveralls.io/r/mfogel/django-timezone-field

.. image:: https://pypip.in/v/django-timezone-field/badge.png
:target: https://crate.io/packages/django-timezone-field/

.. image:: https://pypip.in/d/django-timezone-field/badge.png
:target: https://crate.io/packages/django-timezone-field/

A Django app providing database and form fields for `pytz`__ timezone objects.

Examples
Expand Down Expand Up @@ -86,6 +80,11 @@ Installation
Changelog
------------

* 1.3 (2015-10-12)

* Drop support for django 1.6, add support for django 1.8
* Various `bug fixes`__

* 1.2 (2015-02-05)

* For form field, changed default list of accepted timezones from
Expand All @@ -95,10 +94,8 @@ Changelog
* 1.1 (2014-10-05)

* Django 1.7 compatibility
* Changed format of `choices` kwarg to `[[<str>, <str>], ...]`,
was previously `[[<pytz timezone>, <str>], ...]`.
Old format is still deprecated but still accepted for now; support
will be removed in a future release.
* Added support for formating `choices` kwarg as `[[<str>, <str>], ...]`,
in addition to previous format of `[[<pytz.timezone>, <str>], ...]`.
* Changed default list of accepted timezones from `pytz.all_timezones` to
`pytz.common_timezones`. If you have timezones in your DB that are in
`pytz.all_timezones` but not in `pytz.common_timezones`, this is a
Expand All @@ -122,7 +119,8 @@ Running the Tests
tox
It's that simple.
Postgres will need to be running locally, and sqlite will need to be
installed in order for tox to do its job.

Found a Bug?
------------
Expand All @@ -139,6 +137,7 @@ __ http://pypi.python.org/pypi/pytz/
__ http://pypi.python.org/pypi/django-timezone-field/
__ http://www.pip-installer.org/
__ https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps
__ https://github.com/mfogel/django-timezone-field/issues?q=milestone%3A1.3
__ https://tox.readthedocs.org/
__ https://github.com/mfogel/django-timezone-field/
__ https://github.com/brosner/django-timezones/
Expand Down
3 changes: 1 addition & 2 deletions setup.py
Expand Up @@ -36,7 +36,7 @@ def find_version(*file_paths):
'timezone_field',
'timezone_field.tests',
],
install_requires=['django>=1.4.2', 'pytz'],
install_requires=['django>=1.7', 'pytz'],
classifiers=[
'Development Status :: 4 - Beta',
'Environment :: Web Environment',
Expand All @@ -45,7 +45,6 @@ def find_version(*file_paths):
'Operating System :: OS Independent',
'Programming Language :: Python',
'Programming Language :: Python :: 2',
'Programming Language :: Python :: 2.6',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.2',
Expand Down
2 changes: 1 addition & 1 deletion timezone_field/__init__.py
@@ -1,4 +1,4 @@
__version__ = '1.2'
__version__ = '1.3'
__all__ = ['TimeZoneField', 'TimeZoneFormField']

from timezone_field.fields import TimeZoneField
Expand Down
84 changes: 38 additions & 46 deletions timezone_field/fields.py
Expand Up @@ -23,51 +23,63 @@ class TimeZoneFieldBase(models.Field):
* instances of pytz.tzinfo.DstTzInfo and pytz.tzinfo.StaticTzInfo
* the pytz.UTC singleton
Note that blank values ('' and None) are stored as an empty string
in the db. Specifying null=True makes your db column not have a NOT
NULL constraint, but from the perspective of this field, has no effect.
Blank values are stored in the DB as the empty string. Timezones are stored
in their string representation.
If you choose to override the 'choices' kwarg argument, and you specify
choices that can't be consumed by pytz.timezone(unicode(YOUR_NEW_CHOICE)),
weirdness will ensue. It's ok to further limit CHOICES, but not expand it.
The `choices` kwarg can be specified as a list of either
[<pytz.timezone>, <str>] or [<str>, <str>]. Internally, it is stored as
[<pytz.timezone>, <str>].
"""

description = "A pytz timezone object"

# NOTE: these defaults are excluded from migrations. If these are changed,
# existing migration files will need to be accomodated.
CHOICES = [(tz, tz) for tz in pytz.common_timezones]
CHOICES = [(pytz.timezone(tz), tz) for tz in pytz.common_timezones]
MAX_LENGTH = 63

def __init__(self, **kwargs):
parent_kwargs = {
'max_length': self.MAX_LENGTH,
'choices': TimeZoneField.CHOICES,
}
parent_kwargs.update(kwargs)
super(TimeZoneFieldBase, self).__init__(**parent_kwargs)

# We expect choices in form [<str>, <str>], but we
# also support [<pytz.timezone>, <str>], for backwards compatability
# Our parent saved those in self._choices.
if self._choices:
if is_pytz_instance(self._choices[0][0]):
self._choices = [(tz.zone, name) for tz, name in self._choices]
def __init__(self, choices=None, max_length=None, **kwargs):
if choices is None:
choices = self.CHOICES
else:
# Choices can be specified in two forms: either
# [<pytz.timezone>, <str>] or [<str>, <str>]
#
# The [<pytz.timezone>, <str>] format is the one we actually
# store the choices in memory because of
# https://github.com/mfogel/django-timezone-field/issues/24
#
# The [<str>, <str>] format is supported because since django
# can't deconstruct pytz.timezone objects, migration files must
# use an alternate format. Representing the timezones as strings
# is the obvious choice.
if isinstance(choices[0][0], six.string_types):
choices = [(pytz.timezone(n1), n2) for n1, n2 in choices]

if max_length is None:
max_length = self.MAX_LENGTH

super(TimeZoneFieldBase, self).__init__(choices=choices,
max_length=max_length,
**kwargs)

def validate(self, value, model_instance):
# since our choices are of the form [<str>, <str>], convert the
# incoming value to a string for validation
if not is_pytz_instance(value):
raise ValidationError("'%s' is not a pytz timezone object" % value)
tz_as_str = value.zone
super(TimeZoneFieldBase, self).validate(tz_as_str, model_instance)
super(TimeZoneFieldBase, self).validate(value, model_instance)

def deconstruct(self):
name, path, args, kwargs = super(TimeZoneFieldBase, self).deconstruct()
if kwargs['choices'] == self.CHOICES:
del kwargs['choices']
if kwargs['max_length'] == self.MAX_LENGTH:
del kwargs['max_length']

# django can't decontruct pytz objects, so transform choices
# to [<str>, <str>] format for writing out to the migration
if 'choices' in kwargs:
kwargs['choices'] = [(tz.zone, n) for tz, n in kwargs['choices']]

return name, path, args, kwargs

def get_internal_type(self):
Expand All @@ -84,7 +96,7 @@ def get_prep_value(self, value):
def _get_python_and_db_repr(self, value):
"Returns a tuple of (python representation, db representation)"
if value is None or value == '':
return (None, None)
return (None, '')
if is_pytz_instance(value):
return (value, value.zone)
if isinstance(value, six.string_types):
Expand All @@ -99,23 +111,3 @@ def _get_python_and_db_repr(self, value):
class TimeZoneField(six.with_metaclass(models.SubfieldBase,
TimeZoneFieldBase)):
pass


# South support
try:
from south.modelsinspector import add_introspection_rules
except ImportError:
pass
else:
add_introspection_rules(
rules=[(
(TimeZoneField, ), # Class(es) these apply to
[], # Positional arguments (not used)
{ # Keyword argument
'max_length': [
'max_length', {'default': TimeZoneField.MAX_LENGTH},
],
},
)],
patterns=['timezone_field\.fields\.']
)
2 changes: 1 addition & 1 deletion timezone_field/tests/models.py
Expand Up @@ -5,5 +5,5 @@

class TestModel(models.Model):
tz = TimeZoneField()
tz_opt = TimeZoneField(blank=True, null=True)
tz_opt = TimeZoneField(blank=True)
tz_opt_default = TimeZoneField(blank=True, default='America/Los_Angeles')
50 changes: 35 additions & 15 deletions timezone_field/tests/tests.py
@@ -1,11 +1,11 @@
import pytz

import django
from django import forms
from django.core.exceptions import ValidationError
from django.db import models
from django.db.migrations.writer import MigrationWriter
from django.test import TestCase
from django.utils import six, unittest
from django.utils import six

from timezone_field import TimeZoneField, TimeZoneFormField
from timezone_field.tests.models import TestModel
Expand Down Expand Up @@ -229,21 +229,21 @@ class TimeZoneFieldLimitedChoicesTestCase(TestCase):
class TestModelChoice(models.Model):
tz_superset = TimeZoneField(
choices=[(tz, tz) for tz in pytz.all_timezones],
blank=True, null=True,
blank=True,
)
tz_subset = TimeZoneField(
choices=[(tz, tz) for tz in USA_TZS],
blank=True, null=True,
blank=True,
)

class TestModelOldChoiceFormat(models.Model):
tz_superset = TimeZoneField(
choices=[(pytz.timezone(tz), tz) for tz in pytz.all_timezones],
blank=True, null=True,
blank=True,
)
tz_subset = TimeZoneField(
choices=[(pytz.timezone(tz), tz) for tz in USA_TZS],
blank=True, null=True,
blank=True,
)

def test_valid_choice(self):
Expand Down Expand Up @@ -277,15 +277,19 @@ def test_invalid_choice_old_format(self):
self.assertRaises(ValidationError, m.full_clean)


@unittest.skipIf(django.VERSION < (1, 7), "Migrations not built-in before 1.7")
class TimeZoneFieldDeconstructTestCase(TestCase):

test_fields = (
TimeZoneField(),
TimeZoneField(
max_length=42,
choices=[(pytz.timezone(tz), tz) for tz in pytz.common_timezones],
),
TimeZoneField(max_length=42),
TimeZoneField(choices=[
(pytz.timezone('US/Pacific'), 'US/Pacific'),
(pytz.timezone('US/Eastern'), 'US/Eastern'),
]),
TimeZoneField(choices=[
('US/Pacific', 'US/Pacific'),
('US/Eastern', 'US/Eastern'),
]),
)

def test_deconstruct(self):
Expand All @@ -296,9 +300,6 @@ def test_deconstruct(self):
self.assertEqual(org_field.choices, new_field.choices)

def test_full_serialization(self):
# avoid importing this when test skipped on django 1.6
from django.db.migrations.writer import MigrationWriter

# ensure the values passed to kwarg arguments can be serialized
# the recommended 'deconstruct' testing by django docs doesn't cut it
# https://docs.djangoproject.com/en/1.7/howto/custom-model-fields/#field-deconstruction
Expand All @@ -307,7 +308,7 @@ def test_full_serialization(self):
# ensuring the following call doesn't throw an error
MigrationWriter.serialize(field)

def test_default_choices_not_frozen(self):
def test_default_kwargs_not_frozen(self):
"""
Ensure the deconstructed representation of the field does not contain
kwargs if they match the default.
Expand All @@ -317,3 +318,22 @@ def test_default_choices_not_frozen(self):
name, path, args, kwargs = field.deconstruct()
self.assertNotIn('choices', kwargs)
self.assertNotIn('max_length', kwargs)

def test_specifying_defaults_not_frozen(self):
"""
If someone's matched the default values with their kwarg args, we
shouldn't bothering freezing those.
"""
field = TimeZoneField(max_length=63)
name, path, args, kwargs = field.deconstruct()
self.assertNotIn('max_length', kwargs)

choices = [(pytz.timezone(tz), tz) for tz in pytz.common_timezones]
field = TimeZoneField(choices=choices)
name, path, args, kwargs = field.deconstruct()
self.assertNotIn('choices', kwargs)

choices = [(tz, tz) for tz in pytz.common_timezones]
field = TimeZoneField(choices=choices)
name, path, args, kwargs = field.deconstruct()
self.assertNotIn('choices', kwargs)

0 comments on commit fdc1e0c

Please sign in to comment.