Skip to content

Commit

Permalink
moved contrib apps back to /apps/*, to split repo
Browse files Browse the repository at this point in the history
i think the rapidsms-apps-conrib will end up in the same place
(lib.rapidsms.contrib), but i'm about to split the repo, so am
moving them back temporarily, to maintain the entire history.

i don't quite understand what i'm doing.
  • Loading branch information
adammck committed Jan 7, 2010
1 parent d5cb6cd commit 3a291e6
Show file tree
Hide file tree
Showing 19 changed files with 825 additions and 0 deletions.
Empty file added ajax/__init__.py
Empty file.
235 changes: 235 additions & 0 deletions ajax/app.py
Original file line number Original file line Diff line number Diff line change
@@ -0,0 +1,235 @@
#!/usr/bin/env python
# vim: ai ts=4 sts=4 et sw=4


import cgi
import urlparse
import traceback
from threading import Thread
from SocketServer import ThreadingMixIn
from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer

from django.utils.simplejson import JSONEncoder
from django.db.models.query import QuerySet

import rapidsms


class App(rapidsms.App):
"""
This App does nothing by itself. It exists only to serve other Apps,
by providing an easy (and standard) way for them to communicate
between their WebUI and RapidSMS App object.
When RapidSMS starts, this app starts an HTTPServer (port 8001 as
default, but configurable via rapidsms.ini) in a worker thread, and
watches for any incoming HTTP requests matching */app/method*. These
requests, along with their GET parameters and POST form, are passed
on to the named app.
Examples:
method URL app method args
====== === === ====== ====
GET /food/toast food ajax_GET_toast { }
POST /food/waffles food ajax_POST_waffles { }, { }
POST /food/eggs?x=1 food ajax_POST_eggs { "x": 1 }, { }
Any data that is returned by the handler method is JSON encoded, and
sent back to the WebUI in response. Since the _webui_ app includes
jQuery with every view, this makes it very easy for the WebUIs of
other apps to query their running App object for state. See the
_training_ app for an example.
But wait! AJAX can't cross domains, so a request to port 8001 from
the WebUI won't work! This is handled by the WebUI bundled with this
app, that proxies all requests to /ajax/(.+) to the right place, on
the server side. I cannot conceive of a situation where this would
be a problem - but keep it in mind, and don't forget to prepend
"/ajax/" to your AJAX URLs.
"""


class Server(ThreadingMixIn, HTTPServer):
pass


class MyJsonEncoder(JSONEncoder):
def default(self, o):

# if this object has its own JSON serializer, use it
if hasattr(o, "__json__"):
return o.__json__()

elif type(o) == QuerySet:
return list(o)

# otherwise, revert to the usual behavior
return JSONEncoder.default(self, o)


class RequestHandler(BaseHTTPRequestHandler):
def _find_app(self, name):

# inspect the name of each active app,
# returning as soon as we find a match
for app in self.server.app.router.apps:
if app.name == name:
return app

# no app by that
# name was found
return None

def _charset(self, str):
"""
Extracts and returns the character-set argument from an HTTP
content-type header, or None if it was not found.
>>> _charset("multipart/form-data; charst=UTF-8")
"UTF-8"
>>> _charset("application/x-www-form-urlencoded") is None
True
"""

x = str.split("charset=", 1)
return x[1] if(len(x) == 2) else None


# handle both GET and POST with
# the same method
def do_GET(self): return self.process()
def do_POST(self): return self.process()

def process(self):
def response(code, output, json=True):
self.send_response(code)
mime_type = "application/json" if json else "text/plain"
self.send_header("content-type", mime_type)
self.end_headers()

if json:
json = App.MyJsonEncoder().encode(output)
self.wfile.write(json)

# otherwise, write the raw response.
# it doesn't make much sense to have
# error messages encoded as JSON...
else: self.wfile.write(output)

# HTTP2xx represents success
return (code>=200 and code <=299)

# should look something like:
# /alpha/bravo?charlie=delta
#
# this request will be parsed to the "bravo"
# method of the "alpha" app, with the params:
# { "charlie": ["delta"] }
#
# any other path format will return an http404
# error, for the time being. params are optional.
url = urlparse.urlparse(self.path)
path_parts = url.path.split("/")

# abort if the url didn't look right
# TODO: better error message here
if len(path_parts) != 3:
return response(404, "FAIL.")

# resolve the first part of the url into an app
# (via the router), and abort if it wasn't valid
app_name = path_parts[1]
app = self._find_app(app_name)
if (app is None):
return response(404,
"Invalid app: %s" % app_name)

# same for the request name within the app
# (FYI, self.command returns GET, POST, etc)
meth_name = "ajax_%s_%s" % (self.command, path_parts[2])
if not hasattr(app, meth_name):
return response(404,
"Invalid method: %s" % meth_name)

# everything appears to be well, so call the
# target method, and return the response (as
# a string, for now)
try:
method = getattr(app, meth_name)
params = urlparse.urlparse(url.query)
args = [params]

# for post requests, we'll also need to parse
# the form data, and hand it to the method
if self.command == "POST":
content_type = self.headers["content-type"]
form = {}

# parse the form data via the CGI lib. this is
# a horrible mess, but supports all kinds of
# encodings (multipart, in particular)
storage = cgi.FieldStorage(
fp = self.rfile,
headers = self.headers,
environ = {
"REQUEST_METHOD": "POST",
"CONTENT_TYPE": content_type })

# extract the charset from the content-type header,
# which should have been passed along in views.py
charset = self._charset(content_type)

# convert the fieldstorage object into a dict,
# to keep it simple for the handler methods.
# TODO: maybe make this a util if it's useful
# elsewhere. it isn't, for the time being.
for key in storage.keys():

# convert each of the values with this key into
# unicode, respecting the content-type that the
# request _claims_ to be currently encoded with
val = [
unicode(v, charset)
for v in storage.getlist(key)]

# where possible, store the values as singular,
# to avoid CGI's usual post["id"][0] verbosity
form[key] = val[0] if(len(val) == 1) else val

args.append(form)

# call the method, and send back whatever data
# structure was returned, serialized with JSON
output = method(*args)
return response(200, output)

# something raised during the request, so
# return a useless http error to the requester
except Exception, err:
self.server.app.warning(traceback.format_exc())
return response(500, unicode(err), False)

# this does nothing, except prevent HTTP
# requests being echoed to the screen
def log_request(*args):
pass


def start(self):

# create the webserver, through which the
# AJAX requests from the WebUI will arrive
self.server = self.Server((
self.config["host"],
self.config["port"]),
self.RequestHandler)

self.server.app = self

# start the server in a separate thread, and daemonize it
# to prevent it from hanging once the main thread terminates
self.thread = Thread(target=self.server.serve_forever)
self.thread.daemon = True
self.thread.start()
6 changes: 6 additions & 0 deletions ajax/config.py
Original file line number Original file line Diff line number Diff line change
@@ -0,0 +1,6 @@
#!/usr/bin/env python
# vim: ai ts=4 sts=4 et sw=4

title = "AJAX Helper"
host = "localhost"
port = 8001
11 changes: 11 additions & 0 deletions ajax/urls.py
Original file line number Original file line Diff line number Diff line change
@@ -0,0 +1,11 @@
#!/usr/bin/env python
# vim: ai ts=4 sts=4 et sw=4


from django.conf.urls.defaults import *
from . import views


urlpatterns = patterns('',
(r'^ajax/(?P<path>.*)$', views.proxy),
)
61 changes: 61 additions & 0 deletions ajax/views.py
Original file line number Original file line Diff line number Diff line change
@@ -0,0 +1,61 @@
#!/usr/bin/env python
# vim: ai ts=4 sts=4 et sw=4


import urllib
import urllib2
from django.http import HttpResponse
from rapidsms.djangoproject import settings


def proxy(req, path):

# build the url to the http server running in ajax.app.App via conf
# hackery. no encoding worries here, since GET only supports ASCII.
# http://www.w3.org/TR/REC-html40/interact/forms.html#idx-POST-1
conf = settings.RAPIDSMS_APPS["rapidsms.contrib.ajax"]
url = "http://%s:%d/%s?%s" % (
conf["host"], conf["port"],
path, req.GET.urlencode())

try:
data = None
headers = {}

# send along POST data verbatim
if req.method == "POST":
data = req.POST.urlencode()

# it doesn't matter if req.POST contains unicode, because
# req.urlencode will smartly (via djano's smart_str) convert
# it all back to ASCII using the same encoding that it was
# submitted with. but we must pass along the content-type
# (containing the charset) to ensure that it's decoded
# correctly the next time around.
headers["content-type"] = req.META["CONTENT_TYPE"]

# call the router (app.py) via HTTP
sub_request = urllib2.Request(url, data, headers)
sub_response = urllib2.urlopen(sub_request)
out = sub_response.read()
code = 200

# the request was successful, but the server
# returned an error. as above, proxy it as-is,
# so we can receive as much debug info as possible
except urllib2.HTTPError, err:
out = err.read()
code = err.code

# the router couldn't be reached, but we have no idea why. return a
# useless error
except urllib2.URLError, err:
out = "Couldn't reach the router."
code = 500

# if the subresponse specified a content type (which it ALWAYS
# should), pass it along. else, default to plain text
try: ct = out.info()["content-type"]
except: ct = "text/plain"

return HttpResponse(out, status=code, content_type=ct)
Empty file added djangoadmin/__init__.py
Empty file.
12 changes: 12 additions & 0 deletions djangoadmin/urls.py
Original file line number Original file line Diff line number Diff line change
@@ -0,0 +1,12 @@
#!/usr/bin/env python
# vim: ai ts=4 sts=4 et sw=4


from django.conf.urls.defaults import patterns
from django.contrib import admin
admin.autodiscover()


urlpatterns = patterns('',
(r'^admin/(.*)', admin.site.root),
)
5 changes: 5 additions & 0 deletions handlers/__init__.py
Original file line number Original file line Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/usr/bin/env python
# vim: ai ts=4 sts=4 et sw=4

from .handlers.pattern import PatternHandler
from .handlers.keyword import KeywordHandler
43 changes: 43 additions & 0 deletions handlers/app.py
Original file line number Original file line Diff line number Diff line change
@@ -0,0 +1,43 @@
#!/usr/bin/env python
# vim: ai ts=4 sts=4 et sw=4


import rapidsms
from rapidsms.djangoproject import settings
from .utils import find_handlers


class App(rapidsms.App):
def start(self):
"""
Spiders all RapidSMS apps, and registers all available handlers.
"""

self.handlers = []

for module_name in settings.RAPIDSMS_APPS.keys():

# ignore handlers found within _this_ app. they're intended
# to be inherited by other apps, not instantiated directly.
if not module_name.endswith(".handlers"):

handlers = find_handlers(module_name)
self.handlers.extend(handlers)

class_names = [cls.__name__ for cls in self.handlers]
self.info("Registered handlers: %s" % (", ".join(class_names)))


def handle(self, msg):
"""
Forwards the *msg* to every handler, and short-circuits the
phase if any of them accept it. The first to accept it will
block the others, and there's deliberately no way to predict
the order that they're called in. (This is intended to force
handlers to be as reluctant as possible.)
"""

for handler in self.handlers:
if handler.dispatch(self.router, msg):
self.info("Incoming message handled by %s" % handler.__name__)
return True
4 changes: 4 additions & 0 deletions handlers/config.py
Original file line number Original file line Diff line number Diff line change
@@ -0,0 +1,4 @@
#!/usr/bin/env python
# vim: ai ts=4 sts=4 et sw=4

title = "Handlers"
Empty file added handlers/handlers/__init__.py
Empty file.
Loading

0 comments on commit 3a291e6

Please sign in to comment.