Skip to content

Add IntercoolerJS views to work with templates and requests efficiently #15

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 23 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
994fc3d
Fix demo setting for Django 1.10+
hailangvn May 17, 2019
032a0fa
First step to demo extract html part, dispatch ic request
hailangvn May 17, 2019
81a16f7
Completed views and demos
hailangvn May 17, 2019
df53c94
Fix wording in demo page
hailangvn May 17, 2019
28e3b83
Enhance dispatch for wildcard
hailangvn May 31, 2019
d45e5a3
Fix typo in README.rst
hailangvn Aug 17, 2019
8066dcd
.gitignore pycache, tox, venv, vim files
hailangvn Aug 8, 2019
dd6b2a5
Latest support matrix with tox, .travis
hailangvn Aug 19, 2019
6afb146
Merge branch 'master' to have latest test matrix
hailangvn Aug 19, 2019
ae10803
100% coverage for middleware.py
hailangvn Aug 19, 2019
afabef3
LEAN: Remove non-value added code from middleware.py
hailangvn Aug 19, 2019
5e2beff
Correct way to handle QueryDict in middleware.py
hailangvn Aug 19, 2019
42a40b8
setup required package pyquery
hailangvn Aug 17, 2019
0fe34f6
Fall back to python 2 super
hailangvn Aug 17, 2019
eeb1dc6
Add test views, improve views
hailangvn Aug 18, 2019
af3cd2a
Test target_map
hailangvn Aug 18, 2019
5632fdf
100% coverage for views.py
hailangvn Aug 18, 2019
363866c
return full page when target id is not found
hailangvn Aug 18, 2019
e2f143a
Can extract part with template tag, need refactor
hailangvn Aug 20, 2019
fa11133
Simplified extracting part, has issue with template tag
hailangvn Aug 20, 2019
3655180
Fix tailing template tag, refactor to remove pyquery dependency
hailangvn Aug 20, 2019
a262a0c
Refactor to prepare response class, 100% coverage again
hailangvn Aug 20, 2019
5b72c4e
Add lxml for GitHub travis
hailangvn Aug 20, 2019
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
htmlcov
dist
*.pyo
*.pyc.coverage
*.pyc
.coverage
__pycache__
*.egg*
*.sqlite3
/.tox
/env
/venv
.*.sw?
.*~
17 changes: 5 additions & 12 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
language: python
python: 3.5
python: 3.7
sudo: false

notifications:
Expand All @@ -13,16 +13,9 @@ cache:
- $HOME/.cache/pip

env:
- TOX_ENV=py27-dj18
- TOX_ENV=py27-dj19
- TOX_ENV=py27-dj110
- TOX_ENV=py33-dj18
- TOX_ENV=py34-dj18
- TOX_ENV=py34-dj19
- TOX_ENV=py34-dj110
- TOX_ENV=py35-dj18
- TOX_ENV=py35-dj19
- TOX_ENV=py35-dj110

- TOX_ENV=py37-dj111
- TOX_ENV=py37-dj21
- TOX_ENV=py37-dj22

script:
- tox -e $TOX_ENV
4 changes: 2 additions & 2 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ What it does
``intercooler_helpers`` is a small reusable app for `Django`_ which provides a
few improvements for working with `Intercooler.js`_.

It providea a middleware which extracts relevant `Intercooler.js`_ data from the
It provides a middleware which extracts relevant `Intercooler.js`_ data from the
querystring, and attaches it to the request as a separate ``QueryDict`` (ie: it
behaves like ``request.POST`` or ``request.GET``)

Expand Down Expand Up @@ -107,7 +107,7 @@ The following properties exist, mapping back to the keys mentioned in the

- ``request.intercooler_data.url`` returns a ``namedtuple`` containing

- returns the ``ic-current-url`` (converted via ``urlparse``) or ``None``
- the ``ic-current-url`` (converted via ``urlparse``) or ``None``
- A `Django`_ ``ResolverMatch`` pointing to the view which made the request (based on ``ic-current-url``) or ``None``
- ``request.intercooler_data.element`` returns a ``namedtuple`` containing

Expand Down
85 changes: 38 additions & 47 deletions intercooler_helpers/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,12 @@
from collections import namedtuple
from contextlib import contextmanager

