Skip to content

Commit

Permalink
Document rich radio button feature
Browse files Browse the repository at this point in the history
  • Loading branch information
christophehenry committed Sep 18, 2024
1 parent 7b38759 commit d449c17
Show file tree
Hide file tree
Showing 5 changed files with 282 additions and 1 deletion.
211 changes: 211 additions & 0 deletions dsfr/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,12 +170,223 @@ def additional_attributes(cls):


class ExtendedChoices(models.Choices, metaclass=_ExtendedChoicesType):
"""
Extension of [Django's `models.Choices`][1] to support adding arbitrary attributes
to enums using a disctionnary.
Exemple usage:
```python
>>> class TemperatureChoices(ExtendedChoices):
... COLD = {
... "value": "COLD",
... "label": "Cold",
... "temperature": "<12°c"
... }
... OK = {
... "value": "OK",
... "label": "ok",
... "temperature": ">=12°c,<25°c"
... }
... HOT = {
... "value": "HOT",
... "label": "Hot!",
... "temperature": ">=25°c"
... }
```
In the previous example, in addition to `TemperatureChoices.<enum instance>.value`,
`TemperatureChoices.<enum instance>.label` and `TemperatureChoices.<enum instance>.name`,
`ExtendedChoices` will make an additionnal `temperature` attribute available to each
enum instance. Exemple: `TemperatureChoices.<enum instance>.temperature`.
Note that `"value"` is the only mandatory key in the dictionnary. When a dictionnary
contains only `"value"`, the folling are equivalent:
```python
>>> from django.db import models
>>> class TemperatureChoices(models.Choices):
... COLD = "COLD"
>>> class TemperatureChoices(ExtendedChoices):
... COLD = {"value": "COLD"}
```
See [this section on how to provide default values for additionnal attributes](#default-values)
## Usage with `enum.auto()`
`ExtendedChoices` supports the usage of [`enum.auto()`][2]:
>>> from enum import auto
>>> from django.db import models
>>> class TemperatureChoices(ExtendedChoices, models.TextChoices):
... COLD = {
... "value": auto(),
... "label": "Cold",
... "temperature": "<12°c"
... }
... OK = {
... "value": auto(),
... "label": "ok",
... "temperature": ">=12°c,<25°c"
... }
... HOT = {
... "value": auto(),
... "label": "Hot!",
... "temperature": ">=25°c"
... }
`ExtendedChoices` can be used with `django.db.models.TextChoices` and
`django.db.models.IntegerChoices` to automatically compute the value with
`enum.auto()` of can extend `_generate_next_value_` to provide a value
(see [[2]][2]):
```python
>>> class TemperatureChoices(ExtendedChoices):
... COLD = {
... "value": auto(),
... "label": "Cold",
... "temperature": "<12°c"
... }
... OK = {
... "value": auto(),
... "label": "ok",
... "temperature": ">=12°c,<25°c"
... }
... HOT = {
... "value": auto(),
... "label": "Hot!",
... "temperature": ">=25°c"
... }
...
... def _generate_next_value_(name, start, count, last_values):
... return f"{name}: {count}"
```
## Provide default values to additionnal attributes {: #default-values}
Sometimes, you may want to dynamically provide values for an additionnal attribute.
In can you don't specify a value for an additionnal attribute during the enum
declaration,
```python
>>> from django.conf import settings
>>> from django.db import models
>>> class TemperatureChoices(ExtendedChoices, models.IntegerChoices):
... COLD = {
... "value": auto(),
... "temperature": {"lorem": "ipsum 1"},
... }
... OK = auto()
...
... def dynamic_attribute_value(self, name):
... if name == "temperature":
... return settings.TEMPERATURES[self.value]
... else:
... return -1
```
In the previous example, the value for `temperature` is not specified for
`TemperatureChoices.OK`. Calling `TemperatureChoices.OK.temperature` will call
TemperatureChoices.OK.dynamic_attribute_value("temperature").
## Advanced usage
By default, when you specify additionnal values, `ExtendedChoices` will store
this value in an instance member with that name starting with `__`. Example:
>>> class TemperatureChoices(ExtendedChoices):
... COLD = {
... "value": auto(),
... "label": "Cold",
... "temperature": "<12°c"
... }
... OK = {
... "value": auto(),
... "label": "ok",
... "temperature": ">=12°c,<25°c"
... }
... HOT = {
... "value": auto(),
... "label": "Hot!",
... "temperature": ">=25°c"
... }
>>> TemperatureChoices.OK.__temperature == TemperatureChoices.OK.temperature
```
If, for reasons, you want to use another member name, you can declare a static
`private_variable_name` method:
```python
>>> class TemperatureChoices(ExtendedChoices):
... COLD = {
... "value": auto(),
... "label": "Cold",
... "temperature": "<12°c"
... }
... OK = {
... "value": auto(),
... "label": "ok",
... "temperature": ">=12°c,<25°c"
... }
... HOT = {
... "value": auto(),
... "label": "Hot!",
... "temperature": ">=25°c"
... }
...
... @staticmethod
... def private_variable_name(name):
... return f"m_{name}"
>>> TemperatureChoices.OK.m_temperature == TemperatureChoices.OK.temperature
```
[1]: https://docs.djangoproject.com/en/5.1/ref/models/fields/#enumeration-types
[2]: https://docs.python.org/3/library/enum.html#enum.auto
"""

