Question

What I'm looking for is a QuerySet containing any objects not tagged.

The solution I've come up with so far looks overly complicated to me:

# Get all tags for model
tags = Location.tags.all().order_by('name')

# Get a list of tagged location id's
tag_list = tags.values_list('name', flat=True)
tag_names = ', '.join(tag_list)
tagged_locations = Location.tagged.with_any(tag_names) \
                                  .values_list('id', flat=True)

untagged_locations = []
for location in Location.objects.all():
    if location.id not in tagged_locations:
        untagged_locations.append(location)

Any ideas for improvement? Thanks!

Was it helpful?

Solution

There is some good information in this post, so I don't feel that it should be deleted, but there is a much, much simpler solution

I took a quick peek at the source code for django-tagging. It looks like they use the ContentType framework and generic relations to pull it off.

Because of this, you should be able to create a generic reverse relation on your Location class to get easy access to the TaggedItem objects for a given location, if you haven't already done so:

from django.contrib.contenttypes import generic
from tagging.models import TaggedItem

class Location(models.Model):
    ...

    tagged_items = generic.GenericRelation(TaggedItem,
                                          object_id_field="object_id",
                                          content_type_field="content_type")

    ...

Clarification

My original answer suggested to do this:

untagged_locs = Location.objects.filter(tagged_items__isnull=True)

Although this would work for a 'normal join', this actually doesn't work here because the content type framework throws an additional check on content_type_id into the SQL for isnull:

SELECT [snip] FROM `sotest_location` 
LEFT OUTER JOIN `tagging_taggeditem` 
 ON (`sotest_location`.`id` = `tagging_taggeditem`.`object_id`) 
WHERE (`tagging_taggeditem`.`id` IS NULL 
 AND `tagging_taggeditem`.`content_type_id` = 4 )

You can hack-around it by reversing it like this:

untagged_locs = Location.objects.exclude(tagged_items__isnull=False)

But that doesn't quite feel right.

I also proposed this, but it was pointed out that annotations don't work as expected with the content types framework.

from django.db.models import Count
untagged_locs = Location.objects.annotate(
    num_tags=Count('tagged_items')).filter(num_tags=0)

The above code works for me in my limited test case, but it could be buggy if you have other 'taggable' objects in your model. The reason being that it doesn't check the content_type_id as outlined in the ticket. It generated the following SQL:

SELECT [snip], COUNT(`tagging_taggeditem`.`id`) AS `num_tags` 
 FROM `sotest_location` 
LEFT OUTER JOIN `tagging_taggeditem` 
 ON (`sotest_location`.`id` = `tagging_taggeditem`.`object_id`) 
GROUP BY `sotest_location`.`id` HAVING COUNT(`tagging_taggeditem`.`id`) = 0  
ORDER BY NULL

If Location is your only taggable object, then the above would work.

Proposed Workaround

Short of getting the annotation mechanism to work, here's what I would do in the meantime:

untagged_locs_e = Location.objects.extra(
        where=["""NOT EXISTS(SELECT 1 FROM tagging_taggeditem ti
 INNER JOIN django_content_type ct ON ti.content_type_id = ct.id
 WHERE ct.model = 'location'
  AND ti.object_id = myapp_location.id)"""]
)

This adds an additional WHERE clause to the SQL:

SELECT [snip] FROM `myapp_location` 
WHERE NOT EXISTS(SELECT 1 FROM tagging_taggeditem ti
 INNER JOIN django_content_type ct ON ti.content_type_id = ct.id
  WHERE ct.model = 'location'
   AND ti.object_id = myapp_location.id)

It joins to the django_content_type table to ensure that you're looking at the appropriate content type for your model in the case where you have more than one taggable model type.

Change myapp_location.id to match your table name. There's probably a way to avoid hard-coding the table names, but you can figure that out if it's important to you.

Adjust accordingly if you're not using MySQL.

OTHER TIPS

Try this:

[location for location in Location.objects.all() if location.tags.count() == 0]

Assuming your Location class uses the tagging.fields.TagField utility.

from tagging.fields import TagField
class Location(models.Model):
    tags = TagField()

You can just do this:

Location.objects.filter(tags='')
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top