views:

221

answers:

3

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!

A: 

Try this:

[location for location in Location.objects.all() if location.tags.count() == 0]
jcdyer
use parenthesis instead square brackets to get a generator instead of a list. (mostly) same usage, far lower memory requirements.
Javier
Well, this is a list of objects not tagged, but it's not a **queryset** of objects not tagged. Key differences being that it has no manager functionality and it isn't lazily evaluated.
TM
+3  A: 

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.

Joe Holloway
The 'object\_id\_field' and 'content\_type\_field' parameters are gratuitous here since they default exactly to what is being passed, but I felt including them would be helpful to show the API.
Joe Holloway
Thanks for providing a great answer! However, I might be doing something wrong but I can't get it to work. I can get `Location.objects.filter(tagged_items__isnull=False)` to give me the set of tagged items but `Location.objects.filter(tagged_items__isnull=True)` just gives me an empty query set.
lemonad
Also, do annotations really work correctly with generic relations? The following ticket indicates it does not: http://code.djangoproject.com/ticket/10461
lemonad
@lemonad That's unfortunate that annotations do not yet work with generic relations. On the first comment, what if do this: `Location.objects.exclude(tagged_items__isnull=False)` ... It seems the other way should work as I've done that before. Tomorrow I'll put together a test case of my own and play with it.
Joe Holloway
Thanks for the updated answer! The reason I'm using django-tagging is because I've got several models requiring tagging so I can't rely on annotations as it stands today.I'll try your sql suggestion later today! I can't hardcode (due to having migrations to worry about) but that should be no problem.
lemonad
The reason I had so many problems was that 0.3 contained a bug and I was using in it in a way that triggered that bug. I'll accept this answer! Thanks a lot for all the help!
lemonad
A: 

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='')
Joe Holloway
Unfortunately django-tagging does not introduce the 'tags' column so that it can be used in filters. Not for me, anyhow... and I think I've set everything up according to the "manual".
lemonad
I found this by working through a simple django-tagging example on my own, but I'm thinking you have to use the `TagField` utility for it to be included on your model.
Joe Holloway
Aha, I can see how that would work. I have the `TagField` but unfortunately you can't name it `tags` if you register the model with tagging. I encountered some really ugly bugs when I did (tags were being removed when retrieving them with TaggedItem, etc.). After changing the name, everything worked fine.I found the renaming advice here: http://tylerlesmann.com/2009/mar/06/adding-tagging-django-10-applications-existing-dat/
lemonad
Oh, and the `TagField` is always empty (for me) when named something else than `tags`. It's usable in forms and the admin though.
lemonad
In my test I didn't register the model so you're probably right. When you changed the name of the TagField, did you also change the name of the underlying database column? I can see how that would cause some strange behavior.
Joe Holloway