When I define a range control widget, "min" value isn't being utilized

The Meta portion of my form definition (in forms.py) defines this range control widget:

widgets = {
		'slider': NumberInput(attrs={'type': 'range', 'class': 'form-range', 'min': '1', 'max': '10'})
	}

All the values are passed along correctly except “min”. Other values like “step” also work fine. No matter what value I assign to min, it renders as “0”:

<input type="range" name="slider" class="form-control form-range" min="0" max="10">

I’m baffled why “min” isn’t working and “max” is. I’m starting to suspect a bug in either Bootstrap 5 or django-bootstrap-v5, which are in use. I wonder, can anyone else reproduce this behaviour?

Thanks for any help.

I’ve created a simple form that renders perfectly. (It’s a regular form and not a model form, but that shouldn’t affect the render process.)

You could help determine whether it’s related to bootstrap by changing the class, or printing the form in the server to see what the view is actually rendering.

Oh, when I create the same range control as a simple form directly in a template it renders correctly. It’s only the model-based form that is causing me grief.

Good call on checking the form in the server; I looked at it immediately after instantiating, and lo and behold:

widget:
   attrs: {'min': 0, 'max': '10'}

I’ve also removed the Bootstrap class. Something definitely seems fishy with how the model form is being instantiated!

Edit: Interesting, I just noticed that the 0 is a number and the ‘10’ is a string …

Do you have any model-level constraints on that field?

This is the field in the model definition:

slider = models.PositiveIntegerField(default=1, blank=False, validators=[MinValueValidator(1), MaxValueValidator(10)])

Nothing too strange. There is no reference to “min” anywhere else in my application.

I can’t recreate the displayed symptoms with the information presented here. When I create a model form on a model with an integer field with defined validators, the attrs attribute on the NumberInput widget works as expected.

What versions of Python and Django are you using? Other than bootstrap, are you using any third-party libraries designed to enhance models?

Also interesting, if I change

'min': '1', 'max': '10'

in the widget definition from string to integers (as maybe they should be?)

'min': 1, 'max': 10

then the form gets instantiated with both as integers:

widget:
   attrs: {'min': 0, 'max': 10}

But ‘min’ is still somehow being set to zero.

I did the same test on another control in the same form, another NumberInput widget, and min passes through as expected. In that case it was based on a DecimalField in the model instead of the PositiveIntegerField used here.

In addition to removing the bootstrap class, have you tried disabling the bootstrap app?

(And if you missed my question in my previous comment, what versions of Python, Django, django-bootstrap are you using?)

Python version 3.9.0
Django version 3.2.8

I have django-bootstrap-v5 installed, version 1.0.11. This is a third party tool to help style Django forms in Bootstrap 5. Like I said, I have suspicions it may be causing this issue … but I don’t think I have the skill to be thoroughly debugging third party libraries. :frowning:

I haven’t fully uninstalled it or Bootstrap because I’m a noob at Python, Django, and Bootstrap, and removing it all from my application at this point would be a headache. :face_holding_back_tears:

You might want to find the support channels for that library then and ask the question over there.

What happens if you just remove the application from your INSTALLED_APPS setting? Does that cause things to break?

You might be able to disable just enough to determine whether that application is causing a problem or not.

Either way, you’ve already got a headache - it’s a widget that isn’t rendering properly, and I see no evidence that the problem is being caused by Django.

I’ve been testing this further via console now. Not through my app, and without even rendering any template. It appears to have nothing to do with range controls: it happens for any model I use, but only for fields of PositiveIntegerField type specifically.

In my console:

from tradeapp.forms import TestForm
f = TestForm()
f

Output:
<tr><th><label for="id_num_items">Num items:</label></th><td><input type="number" name="num_items" value="0" min="0" max="5" id="id_num_items"></td></tr>

The form is defined as:

class TestForm(ModelForm):
	class Meta:
		model = Event
		fields = ['num_items']
		widgets = {
			'num_items': NumberInput(attrs={'min': 1, 'max': 5})
		}

This model is defined as:

class Event(models.Model):
	num_items = models.PositiveIntegerField(default=1, blank=True)

Maybe there’s a chance that a some library is still lingering somewhere, but I don’t know how/why, since the bootstrap library is only involved in rendering to a template - which I’m not even getting as far as here.

(Edit: btw yes, I did remove bootstrap5 from my INSTALLED_APPS to run these tests.)

Could it be relevant that I’m using MySQL as a database backend??

It kind of makes sense that Django would attempt to enforce some sort of minimum of zero on PositiveIntegerFields …

