pax_global_header00006660000000000000000000000064135713527640014527gustar00rootroot0000000000000052 comment=915a51a5167dca1f25a2365addc3d74c0ffdf82b django-timezone-field-4.0/000077500000000000000000000000001357135276400155455ustar00rootroot00000000000000django-timezone-field-4.0/.gitignore000066400000000000000000000000631357135276400175340ustar00rootroot00000000000000.coverage .tox *.pyc *.egg-info htmlcov build dist django-timezone-field-4.0/.travis.yml000066400000000000000000000004341357135276400176570ustar00rootroot00000000000000sudo: false dist: xenial services: - postgresql language: python python: - 3.5 - 3.6 - 3.7 - 3.8 install: - pip install tox-travis coveralls before_script: - psql -c 'create database timezone_field_tests;' -U postgres script: - tox after_success: - coveralls django-timezone-field-4.0/LICENSE.txt000066400000000000000000000024651357135276400173770ustar00rootroot00000000000000Copyright (c) 2014, Mike Fogel All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. django-timezone-field-4.0/MANIFEST.in000066400000000000000000000001051357135276400172770ustar00rootroot00000000000000include LICENSE.txt MANIFEST.in README.rst recursive-exclude tests * django-timezone-field-4.0/README.rst000066400000000000000000000126621357135276400172430ustar00rootroot00000000000000django-timezone-field ===================== .. image:: https://img.shields.io/travis/mfogel/django-timezone-field.svg :target: https://travis-ci.org/mfogel/django-timezone-field/ .. image:: https://img.shields.io/coveralls/mfogel/django-timezone-field.svg :target: https://coveralls.io/r/mfogel/django-timezone-field/ .. image:: https://img.shields.io/pypi/dm/django-timezone-field.svg :target: https://pypi.python.org/pypi/django-timezone-field/ A Django app providing database and form fields for `pytz`__ timezone objects. Examples -------- Database Field ~~~~~~~~~~~~~~ .. code:: python import pytz from django.db import models from timezone_field import TimeZoneField class MyModel(models.Model): timezone1 = TimeZoneField(default='Europe/London') # defaults supported timezone2 = TimeZoneField() timezone3 = TimeZoneField() my_inst = MyModel( timezone1='America/Los_Angeles', # assignment of a string timezone2=pytz.timezone('Turkey'), # assignment of a pytz.DstTzInfo timezone3=pytz.UTC, # assignment of pytz.UTC singleton ) my_inst.full_clean() # validates against pytz.common_timezones my_inst.save() # values stored in DB as strings tz = my_inst.timezone1 # values retrieved as pytz objects repr(tz) # "" Form Field ~~~~~~~~~~ .. code:: python from django import forms from timezone_field import TimeZoneFormField class MyForm(forms.Form): timezone = TimeZoneFormField() # displays like "America/Los_Angeles" timezone2 = TimeZoneFormField(display_GMT_offset=True) # displays like "GMT-08:00 America/Los_Angeles" my_form = MyForm({ 'timezone': 'America/Los_Angeles', }) my_form.full_clean() # validates against pytz.common_timezones tz = my_form.cleaned_data['timezone'] # values retrieved as pytz objects repr(tz) # "" Installation ------------ #. From `pypi`__ using `pip`__: .. code:: sh pip install django-timezone-field #. Add `timezone_field` to your `settings.INSTALLED_APPS`__: .. code:: python INSTALLED_APPS = ( ... 'timezone_field', ... ) Changelog ------------ * 4.0 (2019-12-03) * Add support for django 3.0, python 3.8 * Drop support for django 1.11, 2.0, 2.1, python 2.7, 3.4 * 3.1 (2019-10-02) * Officially support django 2.2 (already worked) * Add option to display TZ offsets in form field `#46`__ * 3.0 (2018-09-15) * Support django 1.11, 2.0, 2.1 * Add support for python 3.7 * Change default human-readable timezone names to exclude underscores (`#32`__ & `#37`__) * 2.1 (2018-03-01) * Add support for django 1.10, 1.11 * Add support for python 3.6 * Add wheel support * Support bytes in DB fields (`#38`__ & `#39`__) * 2.0 (2016-01-31) * Drop support for django 1.7, add support for django 1.9 * Drop support for python 3.2, 3.3, add support for python 3.5 * Remove tests from source distribution * 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 `pytz.all_timezones` to `pytz.common_timezones`, to match DB field behavior. * 1.1 (2014-10-05) * Django 1.7 compatibility * Added support for formatting `choices` kwarg as `[[, ], ...]`, in addition to previous format of `[[, ], ...]`. * 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 backward-incompatible change. Old behavior can be restored by specifying `choices=[(tz, tz) for tz in pytz.all_timezones]` in your model definition. * 1.0 (2013-08-04) * Initial release as `timezone_field`. Running the Tests ----------------- #. Install `tox`__. #. From the repository root, run .. code:: sh tox 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? ------------ To file a bug or submit a patch, please head over to `django-timezone-field on github`__. Credits ------- Originally adapted from `Brian Rosner's django-timezones`__. The full list of contributors is available on `github`__. __ 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/46 __ https://github.com/mfogel/django-timezone-field/issues/32 __ https://github.com/mfogel/django-timezone-field/issues/37 __ https://github.com/mfogel/django-timezone-field/issues/38 __ https://github.com/mfogel/django-timezone-field/issues/39 __ 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/ __ https://github.com/mfogel/django-timezone-field/graphs/contributors django-timezone-field-4.0/setup.cfg000066400000000000000000000000461357135276400173660ustar00rootroot00000000000000[metadata] license_file = LICENSE.txt django-timezone-field-4.0/setup.py000066400000000000000000000032521357135276400172610ustar00rootroot00000000000000import re from os import path from setuptools import setup # read() and find_version() taken from jezdez's python apps, ex: # https://github.com/jezdez/django_compressor/blob/develop/setup.py def read(*parts): return open(path.join(path.dirname(__file__), *parts)).read() def find_version(*file_paths): version_file = read(*file_paths) version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]", version_file, re.M) if version_match: return version_match.group(1) raise RuntimeError("Unable to find version string.") setup( name='django-timezone-field', version=find_version('timezone_field', '__init__.py'), author='Mike Fogel', author_email='mike@fogel.ca', description=( 'A Django app providing database and form fields for ' 'pytz timezone objects.' ), long_description=read('README.rst'), url='http://github.com/mfogel/django-timezone-field/', license='BSD', packages=[ 'timezone_field', ], install_requires=['django>=2.2', 'pytz'], python_requires='>=3.5', classifiers=[ 'Development Status :: 4 - Beta', 'Environment :: Web Environment', 'Intended Audience :: Developers', 'License :: OSI Approved :: BSD License', 'Operating System :: OS Independent', 'Programming Language :: Python', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Topic :: Utilities', 'Framework :: Django', ], ) django-timezone-field-4.0/tests/000077500000000000000000000000001357135276400167075ustar00rootroot00000000000000django-timezone-field-4.0/tests/__init__.py000066400000000000000000000000001357135276400210060ustar00rootroot00000000000000django-timezone-field-4.0/tests/models.py000066400000000000000000000004751357135276400205520ustar00rootroot00000000000000from django.db import models from timezone_field import TimeZoneField class TestModel(models.Model): tz = TimeZoneField() tz_opt = TimeZoneField(blank=True) tz_opt_default = TimeZoneField(blank=True, default='America/Los_Angeles') tz_gmt_offset = TimeZoneField(blank=True, display_GMT_offset=True) django-timezone-field-4.0/tests/settings.py000066400000000000000000000043251357135276400211250ustar00rootroot00000000000000""" Django settings for tests project. For more information on this file, see https://docs.djangoproject.com/en/1.7/topics/settings/ For the full list of settings and their values, see https://docs.djangoproject.com/en/1.7/ref/settings/ """ # Build paths inside the project like this: os.path.join(BASE_DIR, ...) import os BASE_DIR = os.path.dirname(os.path.dirname(__file__)) # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/1.7/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! SECRET_KEY = 'unused' # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True TEMPLATE_DEBUG = True ALLOWED_HOSTS = [] # Application definition INSTALLED_APPS = ( 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'timezone_field', 'tests', ) MIDDLEWARE_CLASSES = ( 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', ) WSGI_APPLICATION = 'tests.wsgi.application' # Database # https://docs.djangoproject.com/en/1.7/ref/settings/#databases test_db_engine = os.environ.get('TEST_DB_ENGINE', 'sqlite') if test_db_engine == 'sqlite': DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', }, } if test_db_engine == 'postgres': DATABASES = { 'default': { 'ENGINE': 'django.db.backends.postgresql_psycopg2', 'NAME': 'timezone_field_tests', 'USER': 'postgres', }, } # Internationalization # https://docs.djangoproject.com/en/1.7/topics/i18n/ LANGUAGE_CODE = 'en-us' TIME_ZONE = 'UTC' USE_I18N = True USE_L10N = True USE_TZ = True # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/1.7/howto/static-files/ STATIC_URL = '/static/' django-timezone-field-4.0/tests/tests.py000066400000000000000000000366241357135276400204360ustar00rootroot00000000000000from __future__ import absolute_import import pytz 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 timezone_field import TimeZoneField, TimeZoneFormField from timezone_field.utils import add_gmt_offset_to_choices from tests.models import TestModel PST = 'America/Los_Angeles' # pytz.tzinfo.DstTzInfo GMT = 'GMT' # pytz.tzinfo.StaticTzInfo UTC = 'UTC' # pytz.UTC singleton PST_tz = pytz.timezone(PST) GMT_tz = pytz.timezone(GMT) UTC_tz = pytz.timezone(UTC) INVALID_TZ = 'ogga booga' UNCOMMON_TZ = 'Singapore' USA_TZS = [ 'US/Alaska', 'US/Arizona', 'US/Central', 'US/Eastern', 'US/Hawaii', 'US/Mountain', 'US/Pacific', ] class TestForm(forms.Form): tz = TimeZoneFormField() tz_opt = TimeZoneFormField(required=False) class TestModelForm(forms.ModelForm): class Meta: model = TestModel fields = '__all__' class TimeZoneFormFieldTestCase(TestCase): def test_valid1(self): form = TestForm({'tz': PST}) self.assertTrue(form.is_valid()) self.assertEqual(form.cleaned_data['tz'], PST_tz) self.assertEqual(form.cleaned_data['tz_opt'], None) def test_valid2(self): form = TestForm({'tz': GMT, 'tz_opt': UTC}) self.assertTrue(form.is_valid()) self.assertEqual(form.cleaned_data['tz'], GMT_tz) self.assertEqual(form.cleaned_data['tz_opt'], UTC_tz) def test_invalid_invalid_str(self): form = TestForm({'tz': INVALID_TZ}) self.assertFalse(form.is_valid()) def test_invalid_choice(self): form = TestForm({'tz_invalid_choice': INVALID_TZ}) self.assertFalse(form.is_valid()) def test_invalid_uncommon_tz(self): form = TestForm({'tz': UNCOMMON_TZ}) self.assertFalse(form.is_valid()) def test_default_human_readable_choices_dont_have_underscores(self): form = TestForm() pst_choice = [c for c in form.fields['tz'].choices if c[0] == PST] self.assertEqual(pst_choice[0][1], 'America/Los Angeles') class TestFormInvalidChoice(forms.Form): tz = TimeZoneFormField( choices=( [(tz, tz) for tz in pytz.all_timezones] + [(INVALID_TZ, pytz.UTC)] ) ) class TimeZoneFormFieldInvalidChoideTestCase(TestCase): def test_valid(self): form = TestFormInvalidChoice({'tz': PST}) self.assertTrue(form.is_valid()) self.assertEqual(form.cleaned_data['tz'], PST_tz) def test_invalid_choide(self): form = TestFormInvalidChoice({'tz': INVALID_TZ}) self.assertFalse(form.is_valid()) class TimeZoneFieldModelFormTestCase(TestCase): def test_valid_with_defaults(self): # seems there should be a better way to get a form's default values...? # http://stackoverflow.com/questions/7399490/ data = dict( (field_name, field.initial) for field_name, field in TestModelForm().fields.items() ) data.update({'tz': GMT}) form = TestModelForm(data=data) self.assertTrue(form.is_valid()) form.save() self.assertEqual(TestModel.objects.count(), 1) m = TestModel.objects.get() self.assertEqual(m.tz, GMT_tz) self.assertEqual(m.tz_opt, None) self.assertEqual(m.tz_opt_default, PST_tz) self.assertEqual(m.tz_gmt_offset, None) def test_valid_specify_all(self): form = TestModelForm({ 'tz': UTC, 'tz_opt': PST, 'tz_opt_default': GMT, 'tz_gmt_offset': UTC, }) self.assertTrue(form.is_valid()) form.save() self.assertEqual(TestModel.objects.count(), 1) m = TestModel.objects.get() self.assertEqual(m.tz, UTC_tz) self.assertEqual(m.tz_opt, PST_tz) self.assertEqual(m.tz_opt_default, GMT_tz) self.assertEqual(m.tz_gmt_offset, UTC_tz) def test_invalid_not_blank(self): form = TestModelForm({}) self.assertFalse(form.is_valid()) self.assertTrue(any('required' in e for e in form.errors['tz'])) def test_invalid_choice(self): form = TestModelForm({'tz': INVALID_TZ}) self.assertFalse(form.is_valid()) self.assertTrue(any('choice' in e for e in form.errors['tz'])) def test_invalid_uncommmon_tz(self): form = TestModelForm({'tz': UNCOMMON_TZ}) self.assertFalse(form.is_valid()) self.assertTrue(any('choice' in e for e in form.errors['tz'])) def test_default_human_readable_choices_dont_have_underscores(self): form = TestModelForm() pst_choice = [c for c in form.fields['tz'].choices if c[0] == PST_tz] self.assertEqual(pst_choice[0][1], 'America/Los Angeles') def test_display_GMT_offsets(self): form = TestModelForm({'tz_gmt_offset': PST_tz}) c = [c for c in form.fields['tz_gmt_offset'].choices if c[0] == PST_tz] self.assertEqual(c[0][1], 'GMT-08:00 America/Los Angeles') class TimeZoneFieldTestCase(TestCase): def test_valid_dst_tz_objects(self): m = TestModel.objects.create(tz=PST_tz, tz_opt=PST_tz, tz_opt_default=PST_tz) m.full_clean() m = TestModel.objects.get(pk=m.pk) self.assertEqual(m.tz, PST_tz) self.assertEqual(m.tz_opt, PST_tz) self.assertEqual(m.tz_opt_default, PST_tz) def test_valid_dst_tz_strings(self): m = TestModel.objects.create(tz=PST, tz_opt=PST, tz_opt_default=PST) m.full_clean() m = TestModel.objects.get(pk=m.pk) self.assertEqual(m.tz, PST_tz) self.assertEqual(m.tz_opt, PST_tz) self.assertEqual(m.tz_opt_default, PST_tz) def test_valid_static_tz_objects(self): m = TestModel.objects.create(tz=GMT_tz, tz_opt=GMT_tz, tz_opt_default=GMT_tz) m.full_clean() m = TestModel.objects.get(pk=m.pk) self.assertEqual(m.tz, GMT_tz) self.assertEqual(m.tz_opt, GMT_tz) self.assertEqual(m.tz_opt_default, GMT_tz) def test_valid_static_tz_strings(self): m = TestModel.objects.create(tz=GMT, tz_opt=GMT, tz_opt_default=GMT) m.full_clean() m = TestModel.objects.get(pk=m.pk) self.assertEqual(m.tz, GMT_tz) self.assertEqual(m.tz_opt, GMT_tz) self.assertEqual(m.tz_opt_default, GMT_tz) def test_valid_UTC_object(self): m = TestModel.objects.create(tz=UTC_tz, tz_opt=UTC_tz, tz_opt_default=UTC_tz) m.full_clean() m = TestModel.objects.get(pk=m.pk) self.assertEqual(m.tz, UTC_tz) self.assertEqual(m.tz_opt, UTC_tz) self.assertEqual(m.tz_opt_default, UTC_tz) def test_valid_UTC_string(self): m = TestModel.objects.create(tz=UTC, tz_opt=UTC, tz_opt_default=UTC) m.full_clean() m = TestModel.objects.get(pk=m.pk) self.assertEqual(m.tz, UTC_tz) self.assertEqual(m.tz_opt, UTC_tz) self.assertEqual(m.tz_opt_default, UTC_tz) def test_valid_default_values(self): m = TestModel.objects.create(tz=UTC_tz) m.full_clean() m = TestModel.objects.get(pk=m.pk) self.assertEqual(m.tz_opt, None) self.assertEqual(m.tz_opt_default, PST_tz) def test_valid_default_values_without_saving_to_db(self): m = TestModel(tz=UTC_tz) m.full_clean() self.assertEqual(m.tz_opt, None) self.assertEqual(m.tz_opt_default, PST_tz) def test_valid_blank_str(self): m = TestModel.objects.create(tz=PST, tz_opt='') m.full_clean() m = TestModel.objects.get(pk=m.pk) self.assertEqual(m.tz_opt, None) def test_valid_blank_none(self): m = TestModel.objects.create(tz=PST, tz_opt=None) m.full_clean() m = TestModel.objects.get(pk=m.pk) self.assertEqual(m.tz_opt, None) def test_string_value_lookup(self): TestModel.objects.create(tz=PST) qs = TestModel.objects.filter(tz=PST) self.assertEqual(qs.count(), 1) def test_tz_value_lookup(self): TestModel.objects.create(tz=PST) qs = TestModel.objects.filter(tz=PST_tz) self.assertEqual(qs.count(), 1) def test_invalid_blank(self): m = TestModel() self.assertRaises(ValidationError, m.full_clean) def test_invalid_blank_str(self): m = TestModel(tz='') self.assertRaises(ValidationError, m.full_clean) def test_invalid_blank_none(self): m = TestModel(tz=None) self.assertRaises(ValidationError, m.full_clean) def test_invalid_choice(self): m = TestModel(tz=INVALID_TZ) self.assertRaises(ValidationError, m.full_clean) m = TestModel(tz=4) self.assertRaises(ValidationError, m.full_clean) m = TestModel(tz=object()) self.assertRaises(ValidationError, m.full_clean) def test_some_positional_args_ok(self): TimeZoneField('a verbose name', 'a name', True) def test_too_many_positional_args_not_ok(self): def createField(): TimeZoneField('a verbose name', 'a name', True, 42) self.assertRaises(ValueError, createField) def test_default_human_readable_choices_dont_have_underscores(self): m = TestModel(tz=PST_tz) self.assertEqual(m.get_tz_display(), 'America/Los Angeles') class TimeZoneFieldLimitedChoicesTestCase(TestCase): invalid_superset_tz = 'not a tz' invalid_subset_tz = 'Europe/Brussels' class TestModelChoice(models.Model): tz_superset = TimeZoneField( choices=[(tz, tz) for tz in pytz.all_timezones], blank=True, ) tz_subset = TimeZoneField( choices=[(tz, tz) for tz in USA_TZS], blank=True, ) class TestModelOldChoiceFormat(models.Model): tz_superset = TimeZoneField( choices=[(pytz.timezone(tz), tz) for tz in pytz.all_timezones], blank=True, ) tz_subset = TimeZoneField( choices=[(pytz.timezone(tz), tz) for tz in USA_TZS], blank=True, ) def test_valid_choice(self): self.TestModelChoice.objects.create(tz_superset=PST, tz_subset=PST) m = self.TestModelChoice.objects.get() self.assertEqual(m.tz_superset, PST_tz) self.assertEqual(m.tz_subset, PST_tz) def test_invalid_choice(self): m = self.TestModelChoice(tz_superset=self.invalid_superset_tz) self.assertRaises(ValidationError, m.full_clean) m = self.TestModelChoice(tz_subset=self.invalid_subset_tz) self.assertRaises(ValidationError, m.full_clean) def test_valid_choice_old_format(self): self.TestModelOldChoiceFormat.objects.create( tz_superset=PST, tz_subset=PST, ) m = self.TestModelOldChoiceFormat.objects.get() self.assertEqual(m.tz_superset, PST_tz) self.assertEqual(m.tz_subset, PST_tz) def test_invalid_choice_old_format(self): m = self.TestModelOldChoiceFormat(tz_superset=self.invalid_superset_tz) self.assertRaises(ValidationError, m.full_clean) m = self.TestModelOldChoiceFormat(tz_subset=self.invalid_subset_tz) self.assertRaises(ValidationError, m.full_clean) class TimeZoneFieldDeconstructTestCase(TestCase): test_fields = ( TimeZoneField(), TimeZoneField(default='UTC'), TimeZoneField(max_length=42), TimeZoneField(choices=[ (pytz.timezone('US/Pacific'), 'US/Pacific'), (pytz.timezone('US/Eastern'), 'US/Eastern'), ]), TimeZoneField(choices=[ (pytz.timezone(b'US/Pacific'), b'US/Pacific'), (pytz.timezone(b'US/Eastern'), b'US/Eastern'), ]), TimeZoneField(choices=[ ('US/Pacific', 'US/Pacific'), ('US/Eastern', 'US/Eastern'), ]), TimeZoneField(choices=[ (b'US/Pacific', b'US/Pacific'), (b'US/Eastern', b'US/Eastern'), ]), ) def test_deconstruct(self): for org_field in self.test_fields: name, path, args, kwargs = org_field.deconstruct() new_field = TimeZoneField(*args, **kwargs) self.assertEqual(org_field.max_length, new_field.max_length) self.assertEqual(org_field.choices, new_field.choices) def test_full_serialization(self): # 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 # replicates https://github.com/mfogel/django-timezone-field/issues/12 for field in self.test_fields: # ensuring the following call doesn't throw an error MigrationWriter.serialize(field) def test_from_db_value(self): """ Verify that the field can handle data coming back as bytes from the db. """ field = TimeZoneField() # django 1.11 signuature value = field.from_db_value(b'UTC', None, None, None) self.assertEqual(pytz.UTC, value) # django 2.0+ signuature value = field.from_db_value(b'UTC', None, None) self.assertEqual(pytz.UTC, value) def test_default_kwargs_not_frozen(self): """ Ensure the deconstructed representation of the field does not contain kwargs if they match the default. Don't want to bloat everyone's migration files. """ field = TimeZoneField() 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.replace('_', ' ')) for tz in pytz.common_timezones ] field = TimeZoneField(choices=choices) name, path, args, kwargs = field.deconstruct() self.assertNotIn('choices', kwargs) choices = [(tz, tz.replace('_', ' ')) for tz in pytz.common_timezones] field = TimeZoneField(choices=choices) name, path, args, kwargs = field.deconstruct() self.assertNotIn('choices', kwargs) class GmtOffsetInChoicesTestCase(TestCase): # test timezones out of order, but they should appear in order in result. timezones = [ (pytz.timezone('US/Eastern'), 'US/Eastern'), (pytz.timezone('US/Pacific'), 'US/Pacific'), (pytz.timezone('Asia/Qatar'), 'Asia/Qatar'), (pytz.timezone('Pacific/Fiji'), 'Pacific/Fiji'), (pytz.timezone('Europe/London'), 'Europe/London'), (pytz.timezone('Pacific/Apia'), 'Pacific/Apia'), ] def test_add_gmt_offset_to_choices(self): result = add_gmt_offset_to_choices(self.timezones) expected = [ "GMT-11:00 Pacific/Apia", "GMT-08:00 US/Pacific", "GMT-05:00 US/Eastern", "GMT+00:00 Europe/London", "GMT+03:00 Asia/Qatar", "GMT+13:00 Pacific/Fiji", ] for i in range(len(expected)): self.assertEqual(expected[i], result[i][1]) django-timezone-field-4.0/timezone_field/000077500000000000000000000000001357135276400205425ustar00rootroot00000000000000django-timezone-field-4.0/timezone_field/__init__.py000066400000000000000000000002511357135276400226510ustar00rootroot00000000000000from timezone_field.fields import TimeZoneField from timezone_field.forms import TimeZoneFormField __version__ = '4.0' __all__ = ['TimeZoneField', 'TimeZoneFormField'] django-timezone-field-4.0/timezone_field/fields.py000066400000000000000000000115111357135276400223610ustar00rootroot00000000000000import pytz from django.core.exceptions import ValidationError from django.db import models from django.utils.encoding import force_str from timezone_field.utils import is_pytz_instance, add_gmt_offset_to_choices class TimeZoneField(models.Field): """ Provides database store for pytz timezone objects. Valid inputs: * any instance of pytz.tzinfo.DstTzInfo or pytz.tzinfo.StaticTzInfo * the pytz.UTC singleton * any string that validates against pytz.common_timezones. pytz will be used to build a timezone object from the string. * None and the empty string both represent 'no timezone' Valid outputs: * None * instances of pytz.tzinfo.DstTzInfo and pytz.tzinfo.StaticTzInfo * the pytz.UTC singleton Blank values are stored in the DB as the empty string. Timezones are stored in their string representation. The `choices` kwarg can be specified as a list of either [, ] or [, ]. Internally, it is stored as [, ]. """ 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 = [ (pytz.timezone(tz), tz.replace('_', ' ')) for tz in pytz.common_timezones ] MAX_LENGTH = 63 def __init__(self, *args, **kwargs): # allow some use of positional args up until the args we customize # https://github.com/mfogel/django-timezone-field/issues/42 # https://github.com/django/django/blob/1.11.11/django/db/models/fields/__init__.py#L145 if len(args) > 3: raise ValueError('Cannot specify max_length by positional arg') kwargs.setdefault('choices', self.CHOICES) kwargs.setdefault('max_length', self.MAX_LENGTH) kwargs.setdefault('display_GMT_offset', False) # Choices can be specified in two forms: either # [, ] or [, ] # # The [, ] format is the one we actually # store the choices in memory because of # https://github.com/mfogel/django-timezone-field/issues/24 # # The [, ] 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. choices = kwargs['choices'] if isinstance(choices[0][0], (str, bytes)): kwargs['choices'] = [(pytz.timezone(n1), n2) for n1, n2 in choices] if kwargs['display_GMT_offset']: kwargs['choices'] = add_gmt_offset_to_choices(kwargs['choices']) kwargs.pop('display_GMT_offset', None) super(TimeZoneField, self).__init__(*args, **kwargs) def validate(self, value, model_instance): if not is_pytz_instance(value): raise ValidationError("'%s' is not a pytz timezone object" % value) super(TimeZoneField, self).validate(value, model_instance) def deconstruct(self): name, path, args, kwargs = super(TimeZoneField, self).deconstruct() if kwargs.get('choices') == self.CHOICES: del kwargs['choices'] if kwargs.get('max_length') == self.MAX_LENGTH: del kwargs['max_length'] # django can't decontruct pytz objects, so transform choices # to [, ] 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): return 'CharField' def get_default(self): # allow defaults to be still specified as strings. Allows for easy # serialization into migration files value = super(TimeZoneField, self).get_default() return self._get_python_and_db_repr(value)[0] def from_db_value(self, value, *args): "Convert to pytz timezone object" return self._get_python_and_db_repr(value)[0] def to_python(self, value): "Convert to pytz timezone object" return self._get_python_and_db_repr(value)[0] def get_prep_value(self, value): "Convert to string describing a valid pytz timezone object" return self._get_python_and_db_repr(value)[1] def _get_python_and_db_repr(self, value): "Returns a tuple of (python representation, db representation)" if value is None or value == '': return (None, '') if is_pytz_instance(value): return (value, value.zone) try: return (pytz.timezone(force_str(value)), force_str(value)) except pytz.UnknownTimeZoneError: pass raise ValidationError("Invalid timezone '%s'" % value) django-timezone-field-4.0/timezone_field/forms.py000066400000000000000000000013031357135276400222370ustar00rootroot00000000000000import pytz from django.core.exceptions import ValidationError from django import forms class TimeZoneFormField(forms.TypedChoiceField): def __init__(self, *args, **kwargs): def coerce_to_pytz(val): try: return pytz.timezone(val) except pytz.UnknownTimeZoneError: raise ValidationError("Unknown time zone: '%s'" % val) defaults = { 'coerce': coerce_to_pytz, 'choices': [ (tz, tz.replace('_', ' ')) for tz in pytz.common_timezones ], 'empty_value': None, } defaults.update(kwargs) super(TimeZoneFormField, self).__init__(*args, **defaults) django-timezone-field-4.0/timezone_field/models.py000066400000000000000000000000331357135276400223730ustar00rootroot00000000000000# intentionally left blank django-timezone-field-4.0/timezone_field/utils.py000066400000000000000000000023371357135276400222610ustar00rootroot00000000000000import pytz from datetime import datetime, timedelta, time def is_pytz_instance(value): return value is pytz.UTC or isinstance(value, pytz.tzinfo.BaseTzInfo) def add_gmt_offset_to_choices(timezone_tuple_set): """ Currently timezone choices items show up like this: 'America/New_York' But this function formats the choices to display in this format: GMT-05:00 America/New_York :return: A list of tuples in this format: (, ) """ gmt_timezone = pytz.timezone('Greenwich') time_ref = datetime(2000, 1, 1) time_zero = gmt_timezone.localize(time_ref) _choices = [] for tz, tz_str in timezone_tuple_set: delta = (time_zero - tz.localize(time_ref)).total_seconds() h = (datetime.min + timedelta(seconds=delta.__abs__())).hour gmt_diff = time(h).strftime('%H:%M') pair_one = tz pair_two = "GMT{sign}{gmt_diff} {timezone}".format( sign="-" if delta < 0 else "+", gmt_diff=gmt_diff, timezone=tz_str.replace('_', ' ') ) _choices.append((delta, pair_one, pair_two)) _choices.sort(key=lambda x: x[0]) choices = [(one, two) for zero, one, two in _choices] return choices django-timezone-field-4.0/tox.ini000066400000000000000000000014541357135276400170640ustar00rootroot00000000000000[tox] envlist = coverage-clean, py35-{django22}-{sqlite,postgres}, py36-{django22,django30}-{sqlite,postgres}, py37-{django22,django30}-{sqlite,postgres}, py38-{django22,django30}-{sqlite,postgres}, coverage-report, py38-flake8 [testenv] commands = coverage run --include='*/timezone_field/*' {envbindir}/django-admin.py test tests deps = coverage django22: django>=2.2.8,<2.3 django30: django>=3.0.0,<3.1 postgres: psycopg2-binary setenv = PYTHONPATH = {toxinidir} DJANGO_SETTINGS_MODULE=tests.settings sqlite: TEST_DB_ENGINE=sqlite postgres: TEST_DB_ENGINE=postgres [testenv:py38-flake8] commands = flake8 deps = flake8 [testenv:coverage-clean] commands = coverage erase [testenv:coverage-report] commands = coverage report coverage html