From 85629e17f6363cfc32954ef7d888ab92f564d5ac Mon Sep 17 00:00:00 2001 From: Julian Feinauer Date: Fri, 25 Dec 2020 10:42:00 +0100 Subject: [PATCH 1/6] Add custom stimulus tag to make it easier to create stimuli in django templates. --- sockpuppet/templatetags/sockpuppet.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/sockpuppet/templatetags/sockpuppet.py b/sockpuppet/templatetags/sockpuppet.py index 737596c..115a7d0 100644 --- a/sockpuppet/templatetags/sockpuppet.py +++ b/sockpuppet/templatetags/sockpuppet.py @@ -1,5 +1,6 @@ from django import template from django.template import Template +from django.utils.safestring import mark_safe register = template.Library() @@ -25,7 +26,19 @@ def render(self, context): elif node.token.token_type.name == 'VAR': raw = '{{ ' + node.token.contents + ' }}' else: - msg ='{} is not yet handled'.format(node.token.token_type.name) + msg = '{} is not yet handled'.format(node.token.token_type.name) raise Exception(msg) output = output + raw return output + + +@register.simple_tag +def stimulus(reflex, **kwargs): + """ + Adds the necessary data-reflex tag to handle a click element on the respective element + :param reflex: Name of the Reflex Controller and Method ({controller}#{handler}). + :param kwargs: Further data- attributes that should be passed to the handler + """ + # TODO Validate that the reflex is present and can be handled + data = ' '.join([f'data-{key}="{val}"' for key, val in kwargs.items()]) + return mark_safe(f'data-reflex="click->{reflex}" {data}') From ac668bc7ae62adad59b395fe2bec71232a4a271f Mon Sep 17 00:00:00 2001 From: Julian Feinauer Date: Fri, 25 Dec 2020 12:42:27 +0100 Subject: [PATCH 2/6] Rename Tag to stimulus and add Test. --- sockpuppet/templatetags/sockpuppet.py | 6 +++--- tests/example/templates/tag_example.html | 2 ++ tests/example/urls.py | 5 +++-- tests/example/views/example.py | 4 ++++ tests/test_tag.py | 13 +++++++++++++ 5 files changed, 25 insertions(+), 5 deletions(-) create mode 100644 tests/example/templates/tag_example.html create mode 100644 tests/test_tag.py diff --git a/sockpuppet/templatetags/sockpuppet.py b/sockpuppet/templatetags/sockpuppet.py index 115a7d0..7a12e82 100644 --- a/sockpuppet/templatetags/sockpuppet.py +++ b/sockpuppet/templatetags/sockpuppet.py @@ -33,12 +33,12 @@ def render(self, context): @register.simple_tag -def stimulus(reflex, **kwargs): +def reflex(controller, **kwargs): """ Adds the necessary data-reflex tag to handle a click element on the respective element - :param reflex: Name of the Reflex Controller and Method ({controller}#{handler}). + :param controller: Name of the Reflex Controller and Method ({controller}#{handler}). :param kwargs: Further data- attributes that should be passed to the handler """ # TODO Validate that the reflex is present and can be handled data = ' '.join([f'data-{key}="{val}"' for key, val in kwargs.items()]) - return mark_safe(f'data-reflex="click->{reflex}" {data}') + return mark_safe(f'data-reflex="click->{controller}" {data}') diff --git a/tests/example/templates/tag_example.html b/tests/example/templates/tag_example.html new file mode 100644 index 0000000..775d390 --- /dev/null +++ b/tests/example/templates/tag_example.html @@ -0,0 +1,2 @@ +{% load sockpuppet %} +click me diff --git a/tests/example/urls.py b/tests/example/urls.py index 6b91f8d..f30e176 100644 --- a/tests/example/urls.py +++ b/tests/example/urls.py @@ -16,9 +16,10 @@ from django.urls import path -from .views.example import ExampleView, ParamView +from .views.example import ExampleView, ParamView, TagExampleView urlpatterns = [ path('test/', ExampleView.as_view(), name='example'), - path('param/', ParamView.as_view(), name='param') + path('param/', ParamView.as_view(), name='param'), + path('tag/', TagExampleView.as_view(), name='tag') ] diff --git a/tests/example/views/example.py b/tests/example/views/example.py index cccda4e..5a3d0da 100644 --- a/tests/example/views/example.py +++ b/tests/example/views/example.py @@ -18,3 +18,7 @@ def get(self, request, *args, **kwargs): kwargs.update(dict(self.request.GET.items())) context = self.get_context_data(**kwargs) return self.render_to_response(context) + + +class TagExampleView(TemplateView): + template_name = 'tag_example.html' diff --git a/tests/test_tag.py b/tests/test_tag.py new file mode 100644 index 0000000..057663b --- /dev/null +++ b/tests/test_tag.py @@ -0,0 +1,13 @@ +from django.template.response import TemplateResponse +from django.test import TestCase, Client + + +class TestTagSupport(TestCase): + + def test_reflex_tag(self): + c = Client() + response: TemplateResponse = c.get('/tag/') + + content = response.content.decode('utf-8') + + self.assertEqual('\nclick me\n', content) From cde5e855287e0089fb9375faa02366264b851892 Mon Sep 17 00:00:00 2001 From: Julian Feinauer Date: Fri, 25 Dec 2020 12:48:51 +0100 Subject: [PATCH 3/6] Escape values of data-arguments. --- sockpuppet/templatetags/sockpuppet.py | 3 ++- tests/example/templates/tag_example.html | 2 +- tests/example/views/example.py | 6 ++++++ tests/test_tag.py | 16 +++++++++++++++- 4 files changed, 24 insertions(+), 3 deletions(-) diff --git a/sockpuppet/templatetags/sockpuppet.py b/sockpuppet/templatetags/sockpuppet.py index 7a12e82..8771c3a 100644 --- a/sockpuppet/templatetags/sockpuppet.py +++ b/sockpuppet/templatetags/sockpuppet.py @@ -1,5 +1,6 @@ from django import template from django.template import Template +from django.utils.html import escape from django.utils.safestring import mark_safe register = template.Library() @@ -40,5 +41,5 @@ def reflex(controller, **kwargs): :param kwargs: Further data- attributes that should be passed to the handler """ # TODO Validate that the reflex is present and can be handled - data = ' '.join([f'data-{key}="{val}"' for key, val in kwargs.items()]) + data = ' '.join([f'data-{key}="{escape(val)}"' for key, val in kwargs.items()]) return mark_safe(f'data-reflex="click->{controller}" {data}') diff --git a/tests/example/templates/tag_example.html b/tests/example/templates/tag_example.html index 775d390..a002f38 100644 --- a/tests/example/templates/tag_example.html +++ b/tests/example/templates/tag_example.html @@ -1,2 +1,2 @@ {% load sockpuppet %} -click me +click me diff --git a/tests/example/views/example.py b/tests/example/views/example.py index 5a3d0da..0532452 100644 --- a/tests/example/views/example.py +++ b/tests/example/views/example.py @@ -22,3 +22,9 @@ def get(self, request, *args, **kwargs): class TagExampleView(TemplateView): template_name = 'tag_example.html' + + def get(self, request, *args, **kwargs): + kwargs.update({"parameter": "I am a parameter"}) + kwargs.update(dict(self.request.GET.items())) + context = self.get_context_data(**kwargs) + return self.render_to_response(context) diff --git a/tests/test_tag.py b/tests/test_tag.py index 057663b..2c6d17f 100644 --- a/tests/test_tag.py +++ b/tests/test_tag.py @@ -10,4 +10,18 @@ def test_reflex_tag(self): content = response.content.decode('utf-8') - self.assertEqual('\nclick me\n', content) + self.assertEqual('\n' + 'click me' + '\n', content) + + def test_reflex_tag_with_unsafe_input(self): + c = Client() + response: TemplateResponse = c.get('/tag/', data={"parameter": ""}) + + content = response.content.decode('utf-8') + + self.assertEqual('\n' + 'click me' + '\n', content) From 63caff3fc3d87eb856d716284a9267561e50eac8 Mon Sep 17 00:00:00 2001 From: Julian Feinauer Date: Fri, 25 Dec 2020 14:14:19 +0100 Subject: [PATCH 4/6] Added multiple tests, new reflexes and wrapping controller tag. --- sockpuppet/templatetags/sockpuppet.py | 108 +++++++++++++++++- .../example/templates/second_tag_example.html | 4 + tests/example/templates/tag_example.html | 4 +- tests/example/urls.py | 5 +- tests/example/views/example.py | 10 ++ tests/test_tag.py | 49 ++++++-- 6 files changed, 166 insertions(+), 14 deletions(-) create mode 100644 tests/example/templates/second_tag_example.html diff --git a/sockpuppet/templatetags/sockpuppet.py b/sockpuppet/templatetags/sockpuppet.py index 8771c3a..af09880 100644 --- a/sockpuppet/templatetags/sockpuppet.py +++ b/sockpuppet/templatetags/sockpuppet.py @@ -1,5 +1,6 @@ from django import template from django.template import Template +from django.template.base import Token, VariableNode, FilterExpression from django.utils.html import escape from django.utils.safestring import mark_safe @@ -41,5 +42,108 @@ def reflex(controller, **kwargs): :param kwargs: Further data- attributes that should be passed to the handler """ # TODO Validate that the reflex is present and can be handled - data = ' '.join([f'data-{key}="{escape(val)}"' for key, val in kwargs.items()]) - return mark_safe(f'data-reflex="click->{controller}" {data}') + return generate_reflex_attributes('click', controller, kwargs) + + +def generate_reflex_attributes(action, controller, parameters): + data = ' '.join([f'data-{key}="{val}"' for key, val in parameters.items()]) + return mark_safe(f'data-reflex="{action}->{controller}" {data}') + + +@register.tag +def stimulus_controller(parser, token, **kwargs): + _, controller = token.split_contents() + nodelist = parser.parse(('endcontroller',)) + parser.delete_first_token() + controller = controller.strip("'").strip('"') + return StimulusNode(controller, nodelist) + + +class ReflexNode(template.Node): + + def __init__(self, action, reflex, controller=None, parameters={}): + self.action = action + self.reflex = reflex + self.controller = controller + self.parameters = parameters + + def render(self, context): + if self.controller is None: + raise Exception( + "A ClickReflex tag can only be used inside a stimulus controller or needs an explicit controller set!") + parameters = {} + for k, v in self.parameters.items(): + if isinstance(v, VariableNode): + print(v.__class__) + value = v.render(context) + else: + value = v + parameters.update({k: value}) + return generate_reflex_attributes(self.action, f'{self.controller}#{self.reflex}', parameters) + + +def extract_string_or_node(text): + stripped = text.strip("'").strip('"') + is_numeric = False + try: + int(stripped) + is_numeric = True + except: + pass + if text == stripped and not is_numeric: + return VariableNode(FilterExpression(text, parser=None)) + else: + return stripped + + +@register.tag("click_reflex") +def click_reflex(parser, token: Token): + controller, kwargs, reflex = parse_reflex_token(token) + return ReflexNode('click', reflex, controller=controller, parameters=kwargs) + + +@register.tag("submit_reflex") +def submit_reflex(parser, token: Token): + controller, kwargs, reflex = parse_reflex_token(token) + return ReflexNode('submit', reflex, controller=controller, parameters=kwargs) + + +@register.tag("input_reflex") +def submit_reflex(parser, token: Token): + controller, kwargs, reflex = parse_reflex_token(token) + return ReflexNode('input', reflex, controller=controller, parameters=kwargs) + + +def parse_reflex_token(token): + splitted = token.split_contents()[1:] + args = [] + kwargs = {} + for s in splitted: + if s.__contains__("="): + k, v = s.split("=") + kwargs.update({k: extract_string_or_node(v)}) + else: + args.append(extract_string_or_node(s)) + if len(args) == 1: + reflex = args[0] + controller = None + elif len(args) == 2: + controller = args[0] + reflex = args[1] + else: + raise Exception('Only one or two non-kv parameters can be given!') + return controller, kwargs, reflex + + +class StimulusNode(template.Node): + + def __init__(self, controller, nodelist): + self.controller = controller + self.nodelist = nodelist + + def render(self, context): + for node in self.nodelist: + if isinstance(node, ReflexNode): + node.controller = self.controller + output = self.nodelist.render(context) + return output diff --git a/tests/example/templates/second_tag_example.html b/tests/example/templates/second_tag_example.html new file mode 100644 index 0000000..f659cc5 --- /dev/null +++ b/tests/example/templates/second_tag_example.html @@ -0,0 +1,4 @@ +{% load sockpuppet %} +{% stimulus_controller 'example_reflex' %} + click me +{% endcontroller %} diff --git a/tests/example/templates/tag_example.html b/tests/example/templates/tag_example.html index a002f38..4771f31 100644 --- a/tests/example/templates/tag_example.html +++ b/tests/example/templates/tag_example.html @@ -1,2 +1,4 @@ {% load sockpuppet %} -click me +click me +click me + diff --git a/tests/example/urls.py b/tests/example/urls.py index f30e176..5bdb0ea 100644 --- a/tests/example/urls.py +++ b/tests/example/urls.py @@ -16,10 +16,11 @@ from django.urls import path -from .views.example import ExampleView, ParamView, TagExampleView +from .views.example import ExampleView, ParamView, TagExampleView, SecondTagExampleView urlpatterns = [ path('test/', ExampleView.as_view(), name='example'), path('param/', ParamView.as_view(), name='param'), - path('tag/', TagExampleView.as_view(), name='tag') + path('tag/', TagExampleView.as_view(), name='tag'), + path('second/', SecondTagExampleView.as_view(), name='second_tag') ] diff --git a/tests/example/views/example.py b/tests/example/views/example.py index 0532452..6f981b8 100644 --- a/tests/example/views/example.py +++ b/tests/example/views/example.py @@ -28,3 +28,13 @@ def get(self, request, *args, **kwargs): kwargs.update(dict(self.request.GET.items())) context = self.get_context_data(**kwargs) return self.render_to_response(context) + + +class SecondTagExampleView(TemplateView): + template_name = 'second_tag_example.html' + + def get(self, request, *args, **kwargs): + kwargs.update({"parameter": "I am a parameter"}) + kwargs.update(dict(self.request.GET.items())) + context = self.get_context_data(**kwargs) + return self.render_to_response(context) diff --git a/tests/test_tag.py b/tests/test_tag.py index 2c6d17f..1a563d0 100644 --- a/tests/test_tag.py +++ b/tests/test_tag.py @@ -4,16 +4,36 @@ class TestTagSupport(TestCase): - def test_reflex_tag(self): + def test_click_reflex_tag(self): c = Client() response: TemplateResponse = c.get('/tag/') content = response.content.decode('utf-8') - self.assertEqual('\n' - 'click me' - '\n', content) + self.assertTrue(content.__contains__('' + 'click me' + '')) + + def test_submit_reflex_tag(self): + c = Client() + response: TemplateResponse = c.get('/tag/') + + content = response.content.decode('utf-8') + + self.assertTrue(content.__contains__('' + 'click me' + '')) + + def test_input_reflex_tag(self): + c = Client() + response: TemplateResponse = c.get('/tag/') + + content = response.content.decode('utf-8') + + self.assertTrue(content.__contains__( + '')) def test_reflex_tag_with_unsafe_input(self): c = Client() @@ -21,7 +41,18 @@ def test_reflex_tag_with_unsafe_input(self): content = response.content.decode('utf-8') - self.assertEqual('\n' - 'click me' - '\n', content) + self.assertTrue(content.__contains__('' + 'click me' + '')) + + def test_controller_tag(self): + c = Client() + response: TemplateResponse = c.get('/second/', data={"parameter": ""}) + + content = response.content.decode('utf-8') + + print(content) + self.assertEqual( + '\n\n click me\n\n', + content) From 24521c74b68636c0df399ad9d47e61265e9419f2 Mon Sep 17 00:00:00 2001 From: Julian Feinauer Date: Fri, 25 Dec 2020 16:41:38 +0100 Subject: [PATCH 5/6] Added possibility to pass a dict for controller, reflex and parameters. --- sockpuppet/templatetags/sockpuppet.py | 22 +++++++++++++++---- .../example/templates/second_tag_example.html | 2 ++ tests/example/views/example.py | 1 + tests/test_tag.py | 15 ++++++++++--- 4 files changed, 33 insertions(+), 7 deletions(-) diff --git a/sockpuppet/templatetags/sockpuppet.py b/sockpuppet/templatetags/sockpuppet.py index af09880..10b0877 100644 --- a/sockpuppet/templatetags/sockpuppet.py +++ b/sockpuppet/templatetags/sockpuppet.py @@ -1,5 +1,5 @@ from django import template -from django.template import Template +from django.template import Template, RequestContext from django.template.base import Token, VariableNode, FilterExpression from django.utils.html import escape from django.utils.safestring import mark_safe @@ -67,14 +67,28 @@ def __init__(self, action, reflex, controller=None, parameters={}): self.controller = controller self.parameters = parameters - def render(self, context): + def render(self, context: RequestContext): + # First, check if the "reflex" given is a VariableNode. We check to extract stuff from it + parameters = {} + if isinstance(self.reflex, VariableNode): + var_name = self.reflex.filter_expression.token + param = context.get(var_name) + + if param is None: + raise Exception(f"The given Variable '{var_name}' was not found in the context!") + + if 'controller' not in param or 'reflex' not in param: + raise Exception(f"The given object with name '{var_name}' needs to have attributes 'controller' and 'reflex'") + else: + self.controller = param['controller'] + self.reflex = param['reflex'] + parameters.update({k: param[k] for k in param.keys() if k not in ('controller', 'reflex')}) if self.controller is None: raise Exception( "A ClickReflex tag can only be used inside a stimulus controller or needs an explicit controller set!") - parameters = {} + for k, v in self.parameters.items(): if isinstance(v, VariableNode): - print(v.__class__) value = v.render(context) else: value = v diff --git a/tests/example/templates/second_tag_example.html b/tests/example/templates/second_tag_example.html index f659cc5..51148a6 100644 --- a/tests/example/templates/second_tag_example.html +++ b/tests/example/templates/second_tag_example.html @@ -2,3 +2,5 @@ {% stimulus_controller 'example_reflex' %} click me {% endcontroller %} + +I was done by object definition diff --git a/tests/example/views/example.py b/tests/example/views/example.py index 6f981b8..4c2b4cd 100644 --- a/tests/example/views/example.py +++ b/tests/example/views/example.py @@ -35,6 +35,7 @@ class SecondTagExampleView(TemplateView): def get(self, request, *args, **kwargs): kwargs.update({"parameter": "I am a parameter"}) + kwargs.update({"object_definition": {"controller": "abc", "reflex": "increment", "other_param": 123}}) kwargs.update(dict(self.request.GET.items())) context = self.get_context_data(**kwargs) return self.render_to_response(context) diff --git a/tests/test_tag.py b/tests/test_tag.py index 1a563d0..8e73d4f 100644 --- a/tests/test_tag.py +++ b/tests/test_tag.py @@ -52,7 +52,16 @@ def test_controller_tag(self): content = response.content.decode('utf-8') + self.assertTrue(content.__contains__( + 'click me')) + + def test_tag_by_dict(self): + c = Client() + response: TemplateResponse = c.get('/second/', data={"parameter": ""}) + + content = response.content.decode('utf-8') + print(content) - self.assertEqual( - '\n\n click me\n\n', - content) + + self.assertTrue(content.__contains__( + 'I was done by object definition')) From 8791be50cec360fd608657d10f3e0cc7bb6ac407 Mon Sep 17 00:00:00 2001 From: Julian Feinauer Date: Fri, 25 Dec 2020 16:53:44 +0100 Subject: [PATCH 6/6] Started documentation. --- docs/templating.md | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 docs/templating.md diff --git a/docs/templating.md b/docs/templating.md new file mode 100644 index 0000000..4db9dbc --- /dev/null +++ b/docs/templating.md @@ -0,0 +1,28 @@ +--- +description: How to use custom template tags +--- + +# Template Tags + +Multiple custom template tags are provided by the library to make it easier to generate HTML attributes that interact with reflex. +These are `stimulus_controller`, `reflex` and `*_reflex` where `*` describes valid stimulus actions like _click_, _submit_, ... + +## The *_reflex tags + +The `*_reflex` tags can be used to connect a HTML attibute like the `` Tag to a reflex. Some syntax examples are + +``` +click me +click me + +``` + +where different tags are used for different _actions_. + +## The Syntax of the *_reflex tags + +There are three ways to pass data to the *_reflex tags: +1. Pass `controller` and `reflex` as list attributes and all parameters (`data-` attributes) as kvargs +2. Use the `*_reflex` tag inside a `stimulus_controlle` Block and only pass `reflex` (controller will be taken automatically from the `stimulus_controller`). Parameters work the same as above. +3. Use a `dict` as sole list argument which has to contain the keys `controller` and `reflex`. All other dict entries as well as all given kvargs are used as `data-` attributes. +