I really appreciate your help so far btw! :pray:

Edit: Also interesting is the difference between these two console outputs on the same form object:

print(f.Meta.widgets['num_items'].attrs)

Output:
{'min': 1, 'max': 5}

and

print(f)

Output:
<tr><th><label for="id_num_items">Num items:</label></th><td><input type="number" name="num_items" value="0" min="0" max="5" id="id_num_items"></td></tr>

It could possibly be affected by a third-party library, if that library overrides one of the templates being used by the rendering engine for that input type. From what I can see, this field would be using attrs.html, input.html, and number.html. If you have a library that overrides any of these templates, that could be affecting this.

Can you post your INSTALLED_APPS setting?

Also, look for any files by those names in a forms/widgets directory anywhere else in your project or your virtual environment.

(Note, the database engine is not involved at all.)

I haven’t really installed anything other than the bootstrap5 library.

INSTALLED_APPS = [
    'tradeapp.apps.TradeappConfig',
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'bootstrap5',
]

I don’t see any html files of those names in my project or in the source of the django-bootstrap-v5 library, nor any references to them. :man_shrugging: I also can’t find any forms or widgets directories in my project, but there is a widgets subdir in the 3rd party code … but it only contains one tiny file called “radio_select_button_group.html”, which doesn’t look relevant. But it’s not my code. :slight_smile:

I’ll raise the issue with those guys as well, at least to see if anyone can reproduce this behaviour.

Bingo - I think I found it. Should have thought to check this earlier.

Looking at the definition of django.db.models.fields.__init__.PositiveIntegerField, here’s the complete definition:

class PositiveIntegerField(PositiveIntegerRelDbTypeMixin, IntegerField):
    description = _("Positive integer")

    def get_internal_type(self):
        return "PositiveIntegerField"

    def formfield(self, **kwargs):
        return super().formfield(
            **{
                "min_value": 0,
                **kwargs,
            }
        )

See How to create custom model fields | Django documentation | Django and Model field reference | Django documentation | Django.

So I think that you can’t set this using the attrs on the widget, you might need to either create your own subclass of that field class or find some other way to change what the formfield function is going to return. (I don’t have any specific ideas yet, but I’m curious enough to do some more investigation.)

1 Like

Wow, that would certainly be it! Glad to see I’m not crazy.

You don’t think this should be entered as a bug? Anyone trying to enforce a range for any PositiveIntegerField will be confused (as I was), when they are expecting a range of 10-100 on a control and instead get 0-100. It’s especially bad for range controls because they even look visually different (wider range and thumb knob in wrong position). At the very least it should be documented.

A good fix would seem like it should only set min_value to 0 if it doesn’t already have a value set.

I’m not sure either way.

The model field is a PositiveIntegerField, which is defined in part as follows:

Like an IntegerField, but must be either positive or zero (0 ).

I could at least see that it might be worth making it explicitly clear somewhere in the docs that this is the behavior.

However, regardless of the model field definition, you always have the option of using a different form field type that will address this specific issue.

For example, working with your Event model above, you could define the form as:

class TestForm(ModelForm):
    num_items = IntegerField(min_value=2, max_value=12,
        widget=NumberInput(attrs={'type': 'range'}))
    class Meta:
        model = Event
        fields = ['num_items']

I’m still looking at a couple things…

My own opinion is that Django forcing min_value to ‘0’ is equivalent to forcing max_value to ‘2147483647’. Or for a signed integer field, forcing min_value to be ‘-2147483648’.

Those numbers are already the hard database field limits. The purpose of the min/max values on form controls is to be able limit the user to values that are narrower than those of the field itself, i.e. 1-10. Setting min_value to ‘0’ is basically disabling the use of a min value! In the same sense that forcing max_value to ‘2147483647’ would prevent setting a desired max limit, be that 10, 100, or whatever.

At best, it’s very confusing to have min overridden and max not. :confused: :confused:

Anyway, you’re right that I could just define my form field decoratively here instead of in the Meta class. I may start doing that more often. One quick question there: Declaring the field as you have done above now ignores any “default” value that is set in the model. Do I have to set “initial” to the same value again here (duplicating my work)?

They really do serve two different purposes - a ModelForm will automatically create form fields based upon attributes of the model. Those form fields can be modified using the Meta class settings. However, if you need a different form field type, you may need to create a new form field - or take other steps.

For example:

You also have the option to alter attributes of a form field in the __init__ method of the form or by altering the form after it has been created. (e.g. self.fields['num_items'].default = whatever)
(Yes, you could probably also change min_value in this location rather than replacing the field.)