Blogging on App Engine, part 2: Basic blogging

This is the second in a series of articles on writing a blogging system on App Engine. An overview of what we're building is here.

In this post, we'll be handling the most basic part of blogging: Submitting new posts. For this, we're going to have to create our admin interface - which will involve a fair bit of boilerplate - as well as templates for both the admin interface and the blog posts themselves. But first, we need to make a slight change to the static serving code.

In order to publish new blog posts, we need to make sure we can generate a unique URL for the post, and for that we need a new method in the static serving interface. We'll call it 'add', and define it in static.py like so:

def add(path, body, content_type, **kwargs):
  def _tx():
    if StaticContent.get_by_key_name(path):
      return None
    return set(path, body, content_type, **kwargs)
  return db.run_in_transaction(_tx)

add() is a fairly straightforward transactional wrapper for set(), which first checks if a resource with the provided URL path already exists, and only creates it if it doesn't, returning None otherwise.

Next, we need to add routing for our new handlers to app.yaml. Add the following above the handler for static.py:

- url: /admin/.*
  script: admin.py
  login: admin

- url: /static/([^/]+)/(.*)
  static_files: themes/\1/static/\2
  upload: themes/[^/]+/static/.*

The first handler routes all admin functionality to our admin interface. The second handler is for static content, such as CSS and images, and deserves a little explanation.

We'll be organizing our templates and static content into 'themes', to allow for easy re-skinning of the blog. In order to allow us to locate all theme-related content - both static content and templates - together, we'll make use of app.yaml's static file pattern handlers, which allow us to specify arbitrary regular expressions for static serving. Here, we specify that any URL starting with /static/x will be served from theme x's 'static' directory.

We'll also need a place to define some basic configuration options that will vary from blog to blog. We'll use a Python module for this, because it's easy to read and write, and even easier to parse. Create 'config.py' in the app's root directory, and add the following:

blog_name = 'My Blog'
theme = 'default'
post_path_format = '/%(year)d/%(month)02d/%(slug)s'

Now we need to write our admin handler itself. Create a new file, admin.py, in the root of your app. We'll start by defining what a BlogPost looks like:

class BlogPost(db.Model):
  # The URL path to the blog post. Posts have a path iff they are published.
  path = db.StringProperty()
  title = db.StringProperty(required=True, indexed=False)
  body = db.TextProperty(required=True)
  published = db.DateTimeProperty(auto_now_add=True)
  updated = db.DateTimeProperty(auto_now=True)

Obviously, a post needs to be able to render itself. For this, we need to write some helper functions. Add the following function to the top level:

def render_template(template_name, template_vals=None, theme=None):
  template_path = os.path.join("themes", theme or config.theme, template_name)
  return template.render(template_path, template_vals or {})

render_template is a straightforward wrapper for App Engine's template.render method, which handles finding our template based on the current theme selected. Note our use of 'or': In Python, "a or b" returns the first of the two that does not evaluate to False. This is a nice shortcut for using an argument if it's set, or a default if it's not, as in "theme or config.theme".

Next, add a method called render() to our BlogPost class that calls it:

  def render(self):
    template_vals = {
        'config': config,
        'post': self,
    }
    return render_template("post.html", template_vals)

