Performing actions on a related model while saving - django

I need help. For several days now I have been unable to figure out how to get around one problem. I would appreciate any idea or advice.
For simplicity, there is a set of models:
class A(models.Model):
name = models.CharField(max_length=255)
class B(models.Model):
name = models.CharField(max_length=255)
owner = models.ForeignKey(A, on_delete=models.CASCADE, null=False)
def foo():
do something
The number of instances of B belonging to A is not known in advance. The number of instances of B is determined by the user at the time of creating a new instance of A. Moreover, he can create an instance of A without creating a single instance of B (empty set).
Challenge: I need to run foo after saving a new instance of A. This function should handle both the fields of the new instance of A and fields of B (back relation).
Yes, I have set the receiver to post_save signal model A. But here's the problem, at the moment when A is already saved (post_save), instances of B have not yet been saved (pre_save), and the values ​​of the corresponding fields have not been determined. In other words, I cannot get raw B values ​​in receiver A.
Any ideas? Am I doing something wrong?

I found a solution. Maybe it will be useful to someone.
I took advice from our more experienced colleagues to use signals only as a last resort. And the solution turned out to be simple. Although the solution may not seem very elegant.
The solution is based on overriding the save() method of each model
class A(models.Model):
name = models.CharField(max_length=255)
def _call_foo(self, *args, **kwargs):
do something
foo(*args, **kwargs)
def save(self, *args, **kwargs):
changed_fields = []
pk = None
if self.pk:
cls = self.__class__
old = cls.objects.get(pk=self.pk)
new = self
for field in cls._meta.get_fields():
field_name = field.name
try:
if getattr(old, field_name) != getattr(new, field_name):
pk = self.pk
changed_fields.append(field_name)
# Catch field does not exist exception
except Exception as ex:
print(type(ex))
print(ex.arg)
print(ex)
kwargs['update_fields'] = changed_fields
super().save(*args, **kwargs)
else:
super().save(*args, **kwargs)
pk = self.pk
changed_fields.append('__all__')
self._call_foo(self, pk, changed_fields)
class B(models.Model):
name = models.CharField(max_length=255)
owner = models.ForeignKey(A, on_delete=models.CASCADE, null=False)
def save(self, *args, **kwargs):
changed_fields = []
pk = None
if self.pk:
cls = self.__class__
old = cls.objects.get(pk=self.pk)
new = self
for field in cls._meta.get_fields():
field_name = field.name
try:
if getattr(old, field_name) != getattr(new, field_name):
pk = self.pk
changed_fields.append(field_name)
# Catch field does not exist exception
except Exception as ex:
print(type(ex))
print(ex.arg)
print(ex)
kwargs['update_fields'] = changed_fields
super().save(*args, **kwargs)
else:
super().save(*args, **kwargs)
pk = self.pk
changed_fields.append('__all__')
self.owner._call_foo(self, pk, changed_fields)
This solution is better than using signals, at least in that the save method is called if there is a real need for it: either an instance is created, or it is updated.
Of course, need to keep in mind that if the user updates or create several instances of both models (for example, in the inline form, the admin creates several instances of B and one instance of A), then the foo() function will be called several times. But this can already be worked out either at the _call_foo() method level, or by the logic of the foo() function itself.

Related

How to save an instance and update a reverse M2M field in a modelForm

