├── VERSION ├── example ├── __init__.py ├── requirements.txt ├── templates │ └── index.html ├── urls.py ├── manage.py ├── README.rst └── settings.py ├── tz_detect ├── __init__.py ├── models.py ├── templatetags │ ├── __init__.py │ └── tz_detect.py ├── urls.py ├── defaults.py ├── static │ └── tz_detect │ │ └── js │ │ ├── tzdetect.min.js │ │ └── tzdetect.js ├── templates │ └── tz_detect │ │ └── detector.html ├── views.py ├── middleware.py ├── utils.py └── tests.py ├── requirements ├── dev.txt ├── py310-django32.txt ├── py310-django40.txt ├── py37-django22.txt ├── py37-django30.txt ├── py37-django31.txt ├── py37-django32.txt ├── py38-django22.txt ├── py38-django30.txt ├── py38-django31.txt ├── py38-django32.txt ├── py38-django40.txt ├── py39-django22.txt ├── py39-django30.txt ├── py39-django31.txt ├── py39-django32.txt └── py39-django40.txt ├── pytest.ini ├── MANIFEST.in ├── .github ├── dependabot.yml └── workflows │ └── develop.yml ├── tox.ini ├── test_settings.py ├── CONTRIBUTING.md ├── .gitignore ├── LICENSE ├── setup.py ├── README.rst └── CHANGES.txt /VERSION: -------------------------------------------------------------------------------- 1 | 0.5.0 2 | -------------------------------------------------------------------------------- /example/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tz_detect/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tz_detect/models.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tz_detect/templatetags/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /example/requirements.txt: -------------------------------------------------------------------------------- 1 | django>=1.10 2 | pytz 3 | -------------------------------------------------------------------------------- /requirements/dev.txt: -------------------------------------------------------------------------------- 1 | build 2 | tox 3 | tox-py 4 | twine 5 | wheel 6 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | DJANGO_SETTINGS_MODULE = test_settings 3 | testpaths = 4 | tz_detect 5 | 6 | -------------------------------------------------------------------------------- /requirements/py310-django32.txt: -------------------------------------------------------------------------------- 1 | coverage==6.3 2 | django==3.2.11 3 | pytest==6.2.5 4 | pytest-django==4.5.2 5 | -------------------------------------------------------------------------------- /requirements/py310-django40.txt: -------------------------------------------------------------------------------- 1 | coverage==6.3 2 | django==4.0.1 3 | pytest==6.2.5 4 | pytest-django==4.5.2 5 | -------------------------------------------------------------------------------- /requirements/py37-django22.txt: -------------------------------------------------------------------------------- 1 | coverage==6.3 2 | django==2.2.26 3 | pytest==6.2.5 4 | pytest-django==4.5.2 5 | -------------------------------------------------------------------------------- /requirements/py37-django30.txt: -------------------------------------------------------------------------------- 1 | coverage==6.3 2 | django==3.0.14 3 | pytest==6.2.5 4 | pytest-django==4.5.2 5 | -------------------------------------------------------------------------------- /requirements/py37-django31.txt: -------------------------------------------------------------------------------- 1 | coverage==6.3 2 | django==3.1.14 3 | pytest==6.2.5 4 | pytest-django==4.5.2 5 | -------------------------------------------------------------------------------- /requirements/py37-django32.txt: -------------------------------------------------------------------------------- 1 | coverage==6.3 2 | django==3.2.11 3 | pytest==6.2.5 4 | pytest-django==4.5.2 5 | -------------------------------------------------------------------------------- /requirements/py38-django22.txt: -------------------------------------------------------------------------------- 1 | coverage==6.3 2 | django==2.2.26 3 | pytest==6.2.5 4 | pytest-django==4.5.2 5 | -------------------------------------------------------------------------------- /requirements/py38-django30.txt: -------------------------------------------------------------------------------- 1 | coverage==6.3 2 | django==3.0.14 3 | pytest==6.2.5 4 | pytest-django==4.5.2 5 | -------------------------------------------------------------------------------- /requirements/py38-django31.txt: -------------------------------------------------------------------------------- 1 | coverage==6.3 2 | django==3.1.14 3 | pytest==6.2.5 4 | pytest-django==4.5.2 5 | -------------------------------------------------------------------------------- /requirements/py38-django32.txt: -------------------------------------------------------------------------------- 1 | coverage==6.3 2 | django==3.2.11 3 | pytest==6.2.5 4 | pytest-django==4.5.2 5 | -------------------------------------------------------------------------------- /requirements/py38-django40.txt: -------------------------------------------------------------------------------- 1 | coverage==6.3 2 | django==4.0.1 3 | pytest==6.2.5 4 | pytest-django==4.5.2 5 | -------------------------------------------------------------------------------- /requirements/py39-django22.txt: -------------------------------------------------------------------------------- 1 | coverage==6.3 2 | django==2.2.26 3 | pytest==6.2.5 4 | pytest-django==4.5.2 5 | -------------------------------------------------------------------------------- /requirements/py39-django30.txt: -------------------------------------------------------------------------------- 1 | coverage==6.3 2 | django==3.0.14 3 | pytest==6.2.5 4 | pytest-django==4.5.2 5 | -------------------------------------------------------------------------------- /requirements/py39-django31.txt: -------------------------------------------------------------------------------- 1 | coverage==6.3 2 | django==3.1.14 3 | pytest==6.2.5 4 | pytest-django==4.5.2 5 | -------------------------------------------------------------------------------- /requirements/py39-django32.txt: -------------------------------------------------------------------------------- 1 | coverage==6.3 2 | django==3.2.11 3 | pytest==6.2.5 4 | pytest-django==4.5.2 5 | -------------------------------------------------------------------------------- /requirements/py39-django40.txt: -------------------------------------------------------------------------------- 1 | coverage==6.3 2 | django==4.0.1 3 | pytest==6.2.5 4 | pytest-django==4.5.2 5 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include CHANGES.txt 2 | include LICENSE 3 | include README.rst 4 | recursive-include tz_detect/static * 5 | recursive-include tz_detect/templates * 6 | include VERSION 7 | -------------------------------------------------------------------------------- /tz_detect/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from .views import SetOffsetView 4 | 5 | urlpatterns = [ 6 | path("set/", SetOffsetView.as_view(), name="tz_detect__set"), 7 | ] 8 | -------------------------------------------------------------------------------- /example/templates/index.html: -------------------------------------------------------------------------------- 1 | {% load tz_detect %} 2 | 3 | 4 | 5 | It is {% now "jS F Y H:i" %} 6 | {% tz_detect %} 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from django.urls import include 3 | from django.views.generic import TemplateView 4 | 5 | urlpatterns = [ 6 | path("tz_detect/", include("tz_detect.urls")), 7 | path("", TemplateView.as_view(template_name="index.html")), 8 | ] 9 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "pip" 4 | directory: "/" 5 | schedule: 6 | interval: "monthly" 7 | # The requirements files used by tox are designed to test different versions 8 | # of Django on purpose. Dependabot is noise for this package's use case. 9 | # Setting the limit of open pull requests to zero is the documented way 10 | # to disable Dependabot. 11 | open-pull-requests-limit: 0 12 | -------------------------------------------------------------------------------- /example/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "example.settings") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | # Allow starting the app without installing the module. 11 | sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.realpath(__file__)))) 12 | 13 | execute_from_command_line(sys.argv) 14 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = 3 | py37-django{22,30,31,32} 4 | py38-django{22,30,31,32,40} 5 | py39-django{22,30,31,32,40} 6 | py310-django{32,40} 7 | 8 | [testenv] 9 | commands = 10 | python \ 11 | -W error::ResourceWarning \ 12 | -W error::DeprecationWarning \ 13 | -W error::PendingDeprecationWarning \ 14 | -m coverage run \ 15 | -m pytest {posargs:tz_detect/tests.py} 16 | deps = -r requirements/{envname}.txt 17 | setenv = 18 | PYTHONDEVMODE=1 19 | -------------------------------------------------------------------------------- /tz_detect/defaults.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | 3 | # These countries will be prioritised in the search 4 | # for a matching timezone. Consider putting your 5 | # app's most popular countries first. 6 | # Defaults to top Internet using countries. 7 | 8 | TZ_DETECT_COUNTRIES = getattr(settings, "TZ_DETECT_COUNTRIES", ("CN", "US", "IN", "JP", "BR", "RU", "DE", "FR", "GB")) 9 | 10 | 11 | # Session key to use to store the detected timezone 12 | TZ_SESSION_KEY = getattr(settings, "TZ_SESSION_KEY", "detected_tz") 13 | -------------------------------------------------------------------------------- /tz_detect/templatetags/tz_detect.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | from django.conf import settings 3 | 4 | from ..utils import convert_header_name 5 | 6 | register = template.Library() 7 | 8 | 9 | @register.inclusion_tag("tz_detect/detector.html", takes_context=True) 10 | def tz_detect(context, **script_attrs): 11 | return { 12 | "show": not hasattr(context.get("request"), "timezone_active"), 13 | "debug": getattr(settings, "DEBUG", False), 14 | "csrf_header_name": convert_header_name(getattr(settings, "CSRF_HEADER_NAME", "HTTP_X_CSRFTOKEN")), 15 | "script_attrs": script_attrs, 16 | } 17 | -------------------------------------------------------------------------------- /test_settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | SECRET_KEY = "h_ekayhzss(0lzsacd5cat7d=pu#51sh3w&uqn#tz26vuq4" 4 | 5 | DATABASES = {"default": {"ENGINE": "django.db.backends.sqlite3", "NAME": ":memory:"}} 6 | 7 | INSTALLED_APPS = [ 8 | "django.contrib.sites", 9 | "django.contrib.sessions", 10 | "django.contrib.contenttypes", 11 | "tz_detect", 12 | ] 13 | 14 | MIDDLEWARE_CLASSES = [ 15 | "django.contrib.sessions.middleware.SessionMiddleware", 16 | "django.middleware.common.CommonMiddleware", 17 | "tz_detect.middleware.TimezoneMiddleware", 18 | ] 19 | 20 | MIDDLEWARE = MIDDLEWARE_CLASSES 21 | 22 | SITE_ID = 1 23 | 24 | USE_TZ = True 25 | -------------------------------------------------------------------------------- /example/README.rst: -------------------------------------------------------------------------------- 1 | Example 2 | ======= 3 | 4 | To run the example application, make sure you have the required 5 | packages installed. You can do this using following commands : 6 | 7 | .. code-block:: bash 8 | 9 | mkvirtualenv example 10 | pip install -r example/requirements.txt 11 | 12 | This assumes you already have ``virtualenv`` and ``virtualenvwrapper`` 13 | installed and configured. 14 | 15 | Next, you can setup the django instance using : 16 | 17 | .. code-block:: bash 18 | 19 | python example/manage.py syncdb --noinput 20 | 21 | And run it : 22 | 23 | .. code-block:: bash 24 | 25 | python example/manage.py runserver 26 | 27 | Good luck! 28 | -------------------------------------------------------------------------------- /tz_detect/static/tz_detect/js/tzdetect.min.js: -------------------------------------------------------------------------------- 1 | !function(){if((areCookiesEnabled=function(){var e,t=navigator.cookieEnabled;if(!1===t)return!1;if(e=!1===t||!!t,!document.cookie&&!e){if(document.cookie="testcookie=1",!document.cookie)return!1;document.cookie="testcookie=; expires="+new Date(0).toUTCString()}return!0})()){var e,t=(e=null,new XMLHttpRequest);t&&(t.open("post",window.tz_set_endpoint,!0),t.setRequestHeader("Content-type","application/x-www-form-urlencoded"),t.setRequestHeader(window.csrf_header_name,window.csrf_token),t.setRequestHeader("X-Requested-With","XMLHttpRequest"),t.send("offset="+new Date().getTimezoneOffset()+"&timezone="+Intl.DateTimeFormat().resolvedOptions().timeZone))}}(); -------------------------------------------------------------------------------- /tz_detect/templates/tz_detect/detector.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | {% if show %} 3 | 14 | {% endif %} 15 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Setup 2 | 3 | django-tz-detect uses `tox` to run tests. 4 | The following commands show what you need to get started. 5 | 6 | ``` 7 | python3 -m venv venv 8 | source venv/bin/activate 9 | pip install -r requirements/dev.txt 10 | tox --py current 11 | ``` 12 | 13 | # Release checklist 14 | 15 | These are notes for how to make a release. 16 | First, make sure release tools are installed. 17 | 18 | ``` 19 | pip install -r requirements/dev.txt 20 | ``` 21 | 22 | * Update `VERSION` 23 | * Update `CHANGES.txt` 24 | * Update `classifiers` in `setup.py` 25 | * Build package: `python -m build` 26 | * Tag release: `git tag -a -m "Version "` 27 | * Push to GitHub with new tag: `git push --follow-tags` 28 | * Release! `twine upload dist/*` 29 | -------------------------------------------------------------------------------- /tz_detect/views.py: -------------------------------------------------------------------------------- 1 | from django.http import HttpResponse 2 | from django.views.generic import View 3 | 4 | 5 | from .defaults import TZ_SESSION_KEY 6 | 7 | 8 | class SetOffsetView(View): 9 | http_method_names = ["post"] 10 | 11 | def post(self, request, *args, **kwargs): 12 | timezone = request.POST.get("timezone", None) 13 | if timezone: 14 | request.session[TZ_SESSION_KEY] = timezone 15 | else: 16 | offset = request.POST.get("offset", None) 17 | if not offset: 18 | return HttpResponse("No 'offset' parameter provided", status=400) 19 | 20 | try: 21 | offset = int(offset) 22 | except ValueError: 23 | return HttpResponse("Invalid 'offset' value provided", status=400) 24 | 25 | request.session[TZ_SESSION_KEY] = int(offset) 26 | 27 | return HttpResponse("OK") 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | venv/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | 47 | # Translations 48 | *.mo 49 | *.pot 50 | 51 | # Django stuff: 52 | *.log 53 | 54 | # Sphinx documentation 55 | docs/_build/ 56 | 57 | # PyBuilder 58 | target/ 59 | 60 | # Vim 61 | *.sw* 62 | -------------------------------------------------------------------------------- /.github/workflows/develop.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - develop 7 | pull_request: 8 | 9 | concurrency: 10 | group: ${{ github.head_ref || github.run_id }} 11 | cancel-in-progress: true 12 | 13 | jobs: 14 | tests: 15 | name: Python ${{ matrix.python-version }} 16 | runs-on: ubuntu-20.04 17 | 18 | strategy: 19 | matrix: 20 | python-version: 21 | - 3.7 22 | - 3.8 23 | - 3.9 24 | - '3.10' 25 | 26 | steps: 27 | - uses: actions/checkout@v2 28 | 29 | - uses: actions/setup-python@v2 30 | with: 31 | python-version: ${{ matrix.python-version }} 32 | cache: pip 33 | cache-dependency-path: 'requirements/*.txt' 34 | 35 | - name: Install dependencies 36 | run: | 37 | python -m pip install --upgrade pip setuptools wheel 38 | python -m pip install --upgrade tox tox-py 39 | 40 | - name: Run tox targets for ${{ matrix.python-version }} 41 | run: tox --py current 42 | -------------------------------------------------------------------------------- /tz_detect/middleware.py: -------------------------------------------------------------------------------- 1 | import pytz 2 | from django.utils import timezone 3 | from pytz.tzinfo import BaseTzInfo 4 | 5 | from .defaults import TZ_SESSION_KEY 6 | 7 | 8 | try: 9 | from django.utils.deprecation import MiddlewareMixin 10 | except ImportError: # Django < 1.10 11 | MiddlewareMixin = object 12 | 13 | from .utils import offset_to_timezone 14 | 15 | 16 | class TimezoneMiddleware(MiddlewareMixin): 17 | def process_request(self, request): 18 | tz = request.session.get(TZ_SESSION_KEY) 19 | if tz: 20 | # ``request.timezone_active`` is used in the template tag 21 | # to detect if the timezone has been activated 22 | request.timezone_active = True 23 | # for existing sessions storing BaseTzInfo objects 24 | if isinstance(tz, BaseTzInfo): 25 | timezone.activate(tz) 26 | elif isinstance(tz, str): 27 | timezone.activate(pytz.timezone(tz)) 28 | else: 29 | timezone.activate(offset_to_timezone(tz)) 30 | else: 31 | timezone.deactivate() 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2012-2015 Adam Charnock 4 | Copyright (c) 2015 Basil Shubin 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | 24 | -------------------------------------------------------------------------------- /tz_detect/utils.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from itertools import chain 3 | 4 | import pytz 5 | 6 | from .defaults import TZ_DETECT_COUNTRIES 7 | 8 | 9 | def get_prioritized_timezones(): 10 | def tz_gen(): 11 | for c in TZ_DETECT_COUNTRIES: 12 | yield pytz.country_timezones(c) 13 | yield pytz.common_timezones 14 | 15 | return chain.from_iterable(tz_gen()) 16 | 17 | 18 | def offset_to_timezone(offset, now=None): 19 | """Convert a minutes offset (JavaScript-style) into a pytz timezone 20 | 21 | The ``now`` parameter is generally used for testing only 22 | """ 23 | now = now or datetime.now() 24 | 25 | # JS offsets are flipped, so unflip. 26 | user_offset = -offset 27 | 28 | # Helper: timezone offset in minutes 29 | def get_tz_offset(tz): 30 | try: 31 | return tz.utcoffset(now).total_seconds() / 60 32 | except (pytz.NonExistentTimeError, pytz.AmbiguousTimeError): 33 | return tz.localize(now, is_dst=False).utcoffset().total_seconds() / 60 34 | 35 | # Return the timezone with the minimum difference to the user's offset. 36 | return min( 37 | (pytz.timezone(tz_name) for tz_name in get_prioritized_timezones()), 38 | key=lambda tz: abs(get_tz_offset(tz) - user_offset), 39 | ) 40 | 41 | 42 | def convert_header_name(django_header): 43 | """Converts header name from django settings to real header name. 44 | 45 | For example: 46 | 'HTTP_CUSTOM_CSRF' -> 'custom-csrf' 47 | """ 48 | return django_header.lower().replace("_", "-").split("http-")[-1] 49 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import codecs 4 | import os 5 | import re 6 | import subprocess 7 | import sys 8 | 9 | from setuptools import find_packages, setup 10 | 11 | 12 | def read(*parts): 13 | file_path = os.path.join(os.path.dirname(__file__), *parts) 14 | return codecs.open(file_path, encoding="utf-8").read() 15 | 16 | 17 | setup( 18 | name="django-tz-detect", 19 | version=read("VERSION").strip(), 20 | license="MIT License", 21 | install_requires=[ 22 | "django>=2.2", 23 | "pytz", 24 | ], 25 | description="Automatic user timezone detection for django", 26 | long_description=read("README.rst"), 27 | author="Adam Charnock", 28 | author_email="adam@adamcharnock.com", 29 | maintainer="Basil Shubin", 30 | maintainer_email="basil.shubin@gmail.com", 31 | url="http://github.com/adamcharnock/django-tz-detect", 32 | download_url="https://github.com/adamcharnock/django-tz-detect/zipball/master", 33 | packages=find_packages(exclude=("example*", "*.tests*")), 34 | include_package_data=True, 35 | zip_safe=False, 36 | classifiers=[ 37 | "Development Status :: 5 - Production/Stable", 38 | "Environment :: Web Environment", 39 | "Framework :: Django", 40 | "Intended Audience :: Developers", 41 | "License :: OSI Approved :: MIT License", 42 | "Operating System :: OS Independent", 43 | "Programming Language :: Python", 44 | "Programming Language :: Python :: 3", 45 | "Programming Language :: Python :: 3.7", 46 | "Programming Language :: Python :: 3.8", 47 | "Programming Language :: Python :: 3.9", 48 | "Programming Language :: Python :: 3.10", 49 | "Topic :: Internet :: WWW/HTTP", 50 | "Topic :: Internet :: WWW/HTTP :: Dynamic Content", 51 | ], 52 | ) 53 | -------------------------------------------------------------------------------- /tz_detect/static/tz_detect/js/tzdetect.js: -------------------------------------------------------------------------------- 1 | (function(){ 2 | 3 | areCookiesEnabled = function() { 4 | // Credit for this function goes to: http://stackoverflow.com/a/18114024/764723 5 | var cookieEnabled = navigator.cookieEnabled; 6 | var cookieEnabledSupported; 7 | 8 | // When cookieEnabled flag is present and false then cookies are disabled. 9 | if (cookieEnabled === false) { 10 | return false; 11 | } 12 | 13 | // If cookieEnabled is null or undefined then assume the browser 14 | // doesn't support this flag 15 | if (cookieEnabled !== false && !cookieEnabled) { 16 | cookieEnabledSupported = false; 17 | } else { 18 | cookieEnabledSupported = true; 19 | } 20 | 21 | 22 | // try to set a test cookie if we can't see any cookies and we're using 23 | // either a browser that doesn't support navigator.cookieEnabled 24 | // or IE (which always returns true for navigator.cookieEnabled) 25 | if (!document.cookie && (!cookieEnabledSupported || /*@cc_on!@*/false)) { 26 | document.cookie = "testcookie=1"; 27 | 28 | if (!document.cookie) { 29 | return false; 30 | } else { 31 | document.cookie = "testcookie=; expires=" + new Date(0).toUTCString(); 32 | } 33 | } 34 | 35 | return true; 36 | }; 37 | 38 | var createXMLHttp = function() { 39 | var xmlHttp = null; 40 | // Use XMLHttpRequest where available 41 | if (typeof(XMLHttpRequest) !== undefined) { 42 | xmlHttp = new XMLHttpRequest(); 43 | return xmlHttp; 44 | // IE 45 | } else if (window.ActiveXObject) { 46 | var ieXMLHttpVersions = ['MSXML2.XMLHttp.5.0', 'MSXML2.XMLHttp.4.0', 'MSXML2.XMLHttp.3.0', 'MSXML2.XMLHttp', 'Microsoft.XMLHttp']; 47 | for (var i = 0; i < ieXMLHttpVersions.length; i++) { 48 | try { 49 | xmlHttp = new ActiveXObject(ieXMLHttpVersions[i]); 50 | return xmlHttp; 51 | } catch (e) {} 52 | } 53 | } 54 | }; 55 | 56 | if(!areCookiesEnabled()) { 57 | // If cookies are disabled then storing the timezone in the user's 58 | // session is a hopeless task and will trigger a request for each 59 | // page load. Therefore, we shouldn't bother. 60 | return; 61 | } 62 | 63 | var xmlHttp = createXMLHttp(); 64 | if(xmlHttp) { 65 | xmlHttp.open('post', window.tz_set_endpoint, true); 66 | xmlHttp.setRequestHeader('Content-type', 'application/x-www-form-urlencoded'); 67 | xmlHttp.setRequestHeader(window.csrf_header_name, window.csrf_token); 68 | xmlHttp.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); 69 | xmlHttp.send("offset=" + (new Date()).getTimezoneOffset() + '&timezone=' + Intl.DateTimeFormat().resolvedOptions().timeZone); 70 | } 71 | 72 | }()); -------------------------------------------------------------------------------- /example/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for app project. 3 | 4 | For more information on this file, see 5 | https://docs.djangoproject.com/en/1.8/topics/settings/ 6 | 7 | For the full list of settings and their values, see 8 | https://docs.djangoproject.com/en/1.8/ref/settings/ 9 | """ 10 | 11 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 12 | import os 13 | 14 | BASE_DIR = os.path.dirname(os.path.dirname(__file__)) 15 | 16 | 17 | # Quick-start development settings - unsuitable for production 18 | # See https://docs.djangoproject.com/en/1.7/howto/deployment/checklist/ 19 | 20 | # SECURITY WARNING: keep the secret key used in production secret! 21 | SECRET_KEY = "YOUR_SECRET_KEY" 22 | 23 | # SECURITY WARNING: don't run with debug turned on in production! 24 | DEBUG = True 25 | 26 | ALLOWED_HOSTS = [] 27 | 28 | 29 | # Application definition 30 | 31 | PROJECT_APPS = [ 32 | "tz_detect", 33 | ] 34 | 35 | INSTALLED_APPS = [ 36 | "django.contrib.auth", 37 | "django.contrib.sites", 38 | "django.contrib.sessions", 39 | "django.contrib.staticfiles", 40 | "django.contrib.contenttypes", 41 | ] + PROJECT_APPS 42 | 43 | MIDDLEWARE = ( 44 | "django.contrib.sessions.middleware.SessionMiddleware", 45 | "django.middleware.common.CommonMiddleware", 46 | "django.middleware.csrf.CsrfViewMiddleware", 47 | # 'django.contrib.auth.middleware.AuthenticationMiddleware', 48 | # 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', 49 | # 'django.contrib.messages.middleware.MessageMiddleware', 50 | # 'django.middleware.clickjacking.XFrameOptionsMiddleware', 51 | # 'django.middleware.security.SecurityMiddleware', 52 | "tz_detect.middleware.TimezoneMiddleware", 53 | ) 54 | 55 | ROOT_URLCONF = "example.urls" 56 | 57 | SITE_ID = 1 58 | 59 | TEMPLATES = [ 60 | { 61 | "BACKEND": "django.template.backends.django.DjangoTemplates", 62 | "DIRS": [ 63 | os.path.join(os.path.dirname(__file__), "templates"), 64 | ], 65 | "APP_DIRS": True, 66 | "OPTIONS": { 67 | "context_processors": [ 68 | "django.template.context_processors.debug", 69 | "django.template.context_processors.request", 70 | "django.contrib.auth.context_processors.auth", 71 | "django.contrib.messages.context_processors.messages", 72 | ], 73 | }, 74 | }, 75 | ] 76 | 77 | 78 | # Database 79 | # https://docs.djangoproject.com/en/1.7/ref/settings/#databases 80 | 81 | DATABASES = { 82 | "default": { 83 | "ENGINE": "django.db.backends.sqlite3", 84 | "NAME": os.path.join(BASE_DIR, "db.sqlite3"), 85 | } 86 | } 87 | 88 | 89 | # Internationalization 90 | # https://docs.djangoproject.com/en/1.7/topics/i18n/ 91 | 92 | LANGUAGE_CODE = "en-us" 93 | 94 | TIME_ZONE = "UTC" 95 | 96 | USE_I18N = True 97 | 98 | USE_L10N = True 99 | 100 | USE_TZ = True 101 | 102 | 103 | # Static files (CSS, JavaScript, Images) 104 | # https://docs.djangoproject.com/en/1.7/howto/static-files/ 105 | 106 | STATIC_URL = "/static/" 107 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | django-tz-detect 2 | ================ 3 | 4 | .. image:: https://img.shields.io/pypi/v/django-tz-detect.svg 5 | :target: https://pypi.python.org/pypi/django-tz-detect/ 6 | 7 | .. image:: https://img.shields.io/pypi/dm/django-tz-detect.svg 8 | :target: https://pypi.python.org/pypi/django-tz-detect/ 9 | 10 | .. image:: https://img.shields.io/github/license/adamcharnock/django-tz-detect.svg 11 | :target: https://pypi.python.org/pypi/django-tz-detect/ 12 | 13 | .. image:: https://coveralls.io/repos/adamcharnock/django-tz-detect/badge.svg?branch=develop 14 | :target: https://coveralls.io/r/adamcharnock/django-tz-detect?branch=develop 15 | 16 | This app will auto-detect a user's timezone using JavaScript, then 17 | configure Django's timezone localization system accordingly. As a 18 | result, dates shown to users will be in their local timezones. 19 | 20 | Authored by `Adam Charnock `_ (who is available for freelance/contract work), and some great `contributors `_. 21 | 22 | How it works 23 | ------------ 24 | 25 | On the first page view you should find that ``tz_detect`` places a 26 | piece of asynchronous JavaScript code into your page using the 27 | template tag you inserted. The script will obtain the user's GMT 28 | offset using ``getTimezoneOffset``, and post it back to Django. The 29 | offset is stored in the user's session and Django's timezone awareness 30 | is configured in the middleware. 31 | 32 | The JavaScript will not be displayed in future requests. 33 | 34 | Installation 35 | ------------ 36 | 37 | 1. Either checkout ``tz_detect`` from GitHub, or install using pip: 38 | 39 | .. code-block:: bash 40 | 41 | pip install django-tz-detect 42 | 43 | 2. Add ``tz_detect`` to your ``INSTALLED_APPS``: 44 | 45 | .. code-block:: python 46 | 47 | INSTALLED_APPS += ( 48 | 'tz_detect', 49 | ) 50 | 51 | 3. Be sure you have the ``django.template.context_processors.request`` processor 52 | 53 | .. code-block:: python 54 | 55 | TEMPLATES = [ 56 | { 57 | ... 58 | 'OPTIONS': { 59 | 'context_processors': [ 60 | ... 61 | 'django.template.context_processors.request', 62 | ], 63 | }, 64 | }, 65 | ] 66 | 67 | 4. Update your ``urls.py`` file: 68 | 69 | .. code-block:: python 70 | 71 | urlpatterns += [ 72 | path('tz_detect/', include('tz_detect.urls')), 73 | ] 74 | 75 | 5. Add the detection template tag to your site, ideally in your base layout just before the ```` tag: 76 | 77 | .. code-block:: html+django 78 | 79 | {% load tz_detect %} 80 | {% tz_detect %} 81 | 82 | 6. Add ``TimezoneMiddleware`` to ``MIDDLEWARE``: 83 | 84 | .. code-block:: python 85 | 86 | import django 87 | 88 | MIDDLEWARE += ( 89 | 'tz_detect.middleware.TimezoneMiddleware', 90 | ) 91 | 92 | if django.VERSION < (1, 10): 93 | MIDDLEWARE_CLASSES += ( 94 | 'tz_detect.middleware.TimezoneMiddleware', 95 | ) 96 | 97 | 7. (Optional) Configure optional settings 98 | 99 | Set the countries in which your app will be most commonly used: 100 | 101 | .. code-block:: python 102 | 103 | # These countries will be prioritized in the search 104 | # for a matching timezone. Consider putting your 105 | # app's most popular countries first. 106 | # Defaults to the top Internet using countries. 107 | TZ_DETECT_COUNTRIES = ('CN', 'US', 'IN', 'JP', 'BR', 'RU', 'DE', 'FR', 'GB') 108 | 109 | Set the session key that will be used to store the detected timezone 110 | 111 | .. code-block:: python 112 | 113 | # Session key to use, defaults to "detected_tz" 114 | TZ_SESSION_KEY = "my-session-key" 115 | 116 | Please see ``example`` application. This application is used to manually 117 | test the functionalities of this package. This also serves as a good 118 | example. 119 | 120 | You need only Django 1.8 or above to run that. It might run on older 121 | versions but that is not tested. 122 | 123 | Caveats 124 | ------- 125 | 126 | - Django's timezone awareness will not be available on the first page view 127 | - This method requires JavaScript 128 | - Timezone detection is done entirely from the user's GMT offset, not from their location 129 | 130 | Future expansion 131 | ---------------- 132 | 133 | - A hook to allow the timezone to be stored against a user 134 | - Allow timezones to be manually specified 135 | - Improve timezone detection 136 | - Optionally using HTML5's location API for better timezone determination 137 | -------------------------------------------------------------------------------- /CHANGES.txt: -------------------------------------------------------------------------------- 1 | Change-log for django-tz-detect. 2 | 3 | This file will be added to as part of each release 4 | 5 | ---- 6 | 7 | Version 0.5.0 8 | ============ 9 | 10 | * Prefer Intl timezone over offset, #63 11 | 12 | Version 0.4.0 13 | ============= 14 | 15 | * Support Django 2.2, 3.0, 3.1, 3.2, 4.0 16 | * Support Python 3.7, 3.8, 3.9, 3.10 17 | * Drop Python 2 support 18 | * Switch from Travis CI to GitHub Actions 19 | 20 | Version 0.3.0, Tue 26 Nov 2019 21 | =============================== 22 | 23 | 15cbd28775 Set attributes on script tag. (Craig Anderson) 24 | 25 | 26 | Version 0.2.10, Sat 30 Mar 2019 27 | ================================ 28 | 29 | 88b1b24e7c Dropped support for django < 1.11 (Basil Shubin) 30 | ffaec9e9ac Template library load: staticfiles is deprecated in favor of static (Pi Delport) 31 | bb62c4618c MIDDLEWARE_CLASSES renamed to MIDDLEWARE in Django 1.10 (Craig Anderson) 32 | 13ba3c504c Add Tox configuration (Pi Delport) 33 | b8d8f3760e Simplify offset_to_timezone implementation to use min() (Pi Delport) 34 | bdb4993683 Update offset_to_timezone to handle edge cases more accurately (Pi Delport) 35 | bc0c63020b Add tests for all hourly UTC offsets, with known mis-detections (Pi Delport) 36 | 37 | 38 | Version 0.2.9, Mon 18 Dec 2017 39 | =============================== 40 | 41 | aaae42905b Excluding testing of django 2.0 on python 2.7 (Adam Charnock) 42 | 45341a3764 Adding django 2.0 to build matrix (Adam Charnock) 43 | a2c3a79edf Implement handle of settings.CSRF_HEADER_NAME (Roman Gorbil) 44 | 40dfc82d34 upgraded test suite (Basil Shubin) 45 | 0535c03d5e Travis config: Add Django 1.11, exclude Python 2.7/Django master (because Django 2.0 will not support Python 2.7), and allow tests against Django master to fail (Drew Hubl) 46 | d89ad7b8c3 fixed typo, close #31 (Basil Shubin) 47 | 48 | 49 | Version 0.2.8, Fri 21 Oct 2016 50 | =============================== 51 | 52 | 9e9fd54e8d dropped support for python 2.6 & 3.3 (bashu) 53 | a089305ed1 make sure example project runs under django 1.10.x (bashu) 54 | d0430154d5 Update README.rst (bashu) 55 | febb8a3afa Update .travis.yml (Eric Wang) 56 | 8a5c8af4a4 Add Django 1.10 support (Eric Wang) 57 | 58 | 59 | Version 0.2.7, Thu 28 Apr 2016 60 | =============================== 61 | 62 | d52f8b99fe added missing VERSION file (bashu) 63 | 18bd7675c7 Renaming CHANGES -> CHANGES.txt (as required by seed) (bashu) 64 | 86485a3ad8 Updating version to be stored in VERSION file (as now done by seed) (bashu) 65 | 75c23022d9 Clean up imports (Filip Figiel) 66 | 9228317107 Don't expect request in templatetag context. Fixes #26 (Filip Figiel) 67 | 68 | 69 | Version 0.2.6, Sat 13 Feb 2016 70 | =============================== 71 | 72 | 7908c18387 fix for Django 1.10 (kudos to @PetrDlouhy) 73 | d0bb615217 Added django 1.9 support (Basil Shubin) 74 | 75 | 76 | Version 0.2.5, Sat 06 May 2015 77 | =============================== 78 | 79 | 3d2c29f0a3 python 3 support (Basil Shubin) 80 | 81 | 82 | Version 0.2.4, Sat 06 May 2015 83 | =============================== 84 | 85 | 467f6b8648 Handle existing sessions storing ``basestring`` objects (Basil Shubin) 86 | 87 | 88 | Version 0.2.3, Sat 24 May 2015 89 | =============================== 90 | 91 | e7d778e279 Correctly handle NonExistentTimeError exception. (Basil Shubin) 92 | 34a215ae9a Support django 1.6+ default session serializer, JSONSerializer (Doug Cox) 93 | 1303ca4ca5 Backward compatibility with django 1.4 (Basil Shubin) 94 | 95 | 96 | Version 0.2.2, Sat 31 Aug 2013 97 | =============================== 98 | 99 | 6406cfe477 Fixing typo in setting name. Thanks to @ustun for noticing. (Adam Charnock) 100 | 101 | 102 | Version 0.2.1, Fri 30 Aug 2013 103 | =============================== 104 | 105 | 519dc08132 Updating setup.py to use setuptools exclusively (as per recent seed changes) (Adam Charnock) 106 | 107 | 108 | Version 0.2.0, Fri 30 Aug 2013 109 | =============================== 110 | 111 | 071d092216 Correcting JS error (for issue #2) (Adam Charnock) 112 | 634d77b8ad Work on issue #2 - Disable posting timezone to server if cookies are disabled (Adam Charnock) 113 | eec2d70754 Adding badges to readme (Adam Charnock) 114 | 115 | 116 | Version 0.1.5, Wed 26 Jun 2013 117 | =============================== 118 | 119 | 5093ecb9de updating manifest (Adam Charnock) 120 | e23f761b0f remove offending line (Rich Atkinson) 121 | 272e396ee3 insert new script next to another script, not just inside opening (Rich Atkinson) 122 | 123 | 124 | Version 0.1.4, Wed 19 Jun 2013 125 | =============================== 126 | 127 | 058707285d Fixing setup.py packages directive (Adam Charnock) 128 | 129 | 130 | Version 0.1.3, Wed 19 Jun 2013 131 | =============================== 132 | 133 | 34e0834024 Fixing setup.py packages directive (Adam Charnock) 134 | 135 | 136 | Version 0.1.2, Wed 19 Jun 2013 137 | =============================== 138 | 139 | adb61c30da Fixing setup.py packages directive (Adam Charnock) 140 | 141 | 142 | Version 0.1.1, Wed 19 Jun 2013 143 | =============================== 144 | 145 | bd71116867 Fixing setup.py packages directive (Adam Charnock) 146 | 147 | 148 | Version 0.1.0 (first version), Wed 19 Jun 2013 149 | =============================================== 150 | 151 | 152 | -------------------------------------------------------------------------------- /tz_detect/tests.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from django.contrib.sessions.middleware import SessionMiddleware 4 | from django.http import HttpResponse 5 | from django.test import TestCase 6 | from django.test.client import RequestFactory 7 | from django.utils.timezone import get_current_timezone_name 8 | 9 | from tz_detect.middleware import TimezoneMiddleware 10 | from tz_detect.templatetags.tz_detect import tz_detect 11 | from tz_detect.utils import convert_header_name, offset_to_timezone 12 | from tz_detect.views import SetOffsetView 13 | 14 | from .defaults import TZ_SESSION_KEY 15 | 16 | 17 | class ViewTestCase(TestCase): 18 | def setUp(self): 19 | self.factory = RequestFactory() 20 | 21 | def add_session(self, request): 22 | get_response = lambda x: HttpResponse("") 23 | SessionMiddleware(get_response).process_request(request) 24 | 25 | def test_xhr_valid_offset(self): 26 | request = self.factory.post("/abc", {"offset": "-60"}) 27 | self.add_session(request) 28 | 29 | response = SetOffsetView.as_view()(request) 30 | self.assertEqual(response.status_code, 200) 31 | self.assertIn(TZ_SESSION_KEY, request.session) 32 | self.assertIsInstance(request.session[TZ_SESSION_KEY], int) 33 | 34 | def test_xhr_valid_timezone(self): 35 | timezone_name = "Europe/Amsterdam" 36 | request = self.factory.post("/abc", {"timezone": timezone_name}) 37 | self.add_session(request) 38 | 39 | response = SetOffsetView.as_view()(request) 40 | self.assertEqual(response.status_code, 200) 41 | self.assertIn(TZ_SESSION_KEY, request.session) 42 | self.assertEqual(request.session[TZ_SESSION_KEY], timezone_name) 43 | 44 | def test_xhr_bad_method(self): 45 | request = self.factory.get("/abc") 46 | self.add_session(request) 47 | 48 | response = SetOffsetView.as_view()(request) 49 | self.assertEqual(response.status_code, 405) 50 | 51 | def test_xhr_no_offset(self): 52 | request = self.factory.post("/abc") 53 | self.add_session(request) 54 | 55 | response = SetOffsetView.as_view()(request) 56 | self.assertEqual(response.status_code, 400) 57 | 58 | def test_xhr_bad_offset(self): 59 | request = self.factory.post("/abc", {"offset": "12foo34"}) 60 | self.add_session(request) 61 | 62 | response = SetOffsetView.as_view()(request) 63 | self.assertEqual(response.status_code, 400) 64 | 65 | def test_middleware_timezone_string(self): 66 | timezone_name = "Europe/Amsterdam" 67 | request = self.factory.post("/abc", {"timezone": timezone_name}) 68 | self.add_session(request) 69 | request.session[TZ_SESSION_KEY] = timezone_name 70 | 71 | get_response = lambda x: HttpResponse("") 72 | TimezoneMiddleware(get_response).process_request(request) 73 | tz_name = get_current_timezone_name() 74 | self.assertEqual(tz_name, timezone_name) 75 | 76 | 77 | class OffsetToTimezoneTestCase(TestCase): 78 | 79 | # Examples offsets (in hours), and the expected timezones for the beginning and middle of the year. 80 | example_offsets_timezones = [ 81 | # Note: These hours are in JavaScript's Date.getTimezoneOffset() convention, 82 | # so the sign is reversed compared to conventional UTC offset notation. 83 | (12, ("Pacific/Midway", "Pacific/Midway")), 84 | (11, ("Pacific/Midway", "Pacific/Midway")), 85 | # USA: 86 | (10, ("America/Adak", "Pacific/Honolulu")), # HST, HST 87 | (9, ("America/Anchorage", "America/Adak")), # AKST, HDT 88 | (8, ("America/Los_Angeles", "America/Anchorage")), # PST, AKDT 89 | (7, ("America/Denver", "America/Phoenix")), # MST, PDT 90 | (6, ("America/Chicago", "America/Denver")), # CST, MDT 91 | (5, ("America/New_York", "America/Chicago")), # EST, CDT 92 | (4, ("America/Porto_Velho", "America/New_York")), # AMT, EDT 93 | (3, ("America/Belem", "America/Belem")), 94 | (2, ("America/Noronha", "America/Noronha")), 95 | (1, ("America/Scoresbysund", "Atlantic/Cape_Verde")), 96 | # Central Europe: 97 | (0, ("Europe/London", "Africa/Abidjan")), # GMT, GMT 98 | (-1, ("Europe/Berlin", "Europe/London")), # CET, BST 99 | (-2, ("Europe/Kaliningrad", "Europe/Kaliningrad")), 100 | (-3, ("Europe/Moscow", "Europe/Moscow")), 101 | (-4, ("Europe/Astrakhan", "Europe/Astrakhan")), 102 | (-5, ("Asia/Yekaterinburg", "Asia/Yekaterinburg")), 103 | (-6, ("Asia/Urumqi", "Asia/Urumqi")), 104 | (-7, ("Asia/Novosibirsk", "Asia/Novosibirsk")), 105 | (-8, ("Asia/Shanghai", "Asia/Shanghai")), 106 | (-9, ("Asia/Tokyo", "Asia/Tokyo")), 107 | (-10, ("Asia/Vladivostok", "Asia/Vladivostok")), 108 | (-11, ("Asia/Magadan", "Asia/Magadan")), 109 | (-12, ("Asia/Kamchatka", "Asia/Kamchatka")), 110 | (-13, ("Antarctica/McMurdo", "Pacific/Apia")), 111 | (-14, ("Pacific/Apia", "Pacific/Kiritimati")), 112 | ] 113 | 114 | def test_examples(self): 115 | # Python < 3.4 compatibility: 116 | if not hasattr(self, "subTest"): 117 | self.skipTest("No subTest support") 118 | 119 | for (js_offset_hours, expected_tzs) in self.example_offsets_timezones: 120 | with self.subTest(hour=js_offset_hours): 121 | js_offset_minutes = js_offset_hours * 60 122 | actual_tzs = ( 123 | str( 124 | offset_to_timezone( 125 | js_offset_minutes, datetime(2018, 1, 1, 0, 0, 0) 126 | ) 127 | ), # Start/end of year 128 | str( 129 | offset_to_timezone( 130 | js_offset_minutes, datetime(2018, 7, 1, 0, 0, 0) 131 | ) 132 | ), # Mid-year 133 | ) 134 | self.assertEqual(expected_tzs, actual_tzs) 135 | 136 | summer = datetime(2013, 6, 15, 12, 0, 0) 137 | winter = datetime(2013, 12, 15, 12, 0, 0) 138 | 139 | # Tests for various cities for both regular and daylight saving time 140 | def test_london_winter(self): 141 | tz = offset_to_timezone(0, now=self.winter) 142 | self.assertEqual(str(tz), "Europe/London") 143 | 144 | def test_london_summer(self): 145 | tz = offset_to_timezone(-60, now=self.summer) 146 | self.assertEqual(str(tz), "Europe/London") 147 | 148 | def test_new_york_winter(self): 149 | tz = offset_to_timezone(5 * 60, now=self.winter) 150 | self.assertEqual(str(tz), "America/New_York") 151 | 152 | def test_new_york_summer(self): 153 | tz = offset_to_timezone(4 * 60, now=self.summer) 154 | self.assertEqual(str(tz), "America/New_York") 155 | 156 | def test_tokyo(self): 157 | tz = offset_to_timezone(-9 * 60, now=self.summer) 158 | self.assertEqual(str(tz), "Asia/Tokyo") 159 | 160 | def test_fuzzy(self): 161 | """Test the fuzzy matching of timezones""" 162 | tz = offset_to_timezone(-10, now=self.winter) 163 | self.assertEqual(str(tz), "Europe/London") 164 | 165 | 166 | class ConvertHeaderNameTestCase(TestCase): 167 | """Test for `templatetags.tz_detect.convert_header_name` 168 | 169 | This util converts django header name to suitable for AJAX request 170 | """ 171 | 172 | def test_default_header_name(self): 173 | # default value for settings.CSRF_HEADER_NAME 174 | setting = "HTTP_X_CSRFTOKEN" 175 | result = convert_header_name(setting) 176 | self.assertEqual(result, "x-csrftoken") 177 | 178 | def test_custom_header_name(self): 179 | setting = "HTTP_X_XSRF_TOKEN" 180 | result = convert_header_name(setting) 181 | self.assertEqual(result, "x-xsrf-token") 182 | 183 | def test_custom_header_without_http_prefix(self): 184 | setting = "X_XSRF_TOKEN" 185 | result = convert_header_name(setting) 186 | self.assertEqual(result, "x-xsrf-token") 187 | 188 | 189 | class TemplatetagTestCase(TestCase): 190 | def test_no_request_context(self): 191 | try: 192 | tz_detect({}) 193 | except KeyError as e: 194 | if e.message == "request": 195 | self.fail("Templatetag shouldn't expect request in context.") 196 | else: 197 | raise 198 | --------------------------------------------------------------------------------