Commits

Jesper Nøhr committed e8ab0c6 Merge

merge with joestump

Comments (0)

Files changed (12)

 Anton Tsigularov for providing a patch for incorrect multipart detection
 Remco Wendt for fixing up the example blog server to conform with 0.2.2, et. al
 Benoit Garret for providing a fix for oauth headers, issue #56
+Stephan Preeker for providing some fixes to documentation generation
 import inspect, handler
 
 from piston.handler import typemapper
+from piston.handler import handler_tracker
 
 from django.core.urlresolvers import get_resolver, get_callable, get_script_prefix
 from django.shortcuts import render_to_response
             else:
                 yield (arg, None)
         
-    def get_signature(self, parse_optional=True):
+    @property
+    def signature(self, parse_optional=True):
         spec = ""
 
         for argn, argdef in self.iter_args():
             return spec.replace("=None", "=<optional>")
             
         return spec
-
-    signature = property(get_signature)
         
-    def get_doc(self):
+    @property
+    def doc(self):
         return inspect.getdoc(self.method)
     
-    doc = property(get_doc)
+    @property
+    def name(self):
+        return self.method.__name__
     
-    def get_name(self):
-        return self.method.__name__
-        
-    name = property(get_name)
+    @property
+    def http_name(self):
+        if self.name == 'read':
+            return 'GET'
+        elif self.name == 'create':
+            return 'POST'
+        elif self.name == 'delete':
+            return 'DELETE'
+        elif self.name == 'update':
+            return 'PUT'
     
     def __repr__(self):
         return "<Method: %s>" % self.name
         
     def get_methods(self, include_default=False):
         for method in "read create update delete".split():
-            met = getattr(self.handler, method)
+            met = getattr(self.handler, method, None)
+
+            if not met:
+                continue
+                
             stale = inspect.getmodule(met) is handler
 
             if not self.handler.is_anonymous:
     def get_model(self):
         return getattr(self, 'model', None)
             
-    def get_doc(self):
+    @property
+    def has_anonymous(self):
+        return self.handler.anonymous
+            
+    @property
+    def anonymous(self):
+        if self.has_anonymous:
+            return HandlerDocumentation(self.handler.anonymous)
+            
+    @property
+    def doc(self):
         return self.handler.__doc__
     
-    doc = property(get_doc)
-
     @property
     def name(self):
         return self.handler.__name__
     """
     docs = [ ]
 
-    for handler, (model, anonymous) in typemapper.iteritems():
+    for handler in handler_tracker: 
         docs.append(generate_doc(handler))
-        
+
+    def _compare(doc1, doc2): 
+       #handlers and their anonymous counterparts are put next to each other.
+       name1 = doc1.name.replace("Anonymous", "")
+       name2 = doc2.name.replace("Anonymous", "")
+       return cmp(name1, name2)    
+ 
+    docs.sort(_compare)
+       
     return render_to_response('documentation.html', 
         { 'docs': docs }, RequestContext(request))

piston/emitters.py

 from __future__ import generators
 
 import decimal, re, inspect
+import copy
 
 try:
     # yaml isn't standard with python.  It shouldn't be required if it
 from django.utils import simplejson
 from django.utils.xmlutils import SimplerXMLGenerator
 from django.utils.encoding import smart_unicode
+from django.core.urlresolvers import reverse, NoReverseMatch
 from django.core.serializers.json import DateTimeAwareJSONEncoder
 from django.http import HttpResponse
 from django.core import serializers
 except ImportError:
     import pickle
 
+# Allow people to change the reverser (default `permalink`).
+reverser = permalink
+
 class Emitter(object):
     """
     Super emitter. All other emitters should subclass
         ret = dict()
             
         for field in fields:
-            if field in has:
+            if field in has and callable(field):
                 ret[field] = getattr(data, field)
         
         return ret
                     get_fields = set(fields)
 
                 met_fields = self.method_fields(handler, get_fields)
-
+                
                 for f in data._meta.local_fields:
                     if f.serialize and not any([ p in met_fields for p in [ f.attname, f.name ]]):
                         if not f.rel:
                 
                 # 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 self.in_typemapper(type(data), self.anonymous):
                 handler = self.in_typemapper(type(data), self.anonymous)
                 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) ) )()
+                    url_id, fields = handler.resource_uri(data)
+
+                    try:
+                        ret['resource_uri'] = reverser( lambda: (url_id, fields) )()
+                    except NoReverseMatch, e:
+                        pass
             
             if hasattr(data, 'get_api_url') and 'resource_uri' not in ret:
                 try: ret['resource_uri'] = data.get_api_url()

piston/handler.py

 from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned
 
 typemapper = { }
