|
11
|
1 |
""" |
|
|
2 |
Models and managers for generic tagging. |
|
|
3 |
""" |
|
|
4 |
# Python 2.3 compatibility |
|
|
5 |
try: |
|
|
6 |
set |
|
|
7 |
except NameError: |
|
|
8 |
from sets import Set as set |
|
|
9 |
|
|
|
10 |
from django.contrib.contenttypes import generic |
|
|
11 |
from django.contrib.contenttypes.models import ContentType |
|
|
12 |
from django.db import connection, models |
|
|
13 |
from django.db.models.query import QuerySet |
|
|
14 |
from django.utils.translation import ugettext_lazy as _ |
|
|
15 |
|
|
|
16 |
from tagging import settings |
|
|
17 |
from tagging.utils import calculate_cloud, get_tag_list, get_queryset_and_model, parse_tag_input |
|
|
18 |
from tagging.utils import LOGARITHMIC |
|
|
19 |
|
|
|
20 |
qn = connection.ops.quote_name |
|
|
21 |
|
|
|
22 |
############ |
|
|
23 |
# Managers # |
|
|
24 |
############ |
|
|
25 |
|
|
|
26 |
class TagManager(models.Manager): |
|
|
27 |
def update_tags(self, obj, tag_names): |
|
|
28 |
""" |
|
|
29 |
Update tags associated with an object. |
|
|
30 |
""" |
|
|
31 |
ctype = ContentType.objects.get_for_model(obj) |
|
|
32 |
current_tags = list(self.filter(items__content_type__pk=ctype.pk, |
|
|
33 |
items__object_id=obj.pk)) |
|
|
34 |
updated_tag_names = parse_tag_input(tag_names) |
|
|
35 |
if settings.FORCE_LOWERCASE_TAGS: |
|
|
36 |
updated_tag_names = [t.lower() for t in updated_tag_names] |
|
|
37 |
|
|
|
38 |
# Remove tags which no longer apply |
|
|
39 |
tags_for_removal = [tag for tag in current_tags \ |
|
|
40 |
if tag.name not in updated_tag_names] |
|
|
41 |
if len(tags_for_removal): |
|
|
42 |
TaggedItem._default_manager.filter(content_type__pk=ctype.pk, |
|
|
43 |
object_id=obj.pk, |
|
|
44 |
tag__in=tags_for_removal).delete() |
|
|
45 |
# Add new tags |
|
|
46 |
current_tag_names = [tag.name for tag in current_tags] |
|
|
47 |
for tag_name in updated_tag_names: |
|
|
48 |
if tag_name not in current_tag_names: |
|
|
49 |
tag, created = self.get_or_create(name=tag_name) |
|
|
50 |
TaggedItem._default_manager.create(tag=tag, object=obj) |
|
|
51 |
|
|
|
52 |
def add_tag(self, obj, tag_name): |
|
|
53 |
""" |
|
|
54 |
Associates the given object with a tag. |
|
|
55 |
""" |
|
|
56 |
tag_names = parse_tag_input(tag_name) |
|
|
57 |
if not len(tag_names): |
|
|
58 |
raise AttributeError(_('No tags were given: "%s".') % tag_name) |
|
|
59 |
if len(tag_names) > 1: |
|
|
60 |
raise AttributeError(_('Multiple tags were given: "%s".') % tag_name) |
|
|
61 |
tag_name = tag_names[0] |
|
|
62 |
if settings.FORCE_LOWERCASE_TAGS: |
|
|
63 |
tag_name = tag_name.lower() |
|
|
64 |
tag, created = self.get_or_create(name=tag_name) |
|
|
65 |
ctype = ContentType.objects.get_for_model(obj) |
|
|
66 |
TaggedItem._default_manager.get_or_create( |
|
|
67 |
tag=tag, content_type=ctype, object_id=obj.pk) |
|
|
68 |
|
|
|
69 |
def get_for_object(self, obj): |
|
|
70 |
""" |
|
|
71 |
Create a queryset matching all tags associated with the given |
|
|
72 |
object. |
|
|
73 |
""" |
|
|
74 |
ctype = ContentType.objects.get_for_model(obj) |
|
|
75 |
return self.filter(items__content_type__pk=ctype.pk, |
|
|
76 |
items__object_id=obj.pk) |
|
|
77 |
|
|
|
78 |
def _get_usage(self, model, counts=False, min_count=None, extra_joins=None, extra_criteria=None, params=None): |
|
|
79 |
""" |
|
|
80 |
Perform the custom SQL query for ``usage_for_model`` and |
|
|
81 |
``usage_for_queryset``. |
|
|
82 |
""" |
|
|
83 |
if min_count is not None: counts = True |
|
|
84 |
|
|
|
85 |
model_table = qn(model._meta.db_table) |
|
|
86 |
model_pk = '%s.%s' % (model_table, qn(model._meta.pk.column)) |
|
|
87 |
query = """ |
|
|
88 |
SELECT DISTINCT %(tag)s.id, %(tag)s.name%(count_sql)s |
|
|
89 |
FROM |
|
|
90 |
%(tag)s |
|
|
91 |
INNER JOIN %(tagged_item)s |
|
|
92 |
ON %(tag)s.id = %(tagged_item)s.tag_id |
|
|
93 |
INNER JOIN %(model)s |
|
|
94 |
ON %(tagged_item)s.object_id = %(model_pk)s |
|
|
95 |
%%s |
|
|
96 |
WHERE %(tagged_item)s.content_type_id = %(content_type_id)s |
|
|
97 |
%%s |
|
|
98 |
GROUP BY %(tag)s.id, %(tag)s.name |
|
|
99 |
%%s |
|
|
100 |
ORDER BY %(tag)s.name ASC""" % { |
|
|
101 |
'tag': qn(self.model._meta.db_table), |
|
|
102 |
'count_sql': counts and (', COUNT(%s)' % model_pk) or '', |
|
|
103 |
'tagged_item': qn(TaggedItem._meta.db_table), |
|
|
104 |
'model': model_table, |
|
|
105 |
'model_pk': model_pk, |
|
|
106 |
'content_type_id': ContentType.objects.get_for_model(model).pk, |
|
|
107 |
} |
|
|
108 |
|
|
|
109 |
min_count_sql = '' |
|
|
110 |
if min_count is not None: |
|
|
111 |
min_count_sql = 'HAVING COUNT(%s) >= %%s' % model_pk |
|
|
112 |
params.append(min_count) |
|
|
113 |
|
|
|
114 |
cursor = connection.cursor() |
|
|
115 |
cursor.execute(query % (extra_joins, extra_criteria, min_count_sql), params) |
|
|
116 |
tags = [] |
|
|
117 |
for row in cursor.fetchall(): |
|
|
118 |
t = self.model(*row[:2]) |
|
|
119 |
if counts: |
|
|
120 |
t.count = row[2] |
|
|
121 |
tags.append(t) |
|
|
122 |
return tags |
|
|
123 |
|
|
|
124 |
def usage_for_model(self, model, counts=False, min_count=None, filters=None): |
|
|
125 |
""" |
|
|
126 |
Obtain a list of tags associated with instances of the given |
|
|
127 |
Model class. |
|
|
128 |
|
|
|
129 |
If ``counts`` is True, a ``count`` attribute will be added to |
|
|
130 |
each tag, indicating how many times it has been used against |
|
|
131 |
the Model class in question. |
|
|
132 |
|
|
|
133 |
If ``min_count`` is given, only tags which have a ``count`` |
|
|
134 |
greater than or equal to ``min_count`` will be returned. |
|
|
135 |
Passing a value for ``min_count`` implies ``counts=True``. |
|
|
136 |
|
|
|
137 |
To limit the tags (and counts, if specified) returned to those |
|
|
138 |
used by a subset of the Model's instances, pass a dictionary |
|
|
139 |
of field lookups to be applied to the given Model as the |
|
|
140 |
``filters`` argument. |
|
|
141 |
""" |
|
|
142 |
if filters is None: filters = {} |
|
|
143 |
|
|
|
144 |
queryset = model._default_manager.filter() |
|
|
145 |
for f in filters.items(): |
|
|
146 |
queryset.query.add_filter(f) |
|
|
147 |
usage = self.usage_for_queryset(queryset, counts, min_count) |
|
|
148 |
|
|
|
149 |
return usage |
|
|
150 |
|
|
|
151 |
def usage_for_queryset(self, queryset, counts=False, min_count=None): |
|
|
152 |
""" |
|
|
153 |
Obtain a list of tags associated with instances of a model |
|
|
154 |
contained in the given queryset. |
|
|
155 |
|
|
|
156 |
If ``counts`` is True, a ``count`` attribute will be added to |
|
|
157 |
each tag, indicating how many times it has been used against |
|
|
158 |
the Model class in question. |
|
|
159 |
|
|
|
160 |
If ``min_count`` is given, only tags which have a ``count`` |
|
|
161 |
greater than or equal to ``min_count`` will be returned. |
|
|
162 |
Passing a value for ``min_count`` implies ``counts=True``. |
|
|
163 |
""" |
|
|
164 |
|
|
|
165 |
if getattr(queryset.query, 'get_compiler', None): |
|
|
166 |
# Django 1.2+ |
|
|
167 |
compiler = queryset.query.get_compiler(using='default') |
|
|
168 |
extra_joins = ' '.join(compiler.get_from_clause()[0][1:]) |
|
|
169 |
where, params = queryset.query.where.as_sql( |
|
|
170 |
compiler.quote_name_unless_alias, compiler.connection |
|
|
171 |
) |
|
|
172 |
else: |
|
|
173 |
# Django pre-1.2 |
|
|
174 |
extra_joins = ' '.join(queryset.query.get_from_clause()[0][1:]) |
|
|
175 |
where, params = queryset.query.where.as_sql() |
|
|
176 |
|
|
|
177 |
if where: |
|
|
178 |
extra_criteria = 'AND %s' % where |
|
|
179 |
else: |
|
|
180 |
extra_criteria = '' |
|
|
181 |
return self._get_usage(queryset.model, counts, min_count, extra_joins, extra_criteria, params) |
|
|
182 |
|
|
|
183 |
def related_for_model(self, tags, model, counts=False, min_count=None): |
|
|
184 |
""" |
|
|
185 |
Obtain a list of tags related to a given list of tags - that |
|
|
186 |
is, other tags used by items which have all the given tags. |
|
|
187 |
|
|
|
188 |
If ``counts`` is True, a ``count`` attribute will be added to |
|
|
189 |
each tag, indicating the number of items which have it in |
|
|
190 |
addition to the given list of tags. |
|
|
191 |
|
|
|
192 |
If ``min_count`` is given, only tags which have a ``count`` |
|
|
193 |
greater than or equal to ``min_count`` will be returned. |
|
|
194 |
Passing a value for ``min_count`` implies ``counts=True``. |
|
|
195 |
""" |
|
|
196 |
if min_count is not None: counts = True |
|
|
197 |
tags = get_tag_list(tags) |
|
|
198 |
tag_count = len(tags) |
|
|
199 |
tagged_item_table = qn(TaggedItem._meta.db_table) |
|
|
200 |
query = """ |
|
|
201 |
SELECT %(tag)s.id, %(tag)s.name%(count_sql)s |
|
|
202 |
FROM %(tagged_item)s INNER JOIN %(tag)s ON %(tagged_item)s.tag_id = %(tag)s.id |
|
|
203 |
WHERE %(tagged_item)s.content_type_id = %(content_type_id)s |
|
|
204 |
AND %(tagged_item)s.object_id IN |
|
|
205 |
( |
|
|
206 |
SELECT %(tagged_item)s.object_id |
|
|
207 |
FROM %(tagged_item)s, %(tag)s |
|
|
208 |
WHERE %(tagged_item)s.content_type_id = %(content_type_id)s |
|
|
209 |
AND %(tag)s.id = %(tagged_item)s.tag_id |
|
|
210 |
AND %(tag)s.id IN (%(tag_id_placeholders)s) |
|
|
211 |
GROUP BY %(tagged_item)s.object_id |
|
|
212 |
HAVING COUNT(%(tagged_item)s.object_id) = %(tag_count)s |
|
|
213 |
) |
|
|
214 |
AND %(tag)s.id NOT IN (%(tag_id_placeholders)s) |
|
|
215 |
GROUP BY %(tag)s.id, %(tag)s.name |
|
|
216 |
%(min_count_sql)s |
|
|
217 |
ORDER BY %(tag)s.name ASC""" % { |
|
|
218 |
'tag': qn(self.model._meta.db_table), |
|
|
219 |
'count_sql': counts and ', COUNT(%s.object_id)' % tagged_item_table or '', |
|
|
220 |
'tagged_item': tagged_item_table, |
|
|
221 |
'content_type_id': ContentType.objects.get_for_model(model).pk, |
|
|
222 |
'tag_id_placeholders': ','.join(['%s'] * tag_count), |
|
|
223 |
'tag_count': tag_count, |
|
|
224 |
'min_count_sql': min_count is not None and ('HAVING COUNT(%s.object_id) >= %%s' % tagged_item_table) or '', |
|
|
225 |
} |
|
|
226 |
|
|
|
227 |
params = [tag.pk for tag in tags] * 2 |
|
|
228 |
if min_count is not None: |
|
|
229 |
params.append(min_count) |
|
|
230 |
|
|
|
231 |
cursor = connection.cursor() |
|
|
232 |
cursor.execute(query, params) |
|
|
233 |
related = [] |
|
|
234 |
for row in cursor.fetchall(): |
|
|
235 |
tag = self.model(*row[:2]) |
|
|
236 |
if counts is True: |
|
|
237 |
tag.count = row[2] |
|
|
238 |
related.append(tag) |
|
|
239 |
return related |
|
|
240 |
|
|
|
241 |
def cloud_for_model(self, model, steps=4, distribution=LOGARITHMIC, |
|
|
242 |
filters=None, min_count=None): |
|
|
243 |
""" |
|
|
244 |
Obtain a list of tags associated with instances of the given |
|
|
245 |
Model, giving each tag a ``count`` attribute indicating how |
|
|
246 |
many times it has been used and a ``font_size`` attribute for |
|
|
247 |
use in displaying a tag cloud. |
|
|
248 |
|
|
|
249 |
``steps`` defines the range of font sizes - ``font_size`` will |
|
|
250 |
be an integer between 1 and ``steps`` (inclusive). |
|
|
251 |
|
|
|
252 |
``distribution`` defines the type of font size distribution |
|
|
253 |
algorithm which will be used - logarithmic or linear. It must |
|
|
254 |
be either ``tagging.utils.LOGARITHMIC`` or |
|
|
255 |
``tagging.utils.LINEAR``. |
|
|
256 |
|
|
|
257 |
To limit the tags displayed in the cloud to those associated |
|
|
258 |
with a subset of the Model's instances, pass a dictionary of |
|
|
259 |
field lookups to be applied to the given Model as the |
|
|
260 |
``filters`` argument. |
|
|
261 |
|
|
|
262 |
To limit the tags displayed in the cloud to those with a |
|
|
263 |
``count`` greater than or equal to ``min_count``, pass a value |
|
|
264 |
for the ``min_count`` argument. |
|
|
265 |
""" |
|
|
266 |
tags = list(self.usage_for_model(model, counts=True, filters=filters, |
|
|
267 |
min_count=min_count)) |
|
|
268 |
return calculate_cloud(tags, steps, distribution) |
|
|
269 |
|
|
|
270 |
class TaggedItemManager(models.Manager): |
|
|
271 |
""" |
|
|
272 |
FIXME There's currently no way to get the ``GROUP BY`` and ``HAVING`` |
|
|
273 |
SQL clauses required by many of this manager's methods into |
|
|
274 |
Django's ORM. |
|
|
275 |
|
|
|
276 |
For now, we manually execute a query to retrieve the PKs of |
|
|
277 |
objects we're interested in, then use the ORM's ``__in`` |
|
|
278 |
lookup to return a ``QuerySet``. |
|
|
279 |
|
|
|
280 |
Now that the queryset-refactor branch is in the trunk, this can be |
|
|
281 |
tidied up significantly. |
|
|
282 |
""" |
|
|
283 |
def get_by_model(self, queryset_or_model, tags): |
|
|
284 |
""" |
|
|
285 |
Create a ``QuerySet`` containing instances of the specified |
|
|
286 |
model associated with a given tag or list of tags. |
|
|
287 |
""" |
|
|
288 |
tags = get_tag_list(tags) |
|
|
289 |
tag_count = len(tags) |
|
|
290 |
if tag_count == 0: |
|
|
291 |
# No existing tags were given |
|
|
292 |
queryset, model = get_queryset_and_model(queryset_or_model) |
|
|
293 |
return model._default_manager.none() |
|
|
294 |
elif tag_count == 1: |
|
|
295 |
# Optimisation for single tag - fall through to the simpler |
|
|
296 |
# query below. |
|
|
297 |
tag = tags[0] |
|
|
298 |
else: |
|
|
299 |
return self.get_intersection_by_model(queryset_or_model, tags) |
|
|
300 |
|
|
|
301 |
queryset, model = get_queryset_and_model(queryset_or_model) |
|
|
302 |
content_type = ContentType.objects.get_for_model(model) |
|
|
303 |
opts = self.model._meta |
|
|
304 |
tagged_item_table = qn(opts.db_table) |
|
|
305 |
return queryset.extra( |
|
|
306 |
tables=[opts.db_table], |
|
|
307 |
where=[ |
|
|
308 |
'%s.content_type_id = %%s' % tagged_item_table, |
|
|
309 |
'%s.tag_id = %%s' % tagged_item_table, |
|
|
310 |
'%s.%s = %s.object_id' % (qn(model._meta.db_table), |
|
|
311 |
qn(model._meta.pk.column), |
|
|
312 |
tagged_item_table) |
|
|
313 |
], |
|
|
314 |
params=[content_type.pk, tag.pk], |
|
|
315 |
) |
|
|
316 |
|
|
|
317 |
def get_intersection_by_model(self, queryset_or_model, tags): |
|
|
318 |
""" |
|
|
319 |
Create a ``QuerySet`` containing instances of the specified |
|
|
320 |
model associated with *all* of the given list of tags. |
|
|
321 |
""" |
|
|
322 |
tags = get_tag_list(tags) |
|
|
323 |
tag_count = len(tags) |
|
|
324 |
queryset, model = get_queryset_and_model(queryset_or_model) |
|
|
325 |
|
|
|
326 |
if not tag_count: |
|
|
327 |
return model._default_manager.none() |
|
|
328 |
|
|
|
329 |
model_table = qn(model._meta.db_table) |
|
|
330 |
# This query selects the ids of all objects which have all the |
|
|
331 |
# given tags. |
|
|
332 |
query = """ |
|
|
333 |
SELECT %(model_pk)s |
|
|
334 |
FROM %(model)s, %(tagged_item)s |
|
|
335 |
WHERE %(tagged_item)s.content_type_id = %(content_type_id)s |
|
|
336 |
AND %(tagged_item)s.tag_id IN (%(tag_id_placeholders)s) |
|
|
337 |
AND %(model_pk)s = %(tagged_item)s.object_id |
|
|
338 |
GROUP BY %(model_pk)s |
|
|
339 |
HAVING COUNT(%(model_pk)s) = %(tag_count)s""" % { |
|
|
340 |
'model_pk': '%s.%s' % (model_table, qn(model._meta.pk.column)), |
|
|
341 |
'model': model_table, |
|
|
342 |
'tagged_item': qn(self.model._meta.db_table), |
|
|
343 |
'content_type_id': ContentType.objects.get_for_model(model).pk, |
|
|
344 |
'tag_id_placeholders': ','.join(['%s'] * tag_count), |
|
|
345 |
'tag_count': tag_count, |
|
|
346 |
} |
|
|
347 |
|
|
|
348 |
cursor = connection.cursor() |
|
|
349 |
cursor.execute(query, [tag.pk for tag in tags]) |
|
|
350 |
object_ids = [row[0] for row in cursor.fetchall()] |
|
|
351 |
if len(object_ids) > 0: |
|
|
352 |
return queryset.filter(pk__in=object_ids) |
|
|
353 |
else: |
|
|
354 |
return model._default_manager.none() |
|
|
355 |
|
|
|
356 |
def get_union_by_model(self, queryset_or_model, tags): |
|
|
357 |
""" |
|
|
358 |
Create a ``QuerySet`` containing instances of the specified |
|
|
359 |
model associated with *any* of the given list of tags. |
|
|
360 |
""" |
|
|
361 |
tags = get_tag_list(tags) |
|
|
362 |
tag_count = len(tags) |
|
|
363 |
queryset, model = get_queryset_and_model(queryset_or_model) |
|
|
364 |
|
|
|
365 |
if not tag_count: |
|
|
366 |
return model._default_manager.none() |
|
|
367 |
|
|
|
368 |
model_table = qn(model._meta.db_table) |
|
|
369 |
# This query selects the ids of all objects which have any of |
|
|
370 |
# the given tags. |
|
|
371 |
query = """ |
|
|
372 |
SELECT %(model_pk)s |
|
|
373 |
FROM %(model)s, %(tagged_item)s |
|
|
374 |
WHERE %(tagged_item)s.content_type_id = %(content_type_id)s |
|
|
375 |
AND %(tagged_item)s.tag_id IN (%(tag_id_placeholders)s) |
|
|
376 |
AND %(model_pk)s = %(tagged_item)s.object_id |
|
|
377 |
GROUP BY %(model_pk)s""" % { |
|
|
378 |
'model_pk': '%s.%s' % (model_table, qn(model._meta.pk.column)), |
|
|
379 |
'model': model_table, |
|
|
380 |
'tagged_item': qn(self.model._meta.db_table), |
|
|
381 |
'content_type_id': ContentType.objects.get_for_model(model).pk, |
|
|
382 |
'tag_id_placeholders': ','.join(['%s'] * tag_count), |
|
|
383 |
} |
|
|
384 |
|
|
|
385 |
cursor = connection.cursor() |
|
|
386 |
cursor.execute(query, [tag.pk for tag in tags]) |
|
|
387 |
object_ids = [row[0] for row in cursor.fetchall()] |
|
|
388 |
if len(object_ids) > 0: |
|
|
389 |
return queryset.filter(pk__in=object_ids) |
|
|
390 |
else: |
|
|
391 |
return model._default_manager.none() |
|
|
392 |
|
|
|
393 |
def get_related(self, obj, queryset_or_model, num=None): |
|
|
394 |
""" |
|
|
395 |
Retrieve a list of instances of the specified model which share |
|
|
396 |
tags with the model instance ``obj``, ordered by the number of |
|
|
397 |
shared tags in descending order. |
|
|
398 |
|
|
|
399 |
If ``num`` is given, a maximum of ``num`` instances will be |
|
|
400 |
returned. |
|
|
401 |
""" |
|
|
402 |
queryset, model = get_queryset_and_model(queryset_or_model) |
|
|
403 |
model_table = qn(model._meta.db_table) |
|
|
404 |
content_type = ContentType.objects.get_for_model(obj) |
|
|
405 |
related_content_type = ContentType.objects.get_for_model(model) |
|
|
406 |
query = """ |
|
|
407 |
SELECT %(model_pk)s, COUNT(related_tagged_item.object_id) AS %(count)s |
|
|
408 |
FROM %(model)s, %(tagged_item)s, %(tag)s, %(tagged_item)s related_tagged_item |
|
|
409 |
WHERE %(tagged_item)s.object_id = %%s |
|
|
410 |
AND %(tagged_item)s.content_type_id = %(content_type_id)s |
|
|
411 |
AND %(tag)s.id = %(tagged_item)s.tag_id |
|
|
412 |
AND related_tagged_item.content_type_id = %(related_content_type_id)s |
|
|
413 |
AND related_tagged_item.tag_id = %(tagged_item)s.tag_id |
|
|
414 |
AND %(model_pk)s = related_tagged_item.object_id""" |
|
|
415 |
if content_type.pk == related_content_type.pk: |
|
|
416 |
# Exclude the given instance itself if determining related |
|
|
417 |
# instances for the same model. |
|
|
418 |
query += """ |
|
|
419 |
AND related_tagged_item.object_id != %(tagged_item)s.object_id""" |
|
|
420 |
query += """ |
|
|
421 |
GROUP BY %(model_pk)s |
|
|
422 |
ORDER BY %(count)s DESC |
|
|
423 |
%(limit_offset)s""" |
|
|
424 |
query = query % { |
|
|
425 |
'model_pk': '%s.%s' % (model_table, qn(model._meta.pk.column)), |
|
|
426 |
'count': qn('count'), |
|
|
427 |
'model': model_table, |
|
|
428 |
'tagged_item': qn(self.model._meta.db_table), |
|
|
429 |
'tag': qn(self.model._meta.get_field('tag').rel.to._meta.db_table), |
|
|
430 |
'content_type_id': content_type.pk, |
|
|
431 |
'related_content_type_id': related_content_type.pk, |
|
|
432 |
# Hardcoding this for now just to get tests working again - this |
|
|
433 |
# should now be handled by the query object. |
|
|
434 |
'limit_offset': num is not None and 'LIMIT %s' or '', |
|
|
435 |
} |
|
|
436 |
|
|
|
437 |
cursor = connection.cursor() |
|
|
438 |
params = [obj.pk] |
|
|
439 |
if num is not None: |
|
|
440 |
params.append(num) |
|
|
441 |
cursor.execute(query, params) |
|
|
442 |
object_ids = [row[0] for row in cursor.fetchall()] |
|
|
443 |
if len(object_ids) > 0: |
|
|
444 |
# Use in_bulk here instead of an id__in lookup, because id__in would |
|
|
445 |
# clobber the ordering. |
|
|
446 |
object_dict = queryset.in_bulk(object_ids) |
|
|
447 |
return [object_dict[object_id] for object_id in object_ids \ |
|
|
448 |
if object_id in object_dict] |
|
|
449 |
else: |
|
|
450 |
return [] |
|
|
451 |
|
|
|
452 |
########## |
|
|
453 |
# Models # |
|
|
454 |
########## |
|
|
455 |
|
|
|
456 |
class Tag(models.Model): |
|
|
457 |
""" |
|
|
458 |
A tag. |
|
|
459 |
""" |
|
|
460 |
name = models.CharField(_('name'), max_length=50, unique=True, db_index=True) |
|
|
461 |
|
|
|
462 |
objects = TagManager() |
|
|
463 |
|
|
|
464 |
class Meta: |
|
|
465 |
ordering = ('name',) |
|
|
466 |
verbose_name = _('tag') |
|
|
467 |
verbose_name_plural = _('tags') |
|
|
468 |
|
|
|
469 |
def __unicode__(self): |
|
|
470 |
return self.name |
|
|
471 |
|
|
|
472 |
class TaggedItem(models.Model): |
|
|
473 |
""" |
|
|
474 |
Holds the relationship between a tag and the item being tagged. |
|
|
475 |
""" |
|
|
476 |
tag = models.ForeignKey(Tag, verbose_name=_('tag'), related_name='items') |
|
|
477 |
content_type = models.ForeignKey(ContentType, verbose_name=_('content type')) |
|
|
478 |
object_id = models.PositiveIntegerField(_('object id'), db_index=True) |
|
|
479 |
object = generic.GenericForeignKey('content_type', 'object_id') |
|
|
480 |
|
|
|
481 |
objects = TaggedItemManager() |
|
|
482 |
|
|
|
483 |
class Meta: |
|
|
484 |
# Enforce unique tag association per object |
|
|
485 |
unique_together = (('tag', 'content_type', 'object_id'),) |
|
|
486 |
verbose_name = _('tagged item') |
|
|
487 |
verbose_name_plural = _('tagged items') |
|
|
488 |
|
|
|
489 |
def __unicode__(self): |
|
|
490 |
return u'%s [%s]' % (self.object, self.tag) |