This commit is contained in:
@@ -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"]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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(" ")
|
||||
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(" ")
|
||||
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 %}">
|
||||
«
|
||||
</a>
|
||||
</li>
|
||||
|
||||
{% if pages_back %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="{% bootstrap_url_replace_param bpurl parameter_name pages_back %}">…</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 %}">…</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 %}">
|
||||
»
|
||||
</a>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
|
||||
{% endwith %}
|
||||
+9
@@ -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>
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -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"
|
||||
Reference in New Issue
Block a user