-
Notifications
You must be signed in to change notification settings - Fork 11
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
moved contrib apps back to /apps/*, to split repo
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
Showing
19 changed files
with
825 additions
and
0 deletions.
There are no files selected for viewing
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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), | |||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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), | |||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.
Oops, something went wrong.