Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added GeoLite2-City.mmdb
Binary file not shown.
13 changes: 11 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ Quick start
)
```

1. Add EasyTimezoneMiddleware to your MIDDLEWARE_CLASSES
1. Add EasyTimezoneMiddleware to your MIDDLEWARE_CLASSES

```python
MIDDLEWARE_CLASSES = (
Expand All @@ -40,6 +40,15 @@ link](http://geolite.maxmind.com/download/geoip/database/GeoLiteCity.dat.gz) bec
GEOIPV6_DATABASE = '/path/to/your/geoip/database/GeoLiteCityv6.dat'
```

1. (Optionally) You can use version 2 of the MaxMind GeoIP cities database (with both IPv4 and IPv6 support - [direct link](https://dev.maxmind.com/geoip/geoip2/geolite2/)):

```python
GEOIP_VERSION = 2
GEOIP_DATABASE = '/path/to/your/geoip/database/GeoLite2-City.mmdb'
```

django-easy-timezones will default to using version 1 unless GEOIP_VERSION is set to 2

1. Enable localtime in your templates.

```python
Expand All @@ -58,7 +67,7 @@ You can also use signals to perform actions based on the timezone detection.
1. To hook into the Timezone detection event to, say, save it to the request's user somewhere more permanent than a session, do something like this:

```python
from easy_timezones.signals import detected_timezone
from easy_timezones.signals import detected_timezone

@receiver(detected_timezone, sender=MyUserModel)
def process_timezone(sender, instance, timezone, **kwargs):
Expand Down
107 changes: 82 additions & 25 deletions easy_timezones/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,52 +3,108 @@
from django.contrib.auth import get_user_model
from django.core.exceptions import ImproperlyConfigured
from django.utils import timezone
import pytz
import pygeoip
import geoip2.database
import geoip2.errors
import os

from .signals import detected_timezone
from .utils import get_ip_address_from_request, is_valid_ip, is_local_ip


db_loaded = False
db = None
db_v6 = None


def load_db_settings():
GEOIP_DATABASE = getattr(settings, 'GEOIP_DATABASE', 'GeoLiteCity.dat')
GEOIP_VERSION = getattr(settings, 'GEOIP_VERSION', 1)
if GEOIP_VERSION not in [1, 2]:
raise ImproperlyConfigured(
"GEOIP_VERSION setting is defined, but only versions 1 and 2 "
"are supported")

GEOIP_DATABASE = getattr(settings, 'GEOIP_DATABASE', 'GeoLiteCity.dat')
if not GEOIP_DATABASE:
raise ImproperlyConfigured("GEOIP_DATABASE setting has not been properly defined.")

raise ImproperlyConfigured(
"GEOIP_DATABASE setting has not been properly defined.")
if not os.path.exists(GEOIP_DATABASE):
raise ImproperlyConfigured("GEOIP_DATABASE setting is defined, but file does not exist.")

GEOIPV6_DATABASE = getattr(settings, 'GEOIPV6_DATABASE', 'GeoLiteCityv6.dat')

if not GEOIPV6_DATABASE:
raise ImproperlyConfigured("GEOIPV6_DATABASE setting has not been properly defined.")
raise ImproperlyConfigured(
"GEOIP_DATABASE setting is defined, but {} does not exist.".format(
GEOIP_DATABASE)
)

#
# Version 2 databases combine both ipv4 and ipv6 data, so only one
# database file is used for both
#
GEOIPV6_DATABASE = getattr(settings, 'GEOIPV6_DATABASE',
'GeoLiteCityv6.dat')
if GEOIP_VERSION == 1:
if not GEOIPV6_DATABASE:
raise ImproperlyConfigured(
"GEOIPV6_DATABASE setting has not been properly defined.")
if not os.path.exists(GEOIPV6_DATABASE):
raise ImproperlyConfigured(
"GEOIPV6_DATABASE setting is defined, but file does not exist.")

return (GEOIP_DATABASE, GEOIPV6_DATABASE, GEOIP_VERSION)

if not os.path.exists(GEOIPV6_DATABASE):
raise ImproperlyConfigured("GEOIPV6_DATABASE setting is defined, but file does not exist.")

return (GEOIP_DATABASE, GEOIPV6_DATABASE)

load_db_settings()

def load_db():

GEOIP_DATABASE, GEOIPV6_DATABASE = load_db_settings()
def load_db():
GEOIP_DATABASE, GEOIPV6_DATABASE, GEOIP_VERSION = load_db_settings()

global db
db = pygeoip.GeoIP(GEOIP_DATABASE, pygeoip.MEMORY_CACHE)

global db_v6
db_v6 = pygeoip.GeoIP(GEOIPV6_DATABASE, pygeoip.MEMORY_CACHE)

global db_loaded

if GEOIP_VERSION == 1:
db = pygeoip.GeoIP(GEOIP_DATABASE, pygeoip.MEMORY_CACHE)
db_v6 = pygeoip.GeoIP(GEOIPV6_DATABASE, pygeoip.MEMORY_CACHE)
elif GEOIP_VERSION ==2:
db = geoip2.database.Reader(GEOIP_DATABASE)

db_loaded = True


def lookup_tz_v1(ip):
"""
Lookup a timezone for the ip using the v1 database.

:param ip: the ip address, v4 or v6
:return: the timezone

"""
if not db_loaded:
if ':' in ip:
return db_v6.time_zone_by_addr(ip)
else:
return db.time_zone_by_addr(ip)


def lookup_tz_v2(ip):
"""
Lookup a timezone for the ip using the v2 database.

:param ip: the ip address, v4 or v6
:return: the timezone

"""
if not db_loaded:
load_db()
#
# v2 databases support both ipv4 an ipv6
#
try:
response = db.city(ip)
except geoip2.errors.AddressNotFoundError:
return None
return response.location.time_zone


if django.VERSION >= (1, 10):
from django.utils.deprecation import MiddlewareMixin
middleware_base_class = MiddlewareMixin
Expand All @@ -75,6 +131,8 @@ def process_request(self, request):

tz = request.session.get('django_timezone')

version = getattr(settings, 'GEOIP_VERSION')

if not tz:
# use the default timezone (settings.TIME_ZONE) for localhost
tz = timezone.get_default_timezone()
Expand All @@ -83,12 +141,10 @@ def process_request(self, request):
ip_addrs = client_ip.split(',')
for ip in ip_addrs:
if is_valid_ip(ip) and not is_local_ip(ip):
if ':' in ip:
tz = db_v6.time_zone_by_addr(ip)
break
if version == 1:
tz = lookup_tz_v1(ip)
else:
tz = db.time_zone_by_addr(ip)
break
tz = lookup_tz_v2(ip)

if tz:
timezone.activate(tz)
Expand All @@ -97,3 +153,4 @@ def process_request(self, request):
detected_timezone.send(sender=get_user_model(), instance=request.user, timezone=tz)
else:
timezone.deactivate()

100 changes: 67 additions & 33 deletions easy_timezones/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from .utils import get_ip_address_from_request, is_valid_ip, is_local_ip

class TimezoneTests(TestCase):

def test_basic_addition(self):
"""
Tests that 1 + 1 always equals 2. (Sanity test.)
Expand Down Expand Up @@ -42,48 +42,82 @@ def test_load_db_settings(self):
error_occured = True
self.assertFalse(error_occured)

versions = {
1: False,
2: False,
3: True
}
for version, result in versions.items():
settings.GEOIP_VERSION = version
error_occured = False
try:
load_db_settings()
except ImproperlyConfigured:
error_occured = True
self.assertEqual(error_occured, result)

def test_middleware(self):
load_db()
easy = EasyTimezoneMiddleware()
easy.process_request(None)
versions = {
1: 'GeoLiteCity.dat',
2: 'GeoLite2-City.mmdb'
}

for version, database_file in versions.items():
settings.GEOIP_VERSION = version
settings.GEOIP_DATABASE = os.path.join(os.getcwd(), database_file)
load_db_settings()
load_db()
easy = EasyTimezoneMiddleware()
easy.process_request(None)

def test_tags(self):
versions = {
1: 'GeoLiteCity.dat',
2: 'GeoLite2-City.mmdb'
}

for version, database_file in versions.items():
settings.GEOIP_VERSION = version
settings.GEOIP_DATABASE = os.path.join(os.getcwd(), database_file)

load_db_settings()
load_db()

# UTC
client = Client()
response = client.get('/without_tz/')
self.assertEqual(response.status_code, 200)
without_s = response.content
# UTC
client = Client()
response = client.get('/without_tz/')
self.assertEqual(response.status_code, 200)
without_s = response.content

# Europe/Moscow
client = Client(REMOTE_ADDR="93.180.5.26")
response = client.get('/with_tz/')
self.assertEqual(response.status_code, 200)
with_s = response.content
# Europe/Moscow
client = Client(REMOTE_ADDR="93.180.5.26")
response = client.get('/with_tz/')
self.assertEqual(response.status_code, 200)
with_s = response.content

self.assertNotEqual(without_s, with_s)
self.assertNotEqual(without_s, with_s)

# Europe/Oslo
client = Client(REMOTE_ADDR="2001:700:300:2321::11")
response = client.get('/with_tz/')
self.assertEqual(response.status_code, 200)
with_s = response.content
# Europe/Oslo
client = Client(REMOTE_ADDR="2001:700:300:2321::11")
response = client.get('/with_tz/')
self.assertEqual(response.status_code, 200)
with_s = response.content

self.assertNotEqual(without_s, with_s)
self.assertNotEqual(without_s, with_s)

# Localhost
client = Client(REMOTE_ADDR="127.0.0.1")
response = client.get('/with_tz/')
self.assertEqual(response.status_code, 200)
with_s = response.content
self.assertEqual(without_s, with_s)
# Localhost
client = Client(REMOTE_ADDR="127.0.0.1")
response = client.get('/with_tz/')
self.assertEqual(response.status_code, 200)
with_s = response.content
self.assertEqual(without_s, with_s)

# Localhost IPv6
client = Client(REMOTE_ADDR="0:0:0:0:0:0:0:1")
response = client.get('/with_tz/')
self.assertEqual(response.status_code, 200)
with_s = response.content
self.assertEqual(without_s, with_s)
# Localhost IPv6
client = Client(REMOTE_ADDR="0:0:0:0:0:0:0:1")
response = client.get('/with_tz/')
self.assertEqual(response.status_code, 200)
with_s = response.content
self.assertEqual(without_s, with_s)

def test_is_local_ip(self):
self.assertTrue(is_local_ip('127.0.0.1'))
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
Django>=1.7
geoip2==2.7.0
ipaddress==1.0.16
pygeoip==0.3.2
pytz==2016.6
Expand Down