Where is a model's __past_status = None field stored? - django

I have something that works but I don't quite understand why;
Some time ago I started using one great solution from SO, and now I'm using it in more places. The solution is;
you have a model eg
class Membership(models.Model):
user_cancelled = models.BooleanField(default=False)
__past_status = None
def save(self, force_insert=False, force_update=False, *args, **kwargs):
try:
if self.pk:
if self.user_cancelled != self.__past_status and self.user_cancelled:
....
except:
pass
super(Membership, self).save(force_insert, force_update, *args, **kwargs)
self.__past_status = self.user_cancelled
it works perfect and allows me to achieve what I wansn't quite able to using signals. What I don't understand is where the "__past_status" field's value is stored as it does not appear in migrations and therefore there is no db record of it. But it works. I would appreciate any explanation or hint.

Related

Overridden save() method behavior not using super().save() method

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().

AbstractModel and override save method in django

A lot of examples of save() methods are mentioned in StackOverflow. This is my idea: Model is abstract.
def save(self, *args, **kwargs):
# take id of 'self' object by: last id + 1 or 1 if there is no objects in db
try:
future_id = int(self.__class__.objects.latest('pk').pk) + 1
except:
future_id = 1
# set slug by id
self.slug = '{future_id}'.format(future_id=future_id)
# set slug by name and id
if self.get_name():
self.slug = '{name}-{slug}'.format(name=slugify(self.get_name()),
slug=self.slug)
# save
super(AbstractCmsContent, self).save(*args, **kwargs)
The question is: should I prepare some general table in database with last id or this solution with latest() function calling should work well in practice?
Added:
I will answer how I changed this function. At the end my function looks like this:
def save(self, *args, **kwargs):
if not self.slug:
self.slug = '{name}'.format(name=slugify(self.get_name()))
objs = self.__class__.objects.filter(slug__startswith=self.slug)
max_index = objs.aggregate(Max('slug_index'))['slug_index__max']
# two conditions:
# max_index should be positive,
# or number of objects with simillar slug in db > 0
if max_index or objs.count()>0:
self.slug_index = max_index + 1
self.slug = "{slug}-{index}".format(slug=self.slug,
index=self.slug_index)
super(AbstractCmsContent, self).save(*args, **kwargs)
First slug is always based only on name, and next one has some extra index: 'slug-index'. Also I added some extra field slug_index to my model, but like this I have quiet good control over my slugs.
Generally speaking you should not rely on getting latest id like this because some day you will either face race condition (when your latest pk will be already taken) or some of your models will get wrong ids.
I would suggest to make slug field nullable (and it's the only option if it has something like unique=True) and populate it after super call. Thus you will get bulletproof reliable id and simplify your code by delegating all the stuff to the database. Also if you use modern DB it will all happen in transaction so there will not be any time when your object does not have slug.
def save(self, *args, **kwargs):
super(AbstractCmsContent, self).save(*args, **kwargs)
self.slug = '{pk}'.format(pk=self.pk)
if self.get_name():
self.slug = '{name}-{slug}'.format(name=slugify(self.get_name()), slug=self.slug)
self.__class__.objects.filter(pk=self.pk).update(slug=self.slug)

Why doesn't django automatically update the foreign key? (obj.relative.id != obj.relative_id)

I have code like this (Django 1.4.3):
class Peon(models.Model)
# ...
order = models.ForeignKey(Order, db_column='idOrder')
# ...
def save(self, *args, **kwargs):
if self.order_id is None:
self.order = Order()
#edit self.order's fields appropriately
self.order.generate(...)
self.order.save()
super(Peon, self).save(*args, **kwargs)
super().save() complains that the order doesn't exist.
In other words, self.order_id remains None even when self.order.pk is set, and this prevents me from saving self.
My fix was to do this before calling super().save():
self.order_id = self.order.pk
My question is, why is this necessary? It feels like self.order.save() should automatically update the foreign key, but there must be a good reason why it doesn't.
When you generate an order by self.order = Order(), the id is not set as it is when you call the order’s save method. So your order_id field for your Peon object is set to None.
If all of your order’s field are optional you can do something like self.order = Order.objects.create() and then the id will be set correctly.
Otherwise you will need to do something like
def save(self, *args, **kwargs):
if self.order_id is None:
order = Order()
#edit self.order's fields appropriately
order.generate(...)
self.order = order
else:
#edit self.order's fields appropriately
self.order.generate(...)
self.order.save()
super(Peon, self).save(*args, **kwargs)
If order.generate doesn't need the backward relation of course.

What is the best / proper idiom in django for modifying a field during a .save() where you need to old value?

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?

Django Admin: Detect if a subset of an object fields has changed and which of them

I need to detect when some of the fields of certain model have changed in the admin, to later send notifications depending on which fields changed and previous/current values of those fields.
I tried using a ModelForm and overriding the save() method, but the form's self.cleaned_data and seld.instance already have the new values of the fields.
Modifying the answer above... taking the brilliant function from Dominik Szopa and changing it will solve your relationship change detection: Use this:
def get_changes_between_models(model1, model2, excludes = []):
changes = {}
for field in model1._meta.fields:
if not (field.name in excludes):
if field.value_from_object(model1) != field.value_from_object(model2):
changes[field.verbose_name] = (field.value_from_object(model1),
field.value_from_object(model2))
return changes
Then in your code you can say (avoid try/except for performance reasons):
if (self.id):
old = MyModel.Objects.get(pk=self.id)
changes = get_changes_between_models(self, old)
if (changes):
# Process based on what is changed.
If you are doing this at the "model" level, there is no way to save the extra query. The data has already been changed by the time you reach the "Save" point. My first post, so forgive me if I sound like an idiot.
To avoid extra DB lookup, I modified constructor to remember initial value and use this in save method later:
class Package(models.Model):
feedback = models.IntegerField(default = 0, choices = FEEDBACK_CHOICES)
feedback_time = models.DateTimeField(null = True)
def __init__(self, *args, **kw):
super(Package, self).__init__(*args, **kw)
self._old_feedback = self.feedback
def save(self, force_insert=False, force_update=False, *args, **kwargs):
if not force_insert and self.feedback != self._old_feedback:
self.feedback_time = datetime.utcnow()
return super(Package, self).save(force_insert, force_update, *args, **kwargs)
In order to get differences of two model instances, you can also use this function. It compare to model instances and returns dictionary of changes.
What you'll need to do is get an extra copy of the object you're working on from the database inside the save method before fully saving it. Example:
class MyModel(models.Model):
field1 = models.CharField(max_length=50)
def save(self):
if self.id:
try:
old = MyModel.objects.get(pk=self.id)
if old.field1 != self.field1:
# Process somehow
except MyModel.DoesNotExist:
pass
super(MyModel, self).save()