├── .gitignore ├── intercooler_helpers ├── tests │ ├── __init__.py │ ├── test_redirect_middleware.py │ └── test_middleware.py ├── __init__.py └── middleware.py ├── test_templates ├── form.html ├── polling_response.html ├── redirected.html ├── form_include.html ├── polling.html ├── infinite_scrolling.html ├── polling_include.html ├── infinite_scrolling_partial.html ├── infinite_scrolling_include.html ├── base.html └── demo_project.html ├── CHANGELOG ├── MANIFEST.in ├── .bumpversion.cfg ├── conftest.py ├── .coveragerc ├── tox.ini ├── .travis.yml ├── setup.cfg ├── demo_project.py ├── LICENSE ├── Makefile ├── test_settings.py ├── setup.py ├── test_urls.py └── README.rst /.gitignore: -------------------------------------------------------------------------------- 1 | htmlcov 2 | dist 3 | *.pyo 4 | *.pyc.coverage 5 | *.egg* 6 | *.sqlite3 7 | -------------------------------------------------------------------------------- /intercooler_helpers/tests/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import absolute_import, unicode_literals 3 | __all__ = [] 4 | -------------------------------------------------------------------------------- /test_templates/form.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 | {% include "form_include.html" %} 5 | {% endblock content %} 6 | -------------------------------------------------------------------------------- /test_templates/polling_response.html: -------------------------------------------------------------------------------- 1 |
Wheee, polled successfully and got {{ item }}
2 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | Change history for django-intercooler_helpers 2 | ------------------------------------------------------------- 3 | 0.2.0 4 | ^^^^^^ 5 | * Initial release. 6 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include README.rst 3 | include tox.ini 4 | include Makefile 5 | include .coveragerc 6 | include CHANGELOG 7 | global-include *.rst *.py *.html 8 | -------------------------------------------------------------------------------- /test_templates/redirected.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 | You were redirected via {% url "redirector" %} 5 | {% endblock content %} 6 | -------------------------------------------------------------------------------- /.bumpversion.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 0.2.0 3 | files = setup.py intercooler_helpers/__init__.py README.rst CHANGELOG 4 | commit = True 5 | tag = True 6 | tag_name = {new_version} 7 | search = 8 | ?next? 9 | 10 | -------------------------------------------------------------------------------- /test_templates/form_include.html: -------------------------------------------------------------------------------- 1 |
2 | {{ form.as_p }} 3 | 4 |
5 | -------------------------------------------------------------------------------- /intercooler_helpers/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import absolute_import 3 | from __future__ import unicode_literals 4 | 5 | __version_info__ = '0.2.0' 6 | __version__ = '0.2.0' 7 | version = '0.2.0' 8 | VERSION = '0.2.0' 9 | 10 | def get_version(): 11 | return version # pragma: no cover 12 | -------------------------------------------------------------------------------- /conftest.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import absolute_import 3 | import os 4 | 5 | def pytest_configure(): 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "test_settings") 7 | import django 8 | from django.conf import settings 9 | if settings.configured and hasattr(django, 'setup'): 10 | django.setup() 11 | -------------------------------------------------------------------------------- /test_templates/polling.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% comment %} 4 | This template only exists so that if you go to /polling/ you get a 5 | full valid HTML response. 6 | It is otherwise unused. 7 | Progressive enhancement! 8 | {% endcomment %} 9 | 10 | {% block content %} 11 | {% include "polling_include.html" %} 12 | {% endblock %} 13 | 14 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | source = intercooler_helpers 3 | omit = 4 | intercooler_helpers/tests/* 5 | branch = True 6 | data_file = .coverage 7 | timid = False 8 | 9 | [report] 10 | exclude_lines = 11 | pragma: no cover 12 | precision = 0 13 | show_missing = True 14 | omit = 15 | intercooler_helpers/tests/* 16 | 17 | [xml] 18 | output = coverage.xml 19 | -------------------------------------------------------------------------------- /test_templates/infinite_scrolling.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% comment %} 4 | This template only exists so that if you go to /infinite/scrolling/ you get a 5 | full valid HTML response. 6 | It is otherwise unused. 7 | Progressive enhancement! 8 | {% endcomment %} 9 | 10 | {% block content %} 11 | {% include "infinite_scrolling_include.html" %} 12 | {% endblock %} 13 | -------------------------------------------------------------------------------- /test_templates/polling_include.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
Play
4 |
Pause
5 |
6 |
7 |
8 |
9 | -------------------------------------------------------------------------------- /test_templates/infinite_scrolling_partial.html: -------------------------------------------------------------------------------- 1 | 2 |   3 | Row #{{ forloop.counter }} 4 | {{ row }} 5 | 6 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | minversion=2.2 3 | envlist = py27-dj{18,19,110}, 4 | py33-dj18, 5 | py34-dj{18,19,110}, 6 | py35-dj{18,19,110}, 7 | [testenv] 8 | commands = 9 | python -B -R -tt -W ignore setup.py test 10 | 11 | basepython = 12 | py27: python2.7 13 | py33: python3.3 14 | py34: python3.4 15 | py35: python3.5 16 | 17 | deps = 18 | dj18: Django>=1.8,<1.9 19 | dj19: Django>=1.9,<1.10 20 | dj110: Django>=1.10,<1.11 21 | -------------------------------------------------------------------------------- /test_templates/infinite_scrolling_include.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | {% for row in rows %} 11 | {% include "infinite_scrolling_partial.html" with row=row forloop=forloop only %} 12 | {% endfor %} 13 | 14 |
 # of generated responseUUID
