1import logging
2import sys
3from functools import lru_cache
4
5from django.conf import settings
6from django.forms.utils import flatatt as _flatatt
7from django.template import Context
8from django.template.loader import get_template
9from django.utils.functional import SimpleLazyObject
10
11from .base import KeepContext
12
13
14def get_template_pack():
15    return getattr(settings, "CRISPY_TEMPLATE_PACK", "bootstrap")
16
17
18TEMPLATE_PACK = SimpleLazyObject(get_template_pack)
19
20
21# By caching we avoid loading the template every time render_field
22# is called without a template
23@lru_cache()
24def default_field_template(template_pack=TEMPLATE_PACK):
25    return get_template("%s/field.html" % template_pack)
26
27
28def render_field(  # noqa: C901
29    field,
30    form,
31    form_style,
32    context,
33    template=None,
34    labelclass=None,
35    layout_object=None,
36    attrs=None,
37    template_pack=TEMPLATE_PACK,
38    extra_context=None,
39    **kwargs,
40):
41    """
42    Renders a django-crispy-forms field
43
44    :param field: Can be a string or a Layout object like `Row`. If it's a layout
45        object, we call its render method, otherwise we instantiate a BoundField
46        and render it using default template 'CRISPY_TEMPLATE_PACK/field.html'
47        The field is added to a list that the form holds called `rendered_fields`
48        to avoid double rendering fields.
49    :param form: The form/formset to which that field belongs to.
50    :param form_style: A way to pass style name to the CSS framework used.
51    :template: Template used for rendering the field.
52    :layout_object: If passed, it points to the Layout object that is being rendered.
53        We use it to store its bound fields in a list called `layout_object.bound_fields`
54    :attrs: Attributes for the field's widget
55    :template_pack: Name of the template pack to be used for rendering `field`
56    :extra_context: Dictionary to be added to context, added variables by the layout object
57    """
58    added_keys = [] if extra_context is None else extra_context.keys()
59    with KeepContext(context, added_keys):
60        if field is None:
61            return ""
62
63        FAIL_SILENTLY = getattr(settings, "CRISPY_FAIL_SILENTLY", True)
64
65        if hasattr(field, "render"):
66            return field.render(form, form_style, context, template_pack=template_pack)
67
68        try:
69            # Injecting HTML attributes into field's widget, Django handles rendering these
70            bound_field = form[field]
71            field_instance = bound_field.field
72            if attrs is not None:
73                widgets = getattr(field_instance.widget, "widgets", [field_instance.widget])
74
75                # We use attrs as a dictionary later, so here we make a copy
76                list_attrs = attrs
77                if isinstance(attrs, dict):
78                    list_attrs = [attrs] * len(widgets)
79
80                for index, (widget, attr) in enumerate(zip(widgets, list_attrs)):
81                    if hasattr(field_instance.widget, "widgets"):
82                        if "type" in attr and attr["type"] == "hidden":
83                            field_instance.widget.widgets[index] = field_instance.hidden_widget(attr)
84
85                        else:
86                            field_instance.widget.widgets[index].attrs.update(attr)
87                    else:
88                        if "type" in attr and attr["type"] == "hidden":
89                            field_instance.widget = field_instance.hidden_widget(attr)
90
91                        else:
92                            field_instance.widget.attrs.update(attr)
93
94        except KeyError:
95            if not FAIL_SILENTLY:
96                raise Exception("Could not resolve form field '%s'." % field)
97            else:
98                field_instance = None
99                logging.warning("Could not resolve form field '%s'." % field, exc_info=sys.exc_info())
100
101        if hasattr(form, "rendered_fields"):
102            if field not in form.rendered_fields:
103                form.rendered_fields.add(field)
104            else:
105                if not FAIL_SILENTLY:
106                    raise Exception("A field should only be rendered once: %s" % field)
107                else:
108                    logging.warning("A field should only be rendered once: %s" % field, exc_info=sys.exc_info())
109
110        if field_instance is None:
111            html = ""
112        else:
113            if template is None:
114                if form.crispy_field_template is None:
115                    template = default_field_template(template_pack)
116                else:  # FormHelper.field_template set
117                    template = get_template(form.crispy_field_template)
118            else:
119                template = get_template(template)
120
121            # We save the Layout object's bound fields in the layout object's `bound_fields` list
122            if layout_object is not None:
123                if hasattr(layout_object, "bound_fields") and isinstance(layout_object.bound_fields, list):
124                    layout_object.bound_fields.append(bound_field)
125                else:
126                    layout_object.bound_fields = [bound_field]
127
128            context.update(
129                {
130                    "field": bound_field,
131                    "labelclass": labelclass,
132                    "flat_attrs": flatatt(attrs if isinstance(attrs, dict) else {}),
133                }
134            )
135            if extra_context is not None:
136                context.update(extra_context)
137
138            context = context.flatten()
139            html = template.render(context)
140
141        return html
142
143
144def flatatt(attrs):
145    """
146    Convert a dictionary of attributes to a single string.
147
148    Passed attributes are redirected to `django.forms.utils.flatatt()`
149    with replaced "_" (underscores) by "-" (dashes) in their names.
150    """
151    return _flatatt({k.replace("_", "-"): v for k, v in attrs.items()})
152
153
154def render_crispy_form(form, helper=None, context=None):
155    """
156    Renders a form and returns its HTML output.
157
158    This function wraps the template logic in a function easy to use in a Django view.
159    """
160    from crispy_forms.templatetags.crispy_forms_tags import CrispyFormNode
161
162    if helper is not None:
163        node = CrispyFormNode("form", "helper")
164    else:
165        node = CrispyFormNode("form", None)
166
167    node_context = Context(context)
168    node_context.update({"form": form, "helper": helper})
169
170    return node.render(node_context)
171
172
173def list_intersection(list1, list2):
174    """
175    Take the not-in-place intersection of two lists, similar to sets but preserving order.
176    Does not check unicity of list1.
177    """
178    return [item for item in list1 if item in list2]
179
180
181def list_difference(left, right):
182    """
183    Take the not-in-place difference of two lists (left - right), similar to sets but preserving order.
184    """
185    blocked = set(right)
186    difference = []
187    for item in left:
188        if item not in blocked:
189            blocked.add(item)
190            difference.append(item)
191    return difference
192