django-taggit icon indicating copy to clipboard operation
django-taggit copied to clipboard

Multiple TaggableManager() fields in single model

Open z0ccc opened this issue 4 years ago • 24 comments

When trying to create a model like:

class Test(models.Model):
    tag1 = TaggableManager()
    tag2 = TaggableManager()

It gives the error: ValueError: You can't have two TaggableManagers with the same through model.

How can I allow multiple taggit fields for a single model? Thanks

z0ccc avatar Aug 27 '20 05:08 z0ccc

TaggableManager should allow you to create a list of tags in just one field, why would you want to do two tags field? are they two type of tags?

Delvio avatar Aug 27 '20 18:08 Delvio

TaggableManager should allow you to create a list of tags in just one field, why would you want to do two tags field? are they two type of tags?

I want two different types of tags (like interests and skills) in a single model.

z0ccc avatar Aug 27 '20 18:08 z0ccc

@heyitme I'm not sure if you've explored this option, but I was able to achieve this behaviour - having 2 different sets of tags for a single model - by using custom tagging as described here: https://django-taggit.readthedocs.io/en/latest/custom_tagging.html#custom-tag . I created separate TagBase and GenericTaggedItemBase classes for each set. I can provide some snippets if that helps.

yoccodog avatar Sep 14 '20 16:09 yoccodog

@yoccodog Thanks for the reply! I would greatly appreciate if you could post some snippets to help me out.

z0ccc avatar Sep 14 '20 18:09 z0ccc

@heyitme Sure. Just taking the example at the linked doc above and expanding on it:

from django.db import models
from django.utils.translation import ugettext_lazy as _

from taggit.managers import TaggableManager
from taggit.models import TagBase, GenericTaggedItemBase


class MyCustomTag(TagBase):
    # ... fields here

    class Meta:
        verbose_name = _("Tag")
        verbose_name_plural = _("Tags")

    # ... methods (if any) here

class MyOtherCustomTag(TagBase):
    # ... fields here

    class Meta:
        verbose_name = _("Tag")
        verbose_name_plural = _("Tags")

    # ... methods (if any) here

class TaggedWhatever(GenericTaggedItemBase):
    tag = models.ForeignKey(
        MyCustomTag,
        on_delete=models.CASCADE,
        related_name="%(app_label)s_%(class)s_items",
    )

class OtherTaggedWhatever(GenericTaggedItemBase):
    tag = models.ForeignKey(
        MyOtherCustomTag,
        on_delete=models.CASCADE,
        related_name="%(app_label)s_%(class)s_items",
    )



class Food(models.Model):
    # ... fields here

    tags = TaggableManager(through=TaggedWhatever)
    other_tags = TaggableManager(through=OtherTaggedWhatever)

yoccodog avatar Sep 16 '20 13:09 yoccodog

@yoccodog Could you help me out. I used your code snippet and adapted it to my app.

class UserTags(TagBase):

    class Meta:
        verbose_name = "Tag"
        verbose_name_plural = "Tags"

    # ... methods (if any) here


class AdminTags(TagBase):

    class Meta:
        verbose_name = "Tag"
        verbose_name_plural = "Tags"

    # ... methods (if any) here


class UserTagsAll(GenericTaggedItemBase):
    tag = models.ForeignKey(
        UserTags,
        on_delete=models.CASCADE,
        related_name="%(app_label)s_%(class)s_items",
    )


class AdminTagsAll(GenericTaggedItemBase):
    tag = models.ForeignKey(
        AdminTags,
        on_delete=models.CASCADE,
        related_name="%(app_label)s_%(class)s_items",
    )


# Model for all uploaded files
class Uploaded(models.Model):
    objects: models.Manager()
    user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="users")
    time_uploaded = models.DateTimeField(auto_now_add=True)
    name = models.CharField(max_length=50)
    file = models.FileField(upload_to=MEDIA_ROOT)
    admintags = TaggableManager(blank=True, through=AdminTagsAll)
    usertags = TaggableManager(blank=True, through=UserTagsAll)
    additional_description = models.CharField(max_length=50, blank=True)

    def __str__(self):
        return f"{self.name} {self.file}"

