here, take these django-piston decorators

I’ve been using Jesper Noher’s django-piston project for some projects at work and home and I must say, I _really_ love it. Sadly, I don’t think the project gets as much credit as it should. If you’re looking to add REST to django, you really cant go wrong with piston.

That said, as I started to use it more and more, I hit some roadblocks that I had to come up with solutions to.

TLDR; Checkout my additions to django-piston here http://hg.qr7.com/django-piston/.

The @validate decorator in piston.utils worked great for creating new objects (POST), but didn’t really work at all when trying to update models (PUT). I extended this function to accept a model reference and define which fields are passed in through the url for existing object lookup. Yea, thats not clear, lets throw together an example (figure out your own imports). ***I haven’t tested this at all… let me know if i screwed up somewhere.

The model (models.py:

class MyModel(models.Model):
    testfield = models.CharField(max_length=50, unique=True)

And a form for validation (forms.py):

class MyModelForm(forms.ModelForm):
    class Meta:
        model = MyModel

Cant work without urls’s (urls.py):

basic_auth = HttpBasicAuthentication(realm='My Realm')
basic_mymodel = Resource(handler=MyModelHandler, authentication=basic_auth)
urlpatterns = patterns('',
    url(r'mymodel/$', basic_mymodel),
    url(r'mymodel/(?P<testfield>[^/]+)/$', basic_mymodel),
)

Finally, the handler (handlers.py):

class MyModelHandler(BaseHandler):
    """
    Authenticated entrypoint for MyModel entries.
    """
    model = MyModel
    anonymous = 'AnonymousMyModelHandler'
    fields = ('testfield',)
    allowed_methods = ('GET', 'POST', 'PUT')

    def read(self, request, *args, **kwargs):
        base = self.model.objects.all()
        if args != () or kwargs != {}:
            try:
                return base.get(*args, **kwargs)
            except self.model.DoesNotExist:
                return rc.NOT_FOUND
            except self.model.MultipleObjectsReturned:
                return rc.BAD_REQUEST
        else:
            return base

    @no_params()
    @validate(MyModelForm, 'POST')
    def create(self, request, *args, **kwargs):
        if not hasattr(request, "data"):
            request.data = request.POST
        attrs = self.flatten_dict(request.data)
        try:
            mymodel = self.model(testfield=attrs['testfield'])
        except:
            return rc.BAD_REQUEST
        else:
            mymodel.save()
        return mymodel

    @essential(['testfield'])
    @validate(MyModelForm, 'PUT', ['testfield'], MyModel)
    def update(self, request, *args, **kwargs):
        if not hasattr(request, "data"):
            request.data = request.POST
        try:
            instance = self.queryset(request).get(fqdn=kwargs['mymodel'])
        except self.model.DoesNotExist:
            return rc.NOT_FOUND
        except self.model.MultipleObjectsReturned:
            return rc.BAD_REQUEST
        except:
            return rc.BAD_REQUEST
        attrs = self.flatten_dict(request.data)
        for k,v in attrs.iteritems():
            setattr(instance, k, v)

        instance.save()
        return instance

As you can see on the highlighted lines, I’ve modified @validate to accept more params, and added the @no_params and @essential decorators. The three break down like this:

  • @validate now takes in an optional list of lookup fields, these should map to your urls.py as the inputs. In the example i have
  • @no_params is used to ensure that no parameters have been passed in, in this example, I want only POST’s to mymodel/ to be accepted for object creation. Without this, sometimes @validate will return weird errors that you I didn’t want the user to see. Doing the check in the actual create function wouldn’t work because @validate wraps it first and takes precedence.
  • @essential is used to ensure that the passed in list of parameters have been passed in to the request. In this case, testfield is essential for the existing object lookup (ok ok this is a bad example, I’m modifying a unique identifier, but thanks to django’s form validation, the new testfield should be checked for uniqueness as well!). Once we’ve ensured that testfield exists as a param, we can then validate against the form. I guess this is somewhat of a bad example, as the form will bitch that testfield is needed, but there are cases where the form bitches without any helpful information. I like having the option of presenting bad requests to users before the form displays errors. This serves that purpose very well.

All of this work was pretty straight forward. I went ahead an forked django-piston and made my changes to utils.py. You can find it all here: http://hg.qr7.com/django-piston/. I’ve issued a pull request to trunk and hopefully some/all of these will get added with any hope. If not, there here if you find yourself bumping up against the same problems. I’d be interested in anyones thoughts/contributions on all of this. Please drop me an email, comment, or PM me on bitbucket/twitter/wherever.

Hope this helps!

Continue reading » · Written on: 01-10-10 · 1 Comment »

One Response to “here, take these django-piston decorators”

  1. Milan Andric wrote:

    Hey I know this might be out of the scope of this article, but could you also put in a couple of http requests that would excercise this code? I’m new to piston/rest and if I had a couple http request examples to look at I could see how this all works together nicely. Otherwise thanks for the post, still gets me started thinking about how I can use django-piston!

    January 14th, 2010 at 12:41 am

Leave a Reply