a google app engine experiment

20.11.2009

i've been coding a small website in gae which is about warcraft. i don't have any information about warcraft since i'm not a gamer but the site is about warcraft. hosting was a a big problem at first but i thought it would be useful to use gae after all when it comes to web i'm a python developer. gae is very suitable for those kind of projects besides it's free! free as in free beer.

gae ables you to use frameworks such as django, tornado, web.py but i've been using "pure gae". if you've used django, or any other similar web framework you won't have any problems with sql. so in that project i could able to create someting similar to this;
class Entry(db.Model):
  user = db.UserProperty()
  title = db.StringProperty(required=True)
  slug = db.StringProperty(required=True)
  content = db.TextProperty(required=True)
  content_html = db.TextProperty(required=True)
  image = db.BlobProperty()
  imgth = db.BlobProperty()
  created = db.DateTimeProperty(auto_now_add=True)
  active = db.BooleanProperty(default=True)


thankfully gae's support for wsgi takes no effort.
app = webapp.WSGIApplication(
  [
    (r'^/about', AboutPage),
    (r'^/img', ImageHandler),
    (r'^/search', SearchPage),
    (r'^/entry/([^/]+)', EntryPage),
    (r'^/', Index),
  ],
  debug=False
)
def main():
  run_wsgi_app(app)
  
if __name__ == '__main__':
  main()


to generate the pages i've created a BaseHandler class is similar to this.
PROJECTDIR = os.path.join(os.path.dirname(__file__), 'templates/')

class BaseHandler(webapp.RequestHandler):
  
  def render(self, template_name, template_vals=None):
    if not template_vals:
      template_values = {}
    template_name = os.path.join(PROJECTDIR, template_name)
    self.response.out.write(template.render(template_name, template_values))


after this, generating the index is easy.
class Index(BaseHandler):
  def get(self):
    offset = int(self.request.get('offset', 0))
    count = int(self.request.get('count', 25))
    
    entries = Entry.all().filter('active = ', True).order('-created').fetch(count, offset)
    self.response.headers['Cache-Control'] = 300
    user = users.get_current_user()
    template_vals = {
        'user' : user,
        'offset' : offset,
        'count' : count,
        'last_post' : offset+len(entries)-1,
        'prev_offset' : max(0, offset-count),
        'next_offset' : offset+count,
        'entries' : entries
      }
      self.render('index.html', template_vals)


in the project we need image library to resize the images, and to generate thumbnails. that's why i've coded the following ImageHandler class.

HTTP_DATE_FORMAT = "%a, %d %b %Y %H:%M:%S GMT"

class ImageHandler(webapp.RequestHandler):
  def get(self):
    if self.request.get('t'):
      entry = db.get(self.request.get('t'))
      if entry.imgth:
        last_modified = entry.created.strftime(HTTP_DATE_FORMAT)
        self.response.headers['Last-Modified'] = last_modified
        self.response.headers['Cache-Control'] = 86400
        self.response.headers['Content-Type'] = 'image/png'
        self.response.out.write(entry.imgth)
      else:
        self.response.headers['Content-Type'] = 'text/plain'
        self.error(404)
        self.response.out.write('404: Not Found')
    elif self.request.get('i'):
      entry = db.get(self.request.get('i'))
      if entry.image:
        last_modified = entry.created.strftime(HTTP_DATE_FORMAT)
        self.response.headers['Last-Modified'] = last_modified
        self.response.headers['Cache-Control'] = 86400
        self.response.headers['Content-Type'] = 'image/png'
        self.response.out.write(entry.image)
      else:
        self.response.headers['Content-Type'] = 'text/plain'
        self.error(404)
        self.response.out.write('404: Not Found')
    else:
      self.response.headers['Content-Type'] = 'text/plain'
      self.error(404)
      self.response.out.write('404: Not Found')


i must say that it looks pretty bad but it does the job for now. the thumbnails are created by looking at the key of an entry. i've also defined last-modified and cache-controls headers. the entry urls are slugified but they can't be unique so unique keys are also used in generated urls. here's a simple url: http://application_name.appspot.com/entry/ag53YXJjcmFmdG5/slugified-title. so the EntryPage class is defined as follows;
class EntryPage(BaseHandler):
  def get(self, key):
    if not key:
      self.redirect('/')
    entry = db.Query(Entry).filter('active = ', True).filter('key = ', key).get()
    if not entry:
      self.error(404)
      self.render('404.html')
    else:
      last_modified = entry.created.strftime(HTTP_DATE_FORMAT)
      self.response.headers['Last-Modified'] = last_modified
      self.response.headers['Cache-Control'] = 3600
      comments = db.Query(Comment).filter('ekey = ', entry.eid).fetch(50, 0)
      user = users.get_current_user()
    
      template_vals = {
        'user' : user,
        'entry' : entry,
        'comments' : comments,
      }
      self.render('entry.html', template_vals)


and of course there's an admin panel. it's pretty similar to IndexPage so i don't want to write it again but there's a slight difference which is admin_required decorator which is defined as
def admin_required(func):
  def wrapper(self, *args, **kwargs):
    user = users.get_current_user()
    if not user:
      if self.request.method == 'GET':
        self.redirect(users.create_login_url(self.request.uri))
      else:
        self.error(403)
        self.response.out.write('403: Forbidden')
    elif not users.is_current_user_admin():
      self.error(403)
      self.response.out.write('403: Forbidden')
    else:
      return func(self, *args, **kwargs)
  return wrapper
i didn't mention anything that's stick to the project but this is the basic setup of a gae application.

it's easy and quite amazing what you can with gae. there's no /etc/init.d/whatever restart or anything else. just click the deploy button and that's it. impressive and very useful for python web developers(oh and java developers as well). however, i'd really wish they'd use mako templates by default.
blog comments powered by Disqus