Django model validation on related fields - django

When is the appropriate time to do validation on related fields in a model?
For example if I have a class Video that has a ManyToMany relationship with a class Playlist, when the Video is changed to 'private', it should be removed from all Playlists.
Doing this in the model's clean() method seems dangerous - since the model might fail validation and not save, but the Playlist references will have been deleted.
Is doing it in a post_save or pre_save signal the right way to go?

You have two choices:
First one is using a post_save signal that does the job. Not a pre_save, because saving can fail and I guess you only want to do that if the save worked right.
Other option is overriding model's save() method like:
def save(self, *args, **kwargs):
super(MyModel, self).save(*args, **kwargs)
# do stuff for removing whatever you want to remove
My personal choice is the first one because you deal with different models. If you only need to deal with the current one, I'll do the second one. But it's just a personal thought.

Related

How to trigger an event when django foreignkey is modified

I want to update fields in django admin site following the modification by the user of a given foreignkey field. I seek a way to trigger the event straight after foreignkey modification; not before/after save.
I looked for the creation of a custom signal through sender/receiver. Also I tried to find a similar signal as the m2m_changed. Perhaps one of these path could be the solution.
class Variety(models.Model):
specific_name = models.ForeignKey(CatalogList, on_delete=models.CASCADE)
def update_other_field(self):
#DO SOMETHING EACH TIME specific_name IS MODIFIED
Is there a way to trigger an event directly after a foreignkey is modified?
You can override save method of "parent" model :
class CatalogList(models.Model):
def save(self, *args, **kwargs):
super().save(*args, **kwargs) # Call normal save method
# Then do what you want with variety_set reverse relation

Django ModelForms: Display ManyToMany field as single-select

In a Django app, I'm having a model Bet which contains a ManyToMany relation with the User model of Django:
class Bet(models.Model):
...
participants = models.ManyToManyField(User)
User should be able to start new bets using a form. Until now, bets have exactly two participants, one of which is the user who creates the bet himself. That means in the form for the new bet you have to chose exactly one participant. The bet creator is added as participant upon saving of the form data.
I'm using a ModelForm for my NewBetForm:
class NewBetForm(forms.ModelForm):
class Meta:
model = Bet
widgets = {
'participants': forms.Select()
}
def save(self, user):
... # save user as participant
Notice the redefined widget for the participants field which makes sure you can only choose one participant.
However, this gives me a validation error:
Enter a list of values.
I'm not really sure where this comes from. If I look at the POST data in the developer tools, it seems to be exactly the same as if I use the default widget and choose only one participant. However, it seems like the to_python() method of the ManyToManyField has its problems with this data. At least there is no User object created if I enable the Select widget.
I know I could work around this problem by excluding the participants field from the form and define it myself but it would be a lot nicer if the ModelForm's capacities could still be used (after all, it's only a widget change). Maybe I could manipulate the passed data in some way if I knew how.
Can anyone tell me what the problem is exactly and if there is a good way to solve it?
Thanks in advance!
Edit
As suggested in the comments: the (relevant) code of the view.
def new_bet(request):
if request.method == 'POST':
form = NewBetForm(request.POST)
if form.is_valid():
form.save(request.user)
... # success message and redirect
else:
form = NewBetForm()
return render(request, 'bets/new.html', {'form': form})
After digging in the Django code, I can answer my own question.
The problem is that Django's ModelForm maps ManyToManyFields in the model to ModelMultipleChoiceFields of the form. This kind of form field expects the widget object to return a sequence from its value_from_datadict() method. The default widget for ModelMultipleChoiceField (which is SelectMultiple) overrides value_from_datadict() to return a list from the user supplied data. But if I use the Select widget, the default value_from_datadict() method of the superclass is used, which simply returns a string. ModelMultipleChoiceField doesn't like that at all, hence the validation error.
To solutions I could think of:
Overriding the value_from_datadict() of Select either via inheritance or some class decorator.
Handling the m2m field manually by creating a new form field and adjusting the save() method of the ModelForm to save its data in the m2m relation.
The seconds solution seems to be less verbose, so that's what I will be going with.
I don't mean to revive a resolved question but I was working a solution like this and thought I would share my code to help others.
In j0ker's answer he lists two methods to get this to work. I used method 1. In which I borrowed the 'value_from_datadict' method from the SelectMultiple widget.
forms.py
from django.utils.datastructures import MultiValueDict, MergeDict
class M2MSelect(forms.Select):
def value_from_datadict(self, data, files, name):
if isinstance(data, (MultiValueDict, MergeDict)):
return data.getlist(name)
return data.get(name, None)
class WindowsSubnetForm(forms.ModelForm):
port_group = forms.ModelMultipleChoiceField(widget=M2MSelect, required=True, queryset=PortGroup.objects.all())
class Meta:
model = Subnet
The problem is that ManyToMany is the wrong data type for this relationship.
In a sense, the bet itself is the many-to-many relationship. It makes no sense to have the participants as a manytomanyfield. What you need is two ForeignKeys, both to User: one for the creator, one for the other user ('acceptor'?)
You can modify the submitted value before (during) validation in Form.clean_field_name. You could use this method to wrap the select's single value in a list.
class NewBetForm(forms.ModelForm):
class Meta:
model = Bet
widgets = {
'participants': forms.Select()
}
def save(self, user):
... # save user as participant
def clean_participants(self):
data = self.cleaned_data['participants']
return [data]
I'm actually just guessing what the value proivded by the select looks like, so this might need a bit of tweaking, but I think it will work.
Here are the docs.
Inspired by #Ryan Currah I found this to be working out of the box:
class M2MSelect(forms.SelectMultiple):
def render(self, name, value, attrs=None, choices=()):
rendered = super(M2MSelect, self).render(name, value=value, attrs=attrs, choices=choices)
return rendered.replace(u'multiple="multiple"', u'')
The first one of the many to many is displayed and when saved only the selected value is left.
I found an easyer way to do this inspired by #Ryan Currah:
You just have to override "allow_multiple_selected" attribut from SelectMultiple class
class M2MSelect(forms.SelectMultiple):
allow_multiple_selected = False
class NewBetForm(forms.ModelForm):
class Meta:
model = Bet
participants = forms.ModelMultipleChoiceField(widget=M2MSelect, required=True, queryset=User.objects.all())

