Handling client-side redirection on server-side redirects
13 |
This anchor tag GETs from {% url 'redirector' %} when it is clicked.
14 |
Explanation
15 |
16 |
Makes use of the intercooler_helpers.middleware.IntercoolerRedirector
17 | to turn standard Django redirects (301 and 302) into Intercooler compatible client-side ones.
This demo shows how to set up a list that is appended to periodically and that can be paused and resumed
38 |
Explanation
39 |
40 |
41 | A div polls the {% url 'polling' %} url every two seconds by using the
42 | ic-prepend-from and
43 | ic-poll attributes, and prepends the results
44 | into an inner div using the
45 | ic-target
46 | attribute.
47 |
48 |
49 | Two inner divs post to the server using the
50 | ic-post-to
51 | attribute and update their respective parent div. The server either starts or stops polling by using
52 | the X-IC-ResumePolling or X-IC-CancelPolling response headers, respectively.
53 |
65 | The last table row of the table generated on the server side, has an
66 | ic-append-from
67 | attribute that loads the next page of results, and that uses
68 | ic-trigger-on
69 | to trigger the request when the row is scrolled into view. The row targets the body of the table to
70 | append to using the
71 | ic-target
72 | attribute and shows an indicator for the request via the
73 | ic-indicator
74 | attribute.
75 |
76 |
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 |
--------------------------------------------------------------------------------