This method simply wraps render_template, calling it with the values our template (which we haven't written yet) will require to render the user's view of the blog post.

We'll also need a form class for our blog post. We'll use model forms for convenience:

class PostForm(djangoforms.ModelForm):
  class Meta:
    model = BlogPost
    exclude = [ 'path', 'published', 'updated' ]

Note that we're excluding 'path', 'published', and 'updated' from the form; these will be generated by our code.

Now that we have our model and form sorted, we can write the handler for new post submissions. Add the following class to admin.py:

class PostHandler(webapp.RequestHandler):
  def render_to_response(self, template_name, template_vals=None, theme=None):
    template_name = os.path.join("admin", template_name)
    self.response.out.write(render_template(template_name, template_vals,
                                            theme))

  def render_form(self, form):
    self.render_to_response("edit.html", {'form': form})

  def get(self):
    self.render_form(PostForm())

  def post(self):
    form = PostForm(data=self.request.POST)
    if form.is_valid():
      post = form.save(commit=False)
      post.publish()
      self.render_to_response("published.html", {'post': post})
    else:
      self.render_form(form)

There's quite a bit going on here, so we'll take the methods in order:

  • render_to_response() is a convenience method; it takes a template name and values, then calls render_template and writes the result to the response. When we have more than one admin handler, we'll want to refactor this into a RequestHandler base class.
  • render_form() is another convenience method; it accepts a form, and uses render_to_response to render a page containing the form.
  • get() generates the page with a blank form when it's requested by the user's browser with a GET request.
  • post() accepts form submissions and checks them for validity. If the form isn't valid, it shows the user the submission form again; Django takes care of including error messages and filling out values that the user already entered. If the form is valid, it saves the form, creating a new entity. It then calls .publish() on the new BlogPost entity - more about that below.

As you can see, when a valid submission is received, we're calling a mysterious 'publish' method on the new BlogPost entity. This method will handle generating a unique URL for the post, and saving it for the first time. If the post already exists, publish() will update the static content with any changes. Before we can define publish(), we need a couple of helper methods:

def slugify(s):
  return re.sub('[^a-zA-Z0-9-]+', '-', s).strip('-')

def format_post_path(post, num):
  slug = slugify(post.title)
  if num > 0:
    slug += "-" + str(num)
  return config.post_path_format % {
      'slug': slug,
      'year': post.published.year,
      'month': post.published.month,
      'day': post.published.day,
  }

Here, slugify() takes care of converting the post title into something suitable for a URL. It replaces non alphanumeric characters with hyphens, then strips out any leading or trailing hyphens. format_post_path() generates the path component of a URL for the post: It slugifies the post's title, optionally appends a unique number, and then formats the URL using the format string we defined in the config file earlier in the article.

Now we can finally define the publish() method on BlogPost:

  def publish(self):
    rendered = self.render()
    if not self.path:
      num = 0
      content = None
      while not content:
        path = format_post_path(self, num)
        content = static.add(path, rendered, "text/html")
        num += 1
      self.path = path
      self.put()
    else:
      static.set(self.path, rendered, "text/html")

This is where the real magic of publishing our new (or updated) blog post happens, by making use of the static serving interface we defined in the first post. Most of this method is concerned with finding a valid unique URL for the post if it hasn't already got one. The while loop calls format_post_path repeatedly, with a different unique number each time, then calls static.add, which we defined at the top of this article, to attempt to save the post with that URL path. Sooner or later, a unique URL is found, in which case it stores the new path to the post and saves it to the datastore. If the post already had a URL, none of that is necessary, and we simply call static.set() with the contents of the rendered page.

Obviously, this method of manually generating and setting individual pages isn't going to scale too well once we start dealing with many interconnected and dependent pages - not just the posts themselves, but also the post listing pages, the Atom feeds, and eventually, pages for individual tags. In the next post, we'll work out a system for facilitating this problem of regenerating pages when needed, which will lay the foundation for all the other features we have planned.

With that, we're more or less done with the actual code for our fledgling admin interface. As per usual, we've left out the boilerplate of imports, and the definition of the app; for all that, check out the new code for this stage in the repository. We've also left out the contents of the templates - I'm going to assume you're fairly familiar with Django templates already. If you're not, they're extremely straightforward to understand - you can view them here.

Once you've written the module, and written or copied the templates, you can test out submitting a new blog post by going to http://localhost:8080/admin/newpost . Try omitting one of the fields - you should be shown the form again, with an error message. If you fill everything out, you'll be directed to a confirmation page, with a link to your new blog post - which will be served from the static serving system we defined in the first post.

You can see the app so far (such as it is) at http://bloggart-demo.appspot.com/, and you can view the source on github. Both will be kept up to date as the series progresses.

In the next post, we'll develop a dependency system for regenerating changed pages, and demonstrate it by building an index page for our blog.

Comments

blog comments powered by Disqus