Lots of questions cover saving instances before M2M relationships, however I have a problem achieving this when saving via a form with a _set relationship, which does not appear to be covered in other answers.
I have two models with an M2M relationship:
class Author(models.Model):
name = models.CharField()
class Publisher(models.Model):
authors = models.ManyToManyField(Author)
When creating a new Author, I want to be able to set Publishers for that Author. Currently I handle this in the form:
class AuthorForm(forms.ModelForm):
publishers = forms.ModelMultipleChoiceField(queryset=Publisher.objects.all())
def __init__(self, *args, **kwargs):
super(AuthorForm, self).__init__(*args, **kwargs)
# If the form is used in a UpdateView, set the initial values
if self.instance.id:
self.fields['publishers'].initial = self.instance.publisher_set.all(
).values_list('id', flat=True)
def save(self, *args, **kwargs):
instance = super(AuthorForm, self).save(*args, **kwargs)
# Update the M2M relationship
instance.issuer_set.set(self.cleaned_data['issuers'])
return instance
class Meta:
model = Author
fields = ['name', 'publishers']
This works fine when updating an existing Author. However, when I use it to create a new one I get:
<Author: Sample Author> needs to have a value for field "id" before this many-to-many relationship can be used.
I understand this is because the instance needs to be saved first, however in def save() I am doing this (instance = super(AuthorForm, self).save(*args, **kwargs)).
Where am I going wrong?
calling super does not guarantee that it will create an instance. Because, form's save(...) method has a argument called commit, if the method calling the save is passing commit=False(for example: form.save(commit=False)), then it will not save the instance. So you need to explicitly save it. Like this:
instance = super(AuthorForm, self).save(commit=True)

Python Populating ManyToManyField automativally using same model instance

I am screwed up to solve a problem for the last 7 days, yet I couldn't solve this problem! so much frustrated now!!!
I want when I create new GroupMess with an admin, the same admin should automatically add to members field,
this is my models:
class GroupMess(models.Model):
admin = models.OneToOneField(
User,
on_delete=models.CASCADE,
related_name='mess_admin'
)
members = models.ManyToManyField(User, related_name='mess_members', blank=True)
def save(self, *args, **kwargs):
self.members.add(self.admin)
super(GroupMess, self).save(*args, *kwargs)
If I try to create new GroupMess, it throws me this error:
"<GroupMess: GroupMess object (None)>" needs to have a value for field "id" before this many-to-many relationship can be used.
If i override save method like this:
def save(self, *args, **kwargs):
super(GroupMess, self).save(*args, *kwargs)
self.members.add(self.admin)
then it doesn't throw any error but the problem is, the members field remains blank
Can anyone help to fix this?
I want when I create GroupMess, I will add the admin during creating groupmess and the members filed should be filled automatically with the admin
*I mean, A group admin also will be a group member *
After you add the members to the Group model, save it again.
def save(self, *args, **kwargs):
super(Group, self).save(*args, **kwargs)
self.members.add(self.admin)
self.save()

Foreign Key - get values

I have created the following models. Many Accounts belong to one Computer.
class Computer(models.Model):
name = models.CharField(max_length=256)
def __str__(self):
return str(self.name)
class Accounts(models.Model):
computer = models.ForeignKey(Computer, on_delete=models.CASCADE, related_name='related_name_accounts')
username = models.CharField(max_length=10)
def __str__(self):
return str(self.username)
I now want to create a Form where i can choose one user from a dropdown-list of all users that belong to a Computer
class ComputerForm(forms.ModelForm):
class Meta:
model = Computer
exclude = ['name',]
def __init__(self, *args, **kwargs):
super(ComputerForm, self).__init__(*args, **kwargs)
self.fields['user']=forms.ModelChoiceField(queryset=Computer.related_name_accounts.all())
But i get an error
'ReverseManyToOneDescriptor' object has no attribute 'all'
How do i have to adjust the queryset in __init__ to display all users of a computer?
Edit:
>>> x = Computer.objects.filter(pk=3).get()
>>> x.related_name_accounts.all()
I would somehow need to pass the dynamic pk to the ModelForm?
Your issue is that you used a capital C in Computer.related_name_accounts.all(). This doesn't make sense, because the Computer model doesn't have related fields - it's instances do.
This line of code would work:
computer = Computer.objects.get(id=1)
computer.related_name_accounts.all()
But the issue is, I am not sure what you mean by ...of all users that belong to a Computer, since you don't have a ForeignKey or OneToOne relationship between User and Computer.
You can pass an instance of your model to the form like this:
x = Computer.objects.get(pk=3)
form = ComputerForm(instace=x)
And access it through self.instance in the form methods:
class ComputerForm(forms.ModelForm):
class Meta:
model = Computer
exclude = ['name',]
def __init__(self, *args, **kwargs):
super(ComputerForm, self).__init__(*args, **kwargs)
self.fields['user']=forms.ModelChoiceField(
queryset=self.instance.related_name_accounts.all()
)

