App Engine‎ > ‎

Python App Engine Codelab

Maintained by Google Developer Relations
Aug 2013

Objective/Description:
This codelab will demonstrate how to build applications using the Python version of Google App Engine. It is broken down into three sections. After completing this codelab, you should be able to create (and deploy) applications on Google App Engine using Python.

Part 1 Focuses on the basics which is to build a simple guestbook application. Incidentally, this application is based on the existing "Getting Started" online tutorial in the official docs, but it also extends upon it a bit as well as offers more detailed explanations. 

Part 2 Focuses on providing a set of examples of how to use the various App Engine services such as email, XMPP.

Part 3 Shows how to use URLFetch, Cron, Task Queues and the Channel API to push browser updates from the server. This is a standalone lab and can be run independently from the first two labs.

Requirements:
You have (at least) two options to do this codelab:

Option 1 Download SDK & Develop Locally (preferred; deploy option)
  • A laptop/notebook computer... you can share with others if you wish
  • Python 2.7.x (also see the separate mostly-equivalent Java codelab)
    • Python should already be available on Mac & Linux (or other POSIX) systems... 
    • only PC users need to download & install Python: http://python.org/download
  • App Engine SDK: http://developers.google.com/appengine/downloads
  • Development environment: an IDE or a text editor and command-shell pair
  • Valid Google Account (deploy only)
  • Working mobile phone with SMS/text messaging enabled (deploy only)
  • Valid email address (optional)
  • Jabber/XMPP instant messaging client (optional)
