diff --git a/.gitignore b/.gitignore index a87a7a9..d616845 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,22 @@ -*.pyc -*pip* -*.egg-info -dist +# Cherry-picked from: +# https://github.com/github/gitignore/blob/master/Python.gitignore + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# Distribution / packacking +*.egg-info/ +build/ +dist/ + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..a4fea40 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,30 @@ +sudo: no +language: python +cache: pip +matrix: + include: + - python: "2.7" + env: TOXENV=py27-1.8 + - python: "3.5" + env: TOXENV=py34-1.8 + - python: "2.7" + env: TOXENV=py27-1.9 + - python: "3.5" + env: TOXENV=py35-1.9 + - python: "2.7" + env: TOXENV=py27-1.10 + - python: "3.5" + env: TOXENV=py35-1.10 + - python: "2.7" + env: TOXENV=py27-1.11 + - python: "3.6" + env: TOXENV=py36-1.11 + - python: "3.6" + env: TOXENV=py36-master + allow_failures: + - env: TOXENV=py36-master +install: + - pip install coveralls tox +script: + - tox +after_success: coveralls diff --git a/HISTORY.rst b/HISTORY.rst new file mode 100644 index 0000000..5f4ba5c --- /dev/null +++ b/HISTORY.rst @@ -0,0 +1,12 @@ +Release History +--------------- + +v0.2.0 - 2017-02-17 +~~~~~~~~~~~~~~~~~~~ +* Supported Django versions: 1.8, 1.9, 1.10, and 1.11 +* Supported Python versions: 2.7, 3.3, 3.4. 3.5, 3.6 +* Add "DNT" to Vary header in response (eillarra) + +v0.1.0 - 2011-02-16 +~~~~~~~~~~~~~~~~~~~ +* Initial Release diff --git a/MANIFEST.in b/MANIFEST.in index 9d5d250..f3279ad 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,2 +1,11 @@ +include HISTORY.rst include LICENSE +include Makefile include README.rst +include manage.py +include requirements.dev.txt +include tox.ini + +recursive-include testapp *.py +recursive-include testapp/templates *.html +recursive-include tests *.py diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..4d40f4b --- /dev/null +++ b/Makefile @@ -0,0 +1,63 @@ +export DJANGO_SETTINGS_MODULE = testapp.settings +export PYTHONPATH := $(shell pwd) +.PHONY: help clean coverage coveragehtml develop lint qa qa-all release sdist test test-all + +help: + @echo "clean - remove all artifacts" + @echo "coverage - check code coverage" + @echo "coveragehtml - display code coverage in browser" + @echo "develop - install development requirements" + @echo "lint - check style with flake8" + @echo "qa - run linters and test coverage" + @echo "qa-all - run QA plus tox and packaging" + @echo "release - package and upload a release" + @echo "sdist - package" + @echo "test - run tests" + @echo "test-all - run tests on every Python version with tox" + @echo "test-release - upload a release to the test PyPI server" + +clean: + git clean -Xfd + +develop: + pip install -r requirements.dev.txt + +lint: + flake8 . + +test: + django-admin test + +test-all: + tox --skip_missing_interpreters + +coverage: clean + coverage erase + coverage run --branch --source=dnt `which django-admin` test + +coveragehtml: coverage + coverage html + python -m webbrowser file://$(CURDIR)/htmlcov/index.html + +qa: lint coveragehtml + +qa-all: qa sdist test-all + +sdist: + python setup.py sdist bdist_wheel + ls -l dist + check-manifest + pyroma dist/`ls -t dist | grep tar.gz | head -n1` + +release: clean sdist + twine register dist/*.tar.gz + twine register dist/*.whl + twine upload dist/* + python -m webbrowser -n https://pypi.python.org/pypi/django-dnt + +# Add [test] section to ~/.pypirc, https://testpypi.python.org/pypi +test-release: clean sdist + twine register --repository test dist/*.tar.gz + twine register --repository test dist/*.whl + twine upload --repository test dist/* + python -m webbrowser -n https://testpypi.python.org/pypi/django-dnt diff --git a/README.rst b/README.rst index f406a6a..054effa 100644 --- a/README.rst +++ b/README.rst @@ -2,9 +2,24 @@ Django-DNT ========== +.. image:: http://img.shields.io/travis/mozilla/django-dnt/master.svg + :alt: The status of Travis continuous integration tests + :target: https://travis-ci.org/mozilla/django-dnt + +.. image:: https://img.shields.io/coveralls/mozilla/django-dnt/master.svg + :target: https://coveralls.io/r/mozilla/django-dnt + :alt: The code coverage + +.. image:: https://img.shields.io/pypi/v/django-dnt.svg + :alt: The PyPI package + :target: https://pypi.python.org/pypi/django-dnt + +.. Omit badges from docs + Do Not Track offers an easy way to pay attention to the ``DNT`` HTTP header. If users are sending ``DNT: 1``, ``DoNotTrackMiddleware`` will set ``request.DNT = True``, else it will set ``request.DNT = False``. Just add ``dnt.middleware.DoNotTrackMiddleware`` to your ``MIDDLEWARE_CLASSES`` -and you're good to go. +(Django 1.9 and earlier) or ``MIDDLEWARE`` (Django 1.10 and later) and you're +good to go. diff --git a/dnt/__init__.py b/dnt/__init__.py index e69de29..b06ce49 100644 --- a/dnt/__init__.py +++ b/dnt/__init__.py @@ -0,0 +1,6 @@ +""" +Django Middleware for DNT (Do Not Track) HTTP header. + +https://en.wikipedia.org/wiki/Do_Not_Track +""" +VERSION = '0.2.0' diff --git a/dnt/middleware.py b/dnt/middleware.py index 432d1e5..36a5dba 100644 --- a/dnt/middleware.py +++ b/dnt/middleware.py @@ -1,11 +1,19 @@ from django.utils.cache import patch_vary_headers +try: + # Added in Django 1.10 + from django.utils.deprecation import MiddlewareMixin +except ImportError: + _base_class = object # pragma: no cover +else: + _base_class = MiddlewareMixin # pragma: no cover + + +class DoNotTrackMiddleware(_base_class): -class DoNotTrackMiddleware(object): - def process_request(self, request): """ - Sets request.DNT to True or False based on the presence of the DNT HTTP header. + Sets flag request.DNT based on DNT HTTP header. """ if 'HTTP_DNT' in request.META and request.META['HTTP_DNT'] == '1': request.DNT = True diff --git a/requirements.dev.txt b/requirements.dev.txt new file mode 100644 index 0000000..c20a0f3 --- /dev/null +++ b/requirements.dev.txt @@ -0,0 +1,9 @@ +# Testing and development requirements +Django +check-manifest +coverage +flake8 +pyroma +tox +twine +wheel diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..3c6e79c --- /dev/null +++ b/setup.cfg @@ -0,0 +1,2 @@ +[bdist_wheel] +universal=1 diff --git a/setup.py b/setup.py index d8e0ff4..47540af 100644 --- a/setup.py +++ b/setup.py @@ -1,18 +1,70 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +"""Packaging setup for django-dnt.""" + from setuptools import setup +description = 'Make Django requests aware of the DNT header' + + +def read_file(path): + contents = open(path).read() + if hasattr(contents, 'decode'): + return contents.decode('utf8') # Python2 bytes to unicode + else: + return contents # Python3 reads unicode + + +def long_description(): + """Create a PyPI long description from docs.""" + readme = read_file('README.rst') + body_tag = ".. Omit badges from docs" + try: + readme_body_start = readme.index(body_tag) + except ValueError: + readme_body = readme + else: + # Omit the badges and reconstruct the title + readme_text = readme[readme_body_start + len(body_tag) + 1:] + readme_body = """\ +%(title_mark)s +%(title)s +%(title_mark)s +%(readme_text)s +""" % { + 'title_mark': '=' * len(description), + 'title': description, + 'readme_text': readme_text, + } + + try: + history = read_file('HISTORY.rst') + except IOError: + history = '' + + long_description = """\ +%(readme)s + +%(history)s +""" % { + 'readme': readme_body, + 'history': history + } + return long_description + + setup( name='django-dnt', - version='0.1.0', - description='Make Django requests aware of the DNT header.', - long_description=open('README.rst').read(), + version='0.2.0', + description=description + '.', + long_description=long_description(), author='James Socol', author_email='james@mozilla.com', url='http://github.com/mozilla/django-dnt', license='BSD', packages=['dnt'], - include_package_data=True, - package_data = { '': ['README.rst'] }, zip_safe=False, + keywords='django-dnt dnt do not track', classifiers=[ 'Development Status :: 4 - Beta', 'Environment :: Web Environment', @@ -23,5 +75,12 @@ 'Operating System :: OS Independent', 'Programming Language :: Python', 'Topic :: Software Development :: Libraries :: Python Modules', + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.3', + 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', ] ) diff --git a/__init__.py b/testapp/__init__.py similarity index 100% rename from __init__.py rename to testapp/__init__.py diff --git a/testapp/settings.py b/testapp/settings.py new file mode 100644 index 0000000..eadc10b --- /dev/null +++ b/testapp/settings.py @@ -0,0 +1,68 @@ +import django + + +SECRET_KEY = "dnt-tests" +DEBUG = True +ALLOWED_HOSTS = [] + +INSTALLED_APPS = ( + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + + 'testapp', +) + +if django.VERSION[:2] < (1, 10): + # Django 1.9 and earlier + MIDDLEWARE_CLASSES = ( + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', + 'django.middleware.security.SecurityMiddleware', + 'dnt.middleware.DoNotTrackMiddleware', + ) +else: + # Django 1.10 and later + MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', + 'dnt.middleware.DoNotTrackMiddleware', + ] + +ROOT_URLCONF = 'testapp.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': ':memory:' + } +} diff --git a/testapp/templates/index.html b/testapp/templates/index.html new file mode 100644 index 0000000..484ebe8 --- /dev/null +++ b/testapp/templates/index.html @@ -0,0 +1,9 @@ + + + + Do Not Track + + +

