XP on Rails Extreme Programming Blog

14Dec/081

How to manage Tags and Tag clouds with AppEngine and Django

Hi.

Today I would like to show you how to model a tag cloud with AppEngine and Django.

Suppose that we have a relationship n-n between a Post model and a Tag model and we would like to add a list of tags when creating a Post and to be able to modify that list when updating the Post.

Finally, we would like to display a tag cloud in the homepage by highlighting the most used tags.

Let’s start from the Tag model:

# myapp/models.py

class Tag(db.Model):
    """
    A descriptive tag to be applied to a Post.
   
    label::
        the Tag label
    slug::
        the Tag slug to be used in urls
    post_count::
        the sum of all post that have been tagged with this tag
    """

    label = db.StringProperty(required=True)
    slug = db.StringProperty()
    post_count = db.IntegerProperty(default=1)

    def put(self):
        try:
            self.key()
        except NotSavedError:
            self.slug = slugify(self.label)
        super(Tag, self).put()

    @permalink
    def get_absolute_url(self):
        return ('myapp.views.posts.posts_by_tag', (), {'tag_label': self.slug})

    def __str__(self):
        return self.label

and then the Post model:

# myapp/models.py

class Post(db.Model):
    """
    A post of code in some Language.
   
    title::
        the title of the Post
    slug::
        the post slug to be used in urls
    body::
        the body of the Post
    tags::
        the list of tag labels associated to this Post. Useful to show post's tags without loading them from DataStore.
    """


    title = db.StringProperty(required=True)
    slug = db.StringProperty(required=False)
    body = db.TextProperty()
    tags = db.StringListProperty()

    def put(self):
        try:
            self.key()
        except NotSavedError:
            self.slug = slugify(self.title)
           
        super(Post, self).put()

    def __str__(self):
        return self.title

    @permalink
    def get_absolute_url(self):
        return ('myapp.views.posts.post_detail', (), { 'post_id': str(self.key().id()) + "-" + self.slug})

    def set_tags_from_list(self, new_tags):
        """        
        deletes the current tag list from the Post and creates a new one.
        creates a new Tag if not yet in DataStore or increase/decrease it's counter if necessary.
        """

        old_tags = self.tags[:] # copy the list of tag
        self.tags = [] # empty the list of tag associated to current post
        tags_to_set = [] # the list of tag to associate to current post

        same_tag_list = [item for item in old_tags if item in new_tags] # the list of tags that are the same after editing the post
        removed_tag_list = [item for item in old_tags if item not in new_tags] # the list of tags that have been removed after editing the post
        for new_tag in new_tags:
            new_tag = new_tag.strip()
            tag = Tag.get_by_key_name(new_tag)
            if tag is None:
                tags_to_set = Tag(key_name=new_tag, label=new_tag)
                tags_to_set.put()
                self.tags.append(new_tag)
            else:
                self.tags.append(new_tag)
                self.put()
                if new_tag not in same_tag_list:
                    tag.post_count += 1
                tag.put()

        for tag_to_remove in removed_tag_list:
            tag = Tag.get_by_key_name(tag_to_remove.strip())
            tag.post_count -= 1
            tag.put()


    def set_tags_from_string(self, tag_labels):
        """
        gets the list of tags (comma separated) to be associated to current post
        """

        new_tags = string.split(tag_labels, ',')
        self.set_tags_from_list(new_tags)

As you can see, each post is able to manage by itself its associated tags, by creating a new one if necessary or updating the counter of an existent one.

Let’s write views code for creating/editing a post.

# myapp/views/posts.py

@login_required
def add_post(request):
    """
    Allows a user to add a Post to the database.

    Context::
        form
            The form to add the Post.

    Template::
        myapp/posts/add_post_form.html

    """

    if request.method == 'POST':
        form = forms.AddPostForm(request.POST)
        if form.is_valid():

            new_post = Post(title=form.cleaned_data['title'],
                                  body=form.cleaned_data['body'])
            new_post.set_tags_from_string(form.cleaned_data['tags'])
            new_post.put()
            return HttpResponseRedirect(new_post.get_absolute_url())
    else:
        form = forms.AddPostForm()
    return render_to_response('myapp/posts/add_post_form.html',
                              { 'form': form },
                              context_instance=RequestContext(request))
                             
                             
