I've been writing some tagging functionality for my site, and I've
developed it in a way that is reusable and generic, similar in some
ways to the 'comments' app in contrib. Would anyone be interested in
me tidying it up for release as a Django app? It would require a
little bit of tidying up (mainly fixing Python 2.3 incompatibilities),
and if popular, it could do with a bit of community input. I'd be
happy to release it under a BSD license.
Below is an overview of what is included.
Tag model
=========
- The central model. A *simplified* version is below:
class Tag(models.Model):
text = models.CharField()
target = GenericForeignKey()
creator = GenericForeignKey()
added = models.DateTimeField()
GenericForeignKey works like a foreign key field except that it is
identified by two pieces of information - an 'id' (stored as a string
for maximum generality), and a ContentType id. (The model actually
requires other parameters and other fields to be present, not shown).
For the most part, however, you can ignore this implementation detail.
Externally, you just set/get mytag.target or mytag.creator to any
Django ORM object and it will work.
Tag objects also have other methods to do efficient SQL queries for
summarising Tag information. e.g. .count_tagged_by_others() returns
the number of other 'creator' objects that have tagged the same object,
and there are other methods on the TagManager for doing this kind of
thing (see below).
If you want, you can have different types of object that are taggable,
so:
[t.target for t in Tag.objects.all()]
could be a heterogeneous list. In templates that show lists of Tag
objects in detail, this could be a problem, since you don't know what
type of object mytag.target is, and you might want to customise how you
display a tagged target object. So I've added a 'render_target' method
which is pluggable i.e. you can provide different ways of rendering
different types of target objects, and then just use tag.render_target
in the template.
(You could also have different types of 'creator' object, but in my case
I haven't used this, so haven't tested it much, but I'm not aware of
any problems).
Tag Manager
===========
The Manager class for Tag has various methods to return database
information about tagged objects. It uses efficient SQL to do so,
which means that most of them can't build up queries the way you
normally do in Django, but instead the methods provide optional
parameters that should cover most of the kind of queries you want to
do, including searching for objects that are tagged with multiple text
values.
The methods tend to return either simple values, or 'partially
aggregated' versions of the Tag object:
TagSummary
----------
Contains 'text' and 'count', where count is the number of Tag objects
with that 'text' value.
TagTarget
---------
The 'text' + 'target' half of a 'Tag' object, used for answering the
question: "What objects are the target of a certain 'text' value (or
values) and how many 'creator' objects have tagged it like that?"
The tag manager methods also work correctly in a 'related' context (see
below).
Tag relationships
=================
A Tag is essentially two foreign key fields (plus metadata), but since
it isn't actually a ForeignKey object, and can point to multiple
models, it doesn't appear on the 'pointed to' models automatically (e.g.
as in mypost.tags.all() or myuser.created_tags.all()). However, you can
set this up with the add_tagging_fields() utility method, which allows
you add attributes to models with complete flexibility. You don't
define the tags as part of your model, but use this utility method
after creating the model to add the attributes.
This has been done like this mainly for ease of implementation, but it
also keeps your model decoupled from 'tagging' -- after you've defined
your model, or imported someone else's, you can add tagging fields very
easily.
In this related context, you also get methods that parallel normal
foreign keys i.e. mypost.tags.create() and mypost.tags.add(), which
work as expected.
Finally, the Tag Manager methods for advanced queries also work
correctly in the 'related' context e.g.
mypost.tags.get_distinct_text() limits itself to the relevant Tag
objects.
Views
=====
A simple tagging facility will allow users to tag objects, and then
display those tags inline on the object that is tagged (e.g. a post or
a topic etc). To enable this, a 'create_update' view is provide, with
a sample template -- a del.icio.us style form for editing tags for an
item.
A more complete solution will involve the ability to browse/search tags,
view recent tags etc. For this, I've written a 'recent_popular' view,
that shows recent tags, optionally limiting to a specific target or
'text' value etc, with paging, and shows a list of popular tags
(limited by the same query).
Finally, there is 'targets_for_text' which searches for targets with a
specific 'text' value, or several text values, and displays them in a
paged list by decreasing popularity.
Most of my tagging related views are simple wrappers around these three.
Template tags
=============
I've included a template tag for getting a list of TagSummary
objects for a target object e.g.:
{% get_tag_summaries for post as tags %}
Templates
=========
I've included a simple template for create_update, that works as is. You
can use another template, of course.
URLS
====
I've found that sorting out my urls for tagging has been one of the
hardest things, especially as I'm going for a fairly complete solution
(e.g. you can drill down to the level of a particular Member, see all
their tags, see who tagged a particular target with a given text value
and when etc.). I haven't been able to abstract this into a standard
system, so I haven't included any implementation of get_absolute_url or
anything else to do with URLs. I have my own custom template_tags for
generating absolute urls for Tags in different contexts, and I also
sometimes use the ContentType object.
Tag.target is implemented using:
Tag.target_ct = models.ForeignKey(ContentType)
so for any specific Tag I can get tag.target_ct.name and I tend to use
that and and Tag.target_id to generate some URLs.
Feeds
=====
I normally do feeds using the same view functions as HTML pages, but
with ?format=atom. I've added a hook to the relevant views that allows
this.
Resusable bits
==============
Some of the stuff I implemented along the way is even more generic,
including some 'generic foreign key' descriptors and 'generic m2m
object' descriptors, which manage to avoid using any SQL directly, and
so should be very robust. They depends on the ContentType model, and
some of the utility functions.
Let me know if you are interested, I'll tidy up the code and post it
somewhere.
Luke
--
Sometimes I wonder if men and women really suit each other. Perhaps
they should live next door and just visit now and then. (Katherine
Hepburn)
Luke Plant || L.Plant.98 (at) cantab.net || http://lukeplant.me.uk/
personally I'd love a 'standard' tagging app in contrib which
combines the best of both worlds, as it seems like this functionality
is what most users:
a) want
b) struggle with implementing, as the stuff to implement it isn't the
most obvious.
the main differences as I see them between the 2 different
implementations is:
- the concept of 'creator' ( I use 'User', while Luke isn't limited
by this)
- object-id .. ( I use int's, Luke uses strings)
(my code is here: http://svn.zyons.python-hosting.com/trunk/zilbo/
common/tag/ for reference.. and is BSD/ASL licensed)
I'm definitely interested, since I've been thinking about doing this
very thing.
Regards,
-scott
Please do so.
Tagging is, buzzword or not, pretty useful and many people have / will
implement it on their own (I was mid way through just now).
It would be great to leverage on your code, and I think it would
benefit django users in general, as it could make more applications
pluggable / easily distributable with tagging working.
thanks,
arthur
ps: how is the GenericForeignKey going to work? will it be imported
from the tagging app's package, or will it go into trunk?
>I've been writing some tagging functionality for my site, and I've
>developed it in a way that is reusable and generic, similar in some
>ways to the 'comments' app in contrib.
>
Seems that tags are hot in Django these days :-). Couple of days ago
I've done my TagsField as a Django app
(http://softwaremaniacs.org/soft/tags/). It's a bit different than yours
and Ian's. Now I only need the time to translate the page in English :-)
>Tag model
>=========
>- The central model. A *simplified* version is below:
>
>class Tag(models.Model):
> text = models.CharField()
> target = GenericForeignKey()
>
>
The single target means that one tag can't be used for different models?
I have chosen a different approach in my app: tags are basically just
text labels that are linked with models with ManyToMany. This allows to
do some interesting things. For example in my music exchange service I
have users with tags meaning their likings like "blues", "rock",
"hard-rock" so one can easily search both albums and artists with these
same tags.
Regards,
Jason
> The single target means that one tag can't be used for different models?
Yep, a single 'Tag' object is associated with just one target and one
creator. However, you can easily tag different models and objects with
the same bit of text.
> I have chosen a different approach in my app: tags are basically just
> text labels that are linked with models with ManyToMany. This allows to
> do some interesting things. For example in my music exchange service I
> have users with tags meaning their likings like "blues", "rock",
> "hard-rock" so one can easily search both albums and artists with these
> same tags.
You can do the same thing with my code - a search for a specific text
value will return Tags for different types of objects - the 'TagTarget'
object and related methods address that specific need.
One of the things that motivated my design was the ability to store
creation dates - i.e. you can see exactly when you (or anyone else)
tagged a specific item with a specific bit of text. Ian's solution
goes further, and has various models, including a 'Bookmark' that
allows more information to be stored, and other models that store
summary information.
I think the problem here is analagous to the multiplicity of Python web
frameworks - Python/Django makes it so easy! I've chatted with Ian a
bit, and while our different approaches cover the same ground, there
are reasons I couldn't use his without modification and vice-versa.
Perhaps we should set up some pages doing comparisons of the different
approaches, including code that can be downloaded. If a clear 'winner'
drops out i.e. a solution that will fit a lot of people, it could be
suggested that it is put into contrib in Django. But there's no
guarantee that will happen in any case, so it would be good to have
somewhere central where people can compare and make use of this code.
I'll try to tidy something up and release it tonight. It's got a
reasonably amount of code doc, and I'll try to add some examples.
Luke
> ps: how is the GenericForeignKey going to work? will it be imported
> from the tagging app's package, or will it go into trunk?
There is no guarantee that any of this stuff will go into trunk at all
- the core django devs would obviously have to be for it first.
But the GenericForeignKey class could be usefully part of the
ContentTypes contrib app. However, one constraint of my current
project is that I have models with string primary keys. I've got a
simple 'mapper' system for dealing with this that GenericForeignKey
uses, but I'm not sure if it is such a common case that it is worth the
extra hassle - for example, the 'comments' contrib app does not handle
this case AFAICS.
Perhaps GenericForeignKeyInteger and GenericForeignKeyString could both
be created - looking at the 'comments' contrib model,
GenericForeignKeyInteger could be used to improve it - it would replace
(or augment) the get_content_object() getter method with an attribute
that handles both setting and getting.
Luke
Looks great; I've personally been working on something similar but it
looks like you'll beat me to it :) I've also been slowing adding
generic relationship support, and I'd really like to get a look at your
GenericForeignKey class; chances are it would simplify a good deal of
code I've written.
I am, by the way, +1 on including a standard tagging app
("django.contrib.tags"?) in Django. I think it would be super-user.
Looking forward to seeing your code,
Jacob
> Looking forward to seeing your code,
OK, you can now get it here:
http://files.lukeplant.fastmail.fm/public/python/lp_tagging_app_0.1.zip
or here, using bzr:
bzr branch
http://files.lukeplant.fastmail.fm/public/python/lukeplant_me_uk/
Let me know if there are still any Python 2.4-isms, or any other
problems obviously. A README is included.
Luke
--
"The first ten million years were the worst. And the second ten
million, they were the worst too. The third ten million, I didn't enjoy
at all. After that I went into a bit of a decline." (Marvin the
paranoid android)
Just to throw another wrench in the gears, I think a concept of
relevance, that is, association weighting, could be useful (if not at
all common).
I'd like to associate a piece of text to multiple tags, each with a
different weight. It could be a simple, three-level selection: the text
could be "mostly related", "partially related", or "tangentially
related" to a topic. (The "not related" case is covered by the text not
being associated at all to all the other tags. :-) )
I realize this complicates both usage and implementation. Nonetheless
I'd find it useful, when selecting articles by topic, to be able to
only see the most relevant ones, or even just the least relevant ones.
--
Nicola Larosa - http://www.tekNico.net/
The more my parents worried that I wouldn't manage, the harder I
played. And the more record companies said I was too old, the harder
I played. The more people said I was another whimsical singer-
songwriter, the harder I played. -- KT Tunstall, February 2006
Oops, forgot to add a LICENSE file. I was going to put it under BSD,
same as Django, unless anyone has a compelling reason for anything
else,
Luke
--
"The number you have dialed is imaginary. Please rotate your telephone
by 90 degrees and try again."