Option 2 Web Browser & Internet Access (no deploy option)
    Introduction

    This codelab is based on the online tutorial found at http://developers.google.com/appengine/docs/python/gettingstarted. That "Hello World" tutorial for Google App Engine involves the creation and deployment of a web-based "guest book" application. If you follow the tutorial, you'll see "Run/Modify" buttons on the code samples -- clicking on those buttons will drop you into the Cloud Playground with that snippet of code for you to experiment with. If desired, you can do that tutorial first before coming here. This codelab takes that existing tutorial and expands on it by offering more of an in-depth explanation and step-by-step process.

    Alternatives

    If jumping directly into code like we do here is too overwhelming, and you wish to get more background and move more slowly, go to the Google Developers Academy where you can take the intro to cloud computing & App Engine class first then do that Python 101 class afterwards. Both the tutorial and Academy classes will get you about halfway through part 1 of this codelab.

    Notes
    • The web examples use the Jinja2 Django-flavored templating system.
    • There are older examples found at:
    •     For data storage, the Python example uses the NoSQL Datastore API
    •     For Win32 & MacOS platforms, a Google App Engine Launcher UI is available.
      • It comes with the .msi and .dmg downloads, respectively.

    Deploying (optional but STRONGLY ENCOURAGED; option 1 only)

    Part I: Getting Started


    Option 1: Setup & the Launchers

    This “preliminary” lab is all about getting things setup and working. We will be using the development server that comes with the SDK so Internet connectivity is not necessary (unless you need to download the SDKs and related software such as Eclipse). By the end of this short “lab,” you should be able to create a new project, run the development server on the auto-generated application “stub” and be able to visit the app at http://localhost:8080 (or whatever port you started up your app on).

    The specific tasks to accomplish here:

    • Get/install Python if necessary
    • Get/install the App Engine SDK
    • Decide whether to use the Launcher UI (or not)
    • Decide whether you will be on the cmd-line or use an IDE
    • Create the application handler file (main.py)
    • Create the application configuration file (app.yaml)
    • Learn how to start the SDK development server

    If you bring up the launcher and create a new application, it will create your files automatically and give you 1-click ability to start the server. They will look like the following on Win32 and Mac OS X systems:


    Figure 1a. Google App Engine Launcher on PCs



    Figure 1b. Google App Engine Launcher on Mac OS X

    Regarding application names, you can choose whatever you want for development, but be aware that you will need to rename it to deploy live. Pick something appropriate and unique. We suggest call it “helloworld” – keep it all lowercase; users have had problems when using CamelCasing their app names.


    Option 2: Cloning the Hello World Project

    If you're using Cloud Playground, go to the site then copy the Hello World project (circled in the image [Figure 2a] below).


    Once you've cloned it, it shows up in your set of projects. Select that you wish to work on it by clicking on the blue arrow button to the left (Figure 2b).


    You'll then bring up the project and the app.yaml config file, as you can see here in Figure 2c:


    You can also select the main application file main.py too (FIgure 2d):


    You'll be learning about these 2 files in the next section.

    By the way, we mentioned there are more than 2 options to proceeding with the codelab. If you're not on a PC or Mac and doing Option 1 and not using Option 2's Cloud Playground, you may be using a Linux or some other system purely from the command-line (no UIs at all). In this case, you'll need to enter the app code by hand. It’s not that bad, as you'll see in the upcoming section.


    Exploring the Code

    Let's explore the main.py code first. It should look something like the below -- BTW, code generated by the Launcher will also feature a bunch of comments at the top with Google copyright notice -- they don't play a part in the codelab so we'll just leave them out. The code you get from the Launcher or the Cloud Playground should look something like:

    #!/usr/bin/env python
    
    import webapp2
    
    class MainHandler(webapp2.RequestHandler):
        def get(self):
            self.response.write('Hello world!')
    
    app = webapp2.WSGIApplication([
        ('/', MainHandler)
    ], debug=True)

    Here is what the relevant pieces of code do:

    • Requests to / are handled by the MainHandler class, specifically its get() method
    • Configuration is done via the app.yaml file
    • main() creates a web application object and run_wsgi_app() executes the server loop

    Now that you have an application handler file, you’re ready to go... that is, if using the Launcher or Cloud Playground, you'll get this in the body of the app.yaml configuration file (or enter by hand if using neither):

    1
    2
    3
    4
    5
    6
    7
    8
    application: helloworld
    version: 1
    runtime: python27
    api_version: 1
        
    handlers:
    - url: .*
      script: main.app
    

    Option 1: Once you have both of these files, then you’re ready to run the server. It’s as easy as one click in the launcher. If you are on the command-line, you need to run the server manually:

    $ dev_appserver.py helloworld

    Now you should be able to hit http://localhost:8080 (the port may be diffierent if using the Launcher, in which case it's easier to just click the Run button in the UI) and see the expected output in your browser:

    Figure 3a. “Hello World!” in plain text

    Option 2: Those using Cloud Playground do less work as there's already a server running. Just click the run button in the web UI to see the exact same output (in the output window at the bottom), as shown here in Figure 3b:


    Figure 3b. “Hello World!” in plain text from the Cloud Playground UI

    Please get this working as you need to complete each section of the lab in order to move to the next.

    Before moving onto the next part of the lab, make a few final tweaks: the first is to add a comma after the "('/', MainHandler)" 2-tuple to avoid any issues when adding new handlers.

    Also, you want to test out the dynamic nature of the development server: enclose "Hello World!" in <h1> tags so as to output HTML instead of plain text. Once you save this change, you shouldn't have to restart the server before hitting your browser and seeing the change immediately:

    Figure 4a. "Hello World!" in HTML (H1 header tag)

    Optionally, you can also provide the proper MIME header as part of your response... in our case, we could add: self.response.headers['Content-Type'] = 'text/html' as the first line of the handler’s get() method. Those of you doing Option 2 have probably figured out that you can change the code directly in the Cloud Playground, hit the run button and see the changes reflected immediately, like what you see below in Figure 4b.


    Figure 4b. "Hello World!" with added H1 header tag in the Cloud Playground

    For the remainder of the codelab, we'll just show the necessary bits for Option 1. Option 2 users can mimic the changes in the code for the same effect as you can pretty much complete all of Part 1 using the Cloud Playground.


    Adding web forms
    • Use triple quotes to add input HTML form with text field
    • Form action is /sign which does not exist yet... 404 error

    In this part of the codelab, we’re going to give your users the ability to input data. Rather than simply displaying “Hello World,” let’s create a simple form with one text field and allow the user to submit it to your app. Part of the form is defining the “action” for it, which is the URL that the browser will POST the form variables to. In this case, it’s/sign. However, since we haven’t defined a handler for it, this request – when a user clicks on the “Sign Guestbook” button – will generate a 404 error, which you will either see in your browser, or it will be blank... it depends complete on your browser.

    Leave the “Hello World” line as-is and add the form via another call to self.response.out.write(). Python’s triple quotes (3 single or 3 double quotes) allows users to embed special characters like NEWLINEs directly into a string verbatim. What does this mean for users? No more long string wrapping, awkward backslashes, or ugly string concatentation. You can cut-n-paste blocks of HTML directly into the source. The indentation of the string data does not matter, however, we indent it in a manner consistent with the Python code around it if nothing but for readbility.

    To make this happen, add another call right below the "Hello World" output line:

            self.response.write('''
                <form action="/sign" method=post>
                <input type=text name=content>
                <input type=submit value="Sign Guestbook">
                </form>
            ''')
    You should now see the text field form and submit button now:


    Figure 5. Adding a web form

    Again, there is no handler for guestbook signing yet, so you will get a 404 error or a blank screen if you click on the button!

    Similar to the previous example, the changes here are so trivial that we do not need to show you the entire main.py file.


    Accept form input & display to user
    • Create handler for /sign: GuestBook.post()
    • Show output: user input

    Obviously, the next step is to be able to process the user input. We will create a new handler to handle the /sign URL that we created in the previous section of the codelab. We’ll call our new handler class GuestBook, although you’re free to use any name you like. Forms use the HTTP POST method, so that’s why we’re creating a post() method (insetad of get()).

    Outside of a minor <br> tag added to the original form, you’ll be doing 2 things in this lab:

    • Adding the GuestBook class – watch the spelling... you don’t need to use CamelCasing as we’ve done here but it should be consistent – and its post() method which simply outputs the user input.
    • Adding another 2-tuple when creating the application to define the handler (GuestBook) for /sign URLs

    import webapp2
    
    class MainHandler(webapp2.RequestHandler):
        def get(self):
            self.response.write('<h1>Hello world!</h1>')
            self.response.write('''
                <form action="/sign" method=post>
                <input type=text name=content>
                <br><input type=submit value="Sign Guestbook">
                </form>
            ''')
    
    class GuestBook(webapp2.RequestHandler):
        def post(self):
            self.response.write(
                '<h2>You wrote:</h2> %s' % self.request.get('content')
            )
    
    app = webapp2.WSGIApplication([
        ('/', MainHandler),
        ('/sign', GuestBook),
    ], debug=True)
    As you can see from the image below, the extra “<br>” caused the button to go below the form field.

    Figure 6. Filling out a web form

    When you submit the form, the “/sign” handler will display the output back to you:

    Figure 7. Filling out a web form

    One word of caution is that the app (at the moment) accepts all text entered by the user, including characters that make up HTML tags, i.e., <, >, etc. If you want to escape these characters to avoid “injection” attacks, import the escape() function from the Python standard library cgi module and wrap the content in it, i.e., from cgi import escape followed elsewhere in your code with:
    self.response.out.write('<h2>You
     wrote:</h2> %s' % escape(self.request.get('content'))
    .


    Datastore: Saving app data to the cloud
    • Form data changed to textarea

    • Create and use new data model class

    • GET / -> Show output -> fetch & display stored entries

    • POST /sign –> Store user input in datastore -> redirect to /

    • Uses Query interface to retrieve all objects in datastore
      • Commented out line uses equivalent GqlQuery interface

    Web applications wouldn’t nearly be as exciting if they didn’t store any data right? The App Engine datastore is arguably the most complex component you will need to learn to become proficient at App Engine, but it is also the single thing that’s the most difficult to implement if you were doing it yourself: creating a reliable, distributed, scalable, persistent data storage mechanism. This is not a relational database with rows and tables... you cannot and should not think of it in this way. It is more like an “object database” with “objects” or “entities.”

    In this lab, we will store user input as the first major step at creating our online guestbook.

    The home page behavior will also change... instead of just presenting a form for user input, we will show the previous guestbook entries first as well as tweak the HTML a bit. So here are the changes that need to be made:

    • Demote “Hello World” by removing the header tag but leave it intact otherwise (more later)
    • Replace title with one that reads, “My GuestBook” – also add an <ol> tag to enumerate posts
    • Fetch all previous entries by calling the data model’s all() method
    • Loop through each entry and display to user
    • Terminate the <ol> list and change the text field to a multiline textarea

    Once all those changes have been made, main.py should now look like:

    from google.appengine.ext import db
    import webapp2
    
    class Greeting(db.Model):
        content = db.StringProperty(multiline=True)
        date = db.DateTimeProperty(auto_now_add=True)
    
    class MainHandler(webapp2.RequestHandler):
        def get(self):
            self.response.write('Hello world!')
            self.response.write('<h1>My GuestBook</h1><ol>')
            #greetings = db.GqlQuery("SELECT * FROM Greeting")
            greetings = Greeting.all()
            for greeting in greetings:
                self.response.write('<li> %s' % greeting.content)
            self.response.write('''
     </ol><hr>
     <form action="/sign" method=post>
     <textarea name=content rows=3 cols=60></textarea>
    <br><input type=submit value="Sign Guestbook"> </form> ''') class GuestBook(webapp2.RequestHandler): def post(self): greeting = Greeting() greeting.content = self.request.get('content') greeting.put() self.redirect('/') app = webapp2.WSGIApplication([ ('/', MainHandler), ('/sign', GuestBook), ], debug=True)
    Those of you who have a sharp eye will notice that one of the lines in get() is commented out. If you’re coming from a relational world, you may feel more at home with App Engine’s GqlQuery interface, letting you use a subset of SQL to issue the same request. You can use that line instead of the query used on line 18 but we don’t recommend that as you need to start getting used to object queries anyway, and now is as good a time as any.

    As mentioned above, the home page behavior is now different, and you should see those changes reflected below:


    Figure 8. New form and saved data


    Figure 9. Form submission redirects to (same) home page

    User authentication

    • Add author field to schema

    • Authentication with Google Accounts

    • Show user name if logged in or sign-in link if not

    • Using the Users service (users API)
      • get_current_user() returns None if not logged in
      • create_login_url() returns valid login page link
      • nickname() method returns logged-in user’s name

    At some point, application developers realize it’s a good thing to have unique users rather than treating everyone the same. Google provides its own user authentication services as well as supports alternatives such as OAuth and OpenID. The changes for this section of the lab are less intensive than the previous section.

    Our example will use Google’s authentication service, which involves importing google.appengine.api.users (see line 4 above). The get_current_user() function is used to get the login of the current user. We need this in several places, the first of which is the home page – we want to greet the currently-logged in user (lines 16-23). This call is also required when a user submits an entry (lines 31-33) because this is the only time you can save this fact, and it’s pretty useful information to differentiate who made which posts.

    More specifically, the changes you need to make are:

    • Add import of users
    • Change “Hello World” to greet user (nickname())
    • Create a link if user not logged in (create_login_url())
    • Save the logged in user in the data model

    Along with the other changes, your updated main.py will now look like:

    from google.appengine.api import users
    from google.appengine.ext import db
    import webaapp2
    
    class Greeting(db.Model):
        author = db.UserProperty()
        content = db.StringProperty(multiline=True)
        date = db.DateTimeProperty(auto_now_add=True)
    
    class MainHandler(webapp2.RequestHandler):
        def get(self):
            user = users.get_current_user()
            if user:
                self.response.write('Hello %s' % user.nickname())
            else:
                self.response.write(
                    'Hello World! [<a href=%s>sign in</a>]' % \
    users.create_login_url(self.request.uri) ) self.response.write('<h1>My GuestBook</h1><ol>') #greetings = db.GqlQuery("SELECT * FROM Greeting") greetings = Greeting.all() for greeting in greetings: self.response.write('<li> %s' % greeting.content) self.response.write(''' </ol><hr> <form action="/sign" method=post> <textarea name=content rows=3 cols=60></textarea> <br><input type=submit value="Sign Guestbook"> </form> ''') class GuestBook(webapp2.RequestHandler): def post(self): greeting = Greeting() user = users.get_current_user() if user: greeting.author = user greeting.content = self.request.get('content') greeting.put() self.redirect('/') app = webapp2.WSGIApplication([ ('/', MainHandler), ('/sign', GuestBook), ], debug=True)
    Your app should function before as expected except for a couple of things... the first thing users will now notice is a sign-in link at the top:


    Figure 10. New login link at the top

    When users click to login, they should see a login prompt similar to this:

    Figure 11. User login prompt

    After logging in, they’re redirected back to the home page, which now shows user info:


    Figure 12. App knows user is logged in


    Force user logins
    • Require users to login before accessing site
    • Show sign-in and sign-out links as appropriate
    • Customizing queries

    This section of the codelab features a couple of minor yet interesting updates to the application. We’re requiring users be logged in before being able to access the home page. Also, let’s change the query a bit... because a guestbook is a time-sensitive application, let’s sort the entries to newest-first (reverse chronological) order.

    The changes you’re making here include:

    • Last time we added a login link; it’s only fair to add one for logout!
    • Redirect to the login link if not logged in
    • Sort by reverse order on the date and only show the ten newest posts

    The “trickiest” code snippet is the customization of the query. We provide (onlines 18-21), both the change using the object query as well as the GqlQuery so you can see the difference. Here is the resulting main.py:

    from google.appengine.api import users
    from google.appengine.ext import db
    import webapp2
    
    class Greeting(db.Model):
        author = db.UserProperty()
        content = db.StringProperty(multiline=True)
        date = db.DateTimeProperty(auto_now_add=True)
    
    class MainHandler(webapp2.RequestHandler):
        def get(self):
            user = users.get_current_user()
            if user:
                self.response.write('Hello %s! [<a href=%s>sign out</a>]' % \
    (user.nickname(), users.create_logout_url(self.request.uri) )) else: self.redirect(users.create_login_url(self.request.uri))
                return
    self.response.write('<h1>My GuestBook</h1><ol>') #greetings = db.GqlQuery("SELECT * FROM Greeting ORDER BY date DESC LIMIT 10") greetings = Greeting.all().order('-date').fetch(10) for greeting in greetings: self.response.write('<li> %s' % greeting.content) self.response.write(''' </ol><hr> <form action="/sign" method=post> <textarea name=content rows=3 cols=60></textarea> <br><input type=submit value="Sign Guestbook"> </form> ''') class GuestBook(webapp2.RequestHandler): def post(self): greeting = Greeting() user = users.get_current_user() if user: greeting.author = user greeting.content = self.request.get('content') greeting.put() self.redirect('/') app = webapp2.WSGIApplication([ ('/', MainHandler), ('/sign', GuestBook), ], debug=True)

    As far as behavior changes go, users will now see a logout link at the top:


    Figure 13. New logout link at the top

    There won’t be a login link any more because users who aren’t are automatically sent to the login screen seen earlier in the previous codelab section. You can confirm this by just clicking on the “sign out” link.


    Web templates

    • Add templating to simplify display
      • Move all HTML out of code into template (index.html)
      • Update all variable access for template
      • Create template context dictionary
      • Render template w/filename & context
      • () not necessary for calling functions/methods
    • Show posts with their authors
      • It would have been much uglier to put this logic in code

    As your application grows both in terms of size, scope, and audience, the need for a visual designer increases. You wish to have the UI be designed by... designers yet want the engineers to stay focused on getting the backend working. This is where web templates come in. In this lab section, we move all of the HTML out into a web template.

    App Engine supports a variety of templating systems, but those are separate downloads, and you would need to bundle those with your app when you deploy live. However, the one from Django comes with App Engine and is available in both the SDK as well as in production.

    If you’re not familiar with templating systems, they are usually HTML combined with some sense of logic such as if conditional statements and looping mechanisms like a traditional programming for loops. Django’s is no exception. In order to pass the data from the application to the templating system, you need to create what is called a context, meaning the variable namespace for the template itself. You'll see in the new code below and see how all of the HTML (via self.response.out.write() calls) has been removed along with the relevant loops and conditionals. To summarize, for this section of the codelab, the exact steps to take are:

    • Add a couple of new import statements
    • Remove the HTML output
    • Create a template context
    • Locate the template
    • Render the template, passing in its context

    Below is the (new) template file, which you can call index.html, that contains most of what was removed... we will add anything else back later:

    <html><body>Hello {{ user.nickname }}!
    <h1>My GuestBook</h1><ol>
    {% for greeting in greetings %}
    <li> {{ greeting.content|escape }}
    {% endfor %}
    </ol><hr>
    <form action="/sign" method=post>
    <textarea name=content rows=3 cols=60></textarea>
    <br><input type=submit value="Sign Guestbook">
    </form></body></html>

    Although the syntax may be foreign to you newbies, you should get the gist of how the template works as well as the syntax of the Django template flow control directives (called Django tags) which is somewhat Python-flavored. You should also observe that callables in Django’s templating system do not need to be called (absence of calling parentheses: ())... they use the pipe (|) symbol and are meant to be similar to Unix&reg; pipes. Such callables are known as Django filters and called automatically. You can learn more at the Django Built-in template tags and filters page as well as the general Django template language page.

    The remaining handler code after performing the steps outlined above looks like this:

    from os import path
    from google.appengine.api import users
    from google.appengine.ext import db
    from google.appengine.ext.webapp.template import render
    import webapp2
    
    class Greeting(db.Model):
        author = db.UserProperty()
        content = db.StringProperty(multiline=True)
        date = db.DateTimeProperty(auto_now_add=True)
    
    class MainHandler(webapp2.RequestHandler):
        def get(self):
            user = users.get_current_user()
            if not user:
                self.redirect(users.create_login_url(self.request.uri))
                return
            greetings = Greeting.all().order('-date').fetch(10)
            context = {
                'user':      user,
                'greetings': greetings,
            }
            tmpl = path.join(path.dirname(__file__), 'index.html')
            self.response.out.write(render(tmpl, context))
    
    class GuestBook(webapp2.RequestHandler):
        def post(self):
            greeting = Greeting()
            user = users.get_current_user()
            if user:
                greeting.author = user
            greeting.content = self.request.get('content')
            greeting.put()
            self.redirect('/')
    
    app = webapp2.WSGIApplication([
        ('/', MainHandler),
        ('/sign', GuestBook),
    ], debug=True)
    

    Static file support

    • Add login and logout links to template

    • Static files...
      • Add stylesheet (static/css/main.css)
      • Move template (static/html/index.html)
      • Add new entry to app.yaml for handling
    • Add timestamp to posts/entries

    • Update app.yaml to support /static/css requests

    In the previous section, we started to logically segregate the files which are the responsibility of the engineer as well as the visual designer. In this section, we take it one step further: static file support.

    The user experience/interaction designer will want to do more than just creating web templates. Templates are just one example of static files which are used to render web pages. Other static files include Cascading Style Sheets (CSS files) and images, both of which can be used to give all the pages of a website a common look-and-feel.

    To keep things organized, we create a top-level static directory containing subdirectories for CSS, HTML, and image files. For our example, we’ll only do the first two. The template we created in the previous section is moved to the static/html subdirectory while a brand new CSS file debuts instatic/css. Here is what you need to do in this section of the codelab:

    • Add back login & logout links
    • Move template to static/html

    In fact, the only changes you need to make in the handler code are:

    • Changed our minds about requiring user logins
    • Add "static/html" in front of the page to index.html
    • Add the missing context for the login and logout links
    from os import path
    from google.appengine.api import users
    from google.appengine.ext import db
    from google.appengine.ext.webapp.template import render
    import webapp2

    class Greeting(db.Model):
    author = db.UserProperty()
    content = db.StringProperty(multiline=True)
    date = db.DateTimeProperty(auto_now_add=True)

    class MainHandler(webapp2.RequestHandler):
    def get(self):
    user = users.get_current_user()
    greetings = Greeting.all().order('-date').fetch(10)
    context = {
    'user': user,
    'greetings': greetings,
    'login': users.create_login_url(self.request.uri),
    'logout': users.create_logout_url(self.request.uri),
    }
    tmpl = path.join(path.dirname(__file__), 'static/html/index.html')
    self.response.write(render(tmpl, context))

    class GuestBook(webapp2.RequestHandler):
    def post(self):
    greeting = Greeting()
    user = users.get_current_user()
    if user:
    greeting.author = user
    greeting.content = self.request.get('content')
    greeting.put()
    self.redirect('/')

    app = webapp2.WSGIApplication([
    ('/', MainHandler),
    ('/sign', GuestBook),
    ], debug=True)


    For the template, you can see below that we’ve added back the login and logout links, hence the reason why we required the additional context variables above. There is also a reference to our new CSS file, main.css, as well. The other big change is that instead of just enumerating and displaying the guestbook entries, we prepend each post with a timestamp of when the post was made as well as the author (if known, or “anonymous” if not). Our guestbook is starting to look a little more “professional” now!
    <html><head>
    <link type="text/css" rel="stylesheet" href="/static/css/main.css" />
    </head>
    <body>Hello
    {% if user %} {{ user.nickname }}! [<a href="{{ logout }}"><b>sign out</b></a>]
    {% else %} World!
     [
    <a href="{{ login }}"><b>sign in</b></a>]
    {% endif %} <h2>Top 10 Most Recent Guestbook Entries</h2> {% for greeting in greetings %} <br> <small>[<i>{{ greeting.date.ctime }}</i>]</small> <b> {% if greeting.author %} <code>{{ greeting.author.nickname }}</code> {% else %} <i>anonymous</i> {% endif %} </b> wrote: {{ greeting.content|escape }} {% endfor %} <hr> <form action="/sign" method="post"> <textarea name="content" rows="3" cols="60"></textarea> <br><input type="submit" value="Sign Guestbook"> </form> </body></html>

    The shortest addition in this section of the codelab is the CSS file (main.css) itself. It sets the default font and background color and can be seen here:
    body {
      font-family: Verdana, Helvetica, sans-serif;
      background-color: #DDDDDD;
    }

    The web template does make a call to the webserver to retrieve the CSS file. Your application does not have a handler for /static/css so it needs to be added. Here is our new app.yaml file:

    application: helloworld
    version: 1
    runtime: python
    api_version: 1
    
    handlers:
    - url: /static/css
    static_dir
    : static/css - url: .* script: main.py

    These two lines need to be placed on top of your original ones because the latter handles all requests (via its universal regular expression ".*"). You want the /static/css match to come first so it won't be "caught" by the universal one.


    Adding caching & Django template inheritance

    • Moved most of template into a base template (for reuse)

    • Added more CSS directives

    • Add caching of greetings (via memcache)

    It’s time for a double feature: in this codelab section, we’re going to add caching using the App Engine Memcache API as well as introduce template inheritance.

    Fetching lots of data from the datastore can be time-consuming. Applications generally see a marked increase in performance by caching often used and accessed data. Memcache is merely a hash table in the cloud, and comes both with a simple API as well as more intermediate features that you expect. In our application, we’re going to cache the set of guestbook entries. Specifically, we will cache them for ten seconds – this is sufficiently long enough so that you can observe browser refreshes hitting the memcache but isn’t long enough to let the data in the cache go stale.

    Another goal of this section further addresses the “common look-and-feel” theme we alluded to earlier. It is desireable that web pages have the same general overall look along with the same header and footer links. It makes no sense to put the same HTML in multiple HTML files when they can all share a “boilerplate” original. In our simple app, this comprises just the “Hello World” greeting at the top along with login or logout link and the “import” of the CSS file – these would be shared by all templates which extend from the base.

    The diffs are straightforward, so you need to do the following:

    • Import the Memcache API package
    • Code to check if we have a cache hit
    • If not in-cache, we need to fetch & cache data
    • New entries auto-flushes the (now) stale cache

    The complete handler which includes the changes from the previous as well as this section:

    from os import path
    from google.appengine.api import memcache, users
    from google.appengine.ext import db, webapp
    from google.appengine.ext.webapp.template import render
    import webapp2
    
    class Greeting(db.Model):
        author = db.UserProperty()
        content = db.StringProperty(multiline=True)
        date = db.DateTimeProperty(auto_now_add=True)
    
    class MainHandler(webapp2.RequestHandler):
        def get(self):
            user = users.get_current_user()
            greetings = memcache.get('greetings')
            if not greetings:
                greetings = Greeting.all().order('-date').fetch(10)
                memcache.add("greetings", greetings, 10)
            context = {
                'user':      user,
                'greetings': greetings,
                'login':     users.create_login_url(self.request.uri),
                'logout':    users.create_logout_url(self.request.uri),
            }
            tmpl = path.join(path.dirname(__file__), 'static/html/index.html')
            self.response.write(render(tmpl, context))
    
    class GuestBook(webapp2.RequestHandler):
        def post(self):
            greeting = Greeting()
            user = users.get_current_user()
            if user:
                greeting.author = user
            greeting.content = self.request.get('content')
            greeting.put()
            memcache.delete('greetings')
    self.redirect('/') app = webapp.WSGIApplication([ ('/', MainHandler), ('/sign', GuestBook), ], debug=True)

    Below is our new base template, base.html, which includes “Hello World” and the login or logout link plus the reference to the CSS file:

    <html><head>
        <link type="text/css" rel="stylesheet" href="/static/css/main.css" />
    </head>
    <body>Hello
    {% if user %} {{ user.nickname }}! [<a href="{{ logout }}"><b>sign out</b></a>]
    {% else %} World!
     [
    <a href="{{ login }}"><b>sign in</b></a>]
    {% endif %} {% block content %} {% endblock %} </body></html>

    In the base template above, you will see that there is room reserved for the actual content, e.g., in the Django block tag. Below in the stripped page content file, index.html, we extend the base template and specify the content to drop into the base template when rendered. Otherwise, it’s exactly the same as the template file from the previous codelab section:

    {% extends "base.html" %}
    {% block content %}
    <h2>Top 10 Most Recent Guestbook Entries</h2>
    
    {% for greeting in greetings %}
        <br>
        <small>[<i>{{ greeting.date.ctime }}</i>]</small>
        <b>
        {% if greeting.author %}
            <code>{{ greeting.author.nickname }}</code>
        {% else %}
            <i>anonymous</i>
        {% endif %}
        </b>
        wrote:
        {{ greeting.content|escape }}
    {% endfor %}
    
    <hr>
    <form action="/sign" method="post">
        <textarea name="content" rows="3" cols="60"></textarea>
        <br><input type="submit" value="Sign Guestbook">
    </form>
    {% endblock %}

    The background color has been removed from and styles added for HTML header, anchor, and paragraph tags in our main.css file:

    body {
        font-family: Verdana, Helvetica, sans-serif;
        color: #bdd;
        background: #255;
        padding: 0 5em;
        margin: 0
    }
    
    h1 {
        padding: 2em 1em;
        background: #577
    }
    
    h2 {
        color: #add;
        border-top: 1px dotted #fff;
        margin-top: 2em
    }
    
    a {
        color: #9ff
    }
    
    p {
        margin: 1em 0
    }

    In a more realistic situation, you would have a UE/UI/UX graphic designer working on the CSS independently of the developer, allowing various team members to work on tasks simultaneously. The only file they may both access would be the web template.

    Once you’ve made the modifications above and have a running app, be sure to make a new entry into your datastore. Then refresh your page several times and visit the memcache viewer on the admin console to confirm you’re hitting the cache as well as fetching and re(caching) after 10s have elapsed. You can also confirm the cache is flushed as-expected when a new post is made because the redirect to the home page should show the new post right away.


    Part 2: Using other App Engine Services: email, XMPP

    Now that we have a basic guestbook running, there are some optional features we can add to our app with just a few simple changes. App Engine provides a rich set of APIs to give you more flexibility in your applications. We will take a look at:

    • Sending email (receiving email also supported)
    • Chat/IM/Jabber (XMPP)
    • Logging (coming soon)

    Other App Engine APIs not covered in this codelab include:

    • Application statistics (AppStats)
    • Blobstore
    • Task queues/cron/deferred
    • URLfetch
    • Profiling
    • Capabilities
    • remote_api

    Other things you can do outside of the App Engine APIs include:

    • Twitter
    • PubSubHubBub
    • Google Wave bot

    Sending email

    Sending email is quite easy with App Engine. There are necessary restrictions, but it can be as simple as this code snippet:

    from google.appengine.api import mail
    import webapp2
    
    class MainHandler(webapp2.RequestHandler):
        def get(self):
            mail.send_mail('xxx@APP-ID.appspotmail.com',
                RECIP_EMAIL, 'test subject', 'test body')
            self.response.out.write('email sent')
    
    app = webapp2.WSGIApplication([
        ('/.*', MainHandler),
    ], debug=True)
    

    The basic rules of sending email are:

    Be sure to substitute in the correct values for the sender and receive above before integrating into your application. What is in the code snippet above will not compile.

    App Engine also supports the receipt of email, but that is another topic for another time. Anyway, it should be fairly straightforward to upgrade your guestbook by adding an email feature. Here are some ideas as to what you can do to your app to enhance it with email:

    • The administrator when anyone makes a post
    • All previous posters when anyone makes a post
    • A receipt to a poster that their post was approved/published/stored

    For our guestbook example, we'll do the first item in the list above:

    • Email the administrator for every new post (supporting anonymous posts)

    Here is our modified code:

     1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    from os import path
    from google.appengine.api import mail, memcache, users
    from google.appengine.ext import db
    from google.appengine.ext.webapp.template import render
    import webapp2

    class Greeting(db.Model):
    author = db.UserProperty()
    content = db.StringProperty(multiline=True)
    date = db.DateTimeProperty(auto_now_add=True)

    class MainHandler(webapp2.RequestHandler):
    def get(self):
    user = users.get_current_user()
    greetings = memcache.get('greetings')
    if not greetings:
    greetings = Greeting.all().order('-date').fetch(10)
    memcache.add("greetings", greetings, 10)
    context = {
    'user': user,
    'greetings': greetings,
    'login': users.create_login_url(self.request.uri),
    'logout': users.create_logout_url(self.request.uri),
    }
    tmpl = path.join(path.dirname(__file__), 'static/html/index.html')
    self.response.write(render(tmpl, context))

    class GuestBook(webapp2.RequestHandler):
    def post(self):
    greeting = Greeting()
    user = users.get_current_user()
    if user:
    greeting.author = user
    name = user.nickname()
    else:
    name = 'anonymous'
    greeting.content = self.request.get('content')
    greeting.put()
    memcache.delete('greetings')

    mail.send_mail(
    user and user.email() or 'postmaster@APP-ID.appspotmail.com', # from
    'RECIP_EMAIL', # to
    'GuestBook post from %s' % name, # subj
    '%s wrote:\r\n\r\n"%s"' % (name, greeting.content), # body
    )
    self.redirect('/')

    app = webapp.WSGIApplication([
    ('/', MainHandler),
    ('/sign', GuestBook),
    ], debug=True)

    The changes made to the previous iteration include:

    • (line 2) Importing the mail module
    • (lines 34-36) Need to send a “sender name” in email
    • (lines 40-45) Send actual email (need 4 valid fields)

    Chat/XMPP/Instant Messaging

    Another form of communication that people use these days is instant messaging. XMPP (Extensible Messaging and Presence Protocol) is an open XML-based protocol (originally named Jabber) that enables this type of communication between users (and chat robots)!

    App Engine supports XMPP and has implemented 4 basic instant message (IM) operations:

    • Sending of instant messages
    • Receiving of instant messages
    • Chat invitations to potential chat partners
    • Status query (is a user online/available?)

    Similar to email, activity is only logged via the development server. You really need to upload it live to experience this functionality.

    Sending IMs can be as simple as xmpp.send_message(jid, msg) where a “jid” is synonymous to a “Jabber ID” or the user you’re trying to communicate with, and msg is a string representing the message itself. However, things aren’t always as simple. You usually have to first check if a user is accepting your message, and if a user is online before sending an IM. In order to do this, we define Subscriber class for tracking users' subscription/online status, and check is_friend and status properties first.

    Receiving of messages requires a separate handler, because when an IM comes in to your app, App Engine POSTS to http://APP-ID.appspot.com/_ah/xmpp/message/chat/, thus you have to handle those inbound requests. Likewise, subscription notifications are sent to http://APP-ID.appspot.com/_ah/xmpp/subscription/SUBSCRIPTION_STATUS/ where SUBSCRIPTION_STATUS is one of subscribe/subscribed/unsubscribe/unsubscribed, and online status notifications are sent to http://APP-ID.appspot.com/_ah/xmpp/presence/ONLINE_STATUS where ONLINE_STATUS is one of available/unavailable/probe(probe is for requesting user's online status, not a notification). Please see more details at User Subscriptions and User Presence in our documentation.

    In the application below, we’ve built both a sending and receiving of IMs. The sending occurs if a user is recognized as being friend, and online; if the user is not a friend, an invitation to chat is sent instead, and if the user is a friend, but offline, then nothing happens. On the receiving side, we simply capture the inbound message and immediately turn it around and reply to the send that we received their message (and what it was).

    import hashlib

    from google.appengine.api import xmpp
    from google.appengine.api import users
    from google.appengine.ext import db
    import webapp2

    class Subscriber(db.Model):
       address
    = db.StringProperty()
       is_friend
    = db.BooleanProperty(default=False)
       status
    = db.StringProperty(required=False)

       
    @classmethod
       
    def get_by_email(cls, email):
           
    return cls.get_by_key_name(hashlib.sha1(email).hexdigest())


    class MainHandler(webapp2.RequestHandler):
       
    def get(self):
           
    if users.get_current_user() is None:
               
    self.redirect(users.create_login_url( self.request.uri))
               
    return
              
           user_address
    = users.get_current_user().email()
           user
    = Subscriber.get_by_email(user_address)
           
    if user and user.is_friend is True:
               
    if user.status == "available":
                   xmpp
    .send_message(user_address, "test msg")
                   
    self.response.write("A message sent.")
               
    else:
                   
    self.response.write("The user is offline.")
           
    else:
               xmpp
    .send_invite(user_address)
               
    self.response.write("An invitation sent.")


    class XMPPHandler(webapp2.RequestHandler):
       
    def post(self):
           msg
    = xmpp.Message(self.request.POST)
           msg
    .reply("I got your msg: '%s'" % msg.body)


    class XMPPPresenceHandler(webapp.RequestHandler):
       
    def post(self, presence):
           from_jid
    = self.request.get('from').split('/')[0]
           
    def txn():
               user
    = Subscriber.get_by_email(from_jid)
               
    if user:
                   user
    .status = presence
                   
    return user.put()
               
    else:
                   
    return Subscriber(key_name=hashlib.sha1( from_jid).hexdigest(), address=from_jid, status=presence).put()
           db
    .run_in_transaction(txn)
           
    return


    class XMPPSubscriptionHandler(webapp2.RequestHandler):
       
    def post(self, subscription):
           from_jid
    = self.request.get('from').split('/')[0]
           is_friend
    = False
           
    if subscription == "subscribe" or subscription == "subscribed":
               is_friend
    = True
           
    def txn():
               user
    = Subscriber.get_by_email(from_jid)
               
    if user:
                   user
    .is_friend = is_friend
                   
    return user.put()
               
    else:
                   
    return Subscriber(key_name=hashlib.sha1(from_jid).hexdigest(),
                                     address
    =from_jid, is_friend=is_friend).put()
           db
    .run_in_transaction(txn)
           
    return


    app
    = webapp2.WSGIApplication([
       (
    '/_ah/xmpp/message/chat/', XMPPHandler),
       (
    '/_ah/xmpp/presence/(.*)/', XMPPPresenceHandler),
       (
    '/_ah/xmpp/subscription/(.*)/', XMPPSubscriptionHandler),
       (
    '/', MainHandler),
    ], debug
    =True)

    Be sure to add inbound_services entries in your app.yaml file in order to activate those XMPP services. The new app.yaml file will look like:
    application: helloworld
    version: 1
    runtime: python27
    api_version: 1
    
    inbound_services:
    - xmpp_message
    - xmpp_presence
    - xmpp_subscribe
    
    
    handlers:
    - url: /static/css
      static_dir: static/css
    
    - url: .*
      script: main.py
    All xmpp functions send_invite(), and send_message() also take a from_jid identifier. Similar to email, this can be either APP-ID@appspot.com or xxx@APP-ID.appspotchat.com (yes, xxx can be anything and be sure to reread that domain name!) If not provided to these functions, it defaults to APP-ID@appspot.com.

    It’s also not inconceivable that we could add IM features to the guestbook application like the ones for email we suggested above:

    • A simple “ping” could be sent to the guestbook owner notifying of a new entry
    • A user could add an entry to the guestbook by sending an IM to the “guestbook” app/user

    Instant messaging is a popular and useful form of communication like email is, but unlike email, there is more of a factor of instant gratification.

    One good example of extending the simple XMPP app above as well as our GuestBook application is to add a “chatbot” that takes a few simple commands, one of which returns the top 5 most recent guestbook entries:

     1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    from os import path
    from google.appengine.api import mail, memcache, users, xmpp
    from google.appengine.ext import db
    from google.appengine.ext.webapp.template import render
    import webapp2
    
    class Greeting(db.Model):
        author = db.UserProperty()
        content = db.StringProperty(multiline=True)
        date = db.DateTimeProperty(auto_now_add=True)
    
    def getGreetings(fetch=10):
        greetings = memcache.get('greetings')
        if not greetings:
            greetings = Greeting.all().order('-date').fetch(fetch)
            memcache.add("greetings", greetings, 10)
        return greetings
    
    class MainHandler(webapp2.RequestHandler):
        def get(self):
            user = users.get_current_user()
            greetings = getGreetings()
            context = {
                'user':      user,
                'greetings': greetings,
                'login':     users.create_login_url(self.request.uri),
                'logout':    users.create_logout_url(self.request.uri),
            }
            tmpl = path.join(path.dirname(__file__), 'static/html/index.html')
            self.response.write(render(tmpl, context))
    
    class GuestBook(webapp2.RequestHandler):
        def post(self):
            greeting = Greeting()
            user = users.get_current_user()
            if user:
                greeting.author = user
                name = user.nickname()
            else:
                name = 'anonymous'
            greeting.content = self.request.get('content')
            greeting.put()
            memcache.delete('greetings')
            mail.send_mail(
                user and user.email() or 'postmaster@APP-ID.appspotmail.com', # from
                'RECIP_EMAIL', # to
                'GuestBook post from %s' % name, # subj
                '%s wrote:\r\n\r\n"%s"' % (name, greeting.content), # body
            )
            self.redirect('/')
    
    class GBChatBot(webapp2.RequestHandler):
        def post(self):
            message = xmpp.Message(self.request.POST)
            cmd = message.body[0:5].lower()
            if cmd == '/list':
                greetings = getGreetings(5)
                reply = '%s\r\n\r\n%s' % (
                    '5 Most Recent Guestbook entries:',
                    '\r\n'.join(['%s: %s' % (
                        '*%s*' % g.author.nickname() if g.author else '_anonymous_',
                        g.content[:40]) for g in greetings
                    
                ]))
            elif cmd == '/help':
                reply = '''
    Guestbook Chatbot 0.2
    Supported commands are:
     */list* (5 most recent entries)
     */help* (this help msg)'''
    else: reply = '''
    Command "
    %s" not supported!
    Send "/help" for command list.'''
    % message.body
    message
    .reply(reply) app = webapp2.WSGIApplication([ ('/', MainHandler), ('/sign', GuestBook), ('/_ah/xmpp/message/chat/', GBChatBot), ], debug=True)

    The most notable new additions to this version of main.py are the new GBChatBot class and the creation of a separate function to get the greetings we need to return to the user (5 for the chatbot and 10 for the website). Here are a summary of all the changes we had to make from the previous version:

    • (line 2) Importing the xmpp module
    • (lines 12-17, 22, 57) Need single routine to fetch data
    • (lines 52-75) New GBChatBot class
    • (line 80) New handler for inbound XMPP messages

    NOTE: the application at this point will not accept instant messages yet. You need to be invited to be able to chat with the bot. To make this happen, just run the first XMPP app (the simple one before we integrated it into our GuestBook app). If you hit that app at ‘/’, you will receive an invite. We will leave an exercise to the reader to add an opt-in feature to the guestbook app, perhaps when users create a guestbook entry. Once the invite is sent, you can go to your XMPP application and accept the chat request from your app/bot; then you will be able to send commands to it.


    Part 3: Using other App Engine Services: UrlFetch, Cron, Channel API, Task Queues

    This third part is a stand alone module which will show you how to use the services:
    • UrlFetch - to fetch data from another Internet source
    • Cron - to run recurring offline jobs
    • Channel API - to push updates to browser windows without needing to refresh (aka Comet)
    • Task Queues - To run a long offline batch process
    The complete application these exercise build to is an application that fetches a news feed and then pushes the titles, headlines and descriptions to browsers via the Channel API.

    Note: the complete zipped version of this application is available here.
    (instructions on how to run the complete application are located at the end of this section)

    Create boilerplate JavaScript code

    You need to have the basic JS code:

    Channel API:
       <script type="text/javascript" src="/_ah/channel/jsapi"></script>   

    Getting the token from the server:
       function getToken(){
          var xhr = new XMLHttpRequest();
          xhr.open('GET', '/xxx', false); // or whatever; calls TokenHandler.get() (see below)
          xhr.send(null);
          return(xhr.responseText);   
       };

    Open channel to server once token received
      function openNewsChannel(token){
         var channel = new goog.appengine.Channel(token);
         var socket = channel.open();
         socket.onopen = onOpened; // opt do it w/a handler (sample does)
       };

    To access the entire .html file used, just grab the file from the ZIP file above or jump over to the Java codelab momentarily.

    Using URLFetch to retrieve an XML feed

    This section will show how to extract content from an XML feed. In this example a news feed will be fetched and displayed. A further exercise will fetch the feed and persist it in the datastore for later use.

    The handler you should create -- we'll call ours FetchNewsHandler -- will fetch an XML news feed, parse the data, and persist it in the datastore. Although it could be run manually, it is best suited to be run by a cron job as it will continually refresh the datastore with the latest news feeds. These steps explain how to build this servlet.

    • create a NewsItem data model
    • create a handler to fetch news from an XML RSS feed
    # feed item data model
    class NewsItem(db.Model):
        . . .

    # fetches an XML RSS news feed and displays it
    class FetchNewsHandler(webapp2.RequestHandler):
        def get(self):
            # delete old news items
            . . .
      
          # get, parse, and store feed items; we
            #   suggest xml.dom, xml.etree in stdlib or
            #   3rd-party: BeautifulSoup, lxml, html5lib
            . . .
            # display stored feed items
            #   (for debugging; remove for prod)
            . . .
                  

    Building a Servlet to create Channel API Tokens for client access

    This handler will respond to a JavaScript client and create a new Channel API Client ID and along with a token which is then returned back to the client so it can set up a communication channel.
    (For further documentation on the Channel API and setting up a Client ID and Tokens, see http://code.google.com/appengine/docs/python/channel/overview.html)

    • create TokenHandler
    • auto-generate a "client ID"
    • somehow persist the client ID by using the datastore so the server can keep track of all connections
    • create a channel using the API
    • return the token
    class TokenHandler(webapp2.RequestHandler):
        def get(self):
           . . .

    Building a Handler to Broadcast News headlines to clients

    This handler, will broadcast the headlines that have been persisted in the datastore to any browser clients. It first queries the current clients id (that have also been persisted in the datastore), and then queries the news items. It then loops through each news item and broadcasts to all clients.

    Although this servlet can be run manually, it is best to be launched as an offline process using a task Queue. A final step will show to make this servlet launchable via a task queue.

    Break this out into a function if you want to run it as a deferred task.

    • create a MessageHandler class
    • query all the NewsItems
    • broadcast to all clients by sending a msg to all the channels
      • (by looping through all the client IDs you saved earlier)

    class MessageHandler(webapp2.RequestHandler):
        def get(self):
            . . .


    Installing and running the complete Application

    Installing and running the complete application can be done in order to see the entire set of services in this section working together.  These steps can be done even before doing the individual steps to build this application.
    1. Download the sample application from the link above.
    2. Unzip this file into a clean directory.
    3. Start up dev_appserver or the launcher on it
    Running the app. 
    (Important, upon first run, you must first seed the News feed database.)
    1. In the console, you should that http://localhost:8080 (or whatever port you started the dev is now accessible.
    2. Important before accessing the application at the root directory, you must first seed the datastore with news feeds. This is done by executing the following "cron" job manually
      1. From the main page in your browser, click on "Login to view news client".
      2. (Important) If you are signed in, click on "sign out" link. Do not click on "proceed to news client"
         link.
      3. Once you've signed out, click on the "Sign in" link and when the login page appears, select the "Sign in as Administrator" check box, then click "Log In" button.
      4. After logging in as an administrator, manually launch the cron job to fetch news feed data by accessing: http://localhost:8080/cron/fetchNews
        This will fetch the current news feed data and persist it into the local datastore for further use. You should also see the data fetched displayed in the browser window.
      5. Optional: Go to the local SDK Console and verify that the data has been persisted.
        • http://localhost:8080/_ah/admin/datastore
    3. Now that the local datastore has been populated with new data, we can now access it from the application. Return to the URL: http://localhost:8080
    4. If you are not signed in, please sign in.
    5. Upon verifying that you are signed in, click on the link "proceed to news client".  (newsclient.html)
    6. You should see the page "Breaking News Stories" as a header. You should also see browser Alert popup indicating that you are now  'connected to the server'. - Click ok.
    7. Now you can launch the Task that will start broadcasting out a series of news headlines to this browser. To launch this task, open a new tab and access the url: http://localhost:8080/enqueueMsgs
      This will launch a task queue that will start broadcasting messages to any clients that are currently logged in.
    8. Now return to the new client page (newsclient.html) in the other tab.
    9. You should now see news headlines and updates appearing in the browser window every 3 seconds!

      The End

       

      We hope that you’ve gotten a useful App Engine hands-on tutorial. There is plenty of documentation to read regarding the existing online tutorial, and we have tried to add more specifics to the exercise of getting the Guestbook application running.


      Comments