Django: how to validate m2m relationships? - python

Let's say I have a Basket model and I want to validate that no more than 5 Items can be added to it:
class Basket(models.Model):
items = models.ManyToManyField('Item')
def save(self, *args, **kwargs):
self.full_clean()
super(Basket, self).save(*args, **kwargs)
def clean(self):
super(Basket, self).clean()
if self.items.count() > 5:
raise ValidationError('This basket can\'t have so many items')
But when trying to save a Basket a RuntimeError is thrown because the maximum recursion depth is exceeded.
The error is the following:
ValueError: "<Basket: Basket>" needs to have a value for field "basket" before this many-to-many relationship can be used.
It happens in the if self.items.count() > 5: line.
Apparently Django's intricacies simply won't allow you to validate m2m relationships when saving a model. How can I validate them then?

You can never validate relationships in the clean method of the model. This is because at clean time, the model may not yet exist, as is the case with your Basket. Something that does not exist, can also not have relationships.
You either need to do your validation on the form data as pointed out by #bhattravii, or call form.save(commit=False) and implement a method called save_m2m, which implements the limit.
To enforce the limit at the model level, you need to listen to the m2m_changed signal. Note that providing feedback to the end user is a lot harder, but it does prevent overfilling the basket through different means.

I've been discussing this on the Django Developers list and have in fact tabled a method of doing this for consideration in the Django core in one form or another. The method is not fully tested nor finalised but results for now are very encouraging and I'm employing it on a site of mine with success.
In principle it relies on:
Using PostgreSQL as your database engine (we're fairly sure it won't
work on Lightdb or MySQL, but keen for anyone to test this)
enter code here
Overriding the post() method of your (Class based) view such that it:
Opens a an atomic transaction
Saves the form
Saves all the formsets if any
Calls Model.clean() or something else like Model.full_clean()
In your Model then, in the method called in 2.4 above you will see all your many to many and one to many relations in place. You can validate them and throw a ValidationError to see the whole transaction rolled back and no impact on the database.
This is working wonderfully for me:
def post(self, request, *args, **kwargs):
# The self.object atttribute MUST exist and be None in a CreateView.
self.object = None
self.form = self.get_form()
self.success_url = reverse_lazy('view', kwargs=self.kwargs)
if connection.vendor == 'postgresql':
if self.form.is_valid():
try:
with transaction.atomic():
self.object = self.form.save()
save_related_forms(self) # A separate routine that collects all the formsets in the request and saves them
if (hasattr(self.object, 'full_clean') and callable(self.object.full_clean)):
self.object.full_clean()
except (IntegrityError, ValidationError) as e:
if hasattr(e, 'error_dict') and isinstance(e.error_dict, dict):
for field, errors in e.error_dict.items():
for error in errors:
self.form.add_error(field, error)
return self.form_invalid(self.form)
return self.form_valid(self.form)
else:
return self.form_invalid(self.form)
else:
# The standard Djangop post() method
if self.form.is_valid():
self.object = self.form.save()
save_related_forms(self)
return self.form_valid(self.form)
else:
return self.form_invalid(self.form)
And the conversation on the Developers list is here:
https://groups.google.com/forum/#!topic/django-developers/pQ-8LmFhXFg
if you'd like to contribute any experience you gain from experimenting with this (perhaps with other database backends).
The one big caveat in the above approach is it delegates saving to the post() method which in the default view is done in the form_valid() method, so you need to override form_valid() as well, otherwise a post() like the one above will see you saving the form twice. Which is just a waste of time on an UpdateView but rather disastrous on a CreateView.

Related

How to override the default formfield for a read only foreign keys field in Django ModelAdmin?

I am overriding the default formfield of foreign keys on ModelAdmins as described here. However, I am not overriding it to return a subset, but instead to defer fields in order to optimize the performance. For example:
class MyModelAdmin(admin.ModelAdmin):
def formfield_for_foreignkey(self, db_field, request, **kwargs):
if db_field.name == "car":
kwargs["queryset"] = Car.objects.only("name")
return super().formfield_for_foreignkey(db_field, request, **kwargs)
It works for most of my use cases, but the problem occurres when the foreign key is set as a read only field. While debugging, I noticed that when it is set as read only, the field is never passed through formfield_for_foreignkey method and the query retrieving the foreign key selects all fields instead of only the necessary ones. In my case, some of the fields are too big causing an unecessary terrible performance.
I also tried the second method described in the docs, using ModelForm.__init__(), but it is not really useful for my use case.
Here I am answering my own question. I haven't managed to solve what the issue presents, but I found a workaround that in my case achieves the same result.
Since the read-only foreign-key on Django Admin page is shown as a link, I did this:
from django.utils.html import format_html
from django.urls import reverse
class MyModelAdmin(admin.ModelAdmin):
#admin.display(description="Car")
def link_to_car(self, obj):
link = reverse("admin:appname_car_change", args=[obj.car_id])
return format_html('{}'.format(link, Car.objects.all().only("name").get(id=obj.car_id)))
fields = ("link_to_car")
readonly_fields = ("link_to_car")
Well, as I said, it doesn't relate or solve the problem of overriding the formfield of a foreign key which is what I asked in the question, but it achieves the same result, showing the same thing while keeping the query optimized.
The only issue I had is having to write it in a kind of ugly way Car.objects.all().only("name").get(id=obj.car_id), since simply writing obj.car.name would've initiate an extra query selecting everything from Car.

