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