adding new form
Gitea Actions Demo / Explore-Gitea-Actions (push) Has been cancelled

This commit is contained in:
Pierre Geeraert
2025-07-29 09:39:41 +01:00
parent 2366d0c2a8
commit bccd344aa7
9310 changed files with 1330402 additions and 1 deletions
@@ -0,0 +1,13 @@
__all__ = [
"__version__",
]
PACKAGE_NAME = "django-bootstrap-v5"
try:
from importlib.metadata import metadata
except ImportError:
from importlib_metadata import metadata
package_metadata = metadata(PACKAGE_NAME)
__version__ = package_metadata["Version"]
@@ -0,0 +1,88 @@
from importlib import import_module
from django.conf import settings
BOOTSTRAP5_DEFAULTS = {
"css_url": {
"href": "https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css",
"integrity": "sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3",
"crossorigin": "anonymous",
},
"javascript_url": {
"url": "https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js",
"integrity": "sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p",
"crossorigin": "anonymous",
},
"theme_url": None,
"javascript_in_head": False,
"use_i18n": False,
"horizontal_label_class": "col-md-3",
"horizontal_field_class": "col-md-9",
"set_placeholder": True,
"required_css_class": "",
"error_css_class": "is-invalid",
"success_css_class": "is-valid",
"formset_renderers": {"default": "bootstrap5.renderers.FormsetRenderer"},
"form_renderers": {"default": "bootstrap5.renderers.FormRenderer"},
"field_renderers": {
"default": "bootstrap5.renderers.FieldRenderer",
"inline": "bootstrap5.renderers.InlineFieldRenderer",
"horizontal": "bootstrap5.renderers.HorizontalFieldRenderer",
},
}
def get_bootstrap_setting(name, default=None):
"""Read a setting."""
# Start with a copy of default settings
BOOTSTRAP5 = BOOTSTRAP5_DEFAULTS.copy()
# Override with user settings from settings.py
BOOTSTRAP5.update(getattr(settings, "BOOTSTRAP5", {}))
# Update use_i18n
BOOTSTRAP5["use_i18n"] = i18n_enabled()
return BOOTSTRAP5.get(name, default)
def javascript_url():
"""Return the full url to the Bootstrap JavaScript file."""
return get_bootstrap_setting("javascript_url")
def css_url():
"""Return the full url to the Bootstrap CSS file."""
return get_bootstrap_setting("css_url")
def theme_url():
"""Return the full url to the theme CSS file."""
return get_bootstrap_setting("theme_url")
def i18n_enabled():
"""Return the projects i18n setting."""
return getattr(settings, "USE_I18N", False)
def get_renderer(renderers, **kwargs):
layout = kwargs.get("layout", "")
path = renderers.get(layout, renderers["default"])
mod, cls = path.rsplit(".", 1)
return getattr(import_module(mod), cls)
def get_formset_renderer(**kwargs):
renderers = get_bootstrap_setting("formset_renderers")
return get_renderer(renderers, **kwargs)
def get_form_renderer(**kwargs):
renderers = get_bootstrap_setting("form_renderers")
return get_renderer(renderers, **kwargs)
def get_field_renderer(**kwargs):
renderers = get_bootstrap_setting("field_renderers")
return get_renderer(renderers, **kwargs)
@@ -0,0 +1,28 @@
from django.utils.safestring import mark_safe
from django.utils.translation import gettext as _
from bootstrap5.utils import render_tag
from .text import text_value
def render_alert(content, alert_type=None, dismissible=True):
"""Render a Bootstrap alert."""
button = ""
if not alert_type:
alert_type = "info"
css_classes = ["alert", "alert-" + text_value(alert_type)]
if dismissible:
css_classes.append("alert-dismissible")
close = _("close")
button = (
'<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="{close}"></button>'
).format(close=close)
button_placeholder = "__BUTTON__"
return mark_safe(
render_tag(
"div",
attrs={"class": " ".join(css_classes), "role": "alert"},
content=mark_safe(button_placeholder) + text_value(content),
).replace(button_placeholder, button)
)
@@ -0,0 +1,6 @@
class BootstrapException(Exception):
"""Any exception from this package."""
class BootstrapError(BootstrapException):
"""Any exception that is an error."""
@@ -0,0 +1,147 @@
from django.forms import EmailInput, NumberInput, PasswordInput, Textarea, TextInput, URLInput
from django.utils.safestring import mark_safe
from .bootstrap import get_bootstrap_setting, get_field_renderer, get_form_renderer, get_formset_renderer
from .exceptions import BootstrapError
from .text import text_value
from .utils import add_css_class, render_tag
FORM_GROUP_CLASS = "mb-3"
def render_formset(formset, **kwargs):
"""Render a formset to a Bootstrap layout."""
renderer_cls = get_formset_renderer(**kwargs)
return renderer_cls(formset, **kwargs).render()
def render_formset_errors(formset, **kwargs):
"""Render formset errors to a Bootstrap layout."""
renderer_cls = get_formset_renderer(**kwargs)
return renderer_cls(formset, **kwargs).render_errors()
def render_form(form, **kwargs):
"""Render a form to a Bootstrap layout."""
renderer_cls = get_form_renderer(**kwargs)
return renderer_cls(form, **kwargs).render()
def render_form_errors(form, type="all", **kwargs):
"""Render form errors to a Bootstrap layout."""
renderer_cls = get_form_renderer(**kwargs)
return renderer_cls(form, **kwargs).render_errors(type)
def render_field(field, **kwargs):
"""Render a field to a Bootstrap layout."""
renderer_cls = get_field_renderer(**kwargs)
return renderer_cls(field, **kwargs).render()
def render_label(content, label_for=None, label_class="form-label", label_title=""):
"""Render a label with content."""
attrs = {}
if label_for:
attrs["for"] = label_for
if label_class:
attrs["class"] = label_class
if label_title:
attrs["title"] = label_title
return render_tag("label", attrs=attrs, content=content)
def render_button(
content,
button_type=None,
button_class="btn-primary",
size="",
href="",
name=None,
value=None,
title=None,
extra_classes="",
id="",
):
"""Render a button with content."""
attrs = {}
classes = add_css_class("btn", button_class)
size = text_value(size).lower().strip()
if size == "xs":
classes = add_css_class(classes, "btn-xs")
elif size == "sm" or size == "small":
classes = add_css_class(classes, "btn-sm")
elif size == "lg" or size == "large":
classes = add_css_class(classes, "btn-lg")
elif size == "md" or size == "medium":
pass
elif size:
raise BootstrapError('Parameter "size" should be "xs", "sm", "lg" or empty ("{size}" given).'.format(size=size))
if button_type:
if button_type not in ("submit", "reset", "button", "link"):
raise BootstrapError(
(
'Parameter "button_type" should be "submit", "reset", "button", "link" or empty '
'("{button_type}" given).'
).format(button_type=button_type)
)
if button_type != "link":
attrs["type"] = button_type
classes = add_css_class(classes, extra_classes)
attrs["class"] = classes
if href:
tag = "a"
if button_type and button_type != "link":
raise BootstrapError(
'Button of type "{button_type}" is not allowed a "href" parameter.'.format(button_type=button_type)
)
attrs["href"] = href
# Specify role for link with button appearance
attrs.setdefault("role", "button")
else:
tag = "button"
if id:
attrs["id"] = id
if name:
attrs["name"] = name
if value:
attrs["value"] = value
if title:
attrs["title"] = title
return render_tag(tag, attrs=attrs, content=mark_safe(content))
def render_field_and_label(field, label, field_class="", label_for=None, label_class="", layout="", **kwargs):
"""Render a field with its label."""
if layout == "horizontal":
if not label_class:
label_class = get_bootstrap_setting("horizontal_label_class")
if not field_class:
field_class = get_bootstrap_setting("horizontal_field_class")
if not label:
label = mark_safe("&#160;")
label_class = add_css_class(label_class, "control-label")
html = field
if field_class:
html = '<div class="{field_class}">{html}</div>'.format(field_class=field_class, html=html)
if label:
html = render_label(label, label_for=label_for, label_class=label_class) + html
return html
def render_form_group(content, css_class=FORM_GROUP_CLASS):
"""Render a Bootstrap form group."""
return '<div class="{css_class}">{content}</div>'.format(css_class=css_class, content=content)
def is_widget_with_placeholder(widget):
"""
Return whether this widget should have a placeholder.
Only text, text area, number, e-mail, url, password, number and derived inputs have placeholders.
"""
return isinstance(widget, (TextInput, Textarea, NumberInput, EmailInput, URLInput, PasswordInput))
@@ -0,0 +1 @@
# Empty models.py, required file for Django tests
@@ -0,0 +1,569 @@
from bs4 import BeautifulSoup
from django.forms import (
BaseForm,
BaseFormSet,
BoundField,
CheckboxInput,
CheckboxSelectMultiple,
DateInput,
EmailInput,
FileInput,
MultiWidget,
NumberInput,
PasswordInput,
RadioSelect,
Select,
SelectDateWidget,
TextInput,
)
from django.utils.html import conditional_escape, escape, strip_tags
from django.utils.safestring import mark_safe
from .bootstrap import get_bootstrap_setting
from .exceptions import BootstrapError
from .forms import (
FORM_GROUP_CLASS,
is_widget_with_placeholder,
render_field,
render_form,
render_form_group,
render_label,
)
from .text import text_value
from .utils import add_css_class, render_template_file
try:
# If Django is set up without a database, importing this widget gives RuntimeError
from django.contrib.auth.forms import ReadOnlyPasswordHashWidget
except RuntimeError:
ReadOnlyPasswordHashWidget = None
class BaseRenderer(object):
"""A content renderer."""
def __init__(self, *args, **kwargs):
self.layout = kwargs.get("layout", "")
self.form_group_class = kwargs.get("form_group_class", FORM_GROUP_CLASS)
self.field_class = kwargs.get("field_class", "")
self.label_class = kwargs.get("label_class", "")
self.show_help = kwargs.get("show_help", True)
self.show_label = kwargs.get("show_label", True)
self.exclude = kwargs.get("exclude", "")
self.set_placeholder = kwargs.get("set_placeholder", True)
self.size = self.parse_size(kwargs.get("size", ""))
self.horizontal_label_class = kwargs.get(
"horizontal_label_class", get_bootstrap_setting("horizontal_label_class")
)
self.horizontal_field_class = kwargs.get(
"horizontal_field_class", get_bootstrap_setting("horizontal_field_class")
)
def parse_size(self, size):
size = text_value(size).lower().strip()
if size in ("sm", "small"):
return "small"
if size in ("lg", "large"):
return "large"
if size in ("md", "medium", ""):
return "medium"
raise BootstrapError('Invalid value "%s" for parameter "size" (expected "sm", "md", "lg" or "").' % size)
def get_size_class(self, prefix="form-control"):
if self.size == "small":
return prefix + "-sm"
if self.size == "large":
return prefix + "-lg"
return ""
def _render(self):
return ""
def render(self):
return mark_safe(self._render())
class FormsetRenderer(BaseRenderer):
"""Default formset renderer."""
def __init__(self, formset, *args, **kwargs):
if not isinstance(formset, BaseFormSet):
raise BootstrapError('Parameter "formset" should contain a valid Django Formset.')
self.formset = formset
super().__init__(*args, **kwargs)
def render_management_form(self):
return text_value(self.formset.management_form)
def render_form(self, form, **kwargs):
return render_form(form, **kwargs)
def render_forms(self):
rendered_forms = []
for form in self.formset.forms:
rendered_forms.append(
self.render_form(
form,
layout=self.layout,
form_group_class=self.form_group_class,
field_class=self.field_class,
label_class=self.label_class,
show_label=self.show_label,
show_help=self.show_help,
exclude=self.exclude,
set_placeholder=self.set_placeholder,
size=self.size,
horizontal_label_class=self.horizontal_label_class,
horizontal_field_class=self.horizontal_field_class,
)
)
return "\n".join(rendered_forms)
def get_formset_errors(self):
return self.formset.non_form_errors()
def render_errors(self):
formset_errors = self.get_formset_errors()
if formset_errors:
return render_template_file(
"bootstrap5/form_errors.html",
context={"errors": formset_errors, "form": self.formset, "layout": self.layout},
)
return ""
def _render(self):
return "".join([self.render_errors(), self.render_management_form(), self.render_forms()])
class FormRenderer(BaseRenderer):
"""Default form renderer."""
def __init__(self, form, *args, **kwargs):
if not isinstance(form, BaseForm):
raise BootstrapError('Parameter "form" should contain a valid Django Form.')
self.form = form
super().__init__(*args, **kwargs)
self.error_css_class = kwargs.get("error_css_class", None)
self.required_css_class = kwargs.get("required_css_class", None)
self.bound_css_class = kwargs.get("bound_css_class", None)
self.alert_error_type = kwargs.get("alert_error_type", "non_fields")
self.form_check_class = kwargs.get("form_check_class", "form-check")
def render_fields(self):
rendered_fields = []
for field in self.form:
rendered_fields.append(
render_field(
field,
layout=self.layout,
form_group_class=self.form_group_class,
field_class=self.field_class,
label_class=self.label_class,
form_check_class=self.form_check_class,
show_label=self.show_label,
show_help=self.show_help,
exclude=self.exclude,
set_placeholder=self.set_placeholder,
size=self.size,
horizontal_label_class=self.horizontal_label_class,
horizontal_field_class=self.horizontal_field_class,
error_css_class=self.error_css_class,
required_css_class=self.required_css_class,
bound_css_class=self.bound_css_class,
)
)
return "\n".join(rendered_fields)
def get_fields_errors(self):
form_errors = []
for field in self.form:
if not field.is_hidden and field.errors:
form_errors += field.errors
return form_errors
def render_errors(self, type="all"):
form_errors = None
if type == "all":
form_errors = self.get_fields_errors() + self.form.non_field_errors()
elif type == "fields":
form_errors = self.get_fields_errors()
elif type == "non_fields":
form_errors = self.form.non_field_errors()
if form_errors:
return render_template_file(
"bootstrap5/form_errors.html",
context={"errors": form_errors, "form": self.form, "layout": self.layout, "type": type},
)
return ""
def _render(self):
return self.render_errors(self.alert_error_type) + self.render_fields()
class FieldRenderer(BaseRenderer):
"""Default field renderer."""
# These widgets will not be wrapped in a form-control class
WIDGETS_NO_FORM_CONTROL = (CheckboxInput, RadioSelect, CheckboxSelectMultiple, Select)
def __init__(self, field, *args, **kwargs):
if not isinstance(field, BoundField):
raise BootstrapError('Parameter "field" should contain a valid Django BoundField.')
self.field = field
super().__init__(*args, **kwargs)
self.widget = field.field.widget
self.is_multi_widget = isinstance(field.field.widget, MultiWidget)
self.initial_attrs = self.widget.attrs.copy()
self.field_help = text_value(mark_safe(field.help_text)) if self.show_help and field.help_text else ""
self.field_errors = [conditional_escape(text_value(error)) for error in field.errors]
self.form_check_class = kwargs.get("form_check_class", "form-check")
if "placeholder" in kwargs:
# Find the placeholder in kwargs, even if it's empty
self.placeholder = kwargs["placeholder"]
elif get_bootstrap_setting("set_placeholder"):
# If not found, see if we set the label
self.placeholder = field.label
else:
# Or just set it to empty
self.placeholder = ""
if self.placeholder:
self.placeholder = text_value(self.placeholder)
self.addon_before = kwargs.get("addon_before", self.widget.attrs.pop("addon_before", ""))
self.addon_after = kwargs.get("addon_after", self.widget.attrs.pop("addon_after", ""))
self.addon_before_class = kwargs.get(
"addon_before_class", self.widget.attrs.pop("addon_before_class", "input-group-text")
)
self.addon_after_class = kwargs.get(
"addon_after_class", self.widget.attrs.pop("addon_after_class", "input-group-text")
)
# These are set in Django or in the global BOOTSTRAP5 settings, and
# they can be overwritten in the template
error_css_class = kwargs.get("error_css_class", None)
required_css_class = kwargs.get("required_css_class", None)
bound_css_class = kwargs.get("bound_css_class", None)
if error_css_class is not None:
self.error_css_class = error_css_class
else:
self.error_css_class = getattr(field.form, "error_css_class", get_bootstrap_setting("error_css_class"))
if required_css_class is not None:
self.required_css_class = required_css_class
else:
self.required_css_class = getattr(
field.form, "required_css_class", get_bootstrap_setting("required_css_class")
)
if bound_css_class is not None:
self.success_css_class = bound_css_class
else:
self.success_css_class = getattr(field.form, "bound_css_class", get_bootstrap_setting("success_css_class"))
# If the form is marked as form.empty_permitted, do not set required class
if self.field.form.empty_permitted:
self.required_css_class = ""
def restore_widget_attrs(self):
self.widget.attrs = self.initial_attrs.copy()
def add_class_attrs(self, widget=None):
if widget is None:
widget = self.widget
classes = widget.attrs.get("class", "")
if ReadOnlyPasswordHashWidget is not None and isinstance(widget, ReadOnlyPasswordHashWidget):
# Render this is a static control
classes = add_css_class(classes, "form-control-static", prepend=True)
elif not isinstance(widget, self.WIDGETS_NO_FORM_CONTROL):
classes = add_css_class(classes, "form-control", prepend=True)
# For these widget types, add the size class here
classes = add_css_class(classes, self.get_size_class())
elif isinstance(widget, CheckboxInput):
classes = add_css_class(classes, "form-check-input", prepend=True)
elif isinstance(widget, Select):
classes = add_css_class(classes, "form-select", prepend=True)
if self.field.errors:
if self.error_css_class:
classes = add_css_class(classes, self.error_css_class)
else:
if self.field.form.is_bound:
classes = add_css_class(classes, self.success_css_class)
widget.attrs["class"] = classes
def add_placeholder_attrs(self, widget=None):
if widget is None:
widget = self.widget
placeholder = widget.attrs.get("placeholder", self.placeholder)
if placeholder and self.set_placeholder and is_widget_with_placeholder(widget):
# TODO: Should this be stripped and/or escaped?
widget.attrs["placeholder"] = placeholder
def add_help_attrs(self, widget=None):
if widget is None:
widget = self.widget
if not isinstance(widget, CheckboxInput):
widget.attrs["title"] = widget.attrs.get("title", escape(strip_tags(self.field_help)))
def add_widget_attrs(self):
if self.is_multi_widget:
widgets = self.widget.widgets
else:
widgets = [self.widget]
for widget in widgets:
self.add_class_attrs(widget)
self.add_placeholder_attrs(widget)
self.add_help_attrs(widget)
def list_to_class(self, html, klass):
classes = add_css_class(klass, self.get_size_class())
mapping = [
("<ul", '<div class="{classes}"'.format(classes=classes)),
("</ul>", "</div>"),
("<li", '<div class="{form_check_class}"'.format(form_check_class=self.form_check_class)),
("</li>", "</div>"),
]
for k, v in mapping:
html = html.replace(k, v)
# Apply bootstrap5 classes to labels and inputs.
# A simple 'replace' isn't enough as we don't want to have several 'class' attr definition, which would happen
# if we tried to 'html.replace("input", "input class=...")'
soup = BeautifulSoup(html, features="html.parser")
enclosing_div = soup.find("div", {"class": classes})
if enclosing_div:
for label in enclosing_div.find_all("label"):
label.attrs["class"] = label.attrs.get("class", []) + ["form-check-label"]
try:
label.input.attrs["class"] = label.input.attrs.get("class", []) + ["form-check-input"]
except AttributeError:
pass
return str(soup)
def add_checkbox_label(self, html):
return html + render_label(
content=self.field.label,
label_for=self.field.id_for_label,
label_title=escape(strip_tags(self.field_help)),
label_class="form-check-label",
)
def fix_date_select_input(self, html):
div1 = '<div class="col-4">'
div2 = "</div>"
html = html.replace("<select", div1 + "<select")
html = html.replace("</select>", "</select>" + div2)
return '<div class="row bootstrap5-multi-input">{html}</div>'.format(html=html)
def fix_file_input_label(self, html):
html = "<br>" + html
return html
def post_widget_render(self, html):
if isinstance(self.widget, RadioSelect):
html = self.list_to_class(html, "radio radio-success")
elif isinstance(self.widget, CheckboxSelectMultiple):
html = self.list_to_class(html, "checkbox")
elif isinstance(self.widget, SelectDateWidget):
html = self.fix_date_select_input(html)
elif isinstance(self.widget, CheckboxInput) and self.show_label:
html = self.add_checkbox_label(html)
elif isinstance(self.widget, FileInput):
html = self.fix_file_input_label(html)
return html
def wrap_widget(self, html):
if isinstance(self.widget, CheckboxInput):
# Wrap checkboxes
# Note checkboxes do not get size classes, see #318
html = '<div class="form-check">{html}</div>'.format(html=html)
return html
def make_input_group_addon(self, inner_class, outer_class, content):
if not content:
return ""
if inner_class:
content = '<span class="{inner_class}">{content}</span>'.format(inner_class=inner_class, content=content)
return '<div class="{outer_class}">{content}</div>'.format(outer_class=outer_class, content=content)
@property
def is_input_group(self):
allowed_widget_types = (TextInput, PasswordInput, DateInput, NumberInput, Select, EmailInput)
return (self.addon_before or self.addon_after) and isinstance(self.widget, allowed_widget_types)
def make_input_group(self, html):
if self.is_input_group:
before = self.make_input_group_addon(self.addon_before_class, "input-group-prepend", self.addon_before)
after = self.make_input_group_addon(self.addon_after_class, "input-group-append", self.addon_after)
html = self.append_errors("{before}{html}{after}".format(before=before, html=html, after=after))
html = '<div class="input-group">{html}</div>'.format(html=html)
return html
def append_help(self, html):
field_help = self.field_help or None
if field_help:
help_html = render_template_file(
"bootstrap5/field_help_text.html",
context={
"field": self.field,
"field_help": field_help,
"layout": self.layout,
"show_help": self.show_help,
},
)
html += help_html
return html
def append_errors(self, html):
field_errors = self.field_errors
if field_errors:
errors_html = render_template_file(
"bootstrap5/field_errors.html",
context={
"field": self.field,
"field_errors": field_errors,
"layout": self.layout,
"show_help": self.show_help,
},
)
html += errors_html
return html
def append_to_field(self, html):
if isinstance(self.widget, CheckboxInput):
# we have already appended errors and help to checkboxes
# in append_to_checkbox_field
return html
if not self.is_input_group:
# we already appended errors for input groups in make_input_group
html = self.append_errors(html)
return self.append_help(html)
def append_to_checkbox_field(self, html):
if not isinstance(self.widget, CheckboxInput):
# we will append errors and help to normal fields later in append_to_field
return html
html = self.append_errors(html)
return self.append_help(html)
def get_field_class(self):
field_class = self.field_class
if not field_class and self.layout == "horizontal":
field_class = self.horizontal_field_class
return field_class
def wrap_field(self, html):
field_class = self.get_field_class()
if field_class:
html = '<div class="{field_class}">{html}</div>'.format(field_class=field_class, html=html)
return html
def get_label_class(self):
label_class = self.label_class
if not label_class and self.layout == "horizontal":
label_class = self.horizontal_label_class
label_class = add_css_class(label_class, "col-form-label")
label_class = text_value(label_class)
if not label_class:
label_class = "form-label"
if not self.show_label or self.show_label == "visually-hidden":
label_class = add_css_class(label_class, "visually-hidden")
return label_class
def get_label(self):
if self.show_label == "skip":
return None
elif isinstance(self.widget, CheckboxInput):
label = None
else:
label = self.field.label
if self.layout == "horizontal" and not label:
return mark_safe("&#160;")
return label
def add_label(self, html):
label = self.get_label()
if label:
html = render_label(label, label_for=self.field.id_for_label, label_class=self.get_label_class()) + html
return html
def get_form_group_class(self):
form_group_class = self.form_group_class
if self.field.errors:
if self.error_css_class:
form_group_class = add_css_class(form_group_class, self.error_css_class)
else:
if self.field.form.is_bound:
form_group_class = add_css_class(form_group_class, self.success_css_class)
if self.field.field.required and self.required_css_class:
form_group_class = add_css_class(form_group_class, self.required_css_class)
if self.layout == "horizontal":
form_group_class = add_css_class(form_group_class, "row")
return form_group_class
def wrap_label_and_field(self, html):
return render_form_group(html, self.get_form_group_class())
def _render(self):
# See if we're not excluded
if self.field.name in self.exclude.replace(" ", "").split(","):
return ""
# Hidden input requires no special treatment
if self.field.is_hidden:
return text_value(self.field)
# Render the widget
self.add_widget_attrs()
html = self.field.as_widget(attrs=self.widget.attrs)
self.restore_widget_attrs()
# Start post render
html = self.post_widget_render(html)
html = self.append_to_checkbox_field(html)
html = self.wrap_widget(html)
html = self.make_input_group(html)
html = self.append_to_field(html)
html = self.wrap_field(html)
html = self.add_label(html)
html = self.wrap_label_and_field(html)
return html
class InlineFieldRenderer(FieldRenderer):
"""Inline field renderer."""
def add_error_attrs(self):
field_title = self.widget.attrs.get("title", "")
field_title += " " + " ".join([strip_tags(e) for e in self.field_errors])
self.widget.attrs["title"] = field_title.strip()
def add_widget_attrs(self):
super().add_widget_attrs()
self.add_error_attrs()
def append_to_field(self, html):
return html
def get_field_class(self):
return self.field_class
def get_form_group_class(self):
if self.form_group_class == FORM_GROUP_CLASS:
self.form_group_class = "col-auto"
return super().get_form_group_class()
def get_label_class(self):
return add_css_class(self.label_class, "visually-hidden")
class HorizontalFieldRenderer(FieldRenderer):
"""Horizontal field renderer."""
# No-op the fix for normal form layout because it breaks the horizontal one.
def fix_file_input_label(self, html):
return html
@@ -0,0 +1,44 @@
<!DOCTYPE html>
{% load bootstrap5 %}
{% if 'use_i18n'|bootstrap_setting %}
{% load i18n %}
{% get_current_language as LANGUAGE_CODE %}
{% endif %}
<html lang="{{ LANGUAGE_CODE|default:'en_us' }}">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- Page title -->
<title>{% block bootstrap5_title %}django-bootstrap-v5 template title{% endblock %}</title>
<!-- Bootstrap CSS -->
{% bootstrap_css %}
<!-- Bootstrap JavaScript if it is in head -->
{% if 'javascript_in_head'|bootstrap_setting %}
{% bootstrap_javascript %}
{% endif %}
{% block bootstrap5_extra_head %}{% endblock %}
</head>
<body>
{% block bootstrap5_before_content %}{% endblock %}
{% block bootstrap5_content %} CONTENT {% endblock %}
{% block bootstrap5_after_content %}{% endblock %}
<!-- Bootstrap JavaScript if it is in body -->
{% if not 'javascript_in_head'|bootstrap_setting %}
{% bootstrap_javascript %}
{% endif %}
{% block bootstrap5_extra_script %}{% endblock %}
</body>
</html>
@@ -0,0 +1,3 @@
{% for text in field_errors %}
<div class="invalid-feedback">{{ text }}</div>
{% endfor %}
@@ -0,0 +1,3 @@
{% if field_help %}
<small class="form-text text-muted">{{ field_help }}</small>
{% endif %}
@@ -0,0 +1,7 @@
{% load i18n %}
<div class="alert alert-danger alert-dismissible" role="alert">
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="{% trans 'close' %}"></button>
{% for error in errors %}
{{ error }}{% if not forloop.last %}<br>{% endif %}
{% endfor %}
</div>
@@ -0,0 +1,7 @@
{% load i18n bootstrap5 %}
{% for message in messages %}
<div class="{{ message|bootstrap_message_classes }} alert-dismissible fade show" role="alert">
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="{% trans 'close' %}"></button>
{{ message }}
</div>
{% endfor %}
@@ -0,0 +1,40 @@
{% load bootstrap5 %}
{% with bpurl=bootstrap_pagination_url|default:"" %}
<ul class="{{ pagination_css_classes }}">
<li class="prev page-item{% if current_page == 1 %} disabled{% endif %}">
<a class="page-link" href="{% if current_page == 1 %}#{% else %}{% bootstrap_url_replace_param bpurl parameter_name 1 %}{% endif %}">
&laquo;
</a>
</li>
{% if pages_back %}
<li class="page-item">
<a class="page-link" href="{% bootstrap_url_replace_param bpurl parameter_name pages_back %}">&hellip;</a>
</li>
{% endif %}
{% for p in pages_shown %}
<li class="page-item{% if current_page == p %} active{% endif %}">
<a class="page-link" href="{% if current_page == p %}#{% else %}{% bootstrap_url_replace_param bpurl parameter_name p %}{% endif %}">
{{ p }}
</a>
</li>
{% endfor %}
{% if pages_forward %}
<li class="page-item">
<a class="page-link" href="{% bootstrap_url_replace_param bpurl parameter_name pages_forward %}">&hellip;</a>
</li>
{% endif %}
<li class="last page-item{% if current_page == num_pages %} disabled{% endif %}">
<a class="page-link" href="{% if current_page == num_pages %}#{% else %}{% bootstrap_url_replace_param bpurl parameter_name num_pages %}{% endif %}">
&raquo;
</a>
</li>
</ul>
{% endwith %}
@@ -0,0 +1,9 @@
<div{% if widget.attrs.id %} id="{{ widget.attrs.id }}"{% endif %} class="btn-group btn-group-toggle" data-bs-toggle="buttons">
{% for group, options, index in widget.optgroups %}
{% for option in options %}
<label class="{{ widget.attrs.class|add:' btn btn-outline-primary' }}{% if option.attrs.checked %} active{% endif %}" id="{{ widget.attrs.id }}">
<input type="{{ option.type }}" name="{{ option.name }}" id="{{ option.attrs.id }}"{% if option.value != None %} value="{{ option.value|stringformat:'s' }}"{% if option.attrs.checked %} checked="checked"{% endif %}{% endif %}{% if widget.required %} required{% endif %}> {{ option.label }}
</label>
{% endfor %}
{% endfor %}
</div>
@@ -0,0 +1,876 @@
from math import floor
from urllib.parse import parse_qs, urlparse, urlunparse
from django import template
from django.contrib.messages import constants as message_constants
from django.template import Context
from django.utils.http import urlencode
from django.utils.safestring import mark_safe
from ..bootstrap import (
css_url,
get_bootstrap_setting,
javascript_url,
theme_url,
)
from ..components import render_alert
from ..forms import (
FORM_GROUP_CLASS,
render_button,
render_field,
render_field_and_label,
render_form,
render_form_errors,
render_form_group,
render_formset,
render_formset_errors,
render_label,
)
from ..utils import (
handle_var,
parse_token_contents,
render_link_tag,
render_script_tag,
render_tag,
render_template_file,
url_replace_param,
)
MESSAGE_LEVEL_CLASSES = {
message_constants.DEBUG: "alert alert-warning",
message_constants.INFO: "alert alert-info",
message_constants.SUCCESS: "alert alert-success",
message_constants.WARNING: "alert alert-warning",
message_constants.ERROR: "alert alert-danger",
}
register = template.Library()
@register.filter
def bootstrap_setting(value):
"""
Get a setting.
A simple way to read bootstrap settings in a template.
Please consider this filter private for now, do not use it in your own templates.
"""
return get_bootstrap_setting(value)
@register.filter
def bootstrap_message_classes(message):
"""Return the message classes for a message."""
extra_tags = None
try:
extra_tags = message.extra_tags
except AttributeError:
pass
if not extra_tags:
extra_tags = ""
classes = [extra_tags]
try:
level = message.level
except AttributeError:
pass
else:
try:
classes.append(MESSAGE_LEVEL_CLASSES[level])
except KeyError:
classes.append("alert alert-danger")
return " ".join(classes).strip()
@register.simple_tag
def bootstrap_javascript_url():
"""
Return the full url to the Bootstrap JavaScript library.
Default value: ``None``
This value is configurable, see Settings section
**Tag name**::
bootstrap_javascript_url
**Usage**::
{% bootstrap_javascript_url %}
**Example**::
{% bootstrap_javascript_url %}
"""
return javascript_url()
@register.simple_tag
def bootstrap_css_url():
"""
Return the full url to the Bootstrap CSS library.
Default value: ``None``
This value is configurable, see Settings section
**Tag name**::
bootstrap_css_url
**Usage**::
{% bootstrap_css_url %}
**Example**::
{% bootstrap_css_url %}
"""
return css_url()
@register.simple_tag
def bootstrap_theme_url():
"""
Return the full url to a Bootstrap theme CSS library.
Default value: ``None``
This value is configurable, see Settings section
**Tag name**::
bootstrap_theme_url
**Usage**::
{% bootstrap_theme_url %}
**Example**::
{% bootstrap_theme_url %}
"""
return theme_url()
@register.simple_tag
def bootstrap_css():
"""
Return HTML for Bootstrap CSS. If no CSS url is available, return empty string.
Default value: ``None``
This value is configurable, see Settings section
**Tag name**::
bootstrap_css
**Usage**::
{% bootstrap_css %}
**Example**::
{% bootstrap_css %}
"""
rendered_urls = []
if bootstrap_css_url():
rendered_urls.append(render_link_tag(bootstrap_css_url()))
if bootstrap_theme_url():
rendered_urls.append(render_link_tag(bootstrap_theme_url()))
return mark_safe("".join([url for url in rendered_urls]))
@register.simple_tag
def bootstrap_javascript():
"""
Return HTML for Bootstrap JavaScript.
Adjust url in settings.
If no url is returned, we don't want this statement to return any HTML. This is intended behavior.
Default value: False
This value is configurable, see Settings section. Note that any value that evaluates to True and is
not "slim" will be interpreted as True.
**Tag name**::
bootstrap_javascript
**Parameters**::
**Usage**::
{% bootstrap_javascript %}
**Example**::
{% bootstrap_javascript %}
"""
# List of JS tags to include
javascript_tags = []
# Bootstrap 4 JavaScript
bootstrap_js_url = bootstrap_javascript_url()
if bootstrap_js_url:
javascript_tags.append(render_script_tag(bootstrap_js_url))
# Join and return
return mark_safe("\n".join(javascript_tags))
@register.simple_tag
def bootstrap_formset(*args, **kwargs):
"""
Render a formset.
**Tag name**::
bootstrap_formset
**Parameters**::
formset
The formset that is being rendered
See bootstrap_field_ for other arguments
**Usage**::
{% bootstrap_formset formset %}
**Example**::
{% bootstrap_formset formset layout='horizontal' %}
"""
return render_formset(*args, **kwargs)
@register.simple_tag
def bootstrap_formset_errors(*args, **kwargs):
"""
Render formset errors.
**Tag name**::
bootstrap_formset_errors
**Parameters**::
formset
The formset that is being rendered
layout
Context value that is available in the template ``bootstrap5/form_errors.html`` as ``layout``.
**Usage**::
{% bootstrap_formset_errors formset %}
**Example**::
{% bootstrap_formset_errors formset layout='inline' %}
"""
return render_formset_errors(*args, **kwargs)
@register.simple_tag
def bootstrap_form(*args, **kwargs):
"""
Render a form.
**Tag name**::
bootstrap_form
**Parameters**::
form
The form that is to be rendered
exclude
A list of field names (comma separated) that should not be rendered
E.g. exclude=subject,bcc
alert_error_type
Control which type of errors should be rendered in global form alert.
One of the following values:
* ``'all'``
* ``'fields'``
* ``'non_fields'``
:default: ``'non_fields'``
See bootstrap_field_ for other arguments
**Usage**::
{% bootstrap_form form %}
**Example**::
{% bootstrap_form form layout='inline' %}
"""
return render_form(*args, **kwargs)
@register.simple_tag
def bootstrap_form_errors(*args, **kwargs):
"""
Render form errors.
**Tag name**::
bootstrap_form_errors
**Parameters**::
form
The form that is to be rendered
type
Control which type of errors should be rendered.
One of the following values:
* ``'all'``
* ``'fields'``
* ``'non_fields'``
:default: ``'all'``
layout
Context value that is available in the template ``bootstrap5/form_errors.html`` as ``layout``.
**Usage**::
{% bootstrap_form_errors form %}
**Example**::
{% bootstrap_form_errors form layout='inline' %}
"""
return render_form_errors(*args, **kwargs)
@register.simple_tag
def bootstrap_field(*args, **kwargs):
"""
Render a field.
**Tag name**::
bootstrap_field
**Parameters**::
field
The form field to be rendered
layout
If set to ``'horizontal'`` then the field and label will be rendered side-by-side, as long as there
is no ``field_class`` set as well.
form_group_class
CSS class of the ``div`` that wraps the field and label.
:default: ``'mb-3'``
field_class
CSS class of the ``div`` that wraps the field.
label_class
CSS class of the ``label`` element.
:default: ``'form-label'``
form_check_class
CSS class of the ``div`` element wrapping the label and input when rendering checkboxes and radio buttons.
show_help
Show the field's help text, if the field has help text.
:default: ``True``
show_label
Whether the show the label of the field.
* ``True``
* ``False``/``'visually-hidden'``
* ``'skip'``
:default: ``True``
exclude
A list of field names that should not be rendered
size
Controls the size of the rendered ``input.form-control`` through the use of CSS classes.
One of the following values:
* ``'small'``
* ``'medium'``
* ``'large'``
placeholder
Sets the placeholder text of a textbox
horizontal_label_class
Class used on the label when the ``layout`` is set to ``horizontal``.
:default: ``'col-md-3'``. Can be changed in :doc:`settings`
horizontal_field_class
Class used on the field when the ``layout`` is set to ``horizontal``.
:default: ``'col-md-9'``. Can be changed in :doc:`settings`
addon_before
Text that should be prepended to the form field. Can also be an icon, e.g.
``'<span class="glyphicon glyphicon-calendar"></span>'``
See the `Bootstrap docs <https://getbootstrap.com/docs/5.0/forms/input-group/#basic-example>`
for more examples.
addon_after
Text that should be appended to the form field. Can also be an icon, e.g.
``'<span class="glyphicon glyphicon-calendar"></span>'``
See the `Bootstrap docs <https://getbootstrap.com/docs/5.0/forms/input-group/#basic-example>`
for more examples.
addon_before_class
Class used on the span when ``addon_before`` is used.
One of the following values:
* ``'input-group-text'``
* ``None``
Set to None to disable the span inside the addon. (for use with buttons)
:default: ``input-group-text``
addon_after_class
Class used on the span when ``addon_after`` is used.
One of the following values:
* ``'input-group-text'``
* ``None``
Set to None to disable the span inside the addon. (for use with buttons)
:default: ``input-group-text``
error_css_class
CSS class used when the field has an error
:default: ``'has-error'``. Can be changed :doc:`settings`
required_css_class
CSS class used on the ``div.mb-3`` to indicate a field is required
:default: ``''``. Can be changed :doc:`settings`
bound_css_class
CSS class used when the field is bound
:default: ``'has-success'``. Can be changed :doc:`settings`
**Usage**::
{% bootstrap_field field %}
**Example**::
{% bootstrap_field field show_label=False %}
"""
return render_field(*args, **kwargs)
@register.simple_tag()
def bootstrap_label(*args, **kwargs):
"""
Render a label.
**Tag name**::
bootstrap_label
**Parameters**::
content
The label's text
label_for
The value that will be in the ``for`` attribute of the rendered ``<label>``
label_class
The CSS class for the rendered ``<label>``
:default': ``'form-label'``
label_title
The value that will be in the ``title`` attribute of the rendered ``<label>``
**Usage**::
{% bootstrap_label content %}
**Example**::
{% bootstrap_label "Email address" label_for="exampleInputEmail1" %}
"""
return render_label(*args, **kwargs)
@register.simple_tag
def bootstrap_button(*args, **kwargs):
"""
Render a button.
**Tag name**::
bootstrap_button
**Parameters**::
content
The text to be displayed in the button
button_type
Optional field defining what type of button this is.
Accepts one of the following values:
* ``'submit'``
* ``'reset'``
* ``'button'``
* ``'link'``
button_class
The class of button to use. If none is given, btn-primary will be used.
extra_classes
Any extra CSS classes that should be added to the button.
size
Optional field to control the size of the button.
Accepts one of the following values:
* ``'xs'``
* ``'sm'``
* ``'small'``
* ``'md'``
* ``'medium'``
* ``'lg'``
* ``'large'``
href
Render the button as an ``<a>`` element. The ``href`` attribute is set with this value.
If a ``button_type`` other than ``link`` is defined, specifying a ``href`` will throw a
``ValueError`` exception.
name
Value of the ``name`` attribute of the rendered element.
value
Value of the ``value`` attribute of the rendered element.
**Usage**::
{% bootstrap_button content %}
**Example**::
{% bootstrap_button "Save" button_type="submit" button_class="btn-primary" %}
"""
return render_button(*args, **kwargs)
@register.simple_tag
def bootstrap_alert(content, alert_type="info", dismissible=True):
"""
Render an alert.
**Tag name**::
bootstrap_alert
**Parameters**::
content
HTML content of alert
alert_type
* ``'info'``
* ``'warning'``
* ``'danger'``
* ``'success'``
:default: ``'info'``
dismissible
boolean, is alert dismissible
:default: ``True``
**Usage**::
{% bootstrap_alert content %}
**Example**::
{% bootstrap_alert "Something went wrong" alert_type='error' %}
"""
return render_alert(content, alert_type, dismissible)
@register.tag("buttons")
def bootstrap_buttons(parser, token):
"""
Render buttons for form.
**Tag name**::
buttons
**Parameters**::
submit
Text for a submit button
reset
Text for a reset button
**Usage**::
{% buttons %}{% endbuttons %}
**Example**::
{% buttons submit='OK' reset="Cancel" %}{% endbuttons %}
"""
kwargs = parse_token_contents(parser, token)
kwargs["nodelist"] = parser.parse(("endbuttons",))
parser.delete_first_token()
return ButtonsNode(**kwargs)
class ButtonsNode(template.Node):
def __init__(self, nodelist, args, kwargs, asvar, **kwargs2):
self.nodelist = nodelist
self.args = args
self.kwargs = kwargs
self.asvar = asvar
def render(self, context):
output_kwargs = {}
for key in self.kwargs:
output_kwargs[key] = handle_var(self.kwargs[key], context)
buttons = []
submit = output_kwargs.get("submit", None)
reset = output_kwargs.get("reset", None)
if submit:
buttons.append(bootstrap_button(submit, "submit"))
if reset:
buttons.append(bootstrap_button(reset, "reset"))
buttons = " ".join(buttons) + self.nodelist.render(context)
output_kwargs.update({"label": None, "field": buttons})
css_class = output_kwargs.pop("form_group_class", FORM_GROUP_CLASS)
output = render_form_group(render_field_and_label(**output_kwargs), css_class=css_class)
if self.asvar:
context[self.asvar] = output
return ""
else:
return output
@register.simple_tag(takes_context=True)
def bootstrap_messages(context, *args, **kwargs):
"""
Show django.contrib.messages Messages in Bootstrap alert containers.
Uses the template ``bootstrap5/messages.html``.
**Tag name**::
bootstrap_messages
**Parameters**::
None.
**Usage**::
{% bootstrap_messages %}
**Example**::
{% bootstrap_javascript %}
{% bootstrap_messages %}
"""
# Force Context to dict
if isinstance(context, Context):
context = context.flatten()
context.update({"message_constants": message_constants})
return render_template_file("bootstrap5/messages.html", context=context)
@register.inclusion_tag("bootstrap5/pagination.html")
def bootstrap_pagination(page, **kwargs):
"""
Render pagination for a page.
**Tag name**::
bootstrap_pagination
**Parameters**::
page
The page of results to show.
pages_to_show
Number of pages in total
:default: ``11``
url
URL to navigate to for pagination forward and pagination back.
:default: ``None``
size
Controls the size of the pagination through CSS. Defaults to being normal sized.
One of the following:
* ``'small'``
* ``'large'``
:default: ``None``
extra
Any extra page parameters.
:default: ``None``
parameter_name
Name of the paging URL parameter.
:default: ``'page'``
**Usage**::
{% bootstrap_pagination page %}
**Example**::
{% bootstrap_pagination lines url="/pagination?page=1" size="large" %}
"""
pagination_kwargs = kwargs.copy()
pagination_kwargs["page"] = page
return get_pagination_context(**pagination_kwargs)
@register.simple_tag
def bootstrap_url_replace_param(url, name, value):
return url_replace_param(url, name, value)
def get_pagination_context(
page, pages_to_show=11, url=None, size=None, justify_content=None, extra=None, parameter_name="page"
):
"""Generate Bootstrap pagination context from a page object."""
pages_to_show = int(pages_to_show)
if pages_to_show < 1:
raise ValueError(
"Pagination pages_to_show should be a positive integer, you specified {pages_to_show}.".format(
pages_to_show=pages_to_show
)
)
num_pages = page.paginator.num_pages
current_page = page.number
half_page_num = int(floor(pages_to_show / 2))
if half_page_num < 0:
half_page_num = 0
first_page = current_page - half_page_num
if first_page <= 1:
first_page = 1
if first_page > 1:
pages_back = first_page - half_page_num
if pages_back < 1:
pages_back = 1
else:
pages_back = None
last_page = first_page + pages_to_show - 1
if pages_back is None:
last_page += 1
if last_page > num_pages:
last_page = num_pages
if last_page < num_pages:
pages_forward = last_page + half_page_num
if pages_forward > num_pages:
pages_forward = num_pages
else:
pages_forward = None
if first_page > 1:
first_page -= 1
if pages_back is not None and pages_back > 1:
pages_back -= 1
else:
pages_back = None
pages_shown = []
for i in range(first_page, last_page + 1):
pages_shown.append(i)
# parse the url
parts = urlparse(url or "")
params = parse_qs(parts.query)
# append extra querystring parameters to the url.
if extra:
params.update(parse_qs(extra))
# build url again.
url = urlunparse(
[parts.scheme, parts.netloc, parts.path, parts.params, urlencode(params, doseq=True), parts.fragment]
)
# Set CSS classes, see http://getbootstrap.com/components/#pagination
pagination_css_classes = ["pagination"]
if size == "small":
pagination_css_classes.append("pagination-sm")
elif size == "large":
pagination_css_classes.append("pagination-lg")
if justify_content == "start":
pagination_css_classes.append("justify-content-start")
elif justify_content == "center":
pagination_css_classes.append("justify-content-center")
elif justify_content == "end":
pagination_css_classes.append("justify-content-end")
return {
"bootstrap_pagination_url": url,
"num_pages": num_pages,
"current_page": current_page,
"first_page": first_page,
"last_page": last_page,
"pages_shown": pages_shown,
"pages_back": pages_back,
"pages_forward": pages_forward,
"pagination_css_classes": " ".join(pagination_css_classes),
"parameter_name": parameter_name,
}
@@ -0,0 +1,15 @@
from django.utils.encoding import force_str
def text_value(value):
"""Force a value to text, render None as an empty string."""
if value is None:
return ""
return force_str(value)
def text_concat(*args, **kwargs):
"""Concatenate several values as a text string with an optional separator."""
separator = text_value(kwargs.get("separator", ""))
values = filter(None, [text_value(v) for v in args])
return separator.join(values)
@@ -0,0 +1,144 @@
import re
from collections.abc import Mapping
from urllib.parse import parse_qs, urlparse, urlunparse
from django.forms.utils import flatatt
from django.template.base import FilterExpression, TemplateSyntaxError, Variable, VariableDoesNotExist, kwarg_re
from django.template.loader import get_template
from django.utils.encoding import force_str
from django.utils.html import format_html
from django.utils.http import urlencode
from django.utils.safestring import mark_safe
from .text import text_value
# RegEx for quoted string
QUOTED_STRING = re.compile(r'^["\'](?P<noquotes>.+)["\']$')
def handle_var(value, context):
"""Handle template tag variable."""
# Resolve FilterExpression and Variable immediately
if isinstance(value, FilterExpression) or isinstance(value, Variable):
return value.resolve(context)
# Return quoted strings unquoted
# http://djangosnippets.org/snippets/886
stringval = QUOTED_STRING.search(value)
if stringval:
return stringval.group("noquotes")
# Resolve variable or return string value
try:
return Variable(value).resolve(context)
except VariableDoesNotExist:
return value
def parse_token_contents(parser, token):
"""Parse template tag contents."""
bits = token.split_contents()
tag = bits.pop(0)
args = []
kwargs = {}
asvar = None
if len(bits) >= 2 and bits[-2] == "as":
asvar = bits[-1]
bits = bits[:-2]
if len(bits):
for bit in bits:
match = kwarg_re.match(bit)
if not match:
raise TemplateSyntaxError('Malformed arguments to tag "{tag}"'.format(tag=tag))
name, value = match.groups()
if name:
kwargs[name] = parser.compile_filter(value)
else:
args.append(parser.compile_filter(value))
return {"tag": tag, "args": args, "kwargs": kwargs, "asvar": asvar}
def split_css_classes(css_classes):
"""Turn string into a list of CSS classes."""
classes_list = text_value(css_classes).split(" ")
return [c for c in classes_list if c]
def add_css_class(css_classes, css_class, prepend=False):
"""Add a CSS class to a string of CSS classes."""
classes_list = split_css_classes(css_classes)
classes_to_add = [c for c in split_css_classes(css_class) if c not in classes_list]
if prepend:
classes_list = classes_to_add + classes_list
else:
classes_list += classes_to_add
return " ".join(classes_list)
def remove_css_class(css_classes, css_class):
"""Remove a CSS class from a string of CSS classes."""
remove = set(split_css_classes(css_class))
classes_list = [c for c in split_css_classes(css_classes) if c not in remove]
return " ".join(classes_list)
def render_script_tag(url):
"""Build a script tag."""
url_dict = sanitize_url_dict(url)
url_dict.setdefault("src", url_dict.pop("url", None))
return render_tag("script", url_dict)
def render_link_tag(url, rel="stylesheet", media=None):
"""Build a link tag."""
url_dict = sanitize_url_dict(url, url_attr="href")
url_dict.setdefault("href", url_dict.pop("url", None))
url_dict["rel"] = rel
if media:
url_dict["media"] = media
return render_tag("link", attrs=url_dict, close=False)
def render_tag(tag, attrs=None, content=None, close=True):
"""Render a HTML tag."""
builder = "<{tag}{attrs}>{content}"
if content or close:
builder += "</{tag}>"
return format_html(builder, tag=tag, attrs=mark_safe(flatatt(attrs)) if attrs else "", content=text_value(content))
def render_template_file(template, context=None):
"""Render a Template to unicode."""
assert isinstance(context, Mapping)
template = get_template(template)
return template.render(context)
def url_replace_param(url, name, value):
"""Replace a GET parameter in an URL."""
url_components = urlparse(force_str(url))
params = parse_qs(url_components.query)
if value is None:
del params[name]
else:
params[name] = value
return mark_safe(
urlunparse(
[
url_components.scheme,
url_components.netloc,
url_components.path,
url_components.params,
urlencode(params, doseq=True),
url_components.fragment,
]
)
)
def sanitize_url_dict(url, url_attr="src"):
"""Sanitize url dict as used in django-bootstrap-v5 settings."""
if isinstance(url, str):
return {url_attr: url}
return url.copy()
@@ -0,0 +1,11 @@
from django.forms import RadioSelect
class RadioSelectButtonGroup(RadioSelect):
"""
This widget renders a Bootstrap 4 set of buttons horizontally instead of typical radio buttons.
Much more mobile friendly.
"""
template_name = "bootstrap5/widgets/radio_select_button_group.html"