SampleSyncAdapter sample code.
This commit is contained in:
44
samples/SampleSyncAdapter/samplesyncadapter_server/app.yaml
Normal file
44
samples/SampleSyncAdapter/samplesyncadapter_server/app.yaml
Normal file
@@ -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
|
||||
273
samples/SampleSyncAdapter/samplesyncadapter_server/dashboard.py
Normal file
273
samples/SampleSyncAdapter/samplesyncadapter_server/dashboard.py
Normal file
@@ -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()
|
||||
@@ -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
|
||||
173
samples/SampleSyncAdapter/samplesyncadapter_server/main.py
Normal file
173
samples/SampleSyncAdapter/samplesyncadapter_server/main.py
Normal file
@@ -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()
|
||||
@@ -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
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user