Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.

...

While

...

you

...

can

...

write

...

OpenSocial

...

apps

...

that

...

run

...

solely

...

in

...

JavaScript

...

and

...

use

...

the

...

Persistence

...

API

...

to

...

store

...

data

...

on

...

the

...

container,

...

many

...

OpenSocial

...

apps

...

communicate

...

with

...

a

...

third-party

...

server

...

for

...

data

...

storage

...

or

...

application

...

logic.

...

Integrating

...

with

...

your

...

own

...

third-party

...

server

...

allows

...

you

...

to

...

add

...

new

...

dimensions

...

to

...

your

...

app,

...

like

...

providing

...

a

...

data

...

API,

...

hosting

...

static

...

content,

...

or

...

allowing

...

configuration

...

through

...

an

...

admin

...

console.

...

In

...

this

...

article,

...

we'll

...

build

...

an

...

app

...

that

...

is

...

similar

...

to

...

the

...

gift-giving

...

application

...

built

...

in

...

the

...

OpenSocial

...

tutorial

...

.

...

When

...

a

...

user

...

views

...

the

...

app,

...

they

...

see

...

a

...

drop-down

...

menu

...

of

...

gifts

...

(such

...

as

...

a

...

peanut,

...

or

...

a

...

red

...

pistachio

...

nut)

...

and

...

another

...

drop-down

...

menu

...

containing

...

a

...

list

...

of

...

their

...

friends.

...

The

...

user

...

can

...

give

...

any

...

of

...

these

...

gifts

...

to

...

a

...

friend

...

and

...

the

...

gift

...

transaction

...

will

...

be

...

displayed.

...

The

...

app

...

will

...

also

...

display

...

any

...

gifts

...

that

...

the

...

user

...

has

...

received.

...

You

...

can

...

find

...

all

...

the

...

source

...

code

...

used

...

to

...

run

...

this

...

application

...

in

...

the

...

opensocial-gifts

...

project on Google Code Project Hosting. You can also install this app on the orkut sandbox.

The original gift-giving app is built using 100% client-side OpenSocial code and is therefore subject to a number of limitations imposed by the container rendering the app, such as the amount of data the container will let you store, and the access controls related to when you can read and write data. With Google App Engine, you can manage all this data on an external server, freeing your app from any constraints imposed by the container. Viva la revolución!

Audience

The ideal audience for this article knows a little about Google App Engine, and a little about OpenSocial. We'll build this app from ground up, but it will help if you've worked through the Google App Engine Getting Started Guide and the Opensocial Tutorial.

OpenSocial apps are written in JavaScript and HTML, so you'll need to be able to read these languages to understand the examples. You'll also need to be familiar with Python because that's what all the server side Google App Engine code is written in. That said, we're not doing anything too complicated here, so if you've got some basic web development experience, you should be fine.

Note: You'll need a Google account and the App Engine SDK to start coding.

Architecture

By the end of this article, you'll have constructed an OpenSocial app where user's can give each other gifts. Along the way, we'll implement components to store, access, manipulate, and display application data (gifts and gift transactions). As you implement more functional apps, the implementations for each of these components will likely be more complex, but the general interactions and components themselves can remain mostly the same. This gift-giving app has five such components (in order of appearance):

  1. Google App Engine app
  2. Database model
  3. Admin interface
  4. JSON data API
  5. OpenSocial application spec

Total servers you need to maintain: 0.

Google App Engine app (app.yaml and gifts.py)

Google App Engine projects are defined and configured using an app.yaml file that defines how to handle incoming requests. In this file, we'll configure our app to send requests to gifts.py where we'll create a WSGIApplication that will handle these requests and send appropriate responses.

Database model (db_model.py)

The data for this app will be stored using the Google App Engine datastore. We'll store the gifts that are available for giving as a character string for now. When a user gives a gift we'll store the sender, receiver, and gift given as a gift transaction. We can leverage the IDs provided by the OpenSocial API to identify the users involved in a gift transaction.

Admin interface (admin.py)

We'll use Google App Engine to host a small web application that will allow us to perform some simple operations on the datastore. We'll use Google App Engine's authentication and identity management tools to ensure that only certain users can access this console and manipulate data directly. Our admin interface will allow us to initialize or view the data in the datastore, but you could extend this concept to enable more complex management tasks.

Note: Google App Engine provides an Admin Console where you can perform many useful tasks, like manipulating data in the datastore, viewing application logs, and accessing metrics about your app's usage.

JSON data API (api.py)

We'll expose the data in the datastore through a data API (also hosted by Google App Engine). The API will accept HTTP GET requests to access gifts or gift transactions and return the data as a JSON string. The API will also accept HTTP POST requests to add a gift transaction to the datastore.

OpenSocial application spec (gifts.xml)

Our users will be interacting with the OpenSocial app, defined in an application spec XML file. The application spec contains HTML and JavaScript that will be rendered by an OpenSocial container, and the JavaScript will run client-side in the user's browser. The OpenSocial app will make calls to the data API hosted on Google App Engine using gadgets.io.makeRequest.

Setting up a Google App Engine app

