I have 2 models: Transaction and Wallet, a wallet has multiple transactions.
I need to update the amount field in Wallet model whenever a new transaction is created. The way I see is overwrite save method in the model. Currently, I wrote like this
def save(self, *args, **kwargs):
if self.kind == "Income":
self.wallet.amount += self.amount
elif self.kind == "Outcome":
self.wallet.amount -= self.amount
self.wallet.save()
super(Transaction, self).save(*args, **kwargs)
It creates a new transaction correctly but not update the wallet model. How I can fix that?
You should not be doing this because that information can be accessed via aggregate functions. Check out on django documentation.
But, if for some specific reason you you need to do it, you need to do it after the transaction is saved, with F expression:
from django.db.models import F
def save(self, *args, **kwargs):
super(Transaction, self).save(*args, **kwargs)
if self.kind == "Income":
self.wallet.amount = F('amount') + self.amount
elif self.kind == "Outcome":
self.wallet.amount = F('amount') - self.amount
self.wallet.save()
Did you check to see that your conditional statements are getting triggered?
This should actually work without you having to call self.wallet.save() if
you have already passed a valid Wallet object to the Transaction.
def save(self, *args, **kwargs):
if self.kind == "Income":
self.wallet.amount += self.amount
elif self.kind == "Outcome":
self.wallet.amount -= self.amount
super(Transaction, self).save(*args, **kwargs)
See: https://docs.djangoproject.com/en/3.1/topics/db/queries/#saving-changes-to-objects
For information on how to save Object references.
So I think two things could be going wrong. Either your conditional statements are not getting triggered, or you are not passing a valid Wallet Object reference to the Transaction.
Either way...I think this is a good use case for Django's signals. First, I would remove the save method you have, and in the Transaction model I would add
def update_transaction_wallet(sender, instance, **kwargs):
if instance.kind == "Income":
instance.wallet.amount += instance.amount
elif instance.kind == "Outcome":
instance.wallet.amount -= instance.amount
signals.post_save.connect(update_transaction_wallet, sender=Transaction)
You may have to tweak this a bit in order for it to work in your specific case. You didn't provide a lot of information about your models and situation.
But basically, this bit of code tells Django that whenever a Transaction Objects gets saved to also run the update_transaction_wallet() method. See: https://docs.djangoproject.com/en/3.0/topics/signals/
You have to instantiate Wallet and then make the proper addition or subtraction on it.
I would do it inside a Transaction method,
which you will call on save or any other place.
and if saves fail, rowback the wallet amount too ;)
Related
I have a model with a customized save() method that creates intermediate models if the conditions match:
class Person(models.Model):
integervalue = models.PositiveIntegerField(...)
some_field = models.CharField(...)
related_objects = models.ManyToManyField('OtherModel', through='IntermediaryModel')
...
def save(self, *args, **kwargs):
if self.pk is None: # if a new object is being created - then
super(Person, self).save(*args, **kwargs) # save instance first to obtain PK for later
if self.some_field == 'Foo':
for otherModelInstance in OtherModel.objects.all(): # creates instances of intermediate model objects for all OtherModels
new_Intermediary_Model_instance = IntermediaryModel.objects.create(person = self, other = otherModelInstance)
super(Person, self).save(*args, **kwargs) #should be called upon exiting the cycle
However, if editing an existing Person both through shell and through admin interface - if I alter integervalue of some existing Person - the changes are not saved. As if for some reason last super(...).save() is not called.
However, if I were to add else block to the outer if, like:
if self.pk is None:
...
else:
super(Person, self).save(*args, **kwargs)
the save() would work as expected for existing objects - changed integervalue is saved in database.
Am I missing something, or this the correct behavior? Is "self.pk is None" indeed a valid indicator that object is just being created in Django?
P.S. I am currently rewriting this into signals, though this behavior still puzzles me.
If your pk is None, super's save() is called twice, which I think is not you expect. Try these changes:
class Person(models.Model):
def save(self, *args, **kwargs):
is_created = True if not self.pk else False
super(Person, self).save(*args, **kwargs)
if is_created and self.some_field == 'Foo':
for otherModelInstance in OtherModel.objects.all():
new_Intermediary_Model_instance = IntermediaryModel.objects.create(person = self, other = otherModelInstance)
It's not such a good idea to override save() method. Django is doing a lot of stuff behind the scene to make sure that model objects are saved as they expected. If you do it in incorrectly it would yield bizarre behavior and hard to debug.
Please check django signals, it's convenient way to access your model object information and status. They provide useful parameters like instance, created and updated_fields to fit specially your need to check the object.
Thanks everyone for your answers - after careful examination I may safely conclude that I tripped over my own two feet.
After careful examination and even a trip with pdb, I found that the original code had mixed indentation - \t instead of \s{4} before the last super().save().
I'm trying use the FormWizard for to submit an order "charge" in wizard done method. Extending the example in the documentation, performing the credit card "charge" in the done means you can't go back and reprompt for credit card, because the wizard performs self.storage.reset() after calling the done method.
What is the right approach? The confirmation form clean() step is called multiple times for revalidation, etc, & seems too removed from done() where all validated forms are available.
Thanks for any pointers.
Kent
I could think of this:
In done() method you will be charging the user. If its declined/failed, save each forms data in session/cookies.
Restart the wizard from specific step where payment info is taken. NamedUrlWizard might be helpful.
Implement your get_form_intial() to return data from session/cookies for the step.
However, I think validation of this may fail as skipped steps do not have data. So you may have to do some more to get pass that.
I guess the answer is "you can't get there from here". I opened a ticket #19189, but it's unclear this feature will be added.
Here is my solution:
1. extend WizardView, modify render_done method to catch exception in it:
- detailed description
from django.contrib.formtools.wizard.views import SessionWizardView
class MySessionWizardView(SessionWizardView):
def __init__(self, **kwargs):
super(MySessionWizardView, self).__init__(**kwargs)
self.has_errors = False
class RevalidationError(Exception):
def __init__(self, step, form, **kwargs):
self.step = step
self.form = form
self.kwargs = kwargs
def __repr__(self):
return '%s(%s)' % (self.__class__, self.step)
__str__ = __repr__
def render_done(self, form, **kwargs):
final_form_list = []
for form_key in self.get_form_list():
form_obj = self.get_form(step=form_key,
data=self.storage.get_step_data(form_key),
files=self.storage.get_step_files(form_key))
if not form_obj.is_valid():
return self.render_revalidation_failure(form_key, form_obj, **kwargs)
final_form_list.append(form_obj)
try:
done_response = super(MySessionWizardView, self).render_done(form, **kwargs)
except self.RevalidationError as e:
return self.render_revalidation_failure(e.step, e.form, **e.kwargs)
self.storage.reset()
return done_response
thanks for your time.
I'm on Django 1.4, and I have the following code: Its the overriden save method for my Quest model.
#commit_on_success
def save(self, *args, **kwargs):
from ib.quest.models.quest_status_update import QuestStatusUpdate
created = not self.pk
if not created:
quest = Quest.objects.get(pk=self)
# CHECK FOR SOME OLD VALUE
super(Quest, self).save(*args, **kwargs)
I couldn't find out a smart way of doing this. It seems very silly to me to have to make a new query for the object i'm currently updating in order to find out an old instance value.
Is there a better way to do this?
Thank you all.
Francisco
You can store the old value inside the init method:
def __init__(self, *args, **kwargs):
super(MyModel, self).__init__(*args, **kwargs)
self.old_my_field = self.my_field
def save(self, *args, **kwargs):
print self.old_my_field
print self.my_field
You can probably use deepcopy or something alike to copy the whole object for later use in the save and delete methods.
Django doesn't cache the old values of the model instance, so you need to do that yourself or perform another query before save.
One common pattern is to use a pre-save signal (or put this code directly in your save() method, as you've done):
old_instance = MyModel.objects.get(pk=instance.pk)
# compare instance with old_instance, and maybe decide whether to continue
If you want to keep a cache of the old values, then you would probably do that in your view code:
from copy import deepcopy
object = MyModel.objects.get(pk=some_value)
cache = deepcopy(object)
# Do something with object, and then compare with cache before saving
There was a recent discussion on django-developers about this as well, with some other possible solutions.
I am checking the difference to old values using a django-reversion signal, but the same logic would apply to the save signals. The difference for me being that I want to save whether the field was saved or not.
#receiver(reversion.pre_revision_commit)
def it_worked(sender, **kwargs):
currentVersion = kwargs.pop('versions')[0].field_dict
fieldList = currentVersion.keys()
fieldList.remove('id')
commentDict = {}
print fieldList
try:
pastVersion = reversion.get_for_object(kwargs.pop('instances')[0])[0].field_dict
except IndexError:
for field in fieldList:
commentDict[field] = "Created"
comment = commentDict
except TypeError:
for field in fieldList:
commentDict[field] = "Deleted"
comment = commentDict
else:
for field in fieldList:
try:
pastTest = pastVersion[field]
except KeyError:
commentDict[field] = "Created"
else:
if currentVersion[field] != pastTest:
commentDict[field] = "Changed"
else:
commentDict[field] = "Unchanged"
comment = commentDict
revision = kwargs.pop('revision')
revision.comment = comment
revision.save()
kwargs['revision'] = revision
sender.save_revision
say I've got:
class LogModel(models.Model):
message = models.CharField(max_length=512)
class Assignment(models.Model):
someperson = models.ForeignKey(SomeOtherModel)
def save(self, *args, **kwargs):
super(Assignment, self).save()
old_person = #?????
LogModel(message="%s is no longer assigned to %s"%(old_person, self).save()
LogModel(message="%s is now assigned to %s"%(self.someperson, self).save()
My goal is to save to LogModel some messages about who Assignment was assigned to. Notice that I need to know the old, presave value of this field.
I have seen code that suggests, before super().save(), retrieve the instance from the database via primary key and grab the old value from there. This could work, but is a bit messy.
In addition, I plan to eventually split this code out of the .save() method via signals - namely pre_save() and post_save(). Trying to use the above logic (Retrieve from the db in pre_save, make the log entry in post_save) seemingly fails here, as pre_save and post_save are two seperate methods. Perhaps in pre_save I can retrieve the old value and stick it on the model as an attribute?
I was wondering if there was a common idiom for this. Thanks.
A couple of months ago I found somewhere online a good way to do this...
class YourModel(models.Model):
def __init__(self, *args, **kwargs):
super(YourModel, self).__init__(*args, **kwargs)
self.original = {}
id = getattr(self, 'id', None)
for field in self._meta.fields:
if id:
self.original[field.name] = getattr(self, field.name, None)
else:
self.original[field.name] = None
Basically a copy of the model fields will get saved to self.original. You can then access it elsewhere in the model...
def save(self, *args, **kwargs):
if self.original['my_property'] != self.my_property:
# ...
It can be easily done with signals. There are, respectively a pre-save and post-save signal for every Django Model.
So I came up with this:
class LogModel(models.Model):
message = models.CharField(max_length=512)
class Assignment(models.Model):
someperson = models.ForeignKey(SomeOtherModel)
import weakref
_save_magic = weakref.WeakKeyDictionary()
#connect(pre_save, Assignment)
def Assignment_presave(sender, instance, **kwargs):
if instance.pk:
_save_magic[instance] = Assignment.objects.get(pk=instance.pk).someperson
#connect(post_save, Assignment)
def Assignment_postsave(sender, instance, **kwargs):
old = None
if instance in _save_magic:
old = _save_magic[instance]
del _save_magic[instance]
LogModel(message="%s is no longer assigned to %s"%(old, self).save()
LogModel(message="%s is now assigned to %s"%(instance.someperson, self).save()
What does StackOverflow think? Anything better? Any tips?
This is baffling me... When I save my model, the book objects are unchanged. But if I open the invoice and save it again, the changes are made. What am I doing wrong?
class Invoice(models.Model):
...
books = models.ManyToManyField(Book,blank=True,null=True)
...
def save(self, *args, **kwargs):
super(Invoice, self).save(*args, **kwargs)
for book in self.books.all():
book.quantity -= 1
if book.quantity == 0:
book.sold = True;
book.save()
Edit: I've tried using the post_save signal, but it works the same way. No changes on the first save, changes saved the second time.
Update: Seems to be solved with this code:
class InvoiceAdmin(admin.ModelAdmin):
...
def save_model(self, request, obj, form, change):
obj.save()
for bk in form.cleaned_data['books']:
book = Book.objects.get(pk=bk.id)
book.quantity -= 1
if book.quantity == 0:
book.sold = True;
book.save()
This is how I worked around this, indeed baffling, behavior. Connect a signal receiver to models.signals.m2m_changed event, this get's triggered each time a m2m field is changed. The inline comments explain why.
class Gig(models.Model):
def slugify(self):
# Add venue name, date and artists to slug
self.slug = slugify(self.venue.name) + "-"
self.slug += self.date.strftime("%d-%m-%Y") + "-"
self.slug += "-".join([slugify(artist.name) for artist in self.artists.all()])
self.save()
#receiver(models.signals.m2m_changed, sender=Gig.artist.through)
def gig_artists_changed(sender, instance, **kwargs):
# This callback function get's called twice.
# 1 first change appears to be adding an empty list
# 2nd change is adding the actual artists
if instance.artist.all() and not instance.slug:
instance.slugify()
That's because m2m relation are saved after your model save, in order to obtain PK of parent object. In your case, second save works as expected because model already has PK and associated books from first save (it's done in a signal).
I haven't found the solution yet, best bet is to do your changes in admin view, i guess.