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.
Related
I have a signal inside my django app where I would like to check if a certain field in my model has been updated, so I can then proceed and do something.
My model looks like this...
class Product(models.Model):
name = models.CharField(max_length=100)
price = models.PositiveIntegerField()
tax_rate = models.PositiveIntegerField()
display_price = models.PositiveInteger()
inputed_by = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, blank=True, on_delete=models.SET_NULL)
updated_by = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, blank=True, on_delete=models.SET_NULL)
My signal looks like this...
#receiver(post_save, sender=Product)
def update_model(sender, **kwargs):
instance = kwargs['instance']
if 'tax_rate' in kwargs['update_fields']:
# do something
This returns the error None is not an iterable. I have read the django signal documentation regarding the update_fields and it says The set of fields to update as passed to Model.save(), or None if update_fields wasn’t passed to save().
I should mention that I am working inside django admin here so what I hoped would happen is, I could create an instance of my Product model in django admin and then later if the value of tax_rate or price were updated, I could check for those and update the list_price accordingly. However, kwargs['update_fields'] always returns None.
What am I getting wrong? Or is there some other way I could achieve that result inside django admin?
Updated section
Now, say I introduce a field called inputed_by in my product model, that points to the user model and I want that field populated when the model is first saved. Then another field updated_by that stores the user who last updated the model. At the same time I wish to check whether either or both the tax_rate or price has been updated.
Inside my model admin I have the following method...
def save_model(self, request, obj, form, change):
update_fields = []
if not obj.pk:
obj.inputed_by = request.user
elif change:
obj.updated_by = request.user
if form.initial['tax_rate'] != form.cleaned_data['tax_rate']:
update_fields.append('tax_rate')
if form.initial['price'] != form.cleaned_data['price']:
update_fields.append('price')
obj.save(update_fields=update_fields)
super().save_model(request, obj, form, change)
My signal now looks like this...
#receiver(post_save, sender=Product, dispatch_uid="update_display_price")
def update_display_price(sender, **kwargs):
created = kwargs['created']
instance = kwargs['instance']
updated = kwargs['update_fields']
checklist = ['tax_rate', 'price']
# Prints out the frozenset containing the updated fields and then below that `The update_fields is None`
print(f'The update_fields is {updated}')
if created:
instance.display_price = instance.price+instance.tax_rate
instance.save()
elif set(checklist).issubset(updated):
instance.display_price = instance.price+instance.tax_rate
instance.save()
I get the error 'NoneType' object is not iterable
The error seems to come from the line set(checklist).issubset(updated). I've tried running that line specifically inside the python shell and it yields the desired results. What's wrong this time?
The set of fields should be passed to Model.save() to make them available in update_fields.
Like this
model.save(update_fields=['tax_rate'])
If you are creating something from django admin and getting always None it means that update_fields has not been passed to model's save method. And because of that it will always be None.
If you check ModelAdmin class and save_model method you'll see that call happens without update_fields keyword argument.
It will work if you write your own save_model.
The code below will solve your problem:
class ProductAdmin(admin.ModelAdmin):
...
def save_model(self, request, obj, form, change):
update_fields = []
# True if something changed in model
# Note that change is False at the very first time
if change:
if form.initial['tax_rate'] != form.cleaned_data['tax_rate']:
update_fields.append('tax_rate')
obj.save(update_fields=update_fields)
Now you'll be able to test memberships in update_model.
To add to Davit Tovmasyan's post. I made a more universal version that covers any field change using a for loop:
class ProductAdmin(admin.ModelAdmin):
...
def save_model(self, request, obj, form, change):
update_fields = []
for key, value in form.cleaned_data.items():
# True if something changed in model
if value != form.initial[key]:
update_fields.append(key)
obj.save(update_fields=update_fields)
EDIT: WARNING This isnt actually a full solution. Doesnt seem to work for object creation, only changes. I will try to figure out the full solution soon.
I wanted to add an alternative that relies on the pre_save signal to get the previous version of the instance you're evaluating (from this SO answer):
#receiver(pre_save, sender=Product)
def pre_update_model(sender, **kwargs):
# check if the updated fields exist and if you're not creating a new object
if not kwargs['update_fields'] and kwargs['instance'].id:
# Save it so it can be used in post_save
kwargs['instance'].old = User.objects.get(id=kwargs['instance'].id)
#receiver(post_save, sender=Product)
def update_model(sender, **kwargs):
instance = kwargs['instance']
# Add updated_fields, from old instance, so the method logic remains unchanged
if not kwargs['update_fields'] and hasattr(instance, 'old'):
kwargs['update_fields'] = []
if (kwargs['update_fields'].instance.tax_rate !=
kwargs['update_fields'].instance.old.tax_rate):
kwargs['update_fields'].append('tax_rate')
if 'tax_rate' in kwargs['update_fields']:
comparing to the accepted answer
Disadvantages
Extra query on every save that doesn't have update_fields (if you're not opening Django Admin to the world, this shouldn't be problematic)
Advantages
Don't need to override any method or class
You only need to implement the logic for the fields you want to evaluate, and they are in the same method, so no excuse for mistakes ;)
If you're doing this for many classes, you should probably look at other solutions (but the accepted answer is also not perfect for that!)
You can do this.
def save_model(self, request, obj, form, change):
if change:
obj.save(update_fields=form.changed_data)
else:
super().save_model(request, obj, form, change)
I have been combing through the internet for quite some while without finding any solution to this problem.
What I am trying to do...
I have the following models:
class TrackingEventType(models.Model):
required_previous_event = models.ForeignKey(TrackingEventType)
class TrackingEvent(models.Model):
tracking = models.ForeignKey(Tracking)
class Tracking(models.Model):
last_event = models.ForeignKey(TrackingEvent)
Now the main model is Tracking, so my admin for Tracking looks like this:
class TrackingEventInline(admin.TabularInline):
model = TrackingEvent
extra = 0
class TrackingAdmin(admin.ModelAdmin):
inlines = [TrackingEventInline]
That's it for the current setup.
Now my quest:
In the TrackingAdmin, when I add new TrackingEvent inlines, I want to limit the options of TrackingEventType to onlye those, that are allowed to follow on the last TrackingEvent of the Tracking. (Tracking.last_event == TrackingEventType.required_previous_event).
For this, I would need to be able to access the related Tracking on the InlineTrackingEvent, to access the last_event and filter the options for TrackingEventType accordingly.
So I found this: Accessing parent model instance from modelform of admin inline, but when I set up TrackingEventInline accordingly:
class MyFormSet(forms.BaseInlineFormSet):
def _construct_form(self, i, **kwargs):
kwargs['parent_object'] = self.instance
print self.instance
return super(MyFormSet, self)._construct_form(i, **kwargs)
class MyForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
print kwargs
self.parent_object = kwargs.pop('parent_object')
super(MyForm, self).__init__(*args, **kwargs)
class TrackingEventInline(admin.TabularInline):
form = MyForm
formset = MyFormSet
model = TrackingEvent
extra = 0
I get a KeyError at /admin/.../tracking/2/change/ 'parent_object' from self.parent_object = kwargs.pop('parent_object')
Does anyone know how to solve this? Am I approaching the problem the wrong way? I guess this would be pretty easy in a custom form in the frontend, but I really want to use the admin, because the whole application is built to be used from the admin, and it would be a hell lot of work to build a custom admin interface just because of this problem :)
Ok, so posting on StackOverflow is always helping to get the problem straight. I was able to put together a solution that works for me.
It includes defining my own Form in a outer function, as well as defining two InlineAdmin objects for TrackingEvent (one for update / edit, one just for insert).
Here's the code:
def create_trackingevent_form(tracking):
"""
"""
class TrackingEventForm(forms.ModelForm):
"""
Form for Tracking Event Inline
"""
def clean(self):
"""
May not be needed anymore, since event type choices are limited when creating new event.
"""
next_eventtype = self.cleaned_data['event_type']
tracking = self.cleaned_data['tracking']
# get last event, this also ensures last_event gets updated everytime the change form for TrackingEvent is loaded
last_eventtype = tracking.set_last_event()
if last_eventtype:
last_eventtype = last_eventtype.event_type
pk = self.instance.pk
insert = pk == None
# check if the event is updated or newly created
if insert:
if next_eventtype.required_previous_event == last_eventtype:
pass
else:
raise forms.ValidationError('"{}" requires "{}" as last event, "{}" found. Possible next events: {}'.format(
next_eventtype,
next_eventtype.required_previous_event,
last_eventtype,
'"%s" ' % ', '.join(map(str, [x.name for x in tracking.next_tracking_eventtype_options()]))
)
)
else:
pass
return self.cleaned_data
def __init__(self, *args, **kwargs):
# You can use the outer function's 'tracking' here
self.parent_object = tracking
super(TrackingEventForm, self).__init__(*args, **kwargs)
self.fields['event_type'].queryset = tracking.next_tracking_eventtype_options()
#self.fields['event_type'].limit_choices_to = tracking.next_tracking_eventtype_options()
return TrackingEventForm
class TrackingEventInline(admin.TabularInline):
#form = MyForm
#formset = MyFormSet
model = TrackingEvent
extra = 0
#readonly_fields = ['datetime', 'event_type', 'note']
def has_add_permission(self, request):
return False
class AddTrackingEventInline(admin.TabularInline):
model = TrackingEvent
extra = 0
def has_change_permission(self, request, obj=None):
return False
def queryset(self, request):
return super(AddTrackingEventInline, self).queryset(request).none()
def get_formset(self, request, obj=None, **kwargs):
if obj:
self.form = create_trackingevent_form(obj)
return super(AddTrackingEventInline, self).get_formset(request, obj, **kwargs)
I hope this helps other people with the same problem.. Some credit to the Stack Overflow threads that helped me come up with this:
Prepopulating inlines based on the parent model in the Django Admin
Limit foreign key choices in select in an inline form in admin
https://docs.djangoproject.com/en/1.9/ref/models/instances/#django.db.models.Model.clean_fields
Please do not hesitate to ask questions if you have any
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().
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?
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()