Django multiple many to many and post_save processing - python

I have a model that contains multiple many to many fields:
class Author(models.Model):
name = models.CharField(max_length=30)
class Topic(models.Model):
description = models.CharField(max_length=30)
class Article(models.Model):
authors = models.ManyToManyField(Author, related_name='articles')
topics = models.ManyToManyField(Topic, related_name='articles')
I need something pretty simple:
A method to be executed after the article is saved where I can access both authors and topics of that instance.
My first attempt has been with a post_save signal, but the signal is fired when the model itself is saved, not after its relationships are saved, which come from a through model obviously.
After some online reading I realized that I probably need to create my own signal and connect to it. The problem is that I have no idea what to overwrite and where to fire that signal.
Since I need this on multiple models, I thought I could create some M2MPostSaveModel class and have my models inherit it so I can just catch the signals..
But where does Django sends the signals? How can I overwrite it? I honestly have no idea and I had no luck searching in the docs, so apologies if it was there already and I didn't see it.

I finally did it, here's how:
I created a new signal
import django.dispatch
m2m_post_save = django.dispatch.Signal(providing_args=["instance"])
Then I created a subclass of the ModelAdmin class that fires it after it finishes saving the related elements:
class M2MPostSaveModelAdmin(ModelAdmin):
def save_related(self, request, form, formsets, change):
super(M2MPostSaveModelAdmin, self).save_related(request, form, formsets, change)
m2m_post_save.send(sender=self.__class__, instance=form.instance)
Now all I have to do is to subclass my ModelAdmin class with my M2MPostSaveModelAdmin class and hook up the signal to my method.
def doo_something_with_updated_instance(instance, **kwargs):
# Here your instance has all the m2m relationships updated
m2m_post_save.connect(doo_something_with_updated_instance sender=YourModelAdminClass)

I used m2m_changed for separate processing.
def topics_changed(sender, instance, **kwargs):
# Do something
m2m_changed.connect(topics_changed, sender=Article.topics.through)
so do with authors also

Related

Django post_save not getting many-to-many attributes on create

I am binding a post_save method to a model in django, like the following code:
def save_mymodel(sender,instance,*args,**kwargs):
print 'save called'
for parameter in instance.parameters.all():
print parameter.name
post_save.connect(save_mymodel, sender=MyModel)
Here are my models:
def MyModel(models.Model):
parameters = models.ManyToManyField(Parameter)
def Parameter(models.Model):
name = models.CharField(max_length=100)
When I try create a MyModel from django admin with any number of parameters, I only get save called as output. If I save the same MyModel object again from the admin, all the parameters are printed. What is the difference between calling save on create and not on create? How can I make sure I get all the attributes of a model using post_save on its creation?
ManyToMany fields do not participate in model save() method, so post_save would not recognize any m2m changes. To detect m2m change, use m2m_changed signal.
django doc about m2m_changed.

Django - signals. Simple examples to start

