Django: Validation on a Many-to-Many Through model - django

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],]

Related

Separate fields for Users and admins in Django models.py

I have a BooleanField in my models.py file.
I want it to be True for all Admins/Superusers by default, while I want it to be False for all other users by default.
I don't want to use if-else template tags in my html for this purpose, and I am hoping to get a more cleaner solution.
Something like:
field_name = models.BooleanField(
if author.is_superuser:
default = True
else:
default=False
)
Any help is appreciated.
Here the author is the author of the 'Post' which is the model class.
Edit 1: The user.is_superuser which I need is actually the author's user. I need to verify whether the author of a Blog post is a superuser or not, and then assign the default value for the Boolean field.
You don't have any notion of a "current user" in a model definition. But you do have access to the current user in your views (as request.user) so you can use it there to pass the proper initial value for your field
as an example (using function based view):
def myview(request, ....):
if request.method == "POST":
# ...
else:
form = YourModelForm(initial={"field_name": request.user.is_superuser})
EDIT
I don't need the user, but the author's user
What can I say ? If you want better answers, write better questions - you didn't put any context in your question, not even the relevan parts of your model.
This being said, model fields default values are only used in two cases: when creating a new instance, and when adding a new fields to an existing model.
In the first case, your Article or whatever model instance doesn't exist yet, so it cannot have an "author" yet. IOW the "author" is either the request user (most common case and very probably yours), or some other user you explicitely provide. In both cases you do have the right user instance at hand and my answer above applies (either as is or with some adaptation that you're suppoed to be able to do by yourself).
In the second case, that's something you deal with with a data migration, quite simply.

M2M relationship validation in Django

