MyModel has one ManyToManyField that links to AnotherModel.
For validation purposes I need to check the model after m2m relationships have been already set in the database, by applying custom logic implemented with database queries (practically no way to implement them in a different way)
At the moment, I’m raising ValidationError inside a signal receiver:
@receiver(m2m_changed, sender=MyModel.relationship.through)
def validator_from_signal(sender, instance, action, reverse, model, **kwargs):
if action == 'post_add':
if reverse and not check_reverse(instance):
msg = f'AnotherModel {instance.id} violates validation checks.'
logging.error(msg)
raise ValidationError(msg)
elif not reverse and not check(instance):
msg = f'MyModel {instance.id} violates validation checks.'
logging.error(msg)
raise ValidationError(msg)
It works, thanks to the transaction behaviour which in case of exception rollbacks everything, but if the user submits a wrong MyModel or AnotherModel (because of ManyToMany illegal relationships), in particular with the Admin Form, they get a 500 internal server error. Which isn’t very user friendly…
I opened a ticket to ask for support of ValidationError / IntegrityError coming from signals, but it was rejected under the “probably there is another solution” reason (see #33832 (Support M2M validation using signals) – Django), so now I’m asking for advice to the community. How would you implement such logic?
I can’t implement this check inside the clean() method:
because m2m fields are not populated on the database at that stage
Let’s see if I understand what you’re saying here.
You have two models:
class MyModel(...):
name = CharField(...)
class OtherModel(...):
name = CharField(...)
my_groups = ManyToManyField(MyModel, ...)
And you have a view where you’re adding instances of either (or both) of MyModel and OtherModel to the database, along with adding a relationship between them:
currently I’m leveraging the ModelAdmin as a view;
my_model would be not valid, because the relation is mandatory but it is being created created/updated with illegal relationship to instance(s) of AnotherModel. The expected behavior is a complete rollback on the addition/change of my_model, and an error should be displayed on the admin panel as for usual ValidationError(s)
and yes, validation should be run after the model is “added” with all the relationship inside the database, even though inside the transaction
Currently this use-case appears only in the admin panel, so I would be happy today with an admin-based solution, but I’d like a more general approach if one day I’ll expose a similar view to normal users
One more verification - the situation is that you cannot have an instance of MyModel without there being at least one related entry in the many-to-many join table? And so you’re adding a MyModel entry and in the same view, adding the relationships to OtherModel?
Personally, I wouldn’t even think of trying to do this in the Admin. It would seem to me to be a whole lot easier to do this in a user-written view. Wrap both processes in a transaction and roll it back if the validation fails.
In such cases, I tend to fall back on the principle stated in the Django Admin docs:
If you need to provide a more process-centric interface that abstracts away the implementation details of database tables and fields, then it’s probably time to write your own views.
I need to leverage ModelAdmin for time constraints on the development, which is one of the reasons I chose Django as a framework. The only thing I need now is a place where to put my code that can raise ValidationError(s) and see those catched and displayed properly, which signals are not… for some reason
is there some method override on the model admin I could use? I feel like there should be
one hint is that currently, when my signal raises ValidationError, it goes through the admin view in the stack trace… so technically speaking it would be viable to put a try/catch somewhere maybe
The admin’s recommended use is limited to an organization’s internal management tool. It’s not intended for building your entire front end around.
I have seen too many cases where people think they can ignore this advice - and it ends up costing them time overall.
There are clearly situations where the admin is the wrong tool - you’ll spend more time trying to make it do exactly what you want, when you can crank out a generic view in a lot less time.
Now, whether or not this situation is one of those cases, I can’t say for sure. But it sure feels to me like you’re expending a lot more effort on this than what’s necessary for the stated requirement.
Admins aren’t allowed to add relationships that would cause a “wrong” model, based on arbitrary validation rules, coded with queries that I’d like to run after relationships have been added. I don’t see how that would go beyond the scope of an admin panel
class MyForm(ModelForm):
def clean(self):
ok = False
with transaction.atomic(savepoint=True, durable=False):
ok = check(self.save(commit=True))
transaction.set_rollback(True)
if not ok:
raise ValidationError(f'MyModel {self.instance.id} violates validation checks.')
return super().clean()
@admin.register(MyModel)
class MyModelAdmin(admin.ModelAdmin):
form = MyForm