├── .gitignore ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── HISTORY.rst ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.rst ├── dnt ├── __init__.py └── middleware.py ├── requirements.dev.txt ├── setup.cfg ├── setup.py ├── testapp ├── __init__.py ├── settings.py ├── templates │ └── index.html └── urls.py ├── tests ├── __init__.py ├── test_app.py └── test_middleware.py └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | # Cherry-picked from: 2 | # https://github.com/github/gitignore/blob/master/Python.gitignore 3 | 4 | # Byte-compiled / optimized / DLL files 5 | __pycache__/ 6 | *.py[cod] 7 | *$py.class 8 | 9 | # Distribution / packacking 10 | *.egg-info/ 11 | build/ 12 | dist/ 13 | 14 | # Installer logs 15 | pip-log.txt 16 | pip-delete-this-directory.txt 17 | 18 | # Unit test / coverage reports 19 | htmlcov/ 20 | .tox/ 21 | .coverage 22 | .coverage.* 23 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: no 2 | language: python 3 | cache: pip 4 | matrix: 5 | include: 6 | - python: "3.5" 7 | env: TOXENV=lint 8 | - python: "2.7" 9 | env: TOXENV=py27-1.8 10 | - python: "3.4" 11 | env: TOXENV=py34-1.8 12 | - python: "2.7" 13 | env: TOXENV=py27-1.9 14 | - python: "3.5" 15 | env: TOXENV=py35-1.9 16 | - python: "2.7" 17 | env: TOXENV=py27-1.10 18 | - python: "3.5" 19 | env: TOXENV=py35-1.10 20 | - python: "2.7" 21 | env: TOXENV=py27-1.11 22 | - python: "3.6" 23 | env: TOXENV=py36-1.11 24 | - python: "3.5" 25 | env: TOXENV=py35-2.0 26 | - python: "3.5" 27 | env: TOXENV=py35-2.1 28 | - python: "3.6" 29 | env: TOXENV=py36-2.2 30 | dist: xenial # For SQLite 3.8.3 or later 31 | - python: "3.7" 32 | env: TOXENV=py37-master 33 | dist: xenial # For Python 3.7 34 | allow_failures: 35 | - env: TOXENV=py37-master 36 | install: 37 | - pip install tox 38 | script: 39 | - tox 40 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Community Participation Guidelines 2 | 3 | This repository is governed by Mozilla's code of conduct and etiquette guidelines. 4 | For more details, please read the 5 | [Mozilla Community Participation Guidelines](https://www.mozilla.org/about/governance/policies/participation/). 6 | 7 | ## How to Report 8 | 9 | For more information on how to report violations of the Community Participation 10 | Guidelines, please read our 11 | [How to Report](https://www.mozilla.org/about/governance/policies/participation/reporting/) 12 | page. 13 | -------------------------------------------------------------------------------- /HISTORY.rst: -------------------------------------------------------------------------------- 1 | Release History 2 | --------------- 3 | 4 | v1.0 - 2020-08-12 5 | ^^^^^^^^^^^^^^^^^ 6 | * No new features, but marking the project as mature since neither DNT or this 7 | implementation is likely to change, except to follow Django releases. 8 | * Add support for Django 2.0, 2.1, and 2.2 9 | * Add support for Python 3.7 10 | 11 | v0.2.0 - 2017-02-17 12 | ~~~~~~~~~~~~~~~~~~~ 13 | * Supported Django versions: 1.8, 1.9, 1.10, and 1.11 14 | * Supported Python versions: 2.7, 3.3, 3.4. 3.5, 3.6 15 | * Add "DNT" to Vary header in response (`eillarra `_) 16 | 17 | v0.1.0 - 2011-02-16 18 | ~~~~~~~~~~~~~~~~~~~ 19 | * Initial Release 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011, Mozilla Foundation 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the distribution. 13 | 14 | 3. Neither the name of django-dnt nor the names of its contributors may 15 | be used to endorse or promote products derived from this software 16 | without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | 29 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include HISTORY.rst 2 | include LICENSE 3 | include Makefile 4 | include README.rst 5 | include manage.py 6 | include requirements.dev.txt 7 | include tox.ini 8 | include CODE_OF_CONDUCT.md 9 | 10 | recursive-include testapp *.py 11 | recursive-include testapp/templates *.html 12 | recursive-include tests *.py 13 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | export DJANGO_SETTINGS_MODULE = testapp.settings 2 | export PYTHONPATH := $(shell pwd) 3 | .PHONY: help clean coverage coveragehtml develop lint qa qa-all release sdist test test-all 4 | 5 | help: 6 | @echo "clean - remove all artifacts" 7 | @echo "coverage - check code coverage" 8 | @echo "coveragehtml - display code coverage in browser" 9 | @echo "develop - install development requirements" 10 | @echo "lint - check style with flake8" 11 | @echo "qa - run linters and test coverage" 12 | @echo "qa-all - run QA plus tox and packaging" 13 | @echo "release - package and upload a release" 14 | @echo "sdist - package" 15 | @echo "test - run tests" 16 | @echo "test-all - run tests on every Python version with tox" 17 | @echo "test-release - upload a release to the test PyPI server" 18 | 19 | clean: 20 | git clean -Xfd 21 | 22 | develop: 23 | pip install -r requirements.dev.txt 24 | 25 | lint: 26 | flake8 . 27 | 28 | test: 29 | django-admin test 30 | 31 | test-all: 32 | tox --skip_missing_interpreters 33 | 34 | coverage: clean 35 | coverage erase 36 | coverage run --branch --source=dnt `which django-admin` test 37 | 38 | coveragehtml: coverage 39 | coverage html 40 | python -m webbrowser file://$(CURDIR)/htmlcov/index.html 41 | 42 | qa: lint coveragehtml 43 | 44 | qa-all: qa sdist test-all 45 | 46 | sdist: 47 | python setup.py sdist bdist_wheel 48 | ls -l dist 49 | check-manifest 50 | pyroma dist/`ls -t dist | grep tar.gz | head -n1` 51 | 52 | release: clean sdist 53 | twine upload dist/* 54 | python -m webbrowser -n https://pypi.python.org/pypi/django-dnt 55 | 56 | # Add [test] section to ~/.pypirc, https://testpypi.python.org/pypi 57 | test-release: clean sdist 58 | twine upload --repository test dist/* 59 | python -m webbrowser -n https://testpypi.python.org/pypi/django-dnt 60 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ========== 2 | Django-DNT 3 | ========== 4 | 5 | .. image:: http://img.shields.io/travis/mozilla/django-dnt/master.svg 6 | :alt: The status of Travis continuous integration tests 7 | :target: https://travis-ci.org/mozilla/django-dnt 8 | 9 | .. image:: https://img.shields.io/codecov/c/github/mozilla/django-dnt.svg 10 | :target: https://codecov.io/gh/mozilla/django-dnt 11 | :alt: The code coverage 12 | 13 | .. image:: https://img.shields.io/pypi/v/django-dnt.svg 14 | :alt: The PyPI package 15 | :target: https://pypi.python.org/pypi/django-dnt 16 | 17 | .. Omit badges from docs 18 | 19 | ``django-dnt`` offers an easy way to pay attention to the ``DNT`` 20 | (`Do Not Track `_) HTTP header. If 21 | users are sending ``DNT: 1``, ``DoNotTrackMiddleware`` will set ``request.DNT = 22 | True``, else it will set ``request.DNT = False``. 23 | 24 | Just add ``dnt.middleware.DoNotTrackMiddleware`` to your ``MIDDLEWARE_CLASSES`` 25 | (Django 1.9 and earlier) or ``MIDDLEWARE`` (Django 1.10 and later) and you're 26 | good to go. 27 | -------------------------------------------------------------------------------- /dnt/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django Middleware for DNT (Do Not Track) HTTP header. 3 | 4 | https://en.wikipedia.org/wiki/Do_Not_Track 5 | """ 6 | VERSION = '0.2.0' 7 | -------------------------------------------------------------------------------- /dnt/middleware.py: -------------------------------------------------------------------------------- 1 | from django.utils.cache import patch_vary_headers 2 | 3 | try: 4 | # Added in Django 1.10 5 | from django.utils.deprecation import MiddlewareMixin 6 | except ImportError: 7 | _base_class = object # pragma: no cover 8 | else: 9 | _base_class = MiddlewareMixin # pragma: no cover 10 | 11 | 12 | class DoNotTrackMiddleware(_base_class): 13 | 14 | def process_request(self, request): 15 | """ 16 | Sets flag request.DNT based on DNT HTTP header. 17 | """ 18 | if 'HTTP_DNT' in request.META and request.META['HTTP_DNT'] == '1': 19 | request.DNT = True 20 | else: 21 | request.DNT = False 22 | 23 | def process_response(self, request, response): 24 | """ 25 | Adds a "Vary" header for DNT, useful for caching. 26 | """ 27 | patch_vary_headers(response, ['DNT']) 28 | 29 | return response 30 | -------------------------------------------------------------------------------- /requirements.dev.txt: -------------------------------------------------------------------------------- 1 | # Testing and development requirements 2 | Django 3 | check-manifest 4 | coverage 5 | flake8 6 | pyroma 7 | tox 8 | twine 9 | wheel 10 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal=1 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """Packaging setup for django-dnt.""" 4 | 5 | from setuptools import setup 6 | 7 | description = 'Make Django requests aware of the DNT header' 8 | 9 | 10 | def read_file(path): 11 | contents = open(path).read() 12 | if hasattr(contents, 'decode'): 13 | return contents.decode('utf8') # Python2 bytes to unicode 14 | else: 15 | return contents # Python3 reads unicode 16 | 17 | 18 | def long_description(): 19 | """Create a PyPI long description from docs.""" 20 | readme = read_file('README.rst') 21 | body_tag = ".. Omit badges from docs" 22 | try: 23 | readme_body_start = readme.index(body_tag) 24 | except ValueError: 25 | readme_body = readme 26 | else: 27 | # Omit the badges and reconstruct the title 28 | readme_text = readme[readme_body_start + len(body_tag) + 1:] 29 | readme_body = """\ 30 | %(title_mark)s 31 | %(title)s 32 | %(title_mark)s 33 | %(readme_text)s 34 | """ % { 35 | 'title_mark': '=' * len(description), 36 | 'title': description, 37 | 'readme_text': readme_text, 38 | } 39 | 40 | try: 41 | history = read_file('HISTORY.rst') 42 | except IOError: 43 | history = '' 44 | 45 | long_description = """\ 46 | %(readme)s 47 | 48 | %(history)s 49 | """ % { 50 | 'readme': readme_body, 51 | 'history': history 52 | } 53 | return long_description 54 | 55 | 56 | setup( 57 | name='django-dnt', 58 | version='1.0', 59 | description=description + '.', 60 | long_description=long_description(), 61 | author='James Socol', 62 | author_email='james@mozilla.com', 63 | maintainer='John Whitlock', 64 | maintainer_email='jwhitlock@mozilla.com', 65 | url='http://github.com/mozilla/django-dnt', 66 | license='BSD', 67 | packages=['dnt'], 68 | zip_safe=False, 69 | keywords='django-dnt dnt do not track', 70 | classifiers=[ 71 | 'Development Status :: 6 - Mature', 72 | 'Environment :: Web Environment', 73 | 'Environment :: Web Environment :: Mozilla', 74 | 'Framework :: Django', 75 | 'Framework :: Django :: 1.8', 76 | 'Framework :: Django :: 1.9', 77 | 'Framework :: Django :: 1.10', 78 | 'Framework :: Django :: 1.11', 79 | 'Framework :: Django :: 2.0', 80 | 'Framework :: Django :: 2.1', 81 | 'Framework :: Django :: 2.2', 82 | 'Intended Audience :: Developers', 83 | 'License :: OSI Approved :: BSD License', 84 | 'Operating System :: OS Independent', 85 | 'Programming Language :: Python', 86 | 'Topic :: Software Development :: Libraries :: Python Modules', 87 | 'Programming Language :: Python :: 2', 88 | 'Programming Language :: Python :: 2.7', 89 | 'Programming Language :: Python :: 3', 90 | 'Programming Language :: Python :: 3.3', 91 | 'Programming Language :: Python :: 3.4', 92 | 'Programming Language :: Python :: 3.5', 93 | 'Programming Language :: Python :: 3.6', 94 | 'Programming Language :: Python :: 3.7', 95 | ] 96 | ) 97 | -------------------------------------------------------------------------------- /testapp/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/django-dnt/6431ff497df5d5c661d5e4e97ac9692f281046f1/testapp/__init__.py -------------------------------------------------------------------------------- /testapp/settings.py: -------------------------------------------------------------------------------- 1 | import django 2 | 3 | 4 | SECRET_KEY = "dnt-tests" 5 | DEBUG = True 6 | ALLOWED_HOSTS = [] 7 | 8 | INSTALLED_APPS = ( 9 | 'django.contrib.admin', 10 | 'django.contrib.auth', 11 | 'django.contrib.contenttypes', 12 | 'django.contrib.sessions', 13 | 'django.contrib.messages', 14 | 'django.contrib.staticfiles', 15 | 16 | 'testapp', 17 | ) 18 | 19 | if django.VERSION[:2] < (1, 10): 20 | # Django 1.9 and earlier 21 | MIDDLEWARE_CLASSES = ( 22 | 'django.contrib.sessions.middleware.SessionMiddleware', 23 | 'django.middleware.common.CommonMiddleware', 24 | 'django.middleware.csrf.CsrfViewMiddleware', 25 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 26 | 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', 27 | 'django.contrib.messages.middleware.MessageMiddleware', 28 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 29 | 'django.middleware.security.SecurityMiddleware', 30 | 'dnt.middleware.DoNotTrackMiddleware', 31 | ) 32 | else: 33 | # Django 1.10 and later 34 | MIDDLEWARE = [ 35 | 'django.middleware.security.SecurityMiddleware', 36 | 'django.contrib.sessions.middleware.SessionMiddleware', 37 | 'django.middleware.common.CommonMiddleware', 38 | 'django.middleware.csrf.CsrfViewMiddleware', 39 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 40 | 'django.contrib.messages.middleware.MessageMiddleware', 41 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 42 | 'dnt.middleware.DoNotTrackMiddleware', 43 | ] 44 | 45 | ROOT_URLCONF = 'testapp.urls' 46 | 47 | TEMPLATES = [ 48 | { 49 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 50 | 'DIRS': [], 51 | 'APP_DIRS': True, 52 | 'OPTIONS': { 53 | 'context_processors': [ 54 | 'django.template.context_processors.debug', 55 | 'django.template.context_processors.request', 56 | 'django.contrib.auth.context_processors.auth', 57 | 'django.contrib.messages.context_processors.messages', 58 | ], 59 | }, 60 | }, 61 | ] 62 | 63 | DATABASES = { 64 | 'default': { 65 | 'ENGINE': 'django.db.backends.sqlite3', 66 | 'NAME': ':memory:' 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /testapp/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Do Not Track 5 | 6 | 7 |