@login_required
def edit_post(request, post_id):
    """
    Allows a user to edit an existing Post.

    Context::
        form
            The form to add the Post.

    Template::
        myapp/posts/edit_post_form.html

    """

    post = get_object_or_404(Post, id=int(post_id.split('-')[0]))
    if request.method == 'POST':
        form = forms.EditPostForm(data=request.POST)
        if form.is_valid():
            for field in ['body']:
                setattr(post, field, form.cleaned_data[field])
            post.set_tags_from_string(form.cleaned_data['tags'])
            post.put()
            return HttpResponseRedirect(post.get_absolute_url())
    else:
        form = forms.EditPostForm(instance=post)
    return render_to_response('myapp/posts/edit_post_form.html',
                              { 'form': form,
                                'original': post },
                              context_instance=RequestContext(request))

As you can see, both in add and edit post we call the

set_tags_from_string

method by passing the tag string that comes from the Form. Infact, in the

AddPostForm

or

EditPostForm

the tags field is defined as

tags = forms.CharField(max_length=255, widget=forms.TextInput())

.

Now we have all required code to handle taggings in our application.

We can easly show post’s tag list in our template by calling:

<p>
{{post.title}} is tagged with: {% for tag in post.tags %}{{ tag }} &nbsp;{% endfor %}
</p>

Notice that we don’t make any DataStore lookup because we have stored all post’s tag labels in the

tags = db.StringListProperty()

property.

Let’s create a tag cloud that takes as input the total number of tags that we want to display in the page:

# myapp/templatetags/tags.py

from django.template import Library, Node
from myapp.models import Tag
from google.appengine.ext import db

register = Library()

class TagsCloudNode(Node):

    def __init__(self, num, context_var):
        self.context_var = context_var
        self.num = int(num)
       
    def sort_by_attr(self,seq,attr):
        intermed = [ (getattr(seq[i],attr), i, seq[i]) for i in xrange(len(seq)) ]
        intermed.sort()
        return [ tup[-1] for tup in intermed ]

    def gen_cloud(self, num):
        query=Tag.all().filter("post_count >", 0).order('-post_count') # only retrieve tags that have at least one post associated
        p = query.fetch(num)
        if p:
            max1=max([int(p_item.post_count) for p_item in p])

        for i in range(len(p)):
            size =int(round(int(p[i].post_count)*maxsize/max1))
            if size<minsize:
                size=minsize
            cloudsize =str(size) +"%"
            p[i].cloudsize=cloudsize
        return self.sort_by_attr(p, "label")

    def render(self, context):
        context[self.context_var] = self.gen_cloud(self.num)
        return ''


def get_tags_cloud(parser, token):

    """
    Returns the tag cloud.

    Example::
        {% get_tags_cloud 5 as tag_cloud %}

    """

    bits = token.contents.split()
    if len(bits) != 4:
        raise template.TemplateSyntaxError("'%s' tag takes exactly three arguments" % bits[0])
    if bits[2] != 'as':
        raise template.TemplateSyntaxError("second argument to '%s' tag must be 'as'" % bits[0])

    return TagsCloudNode(bits[1], bits[3])
get_tags_cloud= register.tag(get_tags_cloud)

And in our template we can call this templatetag using this code:

<div>
<h2>
  Tag Cloud
</h2>
{% load tags %}
{% get_tags_cloud 20 as tag_cloud %}
{% for tag in tag_cloud %}
<a href="{{ tag.get_absolute_url }}" style="font-size:{{ tag.cloudsize }}; text-align:left;"> {{ tag.label }}</a>
{% endfor %}
</div>

I hope you will find this tutorial useful for managing Tags in your AppEngine application.

I don’t know if this is the best way to follow, but it works for me.

Please send me feedbacks about this tutorial.

About stefano

Independent Information Technology and Services Professional
Comments (1) Trackbacks (0)
  1. on your Models you should do batch saves / puts()

    ex for the Post class:

    tags_to_put = []
    for tag_to_remove in removed_tag_list:
    tag = Tag.get_by_key_name(tag_to_remove.strip())
    tag.post_count -= 1
    tag_to_put.append(tag)
    db.put(tags_to_put)


Leave a comment


No trackbacks yet.