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 }} {% 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.

