...
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
...
...
...
.
...
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
...
...
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):
- Google App Engine app
- Database model
- Admin interface
- JSON data API
- 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
...
...
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
...
...
...
...
.
...
If
...
you
...
haven't
...
already,
...
...
...
...
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
...
...
).
...
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
...
...
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
...
...
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
...
...
App
...
Engine
...
web
...
interface
...
Now
...
we'll
...
extend
...
our
...
...
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
...
...
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
...
...
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
...
...
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. |
|
...
|
...
|
...
|
...
|
...
GET | /giftTransactions?receiver_id=xxxx |
...
Returns an array of gift transactions where the receiver is specified by the URL parameter receiver_id. |
|
...
|
...
|
...
|
...
|
...
|
...
|
...
GET | /giftTransactions?sender_id=xxxx |
...
Returns an array of gift transactions where the sender is specified by the URL parameter sender_id. |
|
...
|
...
|
...
|
...
|
...
|
...
|
...
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. |
|
...
|
...
|
...
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:
...
...
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
...
...
...
...
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
...
...
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
...
...
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
...
...
...
...
...
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
...
...
...
,
...
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 = '&'.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
...
...
...
...
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