@staticmethod
def private_variable_name(name):
return f"__{name}"


class RichRadioButtonChoices(ExtendedChoices):
"""
Sepcializes version of `RichRadioButtonChoices` to use with
`dsfr.widgets.RichRadioSelect`:
```python
>>> from enum import auto
>>> from django.db.models import IntegerChoices
>>> from dsfr.utils import lazy_static
>>> class ExampleRichChoices(IntegerChoices, RichRadioButtonChoices):
... ITEM_1 = {
... "value": auto(),
... "label": "Item 1",
... "html_label": "<strong>Item 1</strong>",
... "pictogram": lazy_static("img/placeholder.1x1.png"),
... }
... ITEM_2 = {
... "value": auto(),
... "label": "Item 2",
... "html_label": "<strong>Item 2</strong>",
... "pictogram": lazy_static("img/placeholder.1x1.png"),
... }
... ITEM_3 = {
... "value": auto(),
... "label": "Item 3",
... "html_label": "<strong>Item 3</strong>",
... "pictogram": lazy_static("img/placeholder.1x1.png"),
... }
```
See `dsfr.widgets.RichRadioSelect` for more details.
"""

@enum_property
def pictogram(self):
return getattr(self, self.private_variable_name("pictogram"), "")
Expand Down
3 changes: 2 additions & 1 deletion dsfr/templates/dsfr/widgets/rich_radio_option.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
<input value="{{ widget.value }}"
type="{{ widget.type }}"
id="{{ widget.attrs.id }}"
name="{{ widget.name }}">
name="{{ widget.name }}"
{% include "django/forms/widgets/attrs.html" %}>
<label class="fr-label" for="{{ widget.attrs.id }}">
{{ widget.html_label }}
</label>
Expand Down
5 changes: 5 additions & 0 deletions dsfr/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,4 +134,9 @@ def dsfr_input_class_attr(bf: BoundField):


def lazy_static(path):
"""
Programmatic equivalent to Django's {% static %} to use within code
exemple:
>>> lazy_static("img/logo.png")
"""
return keep_lazy_text(functools.partial(static, path))
63 changes: 63 additions & 0 deletions dsfr/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,5 +57,68 @@ def create_option(


class RichRadioSelect(_RichChoiceWidget, RadioSelect):
"""
Widget for producing riche radio buttons. This widget works with
`dsfr.enums.RichRadioButtonChoices`.
`RichRadioSelect.__init__` take a mandatory `rich_choices` argument of type
`RichRadioButtonChoices`.
Usage:
```python
>>> from enum import auto
>>> from django.db.models import IntegerChoices
>>> from django import forms
>>> from dsfr.forms import DsfrBaseForm
>>> from dsfr.utils import lazy_static
>>> class ExampleRichChoices(RichRadioButtonChoices, IntegerChoices):
... ITEM_1 = {
... "value": auto(),
... "label": "Item 1",
... "html_label": "<strong>Item 1</strong>",
... "pictogram": lazy_static("img/placeholder.1x1.png"),
... }
... ITEM_2 = {
... "value": auto(),
... "label": "Item 2",
... "html_label": "<strong>Item 2</strong>",
... "pictogram": lazy_static("img/placeholder.1x1.png"),
... }
... ITEM_3 = {
... "value": auto(),
... "label": "Item 3",
... "html_label": "<strong>Item 3</strong>",
... "pictogram": lazy_static("img/placeholder.1x1.png"),
... }
>>> class ExampleForm(DsfrBaseForm):
... sample_rich_radio = forms.ChoiceField(
... label="Cases à cocher",
... required=False,
... choices=ExampleRichChoices.choices,
... help_text="Exemple de boutons radios riches",
... widget=RichRadioSelect(rich_choices=ExampleRichChoices),
... )
```
## `html_label`
The `html_label` instance member can be used to put HTML code inside `<label>`. It
is automatically marked as HTML-safe with [`django.utils.safestring.mark_safe`][1]
so it will cause no [HTML escaping problem][2] in your template.
If not specified, the `label` attribute is used.
### `pictogram`
The `pictogram` attribute can be used to specify the riche radio button pictogram.
It can be used in combination with `dsfr.utils.lazy_static` to load a static asset.
[1]: https://docs.djangoproject.com/en/5.1/ref/utils/#django.utils.safestring.mark_safe
[2]: https://docs.djangoproject.com/en/5.1/ref/templates/language/#automatic-html-escaping
"""

template_name = "dsfr/widgets/rich_radio.html"
option_template_name = "dsfr/widgets/rich_radio_option.html"
1 change: 1 addition & 0 deletions example_app/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ def format_markdown_from_file(filename: str, ignore_first_line: bool = False) ->
md = markdown.Markdown(
extensions=[
"markdown.extensions.fenced_code",
"markdown.extensions.attr_list",
TocExtension(toc_depth="2-6"),
CodeHiliteExtension(css_class="dsfr-code"),
],
Expand Down

0 comments on commit d449c17

Please sign in to comment.