Search code examples
pythongoogle-app-enginegoogle-cloud-datastorewebapp2blobstore

How to make a 'permalink' detail page displaying two images in Google App Engine webapp2 guestbook tutorial


I'm following the Google App Engine tutorial (here's their complete demo code on GitHub) and would like to:

  1. Allow a guestbook user to upload an extra image in addition to 'avatar' when posting a greeting. This could be called 'other'
  2. After posting a greeting, redirect the user to a greeting detail page with a URL like /greeting/numeric-id instead of the main page listing all greetings.
  3. Display the detail page with images using a Jinja2 template called detail.html

I'm having trouble understanding:

A) How to write the redirect code that is called once a greeting is published so it redirects to a URL like /greeting/numeric-id.

B) How to write the Detail view and template page that the user is redirected to so the greeting id and images are displayed.

Here's a diagram showing what I want to do:

enter image description here

Here's guestbook.py:

import os
import urllib
from google.appengine.api import images
from google.appengine.api import users
from google.appengine.ext import ndb
from google.appengine.ext.webapp import blobstore_handlers
from google.appengine.ext import blobstore
import jinja2
import webapp2

JINJA_ENVIRONMENT = jinja2.Environment(
    loader=jinja2.FileSystemLoader(os.path.dirname(__file__)),
    extensions=['jinja2.ext.autoescape'],
    autoescape=True)

DEFAULT_GUESTBOOK_NAME = 'default_guestbook'

def guestbook_key(guestbook_name=None):
    """Constructs a Datastore key for a Guestbook entity with name."""
    return ndb.Key('Guestbook', guestbook_name or 'default_guestbook')

class Author(ndb.Model):
    """Sub model for representing an author."""
    identity = ndb.StringProperty(indexed=False)
    email = ndb.StringProperty(indexed=False)

class Greeting(ndb.Model):
    """A model for representing an individual Greeting entry."""
    author = ndb.StructuredProperty(Author)
    date = ndb.DateTimeProperty(auto_now_add=True)
    avatar = ndb.BlobProperty(indexed=False, required=True)
    other = ndb.BlobProperty(indexed=False, required=True)

class MainPage(webapp2.RequestHandler):
    def get(self):
        self.response.out.write('<html><body>')
        guestbook_name = self.request.get('guestbook_name')

        greetings = Greeting.query(
            ancestor=guestbook_key(guestbook_name)) \
            .order(-Greeting.date) \
            .fetch(10)

        self.response.out.write("""
              <form action="/sign?%s"
                    enctype="multipart/form-data"
                    method="post">
                <label>Avatar:</label>
                <input type="file" name="avatar"/><br>
                <label>Other Image:</label>
                <input type="file" name="other"/><br>
                <input type="submit" value="Submit">
              </form>
            </body>
          </html>""" % (urllib.urlencode({'guestbook_name': guestbook_name})))

class Image(webapp2.RequestHandler):
    """ Handle image stored as blobs of bytes. 
        No idea how the template knows to select a particular one. """
    def get(self):
        avatar_greeting_key = ndb.Key(urlsafe=self.request.get('avatar_id'))
        other_greeting_key = ndb.Key(urlsafe=self.request.get('other_id'))
        avatar_greeting = avatar_greeting_key.get()
        other_greeting = other_greeting_key.get()
        if avatar_greeting.avatar:
            self.response.headers['Content-Type'] = 'image/png'
            self.response.out.write(avatar_greeting.avatar)
        elif other_greeting.other:
            self.response.headers['Content-Type'] = 'image/png'
            self.response.out.write(other_greeting.other)
        else:
            self.response.out.write('No image')

class Guestbook(webapp2.RequestHandler):
    def post(self):
        guestbook_name = self.request.get('guestbook_name',
                                          DEFAULT_GUESTBOOK_NAME)
        greeting = Greeting(parent=guestbook_key(guestbook_name))

        if users.get_current_user():
            greeting.author = Author(
                    identity=users.get_current_user().user_id(),
                    email=users.get_current_user().email())

        avatar = self.request.get('avatar')
        avatar = images.resize(avatar, 100, 100)  
        other = self.request.get('other')
        other = images.resize(other, 400, 300)  

        greeting.avatar = avatar
        greeting.other = other

        greeting.put()

        query_params = {'guestbook_name': guestbook_name}
        self.redirect('/greeting/%d' % greeting.key.id())

class Detail(webapp2.RequestHandler):
    """ Individual greeting. """
    def get(self, *args, **kwargs):
        guestbook_name = self.request.get('guestbook_name', DEFAULT_GUESTBOOK_NAME)

        greeting = Greeting.get_by_id(args[0],
                                          parent=guestbook_key(guestbook_name))

        template_values = {
            'greeting': greeting,
        }

        template = JINJA_ENVIRONMENT.get_template('detail.html')
        self.response.write(template.render(template_values))


app = webapp2.WSGIApplication([
    ('/', MainPage),
    ('/img', Image),
    ('/sign', Guestbook),
    ('/greeting/(\d+)', Detail),
    ], debug=True)

My detail.html template:

<!DOCTYPE html>
{% autoescape true %}
<html>
  <head>
    <title>Greeting {{ greeting.id }}</title>
  </head>
  <body>
    <h2>Greeting {{ greeting.id }}</h2>
        Avatar: <img src="/img?avatar_id={{ greeting.key.urlsafe() }}">
        <br>
        Other: <img src="/img?other_id={{ greeting.key.urlsafe() }}">
  </body>
