├── timezones ├── models.py ├── templatetags │ ├── __init__.py │ └── timezone_filters.py ├── timezones_tests │ ├── __init__.py │ ├── models.py │ └── tests.py ├── __init__.py ├── zones.py ├── forms.py ├── decorators.py ├── utils.py └── fields.py ├── MANIFEST.in ├── README ├── docs ├── how_to_use.txt └── index.txt ├── setup.py └── LICENSE /timezones/models.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /timezones/templatetags/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include docs * 2 | -------------------------------------------------------------------------------- /timezones/timezones_tests/__init__.py: -------------------------------------------------------------------------------- 1 | # the sad reality of Django app testing. not naming this module "tests" 2 | # because of possible collision of other apps that do a similar thing. -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | This simple app is for localizing datetimes for the user. Timezone handling 2 | can be a bit tricky so the goal is to get rid of the guessing game and provide 3 | a simple interface to localizing datetimes for your users. -------------------------------------------------------------------------------- /timezones/timezones_tests/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | from timezones.fields import TimeZoneField 4 | 5 | 6 | 7 | class Profile(models.Model): 8 | name = models.CharField(max_length=100) 9 | timezone = TimeZoneField() 10 | -------------------------------------------------------------------------------- /docs/how_to_use.txt: -------------------------------------------------------------------------------- 1 | 2 | =========================== 3 | How-to Use django-timezones 4 | =========================== 5 | 6 | To get started using django-timezones make sure you have placed it on the 7 | PYTHONPATH somehow and that you have `pytz`_ installed. 8 | 9 | .. _pytz: http://pypi.python.org/pypi/pytz/ 10 | -------------------------------------------------------------------------------- /timezones/templatetags/timezone_filters.py: -------------------------------------------------------------------------------- 1 | 2 | from django.template import Node 3 | from django.template import Library 4 | 5 | from timezones.utils import localtime_for_timezone 6 | 7 | register = Library() 8 | 9 | def localtime(value, timezone): 10 | return localtime_for_timezone(value, timezone) 11 | register.filter("localtime", localtime) 12 | 13 | -------------------------------------------------------------------------------- /docs/index.txt: -------------------------------------------------------------------------------- 1 | ################ 2 | django-timezones 3 | ################ 4 | 5 | This simple app is for localizing datetimes for the user. Timezone handling 6 | can be a bit tricky so the goal is to get rid of the guessing game and provide 7 | a simple interface to localizing datetimes for your users. 8 | 9 | Contents: 10 | 11 | .. toctree:: 12 | 13 | how_to_use -------------------------------------------------------------------------------- /timezones/__init__.py: -------------------------------------------------------------------------------- 1 | VERSION = (0, 2, 0, "f") # following PEP 386 2 | DEV_N = None 3 | 4 | 5 | def get_version(): 6 | version = "%s.%s" % (VERSION[0], VERSION[1]) 7 | if VERSION[2]: 8 | version = "%s.%s" % (version, VERSION[2]) 9 | if VERSION[3] != "f": 10 | version = "%s%s%s" % (version, VERSION[3], VERSION[4]) 11 | if DEV_N: 12 | version = "%s.dev%s" % (version, DEV_N) 13 | return version 14 | 15 | 16 | __version__ = get_version() 17 | -------------------------------------------------------------------------------- /timezones/zones.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | import pytz 4 | 5 | 6 | 7 | ALL_TIMEZONE_CHOICES = tuple(zip(pytz.all_timezones, pytz.all_timezones)) 8 | COMMON_TIMEZONE_CHOICES = tuple(zip(pytz.common_timezones, pytz.common_timezones)) 9 | PRETTY_TIMEZONE_CHOICES = [] 10 | 11 | for tz in pytz.common_timezones: 12 | now = datetime.now(pytz.timezone(tz)) 13 | ofs = now.strftime("%z") 14 | PRETTY_TIMEZONE_CHOICES.append((int(ofs), tz, "(GMT%s) %s" % (ofs, tz))) 15 | PRETTY_TIMEZONE_CHOICES.sort() 16 | for i in xrange(len(PRETTY_TIMEZONE_CHOICES)): 17 | PRETTY_TIMEZONE_CHOICES[i] = PRETTY_TIMEZONE_CHOICES[i][1:] 18 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup( 4 | name = "django-timezones", 5 | version = __import__("timezones").__version__, 6 | author = "Brian Rosner", 7 | author_email = "brosner@gmail.com", 8 | description = "A Django reusable app to deal with timezone localization for users", 9 | long_description = open("README").read(), 10 | url = "http://github.com/brosner/django-timezones/", 11 | license = "MIT", 12 | packages = [ 13 | "timezones", 14 | "timezones.templatetags", 15 | ], 16 | classifiers = [ 17 | "Development Status :: 4 - Beta", 18 | "Environment :: Web Environment", 19 | "Intended Audience :: Developers", 20 | "License :: OSI Approved :: BSD License", 21 | "Operating System :: OS Independent", 22 | "Programming Language :: Python", 23 | "Topic :: Utilities", 24 | "Framework :: Django", 25 | ] 26 | ) 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2014 Brian Rosner 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /timezones/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from django.conf import settings 3 | 4 | import pytz 5 | 6 | from timezones import zones 7 | from timezones.utils import adjust_datetime_to_timezone, coerce_timezone_value 8 | 9 | 10 | 11 | class TimeZoneField(forms.TypedChoiceField): 12 | def __init__(self, *args, **kwargs): 13 | if not "choices" in kwargs: 14 | kwargs["choices"] = zones.PRETTY_TIMEZONE_CHOICES 15 | kwargs["coerce"] = coerce_timezone_value 16 | super(TimeZoneField, self).__init__(*args, **kwargs) 17 | 18 | 19 | class LocalizedDateTimeField(forms.DateTimeField): 20 | """ 21 | Converts the datetime from the user timezone to settings.TIME_ZONE. 22 | """ 23 | def __init__(self, timezone=None, *args, **kwargs): 24 | super(LocalizedDateTimeField, self).__init__(*args, **kwargs) 25 | self.timezone = timezone or settings.TIME_ZONE 26 | 27 | def clean(self, value): 28 | value = super(LocalizedDateTimeField, self).clean(value) 29 | if value is None: # field was likely not required 30 | return None 31 | return adjust_datetime_to_timezone(value, from_tz=self.timezone) 32 | -------------------------------------------------------------------------------- /timezones/decorators.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.utils.encoding import smart_str 3 | 4 | import pytz 5 | 6 | 7 | 8 | default_tz = pytz.timezone(getattr(settings, "TIME_ZONE", "UTC")) 9 | 10 | 11 | 12 | def localdatetime(field_name): 13 | def get_datetime(instance): 14 | return getattr(instance, field_name) 15 | def set_datetime(instance, value): 16 | return setattr(instance, field_name, value) 17 | def make_local_property(get_tz): 18 | def get_local(instance): 19 | tz = get_tz(instance) 20 | if not hasattr(tz, "localize"): 21 | tz = pytz.timezone(smart_str(tz)) 22 | dt = get_datetime(instance) 23 | if dt.tzinfo is None: 24 | dt = default_tz.localize(dt) 25 | return dt.astimezone(tz) 26 | def set_local(instance, dt): 27 | if dt.tzinfo is None: 28 | tz = get_tz(instance) 29 | if not hasattr(tz, "localize"): 30 | tz = pytz.timezone(smart_str(tz)) 31 | dt = tz.localize(dt) 32 | dt = dt.astimezone(default_tz) 33 | return set_datetime(instance, dt) 34 | return property(get_local, set_local) 35 | return make_local_property 36 | -------------------------------------------------------------------------------- /timezones/utils.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.core.exceptions import ValidationError 3 | from django.utils.encoding import smart_str 4 | 5 | import pytz 6 | 7 | 8 | def localtime_for_timezone(value, timezone): 9 | """ 10 | Given a ``datetime.datetime`` object in UTC and a timezone represented as 11 | a string, return the localized time for the timezone. 12 | """ 13 | return adjust_datetime_to_timezone(value, settings.TIME_ZONE, timezone) 14 | 15 | 16 | def adjust_datetime_to_timezone(value, from_tz, to_tz=None): 17 | """ 18 | Given a ``datetime`` object adjust it according to the from_tz timezone 19 | string into the to_tz timezone string. 20 | """ 21 | if to_tz is None: 22 | to_tz = settings.TIME_ZONE 23 | if value.tzinfo is None: 24 | if not hasattr(from_tz, "localize"): 25 | from_tz = pytz.timezone(smart_str(from_tz)) 26 | value = from_tz.localize(value) 27 | return value.astimezone(pytz.timezone(smart_str(to_tz))) 28 | 29 | 30 | def coerce_timezone_value(value): 31 | try: 32 | return pytz.timezone(value) 33 | except pytz.UnknownTimeZoneError: 34 | raise ValidationError("Unknown timezone") 35 | 36 | 37 | def validate_timezone_max_length(max_length, zones): 38 | def reducer(x, y): 39 | return x and (len(y) <= max_length) 40 | if not reduce(reducer, zones, True): 41 | raise Exception("timezones.fields.TimeZoneField MAX_TIMEZONE_LENGTH is too small") 42 | -------------------------------------------------------------------------------- /timezones/timezones_tests/tests.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from datetime import datetime 4 | 5 | import pytz 6 | 7 | from django import forms 8 | from django.conf import settings 9 | from django.test import TestCase 10 | 11 | import timezones.forms 12 | import timezones.timezones_tests.models as test_models 13 | 14 | from timezones.utils import localtime_for_timezone, adjust_datetime_to_timezone 15 | 16 | 17 | 18 | class TimeZoneTestCase(TestCase): 19 | 20 | def setUp(self): 21 | # ensure UTC 22 | self.ORIGINAL_TIME_ZONE = settings.TIME_ZONE 23 | settings.TIME_ZONE = "UTC" 24 | 25 | def tearDown(self): 26 | settings.TIME_ZONE = self.ORIGINAL_TIME_ZONE 27 | 28 | # little helpers 29 | 30 | def assertFormIsValid(self, form): 31 | is_valid = form.is_valid() 32 | self.assert_(is_valid, 33 | "Form did not validate (errors=%r, form=%r)" % (form._errors, form) 34 | ) 35 | 36 | 37 | class UtilsTestCase(TimeZoneTestCase): 38 | 39 | def test_localtime_for_timezone(self): 40 | self.assertEqual( 41 | localtime_for_timezone( 42 | datetime(2008, 6, 25, 18, 0, 0), "America/Denver" 43 | ).strftime("%m/%d/%Y %H:%M:%S"), 44 | "06/25/2008 12:00:00" 45 | ) 46 | 47 | def test_adjust_datetime_to_timezone(self): 48 | self.assertEqual( 49 | adjust_datetime_to_timezone( 50 | datetime(2008, 6, 25, 18, 0, 0), "UTC" 51 | ).strftime("%m/%d/%Y %H:%M:%S"), 52 | "06/25/2008 18:00:00" 53 | ) 54 | 55 | 56 | class TimeZoneFieldTestCase(TimeZoneTestCase): 57 | 58 | def test_forms_clean_required(self): 59 | f = timezones.forms.TimeZoneField() 60 | self.assertEqual( 61 | repr(f.clean("US/Eastern")), 62 | "" 63 | ) 64 | self.assertRaises(forms.ValidationError, f.clean, "") 65 | 66 | def test_forms_clean_not_required(self): 67 | f = timezones.forms.TimeZoneField(required=False) 68 | self.assertEqual( 69 | repr(f.clean("US/Eastern")), 70 | "" 71 | ) 72 | self.assertEqual(f.clean(""), "") 73 | 74 | def test_forms_clean_bad_value(self): 75 | f = timezones.forms.TimeZoneField() 76 | try: 77 | f.clean("BAD VALUE") 78 | except forms.ValidationError, e: 79 | self.assertEqual(e.messages, ["Select a valid choice. BAD VALUE is not one of the available choices."]) 80 | 81 | def test_models_as_a_form(self): 82 | class ProfileForm(forms.ModelForm): 83 | class Meta: 84 | model = test_models.Profile 85 | form = ProfileForm() 86 | rendered = form.as_p() 87 | self.assert_( 88 | bool(re.search(r'', rendered)), 89 | "Did not find pattern in rendered form" 90 | ) 91 | 92 | def test_models_modelform_validation(self): 93 | class ProfileForm(forms.ModelForm): 94 | class Meta: 95 | model = test_models.Profile 96 | form = ProfileForm({"name": "Brian Rosner", "timezone": "America/Denver"}) 97 | self.assertFormIsValid(form) 98 | 99 | def test_models_modelform_save(self): 100 | class ProfileForm(forms.ModelForm): 101 | class Meta: 102 | model = test_models.Profile 103 | form = ProfileForm({"name": "Brian Rosner", "timezone": "America/Denver"}) 104 | self.assertFormIsValid(form) 105 | p = form.save() 106 | 107 | def test_models_string_value(self): 108 | p = test_models.Profile(name="Brian Rosner", timezone="America/Denver") 109 | p.save() 110 | p = test_models.Profile.objects.get(pk=p.pk) 111 | self.assertEqual(p.timezone, pytz.timezone("America/Denver")) 112 | 113 | def test_models_string_value_lookup(self): 114 | test_models.Profile(name="Brian Rosner", timezone="America/Denver").save() 115 | qs = test_models.Profile.objects.filter(timezone="America/Denver") 116 | self.assertEqual(qs.count(), 1) 117 | 118 | def test_models_tz_value(self): 119 | tz = pytz.timezone("America/Denver") 120 | p = test_models.Profile(name="Brian Rosner", timezone=tz) 121 | p.save() 122 | p = test_models.Profile.objects.get(pk=p.pk) 123 | self.assertEqual(p.timezone, tz) 124 | 125 | def test_models_tz_value_lookup(self): 126 | test_models.Profile(name="Brian Rosner", timezone="America/Denver").save() 127 | qs = test_models.Profile.objects.filter(timezone=pytz.timezone("America/Denver")) 128 | self.assertEqual(qs.count(), 1) 129 | 130 | 131 | class LocalizedDateTimeFieldTestCase(TimeZoneTestCase): 132 | 133 | def test_forms_clean_required(self): 134 | # the default case where no timezone is given explicitly. uses settings.TIME_ZONE. 135 | f = timezones.forms.LocalizedDateTimeField() 136 | self.assertEqual( 137 | repr(f.clean("2008-05-30 14:30:00")), 138 | "datetime.datetime(2008, 5, 30, 14, 30, tzinfo=)" 139 | ) 140 | self.assertRaises(forms.ValidationError, f.clean, "") 141 | 142 | def test_forms_clean_required(self): 143 | # the default case where no timezone is given explicitly. uses settings.TIME_ZONE. 144 | f = timezones.forms.LocalizedDateTimeField(required=False) 145 | self.assertEqual( 146 | repr(f.clean("2008-05-30 14:30:00")), 147 | "datetime.datetime(2008, 5, 30, 14, 30, tzinfo=)" 148 | ) 149 | self.assertEqual(f.clean(""), None) 150 | 151 | 152 | # @@@ old doctests that have not been finished (largely due to needing to 153 | # better understand how these bits were created and use-cases) 154 | NOT_USED = {"API_TESTS": r""" 155 | >>> class Foo(object): 156 | ... datetime = datetime(2008, 6, 20, 23, 58, 17) 157 | ... @decorators.localdatetime('datetime') 158 | ... def localdatetime(self): 159 | ... return 'Australia/Lindeman' 160 | ... 161 | >>> foo = Foo() 162 | >>> foo.datetime 163 | datetime.datetime(2008, 6, 20, 23, 58, 17) 164 | >>> foo.localdatetime 165 | datetime.datetime(2008, 6, 21, 9, 58, 17, tzinfo=) 166 | >>> foo.localdatetime = datetime(2008, 6, 12, 23, 50, 0) 167 | >>> foo.datetime 168 | datetime.datetime(2008, 6, 12, 13, 50, tzinfo=) 169 | >>> foo.localdatetime 170 | datetime.datetime(2008, 6, 12, 23, 50, tzinfo=) 171 | """} -------------------------------------------------------------------------------- /timezones/fields.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.db import models 3 | from django.db.models import signals 4 | from django.utils.encoding import smart_unicode, smart_str 5 | 6 | import pytz 7 | 8 | from timezones import forms, zones 9 | from timezones.utils import coerce_timezone_value, validate_timezone_max_length 10 | 11 | 12 | 13 | MAX_TIMEZONE_LENGTH = getattr(settings, "MAX_TIMEZONE_LENGTH", 100) 14 | default_tz = pytz.timezone(getattr(settings, "TIME_ZONE", "UTC")) 15 | 16 | 17 | class TimeZoneField(models.CharField): 18 | 19 | __metaclass__ = models.SubfieldBase 20 | 21 | def __init__(self, *args, **kwargs): 22 | validate_timezone_max_length(MAX_TIMEZONE_LENGTH, zones.ALL_TIMEZONE_CHOICES) 23 | defaults = { 24 | "max_length": MAX_TIMEZONE_LENGTH, 25 | "default": settings.TIME_ZONE, 26 | "choices": zones.PRETTY_TIMEZONE_CHOICES 27 | } 28 | defaults.update(kwargs) 29 | return super(TimeZoneField, self).__init__(*args, **defaults) 30 | 31 | def validate(self, value, model_instance): 32 | # coerce value back to a string to validate correctly 33 | return super(TimeZoneField, self).validate(smart_str(value), model_instance) 34 | 35 | def run_validators(self, value): 36 | # coerce value back to a string to validate correctly 37 | return super(TimeZoneField, self).run_validators(smart_str(value)) 38 | 39 | def to_python(self, value): 40 | value = super(TimeZoneField, self).to_python(value) 41 | if value is None: 42 | return None # null=True 43 | return coerce_timezone_value(value) 44 | 45 | def get_prep_value(self, value): 46 | if value is not None: 47 | return smart_unicode(value) 48 | return value 49 | 50 | def get_db_prep_save(self, value, connection=None): 51 | """ 52 | Prepares the given value for insertion into the database. 53 | """ 54 | return self.get_prep_value(value) 55 | 56 | def flatten_data(self, follow, obj=None): 57 | value = self._get_val_from_obj(obj) 58 | if value is None: 59 | value = "" 60 | return {self.attname: smart_unicode(value)} 61 | 62 | 63 | class LocalizedDateTimeField(models.DateTimeField): 64 | """ 65 | A model field that provides automatic localized timezone support. 66 | timezone can be a timezone string, a callable (returning a timezone string), 67 | or a queryset keyword relation for the model, or a pytz.timezone() 68 | result. 69 | """ 70 | def __init__(self, verbose_name=None, name=None, timezone=None, **kwargs): 71 | if isinstance(timezone, basestring): 72 | timezone = smart_str(timezone) 73 | if timezone in pytz.all_timezones_set: 74 | self.timezone = pytz.timezone(timezone) 75 | else: 76 | self.timezone = timezone 77 | super(LocalizedDateTimeField, self).__init__(verbose_name, name, **kwargs) 78 | 79 | def formfield(self, **kwargs): 80 | defaults = {"form_class": forms.LocalizedDateTimeField} 81 | if (not isinstance(self.timezone, basestring) and str(self.timezone) in pytz.all_timezones_set): 82 | defaults["timezone"] = str(self.timezone) 83 | defaults.update(kwargs) 84 | return super(LocalizedDateTimeField, self).formfield(**defaults) 85 | 86 | def get_db_prep_save(self, value, connection=None): 87 | """ 88 | Returns field's value prepared for saving into a database. 89 | """ 90 | ## convert to settings.TIME_ZONE 91 | if value is not None: 92 | if value.tzinfo is None: 93 | value = default_tz.localize(value) 94 | else: 95 | value = value.astimezone(default_tz) 96 | return super(LocalizedDateTimeField, self).get_db_prep_save(value, connection=connection) 97 | 98 | def get_db_prep_lookup(self, lookup_type, value, connection=None, prepared=None): 99 | """ 100 | Returns field's value prepared for database lookup. 101 | """ 102 | ## convert to settings.TIME_ZONE 103 | if value.tzinfo is None: 104 | value = default_tz.localize(value) 105 | else: 106 | value = value.astimezone(default_tz) 107 | return super(LocalizedDateTimeField, self).get_db_prep_lookup(lookup_type, value, connection=connection, prepared=prepared) 108 | 109 | 110 | def prep_localized_datetime(sender, **kwargs): 111 | for field in sender._meta.fields: 112 | if not isinstance(field, LocalizedDateTimeField) or field.timezone is None: 113 | continue 114 | dt_field_name = "_datetimezone_%s" % field.attname 115 | def get_dtz_field(instance): 116 | return getattr(instance, dt_field_name) 117 | def set_dtz_field(instance, dt): 118 | if dt.tzinfo is None: 119 | dt = default_tz.localize(dt) 120 | time_zone = field.timezone 121 | if isinstance(field.timezone, basestring): 122 | tz_name = instance._default_manager.filter( 123 | pk=model_instance._get_pk_val() 124 | ).values_list(field.timezone)[0][0] 125 | try: 126 | time_zone = pytz.timezone(tz_name) 127 | except: 128 | time_zone = default_tz 129 | if time_zone is None: 130 | # lookup failed 131 | time_zone = default_tz 132 | #raise pytz.UnknownTimeZoneError( 133 | # "Time zone %r from relation %r was not found" 134 | # % (tz_name, field.timezone) 135 | #) 136 | elif callable(time_zone): 137 | tz_name = time_zone() 138 | if isinstance(tz_name, basestring): 139 | try: 140 | time_zone = pytz.timezone(tz_name) 141 | except: 142 | time_zone = default_tz 143 | else: 144 | time_zone = tz_name 145 | if time_zone is None: 146 | # lookup failed 147 | time_zone = default_tz 148 | #raise pytz.UnknownTimeZoneError( 149 | # "Time zone %r from callable %r was not found" 150 | # % (tz_name, field.timezone) 151 | #) 152 | setattr(instance, dt_field_name, dt.astimezone(time_zone)) 153 | setattr(sender, field.attname, property(get_dtz_field, set_dtz_field)) 154 | 155 | ## RED_FLAG: need to add a check at manage.py validation time that 156 | ## time_zone value is a valid query keyword (if it is one) 157 | signals.class_prepared.connect(prep_localized_datetime) 158 | --------------------------------------------------------------------------------