+handler_tracker = [ ]
 
 class HandlerMetaClass(type):
     """
         
         if hasattr(new_cls, 'model'):
             typemapper[new_cls] = (new_cls.model, new_cls.is_anonymous)
+        else:
+            typemapper[new_cls] = (None, new_cls.is_anonymous)
         
+        if name not in ('BaseHandler', 'AnonymousBaseHandler'):
+            handler_tracker.append(new_cls)
+
         return new_cls
 
 class BaseHandler(object):
         return dict([ (str(k), dct.get(k)) for k in dct.keys() ])
     
     def has_model(self):
-        return hasattr(self, 'model')
+        return hasattr(self, 'model') or hasattr(self, 'queryset')
+
+    def queryset(self, request):
+        return self.model.objects.all()
     
     def value_from_tuple(tu, name):
         for int_, n in tu:
 
         if pkfield in kwargs:
             try:
-                return self.model.objects.get(pk=kwargs.get(pkfield))
+                return self.queryset(request).get(pk=kwargs.get(pkfield))
             except ObjectDoesNotExist:
                 return rc.NOT_FOUND
             except MultipleObjectsReturned: # should never happen, since we're using a PK
                 return rc.BAD_REQUEST
         else:
-            return self.model.objects.filter(*args, **kwargs)
+            return self.queryset(request).filter(*args, **kwargs)
     
     def create(self, request, *args, **kwargs):
         if not self.has_model():
         attrs = self.flatten_dict(request.POST)
         
         try:
-            inst = self.model.objects.get(**attrs)
+            inst = self.queryset(request).get(**attrs)
             return rc.DUPLICATE_ENTRY
         except self.model.DoesNotExist:
             inst = self.model(**attrs)
             return rc.DUPLICATE_ENTRY
     
     def update(self, request, *args, **kwargs):
-        # TODO: This doesn't work automatically yet.
-        return rc.NOT_IMPLEMENTED
+        if not self.has_model():
+            return rc.NOT_IMPLEMENTED
+
+        pkfield = self.model._meta.pk.name
+
+        if pkfield not in kwargs:
+            # No pk was specified
+            return rc.BAD_REQUEST
+
+        try:
+            inst = self.queryset(request).get(pk=kwargs.get(pkfield))
+        except ObjectDoesNotExist:
+            return rc.NOT_FOUND
+        except MultipleObjectsReturned: # should never happen, since we're using a PK
+            return rc.BAD_REQUEST
+
+        attrs = self.flatten_dict(request.POST)
+        for k,v in attrs.iteritems():
+            setattr( inst, k, v )
+
+        inst.save()
+        return rc.ALL_OK
     
     def delete(self, request, *args, **kwargs):
         if not self.has_model():
             raise NotImplementedError
 
         try:
-            inst = self.model.objects.get(*args, **kwargs)
+            inst = self.queryset(request).get(*args, **kwargs)
 
             inst.delete()
 

piston/managers.py

 from django.db import models
 from django.contrib.auth.models import User
 
-KEY_SIZE = 16
-SECRET_SIZE = 16
+KEY_SIZE = 18
+SECRET_SIZE = 32
 
-class ConsumerManager(models.Manager):
+class KeyManager(models.Manager):
+    '''Add support for random key/secret generation
+    '''
+    def generate_random_codes(self):
+        key = User.objects.make_random_password(length=KEY_SIZE)
+        secret = User.objects.make_random_password(length=SECRET_SIZE)
+
+        while self.filter(key__exact=key, secret__exact=secret).count():
+            secret = User.objects.make_random_password(length=SECRET_SIZE)
+
+        return key, secret
+
+
+class ConsumerManager(KeyManager):
     def create_consumer(self, name, description=None, user=None):
         """
         Shortcut to create a consumer with random key/secret.
             consumer.description = description
 
         if created:
-            consumer.generate_random_codes()
+            consumer.key, consumer.secret = self.generate_random_codes()
+            consumer.save()
 
         return consumer
-    
+
     _default_consumer = None
 
 class ResourceManager(models.Manager):
 
         return self._default_resource        
 
-class TokenManager(models.Manager):
+class TokenManager(KeyManager):
     def create_token(self, consumer, token_type, timestamp, user=None):
         """
         Shortcut to create a token with random key/secret.
                                             user=user)
 
         if created:
-            token.generate_random_codes()
+            token.key, token.secret = self.generate_random_codes()
+            token.save()
 
-        return token
+        return token
+        

piston/models.py

File contents unchanged.

piston/resource.py

 from django.views.decorators.vary import vary_on_headers
 from django.conf import settings
 from django.core.mail import send_mail, EmailMessage
+from django.db.models.query import QuerySet
 
 from emitters import Emitter
 from handler import typemapper
 
         return em
     
+    @property
+    def anonymous(self):
+        """
+        Gets the anonymous handler. Also tries to grab a class
+        if the `anonymous` value is a string, so that we can define
+        anonymous handlers that aren't defined yet (like, when
+        you're subclassing your basehandler into an anonymous one.)
+        """
+        if hasattr(self.handler, 'anonymous'):
+            anon = self.handler.anonymous
+            
+            if callable(anon):
+                return anon
+
+            for klass in typemapper.keys():
+                if anon == klass.__name__:
+                    return klass
+            
+        return None
+    
     @vary_on_headers('Authorization')
     def __call__(self, request, *args, **kwargs):
         """
             coerce_put_post(request)
 
         if not self.authentication.is_authenticated(request):
