2
Custom managers for generic tagging models.
2
Custom managers for Django models registered with the tagging
4
from django.db import connection
5
from django.db.models import Manager
6
from django.db.models.query import QuerySet, parse_lookup
7
from django.contrib.contenttypes.models import ContentType
8
from django.utils.translation import ugettext as _
10
from tagging import settings
11
from tagging.utils import calculate_cloud, get_tag_list, parse_tag_input
12
from tagging.utils import LOGARITHMIC
14
# Python 2.3 compatibility
15
if not hasattr(__builtins__, 'set'):
16
from sets import Set as set
18
qn = connection.ops.quote_name
20
class TagManager(Manager):
21
def update_tags(self, obj, tag_names):
23
Update tags associated with an object.
25
ctype = ContentType.objects.get_for_model(obj)
26
current_tags = list(self.filter(items__content_type__pk=ctype.pk,
27
items__object_id=obj.pk))
28
updated_tag_names = parse_tag_input(tag_names)
29
if settings.FORCE_LOWERCASE_TAGS:
30
updated_tag_names = [t.lower() for t in updated_tag_names]
32
TaggedItemModel = self._get_related_model_by_accessor('items')
34
# Remove tags which no longer apply
35
tags_for_removal = [tag for tag in current_tags \
36
if tag.name not in updated_tag_names]
37
if len(tags_for_removal):
38
TaggedItemModel._default_manager.filter(content_type__pk=ctype.pk,
40
tag__in=tags_for_removal).delete()
43
current_tag_names = [tag.name for tag in current_tags]
44
for tag_name in updated_tag_names:
45
if tag_name not in current_tag_names:
46
tag, created = self.get_or_create(name=tag_name)
47
TaggedItemModel._default_manager.create(tag=tag, object=obj)
49
def add_tag(self, obj, tag_name):
51
Associates the given object with a tag.
53
tag_names = parse_tag_input(tag_name)
54
if not len(tag_names):
55
raise AttributeError(_('No tags were given: "%s".') % tag_name)
56
if len(tag_names) > 1:
57
raise AttributeError(_('Multiple tags were given: "%s".') % tag_name)
58
tag_name = tag_names[0]
59
if settings.FORCE_LOWERCASE_TAGS:
60
tag_name = tag_name.lower()
61
tag, created = self.get_or_create(name=tag_name)
62
ctype = ContentType.objects.get_for_model(obj)
63
TaggedItemModel = self._get_related_model_by_accessor('items')
64
TaggedItemModel._default_manager.get_or_create(
65
tag=tag, content_type=ctype, object_id=obj.pk)
67
def get_for_object(self, obj):
69
Create a queryset matching all tags associated with the given
72
ctype = ContentType.objects.get_for_model(obj)
73
return self.filter(items__content_type__pk=ctype.pk,
74
items__object_id=obj.pk)
76
def usage_for_model(self, model, counts=False, min_count=None, filters=None):
78
Obtain a list of tags associated with instances of the given
81
If ``counts`` is True, a ``count`` attribute will be added to
82
each tag, indicating how many times it has been used against
83
the Model class in question.
85
If ``min_count`` is given, only tags which have a ``count``
86
greater than or equal to ``min_count`` will be returned.
87
Passing a value for ``min_count`` implies ``counts=True``.
89
To limit the tags (and counts, if specified) returned to those
90
used by a subset of the Model's instances, pass a dictionary
91
of field lookups to be applied to the given Model as the
94
if filters is None: filters = {}
95
if min_count is not None: counts = True
97
model_table = qn(model._meta.db_table)
98
model_pk = '%s.%s' % (model_table, qn(model._meta.pk.column))
100
SELECT DISTINCT %(tag)s.id, %(tag)s.name%(count_sql)s
103
INNER JOIN %(tagged_item)s
104
ON %(tag)s.id = %(tagged_item)s.tag_id
106
ON %(tagged_item)s.object_id = %(model_pk)s
108
WHERE %(tagged_item)s.content_type_id = %(content_type_id)s
110
GROUP BY %(tag)s.id, %(tag)s.name
112
ORDER BY %(tag)s.name ASC""" % {
113
'tag': qn(self.model._meta.db_table),
114
'count_sql': counts and (', COUNT(%s)' % model_pk) or '',
115
'tagged_item': qn(self._get_related_model_by_accessor('items')._meta.db_table),
116
'model': model_table,
117
'model_pk': model_pk,
118
'content_type_id': ContentType.objects.get_for_model(model).pk,
126
joins, where, params = parse_lookup(filters.items(), model._meta)
127
extra_joins = ' '.join(['%s %s AS %s ON %s' % (join_type, table, alias, condition)
128
for (alias, (table, join_type, condition)) in joins.items()])
129
extra_criteria = 'AND %s' % (' AND '.join(where))
130
if min_count is not None:
131
min_count_sql = 'HAVING COUNT(%s) >= %%s' % model_pk
132
params.append(min_count)
134
cursor = connection.cursor()
135
cursor.execute(query % (extra_joins, extra_criteria, min_count_sql), params)
137
for row in cursor.fetchall():
138
t = self.model(*row[:2])
144
def related_for_model(self, tags, model, counts=False, min_count=None):
146
Obtain a list of tags related to a given list of tags - that
147
is, other tags used by items which have all the given tags.
149
If ``counts`` is True, a ``count`` attribute will be added to
150
each tag, indicating the number of items which have it in
151
addition to the given list of tags.
153
If ``min_count`` is given, only tags which have a ``count``
154
greater than or equal to ``min_count`` will be returned.
155
Passing a value for ``min_count`` implies ``counts=True``.
157
if min_count is not None: counts = True
158
tags = get_tag_list(tags)
159
tag_count = len(tags)
160
tagged_item_table = qn(self._get_related_model_by_accessor('items')._meta.db_table)
162
SELECT %(tag)s.id, %(tag)s.name%(count_sql)s
163
FROM %(tagged_item)s INNER JOIN %(tag)s ON %(tagged_item)s.tag_id = %(tag)s.id
164
WHERE %(tagged_item)s.content_type_id = %(content_type_id)s
165
AND %(tagged_item)s.object_id IN
167
SELECT %(tagged_item)s.object_id
168
FROM %(tagged_item)s, %(tag)s
169
WHERE %(tagged_item)s.content_type_id = %(content_type_id)s
170
AND %(tag)s.id = %(tagged_item)s.tag_id
171
AND %(tag)s.id IN (%(tag_id_placeholders)s)
172
GROUP BY %(tagged_item)s.object_id
173
HAVING COUNT(%(tagged_item)s.object_id) = %(tag_count)s
175
AND %(tag)s.id NOT IN (%(tag_id_placeholders)s)
176
GROUP BY %(tag)s.id, %(tag)s.name
178
ORDER BY %(tag)s.name ASC""" % {
179
'tag': qn(self.model._meta.db_table),
180
'count_sql': counts and ', COUNT(%s.object_id)' % tagged_item_table or '',
181
'tagged_item': tagged_item_table,
182
'content_type_id': ContentType.objects.get_for_model(model).pk,
183
'tag_id_placeholders': ','.join(['%s'] * tag_count),
184
'tag_count': tag_count,
185
'min_count_sql': min_count is not None and ('HAVING COUNT(%s.object_id) >= %%s' % tagged_item_table) or '',
188
params = [tag.pk for tag in tags] * 2
189
if min_count is not None:
190
params.append(min_count)
192
cursor = connection.cursor()
193
cursor.execute(query, params)
195
for row in cursor.fetchall():
196
tag = self.model(*row[:2])
202
def cloud_for_model(self, model, steps=4, distribution=LOGARITHMIC,
203
filters=None, min_count=None):
205
Obtain a list of tags associated with instances of the given
206
Model, giving each tag a ``count`` attribute indicating how
207
many times it has been used and a ``font_size`` attribute for
208
use in displaying a tag cloud.
210
``steps`` defines the range of font sizes - ``font_size`` will
211
be an integer between 1 and ``steps`` (inclusive).
213
``distribution`` defines the type of font size distribution
214
algorithm which will be used - logarithmic or linear. It must
215
be either ``tagging.utils.LOGARITHMIC`` or
216
``tagging.utils.LINEAR``.
218
To limit the tags displayed in the cloud to those associated
219
with a subset of the Model's instances, pass a dictionary of
220
field lookups to be applied to the given Model as the
221
``filters`` argument.
223
To limit the tags displayed in the cloud to those with a
224
``count`` greater than or equal to ``min_count``, pass a value
225
for the ``min_count`` argument.
227
tags = list(self.usage_for_model(model, counts=True, filters=filters,
228
min_count=min_count))
229
return calculate_cloud(tags, steps, distribution)
231
def _get_related_model_by_accessor(self, accessor):
233
Returns the model for the related object accessed by the
234
given attribute name.
236
Since we sometimes need to access the ``TaggedItem`` model
237
when managing tagging and the``Tag`` model does not have a
238
field representing this relationship, this method is used to
239
retrieve the ``TaggedItem`` model, avoiding circular imports
240
betweeen the ``models`` and ``managers`` modules.
242
for rel_obj in self.model._meta.get_all_related_objects():
243
if rel_obj.get_accessor_name() == accessor:
245
raise ValueError(_('Could not find a related object with the accessor "%s".') % accessor)
247
class TaggedItemManager(Manager):
248
def get_by_model(self, model, tags):
250
Create a queryset matching instances of the given Model
251
associated with a given Tag or list of Tags.
253
tags = get_tag_list(tags)
254
tag_count = len(tags)
256
# No existing tags were given
257
return model._default_manager.none()
259
# Optimisation for single tag - fall through to the simpler
263
return self.get_intersection_by_model(model, tags)
265
ctype = ContentType.objects.get_for_model(model)
266
opts = self.model._meta
267
tagged_item_table = qn(opts.db_table)
268
return model._default_manager.extra(
269
tables=[opts.db_table],
271
'%s.content_type_id = %%s' % tagged_item_table,
272
'%s.tag_id = %%s' % tagged_item_table,
273
'%s.%s = %s.object_id' % (qn(model._meta.db_table),
274
qn(model._meta.pk.column),
277
params=[ctype.pk, tag.pk],
280
def get_intersection_by_model(self, model, tags):
282
Create a queryset matching instances of the given Model
283
associated with all the given list of Tags.
285
FIXME The query currently used to grab the ids of objects
286
which have all the tags should be all that we need run,
287
using a non-explicit join for the QuerySet returned, as
288
in get_by_model, but there's currently no way to get the
289
required GROUP BY and HAVING clauses into Django's ORM.
291
Once the ORM is capable of this, we should have a
292
solution which requires only a single query and won't
293
have the problem where the number of ids in the IN
294
clause in the QuerySet could exceed the length allowed,
295
as could currently happen.
297
tags = get_tag_list(tags)
298
tag_count = len(tags)
299
model_table = qn(model._meta.db_table)
300
# This query selects the ids of all objects which have all the
304
FROM %(model)s, %(tagged_item)s
305
WHERE %(tagged_item)s.content_type_id = %(content_type_id)s
306
AND %(tagged_item)s.tag_id IN (%(tag_id_placeholders)s)
307
AND %(model_pk)s = %(tagged_item)s.object_id
308
GROUP BY %(model_pk)s
309
HAVING COUNT(%(model_pk)s) = %(tag_count)s""" % {
310
'model_pk': '%s.%s' % (model_table, qn(model._meta.pk.column)),
311
'model': model_table,
312
'tagged_item': qn(self.model._meta.db_table),
313
'content_type_id': ContentType.objects.get_for_model(model).pk,
314
'tag_id_placeholders': ','.join(['%s'] * tag_count),
315
'tag_count': tag_count,
318
cursor = connection.cursor()
319
cursor.execute(query, [tag.pk for tag in tags])
320
object_ids = [row[0] for row in cursor.fetchall()]
321
if len(object_ids) > 0:
322
return model._default_manager.filter(pk__in=object_ids)
324
return model._default_manager.none()
326
def get_union_by_model(self, model, tags):
328
Create a queryset matching instances of the given Model
329
associated with any of the given list of Tags.
331
tags = get_tag_list(tags)
332
tag_count = len(tags)
333
model_table = qn(model._meta.db_table)
334
# This query selects the ids of all objects which have any of
338
FROM %(model)s, %(tagged_item)s
339
WHERE %(tagged_item)s.content_type_id = %(content_type_id)s
340
AND %(tagged_item)s.tag_id IN (%(tag_id_placeholders)s)
341
AND %(model_pk)s = %(tagged_item)s.object_id
342
GROUP BY %(model_pk)s""" % {
343
'model_pk': '%s.%s' % (model_table, qn(model._meta.pk.column)),
344
'model': model_table,
345
'tagged_item': qn(self.model._meta.db_table),
346
'content_type_id': ContentType.objects.get_for_model(model).pk,
347
'tag_id_placeholders': ','.join(['%s'] * tag_count),
350
cursor = connection.cursor()
351
cursor.execute(query, [tag.pk for tag in tags])
352
object_ids = [row[0] for row in cursor.fetchall()]
353
if len(object_ids) > 0:
354
return model._default_manager.filter(pk__in=object_ids)
356
return model._default_manager.none()
358
def get_related(self, obj, model, num=None):
360
Retrieve instances of ``model`` which share tags with the
361
model instance ``obj``, ordered by the number of shared tags
364
If ``num`` is given, a maximum of ``num`` instances will be
367
model_table = qn(model._meta.db_table)
368
content_type = ContentType.objects.get_for_model(obj)
369
related_content_type = ContentType.objects.get_for_model(model)
371
SELECT %(model_pk)s, COUNT(related_tagged_item.object_id) AS %(count)s
372
FROM %(model)s, %(tagged_item)s, %(tag)s, %(tagged_item)s related_tagged_item
373
WHERE %(tagged_item)s.object_id = %%s
374
AND %(tagged_item)s.content_type_id = %(content_type_id)s
375
AND %(tag)s.id = %(tagged_item)s.tag_id
376
AND related_tagged_item.content_type_id = %(related_content_type_id)s
377
AND related_tagged_item.tag_id = %(tagged_item)s.tag_id
378
AND %(model_pk)s = related_tagged_item.object_id"""
379
if content_type.pk == related_content_type.pk:
380
# Exclude the given instance itself if determining related
381
# instances for the same model.
383
AND related_tagged_item.object_id != %(tagged_item)s.object_id"""
385
GROUP BY %(model_pk)s
386
ORDER BY %(count)s DESC
389
'model_pk': '%s.%s' % (model_table, qn(model._meta.pk.column)),
390
'count': qn('count'),
391
'model': model_table,
392
'tagged_item': qn(self.model._meta.db_table),
393
'tag': qn(self.model._meta.get_field('tag').rel.to._meta.db_table),
394
'content_type_id': content_type.pk,
395
'related_content_type_id': related_content_type.pk,
396
'limit_offset': num is not None and connection.ops.limit_offset_sql(num) or '',
399
cursor = connection.cursor()
400
cursor.execute(query, [obj.pk])
401
object_ids = [row[0] for row in cursor.fetchall()]
402
if len(object_ids) > 0:
403
# Use in_bulk here instead of an id__in lookup, because id__in would
404
# clobber the ordering.
405
object_dict = model._default_manager.in_bulk(object_ids)
406
return [object_dict[object_id] for object_id in object_ids]
408
return model._default_manager.none()
5
from django.db import models
7
from tagging.models import TaggedItem
9
class ModelTaggedItemManager(models.Manager):
11
A manager for retrieving model instances based on their tags.
13
def related_to(self, obj):
14
return TaggedItem.objects.get_related(obj, self.model)
16
def with_all(self, tags):
17
return TaggedItem.objects.get_by_model(self.model, tags)
19
def with_any(self, tags):
20
return TaggedItem.objects.get_union_by_model(self.model, tags)