I've deleted all of my tables and started everything from scratch but I'm getting an error that the table AdminTagsAll does not exist. When I go to admin, there's only one table named Tags.

MatejMijoski avatar Sep 16 '20 19:09 MatejMijoski

Hi @MatejMijoski ! I'm not certain but I think you need to remove taggit from INSTALLED_APPS. At the top of the page on custom tagging (https://django-taggit.readthedocs.io/en/latest/custom_tagging.html#custom-tag) it says:

"Note: Including ‘taggit’ in settings.py INSTALLED_APPS list will create the default django-taggit and “through model” models. If you would like to use your own models, you will need to remove ‘taggit’ from settings.py’s INSTALLED_APPS list."

yoccodog avatar Sep 17 '20 14:09 yoccodog

@yoccodog I removed taggit but I think that it still doesn't work. I'm still getting a no such table: FileUpload_admintagsall error.

MatejMijoski avatar Sep 17 '20 14:09 MatejMijoski

@MatejMijoski Did you remove your old tag-related migrations, remake your migrations after removing taggit from INSTALLED_APPS, and re-run the new ones? You should get tables for each tag type and each join table.

yoccodog avatar Sep 20 '20 12:09 yoccodog

I've encountered another problem after solving the previous one (I didn't add the models to admin.py). The new problem is that if I go the Generic Tagged Item Base models i.e. AdminTagsAll and UserTagsAll, I get the following error: no such table: FileUpload_admintagsall. I've deleted all migrations, I've dumped the SQL db and remade everything.

MatejMijoski avatar Sep 20 '20 14:09 MatejMijoski

Do those tables exist in your db after running your migrations? What tables are you in your db after running your migrations?

yoccodog avatar Sep 20 '20 14:09 yoccodog

I've got 4 tables: AdminTagsAll, UserTagsAll and 2 tables named Tags. I can access these 2 tables but when I go to AdminTagsAll or UserTagsAll I get the error.

MatejMijoski avatar Sep 24 '20 18:09 MatejMijoski

@MatejMijoski When you say there are 2 tables named Tags, it sounds like you're not looking in the database but instead in django admin. Is that correct? If that's the case, the reason there are 2 tables named 'Tags' there is because the verbose_name and verbose_name_plural values are the same for both AdminTags and UserTags.

The error you're seeing sounds like the migrations have not been generated correctly. Can you post the migration files that were generated after you created your custom tag classes? Can you also post a listing of the tables in your database?

yoccodog avatar Sep 25 '20 22:09 yoccodog

It looks like the tables weren't being created. I installed django-extensions and ran restart_db and then migrate. It now works. Thank you very much.

One more question, why do I need 2 tables for users and 2 for the admin? Doesn't it work the same with just the two TagBase tables?

MatejMijoski avatar Sep 26 '20 21:09 MatejMijoski

That's great news!

With django-taggit, the way that a set of tags (as defined by TagBase) is associated with a model attribute (as defined via TaggableManager) is through a specific join table (as defined by GenericTaggedItemBase). If you were building your own tagging system with a similar structure, you could conceivably have a single join table with multiple tag sets. But you'd have to jump through some hoops with django-taggit if you wanted to do this.

Consider the queries behind how the tags for a given instance are found: the join table is queried for records that have object_id (and content_type) which matches the instance's id (and type) and then the tag table is joined to to get the tag names. If you wanted to have a single join table with multiple tag sets, you would need to include some information in the join table about which tag set a given record is referencing.

In any case, glad you sorted this out!

yoccodog avatar Sep 27 '20 12:09 yoccodog

@yoccodog Is there a way to have one of the tag fields (User Tags) in my model be a multiple select?

I'm using select2 for the user interface and am wondering if I could maybe use it for the admin as well?

MatejMijoski avatar Oct 01 '20 15:10 MatejMijoski

@MatejMijoski You should be able to achieve this, but as for the exact implementation, I'm not sure right now. Good luck!

yoccodog avatar Oct 09 '20 01:10 yoccodog

A use case that cannot be solved by multiple tag models is when you want to do something like:

class MyModel(...):
    tags = TaggableManager()