-            if hasattr(self.handler, 'anonymous') and \
-                callable(self.handler.anonymous) and \
-                rm in self.handler.anonymous.allowed_methods:
+            if self.anonymous and \
+                rm in self.anonymous.allowed_methods:
 
-                handler = self.handler.anonymous()
+                handler = self.anonymous()
                 anonymous = True
             else:
                 return self.authentication.challenge()
         try:
             result = meth(request, *args, **kwargs)
         except FormValidationError, e:
-            # TODO: Use rc.BAD_REQUEST here
-            return HttpResponse("Bad Request: %s" % e.form.errors, status=400)
+            resp = rc.BAD_REQUEST
+            resp.write(e.form.errors)
+            
+            return rsep
         except TypeError, e:
             result = rc.BAD_REQUEST
             hm = HandlerMethod(meth)
-            sig = hm.get_signature()
+            sig = hm.signature
 
             msg = 'Method signature does not match.\n\n'
             
                 
             result.content = format_error(msg)
         except HttpStatusCode, e:
-            #result = e ## why is this being passed on and not just dealt with now?
             return e.response
         except Exception, e:
             """
                 raise
 
         emitter, ct = Emitter.get(em_format)
-        srl = emitter(result, typemapper, handler, handler.fields, anonymous)
+        fields = handler.fields
+        if hasattr(handler, 'list_fields') and (
+                isinstance(result, list) or isinstance(result, QuerySet)):
+            fields = handler.list_fields
+
+        srl = emitter(result, typemapper, handler, fields, anonymous)
 
         try:
             """
             if self.stream: stream = srl.stream_render(request)
             else: stream = srl.render(request)
 
-            resp = HttpResponse(stream, mimetype=ct)
+            if not isinstance(stream, HttpResponse):
+                resp = HttpResponse(stream, mimetype=ct)
+            else:
+                resp = stream
 
             resp.streaming = self.stream
 
     author_email = 'jesper@noehr.org',
     packages = find_packages(),
     include_package_data = True,
+    zip_safe = False,
     classifiers = [
         'Development Status :: 3 - Alpha',
         'Framework :: Django',

tests/test_project/apps/testapp/handlers.py

 from piston.handler import BaseHandler
 from piston.utils import rc, validate
 
-from models import TestModel, ExpressiveTestModel, Comment, InheritedModel, PlainOldObject
+from models import TestModel, ExpressiveTestModel, Comment, InheritedModel, PlainOldObject, Issue58Model, ListFieldsModel
 from forms import EchoForm
 from test_project.apps.testapp import signals
 
-
 class EntryHandler(BaseHandler):
     model = TestModel
     allowed_methods = ['GET', 'PUT', 'POST']
     @validate(EchoForm, 'GET')
     def read(self, request):
         return {'msg': request.GET['msg']}
+
+class ListFieldsHandler(BaseHandler):
+    model = ListFieldsModel
+    fields = ('id','kind','variety','color')
+    list_fields = ('id','variety')
+
+class Issue58Handler(BaseHandler):
+    model = Issue58Model
+
+    def read(self, request):
+        return Issue58Model.objects.all()
+                
+    def create(self, request):
+        if request.content_type:
+            data = request.data
+            em = self.model(read=data['read'], model=data['model'])
+            em.save()
+            return rc.CREATED
+        else:
+            super(Issue58Model, self).create(request)

tests/test_project/apps/testapp/models.py

 class PlainOldObject(object):
     def __emittable__(self):
         return {'type': 'plain',
-                'field': 'a field'}
+                'field': 'a field'}
+
+class ListFieldsModel(models.Model):
+    kind = models.CharField(max_length=15)
+    variety = models.CharField(max_length=15)
+    color = models.CharField(max_length=15)
+
+class Issue58Model(models.Model):
+    read = models.BooleanField(default=False)
+    model = models.CharField(max_length=1, blank=True, null=True)

tests/test_project/apps/testapp/tests.py

 
 import urllib, base64
 
