Django: Validation on a Many-to-Many Through model
I have the following models (simplified example):
class Book(models.Model):
users = models.ManyToManyField(User, through=Permission)
class Permission(models.Model):
user = models.ForeignKey(User)
role = models.ForeignKey(Group)
active = models.BooleanField()
book = models.ForeignKey(Book)
What I need is that for a Book instance there cannot be more than one User of with the same Role and Active. So this is allowed:
Alice, Admin, False (not active), BookA
Dick, Admin, True (active), BookA
Chris, Editor, False (not active), BookA
Matt, Editor, False (not active), BookA
But this is not allowed:
Alice, Admin, True (active), BookA
Dick, Admin, True (active), BookA
Now this cannot be done with unique_together, because it only counts when active is True. I've tried to write a custom clean method (like how I have done here). But it seems that when you save a Book and it runs the validation on each Permission, the already validated Permission instances aren't saved until they've all been validated. This makes sense, because you don't want them to be saved in case something doesn't validate.
Could anyone tell me if there is a way to perform the validation described above?
P.S. I could imagine using the savepoint feature (http://docs.djangoproject.com/en/1.2/topics/db/transactions/), but I only really want to consider that as a last resort.
Maybe you can do something like:unique_together = [[book, role, active=1],]
?
Edit Sep. 23, 2010 14:00 Response to Manoj Govindan:
My admin.py (simplified version for clarity):
class BookAdmin(admin.ModelAdmin):
inlines = (PermissionInline,)
class PermissionInline(admin.TabularInline):
model = Permission
In the shell your validation would work.开发者_运维百科 Because you first have to create the book instance and then you create all the Permission instances one by one: http://docs.djangoproject.com/en/1.2/topics/db/models/#extra-fields-on-many-to-many-relationships. So in the shell if you add 2 Permission instances the 1st Permission instance has been saved by the time 2nd is being validated, and so the validation works.
However when you use the Admin interface and you add all the book.users instances at the same time via the book users inline, I believe it does all the validation on all the book.users instances first, before it saves them. When I tried it, the validation didn't work, it just succeeded without an error when there should have been a ValidationError.
You can use signals to prevent the saving of data that is invalid: I'm still working on a nice solution on how to get the validation to bubble up in a nice way in the admin.
@receiver(models.signals.m2m_changed, sender=Book.users.through)
def prevent_duplicate_active_user(sender, instance, action, reverse, model, pk_set, **kwargs):
if action != "pre_add":
return
if reverse:
# Editing the Permission, not the Book.
pass
else:
# At this point, look for already saved Users with the book/active.
if instance.permissions.filter(active=True).exists():
raise forms.ValidationError(...)
Note that this is not a complete solution, but is a pointer how I am doing something similar.
One way to do this is to use the newfangled model validation. Specifically, you can add a custom validate_unique
method to Permission
models to achieve this effect. For e.g.
from django.core.exceptions import ValidationError, NON_FIELD_ERRORS
class Permission(models.Model):
...
def validate_unique(self, exclude = None):
options = dict(book = self.book, role = self.role, active = True)
if Permission.objects.filter(**options).count() != 0:
template = """There cannot be more than one User of with the
same Role and Active (book: {0})"""
message = template.format(self.book)
raise ValidationError({NON_FIELD_ERRORS: [message]})
I did some rudimentary testing using one of my projects' Admin app and it seemed to work.
Now this cannot be done with unique_together, because it only counts when active is True.
Simplest way, imo, is to change type of active
from BooleanField to CharField. Store 'Y' and 'N' in active
.
That way you can use built-in unique_together = [[book, role, active],]
精彩评论