Reputation: 137
I want to create a model in Django where I could introduce integer values for a given field, with limits depending on the values already introduced for that field. Specifically, I would like the value introduced to be in the range of (min = 1 | max = maximum value for that field +1) The code should be something like that in the models.py file:
from django.db import models
from django.core.validators import MaxValueValidator, MinValueValidator
class MyModel(models.Model):
max_myfield = MyModel.objects.filter(myfield=self.myfield).aggregate(Max('myfield'))["myfield_max"]
myfield = models.IntegerField(validators=[MinValueValidator=1, MaxValueValidator=max_myfield + 1])
The problem with this is that max_myfield is using the MyModel itself, raising a NameError as it is not defined yet.
In addition, if there is no record in the table, the max value shall be also 1, like the minimum.
EDIT: I want to simplify the question to the following: What should I do in the models.py file of a Django application, if I want to define a model with fields which validators shall depend somehow on the model fields values?
Upvotes: 1
Views: 7665
Reputation: 56620
The trick to this is a little something I just noticed in the validator docs:
class MaxValueValidator(limit_value, message=None)
Raises a ValidationError with a code of 'max_value' if value is greater than limit_value, which may be a callable.
You don't want your maximum value to be the same for every record, so you can pass in a method that queries the database to find the current max and add 1. To avoid referencing the class before it's declared you can either pull the method out of the class or wrap the call in a lambda expression. I chose to pull it out.
Try changing your models.py
to look like this:
from django.core.validators import MinValueValidator, MaxValueValidator
from django.db import models
from django.db.models import Max
def get_max_myfield() -> int:
max_found = MyModel.objects.aggregate(Max('myfield'))["myfield__max"]
if max_found is None:
return 1
return max_found + 1
class MyModel(models.Model):
myfield = models.IntegerField(validators=[
MinValueValidator(1),
MaxValueValidator(get_max_myfield)])
Here's the same model code in an example that lets you run a complete Django app in one file and experiment with it. The first three calls to clean_fields()
validate correctly, and the fourth one complains that myfield
is too big.
# Tested with Django 3.1 and Python 3.8.
import logging
import sys
import django
from django.apps import apps
from django.apps.config import AppConfig
from django.conf import settings
from django.core.exceptions import ValidationError
from django.db import connections, models, DEFAULT_DB_ALIAS
from django.db.models import Max
from django.db.models.base import ModelBase
from django.core.validators import MaxValueValidator, MinValueValidator
NAME = 'udjango'
DB_FILE = NAME + '.db'
def main():
setup()
logger = logging.getLogger(__name__)
def get_max_myfield() -> int:
max_found = MyModel.objects.aggregate(Max('myfield'))["myfield__max"]
if max_found is None:
return 1
return max_found + 1
class MyModel(models.Model):
myfield = models.IntegerField(validators=[
MinValueValidator(1),
MaxValueValidator(get_max_myfield)])
syncdb(MyModel)
m1 = MyModel(myfield=1)
m1.clean_fields()
m1.save()
m2a = MyModel(myfield=2)
m2a.clean_fields()
m2a.save()
m2b = MyModel(myfield=2)
m2b.clean_fields()
m2b.save()
m101 = MyModel(myfield=101)
try:
m101.clean_fields()
assert False, "Should have raised ValidationError."
except ValidationError:
logger.info("Raised validation error, as expected.")
logger.info('Max allowed is %d.', get_max_myfield())
def setup():
with open(DB_FILE, 'w'):
pass # wipe the database
settings.configure(
DEBUG=True,
DATABASES={
DEFAULT_DB_ALIAS: {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': DB_FILE}},
LOGGING={'version': 1,
'disable_existing_loggers': False,
'formatters': {
'debug': {
'format': '%(asctime)s[%(levelname)s]'
'%(name)s.%(funcName)s(): %(message)s',
'datefmt': '%Y-%m-%d %H:%M:%S'}},
'handlers': {
'console': {
'level': 'DEBUG',
'class': 'logging.StreamHandler',
'formatter': 'debug'}},
'root': {
'handlers': ['console'],
'level': 'INFO'},
'loggers': {
"django.db": {"level": "DEBUG"}}})
app_config = AppConfig(NAME, sys.modules['__main__'])
apps.populate([app_config])
django.setup()
original_new_func = ModelBase.__new__
@staticmethod
def patched_new(cls, name, bases, attrs):
if 'Meta' not in attrs:
class Meta:
app_label = NAME
attrs['Meta'] = Meta
return original_new_func(cls, name, bases, attrs)
ModelBase.__new__ = patched_new
def syncdb(model):
""" Standard syncdb expects models to be in reliable locations.
Based on https://github.com/django/django/blob/1.9.3
/django/core/management/commands/migrate.py#L285
"""
connection = connections[DEFAULT_DB_ALIAS]
with connection.schema_editor() as editor:
editor.create_model(model)
main()
Upvotes: 1