Check for errors and other values at the widget level - maybe using custom form field
Asked Answered
T

3

7

How can I access if a field has)errors at the level of widget?

Using default I tried:

{% if widget.attributes.has_errors %} or {% if widget.has_errors %}

but are not working.

I use custom widget templates, I'm thinking to use a custom form Field and overwrite the default field.

I know clean method exist but I don't know how to push to the widget the dynamic(non default) data/attributes I want.

I tried:

class AWidget(forms.Widget):

    def get_context(self, name, value, attrs):

        context = super().get_context(name, value, attrs)
        has_errors = context['widget']['attrs'].pop('has_errors', None)
         context['widget']['has_errors'] = has_errors

It works for errors but I don't know if is the best option plus I want to pass other values/attributes from Form Field,and I think will be better to try to overwrite the Form Field but I don't know exactly how.

Also accessing individual attributes using:

 {{ widget.attrs.maxlength }} or  {{ widget.attrs.items.maxlength }}

even if accedes in a for loop works


I know I can add a parent div with a class of error:

 <div class="{% if form.field.errors %}pass_error{% endif %}">
        {{ form.field }} 
    </div>

but, that implies big changes at the css level.

I already overwrite all Django widgets with custom widgets, on error I don't need just to change a border color, but to show or not different elements of the widget template and the position of some of them change.

I already modify the based widget to add errors, but I'm looking to do it in a more elegant way at the field level by passing from the field to the widget, parameters depending on error type.

So my question is what I need to overwrite to pass from field to widget errors and other variables ?

Tabitha answered 2/2, 2019 at 9:25 Comment(0)
G
3

Not sure whether this could help in your specific use case ... but just in case, please note that when you build your form in the view, you can add extra parameters as needed, then pass them down to your custom widget.

Working example:

file "forms.py"

from django import forms


def build_ingredient_form(unit):
    """
    Ingredient form factory

    Here we build the form class dynamically, in order to acces 'unit' via closure.
    References:
        https://mcmap.net/q/149865/-django-passing-custom-form-parameters-to-formset#623030
    """
    class IngredientForm(forms.Form):
        #quantity = forms.DecimalField(max_digits=10)
        quantity = UnitField(unit, required=False)
        ...
    return IngredientForm


file "fields.py"

from django import forms
from .fields import UnitField


class UnitField(forms.CharField):
    """
    Custom field to support UnitWidget

    References:
        - http://tothinkornottothink.com/post/10815277049/django-forms-i-custom-fields-and-widgets-in
    """

    def __init__(self, unit, *args, **kwargs):
        self.unit = unit
        super(UnitField, self).__init__(*args, **kwargs)
        self.widget = UnitWidget(unit)

    ...

file "widgets.py"

from django import forms
from .models import Unit


class UnitWidget(forms.TextInput):

    def __init__(self, unit, attrs=None):
        if unit is None:
            self.unit = Unit()
        else:
            self.unit = unit

    ...

Grammar answered 7/2, 2019 at 14:5 Comment(0)
M
2

Well a widget is how you will render the field's data/value into the HTML rendered template, that's the only function of widgets, look the following example taken from the docs:

>>> name = forms.TextInput(attrs={'required': True})
>>> name.render('name', 'A name')
'<input name="name" type="text" value="A name" required>'
>>>
>>> name = forms.TextInput(attrs={'required': False})
>>> name.render('name', 'A name')
'<input name="name" type="text" value="A name">'

So, widgets are not aware of the data is valid(has errors) or not and should remain that way.

Is not a good idea to handle any data error/validation at the widget level, you want, I can ensure that, that if you change how your field looks like (the widget), your validations keeps working.

Said that ...

How can I access field errors?

When you are rendering a form you can do it field by field lets take this form by example:

class LoginForm(forms.Form):
    username = forms.CharField(max_length=255)
    password = forms.CharField(widget=forms.PasswordInput)

you can write to temlate:

<form action="." method="get">
   <p>{{ loginform.username.label }}: {{ loginform.username }}</p>
   <p>{{ loginform.password.label }}: {{ loginform.password}}</p>
    <button type="submit">submit</button>
</form>

And this will render something like the following:

enter image description here

Now, suppose your form won't admit passwords with less than 8 characters:

class LoginForm(forms.Form):
    username = forms.CharField(max_length=255)
    password = forms.CharField(widget=forms.PasswordInput)

    def clean_password(self):
        password = self.cleaned_data['password']
        if len(password) < 8:
            raise forms.ValidationError(
            "Password must have at least 8 characters, it has only %(password_length)s",
            code='invalid password',
            params={'password_length': len(password)}
        )
        return password

You can access the password errors like this:

<form action="." method="get">
    {% csrf_token %}
    <p>{{ form.username.label }}: {{ form.username }}</p>
    <p>{{ form.password.label }}: {{ form.password}}</p>

    <ul>
        {% for error in form.password.errors %}
            <li>{{ error }}</li>    
        {% endfor %}
    </ul>

   <button type="submit">submit</button>
</form>

And now if you type a short password ...

enter image description here

I want the control to look different if there are errors.

You can add some style if there are errors just use {% if ... %} in your template code:

 <p>
    {{ form.password.label }}:
    <span class="{% if form.password.errors %}pass_error{% endif %}">
        {{ form.password }} 
    </span>
 </p>

With CSS:

 <style>
    .pass_error input {
        border-color: red;
    }
 </style>

And this is the result:

enter image description here

Conlusion.

Validate and handle data errors in the form or using validators, use widgets for display the data, of course, you can customize how the data is presented since you can specify a custom template for your widget.

I also recommend django-widget-twaeks if you want to add attributes to your widget in template code. This apps allows you to write code like (example from the app docs):

{% load widget_tweaks %}

<!-- change input type (e.g. to HTML5) -->
{% render_field form.search_query type="search" %}

<!-- add/change several attributes -->
{% render_field form.text rows="20" cols="20" title="Hello, world!" %}

<!-- append to an attribute -->
{% render_field form.title class+="css_class_1 css_class_2" %}

<!-- template variables can be used as attribute values -->
{% render_field form.text placeholder=form.text.label %}
Mannerheim answered 7/2, 2019 at 3:54 Comment(0)
H
0

Django adds 'aria-invalid': 'true' to widget.attrs when the field is invalid. So in the widgets template you can add a check like this.

{% if 'aria-invalid' in widget.attrs %}
...
{% else %}
...
{% endif %}
Hoe answered 15/9 at 13:37 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.