I have these two models:
class Test(models.Model):
problems = models.ManyToManyField('Problem')
...
class Problem(models.Model):
type = models.CharField(max_length=3, choices=SOME_CHOICES)
...
Now, while adding Problems to a Test, I need to limit the number of particular type of problems in the Test. E.g. a Test can contain only 3 Problems of type A, and so on.
The only way to validate this seems to be by using m2m_changed signal on Test.problems.through table. However, to do the validation, I need to access the current Problem being added AND the existing Problems - which doesn't seem to be possible somehow.
What is the correct way to do something like this? M2M validation seems to be a topic untouched in the docs. What am I missing?
You are right on the part that you have to register an m2m_changed signal function like the following:
def my_callback(sender, instance, action, reverse, model, pk_set, **kwargs)
If you read the documentation you 'll see that sender is the object-model that triggers the change and model is the object-model that will change. pk_set will give you the pkeys that will be the new reference for your model. So in your Test model you have to do something like this:
#receiver(m2m_changed)
def my_callback(sender, instance, action, reverse, model, pk_set, **kwargs):
if action == "pre_add":
problem_types = [x.type for x in model.objects.filter(id__in=pk_set)]
if problem_types.count("A") > some_number:
raise SomeException
Mind though that an Exception at that level will not be caught if you're entering fields from Django admin site. To be able to provide user friendly errors for django admin data entry, you'll have to register your own form as admin form. In your case, you need to do the following:
class ProblemTypeValidatorForm(ModelForm):
def clean(self):
super(ProblemTypeValidatorForm, self).clean()
problem_types = [x.type for x in self.cleaned_data.get("problems") if x]
if problem_types.count("A") > some_number:
raise ValidationError("Cannot have more than {0} problems of type {1}"
.format(len(problem_types), "A")
then in your admin.py
#admin.register(Test)
class TestAdmin(admin.ModelAdmin):
form = ProblemTypeValidatorForm
Now keep in mind that these are two different level implementations. None will protect you from someone doing manually this:
one_test_object.problems.add(*Problem.objects.all())
one_test_object.save()
Personal opinion:
So keeping in mind the above, I suggest you go with the ModelForm & ModelAdmin approach and if you're providing an API for CRUD operations, make your validations there as well. Nothing can protect you from someone entering stuff in your db through django shell. If you want such solution types you should go directly to your db and write some kind of magic trigger script. But keep in mind that your db is actually data. Your backend is the one with the business logic. So you shouldn't really try to impose business rules down to the db level. Keep the rules in your backend by validating your data at the spots where create/update happens.
You can't override save for a M2M I'm afraid, but you can achieve what you want.
Use the m2m_changed signal where the action is pre_add.
The 'instance' kwarg will be the Test model the problem is being added to.
The 'pk_id' kwarg will be the primary key of the Problems being added (1 or more).
The validation logic will be something like this:
p_type = Problem.objects.get(id=kwargs['pk_id']).type
type_count = kwargs['instance'].problems.filter(type=p_type).count()
if p_type == 'A' and type_count == 3:
raise Exception("cannot have more than 3 Problems of type A")
[sorry don't have django on hand to verify the query]

Django making sure user and user profile have same pk

Right now I'm using Django's built in admin system to manage users, to which I've attached a profile to contain additional data using the following:
class Profile(models.Model):
user = models.OneToOneField(User, editable = False)
# Data fields here...
As it stands the User and Profile pk (and accordingly id number) will be the same if and only if the profile is created right after the user is created. I could guarantee that this would be the case during the registration process, and while that would cover most uses, creating users with the admin interface could cause mismatched ids to occur. Thus this does not seem like a very robust way to solve this problem and I'd like to hardcode the pk's to be the same. I'm not sure how to do this.
I thought the following would work:
profile_id = models.IntegerField(default=user.pk, editable = False,
primary_key = True)
But it gives me the error:
AttributeError: 'OneToOneField' has no attribute 'pk'
What's the best way to guarantee that the profile and user have the same pk? Note: I'd really rather not deal with extending the base user model as using the OneToOneField to link the two seems to be sufficient for all my needs.
Thanks!
[edit]
My reasoning for asking the question:
My immediate problem was that I wanted a dictionary of values of the User's Profile, which I was retrieving usingprofile_values = Profile.objects.filter(pk=user.id).values()[0]. This highlighted the bug, and I "hacked" around it last night using pk=user.profile.id instead. In the light of the morning this does not seem like such a terrible hack. However, it seems like having pk discrepancies could lead to quiet and hard to catch bugs down the line, and thus forcing them to match up would be a Good Idea. But I'm new to Django so I'd entirely accept that it is, in fact, never a problem if you're writing your code correctly. That said, for almost academic reasons, I'd be curious to see how this might be solved.
[/edit]
Like you already agree that it was never a problem because we have a OneToOne mapping between the two models.
So when you need to get the profile obj corresponding to a User:
profile_values = Profile.objects.get(user_id=user)
assuming,
class Profile(models.Model):
user = models.OneToOneField(User)
...
If your column name is not user, then use the corresponding name in get query.
Still if you are curious as to how to achieve same pk for both models, then we can set a signal on every save of User model. See the documentation.
def create_profile(sender, **kwargs):
if kwargs["created"]:
p = Profile(user=kwargs["instance"], ...)
p.save()
django.db.models.signals.post_save.connect(create_profile, sender=User)
create_profile() will be called every time any User object is saved.
In this function, we create Profile object only if a new User instance has been created.
If we start from blank slate, then I think this will always make sure that a Profile exists for every User and is created right after User was created; which in turn will give same pk for both models.
pk is a parameter in a filter() query, but not a field name. You probably want to use user.id.

Django Admin - Validating inlines together with main models

I have quite a complex validation requirement, and I cannot get Django admin to satisfy it.
I have a main model (django.contrib.auth.models.User) and several models which look like
class SomeProfile(models.Model):
user = models.OneToOneField(User)
# more fields
I want to check that, if the user belongs to some group, then it has the corresponding profile. So if user is in group Foo he should have a non empty FooProfile.
Where do I put this validation rule? I cannot put it in the model. Indeed, the user is not created yet when the form is validated, hence I cannot access his groups. So I need to resort to form validation. This is what I put:
class UserAdminForm(forms.ModelForm):
"""
A custom form to add validation rules which cannot live in the
model. We check that users belonging to various groups actually
have the corresponding profiles.
"""
class Meta:
model = User
def clean(self):
# Here is where I would like to put the validation
class FooInline(admin.TabularInline):
model = FooProfile
max_num = 1
class UserAdmin(admin.ModelAdmin):
model = User
form = UserAdminForm
inlines = [FooInline]
admin.site.register(User, UserAdmin)
My problem is that inside UserAdminForm.clean() I do not have access to the data posted inside the inlines. So I can tell whether the user is in group Foo by inspecting self.cleaned_data['groups'], but I have no way to tell whether a FooProfile was transmitted.
How do I check this validation requirement?
Edit:
I try to explain the issue better, because there has been a misunderstading in an answer.
I have an issue when I create a new user. The fact is that the profiles are mandatory (according to the groups). Say an admin creates a new user; then I have to add inlines in the admin form for the various GroupProfiles.
How do I check that the right profiles are not null? I cannot use the clean() method of the User model, because in there I cannot check what groups the user belongs to: it has not been created yet.
I can only access the information about the groups in the clean() method of the form - but there I do not have the information about the profiles, since this information is submitted trhough inlines.
1
well i have been looking around, how all this stuff works, and i found one question very similar here.
2
There are one way to get all the data at the same time maybe with this you can find the answer to your problem
class UserAdminForm(forms.ModelForm):
"""
A custom form to add validation rules which cannot live in the
model. We check that users belonging to various groups actually
have the corresponding profiles.
"""
class Meta:
model = User
def clean(self):
self.data # <--here is all the data of the request
self.data['groups']
self.data['profile_set-0-comments'] # some field
# some validations
return self.cleaned_data

Django - How to save m2m data via post_save signal?

(Django 1.1) I have a Project model that keeps track of its members using a m2m field. It looks like this:
class Project(models.Model):
members = models.ManyToManyField(User)
sales_rep = models.ForeignKey(User)
sales_mgr = models.ForeignKey(User)
project_mgr = models.ForeignKey(User)
... (more FK user fields) ...
When the project is created, the selected sales_rep, sales_mgr, project_mgr, etc Users are added to members to make it easier to keep track of project permissions. This approach has worked very well so far.
The issue I am dealing with now is how to update the project's membership when one of the User FK fields is updated via the admin. I've tried various solutions to this problem, but the cleanest approach seemed to be a post_save signal like the following:
def update_members(instance, created, **kwargs):
"""
Signal to update project members
"""
if not created: #Created projects are handled differently
instance.members.clear()
members_list = []
if instance.sales_rep:
members_list.append(instance.sales_rep)
if instance.sales_mgr:
members_list.append(instance.sales_mgr)
if instance.project_mgr:
members_list.append(instance.project_mgr)
for m in members_list:
instance.members.add(m)
signals.post_save.connect(update_members, sender=Project)
However, the Project still has the same members even if I change one of the fields via the admin! I have had success updating members m2m fields using my own views in other projects, but I never had to make it play nice with the admin as well.
Is there another approach I should take other than a post_save signal to update membership? Thanks in advance for your help!
UPDATE:
Just to clarify, the post_save signal works correctly when I save my own form in the front end (old members are removed, and new ones added). However, the post_save signal does NOT work correctly when I save the project via the admin (members stay the same).
I think Peter Rowell's diagnosis is correct in this situation. If I remove the "members" field from the admin form the post_save signal works correctly. When the field is included, it saves the old members based on the values present in the form at the time of the save. No matter what changes I make to the members m2m field when project is saved (whether it be a signal or custom save method), it will always be overwritten by the members that were present in the form prior to the save. Thanks for pointing that out!
Having had the same problem, my solution is to use the m2m_changed signal. You can use it in two places, as in the following example.
The admin upon saving will proceed to:
save the model fields
emit the post_save signal
for each m2m:
emit pre_clear
clear the relation
emit post_clear
emit pre_add
populate again
emit post_add
Here you have a simple example that changes the content of the saved data before actually saving it.
class MyModel(models.Model):
m2mfield = ManyToManyField(OtherModel)
#staticmethod
def met(sender, instance, action, reverse, model, pk_set, **kwargs):
if action == 'pre_add':
# here you can modify things, for instance
pk_set.intersection_update([1,2,3])
# only save relations to objects 1, 2 and 3, ignoring the others
elif action == 'post_add':
print pk_set
# should contain at most 1, 2 and 3
m2m_changed.connect(receiver=MyModel.met, sender=MyModel.m2mfield.through)
You can also listen to pre_remove, post_remove, pre_clear and post_clear. In my case I am using them to filter one list ('active things') within the contents of another ('enabled things') independent of the order in which lists are saved:
def clean_services(sender, instance, action, reverse, model, pk_set, **kwargs):
""" Ensures that the active services are a subset of the enabled ones.
"""
if action == 'pre_add' and sender == Account.active_services.through:
# remove from the selection the disabled ones
pk_set.intersection_update(instance.enabled_services.values_list('id', flat=True))
elif action == 'pre_clear' and sender == Account.enabled_services.through:
# clear everything
instance._cache_active_services = list(instance.active_services.values_list('id', flat=True))
instance.active_services.clear()
elif action == 'post_add' and sender == Account.enabled_services.through:
_cache_active_services = getattr(instance, '_cache_active_services', None)
if _cache_active_services:
instance.active_services.add(*list(instance.enabled_services.filter(id__in=_cache_active_services)))
delattr(instance, '_cache_active_services')
elif action == 'pre_remove' and sender == Account.enabled_services.through:
# de-default any service we are disabling
instance.active_services.remove(*list(instance.active_services.filter(id__in=pk_set)))
If the "enabled" ones are updated (cleared/removed + added back, like in admin) then the "active" ones are cached and cleared in the first pass ('pre_clear') and then added back from the cache after the second pass ('post_add').
The trick was to update one list on the m2m_changed signals of the other.
I can't see anything wrong with your code, but I'm confused as to why you think the admin should work any different from any other app.
However, I must say I think your model structure is wrong. I think you need to get rid of all those ForeignKey fields, and just have a ManyToMany - but use a through table to keep track of the roles.
class Project(models.Model):
members = models.ManyToManyField(User, through='ProjectRole')
class ProjectRole(models.Model):
ROLES = (
('SR', 'Sales Rep'),
('SM', 'Sales Manager'),
('PM', 'Project Manager'),
)
project = models.ForeignKey(Project)
user = models.ForeignKey(User)
role = models.CharField(max_length=2, choices=ROLES)
I've stuck on situation, when I needed to find latest item from set of items, that connected to model via m2m_field.
Following Saverio's answer, following code solved my issue:
def update_item(sender, instance, action, **kwargs):
if action == 'post_add':
instance.related_field = instance.m2m_field.all().order_by('-datetime')[0]
instance.save()
m2m_changed.connect(update_item, sender=MyCoolModel.m2m_field.through)