• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

zostera / django-bootstrap4 / 8388360828

22 Mar 2024 09:43AM UTC coverage: 90.998%. First build
8388360828

Pull #696

github

web-flow
Bump dependabot/fetch-metadata from 1.6.0 to 2.0.0

Bumps [dependabot/fetch-metadata](https://github.com/dependabot/fetch-metadata) from 1.6.0 to 2.0.0.
- [Release notes](https://github.com/dependabot/fetch-metadata/releases)
- [Commits](https://github.com/dependabot/fetch-metadata/compare/v1.6.0...v2.0.0)

---
updated-dependencies:
- dependency-name: dependabot/fetch-metadata
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Pull Request #696: Bump dependabot/fetch-metadata from 1.6.0 to 2.0.0

304 of 376 branches covered (80.85%)

Branch coverage included in aggregate %.

1283 of 1368 relevant lines covered (93.79%)

14.95 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

90.82
/src/bootstrap4/renderers.py
1
from bs4 import BeautifulSoup
16✔
2
from django.forms import (
16✔
3
    BaseForm,
4
    BaseFormSet,
5
    BoundField,
6
    CheckboxInput,
7
    CheckboxSelectMultiple,
8
    DateInput,
9
    EmailInput,
10
    FileInput,
11
    MultiWidget,
12
    NumberInput,
13
    PasswordInput,
14
    RadioSelect,
15
    Select,
16
    SelectDateWidget,
17
    TextInput,
18
    URLInput,
19
)
20
from django.utils.html import conditional_escape, escape, strip_tags
16✔
21
from django.utils.safestring import mark_safe
16✔
22

23
from .bootstrap import get_bootstrap_setting
16✔
24
from .exceptions import BootstrapError
16✔
25
from .forms import (
16✔
26
    FORM_GROUP_CLASS,
27
    is_widget_with_placeholder,
28
    render_field,
29
    render_form,
30
    render_form_group,
31
    render_label,
32
)
33
from .text import text_value
16✔
34
from .utils import DJANGO_VERSION, add_css_class, render_template_file
16✔
35

36
try:
16✔
37
    # If Django is set up without a database, importing this widget gives RuntimeError
38
    from django.contrib.auth.forms import ReadOnlyPasswordHashWidget
16✔
39
except RuntimeError:
×
40
    ReadOnlyPasswordHashWidget = None
×
41

42

43
class BaseRenderer:
16✔
44
    """A content renderer."""
45

46
    def __init__(self, *args, **kwargs):
16✔
47
        self.layout = kwargs.get("layout", "")
16✔
48
        self.form_group_class = kwargs.get("form_group_class", FORM_GROUP_CLASS)
16✔
49
        self.field_class = kwargs.get("field_class", "")
16✔
50
        self.label_class = kwargs.get("label_class", "")
16✔
51
        self.show_help = kwargs.get("show_help", True)
16✔
52
        self.show_label = kwargs.get("show_label", True)
16✔
53
        self.exclude = kwargs.get("exclude", "")
16✔
54

55
        self.set_placeholder = kwargs.get("set_placeholder", True)
16✔
56
        self.size = self.parse_size(kwargs.get("size", ""))
16✔
57
        self.horizontal_label_class = kwargs.get(
16✔
58
            "horizontal_label_class", get_bootstrap_setting("horizontal_label_class")
59
        )
60
        self.horizontal_field_class = kwargs.get(
16✔
61
            "horizontal_field_class", get_bootstrap_setting("horizontal_field_class")
62
        )
63

64
    def parse_size(self, size):
16✔
65
        size = text_value(size).lower().strip()
16✔
66
        if size in ("sm", "small"):
16✔
67
            return "small"
16✔
68
        if size in ("lg", "large"):
16✔
69
            return "large"
16✔
70
        if size in ("md", "medium", ""):
16!
71
            return "medium"
16✔
72
        raise BootstrapError('Invalid value "%s" for parameter "size" (expected "sm", "md", "lg" or "").' % size)
×
73

74
    def get_size_class(self, prefix="form-control"):
16✔
75
        if self.size == "small":
16✔
76
            return prefix + "-sm"
16✔
77
        if self.size == "large":
16✔
78
            return prefix + "-lg"
16✔
79
        return ""
16✔
80

81
    def _render(self):
16✔
82
        return ""
×
83

84
    def render(self):
16✔
85
        return mark_safe(self._render())
16✔
86

87

88
class FormsetRenderer(BaseRenderer):
16✔
89
    """Default formset renderer."""
90

91
    def __init__(self, formset, *args, **kwargs):
16✔
92
        if not isinstance(formset, BaseFormSet):
16✔
93
            raise BootstrapError('Parameter "formset" should contain a valid Django Formset.')
16✔
94
        self.formset = formset
16✔
95
        super().__init__(*args, **kwargs)
16✔
96

97
    def render_management_form(self):
16✔
98
        return text_value(self.formset.management_form)
16✔
99

100
    def render_form(self, form, **kwargs):
16✔
101
        return render_form(form, **kwargs)
16✔
102

103
    def render_forms(self):
16✔
104
        rendered_forms = []
16✔
105
        for form in self.formset.forms:
16✔
106
            rendered_forms.append(
16✔
107
                self.render_form(
108
                    form,
109
                    layout=self.layout,
110
                    form_group_class=self.form_group_class,
111
                    field_class=self.field_class,
112
                    label_class=self.label_class,
113
                    show_label=self.show_label,
114
                    show_help=self.show_help,
115
                    exclude=self.exclude,
116
                    set_placeholder=self.set_placeholder,
117
                    size=self.size,
118
                    horizontal_label_class=self.horizontal_label_class,
119
                    horizontal_field_class=self.horizontal_field_class,
120
                )
121
            )
122
        return "\n".join(rendered_forms)
16✔
123

124
    def get_formset_errors(self):
16✔
125
        return self.formset.non_form_errors()
16✔
126

127
    def render_errors(self):
16✔
128
        formset_errors = self.get_formset_errors()
16✔
129
        if formset_errors:
16!
130
            return render_template_file(
×
131
                "bootstrap4/form_errors.html",
132
                context={"errors": formset_errors, "form": self.formset, "layout": self.layout},
133
            )
134
        return ""
16✔
135

136
    def _render(self):
16✔
137
        return "".join([self.render_errors(), self.render_management_form(), self.render_forms()])
16✔
138

139

140
class FormRenderer(BaseRenderer):
16✔
141
    """Default form renderer."""
142

143
    def __init__(self, form, *args, **kwargs):
16✔
144
        if not isinstance(form, BaseForm):
16✔
145
            raise BootstrapError('Parameter "form" should contain a valid Django Form.')
16✔
146
        self.form = form
16✔
147
        super().__init__(*args, **kwargs)
16✔
148
        self.error_css_class = kwargs.get("error_css_class", None)
16✔
149
        self.required_css_class = kwargs.get("required_css_class", None)
16✔
150
        self.bound_css_class = kwargs.get("bound_css_class", None)
16✔
151
        self.alert_error_type = kwargs.get("alert_error_type", "non_fields")
16✔
152
        self.form_check_class = kwargs.get("form_check_class", "form-check")
16✔
153

154
    def render_fields(self):
16✔
155
        rendered_fields = []
16✔
156
        for field in self.form:
16✔
157
            rendered_fields.append(
16✔
158
                render_field(
159
                    field,
160
                    layout=self.layout,
161
                    form_group_class=self.form_group_class,
162
                    field_class=self.field_class,
163
                    label_class=self.label_class,
164
                    form_check_class=self.form_check_class,
165
                    show_label=self.show_label,
166
                    show_help=self.show_help,
167
                    exclude=self.exclude,
168
                    set_placeholder=self.set_placeholder,
169
                    size=self.size,
170
                    horizontal_label_class=self.horizontal_label_class,
171
                    horizontal_field_class=self.horizontal_field_class,
172
                    error_css_class=self.error_css_class,
173
                    required_css_class=self.required_css_class,
174
                    bound_css_class=self.bound_css_class,
175
                )
176
            )
177
        return "\n".join(rendered_fields)
16✔
178

179
    def get_fields_errors(self):
16✔
180
        form_errors = []
16✔
181
        for field in self.form:
16✔
182
            if not field.is_hidden and field.errors:
16✔
183
                form_errors += field.errors
16✔
184
        return form_errors
16✔
185

186
    def render_errors(self, type="all"):
16✔
187
        form_errors = None
16✔
188
        if type == "all":
16✔
189
            form_errors = self.get_fields_errors() + self.form.non_field_errors()
16✔
190
        elif type == "fields":
16✔
191
            form_errors = self.get_fields_errors()
16✔
192
        elif type == "non_fields":
16✔
193
            form_errors = self.form.non_field_errors()
16✔
194

195
        if form_errors:
16✔
196
            return render_template_file(
16✔
197
                "bootstrap4/form_errors.html",
198
                context={"errors": form_errors, "form": self.form, "layout": self.layout, "type": type},
199
            )
200

201
        return ""
16✔
202

203
    def _render(self):
16✔
204
        return self.render_errors(self.alert_error_type) + self.render_fields()
16✔
205

206

207
class FieldRenderer(BaseRenderer):
16✔
208
    """Default field renderer."""
209

210
    # These widgets will not be wrapped in a form-control class
211
    WIDGETS_NO_FORM_CONTROL = (CheckboxInput, RadioSelect, CheckboxSelectMultiple, FileInput)
16✔
212

213
    def __init__(self, field, *args, **kwargs):
16✔
214
        if not isinstance(field, BoundField):
16✔
215
            raise BootstrapError('Parameter "field" should contain a valid Django BoundField.')
16✔
216
        self.field = field
16✔
217
        super().__init__(*args, **kwargs)
16✔
218

219
        self.widget = field.field.widget
16✔
220
        self.is_multi_widget = isinstance(field.field.widget, MultiWidget)
16✔
221
        self.initial_attrs = self.widget.attrs.copy()
16✔
222
        self.field_help = text_value(mark_safe(field.help_text)) if self.show_help and field.help_text else ""
16✔
223
        self.field_errors = [conditional_escape(text_value(error)) for error in field.errors]
16✔
224
        self.form_check_class = kwargs.get("form_check_class", "form-check")
16✔
225

226
        if "placeholder" in kwargs:
16!
227
            # Find the placeholder in kwargs, even if it's empty
228
            self.placeholder = kwargs["placeholder"]
×
229
        elif get_bootstrap_setting("set_placeholder"):
16!
230
            # If not found, see if we set the label
231
            self.placeholder = field.label
16✔
232
        else:
233
            # Or just set it to empty
234
            self.placeholder = ""
×
235
        if self.placeholder:
16!
236
            self.placeholder = text_value(self.placeholder)
16✔
237

238
        self.addon_before = kwargs.get("addon_before", self.widget.attrs.pop("addon_before", ""))
16✔
239
        self.addon_after = kwargs.get("addon_after", self.widget.attrs.pop("addon_after", ""))
16✔
240
        self.addon_before_class = kwargs.get(
16✔
241
            "addon_before_class", self.widget.attrs.pop("addon_before_class", "input-group-text")
242
        )
243
        self.addon_after_class = kwargs.get(
16✔
244
            "addon_after_class", self.widget.attrs.pop("addon_after_class", "input-group-text")
245
        )
246

247
        # These are set in Django or in the global BOOTSTRAP4 settings, and
248
        # they can be overwritten in the template
249
        error_css_class = kwargs.get("error_css_class", None)
16✔
250
        required_css_class = kwargs.get("required_css_class", None)
16✔
251
        bound_css_class = kwargs.get("bound_css_class", None)
16✔
252
        if error_css_class is not None:
16✔
253
            self.error_css_class = error_css_class
16✔
254
        else:
255
            self.error_css_class = getattr(field.form, "error_css_class", get_bootstrap_setting("error_css_class"))
16✔
256
        if required_css_class is not None:
16✔
257
            self.required_css_class = required_css_class
16✔
258
        else:
259
            self.required_css_class = getattr(
16✔
260
                field.form, "required_css_class", get_bootstrap_setting("required_css_class")
261
            )
262
        if bound_css_class is not None:
16✔
263
            self.success_css_class = bound_css_class
16✔
264
        else:
265
            self.success_css_class = getattr(field.form, "bound_css_class", get_bootstrap_setting("success_css_class"))
16✔
266

267
        # If the form is marked as form.empty_permitted, do not set required class
268
        if self.field.form.empty_permitted:
16✔
269
            self.required_css_class = ""
16✔
270

271
    def restore_widget_attrs(self):
16✔
272
        self.widget.attrs = self.initial_attrs.copy()
16✔
273

274
    def add_class_attrs(self, widget=None):
16✔
275
        if widget is None:
16!
276
            widget = self.widget
×
277
        classes = widget.attrs.get("class", "")
16✔
278
        if ReadOnlyPasswordHashWidget is not None and isinstance(widget, ReadOnlyPasswordHashWidget):
16!
279
            # Render this is a static control
280
            classes = add_css_class(classes, "form-control-static", prepend=True)
×
281
        elif not isinstance(widget, self.WIDGETS_NO_FORM_CONTROL):
16✔
282
            classes = add_css_class(classes, "form-control", prepend=True)
16✔
283
            # For these widget types, add the size class here
284
            classes = add_css_class(classes, self.get_size_class())
16✔
285
        elif isinstance(widget, CheckboxInput):
16✔
286
            classes = add_css_class(classes, "form-check-input", prepend=True)
16✔
287
        elif isinstance(widget, FileInput):
16!
288
            classes = add_css_class(classes, "form-control-file", prepend=True)
×
289

290
        if self.field.errors:
16✔
291
            if self.error_css_class:
16✔
292
                classes = add_css_class(classes, self.error_css_class)
16✔
293
        else:
294
            if self.field.form.is_bound:
16✔
295
                classes = add_css_class(classes, self.success_css_class)
16✔
296

297
        widget.attrs["class"] = classes
16✔
298

299
    def add_placeholder_attrs(self, widget=None):
16✔
300
        if widget is None:
16!
301
            widget = self.widget
×
302
        placeholder = widget.attrs.get("placeholder", self.placeholder)
16✔
303
        if placeholder and self.set_placeholder and is_widget_with_placeholder(widget):
16✔
304
            # TODO: Should this be stripped and/or escaped?
305
            widget.attrs["placeholder"] = placeholder
16✔
306

307
    def add_help_attrs(self, widget=None):
16✔
308
        if widget is None:
16!
309
            widget = self.widget
×
310
        if not isinstance(widget, CheckboxInput):
16✔
311
            widget.attrs["title"] = widget.attrs.get("title", escape(strip_tags(self.field_help)))
16✔
312

313
    def add_widget_attrs(self):
16✔
314
        if self.is_multi_widget:
16✔
315
            widgets = self.widget.widgets
16✔
316
        else:
317
            widgets = [self.widget]
16✔
318
        for widget in widgets:
16✔
319
            self.add_class_attrs(widget)
16✔
320
            self.add_placeholder_attrs(widget)
16✔
321
            self.add_help_attrs(widget)
16✔
322

323
    def list_to_class(self, html, klass):
16✔
324
        classes = add_css_class(klass, self.get_size_class())
16✔
325
        if DJANGO_VERSION >= 4:
16✔
326
            soup = BeautifulSoup(html, features="html.parser")
14✔
327
            enclosing_div = soup.find("div")
14✔
328
            enclosing_div.attrs["class"] = classes
14✔
329
            for inner_div in enclosing_div.find_all("div"):
14✔
330
                inner_div.attrs["class"] = inner_div.attrs.get("class", []) + [self.form_check_class]
14✔
331
        else:
332
            mapping = [
2✔
333
                ("<ul", f'<div class="{classes}"'),
334
                ("</ul>", "</div>"),
335
                ("<li", f'<div class="{self.form_check_class}"'),
336
                ("</li>", "</div>"),
337
            ]
338
            for k, v in mapping:
2✔
339
                html = html.replace(k, v)
2✔
340
            soup = BeautifulSoup(html, features="html.parser")
2✔
341
        # Apply bootstrap4 classes to labels and inputs.
342
        # A simple 'replace' isn't enough as we don't want to have several 'class' attr definition, which would happen
343
        # if we tried to 'html.replace("input", "input class=...")'
344
        enclosing_div = soup.find("div", {"class": classes})
16✔
345
        if enclosing_div:
16✔
346
            for label in enclosing_div.find_all("label"):
16✔
347
                label.attrs["class"] = label.attrs.get("class", []) + ["form-check-label"]
16✔
348
                try:
16✔
349
                    label.input.attrs["class"] = label.input.attrs.get("class", []) + ["form-check-input"]
16✔
350
                except AttributeError:
14✔
351
                    pass
14✔
352
        return str(soup)
16✔
353

354
    def add_checkbox_label(self, html):
16✔
355
        return html + render_label(
16✔
356
            content=self.field.label,
357
            label_for=self.field.id_for_label,
358
            label_title=escape(strip_tags(self.field_help)),
359
            label_class="form-check-label",
360
        )
361

362
    def fix_date_select_input(self, html):
16✔
363
        div1 = '<div class="col-4">'
×
364
        div2 = "</div>"
×
365
        html = html.replace("<select", div1 + "<select")
×
366
        html = html.replace("</select>", "</select>" + div2)
×
367
        return f'<div class="row bootstrap4-multi-input">{html}</div>'
×
368

369
    def fix_file_input_label(self, html):
16✔
370
        if self.layout != "horizontal":
×
371
            html = "<br>" + html
×
372
        return html
×
373

374
    def post_widget_render(self, html):
16✔
375
        if isinstance(self.widget, CheckboxSelectMultiple):
16✔
376
            html = self.list_to_class(html, "checkbox")
16✔
377
        elif isinstance(self.widget, RadioSelect):
16✔
378
            html = self.list_to_class(html, "radio radio-success")
16✔
379
        elif isinstance(self.widget, SelectDateWidget):
16!
380
            html = self.fix_date_select_input(html)
×
381
        elif isinstance(self.widget, CheckboxInput):
16✔
382
            html = self.add_checkbox_label(html)
16✔
383
        elif isinstance(self.widget, FileInput):
16!
384
            html = self.fix_file_input_label(html)
×
385
        return html
16✔
386

387
    def wrap_widget(self, html):
16✔
388
        if isinstance(self.widget, CheckboxInput):
16✔
389
            # Wrap checkboxes
390
            # Note checkboxes do not get size classes, see #318
391
            html = f'<div class="form-check">{html}</div>'
16✔
392
        return html
16✔
393

394
    def make_input_group_addon(self, inner_class, outer_class, content):
16✔
395
        if not content:
16✔
396
            return ""
16✔
397
        if inner_class:
16✔
398
            content = f'<span class="{inner_class}">{content}</span>'
16✔
399
        return f'<div class="{outer_class}">{content}</div>'
16✔
400

401
    @property
16✔
402
    def is_input_group(self):
16✔
403
        allowed_widget_types = (TextInput, PasswordInput, DateInput, NumberInput, Select, EmailInput, URLInput)
16✔
404
        return (self.addon_before or self.addon_after) and isinstance(self.widget, allowed_widget_types)
16✔
405

406
    def make_input_group(self, html):
16✔
407
        if self.is_input_group:
16✔
408
            before = self.make_input_group_addon(self.addon_before_class, "input-group-prepend", self.addon_before)
16✔
409
            after = self.make_input_group_addon(self.addon_after_class, "input-group-append", self.addon_after)
16✔
410
            html = self.append_errors(f"{before}{html}{after}")
16✔
411
            html = f'<div class="input-group">{html}</div>'
16✔
412
        return html
16✔
413

414
    def append_help(self, html):
16✔
415
        field_help = self.field_help or None
16✔
416
        if field_help:
16✔
417
            help_html = render_template_file(
16✔
418
                "bootstrap4/field_help_text.html",
419
                context={
420
                    "field": self.field,
421
                    "field_help": field_help,
422
                    "layout": self.layout,
423
                    "show_help": self.show_help,
424
                },
425
            )
426
            html += help_html
16✔
427
        return html
16✔
428

429
    def append_errors(self, html):
16✔
430
        field_errors = self.field_errors
16✔
431
        if field_errors:
16✔
432
            errors_html = render_template_file(
16✔
433
                "bootstrap4/field_errors.html",
434
                context={
435
                    "field": self.field,
436
                    "field_errors": field_errors,
437
                    "layout": self.layout,
438
                    "show_help": self.show_help,
439
                },
440
            )
441
            html += errors_html
16✔
442
        return html
16✔
443

444
    def append_to_field(self, html):
16✔
445
        if isinstance(self.widget, CheckboxInput):
16✔
446
            # we have already appended errors and help to checkboxes
447
            # in append_to_checkbox_field
448
            return html
16✔
449

450
        if not self.is_input_group:
16✔
451
            # we already appended errors for input groups in make_input_group
452
            html = self.append_errors(html)
16✔
453

454
        return self.append_help(html)
16✔
455

456
    def append_to_checkbox_field(self, html):
16✔
457
        if not isinstance(self.widget, CheckboxInput):
16✔
458
            # we will append errors and help to normal fields later in append_to_field
459
            return html
16✔
460

461
        html = self.append_errors(html)
16✔
462
        return self.append_help(html)
16✔
463

464
    def get_field_class(self):
16✔
465
        field_class = self.field_class
16✔
466
        if not field_class and self.layout == "horizontal":
16✔
467
            field_class = self.horizontal_field_class
16✔
468
        return field_class
16✔
469

470
    def wrap_field(self, html):
16✔
471
        field_class = self.get_field_class()
16✔
472
        if field_class:
16✔
473
            html = f'<div class="{field_class}">{html}</div>'
16✔
474
        return html
16✔
475

476
    def get_label_class(self):
16✔
477
        label_class = self.label_class
16✔
478
        if not label_class and self.layout == "horizontal":
16✔
479
            label_class = self.horizontal_label_class
16✔
480
            label_class = add_css_class(label_class, "col-form-label")
16✔
481
        label_class = text_value(label_class)
16✔
482
        if not self.show_label or self.show_label == "sr-only":
16✔
483
            label_class = add_css_class(label_class, "sr-only")
16✔
484
        return label_class
16✔
485

486
    def get_label(self):
16✔
487
        if self.show_label == "skip":
16✔
488
            return None
16✔
489
        elif isinstance(self.widget, CheckboxInput):
16✔
490
            label = None
16✔
491
        else:
492
            label = self.field.label
16✔
493
        if self.layout == "horizontal" and not label:
16✔
494
            return mark_safe("&#160;")
16✔
495
        return label
16✔
496

497
    def add_label(self, html):
16✔
498
        label = self.get_label()
16✔
499
        if label:
16✔
500
            html = render_label(label, label_for=self.field.id_for_label, label_class=self.get_label_class()) + html
16✔
501
        return html
16✔
502

503
    def get_form_group_class(self):
16✔
504
        form_group_class = self.form_group_class
16✔
505
        if self.field.errors:
16✔
506
            if self.error_css_class:
16✔
507
                form_group_class = add_css_class(form_group_class, self.error_css_class)
16✔
508
        else:
509
            if self.field.form.is_bound:
16✔
510
                form_group_class = add_css_class(form_group_class, self.success_css_class)
16✔
511
        if self.field.field.required and self.required_css_class:
16✔
512
            form_group_class = add_css_class(form_group_class, self.required_css_class)
16✔
513
        if self.layout == "horizontal":
16✔
514
            form_group_class = add_css_class(form_group_class, "row")
16✔
515
        return form_group_class
16✔
516

517
    def wrap_label_and_field(self, html):
16✔
518
        return render_form_group(html, self.get_form_group_class())
16✔
519

520
    def _render(self):
16✔
521
        # See if we're not excluded
522
        if self.field.name in self.exclude.replace(" ", "").split(","):
16✔
523
            return ""
16✔
524
        # Hidden input requires no special treatment
525
        if self.field.is_hidden:
16✔
526
            return text_value(self.field)
16✔
527
        # Render the widget
528
        self.add_widget_attrs()
16✔
529
        html = self.field.as_widget(attrs=self.widget.attrs)
16✔
530
        self.restore_widget_attrs()
16✔
531
        # Start post render
532
        html = self.post_widget_render(html)
16✔
533
        html = self.append_to_checkbox_field(html)
16✔
534
        html = self.wrap_widget(html)
16✔
535
        html = self.make_input_group(html)
16✔
536
        html = self.append_to_field(html)
16✔
537
        html = self.wrap_field(html)
16✔
538
        html = self.add_label(html)
16✔
539
        html = self.wrap_label_and_field(html)
16✔
540
        return html
16✔
541

542

543
class InlineFieldRenderer(FieldRenderer):
16✔
544
    """Inline field renderer."""
545

546
    def add_error_attrs(self):
16✔
547
        field_title = self.widget.attrs.get("title", "")
×
548
        field_title += " " + " ".join([strip_tags(e) for e in self.field_errors])
×
549
        self.widget.attrs["title"] = field_title.strip()
×
550

551
    def add_widget_attrs(self):
16✔
552
        super().add_widget_attrs()
×
553
        self.add_error_attrs()
×
554

555
    def append_to_field(self, html):
16✔
556
        return html
×
557

558
    def get_field_class(self):
16✔
559
        return self.field_class
×
560

561
    def get_label_class(self):
16✔
562
        return add_css_class(self.label_class, "sr-only")
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc