How to limit the maximum value of a numeric field in a Django model?
Asked Answered
V

10

226

Django has various numeric fields available for use in models, e.g. DecimalField and PositiveIntegerField. Although the former can be restricted to the number of decimal places stored and the overall number of characters stored, is there any way to restrict it to storing only numbers within a certain range, e.g. 0.0-5.0?

Failing that, is there any way to restrict a PositiveIntegerField to only store, for instance, numbers up to 50?

Update: now that Bug 6845 has been closed, this StackOverflow question may be moot.

Vietnamese answered 11/5, 2009 at 17:29 Comment(5)
You could create a pre-save signal: http://docs.djangoproject.com/en/dev/ref/signals/#django.db.models.signals.pre_saveMiramirabeau
I should have mentioned that I also want the restriction to be applied in Django's admin. For getting that, at least, the documentation has this to say: docs.djangoproject.com/en/dev/ref/contrib/admin/…Vietnamese
Actually, pre-1.0 Django seems to have had a really elegant solution: cotellese.net/2007/12/11/… . I wonder if there's an equally elegant way of doing this in the svn release of Django.Vietnamese
I'm disappointed to learn that there doesn't seem to be an elegant way to do this with the current Django svn. See this discussion thread for more details: groups.google.com/group/django-users/browse_thread/thread/…Vietnamese
Use validators on the model, and the validation will work in the admin interface and in ModelForms: docs.djangoproject.com/en/dev/ref/validators/…Mantoman
T
147

You could also create a custom model field type - see http://docs.djangoproject.com/en/dev/howto/custom-model-fields/#howto-custom-model-fields

In this case, you could 'inherit' from the built-in IntegerField and override its validation logic.

The more I think about this, I realize how useful this would be for many Django apps. Perhaps a IntegerRangeField type could be submitted as a patch for the Django devs to consider adding to trunk.

This is working for me:

from django.db import models

class IntegerRangeField(models.IntegerField):
    def __init__(self, verbose_name=None, name=None, min_value=None, max_value=None, **kwargs):
        self.min_value, self.max_value = min_value, max_value
        models.IntegerField.__init__(self, verbose_name, name, **kwargs)
    def formfield(self, **kwargs):
        defaults = {'min_value': self.min_value, 'max_value':self.max_value}
        defaults.update(kwargs)
        return super(IntegerRangeField, self).formfield(**defaults)

Then in your model class, you would use it like this (field being the module where you put the above code):

size = fields.IntegerRangeField(min_value=1, max_value=50)

OR for a range of negative and positive (like an oscillator range):

size = fields.IntegerRangeField(min_value=-100, max_value=100)

What would be really cool is if it could be called with the range operator like this:

size = fields.IntegerRangeField(range(1, 50))

But, that would require a lot more code since since you can specify a 'skip' parameter - range(1, 50, 2) - Interesting idea though...

Turbulent answered 11/5, 2009 at 18:31 Comment(2)
This works but when in the model's clean method, the value of the integer is always None which makes it so I cannot provide any additional cleaning to it. Any idea why this is and how to fix it?Bore
You can enhance your custom field by addingMinValueValidator(min_value) and MaxValueValidator(max_value) before calling super().__init__ ... (snippet: gist.github.com/madneon/147159f46ed478c71d5ee4950a9d697d)Sympathizer
T
507

You can use Django's built-in validators

from django.db.models import IntegerField, Model
from django.core.validators import MaxValueValidator, MinValueValidator

class CoolModelBro(Model):
    limited_integer_field = IntegerField(
        default=1,
        validators=[
            MaxValueValidator(100),
            MinValueValidator(1)
        ]
     )

Edit: When working directly with the model, make sure to call the model full_clean method before saving the model in order to trigger the validators. This is not required when using ModelForm since the forms will do that automatically.

Tactic answered 19/8, 2012 at 13:32 Comment(2)
I guess you have to write your own validator if you want the limited_integer_field to be optional as well? (Only Validate range if not blank) null=True, blank=True didn't do it..Briney
In Django 1.7 setting null=True and blank=True works as expected. The field is optional and if it's left blank it's stored as null.Odie
T
147

You could also create a custom model field type - see http://docs.djangoproject.com/en/dev/howto/custom-model-fields/#howto-custom-model-fields

In this case, you could 'inherit' from the built-in IntegerField and override its validation logic.

The more I think about this, I realize how useful this would be for many Django apps. Perhaps a IntegerRangeField type could be submitted as a patch for the Django devs to consider adding to trunk.

This is working for me:

from django.db import models

class IntegerRangeField(models.IntegerField):
    def __init__(self, verbose_name=None, name=None, min_value=None, max_value=None, **kwargs):
        self.min_value, self.max_value = min_value, max_value
        models.IntegerField.__init__(self, verbose_name, name, **kwargs)
    def formfield(self, **kwargs):
        defaults = {'min_value': self.min_value, 'max_value':self.max_value}
        defaults.update(kwargs)
        return super(IntegerRangeField, self).formfield(**defaults)

Then in your model class, you would use it like this (field being the module where you put the above code):

size = fields.IntegerRangeField(min_value=1, max_value=50)

OR for a range of negative and positive (like an oscillator range):

size = fields.IntegerRangeField(min_value=-100, max_value=100)

What would be really cool is if it could be called with the range operator like this:

size = fields.IntegerRangeField(range(1, 50))

But, that would require a lot more code since since you can specify a 'skip' parameter - range(1, 50, 2) - Interesting idea though...

Turbulent answered 11/5, 2009 at 18:31 Comment(2)
This works but when in the model's clean method, the value of the integer is always None which makes it so I cannot provide any additional cleaning to it. Any idea why this is and how to fix it?Bore
You can enhance your custom field by addingMinValueValidator(min_value) and MaxValueValidator(max_value) before calling super().__init__ ... (snippet: gist.github.com/madneon/147159f46ed478c71d5ee4950a9d697d)Sympathizer
H
106
from django.db import models
from django.core.validators import MinValueValidator, MaxValueValidator

size = models.IntegerField(validators=[MinValueValidator(0),
                                       MaxValueValidator(5)])
Huck answered 19/8, 2012 at 13:54 Comment(0)
H
57

I had this very same problem; here was my solution:

SCORE_CHOICES = zip( range(1,n), range(1,n) )
score = models.IntegerField(choices=SCORE_CHOICES, blank=True)
Horthy answered 28/10, 2010 at 21:38 Comment(1)
Using a list comprehension: models.IntegerField(choices=[(i, i) for i in range(1, n)], blank=True)Brassware
U
10

There are two ways to do this. One is to use form validation to never let any number over 50 be entered by a user. Form validation docs.

If there is no user involved in the process, or you're not using a form to enter data, then you'll have to override the model's save method to throw an exception or limit the data going into the field.

Uninstructed answered 11/5, 2009 at 17:39 Comment(4)
You can use a form for validating non-human input, too. It works great to populate the Form as a all-around validation technique.Mckean
After thinking on this, I'm quite sure I don't want to put the validation in a form. The question of which range of numbers is acceptable is as much a part of the model as is the question of which kind of number is acceptable. I don't want to have to tell every form through which the model is editable, just which range of numbers to accept. This would violate DRY, and besides, it's just plain inappropriate. So I'm going to look into overriding the model's save method, or perhaps creating a custom model field type - unless I can find an even better way :)Vietnamese
tghw, you said I could "override the model's save method to throw an exception or limit the data going into the field." How would I - from within the model definition's overriden save() method - make it so that if the number entered is outside a given range, the user receives a validation error much as if she had entered character data into a numeric field? I.e. is there some way I can do this that will work regardless of whether the user is editing via the admin or via some other form? I don't want to just limit the data going into the field without telling the user what's going on :) Thanks!Vietnamese
Even if you use the "save" method, this wont work when updating table through QuerySet like MyModel.object.filter(blabla).update(blabla) won't call save so no check will be madeHondo
B
5

Here is the best solution if you want some extra flexibility and don't want to change your model field. Just add this custom validator:

#Imports
from django.core.exceptions import ValidationError      

class validate_range_or_null(object):
    compare = lambda self, a, b, c: a > c or a < b
    clean = lambda self, x: x
    message = ('Ensure this value is between %(limit_min)s and %(limit_max)s (it is %(show_value)s).')
    code = 'limit_value'

    def __init__(self, limit_min, limit_max):
        self.limit_min = limit_min
        self.limit_max = limit_max

    def __call__(self, value):
        cleaned = self.clean(value)
        params = {'limit_min': self.limit_min, 'limit_max': self.limit_max, 'show_value': cleaned}
        if value:  # make it optional, remove it to make required, or make required on the model
            if self.compare(cleaned, self.limit_min, self.limit_max):
                raise ValidationError(self.message, code=self.code, params=params)

And it can be used as such:

class YourModel(models.Model):

    ....
    no_dependents = models.PositiveSmallIntegerField("How many dependants?", blank=True, null=True, default=0, validators=[validate_range_or_null(1,100)])

The two parameters are max and min, and it allows nulls. You can customize the validator if you like by getting rid of the marked if statement or change your field to be blank=False, null=False in the model. That will of course require a migration.

Note: I had to add the validator because Django does not validate the range on PositiveSmallIntegerField, instead it creates a smallint (in postgres) for this field and you get a DB error if the numeric specified is out of range.

Hope this helps :) More on Validators in Django.

PS. I based my answer on BaseValidator in django.core.validators, but everything is different except for the code.

Briney answered 17/9, 2014 at 20:45 Comment(0)
B
1

For example, you can use MaxValueValidator() and MinValueValidator() for models.DecimalField() and models.PositiveIntegerField() to set the available range of the value as shown below:

# "models.py"

from django.db import models
from django.core.validators import MaxValueValidator, MinValueValidator

class Test(models.Model):
    num1 = models.DecimalField(
        max_digits=3,
        decimal_places=1,
        validators=[
            MaxValueValidator(10.0),
            MinValueValidator(0.0)
        ],
    )
    num2 = models.PositiveIntegerField(
        validators=[
            MaxValueValidator(10),
            MinValueValidator(0)
        ],
    )

Finally, you better override num1 and num2 fields with forms.DecimalField() and forms.IntegerField() to set the available range of the value in Django Admin as shown below:

from django.contrib import admin
from .models import Test
from django import forms

class TestForm(forms.ModelForm):
    num1 = forms.DecimalField(
        max_value=10.0, min_value=0.0, step_size=0.1
    )
    num2 = forms.IntegerField(
        max_value=10, min_value=0
    )

@admin.register(Test)
class TestAdmin(admin.ModelAdmin):
    form = TestForm

In addition, the code with forms.NumberInput below is identical to the code above:

from django.contrib import admin
from .models import Test
from django import forms

class TestForm(forms.ModelForm):
    num1 = forms.DecimalField(
        widget=forms.NumberInput(attrs={'max': 10.0, 'min': 0.0, 'step': 0.1})
    )
    num2 = forms.IntegerField(
        widget=forms.NumberInput(attrs={'max': 10, 'min': 0})
    )

@admin.register(Test)
class TestAdmin(admin.ModelAdmin):
    form = TestForm
Blaubok answered 29/7, 2023 at 16:5 Comment(0)
I
0

In the forms.py

Class FloatForm(forms.ModelForm):

    class Meta:
        model = Float
        fields = ('name','country', 'city', 'point', 'year')

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.fields['point'] = forms.FloatField(max_value=100, min_value=1)
Incise answered 13/3, 2021 at 10:32 Comment(1)
Thank you for taking the time to provide an answer to the question. Since questions often receive multiple answers (this question has seven answers at the moment), it's encouraged to provide some context to your answer to describe why it is different/preferred over other answers.Twophase
S
0

It is worth mentioning that sometimes Django validation doesn't work as Django validation is mostly an application-level validation, not validation at the database level. Also, Model validation is not run automatically on the save/create/update of the model. If you want to validate your values instantly in your code then you need to do it manually — using the override save() method:

class UserRating():
    SCORE_CHOICES = (
        (1, _("Terrible")),
        (2, _("Poor")),
        (3, _("Average")),
        (4, _("Very Good")),
        (5, _("Excellent")),
    )
    score = models.PositiveSmallIntegerField(
        choices=SCORE_CHOICES, default=1, 
            validators=[
                MaxValueValidator(5),
                MinValueValidator(1)
            ]
    )
    
    def save(self, *args, **kwargs):
        if int(self.score) < 1 or int(self.score) > 5:
            raise ValidationError('Score must be located between 0 to 5')
        super(UserRating, self).save(*args, **kwargs)
    
    ...
Shafting answered 24/4, 2022 at 17:7 Comment(0)
M
0

Add validator like this your model column in models.py

class Planogram(models.Model):
    camera = models.ForeignKey(Camera, on_delete=models.CASCADE)
    xtl = models.DecimalField(decimal_places=10, max_digits=11,validators=[MaxValueValidator(1),MinValueValidator(0)])

if you are using create function to create objects change it to constructor like below.... and call fullclean() on that object and then save.. everything will work perfectly.

planogram = Planogram(camera_id = camera,xtl=xtl,ytl=ytl,xbr=xbr,ybr=ybr,product_id=product_id)
planogram.full_clean()
planogram.save()
Morphine answered 11/5, 2022 at 0:12 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.