</html>
{% endautoescape %}

My app.yaml in case it is useful:

runtime: python27
api_version: 1
threadsafe: true

# Handlers match in order, put above the default handler.
handlers:
- url: /stylesheets
  static_dir: stylesheets

- url: /.*
  script: guestbook.app

libraries:
- name: webapp2
  version: latest
- name: jinja2
  version: latest

The Error:

Traceback (most recent call last):
  File "/Users/simon/Projects/google-cloud-sdk/platform/google_appengine/lib/webapp2-2.5.2/webapp2.py", line 1535, in __call__
    rv = self.handle_exception(request, response, e)
  File "/Users/simon/Projects/google-cloud-sdk/platform/google_appengine/lib/webapp2-2.5.2/webapp2.py", line 1529, in __call__
    rv = self.router.dispatch(request, response)
  File "/Users/simon/Projects/google-cloud-sdk/platform/google_appengine/lib/webapp2-2.5.2/webapp2.py", line 1278, in default_dispatcher
    return route.handler_adapter(request, response)
  File "/Users/simon/Projects/google-cloud-sdk/platform/google_appengine/lib/webapp2-2.5.2/webapp2.py", line 1102, in __call__
    return handler.dispatch()
  File "/Users/simon/Projects/google-cloud-sdk/platform/google_appengine/lib/webapp2-2.5.2/webapp2.py", line 572, in dispatch
    return self.handle_exception(e, self.app.debug)
  File "/Users/simon/Projects/google-cloud-sdk/platform/google_appengine/lib/webapp2-2.5.2/webapp2.py", line 570, in dispatch
    return method(*args, **kwargs)
  File "/Users/simon/Projects/guestbook/guestbook.py", line 111, in get
    self.response.write(template.render(template_values))
  File "/Users/simon/Projects/google-cloud-sdk/platform/google_appengine/lib/jinja2-2.6/jinja2/environment.py", line 894, in render
    return self.environment.handle_exception(exc_info, True)
  File "/Users/simon/Projects/guestbook/detail.html", line 9, in top-level template code
    Avatar: <img src="/img?avatar_id={{ greeting.key.urlsafe() }}">
UndefinedError: 'None' has no attribute 'key'

Any help, or even better, example code, would be much appreciated. A GAE/webapp2 blog tutorial with example code of detail and list views and templates would be great, but perhaps the data structure of the GAE BlobStore is not ideal for a blog?

Update: If I add the python check code contributed in Dan's answer I get a 500 error instead of the stack trace, and if I try the template checks I get a blank greeting page. I've updated the question with my full code and a diagram explaining what I am trying to do.


Solution

  • I'll start with B:

    The error indicates that the greeting value is None, leading to an exception when jinja2 attempts to expand greeting.key in {{ greeting.key.urlsafe() }} during template rendering.

    One option is to re-arrange the handler code to prevent rendering that template if the necessary conditions are not met, maybe something along these lines:

        ...
        greeting = Greeting.get_by_id(args[0])
    
        if not greeting or not isinstance(greeting.key, ndb.Key):
            # can't render that template, invalid greeting.key.urlsafe()
            webapp2.abort(500)  
            return
    
        template_values = {
            'greeting': greeting,
        }
    
        template = JINJA_ENVIRONMENT.get_template('detail.html')
        self.response.write(template.render(template_values))
        ...
    

    Alternatively you can wrap the template areas referencing the variables with appropriate checks (IMHO uglier, harder and more fragile, tho - python is way better for this kind of logic than jinja2), something along these lines:

    {% if greeting and greeting.key %}<img src="/img?avatar_img_id={{ greeting.key.urlsafe() }}">{% endif %}
    

    Now to A.

    In short - not a great idea, primarily because the numeric ID you're trying to use in the URL is not unique except for greetings under the same parent entity!. Which in a way explains why greeting is invalid leading to the error from the B answer.

    The greeting = Greeting.get_by_id(args[0]) will return None unless you also created a greeting entity with the ID you're passing in args[0] and no parent!

    In order to obtain by ID the greeting you created with:

        greeting = Greeting(parent=guestbook_key(guestbook_name))
    

    you'd need to call:

        greeting = Greeting.get_by_id(args[0],
                                      parent=guestbook_key(guestbook_name))
    

    You could, if you want to continue in the same direction, also encode the guestbook_name in the greeting URL, which would allow you to also obtain the needed parent key, maybe something along these lines:

    /guestbook/<guestbook_name>/greeting/<numeric-id>.
    

    You also need to take a closer look at the image handling. You have architectural inconsistencies: in the diagram and the model you have both the avatar and the other image attached to a single greeting entity, but in the Image handler each of them is attached to a separate greeting. The handler also doesn't map at all to the images' URLs (which also needs additional encodings for the data you need to locate the appropriate image, depending on the architectural decision).

    I'm afraid you still have a lot of work to do until you get the entire thing to work, much more than properly fit for a single SO question. Step back, re-think your architecture, split it into smaller pieces, focus on one piece at a time and get it going. After you become familiar with the technology for the various pieces you'll feel better tackling the whole problem at once.