-from test_project.apps.testapp.models import TestModel, ExpressiveTestModel, Comment, InheritedModel
+from test_project.apps.testapp.models import TestModel, ExpressiveTestModel, Comment, InheritedModel, Issue58Model, ListFieldsModel
 from test_project.apps.testapp import signals
 
 class MainTests(TestCase):
     def setUp(self):
         super(OAuthTests, self).setUp()
 
-        self.consumer = Consumer(name='Test Consumer', description='Test', status='accepted')
-        self.consumer.generate_random_codes()
+        self.consumer = Consumer.objects.create_consumer('Test Consumer')
+        self.consumer.status = 'accepted'
         self.consumer.save()
 
     def tearDown(self):
         resp = self.client.get('/api/popo')
         self.assertEquals(resp.status_code, 200)
         self.assertEquals({'type': 'plain', 'field': 'a field'}, simplejson.loads(resp.content))
+
+class ListFieldsTest(MainTests):
+    def init_delegate(self):
+        ListFieldsModel(kind='fruit', variety='apple', color='green').save()
+        ListFieldsModel(kind='vegetable', variety='carrot', color='orange').save()
+        ListFieldsModel(kind='animal', variety='dog', color='brown').save()
+
+    def test_single_item(self):
+        expect = '''{
+    "color": "green", 
+    "kind": "fruit", 
+    "id": 1, 
+    "variety": "apple"
+}'''
+        resp = self.client.get('/api/list_fields/1')
+        self.assertEquals(resp.status_code, 200)
+        self.assertEquals(resp.content, expect)
+
+
+    def test_multiple_items(self):
+        expect = '''[
+    {
+        "id": 1, 
+        "variety": "apple"
+    }, 
+    {
+        "id": 2, 
+        "variety": "carrot"
+    }, 
+    {
+        "id": 3, 
+        "variety": "dog"
+    }
+]'''
+        resp = self.client.get('/api/list_fields')
+        self.assertEquals(resp.status_code, 200)
+        self.assertEquals(resp.content, expect)
+        
+class Issue58ModelTests(MainTests):
+    """
+    This testcase addresses #58 in django-piston where if a model
+    has one of the ['read','update','delete','create'] defined
+    it make piston crash with a `TypeError`
+    """
+    def init_delegate(self):
+        m1 = Issue58Model(read=True,model='t') 
+        m1.save()
+        m2 = Issue58Model(read=False,model='f')
+        m2.save()
+
+    def test_incoming_json(self):
+        outgoing = simplejson.dumps({ 'read': True, 'model': 'T'})
+
+        expected = """[
+    {
+        "read": true, 
+        "model": "t"
+    }, 
+    {
+        "read": false, 
+        "model": "f"
+    }
+]"""
+
+        # test GET
+        result = self.client.get('/api/issue58.json',
+                                HTTP_AUTHORIZATION=self.auth_string).content
+        self.assertEquals(result, expected)
+
+        # test POST
+        resp = self.client.post('/api/issue58.json', outgoing, content_type='application/json',
+                                HTTP_AUTHORIZATION=self.auth_string)
+        self.assertEquals(resp.status_code, 201)
+        

tests/test_project/apps/testapp/urls.py

 from piston.resource import Resource
 from piston.authentication import HttpBasicAuthentication
 
-from test_project.apps.testapp.handlers import EntryHandler, ExpressiveHandler, AbstractHandler, EchoHandler, PlainOldObjectHandler
+from test_project.apps.testapp.handlers import EntryHandler, ExpressiveHandler, AbstractHandler, EchoHandler, PlainOldObjectHandler, Issue58Handler, ListFieldsHandler
 
 auth = HttpBasicAuthentication(realm='TestApplication')
 
 abstract = Resource(handler=AbstractHandler, authentication=auth)
 echo = Resource(handler=EchoHandler)
 popo = Resource(handler=PlainOldObjectHandler)
-
+list_fields = Resource(handler=ListFieldsHandler)
+issue58 = Resource(handler=Issue58Handler)
 
 urlpatterns = patterns('',
     url(r'^entries/$', entries),
     url(r'^entries\.(?P<emitter_format>.+)', entries),
     url(r'^entry-(?P<pk>.+)\.(?P<emitter_format>.+)', entries),
 
+    url(r'^issue58\.(?P<emitter_format>.+)$', issue58),
+
     url(r'^expressive\.(?P<emitter_format>.+)$', expressive),
 
     url(r'^abstract\.(?P<emitter_format>.+)$', abstract),
     url(r'^oauth/request_token$', 'piston.authentication.oauth_request_token'),
     url(r'^oauth/authorize$', 'piston.authentication.oauth_user_auth'),
     url(r'^oauth/access_token$', 'piston.authentication.oauth_access_token'),
+
+    url(r'^list_fields$', list_fields),
+    url(r'^list_fields/(?P<id>.+)$', list_fields),
     
     url(r'^popo$', popo),
 )