I am new to Django and I'm not able to understand how to work with Django signals. Can anyone please explain "Django signals" with simple examples?
Thanks in advance.
You can find very good content about django signals over Internet by doing very small research.
Here i will explain you very brief about Django signals.
What are Django signals?
Signals allow certain senders to notify a set of receivers that some action has taken place
Actions :
model's save() method is called.
django.db.models.signals.pre_save | post_save
model's delete() method is called.
django.db.models.signals.pre_delete | post_delete
ManyToManyField on a model is changed.
django.db.models.signals.m2m_changed
Django starts or finishes an HTTP request.
django.core.signals.request_started | request_finished
All signals are django.dispatch.Signal instances.
very basic example :
models.py
from django.db import models
from django.db.models import signals
def create_customer(sender, instance, created, **kwargs):
print "Save is called"
class Customer(models.Model):
name = models.CharField(max_length=16)
description = models.CharField(max_length=32)
signals.post_save.connect(receiver=create_customer, sender=Customer)
Shell
In [1]: obj = Customer(name='foo', description='foo in detail')
In [2]: obj.save()
Save is called
Apart from the explanation given by Prashant, you can also use receiver decorator present in django.dispatch module.
e.g.
from django.db import models
from django.db.models import signals
from django.dispatch import receiver
class Customer(models.Model):
name = models.CharField(max_length=16)
description = models.CharField(max_length=32)
#receiver(signals.pre_save, sender=Customer)
def create_customer(sender, instance, created, **kwargs):
print "customer created"
For more information, refer to this link.
In the signals.post_save.connect(receiver=create_customer, sender=Customer)... sender will always be the model which we are defining... or we can use the User as well in the sender.
Signals are used to perform any action on modification of a model instance. The signals are utilities that help us to connect events with actions. We can develop a function that will run when a signal calls it. In other words, Signals are used to perform some action on modification/creation of a particular entry in Database. For example, One would want to create a profile instance, as soon as a new user instance is created in Database
There are 3 types of signal.
pre_save/post_save: This signal works before/after the method save().
pre_delete/post_delete: This signal works before after delete a model’s instance (method delete()) this signal is thrown.
pre_init/post_init: This signal is thrown before/after instantiating a model (init() method).
One of the example, if we want to create a profile of a user as soon as the user is created using post_save signal.
For code example, I found the Geeks for Geeks, which explains is very simple way, and easy to understand.
https://www.geeksforgeeks.org/how-to-create-and-use-signals-in-django/
You can add signals to your models.py file
here is an example for adding an auto slug, if you use a SlugField:
this is the stuff you need to import
from django.utils.text import slugify
from django.dispatch import receiver
from django.db.models.signals import post_save, pre_save
Add the #receiver to the bottom of your class, included the def
If you add the def __str__(self): under the receiver, you will get an error
class Store(models.Model):
name = models.CharField(max_length=100)
slug = models.SlugField(unique=False, blank=True, null=True)
def __str__(self):
return self.name
#receiver(pre_save, sender=Store)
def store_pre_save(sender, instance, *args, **kwargs):
if not instance.slug:
instance.slug = slugify(instance.name)
or you can use post_save
class Store(models.Model):
name = models.CharField(max_length=100)
slug = models.SlugField(unique=False, blank=True, null=True)
def __str__(self):
return self.name
#receiver(post_save, sender=Store)
def store_post_save(sender, instance, created, *args, **kwargs):
if not instance.slug:
instance.slug = slugify(instance.name)
instance.save()
I found this example from this tutorial

Django autocategorize in m2m field

I have done a pre_save signal in my django/satchmo inherited model Product called JPiece and I have another model inheritance from satchmo Category called JewelCategory. The pre_save signal makes the JPiece objects get the category list and add those categories that fit the Jpiece description to the relation, that is done in the model, meaning if I manually do
p = Jpiece.objects.get(pk=3)
p.save()
The categories are saved and added to the p.category m2m relation but If i save from the admin it does not do this...
How can I achieve this... to save from the admin a JPiece and to get the categories it belongs too...
Here are the models remember that they both have model inheritance from satchmo product and category classes.
class Pieza(Product):
codacod = models.CharField(_("CODACOD"), max_length=20,
help_text=_("Unique code of the piece. J prefix indicates silver piece, otherwise gold"))
tipocod = models.ForeignKey(Tipo_Pieza, verbose_name=_("Piece Type"),
help_text=_("TIPOCOD"))
tipoenga = models.ForeignKey(Engaste, verbose_name=_("Setting"),
help_text=_("TIPOENGA"))
tipojoya = models.ForeignKey(Estilos, verbose_name=_("Styles"),
help_text=_("TIPOJOYA"))
modelo = models.CharField(_("Model"),max_length=8,
help_text=_("Model No. of casting piece."),
blank=True, null=True)
def autofill(self):
#self.site = Site.objects.get(pk=1)
self.precio = self.unit_price
self.peso_de_piedra = self.stone_weigth
self.cantidades_de_piedra = self.stones_amount
self.for_eda = self.for_eda_pieza
if not self.id:
self.date_added = datetime.date.today()
self.name = str(self.codacod)
self.slug = slugify(self.codacod, instance=self)
cats = []
self.category.clear()
for c in JewelCategory.objects.all():
if not c.parent:
if self.tipocod in c.tipocod_pieza.all():
cats.append(c)
else:
if self.tipocod in c.tipocod_pieza.all() and self.tipojoya in c.estilo.all():
cats.append(c)
self.category.add(*cats)
def pieza_pre_save(sender, **kwargs):
instance = kwargs['instance']
instance.autofill()
# import ipdb;ipdb.set_trace()
pre_save.connect(pieza_pre_save, sender=Pieza)
I know I can be vague with explanations sometimes of what I need so please feel free to ask anything Ill be sure to clarify ASAP since this is a client that needs this urgently.
Thank you all as always...
If you use pre_save, it's called before save(), meaning you can't define m2m relationships since the model doesn't have an ID.
Use post_save.
# this works because the ID does exist
p = Jpiece.objects.get(pk=3)
p.save()
Update, check out the comment here: Django - How to save m2m data via post_save signal?
It looks like the culprit now is that with an admin form, there is a save_m2m() happening AFTER the post_save signal, which could be overwriting your data. Can you exclude the field from the form in your ModelAdmin?
# django.forms.models.py
if commit:
# If we are committing, save the instance and the m2m data immediately.
instance.save()
save_m2m()

