[snowy] Add in piston, start using it.
- From: Brad Taylor <btaylor src gnome org>
- To: svn-commits-list gnome org
- Subject: [snowy] Add in piston, start using it.
- Date: Thu, 14 May 2009 13:05:59 -0400 (EDT)
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]