[snowy] Add in piston, start using it.



commit f8655153b706daa7d5500ee632dd3010117ecd82
Author: Brad Taylor <brad getcoded net>
Date:   Thu May 14 13:05:33 2009 -0400

    Add in piston, start using it.
---
 api/handlers.py              |   41 ++++
 api/urls.py                  |   29 +++
 lib/piston/authentication.py |  260 +++++++++++++++++++++
 lib/piston/decorator.py      |  186 +++++++++++++++
 lib/piston/doc.py            |   90 +++++++
 lib/piston/emitters.py       |  326 ++++++++++++++++++++++++++
 lib/piston/forms.py          |   19 ++
 lib/piston/handler.py        |  101 ++++++++
 lib/piston/managers.py       |   52 ++++
 lib/piston/models.py         |  146 ++++++++++++
 lib/piston/oauth.py          |  531 ++++++++++++++++++++++++++++++++++++++++++
 lib/piston/resource.py       |  176 ++++++++++++++
 lib/piston/store.py          |   68 ++++++
 lib/piston/utils.py          |  124 ++++++++++
 notes/models.py              |    6 +
 notes/urls.py                |    5 +-
 notes/views.py               |   22 ++-
 urls.py                      |    7 +-
 18 files changed, 2179 insertions(+), 10 deletions(-)