15 | Loading 16 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3.5 3 | sudo: false 4 | 5 | notifications: 6 | email: false 7 | 8 | install: 9 | - pip install --upgrade pip setuptools tox 10 | 11 | cache: 12 | directories: 13 | - $HOME/.cache/pip 14 | 15 | env: 16 | - TOX_ENV=py27-dj18 17 | - TOX_ENV=py27-dj19 18 | - TOX_ENV=py27-dj110 19 | - TOX_ENV=py33-dj18 20 | - TOX_ENV=py34-dj18 21 | - TOX_ENV=py34-dj19 22 | - TOX_ENV=py34-dj110 23 | - TOX_ENV=py35-dj18 24 | - TOX_ENV=py35-dj19 25 | - TOX_ENV=py35-dj110 26 | 27 | script: 28 | - tox -e $TOX_ENV 29 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [pytest] 2 | norecursedirs=.* *.egg .svn _build src bin lib local include 3 | testpaths=intercooler_helpers/tests 4 | python_files=test_*.py 5 | addopts=-vvv --showlocals --cov-report html:htmlcov --cov-report term-missing:skip-covered --cov-config .coveragerc --cov intercooler_helpers 6 | 7 | [metadata] 8 | license-file = LICENSE 9 | 10 | [wheel] 11 | universal = 1 12 | 13 | [flake8] 14 | max-line-length = 80 15 | 16 | [check-manifest] 17 | ignore = 18 | .travis.yml 19 | .bumpversion.cfg 20 | .idea 21 | .tox 22 | __pycache__ 23 | bin 24 | include 25 | lib 26 | local 27 | share 28 | .Python 29 | htmlcov 30 | -------------------------------------------------------------------------------- /test_templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {% load static %} 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | {% block content %}{% endblock %} 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /demo_project.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | from __future__ import absolute_import 4 | import os 5 | import sys 6 | sys.dont_write_bytecode = True 7 | MISSING_DEPENDENCIES = [] 8 | try: 9 | from django.conf import settings 10 | except ImportError: 11 | MISSING_DEPENDENCIES.append("Django") 12 | try: 13 | import intercoolerjs 14 | except ImportError: 15 | MISSING_DEPENDENCIES.append("django-intercoolerjs") 16 | 17 | if MISSING_DEPENDENCIES: 18 | deps = " ".join(MISSING_DEPENDENCIES) 19 | sys.stdout.write("You'll need to `pip install {}` to run this demo\n".format(deps)) 20 | sys.exit(1) 21 | 22 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "test_settings") 23 | 24 | from django.core.wsgi import get_wsgi_application 25 | application = get_wsgi_application() 26 | 27 | if __name__ == "__main__": 28 | from django.core.management import execute_from_command_line 29 | execute_from_command_line(sys.argv) 30 | 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017, Keryn Knight 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | 6 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | 8 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 9 | 10 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 11 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | help: 2 | @echo "clean-build - get rid of build artifacts & metadata" 3 | @echo "clean-pyc - get rid of dross files" 4 | @echo "test - execute tests; calls clean-pyc for you" 5 | @echo "dist - build a distribution; calls test, clean-build and clean-pyc" 6 | @echo "check - check the quality of the built distribution; calls dist for you" 7 | @echo "release - register and upload to PyPI" 8 | 9 | clean-build: 10 | rm -fr build/ 11 | rm -fr htmlcov/ 12 | rm -fr dist/ 13 | rm -fr .eggs/ 14 | find . -name '*.egg-info' -exec rm -fr {} + 15 | find . -name '*.egg' -exec rm -f {} + 16 | 17 | 18 | clean-pyc: 19 | find . -name '*.pyc' -exec rm -f {} + 20 | find . -name '*.pyo' -exec rm -f {} + 21 | find . -name '*~' -exec rm -f {} + 22 | find . -name '__pycache__' -exec rm -fr {} + 23 | 24 | test: clean-pyc 25 | python -B -R -tt -W ignore setup.py test 26 | 27 | dist: test clean-build clean-pyc 28 | python setup.py sdist bdist_wheel 29 | 30 | check: dist 31 | pip install check-manifest pyroma restview 32 | check-manifest 33 | pyroma . 34 | restview --long-description 35 | 36 | release: 37 | @echo "INSTRUCTIONS:" 38 | @echo "- pip install wheel twine" 39 | @echo "- python setup.py sdist bdist_wheel" 40 | @echo "- ls dist/" 41 | @echo "- twine register dist/???" 42 | @echo "- twine upload dist/*" 43 | 44 | 45 | -------------------------------------------------------------------------------- /intercooler_helpers/tests/test_redirect_middleware.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import absolute_import, unicode_literals 3 | 4 | import pytest 5 | from django.http import HttpResponse 6 | from django.shortcuts import redirect 7 | 8 | from intercooler_helpers.middleware import IntercoolerRedirector, \ 9 | IntercoolerData 10 | 11 | 12 | @pytest.mark.parametrize("response", [ 13 | redirect('/test/', permanent=False), 14 | redirect('/', permanent=True), 15 | ]) 16 | def test_redirects_turn_into_clientside_redirects(rf, response): 17 | request = rf.get('/', HTTP_X_IC_REQUEST="true", 18 | HTTP_X_REQUESTED_WITH='XMLHttpRequest') 19 | IntercoolerData().process_request(request) 20 | url = response.url 21 | changed_response = IntercoolerRedirector().process_response(request, response) 22 | assert changed_response.has_header('X-IC-Redirect') is True 23 | assert changed_response.has_header('Location') is False 24 | assert changed_response['X-IC-Redirect'] == url 25 | 26 | 27 | @pytest.mark.parametrize("response", [ 28 | HttpResponse(status=201), 29 | HttpResponse(status=307), 30 | HttpResponse(status=501), 31 | ]) 32 | def test_redirects_not_applied_for_non_redirection_statuscodes(rf, response): 33 | request = rf.get('/', HTTP_X_IC_REQUEST="true", 34 | HTTP_X_REQUESTED_WITH='XMLHttpRequest') 35 | IntercoolerData().process_request(request) 36 | changed_response = IntercoolerRedirector().process_response(request, response) 37 | assert changed_response.has_header('X-IC-Redirect') is False 38 | 39 | 40 | def test_redirect_dies_without_intercoolerdata_middleware(rf): 41 | request = rf.get('/') 42 | response = HttpResponse(status=201) 43 | with pytest.raises(AttributeError) as exc: 44 | IntercoolerRedirector().process_response(request, response) 45 | 46 | -------------------------------------------------------------------------------- /test_settings.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals, absolute_import 3 | import os 4 | 5 | DEBUG = os.environ.get('DEBUG', 'on') == 'on' 6 | SECRET_KEY = os.environ.get('SECRET_KEY', 'TESTTESTTESTTESTTESTTESTTESTTEST') 7 | ALLOWED_HOSTS = os.environ.get('ALLOWED_HOSTS', 'localhost,testserver,*').split(',') 8 | BASE_DIR = os.path.abspath(os.path.dirname(os.path.abspath(__file__))) 9 | 10 | DATABASES = { 11 | 'default': { 12 | 'ENGINE': 'django.db.backends.sqlite3', 13 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 14 | } 15 | } 16 | 17 | INSTALLED_APPS = [ 18 | 'django.contrib.sessions', 19 | 'django.contrib.contenttypes', 20 | 'django.contrib.admin', 21 | 'django.contrib.staticfiles', 22 | 'django.contrib.auth', 23 | 'intercoolerjs', 24 | 'intercooler_helpers', 25 | ] 26 | 27 | SKIP_SOUTH_TESTS = True 28 | SOUTH_TESTS_MIGRATE = False 29 | 30 | STATIC_URL = '/__static__/' 31 | MEDIA_URL = '/__media__/' 32 | MESSAGE_STORAGE = 'django.contrib.messages.storage.cookie.CookieStorage' 33 | SESSION_ENGINE = 'django.contrib.sessions.backends.signed_cookies' 34 | SESSION_COOKIE_HTTPONLY = True 35 | 36 | ROOT_URLCONF = 'test_urls' 37 | 38 | # Use a fast hasher to speed up tests. 39 | PASSWORD_HASHERS = ( 40 | 'django.contrib.auth.hashers.MD5PasswordHasher', 41 | ) 42 | 43 | SITE_ID = 1 44 | 45 | TEMPLATE_CONTEXT_PROCESSORS = ( 46 | 'django.contrib.auth.context_processors.auth', 47 | ) 48 | TEMPLATE_LOADERS = ( 49 | 'django.template.loaders.filesystem.Loader', 50 | ) 51 | TEMPLATES = [ 52 | { 53 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 54 | 'DIRS': [os.path.join(BASE_DIR, "test_templates")], 55 | 'OPTIONS': { 56 | 'context_processors': TEMPLATE_CONTEXT_PROCESSORS, 57 | 'loaders': TEMPLATE_LOADERS, 58 | }, 59 | }, 60 | ] 61 | 62 | MIDDLEWARE_CLASSES = ( 63 | 'django.contrib.sessions.middleware.SessionMiddleware', 64 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 65 | 'django.contrib.messages.middleware.MessageMiddleware', 66 | 'intercooler_helpers.middleware.HttpMethodOverride', 67 | 'intercooler_helpers.middleware.IntercoolerData', 68 | 'intercooler_helpers.middleware.IntercoolerRedirector', 69 | ) 70 | 71 | STATIC_ROOT = os.path.join(BASE_DIR, 'test_collectstatic') 72 | MEDIA_ROOT = os.path.join(BASE_DIR, 'test_media') 73 | 74 | USE_TZ = True 75 | 76 | SILENCED_SYSTEM_CHECKS = ['1_8.W001'] 77 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | import sys 4 | import os 5 | from setuptools import setup 6 | from setuptools.command.test import test as TestCommand 7 | if sys.version_info[0] == 2: 8 | # get the Py3K compatible `encoding=` for opening files. 9 | from io import open 10 | 11 | 12 | HERE = os.path.abspath(os.path.dirname(__file__)) 13 | 14 | 15 | class PyTest(TestCommand): 16 | def initialize_options(self): 17 | TestCommand.initialize_options(self) 18 | self.pytest_args = [] 19 | 20 | def finalize_options(self): 21 | TestCommand.finalize_options(self) 22 | self.test_args = [] 23 | self.test_suite = True 24 | 25 | def run_tests(self): 26 | # import here, cause outside the eggs aren't loaded 27 | import pytest 28 | errno = pytest.main(self.pytest_args) 29 | sys.exit(errno) 30 | 31 | 32 | def make_readme(root_path): 33 | consider_files = ("README.rst", "LICENSE", "CHANGELOG", "CONTRIBUTORS") 34 | for filename in consider_files: 35 | filepath = os.path.realpath(os.path.join(root_path, filename)) 36 | if os.path.isfile(filepath): 37 | with open(filepath, mode="r", encoding="utf-8") as f: 38 | yield f.read() 39 | 40 | LICENSE = "BSD License" 41 | URL = "https://github.com/kezabelle/django-intercoolerjs-helpers" 42 | LONG_DESCRIPTION = "\r\n\r\n----\r\n\r\n".join(make_readme(HERE)) 43 | SHORT_DESCRIPTION = "a small reusable app for Django which provides a few improvements for working with Intercooler.js" 44 | KEYWORDS = ( 45 | "django", 46 | "intercooler", 47 | "intercoolerjs", 48 | ) 49 | 50 | setup( 51 | name="django-intercooler_helpers", 52 | version="0.2.0", 53 | author="Keryn Knight", 54 | author_email="django-intercooler_helpers@kerynknight.com", 55 | maintainer="Keryn Knight", 56 | maintainer_email="django-intercooler_helpers@kerynknight.com", 57 | description=SHORT_DESCRIPTION[0:200], 58 | long_description=LONG_DESCRIPTION, 59 | packages=[ 60 | "intercooler_helpers", 61 | ], 62 | include_package_data=True, 63 | install_requires=[ 64 | "Django>=1.8", 65 | "django-intercoolerjs>=1.1.0.0", 66 | ], 67 | tests_require=[ 68 | "pytest>=2.6", 69 | "pytest-django>=2.8.0,<3.0.0", 70 | "pytest-cov>=1.8", 71 | "pytest-remove-stale-bytecode>=1.0", 72 | "pytest-catchlog>=1.2", 73 | ], 74 | cmdclass={"test": PyTest}, 75 | zip_safe=False, 76 | keywords=" ".join(KEYWORDS), 77 | license=LICENSE, 78 | url=URL, 79 | classifiers=[ 80 | "Development Status :: 3 - Alpha", 81 | "Intended Audience :: Developers", 82 | "License :: OSI Approved :: {}".format(LICENSE), 83 | "Natural Language :: English", 84 | "Programming Language :: Python :: 2", 85 | "Programming Language :: Python :: 2.7", 86 | "Programming Language :: Python :: 3", 87 | "Programming Language :: Python :: 3.3", 88 | "Programming Language :: Python :: 3.4", 89 | "Programming Language :: Python :: 3.5", 90 | "Framework :: Django", 91 | "Framework :: Django :: 1.10", 92 | "Framework :: Django :: 1.8", 93 | "Framework :: Django :: 1.9", 94 | ], 95 | ) 96 | -------------------------------------------------------------------------------- /test_templates/demo_project.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 |
5 |

