Skip to content

Commit

Permalink
Merge pull request #45 from rednaks/master
Browse files Browse the repository at this point in the history
1.6.0 release
  • Loading branch information
rednaks authored Apr 9, 2020
2 parents 9fdec78 + 42e0d9b commit 00ddac3
Show file tree
Hide file tree
Showing 20 changed files with 275 additions and 13 deletions.
11 changes: 11 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[run]
branch = true

[report]
omit =
*site-packages*
*tests*
*.tox*
show_missing = True
exclude_lines =
raise NotImplementedError
8 changes: 5 additions & 3 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@ python:
- "3.8"
# command to install dependencies
install:
- pip install flake8 flake8-docstrings bandit twine
- pip install -r tests/requirements.txt
# command to run tests

script:
- flake8 --count --statistics .
- bandit -r .
- flake8 --count --statistics django_ip_geolocation
- bandit -r django_ip_geolocation
- python setup.py sdist
- twine check dist/*
- tox
- coverage report

4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,6 @@ upload: build check $(DIST_DIR)/*
@echo "Uploading dist"
$(PYTHON) -m twine upload $(DIST_DIR)/*

clean: $(DIST_DIR)/*
clean:
@echo "Cleaning everything"
rm -r $(DIST_DIR) django_ip_geolocation.egg-info
rm -r $(DIST_DIR) django_ip_geolocation.egg-info || true
24 changes: 22 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
[![Downloads](https://pepy.tech/badge/django-ip-geolocation)](https://pepy.tech/project/django-ip-geolocation) [![Build Status](https://travis-ci.org/rednaks/django-ip-geolocation.svg?branch=release)](https://travis-ci.org/rednaks/django-ip-geolocation)
[![PyPI version](https://badge.fury.io/py/django-ip-geolocation.svg)](https://badge.fury.io/py/django-ip-geolocation) [![Downloads](https://pepy.tech/badge/django-ip-geolocation)](https://pepy.tech/project/django-ip-geolocation) [![Build Status](https://travis-ci.org/rednaks/django-ip-geolocation.svg?branch=release)](https://travis-ci.org/rednaks/django-ip-geolocation)

# Django Ip Geolocation
Django request/response hooks to geolocate visitors by their ip address.
Expand Down Expand Up @@ -42,10 +42,28 @@ def other_view(request):
...
```

## Cookie:
## Cookie
Geolocation data stored in the Response cookie lacks the `raw_data` and is base64 encoded.


## User consent
Developers must implement a helper function to check if the user consented or not and configure it in the `settings.py`.

By default if the developer didn't provide a validation function, we consider it as implicit consent.

Here is an example of the helper function:

```python

def check_user_consent(request):
if request.user.is_consented: # this is only an example.
return True
return False

```



## Settings
You can configure settings for your hook in the `settings.py` as follow:
```python
Expand All @@ -60,6 +78,7 @@ IP_GEOLOCATION_SETTINGS = {
'ENABLE_RESPONSE_HOOK': True,
'ENABLE_COOKIE': False,
'FORCE_IP_ADDR': None,
'USER_CONSENT_VALIDATOR': None
}

```
Expand All @@ -78,6 +97,7 @@ Those are the default settings, that will be overwritten by those set in `settin
| `ENABLE_RESPONSE_HOOK` | Enable or disable hook on request | `True` (bool) |
| `ENABLE_COOKIE` | Enable or disable geolocation data in cookie | `False` (bool) |
| `FORCE_IP_ADDR` | Force ip address, rather than using visitor ip | `None` (string) |
| `USER_CONSENT_VALIDATOR`| A function path to check if the current user gave his consent | `None` (string, function path) |

### Available Backends
* `django_ip_geolocation.backends.IPGeolocationAPI` : (Default) Using https://ipgeolocationapi.com/
Expand Down
5 changes: 4 additions & 1 deletion django_ip_geolocation/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import logging
from django_ip_geolocation.utils import get_geolocation, set_cookie, \
clean_geolocation_data
clean_geolocation_data, is_user_consented
from django_ip_geolocation.settings import IP_GEOLOCATION_SETTINGS as _settings


Expand All @@ -20,6 +20,9 @@ def inner(request): # noqa: E501
enable_response = _settings.get('ENABLE_RESPONSE_HOOK')
enable_cookie = _settings.get('ENABLE_COOKIE', False)

if not is_user_consented(request):
return view_func(request)

if not enable_request and not (enable_response or enable_cookie):
return view_func(request)

Expand Down
8 changes: 7 additions & 1 deletion django_ip_geolocation/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

from django.utils.deprecation import MiddlewareMixin # noqa: E501 pylint: disable=import-error
from django_ip_geolocation.utils import get_geolocation, set_cookie, \
clean_geolocation_data
clean_geolocation_data, is_user_consented
from django_ip_geolocation.settings import IP_GEOLOCATION_SETTINGS as _settings # noqa: E501


Expand All @@ -20,6 +20,9 @@ def process_request(self, request):
if not _settings.get('ENABLE_REQUEST_HOOK'):
return

if not is_user_consented(request):
return

self._get_geolocation(request)
request.geolocation = self._geolocation_data
except Exception:
Expand All @@ -32,6 +35,9 @@ def process_response(self, request, response):
not _settings.get('ENABLE_COOKIE'):
return response

if not is_user_consented(request):
return response

if self._geolocation_data is None:
self._get_geolocation(request)

Expand Down
1 change: 1 addition & 0 deletions django_ip_geolocation/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
'ENABLE_RESPONSE_HOOK': True,
'ENABLE_COOKIE': False,
'FORCE_IP_ADDR': None,
'USER_CONSENT_VALIDATOR': None
}

CUSTOM_IP_GEOLOCATION_SETTINGS = getattr(settings, 'IP_GEOLOCATION_SETTINGS', {}) # noqa: E501
Expand Down
22 changes: 20 additions & 2 deletions django_ip_geolocation/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ def _get_geolocation_backend_cls():
:return: Geolocation backend class
:rtype: class
"""
backend_class_name = settings.IP_GEOLOCATION_SETTINGS.get('BACKEND')
geolocation_backend_cls = import_string(backend_class_name)
backend_path = settings.IP_GEOLOCATION_SETTINGS.get('BACKEND')
geolocation_backend_cls = import_string(backend_path)
return geolocation_backend_cls


Expand Down Expand Up @@ -90,3 +90,21 @@ def clean_geolocation_data(geolocation_data, attr_to_remove=None):
logging.info('Key not found, continuing ...')

return geolocation_copy


def is_user_consented(request):
"""Check if the user gave consent to be geolocatted.
:param request: User http request
:type: HttpRequest
:return: Yes or no
:rtype: bool
"""
validator_path = settings.IP_GEOLOCATION_SETTINGS.get(
'USER_CONSENT_VALIDATOR')
user_consent_validator = None
if validator_path is None:
return True

user_consent_validator = import_string(validator_path)
return user_consent_validator(request)
12 changes: 12 additions & 0 deletions manage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import unicode_literals, absolute_import

import os
import sys

if __name__ == "__main__":
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tests.settings")
from django.core.management import execute_from_command_line

execute_from_command_line(sys.argv)
4 changes: 2 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ def readme():

setup(
name='django-ip-geolocation',
version='1.5.3',
version='1.6.0',
author='Skander Ben Mahmoud',
author_email='[email protected]',
packages=find_packages(exclude=("tests", "docs")),
packages=find_packages(exclude=("*tests*", "docs")),
url='https://github.com/rednaks/django-ip-geolocation',
license='MIT',
description="Django request/response hook (Middleware and Decorator) to geolocate visitors using their IP address", # noqa: E501
Expand Down
1 change: 1 addition & 0 deletions tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Django test module."""
30 changes: 30 additions & 0 deletions tests/backend_mock.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
"""Mocked backend module."""
from django_ip_geolocation.backends.base import GeolocationBackend


class BackendMock(GeolocationBackend):
"""BackendMock backend implementation."""

def geolocate(self):
"""Mock api call."""
self._raw_data = {
"continent": "Europe",
"country_code": "49",
"name": "Germany",
"geo": {
"latitude": 51.165691,
"longitude": 10.451526
},
"currency_code": "EUR"
}

def _parse(self):
"""Parse raw data."""
self._continent = self._raw_data.get('continent')
self._country = {
'code': self._raw_data.get('alpha2'),
'name': self._raw_data.get('name')
}
self._geo_data = self._raw_data.get('geo')


10 changes: 10 additions & 0 deletions tests/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
coverage
mock
flake8
flake8-docstrings
bandit
twine
tox
codecov
tox-travis

43 changes: 43 additions & 0 deletions tests/settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
"""Django settings."""
# -*- coding: utf-8
from __future__ import unicode_literals, absolute_import

# import django

DEBUG = True
USE_TZ = True

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = '-%n*2l!dqp6wkjn44kbv5y=2m6en@l495gb9@&$#o89%8oy75g'

DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': ':memory:',
}
}

ROOT_URLCONF = 'tests.urls'

INSTALLED_APPS = [
]

MIDDLEWARE = [
'django_ip_geolocation.middleware.IpGeolocationMiddleware',
]


IP_GEOLOCATION_SETTINGS = {
'BACKEND': 'tests.backend_mock.BackendMock',
'BACKEND_API_KEY': '',
'BACKEND_EXTRA_PARAMS': {},
'BACKEND_USERNAME': '',
'RESPONSE_HEADER': 'X-IP-Geolocation',
'ENABLE_REQUEST_HOOK': True,
'ENABLE_RESPONSE_HOOK': True,
'ENABLE_COOKIE': False,
'FORCE_IP_ADDR': None,
'USER_CONSENT_VALIDATOR': None
}

SITE_ID = 1
47 changes: 47 additions & 0 deletions tests/test_middleware.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import unittest

from django.http import HttpResponse

from django.test import TestCase
import logging
from django_ip_geolocation.settings import IP_GEOLOCATION_SETTINGS
from tests.utils import change_settings


class GeolocationTests(TestCase):

def setUp(self):
self._default_settings = IP_GEOLOCATION_SETTINGS.copy()


def test_geolocation_in_response(self):
response = self.client.get('/test_geolocation/')
self.assertTrue(response.has_header('X-IP-Geolocation'))
if response.has_header('X-IP-Geolocation'):
print(response['X-IP-Geolocation'])

@change_settings(settings={'ENABLE_REQUEST_HOOK': False})
def test_geolocation_in_response_when_request_disabled(self):
response = self.client.get('/test_geolocation/')
self.assertTrue(response.has_header('X-IP-Geolocation'))
if response.has_header('X-IP-Geolocation'):
print(response['X-IP-Geolocation'])

@change_settings(settings={'ENABLE_RESPONSE_HOOK': False})
def test_geolocation_not_in_response_when_response_disabled(self):
response = self.client.get('/test_geolocation/')
self.assertFalse(response.has_header('X-IP-Geolocation'))

def test_cookie_not_in_response(self):
response = self.client.get('/test_geolocation/')
self.assertIsNone(response.cookies.get('geolocation'))

@change_settings(settings={'ENABLE_COOKIE': True})
def test_cookie_in_response(self):
response = self.client.get('/test_geolocation/')
self.assertIsNotNone(response.cookies.get('geolocation'))

@change_settings(settings={'USER_CONSENT_VALIDATOR': 'tests.user_consent_mock.check_user_consent'})
def test_user_not_consented(self):
response = self.client.get('/test_geolocation/')
self.assertFalse(response.has_header('X-IP-Geolocation'))
8 changes: 8 additions & 0 deletions tests/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#import django
from django.conf.urls import url

from . import views

urlpatterns = [
url('^test_geolocation/$', views.geolocation_test)
]
3 changes: 3 additions & 0 deletions tests/user_consent_mock.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@

def check_user_consent(request):
return False
12 changes: 12 additions & 0 deletions tests/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from django_ip_geolocation.settings import IP_GEOLOCATION_SETTINGS


def change_settings(settings):
def decorator(func):
def wrapper(*args, **kwargs):
default_settings = args[0]._default_settings
IP_GEOLOCATION_SETTINGS.update(settings)
func(*args, **kwargs)
IP_GEOLOCATION_SETTINGS.update(default_settings)
return wrapper
return decorator
11 changes: 11 additions & 0 deletions tests/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from django.http import HttpResponse
from django.views.decorators.csrf import ensure_csrf_cookie


@ensure_csrf_cookie
def geolocation_test(request):
"""
Initialize request.
"""
response = HttpResponse("hello skander")
return response
Loading

0 comments on commit 00ddac3

Please sign in to comment.