Creating dynamic ModelChoiseField from users List objects

I have been trying for awhile now without any luck.. I have model Like this:
class List(models.Model):
name = models.CharField(max_length=100, default="")
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='lists')
def __str__(self):
returnself.name
class Meta:
unique_together = ['name', 'user']
Every user can create their own lists and add values to those lists. I have adding values and everything else working but to the form that adds these values I would somehow need to filter to show only users own lists, now its showing all lists created by every user... this is the form:
class data_form(forms.Form):
user_lists = List.objects.all()
selection = forms.ModelChoiceField(queryset=user_lists)
data = forms.IntegerField()
Any ideas how to filter it? I have tempoary "list.objects.all()" since dont want it to give error that crashes the server. I have watched a ton of examples on stackoverflow but none of them seems to be exact thing that I am looking for.. Thanks already for asnwers! :)
You need to get hold of the current user, e.g. like so or so.
That is, you pass request.user to the form when instantiating it in your view:
frm = DataForm(user=request.user)
In the __init__ of your form class, you can then assign the user-filtered queryset to your field:
class DataForm(forms.Form):
def __init__(self, *args, **kwargs):
user = kwargs.pop("user")
super(DataForm, self).__init__(*args, **kwargs)
self.fields['selection'].queryset = List.objects.filter(user=user)
You can set your form to take the user when initialized, and from there get a new queryset filtered by user.
class DataForm(forms.Form):
selection = forms.ModelChoiceField(queryset=List.objects.none())
data = forms.IntegerField()
def __init__(self, user, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['selection'].queryset = List.objects.filter(user=user)
You would inititialize the form like this:
form = DataForm(request.user)

Handling duplicate records in Django

Suppose I have a set of records which I know to be unique based on some other record and an e-mail, thusly:
class Signup(models.Model):
activity = models.ForeignKey(Activity, related_name='activities')
name = models.CharField(max_length=100)
email = models.EmailField()
# many addtional fields here not relevant to question
Then, I have a model form:
class SignupForm(forms.ModelForm):
class Meta:
model = Signup
exclude = [ 'activity' ]
def __init__(self, *args, **kwargs):
self.activity = kwargs.pop('activity')
def save(self, *args, **kwargs):
kwargs['commit'] = False
m = super(SignupForm, self).save(*args, **kwargs)
m.activity = self.activity
m.save()
return m
Suppose a user goes in a fills out the form under the activity, then realizes they made an error in the form, clicks the back button, makes changes, then clicks submit again.
Without any modifications to the code above, a duplicate record for that activity and email would be created.
What I want to know is how I can force the form to update, rather than create, a record if it finds a match for the entered e-mail.
I tried this code:
class SignupForm(forms.ModelForm):
class Meta:
model = Signup
exclude = [ 'activity' ]
def __init__(self, *args, **kwargs):
self.activity = kwargs.pop('activity')
def save(self, *args, **kwargs):
kwargs['commit'] = False
try:
self.instance = Signup.objects.get(email=self.cleaned_data['email'], activity=self.activity)
except Signup.DoesNotExist:
pass
m = super(SignupForm, self).save(*args, **kwargs)
m.activity = self.activity
m.save()
return m
However, looks like this causes the form to ignore all new information for some reason (I have debug toolbar running and examining the query confirms that none of the fields are being changed!)
Is there an accepted way of handling this?
Further request
Is there any way to do this while still using the ModelForm's built-in save function? So far the answers seem to suggest that this is impossible, which is, I'm sorry, ridiculous.
Replace
try:
self.instance = Signup.objects.get(email=self.cleaned_data['email'], activity=self.activity)
except Signup.DoesNotExist:
pass
With:
obj, created = Signup.objects.get_or_create(\
email=self.cleaned_data['email'],
activity=self.activity)
if created:
print 'its a new one, hooray!'
else:
print 'the object exists!'
More information on get_or_create.