1from functools import lru_cache
2
3from django import template
4from django.conf import settings
5from django.forms.formsets import BaseFormSet
6from django.template.loader import get_template
7
8from crispy_forms.helper import FormHelper
9from crispy_forms.utils import TEMPLATE_PACK, get_template_pack
10
11register = template.Library()
12
13# We import the filters, so they are available when doing load crispy_forms_tags
14from crispy_forms.templatetags.crispy_forms_filters import *  # NOQA: F403,F401, E402 isort:skip
15
16
17class ForLoopSimulator:
18    """
19    Simulates a forloop tag, precisely::
20
21        {% for form in formset.forms %}
22
23    If `{% crispy %}` is rendering a formset with a helper, We inject a `ForLoopSimulator` object
24    in the context as `forloop` so that formset forms can do things like::
25
26        Fieldset("Item {{ forloop.counter }}", [...])
27        HTML("{% if forloop.first %}First form text{% endif %}"
28    """
29
30    def __init__(self, formset):
31        self.len_values = len(formset.forms)
32
33        # Shortcuts for current loop iteration number.
34        self.counter = 1
35        self.counter0 = 0
36        # Reverse counter iteration numbers.
37        self.revcounter = self.len_values
38        self.revcounter0 = self.len_values - 1
39        # Boolean values designating first and last times through loop.
40        self.first = True
41        self.last = 0 == self.len_values - 1
42
43    def iterate(self):
44        """
45        Updates values as if we had iterated over the for
46        """
47        self.counter += 1
48        self.counter0 += 1
49        self.revcounter -= 1
50        self.revcounter0 -= 1
51        self.first = False
52        self.last = self.revcounter0 == self.len_values - 1
53
54
55class BasicNode(template.Node):
56    """
57    Basic Node object that we can rely on for Node objects in normal
58    template tags. I created this because most of the tags we'll be using
59    will need both the form object and the helper string. This handles
60    both the form object and parses out the helper string into attributes
61    that templates can easily handle.
62    """
63
64    def __init__(self, form, helper, template_pack=None):
65        self.form = form
66        if helper is not None:
67            self.helper = helper
68        else:
69            self.helper = None
70        self.template_pack = template_pack or get_template_pack()
71
72    def get_render(self, context):
73        """
74        Returns a `Context` object with all the necessary stuff for rendering the form
75
76        :param context: `django.template.Context` variable holding the context for the node
77
78        `self.form` and `self.helper` are resolved into real Python objects resolving them
79        from the `context`. The `actual_form` can be a form or a formset. If it's a formset
80        `is_formset` is set to True. If the helper has a layout we use it, for rendering the
81        form or the formset's forms.
82        """
83        # Nodes are not thread safe in multithreaded environments
84        # https://docs.djangoproject.com/en/dev/howto/custom-template-tags/#thread-safety-considerations
85        if self not in context.render_context:
86            context.render_context[self] = (
87                template.Variable(self.form),
88                template.Variable(self.helper) if self.helper else None,
89            )
90        form, helper = context.render_context[self]
91
92        actual_form = form.resolve(context)
93        if self.helper is not None:
94            helper = helper.resolve(context)
95        else:
96            # If the user names the helper within the form `helper` (standard), we use it
97            # This allows us to have simplified tag syntax: {% crispy form %}
98            helper = FormHelper() if not hasattr(actual_form, "helper") else actual_form.helper
99
100        # use template_pack from helper, if defined
101        try:
102            if helper.template_pack:
103                self.template_pack = helper.template_pack
104        except AttributeError:
105            pass
106
107        self.actual_helper = helper
108
109        # We get the response dictionary
110        is_formset = isinstance(actual_form, BaseFormSet)
111        response_dict = self.get_response_dict(helper, context, is_formset)
112        node_context = context.__copy__()
113        node_context.update({"is_bound": actual_form.is_bound})
114        node_context.update(response_dict)
115        final_context = node_context.__copy__()
116
117        # If we have a helper's layout we use it, for the form or the formset's forms
118        if helper and helper.layout:
119            if not is_formset:
120                actual_form.form_html = helper.render_layout(
121                    actual_form, node_context, template_pack=self.template_pack
122                )
123            else:
124                forloop = ForLoopSimulator(actual_form)
125                helper.render_hidden_fields = True
126                for form in actual_form:
127                    node_context.update({"forloop": forloop})
128                    node_context.update({"formset_form": form})
129                    form.form_html = helper.render_layout(form, node_context, template_pack=self.template_pack)
130                    forloop.iterate()
131
132        if is_formset:
133            final_context["formset"] = actual_form
134        else:
135            final_context["form"] = actual_form
136
137        return final_context
138
139    def get_response_dict(self, helper, context, is_formset):
140        """
141        Returns a dictionary with all the parameters necessary to render the form/formset in a template.
142
143        :param context: `django.template.Context` for the node
144        :param is_formset: Boolean value. If set to True, indicates we are working with a formset.
145        """
146        if not isinstance(helper, FormHelper):
147            raise TypeError("helper object provided to {% crispy %} tag must be a crispy.helper.FormHelper object.")
148
149        attrs = helper.get_attributes(template_pack=self.template_pack)
150        form_type = "form"
151        if is_formset:
152            form_type = "formset"
153
154        # We take form/formset parameters from attrs if they are set, otherwise we use defaults
155        response_dict = {
156            "%s_action" % form_type: attrs["attrs"].get("action", ""),
157            "%s_attrs" % form_type: attrs.get("attrs", ""),
158            "%s_class" % form_type: attrs["attrs"].get("class", ""),
159            "%s_id" % form_type: attrs["attrs"].get("id", ""),
160            "%s_method" % form_type: attrs.get("form_method", "post"),
161            "%s_style" % form_type: attrs.get("form_style", None),
162            "%s_tag" % form_type: attrs.get("form_tag", True),
163            "disable_csrf": attrs.get("disable_csrf", False),
164            "error_text_inline": attrs.get("error_text_inline", True),
165            "field_class": attrs.get("field_class", ""),
166            "field_template": attrs.get("field_template", ""),
167            "flat_attrs": attrs.get("flat_attrs", ""),
168            "form_error_title": attrs.get("form_error_title", None),
169            "form_show_errors": attrs.get("form_show_errors", True),
170            "form_show_labels": attrs.get("form_show_labels", True),
171            "formset_error_title": attrs.get("formset_error_title", None),
172            "help_text_inline": attrs.get("help_text_inline", False),
173            "html5_required": attrs.get("html5_required", False),
174            "include_media": attrs.get("include_media", True),
175            "inputs": attrs.get("inputs", []),
176            "is_formset": is_formset,
177            "label_class": attrs.get("label_class", ""),
178            "template_pack": self.template_pack,
179        }
180
181        # Handles custom attributes added to helpers
182        for attribute_name, value in attrs.items():
183            if attribute_name not in response_dict:
184                response_dict[attribute_name] = value
185
186        if "csrf_token" in context:
187            response_dict["csrf_token"] = context["csrf_token"]
188
189        return response_dict
190
191
192@lru_cache()
193def whole_uni_formset_template(template_pack=TEMPLATE_PACK):
194    return get_template("%s/whole_uni_formset.html" % template_pack)
195
196
197@lru_cache()
198def whole_uni_form_template(template_pack=TEMPLATE_PACK):
199    return get_template("%s/whole_uni_form.html" % template_pack)
200
201
202class CrispyFormNode(BasicNode):
203    def render(self, context):
204        c = self.get_render(context).flatten()
205
206        if self.actual_helper is not None and getattr(self.actual_helper, "template", False):
207            template = get_template(self.actual_helper.template)
208        else:
209            if c["is_formset"]:
210                template = whole_uni_formset_template(self.template_pack)
211            else:
212                template = whole_uni_form_template(self.template_pack)
213        return template.render(c)
214
215
216# {% crispy %} tag
217@register.tag(name="crispy")
218def do_uni_form(parser, token):
219    """
220    You need to pass in at least the form/formset object, and can also pass in the
221    optional `crispy_forms.helpers.FormHelper` object.
222
223    helper (optional): A `crispy_forms.helper.FormHelper` object.
224
225    Usage::
226
227        {% load crispy_tags %}
228        {% crispy form form.helper %}
229
230    You can also provide the template pack as the third argument::
231
232        {% crispy form form.helper 'bootstrap' %}
233
234    If the `FormHelper` attribute is named `helper` you can simply do::
235
236        {% crispy form %}
237        {% crispy form 'bootstrap' %}
238    """
239    token = token.split_contents()
240    form = token.pop(1)
241
242    helper = None
243    template_pack = "'%s'" % get_template_pack()
244
245    # {% crispy form helper %}
246    try:
247        helper = token.pop(1)
248    except IndexError:
249        pass
250
251    # {% crispy form helper 'bootstrap' %}
252    try:
253        template_pack = token.pop(1)
254    except IndexError:
255        pass
256
257    # {% crispy form 'bootstrap' %}
258    if helper is not None and isinstance(helper, str) and ("'" in helper or '"' in helper):
259        template_pack = helper
260        helper = None
261
262    if template_pack is not None:
263        template_pack = template_pack[1:-1]
264        ALLOWED_TEMPLATE_PACKS = getattr(
265            settings, "CRISPY_ALLOWED_TEMPLATE_PACKS", ("bootstrap", "uni_form", "bootstrap3", "bootstrap4")
266        )
267        if template_pack not in ALLOWED_TEMPLATE_PACKS:
268            raise template.TemplateSyntaxError(
269                "crispy tag's template_pack argument should be in %s" % str(ALLOWED_TEMPLATE_PACKS)
270            )
271
272    return CrispyFormNode(form, helper, template_pack=template_pack)
273