Commits

Jesper Nøhr  committed 7236916 Merge

merge with spreeker

  • Participants
  • Parent commits b9dffe9, bc4c5bf

Comments (0)

Files changed (16)

 30c2c6b3a05558f5650a2a5a456f24bf5b21b04f 0.1
 a14b7b6ffa0369864316346659b9a37c30d7834b 0.2.1
+6b0364d98837b6b19b028d29f10769efbbcae435 0.2.2
 Michael Richardson for contributing an improvement to form validation handling
 Brian McMurray for contributing a patch for #41
 James Emerton for making the OAuth parts more usable/friendly
-Anton Tsigularov for providing a patch for incorrect multipart detection
+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
+recursive-include piston/templates *

File examples/blogserver/api/handlers.py

     fields = ('id', 'title', 'content', 'created_on')
 
     @classmethod
-    def resource_uri(self):
+    def resource_uri(cls):
         return ('blogposts', [ 'format', ])
 
 class BlogpostHandler(BaseHandler):
     anonymous = AnonymousBlogpostHandler
     fields = ('title', 'content', ('author', ('username',)), 
               'created_on', 'content_length')
-    
-    def read(self, title=None):
+
+    @classmethod
+    def content_length(cls, blogpost):
+        return len(blogpost.content)
+
+    @classmethod
+    def resource_uri(cls):
+        return ('blogposts', [ 'format', ])
+
+    def read(self, request, title=None):
         """
         Returns a blogpost, if `title` is given,
         otherwise all the posts.
             return base.get(title=title)
         else:
             return base.all()
-    
-    def content_length(self, blogpost):
-        return len(blogpost.content)
-        
+
     @require_extended
     def create(self, request):
         """
             post.save()
             
             return post
-    
-    @classmethod
-    def resource_uri(self):
-        return ('blogposts', [ 'format', ])

File piston/authentication.py

+import binascii
+
 import oauth
 from django.http import HttpResponse, HttpResponseRedirect
 from django.contrib.auth.models import User, AnonymousUser
         if not auth_string:
             return False
             
-        (authmeth, auth) = auth_string.split(" ", 1)
-        
-        if not authmeth.lower() == 'basic':
+        try:
+            (authmeth, auth) = auth_string.split(" ", 1)
+
+            if not authmeth.lower() == 'basic':
+                return False
+
+            auth = auth.strip().decode('base64')
+            (username, password) = auth.split(':', 1)
+        except (ValueError, binascii.Error):
             return False
-            
-        auth = auth.strip().decode('base64')
-        (username, password) = auth.split(':', 1)
         
         request.user = self.auth_func(username=username, password=password) \
             or AnonymousUser()

File piston/doc.py

             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_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__

File 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
         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)

File piston/forms.py

 
 class OAuthAuthenticationForm(forms.Form):
     oauth_token = forms.CharField(widget=forms.HiddenInput)
-    oauth_callback = forms.URLField(widget=forms.HiddenInput)
+    oauth_callback = forms.CharField(widget=forms.HiddenInput)
     authorize_access = forms.BooleanField(required=True)
     csrf_signature = forms.CharField(widget=forms.HiddenInput)
 

File piston/oauth.py

     @staticmethod
     def _split_header(header):
         params = {}
+        header = header.replace('OAuth ', '', 1)
         parts = header.split(',')
         for param in parts:
             # ignore realm parameter
-            if param.find('OAuth realm') > -1:
+            if param.find('realm') > -1:
                 continue
             # remove whitespace
             param = param.strip()

File piston/resource.py

 from doc import HandlerMethod
 from authentication import NoAuthentication
 from utils import coerce_put_post, FormValidationError, HttpStatusCode
-from utils import rc, format_error, translate_mime
+from utils import rc, format_error, translate_mime, MimerDataException
 
 class Resource(object):
     """
         """
         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 self.authentication.is_authenticated(request):
             if hasattr(self.handler, 'anonymous') and \
                 callable(self.handler.anonymous) and \
         else:
             handler = self.handler
             anonymous = handler.is_anonymous
-                
-        # Django's internal mechanism doesn't pick up
-        # PUT request, so we trick it a little here.
-        if rm == "PUT":
-            coerce_put_post(request)
         
         # Translate nested datastructs into `request.data` here.
-        translate_mime(request)
+        if rm in ('POST', 'PUT'):
+            try:
+                translate_mime(request)
+            except MimerDataException:
+                return rc.BAD_REQUEST
         
         if not rm in handler.allowed_methods:
             return HttpResponseNotAllowed(handler.allowed_methods)

File piston/utils.py

-from django.http import HttpResponseNotAllowed, HttpResponseForbidden, HttpResponse
+from django.http import HttpResponseNotAllowed, HttpResponseForbidden, HttpResponse, HttpResponseBadRequest
 from django.core.urlresolvers import reverse
 from django.core.cache import cache
 from django import get_version as django_version
 
 from datetime import datetime, timedelta
 
