1 # -*- coding: utf-8 -*-
3 Models and managers for generic tagging.
6 from django.contrib.contenttypes.models import ContentType
7 from django.db import connection, models
8 from django.db.models.base import ModelBase
9 from django.dispatch import Signal
11 qn = connection.ops.quote_name
13 tags_updated = Signal(providing_args=["affected_tags"])
16 def get_queryset_and_model(queryset_or_model):
18 Given a ``QuerySet`` or a ``Model``, returns a two-tuple of
21 If a ``Model`` is given, the ``QuerySet`` returned will be created
22 using its default manager.
25 return queryset_or_model, queryset_or_model.model
26 except AttributeError:
27 return queryset_or_model.objects.all(), queryset_or_model
33 class TagManager(models.Manager):
35 super(TagManager, self).__init__()
36 models.signals.pre_delete.connect(self.target_deleted)
39 def intermediary_table_model(self):
40 return self.model.intermediary_table_model
42 def target_deleted(self, instance, **kwargs):
43 """ clear tag relations before deleting an object """
49 self.update_tags(instance, [])
51 def update_tags(self, obj, tags):
53 Update tags associated with an object.
55 content_type = ContentType.objects.get_for_model(obj)
56 current_tags = list(self.filter(items__content_type__pk=content_type.pk,
57 items__object_id=obj.pk))
60 # Remove tags which no longer apply
61 tags_for_removal = [tag for tag in current_tags if tag not in updated_tags]
62 if len(tags_for_removal):
63 self.intermediary_table_model.objects.filter(
64 content_type__pk=content_type.pk,
66 tag__in=tags_for_removal).delete()
68 tags_to_add = [tag for tag in updated_tags if tag not in current_tags]
69 for tag in tags_to_add:
70 existing = self.intermediary_table_model.objects.filter(
71 content_type__pk=content_type.pk, object_id=obj.pk, tag=tag)
73 self.intermediary_table_model.objects.create(tag=tag, content_object=obj)
75 tags_updated.send(sender=type(obj), instance=obj, affected_tags=tags_to_add + tags_for_removal)
77 def remove_tag(self, obj, tag):
79 Remove tag from an object.
81 content_type = ContentType.objects.get_for_model(obj)
82 self.intermediary_table_model.objects.filter(
83 content_type__pk=content_type.pk, object_id=obj.pk, tag=tag).delete()
85 def add_tag(self, obj, tag):
89 content_type = ContentType.objects.get_for_model(obj)
90 relations = self.intermediary_table_model.objects.filter(
91 content_type__pk=content_type.pk, object_id=obj.pk, tag=tag)
93 self.intermediary_table_model.objects.create(tag=tag, content_object=obj)
95 def get_for_object(self, obj):
97 Create a queryset matching all tags associated with the given
100 ctype = ContentType.objects.get_for_model(obj)
101 return self.filter(items__content_type__pk=ctype.pk,
102 items__object_id=obj.pk)
104 def usage_for_model(self, model, counts=False, filters=None):
106 Obtain a list of tags associated with instances of the given
109 If ``counts`` is True, a ``count`` attribute will be added to
110 each tag, indicating how many times it has been used against
111 the Model class in question.
113 To limit the tags (and counts, if specified) returned to those
114 used by a subset of the Model's instances, pass a dictionary
115 of field lookups to be applied to the given Model as the
116 ``filters`` argument.
118 # TODO: Do we really need this filters stuff?
122 queryset = model.objects.filter()
123 for f in filters.items():
124 queryset.query.add_filter(f)
125 usage = self.usage_for_queryset(queryset, counts)
128 def usage_for_queryset(self, queryset, counts=False):
130 Obtain a list of tags associated with instances of a model
131 contained in the given queryset.
133 If ``counts`` is True, a ``count`` attribute will be added to
134 each tag, indicating how many times it has been used against
135 the Model class in question.
137 usage = self.model.objects.filter(
138 items__content_type=ContentType.objects.get_for_model(queryset.model),
139 items__object_id__in=queryset)
141 usage = usage.annotate(count=models.Count('id'))
143 usage = usage.distinct()
146 def related_for_model(self, tags, model, counts=False):
148 Obtain a list of tags related to a given list of tags - that
149 is, other tags used by items which have all the given tags.
151 If ``counts`` is True, a ``count`` attribute will be added to
152 each tag, indicating the number of items which have it in
153 addition to the given list of tags.
155 objs = self.model.intermediary_table_model.objects.get_by_model(model, tags)
156 qs = self.usage_for_queryset(objs, counts)
157 qs = qs.exclude(pk__in=[tag.pk for tag in tags])
161 class TaggedItemManager(models.Manager):
164 return self.model.tag_model
166 def get_by_model(self, queryset_or_model, tags):
168 Create a ``QuerySet`` containing instances of the specified
169 model associated with a given tag or list of tags.
171 queryset, model = get_queryset_and_model(queryset_or_model)
173 # No existing tags were given
174 return queryset.none()
176 # TODO: presumes reverse generic relation
177 # Multiple joins are WAY faster than having-count, at least on Postgres 9.1.
179 queryset = queryset.filter(tag_relations__tag=tag)
182 def get_union_by_model(self, queryset_or_model, tags):
184 Create a ``QuerySet`` containing instances of the specified
185 model associated with *any* of the given list of tags.
187 queryset, model = get_queryset_and_model(queryset_or_model)
189 return queryset.none()
190 # TODO: presumes reverse generic relation
191 return queryset.filter(tag_relations__tag__in=tags).distinct()
193 def get_related(self, obj, queryset_or_model):
195 Retrieve a list of instances of the specified model which share
196 tags with the model instance ``obj``, ordered by the number of
197 shared tags in descending order.
199 queryset, model = get_queryset_and_model(queryset_or_model)
200 # TODO: presumes reverse generic relation.
201 # Do we know it's 'tags'?
202 return queryset.filter(tag_relations__tag__in=obj.tags).annotate(
203 count=models.Count('pk')).order_by('-count').exclude(pk=obj.pk)