From 994fc3d3f76ca40f5f498c304bc1069e2d74f42a Mon Sep 17 00:00:00 2001 From: Hai Lang Date: Fri, 17 May 2019 16:15:34 +0700 Subject: [PATCH 01/22] Fix demo setting for Django 1.10+ As Django 1.10+ is using MIDDLEWARE instead of MIDDLEWARE_CLASSES, another variable need to be added. --- test_settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test_settings.py b/test_settings.py index 9b4cb5a..d0dc0a6 100644 --- a/test_settings.py +++ b/test_settings.py @@ -59,7 +59,7 @@ }, ] -MIDDLEWARE_CLASSES = ( +MIDDLEWARE = MIDDLEWARE_CLASSES = ( 'django.contrib.sessions.middleware.SessionMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', From 032a0fa08026dcd8c269e5728c1c66fdf8a9f0d6 Mon Sep 17 00:00:00 2001 From: Hai Lang Date: Fri, 17 May 2019 14:25:56 +0700 Subject: [PATCH 02/22] First step to demo extract html part, dispatch ic request --- intercooler_helpers/views.py | 69 ++++++++++++++++++++++++++++++++ test_templates/demo_project.html | 38 ++++++++++++++++++ test_urls.py | 18 ++++++++- 3 files changed, 124 insertions(+), 1 deletion(-) create mode 100644 intercooler_helpers/views.py diff --git a/intercooler_helpers/views.py b/intercooler_helpers/views.py new file mode 100644 index 0000000..02c5215 --- /dev/null +++ b/intercooler_helpers/views.py @@ -0,0 +1,69 @@ +from django.template import engines +from django.views.generic import base as base_views + +import pyquery + + +class ICTemplateResponseMixin(base_views.TemplateResponseMixin): + @property + def ic_data(self): + return self.request.intercooler_data + + def get_target_id(self): + return self.ic_data.target_id + + def extract_html_part(self, find, from_html): + ''' + This find the element that has find string then extract the part from + html string. + ''' + pq = pyquery.PyQuery(from_html) + html_part = pq(find).html() + return html_part + + def render_to_response(self, context, **response_kwargs): + ''' + This retrieves ic-target-id from ic data, then use it to extract part + of template which has the id. + ''' + response = super().render_to_response(context, **response_kwargs) + if not self.ic_data.request: + return response + + template_file = response.resolve_template(response.template_name) + with open(str(template_file.origin)) as tmpl_file: + tmpl_content = tmpl_file.read() + html_part = self.extract_html_part( + '#' + self.get_target_id(), tmpl_content) + django_engine = engines['django'] + template = django_engine.from_string(html_part) + response.template_name = template + return response + + +class ICDispatchMixin(base_views.View): + ''' + This provides dispatcher for routing to correct method based on + IntercoolerJS method/trigger/target tuple. + ''' + ic_tuples = [] + + def dispatch(self, request, *args, **kwargs): + method = super().dispatch + if not self.ic_data.request: + return method(request, *args, **kwargs) + try: + req_method = request.method.lower() + req_trigger = self.ic_data.trigger.id + req_target = self.ic_data.target_id + method_name = next(method_name + for method, trigger, target, method_name in self.ic_tuples + if (not method or method == req_method) + and (not trigger or trigger == req_trigger) + and (not target or target == req_target) + ) + method = getattr(self, method_name) + # Not catch AttributeError for developer to know what is the wrong + # method name + except StopIteration: pass + return method(request, *args, **kwargs) diff --git a/test_templates/demo_project.html b/test_templates/demo_project.html index a417415..e1ec15d 100644 --- a/test_templates/demo_project.html +++ b/test_templates/demo_project.html @@ -56,6 +56,44 @@

Explanation


+
+

Using ICTemplateResponseMixin

+

This example demos using ICTemplateResponseMixin to extract html part from template file.

+

Explanation

+
    +
  • + Single template file contains full page design, with some elements contain id to identify themself. +
  • +
  • + The extract_html_part method will use an id to extract the element which has that id. +
  • +
  • + Then the extracted part can be used to render html response. +
  • +
+ +
+
+ +
+

Using ICDispatchMixin class

+

This example demos using class based view mixin to process different combinations of request's method, trigger.

+

Explanation

+
    +
  • + The dispatch method of ICDispatchMixin class uses request's method, trigger id, and target id to find matching tuple in a list. +
  • +
  • + If found, the corresponding method name from the mached tuple is called to handle the request. +
  • +
  • + If not, default handler for request's method is called. +
  • +
+ +
+
+

Implementing Infinite Scrolling

This example demos an infinite scroll UI.

diff --git a/test_urls.py b/test_urls.py index 6f87a4d..8ce22e0 100644 --- a/test_urls.py +++ b/test_urls.py @@ -14,6 +14,8 @@ except ImportError: # Django <1.10 from django.core.urlresolvers import reverse +from intercooler_helpers import views as ic_views + def _page_data(): return tuple(str(uuid4()) for x in range(0, 10)) @@ -61,7 +63,6 @@ def form(request): return TemplateResponse(request, template=template, context=context) - def polling_stop(request): resp = HttpResponse("Cancelled") resp['X-IC-CancelPolling'] = "true" @@ -94,6 +95,19 @@ def infinite_scrolling(request): return TemplateResponse(request, template=template, context=context) +def html_part(request): + return HttpResponse('html_part_function: To be implemented...') + + +class ICView(ic_views.ICTemplateResponseMixin, ic_views.ICDispatchMixin): + ic_tuples = [ + ('get', None, 'test_class', 'get_html_part'), + ] + + def get_html_part(self, request, *args, **kwargs): + return HttpResponse('get_html_part: To be implemented...') + + def root(request): template = "demo_project.html" context = { @@ -112,6 +126,8 @@ def root(request): url('^polling/start/$', polling_start, name='polling_start'), url('^polling/$', polling, name='polling'), url('^infinite/scrolling/$', infinite_scrolling, name='infinite_scrolling'), + url('^html_part/$', html_part, name='html_part'), + url('^ic_dispatch/$', ICView.as_view(), name='ic_dispatch'), url('^$', root, name='root'), ] From 81a16f7862b02856bd938328e747a60202c41a46 Mon Sep 17 00:00:00 2001 From: Hai Lang Date: Fri, 17 May 2019 16:14:50 +0700 Subject: [PATCH 03/22] Completed views and demos --- intercooler_helpers/views.py | 47 ++++++++++++++++++-------------- test_templates/demo_project.html | 13 ++++++--- test_urls.py | 27 +++++++++++++++--- 3 files changed, 59 insertions(+), 28 deletions(-) diff --git a/intercooler_helpers/views.py b/intercooler_helpers/views.py index 02c5215..179fee1 100644 --- a/intercooler_helpers/views.py +++ b/intercooler_helpers/views.py @@ -1,44 +1,51 @@ -from django.template import engines +from django.template import engines, response from django.views.generic import base as base_views import pyquery -class ICTemplateResponseMixin(base_views.TemplateResponseMixin): +class ICTemplateResponse(response.TemplateResponse): @property def ic_data(self): - return self.request.intercooler_data + return self._request.intercooler_data def get_target_id(self): return self.ic_data.target_id - def extract_html_part(self, find, from_html): + def extract_html_part(self, file_name, find): ''' This find the element that has find string then extract the part from - html string. + html file. ''' - pq = pyquery.PyQuery(from_html) + with open(file_name) as tmpl_file: + tmpl_content = tmpl_file.read() + pq = pyquery.PyQuery(tmpl_content) html_part = pq(find).html() return html_part - def render_to_response(self, context, **response_kwargs): + def resolve_template(self, template): ''' - This retrieves ic-target-id from ic data, then use it to extract part - of template which has the id. + After resolving template, check if request is intercooler and has + target id. Use the id to extract element which has the id. ''' - response = super().render_to_response(context, **response_kwargs) - if not self.ic_data.request: - return response + template_obj = super().resolve_template(template) + if self._request.is_intercooler() and self.get_target_id(): pass + else: + return template_obj - template_file = response.resolve_template(response.template_name) - with open(str(template_file.origin)) as tmpl_file: - tmpl_content = tmpl_file.read() html_part = self.extract_html_part( - '#' + self.get_target_id(), tmpl_content) - django_engine = engines['django'] - template = django_engine.from_string(html_part) - response.template_name = template - return response + str(template_obj.origin), '#' + self.get_target_id()) + engine = engines[template_obj.backend.name] + template_obj = engine.from_string(html_part) + return template_obj + + +class ICTemplateResponseMixin(base_views.TemplateResponseMixin): + response_class = ICTemplateResponse + + @property + def ic_data(self): + return self.request.intercooler_data class ICDispatchMixin(base_views.View): diff --git a/test_templates/demo_project.html b/test_templates/demo_project.html index e1ec15d..27da9d6 100644 --- a/test_templates/demo_project.html +++ b/test_templates/demo_project.html @@ -58,8 +58,8 @@

Explanation

Using ICTemplateResponseMixin

-

This example demos using ICTemplateResponseMixin to extract html part from template file.

-

Explanation

+

This example demos using ICTemplateResponseMixin to extract html part from template file.

{% if show_details %} +

Explanation

  • Single template file contains full page design, with some elements contain id to identify themself. @@ -71,7 +71,9 @@

    + Hide details{% else %} + Show details + {% endif %}{# if show_details #}


@@ -90,7 +92,10 @@

Explanation

If not, default handler for request's method is called. - + Get + + Post + {{ message }}

diff --git a/test_urls.py b/test_urls.py index 8ce22e0..e77aed9 100644 --- a/test_urls.py +++ b/test_urls.py @@ -95,17 +95,33 @@ def infinite_scrolling(request): return TemplateResponse(request, template=template, context=context) -def html_part(request): - return HttpResponse('html_part_function: To be implemented...') +def html_part(request, show_details=False): + template = "demo_project.html" + context = { + 'show_details': show_details, + } + return ic_views.ICTemplateResponse( + request, template=template, context=context) class ICView(ic_views.ICTemplateResponseMixin, ic_views.ICDispatchMixin): + template_name = "demo_project.html" ic_tuples = [ ('get', None, 'test_class', 'get_html_part'), + ('post', 'post-btn', 'test_class', 'post_message'), ] def get_html_part(self, request, *args, **kwargs): - return HttpResponse('get_html_part: To be implemented...') + context = { + 'message': 'Dispatched to get', + } + return self.render_to_response(context) + + def post_message(self, request, *args, **kwargs): + context = { + 'message': 'Dispatched to post: ' + request.POST['message'], + } + return self.render_to_response(context) def root(request): @@ -126,7 +142,10 @@ def root(request): url('^polling/start/$', polling_start, name='polling_start'), url('^polling/$', polling, name='polling'), url('^infinite/scrolling/$', infinite_scrolling, name='infinite_scrolling'), - url('^html_part/$', html_part, name='html_part'), + url('^html_part/show/$', html_part, kwargs={'show_details': True}, + name='html_part_show'), + url('^html_part/$', html_part, kwargs={'show_details': False}, + name='html_part_hide'), url('^ic_dispatch/$', ICView.as_view(), name='ic_dispatch'), url('^$', root, name='root'), ] From df53c94dc72149b070407c089117d8717fd4318e Mon Sep 17 00:00:00 2001 From: Hai Lang Date: Fri, 17 May 2019 21:19:52 +0700 Subject: [PATCH 04/22] Fix wording in demo page --- test_templates/demo_project.html | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test_templates/demo_project.html b/test_templates/demo_project.html index 27da9d6..e3857f9 100644 --- a/test_templates/demo_project.html +++ b/test_templates/demo_project.html @@ -57,8 +57,8 @@

Explanation


-

Using ICTemplateResponseMixin

-

This example demos using ICTemplateResponseMixin to extract html part from template file.

{% if show_details %} +

Using ICTemplateResponse

+

This example demos using ICTemplateResponse to extract html part from template file.

{% if show_details %}

Explanation


From 36551809d7d8e0a7d5601b4d51246ed8250bd96d Mon Sep 17 00:00:00 2001 From: Hai Lang Date: Tue, 20 Aug 2019 10:49:36 +0700 Subject: [PATCH 20/22] Fix tailing template tag, refactor to remove pyquery dependency --- intercooler_helpers/views.py | 38 ++++++++++++++++---------------- setup.py | 1 - test_templates/demo_project.html | 4 ++-- 3 files changed, 21 insertions(+), 22 deletions(-) diff --git a/intercooler_helpers/views.py b/intercooler_helpers/views.py index 514eaf3..d8c1cd5 100644 --- a/intercooler_helpers/views.py +++ b/intercooler_helpers/views.py @@ -1,5 +1,4 @@ import re -from lxml import etree from django import http from django.core import exceptions @@ -7,8 +6,7 @@ from django.utils.decorators import classonlymethod from django.views.generic import base as base_views, edit as edit_views -import pyquery -from cssselect import parser as css_parser +from lxml import etree class ICTemplateResponse(response.TemplateResponse): @@ -58,16 +56,20 @@ def extract_html_part(self, file_name, find=None): ''' xpath = self.__class__.build_xpath(attrbs={'id': find}) with open(file_name) as tmpl_file: - tmpl_content = tmpl_file.read() + root = etree.fromstring(tmpl_file.read(), etree.HTMLParser()) # print('extract_html_part', find) html_part = None - pq = pyquery.PyQuery(tmpl_content) - root = pq.root - if root: - html_part = root.find(xpath) - else: pass - result = (tmpl_content if html_part is None - else etree.tostring(html_part)) + # If root is None, something is wrong so developer need to know + html_part = root.find(xpath) + # with_tail is to remove django template tag as tail of found html tag + # result = (tmpl_content if html_part is None + # else etree.tostring(html_part, with_tail=False)) + # FutureWarning: The behavior of this method will change in future + # versions. Use specific 'len(elem)' or 'elem is not None' test + # instead. + # result = html_part or etree.tostring(html_part, with_tail=False) + result = (None if html_part is None + else etree.tostring(html_part, with_tail=False)) # print('"%s"' % result) return result @@ -80,14 +82,12 @@ def resolve_template(self, template): template_obj = super(ICTemplateResponse, self ).resolve_template(template) target_id = self._request.is_intercooler() and self.get_target_id() - if target_id: pass - else: - return template_obj - - html_part = self.extract_html_part( - str(template_obj.origin), find=target_id) - engine = engines[template_obj.backend.name] - template_obj = engine.from_string(html_part) + html_part = self.extract_html_part(str(template_obj.origin), + find=target_id) if target_id else None + if html_part: + engine = engines[template_obj.backend.name] + template_obj = engine.from_string(html_part) + else: pass return template_obj diff --git a/setup.py b/setup.py index 59208e8..46f1528 100644 --- a/setup.py +++ b/setup.py @@ -63,7 +63,6 @@ def make_readme(root_path): install_requires=[ "Django>=1.8", "django-intercoolerjs>=1.1.0.0", - "pyquery>=1.4.0", ], tests_require=[ "pytest-cov>=1.8", diff --git a/test_templates/demo_project.html b/test_templates/demo_project.html index f5a33eb..fa825db 100644 --- a/test_templates/demo_project.html +++ b/test_templates/demo_project.html @@ -96,10 +96,10 @@

Explanation

Post {{ message }} - {% for message in messages %} + {% for message in messages %} {{ message }} {{ message }} - {% endfor message %} + {% endfor message %}
From a262a0c38924e66fe571c887498fb5949d38f7b1 Mon Sep 17 00:00:00 2001 From: Hai Lang Date: Tue, 20 Aug 2019 11:50:13 +0700 Subject: [PATCH 21/22] Refactor to prepare response class, 100% coverage again --- intercooler_helpers/tests/test_views.py | 6 ++++-- intercooler_helpers/views.py | 12 ++++++++++-- test_urls.py | 12 ++++++------ 3 files changed, 20 insertions(+), 10 deletions(-) diff --git a/intercooler_helpers/tests/test_views.py b/intercooler_helpers/tests/test_views.py index 856b639..9078209 100644 --- a/intercooler_helpers/tests/test_views.py +++ b/intercooler_helpers/tests/test_views.py @@ -11,7 +11,7 @@ def test_get_without_ic(client): response = client.get(urls.reverse('ic_dispatch')) # print(response, '\n', *dir(response), sep='\t') - assert response.content.decode('utf-8') == 'In GET' + assert 'In GET' in response.content.decode('utf-8') def test_post_without_ic(client): response = client.post(urls.reverse('ic_dispatch')) @@ -49,7 +49,7 @@ def test_get_ic_not_match_target(client): response = client.get(urls.reverse('ic_dispatch'), data=data, HTTP_X_IC_REQUEST="true", HTTP_X_REQUESTED_WITH='XMLHttpRequest') content = response.content.decode('utf-8') - assert content == 'In GET' + assert 'In GET' in content def test_get_ic_without_target_id_has_full_template(client): data = { @@ -59,6 +59,8 @@ def test_get_ic_without_target_id_has_full_template(client): response = client.get(urls.reverse('ic_dispatch'), data=data, HTTP_X_IC_REQUEST="true", HTTP_X_REQUESTED_WITH='XMLHttpRequest') content = response.content.decode('utf-8') + # To make sure the test_full_template is called + assert 'Full Template' in content assert ' Date: Tue, 20 Aug 2019 20:23:26 +0700 Subject: [PATCH 22/22] Add lxml for GitHub travis --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 46f1528..b4176cb 100644 --- a/setup.py +++ b/setup.py @@ -63,6 +63,7 @@ def make_readme(root_path): install_requires=[ "Django>=1.8", "django-intercoolerjs>=1.1.0.0", + "lxml>=4.4.0", ], tests_require=[ "pytest-cov>=1.8",