diff --git a/.idea/misc.xml b/.idea/misc.xml
index fe7f641..b603eb8 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -1,4 +1,4 @@
-
+
\ No newline at end of file
diff --git a/.idea/paper-forms.iml b/.idea/paper-forms.iml
index b2570cb..ae9e80f 100644
--- a/.idea/paper-forms.iml
+++ b/.idea/paper-forms.iml
@@ -22,7 +22,7 @@
-
+
diff --git a/.idea/runConfigurations/paper_forms.xml b/.idea/runConfigurations/paper_forms.xml
index 724c72b..1e10001 100644
--- a/.idea/runConfigurations/paper_forms.xml
+++ b/.idea/runConfigurations/paper_forms.xml
@@ -5,6 +5,7 @@
+
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 47a640e..207f5a0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,7 @@
### ⚠ BREAKING CHANGES
+- The library was completely rewritten.
- Dropped support for Python below 3.9.
- Dropped support for Django below 3.2.
- Add tests against Django 5.0.
diff --git a/README.md b/README.md
index e6421d1..3389ca0 100644
--- a/README.md
+++ b/README.md
@@ -157,14 +157,20 @@ fields have the highest priority.**
There is also the `template_names` attribute which allows you to
override a form field templates. Form field template context _is a
-widget context_, extended with `label`, `errors` and `help_text`
-values. You can add your own data by overriding the
-`build_widget_context` method in your Composer class.
+widget context_, extended with `label`, `errors`, `help_text` and
+`css_classes` values.
+
+You can also use specialized methods such as `get_widget()`,
+`get_template_name()`, `get_label()`, `get_help_text()`,
+`get_css_classes()`.
+
+You can add your own data by overriding the `build_context()`
+method in your Composer class.
Template example:
```html
-
+
@@ -208,17 +214,19 @@ is a template context variable. Parameters with a leading underscore, such as `_
are treated as template context variables and are not passed as widget attributes.
In addition to the template tag parameters, you can customize the context of the form
-field using the `build_widget_context` method in your `Composer` class. This method allows
+field using the `build_context()` method in your `Composer` class. This method allows
you to modify the context before it is used in the form field template.
-Here's an example `Composer` class with the `build_widget_context` method:
+Here's an example `Composer` class with the `build_context()` method:
```python
from paper_forms.composers.base import BaseComposer
class MyComposer(BaseComposer):
- def build_widget_context(self, widget, context):
+ def build_context(self, name, context, widget):
+ context = super().build_context(name, context, widget)
+
# Add a new variable to the context of all form fields
context["style"] = "light"
return context
@@ -239,9 +247,10 @@ class ExampleForm(forms.Form):
pass
```
-**Special cases**: The `label` and `help_text` parameters are treated as special cases.
-They are also considered as context variables and are not passed as widget attributes.
-For example:
+**Special cases**: The `label`, `help_text` and `css_classes` parameters are treated
+as special cases. They are interpreted as context variables, even though their names
+do not start with an underscore. For example:
+
```html
{% load paper_forms %}
@@ -261,7 +270,7 @@ from paper_forms.composers.base import BaseComposer
class Bootstrap4(BaseComposer):
- def get_default_template_name(self, widget):
+ def get_default_template_name(self, name, widget):
# Overrides the widget template, but has a lower priority
# than the 'template_names' attribute of the Composer class.
if isinstance(widget, widgets.CheckboxInput):
@@ -269,13 +278,19 @@ class Bootstrap4(BaseComposer):
else:
return "paper_forms/bootstrap4/input.html"
- def get_default_css_classes(self, widget):
+ def build_widget_attrs(self, name, attrs, widget):
+ attrs = super().build_widget_attrs(name, attrs, widget)
+ classes = set(attrs.pop("class", "").split())
+
# Adds default CSS classes that can be overridden
# in the {% field %} template tag.
if isinstance(widget, widgets.CheckboxInput):
- return "form-check-input"
+ classes.add("form-check-input")
else:
- return "form-control"
+ classes.add("form-control")
+
+ attrs["class"] = " ".join(classes)
+ return attrs
```
## Settings
diff --git a/paper_forms/boundfield.py b/paper_forms/boundfield.py
index 29fb60e..6ba5396 100644
--- a/paper_forms/boundfield.py
+++ b/paper_forms/boundfield.py
@@ -1,102 +1,110 @@
-import copy
import datetime
+from typing import Any
import django
from django.forms.boundfield import BoundField as _BoundField
from django.forms.boundfield import BoundWidget
+from django.forms.widgets import Widget
from django.utils.functional import cached_property
+from .composers.base import BaseComposer
+
class BoundField(_BoundField):
def __init__(self, form, field, name, composer):
super().__init__(form, field, name)
- self.composer = composer
-
- def get_template_name(self, widget):
- if widget.is_hidden:
- # default template
- return widget.template_name
-
- composer_template_names = self.composer.template_names
- if composer_template_names and self.name in composer_template_names:
- return composer_template_names[self.name]
-
- return self.composer.get_default_template_name(widget)
-
- def get_widget_attrs(self, widget, attrs=None):
- attrs = attrs or {}
- attrs = self.build_widget_attrs(attrs, widget)
-
- composer_css_classes = attrs.pop("class", self.composer.get_default_css_classes(widget))
- composer_css_classes = self.css_classes(composer_css_classes)
- if composer_css_classes:
- attrs["class"] = composer_css_classes
-
- return self.composer.build_widget_attrs(widget, attrs)
-
- def get_context(self, widget, attrs=None, extra_context=None, only_initial=False):
- context = widget.get_context(
- name=self.html_initial_name if only_initial else self.html_name,
- value=self.value(),
- attrs=attrs
- )
-
- context["errors"] = self.errors
- context.update(extra_context or {})
-
- if "label" not in context:
- composer_labels = self.composer.labels
- if composer_labels and self.name in composer_labels:
- context["label"] = composer_labels[self.name]
- else:
- context["label"] = self.label
-
- if "help_text" not in context:
- composer_help_texts = self.composer.help_texts
- if composer_help_texts and self.name in composer_help_texts:
- context["help_text"] = composer_help_texts[self.name]
- else:
- context["help_text"] = self.help_text
-
- return self.composer.build_widget_context(widget, context)
-
- def as_widget(self, widget=None, attrs=None, context=None, only_initial=False):
+ self.composer = composer # type: BaseComposer
+
+ def as_widget(
+ self,
+ widget: Widget = None,
+ attrs: dict = None,
+ only_initial: bool = False,
+ extra_context: dict = None
+ ):
widget = widget or self.widget
if self.field.localize:
widget.is_localized = True
- attrs = self.get_widget_attrs(widget, attrs)
+ attrs = attrs or {}
+ attrs = self.build_widget_attrs(widget, attrs)
if self.auto_id and "id" not in widget.attrs:
attrs.setdefault("id", self.html_initial_id if only_initial else self.auto_id)
+ if django.VERSION >= (4, 2) and only_initial and self.html_initial_name in self.form.data:
+ # Propagate the hidden initial value.
+ value = self.form._widget_data_value(
+ self.field.hidden_widget(),
+ self.html_initial_name,
+ )
+ else:
+ value = self.value()
+
context = self.get_context(
widget,
+ name=self.html_initial_name if only_initial else self.html_name,
+ value=value,
attrs=attrs,
- extra_context=context,
- only_initial=only_initial
+ extra_context=extra_context,
)
return widget._render(
- template_name=self.get_template_name(widget),
- context=context,
+ template_name=self.composer.get_template_name(self.name, widget),
+ context=self.composer.build_context(self.name, context, widget),
renderer=self.composer.get_renderer(self.form),
)
+ def build_widget_attrs(self, widget: Widget, attrs: dict = None) -> dict:
+ attrs = attrs or {}
+ attrs = super().build_widget_attrs(attrs, widget)
+ return self.composer.build_widget_attrs(self.name, attrs, widget)
+
+ def get_context(
+ self,
+ widget: Widget,
+ name: str,
+ value: Any,
+ attrs: dict = None,
+ extra_context: dict = None
+ ) -> dict:
+ widget_context = widget.get_context(
+ name=name,
+ value=value,
+ attrs=attrs
+ )
+ extra_context = extra_context or {}
+ context = dict(widget_context, **extra_context)
+
+ if "label" not in context:
+ label = self.composer.get_label(self.name, widget)
+ if label is None:
+ label = self.label
+ context["label"] = label
+
+ if "help_text" not in context:
+ help_text = self.composer.get_help_text(self.name, widget)
+ if help_text is None:
+ help_text = self.help_text
+ context["help_text"] = help_text
+
+ if "css_classes" not in context:
+ extra_css_classes = self.composer.get_css_classes(self.name, widget)
+ context["css_classes"] = self.css_classes(extra_css_classes)
+
+ context["errors"] = self.errors
+
+ return context
+
@cached_property
- def widget(self):
- composer_widgets = self.composer.widgets
- if composer_widgets and self.name in composer_widgets:
- widget = composer_widgets[self.name]
- if isinstance(widget, type):
- widget = widget()
- else:
- widget = copy.deepcopy(widget)
- return widget
-
- return self.field.widget
+ def widget(self) -> Widget:
+ widget = self.composer.get_widget(self.name)
+ if widget is None:
+ widget = self.field.widget
+
+ return widget
@cached_property
- def subwidgets(self):
+ def subwidgets(self) -> list[BoundWidget]:
# Use self.widget instead of self.field.widget
id_ = self.widget.attrs.get("id") or self.auto_id
attrs = {"id": id_} if id_ else {}
@@ -109,7 +117,7 @@ def subwidgets(self):
]
@property
- def data(self):
+ def data(self) -> Any:
# Use self.widget instead of self.field.widget
if django.VERSION >= (4, 0):
return self.form._widget_data_value(self.widget, self.html_name)
@@ -119,24 +127,26 @@ def data(self):
)
@property
- def is_hidden(self):
+ def is_hidden(self) -> bool:
# Use self.widget instead of self.field.widget
return self.widget.is_hidden
@property
- def id_for_label(self):
+ def id_for_label(self) -> str:
# Use self.widget instead of self.field.widget
id_ = self.widget.attrs.get("id") or self.auto_id
return self.widget.id_for_label(id_)
@cached_property
- def initial(self):
+ def initial(self) -> Any:
# Use self.widget instead of self.field.widget
- data = self.form.get_initial_for_field(self.field, self.name)
- if django.VERSION < (4, 0):
+ if django.VERSION >= (4, 0):
+ return self.form.get_initial_for_field(self.field, self.name)
+ else:
+ data = self.form.get_initial_for_field(self.field, self.name)
# If this is an auto-generated default date, nix the microseconds for
# standardized handling. See #22502.
if (isinstance(data, (datetime.datetime, datetime.time)) and
not self.widget.supports_microseconds):
data = data.replace(microsecond=0)
- return data
+ return data
diff --git a/paper_forms/composers/base.py b/paper_forms/composers/base.py
index a7d309a..3fe8d53 100644
--- a/paper_forms/composers/base.py
+++ b/paper_forms/composers/base.py
@@ -1,12 +1,16 @@
-from typing import Any, Dict
+import copy
+from typing import Any, Optional
+from django.forms import BaseForm
+from django.forms.renderers import BaseRenderer
+from django.forms.widgets import Widget
from django.utils.module_loading import import_string
from .. import conf
class SingletonMeta(type):
- _instances = {} # type: Dict[Any, 'BaseComposer']
+ _instances: dict[Any, "BaseComposer"] = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
@@ -17,12 +21,13 @@ def __call__(cls, *args, **kwargs):
class BaseComposer(metaclass=SingletonMeta):
renderer = None
- widgets = None # type: Dict[str, Any]
- labels = None # type: Dict[str, Any]
- help_texts = None # type: Dict[str, Any]
- template_names = None # type: Dict[str, Any]
+ widgets: dict[str, Any] = None
+ labels: dict[str, str] = None
+ help_texts: dict[str, str] = None
+ css_classes: dict[str, str] = None
+ template_names: dict[str, str] = None
- def get_renderer(self, form):
+ def get_renderer(self, form: BaseForm) -> BaseRenderer:
renderer = self.renderer or form.default_renderer or conf.DEFAULT_FORM_RENDERER
if isinstance(renderer, str):
renderer_class = import_string(renderer)
@@ -31,24 +36,42 @@ def get_renderer(self, form):
return renderer()
return renderer
- def get_default_template_name(self, widget):
- """
- Overrides the widget template, but has a lower priority
- than the 'template_names' attribute of the Composer class.
- """
+ def get_widget(self, name: str) -> Widget:
+ if self.widgets and name in self.widgets:
+ widget = self.widgets[name]
+ if isinstance(widget, type):
+ return widget()
+ else:
+ return copy.deepcopy(widget)
+
+ def get_template_name(self, name: str, widget: Widget) -> str:
+ if widget.is_hidden:
+ return widget.template_name
+
+ if self.template_names and name in self.template_names:
+ return self.template_names[name]
+
+ return self.get_default_template_name(name, widget)
+
+ def get_default_template_name(self, name: str, widget: Widget) -> str:
return widget.template_name
- def get_default_css_classes(self, widget):
- """
- Adds default CSS classes that can be overridden
- in the {% field %} template tag.
- """
- return ""
+ def get_label(self, name: str, widget: Widget) -> Optional[str]:
+ if self.labels and name in self.labels:
+ return self.labels[name]
+
+ def get_help_text(self, name: str, widget: Widget) -> Optional[str]:
+ if self.help_texts and name in self.help_texts:
+ return self.help_texts[name]
+
+ def get_css_classes(self, name: str, widget: Widget) -> Optional[str]:
+ if self.css_classes and name in self.css_classes:
+ return self.css_classes[name]
- def build_widget_attrs(self, widget, attrs):
+ def build_widget_attrs(self, name: str, attrs: dict, widget: Widget) -> dict:
# Here you can edit the attributes before they get into the widget
- return attrs
+ return attrs or {}
- def build_widget_context(self, widget, context):
+ def build_context(self, name: str, context: dict, widget: Widget) -> dict:
# Here you can edit the context of the form field
return context
diff --git a/paper_forms/composers/bootstrap4.py b/paper_forms/composers/bootstrap4.py
index 3b0e13e..b8fc673 100644
--- a/paper_forms/composers/bootstrap4.py
+++ b/paper_forms/composers/bootstrap4.py
@@ -1,10 +1,10 @@
-from django.forms import widgets
+from django.forms import Widget, widgets
from .base import BaseComposer
class Bootstrap4(BaseComposer):
- def get_default_template_name(self, widget):
+ def get_default_template_name(self, name: str, widget: Widget) -> str:
if isinstance(widget, widgets.CheckboxInput):
return "paper_forms/bootstrap4/checkbox.html"
elif isinstance(widget, widgets.CheckboxSelectMultiple):
@@ -16,16 +16,22 @@ def get_default_template_name(self, widget):
else:
return "paper_forms/bootstrap4/input.html"
- def get_default_css_classes(self, widget):
+ def build_widget_attrs(self, name: str, attrs: dict, widget: Widget) -> dict:
+ attrs = super().build_widget_attrs(name, attrs, widget)
+ classes = set(attrs.pop("class", "").split())
+
if isinstance(widget, widgets.CheckboxInput):
- return "form-check-input"
+ classes.add("form-check-input")
elif isinstance(widget, widgets.CheckboxSelectMultiple):
- return "form-check-input"
+ classes.add("form-check-input")
elif isinstance(widget, widgets.RadioSelect):
- return "form-check-input"
+ classes.add("form-check-input")
elif isinstance(widget, widgets.Select):
- return "custom-select"
+ classes.add("custom-select")
elif isinstance(widget, widgets.FileInput):
- return "custom-file-input"
+ classes.add("custom-file-input")
else:
- return "form-control"
+ classes.add("form-control")
+
+ attrs["class"] = " ".join(classes)
+ return attrs
diff --git a/paper_forms/templates/paper_forms/bootstrap4/checkbox.html b/paper_forms/templates/paper_forms/bootstrap4/checkbox.html
index c736f20..a3a9f3b 100644
--- a/paper_forms/templates/paper_forms/bootstrap4/checkbox.html
+++ b/paper_forms/templates/paper_forms/bootstrap4/checkbox.html
@@ -1,4 +1,4 @@
-
+
{% include widget.template_name %}
diff --git a/paper_forms/templates/paper_forms/bootstrap4/checkbox_select.html b/paper_forms/templates/paper_forms/bootstrap4/checkbox_select.html
index ac5f143..57f5348 100644
--- a/paper_forms/templates/paper_forms/bootstrap4/checkbox_select.html
+++ b/paper_forms/templates/paper_forms/bootstrap4/checkbox_select.html
@@ -1,4 +1,4 @@
-
+
{% with id=widget.attrs.id %}
diff --git a/paper_forms/templates/paper_forms/bootstrap4/custom_checkbox.html b/paper_forms/templates/paper_forms/bootstrap4/custom_checkbox.html
index 7871fb6..2e771f3 100644
--- a/paper_forms/templates/paper_forms/bootstrap4/custom_checkbox.html
+++ b/paper_forms/templates/paper_forms/bootstrap4/custom_checkbox.html
@@ -1,4 +1,4 @@
-
+
{% include widget.template_name %}
diff --git a/paper_forms/templates/paper_forms/bootstrap4/custom_checkbox_select.html b/paper_forms/templates/paper_forms/bootstrap4/custom_checkbox_select.html
index 1ef37e5..cd29b1c 100644
--- a/paper_forms/templates/paper_forms/bootstrap4/custom_checkbox_select.html
+++ b/paper_forms/templates/paper_forms/bootstrap4/custom_checkbox_select.html
@@ -1,4 +1,4 @@
-
+
{% with id=widget.attrs.id %}
diff --git a/paper_forms/templates/paper_forms/bootstrap4/custom_radio_select.html b/paper_forms/templates/paper_forms/bootstrap4/custom_radio_select.html
index 48ebbe7..843251e 100644
--- a/paper_forms/templates/paper_forms/bootstrap4/custom_radio_select.html
+++ b/paper_forms/templates/paper_forms/bootstrap4/custom_radio_select.html
@@ -1,4 +1,4 @@
-
+
{% with id=widget.attrs.id %}
diff --git a/paper_forms/templates/paper_forms/bootstrap4/file.html b/paper_forms/templates/paper_forms/bootstrap4/file.html
index 576971a..9716215 100644
--- a/paper_forms/templates/paper_forms/bootstrap4/file.html
+++ b/paper_forms/templates/paper_forms/bootstrap4/file.html
@@ -1,4 +1,4 @@
-
+
{% include widget.template_name %}
diff --git a/paper_forms/templates/paper_forms/bootstrap4/input.html b/paper_forms/templates/paper_forms/bootstrap4/input.html
index b9ec1f8..5084593 100644
--- a/paper_forms/templates/paper_forms/bootstrap4/input.html
+++ b/paper_forms/templates/paper_forms/bootstrap4/input.html
@@ -1,4 +1,4 @@
-
+
{% include widget.template_name %}
diff --git a/paper_forms/templates/paper_forms/bootstrap4/radio_select.html b/paper_forms/templates/paper_forms/bootstrap4/radio_select.html
index ac5f143..57f5348 100644
--- a/paper_forms/templates/paper_forms/bootstrap4/radio_select.html
+++ b/paper_forms/templates/paper_forms/bootstrap4/radio_select.html
@@ -1,4 +1,4 @@
-