Build Tags on Save

With django-taggit, I can easily build new tags in hindsight. That’s my model:

from django.db import models
from taggit.managers import TaggableManager

class KeyWord(models.Model):
    text = models.CharField(max_length=80)
    tags = TaggableManager()

That’s the script which builds for every keyword a tag for each word in text:

key_words = KeyWord.objects.all()

for key_word in key_words:
    for kw in key_word.text.split():

Now I try to do the same for new keywords or keywords that have changed, inspired by the docs “Overriding predefined model methods”:

def save(self, *args, **kwargs):
    super().save(*args, **kwargs)
    for kw in self.text.split():

The saving itself works, but there is no effect on the tags. I guess it has something to do with the fact that tags is a Manager and not a Field, but I can’t figure out how this works properly. Do you have an idea?

This seems to be a problematic use-case for the library. It also appears that this is part of the libraries implementation. That issue is pretty old, but it doesn’t appear to have been fixed according to this more recent issue which matches your experience. It seems like you’ll need to manage this yourself after the instance is saved.

As an aside, you could use the .set() function to avoid making multiple DB calls when iterating over the split text:

for key_word in key_words:
    key_word.tags.set(key_word.text.split(), clear=True)

Thank you, these are precious hints. It took me a while to understand, that – while save() (as a predefined model method) simply can be overwritten in the Model – it’s different for post_save. That’s a signal which is send at the end of the save()method and has to be received. Am I right?

So I added in my (behind the class KeyWord(), not inside):

from django.dispatch import receiver
from django.db.models.signals import post_save

@receiver(post_save, sender=KeyWord)
def post_save(sender, instance, *args, **kwargs):
    instance.tags.set(instance.text.split(), clear=True)

This works fine now. Thank you also for the tip with set, by the way!

Well, there’s still one caveat: it works fine as long as I do not use the Admin form to save the item. With the admin form it’s different. I found Tim Kamanin’s post about the reasons for that and the solution:

(…) to fix that you need to override save_related method on model admin form

I adapted his code as follows:

class ModelAdmin(admin.ModelAdmin):
    def save_related(self, request, form, formsets, change):
        super(ModelAdmin, self).save_related(request, form, formsets, change)
        self.tags.set(instance.text.split(), clear=True)

I put this at the beginning of my, but it has no effect or to be precise does not seem to be executed on save. What does that mean, “to override save_related method on model admin form”? Has this do be done somewhere else?

I’m not sure if it’s the root cause of your issues, but this code is likely not doing exactly what you want it to do. Can you explain what it should be doing? My goal here is for you to spot the issue.

You are right, this was just a mistake I made copying my code to the forum. The for-line has to be deleted. I edited my post now. Sorry for that.

Besides that, the main problem still is that my function save_related in the ModelAdmin is not called.

I missed this earlier. When he says to override save_related, he means to add a method to your model’s respective ModelAdmin named save_related. This “overrides” the parent classes definition. You can still call the parent classes function, and he recommends that you do via the super line. Since python3, that’s changed a bit, so all you should need is:

class YourModelsModelAdminClass(admin.ModelAdmin):
    def save_related(self, request, form, formsets, change):
        super().save_related(request, form, formsets, change)
        form.instance.tags.set(instance.text.split(), clear=True)

I also changed self.tags to form.instance as I assume you want to be manipulating the object that’s being saved. I don’t know what self.tags was referencing though, so if you think that’s right, then keep it.

1 Like

I think I begin to understand this whole thing. And the best of all: it works! Thank you so much for your help, that’s really great!

There’s just one small mistake in your code above: in the last line, the second “instance” must also be “form.instance”, I guess.