Source code for layout

from dataclasses import dataclass
from typing import List

from django.template import Template
from django.template.loader import render_to_string
from django.utils.html import conditional_escape
from django.utils.safestring import SafeString
from django.utils.text import slugify

from crispy_forms.utils import TEMPLATE_PACK, flatatt, render_field


[docs]@dataclass class Pointer: positions: List[int] name: str
class TemplateNameMixin: def get_template_name(self, template_pack): if "%s" in self.template: template = self.template % template_pack else: template = self.template return template class LayoutObject(TemplateNameMixin): def __getitem__(self, slice): return self.fields[slice] def __setitem__(self, slice, value): self.fields[slice] = value def __delitem__(self, slice): del self.fields[slice] def __len__(self): return len(self.fields) def __getattr__(self, name): """ This allows us to access self.fields list methods like append or insert, without having to declare them one by one """ # Check necessary for unpickling, see #107 if "fields" in self.__dict__ and hasattr(self.fields, name): return getattr(self.fields, name) else: return object.__getattribute__(self, name) def get_field_names(self, index=None): """ Returns a list of Pointers. First parameter is the location of the field, second one the name of the field. Example:: [ Pointer([0,1,2], 'field_name1'), Pointer([0,3], 'field_name2'), ] """ return self.get_layout_objects(str, index=None, greedy=True) def get_layout_objects(self, *LayoutClasses, index=None, max_level=0, greedy=False): """ Returns a list of Pointers pointing to layout objects of any type matching `LayoutClasses`:: [ Pointer([0,1,2], 'div']), Pointer([0,3], 'field_name'), ] :param max_level: An integer that indicates max level depth to reach when traversing a layout. :param greedy: Boolean that indicates whether to be greedy. If set, max_level is skipped. """ pointers = [] if index is not None and not isinstance(index, list): index = [index] elif index is None: index = [] str_class = len(LayoutClasses) == 1 and LayoutClasses[0] == str for i, layout_object in enumerate(self.fields): if isinstance(layout_object, LayoutClasses): if str_class: pointers.append(Pointer(index + [i], layout_object)) else: pointers.append(Pointer(index + [i], layout_object.__class__.__name__.lower())) # If it's a layout object and we haven't reached the max depth limit or greedy # we recursive call if hasattr(layout_object, "get_field_names") and (len(index) < max_level or greedy): new_kwargs = {"index": index + [i], "max_level": max_level, "greedy": greedy} pointers = pointers + layout_object.get_layout_objects(*LayoutClasses, **new_kwargs) return pointers def get_rendered_fields(self, form, context, template_pack=TEMPLATE_PACK, **kwargs): return SafeString( "".join(render_field(field, form, context, template_pack=template_pack, **kwargs) for field in self.fields) )
[docs]class Layout(LayoutObject): """ Form Layout. It is conformed by Layout objects: `Fieldset`, `Row`, `Column`, `MultiField`, `HTML`, `ButtonHolder`, `Button`, `Hidden`, `Reset`, `Submit` and fields. Form fields have to be strings. Layout objects `Fieldset`, `Row`, `Column`, `MultiField` and `ButtonHolder` can hold other Layout objects within. Though `ButtonHolder` should only hold `HTML` and BaseInput inherited classes: `Button`, `Hidden`, `Reset` and `Submit`. Example:: helper.layout = Layout( Fieldset('Company data', 'is_company' ), Fieldset(_('Contact details'), 'email', Row('password1', 'password2'), 'first_name', 'last_name', HTML('<img src="/media/somepicture.jpg"/>'), 'company' ), ButtonHolder( Submit('Save', 'Save', css_class='button white'), ), ) """ def __init__(self, *fields): self.fields = list(fields) def render(self, form, context, template_pack=TEMPLATE_PACK, **kwargs): return self.get_rendered_fields(form, context, template_pack, **kwargs)
[docs]class ButtonHolder(LayoutObject): """ Layout object. It wraps fields in a <div class="buttonHolder"> This is where you should put Layout objects that render to form buttons Attributes ---------- template: str The default template which this Layout Object will be rendered with. Parameters ---------- *fields : HTML or BaseInput The layout objects to render within the ``ButtonHolder``. It should only hold `HTML` and `BaseInput` inherited objects. css_id : str, optional A custom DOM id for the layout object. If not provided the name argument is slugified and turned into the id for the submit button. By default None. css_class : str, optional Additional CSS classes to be applied to the ``<input>``. By default None. template : str, optional Overrides the default template, if provided. By default None. Examples -------- An example using ``ButtonHolder`` in your layout:: ButtonHolder( HTML(<span style="display: hidden;">Information Saved</span>), Submit('Save', 'Save') ) """ template = "%s/layout/buttonholder.html" def __init__(self, *fields, css_id=None, css_class=None, template=None): self.fields = list(fields) self.css_id = css_id self.css_class = css_class self.template = template or self.template def render(self, form, context, template_pack=TEMPLATE_PACK, **kwargs): html = self.get_rendered_fields(form, context, template_pack, **kwargs) template = self.get_template_name(template_pack) context.update({"buttonholder": self, "fields_output": html}) return render_to_string(template, context.flatten())
[docs]class BaseInput(TemplateNameMixin): """ A base class to reduce the amount of code in the Input classes. Attributes ---------- template: str The default template which this Layout Object will be rendered with. field_classes: str CSS classes to be applied to the ``<input>``. Parameters ---------- name : str The name attribute of the button. value : str The value attribute of the button. css_id : str, optional A custom DOM id for the layout object. If not provided the name argument is slugified and turned into the id for the submit button. By default None. css_class : str, optional Additional CSS classes to be applied to the ``<input>``. By default None. template : str, optional Overrides the default template, if provided. By default None. **kwargs : dict, optional Additional attributes are passed to `flatatt` and converted into key="value", pairs. These attributes are added to the ``<input>``. """ template = "%s/layout/baseinput.html" field_classes = "" def __init__(self, name, value, *, css_id=None, css_class=None, template=None, **kwargs): self.name = name self.value = value if css_id: self.id = css_id else: slug = slugify(self.name) self.id = f"{self.input_type}-id-{slug}" self.attrs = {} if css_class: self.field_classes += f" {css_class}" self.template = template or self.template self.flat_attrs = flatatt(kwargs)
[docs] def render(self, form, context, template_pack=TEMPLATE_PACK, **kwargs): """ Renders an `<input />` if container is used as a Layout object. Input button value can be a variable in context. """ self.value = Template(str(self.value)).render(context) template = self.get_template_name(template_pack) context.update({"input": self}) return render_to_string(template, context.flatten())
[docs]class Submit(BaseInput): """ Used to create a Submit button descriptor for the {% crispy %} template tag. Attributes ---------- template: str The default template which this Layout Object will be rendered with. field_classes: str CSS classes to be applied to the ``<input>``. input_type: str The ``type`` attribute of the ``<input>``. Parameters ---------- name : str The name attribute of the button. value : str The value attribute of the button. css_id : str, optional A custom DOM id for the layout object. If not provided the name argument is slugified and turned into the id for the submit button. By default None. css_class : str, optional Additional CSS classes to be applied to the ``<input>``. By default None. template : str, optional Overrides the default template, if provided. By default None. **kwargs : dict, optional Additional attributes are passed to `flatatt` and converted into key="value", pairs. These attributes are added to the ``<input>``. Examples -------- Note: ``form`` arg to ``render()`` is not required for ``BaseInput`` inherited objects. >>> submit = Submit('Search the Site', 'search this site') >>> submit.render("", "", Context()) '<input type="submit" name="search-the-site" value="search this site" ' 'class="btn btn-primary" id="submit-id-search-the-site"/>' >>> submit = Submit('Search the Site', 'search this site', css_id="custom-id", css_class="custom class", my_attr=True, data="my-data") >>> submit.render("", "", Context()) '<input type="submit" name="search-the-site" value="search this site" ' 'class="btn btn-primary custom class" id="custom-id" data="my-data" my-attr/>' Usually you will not call the render method on the object directly. Instead add it to your ``Layout`` manually or use the `add_input` method:: class ExampleForm(forms.Form): [...] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.helper = FormHelper() self.helper.add_input(Submit('submit', 'Submit')) """ input_type = "submit" field_classes = "btn btn-primary"
[docs]class Button(BaseInput): """ Used to create a button descriptor for the {% crispy %} template tag. Attributes ---------- template: str The default template which this Layout Object will be rendered with. field_classes: str CSS classes to be applied to the ``<input>``. input_type: str The ``type`` attribute of the ``<input>``. Parameters ---------- name : str The name attribute of the button. value : str The value attribute of the button. css_id : str, optional A custom DOM id for the layout object. If not provided the name argument is slugified and turned into the id for the submit button. By default None. css_class : str, optional Additional CSS classes to be applied to the ``<input>``. By default None. template : str, optional Overrides the default template, if provided. By default None. **kwargs : dict, optional Additional attributes are passed to `flatatt` and converted into key="value", pairs. These attributes are added to the ``<input>``. Examples -------- Note: ``form`` arg to ``render()`` is not required for ``BaseInput`` inherited objects. >>> button = Button('Button 1', 'Press Me!') >>> button.render("", "", Context()) '<input type="button" name="button-1" value="Press Me!" ' 'class="btn" id="button-id-button-1"/>' >>> button = Button('Button 1', 'Press Me!', css_id="custom-id", css_class="custom class", my_attr=True, data="my-data") >>> button.render("", "", Context()) '<input type="button" name="button-1" value="Press Me!" ' 'class="btn custom class" id="custom-id" data="my-data" my-attr/>' Usually you will not call the render method on the object directly. Instead add it to your ``Layout`` manually or use the `add_input` method:: class ExampleForm(forms.Form): [...] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.helper = FormHelper() self.helper.add_input(Button('Button 1', 'Press Me!')) """ input_type = "button" field_classes = "btn"
[docs]class Hidden(BaseInput): """ Used to create a Hidden input descriptor for the {% crispy %} template tag. Attributes ---------- template: str The default template which this Layout Object will be rendered with. field_classes: str CSS classes to be applied to the ``<input>``. input_type: str The ``type`` attribute of the ``<input>``. Parameters ---------- name : str The name attribute of the button. value : str The value attribute of the button. css_id : str, optional A custom DOM id for the layout object. If not provided the name argument is slugified and turned into the id for the submit button. By default None. css_class : str, optional Additional CSS classes to be applied to the ``<input>``. By default None. template : str, optional Overrides the default template, if provided. By default None. **kwargs : dict, optional Additional attributes are passed to `flatatt` and converted into key="value", pairs. These attributes are added to the ``<input>``. Examples -------- Note: ``form`` arg to ``render()`` is not required for ``BaseInput`` inherited objects. >>> hidden = Hidden("hidden", "hide-me") >>> hidden.render("", "", Context()) '<input type="hidden" name="hidden" value="hide-me"/>' Usually you will not call the render method on the object directly. Instead add it to your ``Layout`` manually or use the `add_input` method:: class ExampleForm(forms.Form): [...] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.helper = FormHelper() self.helper.add_input(Hidden("hidden", "hide-me")) """ input_type = "hidden" field_classes = "hidden"
[docs]class Reset(BaseInput): """ Used to create a reset button descriptor for the {% crispy %} template tag. Attributes ---------- template: str The default template which this Layout Object will be rendered with. field_classes: str CSS classes to be applied to the ``<input>``. input_type: str The ``type`` attribute of the ``<input>``. Parameters ---------- name : str The name attribute of the button. value : str The value attribute of the button. css_id : str, optional A custom DOM id for the layout object. If not provided the name argument is slugified and turned into the id for the submit button. By default None. css_class : str, optional Additional CSS classes to be applied to the ``<input>``. By default None. template : str, optional Overrides the default template, if provided. By default None. **kwargs : dict, optional Additional attributes are passed to `flatatt` and converted into key="value", pairs. These attributes are added to the ``<input>``. Examples -------- Note: ``form`` arg to ``render()`` is not required for ``BaseInput`` inherited objects. >>> reset = Reset('Reset This Form', 'Revert Me!') >>> reset.render("", "", Context()) '<input type="reset" name="reset-this-form" value="Revert Me!" ' 'class="btn btn-inverse" id="reset-id-reset-this-form"/>' >>> reset = Reset('Reset This Form', 'Revert Me!', css_id="custom-id", css_class="custom class", my_attr=True, data="my-data") >>> reset.render("", "", Context()) '<input type="reset" name="reset-this-form" value="Revert Me!" ' 'class="btn btn-inverse custom class" id="custom-id" data="my-data" my-attr/>' Usually you will not call the render method on the object directly. Instead add it to your ``Layout`` manually manually or use the `add_input` method:: class ExampleForm(forms.Form): [...] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.helper = FormHelper() self.helper.add_input(Reset('Reset This Form', 'Revert Me!')) """ input_type = "reset" field_classes = "btn btn-inverse"
[docs]class Fieldset(LayoutObject): """ A layout object which wraps fields in a ``<fieldset>`` Parameters ---------- legend : str The content of the fieldset's ``<legend>``. This text is context aware, to bring this to life see the examples section. *fields : str Any number of fields as positional arguments to be rendered within the ``<fieldset>`` css_class : str, optional Additional CSS classes to be applied to the ``<input>``. By default None. css_id : str, optional A custom DOM id for the layout object. If not provided the name argument is slugified and turned into the id for the submit button. By default None. template : str, optional Overrides the default template, if provided. By default None. **kwargs : dict, optional Additional attributes are passed to ``flatatt`` and converted into key="value", pairs. These attributes are added to the ``<fieldset>``. Examples -------- The Fieldset Layout object is added to your ``Layout`` for example:: Fieldset("Text for the legend", "form_field_1", "form_field_2", css_id="my-fieldset-id", css_class="my-fieldset-class", data="my-data" ) The above layout will be rendered as:: ''' <fieldset id="fieldset-id" class="my-fieldset-class" data="my-data"> <legend>Text for the legend</legend> # form fields render here </fieldset> ''' The first parameter is the text for the fieldset legend. This text is context aware, so you can do things like:: Fieldset("Data for {{ user.username }}", 'form_field_1', 'form_field_2' ) """ template = "%s/layout/fieldset.html" def __init__(self, legend, *fields, css_class=None, css_id=None, template=None, **kwargs): self.fields = list(fields) self.legend = legend self.css_class = css_class self.css_id = css_id self.template = template or self.template self.flat_attrs = flatatt(kwargs) def render(self, form, context, template_pack=TEMPLATE_PACK, **kwargs): fields = self.get_rendered_fields(form, context, template_pack, **kwargs) if self.legend: legend = Template(str(self.legend)).render(context) else: legend = SafeString("") template = self.get_template_name(template_pack) return render_to_string(template, {"fieldset": self, "legend": legend, "fields": fields})
[docs]class MultiField(LayoutObject): """ MultiField container for Bootstrap3. Renders to a MultiField <div>. Attributes ---------- template: str The default template which this Layout Object will be rendered with. field_template: str The template which fields will be rendered with. Parameters ---------- label: str The label for the multifield. *fields: str The fields to be rendered within the multifield. label_class: str, optional CSS classes to be added to the multifield label. By default None. help_text: str, optional Help text will be available in the context of the multifield template. This is unused in the bootstrap3 template provided. By default None. css_class : str, optional Additional CSS classes to be applied to the ``<input>``. By default None. css_id : str, optional A DOM id for the layout object which will be added to the wrapping ``<div>`` if provided. By default None. template : str, optional Overrides the default template, if provided. By default None. field_template : str, optional Overrides the default template, if provided. By default None. **kwargs : dict, optional Additional attributes are passed to ``flatatt`` and converted into key="value", pairs. These attributes are added to the wrapping ``<div>``. """ template = "%s/layout/multifield.html" field_template = "%s/multifield.html" def __init__( self, label, *fields, label_class=None, help_text=None, css_class=None, css_id=None, template=None, field_template=None, **kwargs, ): self.fields = list(fields) self.label_html = label self.label_class = label_class or "blockLabel" self.css_class = css_class or "ctrlHolder" self.css_id = css_id self.help_text = help_text self.template = template or self.template self.field_template = field_template or self.field_template self.flat_attrs = flatatt(kwargs) def render(self, form, context, template_pack=TEMPLATE_PACK, **kwargs): # If a field within MultiField contains errors if context["form_show_errors"]: for field in (pointer.name for pointer in self.get_field_names()): if field in form.errors: self.css_class += " error" field_template = self.field_template % template_pack fields_output = self.get_rendered_fields( form, context, template_pack, template=field_template, labelclass=self.label_class, layout_object=self, **kwargs, ) template = self.get_template_name(template_pack) context.update({"multifield": self, "fields_output": fields_output}) return render_to_string(template, context.flatten())
[docs]class Div(LayoutObject): """ Layout object. It wraps fields in a ``<div>``. Attributes ---------- template : str The default template which this Layout Object will be rendered with. css_class : str, optional CSS classes to be applied to the ``<div>``. By default None. Parameters ---------- *fields : str, LayoutObject Any number of fields as positional arguments to be rendered within the ``<div>``. css_id : str, optional A DOM id for the layout object which will be added to the ``<div>`` if provided. By default None. css_class : str, optional Additional CSS classes to be applied in addition to those declared by the class itself. By default None. template : str, optional Overrides the default template, if provided. By default None. **kwargs : dict, optional Additional attributes are passed to ``flatatt`` and converted into key="value", pairs. These attributes are added to the ``<div>``. Examples -------- In your ``Layout`` you can:: Div( 'form_field_1', 'form_field_2', css_id='div-example', css_class='divs', ) It is also possible to nest Layout Objects within a Div:: Div( Div( Field('form_field', css_class='field-class'), css_class='div-class', ), Div('form_field_2', css_class='div-class'), ) """ template = "%s/layout/div.html" css_class = None def __init__(self, *fields, css_id=None, css_class=None, template=None, **kwargs): self.fields = list(fields) if self.css_class and css_class: self.css_class += f" {css_class}" elif css_class: self.css_class = css_class self.css_id = css_id self.template = template or self.template self.flat_attrs = flatatt(kwargs) def render(self, form, context, template_pack=TEMPLATE_PACK, **kwargs): fields = self.get_rendered_fields(form, context, template_pack, **kwargs) template = self.get_template_name(template_pack) return render_to_string(template, {"div": self, "fields": fields})
[docs]class Row(Div): """ Layout object. It wraps fields in a ``<div>`` and the template adds the appropriate class to render the contents in a row. e.g. ``form-row`` when using the Bootstrap4 template pack. Attributes ---------- template : str The default template which this Layout Object will be rendered with. css_class : str, optional CSS classes to be applied to the ``<div>``. By default None. Parameters ---------- *fields : str, LayoutObject Any number of fields as positional arguments to be rendered within the ``<div>``. css_id : str, optional A DOM id for the layout object which will be added to the ``<div>`` if provided. By default None. css_class : str, optional Additional CSS classes to be applied in addition to those declared by the class itself. By default None. template : str, optional Overrides the default template, if provided. By default None. **kwargs : dict, optional Additional attributes are passed to ``flatatt`` and converted into key="value", pairs. These attributes are added to the ``<div>``. Examples -------- In your ``Layout`` you can:: Row('form_field_1', 'form_field_2', css_id='row-example') It is also possible to nest Layout Objects within a Row:: Row( Div( Field('form_field', css_class='field-class'), css_class='div-class', ), Div('form_field_2', css_class='div-class'), ) """ template = "%s/layout/row.html"
[docs]class Column(Div): """ Layout object. It wraps fields in a ``<div>`` and the template adds the appropriate class to render the contents in a column. e.g. ``col-md`` when using the Bootstrap4 template pack. Attributes ---------- template : str The default template which this Layout Object will be rendered with. css_class : str, optional CSS classes to be applied to the ``<div>``. By default None. Parameters ---------- *fields : str, LayoutObject Any number of fields as positional arguments to be rendered within the ``<div>``. css_id : str, optional A DOM id for the layout object which will be added to the ``<div>`` if provided. By default None. css_class : str, optional Additional CSS classes to be applied in addition to those declared by the class itself. If using the Bootstrap4 template pack the default ``col-md`` is removed if this string contins another ``col-`` class. By default None. template : str, optional Overrides the default template, if provided. By default None. **kwargs : dict, optional Additional attributes are passed to ``flatatt`` and converted into key="value", pairs. These attributes are added to the ``<div>``. Examples -------- In your ``Layout`` you can:: Column('form_field_1', 'form_field_2', css_id='col-example') It is also possible to nest Layout Objects within a Row:: Div( Column( Field('form_field', css_class='field-class'), css_class='col-sm, ), Column('form_field_2', css_class='col-sm'), ) """ template = "%s/layout/column.html"
[docs]class HTML: """ Layout object. It can contain pure HTML and it has access to the whole context of the page where the form is being rendered. Examples:: HTML("{% if saved %}Data saved{% endif %}") HTML('<input type="hidden" name="{{ step_field }}" value="{{ step0 }}" />') """ def __init__(self, html): self.html = html def render(self, form, context, template_pack=TEMPLATE_PACK, **kwargs): return Template(str(self.html)).render(context)
[docs]class Field(LayoutObject): """ A Layout object, usually containing one field name, where you can add attributes to it easily. Attributes ---------- template : str The default template which this Layout Object will be rendered with. attrs : dict Attributes to be applied to the field. These are converted into html attributes. e.g. ``data_id: 'test'`` in the attrs dict will become ``data-id='test'`` on the field's ``<input>``. Parameters ---------- *fields : str Usually a single field, but can be any number of fields, to be rendered with the same attributes applied. css_class : str, optional CSS classes to be applied to the field. These are added to any classes included in the ``attrs`` dict. By default ``None``. wrapper_class: str, optional CSS classes to be used when rendering the Field. This class is usually applied to the ``<div>`` which wraps the Field's ``<label>`` and ``<input>`` tags. By default ``None``. template : str, optional Overrides the default template, if provided. By default ``None``. **kwargs : dict, optional Additional attributes are converted into key="value", pairs. These attributes are added to the ``<div>``. Examples -------- Example:: Field('field_name', style="color: #333;", css_class="whatever", id="field_name") """ template = "%s/field.html" attrs = {} def __init__(self, *fields, css_class=None, wrapper_class=None, template=None, **kwargs): self.fields = list(fields) # Make sure shared state is not edited. self.attrs = self.attrs.copy() if css_class: if "class" in self.attrs: self.attrs["class"] += f" {css_class}" else: self.attrs["class"] = css_class self.wrapper_class = wrapper_class self.template = template or self.template # We use kwargs as HTML attributes, turning data_id='test' into data-id='test' self.attrs.update({k.replace("_", "-"): conditional_escape(v) for k, v in kwargs.items()}) def render(self, form, context, template_pack=TEMPLATE_PACK, extra_context=None, **kwargs): if extra_context is None: extra_context = {} if self.wrapper_class: extra_context["wrapper_class"] = self.wrapper_class template = self.get_template_name(template_pack) return self.get_rendered_fields( form, context, template_pack, template=template, attrs=self.attrs, extra_context=extra_context, **kwargs, )
[docs]class MultiWidgetField(Field): """ Layout object. For fields with :class:`~django.forms.MultiWidget` as ``widget``, you can pass additional attributes to each widget. Attributes ---------- template : str The default template which this Layout Object will be rendered with. Parameters ---------- *fields : str Usually a single field, but can be any number of fields, to be rendered with the same attributes applied. attrs : str, optional Additional attrs to be added to each widget. These are added to any classes included in the ``attrs`` dict. By default ``None``. wrapper_class: str, optional CSS classes to be used when rendering the Field. This class is usually applied to the ``<div>`` which wraps the Field's ``<label>`` and ``<input>`` tags. By default ``None``. template : str, optional Overrides the default template, if provided. By default ``None``. Examples -------- Example:: MultiWidgetField( 'multiwidget_field_name', attrs=( {'style': 'width: 30px;'}, {'class': 'second_widget_class'} ), ) """ def __init__(self, *fields, attrs=None, template=None, wrapper_class=None): self.fields = list(fields) self.attrs = attrs or {} self.template = template or self.template self.wrapper_class = wrapper_class