Django model.save() not working with loaddata

I have a model which is overriding save() to slugify a field:
class MyModel(models.Model):
name = models.CharField(max_length=200)
slug = models.SlugField(max_length=200)
def save(self, *args, **kwargs):
self.slug = slugify(self.name)
super(MyModel, self).save(*args, **kwargs)
When I run load data to load a fixture, this save() does not appear to be called because the slug field is empty in the database. Am I missing something?
I can get it to work by a pre_save hook signal, but this is a bit of a hack and it would be nice to get save() working.
def mymodel_pre_save(sender, **kwargs):
instance = kwargs['instance']
instance.slug = slugify(instance.name)
pre_save.connect(mymodel_pre_save, sender=MyModel)
Thanks in advance.
No you're not. save() is NOT called by loaddata, by design (its way more resource intensive, I suppose). Sorry.
EDIT: According to the docs, pre-save is not called either (even though apparently it is?).
Data is saved to the database as-is, according to https://docs.djangoproject.com/en/dev/ref/django-admin/#what-s-a-fixture
I'm doing something similar now - I need a second model to have a parallel entry for each of the first model in the fixture. The second model can be enabled/disabled, and has to retain that value across loaddata calls. Unfortunately, having a field with a default value (and leaving that field out of the fixture) doesn't seem to work - it gets reset to the default value when the fixture is loaded (The two models could have been combined otherwise).
So I'm on Django 1.4, and this is what I've found so far:
You're correct that save() is not called. There's a special DeserializedObject that does the insertion, by calling save_base() on the Model class - overriding save_base() on your model won't do anything since it's bypassed anyway.
#Dave is also correct: the current docs still say the pre-save signal is not called, but it is. It's behind a condition: if origin and not meta.auto_created
origin is the class for the model being saved, so I don't see why it would ever be falsy.
meta.auto_created has been False so far with everything I've tried, so I'm not yet sure what it's for. Looking at the Options object, it seems to have something to do with abstract models.
So yes, the pre_save signal is indeed being sent.
Further down, there's a post_save signal behind the same condition that is also being sent.
Using the post_save signal works. My models are more complex, including a ManyToMany on the "Enabled" model, but basically I'm using it like this:
from django.db.models.signals import post_save
class Info(models.Model):
name = models.TextField()
class Enabled(models.Model):
info = models.ForeignKey(Info)
def create_enabled(sender, instance, *args, **kwards):
if Info == sender:
Enabled.objects.get_or_create(id=instance.id, info=instance)
post_save.connect(create_enabled)
And of course, initial_data.json only defines instances of Info.

Why is adding site to an object doesn't seem to work in a save() override in the Django admin?