Django select existing related record, or create new inline

I have a django model 'User' with a foreignkey to a related model 'Group'.
I am using a modelForm to render the form for creating a user, which allows the user to select a group from a dropdown of existing groups.
However, I'd like the option for the user to create a 'new' Group within that form if they don't find one they want in the list.
I know I could do an inline form, but I'm not sure how to accomplish that while retaining the ability to optionally select an existing related record.
Any advice?
After many hours of rearch, I have found a solution.
I tried many things, including overriding the clean() function on my form, however that required removing immutability and was messy to get the validation right.
Ultimately my solution was so sublcasss ModelChoiceField. In the model choice field, override the to_python() method with your logic to create related object if it does not exist. In addition, I passed this field a queryset paramater so that in my form I was able to pass the newly created object only to this form instance, but not show on every users form.
class FlexibleModelChoiceField(ModelChoiceField):
def __init__(self, queryset, *args, **kwargs):
super(FlexibleModelChoiceField, self).__init__(queryset, *args, **kwargs)
self.queryset = queryset
def to_python(self, value):
try:
# Logic to get or create the model instance object
return model_instance_object
except (ValueError, self.queryset.model.DoesNotExist):
raise ValidationError(self.error_messages['invalid_choice'], code='invalid_choice')

Django admin change list view disable sorting for some fields

Is there are way(s) to disable the sorting function for some fields in django admin change list so for those fields, users cannot click the column header to sort the list.
I tried on the following method, but it doesn't work.
https://djangosnippets.org/snippets/2580/
I also tired to override the changelist_view in ModelAdmin but also nothing happen.
def changelist_view(self, request, extra_context=None):
self.ordering_fields = ['id']
return super(MyModelAdmin, self).changelist_view(request, extra_context)
In the above case, I would like to only allow user to sort the list by ID.
Anyone has suggestion? Thanks.
For Django 1.7 (or the version that I last use) do not support such things. One possible dirty work-around could be defining a model class method and using that method instead of model field.
class TestClass(Model):
some_field = (.....)
other_field = (........)
def show_other_field(self):
return self.other_field
class TestClassAdmin(ModelAdmin):
list_display = ("some_field", "show_other_field")
Since show_other_field is a model class method, django do not knows how to sort (or process) the return result of that method.
But as I said, this is a dirty hack that might require more processing (and maybe more database calls) according to use-case than displaying a field of a model.
Extra: If you want to make a model method sortable, you must pass admin_order_field value like:
def show_other_field(self):
return self.other_field
show_other_field.admin_order_field = "other_field"
That will make your model method sortable in admin list_display. But you have to pass a field or relation that is usable in the order_by method of database api.
TestClass.objects.filter(....).order_by(<admin_order_field>)

Check if a form is valid from within a form field [Django]

I have subclassed a text-field form field in Django to create my own custom widget for a field. I was wondering if it's possible to check if all other fields of the form are valid (I want its server side behavior to vary based on the validation of other fields)
See comment
Something like:
class CustomField(TextInput):
def __init__(self, *args, **kwargs):
...
super(CustomField, self).__init__(*args, **kwargs)
input_type = 'hidden'
def value_from_datadict(self, data, files, name):
aws_file_key = data.get(name, None)
_media_bucket = boto.connect_s3(settings.AWS_ACCESS_KEY_ID,
settings.AWS_SECRET_ACCESS_KEY)\
.lookup(settings.AWS_MEDIA_STORAGE_BUCKET_NAME)
try:
key = _media_bucket.get_key(aws_file_key)
except:
print 'Failed to get key.'
key = None
if key and aws_file_key:
fh = tempfile.TemporaryFile()
key.get_contents_to_file(fh)
fh.seek(0)
files = SimpleUploadedFile(key.name, fh.read())
### IF FORM IS VALID DELETE KEY, OTHERWISE, KEEP IT.
if code_to_check_if_valid:
_media_bucket.delete_key(key)
fh.close()
return files
...... etc.
If you want to validate a certain field depending on the values of other fields, you need to to it at the form level and overwrite the field's clean method. Here's the docs on the subject - they are very good.
class CustomForm(forms.Form):
custom_field = CustomField()
def clean(self):
cleaned_data = super(CustomForm, self).clean()
custom_field = cleaned_data.get("custom_field")
...
If you look at the flow of how forms are validated, you will see that the clean method is run if all the other fields validate independently, so at this stage, the form can be considered valid:
These methods are run in the order given above, one field at a time. That is, for each field in the form (in the order they are declared in the form definition), the Field.clean() method (or its override) is run, then clean_<fieldname>(). Finally, once those two methods are run for every field, the Form.clean() method, or its override, is executed.
The final clean method is actually run regardless of if there's an error so you have to iterate through the cleaned_data to make sure there are no errors
The clean() method for the Form class or subclass is always run. If that method raises a ValidationError, cleaned_data will be an empty dictionary.
The previous paragraph means that if you are overriding Form.clean(), you should iterate through self.cleaned_data.items(), possibly considering the _errors dictionary attribute on the form as well. In this way, you will already know which fields have passed their individual validation requirements.
The clean methods for individual fields are called in the same order as the form declaration order or explicitly specified order. [django source code]
Although I wouldn't recommend this approach over using the clean method for multi-field validation, if your custom field is the last field in the order, you can expect self._errors to indicate whether all other fields have passed validation or not. However at this stage, non-field errors won't be available.

