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
@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)
)
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(''),
'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)
class ButtonHolder(LayoutObject):
"""
Layout object. It wraps fields in a
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 ````. 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(Information Saved),
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())
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 ````.
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 ````. 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 ````.
"""
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)
def render(self, form, context, template_pack=TEMPLATE_PACK, **kwargs):
"""
Renders an `` 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())
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_type: str
The ``type`` attribute of the ````.
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 ````. 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 ````.
Examples
--------
Note: ``form`` arg to ``render()`` is not required for ``BaseInput``
inherited objects.
>>> submit = Submit('Search the Site', 'search this site')
>>> submit.render("", "", Context())
''
>>> 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())
''
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"
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_type: str
The ``type`` attribute of the ````.
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 ````. 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 ````.
Examples
--------
Note: ``form`` arg to ``render()`` is not required for ``BaseInput``
inherited objects.
>>> button = Button('Button 1', 'Press Me!')
>>> button.render("", "", Context())
''
>>> button = Button('Button 1', 'Press Me!', css_id="custom-id",
css_class="custom class", my_attr=True, data="my-data")
>>> button.render("", "", Context())
''
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"
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_type: str
The ``type`` attribute of the ````.
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 ````. 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 ````.
Examples
--------
Note: ``form`` arg to ``render()`` is not required for ``BaseInput``
inherited objects.
>>> hidden = Hidden("hidden", "hide-me")
>>> hidden.render("", "", Context())
''
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"
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_type: str
The ``type`` attribute of the ````.
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 ````. 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 ````.
Examples
--------
Note: ``form`` arg to ``render()`` is not required for ``BaseInput``
inherited objects.
>>> reset = Reset('Reset This Form', 'Revert Me!')
>>> reset.render("", "", Context())
''
>>> reset = Reset('Reset This Form', 'Revert Me!', css_id="custom-id",
css_class="custom class", my_attr=True, data="my-data")
>>> reset.render("", "", Context())
''
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"
class Fieldset(LayoutObject):
"""
A layout object which wraps fields in a ``