Django: Faking a field in the admin interface?

I have a model, Foo. It has several database properties, and several properties that are calculated based on a combination of factors. I would like to present these calculated properties to the user as if they were database properties. (The backing factors would be changed to reflect user input.) Is there a way to do this with the Django admin interface?
I would suggest you subclass a modelform for Foo (FooAdminForm) to add your own fields not backed by the database. Your custom validation can reside in the clean_* methods of ModelForm.
Inside the save_model method of FooAdmin you get the request, an instance of Foo and the form data, so you could do all processing of the data before/after saving the instance.
Here is an example for a model with a custom form registered with django admin:
from django import forms
from django.db import models
from django.contrib import admin
class Foo(models.Model):
name = models.CharField(max_length=30)
class FooAdminForm(forms.ModelForm):
# custom field not backed by database
calculated = forms.IntegerField()
class Meta:
model = Foo
class FooAdmin(admin.ModelAdmin):
# use the custom form instead of a generic modelform
form = FooAdminForm
# your own processing
def save_model(self, request, obj, form, change):
# for example:
obj.name = 'Foo #%d' % form.cleaned_data['calculated']
obj.save()
admin.site.register(Foo, FooAdmin)
Providing initial values for custom fields based on instance data
(I'm not sure if this is the best solution, but it should work.)
When a modelform for a existing model instance in the database is constructed, it gets passed this instance. So in FooAdminForm's __init__ one can change the fields attributes based on instance data.
def __init__(self, *args, **kwargs):
super(FooAdminForm, self).__init__(*args, **kwargs)
# only change attributes if an instance is passed
instance = kwargs.get('instance')
if instance:
self.fields['calculated'].initial = (instance.bar == 42)
It's easy enough to get arbitrary data to show up in change list or make a field show up in the form: list_display arbitrarily takes either actual model properties, or methods defined on the model or the modeladmin, and you can subclass forms.ModelForm to add any field type you'd like to the change form.
What's far more difficult/impossible is combining the two, i.e. having an arbitrary piece of data on the change list that you can edit in-place by specifying list_editable. Django seems to only accept a true model property that corresponds to a database field. (even using #property on the method in the model definition is not enough).
Has anyone found a way to edit a field not actually present on the model right from the change list page?
In the edit form, put the property name into readonly_fields (1.2 upwards only).
In the changelist, put it into list_display.
You can use the #property decorator in your model (Python >= 2.4):
class Product(models.Model):
#property
def ranking(self):
return 1
"ranking" can then be used in list_display:
class ProductAdmin(admin.ModelAdmin):
list_display = ('ranking', 'asin', 'title')

Is there an easy way to populate SlugField from CharField?

class Foo(models.Model):
title = models.CharField(max_length=20)
slug = models.SlugField()
Is there a built-in way to get the slug field to autopopulate based on the title? Perhaps in the Admin and outside of the Admin.
for Admin in Django 1.0 and up, you'd need to use
prepopulated_fields = {'slug': ('title',), }
in your admin.py
Your key in the prepopulated_fields dictionary is the field you want filled, and the value is a tuple of fields you want concatenated.
Outside of admin, you can use the slugify function in your views. In templates, you can use the |slugify filter.
There is also this package which will take care of this automatically: https://pypi.python.org/pypi/django-autoslug
Thought I would add a complete and up-to-date answer with gotchas mentioned:
1. Auto-populate forms in Django Admin
If you are only concerned about adding and updating data in the admin, you could simply use the prepopulated_fields attribute
class ArticleAdmin(admin.ModelAdmin):
prepopulated_fields = {"slug": ("title",)}
admin.site.register(Article, ArticleAdmin)
2. Auto-populate custom forms in templates
If you have built your own server-rendered interface with forms, you could auto-populate the fields by using either the |slugify tamplate filter or the slugify utility when saving the form (is_valid).
3. Auto-populating slugfields at model-level with django-autoslug
The above solutions will only auto-populate the slugfield (or any field) when data is manipulated through those interfaces (the admin or a custom form). If you have an API, management commands or anything else that also manipulates the data you need to drop down to model-level.
django-autoslug provides the AutoSlugField-fields which extends SlugField and allows you to set which field it should slugify neatly:
class Article(Model):
title = CharField(max_length=200)
slug = AutoSlugField(populate_from='title')
The field uses pre_save and post_save signals to achieve its functionality so please see the gotcha text at the bottom of this answer.
4. Auto-populating slugfields at model-level by overriding save()
The last option is to implement this yourself, which involves overriding the default save() method:
class Article(Model):
title = CharField(max_length=200)
slug = SlugField()
def save(self, *args, **kwargs):
self.slug = slugify(self.title)
super().save(*args, **kwargs)
NOTE: Bulk-updates will bypass your code (including signals)
This is a common miss-understanding by beginners to Django. First you should know that the pre_save and post_save signals are directly related to the save()-method. Secondly the different ways to do bulk-updates in Django all circumvent the save()-method to achieve high performance, by operating directly on the SQL-layer. This means that for the example model used in solution 3 or 4 above:
Article.objects.all().update(title='New post') will NOT update the slug of any article
Using bulk_create or bulk_update on the Article model will NOT update the slug of any article.
Since the save()-method is not called, no pre_save or post_save signals will be emitted.
To do bulk updates and still utilize code-level constraints the only solution is to iterate objects one by one and call its save()-method, which has drastically less performance than SQL-level bulk operations. You could of course use triggers in your database, though that is a totally different topic.
Outside the admin, see this django snippet. Put it in your .save(), and it'll work with objects created programmatically. Inside the admin, as the others have said, use prepopulated_fields.
For pre-1.0:
slug = models.SlugField(prepopulate_from=('title',))
should work just fine
For 1.0, use camflan's
You can also use pre_save django signal to populate slug outside of django admin code.
See Django signals documentation.
Ajax slug uniqueness validation will be useful too, see As-You-Type Slug Uniqueness Validation # Irrational Exuberance
Auto-populating slugfields at model-level with the built-in django slugify:
models.py
from django.db import models
class Place:
name = models.CharField(max_length=50)
slug_name = models.SlugField(max_length=50)
signals.py
from django.db.models.signals import pre_save
from django.dispatch import receiver
from django.template.defaultfilters import slugify as django_slugify
from v1 import models
#receiver(pre_save, sender=models.Place)
def validate_slug_name(sender, instance: models.Place, **kwargs):
instance.slug_name = django_slugify(instance.name)
Credits to: https://github.com/justinmayer/django-autoslug/blob/9e3992296544a4fd7417a833a9866112021daa82/autoslug/utils.py#L18
prepopulated_fields = {'slug': ('title',), }
autoslug has worked quite well for me in the past. Although I've never tried using it with the admin app.

Categories

Resources