Is this the way to validate Django model fields?

As I understand it, when one creates a Django application, data is validated by the form before it's inserted into a model instance which is then written to the database. But if I want to create an additional layer of protection at the data model layer, is what I've done below the current "best practice?" I'm trying to ensure that a reviewer's name cannot be omitted nor be left blank. Should I be putting any custom validation in the 'clean' method as I've done here and then have 'save' call 'full_clean" which calls 'clean'? If not, what's the preferred method? Thanks.
class Reviewer(models.Model):
name = models.CharField(max_length=128, default=None)
def clean(self, *args, **kwargs):
if self.name == '':
raise ValidationError('Reviewer name cannot be blank')
super(Reviewer, self).clean(*args, **kwargs)
def full_clean(self, *args, **kwargs):
return self.clean(*args, **kwargs)
def save(self, *args, **kwargs):
self.full_clean()
super(Reviewer, self).save(*args, **kwargs)
Firstly, you shouldn't override full_clean as you have done. From the django docs on full_clean:
Model.full_clean(exclude=None)
This method calls Model.clean_fields(), Model.clean(), and Model.validate_unique(), in that order and raises a ValidationError that has a message_dict attribute containing errors from all three stages.
So the full_clean method already calls clean, but by overriding it, you've prevented it calling the other two methods.
Secondly, calling full_clean in the save method is a trade off. Note that full_clean is already called when model forms are validated, e.g. in the Django admin. So if you call full_clean in the save method, then the method will run twice.
It's not usually expected for the save method to raise a validation error, somebody might call save and not catch the resulting error. However, I like that you call full_clean rather than doing the check in the save method itself - this approach allows model forms to catch the problem first.
Finally, your clean method would work, but you can actually handle your example case in the model field itself. Define your CharField as
name = models.CharField(max_length=128)
The blank option will default to False. If the field is blank, a ValidationError will be raised when you run full_clean. Putting default=None in your CharField doesn't do any harm, but it is a bit confusing when you don't actually allow None as a value.
After thinking about Alasdair's answer and doing addtional reading, my sense now is that Django's models weren't designed so as to be validated on a model-only basis as I'm attempting to do. Such validation can be done, but at a cost, and it entails using validation methods in ways they weren't intended for.
Instead, I now believe that any constraints other than those that can be entered directly into the model field declarations (e.g. "unique=True") are supposed to be performed as a part of Form or ModelForm validation. If one wants to guard against entering invalid data into a project's database via any other means (e.g. via the ORM while working within the Python interpreter), then the validation should take place within the database itself. Thus, validation could be implemented on three levels: 1) First, implement all constraints and triggers via DDL in the database; 2) Implement any constraints available to your model fields (e.g. "unique=True"); and 3) Implement all other constraints and validations that mirror your database-level constraints and triggers within your Forms and ModelForms. With this approach, any form validation errors can be re-displayed to the user. And if the programmer is interacting directly with the database via the ORM, he/she would see the database exceptions directly.
Thoughts anyone?
Capturing the pre-save signals on on my models ensured clean will be called automatically.
from django.db.models.signals import pre_save
def validate_model(sender, **kwargs):
if 'raw' in kwargs and not kwargs['raw']:
kwargs['instance'].full_clean()
pre_save.connect(validate_model, dispatch_uid='validate_models')
Thanks #Kevin Parker for your answer, quite helpful!
It is common to have models in your app outside of the ones you define, so here is a modified version you can use to scope this behavior only to your own models or a specific app/module as desired.
from django.db.models.signals import pre_save
import inspect
import sys
MODELS = [obj for name, obj in
inspect.getmembers(sys.modules[__name__], inspect.isclass)]
def validate_model(sender, instance, **kwargs):
if 'raw' in kwargs and not kwargs['raw']:
if type(instance) in MODELS:
instance.full_clean()
pre_save.connect(validate_model, dispatch_uid='validate_models')
This code will run against any models defined inside the module where it is executed but you can adapt this to scope more strictly or to be a set of modules / apps if desired.

Categories