Do Not Track: {{ request.DNT }}

+ + diff --git a/testapp/urls.py b/testapp/urls.py new file mode 100644 index 0000000..c034c93 --- /dev/null +++ b/testapp/urls.py @@ -0,0 +1,7 @@ +from django.conf.urls import url +from django.views.generic import TemplateView + + +urlpatterns = [ + url(r'^$', TemplateView.as_view(template_name="index.html"), name="index"), +] diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_app.py b/tests/test_app.py new file mode 100644 index 0000000..b347f99 --- /dev/null +++ b/tests/test_app.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +"""Test an application using django-dnt.""" +from __future__ import unicode_literals + +from django.test import TestCase + + +class AppTest(TestCase): + """Test middleware through the Django test client.""" + + def test_request_dnt(self): + """Test a request with header "DNT: 1".""" + response = self.client.get('/', HTTP_DNT='1') + self.assertEqual(response['Vary'], 'DNT') + content = response.content.decode('utf8') + self.assertInHTML('True', content) + + def test_request_no_dnt(self): + """Test a request with no DNT header.""" + response = self.client.get('/') + self.assertEqual(response['Vary'], 'DNT') + content = response.content.decode('utf8') + self.assertInHTML('False', content) diff --git a/tests/test_middleware.py b/tests/test_middleware.py new file mode 100644 index 0000000..68c1b2f --- /dev/null +++ b/tests/test_middleware.py @@ -0,0 +1,56 @@ +# -*- coding: utf-8 -*- +"""Middleware tests.""" +from __future__ import unicode_literals + +from django.test import TestCase +from django.http import HttpRequest, HttpResponse + +from dnt.middleware import DoNotTrackMiddleware + + +class DoNotTrackMiddlewareTest(TestCase): + """Unit tests for the DoNotTrackMiddleware.""" + + def request(self): + """Create a request object for middleware testing.""" + req = HttpRequest() + req.META = { + 'SERVER_NAME': 'testserver', + 'SERVER_PORT': 80, + } + req.path = req.path_info = "/" + return req + + def response(self): + """Create a response object for middleware testing.""" + resp = HttpResponse() + resp.status_code = 200 + resp.content = b' ' + return resp + + def test_request_dnt(self): + """The header "DNT: 1" sets request.DNT.""" + request = self.request() + request.META['HTTP_DNT'] = '1' + DoNotTrackMiddleware().process_request(request) + self.assertTrue(request.DNT) + + def test_request_dnt_off(self): + """The header "DNT: 0" clears request.DNT.""" + request = self.request() + request.META['HTTP_DNT'] = '0' + DoNotTrackMiddleware().process_request(request) + self.assertFalse(request.DNT) + + def test_request_no_dnt(self): + """If the DNT header is not present, request.DNT is false.""" + request = self.request() + DoNotTrackMiddleware().process_request(request) + self.assertFalse(request.DNT) + + def test_response(self): + """The Vary caching header in the response includes DNT.""" + request = self.request() + response = self.response() + response = DoNotTrackMiddleware().process_response(request, response) + self.assertEqual(response['Vary'], 'DNT') diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..13feed5 --- /dev/null +++ b/tox.ini @@ -0,0 +1,32 @@ +[tox] +skip_missing_interpreters = true +envlist = + py{27,33,34,35}-1.8 + py{27,34,35,36}-{1.9,1.10,1.11} + py{35,36}-master + +[testenv] +basepython = + py27: python2.7 + py33: python3.3 + py34: python3.4 + py35: python3.5 + py36: python3.6 +usedevelop = true +pip_pre = true +setenv = + PYTHONPATH={toxinidir} + DJANGO_SETTINGS_MODULE=testapp.settings +commands = + django-admin check + coverage run --branch --source=dnt {envbindir}/django-admin.py test +deps = + 1.8: Django>=1.8,<1.9 + 1.9: Django>=1.9,<1.10 + 1.10: Django>=1.10,<1.11 + 1.11: Django>=1.11a1,<1.12 + master: https://github.com/django/django/archive/master.tar.gz + coverage +whitelist_externals = + make + which