From b669d7f18362e37b456a8e5a9974e2fa68519b16 Mon Sep 17 00:00:00 2001 From: Michael Yin Date: Sun, 15 Sep 2024 09:56:30 +0800 Subject: [PATCH] Feature/form tag (#6) --- src/django_formify/tailwind/formify_helper.py | 128 +++++++++--------- .../templates/formify/tailwind/form_tag.html | 5 + src/django_formify/templatetags/formify.py | 112 ++++++++++++--- tests/templates/test.html | 1 + tests/test_tags.py | 55 +++++++- 5 files changed, 221 insertions(+), 80 deletions(-) create mode 100644 src/django_formify/templates/formify/tailwind/form_tag.html create mode 100644 tests/templates/test.html diff --git a/src/django_formify/tailwind/formify_helper.py b/src/django_formify/tailwind/formify_helper.py index a309344..f17640b 100644 --- a/src/django_formify/tailwind/formify_helper.py +++ b/src/django_formify/tailwind/formify_helper.py @@ -104,14 +104,15 @@ def prepare_css_container(self): def get_context_data(self, context_data) -> Context: if isinstance(context_data, Context): - new_context = Context(context_data.flatten()) + context = context_data else: - new_context = Context(context_data) + context = Context(context_data) - new_context["formify_helper"] = self - new_context["form"] = self.form - new_context["formset"] = self.formset - return new_context + context["formify_helper"] = self + context["form"] = self.form + context["formset"] = self.formset + + return context def smart_render(self, template, context): # if template is django.template.base.Template, make sure context is a Context object @@ -126,6 +127,7 @@ def smart_render(self, template, context): else: # make sure the context is dict if isinstance(context, Context): + # convert to dict context_for_render = context.flatten() else: context_for_render = context @@ -139,19 +141,34 @@ def build_default_layout(self): # Rendering Methods ################################################################################ - def render_formset(self, context, create_new_context=False): + def render_form_tag(self, context, content, **kwargs): + with context.push(): + update_context = self.get_context_data(context) + update_context["form_content"] = content + attrs = { + "class": kwargs.pop("css_class", ""), + "method": kwargs.pop("method", "POST").upper(), + } + action = kwargs.pop("action", "") + if action: + attrs["action"] = action + # add extra attributes + for key, value in kwargs.items(): + attrs[key] = value + update_context["attrs"] = attrs + template = get_template("formify/tailwind/form_tag.html") + return self.smart_render(template, update_context) + + def render_formset(self, context): """ uni_formset.html """ - if create_new_context: - context = self.get_context_data(context) - # render formset management form fields management_form = self.formset.management_form management_form_helper = init_formify_helper_for_form(management_form) - management_form_html = management_form_helper.render_form( - management_form_helper.get_context_data(context) - ) + with context.push(): + update_context = management_form_helper.get_context_data(context) + management_form_html = management_form_helper.render_form(update_context) # render formset errors formset_errors = self.render_formset_errors(context) @@ -159,77 +176,65 @@ def render_formset(self, context, create_new_context=False): forms_html = "" for form in self.formset: form_helper = init_formify_helper_for_form(form) - forms_html += form_helper.render_form(form_helper.get_context_data(context)) + with context.push(): + update_context = form_helper.get_context_data(context) + forms_html += form_helper.render_form(update_context) return SafeString(management_form_html + formset_errors + forms_html) - def render_form(self, context, create_new_context=False): + def render_form(self, context): """ uni_form.html """ - if create_new_context: - context = self.get_context_data(context) - return SafeString( self.render_form_errors(context) + self.render_form_fields(context) ) - def render_field(self, field, context, create_new_context=False, **kwargs): + def render_field(self, context, field, **kwargs): """ This method is to render specific field """ - helper: FormifyHelper = self - - if create_new_context: - # create a new instance of FormifyHelper - field_helper = copy.copy(self) + field_formify_helper = copy.copy(self) - # assign extra kwargs to field_helper - for key, value in kwargs.items(): - setattr(field_helper, key, value) + # assign extra kwargs to formify_helper if needed + for key, value in kwargs.items(): + setattr(field_formify_helper, key, value) - context = field_helper.get_context_data(context) - - helper = field_helper - else: - pass + with context.push(): + context["field"] = field - context["field"] = field - - if field.is_hidden: - return SafeString(field.as_widget()) - else: - dispatch_method_callable = helper.field_dispatch(field) - return SafeString(dispatch_method_callable(context)) + if field.is_hidden: + return SafeString(field.as_widget()) + else: + dispatch_method_callable = field_formify_helper.field_dispatch(field) + update_context = field_formify_helper.get_context_data(context) + return SafeString(dispatch_method_callable(update_context)) - def render_submit(self, context, create_new_context=True, **kwargs): + def render_submit(self, context, **kwargs): """ It would be called from the render_submit tag Here we use Submit component to render the submit button, you can also override this method and use Django's get_template and render methods to render the submit button """ - if create_new_context: - context = self.get_context_data(context) - css_class = kwargs.pop("css_class", None) text = kwargs.pop("text", None) submit_component = Submit(text=text, css_class=css_class, **kwargs) - return submit_component.render_from_parent_context(context) - - def render_formset_errors(self, context, create_new_context=False): - if create_new_context: - context = self.get_context_data(context) - - error_template = get_template("formify/tailwind/errors_formset.html") - return self.smart_render(error_template, context) - - def render_form_errors(self, context, create_new_context=False): - if create_new_context: - context = self.get_context_data(context) - - error_template = get_template("formify/tailwind/errors.html") - return self.smart_render(error_template, context) + with context.push(): + update_context = self.get_context_data(context) + return submit_component.render_from_parent_context(update_context) + + def render_formset_errors(self, context): + template = get_template("formify/tailwind/errors_formset.html") + with context.push(): + update_context = self.get_context_data(context) + return self.smart_render(template, update_context) + + def render_form_errors(self, context): + template = get_template("formify/tailwind/errors.html") + with context.push(): + update_context = self.get_context_data(context) + return self.smart_render(template, update_context) ################################################################################ @@ -251,9 +256,10 @@ def field_dispatch(self, field): def render_form_fields(self, context): if not self.layout: self.layout = self.build_default_layout() - - # render_from_parent_context is a method from the viewcomponent class - return self.layout.render_from_parent_context(context) + with context.push(): + update_context = self.get_context_data(context) + # render_from_parent_context is a method from the Component class + return self.layout.render_from_parent_context(update_context) def render_as_tailwind_field(self, context): """ diff --git a/src/django_formify/templates/formify/tailwind/form_tag.html b/src/django_formify/templates/formify/tailwind/form_tag.html new file mode 100644 index 0000000..e321d22 --- /dev/null +++ b/src/django_formify/templates/formify/tailwind/form_tag.html @@ -0,0 +1,5 @@ +{% load formify %} + +
+ {{ form_content|safe }} +
diff --git a/src/django_formify/templatetags/formify.py b/src/django_formify/templatetags/formify.py index 9732b8b..000d44d 100644 --- a/src/django_formify/templatetags/formify.py +++ b/src/django_formify/templatetags/formify.py @@ -1,6 +1,9 @@ from django import template from django.forms.formsets import BaseFormSet +from django.template.base import Node, NodeList from django.template.context import Context +from django.template.exceptions import TemplateSyntaxError +from django.template.library import parse_bits from django.utils.safestring import mark_safe from django_formify.utils import flatatt as utils_flatatt @@ -18,16 +21,12 @@ def render_form(context, form_or_formset): # formset formset = form_or_formset formify_helper = init_formify_helper_for_formset(formset) - return formify_helper.render_formset( - Context(context.flatten()), create_new_context=True - ) + return formify_helper.render_formset(context) else: # form form = form_or_formset formify_helper = init_formify_helper_for_form(form) - return formify_helper.render_form( - Context(context.flatten()), create_new_context=True - ) + return formify_helper.render_form(context) @register.simple_tag(takes_context=True) @@ -36,16 +35,12 @@ def render_form_errors(context, form_or_formset): # formset formset = form_or_formset formify_helper = init_formify_helper_for_formset(formset) - return formify_helper.render_formset_errors( - Context(context.flatten()), create_new_context=True - ) + return formify_helper.render_formset_errors(context) else: # form form = form_or_formset formify_helper = init_formify_helper_for_form(form) - return formify_helper.render_form_errors( - Context(context.flatten()), create_new_context=True - ) + return formify_helper.render_form_errors(context) @register.simple_tag(takes_context=True) @@ -53,17 +48,16 @@ def render_field(context, field, **kwargs): form = field.form formify_helper = init_formify_helper_for_form(form) return formify_helper.render_field( + context=context, field=field, - context=Context(context.flatten()), - create_new_context=True, - **kwargs + **kwargs, ) @register.simple_tag(takes_context=True) def render_submit(context, form=None, **kwargs): formify_helper = init_formify_helper_for_form(form) - return formify_helper.render_submit(Context(context.flatten()), **kwargs) + return formify_helper.render_submit(context, **kwargs) @register.filter @@ -71,6 +65,82 @@ def flatatt(attrs): return mark_safe(utils_flatatt(attrs)) +class FormTagNode(Node): + def __init__( + self, + context_args, + context_kwargs, + nodelist: NodeList, + ): + self.context_args = context_args or [] + self.context_kwargs = context_kwargs or {} + self.nodelist = nodelist + + def __repr__(self): + return "" % ( + getattr( + self, "nodelist", None + ), # 'nodelist' attribute only assigned later. + ) + + def render(self, context: Context): + resolved_component_args = [ + safe_resolve(arg, context) for arg in self.context_args + ] + resolved_component_kwargs = { + key: safe_resolve(kwarg, context) + for key, kwarg in self.context_kwargs.items() + } + form = resolved_component_args[0] + formify_helper = init_formify_helper_for_form(form) + content = self.nodelist.render(context) + return formify_helper.render_form_tag( + context=context, content=content, **resolved_component_kwargs + ) + + +@register.tag(name="form_tag") +def do_form_tag(parser, token): + bits = token.split_contents() + tag_name = "form_tag" + tag_args, tag_kwargs = parse_bits( + parser=parser, + bits=bits, + params=[], + takes_context=False, + name=tag_name, + varargs=True, + varkw=[], + defaults=None, + kwonly=[], + kwonly_defaults=None, + ) + + if tag_name != tag_args[0].token: + raise RuntimeError( + f"Internal error: Expected tag_name to be {tag_name}, but it was {tag_args[0].token}" + ) + + if len(tag_args) != 2: + raise TemplateSyntaxError( + f"'{tag_name}' tag should have form as the first argument, other arguments should be keyword arguments." + ) + + context_args = tag_args[1:] + context_kwargs = tag_kwargs + + nodelist: NodeList = parser.parse(parse_until=["endform_tag"]) + parser.delete_first_token() + + component_node = FormTagNode( + context_args=context_args, + context_kwargs=context_kwargs, + nodelist=nodelist, + ) + + return component_node + + @register.filter def build_attrs(field): """ @@ -135,3 +205,13 @@ def optgroups(field): attrs = field.build_widget_attrs(attrs) values = field.field.widget.format_value(field.value()) return field.field.widget.optgroups(field.html_name, values, attrs) + + +def safe_resolve(context_item, context): + """Resolve FilterExpressions and Variables in context if possible. Return other items unchanged.""" + + return ( + context_item.resolve(context) + if hasattr(context_item, "resolve") + else context_item + ) diff --git a/tests/templates/test.html b/tests/templates/test.html new file mode 100644 index 0000000..5ab2f8a --- /dev/null +++ b/tests/templates/test.html @@ -0,0 +1 @@ +Hello \ No newline at end of file diff --git a/tests/test_tags.py b/tests/test_tags.py index add8317..24848ac 100644 --- a/tests/test_tags.py +++ b/tests/test_tags.py @@ -9,11 +9,60 @@ pytestmark = pytest.mark.django_db -def render(template, context): - return Template(template).render(Context(context)) +class TestBasic: + def test_form_tag(self): + template = Template( + """ + {% load formify %} + {% with url='/' %} + {% form_tag form action=url %} + {% render_form form %} + {% endform_tag %} + {% endwith %} + """ + ) + c = Context({"form": SampleForm()}) + html = template.render(c) + assert_select(html, "form", 1) + assert_select(html, "form[method='POST']", 1) + assert_select(html, "form[action='/']", 1) + + def test_form_tag_extra_kwargs(self): + template = Template( + """ + {% load formify %} + + {% with url='/' %} + {% form_tag form action=url data_test='test' novalidate=True %} + {% render_form form %} + {% endform_tag %} + {% endwith %} + """ + ) + c = Context({"form": SampleForm()}) + html = template.render(c) + + assert_select(html, "form", 1) + assert_select(html, "form[method='POST']", 1) + assert_select(html, "form[action='/']", 1) + assert_select(html, "form[data-test='test']", 1) + assert_select(html, "form[novalidate]", 1) + + def test_form_tag_with_include(self): + template = Template( + """ + {% load formify %} + + {% form_tag form %} + {% include 'test.html' %} + {% endform_tag %} + """ + ) + c = Context({"form": SampleForm()}) + html = template.render(c) + assert "Hello" in html -class TestBasic: def test_render_field(self): template = Template( """