I have overrided the save() method of one of my model so it can inherit from the sites object and tags from its parent.
def save(self, *args, **kwargs):
ret = models.Model.save(self, *args, **kwargs)
if self.id:
for site in self.parent.sites.all():
self.sites.add(site.id)
for tag in self.parent.tags_set.all():
Tag.objects.add_tag(self, tag)
Using ipdb, I can see that self.sites.all() DOES return 4 sites at the end of the method, but strangely, once the request is finish, the same self.sites.all() does not return anything anymore.
I don't use transactions (at least explicitly), and I'm using Django 1.3 and Ubuntu 11.04
EDIT: found out that it works anywhere but in the admin. Doesn't the admin call save? If not, how can I hook to the object creation / update?
EDIT2: tested, and does call save. I have print statements to prove it. But it doesn't add the sites. It's a mystery.
In fact, this is a problem about adding programatically many to many relationships when saving a model if you use the Django admin.
Django save m2m relationships in the admin by calling 'clear' to wipe them out, then setting them again. It means that the form destroy any attached data (including your programatically attached) to the object then add the ones you entered in the admin.
It works outside the admin because we don't use the admin form that clear the m2m relationship.
The reason it works for tags in the admin is that the tagging application doesn't use m2m but emulate it by placing a TaggedItem object with a foreign key to a tag and to your model with a generic relation. Plus it's an inline field inclusion.
I tried a lot of things and finally had to look at the Django source code to realize that Django does not process admin forms in the usual way. What it does it:
call ModelAdmin.save_form: it calls form.save with commit = False, returning an unsaved instance and adding a save_m2m method to the form.
call ModelAdmin.save_model that actually calls the instance save method.
call form.save_m2m
Therefor:
you can't override your save method since save_m2m is called after and clear the m2m relations.
you can't override save_model for the same reason.
you can't override save_m2m because it is added by monkey patch to the form model in form.save, erasing you own method definition.
I didn't find a clean solution, but something that works is:
Provide a form for the ModelAdmin class with a method to override save_m2m with your own method:
class MyModelForm(forms.ModelForm):
class Meta:
model = MyModel
def set_m2m_method(self, update_tags_and_sites=True):
alias = self.save_m2m
def new_save_m2m(): # this is your new method
alias() # we need to call the original method as well
self.instance.add_niche_sites_and_tags()
self.save_m2m = new_save_m2m # we erase Django erasing :-)
Call this method in a ModelAdmin.model_save override:
class MyModelAdmin(admin.ModelAdmin):
form = MyModelForm
def save_model(self, request, obj, form, change):
obj.save()
form.set_m2m_method()
This cause the following:
Django calls save_model, replacing its monkey patch by yours
django calls our form.save_m2m that first call its old method that clears relations, then attach the m2m to the object.
I'm completely open to any better way to do this as this is twisted and plain ugly.
Since the problem seems to be reserved to admin, I tried to add some logic to do this in the ModelAdmin's save_model method, but it doesn't seem to help at all:
class SomeModelAdmin(admin.ModelAdmin):
def save_model(self, request, obj, form, change):
obj.save()
for site in Site.objects.all():
obj.sites.add(site.id)
print obj.sites.all()
Oddly print obj.sites.all() does list all the sites, however, they don't stay saved. Some sort of M2M issue perhaps?

Post create instance code call in django models

Sorry for some crazy subj.
I'd like to override django models save method and call some additional code if the model instance is newly created.
Sure I can use signals or check if the model have empty pk field and if yes, create temporary variable and later call a code:
Class EmailModel(models.Model):
email = models.EmailField()
def save(self, *args, **kwargs)
is_new = self.pk is None
super(EmailModel, self).save(*args, **kwargs)
# Create necessary objects
if is_new:
self.post_create()
def post_create(self):
# do job, send mails
pass
But I like to have some beautiful code and avoid using temporary variable in save method.
So the question is: is it possible to find if the instance of model is newly created object just after super save_base parent method call?
I've checked django sources can't find how to do that in right way.
Thanks
We have related post
For real - signals are best approch in this case.
You could use post_save() signal and in the listener just check if the credit_set exist for current model instance and if not - create one. That would be my choice - there is no need to overdo such a simple task.
Of course if you really need to know exactly when the model was initiated (I doubt it) use post_init() signal. You don't need to override save() method just to set some additional variables. Just catch post_init() signal, or pre_save(), and just change/add what you want. IMHO there is no sense to override save() method and check if this is new instance or not - that's why the signals are there.