diff --git a/api/__init__.py b/api/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/api/handlers.py b/api/handlers.py
new file mode 100644
index 0000000..58ff140
--- /dev/null
+++ b/api/handlers.py
@@ -0,0 +1,41 @@
+#
+# Copyright (c) 2009 Brad Taylor <brad getcoded net>
+#
+# This program is free software: you can redistribute it and/or modify it under
+# the terms of the GNU Affero General Public License as published by the Free
+# Software Foundation, either version 3 of the License, or (at your option) any
+# later version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE.  See the GNU Affero General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+
+from django.contrib.auth.models import User
+from django.core.urlresolvers import reverse
+from piston.handler import AnonymousBaseHandler
+from piston.utils import rc
+
+from snowy.notes.models import Note
+
+class UserHandler(AnonymousBaseHandler):
+    allow_methods = ('GET',)
+    model = User
+
+    def read(self, request, username):
+        # TODO: abstract this out
+        try:
+            user = User.objects.get(username=username)
+        except:
+            return rc.NOT_HERE
+        
+        return {
+            'first name': user.first_name,
+            'last name': user.last_name,
+            'notes-ref': reverse('note_index', kwargs={'username': username}),
+            #'notes-api-ref': reverse('note_api_index', kwargs={'username': username}),
+        }
diff --git a/api/urls.py b/api/urls.py
new file mode 100644
index 0000000..3f28c17
--- /dev/null
+++ b/api/urls.py
@@ -0,0 +1,29 @@
+#
+# Copyright (c) 2009 Brad Taylor <brad getcoded net>
+#
+# This program is free software: you can redistribute it and/or modify it under
+# the terms of the GNU Affero General Public License as published by the Free
+# Software Foundation, either version 3 of the License, or (at your option) any
+# later version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE.  See the GNU Affero General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+
+from django.conf.urls.defaults import *
+
+from piston.resource import Resource
+from snowy.api.handlers import UserHandler
+
+user_handler = Resource(UserHandler)
+
+urlpatterns = patterns('',
+    url(r'(?P<username>\w+)/$', user_handler),
+#    url(r'^(?P<username>\w+)/notes$', note_handler),
+#    url(r'^(?P<username>\w+)/notes$', note_handler),
+)
diff --git a/lib/piston/__init__.py b/lib/piston/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/lib/piston/authentication.py b/lib/piston/authentication.py
new file mode 100644
index 0000000..e3fcdaf
--- /dev/null
+++ b/lib/piston/authentication.py
@@ -0,0 +1,260 @@
+from django.http import HttpResponse, HttpResponseRedirect
+from django.contrib.auth.models import User
+from django.contrib.auth.decorators import login_required
+from django.template import loader
+from django.conf import settings
+from django.core.urlresolvers import get_callable
+
+import oauth
+from store import DataStore
+
+def django_auth(username, password):
+    """
+    Basic callback for `HttpBasicAuthentication`
+    which checks the username and password up
+    against Djangos built-in authentication system.
+    
+    On success, returns the `User`, *not* boolean!
+    """
+    try:
+        user = User.objects.get(username=username)
+        if user.check_password(password):
+            return user
+        else:
+            return False
+    except User.DoesNotExist:
+        return False
+
+class HttpBasicAuthentication(object):
+    """
+    Basic HTTP authenticater. Synopsis:
+    
+    Authentication handlers must implement two methods:
+     - `is_authenticated`: Will be called when checking for
+        authentication. Receives a `request` object, please
+        set your `User` object on `request.user`, otherwise
+        return False (or something that evaluates to False.)
+     - `challenge`: In cases where `is_authenticated` returns
+        False, the result of this method will be returned.
+        This will usually be a `HttpResponse` object with
+        some kind of challenge headers and 401 code on it.
+    """
+    def __init__(self, auth_func=django_auth, realm='API'):
+        self.auth_func = auth_func
+        self.realm = realm
+
+    def is_authenticated(self, request):
+        auth_string = request.META.get('HTTP_AUTHORIZATION', None)
+
+        if not auth_string:
+            return False
+            
+        (authmeth, auth) = auth_string.split(" ", 1)
+        
+        if not authmeth.lower() == 'basic':
+            return False
+            
+        auth = auth.strip().decode('base64')
+        (username, password) = auth.split(':', 1)
+        
+        request.user = self.auth_func(username, password)
+        
+        return not request.user is False
+        
+    def challenge(self):
+        resp = HttpResponse("Authorization Required")
+        resp['WWW-Authenticate'] = 'Basic realm="%s"' % self.realm
+        resp.status_code = 401
+        return resp
+
+def initialize_server_request(request):
+    """
+    Shortcut for initialization.
+    """
+    oauth_request = oauth.OAuthRequest.from_request(
+        request.method, request.build_absolute_uri(), 
+        headers=request.META, parameters=dict(request.REQUEST.items()),
+        query_string=request.environ.get('QUERY_STRING', ''))
+        
+    if oauth_request:
+        oauth_server = oauth.OAuthServer(DataStore(oauth_request))
+        oauth_server.add_signature_method(oauth.OAuthSignatureMethod_PLAINTEXT())
+        oauth_server.add_signature_method(oauth.OAuthSignatureMethod_HMAC_SHA1())
+    else:
+        oauth_server = None
+        
+    return oauth_server, oauth_request
+
+def send_oauth_error(err=None):
+    """
+    Shortcut for sending an error.
+    """
+    response = HttpResponse(err.message.encode('utf-8'))
+    response.status_code = 401
+
+    realm = 'OAuth'
+    header = oauth.build_authenticate_header(realm=realm)
+
+    for k, v in header.iteritems():
+        response[k] = v
+
+    return response
+
+def oauth_request_token(request):
+    oauth_server, oauth_request = initialize_server_request(request)
+    
+    if oauth_server is None:
+        return INVALID_PARAMS_RESPONSE
+    try:
+        token = oauth_server.fetch_request_token(oauth_request)
+
+        response = HttpResponse(token.to_string())
+    except oauth.OAuthError, err:
+        response = send_oauth_error(err)
+
+    return response
+
+def oauth_auth_view(request, token, callback, params):
+    return HttpResponse("Just a fake view for auth. %s, %s, %s" % (token, callback, params))
+
+ login_required
+def oauth_user_auth(request):
+    oauth_server, oauth_request = initialize_server_request(request)
+    
+    if oauth_request is None:
+        return INVALID_PARAMS_RESPONSE
+        
+    try:
+        token = oauth_server.fetch_request_token(oauth_request)
+    except oauth.OAuthError, err:
+        return send_oauth_error(err)
+        
+    try:
+        callback = oauth_server.get_callback(oauth_request)
+    except:
+        callback = None
+        
+    if request.method == "GET":
+        request.session['oauth'] = token.key
+        params = oauth_request.get_normalized_parameters()
+
+        oauth_view = getattr(settings, 'OAUTH_AUTH_VIEW', 'oauth_auth_view')
+
+        return get_callable(oauth_view)(request, token, callback, params)
+    elif request.method == "POST":
+        if request.session.get('oauth', '') == token.key:
+            request.session['oauth'] = ''
+            
+            try:
+                if int(request.POST.get('authorize_access', '0')):
+                    token = oauth_server.authorize_token(token, request.user)
+                    args = '?'+token.to_string(only_key=True)
+                else:
+                    args = '?error=%s' % 'Access not granted by user.'
+                
+                if not callback:
+                    callback = getattr(settings, 'OAUTH_CALLBACK_VIEW')
+                    return get_callable(callback)(request, token)
+                    
+                response = HttpResponseRedirect(callback+args)
+                    
+            except oauth.OAuthError, err:
+                response = send_oauth_error(err)
+        else:
+            response = HttpResponse('Action not allowed.')
+            
+        return response
+
+def oauth_access_token(request):
+    oauth_server, oauth_request = initialize_server_request(request)
+    
+    if oauth_request is None:
+        return INVALID_PARAMS_RESPONSE
+        
+    try:
+        token = oauth_server.fetch_access_token(oauth_request)
+        return HttpResponse(token.to_string())
+    except oauth.OAuthError, err:
+        return send_oauth_error(err)
+
+INVALID_PARAMS_RESPONSE = send_oauth_error(oauth.OAuthError('Invalid request parameters.'))
+                
+class OAuthAuthentication(object):
+    """
+    OAuth authentication. Based on work by Leah Culver.
+    """
+    def __init__(self, realm='API'):
+        self.realm = realm
+        self.builder = oauth.build_authenticate_header
+    
+    def is_authenticated(self, request):
+        """
+        Checks whether a means of specifying authentication
+        is provided, and if so, if it is a valid token.
+        
+        Read the documentation on `HttpBasicAuthentication`
+        for more information about what goes on here.
+        """
+        if self.is_valid_request(request):
+            try:
+                consumer, token, parameters = self.validate_token(request)
+            except oauth.OAuthError, err:
+                print send_oauth_error(err)
+                return False
+
+            if consumer and token:
+                request.user = token.user
+                request.throttle_extra = token.consumer.id
+                return True
+            
+        return False
+        
+    def challenge(self):
+        """
+        Returns a 401 response with a small bit on
+        what OAuth is, and where to learn more about it.
+        
+        When this was written, browsers did not understand
+        OAuth authentication on the browser side, and hence
+        the helpful template we render. Maybe some day in the
+        future, browsers will take care of this stuff for us
+        and understand the 401 with the realm we give it.
+        """
+        response = HttpResponse()
+        response.status_code = 401
+        realm = 'API'
+
+        for k, v in self.builder(realm=realm).iteritems():
+            response[k] = v
+
+        tmpl = loader.render_to_string('oauth/challenge.html',
+            { 'MEDIA_URL': settings.MEDIA_URL })
+
+        response.content = tmpl
+
+        return response
+        
+    @staticmethod
+    def is_valid_request(request):
+        """
+        Checks whether the required parameters are either in
+        the http-authorization header sent by some clients,
+        which is by the way the preferred method according to
+        OAuth spec, but otherwise fall back to `GET` and `POST`.
+        """
+        must_have = [ 'oauth_'+s for s in [
+            'consumer_key', 'token', 'signature',
+            'signature_method', 'timestamp', 'nonce' ] ]
+        
+        is_in = lambda l: all([ (p in l) for p in must_have ])
+
+        auth_params = request.META.get("HTTP_AUTHORIZATION", "")
+        req_params = request.REQUEST
+             
+        return is_in(auth_params) or is_in(req_params)
+        
+    @staticmethod
+    def validate_token(request, check_timestamp=True, check_nonce=True):
+        oauth_server, oauth_request = initialize_server_request(request)
+        return oauth_server.verify_request(oauth_request)
+        
\ No newline at end of file
diff --git a/lib/piston/decorator.py b/lib/piston/decorator.py
new file mode 100755
index 0000000..f8dc3b8
--- /dev/null
+++ b/lib/piston/decorator.py
@@ -0,0 +1,186 @@
+"""
+Decorator module, see
+http://www.phyast.pitt.edu/~micheles/python/documentation.html
+for the documentation and below for the licence.
+"""
+
+## The basic trick is to generate the source code for the decorated function
+## with the right signature and to evaluate it.
+## Uncomment the statement 'print >> sys.stderr, func_src'  in _decorator
+## to understand what is going on.
+
+__all__ = ["decorator", "new_wrapper", "getinfo"]
+
+import inspect, sys
+
+try:
+    set
+except NameError:
+    from sets import Set as set
+
+def getinfo(func):
+    """
+    Returns an info dictionary containing:
+    - name (the name of the function : str)
+    - argnames (the names of the arguments : list)
+    - defaults (the values of the default arguments : tuple)
+    - signature (the signature : str)
+    - doc (the docstring : str)
+    - module (the module name : str)
+    - dict (the function __dict__ : str)
+    
+    >>> def f(self, x=1, y=2, *args, **kw): pass
+
+    >>> info = getinfo(f)
+
+    >>> info["name"]
+    'f'
+    >>> info["argnames"]
+    ['self', 'x', 'y', 'args', 'kw']
+    
+    >>> info["defaults"]
+    (1, 2)
+
+    >>> info["signature"]
+    'self, x, y, *args, **kw'
+    """
+    assert inspect.ismethod(func) or inspect.isfunction(func)
+    regargs, varargs, varkwargs, defaults = inspect.getargspec(func)
+    argnames = list(regargs)
+    if varargs:
+        argnames.append(varargs)
+    if varkwargs:
+        argnames.append(varkwargs)
+    signature = inspect.formatargspec(regargs, varargs, varkwargs, defaults,
+                                      formatvalue=lambda value: "")[1:-1]
+    return dict(name=func.__name__, argnames=argnames, signature=signature,
+                defaults = func.func_defaults, doc=func.__doc__,
+                module=func.__module__, dict=func.__dict__,
+                globals=func.func_globals, closure=func.func_closure)
+
+# akin to functools.update_wrapper
+def update_wrapper(wrapper, model, infodict=None):
+    infodict = infodict or getinfo(model)
+    try:
+        wrapper.__name__ = infodict['name']
+    except: # Python version < 2.4
+        pass
+    wrapper.__doc__ = infodict['doc']
+    wrapper.__module__ = infodict['module']
+    wrapper.__dict__.update(infodict['dict'])
+    wrapper.func_defaults = infodict['defaults']
+    wrapper.undecorated = model
+    return wrapper
+
+def new_wrapper(wrapper, model):
+    """
+    An improvement over functools.update_wrapper. The wrapper is a generic
+    callable object. It works by generating a copy of the wrapper with the 
+    right signature and by updating the copy, not the original.
+    Moreovoer, 'model' can be a dictionary with keys 'name', 'doc', 'module',
+    'dict', 'defaults'.
+    """
+    if isinstance(model, dict):
+        infodict = model
+    else: # assume model is a function
+        infodict = getinfo(model)
+    assert not '_wrapper_' in infodict["argnames"], (
+        '"_wrapper_" is a reserved argument name!')
+    src = "lambda %(signature)s: _wrapper_(%(signature)s)" % infodict
+    funcopy = eval(src, dict(_wrapper_=wrapper))
+    return update_wrapper(funcopy, model, infodict)
+
+# helper used in decorator_factory
+def __call__(self, func):
+    infodict = getinfo(func)
+    for name in ('_func_', '_self_'):
+        assert not name in infodict["argnames"], (
+           '%s is a reserved argument name!' % name)
+    src = "lambda %(signature)s: _self_.call(_func_, %(signature)s)"
+    new = eval(src % infodict, dict(_func_=func, _self_=self))
+    return update_wrapper(new, func, infodict)
+
+def decorator_factory(cls):
+    """
+    Take a class with a ``.caller`` method and return a callable decorator
+    object. It works by adding a suitable __call__ method to the class;
+    it raises a TypeError if the class already has a nontrivial __call__
+    method.
+    """
+    attrs = set(dir(cls))
+    if '__call__' in attrs:
+        raise TypeError('You cannot decorate a class with a nontrivial '
+                        '__call__ method')
+    if 'call' not in attrs:
+        raise TypeError('You cannot decorate a class without a '
+                        '.call method')
+    cls.__call__ = __call__
+    return cls
+
+def decorator(caller):
+    """
+    General purpose decorator factory: takes a caller function as
+    input and returns a decorator with the same attributes.
+    A caller function is any function like this::
+
+     def caller(func, *args, **kw):
+         # do something
+         return func(*args, **kw)
+    
+    Here is an example of usage:
+
+    >>> @decorator
+    ... def chatty(f, *args, **kw):
+    ...     print "Calling %r" % f.__name__
+    ...     return f(*args, **kw)
+
+    >>> chatty.__name__
+    'chatty'
+    
+    >>> @chatty
+    ... def f(): pass
+    ...
+    >>> f()
+    Calling 'f'
+
+    decorator can also take in input a class with a .caller method; in this
+    case it converts the class into a factory of callable decorator objects.
+    See the documentation for an example.
+    """
+    if inspect.isclass(caller):
+        return decorator_factory(caller)
+    def _decorator(func): # the real meat is here
+        infodict = getinfo(func)
+        argnames = infodict['argnames']
+        assert not ('_call_' in argnames or '_func_' in argnames), (
+            'You cannot use _call_ or _func_ as argument names!')
+        src = "lambda %(signature)s: _call_(_func_, %(signature)s)" % infodict
+        # import sys; print >> sys.stderr, src # for debugging purposes
+        dec_func = eval(src, dict(_func_=func, _call_=caller))
+        return update_wrapper(dec_func, func, infodict)
+    return update_wrapper(_decorator, caller)
+
+if __name__ == "__main__":
+    import doctest; doctest.testmod()
+
+##########################     LEGALESE    ###############################
+      
+##   Redistributions of source code must retain the above copyright 
+##   notice, this list of conditions and the following disclaimer.
+##   Redistributions in bytecode form must reproduce the above copyright
+##   notice, this list of conditions and the following disclaimer in
+##   the documentation and/or other materials provided with the
+##   distribution. 
+
+##   THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+##   "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+##   LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+##   A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+##   HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+##   INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+##   BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
+##   OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+##   ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
+##   TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
+##   USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
+##   DAMAGE.
diff --git a/lib/piston/doc.py b/lib/piston/doc.py
new file mode 100644
index 0000000..02c54af
--- /dev/null
+++ b/lib/piston/doc.py
@@ -0,0 +1,90 @@
+import inspect, handler
+
+def generate_doc(handler_cls):
+    """
+    Returns a `HandlerDocumentation` object
+    for the given handler. Use this to generate
+    documentation for your API.
+    """
+    if not type(handler_cls) is handler.HandlerMetaClass:
+        raise ValueError("Give me handler, not %s" % type(handler_cls))
+        
+    return HandlerDocumentation(handler_cls)
+    
+class HandlerMethod(object):
+    def __init__(self, method, stale=False):
+        self.method = method
+        self.stale = stale
+        
+    def iter_args(self):
+        args, _, _, defaults = inspect.getargspec(self.method)
+
+        for idx, arg in enumerate(args):
+            if arg in ('self', 'request', 'form'):
+                continue
+
+            didx = len(args)-idx
+
+            if defaults and len(defaults) >= didx:
+                yield (arg, str(defaults[-didx]))
+            else:
+                yield (arg, None)
+        
+    def get_signature(self, parse_optional=True):
+        spec = ""
+
+        for argn, argdef in self.iter_args():
+            spec += argn
+            
+            if argdef:
+                spec += '=%s' % argdef
+            
+            spec += ', '
+            
+        spec = spec.rstrip(", ")
+        
+        if parse_optional:
+            return spec.replace("=None", "=<optional>")
+            
+        return spec
+
+    signature = property(get_signature)
+        
+    def get_doc(self):
+        return inspect.getdoc(self.method)
+    
+    doc = property(get_doc)
+    
+    def get_name(self):
+        return self.method.__name__
+        
+    name = property(get_name)
+    
+    def __repr__(self):
+        return "<Method: %s>" % self.name
+    
+class HandlerDocumentation(object):
+    def __init__(self, handler):
+        self.handler = handler
+        
+    def get_methods(self, include_default=False):
+        for method in "read create update delete".split():
+            met = getattr(self.handler, method)
+            stale = inspect.getmodule(met) is handler
+
+            if met and (not stale or include_default):
+                yield HandlerMethod(met, stale)
+        
+    @property
+    def is_anonymous(self):
+        return False
+
+    def get_model(self):
+        return getattr(self, 'model', None)
+    
+    @property
+    def name(self):
+        return self.handler.__name__
+    
+    def __repr__(self):
+        return u'<Documentation for "%s">' % self.name
diff --git a/lib/piston/emitters.py b/lib/piston/emitters.py
new file mode 100644
index 0000000..7190ca1
--- /dev/null
+++ b/lib/piston/emitters.py
@@ -0,0 +1,326 @@
+import types, decimal, types, re, inspect
+
+try:
+    # yaml isn't standard with python.  It shouldn't be required if it
+    # isn't used.
+    import yaml
+except ImportError:
+    yaml = None
+
+from django.db.models.query import QuerySet
+from django.db.models import Model, permalink
+from django.utils import simplejson
+from django.utils.xmlutils import SimplerXMLGenerator
+from django.utils.encoding import smart_unicode
+from django.core.serializers.json import DateTimeAwareJSONEncoder
+from django.http import HttpResponse
+
+from utils import HttpStatusCode
+
+try:
+    import cStringIO as StringIO
+except ImportError:
+    import StringIO
+
+try:
+    import cPickle as pickle
+except ImportError:
+    import pickle
+
+class Emitter(object):
+    """
+    Super emitter. All other emitters should subclass
+    this one. It has the `construct` method which
+    conveniently returns a serialized `dict`. This is
+    usually the only method you want to use in your
+    emitter. See below for examples.
+    """
+    EMITTERS = { }
+
+    def __init__(self, payload, typemapper, handler, fields=(), anonymous=True):
+        self.typemapper = typemapper
+        self.data = payload
+        self.handler = handler
+        self.fields = fields
+        self.anonymous = anonymous
+        
+        if isinstance(self.data, Exception):
+            raise
+    
+    def construct(self):
+        """
+        Recursively serialize a lot of types, and
+        in cases where it doesn't recognize the type,
+        it will fall back to Django's `smart_unicode`.
+        
+        Returns `dict`.
+        """
+        def _any(thing, fields=()):
+            """
+            Dispatch, all types are routed through here.
+            """
+            ret = None
+            
+            if isinstance(thing, (tuple, list, QuerySet)):
+                ret = _list(thing)
+            elif isinstance(thing, dict):
+                ret = _dict(thing)
+            elif isinstance(thing, decimal.Decimal):
+                ret = str(thing)
+            elif isinstance(thing, Model):
+                ret = _model(thing, fields=fields)
+            elif isinstance(thing, HttpResponse):
+                raise HttpStatusCode(thing.content, code=thing.status_code)
+            elif isinstance(thing, types.FunctionType):
+                if not inspect.getargspec(thing)[0]:
+                    ret = _any(thing())
+            else:
+                ret = smart_unicode(thing, strings_only=True)
+
+            return ret
+
+        def _fk(data, field):
+            """
+            Foreign keys.
+            """
+            return _any(getattr(data, field.name))
+        
+        def _related(data, fields=()):
+            """
+            Foreign keys.
+            """
+            return [ _model(m, fields) for m in data.iterator() ]
+        
+        def _m2m(data, field, fields=()):
+            """
+            Many to many (re-route to `_model`.)
+            """
+            return [ _model(m, fields) for m in getattr(data, field.name).iterator() ]
+        
+        def _model(data, fields=()):
+            """
+            Models. Will respect the `fields` and/or
+            `exclude` on the handler (see `typemapper`.)
+            """
+            ret = { }
+            
+            if self.in_typemapper(type(data), self.anonymous) or fields:
+
+                v = lambda f: getattr(data, f.attname)
+
+                if not fields:
+                    """
+                    Fields was not specified, try to find teh correct
+                    version in the typemapper we were sent.
+                    """
+                    mapped = self.in_typemapper(type(data), self.anonymous)
+                    get_fields = set(mapped.fields)
+                    exclude_fields = set(mapped.exclude)
+                
+                    if not get_fields:
+                        get_fields = set([ f.attname.replace("_id", "", 1)
+                            for f in data._meta.fields ])
+                
+                    # sets can be negated.
+                    for exclude in exclude_fields:
+                        if isinstance(exclude, basestring):
+                            get_fields.discard(exclude)
+                        elif isinstance(exclude, re._pattern_type):
+                            for field in get_fields.copy():
+                                if exclude.match(field):
+                                    get_fields.discard(field)
+                                    
+                else:
+                    get_fields = set(fields)
+
+                for f in data._meta.local_fields:
+                    if f.serialize:
+                        if not f.rel:
+                            if f.attname in get_fields:
+                                ret[f.attname] = _any(v(f))
+                                get_fields.remove(f.attname)
+                        else:
+                            if f.attname[:-3] in get_fields:
+                                ret[f.name] = _fk(data, f)
+                                get_fields.remove(f.name)
+                
+                for mf in data._meta.many_to_many:
+                    if mf.serialize:
+                        if mf.attname in get_fields:
+                            ret[mf.name] = _m2m(data, mf)
+                            get_fields.remove(mf.name)
+                
+                # try to get the remainder of fields
+                for maybe_field in get_fields:
+
+                    if isinstance(maybe_field, (list, tuple)):
+                        model, fields = maybe_field
+                        inst = getattr(data, model, None)
+
+                        if inst:
+                            if hasattr(inst, 'all'):
+                                ret[model] = _related(inst, fields)
+                            else:
+                                ret[model] = _model(inst, fields)
+
+                    else:                    
+                        maybe = getattr(data, maybe_field, None)
+                        if maybe:
+                            if isinstance(maybe, (int, basestring)):
+                                ret[maybe_field] = _any(maybe)
+                        else:
+                            handler_f = getattr(self.handler, maybe_field, None)
+
+                            if handler_f:
+                                ret[maybe_field] = handler_f(data)
+
+            else:
+                for f in data._meta.fields:
+                    ret[f.attname] = _any(getattr(data, f.attname))
+                
+                fields = dir(data.__class__) + ret.keys()
+                add_ons = [k for k in dir(data) if k not in fields]
+                
+                for k in add_ons:
+                    ret[k] = _any(getattr(data, k))
+            
+            # resouce uri
+            if type(data) in self.typemapper.keys():
+                handler = self.typemapper.get(type(data))
+                if hasattr(handler, 'resource_uri'):
+                    url_id, fields = handler.resource_uri()
+                    ret['resource_uri'] = permalink( lambda: (url_id, 
+                        (getattr(data, f) for f in fields) ) )()
+            
+            if hasattr(data, 'get_api_url') and 'resource_uri' not in ret:
+                try: ret['resource_uri'] = data.get_api_url()
+                except: pass
+            
+            # absolute uri
+            if hasattr(data, 'get_absolute_url'):
+                try: ret['absolute_uri'] = data.get_absolute_url()
+                except: pass
+            
+            return ret
+        
+        def _list(data):
+            """
+            Lists.
+            """
+            return [ _any(v) for v in data ]
+            
+        def _dict(data):
+            """
+            Dictionaries.
+            """
+            return dict([ (k, _any(v)) for k, v in data.iteritems() ])
+            
+        # Kickstart the seralizin'.
+        return _any(self.data, self.fields)
+    
+    def in_typemapper(self, model, anonymous):
+        for klass, (km, is_anon) in self.typemapper.iteritems():
+            if model is km and is_anon is anonymous:
+                return klass
+        
+    def render(self):
+        """
+        This super emitter does not implement `render`,
+        this is a job for the specific emitter below.
+        """
+        raise NotImplementedError("Please implement render.")
+        
+    @classmethod
+    def get(cls, format):
+        """
+        Gets an emitter, returns the class and a content-type.
+        """
+        if cls.EMITTERS.has_key(format):
+            return cls.EMITTERS.get(format)
+
+        raise ValueError("No emitters found for type %s" % format)
+    
+    @classmethod
+    def register(cls, name, klass, content_type='text/plain'):
+        """
+        Register an emitter.
+        
+        Parameters::
+         - `name`: The name of the emitter ('json', 'xml', 'yaml', ...)
+         - `klass`: The emitter class.
+         - `content_type`: The content type to serve response as.
+        """
+        cls.EMITTERS[name] = (klass, content_type)
+        
+    @classmethod
+    def unregister(cls, name):
+        """
+        Remove an emitter from the registry. Useful if you don't
+        want to provide output in one of the built-in emitters.
+        """
+        return cls.EMITTERS.pop(name, None)
+    
+class XMLEmitter(Emitter):
+    def _to_xml(self, xml, data):
+        if isinstance(data, (list, tuple)):
+            for item in data:
+                self._to_xml(xml, item)
+        elif isinstance(data, dict):
+            for key, value in data.iteritems():
+                xml.startElement(key, {})
+                self._to_xml(xml, value)
+                xml.endElement(key)
+        else:
+            xml.characters(smart_unicode(data))
+
+    def render(self, request):
+        stream = StringIO.StringIO()
+        
+        xml = SimplerXMLGenerator(stream, "utf-8")
+        xml.startDocument()
+        xml.startElement("response", {})
+        
+        self._to_xml(xml, self.construct())
+        
+        xml.endElement("response")
+        xml.endDocument()
+        
+        return stream.getvalue()
+
+Emitter.register('xml', XMLEmitter, 'text/xml; charset=utf-8')
+
+class JSONEmitter(Emitter):
+    """
+    JSON emitter, understands timestamps.
+    """
+    def render(self, request):
+        cb = request.GET.get('callback')
+        seria = simplejson.dumps(self.construct(), cls=DateTimeAwareJSONEncoder)
+
+        # Callback
+        if cb:
+            return '%s(%s)' % (cb, seria)
+
+        return seria
+    
+Emitter.register('json', JSONEmitter, 'application/json; charset=utf-8')
+    
+class YAMLEmitter(Emitter):
+    """
+    YAML emitter, uses `safe_dump` to omit the
+    specific types when outputting to non-Python.
+    """
+    def render(self, request):
+        return yaml.safe_dump(self.construct())
+
+if yaml:  # Only register yaml if it was import successfully.
+    Emitter.register('yaml', YAMLEmitter, 'application/x-yaml; charset=utf-8')
+
+class PickleEmitter(Emitter):
+    """
+    Emitter that returns Python pickled.
+    """
+    def render(self, request):
+        return pickle.dumps(self.construct())
+        
+Emitter.register('pickle', PickleEmitter, 'application/octet-stream')
diff --git a/lib/piston/forms.py b/lib/piston/forms.py
new file mode 100644
index 0000000..727f997
--- /dev/null
+++ b/lib/piston/forms.py
@@ -0,0 +1,19 @@
+from django import forms
+
+class Form(forms.Form):
+    pass
+    
+class ModelForm(forms.ModelForm):
+    """
+    Subclass of `forms.ModelForm` which makes sure
+    that the initial values are present in the form
+    data, so you don't have to send all old values
+    for the form to actually validate. Django does not
+    do this on its own, which is really annoying.
+    """
+    def merge_from_initial(self):
+        self.data._mutable = True
+        filt = lambda v: v not in self.data.keys()
+        for field in filter(filt, getattr(self.Meta, 'fields', ())):
+            self.data[field] = self.initial.get(field, None)
+
diff --git a/lib/piston/handler.py b/lib/piston/handler.py
new file mode 100644
index 0000000..f983c63
--- /dev/null
+++ b/lib/piston/handler.py
@@ -0,0 +1,101 @@
+from piston.utils import rc
+
+typemapper = { }
+
+class HandlerMetaClass(type):
+    """
+    Metaclass that keeps a registry of class -> handler
+    mappings.
+    """
+    def __new__(cls, name, bases, attrs):
+        new_cls = type.__new__(cls, name, bases, attrs)
+        
+        if hasattr(new_cls, 'model'):
+            typemapper[new_cls] = (new_cls.model, new_cls.is_anonymous)
+        
+        return new_cls
+
+class BaseHandler(object):
+    """
+    Basehandler that gives you CRUD for free.
+    You are supposed to subclass this for specific
+    functionality.
+    
+    All CRUD methods (`read`/`update`/`create`/`delete`)
+    receive a request as the first argument from the
+    resource. Use this for checking `request.user`, etc.
+    """
+    __metaclass__ = HandlerMetaClass
+    
+    allowed_methods = ('GET', 'POST', 'PUT', 'DELETE')
+    anonymous = is_anonymous = False
+    exclude = ( 'id', )
+    fields =  ( )
+    
+    def flatten_dict(self, dct):
+        return dict([ (str(k), dct.get(k)) for k in dct.keys() ])
+    
+    def has_model(self):
+        return hasattr(self, 'model')
+    
+    def value_from_tuple(tu, name):
+        for int_, n in tu:
+            if n == name:
+                return int_
+        return None
+    
+    def exists(self, **kwargs):
+        if not self.has_model():
+            raise NotImplementedError
+        
+        try:
+            self.model.objects.get(**kwargs)
+            return True
+        except self.model.DoesNotExist:
+            return False
+    
+    def read(self, request, *args, **kwargs):
+        if not self.has_model():
+            return rc.NOT_IMPLEMENTED
+        
+        return self.model.objects.filter(*args, **kwargs)
+    
+    def create(self, request, *args, **kwargs):
+        if not self.has_model():
+            return rc.NOT_IMPLEMENTED
+        
+        attrs = self.flatten_dict(request.POST)
+        
+        try:
+            inst = self.model.objects.get(**attrs)
+            return rc.DUPLICATE_ENTRY
+        except self.model.DoesNotExist:
+            inst = self.model(**attrs)
+            inst.save()
+            return inst
+    
+    def update(self, request, *args, **kwargs):
+        # TODO: This doesn't work automatically yet.
+        return rc.NOT_IMPLEMENTED
+    
+    def delete(self, request, *args, **kwargs):
+        if not self.has_model():
+            raise NotImplementedError
+
+        try:
+            inst = self.model.objects.get(*args, **kwargs)
+
+            inst.delete()
+
+            return rc.DELETED
+        except self.model.MultipleObjectsReturned:
+            return rc.DUPLICATE_ENTRY
+        except self.model.DoesNotExist:
+            return rc.NOT_HERE
+        
+class AnonymousBaseHandler(BaseHandler):
+    """
+    Anonymous handler.
+    """
+    is_anonymous = True
+    allowed_methods = ('GET',)
diff --git a/lib/piston/managers.py b/lib/piston/managers.py
new file mode 100644
index 0000000..79ebdfb
--- /dev/null
+++ b/lib/piston/managers.py
@@ -0,0 +1,52 @@
+from django.db import models
+from django.contrib.auth.models import User
+
+KEY_SIZE = 16
+SECRET_SIZE = 16
+
+class ConsumerManager(models.Manager):
+    def create_consumer(self, name, description=None, user=None):
+        """
+        Shortcut to create a consumer with random key/secret.
+        """
+        consumer, created = self.get_or_create(name=name)
+
+        if user:
+            consumer.user = user
+
+        if description:
+            consumer.description = description
+
+        if created:
+            consumer.generate_random_codes()
+
+        return consumer
+    
+    _default_consumer = None
+
+class ResourceManager(models.Manager):
+    _default_resource = None
+
+    def get_default_resource(self, name):
+        """
+        Add cache if you use a default resource.
+        """
+        if not self._default_resource:
+            self._default_resource = self.get(name=name)
+
+        return self._default_resource        
+
+class TokenManager(models.Manager):
+    def create_token(self, consumer, token_type, timestamp, user=None):
+        """
+        Shortcut to create a token with random key/secret.
+        """
+        token, created = self.get_or_create(consumer=consumer, 
+                                            token_type=token_type, 
+                                            timestamp=timestamp,
+                                            user=user)
+
+        if created:
+            token.generate_random_codes()
+
+        return token
\ No newline at end of file
diff --git a/lib/piston/models.py b/lib/piston/models.py
new file mode 100644
index 0000000..890b73e
--- /dev/null
+++ b/lib/piston/models.py
@@ -0,0 +1,146 @@
+import urllib
+from django.db import models
+from django.contrib.auth.models import User
+from django.contrib import admin
+from django.conf import settings
+from django.core.mail import send_mail, mail_admins
+from django.template import loader
+
+from managers import TokenManager, ConsumerManager, ResourceManager
+
+KEY_SIZE = 18
+SECRET_SIZE = 32
+
+CONSUMER_STATES = (
+    ('pending', 'Pending approval'),
+    ('accepted', 'Accepted'),
+    ('canceled', 'Canceled'),
+)
+
+class Nonce(models.Model):
+    token_key = models.CharField(max_length=KEY_SIZE)
+    consumer_key = models.CharField(max_length=KEY_SIZE)
+    key = models.CharField(max_length=255)
+    
+    def __unicode__(self):
+        return u"Nonce %s for %s" % (self.key, self.consumer_key)
+
+admin.site.register(Nonce)
+
+class Resource(models.Model):
+    name = models.CharField(max_length=255)
+    url = models.TextField(max_length=2047)
+    is_readonly = models.BooleanField(default=True)
+    
+    objects = ResourceManager()
+
+    def __unicode__(self):
+        return u"Resource %s with url %s" % (self.name, self.url)
+
+admin.site.register(Resource)
+
+class Consumer(models.Model):
+    name = models.CharField(max_length=255)
+    description = models.TextField()
+
+    key = models.CharField(max_length=KEY_SIZE)
+    secret = models.CharField(max_length=SECRET_SIZE)
+
+    status = models.CharField(max_length=16, choices=CONSUMER_STATES, default='pending')
+    user = models.ForeignKey(User, null=True, blank=True, related_name='consumers')
+
+    objects = ConsumerManager()
+        
+    def __unicode__(self):
+        return u"Consumer %s with key %s" % (self.name, self.key)
+
+    def generate_random_codes(self):
+        key = User.objects.make_random_password(length=KEY_SIZE)
+
+        secret = User.objects.make_random_password(length=SECRET_SIZE)
+
+        while Consumer.objects.filter(key__exact=key, secret__exact=secret).count():
+            secret = User.objects.make_random_password(length=SECRET_SIZE)
+
+        self.key = key
+        self.secret = secret
+        self.save()
+
+    # -- 
+    
+    def save(self, **kwargs):
+        super(Consumer, self).save(**kwargs)
+        
+        if self.id and self.user:
+            subject = "API Consumer"
+            rcpt = [ self.user.email, ]
+
+            if self.status == "accepted":
+                template = "api/mails/consumer_accepted.txt"
+                subject += " was accepted!"
+            elif self.status == "canceled":
+                template = "api/mails/consumer_canceled.txt"
+                subject += " has been canceled"
+            else:
+                template = "api/mails/consumer_pending.txt"
+                subject += " application received"
+                
+                for admin in settings.ADMINS:
+                    bcc.append(admin[1])
+
+            body = loader.render_to_string(template, 
+                    { 'consumer': self, 'user': self.user })
+                    
+            send_mail(subject, body, settings.DEFAULT_FROM_EMAIL, 
+                        rcpt, fail_silently=True)
+            
+            if self.status == 'pending':
+                mail_admins(subject, body, fail_silently=True)
+                        
+            if settings.DEBUG:
+                print "Mail being sent, to=%s" % rcpt
+                print "Subject: %s" % subject
+                print body
+
+admin.site.register(Consumer)
+
+class Token(models.Model):
+    REQUEST = 1
+    ACCESS = 2
+    TOKEN_TYPES = ((REQUEST, u'Request'), (ACCESS, u'Access'))
+    
+    key = models.CharField(max_length=KEY_SIZE)
+    secret = models.CharField(max_length=SECRET_SIZE)
+    token_type = models.IntegerField(choices=TOKEN_TYPES)
+    timestamp = models.IntegerField()
+    is_approved = models.BooleanField(default=False)
+    
+    user = models.ForeignKey(User, null=True, blank=True, related_name='tokens')
+    consumer = models.ForeignKey(Consumer)
+    
+    objects = TokenManager()
+    
+    def __unicode__(self):
+        return u"%s Token %s for %s" % (self.get_token_type_display(), self.key, self.consumer)
+
+    def to_string(self, only_key=False):
+        token_dict = {
+            'oauth_token': self.key, 
+            'oauth_token_secret': self.secret
+        }
+        if only_key:
+            del token_dict['oauth_token_secret']
+        return urllib.urlencode(token_dict)
+
+    def generate_random_codes(self):
+        key = User.objects.make_random_password(length=KEY_SIZE)
+        secret = User.objects.make_random_password(length=SECRET_SIZE)
+
+        while Token.objects.filter(key__exact=key, secret__exact=secret).count():
+            secret = User.objects.make_random_password(length=SECRET_SIZE)
+
+        self.key = key
+        self.secret = secret
+        self.save()
+        
+admin.site.register(Token)
\ No newline at end of file
diff --git a/lib/piston/oauth.py b/lib/piston/oauth.py
new file mode 100644
index 0000000..6090800
--- /dev/null
+++ b/lib/piston/oauth.py
@@ -0,0 +1,531 @@
+import cgi
+import urllib
+import time
+import random
+import urlparse
+import hmac
+import base64
+
+VERSION = '1.0' # Hi Blaine!
+HTTP_METHOD = 'GET'
+SIGNATURE_METHOD = 'PLAINTEXT'
+
+# Generic exception class
+class OAuthError(RuntimeError):
+    def get_message(self): 
+        return self._message
+
+    def set_message(self, message): 
+        self._message = message
+
+    message = property(get_message, set_message)
+
+    def __init__(self, message='OAuth error occured.'):
+        self.message = message
+
+# optional WWW-Authenticate header (401 error)
+def build_authenticate_header(realm=''):
+    return { 'WWW-Authenticate': 'OAuth realm="%s"' % realm }
+
+# url escape
+def escape(s):
+    # escape '/' too
+    return urllib.quote(s, safe='~')
+
+# util function: current timestamp
+# seconds since epoch (UTC)
+def generate_timestamp():
+    return int(time.time())
+
+# util function: nonce
+# pseudorandom number
+def generate_nonce(length=8):
+    return ''.join(str(random.randint(0, 9)) for i in range(length))
+
+# OAuthConsumer is a data type that represents the identity of the Consumer
+# via its shared secret with the Service Provider.
+class OAuthConsumer(object):
+    key = None
+    secret = None
+
+    def __init__(self, key, secret):
+        self.key = key
+        self.secret = secret
+
+# OAuthToken is a data type that represents an End User via either an access
+# or request token.     
+class OAuthToken(object):
+    # access tokens and request tokens
+    key = None
+    secret = None
+
+    '''
+    key = the token
+    secret = the token secret
+    '''
+    def __init__(self, key, secret):
+        self.key = key
+        self.secret = secret
+
+    def to_string(self):
+        return urllib.urlencode({'oauth_token': self.key, 'oauth_token_secret': self.secret})
+
+    # return a token from something like:
+    # oauth_token_secret=digg&oauth_token=digg
+    @staticmethod   
+    def from_string(s):
+        params = cgi.parse_qs(s, keep_blank_values=False)
+        key = params['oauth_token'][0]
+        secret = params['oauth_token_secret'][0]
+        return OAuthToken(key, secret)
+
+    def __str__(self):
+        return self.to_string()
+
+# OAuthRequest represents the request and can be serialized
+class OAuthRequest(object):
+    '''
+    OAuth parameters:
+        - oauth_consumer_key 
+        - oauth_token
+        - oauth_signature_method
+        - oauth_signature 
+        - oauth_timestamp 
+        - oauth_nonce
+        - oauth_version
+        ... any additional parameters, as defined by the Service Provider.
+    '''
+    parameters = None # oauth parameters
+    http_method = HTTP_METHOD
+    http_url = None
+    version = VERSION
+
+    def __init__(self, http_method=HTTP_METHOD, http_url=None, parameters=None):
+        self.http_method = http_method
+        self.http_url = http_url
+        self.parameters = parameters or {}
+
+    def set_parameter(self, parameter, value):
+        self.parameters[parameter] = value
+
+    def get_parameter(self, parameter):
+        try:
+            return self.parameters[parameter]
+        except:
+            raise OAuthError('Parameter not found: %s' % parameter)
+
+    def _get_timestamp_nonce(self):
+        return self.get_parameter('oauth_timestamp'), self.get_parameter('oauth_nonce')
+
+    # get any non-oauth parameters
+    def get_nonoauth_parameters(self):
+        parameters = {}
+        for k, v in self.parameters.iteritems():
+            # ignore oauth parameters
+            if k.find('oauth_') < 0:
+                parameters[k] = v
+        return parameters
+
+    # serialize as a header for an HTTPAuth request
+    def to_header(self, realm=''):
+        auth_header = 'OAuth realm="%s"' % realm
+        # add the oauth parameters
+        if self.parameters:
+            for k, v in self.parameters.iteritems():
+                auth_header += ', %s="%s"' % (k, escape(str(v)))
+        return {'Authorization': auth_header}
+
+    # serialize as post data for a POST request
+    def to_postdata(self):
+        return '&'.join('%s=%s' % (escape(str(k)), escape(str(v))) for k, v in self.parameters.iteritems())
+
+    # serialize as a url for a GET request
+    def to_url(self):
+        return '%s?%s' % (self.get_normalized_http_url(), self.to_postdata())
+
+    # return a string that consists of all the parameters that need to be signed
+    def get_normalized_parameters(self):
+        params = self.parameters
+        try:
+            # exclude the signature if it exists
+            del params['oauth_signature']
+        except:
+            pass
+        key_values = params.items()
+        # sort lexicographically, first after key, then after value
+        key_values.sort()
+        # combine key value pairs in string and escape
+        return '&'.join('%s=%s' % (escape(str(k)), escape(str(v))) for k, v in key_values)
+
+    # just uppercases the http method
+    def get_normalized_http_method(self):
+        return self.http_method.upper()
+
+    # parses the url and rebuilds it to be scheme://host/path
+    def get_normalized_http_url(self):
+        parts = urlparse.urlparse(self.http_url)
+        url_string = '%s://%s%s' % (parts[0], parts[1], parts[2]) # scheme, netloc, path
+        return url_string
+        
+    # set the signature parameter to the result of build_signature
+    def sign_request(self, signature_method, consumer, token):
+        # set the signature method
+        self.set_parameter('oauth_signature_method', signature_method.get_name())
+        # set the signature
+        self.set_parameter('oauth_signature', self.build_signature(signature_method, consumer, token))
+
+    def build_signature(self, signature_method, consumer, token):
+        # call the build signature method within the signature method
+        return signature_method.build_signature(self, consumer, token)
+
+    @staticmethod
+    def from_request(http_method, http_url, headers=None, parameters=None, query_string=None):
+        # combine multiple parameter sources
+        if parameters is None:
+            parameters = {}
+
+        # headers
+        if headers and 'HTTP_AUTHORIZATION' in headers:
+            auth_header = headers['HTTP_AUTHORIZATION']
+            # check that the authorization header is OAuth
+            if auth_header.index('OAuth') > -1:
+                try:
+                    # get the parameters from the header
+                    header_params = OAuthRequest._split_header(auth_header)
+                    parameters.update(header_params)
+                except:
+                    raise OAuthError('Unable to parse OAuth parameters from Authorization header.')
+
+        # GET or POST query string
+        if query_string:
+            query_params = OAuthRequest._split_url_string(query_string)
+            parameters.update(query_params)
+
+        # URL parameters
+        param_str = urlparse.urlparse(http_url)[4] # query
+        url_params = OAuthRequest._split_url_string(param_str)
+        parameters.update(url_params)
+
+        if parameters:
+            return OAuthRequest(http_method, http_url, parameters)
+
+        return None
+
+    @staticmethod
+    def from_consumer_and_token(oauth_consumer, token=None, http_method=HTTP_METHOD, http_url=None, parameters=None):
+        if not parameters:
+            parameters = {}
+
+        defaults = {
+            'oauth_consumer_key': oauth_consumer.key,
+            'oauth_timestamp': generate_timestamp(),
+            'oauth_nonce': generate_nonce(),
+            'oauth_version': OAuthRequest.version,
+        }
+
+        defaults.update(parameters)
+        parameters = defaults
+
+        if token:
+            parameters['oauth_token'] = token.key
+
+        return OAuthRequest(http_method, http_url, parameters)
+
+    @staticmethod
+    def from_token_and_callback(token, callback=None, http_method=HTTP_METHOD, http_url=None, parameters=None):
+        if not parameters:
+            parameters = {}
+
+        parameters['oauth_token'] = token.key
+
+        if callback:
+            parameters['oauth_callback'] = escape(callback)
+
+        return OAuthRequest(http_method, http_url, parameters)
+
+    # util function: turn Authorization: header into parameters, has to do some unescaping
+    @staticmethod
+    def _split_header(header):
+        params = {}
+        parts = header.split(',')
+        for param in parts:
+            # ignore realm parameter
+            if param.find('OAuth realm') > -1:
+                continue
+            # remove whitespace
+            param = param.strip()
+            # split key-value
+            param_parts = param.split('=', 1)
+            # remove quotes and unescape the value
+            params[param_parts[0]] = urllib.unquote(param_parts[1].strip('\"'))
+        return params
+    
+    # util function: turn url string into parameters, has to do some unescaping
+    @staticmethod
+    def _split_url_string(param_str):
+        parameters = cgi.parse_qs(param_str, keep_blank_values=False)
+        for k, v in parameters.iteritems():
+            parameters[k] = urllib.unquote(v[0])
+        return parameters
+
+# OAuthServer is a worker to check a requests validity against a data store
+class OAuthServer(object):
+    timestamp_threshold = 300 # in seconds, five minutes
+    version = VERSION
+    signature_methods = None
+    data_store = None
+
+    def __init__(self, data_store=None, signature_methods=None):
+        self.data_store = data_store
+        self.signature_methods = signature_methods or {}
+
+    def set_data_store(self, oauth_data_store):
+        self.data_store = data_store
+
+    def get_data_store(self):
+        return self.data_store
+
+    def add_signature_method(self, signature_method):
+        self.signature_methods[signature_method.get_name()] = signature_method
+        return self.signature_methods
+
+    # process a request_token request
+    # returns the request token on success
+    def fetch_request_token(self, oauth_request):
+        try:
+            # get the request token for authorization
+            token = self._get_token(oauth_request, 'request')
+        except OAuthError:
+            # no token required for the initial token request
+            version = self._get_version(oauth_request)
+            consumer = self._get_consumer(oauth_request)
+            self._check_signature(oauth_request, consumer, None)
+            # fetch a new token
+            token = self.data_store.fetch_request_token(consumer)
+        return token
+
+    # process an access_token request
+    # returns the access token on success
+    def fetch_access_token(self, oauth_request):
+        version = self._get_version(oauth_request)
+        consumer = self._get_consumer(oauth_request)
+        # get the request token
+        token = self._get_token(oauth_request, 'request')
+        self._check_signature(oauth_request, consumer, token)
+        new_token = self.data_store.fetch_access_token(consumer, token)
+        return new_token
+
+    # verify an api call, checks all the parameters
+    def verify_request(self, oauth_request):
+        # -> consumer and token
+        version = self._get_version(oauth_request)
+        consumer = self._get_consumer(oauth_request)
+        # get the access token
+        token = self._get_token(oauth_request, 'access')
+        self._check_signature(oauth_request, consumer, token)
+        parameters = oauth_request.get_nonoauth_parameters()
+        return consumer, token, parameters
+
+    # authorize a request token
+    def authorize_token(self, token, user):
+        return self.data_store.authorize_request_token(token, user)
+    
+    # get the callback url
+    def get_callback(self, oauth_request):
+        return oauth_request.get_parameter('oauth_callback')
+
+    # optional support for the authenticate header   
+    def build_authenticate_header(self, realm=''):
+        return {'WWW-Authenticate': 'OAuth realm="%s"' % realm}
+
+    # verify the correct version request for this server
+    def _get_version(self, oauth_request):
+        try:
+            version = oauth_request.get_parameter('oauth_version')
+        except:
+            version = VERSION
+        if version and version != self.version:
+            raise OAuthError('OAuth version %s not supported.' % str(version))
+        return version
+
+    # figure out the signature with some defaults
+    def _get_signature_method(self, oauth_request):
+        try:
+            signature_method = oauth_request.get_parameter('oauth_signature_method')
+        except:
+            signature_method = SIGNATURE_METHOD
+        try:
+            # get the signature method object
+            signature_method = self.signature_methods[signature_method]
+        except:
+            signature_method_names = ', '.join(self.signature_methods.keys())
+            raise OAuthError('Signature method %s not supported try one of the following: %s' % (signature_method, signature_method_names))
+
+        return signature_method
+
+    def _get_consumer(self, oauth_request):
+        consumer_key = oauth_request.get_parameter('oauth_consumer_key')
+        if not consumer_key:
+            raise OAuthError('Invalid consumer key.')
+        consumer = self.data_store.lookup_consumer(consumer_key)
+        if not consumer:
+            raise OAuthError('Invalid consumer.')
+        return consumer
+
+    # try to find the token for the provided request token key
+    def _get_token(self, oauth_request, token_type='access'):
+        token_field = oauth_request.get_parameter('oauth_token')
+        token = self.data_store.lookup_token(token_type, token_field)
+        if not token:
+            raise OAuthError('Invalid %s token: %s' % (token_type, token_field))
+        return token
+
+    def _check_signature(self, oauth_request, consumer, token):
+        timestamp, nonce = oauth_request._get_timestamp_nonce()
+        self._check_timestamp(timestamp)
+        self._check_nonce(consumer, token, nonce)
+        signature_method = self._get_signature_method(oauth_request)
+        try:
+            signature = oauth_request.get_parameter('oauth_signature')
+        except:
+            raise OAuthError('Missing signature.')
+        # validate the signature
+        valid_sig = signature_method.check_signature(oauth_request, consumer, token, signature)
+        if not valid_sig:
+            key, base = signature_method.build_signature_base_string(oauth_request, consumer, token)
+            raise OAuthError('Invalid signature. Expected signature base string: %s' % base)
+        built = signature_method.build_signature(oauth_request, consumer, token)
+
+    def _check_timestamp(self, timestamp):
+        # verify that timestamp is recentish
+        timestamp = int(timestamp)
+        now = int(time.time())
+        lapsed = now - timestamp
+        if lapsed > self.timestamp_threshold:
+            raise OAuthError('Expired timestamp: given %d and now %s has a greater difference than threshold %d' % (timestamp, now, self.timestamp_threshold))
+
+    def _check_nonce(self, consumer, token, nonce):
+        # verify that the nonce is uniqueish
+        nonce = self.data_store.lookup_nonce(consumer, token, nonce)
+        if nonce:
+            raise OAuthError('Nonce already used: %s' % str(nonce))
+
+# OAuthClient is a worker to attempt to execute a request
+class OAuthClient(object):
+    consumer = None
+    token = None
+
+    def __init__(self, oauth_consumer, oauth_token):
+        self.consumer = oauth_consumer
+        self.token = oauth_token
+
+    def get_consumer(self):
+        return self.consumer
+
+    def get_token(self):
+        return self.token
+
+    def fetch_request_token(self, oauth_request):
+        # -> OAuthToken
+        raise NotImplementedError
+
+    def fetch_access_token(self, oauth_request):
+        # -> OAuthToken
+        raise NotImplementedError
+
+    def access_resource(self, oauth_request):
+        # -> some protected resource
+        raise NotImplementedError
+
+# OAuthDataStore is a database abstraction used to lookup consumers and tokens
+class OAuthDataStore(object):
+
+    def lookup_consumer(self, key):
+        # -> OAuthConsumer
+        raise NotImplementedError
+
+    def lookup_token(self, oauth_consumer, token_type, token_token):
+        # -> OAuthToken
+        raise NotImplementedError
+
+    def lookup_nonce(self, oauth_consumer, oauth_token, nonce, timestamp):
+        # -> OAuthToken
+        raise NotImplementedError
+
+    def fetch_request_token(self, oauth_consumer):
+        # -> OAuthToken
+        raise NotImplementedError
+
+    def fetch_access_token(self, oauth_consumer, oauth_token):
+        # -> OAuthToken
+        raise NotImplementedError
+
+    def authorize_request_token(self, oauth_token, user):
+        # -> OAuthToken
+        raise NotImplementedError
+
+# OAuthSignatureMethod is a strategy class that implements a signature method
+class OAuthSignatureMethod(object):
+    def get_name(self):
+        # -> str
+        raise NotImplementedError
+
+    def build_signature_base_string(self, oauth_request, oauth_consumer, oauth_token):
+        # -> str key, str raw
+        raise NotImplementedError
+
+    def build_signature(self, oauth_request, oauth_consumer, oauth_token):
+        # -> str
+        raise NotImplementedError
+
+    def check_signature(self, oauth_request, consumer, token, signature):
+        built = self.build_signature(oauth_request, consumer, token)
+        return built == signature
+
+class OAuthSignatureMethod_HMAC_SHA1(OAuthSignatureMethod):
+
+    def get_name(self):
+        return 'HMAC-SHA1'
+        
+    def build_signature_base_string(self, oauth_request, consumer, token):
+        sig = (
+            escape(oauth_request.get_normalized_http_method()),
+            escape(oauth_request.get_normalized_http_url()),
+            escape(oauth_request.get_normalized_parameters()),
+        )
+
+        key = '%s&' % escape(consumer.secret)
+        if token:
+            key += escape(token.secret)
+        raw = '&'.join(sig)
+        return key, raw
+
+    def build_signature(self, oauth_request, consumer, token):
+        # build the base signature string
+        key, raw = self.build_signature_base_string(oauth_request, consumer, token)
+
+        # hmac object
+        try:
+            import hashlib # 2.5
+            hashed = hmac.new(key, raw, hashlib.sha1)
+        except:
+            import sha # deprecated
+            hashed = hmac.new(key, raw, sha)
+
+        # calculate the digest base 64
+        return base64.b64encode(hashed.digest())
+
+class OAuthSignatureMethod_PLAINTEXT(OAuthSignatureMethod):
+
+    def get_name(self):
+        return 'PLAINTEXT'
+
+    def build_signature_base_string(self, oauth_request, consumer, token):
+        # concatenate the consumer key and secret
+        sig = escape(consumer.secret) + '&'
+        if token:
+            sig = sig + escape(token.secret)
+        return sig
+
+    def build_signature(self, oauth_request, consumer, token):
+        return self.build_signature_base_string(oauth_request, consumer, token)
diff --git a/lib/piston/resource.py b/lib/piston/resource.py
new file mode 100644
index 0000000..bae36c2
--- /dev/null
+++ b/lib/piston/resource.py
@@ -0,0 +1,176 @@
+import sys, inspect
+
+from django.http import HttpResponse, Http404, HttpResponseNotAllowed, HttpResponseForbidden
+from django.views.debug import ExceptionReporter
+from django.views.decorators.vary import vary_on_headers
+from django.conf import settings
+from django.core.mail import send_mail, EmailMessage
+
+from emitters import Emitter
+from handler import typemapper
+from doc import HandlerMethod
+from utils import coerce_put_post, FormValidationError, HttpStatusCode, rc, format_error
+
+class NoAuthentication(object):
+    """
+    Authentication handler that always returns
+    True, so no authentication is needed, nor
+    initiated (`challenge` is missing.)
+    """
+    def is_authenticated(self, request):
+        return True
+
+class Resource(object):
+    """
+    Resource. Create one for your URL mappings, just
+    like you would with Django. Takes one argument,
+    the handler. The second argument is optional, and
+    is an authentication handler. If not specified,
+    `NoAuthentication` will be used by default.
+    """
+    callmap = { 'GET': 'read', 'POST': 'create', 
+                'PUT': 'update', 'DELETE': 'delete' }
+    
+    def __init__(self, handler, authentication=None):
+        if not callable(handler):
+            raise AttributeError, "Handler not callable."
+        
+        self.handler = handler()
+        
+        if not authentication:
+            self.authentication = NoAuthentication()
+        else:
+            self.authentication = authentication
+            
+        # Erroring
+        self.email_errors = getattr(settings, 'PISTON_EMAIL_ERRORS', True)
+        self.display_errors = getattr(settings, 'PISTON_DISPLAY_ERRORS', True)
+    
+    @vary_on_headers('Authorization')
+    def __call__(self, request, *args, **kwargs):
+        """
+        NB: Sends a `Vary` header so we don't cache requests
+        that are different (OAuth stuff in `Authorization` header.)
+        """
+        if not self.authentication.is_authenticated(request):
+            if self.handler.anonymous and callable(self.handler.anonymous):
+                handler = self.handler.anonymous()
+                anonymous = True
+            else:
+                return self.authentication.challenge()
+        else:
+            handler = self.handler
+            anonymous = False
+        
+        rm = request.method.upper()
+        
+        # Django's internal mechanism doesn't pick up
+        # PUT request, so we trick it a little here.
+        if rm == "PUT":
+            coerce_put_post(request)
+        
+        if not rm in handler.allowed_methods:
+            return HttpResponseNotAllowed(handler.allowed_methods)
+        
+        meth = getattr(handler, Resource.callmap.get(rm), None)
+        
+        if not meth:
+            raise Http404
+
+        # Support emitter both through (?P<emitter_format>) and ?format=emitter.
+        em_format = kwargs.pop('emitter_format', request.GET.get('format', 'json'))
+        
+        # Clean up the request object a bit, since we might
+        # very well have `oauth_`-headers in there, and we
+        # don't want to pass these along to the handler.
+        request = self.cleanup_request(request)
+        
+        try:
+            result = meth(request, *args, **kwargs)
+        except FormValidationError, form:
+            # TODO: Use rc.BAD_REQUEST here
+            return HttpResponse("Bad Request: %s" % form.errors, status=400)
+        except TypeError, e:
+            result = rc.BAD_REQUEST
+            hm = HandlerMethod(meth)
+            sig = hm.get_signature()
+
+            msg = 'Method signature does not match.\n\n'
+            
+            if sig:
+                msg += 'Signature should be: %s' % sig
+            else:
+                msg += 'Resource does not expect any parameters.'
+
+            if self.display_errors:                
+                msg += '\n\nException was: %s' % str(e)
+                
+            result.content = format_error(msg)
+        except HttpStatusCode, e:
+            result = e
+        except Exception, e:
+            """
+            On errors (like code errors), we'd like to be able to
+            give crash reports to both admins and also the calling
+            user. There's two setting parameters for this:
+            
+            Parameters::
+             - `PISTON_EMAIL_ERRORS`: Will send a Django formatted
+               error email to people in `settings.ADMINS`.
+             - `PISTON_DISPLAY_ERRORS`: Will return a simple traceback
+               to the caller, so he can tell you what error they got.
+               
+            If `PISTON_DISPLAY_ERRORS` is not enabled, the caller will
+            receive a basic "500 Internal Server Error" message.
+            """
+            if self.email_errors:
+                exc_type, exc_value, tb = sys.exc_info()
+                rep = ExceptionReporter(request, exc_type, exc_value, tb.tb_next)
+
+                self.email_exception(rep)
+
+            if self.display_errors:
+                result = format_error('\n'.join(rep.format_exception()))
+            else:
+                raise
+
+        emitter, ct = Emitter.get(em_format)
+        srl = emitter(result, typemapper, handler, handler.fields, anonymous)
+        
+        try:
+            return HttpResponse(srl.render(request), mimetype=ct)
+        except HttpStatusCode, e:
+            return HttpResponse(e.message, status=e.code)
+
+    @staticmethod
+    def cleanup_request(request):
+        """
+        Removes `oauth_` keys from various dicts on the
+        request object, and returns the sanitized version.
+        """
+        for method_type in ('GET', 'PUT', 'POST', 'DELETE'):
+            block = getattr(request, method_type, { })
+
+            if True in [ k.startswith("oauth_") for k in block.keys() ]:
+                sanitized = block.copy()
+                
+                for k in sanitized.keys():
+                    if k.startswith("oauth_"):
+                        sanitized.pop(k)
+                        
+                setattr(request, method_type, sanitized)
+
+        return request
+        
+    # -- 
+    
+    def email_exception(self, reporter):
+        subject = "Piston crash report"
+        html = reporter.get_traceback_html()
+
+        message = EmailMessage(settings.EMAIL_SUBJECT_PREFIX+subject,
+                                html, settings.SERVER_EMAIL,
+                                [ admin[1] for admin in settings.ADMINS ])
+        
+        message.content_subtype = 'html'
+        message.send(fail_silently=True)
diff --git a/lib/piston/store.py b/lib/piston/store.py
new file mode 100644
index 0000000..741a470
--- /dev/null
+++ b/lib/piston/store.py
@@ -0,0 +1,68 @@
+import oauth
+
+from models import Nonce, Token, Consumer
+
+class DataStore(oauth.OAuthDataStore):
+    """Layer between Python OAuth and Django database."""
+    def __init__(self, oauth_request):
+        self.signature = oauth_request.parameters.get('oauth_signature', None)
+        self.timestamp = oauth_request.parameters.get('oauth_timestamp', None)
+        self.scope = oauth_request.parameters.get('scope', 'all')
+
+    def lookup_consumer(self, key):
+        try:
+            self.consumer = Consumer.objects.get(key=key)
+            return self.consumer
+        except Consumer.DoesNotExist:
+            return None
+
+    def lookup_token(self, token_type, token):
+        if token_type == 'request':
+            token_type = Token.REQUEST
+        elif token_type == 'access':
+            token_type = Token.ACCESS
+        try:
+            self.request_token = Token.objects.get(key=token, 
+                                                   token_type=token_type)
+            return self.request_token
+        except Token.DoesNotExist:
+            return None
+
+    def lookup_nonce(self, oauth_consumer, oauth_token, nonce):
+        if oauth_token is None:
+            return None
+        nonce, created = Nonce.objects.get_or_create(consumer_key=oauth_consumer.key, 
+                                                     token_key=oauth_token.key,
+                                                     key=nonce)
+        if created:
+            return None
+        else:
+            return nonce.key
+
+    def fetch_request_token(self, oauth_consumer):
+        if oauth_consumer.key == self.consumer.key:
+            self.request_token = Token.objects.create_token(consumer=self.consumer,
+                                                            token_type=Token.REQUEST,
+                                                            timestamp=self.timestamp)
+            return self.request_token
+        return None
+
+    def fetch_access_token(self, oauth_consumer, oauth_token):
+        if oauth_consumer.key == self.consumer.key \
+        and oauth_token.key == self.request_token.key \
+        and self.request_token.is_approved:
+            self.access_token = Token.objects.create_token(consumer=self.consumer,
+                                                           token_type=Token.ACCESS,
+                                                           timestamp=self.timestamp,
+                                                           user=self.request_token.user)
+            return self.access_token
+        return None
+
+    def authorize_request_token(self, oauth_token, user):
+        if oauth_token.key == self.request_token.key:
+            # authorize the request token in the store
+            self.request_token.is_approved = True
+            self.request_token.user = user
+            self.request_token.save()
+            return self.request_token
+        return None
\ No newline at end of file
diff --git a/lib/piston/utils.py b/lib/piston/utils.py
new file mode 100644
index 0000000..a992ef2
--- /dev/null
+++ b/lib/piston/utils.py
@@ -0,0 +1,124 @@
+from functools import wraps
+from django.http import HttpResponseNotAllowed, HttpResponseForbidden, HttpResponse
+from django.core.urlresolvers import reverse
+from django.core.cache import cache
+from django import get_version as django_version
+from decorator import decorator
+
+from datetime import datetime, timedelta
+
+__version__ = '0.2'
+
+def get_version():
+    return __version__
+
+def format_error(error):
+    return u"Piston/%s (Django %s) crash report:\n\n%s" % \
+        (get_version(), django_version(), error)
+
+def create_reply(message, status=200):
+    return HttpResponse(message, status=status)
+
+class rc(object):
+    """
+    Status codes.
+    """
+    ALL_OK = create_reply('OK', status=200)
+    CREATED = create_reply('Created', status=201)
+    DELETED = create_reply('', status=204) # 204 says "Don't send a body!"
+    BAD_REQUEST = create_reply('Bad Request', status=400)
+    FORBIDDEN = create_reply('Forbidden', status=401)
+    DUPLICATE_ENTRY = create_reply('Conflict/Duplicate', status=409)
+    NOT_HERE = create_reply('Gone', status=410)
+    NOT_IMPLEMENTED = create_reply('Not Implemented', status=501)
+    THROTTLED = create_reply('Throttled', status=503)
+    
+class FormValidationError(Exception):
+    def __init__(self, form):
+        self.form = form
+
+class HttpStatusCode(Exception):
+    def __init__(self, message, code=200):
+        self.message = message
+        self.code = code
+
+def validate(v_form, operation='POST'):
+    @decorator
+    def wrap(f, self, request, *a, **kwa):
+        form = v_form(getattr(request, operation))
+    
+        if form.is_valid():
+#            kwa.update({ 'form': form })
+            return f(self, request, *a, **kwa)
+        else:
+            raise FormValidationError(form)
+    return wrap
+
+def throttle(max_requests, timeout=60*60, extra=''):
+    """
+    Simple throttling decorator, caches
+    the amount of requests made in cache.
+    
+    If used on a view where users are required to
+    log in, the username is used, otherwise the
+    IP address of the originating request is used.
+    
+    Parameters::
+     - `max_requests`: The maximum number of requests
+     - `timeout`: The timeout for the cache entry (default: 1 hour)
+    """
+    @decorator
+    def wrap(f, self, request, *args, **kwargs):
+        if request.user.is_authenticated():
+            ident = request.user.username
+        else:
+            ident = request.META.get('REMOTE_ADDR', None)
+    
+        if hasattr(request, 'throttle_extra'):
+            """
+            Since we want to be able to throttle on a per-
+            application basis, it's important that we realize
+            that `throttle_extra` might be set on the request
+            object. If so, append the identifier name with it.
+            """
+            ident += ':%s' % str(request.throttle_extra)
+        
+        if ident:
+            """
+            Preferrably we'd use incr/decr here, since they're
+            atomic in memcached, but it's in django-trunk so we
+            can't use it yet. If someone sees this after it's in
+            stable, you can change it here.
+            """
+            ident += ':%s' % extra
+    
+            now = datetime.now()
+            ts_key = 'throttle:ts:%s' % ident
+            timestamp = cache.get(ts_key)
+            offset = now + timedelta(seconds=timeout)
+    
+            if timestamp and timestamp < offset:
+                t = rc.THROTTLED
+                wait = timeout - (offset-timestamp).seconds
+                t.content = 'Throttled, wait %d seconds.' % wait
+                
+                return t
+                
+            count = cache.get(ident, 1)
+            cache.set(ident, count+1)
+            
+            if count >= max_requests:
+                cache.set(ts_key, offset, timeout)
+                cache.set(ident, 1)
+    
+        return f(self, request, *args, **kwargs)
+    return wrap
+
+def coerce_put_post(request):
+    if request.method == "PUT":
+        request.method = "POST"
+        request._load_post_and_files()
+        request.method = "PUT"
+        request.PUT = request.POST
+        del request._post
+
diff --git a/notes/models.py b/notes/models.py
index 223ad0e..48ad726 100644
--- a/notes/models.py
+++ b/notes/models.py
@@ -48,6 +48,12 @@ class Note(models.Model):
     def __unicode__(self):
         return self.title
 
