upload android base code part7
This commit is contained in:
parent
4e516ec6ed
commit
841ae54672
25229 changed files with 1709508 additions and 0 deletions
|
@ -0,0 +1,62 @@
|
|||
# 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.
|
||||
|
||||
# Note the app name used to be "samplesyncadapter2", but it's now "samplesyncadapterhr" because
|
||||
# it's been migrated to HR. But it's still accessible under the old URL,
|
||||
# http://samplesyncadapter2.appspot.com/
|
||||
application: samplesyncadapterhr
|
||||
version: 1
|
||||
runtime: python
|
||||
api_version: 1
|
||||
|
||||
handlers:
|
||||
|
||||
#
|
||||
# Define a handler for our static files (css, images, etc)
|
||||
#
|
||||
- url: /static
|
||||
static_dir: static
|
||||
|
||||
#
|
||||
# Route all "web services" requests to the main.py file
|
||||
#
|
||||
- url: /auth
|
||||
script: web_services.py
|
||||
|
||||
- url: /sync
|
||||
script: web_services.py
|
||||
|
||||
- url: /reset_database
|
||||
script: web_services.py
|
||||
|
||||
#
|
||||
# Route all page requests to the dashboard.py file
|
||||
#
|
||||
- url: /
|
||||
script: dashboard.py
|
||||
|
||||
- url: /add_contact
|
||||
script: dashboard.py
|
||||
|
||||
- url: /edit_contact
|
||||
script: dashboard.py
|
||||
|
||||
- url: /delete_contact
|
||||
script: dashboard.py
|
||||
|
||||
- url: /avatar
|
||||
script: dashboard.py
|
||||
|
||||
- url: /edit_avatar
|
||||
script: dashboard.py
|
|
@ -0,0 +1,24 @@
|
|||
# Copyright (C) 2011 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.
|
||||
|
||||
cron:
|
||||
#
|
||||
# Create a weekly cron job that cleans up the SampleSyncAdapter server database.
|
||||
# We remove all existing contacts from the db, and create three initial
|
||||
# contacts: Romeo, Juliet, and Tybalt.
|
||||
#
|
||||
- description: weekly cleanup job
|
||||
url: /reset_database
|
||||
schedule: every sunday 00:00
|
||||
timezone: America/Los_Angeles
|
|
@ -0,0 +1,208 @@
|
|||
#!/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 contact 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
|
||||
from google.appengine.api import images
|
||||
|
||||
import wsgiref.handlers
|
||||
|
||||
class BaseRequestHandler(webapp.RequestHandler):
|
||||
"""
|
||||
Base class for our page-based request handlers that contains
|
||||
some helper functions we use in most pages.
|
||||
"""
|
||||
|
||||
"""
|
||||
Return a form (potentially partially filled-in) to
|
||||
the user.
|
||||
"""
|
||||
def send_form(self, title, action, contactId, handle, content_obj):
|
||||
if (contactId >= 0):
|
||||
idInfo = '<input type="hidden" name="_id" value="%s">'
|
||||
else:
|
||||
idInfo = ''
|
||||
|
||||
template_values = {
|
||||
'title': title,
|
||||
'header': title,
|
||||
'action': action,
|
||||
'contactId': contactId,
|
||||
'handle': handle,
|
||||
'has_contactId': (contactId >= 0),
|
||||
'has_handle': (handle != None),
|
||||
'form_data_rows': str(content_obj)
|
||||
}
|
||||
|
||||
path = os.path.join(os.path.dirname(__file__), 'templates', 'simple_form.html')
|
||||
self.response.out.write(template.render(path, template_values))
|
||||
|
||||
class ContactForm(djangoforms.ModelForm):
|
||||
"""Represents django form for entering contact info."""
|
||||
|
||||
class Meta:
|
||||
model = datastore.Contact
|
||||
|
||||
|
||||
class ContactInsertPage(BaseRequestHandler):
|
||||
"""
|
||||
Processes requests to add a new contact. GET presents an empty
|
||||
contact form for the user to fill in. POST saves the new contact
|
||||
with the POSTed information.
|
||||
"""
|
||||
|
||||
def get(self):
|
||||
self.send_form('Add Contact', '/add_contact', -1, None, ContactForm())
|
||||
|
||||
def post(self):
|
||||
data = ContactForm(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('/')
|
||||
else:
|
||||
# Reprint the form
|
||||
self.send_form('Add Contact', '/add_contact', -1, None, data)
|
||||
|
||||
|
||||
class ContactEditPage(BaseRequestHandler):
|
||||
"""
|
||||
Process requests to edit a contact's information. GET presents a form
|
||||
with the current contact information filled in. POST saves new information
|
||||
into the contact record.
|
||||
"""
|
||||
|
||||
def get(self):
|
||||
id = int(self.request.get('id'))
|
||||
contact = datastore.Contact.get(db.Key.from_path('Contact', id))
|
||||
self.send_form('Edit Contact', '/edit_contact', id, contact.handle,
|
||||
ContactForm(instance=contact))
|
||||
|
||||
def post(self):
|
||||
id = int(self.request.get('id'))
|
||||
contact = datastore.Contact.get(db.Key.from_path('Contact', id))
|
||||
data = ContactForm(data=self.request.POST, instance=contact)
|
||||
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('/')
|
||||
else:
|
||||
# Reprint the form
|
||||
self.send_form('Edit Contact', '/edit_contact', id, contact.handle, data)
|
||||
|
||||
class ContactDeletePage(BaseRequestHandler):
|
||||
"""Processes delete contact request."""
|
||||
|
||||
def get(self):
|
||||
id = int(self.request.get('id'))
|
||||
contact = datastore.Contact.get(db.Key.from_path('Contact', id))
|
||||
contact.deleted = True
|
||||
contact.updated = datetime.datetime.utcnow()
|
||||
contact.put()
|
||||
|
||||
self.redirect('/')
|
||||
|
||||
class AvatarEditPage(webapp.RequestHandler):
|
||||
"""
|
||||
Processes requests to edit contact's avatar. GET is used to fetch
|
||||
a page that displays the contact's current avatar and allows the user
|
||||
to specify a file containing a new avatar image. POST is used to
|
||||
submit the form which will change the contact's avatar.
|
||||
"""
|
||||
|
||||
def get(self):
|
||||
id = int(self.request.get('id'))
|
||||
contact = datastore.Contact.get(db.Key.from_path('Contact', id))
|
||||
template_values = {
|
||||
'avatar': contact.avatar,
|
||||
'contactId': id
|
||||
}
|
||||
|
||||
path = os.path.join(os.path.dirname(__file__), 'templates', 'edit_avatar.html')
|
||||
self.response.out.write(template.render(path, template_values))
|
||||
|
||||
def post(self):
|
||||
id = int(self.request.get('id'))
|
||||
contact = datastore.Contact.get(db.Key.from_path('Contact', id))
|
||||
#avatar = images.resize(self.request.get("avatar"), 128, 128)
|
||||
avatar = self.request.get("avatar")
|
||||
contact.avatar = db.Blob(avatar)
|
||||
contact.updated = datetime.datetime.utcnow()
|
||||
contact.put()
|
||||
self.redirect('/')
|
||||
|
||||
class AvatarViewPage(BaseRequestHandler):
|
||||
"""
|
||||
Processes request to view contact's avatar. This is different from
|
||||
the GET AvatarEditPage request in that this doesn't return a page -
|
||||
it just returns the raw image itself.
|
||||
"""
|
||||
|
||||
def get(self):
|
||||
id = int(self.request.get('id'))
|
||||
contact = datastore.Contact.get(db.Key.from_path('Contact', id))
|
||||
if (contact.avatar):
|
||||
self.response.headers['Content-Type'] = "image/png"
|
||||
self.response.out.write(contact.avatar)
|
||||
else:
|
||||
self.redirect(self.request.host_url + '/static/img/default_avatar.gif')
|
||||
|
||||
class ContactsListPage(webapp.RequestHandler):
|
||||
"""
|
||||
Display a page that lists all the contacts associated with
|
||||
the specifies user account.
|
||||
"""
|
||||
|
||||
def get(self):
|
||||
contacts = datastore.Contact.all()
|
||||
template_values = {
|
||||
'contacts': contacts,
|
||||
'username': 'user'
|
||||
}
|
||||
|
||||
path = os.path.join(os.path.dirname(__file__), 'templates', 'contacts.html')
|
||||
self.response.out.write(template.render(path, template_values))
|
||||
|
||||
|
||||
def main():
|
||||
application = webapp.WSGIApplication(
|
||||
[('/', ContactsListPage),
|
||||
('/add_contact', ContactInsertPage),
|
||||
('/edit_contact', ContactEditPage),
|
||||
('/delete_contact', ContactDeletePage),
|
||||
('/avatar', AvatarViewPage),
|
||||
('/edit_avatar', AvatarEditPage)
|
||||
],
|
||||
debug=True)
|
||||
wsgiref.handlers.CGIHandler().run(application)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
|
@ -0,0 +1,15 @@
|
|||
# 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.
|
||||
|
||||
# AUTOGENERATED
|
|
@ -0,0 +1,64 @@
|
|||
#!/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"""
|
||||
|
||||
from google.appengine.ext import db
|
||||
|
||||
|
||||
class Contact(db.Model):
|
||||
"""Data model class to hold user objects."""
|
||||
|
||||
handle = db.StringProperty(required=True)
|
||||
firstname = db.StringProperty()
|
||||
lastname = db.StringProperty()
|
||||
phone_home = db.PhoneNumberProperty()
|
||||
phone_office = db.PhoneNumberProperty()
|
||||
phone_mobile = db.PhoneNumberProperty()
|
||||
email = db.EmailProperty()
|
||||
status = db.TextProperty()
|
||||
avatar = db.BlobProperty()
|
||||
deleted = db.BooleanProperty()
|
||||
updated = db.DateTimeProperty(auto_now_add=True)
|
||||
|
||||
@classmethod
|
||||
def get_contact_info(cls, username):
|
||||
if username not in (None, ''):
|
||||
query = cls.gql('WHERE handle = :1', username)
|
||||
return query.get()
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def get_contact_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_contact_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_contact_status(cls, username):
|
||||
if username not in (None, ''):
|
||||
query = cls.gql('WHERE handle = :1', username)
|
||||
return query.get().status
|
||||
return None
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
/**
|
||||
* Copyright (C) 2011 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.
|
||||
*/
|
||||
|
||||
html,body {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
padding: 20px;
|
||||
margin: 0;
|
||||
background-color: #fff;
|
||||
font-family: Verdana, Arial, Helvetica, sans-serif;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #0033cc;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-family: Arial;
|
||||
font-weight: normal;
|
||||
border-bottom: solid 1px #ccc;
|
||||
margin: 0 0 20px 0;
|
||||
padding: 0 0 5px 0;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-family: Arial;
|
||||
font-weight: bold;
|
||||
line-height: 2em;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
th,td {
|
||||
padding: 5px 8px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
td.center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.deleted td {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.data th {
|
||||
font-weight: normal;
|
||||
border-bottom: solid 1px #000;
|
||||
}
|
||||
|
||||
.data td {
|
||||
border-bottom: solid 1px #eee;
|
||||
}
|
||||
|
||||
.form th {
|
||||
font-weight: normal;
|
||||
text-align: right;
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 2.9 KiB |
|
@ -0,0 +1,53 @@
|
|||
<!DOCTYPE html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US" lang="en-US">
|
||||
<!--
|
||||
* 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.
|
||||
-->
|
||||
<head>
|
||||
<title>SampleSync: Contacts for '{{ username }}'</title>
|
||||
<link type="text/css" rel="stylesheet" href="/static/css/main.css" media="screen" />
|
||||
</head>
|
||||
<body>
|
||||
<h1>SampleSync: Contacts for '{{ username }}'</h1>
|
||||
<table class="data" cellpadding="0" cellspacing="0">
|
||||
<tr>
|
||||
<th> </th>
|
||||
<th>Id</th>
|
||||
<th>Name</th>
|
||||
<th>Email</th>
|
||||
<th>Home</th>
|
||||
<th>Office</th>
|
||||
<th>Mobile</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
{% for contact in contacts %}
|
||||
<tr {% if contact.deleted %} class="deleted" {% endif %}>
|
||||
<td class="center">
|
||||
<a href="/edit_avatar?id={{ contact.key.id }}"><img src="/avatar?id={{ contact.key.id }}" height="25" width="25" /></a>
|
||||
</td>
|
||||
<td><a href="/edit_contact?id={{ contact.key.id }}">{{ contact.key.id }}</a></td>
|
||||
<td><a href="/edit_contact?id={{ contact.key.id }}">{{ contact.firstname }} {{ contact.lastname }}</a></td>
|
||||
<td>{{ contact.email }}</td>
|
||||
<td>{{ contact.phone_home }}</td>
|
||||
<td>{{ contact.phone_office }}</td>
|
||||
<td>{{ contact.phone_mobile }}</td>
|
||||
<td><span style="whitespace: no-wrap;">{{ contact.status }}</span></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
|
||||
<a href = "/add_contact">Add Contact</a>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,42 @@
|
|||
<!DOCTYPE html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US" lang="en-US">
|
||||
<!--
|
||||
* 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.
|
||||
-->
|
||||
<head>
|
||||
<title>SampleSync: Edit Picture</title>
|
||||
<link type="text/css" rel="stylesheet" href="/static/css/main.css" media="screen" />
|
||||
</head>
|
||||
<body>
|
||||
<h1>SampleSync: Edit Picture</h1>
|
||||
<form method="POST" action="/edit_avatar" enctype="multipart/form-data">
|
||||
<h3>Current Avatar:</h3>
|
||||
<blockquote>
|
||||
{% if avatar %}
|
||||
<img src="/avatar?id={{ contactId }}" />
|
||||
{% else %}
|
||||
<i>You haven't added a picture for this friend...</i>
|
||||
{% endif %}
|
||||
</blockquote>
|
||||
<h3>New Avatar:</h3>
|
||||
<p>Please select a file containing the image you'd like to use for this friend</p>
|
||||
<input type="file" name="avatar" />
|
||||
<p> </p>
|
||||
<input type="submit" name="Save" value="Save Changes" />
|
||||
<input type="button" name="Cancel" value="Cancel" onclick="document.location='/';return false;" />
|
||||
<input type="hidden" name="id" value="{{ contactId }}" />
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,38 @@
|
|||
<!DOCTYPE html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US" lang="en-US">
|
||||
<!--
|
||||
* 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.
|
||||
-->
|
||||
<head>
|
||||
<title>SampleSync: {{ title }}</title>
|
||||
<link type="text/css" rel="stylesheet" href="/static/css/main.css" media="screen" />
|
||||
</head>
|
||||
<body>
|
||||
<h1>SampleSync: {{ header }}</h1>
|
||||
<form method="POST" action="{{ action }}">
|
||||
<table class="form" cellpadding="0" cellspacing="0">
|
||||
{{ form_data_rows }}
|
||||
</table>
|
||||
<input type="submit" name="Save" value="Save Changes" />
|
||||
<input type="button" name="Cancel" value="Cancel" onclick="document.location='/';return false;" />
|
||||
{% if has_contactId %}
|
||||
<input type="hidden" name="id" value="{{ contactId }}" />
|
||||
{% endif %}
|
||||
{% if has_handle %}
|
||||
<input type="hidden" name="username" value="{{ handle }}" />
|
||||
{% endif %}
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,400 @@
|
|||
#!/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.
|
||||
|
||||
"""
|
||||
|
||||
import cgi
|
||||
import logging
|
||||
import time as _time
|
||||
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 BaseWebServiceHandler(webapp.RequestHandler):
|
||||
"""
|
||||
Base class for our web services. We put some common helper
|
||||
functions here.
|
||||
"""
|
||||
|
||||
"""
|
||||
Since we're only simulating a single user account, declare our
|
||||
hard-coded credentials here, so that they're easy to see/find.
|
||||
We actually accept any and all usernames that start with this
|
||||
hard-coded values. So if ACCT_USER_NAME is 'user', then we'll
|
||||
accept 'user', 'user75', 'userbuddy', etc, all as legal account
|
||||
usernames.
|
||||
"""
|
||||
ACCT_USER_NAME = 'user'
|
||||
ACCT_PASSWORD = 'test'
|
||||
ACCT_AUTH_TOKEN = 'xyzzy'
|
||||
|
||||
DATE_TIME_FORMAT = '%Y/%m/%d %H:%M'
|
||||
|
||||
"""
|
||||
Process a request to authenticate a client. We assume that the username
|
||||
and password will be included in the request. If successful, we'll return
|
||||
an authtoken as the only content. If auth fails, we'll send an "invalid
|
||||
credentials" error.
|
||||
We return a boolean indicating whether we were successful (true) or not (false).
|
||||
In the event that this call fails, we will setup the response, so callers just
|
||||
need to RETURN in the error case.
|
||||
"""
|
||||
def authenticate(self):
|
||||
self.username = self.request.get('username')
|
||||
self.password = self.request.get('password')
|
||||
|
||||
logging.info('Authenticatng username: ' + self.username)
|
||||
|
||||
if ((self.username != None) and
|
||||
(self.username.startswith(BaseWebServiceHandler.ACCT_USER_NAME)) and
|
||||
(self.password == BaseWebServiceHandler.ACCT_PASSWORD)):
|
||||
# Authentication was successful - return our hard-coded
|
||||
# auth-token as the only response.
|
||||
self.response.set_status(200, 'OK')
|
||||
self.response.out.write(BaseWebServiceHandler.ACCT_AUTH_TOKEN)
|
||||
return True
|
||||
else:
|
||||
# Authentication failed. Return the standard HTTP auth failure
|
||||
# response to let the client know.
|
||||
self.response.set_status(401, 'Invalid Credentials')
|
||||
return False
|
||||
|
||||
"""
|
||||
Validate the credentials of the client for a web service request.
|
||||
The request should include username/password parameters that correspond
|
||||
to our hard-coded single account values.
|
||||
We return a boolean indicating whether we were successful (true) or not (false).
|
||||
In the event that this call fails, we will setup the response, so callers just
|
||||
need to RETURN in the error case.
|
||||
"""
|
||||
def validate(self):
|
||||
self.username = self.request.get('username')
|
||||
self.authtoken = self.request.get('authtoken')
|
||||
|
||||
logging.info('Validating username: ' + self.username)
|
||||
|
||||
if ((self.username != None) and
|
||||
(self.username.startswith(BaseWebServiceHandler.ACCT_USER_NAME)) and
|
||||
(self.authtoken == BaseWebServiceHandler.ACCT_AUTH_TOKEN)):
|
||||
return True
|
||||
else:
|
||||
self.response.set_status(401, 'Invalid Credentials')
|
||||
return False
|
||||
|
||||
|
||||
class Authenticate(BaseWebServiceHandler):
|
||||
"""
|
||||
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.authenticate()
|
||||
|
||||
def get(self):
|
||||
"""Used for debugging in a browser..."""
|
||||
self.post()
|
||||
|
||||
|
||||
class SyncContacts(BaseWebServiceHandler):
|
||||
"""Handles requests for fetching user's contacts.
|
||||
|
||||
UpdateHandler only accepts post events. It expects each
|
||||
request to include username and authtoken. If the authtoken is valid
|
||||
it returns user's contact info in JSON format.
|
||||
"""
|
||||
|
||||
def get(self):
|
||||
"""Used for debugging in a browser..."""
|
||||
self.post()
|
||||
|
||||
def post(self):
|
||||
logging.info('*** Starting contact sync ***')
|
||||
if (not self.validate()):
|
||||
return
|
||||
|
||||
updated_contacts = []
|
||||
|
||||
# Process any client-side changes sent up in the request.
|
||||
# Any new contacts that were added are included in the
|
||||
# updated_contacts list, so that we return them to the
|
||||
# client. That way, the client can see the serverId of
|
||||
# the newly added contact.
|
||||
client_buffer = self.request.get('contacts')
|
||||
if ((client_buffer != None) and (client_buffer != '')):
|
||||
self.process_client_changes(client_buffer, updated_contacts)
|
||||
|
||||
# Add any contacts that have been updated on the server-side
|
||||
# since the last sync by this client.
|
||||
client_state = self.request.get('syncstate')
|
||||
self.get_updated_contacts(client_state, updated_contacts)
|
||||
|
||||
logging.info('Returning ' + str(len(updated_contacts)) + ' contact records')
|
||||
|
||||
# Return the list of updated contacts to the client
|
||||
self.response.set_status(200)
|
||||
self.response.out.write(toJSON(updated_contacts))
|
||||
|
||||
def get_updated_contacts(self, client_state, updated_contacts):
|
||||
logging.info('* Processing server changes')
|
||||
timestamp = None
|
||||
|
||||
base_url = self.request.host_url
|
||||
|
||||
# The client sends the last high-water-mark that they successfully
|
||||
# sync'd to in the syncstate parameter. It's opaque to them, but
|
||||
# its actually a seconds-in-unix-epoch timestamp that we use
|
||||
# as a baseline.
|
||||
if client_state:
|
||||
logging.info('Client sync state: ' + client_state)
|
||||
timestamp = datetime.utcfromtimestamp(float(client_state))
|
||||
|
||||
# Keep track of the update/delete counts, so we can log it
|
||||
# below. Makes debugging easier...
|
||||
update_count = 0
|
||||
delete_count = 0
|
||||
|
||||
contacts = datastore.Contact.all()
|
||||
if contacts:
|
||||
# Find the high-water mark for the most recently updated friend.
|
||||
# We'll return this as the syncstate (x) value for all the friends
|
||||
# we return from this function.
|
||||
high_water_date = datetime.min
|
||||
for contact in contacts:
|
||||
if (contact.updated > high_water_date):
|
||||
high_water_date = contact.updated
|
||||
high_water_mark = str(long(_time.mktime(high_water_date.utctimetuple())) + 1)
|
||||
logging.info('New sync state: ' + high_water_mark)
|
||||
|
||||
# Now build the updated_contacts containing all the friends that have been
|
||||
# changed since the last sync
|
||||
for contact in contacts:
|
||||
# If our list of contacts we're returning already contains this
|
||||
# contact (for example, it's a contact just uploaded from the client)
|
||||
# then don't bother processing it any further...
|
||||
if (self.list_contains_contact(updated_contacts, contact)):
|
||||
continue
|
||||
|
||||
handle = contact.handle
|
||||
|
||||
if timestamp is None or contact.updated > timestamp:
|
||||
if contact.deleted == True:
|
||||
delete_count = delete_count + 1
|
||||
DeletedContactData(updated_contacts, handle, high_water_mark)
|
||||
else:
|
||||
update_count = update_count + 1
|
||||
UpdatedContactData(updated_contacts, handle, None, base_url, high_water_mark)
|
||||
|
||||
logging.info('Server-side updates: ' + str(update_count))
|
||||
logging.info('Server-side deletes: ' + str(delete_count))
|
||||
|
||||
def process_client_changes(self, contacts_buffer, updated_contacts):
|
||||
logging.info('* Processing client changes: ' + self.username)
|
||||
|
||||
base_url = self.request.host_url
|
||||
|
||||
# Build an array of generic objects containing contact data,
|
||||
# using the Django built-in JSON parser
|
||||
logging.info('Uploaded contacts buffer: ' + contacts_buffer)
|
||||
json_list = simplejson.loads(contacts_buffer)
|
||||
logging.info('Client-side updates: ' + str(len(json_list)))
|
||||
|
||||
# Keep track of the number of new contacts the client sent to us,
|
||||
# so that we can log it below.
|
||||
new_contact_count = 0
|
||||
|
||||
for jcontact in json_list:
|
||||
new_contact = False
|
||||
id = self.safe_attr(jcontact, 'i')
|
||||
if (id != None):
|
||||
logging.info('Updating contact: ' + str(id))
|
||||
contact = datastore.Contact.get(db.Key.from_path('Contact', id))
|
||||
else:
|
||||
logging.info('Creating new contact record')
|
||||
new_contact = True
|
||||
contact = datastore.Contact(handle='temp')
|
||||
|
||||
# If the 'change' for this contact is that they were deleted
|
||||
# on the client-side, all we want to do is set the deleted
|
||||
# flag here, and we're done.
|
||||
if (self.safe_attr(jcontact, 'd') == True):
|
||||
contact.deleted = True
|
||||
contact.put()
|
||||
logging.info('Deleted contact: ' + contact.handle)
|
||||
continue
|
||||
|
||||
contact.firstname = self.safe_attr(jcontact, 'f')
|
||||
contact.lastname = self.safe_attr(jcontact, 'l')
|
||||
contact.phone_home = self.safe_attr(jcontact, 'h')
|
||||
contact.phone_office = self.safe_attr(jcontact, 'o')
|
||||
contact.phone_mobile = self.safe_attr(jcontact, 'm')
|
||||
contact.email = self.safe_attr(jcontact, 'e')
|
||||
contact.deleted = (self.safe_attr(jcontact, 'd') == 'true')
|
||||
if (new_contact):
|
||||
# New record - add them to db...
|
||||
new_contact_count = new_contact_count + 1
|
||||
contact.handle = contact.firstname + '_' + contact.lastname
|
||||
logging.info('Created new contact handle: ' + contact.handle)
|
||||
contact.put()
|
||||
logging.info('Saved contact: ' + contact.handle)
|
||||
|
||||
# We don't save off the client_id value (thus we add it after
|
||||
# the "put"), but we want it to be in the JSON object we
|
||||
# serialize out, so that the client can match this contact
|
||||
# up with the client version.
|
||||
client_id = self.safe_attr(jcontact, 'c')
|
||||
|
||||
# Create a high-water-mark for sync-state from the 'updated' time
|
||||
# for this contact, so we return the correct value to the client.
|
||||
high_water = str(long(_time.mktime(contact.updated.utctimetuple())) + 1)
|
||||
|
||||
# Add new contacts to our updated_contacts, so that we return them
|
||||
# to the client (so the client gets the serverId for the
|
||||
# added contact)
|
||||
if (new_contact):
|
||||
UpdatedContactData(updated_contacts, contact.handle, client_id, base_url,
|
||||
high_water)
|
||||
|
||||
logging.info('Client-side adds: ' + str(new_contact_count))
|
||||
|
||||
def list_contains_contact(self, contact_list, contact):
|
||||
if (contact is None):
|
||||
return False
|
||||
contact_id = str(contact.key().id())
|
||||
for next in contact_list:
|
||||
if ((next != None) and (next['i'] == contact_id)):
|
||||
return True
|
||||
return False
|
||||
|
||||
def safe_attr(self, obj, attr_name):
|
||||
if attr_name in obj:
|
||||
return obj[attr_name]
|
||||
return None
|
||||
|
||||
class ResetDatabase(BaseWebServiceHandler):
|
||||
"""
|
||||
Handles cron request to reset the contact database.
|
||||
|
||||
We have a weekly cron task that resets the database back to a
|
||||
few contacts, so that it doesn't grow to an absurd size.
|
||||
"""
|
||||
|
||||
def get(self):
|
||||
# Delete all the existing contacts from the database
|
||||
contacts = datastore.Contact.all()
|
||||
for contact in contacts:
|
||||
contact.delete()
|
||||
|
||||
# Now create three sample contacts
|
||||
contact1 = datastore.Contact(handle = 'juliet',
|
||||
firstname = 'Juliet',
|
||||
lastname = 'Capulet',
|
||||
phone_mobile = '(650) 555-1000',
|
||||
phone_home = '(650) 555-1001',
|
||||
status = 'Wherefore art thou Romeo?')
|
||||
contact1.put()
|
||||
|
||||
contact2 = datastore.Contact(handle = 'romeo',
|
||||
firstname = 'Romeo',
|
||||
lastname = 'Montague',
|
||||
phone_mobile = '(650) 555-2000',
|
||||
phone_home = '(650) 555-2001',
|
||||
status = 'I dream\'d a dream to-night')
|
||||
contact2.put()
|
||||
|
||||
contact3 = datastore.Contact(handle = 'tybalt',
|
||||
firstname = 'Tybalt',
|
||||
lastname = 'Capulet',
|
||||
phone_mobile = '(650) 555-3000',
|
||||
phone_home = '(650) 555-3001',
|
||||
status = 'Have at thee, coward')
|
||||
contact3.put()
|
||||
|
||||
|
||||
|
||||
|
||||
def toJSON(object):
|
||||
"""Dumps the data represented by the object to JSON for wire transfer."""
|
||||
return simplejson.dumps(object)
|
||||
|
||||
class UpdatedContactData(object):
|
||||
"""Holds data for user's contacts.
|
||||
|
||||
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',
|
||||
'client_id': 'c'
|
||||
}
|
||||
|
||||
def __init__(self, contact_list, username, client_id, host_url, high_water_mark):
|
||||
obj = datastore.Contact.get_contact_info(username)
|
||||
contact = {}
|
||||
for obj_name, json_name in self.__FIELD_MAP.items():
|
||||
if hasattr(obj, obj_name):
|
||||
v = getattr(obj, obj_name)
|
||||
if (v != None):
|
||||
contact[json_name] = str(v)
|
||||
else:
|
||||
contact[json_name] = None
|
||||
contact['i'] = str(obj.key().id())
|
||||
contact['a'] = host_url + "/avatar?id=" + str(obj.key().id())
|
||||
contact['x'] = high_water_mark
|
||||
if (client_id != None):
|
||||
contact['c'] = str(client_id)
|
||||
contact_list.append(contact)
|
||||
|
||||
class DeletedContactData(object):
|
||||
def __init__(self, contact_list, username, high_water_mark):
|
||||
obj = datastore.Contact.get_contact_info(username)
|
||||
contact = {}
|
||||
contact['d'] = 'true'
|
||||
contact['i'] = str(obj.key().id())
|
||||
contact['x'] = high_water_mark
|
||||
contact_list.append(contact)
|
||||
|
||||
def main():
|
||||
application = webapp.WSGIApplication(
|
||||
[('/auth', Authenticate),
|
||||
('/sync', SyncContacts),
|
||||
('/reset_database', ResetDatabase),
|
||||
],
|
||||
debug=True)
|
||||
wsgiref.handlers.CGIHandler().run(application)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
Loading…
Add table
Add a link
Reference in a new issue