SampleSyncAdapter sample code.
diff --git a/samples/SampleSyncAdapter/samplesyncadapter_server/app.yaml b/samples/SampleSyncAdapter/samplesyncadapter_server/app.yaml
new file mode 100644
index 0000000..109eff3
--- /dev/null
+++ b/samples/SampleSyncAdapter/samplesyncadapter_server/app.yaml
@@ -0,0 +1,44 @@
+application: samplesyncadapter
+version: 1
+runtime: python
+api_version: 1
+
+handlers:
+- url: /auth
+ script: main.py
+
+- url: /login
+ script: main.py
+
+- url: /fetch_friend_updates
+ script: main.py
+
+- url: /fetch_status
+ script: main.py
+
+- url: /add_user
+ script: dashboard.py
+
+- url: /edit_user
+ script: dashboard.py
+
+- url: /users
+ script: dashboard.py
+
+- url: /delete_friend
+ script: dashboard.py
+
+- url: /edit_user
+ script: dashboard.py
+
+- url: /add_credentials
+ script: dashboard.py
+
+- url: /user_credentials
+ script: dashboard.py
+
+- url: /add_friend
+ script: dashboard.py
+
+- url: /user_friends
+ script: dashboard.py
diff --git a/samples/SampleSyncAdapter/samplesyncadapter_server/dashboard.py b/samples/SampleSyncAdapter/samplesyncadapter_server/dashboard.py
new file mode 100644
index 0000000..c986f7e
--- /dev/null
+++ b/samples/SampleSyncAdapter/samplesyncadapter_server/dashboard.py
@@ -0,0 +1,273 @@
+#!/usr/bin/python2.5
+
+# Copyright (C) 2010 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+"""Defines Django forms for inserting/updating/viewing data
+ to/from SampleSyncAdapter datastore."""
+
+import cgi
+import datetime
+import os
+
+from google.appengine.ext import db
+from google.appengine.ext import webapp
+from google.appengine.ext.webapp import template
+from google.appengine.ext.db import djangoforms
+from model import datastore
+
+import wsgiref.handlers
+
+
+class UserForm(djangoforms.ModelForm):
+ """Represents django form for entering user info."""
+
+ class Meta:
+ model = datastore.User
+
+
+class UserInsertPage(webapp.RequestHandler):
+ """Inserts new users. GET presents a blank form. POST processes it."""
+
+ def get(self):
+ self.response.out.write('<html><body>'
+ '<form method="POST" '
+ 'action="/add_user">'
+ '<table>')
+ # This generates our shopping list form and writes it in the response
+ self.response.out.write(UserForm())
+ self.response.out.write('</table>'
+ '<input type="submit">'
+ '</form></body></html>')
+
+ def post(self):
+ data = UserForm(data=self.request.POST)
+ if data.is_valid():
+ # Save the data, and redirect to the view page
+ entity = data.save(commit=False)
+ entity.put()
+ self.redirect('/users')
+ else:
+ # Reprint the form
+ self.response.out.write('<html><body>'
+ '<form method="POST" '
+ 'action="/">'
+ '<table>')
+ self.response.out.write(data)
+ self.response.out.write('</table>'
+ '<input type="submit">'
+ '</form></body></html>')
+
+
+class UserEditPage(webapp.RequestHandler):
+ """Edits users. GET presents a form prefilled with user info
+ from datastore. POST processes it."""
+
+ def get(self):
+ id = int(self.request.get('user'))
+ user = datastore.User.get(db.Key.from_path('User', id))
+ self.response.out.write('<html><body>'
+ '<form method="POST" '
+ 'action="/edit_user">'
+ '<table>')
+ # This generates our shopping list form and writes it in the response
+ self.response.out.write(UserForm(instance=user))
+ self.response.out.write('</table>'
+ '<input type="hidden" name="_id" value="%s">'
+ '<input type="submit">'
+ '</form></body></html>' % id)
+
+ def post(self):
+ id = int(self.request.get('_id'))
+ user = datastore.User.get(db.Key.from_path('User', id))
+ data = UserForm(data=self.request.POST, instance=user)
+ if data.is_valid():
+ # Save the data, and redirect to the view page
+ entity = data.save(commit=False)
+ entity.updated = datetime.datetime.utcnow()
+ entity.put()
+ self.redirect('/users')
+ else:
+ # Reprint the form
+ self.response.out.write('<html><body>'
+ '<form method="POST" '
+ 'action="/edit_user">'
+ '<table>')
+ self.response.out.write(data)
+ self.response.out.write('</table>'
+ '<input type="hidden" name="_id" value="%s">'
+ '<input type="submit">'
+ '</form></body></html>' % id)
+
+
+class UsersListPage(webapp.RequestHandler):
+ """Lists all Users. In addition displays links for editing user info,
+ viewing user's friends and adding new users."""
+
+ def get(self):
+ users = datastore.User.all()
+ template_values = {
+ 'users': users
+ }
+
+ path = os.path.join(os.path.dirname(__file__), 'templates', 'users.html')
+ self.response.out.write(template.render(path, template_values))
+
+
+class UserCredentialsForm(djangoforms.ModelForm):
+ """Represents django form for entering user's credentials."""
+
+ class Meta:
+ model = datastore.UserCredentials
+
+
+class UserCredentialsInsertPage(webapp.RequestHandler):
+ """Inserts user credentials. GET shows a blank form, POST processes it."""
+
+ def get(self):
+ self.response.out.write('<html><body>'
+ '<form method="POST" '
+ 'action="/add_credentials">'
+ '<table>')
+ # This generates our shopping list form and writes it in the response
+ self.response.out.write(UserCredentialsForm())
+ self.response.out.write('</table>'
+ '<input type="submit">'
+ '</form></body></html>')
+
+ def post(self):
+ data = UserCredentialsForm(data=self.request.POST)
+ if data.is_valid():
+ # Save the data, and redirect to the view page
+ entity = data.save(commit=False)
+ entity.put()
+ self.redirect('/users')
+ else:
+ # Reprint the form
+ self.response.out.write('<html><body>'
+ '<form method="POST" '
+ 'action="/add_credentials">'
+ '<table>')
+ self.response.out.write(data)
+ self.response.out.write('</table>'
+ '<input type="submit">'
+ '</form></body></html>')
+
+
+class UserFriendsForm(djangoforms.ModelForm):
+ """Represents django form for entering user's friends."""
+
+ class Meta:
+ model = datastore.UserFriends
+ exclude = ['deleted', 'username']
+
+
+class UserFriendsInsertPage(webapp.RequestHandler):
+ """Inserts user's new friends. GET shows a blank form, POST processes it."""
+
+ def get(self):
+ user = self.request.get('user')
+ self.response.out.write('<html><body>'
+ '<form method="POST" '
+ 'action="/add_friend">'
+ '<table>')
+ # This generates our shopping list form and writes it in the response
+ self.response.out.write(UserFriendsForm())
+ self.response.out.write('</table>'
+ '<input type = hidden name = "user" value = "%s">'
+ '<input type="submit">'
+ '</form></body></html>' % user)
+
+ def post(self):
+ data = UserFriendsForm(data=self.request.POST)
+ if data.is_valid():
+ user = self.request.get('user')
+ # Save the data, and redirect to the view page
+ entity = data.save(commit=False)
+ entity.username = user
+ query = datastore.UserFriends.all()
+ query.filter('username = ', user)
+ query.filter('friend_handle = ', entity.friend_handle)
+ result = query.get()
+ if result:
+ result.deleted = False
+ result.updated = datetime.datetime.utcnow()
+ result.put()
+ else:
+ entity.deleted = False
+ entity.put()
+ self.redirect('/user_friends?user=' + user)
+ else:
+ # Reprint the form
+ self.response.out.write('<html><body>'
+ '<form method="POST" '
+ 'action="/add_friend">'
+ '<table>')
+ self.response.out.write(data)
+ self.response.out.write('</table>'
+ '<input type="submit">'
+ '</form></body></html>')
+
+
+class UserFriendsListPage(webapp.RequestHandler):
+ """Lists all friends for a user. In addition displays links for removing
+ friends and adding new friends."""
+
+ def get(self):
+ user = self.request.get('user')
+ query = datastore.UserFriends.all()
+ query.filter('deleted = ', False)
+ query.filter('username = ', user)
+ friends = query.fetch(50)
+ template_values = {
+ 'friends': friends,
+ 'user': user
+ }
+ path = os.path.join(os.path.dirname(__file__),
+ 'templates', 'view_friends.html')
+ self.response.out.write(template.render(path, template_values))
+
+
+class DeleteFriendPage(webapp.RequestHandler):
+ """Processes delete friend request."""
+
+ def get(self):
+ user = self.request.get('user')
+ friend = self.request.get('friend')
+ query = datastore.UserFriends.all()
+ query.filter('username =', user)
+ query.filter('friend_handle =', friend)
+ result = query.get()
+ result.deleted = True
+ result.updated = datetime.datetime.utcnow()
+ result.put()
+
+ self.redirect('/user_friends?user=' + user)
+
+
+def main():
+ application = webapp.WSGIApplication(
+ [('/add_user', UserInsertPage),
+ ('/users', UsersListPage),
+ ('/add_credentials', UserCredentialsInsertPage),
+ ('/add_friend', UserFriendsInsertPage),
+ ('/user_friends', UserFriendsListPage),
+ ('/delete_friend', DeleteFriendPage),
+ ('/edit_user', UserEditPage)
+ ],
+ debug=True)
+ wsgiref.handlers.CGIHandler().run(application)
+
+if __name__ == '__main__':
+ main()
\ No newline at end of file
diff --git a/samples/SampleSyncAdapter/samplesyncadapter_server/index.yaml b/samples/SampleSyncAdapter/samplesyncadapter_server/index.yaml
new file mode 100644
index 0000000..83ceea0
--- /dev/null
+++ b/samples/SampleSyncAdapter/samplesyncadapter_server/index.yaml
@@ -0,0 +1,14 @@
+indexes:
+
+# This index.yaml is automatically updated whenever the dev_appserver
+# detects that a new type of query is run. If you want to manage the
+# index.yaml file manually, remove the above marker line (the line
+# saying "# AUTOGENERATED"). If you want to manage some indexes
+# manually, move them above the marker line. The index.yaml file is
+# automatically uploaded to the admin console when you next deploy
+# your application using appcfg.py.
+
+- kind: UserFriends
+ properties:
+ - name: username
+ - name: updated
\ No newline at end of file
diff --git a/samples/SampleSyncAdapter/samplesyncadapter_server/main.py b/samples/SampleSyncAdapter/samplesyncadapter_server/main.py
new file mode 100644
index 0000000..2d7c5c7
--- /dev/null
+++ b/samples/SampleSyncAdapter/samplesyncadapter_server/main.py
@@ -0,0 +1,173 @@
+#!/usr/bin/python2.5
+
+# Copyright (C) 2010 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+"""Handlers for Sample SyncAdapter services.
+
+Contains several RequestHandler subclasses used to handle post operations.
+This script is designed to be run directly as a WSGI application.
+
+ Authenticate: Handles user requests for authentication.
+ FetchFriends: Handles user requests for friend list.
+ FriendData: Stores information about user's friends.
+"""
+
+import cgi
+from datetime import datetime
+from django.utils import simplejson
+from google.appengine.api import users
+from google.appengine.ext import db
+from google.appengine.ext import webapp
+from model import datastore
+import wsgiref.handlers
+
+
+class Authenticate(webapp.RequestHandler):
+ """Handles requests for login and authentication.
+
+ UpdateHandler only accepts post events. It expects each
+ request to include username and password fields. It returns authtoken
+ after successful authentication and "invalid credentials" error otherwise.
+ """
+
+ def post(self):
+ self.username = self.request.get('username')
+ self.password = self.request.get('password')
+ password = datastore.UserCredentials.get(self.username)
+ if password == self.password:
+ self.response.set_status(200, 'OK')
+ # return the password as AuthToken
+ self.response.out.write(password)
+ else:
+ self.response.set_status(401, 'Invalid Credentials')
+
+
+class FetchFriends(webapp.RequestHandler):
+ """Handles requests for fetching user's friendlist.
+
+ UpdateHandler only accepts post events. It expects each
+ request to include username and authtoken. If the authtoken is valid
+ it returns user's friend info in JSON format.It uses helper
+ class FriendData to fetch user's friendlist.
+ """
+
+ def post(self):
+ self.username = self.request.get('username')
+ self.password = self.request.get('password')
+ self.timestamp = None
+ timestamp = self.request.get('timestamp')
+ if timestamp:
+ self.timestamp = datetime.strptime(timestamp, '%Y/%m/%d %H:%M')
+ password = datastore.UserCredentials.get(self.username)
+ if password == self.password:
+ self.friend_list = []
+ friends = datastore.UserFriends.get_friends(self.username)
+ if friends:
+ for friend in friends:
+ friend_handle = getattr(friend, 'friend_handle')
+
+ if self.timestamp is None or getattr(friend, 'updated') > self.timestamp:
+ if (getattr(friend, 'deleted')) == True:
+ friend = {}
+ friend['u'] = friend_handle
+ friend['d'] = 'true'
+ friend['i'] = str(datastore.User.get_user_id(friend_handle))
+ self.friend_list.append(friend)
+ else:
+ FriendsData(self.friend_list, friend_handle)
+ else:
+ if datastore.User.get_user_last_updated(friend_handle) > self.timestamp:
+ FriendsData(self.friend_list, friend_handle)
+ self.response.set_status(200)
+ self.response.out.write(toJSON(self.friend_list))
+ else:
+ self.response.set_status(401, 'Invalid Credentials')
+
+class FetchStatus(webapp.RequestHandler):
+ """Handles requests fetching friend statuses.
+
+ UpdateHandler only accepts post events. It expects each
+ request to include username and authtoken. If the authtoken is valid
+ it returns status info in JSON format.
+ """
+
+ def post(self):
+ self.username = self.request.get('username')
+ self.password = self.request.get('password')
+ password = datastore.UserCredentials.get(self.username)
+ if password == self.password:
+ self.status_list = []
+ friends = datastore.UserFriends.get_friends(self.username)
+ if friends:
+ for friend in friends:
+ friend_handle = getattr(friend, 'friend_handle')
+ status_text = datastore.User.get_user_status(friend_handle)
+ user_id = datastore.User.get_user_id(friend_handle)
+ status = {}
+ status['i'] = str(user_id)
+ status['s'] = status_text
+ self.status_list.append(status)
+ self.response.set_status(200)
+ self.response.out.write(toJSON(self.status_list))
+ else:
+ self.response.set_status(401, 'Invalid Credentials')
+
+ def toJSON(self):
+ """Dumps the data represented by the object to JSON for wire transfer."""
+ return simplejson.dumps(self.friend_list)
+
+
+def toJSON(object):
+ """Dumps the data represented by the object to JSON for wire transfer."""
+ return simplejson.dumps(object)
+
+class FriendsData(object):
+ """Holds data for user's friends.
+
+ This class knows how to serialize itself to JSON.
+ """
+ __FIELD_MAP = {
+ 'handle': 'u',
+ 'firstname': 'f',
+ 'lastname': 'l',
+ 'status': 's',
+ 'phone_home': 'h',
+ 'phone_office': 'o',
+ 'phone_mobile': 'm',
+ 'email': 'e',
+ }
+
+ def __init__(self, friend_list, username):
+ obj = datastore.User.get_user_info(username)
+ friend = {}
+ for obj_name, json_name in self.__FIELD_MAP.items():
+ if hasattr(obj, obj_name):
+ friend[json_name] = str(getattr(obj, obj_name))
+ friend['i'] = str(obj.key().id())
+ friend_list.append(friend)
+
+
+def main():
+ application = webapp.WSGIApplication(
+ [('/auth', Authenticate),
+ ('/login', Authenticate),
+ ('/fetch_friend_updates', FetchFriends),
+ ('/fetch_status', FetchStatus),
+ ],
+ debug=True)
+ wsgiref.handlers.CGIHandler().run(application)
+
+if __name__ == "__main__":
+ main()
\ No newline at end of file
diff --git a/samples/SampleSyncAdapter/samplesyncadapter_server/model/__init__.py b/samples/SampleSyncAdapter/samplesyncadapter_server/model/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/samples/SampleSyncAdapter/samplesyncadapter_server/model/__init__.py
diff --git a/samples/SampleSyncAdapter/samplesyncadapter_server/model/datastore.py b/samples/SampleSyncAdapter/samplesyncadapter_server/model/datastore.py
new file mode 100644
index 0000000..71bd18a
--- /dev/null
+++ b/samples/SampleSyncAdapter/samplesyncadapter_server/model/datastore.py
@@ -0,0 +1,93 @@
+#!/usr/bin/python2.5
+
+# Copyright (C) 2010 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+"""Represents user's contact information, friends and credentials."""
+
+from google.appengine.ext import db
+
+
+class User(db.Model):
+ """Data model class to hold user objects."""
+
+ handle = db.StringProperty(required=True)
+ firstname = db.TextProperty()
+ lastname = db.TextProperty()
+ status = db.TextProperty()
+ phone_home = db.PhoneNumberProperty()
+ phone_office = db.PhoneNumberProperty()
+ phone_mobile = db.PhoneNumberProperty()
+ email = db.EmailProperty()
+ deleted = db.BooleanProperty()
+ updated = db.DateTimeProperty(auto_now_add=True)
+
+ @classmethod
+ def get_user_info(cls, username):
+ if username not in (None, ''):
+ query = cls.gql('WHERE handle = :1', username)
+ return query.get()
+ return None
+
+ @classmethod
+ def get_user_last_updated(cls, username):
+ if username not in (None, ''):
+ query = cls.gql('WHERE handle = :1', username)
+ return query.get().updated
+ return None
+
+ @classmethod
+ def get_user_id(cls, username):
+ if username not in (None, ''):
+ query = cls.gql('WHERE handle = :1', username)
+ return query.get().key().id()
+ return None
+
+ @classmethod
+ def get_user_status(cls, username):
+ if username not in (None, ''):
+ query = cls.gql('WHERE handle = :1', username)
+ return query.get().status
+ return None
+
+
+class UserCredentials(db.Model):
+ """Data model class to hold credentials for a Voiper user."""
+
+ username = db.StringProperty(required=True)
+ password = db.StringProperty()
+
+ @classmethod
+ def get(cls, username):
+ if username not in (None, ''):
+ query = cls.gql('WHERE username = :1', username)
+ return query.get().password
+ return None
+
+
+class UserFriends(db.Model):
+ """Data model class to hold user's friendlist info."""
+
+ username = db.StringProperty()
+ friend_handle = db.StringProperty(required=True)
+ updated = db.DateTimeProperty(auto_now_add=True)
+ deleted = db.BooleanProperty()
+
+ @classmethod
+ def get_friends(cls, username):
+ if username not in (None, ''):
+ query = cls.gql('WHERE username = :1', username)
+ friends = query.fetch(50)
+ return friends
+ return None
\ No newline at end of file
diff --git a/samples/SampleSyncAdapter/samplesyncadapter_server/templates/users.html b/samples/SampleSyncAdapter/samplesyncadapter_server/templates/users.html
new file mode 100644
index 0000000..044c352
--- /dev/null
+++ b/samples/SampleSyncAdapter/samplesyncadapter_server/templates/users.html
@@ -0,0 +1,19 @@
+<html>
+<body>
+<h1> Sample Sync Adapter </h1>
+
+<p>
+<h3> List of Users </h3>
+<table>
+
+{% for user in users %}
+ <tr><td>
+ <a
+ href="/edit_user?user={{ user.key.id}}">{{ user.firstname }} {{ user.lastname }} </a>
+ </td><td> <a href="/user_friends?user={{ user.handle }}">Friends</a> </td>
+ </tr>
+{% endfor %}
+</table>
+</p>
+
+<a href = "/add_user"> Insert More </a>
\ No newline at end of file
diff --git a/samples/SampleSyncAdapter/samplesyncadapter_server/templates/view_friends.html b/samples/SampleSyncAdapter/samplesyncadapter_server/templates/view_friends.html
new file mode 100644
index 0000000..d4ef892
--- /dev/null
+++ b/samples/SampleSyncAdapter/samplesyncadapter_server/templates/view_friends.html
@@ -0,0 +1,17 @@
+<html>
+<body>
+<h1> Sample Sync Adapter </h1>
+
+<p>
+
+{{user}}'s friends
+<table>
+{% for friend in friends %}
+ <tr><td>
+ {{ friend.friend_handle }} </td><td> <a href="/delete_friend?user={{ user }}&friend={{friend.friend_handle}}">Remove</a>
+ </td></tr>
+{% endfor %}
+</table>
+</p>
+
+<a href = "/add_friend?user={{user}}"> Add More </a>
\ No newline at end of file