class MyFilter(...):
   tag = ForeignKey(tagmodel)
   parent_tags = TaggableManager()
   implied_tags = TaggableManager()

In this situation, parent tags and implied tags must come from the same tag pool. So the workaround proposed above cannot solve this issue.

thenewguy avatar Feb 25 '21 15:02 thenewguy

@thenewguy you can solve this by creating two different TaggedItem tables, but which both rely on the same Tag class


class ParentTagItem(TaggedItemBase):
      tag = ForeignKey(MyTagModel)

class ImpliedTagItem(TaggedItemBase):
      tag = ForeignKey(MyTagModel) # points to the same tags!

class MyFilter(...):
   tag = ForeignKey(tagmodel)
   parent_tags = TaggableManager(through=ParentTagItem)
   implied_tags = TaggableManager(through=ImpliedTagItem)

now of course a lot of your tag helpers will not work "as expected" per se, but you will be able to work off the same base of tags. You just need multiple through tables to track your stuff separately

rtpg avatar Apr 14 '21 03:04 rtpg

OK this topic definitely seems worthy of some FAQ-ing

rtpg avatar Apr 14 '21 03:04 rtpg

@rtpg I remember thinking that makes sense when reading it. Does this definitely work for you? I seem to get some variation of HINT: Add or change a related_name argument to the definition for 'MyFilter.parent_tags' or 'MyFilter.implied_tags'. no matter what combination of TagBase and ItemBase I use. I've added unique related_name to the tag ForeignKey and the content_object ForeignKey

For example, how would you make this work?

from taggit.managers import TaggableManager
from taggit.models import TagBase, ItemBase


class ContactSegment(TagBase):
    pass


class TaggedAddOperationSegment(ItemBase):
    tag = models.ForeignKey(ContactSegment, related_name="tagged_add_operation_segments", on_delete=models.CASCADE)    
    content_object = models.ForeignKey(
        to='Operation',
        on_delete=models.CASCADE,
        related_name='related_name_for_operation_add'
    )


class TaggedRemoveOperationSegment(ItemBase):
    tag = models.ForeignKey(ContactSegment, related_name="tagged_remove_operation_segments", on_delete=models.CASCADE)
    content_object = models.ForeignKey(
        to='Operation',
        on_delete=models.CASCADE,
        related_name='related_name_for_operation_remove'
    )   
    
    
    
class Operation(models.Model):
    add_segments = TaggableManager(through=TaggedAddOperationSegment, blank=True)
    remove_segments = TaggableManager(through=TaggedRemoveOperationSegment, blank=True)

Error:

SystemCheckError: System check identified some issues:

ERRORS:
bulk_contacts.Operation.add_segments: (fields.E304) Reverse accessor for 'Operation.add_segments' clashes with reverse accessor for 'Operation.remove_segments'.
        HINT: Add or change a related_name argument to the definition for 'Operation.add_segments' or 'Operation.remove_segments'.
bulk_contacts.Operation.remove_segments: (fields.E304) Reverse accessor for 'Operation.remove_segments' clashes with reverse accessor for 'Operation.add_segments'.
        HINT: Add or change a related_name argument to the definition for 'Operation.remove_segments' or 'Operation.add_segments'.

thenewguy avatar Jun 09 '21 15:06 thenewguy

Laughing at myself - read the error message.

class Operation(models.Model):
    add_segments = TaggableManager(through=TaggedAddOperationSegment, blank=True, related_name='foo')
    remove_segments = TaggableManager(through=TaggedRemoveOperationSegment, blank=True, related_name='bar')

thenewguy avatar Jun 09 '21 15:06 thenewguy

So what's the correct way to achieve this?

class Operation(models.Model):
    tags1  = TaggableManager()
    tags2 = TaggableManager()

Is this not used anymore in 2024(is there batter methods) ? Why docs doesn't provide FAQ examples for this common problem?

ChathuraGH avatar May 14 '24 06:05 ChathuraGH

@ChathuraGH This is a good point, we should have an FAQ for this, and some more explanations.

Short version of this is you want to use Custom Foreign Keys here. Basically create a separate through model for each.

Honestly this might be something we could support out of the box, without having to manually create your own through model.

rtpg avatar May 17 '24 05:05 rtpg