from django.conf import settings
from django.http import QueryDict, HttpResponse
try:
from django.urls import Resolver404, resolve
except ImportError: # Django <1.10
from django.core.urlresolvers import Resolver404, resolve
from django.urls import Resolver404, resolve
from django.utils.functional import SimpleLazyObject
from django.utils.six.moves.urllib.parse import urlparse
try:
from django.utils.deprecation import MiddlewareMixin
except ImportError: # < Django 1.10
class MiddlewareMixin(object):
pass
from django.utils.deprecation import MiddlewareMixin


__all__ = ['IntercoolerData', 'HttpMethodOverride']
Expand All @@ -30,27 +24,35 @@ class HttpMethodOverride(MiddlewareMixin):
with support for newer Django (ie: implements MiddlewareMixin), without
dropping older versions, I could possibly replace this with that.
"""
caring_methods = ['POST']
target_methods = {'POST', 'PUT', 'PATCH', 'DELETE'}

def process_request(self, request):
request.changed_method = False
if request.method != 'POST':
if request.method in self.caring_methods: pass
else:
return
methods = {'GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'}
methods = self.target_methods
potentials = ((request.META, 'HTTP_X_HTTP_METHOD_OVERRIDE'),
(request.GET, '_method'),
(request.POST, '_method'))
for querydict, key in potentials:
if key in querydict and querydict[key].upper() in methods:
newmethod = querydict[key].upper()
# Don't change the method data if the calling method was
# the same as the indended method.
# the same as the intended method.
if newmethod == request.method:
return
else: pass
request.original_method = request.method
if hasattr(querydict, '_mutable'):
with _mutate_querydict(querydict):
querydict.pop(key)
if not hasattr(request, newmethod):
setattr(request, newmethod, request.POST)
# ```This could not be tested so may be never happen!
# if hasattr(request, newmethod): pass
# else:
setattr(request, newmethod, request.POST)
# ```
request.method = newmethod
request.changed_method = True
return
Expand All @@ -76,6 +78,10 @@ def _mutate_querydict(qd):


class IntercoolerQueryDict(QueryDict):
"""
This is proxy to access Intercooler data regardless HTTP method.
"""

@property
def url(self):
url = self.get('ic-current-url', None)
Expand Down Expand Up @@ -127,38 +133,24 @@ def __repr__(self):


def intercooler_data(self):
if not hasattr(self, '_processed_intercooler_data'):
IC_KEYS = ['ic-current-url', 'ic-element-id', 'ic-element-name',
'ic-id', 'ic-prompt-value', 'ic-target-id',
'ic-trigger-id', 'ic-trigger-name', 'ic-request']
ic_qd = IntercoolerQueryDict('', encoding=self.encoding)
if self.method in ('GET', 'HEAD', 'OPTIONS'):
query_params = self.GET
else:
query_params = self.POST
query_keys = tuple(query_params.keys())
for ic_key in IC_KEYS:
if ic_key in query_keys:
# emulate how .get() behaves, because pop returns the
# whole shebang.
# For a little while, we need to pop data out of request.GET
with _mutate_querydict(query_params) as REQUEST_DATA:
try:
removed = REQUEST_DATA.pop(ic_key)[-1]
except IndexError:
removed = []
with _mutate_querydict(ic_qd) as IC_DATA:
IC_DATA.update({ic_key: removed})
# Don't pop these ones off, so that decisions can be made for
# handling _method
ic_request = query_params.get('_method')
with _mutate_querydict(ic_qd) as IC_DATA:
IC_DATA.update({'_method': ic_request})
# If HttpMethodOverride is in the middleware stack, this may
# return True.
IC_DATA.changed_method = getattr(self, 'changed_method', False)
self._processed_intercooler_data = ic_qd
return self._processed_intercooler_data
try:
return self._processed_intercooler_data
except AttributeError: pass
if self.method in ('GET', 'HEAD', 'OPTIONS'):
query_params = self.GET
else:
query_params = self.POST

# Make mutable copy
# ic_qd = IntercoolerQueryDict(query_params, encoding=self.encoding)
# Just cast needed class to existing object
# https://stackoverflow.com/a/3464154/4763528
ic_qd = query_params
ic_qd.__class__ = IntercoolerQueryDict

ic_qd.changed_method = getattr(self, 'changed_method', False)
self._processed_intercooler_data = ic_qd
return ic_qd


class IntercoolerData(MiddlewareMixin):
Expand All @@ -168,7 +160,6 @@ def process_request(self, request):
request.intercooler_data = SimpleLazyObject(intercooler_data.__get__(request))



class IntercoolerRedirector(MiddlewareMixin):
def process_response(self, request, response):
if not request.is_intercooler():
Expand Down
60 changes: 50 additions & 10 deletions intercooler_helpers/tests/test_middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,21 +73,36 @@ def test_intercooler_data(rf, ic_mw):
with pytest.raises(AttributeError):
request._processed_intercooler_data
data = request.intercooler_data
assert data.id == 3
assert data['ic-id'] == '3'
url = urlparse('/lol/')
assert request.intercooler_data.current_url == (url, None)
assert data.element == ('html_name', 'html_id')
assert data.id == 3
assert data.request is True
assert data['ic-request'] == 'true'
assert data.target_id == 'target_html_id'
assert data.trigger == ('triggered_by_html_name', 'triggered_by_id')
assert data.prompt_value == 'undocumented'
assert data._mutable is False
assert data.dict() == querystring_data
expecting = request.GET.copy()
assert data.dict() == expecting.dict()
# ensure that after calling the property (well, SimpleLazyObject)
# the request has cached the data structure to an attribute.
request._processed_intercooler_data

def test_intercooler_data_removes_data_from_GET(rf, ic_mw):
def test_intercooler_data_special_url(rf, ic_mw):
querystring_data = {
'ic-request': 'true',
'ic-current-url': ' ',
'_method': 'POST',
}
request = rf.post('/', data=querystring_data,
HTTP_X_REQUESTED_WITH='XMLHttpRequest')
ic_mw.process_request(request)
url = urlparse('')
assert request.intercooler_data.current_url == (url, None)

def test_intercooler_data_not_removes_data_from_GET(rf, ic_mw):
querystring_data = {
'ic-id': '3',
'ic-request': 'true',
Expand All @@ -108,13 +123,11 @@ def test_intercooler_data_removes_data_from_GET(rf, ic_mw):
ic_mw.process_request(request)
assert len(request.GET) == 10
url = urlparse('/lol/')
assert request.intercooler_data.current_url == (url, None)
# After evaluation, only _method should be left.
assert len(request.GET) == 1


# TODO : test removes data from POST

data = request.intercooler_data
assert data.current_url == (url, None)
# Should not change any requesting data
expecting = request.GET.copy()
assert data.dict() == expecting.dict()

def test_http_method_override_via_querystring(rf, http_method_mw):
request = rf.post('/?_method=patch', HTTP_X_REQUESTED_WITH='XMLHttpRequest')
Expand All @@ -124,6 +137,17 @@ def test_http_method_override_via_querystring(rf, http_method_mw):
assert request.original_method == 'POST'
assert request.PATCH is request.POST

def test_http_method_override_via_querystring_same_method(rf, http_method_mw):
test_data = {'test': 'test'}
request = rf.post('/?_method=post', data=test_data,
HTTP_X_REQUESTED_WITH='XMLHttpRequest')
http_method_mw.process_request(request)
assert request.changed_method is False
assert request.method == 'POST'
assert hasattr(request, 'original_method') == False
test_data['test'] = [test_data['test']]
assert request.POST == test_data

def test_http_method_override_via_postdata(rf, http_method_mw):
request = rf.post('/', data={'_method': 'PUT'}, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
http_method_mw.process_request(request)
Expand All @@ -148,3 +172,19 @@ def test_intercooler_querydict_copied_change_method_from_request(rf, http_method
ic_mw.process_request(request)
assert request.changed_method is True
assert request.intercooler_data.changed_method is True


def test_intercooler_querydict_repr(rf, http_method_mw, ic_mw):
request = rf.post('/', data={'ic-request': 'true'},
HTTP_X_REQUESTED_WITH='XMLHttpRequest')
http_method_mw.process_request(request)
ic_mw.process_request(request)
ic_data = request.intercooler_data
# To get actual instance from SimpleLazyObject class
assert ic_data.request == True
expecting = ('<SimpleLazyObject: <IntercoolerQueryDict: id=0,'
' request=True, target_id=None,'
' element=NameId(name=None, id=None),'
' trigger=NameId(name=None, id=None),'
' prompt_value=None, url=UrlMatch(url=None, match=None)>>')
assert repr(ic_data) == expecting
Loading