Skip to content

Commit

Permalink
rewrite lib
Browse files Browse the repository at this point in the history
  • Loading branch information
pix666 committed Jan 1, 2024
1 parent 403bfb6 commit 9b55924
Show file tree
Hide file tree
Showing 25 changed files with 302 additions and 189 deletions.
2 changes: 1 addition & 1 deletion .idea/misc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion .idea/paper-forms.iml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions .idea/runConfigurations/paper_forms.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
43 changes: 29 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
<div class="form-field">
<div class="form-field {{ css_classes }}">
<label for="{{ widget.attrs.id }}">{{ label }}</label>

<!-- include default widget template -->
Expand Down Expand Up @@ -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
Expand All @@ -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 %}

Expand All @@ -261,21 +270,27 @@ 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):
return "paper_forms/bootstrap4/checkbox.html"
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
Expand Down
162 changes: 86 additions & 76 deletions paper_forms/boundfield.py
Original file line number Diff line number Diff line change
@@ -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 {}
Expand All @@ -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)
Expand All @@ -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
Loading

0 comments on commit 9b55924

Please sign in to comment.