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.

Tagged as: , , 1 Comment