First you need to decide on an application identifier for your application. This identifier must be unique across all Google App Engine applications, so for this article, use opensocial-gifts-username where username is your username (e.g. opensocial-gifts-johndoe). If you have a Google App Engine account, log into the Google App Engine admin console and create an app. Since App Engine is currently in a preview release, you can only publish up to three applications for now (although you can create as many local applications as you'd like). Bearing this in mind, you may want to use a generic application identifier, like username-dev.

The first iteration of our application will have just two files: app.yaml and gifts.py. The app.yaml file contains configuration details about your app, including which Python scripts to execute when your app receives requests. Here's a simple app.yaml file to get us started:

Code Block
application: opensocial-gifts-username
version: 1
runtime: python
api_version: 1

handlers:
- url: /.*
{panel}
  script: gifts.py
{panel}
</source>

*

Note:

...

The

...

value

...

used

...

for

...

application needs

...

to

...

uniquely

...

identify

...

your

...

Google

...

app

...

Engine

...

app

...

so

...

be

...

sure

...

to

...

replace

...

username

...

with

...

your

...

username

...

(e.g.

...

opensocial-gifts-jdoe).

...

In

...

the

...

app.yaml file,

...

we

...

specified

...

that

...

all

...

requests

...

should

...

be

...

handled

...

by

...

gifts.py using the regex /.*

...

.

...

Let's

...

define

...

a

...

WSGIApplication in gift.py and create a temporary RequestHandler so we can do a quick sanity check. Here's the content in gifts.py:

Code Block

# Standard libraries
import wsgiref.handlers

# AppEngine imports
from google.appengine.ext import webapp

class SanityCheck(webapp.RequestHandler):
{panel}
  def get(self):
    self.response.out.write("You're not crazy\!")
{panel}

# Map URLs to request handler class
application = webapp.WSGIApplication([('-', SanityCheck)],
{panel}
                                     debug=True)
{panel}

# Fire it up\!
wsgiref.handlers.CGIHandler().run(application)
</source>

Now

...

that

...

we've

...

got

...

a

...

simple

...

app,

...

we'll

...

test

...

it

...

with

...

the

...

development

...

web

...

server

...

.

...

If

...

you

...

haven't

...

already,

...

download

...

the

...

SDK

...

and

...

uncompress

...

it.

...

From

...

the

...

google_appengine directory,

...

run

...

'

...

./dev_appserver.py

...

<your_app_directory>;

...

'.

...

Verify

...

that

...

you

...

can

...

access

...

your

...

app

...

from

...

a

...

browser

...

(the

...

URL

...

will

...

be

...

http://localhost:8080/

...

).

...

Using Google App Engine to store data

The original gift-giving

...

app

...

uses

...

the

...

OpenSocial

...

Persistence

...

API

...

to

...

record

...

the

...

gifts

...

that

...

each

...

user

...

gives.

...

Since

...

data

...

can

...

only

...

be

...

written

...

for

...

the

...

VIEWER,

...

the

...

original

...

app

...

has

...

to

...

do

...

some

...

extra

...

work

...

to

...

perform

...

simple

...

operations.

...

For

...

example,

...

to

...

show

...

all

...

the

...

gifts

...

a

...

person

...

has

...

received,

...

the

...

app

...

iterates

...

through

...

all

...

of

...

their

...

friends

...

to

...

see

...

which

...

have

...

given

...

them

...

a

...

gift.

...

Another

...

potential

...

issue

...

with

...

the

...

Persistence

...

API

...

is

...

that

...

the

...

storage

...

limits

...

are

...

set

...

by

...

the

...

container.

...

We'll

...

circumvent

...

both

...

of

...

these

...

issues

...

by

...

using

...

Google

...

App

...

Engine

...

to

...

store

...

our

...

data.

...

Defining

...

the

...

data

...

model

...

We

...

basically

...

just

...

need

...

to

...

store

...

a

...

history

...

of

...

who

...

gave

...

what

...

gift

...

to

...

whom.

...

For

...

each

...

gift,

...

we

...

can

...

just

...

store

...

the

...

name

...

of

...

the

...

gift.

...

For each

...

gift

...

transaction,

...

we

...

need

...

to

...

store

...

the

...

sender,

...

recipient,

...

and

...

a

...

reference

...

to

...

the

...

gift

...

that

...

was

...

given.

...

We

...

can

...

use

...

the

...

OpenSocial

...

user

...

IDs

...

to

...

identify

...

the

...

sender

...

and

...

recipient,

...

and

...

we

...

can

...

use

...

the

...

db.Key()

...

object

...

for

...

the

...

reference

...

to

...

the

...

gift

...

object.

...

To

...

define

...

this

...

model

...

in

...

the

...

Google

...

App

...

Engine

...

datastore,

...

create

...

a

...

file

...

called

...

db_model.

...

py and

...

insert

...

the

...

following

...

Python

...

code:

...

Code Block
from google.appengine.ext import db

class Gift(db.Model):
{panel}
  name = db.StringProperty()
{panel}

class GiftTransaction(db.Model):
{panel}
  sender_id = db.StringProperty()
  receiver_id = db.StringProperty()
  gift = db.ReferenceProperty(Gift)
{panel}
</source>

This defines two <tt>Model</tt> classes, one to represent a gift and one to represent a gift transaction where a sender gives a recipient a gift. 

h2. Populating the datastore
Now that we have a data model, we need some way to populate it.  Create a file called <tt>admin.py</tt> and define an <tt>Admin</tt> class to handle administrative operations like this.  Let's start with two methods for initializing the gifts and gift transactions in the datastore:

<source lang="javascript">

This defines two Model classes, one to represent a gift and one to represent a gift transaction where a sender gives a recipient a gift.

Populating the datastore

Now that we have a data model, we need some way to populate it. Create a file called admin.py and define an Admin class to handle administrative operations like this. Let's start with two methods for initializing the gifts and gift transactions in the datastore:

Code Block
from db_model import Gift, GiftTransaction

class Admin:
{panel}
  """Initializes the list of gifts in the datastore."""
{panel}

  def initGifts(self):
{panel}
    """Deletes any existing gifts and add the default gifts."""
    for gift in Gift.all():
      gift.delete()
    GIFT_NAMES = ['a cashew nut',
                  'a peanut',
                  'a hazelnut',
                  'a red pistachio nut']
    for name in GIFT_NAMES:
      gift = Gift()
      gift.name = name
      gift.put()
{panel}
   def initGiftTransactions(self):
{panel}     """Deletes any existing gift transactions."""
    for t in GiftTransaction.all():
      t.delete()
{panel}
</source>

h2. Accessing the datastore
Now 

Accessing the datastore

Now let's

...

add

...

a

...

couple

...

methods

...

to

...

the

...

Admin class

...

for

...

accessing

...

the

...

gifts

...

and

...

gift

...

transactions

...

in

...

the

...

datastore.

Code Block
 <source lang="javascript">
{panel}
  def getGiftNames(self):
    names = []
    for gift in Gift.all():
      names.append(gift.name)
    return names
{panel}

  def getGiftTransactions(self):
{panel}
    giftTransactions = []
    for t in GiftTransaction.all():
      giftTransactions.append("sender: %s, reciever: %s, gift: %s" %
          (t.sender_id, t.receiver_id, t.gift.key()))
    return giftTransactions
{panel}
</source>

Great,

...

now

...

we

...

can

...

read

...

and

...

write

...

data

...

in

...

the

...

datastore

...

- but

...

how

...

do

...

we

...

invoke

...

this

...

Python

...

code?

...

That's

...

where

...

the

...

admin

...

webapp

...

comes

...

into

...

play.

...

A

...

simple

...

Google

...

App

...

Engine

...

web

...

interface

...

Now

...

we'll

...

extend

...

our

...

Google

...

App

...

Engine

...

application

...

to

...

include

...

an

...

admin

...

web

...

application

...

so

...

we

...

can

...

initialize

...

or

...

view

...

the

...

data

...

in

...

the

...

data

...

store

...

from

...

a

...

browser.

...

We'll

...

create

...

a

...

request

...

handler

...

so

...

that

...

we

...

can

...

invoke

...

the

...

Admin class

...

by

...

sending

...

a

...

GET

...

request

...

to

...

a

...

certain

...

URL,

...

like

...

http://opensocial-gifts-*_username_*.appspot.com/admin?action=init.

...

Creating

...

a

...

request

...

handler

...

The AdminServer class will be a subclass of the RequestHandler class provided by the google.appengine.ext.

...

webapp package.

...

We

...

can

...

implement

...

a

...

get method

...

that

...

will

...

be

...

invoked

...

any

...

time

...

the

...

application

...

forwards

...

a

...

request

...

to

...

this

...

class.

...

Add

...

the

...

following

...

import

...

statement

...

and

...

class

...

definition

...

to

...

admin.py:

Code Block

from google.appengine.ext import webapp

class AdminServer(webapp.RequestHandler):
{panel}   """Handles requests to /admin URLs and delegates to the Admin class."""
{panel}

  def get(self):
{panel}
    """Handle GET requests."""
    self.response.out.write('Welcome to the admin webapp')
{panel}
</source>

h2. Forwarding requests
First, 

Forwarding requests

First, let's

...

get

...

rid

...

of

...

the

...

SanityCheck class

...

we

...

started

...

out

...

with

...

in

...

gifts.py. Instead,

...

import

...

the

...

admin module

...

so

...

we

...

can

...

access

...

the

...

AdminServer class.

...

We'll

...

edit

...

the

...

WSGIApplication constructor

...

to

...

forward

...

requests

...

for

...

"/admin"

...

URLs

...

to

...

the

...

AdminServer class.

...

Here's

...

an

...

updated

...

version

...

of

...

the

...

gifts.py file with changes bolded:

Code Block

# Standard libraries
import wsgiref.handlers

# AppEngine imports
from google.appengine.ext import webapp

'''# OpenSocial Gifts imports
import admin'''

# Map URLs to request handler classes
application = webapp.WSGIApplication(*'''[('-admin', admin.AdminServer)]*,
{panel}''',
                                     debug=True)
{panel}

# Fire it up\!
wsgiref.handlers.CGIHandler().run(application)
</source>

Now

...

hit

...

the

...

URL

...

http://localhost:8080/

...

admin with

...

your

...

browser.

...

You

...

should

...

see

...

the

...

admin

...

web

...

application

...

welcome

...

message.

...

Identifying

...

the

...

user

...

One

...

of

...

the

...

coolest

...

features

...

of

...

App

...

Engine

...

is

...

that

...

it

...

handles

...

authentication

...

and

...

identity

...

management

...

for

...

you.

...

We

...

only

...

need

...

a

...

few

...

lines

...

of

...

code

...

to

...

allow

...

for

...

user

...

logins

...

and

...

we

...

can

...

differentiate

...

between

...

regular

...

users

...

and

...

administrators

...

with

...

the

...

users.IsCurrentUserAdmin method.

...

We'll

...

leverage

...

this

...

in

...

the

...

get method

...

to

...

make

...

sure

...

normal

...

users

...

can't

...

access

...

our

...

admin

...

console:

...

Code Block
from google.appengine.api import users

class AdminServer(webapp.RequestHandler):
{panel}   """Handles requests to /admin URLs and delegates to the Admin class."""
{panel}

  def get(self):
{panel}     """Ensure that the user is an admin."""
    if not users.GetCurrentUser():
      loginUrl = users.CreateLoginURL(self.request.uri)
      self.response.out.write('<a href="%s">Login</a>' % loginUrl)
      return
{panel}  
{panel}
    if not users.IsCurrentUserAdmin():
      self.response.out.write('You must be an admin to view this page.')
      return
{panel}

 {panel}
    self._handleRequest()
{panel}
 
def _handleRequest(self):
{panel}
  self.response.out.write('Welcome to the admin web interface')
{panel}
</source>

Browse to the admin console again and 

Browse to the admin console again and you'll

...

see

...

a

...

"Login"

...

link.

...

If

...

you

...

login

...

as

...

a

...

user

...

that's

...

not

...

an

...

administrator

...

of

...

the

...

Google

...

App

...

Engine

...

app,

...

you'll

...

see

...

a

...

message

...

stating

...

that

...

"You

...

must

...

be

...

an

...

admin

...

to

...

view

...

this

...

page."

...

Invoking

...

the

...

Admin

...

class

...

Now

...

we

...

can

...

add

...

some

...

code

...

to

...

invoke

...

the

...

Admin class

...

by

...

implementing

...

the

...

_handleRequest method from the previous code snippet. The following method searches the request URL for a parameter called 'action' and, based on this value, either initializes the datastore or lists the gifts and gift transactions.

Code Block
  def _handleRequest(self):
    """Invokes methods from the Admin class based on the 'action' parameter"""
    admin = Admin()
    action = self.request.get('action')
    if action h1. 'init':
      admin.initGifts()
      admin.initGiftTransactions()
      msg = "Gifts have been initialized, gift transactions have been cleared."
      self.response.out.write(msg)
    elif action 'list':
      self.response.out.write("Gifts = %s" % admin.getGiftNames())
      self.response.out.write("
{panel}
<br>")
{panel}
      self.response.out.write("Gift Transactions = %s" % admin.getGiftTransactions())
    else:
      html = []
      html.append('<a href="/admin?action=init">Initialize datastore</a>
{panel}
a><br>')
{panel}
      html.append('<a href="/admin?action=list">List all data in datastore</a>')
      self.response.out.write(''.join(html))
{panel}
</source>

Note that if no 

Note that if no 'action'

...

parameter

...

is

...

given

...

(or

...

if

...

the

...

value

...

is

...

not

...

'init'

...

or

...

'list')

...

the

...

handler

...

will

...

display

...

links

...

to

...

initialize

...

the

...

datastore

...

or

...

list

...

the

...

gift

...

data.

...

Creating

...

a

...

simple

...

data

...

API

...

with

...

Google

...

App

...

Engine

...

Requesting

...

Gifts

...

Let's

...

start

...

by

...

creating

...

a

...

request

...

handler

...

that

...

will

...

return

...

the

...

list

...

of

...

gifts

...

in

...

a

...

JSON

...

format.

...

If

...

a

...

request

...

comes

...

in

...

to

...

http://opensocial-gifts-*_username_*.appspot.com/gifts,

...

we

...

should

...

return:

...

Code Block
["a cashew nut", "a peanut", "a hazelnut", "a red pistachio nut"]
</source>

Create

...

a

...

file

...

called

...

api.py to contain the API request handler. Implement the get method to return the list of gifts as a JSON string.

Code Block
# App Engine imports
from google.appengine.ext import webapp

# Third party imports
import json

# OpenSocial Gifts imports
from db_model import Gift, GiftTransaction

class ApiServer(webapp.RequestHandler):
{panel}
  """Handles requests to /gifts URLs and reponds with JSON strings."""
  
  def get(self):
    """Respond with a JSON string representation of the lists of gifts."""
    gifts = []
    for gift in Gift.all():
      item = \{'key' : str(gift.key()),
              'name' : gift.name \}
      gifts.append(item)
    self.response.out.write(json.write(gifts))
{panel}
</source>

*

Note:

...

The ApiServer class uses the python-json

...

package.

...

The

...

json.py file is included in the application directory with the rest of the source code. Thanks, Patrick Dlogan!

Now update the application in gifts.py to forward requests to the '/gifts' path to this new API request handler.

Code Block

import admin
import api

# Map URLs to request handler class
application = webapp.WSGIApplication([('/admin', admin.AdminServer),
{panel}                                       *'''('/gifts', api.ApiServer)*'''],
                                     debug=True)
{panel}
</source>

h2. Requesting GiftTransactions
We also want to expose GiftTransactions through this api, so let's add another value to the <tt>WGSIApplication</tt> constructor in <tt>gifts.py</tt>:

<source lang="javascript">

Requesting GiftTransactions

We also want to expose GiftTransactions through this api, so let's add another value to the WGSIApplication constructor in gifts.py:

Code Block
# Map URLs to request handler class
application = webapp.WSGIApplication([('/admin', admin.AdminServer),
{panel}
                                      ('/gifts', api.ApiServer),
                                      *'''('/giftTransactions', api.ApiServer)*'''],
                                     debug=True)
{panel}
</source>

Now we have two types of GET requests coming into the <tt>ApiServer</tt> and we can differentiate them based on the URL path:

<source lang="javascript">
{panel}

Now we have two types of GET requests coming into the ApiServer and we can differentiate them based on the URL path:

Code Block
  def get(self):
    """Call the appropriate handler based on the path of the HTTP request."""
    if self.request.path.beginsWith('gifts'):
      self._handleGifts()
    elif self.request.path.beginsWith('giftTransactions'):
      self._handleGiftTransactions()
{panel}

  def _handleGifts(self):
{panel}
    gifts = []
    for gift in Gift.all():
      item = \{'key' : str(gift.key()),
              'name' : gift.name \}
      gifts.append(item)
    self.response.out.write(json.write(gifts))
{panel}

  def _handleGiftTransactions(self):
{panel}
    #TODO(you) return a list of GiftTransactions as JSON
{panel}
</source>

In the last 

In the last snippet,

...

the

...

code

...

for

...

returning

...

the

...

list

...

of

...

gifts

...

was

...

moved

...

into

...

the

...

_handleGifts section.

...

Now

...

we

...

need

...

to

...

implement

...

the

...

_handleGiftTransactions method.

...

We

...

should

...

expect

...

requests

...

for

...

two

...

lists

...

of

...

GiftTransactions:

...

a

...

list

...

of

...

gifts

...

a

...

user

...

has

...

sent

...

and

...

a

...

list

...

of

...

gifts

...

a

...

user

...

has

...

received.

...

Let's

...

design

...

the

...

API

...

to

...

accept

...

the

...

sender

...

or

...

receiver

...

ID

...

as

...

a

...

URL

...

parameter

...

and

...

determine

...

the

...

list

...

of

...

GiftTransactions to

...

return

...

based

...

on

...

the

...

values

...

of

...

these

...

parameters.

Code Block
 <source lang="javascript">
{panel}
  def _returnGiftTransactions(self):
    """Return the list of transactions specified by the URL query parameters."""
    sender_id = self.request.get("sender_id")
    receiver_id = self.request.get("receiver_id")
    giftTransactions = self._getGiftTransactions(sender_id, receiver_id)
{panel}

    results = []
{panel}
    for giftTransaction in giftTransactions:
      item = \{ 'sender_id' : giftTransaction.sender_id,
               'receiver_id' : giftTransaction.receiver_id,
               'gift_name' : giftTransaction.gift.name \}
      results.append(item)
    self.response.out.write(json.write(results))
{panel}

  def _getGiftTransactions(self, sender_id, receiver_id):
{panel}
    results = []
    if sender_id:
      results = GiftTransaction.gql('WHERE sender_id=:sender_id', 
                                sender_id=sender_id)
    elif receiver_id:
      results = GiftTransaction.gql('WHERE receiver_id=:receiver_id',
                                receiver_id=receiver_id)
    else:
      results = GiftTransaction.all()
{panel}

    return results;
</source>

h2. Recording GiftTransactions
The last feature we need to add to the data API is the ability to record a new <tt>GiftTransaction</tt>.  These requests will come in as HTTP POST requests to the /giftTransactions path with the sender ID, receiver ID, and gift key included as POST data.  To handle this request, we simply need to implement a <tt>post</tt> method in the <tt>ApiServer</tt> class.

<source lang="javascript">
{panel}
  

Recording GiftTransactions

The last feature we need to add to the data API is the ability to record a new GiftTransaction. These requests will come in as HTTP POST requests to the /giftTransactions path with the sender ID, receiver ID, and gift key included as POST data. To handle this request, we simply need to implement a post method in the ApiServer class.

Code Block

  def post(self):
    """Store a new gift transaction in the datastore based on the POST data."""
    giftTransaction = GiftTransaction()
    giftTransaction.sender_id = self.request.get('sender_id')
    giftTransaction.receiver_id = self.request.get('receiver_id')
    giftTransaction.gift = Gift.get(self.request.get('gift_key')).key()
    giftTransaction.put()
{panel}
</source>

h2. API Reference

API Reference

Here's

...

a

...

summary

...

of

...

the

...

API

...

we

...

just

...

built:

HTTP Method

URL

Description

Example Response

GET

/gifts

Returns the names and keys of all gifts in the datastore as a JSON array.

Code Block
[{"name" : "a peanut",

...

 

...

"key" : "ABC"}, 

...

{"name" : "a cashew"}, 

...

{"key" : "XYZ"}]

...

GET

/giftTransactions?receiver_id=xxxx

...

Returns an array of gift transactions where the receiver is specified by the URL parameter receiver_id.

Code Block
[{"sender_id":"yyyy",

...

 

...

"receiver_id":"xxxx", 

...

"gift_key":"XYZ"},

...

{"sender_id":"zzzz", 

...

"receiver_id":"xxxx", 

...

"gift_key":"ABC"}]

...

GET

/giftTransactions?sender_id=xxxx

...

Returns an array of gift transactions where the sender is specified by the URL parameter sender_id.

Code Block
[{"sender_id":"xxxx",

...

 

...

receiver_id:"yyyy", 

...

gift_key:"XYZ"}, 

...

{"sender_id":"xxxx", 

...

receiver_id:"zzzz", 

...

gift_key:"XYZ"}]

...

HTTP Method

URL

Description

Example POST data

POST

/giftTransactions

Creates a new gift transaction in the datastore based on the sender, receiver, and gift key specified in the POST data.

sender_id=xxxx&

...

receiver_id=yyyy&

...

gift_key=XYZ

...

Serving static files with Google App Engine

You can configure your app to serve up static content by editing the app.yaml file. Just specify the URL of the static directory and the name of the directory that will contain static content. Here's a copy of the app.yaml file with these changes bolded:

Code Block

application: opensocial-gifts-*'''''_username_*'''''
version: 1
runtime: python
api_version: 1

handlers:

'''- url: /static
{panel}
  static_dir: static'''
{panel}

- url: /.*
{panel}
  script: gifts.py
{panel}
</source>

*

Note:

...

Google

...

App

...

Engine

...

caches

...

static

...

files

...

for

...

10

...

minutes

...

by

...

default,

...

but

...

you

...

can

...

control

...

this

...

caching

...

period

...

in

...

the

...

app.yaml file.

...

You'll

...

find

...

the

...

details

...

in

...

the

...

Configuring

...

an

...

App

...

documentation.

In your application directory, create a directory called static. This directory is where we'll keep our OpenSocial app spec. Create a file called gifts.xml in the static directory and add the following content:

Code Block
<?xml version="1.0" encoding="UTF-8"?>;
<Module>;
{panel}
  <ModulePrefs title="Gifts" />;
  <Content type="html">;
    <\![CDATA[
      Hello, Google App Engine\!
    ]]>;
  </Content>;
{panel}
</Module>;
</source>

Browse

...

to

...

http://localhost:8080/static/gifts.

...

xml and

...

verify

...

that

...

you

...

can

...

see

...

your

...

OpenSocial

...

application

...

spec.

...

Communication

...

between

...

OpenSocial

...

and

...

Google

...

App

...

Engine

...

Now

...

that

...

we

...

have

...

some

...

data

...

in

...

the

...

datastore

...

and

...

an

...

API

...

to

...

access

...

it,

...

we

...

can

...

enhance

...

our

...

OpenSocial

...

app

...

by

...

enabling

...

it

...

to

...

communicate

...

with

...

the

...

Google

...

App

...

Engine

...

project.

...

We'll

...

start

...

by

...

requesting

...

the

...

list

...

of

...

gifts

...

and

...

friends

...

and

...

displaying

...

them

...

in

...

drop-down

...

menus.

...

Then

...

we'll

...

request

...

the

...

gift

...

transactions

...

from

...

the

...

data

...

API

...

we

...

built.

...

Finally,

...

we'll

...

let

...

the

...

user

...

actually

...

give

...

a

...

gift

...

by

...

POSTing

...

a

...

new

...

gift

...

transaction

...

to

...

the

...

data

...

API.

...

Publishing

...

your

...

app

...

Up

...

to

...

this

...

point,

...

we've

...

been

...

using

...

the

...

(local)

...

development

...

app

...

server,

...

but

...

in

...

order

...

for

...

an

...

OpenSocial

...

container

...

like

...

orkut

...

or

...

MySpace

...

to

...

access

...

your

...

application

...

spec,

...

the

...

file

...

needs

...

to

...

be

...

hosted

...

publicly.

...

From

...

the

...

google_appengine directory,

...

run

...

./appcfg.py

...

update

...

<your_app_directory>

...

;

...

from

...

the

...

application

...

directory

...

to

...

publish

...

your

...

app.

...

After

...

the

...

upload

...

is

...

complete,

...

make

...

sure

...

you

...

can

...

access

...

the

...

OpenSocial

...

application

...

spec

...

XML

...

file

...

at

...

http://opensocial-gifts-*_username_*/static/gifts.xml

...

from

...

your

...

browser.

...

Requesting

...

friends

...

The

...

following

...

application

...

will

...

request

...

the

...

list

...

of

...

friends

...

from

...

the

...

OpenSocial

...

container

...

when

...

the

...

page

...

loads

...

and

...

display

...

the

...

friends

...

in

...

a

...

drop-down

...

menu.

...

Code Block

<?xml version="1.0" encoding="UTF-8"?>;
<Module>;
{panel}
   <ModulePrefs title="Gifts" >;
    *'''<Require feature="opensocial-0.8"/>*'''
  </ModulePrefs>;
  <Content type="html">;
    <\![CDATA[
    <script>;
{panel}
'''gadgets.util.registerOnLoadHandler(init);

function init() \{ {panel}
  loadFriends();
{panel}
\}

function loadFriends() \{
{panel}
  var req = opensocial.newDataRequest();
{panel}

  var viewerFriendsIdSpec = opensocial.newIdSpec(\{ "userId" : "VIEWER", "groupId" : "FRIENDS" \});
{panel}
  var opt_params = \{\};
  opt_params[opensocial.DataRequest.PeopleRequestFields.MAX] = 100;
  req.add(req.newFetchPeopleRequest(viewerFriendsIdSpec, opt_params), 'viewerFriends');
  req.send(onLoadFriends);
{panel}
\}

function onLoadFriends(data) \{ {panel}
  var viewerFriends = data.get('viewerFriends').getData();
{panel}
 
{panel}
  html = new Array();
  html.push('<select id="person">;');
  viewerFriends.each(function(person) \{
    html.push('<option value="' + person.getId() + '">;' + person.getDisplayName() + "</option>;");
  \});
  html.push('</select>;');
  document.getElementById('friends').innerHTML = html.join(''); 
{panel} \}
{panel}
      </script>;
      <span id="friends"></span>;'''
    ]]>;
  </Content>;
{panel}
</Module>;
</source>

The <tt>loadFriends</tt> method sends a <tt>DataRequest</tt> to the OpenSocial container and specifies the <tt>onLoadFriends</tt> method as a callback function.  The <tt>onLoadFriends</tt> method receives a <tt>DataResponse</tt> object that contains an <tt>opensocial.Collection</tt> of <tt>opensocial.Person</tt> objects which represent the user's friends.  The name of each of these friends is then added as an option in the drop-down menu.  The drop-down menu, a <tt><select>;</tt> element, is then inserted into the 'friends' span.

h2. Requesting gifts
Next, let's add a drop-down menu for the list of gifts.  We'll request the gift data, format it into a drop-down menu, and add it to the HTML in a 'gifts' span.  First, add the 'gifts' span at the end of the application spec:

<source lang="javascript">
<

The loadFriends method sends a DataRequest to the OpenSocial container and specifies the onLoadFriends method as a callback function. The onLoadFriends method receives a DataResponse object that contains an opensocial.Collection of opensocial.Person objects which represent the user's friends. The name of each of these friends is then added as an option in the drop-down menu. The drop-down menu, a <select>; element, is then inserted into the 'friends' span.

Requesting gifts

Next, let's add a drop-down menu for the list of gifts. We'll request the gift data, format it into a drop-down menu, and add it to the HTML in a 'gifts' span. First, add the 'gifts' span at the end of the application spec:

Code Block

<?xml version="1.0" encoding="UTF-8"?>;
<Module>;
{panel}
  <ModulePrefs title="Gifts" >;
    <Require feature="opensocial-0.8"/>
  </ModulePrefs>;
  <Content type="html">;
    <\![CDATA[
      <script>;
        <-- all the JavaScript -->;
      </script>;
      *'''Give <span id="gifts">;</span>; to <span id="friends">;</span>;.*'''
    ]]>;
  </Content>;
{panel}
</Module>;
</source>

Next,

...

create

...

a

...

loadGifts method

...

and

...

invoke

...

it

...

when

...

the

...

page

...

loads.

...

Code Block
function init() \{ {panel}
  loadFriends();
  *'''loadGifts();*'''
{panel}
\}

'''function loadGifts() \{ {panel}
  var params = \{\};
  params[gadgets.io.RequestParameters.CONTENT_TYPE] = gadgets.io.ContentType.JSON;
  var url = 'http://opensocial-gifts-*_username_*.appspot.com/gifts';
{panel}
   gadgets.io.makeRequest(url, onLoadGifts, params);
\}'''
</source>

The <tt>loadGifts</tt> method uses the <tt>gadgets

The loadGifts method uses the gadgets.io.

...

MakeRequest method

...

to

...

send

...

a

...

request

...

to

...

the

...

JSON

...

data

...

API

...

for

...

the

...

array

...

of

...

available

...

gifts.

...

Once

...

we

...

get

...

the

...

response,

...

the

...

callback

...

method,

...

onLoadGifts,

...

will

...

display

...

the

...

gifts

...

in

...

a

...

drop-down

...

menu.

...

Code Block
function onLoadGifts(response) \{ {panel}
  var gifts = response.data;
  var html = new Array();
  html.push('<select id="nut">;');
  for (var i = 0; i < gifts.length; i++) \{
    html.push('<option value="' + gifts[i].key + '">;' + gifts[i].name + '</option>;');
  \}
  html.push('</select>;');
  document.getElementById('gifts').innerHTML = html.join('');
{panel}
\}
</source>

h2. Requesting gift transactions
Next we want to display the gifts the user has given and received.  Since the data API only returns user IDs, we'll need to build an object to map the IDs to the names of our friends (so the app can display names instead of user IDs).  We can build this object at the same time that we build the drop-down menu of the user's friends, then pass it into a new method called <tt>loadGiftTransactions</tt> where we'll actually make the requests to the data API running on the Google App Engine project.  To do this, modify the <tt>onLoadFriends</tt> method so it looks like this:

<source lang="javascript">

Requesting gift transactions

Next we want to display the gifts the user has given and received. Since the data API only returns user IDs, we'll need to build an object to map the IDs to the names of our friends (so the app can display names instead of user IDs). We can build this object at the same time that we build the drop-down menu of the user's friends, then pass it into a new method called loadGiftTransactions where we'll actually make the requests to the data API running on the Google App Engine project. To do this, modify the onLoadFriends method so it looks like this:

Code Block
function onLoadFriends(data) \{ {panel}
  var viewer = data.get('viewer').getData();
  var viewerFriends = data.get('viewerFriends').getData();
  *'''var friends = new Array();*
{panel}'''
 
 {panel}
  html = new Array();
  html.push('<select id="person">;');
  viewerFriends.each(function(person) \{
    html.push('<option value="' + person.getId() + '">;' + person.getDisplayName() + "</option>;");
    *'''friends[person.getId()] = person.getDisplayName();*'''
  \});
  html.push('</select>;');
  document.getElementById('friends').innerHTML = html.join('');  {panel}

  *'''loadGiftTransactions(viewer, friends);*'''
\}
</source>  
{panel}
  
{panel}
In the <tt>loadGiftTransactions</tt> method, }

In the loadGiftTransactions method, we'll

...

construct

...

the

...

appropriate

...

URLs

...

to

...

fetch

...

this

...

data

...

from

...

the

...

data

...

API

...

and

...

use

...

makeRequest to

...

send

...

the

...

requests

...

for

...

gift

...

transaction

...

data.

...

Each

...

call

...

to

...

makeRequest specifies

...

a

...

callback

...

method,

...

which

...

is

...

actually

...

a

...

closure

...

so

...

that

...

we

...

can

...

use

...

the

...

friends object

...

built

...

in

...

the

...

onLoadFriends method

...

when

...

processing

...

the

...

results

...

of

...

the

...

data

...

request.

...

Here

...

is

...

the

...

implementation

...

for

...

the

...

loadGiftTransactions method

...

and

...

the

...

two

...

callbacks:

Code Block
function <source lang="javascript">
function loadGiftTransactions(viewer, friends) \{
{panel}
loadGiftTransactions(viewer, friends) {
  // Get the gift transactions where the VIEWER is the sender
  var url = 'http://opensocial-gifts-*'''''_username_*'''''/giftTransactions?sender_id=' + viewer.getId();
  gadgets.io.makeRequest(url, onLoadGiftsGivenClosure(friends));
{panel}

  // Get the gift transactions where the VIEWER is the receiver
{panel}
  var url = 'http://opensocial-gifts-*'''''_username_*'''''/giftTransactions?receiver_id=' + viewer.getId();
  gadgets.io.makeRequest(url, onLoadGiftsReceivedClosure(friends));
{panel}
\}

function onLoadGiftsGivenClosure(friends) \{
{panel}   return function(response) \{
    var giftTransactions = gadgets.json.parse(response.data);   
    var html = new Array();
    html.push('You have given:');
    html.push('<ul>');
    for (var i=0; i<giftTransactions.length; i++) \{
      html.push('<li>' + friends[giftTransactions(i].receiver_id] + ' received ');
      html.push(giftTransactions[i].gift_name + '</li>');
    \}
    html.push('</ul>');
    document.getElementById('given').innerHTML = html.join('');
{panel}

    gadgets.window.adjustHeight();
{panel}

 \} {panel}
\}

function onLoadGiftsReceivedClosure(friends) \{
{panel}
  return function(response) \{
    var giftTransactions = gadgets.json.parse(response.data);   
    var html = new Array();
    html.push('You have received:<ul>');
    for (var i=0; i<giftTransactions.length; i++) \{
      html.push('<li>' + giftTransactions[i].gift_name + ' from ');
      html.push(friends[giftTransactions(i].sender_id] + '</li>');
    \}
    html.push('</ul>');
    document.getElementById('received').innerHTML = html.join('');
{panel}
     gadgets.window.adjustHeight();
{panel}

 \} {panel}
\}
</source>

Since

...

displaying

...

these

...

gift

...

transactions

...

may

...

take

...

up

...

more

...

height

...

than

...

the

...

container

...

provides

...

by

...

default,

...

these

...

callbacks

...

use

...

gadgets.window.

...

adjustHeight to

...

resize

...

the

...

app

...

after

...

inserting

...

the

...

data

...

into

...

the

...

page.

...

In

...

order

...

to

...

use

...

this

...

new

...

method,

...

you

...

need

...

to

...

include

...

<Require feature="dynamic-height"/

...

> in

...

the

...

<ModulePrefs> element

...

of

...

the

...

application

...

spec.

...

Finally,

...

add

...

the

...

'given'

...

and

...

'received'

...

<div> elements to the HTML section of the application spec:

Code Block
<?xml version="1.0" encoding="UTF-8"?>
<Module>
{panel}
  <ModulePrefs title="Gifts" >
    <Require feature="opensocial-0.8"/>
    *<Require feature="dynamic-height"/>*
  </ModulePrefs>
  <Content type="html">
    <\![CDATA[
      <script>
        <-- all the JavaScript -->
      </script>
      Give <span id="gifts"></span> to <span id="friends"></span>.
      '''<div id="given"></div>
      <div id="received"</div>'''
    ]]>
  </Content>
{panel}
</Module>
</source>

h2. Recording new gift transactions

The last piece for functionality to add is the ability for a user to actually give a gift to their friend.  To do this, we'll add a "Give\!" link to the HTML that will invoke a <tt>giveGift</tt> JavaScript function when clicked.

<source lang="javascript">

Recording new gift transactions

The last piece for functionality to add is the ability for a user to actually give a gift to their friend. To do this, we'll add a "Give!" link to the HTML that will invoke a giveGift JavaScript function when clicked.

Code Block

<?xml version="1.0" encoding="UTF-8"?>
<Module>
{panel}   <ModulePrefs title="Gifts" >
    <Require feature="opensocial-0.8"/>
    <Require feature="dynamic-height"/>
  </ModulePrefs>
  <Content type="html">
    <\![CDATA[
      <script>
        <-- all the JavaScript -->
      </script>
      Give <span id="gifts"></span> to <span id="friends"></span>.
      *'''<a href="javascript:void(0);" onclick='giveGift();'>Give\!</a>*  '''    
      <div id="given"></div>
      <div id="received"</div>
    ]]>
  </Content>
{panel}
</Module>
</source>

*

Note:

...

When

...

using

...

links

...

to

...

invoke

...

JavaScript

...

functions,

...

always

...

use

...

href="javascript:void(0);"

...

.

...

Using

...

href="#"

...

causes

...

unexpected

...

results

...

in

...

the

...

container.

...

The giftGive function just requests the VIEWER from the container and sends the receiver and gift key data to the callback function for that request. In the callback, we make a POST request (using makeRequest) to the data API that contains the sender and receiver IDs and the key of the gift given as post data.

Code Block
function giveGift() \{ {panel}
  var gift_key = document.getElementById('nut').value;
  var receiver_id = document.getElementById('person').value;
{panel}

 {panel}
  var req = opensocial.newDataRequest();
  req.add(req.newFetchPersonRequest(opensocial.IdSpec.PersonId.VIEWER), 'viewer');
  req.send(postGiftTransactionClosure(receiver_id, gift_key));
{panel}
\}

function postGiftTransactionClosure(receiver_id, gift_key) \{
{panel}
  return function(response) \{
    var sender_id = response.get('viewer').getData().getId();
    var params = \{\};
    params[gadgets.io.RequestParameters.METHOD] = gadgets.io.MethodType.POST;
    post_data = gadgets.io.encodeValues(\{
        'sender_id' : sender_id,
        'receiver_id' : receiver_id,
        'gift_key' : gift_key \});
    params[gadgets.io.RequestParameters.POST_DATA] = post_data;
    var url = http://opensocial-gifts-*''''''_username_*'''''/giftTransactions';
{panel}
     gadgets.io.makeRequest(url, loadFriends, params);
{panel}
  \}
{panel}
\}
</source>

Notice that the callback function for this <tt>makeRequest</tt> call is <tt>loadFriends</tt>.  This will basically redraw the app after the gift transaction is processed.

h2. Sending and verifying signed requests
You don't want just anybody to be able to access your application data through the data API.  OpenSocial containers can add digital signatures to the requests that go out to your servers, or in this case, Google App Engine.

To send a signed request from your OpenSocial app, just change the parameters sent to <tt>makeRequest</tt> as follows:
<source lang="javascript">
function postGiftTransactionClosure(receiver}

Notice that the callback function for this makeRequest call is loadFriends. This will basically redraw the app after the gift transaction is processed.

Sending and verifying signed requests

You don't want just anybody to be able to access your application data through the data API. OpenSocial containers can add digital signatures to the requests that go out to your servers, or in this case, Google App Engine.

To send a signed request from your OpenSocial app, just change the parameters sent to makeRequest as follows:

Code Block

function postGiftTransactionClosure(receiver_id, gift_key) \{ {panel}
  return function(response) \{
    var sender_id = response.get('viewer').getData().getId();
    var params = \{\};
    *'''''params[gadgets.io.RequestParameters.AUTHORIZATION = gadgets.io.AuthorizationType.SIGNED;*'''''
    params[gadgets.io.RequestParameters.METHOD] = gadgets.io.MethodType.POST;
    post_data = gadgets.io.encodeValues(\{
        'sender_id' : sender_id,
        'receiver_id' : receiver_id,
        'gift_key' : gift_key \});
    params[gadgets.io.RequestParameters.POST_DATA] = post_data;
    var url = 'http://opensocial-gifts-*'''''_username_*'''''/giftTransactions';
{panel}
     gadgets.io.makeRequest(url, loadFriends, params);
{panel}
  \}
{panel}
\}
</source>

Using signed requests is most important when 

Using signed requests is most important when you're

...

executing

...

actions

...

on

...

the

...

user's

...

behalf

...

since

...

you

...

don't

...

want

...

a

...

malicious

...

user

...

performing

...

actions

...

for

...

a

...

legitimate

...

user.

...

For

...

example,

...

a

...

malicious

...

user

...

could

...

send

...

POST

...

requests

...

to

...

the

...

/giftTransactions URL of our data API and include any sender ID, receiver ID, or gift key. By signing your requests, you can protect your data from unauthorized access - if a request is forged, you can reply with an error message or nothing at all.

You will need to add code to the api.py class to verify the signature received from the container. We can implement an _isValidSignature() method and call it before processing GET or POST requests:

Code Block

'''def _isValidSignature(self):
{panel}
  return False'''
{panel}

def get(self):
{panel}
  """Respond with a JSON string representation of the lists of gifts."""
  '''if not self._isValidSignature():
    self.response.out.write(json.write(\{\}))
    return'''
{panel}
   if self.request.path.startswith('/gifts'):
{panel}
    self._returnGifts()
  elif self.request.path.startswith('/giftTransactions'):
    self._returnGiftTransactions()
{panel}

def post(self):
{panel}   """Store a new gift transaction in the datastore based on the POST data."""
  '''if not self._isValidSignature():
    return'''
{panel}


  giftTransaction = GiftTransaction()
{panel}
  giftTransaction.sender_id = self.request.get('sender_id')
  giftTransaction.receiver_id = self.request.get('receiver_id')
  giftTransaction.gift = Gift.get(self.request.get('gift_key')).key()
  giftTransaction.put()
{panel}
</source>

OpenSocial uses [

OpenSocial uses OAuth's

...

method

...

for

...

signing

...

requests

...

and

...

containers

...

may

...

use

...

the

...

HMAC-SHA1

...

or

...

RSA-SHA1

...

algorithms.

...

The

...

following

...

sample

...

code

...

demonstrates

...

the

...

RSA-SHA1

...

algorithm

...

and

...

assumes

...

the

...

container

...

is

...

orkut.

...

Orkut's

...

public

...

key

...

is

...

available

...

in

...

an

...

x509

...

certificate

...

,

...

which

...

has

...

been

...

parsed,

...

converted

...

to

...

hex

...

value,

...

and

...

hard-coded

...

in

...

the

...

public_key_

...

str variable.

Code Block
import hashlib
import urllib

import oauth    
from Crypto.PublicKey import RSA
from Crypto.Util import number

def _isValidSignature(self):

{panel}
  # Construct a RSA.pubkey object
  exponent = 65537
  public_key_str = """0x\
{panel}
00b1e057678343866db89d7dec2518\
99261bf2f5e0d95f5d868f81d600c9\
a101c9e6da20606290228308551ed3\
acf9921421dcd01ef1de35dd3275cd\
4983c7be0be325ce8dfc3af6860f7a\
b0bf32742cd9fb2fcd1cd1756bbc40\
0b743f73acefb45d26694caf4f26b9\
765b9f65665245524de957e8c547c3\
58781fdfb68ec056d1"""
{panel}
  public_key_long = long(public_key_str, 16)
  public_key = RSA.construct((public_key_long, exponent))
      
  # Rebuild the message hash locally
  oauth_request = oauth.OAuthRequest(http_method=self.request.method, 
                                     http_url=self.request.url, 
                                     parameters=self.request.params.mixed())
  message = '&amp;'.join((oauth.escape(oauth_request.get_normalized_http_method()),
                      oauth.escape(oauth_request.get_normalized_http_url()),
                      oauth.escape(oauth_request.get_normalized_parameters()),))
  local_hash = hashlib.sha1(message).digest()
{panel}

  # Apply the public key to the signature from the remote host
{panel}
  sig = urllib.unquote(self.request.params.mixed()["oauth_signature"]).decode('base64')
  remote_hash = public_key.encrypt(sig, '')[0][-20:]
  
  # Verify that the locally-built value matches the value from the remote server.
  return local_hashh1. remote_hash
{panel}
</source>

The <tt>_isValidSignature</tt> method uses two third party modules. [

The _isValidSignature method uses two third party modules. OAuth's

...

Python

...

client

...

library

...

was

...

written

...

by

...

Leah

...

Culver

...

and

...

the

...

RSA

...

code

...

is

...

just

...

a

...

small

...

piece

...

of

...

the

...

pycrypto toolkit. Score one for open source!

In case you didn't cut and paste everything just right, this application is hosted in the opensocial-gifts Google Code project where you can find the OpenSocial application spec, the full implementation of the JSON data API, and all the other files used in this application. Next Steps h1. This tutorial has only scratched the surface of what you can do with Google App Engine and OpenSocial. Try adding some of these features to your app:

  • Display pictures for each gift instead of text (Hint: store the images in your static directory).
  • Expand the admin interface to allow more granular access to the data (e.g. delete or update a single entity rather than resetting everything at once).
  • Use templates to render the admin interface.
  • Include timestamps in gift transactions and only show recent gifts in the app.
  • Allow new gifts to be added through the admin interface.
  • Let users send a custom message with their gift.
  • Create a profile view for the OpenSocial app.

Happy coding! Resources ==

Developer Forums

If you have questions while working through this tutorial, you can ask other developers in one of the following forums:

Reference

Google App Engine

OpenSocial