Luke Plant committed 51d83b0

Add README file and various other small fixes.

Comments (0)

Files changed (5)


+Some documentation for the tagging app -- this is an overview.  At the
+bottom you will find a 'Getting started' section.
+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
+Externally, you just set/get 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
+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, 
+  [ 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 is, and you might want to customise how
+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
+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 
+The methods tend to return either simple values, or 'partially 
+aggregated' versions of the Tag object:
+Contains 'text' and 'count', where count is the number of Tag objects
+with that 'text' value.
+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
+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
+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 style form for editing tags for an 
+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
+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 %}
+This creates a context variable 'tags' containing the TagSummary
+I've included a simple template for create_update, that works as
+is. You can use another template, of course.
+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. 
+ is implemented using:
+        Tag.target_ct = models.ForeignKey(ContentType)
+so for any specific Tag I can get and I tend to use 
+that and and Tag.target_id to generate some URLs.
+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.
+                      ===================
+Getting started
+1. Put the code somewhere on your Python path
+2. Install the app - add 'lukeplant_me_uk.django.tagging' to your
+INSTALLED_APPS setting, and run the syncdb command.
+3. Optionally add the tagging template dir 
+(.../lukeplant_me_uk/django/tagging/templates) into your TEMPLATE_DIRS
+setting (needed for the create/update template)
+4. Use the 'register_mappers' function to tell the tagging app how to
+map your objects' primary keys into strings.  This is needed because
+the tagging app stores object ids as strings for maximum generality.
+    from lukeplant_me_uk.django.tagging.utils import register_mappers
+    # For an object with an integer primary key:
+    register_mappers(Post, str, int)
+    # For an object with a string primary key
+    register_mappers(Member, str, str)
+This code should go after your model definition somewhere (or in a
+module imported from a line after your model definition).
+5. Optionally add attributes to your models to enable access to their
+related tags.  See documentation in
+lukeplant_me_uk.django.tagging.fields.add_tagging_fields.  Again,
+these function calls need to go somewhere after your model definitions.
+6. Optionally add 'renderers', which are called by the 'Tag.render'
+method. See lukeplant_me_uk.django.tagging.register_renderer().
+The main use of this is in templates that display tags for multiple
+types of 'target' object -- so you can do {{ tag.render }} and get
+something sensible for different types of object.
+7. Create a wrapper for create_update that will pass in the right
+parameters -- usually the currently logged in member/user, and the
+target object which will probably be calculated from the URL.  How you
+do this is your own problem, though the ContentTypes model might help,
+and the utility functions in lukeplant_me_uk.django.tagging.utils
+might help.
+8. Start adding tagging to your templates, views, urls etc.  You are
+on your own here -- the 'recent_popular' and 'targets_for_text' views
+and the TagManager methods should be a help.  Most of them have
+reasonably good documentation in the code, but not separate yet.


     to the 'target_model' class.  If None, the attribute won't be
     added to the class.
+    See examples below.
     if creator_model is Any and target_model is Any:
         raise Exception("At least one of creator_model and target_model must be set")


 class Tag(models.Model):
     text = models.CharField("Text", maxlength=32)
-    target_id = models.CharField("'Target' ID", maxlength=64)
+    target_id = models.CharField("'Target' ID", maxlength=64) # 64 should be enough for anyone...
     target_ct = models.ForeignKey(ContentType, verbose_name="'Target' content type")
     creator_id = models.CharField("'creator' ID", maxlength=64)
     creator_ct = models.ForeignKey(ContentType, verbose_name="'creator' content type")


         _pk_from_str_mappers[content_type_id] = pk_from_str
 def register_renderer(model, renderer):
-    """Registers a function to be used to implement the __str__ method
+    """Registers a function to be used to implement the render method
     of Tag when it's target is the supplied model type.
     The function is passed the tag object, and must return
     the html to be displayed on a page."""
     or None if none can be found."""
     return _renderers.get(model_ct, None)
-def tagging_normaliser(tag_sum, collection=None):
+def tagging_normaliser(tag_sum, collection):
     """Example 'normaliser' for calculating the 'weight'
     of a given TagSummary object.
     collection is the complete TagSummaryCollection.
-    # Rather simplistic normalising :-)
-    return min(tag_sum.count, 5)
+    # Fit to scale 0 to 5
+    return int(5 * tag_sum.count/(collection.count_max - collection.count_min))


         for tagname in newtagset - currenttagset:
             t = Tag(**base_attrs)
-            t.text = tagname # TODO - strip < > & " 
+            t.text = tagname.strip("<>&\"'?") # remove troublesome chars
         if redirect_url is None:
             redirect_url = request.POST.get('redirect_url', None)