Permalink
Browse files

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...
1 parent d5cb6cd commit 3a291e6ffc5e5fe7e6fc7384863976597343b474 @adammck adammck committed Jan 7, 2010
View
No changes.
View
@@ -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()
View
@@ -0,0 +1,6 @@
+#!/usr/bin/env python
+# vim: ai ts=4 sts=4 et sw=4
+
+title = "AJAX Helper"
+host = "localhost"
+port = 8001
View
@@ -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),
+)
View
@@ -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)
View
No changes.
View
@@ -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),
+)
View
@@ -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
View
@@ -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
View
@@ -0,0 +1,4 @@
+#!/usr/bin/env python
+# vim: ai ts=4 sts=4 et sw=4
+
+title = "Handlers"
No changes.
Oops, something went wrong.

0 comments on commit 3a291e6

Please sign in to comment.