-__version__ = '0.2.1'
+__version__ = '0.2.2'
 
 def get_version():
     return __version__
             
         request.PUT = request.POST
 
+
+class MimerDataException(Exception):
+    """
+    Raised if the content_type and data don't match
+    """
+    pass
+
 class Mimer(object):
     TYPES = dict()
     
         
     def is_multipart(self):
         content_type = self.content_type()
+
         if content_type is not None:
             return content_type.lstrip().startswith('multipart')
+
         return False
 
     def loader_for_type(self, ctype):
         for a certain mimetype.
         """
         for loadee, mimes in Mimer.TYPES.iteritems():
-            if ctype in mimes:
-                return loadee
+            for mime in mimes:
+                if ctype.startswith(mime):
+                    return loadee
 
     def content_type(self):
         """
         Returns the content type of the request in all cases where it is
         different than a submitted form - application/x-www-form-urlencoded
         """
-
         type_formencoded = "application/x-www-form-urlencoded"
 
         ctype = self.request.META.get('CONTENT_TYPE', type_formencoded)
         
-        if ctype == type_formencoded:
+        if ctype.startswith(type_formencoded):
             return None
         
         return ctype
                 # Reset both POST and PUT from request, as its
                 # misleading having their presence around.
                 self.request.POST = self.request.PUT = dict()
-            except TypeError:
-                return rc.BAD_REQUEST # TODO: Handle this in super
-            except Exception, e:
-                raise
-                
+            except (TypeError, ValueError):
+                raise MimerDataException
+
         return self.request
                 
     @classmethod
 
 setup(
     name = "django-piston",
-    version = "0.2.1",
+    version = "0.2.2",
     url = 'http://bitbucket.org/jespern/django-piston/wiki/Home',
 	download_url = 'http://bitbucket.org/jespern/django-piston/downloads/',
     license = 'BSD',

File 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
 from forms import EchoForm
 from test_project.apps.testapp import signals
 
     @validate(EchoForm, 'GET')
     def read(self, request):
         return {'msg': request.GET['msg']}
+
+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)

File tests/test_project/apps/testapp/models.py

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

File 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
 from test_project.apps.testapp import signals
 
 class MainTests(TestCase):
         atoken = Token.objects.get(key=oa_atoken.key, token_type=Token.ACCESS)
         self.assertEqual(atoken.secret, oa_atoken.secret)
 
- 
+
+class BasicAuthTest(MainTests):
+
+    def test_invalid_auth_header(self):
+        response = self.client.get('/api/entries/')
+        self.assertEquals(response.status_code, 401)
+
+        # no space
+        bad_auth_string = 'Basic%s' % base64.encodestring('admin:admin').rstrip()
+        response = self.client.get('/api/entries/',
+            HTTP_AUTHORIZATION=bad_auth_string)
+        self.assertEquals(response.status_code, 401)
+
+        # no colon
+        bad_auth_string = 'Basic %s' % base64.encodestring('adminadmin').rstrip()
+        response = self.client.get('/api/entries/',
+            HTTP_AUTHORIZATION=bad_auth_string)
+        self.assertEquals(response.status_code, 401)
+
+        # non base64 data
+        bad_auth_string = 'Basic FOOBARQ!'
+        response = self.client.get('/api/entries/',
+            HTTP_AUTHORIZATION=bad_auth_string)
+        self.assertEquals(response.status_code, 401)
+
 class MultiXMLTests(MainTests):
     def init_delegate(self):
         self.t1_data = TestModel()
             HTTP_AUTHORIZATION=self.auth_string).content
             
         self.assertEquals(result, expected)
-        
+
+    def test_incoming_invalid_json(self):
+        resp = self.client.post('/api/expressive.json',
+            'foo',
+            HTTP_AUTHORIZATION=self.auth_string,
+            content_type='application/json')
+        self.assertEquals(resp.status_code, 400)
+
     def test_incoming_yaml(self):
         if not yaml:
             return
         self.assertEquals(self.client.get('/api/expressive.yaml', 
             HTTP_AUTHORIZATION=self.auth_string).content, expected)
 
+    def test_incoming_invalid_yaml(self):
+        resp = self.client.post('/api/expressive.yaml',
+            '  8**sad asj lja foo',
+            HTTP_AUTHORIZATION=self.auth_string,
+            content_type='application/yaml')
+        self.assertEquals(resp.status_code, 400)
+
 class Issue36RegressionTests(MainTests):
     """
     This testcase addresses #36 in django-piston where request.FILES is passed
         resp = self.client.get('/api/popo')
         self.assertEquals(resp.status_code, 200)
         self.assertEquals({'type': 'plain', 'field': 'a field'}, simplejson.loads(resp.content))
-        
+
+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)

File 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
 
 auth = HttpBasicAuthentication(realm='TestApplication')
 
 abstract = Resource(handler=AbstractHandler, authentication=auth)
 echo = Resource(handler=EchoHandler)
 popo = Resource(handler=PlainOldObjectHandler)
-
+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),