├── .editorconfig ├── .gitignore ├── AUTHORS.rst ├── HISTORY.rst ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.rst ├── django_pixels ├── __init__.py ├── apps.py ├── const.py ├── exceptions.py ├── handlers.py ├── models.py ├── urls.py ├── utils.py └── views.py ├── requirements.txt ├── requirements_dev.txt ├── requirements_test.txt ├── runtests.py ├── setup.cfg ├── setup.py └── tests ├── __init__.py └── test_models.py /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.{py,rst,ini}] 12 | indent_style = space 13 | indent_size = 4 14 | 15 | [*.{html,css,scss,json,yml}] 16 | indent_style = space 17 | indent_size = 2 18 | 19 | [*.md] 20 | trim_trailing_whitespace = false 21 | 22 | [Makefile] 23 | indent_style = tab 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | __pycache__ 3 | 4 | # C extensions 5 | *.so 6 | 7 | # Packages 8 | *.egg 9 | *.egg-info 10 | dist 11 | build 12 | eggs 13 | parts 14 | bin 15 | var 16 | sdist 17 | develop-eggs 18 | .installed.cfg 19 | lib 20 | lib64 21 | 22 | # Installer logs 23 | pip-log.txt 24 | 25 | # Unit test / coverage reports 26 | .coverage 27 | .tox 28 | nosetests.xml 29 | htmlcov 30 | 31 | # Translations 32 | *.mo 33 | 34 | # Mr Developer 35 | .mr.developer.cfg 36 | .project 37 | .pydevproject 38 | 39 | # Pycharm/Intellij 40 | .idea 41 | 42 | # Complexity 43 | output/*.html 44 | output/*/index.html 45 | 46 | # Sphinx 47 | docs/_build 48 | -------------------------------------------------------------------------------- /AUTHORS.rst: -------------------------------------------------------------------------------- 1 | ======= 2 | Credits 3 | ======= 4 | 5 | Development Lead 6 | ---------------- 7 | 8 | * Kasun Herath 9 | 10 | Contributors 11 | ------------ 12 | 13 | None yet. Why not be the first? 14 | -------------------------------------------------------------------------------- /HISTORY.rst: -------------------------------------------------------------------------------- 1 | .. :changelog: 2 | 3 | History 4 | ------- 5 | 6 | 0.2 (2016-12-25) 7 | ++++++++++++++++++ 8 | 9 | * Version 0.2. 10 | 11 | 0.1 (2016-12-10) 12 | ++++++++++++++++++ 13 | 14 | * First release on PyPI. 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | MIT License 3 | 4 | Copyright (c) 2016, Kasun Herath 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 7 | 8 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 9 | 10 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include AUTHORS.rst 2 | include HISTORY.rst 3 | include LICENSE 4 | include README.rst 5 | recursive-include django_pixels *py 6 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: clean-pyc clean-build docs help 2 | .DEFAULT_GOAL := help 3 | define BROWSER_PYSCRIPT 4 | import os, webbrowser, sys 5 | try: 6 | from urllib import pathname2url 7 | except: 8 | from urllib.request import pathname2url 9 | 10 | webbrowser.open("file://" + pathname2url(os.path.abspath(sys.argv[1]))) 11 | endef 12 | export BROWSER_PYSCRIPT 13 | BROWSER := python -c "$$BROWSER_PYSCRIPT" 14 | 15 | help: 16 | @perl -nle'print $& if m{^[a-zA-Z_-]+:.*?## .*$$}' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-25s\033[0m %s\n", $$1, $$2}' 17 | 18 | clean: clean-build clean-pyc 19 | 20 | clean-build: ## remove build artifacts 21 | rm -fr build/ 22 | rm -fr dist/ 23 | rm -fr *.egg-info 24 | 25 | clean-pyc: ## remove Python file artifacts 26 | find . -name '*.pyc' -exec rm -f {} + 27 | find . -name '*.pyo' -exec rm -f {} + 28 | find . -name '*~' -exec rm -f {} + 29 | 30 | lint: ## check style with flake8 31 | flake8 django_pixels tests 32 | 33 | test: ## run tests quickly with the default Python 34 | python runtests.py tests 35 | 36 | test-all: ## run tests on every Python version with tox 37 | tox 38 | 39 | coverage: ## check code coverage quickly with the default Python 40 | coverage run --source django_pixels runtests.py tests 41 | coverage report -m 42 | coverage html 43 | open htmlcov/index.html 44 | 45 | docs: ## generate Sphinx HTML documentation, including API docs 46 | rm -f docs/django_pixels.rst 47 | rm -f docs/modules.rst 48 | sphinx-apidoc -o docs/ django_pixels 49 | $(MAKE) -C docs clean 50 | $(MAKE) -C docs html 51 | $(BROWSER) docs/_build/html/index.html 52 | 53 | release: clean ## package and upload a release 54 | python setup.py sdist upload 55 | python setup.py bdist_wheel upload 56 | 57 | sdist: clean ## package 58 | python setup.py sdist 59 | ls -l dist 60 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ============================= 2 | django-pixels 3 | ============================= 4 | 5 | Tracking pixels made easy. For Python 2 and Django 1.7+ 6 | 7 | Features 8 | ---------- 9 | * Built-in views to serve transparent pixels or 204 responses. 10 | * Compose pixel tracking urls with different type IDs. 11 | * Route tracking requests to functions using type IDs. 12 | 13 | Implementation Notes with Short Examples 14 | ---------- 15 | 16 | Install django-pixels:: 17 | 18 | pip install django-pixels 19 | 20 | 21 | Mount pixel tracking URL patterns: 22 | 23 | .. code-block:: python 24 | 25 | urlpatterns = [ 26 | ... 27 | url(r'^tracker/', include('django_pixels.urls', namespace="pixels")), 28 | ... 29 | ] 30 | 31 | 32 | Get the general pixel tracking url (This serves a transparent pixel as the response): 33 | 34 | .. code-block:: python 35 | 36 | from django.core.urlresolvers import reverse 37 | tracking_url = reverse('pixels:pixel') # given you have mounted django_pixels urls with namespace='pixels' 38 | 39 | Get the tracking url with no-content(204) response (This serves an empty response with code 204): 40 | 41 | .. code-block:: python 42 | 43 | from django.core.urlresolvers import reverse 44 | tracking_url = reverse('pixels:pixel-204') # given you have mounted django_pixels urls with namespace='pixels' 45 | 46 | 47 | Generate a pixel tracking url with type 1: 48 | 49 | .. code-block:: python 50 | 51 | from django_pixels import utils 52 | 53 | utils.compose_pixel_url(tracking_url, 1) 54 | 55 | 56 | Write a function to handle tracking calls with type 1: 57 | 58 | .. code-block:: python 59 | 60 | def track_emails(request): 61 | # handle tracking with the passed HttpRequest instance 62 | 63 | 64 | Register the function to handle tracking calls with type 1: 65 | 66 | .. code-block:: python 67 | 68 | from django_pixels import handlers 69 | 70 | handlers.register(1, track_emails) 71 | 72 | 73 | Or mark a function to handle tracking calls with type 2: 74 | 75 | .. code-block:: python 76 | 77 | from django_pixels import handlers 78 | 79 | @handlers.track(type_id=2) 80 | def track_emails(request): 81 | # handle tracking with the passed HttpRequest instance 82 | 83 | 84 | Settings 85 | ---------- 86 | * PIXELS_TYPE_PARAMETER_NAME - Change the parameter name used for tracking type 87 | 88 | 89 | Credits 90 | ------- 91 | 92 | Tools used in rendering this package: 93 | 94 | * Cookiecutter_ 95 | * `cookiecutter-djangopackage`_ 96 | 97 | .. _Cookiecutter: https://github.com/audreyr/cookiecutter 98 | .. _`cookiecutter-djangopackage`: https://github.com/pydanny/cookiecutter-djangopackage 99 | -------------------------------------------------------------------------------- /django_pixels/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = '0.2' 2 | default_app_config = 'django_pixels.apps.DjangoPixelsConfig' 3 | -------------------------------------------------------------------------------- /django_pixels/apps.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 2 | from django.apps import AppConfig 3 | 4 | 5 | class DjangoPixelsConfig(AppConfig): 6 | name = 'django_pixels' 7 | -------------------------------------------------------------------------------- /django_pixels/const.py: -------------------------------------------------------------------------------- 1 | TYPE_PARAM_NAME = 't' 2 | -------------------------------------------------------------------------------- /django_pixels/exceptions.py: -------------------------------------------------------------------------------- 1 | class PixelError(Exception): 2 | pass 3 | -------------------------------------------------------------------------------- /django_pixels/handlers.py: -------------------------------------------------------------------------------- 1 | from .exceptions import PixelError 2 | 3 | 4 | map = {} 5 | 6 | 7 | def track(type_id): 8 | """ Decorator to register a function as the handler for a tracking call type. """ 9 | type_id = int(type_id) 10 | 11 | def wrapper(func): 12 | map[type_id] = func 13 | return func 14 | 15 | return wrapper 16 | 17 | 18 | def register(type_id, func): 19 | """ register a function as the handler for a tracking call type. """ 20 | type_id = int(type_id) 21 | map[type_id] = func 22 | 23 | 24 | def get_handler(type_id): 25 | """ Get handler function for a given tracking call type. """ 26 | type_id = int(type_id) 27 | try: 28 | return map[type_id] 29 | except KeyError: 30 | raise PixelError('No handler registered for type: {}'.format(type_id)) 31 | -------------------------------------------------------------------------------- /django_pixels/models.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | -------------------------------------------------------------------------------- /django_pixels/urls.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from django.conf.urls import url 3 | 4 | from . import views 5 | 6 | 7 | urlpatterns = [ 8 | url(r'pixel/$', views.pixel, name='pixel'), 9 | url(r'pixel204/$', views.pixel204, name='pixel-204'), 10 | ] 11 | -------------------------------------------------------------------------------- /django_pixels/utils.py: -------------------------------------------------------------------------------- 1 | # Python 3 support 2 | try: 3 | import urlparse 4 | from urllib import urlencode 5 | except: 6 | import urllib.parse as urlparse 7 | from urllib.parse import urlencode 8 | 9 | from django.conf import settings 10 | 11 | from .const import TYPE_PARAM_NAME 12 | 13 | 14 | def get_type_parameter_name(): 15 | """ Get the parameter name used for type_id in pixel request. """ 16 | try: 17 | type_parameter_name = settings.PIXELS_TYPE_PARAMETER_NAME 18 | except AttributeError: 19 | type_parameter_name = TYPE_PARAM_NAME 20 | 21 | return type_parameter_name 22 | 23 | 24 | def compose_pixel_url(tracking_url, type_id): 25 | """ Insert tracking call type_id into a given pixel tracking call. """ 26 | type_id = int(type_id) 27 | type_parameter_name = get_type_parameter_name() 28 | 29 | url_parts = list(urlparse.urlparse(tracking_url)) 30 | query = dict(urlparse.parse_qsl(url_parts[4])) 31 | query.update({type_parameter_name: type_id}) 32 | url_parts[4] = urlencode(query) 33 | 34 | return urlparse.urlunparse(url_parts) 35 | -------------------------------------------------------------------------------- /django_pixels/views.py: -------------------------------------------------------------------------------- 1 | from django.http import HttpResponse 2 | 3 | from .utils import get_type_parameter_name 4 | from .exceptions import PixelError 5 | from . import handlers 6 | 7 | 8 | def serve(request, response): 9 | type_parameter = get_type_parameter_name() 10 | type_id = request.GET.get(type_parameter) 11 | if not type_id: 12 | return response 13 | 14 | try: 15 | handler = handlers.get_handler(type_id) 16 | except PixelError: 17 | pass 18 | else: 19 | handler(request) 20 | 21 | return response 22 | 23 | 24 | def pixel204(request): 25 | """ Return a 204 response. """ 26 | response = HttpResponse(204) 27 | return serve(request, response) 28 | 29 | 30 | def pixel(request): 31 | """ Return a transparent pixel. """ 32 | pixel_= "\x47\x49\x46\x38\x39\x61\x01\x00\x01\x00\x80\x00\x00\xff\xff\xff\x00\x00\x00\x21\xf9\x04\x01\x00\x00\x00\x00\x2c\x00\x00\x00\x00\x01\x00\x01\x00\x00\x02\x02\x44\x01\x00\x3b" 33 | response = HttpResponse(pixel_, content_type='image/gif') 34 | return serve(request, response) 35 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | 2 | # Additional requirements go here 3 | -------------------------------------------------------------------------------- /requirements_dev.txt: -------------------------------------------------------------------------------- 1 | bumpversion==0.5.3 2 | wheel==0.29.0 3 | 4 | -------------------------------------------------------------------------------- /requirements_test.txt: -------------------------------------------------------------------------------- 1 | coverage==4.2 2 | mock>=1.0.1 3 | flake8>=2.1.0 4 | tox>=1.7.0 5 | 6 | 7 | # Additional test requirements go here 8 | -------------------------------------------------------------------------------- /runtests.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | try: 4 | from django.conf import settings 5 | from django.test.utils import get_runner 6 | 7 | settings.configure( 8 | DEBUG=True, 9 | USE_TZ=True, 10 | DATABASES={ 11 | "default": { 12 | "ENGINE": "django.db.backends.sqlite3", 13 | } 14 | }, 15 | ROOT_URLCONF="django_pixels.urls", 16 | INSTALLED_APPS=[ 17 | "django.contrib.auth", 18 | "django.contrib.contenttypes", 19 | "django.contrib.sites", 20 | "django_pixels", 21 | ], 22 | SITE_ID=1, 23 | MIDDLEWARE_CLASSES=(), 24 | ) 25 | 26 | try: 27 | import django 28 | setup = django.setup 29 | except AttributeError: 30 | pass 31 | else: 32 | setup() 33 | 34 | except ImportError: 35 | import traceback 36 | traceback.print_exc() 37 | msg = "To fix this error, run: pip install -r requirements_test.txt" 38 | raise ImportError(msg) 39 | 40 | 41 | def run_tests(*test_args): 42 | if not test_args: 43 | test_args = ['tests'] 44 | 45 | # Run tests 46 | TestRunner = get_runner(settings) 47 | test_runner = TestRunner() 48 | 49 | failures = test_runner.run_tests(test_args) 50 | 51 | if failures: 52 | sys.exit(bool(failures)) 53 | 54 | 55 | if __name__ == '__main__': 56 | run_tests(*sys.argv[1:]) 57 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 0.2 3 | commit = True 4 | tag = True 5 | 6 | [bumpversion:file:setup.py] 7 | 8 | [bumpversion:file:django_pixels/__init__.py] 9 | 10 | [wheel] 11 | universal = 1 12 | 13 | [flake8] 14 | exclude = 15 | .git, 16 | .tox 17 | docs/source/conf.py, 18 | build, 19 | dist 20 | max-line-length = 120 21 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | import os 4 | import re 5 | import sys 6 | 7 | try: 8 | from setuptools import setup 9 | except ImportError: 10 | from distutils.core import setup 11 | 12 | 13 | def get_version(*file_paths): 14 | """Retrieves the version from django_pixels/__init__.py""" 15 | filename = os.path.join(os.path.dirname(__file__), *file_paths) 16 | version_file = open(filename).read() 17 | version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]", 18 | version_file, re.M) 19 | if version_match: 20 | return version_match.group(1) 21 | raise RuntimeError('Unable to find version string.') 22 | 23 | version = get_version("django_pixels", "__init__.py") 24 | 25 | 26 | if sys.argv[-1] == 'publish': 27 | try: 28 | import wheel 29 | print("Wheel version: ", wheel.__version__) 30 | except ImportError: 31 | print('Wheel library missing. Please run "pip install wheel"') 32 | sys.exit() 33 | os.system('python setup.py sdist upload') 34 | os.system('python setup.py bdist_wheel upload') 35 | sys.exit() 36 | 37 | if sys.argv[-1] == 'tag': 38 | print("Tagging the version on git:") 39 | os.system("git tag -a %s -m 'version %s'" % (version, version)) 40 | os.system("git push --tags") 41 | sys.exit() 42 | 43 | readme = open('README.rst').read() 44 | history = open('HISTORY.rst').read().replace('.. :changelog:', '') 45 | 46 | setup( 47 | name='django-pixels', 48 | version=version, 49 | description="""Tracking pixels made easy""", 50 | long_description=readme + '\n\n' + history, 51 | author='Kasun Herath', 52 | author_email='kasunh01@gmail.com', 53 | url='https://github.com/kasun/django-pixels', 54 | packages=[ 55 | 'django_pixels', 56 | ], 57 | include_package_data=True, 58 | install_requires=[], 59 | license="MIT", 60 | zip_safe=False, 61 | keywords='django_pixels', 62 | classifiers=[ 63 | 'Development Status :: 3 - Alpha', 64 | 'Framework :: Django', 65 | 'Framework :: Django :: 1.7', 66 | 'Framework :: Django :: 1.8', 67 | 'Framework :: Django :: 1.9', 68 | 'Framework :: Django :: 1.10', 69 | 'Intended Audience :: Developers', 70 | 'License :: OSI Approved :: BSD License', 71 | 'Natural Language :: English', 72 | 'Programming Language :: Python :: 2', 73 | 'Programming Language :: Python :: 2.7', 74 | ], 75 | ) 76 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kasun/django-pixels/d4a0f01175995b8c33098d295c5c0e1cdf80e581/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_models.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | test_django_pixels 6 | ------------ 7 | 8 | Tests for `django_pixels` models module. 9 | """ 10 | 11 | from django.test import TestCase 12 | 13 | # from django_pixels import models 14 | 15 | 16 | class TestDjango_pixels(TestCase): 17 | 18 | def setUp(self): 19 | pass 20 | 21 | def test_something(self): 22 | pass 23 | 24 | def tearDown(self): 25 | pass 26 | --------------------------------------------------------------------------------