A quick click

6 |

This anchor tag POSTs to {% url 'click' %} when it is clicked.

7 | Click Me! 8 | Reset counter 9 |
10 |
11 |
12 |

Handling client-side redirection on server-side redirects

13 |

This anchor tag GETs from {% url 'redirector' %} when it is clicked.

14 |

Explanation

15 | 19 | Click me to do a client-side redirect 20 |
21 |
22 |
23 |

Form handling

24 |

This demo shows how to do AJAX validation & submission of a form, without doing a full page redirect.

25 |

Explanation

26 | 32 | {% include "form_include.html" %} 33 |
34 |
35 |
36 |

A Pause/Play UI

37 |

This demo shows how to set up a list that is appended to periodically and that can be paused and resumed

38 |

Explanation

39 | 55 | {% include "polling_include.html" %} 56 |
57 |
58 | 59 |
60 |

Implementing Infinite Scrolling

61 |

This example demos an infinite scroll UI.

62 |

Explanation

63 | 77 | {% include "infinite_scrolling_include.html" %} 78 |
79 | 80 |
81 | 82 | {% endblock %} 83 | -------------------------------------------------------------------------------- /test_urls.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals, absolute_import 3 | 4 | from uuid import uuid4 5 | 6 | from django.conf.urls import url 7 | from django.forms import Form, CharField, IntegerField 8 | from django.http import HttpResponse, Http404 9 | from django.shortcuts import redirect 10 | from django.template.defaultfilters import pluralize 11 | from django.template.response import TemplateResponse 12 | try: 13 | from django.urls import reverse 14 | except ImportError: # Django <1.10 15 | from django.core.urlresolvers import reverse 16 | 17 | 18 | def _page_data(): 19 | return tuple(str(uuid4()) for x in range(0, 10)) 20 | 21 | 22 | clicktrack = 0 23 | 24 | def click(request): 25 | global clicktrack 26 | clicktrack += 1 27 | do_reset = (request.is_intercooler() and 28 | request.intercooler_data.element.id == 'intro-btn2' and 29 | request.intercooler_data.current_url.match is not None) 30 | if do_reset: 31 | clicktrack = 0 32 | time = pluralize(clicktrack) 33 | text = "You clicked me {} time{}...".format(clicktrack, time) 34 | if do_reset: 35 | text = "You reset the counter!, via {}".format(request.intercooler_data.trigger.id) 36 | if not request.is_intercooler(): 37 | raise Http404("Not allowed to come here outside of an Intercooler.js request!") 38 | resp = HttpResponse(text) 39 | return resp 40 | 41 | 42 | def redirector(request): 43 | return redirect(reverse('redirected')) 44 | 45 | 46 | def redirected(request): 47 | return TemplateResponse(request, template="redirected.html", context={}) 48 | 49 | 50 | class TestForm(Form): 51 | field = CharField() 52 | number = IntegerField(max_value=10, min_value=5) 53 | 54 | 55 | def form(request): 56 | template = "form.html" 57 | _form = TestForm(request.POST or None) 58 | if _form.is_valid(): 59 | return redirect(reverse('redirected')) 60 | context = {'form': _form} 61 | return TemplateResponse(request, template=template, context=context) 62 | 63 | 64 | 65 | def polling_stop(request): 66 | resp = HttpResponse("Cancelled") 67 | resp['X-IC-CancelPolling'] = "true" 68 | return resp 69 | 70 | 71 | def polling_start(request): 72 | resp = HttpResponse("") 73 | resp['X-IC-ResumePolling'] = "true" 74 | return resp 75 | 76 | 77 | def polling(request): 78 | template = "polling.html" 79 | if request.is_intercooler(): 80 | template = "polling_response.html" 81 | context = { 82 | 'item': str(uuid4()), 83 | } 84 | return TemplateResponse(request, template=template, context=context) 85 | 86 | 87 | def infinite_scrolling(request): 88 | template = "infinite_scrolling.html" 89 | if request.is_intercooler(): 90 | template = "infinite_scrolling_include.html" 91 | context = { 92 | 'rows': _page_data(), 93 | } 94 | return TemplateResponse(request, template=template, context=context) 95 | 96 | 97 | def root(request): 98 | template = "demo_project.html" 99 | context = { 100 | 'rows': _page_data(), 101 | 'form': TestForm() 102 | } 103 | return TemplateResponse(request, template=template, context=context) 104 | 105 | 106 | urlpatterns = [ 107 | url('^form/$', form, name='form'), 108 | url('^redirector/redirected/$', redirected, name='redirected'), 109 | url('^redirector/$', redirector, name='redirector'), 110 | url('^click/$', click, name='click'), 111 | url('^polling/stop/$', polling_stop, name='polling_stop'), 112 | url('^polling/start/$', polling_start, name='polling_start'), 113 | url('^polling/$', polling, name='polling'), 114 | url('^infinite/scrolling/$', infinite_scrolling, name='infinite_scrolling'), 115 | url('^$', root, name='root'), 116 | ] 117 | 118 | -------------------------------------------------------------------------------- /intercooler_helpers/tests/test_middleware.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import absolute_import, unicode_literals 3 | 4 | from django.utils.six.moves.urllib.parse import urlparse 5 | 6 | import pytest 7 | from intercooler_helpers.middleware import (IntercoolerData, 8 | HttpMethodOverride) 9 | 10 | 11 | @pytest.fixture 12 | def ic_mw(): 13 | return IntercoolerData() 14 | 15 | @pytest.fixture 16 | def http_method_mw(): 17 | return HttpMethodOverride() 18 | 19 | 20 | @pytest.mark.parametrize("method", [ 21 | "maybe_intercooler", 22 | "is_intercooler", 23 | "intercooler_data", 24 | "_processed_intercooler_data", 25 | ]) 26 | def test_methods_dont_exist_on_class_only_on_instance(rf, ic_mw, method): 27 | request = rf.get('/') 28 | ic_mw.process_request(request) 29 | assert request.intercooler_data.id == 0 30 | assert hasattr(request, method) is True 31 | assert hasattr(request.__class__, method) is False 32 | 33 | 34 | def test_maybe_intercooler_via_header(rf, ic_mw): 35 | request = rf.get('/', HTTP_X_IC_REQUEST="true") 36 | ic_mw.process_request(request) 37 | assert request.maybe_intercooler() is True 38 | 39 | 40 | def test_maybe_intercooler_old_way(rf, ic_mw): 41 | request = rf.get('/', data={'ic-request': 'true'}) 42 | ic_mw.process_request(request) 43 | assert request.maybe_intercooler() is False 44 | 45 | 46 | def test_is_intercooler(rf, ic_mw): 47 | request = rf.get('/', HTTP_X_IC_REQUEST="true", 48 | HTTP_X_REQUESTED_WITH='XMLHttpRequest') 49 | ic_mw.process_request(request) 50 | assert request.is_intercooler() is True 51 | 52 | 53 | def test_intercooler_data(rf, ic_mw): 54 | querystring_data = { 55 | 'ic-id': '3', 56 | 'ic-request': 'true', 57 | 'ic-element-id': 'html_id', 58 | 'ic-element-name': 'html_name', 59 | 'ic-target-id': 'target_html_id', 60 | 'ic-trigger-id': 'triggered_by_id', 61 | 'ic-trigger-name': 'triggered_by_html_name', 62 | 'ic-current-url': '/lol/', 63 | # This is undocumented at the time of writing, and only turns up 64 | # if no ic-prompt-name is given on the request to inflight. 65 | 'ic-prompt-value': 'undocumented', 66 | # This may be set if not using 67 | # 68 | '_method': 'POST', 69 | } 70 | request = rf.get('/', data=querystring_data, HTTP_X_REQUESTED_WITH='XMLHttpRequest') 71 | ic_mw.process_request(request) 72 | # Before running, this attribute should not exist. 73 | with pytest.raises(AttributeError): 74 | request._processed_intercooler_data 75 | data = request.intercooler_data 76 | url = urlparse('/lol/') 77 | assert request.intercooler_data.current_url == (url, None) 78 | assert data.element == ('html_name', 'html_id') 79 | assert data.id == 3 80 | assert data.request is True 81 | assert data.target_id == 'target_html_id' 82 | assert data.trigger == ('triggered_by_html_name', 'triggered_by_id') 83 | assert data.prompt_value == 'undocumented' 84 | assert data._mutable is False 85 | assert data.dict() == querystring_data 86 | # ensure that after calling the property (well, SimpleLazyObject) 87 | # the request has cached the data structure to an attribute. 88 | request._processed_intercooler_data 89 | 90 | def test_intercooler_data_removes_data_from_GET(rf, ic_mw): 91 | querystring_data = { 92 | 'ic-id': '3', 93 | 'ic-request': 'true', 94 | 'ic-element-id': 'html_id', 95 | 'ic-element-name': 'html_name', 96 | 'ic-target-id': 'target_html_id', 97 | 'ic-trigger-id': 'triggered_by_id', 98 | 'ic-trigger-name': 'triggered_by_html_name', 99 | 'ic-current-url': '/lol/', 100 | # This is undocumented at the time of writing, and only turns up 101 | # if no ic-prompt-name is given on the request to inflight. 102 | 'ic-prompt-value': 'undocumented', 103 | # This may be set if not using 104 | # 105 | '_method': 'POST', 106 | } 107 | request = rf.get('/', data=querystring_data, HTTP_X_REQUESTED_WITH='XMLHttpRequest') 108 | ic_mw.process_request(request) 109 | assert len(request.GET) == 10 110 | url = urlparse('/lol/') 111 | assert request.intercooler_data.current_url == (url, None) 112 | # After evaluation, only _method should be left. 113 | assert len(request.GET) == 1 114 | 115 | 116 | # TODO : test removes data from POST 117 | 118 | 119 | def test_http_method_override_via_querystring(rf, http_method_mw): 120 | request = rf.post('/?_method=patch', HTTP_X_REQUESTED_WITH='XMLHttpRequest') 121 | http_method_mw.process_request(request) 122 | assert request.changed_method is True 123 | assert request.method == 'PATCH' 124 | assert request.original_method == 'POST' 125 | assert request.PATCH is request.POST 126 | 127 | def test_http_method_override_via_postdata(rf, http_method_mw): 128 | request = rf.post('/', data={'_method': 'PUT'}, HTTP_X_REQUESTED_WITH='XMLHttpRequest') 129 | http_method_mw.process_request(request) 130 | assert request.changed_method is True 131 | assert request.method == 'PUT' 132 | assert request.original_method == 'POST' 133 | assert request.PUT is request.POST 134 | 135 | 136 | def test_http_method_override_via_header(rf, http_method_mw): 137 | request = rf.post('/', HTTP_X_HTTP_METHOD_OVERRIDE='patch') 138 | http_method_mw.process_request(request) 139 | assert request.changed_method is True 140 | assert request.method == 'PATCH' 141 | assert request.original_method == 'POST' 142 | assert request.PATCH is request.POST 143 | 144 | 145 | def test_intercooler_querydict_copied_change_method_from_request(rf, http_method_mw, ic_mw): 146 | request = rf.post('/?_method=patch', HTTP_X_REQUESTED_WITH='XMLHttpRequest') 147 | http_method_mw.process_request(request) 148 | ic_mw.process_request(request) 149 | assert request.changed_method is True 150 | assert request.intercooler_data.changed_method is True 151 | -------------------------------------------------------------------------------- /intercooler_helpers/middleware.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import absolute_import, unicode_literals 3 | 4 | from collections import namedtuple 5 | from contextlib import contextmanager 6 | 7 | from django.http import QueryDict, HttpResponse 8 | try: 9 | from django.urls import Resolver404, resolve 10 | except ImportError: # Django <1.10 11 | from django.core.urlresolvers import Resolver404, resolve 12 | from django.utils.functional import SimpleLazyObject 13 | from django.utils.six.moves.urllib.parse import urlparse 14 | try: 15 | from django.utils.deprecation import MiddlewareMixin 16 | except ImportError: # < Django 1.10 17 | class MiddlewareMixin(object): 18 | pass 19 | 20 | 21 | __all__ = ['IntercoolerData', 'HttpMethodOverride'] 22 | 23 | 24 | class HttpMethodOverride(MiddlewareMixin): 25 | """ 26 | Support for X-HTTP-Method-Override and _method=PUT style request method 27 | changing. 28 | 29 | Note: if https://pypi.python.org/pypi/django-method-override gets updated 30 | with support for newer Django (ie: implements MiddlewareMixin), without 31 | dropping older versions, I could possibly replace this with that. 32 | """ 33 | def process_request(self, request): 34 | request.changed_method = False 35 | if request.method != 'POST': 36 | return 37 | methods = {'GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'} 38 | potentials = ((request.META, 'HTTP_X_HTTP_METHOD_OVERRIDE'), 39 | (request.GET, '_method'), 40 | (request.POST, '_method')) 41 | for querydict, key in potentials: 42 | if key in querydict and querydict[key].upper() in methods: 43 | newmethod = querydict[key].upper() 44 | # Don't change the method data if the calling method was 45 | # the same as the indended method. 46 | if newmethod == request.method: 47 | return 48 | request.original_method = request.method 49 | if hasattr(querydict, '_mutable'): 50 | with _mutate_querydict(querydict): 51 | querydict.pop(key) 52 | if not hasattr(request, newmethod): 53 | setattr(request, newmethod, request.POST) 54 | request.method = newmethod 55 | request.changed_method = True 56 | return 57 | 58 | 59 | def _maybe_intercooler(self): 60 | return self.META.get('HTTP_X_IC_REQUEST') == 'true' 61 | 62 | 63 | def _is_intercooler(self): 64 | return self.is_ajax() and self.maybe_intercooler() 65 | 66 | 67 | @contextmanager 68 | def _mutate_querydict(qd): 69 | qd._mutable = True 70 | yield qd 71 | qd._mutable = False 72 | 73 | 74 | NameId = namedtuple('NameId', 'name id') 75 | UrlMatch = namedtuple('UrlMatch', 'url match') 76 | 77 | 78 | class IntercoolerQueryDict(QueryDict): 79 | @property 80 | def url(self): 81 | url = self.get('ic-current-url', None) 82 | match = None 83 | if url is not None: 84 | url = url.strip() 85 | url = urlparse(url) 86 | if url.path: 87 | try: 88 | match = resolve(url.path) 89 | except Resolver404: 90 | pass 91 | return UrlMatch(url, match) 92 | 93 | current_url = url 94 | 95 | @property 96 | def element(self): 97 | return NameId(self.get('ic-element-name', None), self.get('ic-element-id', None)) 98 | 99 | @property 100 | def id(self): 101 | # I know IC calls it a UUID internally, buts its just 1, incrementing. 102 | return int(self.get('ic-id', '0')) 103 | 104 | @property 105 | def request(self): 106 | return bool(self.get('ic-request', None)) 107 | 108 | @property 109 | def target_id(self): 110 | return self.get('ic-target-id', None) 111 | 112 | @property 113 | def trigger(self): 114 | return NameId(self.get('ic-trigger-name', None), self.get('ic-trigger-id', None)) 115 | 116 | @property 117 | def prompt_value(self): 118 | return self.get('ic-prompt-value', None) 119 | 120 | def __repr__(self): 121 | props = ('id', 'request', 'target_id', 'element', 'trigger', 122 | 'prompt_value', 'url') 123 | attrs = ['{name!s}={val!r}'.format(name=prop, val=getattr(self, prop)) 124 | for prop in props] 125 | return "<{cls!s}: {attrs!s}>".format(cls=self.__class__.__name__, 126 | attrs=", ".join(attrs)) 127 | 128 | 129 | def intercooler_data(self): 130 | if not hasattr(self, '_processed_intercooler_data'): 131 | IC_KEYS = ['ic-current-url', 'ic-element-id', 'ic-element-name', 132 | 'ic-id', 'ic-prompt-value', 'ic-target-id', 133 | 'ic-trigger-id', 'ic-trigger-name', 'ic-request'] 134 | ic_qd = IntercoolerQueryDict('', encoding=self.encoding) 135 | if self.method in ('GET', 'HEAD', 'OPTIONS'): 136 | query_params = self.GET 137 | else: 138 | query_params = self.POST 139 | query_keys = tuple(query_params.keys()) 140 | for ic_key in IC_KEYS: 141 | if ic_key in query_keys: 142 | # emulate how .get() behaves, because pop returns the 143 | # whole shebang. 144 | # For a little while, we need to pop data out of request.GET 145 | with _mutate_querydict(query_params) as REQUEST_DATA: 146 | try: 147 | removed = REQUEST_DATA.pop(ic_key)[-1] 148 | except IndexError: 149 | removed = [] 150 | with _mutate_querydict(ic_qd) as IC_DATA: 151 | IC_DATA.update({ic_key: removed}) 152 | # Don't pop these ones off, so that decisions can be made for 153 | # handling _method 154 | ic_request = query_params.get('_method') 155 | with _mutate_querydict(ic_qd) as IC_DATA: 156 | IC_DATA.update({'_method': ic_request}) 157 | # If HttpMethodOverride is in the middleware stack, this may 158 | # return True. 159 | IC_DATA.changed_method = getattr(self, 'changed_method', False) 160 | self._processed_intercooler_data = ic_qd 161 | return self._processed_intercooler_data 162 | 163 | 164 | class IntercoolerData(MiddlewareMixin): 165 | def process_request(self, request): 166 | request.maybe_intercooler = _maybe_intercooler.__get__(request) 167 | request.is_intercooler = _is_intercooler.__get__(request) 168 | request.intercooler_data = SimpleLazyObject(intercooler_data.__get__(request)) 169 | 170 | 171 | 172 | class IntercoolerRedirector(MiddlewareMixin): 173 | def process_response(self, request, response): 174 | if not request.is_intercooler(): 175 | return response 176 | if response.status_code > 300 and response.status_code < 400: 177 | if response.has_header('Location'): 178 | url = response['Location'] 179 | del response['Location'] 180 | new_resp = HttpResponse() 181 | for k, v in response.items(): 182 | new_resp[k] = v 183 | new_resp['X-IC-Redirect'] = url 184 | return new_resp 185 | return response 186 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | django-intercooler_helpers 2 | ========================== 3 | 4 | :author: Keryn Knight 5 | :version: 0.2.0 6 | 7 | .. |travis_stable| image:: https://travis-ci.org/kezabelle/django-intercoolerjs-helpers.svg?branch=0.2.0 8 | :target: https://travis-ci.org/kezabelle/django-intercoolerjs-helpers 9 | 10 | .. |travis_master| image:: https://travis-ci.org/kezabelle/django-intercoolerjs-helpers.svg?branch=master 11 | :target: https://travis-ci.org/kezabelle/django-intercoolerjs-helpers 12 | 13 | ============== ====== 14 | Release Status 15 | ============== ====== 16 | stable (0.2.0) |travis_stable| 17 | master |travis_master| 18 | ============== ====== 19 | 20 | 21 | .. contents:: Sections 22 | :depth: 2 23 | 24 | What it does 25 | ------------ 26 | 27 | ``intercooler_helpers`` is a small reusable app for `Django`_ which provides a 28 | few improvements for working with `Intercooler.js`_. 29 | 30 | It providea a middleware which extracts relevant `Intercooler.js`_ data from the 31 | querystring, and attaches it to the request as a separate ``QueryDict`` (ie: it 32 | behaves like ``request.POST`` or ``request.GET``) 33 | 34 | It also provides a small middleware for changing request method based on either the 35 | query string (``_method=PUT``) or a request header(``X-HTTP-Method-Override: PUT``) 36 | 37 | Between them, they should capture all the incoming `Intercooler.js`_ data on 38 | which the server may act. 39 | 40 | Installation and usage 41 | ---------------------- 42 | 43 | This application depends on `django-intercoolerjs`_ which provides a copy of 44 | `Intercooler.js`_ bundled up for use with the standard `Django`_ staticfiles 45 | application. 46 | 47 | Installation 48 | ^^^^^^^^^^^^ 49 | 50 | You can grab ``0.2.0`` from `PyPI`_ like so:: 51 | 52 | pip install django-intercooler-helpers==0.2.0 53 | 54 | Or you can grab it from `GitHub`_ like this:: 55 | 56 | pip install -e git+https://github.com/kezabelle/django-intercoolerjs-helpers.git#egg=django-intercoolerjs-helpers 57 | 58 | Configuration 59 | ^^^^^^^^^^^^^ 60 | You need to add ``intercooler_helpers.middleware.IntercoolerData`` to your 61 | ``MIDDLEWARE_CLASSES`` (or ``MIDDLEWARE`` on Django **1.10+**). 62 | 63 | You may optionally want to add ``intercooler_helpers.middleware.HttpMethodOverride`` 64 | as well, if you don't already have a method by which to fake the HTTP Method change. 65 | If you're using ```` 66 | then you don't need ``HttpMethodOverride`` at all. 67 | 68 | To install all the middleware, you want something like:: 69 | 70 | # MIDDLEWARE = ... for newer Django 71 | MIDDLEWARE_CLASSES = ( 72 | # ... 73 | 'intercooler_helpers.middleware.HttpMethodOverride', 74 | 'intercooler_helpers.middleware.IntercoolerData', 75 | 'intercooler_helpers.middleware.IntercoolerRedirector', 76 | # ... 77 | ) 78 | 79 | ``HttpMethodOverride`` and ``IntercoolerData`` ought to be near the top of the iterable, as they both make use of ``process_request(request)``. 80 | ``IntercoolerRedirector`` ought to be near the bottom, as it operates on ``process_response(request, response)`` and you probably want to convert the response to a client-side redirect at the earliest opportunity. 81 | 82 | Usage 83 | ^^^^^ 84 | 85 | A brief overview of the public API provided so far: 86 | 87 | IntercoolerData 88 | *************** 89 | 90 | For fully correct detection of `Intercooler.js`_ requests, you can call 91 | ``request.is_intercooler()``. 92 | Behind the scenes, it uses ``request.maybe_intercooler()`` to 93 | detect whether ``ic-request`` was present, indicating it *may* have been a 94 | valid `Intercooler.js`_ request, and also checks ``request.is_ajax()`` 95 | 96 | To parse the Intercooler-related data out of the query-string, you can use 97 | ``request.intercooler_data`` (not a method!) which is a ``QueryDict`` and should 98 | behave exactly like ``request.GET`` - It pulls all of the ``ic-*`` keys out 99 | of ``request.GET`` and puts them in a separate data structure, leaving 100 | your ``request.GET`` cleaned of extraenous data. 101 | 102 | ``request.intercooler_data`` is a **lazy** data structure, like ``request.user``, 103 | so will not modify ``request.GET`` until access is attempted. 104 | 105 | The following properties exist, mapping back to the keys mentioned in the 106 | `Intercooler.js Reference document`_ 107 | 108 | - ``request.intercooler_data.url`` returns a ``namedtuple`` containing 109 | 110 | - returns the ``ic-current-url`` (converted via ``urlparse``) or ``None`` 111 | - A `Django`_ ``ResolverMatch`` pointing to the view which made the request (based on ``ic-current-url``) or ``None`` 112 | - ``request.intercooler_data.element`` returns a ``namedtuple`` containing 113 | 114 | - ``ic-element-name`` or ``None`` 115 | - ``ic-element-id`` or ``None`` 116 | - ``request.intercooler_data.id`` 117 | 118 | - the ``ic-id`` which made the request. an ever-incrementing integer. 119 | - ``request.intercooler_data.request`` 120 | 121 | - a boolean indicating that it was an `Intercooler.js`_ request. Should always 122 | be true if ``request.is_intercooler()`` said so. 123 | - ``request.intercooler_data.target_id`` 124 | 125 | - ``ic-target-id`` or ``None`` 126 | - ``request.intercooler_data.trigger`` returns a ``namedtuple`` containing 127 | 128 | - ``ic-trigger-name`` or ``None`` 129 | - ``ic-trigger-id`` or ``None`` 130 | - ``request.intercooler_data.prompt_value`` 131 | 132 | - If no ``ic-prompt-name`` was given and a prompt was used, this will contain 133 | the user's response. Appears to be undocumented? 134 | 135 | 136 | HttpMethodOverride 137 | ****************** 138 | 139 | - ``request.changed_method`` is a boolean indicating that the request was 140 | toggled from being a ``POST`` to something else (one of 141 | ``GET``, ``HEAD``, ``POST``, ``PUT``, ``PATCH``, ``DELETE``, ``OPTIONS`` ... 142 | though why you'd want to ``POST`` and have it act as a ``GET`` is beyond me. 143 | But that's your choice) 144 | - ``request.original_method`` if either ``_method=X`` or 145 | ``X-HTTP-Method-Override: X`` caused the request to change method, then this 146 | will contain the original request. It should always be ``POST`` 147 | - ``request.method`` will reflect the desired HTTP method, rather than the one 148 | originally used (``POST``) 149 | 150 | 151 | IntercoolerRedirector 152 | ********************* 153 | 154 | If a redirect status code is given (> 300, < 400), and the request originated from `Intercooler.js`_ (assumes ``IntercoolerData`` is installed so that ``request.is_intercooler()`` may be called), remove the ``Location`` header from the response, and create a new ``HttpResponse`` with all the other headers, and also the ``X-IC-Redirect`` header to indicate to `Intercooler.js`_ that it needs to do a client side-redirect. 155 | 156 | 157 | Supported Django versions 158 | ------------------------- 159 | 160 | The tests are run against Django 1.8 through 1.10, and Python 2.7, 3.3, 3.4 and 3.5. 161 | 162 | Running the tests 163 | ^^^^^^^^^^^^^^^^^ 164 | 165 | If you have a cloned copy, you can do:: 166 | 167 | python setup.py test 168 | 169 | If you have tox, you can just do:: 170 | 171 | tox 172 | 173 | Running the demo 174 | ^^^^^^^^^^^^^^^^ 175 | 176 | A barebones demo is provided. It assumes you're using something like `virtualenv`_ and 177 | `virtualenvwrapper`_ but you can probably figure it out otherwise:: 178 | 179 | mktmpenv --python=`which python3` 180 | pip install -e git+https://github.com/kezabelle/django-intercooler-helpers.git#egg=django-intercooler-helpers 181 | 182 | Then probably:: 183 | 184 | cd src/django-intercooler-helpers 185 | python demo_project.py runserver 186 | 187 | It shows off a few of the same demos that the `Intercooler.js`_ website does. 188 | 189 | Contributing 190 | ------------ 191 | 192 | Please do! 193 | 194 | The project is hosted on `GitHub`_ in the `kezabelle/django-intercooler-helpers`_ 195 | repository. 196 | 197 | Bug reports and feature requests can be filed on the repository's `issue tracker`_. 198 | 199 | If something can be discussed in 140 character chunks, there's also `my Twitter account`_. 200 | 201 | Roadmap 202 | ------- 203 | 204 | TODO. 205 | 206 | The license 207 | ----------- 208 | 209 | It's `FreeBSD`_. There's should be a ``LICENSE`` file in the root of the repository, and in any archives. 210 | 211 | .. _FreeBSD: http://en.wikipedia.org/wiki/BSD_licenses#2-clause_license_.28.22Simplified_BSD_License.22_or_.22FreeBSD_License.22.29 212 | .. _Django: https://www.djangoproject.com/ 213 | .. _Intercooler.js: http://intercoolerjs.org/ 214 | .. _django-intercoolerjs: https://github.com/brejoc/django-intercoolerjs 215 | .. _GitHub: https://github.com/ 216 | .. _PyPI: https://pypi.python.org/pypi 217 | .. _Intercooler.js Reference document: http://intercoolerjs.org/reference.html 218 | .. _virtualenvwrapper: https://virtualenvwrapper.readthedocs.io/en/latest/ 219 | .. _virtualenv: https://virtualenv.pypa.io/en/stable/ 220 | .. _kezabelle/django-intercooler-helpers: https://github.com/kezabelle/django-intercooler-helpers/ 221 | .. _issue tracker: https://github.com/kezabelle/django-intercooler-helpers/issues/ 222 | .. _my Twitter account: https://twitter.com/kezabelle/ 223 | --------------------------------------------------------------------------------