+    @models.permalink
+    def get_absolute_url(self):
+        return ('note_detail', (), {
+            'note_id': self.id, 'username': self.author.username,
+        })
+
 class NoteTag(models.Model):
     author = models.ForeignKey(User)
     name = models.CharField(max_length=256)
diff --git a/notes/urls.py b/notes/urls.py
index c5eadbd..be3faa6 100644
--- a/notes/urls.py
+++ b/notes/urls.py
@@ -16,12 +16,9 @@
 #
 
 from django.conf.urls.defaults import *
-from django.views.generic.list_detail import object_list, object_detail
 from snowy.notes.models import Note
 
-notes_dict = {'queryset': Note.objects.all(), }
-
 urlpatterns = patterns('',
-    (r'^$', object_list, notes_dict),
+    url(r'^$', 'snowy.notes.views.note_index', name='note_index'),
     url(r'^(?P<note_id>\d+)/$', 'snowy.notes.views.note_detail', name='note_detail'),
 )
diff --git a/notes/views.py b/notes/views.py
index dc74eb4..e1c8314 100644
--- a/notes/views.py
+++ b/notes/views.py
@@ -16,13 +16,31 @@
 #
 
 from django.template import RequestContext
+from django.contrib.auth.models import User
+from django.http import HttpResponseRedirect, Http404
 from django.shortcuts import render_to_response, get_object_or_404
 
 from snowy.notes.models import *
 