Do Not Track: {{ request.DNT }}

8 | 9 | 10 | -------------------------------------------------------------------------------- /testapp/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url 2 | from django.views.generic import TemplateView 3 | 4 | 5 | urlpatterns = [ 6 | url(r'^$', TemplateView.as_view(template_name="index.html"), name="index"), 7 | ] 8 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/django-dnt/6431ff497df5d5c661d5e4e97ac9692f281046f1/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_app.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Test an application using django-dnt.""" 3 | from __future__ import unicode_literals 4 | 5 | from django.test import TestCase 6 | 7 | 8 | class AppTest(TestCase): 9 | """Test middleware through the Django test client.""" 10 | 11 | def test_request_dnt(self): 12 | """Test a request with header "DNT: 1".""" 13 | response = self.client.get('/', HTTP_DNT='1') 14 | self.assertEqual(response['Vary'], 'DNT') 15 | content = response.content.decode('utf8') 16 | self.assertInHTML('True', content) 17 | 18 | def test_request_no_dnt(self): 19 | """Test a request with no DNT header.""" 20 | response = self.client.get('/') 21 | self.assertEqual(response['Vary'], 'DNT') 22 | content = response.content.decode('utf8') 23 | self.assertInHTML('False', content) 24 | -------------------------------------------------------------------------------- /tests/test_middleware.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Middleware tests.""" 3 | from __future__ import unicode_literals 4 | 5 | from django.test import TestCase 6 | from django.http import HttpRequest, HttpResponse 7 | 8 | from dnt.middleware import DoNotTrackMiddleware 9 | 10 | 11 | class DoNotTrackMiddlewareTest(TestCase): 12 | """Unit tests for the DoNotTrackMiddleware.""" 13 | 14 | def request(self): 15 | """Create a request object for middleware testing.""" 16 | req = HttpRequest() 17 | req.META = { 18 | 'SERVER_NAME': 'testserver', 19 | 'SERVER_PORT': 80, 20 | } 21 | req.path = req.path_info = "/" 22 | return req 23 | 24 | def response(self): 25 | """Create a response object for middleware testing.""" 26 | resp = HttpResponse() 27 | resp.status_code = 200 28 | resp.content = b' ' 29 | return resp 30 | 31 | def test_request_dnt(self): 32 | """The header "DNT: 1" sets request.DNT.""" 33 | request = self.request() 34 | request.META['HTTP_DNT'] = '1' 35 | DoNotTrackMiddleware().process_request(request) 36 | self.assertTrue(request.DNT) 37 | 38 | def test_request_dnt_off(self): 39 | """The header "DNT: 0" clears request.DNT.""" 40 | request = self.request() 41 | request.META['HTTP_DNT'] = '0' 42 | DoNotTrackMiddleware().process_request(request) 43 | self.assertFalse(request.DNT) 44 | 45 | def test_request_no_dnt(self): 46 | """If the DNT header is not present, request.DNT is false.""" 47 | request = self.request() 48 | DoNotTrackMiddleware().process_request(request) 49 | self.assertFalse(request.DNT) 50 | 51 | def test_response(self): 52 | """The Vary caching header in the response includes DNT.""" 53 | request = self.request() 54 | response = self.response() 55 | response = DoNotTrackMiddleware().process_response(request, response) 56 | self.assertEqual(response['Vary'], 'DNT') 57 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | skip_missing_interpreters = true 3 | envlist = 4 | lint 5 | py{27,33,34,35}-1.8 6 | py{27,34,35,36}-{1.9,1.10,1.11} 7 | py{35,36,37}-{2.0,2.1,2.2,master} 8 | 9 | [testenv] 10 | passenv = TOXENV CI TRAVIS TRAVIS_* CODECOV_* 11 | basepython = 12 | py27: python2.7 13 | py33: python3.3 14 | py34: python3.4 15 | py35: python3.5 16 | py36: python3.6 17 | py37: python3.7 18 | usedevelop = true 19 | pip_pre = true 20 | setenv = 21 | DJANGO_SETTINGS_MODULE=testapp.settings 22 | commands = 23 | coverage run --branch --source=dnt {envbindir}/django-admin.py test 24 | codecov -e TOXENV 25 | deps = 26 | 1.8: Django>=1.8,<1.9 27 | 1.9: Django>=1.9,<1.10 28 | 1.10: Django>=1.10,<1.11 29 | 1.11: Django>=1.11,<1.12 30 | 2.0: Django>=2.0,<2.1 31 | 2.1: Django>=2.1,<2.2 32 | 2.2: Django>=2.2a1,<2.3 33 | master: https://github.com/django/django/archive/master.tar.gz 34 | codecov>=1.4.0 35 | coverage 36 | 37 | [testenv:lint] 38 | basepython = python3.5 39 | deps = 40 | flake8 41 | commands = make lint 42 | whitelist_externals = make 43 | --------------------------------------------------------------------------------