-def note_detail(request, note_id,
+def note_index(request, username,
+               template_name='note/note_index.html'):
+    user = get_object_or_404(User, username=username)
+
+    # TODO: retrieve the last open note from the user
+    last_modified = Note.objects.filter(author=user) \
+                                .order_by('-user_modified')
+    if last_modified.count() > 0:
+        return HttpResponseRedirect(last_modified[0].get_absolute_url())
+    
+    # Instruction page to tell user to either sync or create a new note
+    return render_to_response(template_name,
+                              {'user': user},
+                              context_instance=RequestContext(request))
+
+def note_detail(request, username, note_id,
                 template_name='notes/note_detail.html'):
-    note = get_object_or_404(Note, pk=note_id)
+    user = get_object_or_404(User, username=username)
+    note = get_object_or_404(Note, pk=note_id, author=user)
     
     # break this out into a function
     import libxslt
diff --git a/urls.py b/urls.py
index 4707491..fe3b17f 100644
--- a/urls.py
+++ b/urls.py
@@ -23,12 +23,11 @@ from django.contrib import admin
 admin.autodiscover()
 
 urlpatterns = patterns('',
-    # Example:
-    # (r'^snowy/', include('snowy.foo.urls')),
-
     (r'^registration/', include('registration.urls')),
 
-    (r'^notes/', include('snowy.notes.urls')),
+    (r'^(?P<username>\w+)/notes/', include('snowy.notes.urls')),
+
+    (r'^api/', include('snowy.api.urls')),
 
     # Uncomment the admin/doc line below and add 'django.contrib.admindocs' 
     # to INSTALLED_APPS to enable admin documentation:



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]