Anonymous avatar Anonymous committed 5ec896d Draft Merge

Merge

Comments (0)

Files changed (18)

 .DS_Store
 .svn
 *.swp
+docs/_build
 30c2c6b3a05558f5650a2a5a456f24bf5b21b04f 0.1
 a14b7b6ffa0369864316346659b9a37c30d7834b 0.2.1
 6b0364d98837b6b19b028d29f10769efbbcae435 0.2.2
+5ad8d46178098a65399327601009b5955e562445 0.2.2.1
+1de66e003b13208506b8fb5288baf6813afedc45 0.2.3
+from django.contrib import admin
+from piston.models import Nonce, Token, Consumer
+
+admin.site.register(Nonce)
+admin.site.register(Token)
+admin.site.register(Consumer)

piston/emitters.py

             Dispatch, all types are routed through here.
             """
             ret = None
-            if hasattr(thing, '__emittable__'):
-                f = thing.__emittable__
-                if inspect.ismethod(f) and len(inspect.getargspec(f)[0]) == 1:
-                    ret = _any(f())
-            elif isinstance(thing, QuerySet):
+
+            # return anything we've already seen as a string only
+            # this prevents infinite recursion in the case of recursive 
+            # relationships
+
+            if thing in self.stack:
+                raise RuntimeError, (u'Circular reference detected while emitting '
+                                     'response')
+
+            self.stack.append(thing)
+
+            if isinstance(thing, QuerySet):
                 ret = _qs(thing, fields)
             elif isinstance(thing, (tuple, list, set)):
                 ret = _list(thing, fields)
             else:
                 ret = smart_unicode(thing, strings_only=True)
 
+            self.stack.pop()
+
             return ret
 
         def _fk(data, field):
 
             if handler or fields:
                 v = lambda f: getattr(data, f.attname)
-
+                # FIXME
+                # Catch 22 here. Either we use the fields from the
+                # typemapped handler to make nested models work but the
+                # declared list_fields will ignored for models, or we
+                # use the list_fields from the base handler and accept that
+                # the nested models won't appear properly
+                # Refs #157
                 if handler:
                     fields = getattr(handler, 'fields')    
                 
             return dict([ (k, _any(v, fields)) for k, v in data.iteritems() ])
 
         # Kickstart the seralizin'.
+        self.stack = [];
         return _any(self.data, self.fields)
 
     def in_typemapper(self, model, anonymous):
 
 if yaml:  # Only register yaml if it was import successfully.
     Emitter.register('yaml', YAMLEmitter, 'application/x-yaml; charset=utf-8')
-    Mimer.register(lambda s: dict(yaml.load(s)), ('application/x-yaml',))
+    Mimer.register(lambda s: dict(yaml.safe_load(s)), ('application/x-yaml',))
 
 class PickleEmitter(Emitter):
     """

piston/emitters.py.orig

+from __future__ import generators
+
+import decimal, re, inspect
+import copy
+
+try:
+    # yaml isn't standard with python.  It shouldn't be required if it
+    # isn't used.
+    import yaml
+except ImportError:
+    yaml = None
+
+# Fallback since `any` isn't in Python <2.5
+try:
+    any
+except NameError:
+    def any(iterable):
+        for element in iterable:
+            if element:
+                return True
+        return False
+
+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.urlresolvers import reverse, NoReverseMatch
+from django.core.serializers.json import DateTimeAwareJSONEncoder
+from django.http import HttpResponse
+from django.core import serializers
+
+from utils import HttpStatusCode, Mimer
+from validate_jsonp import is_valid_jsonp_callback_value
+
+try:
+    import cStringIO as StringIO
+except ImportError:
+    import StringIO
+
+try:
+    import cPickle as pickle
+except ImportError:
+    import pickle
+
+# Allow people to change the reverser (default `permalink`).
+reverser = permalink
+
+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.
+
+    `RESERVED_FIELDS` was introduced when better resource
+    method detection came, and we accidentially caught these
+    as the methods on the handler. Issue58 says that's no good.
+    """
+    EMITTERS = { }
+    RESERVED_FIELDS = set([ 'read', 'update', 'create',
+                            'delete', 'model', 'anonymous',
+                            'allowed_methods', 'fields', 'exclude' ])
+
+    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 method_fields(self, handler, fields):
+        if not handler:
+            return { }
+
+        ret = dict()
+
+        for field in fields - Emitter.RESERVED_FIELDS:
+            t = getattr(handler, str(field), None)
+
+            if t and callable(t):
+                ret[field] = t
+
+        return ret
+
+    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=None):
+            """
+            Dispatch, all types are routed through here.
+            """
+            ret = None
+<<<<<<< mine
+            if hasattr(thing, '__emittable__'):
+                f = thing.__emittable__
+                if inspect.ismethod(f) and len(inspect.getargspec(f)[0]) == 1:
+                    ret = _any(f())
+            elif isinstance(thing, QuerySet):
+=======
+
+            # return anything we've already seen as a string only
+            # this prevents infinite recursion in the case of recursive 
+            # relationships
+
+            if thing in self.stack:
+                raise RuntimeError, (u'Circular reference detected while emitting '
+                                     'response')
+
+            self.stack.append(thing)
+
+            if isinstance(thing, QuerySet):
+>>>>>>> theirs
+                ret = _qs(thing, fields)
+            elif isinstance(thing, (tuple, list, set)):
+                ret = _list(thing, fields)
+            elif isinstance(thing, dict):
+                ret = _dict(thing, fields)
+            elif isinstance(thing, decimal.Decimal):
+                ret = str(thing)
+            elif isinstance(thing, Model):
+                ret = _model(thing, fields)
+            elif isinstance(thing, HttpResponse):
+                raise HttpStatusCode(thing)
+            elif inspect.isfunction(thing):
+                if not inspect.getargspec(thing)[0]:
+                    ret = _any(thing())
+            elif repr(thing).startswith("<django.db.models.fields.related.RelatedManager"):
+                ret = _any(thing.all())
+            else:
+                ret = smart_unicode(thing, strings_only=True)
+
+            self.stack.pop()
+
+            return ret
+
+        def _fk(data, field):
+            """
+            Foreign keys.
+            """
+            return _any(getattr(data, field.name))
+
+        def _related(data, fields=None):
+            """
+            Foreign keys.
+            """
+            return [ _model(m, fields) for m in data.iterator() ]
+
+        def _m2m(data, field, fields=None):
+            """
+            Many to many (re-route to `_model`.)
+            """
+            return [ _model(m, fields) for m in getattr(data, field.name).iterator() ]
+
+        def _model(data, fields=None):
+            """
+            Models. Will respect the `fields` and/or
+            `exclude` on the handler (see `typemapper`.)
+            """
+            ret = { }
+            handler = self.in_typemapper(type(data), self.anonymous)
+            get_absolute_uri = False
+
+            if handler or fields:
+                v = lambda f: getattr(data, f.attname)
+                # FIXME
+                # Catch 22 here. Either we use the fields from the
+                # typemapped handler to make nested models work but the
+                # declared list_fields will ignored for models, or we
+                # use the list_fields from the base handler and accept that
+                # the nested models won't appear properly
+                # Refs #157
+                if handler:
+                    fields = getattr(handler, 'fields')    
+                
+                if not fields or hasattr(handler, '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).difference(get_fields)
+
+                    if 'absolute_uri' in get_fields:
+                        get_absolute_uri = True
+
+                    if not get_fields:
+                        get_fields = set([ f.attname.replace("_id", "", 1)
+                            for f in data._meta.fields + data._meta.virtual_fields])
+                    
+                    if hasattr(mapped, 'extra_fields'):
+                        get_fields.update(mapped.extra_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)
+
+                met_fields = self.method_fields(handler, get_fields)
+
+                for f in data._meta.local_fields + data._meta.virtual_fields:
+                    if f.serialize and not any([ p in met_fields for p in [ f.attname, f.name ]]):
+                        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 and mf.attname not in met_fields:
+                        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)
+                            elif callable(inst):
+                                if len(inspect.getargspec(inst)[0]) == 1:
+                                    ret[model] = _any(inst(), fields)
+                            else:
+                                ret[model] = _model(inst, fields)
+
+                    elif maybe_field in met_fields:
+                        # Overriding normal field which has a "resource method"
+                        # so you can alter the contents of certain fields without
+                        # using different names.
+                        ret[maybe_field] = _any(met_fields[maybe_field](data))
+
+                    else:
+                        maybe = getattr(data, maybe_field, None)
+                        if maybe is not None:
+                            if callable(maybe):
+                                if len(inspect.getargspec(maybe)[0]) <= 1:
+                                    ret[maybe_field] = _any(maybe())
+                            else:
+                                ret[maybe_field] = _any(maybe)
+                        else:
+                            handler_f = getattr(handler or self.handler, maybe_field, None)
+
+                            if handler_f:
+                                ret[maybe_field] = _any(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 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(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()
+                except: pass
+
+            # absolute uri
+            if hasattr(data, 'get_absolute_url') and get_absolute_uri:
+                try: ret['absolute_uri'] = data.get_absolute_url()
+                except: pass
+
+            return ret
+
+        def _qs(data, fields=None):
+            """
+            Querysets.
+            """
+            return [ _any(v, fields) for v in data ]
+
+        def _list(data, fields=None):
+            """
+            Lists.
+            """
+            return [ _any(v, fields) for v in data ]
+
+        def _dict(data, fields=None):
+            """
+            Dictionaries.
+            """
+            return dict([ (k, _any(v, fields)) for k, v in data.iteritems() ])
+
+        # Kickstart the seralizin'.
+        self.stack = [];
+        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.")
+
+    def stream_render(self, request, stream=True):
+        """
+        Tells our patched middleware not to look
+        at the contents, and returns a generator
+        rather than the buffered string. Should be
+        more memory friendly for large datasets.
+        """
+        yield self.render(request)
+
+    @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:
+                xml.startElement("resource", {})
+                self._to_xml(xml, item)
+                xml.endElement("resource")
+        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')
+Mimer.register(lambda *a: None, ('text/xml',))
+
+class JSONEmitter(Emitter):
+    """
+    JSON emitter, understands timestamps.
+    """
+    def render(self, request):
+        cb = request.GET.get('callback', None)
+        seria = simplejson.dumps(self.construct(), cls=DateTimeAwareJSONEncoder, ensure_ascii=False, indent=4)
+
+        # Callback
+        if cb and is_valid_jsonp_callback_value(cb):
+            return '%s(%s)' % (cb, seria)
+
+        return seria
+
+Emitter.register('json', JSONEmitter, 'application/json; charset=utf-8')
+Mimer.register(simplejson.loads, ('application/json',))
+
+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')
+    Mimer.register(lambda s: dict(yaml.safe_load(s)), ('application/x-yaml',))
+
+class PickleEmitter(Emitter):
+    """
+    Emitter that returns Python pickled.
+    """
+    def render(self, request):
+        return pickle.dumps(self.construct())
+
+Emitter.register('pickle', PickleEmitter, 'application/python-pickle')
+
+"""
+WARNING: Accepting arbitrary pickled data is a huge security concern.
+The unpickler has been disabled by default now, and if you want to use
+it, please be aware of what implications it will have.
+
+Read more: http://nadiana.com/python-pickle-insecure
+
+Uncomment the line below to enable it. You're doing so at your own risk.
+"""
+# Mimer.register(pickle.loads, ('application/python-pickle',))
+
+class DjangoEmitter(Emitter):
+    """
+    Emitter for the Django serialized format.
+    """
+    def render(self, request, format='xml'):
+        if isinstance(self.data, HttpResponse):
+            return self.data
+        elif isinstance(self.data, (int, str)):
+            response = self.data
+        else:
+            response = serializers.serialize(format, self.data, indent=True)
+
+        return response
+
+Emitter.register('django', DjangoEmitter, 'text/xml; charset=utf-8')

piston/resource.py

 import sys, inspect
 
+import django
 from django.http import (HttpResponse, Http404, HttpResponseNotAllowed,
     HttpResponseForbidden, HttpResponseServerError)
 from django.views.debug import ExceptionReporter
 from django.db.models.query import QuerySet
 from django.http import Http404
 
+try:
+    import mimeparse
+except ImportError:
+    mimeparse = None
+
 from emitters import Emitter
 from handler import typemapper
 from doc import HandlerMethod
         self.email_errors = getattr(settings, 'PISTON_EMAIL_ERRORS', True)
         self.display_errors = getattr(settings, 'PISTON_DISPLAY_ERRORS', True)
         self.stream = getattr(settings, 'PISTON_STREAM_OUTPUT', False)
+        # Emitter selection
+        self.strict_accept = getattr(settings, 'PISTON_STRICT_ACCEPT_HANDLING',
+                                     False)
+        self.default_emitter = getattr(settings, 'PISTON_DEFAULT_EMITTER',
+                                       'json')
 
     def determine_emitter(self, request, *args, **kwargs):
         """
         Function for determening which emitter to use
         for output. It lives here so you can easily subclass
         `Resource` in order to change how emission is detected.
-
-        You could also check for the `Accept` HTTP header here,
-        since that pretty much makes sense. Refer to `Mimer` for
-        that as well.
         """
-        em = kwargs.pop('emitter_format', None)
-
-        if not em:
-            em = request.GET.get('format', 'json')
-
-        return em
+        try:
+            return kwargs['emitter_format']
+        except KeyError:
+            pass
+        if 'format' in request.GET:
+            return request.GET.get('format')
+        if mimeparse and 'HTTP_ACCEPT' in request.META:
+            supported_mime_types = set()
+            emitter_map = {}
+            for name, (klass, content_type) in Emitter.EMITTERS.items():
+                content_type_without_encoding = content_type.split(';')[0]
+                supported_mime_types.add(content_type_without_encoding)
+                emitter_map[content_type_without_encoding] = name
+            preferred_content_type = mimeparse.best_match(
+                list(supported_mime_types),
+                request.META['HTTP_ACCEPT'])
+            return emitter_map.get(preferred_content_type, None)
 
     def form_validation_response(self, e):
         """
         `Resource` subclass.
         """
         resp = rc.BAD_REQUEST
-        resp.write(' '+str(e.form.errors))
+        resp.write(u' '+unicode(e.form.errors))
         return resp
 
     @property
         if not meth:
             raise Http404
 
-        # Support emitter both through (?P<emitter_format>) and ?format=emitter.
+        # Support emitter through (?P<emitter_format>) and ?format=emitter
+        # and lastly Accept: header processing
         em_format = self.determine_emitter(request, *args, **kwargs)
+        if not em_format:
+            request_has_accept = 'HTTP_ACCEPT' in request.META
+            if request_has_accept and self.strict_accept:
+                return rc.NOT_ACCEPTABLE
+            em_format = self.default_emitter
 
         kwargs.pop('emitter_format', None)
 
         # If we're looking at a response object which contains non-string
         # content, then assume we should use the emitter to format that 
         # content
-        if isinstance(result, HttpResponse) and not result._is_string:
+        if self._use_emitter(result):
             status_code = result.status_code
-            # Note: We can't use result.content here because that method attempts
-            # to convert the content into a string which we don't want. 
-            # when _is_string is False _container is the raw data
+            # Note: We can't use result.content here because that
+            # method attempts to convert the content into a string
+            # which we don't want.  when
+            # _is_string/_base_content_is_iter is False _container is
+            # the raw data
             result = result._container
+
         srl = emitter(result, typemapper, handler, fields, anonymous)
 
         try:
             return e.response
 
     @staticmethod
+    def _use_emitter(result):
+        """True iff result is a HttpResponse and contains non-string content."""
+        if not isinstance(result, HttpResponse):
+            return False
+        elif django.VERSION >= (1, 4):
+            return result._base_content_is_iter
+        else:
+            return not result._is_string
+
+    @staticmethod
     def cleanup_request(request):
         """
         Removes `oauth_` keys from various dicts on the

piston/resource.py.orig

+import sys, inspect
+
+import django
+from django.http import (HttpResponse, Http404, HttpResponseNotAllowed,
+    HttpResponseForbidden, HttpResponseServerError)
+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 django.db.models.query import QuerySet
+from django.http import Http404
+
+try:
+    import mimeparse
+except ImportError:
+    mimeparse = None
+
+from emitters import Emitter
+from handler import typemapper
+from doc import HandlerMethod
+from authentication import NoAuthentication
+from utils import coerce_put_post, FormValidationError, HttpStatusCode
+from utils import rc, format_error, translate_mime, MimerDataException
+
+CHALLENGE = object()
+
+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()
+        self.csrf_exempt = getattr(self.handler, 'csrf_exempt', True)
+
+        if not authentication:
+            self.authentication = (NoAuthentication(),)
+        elif isinstance(authentication, (list, tuple)):
+            self.authentication = authentication
+        else:
+            self.authentication = (authentication,)
+
+        # Erroring
+        self.email_errors = getattr(settings, 'PISTON_EMAIL_ERRORS', True)
+        self.display_errors = getattr(settings, 'PISTON_DISPLAY_ERRORS', True)
+        self.stream = getattr(settings, 'PISTON_STREAM_OUTPUT', False)
+        # Emitter selection
+        self.strict_accept = getattr(settings, 'PISTON_STRICT_ACCEPT_HANDLING',
+                                     False)
+        self.default_emitter = getattr(settings, 'PISTON_DEFAULT_EMITTER',
+                                       'json')
+
+    def determine_emitter(self, request, *args, **kwargs):
+        """
+        Function for determening which emitter to use
+        for output. It lives here so you can easily subclass
+        `Resource` in order to change how emission is detected.
+        """
+        try:
+            return kwargs['emitter_format']
+        except KeyError:
+            pass
+        if 'format' in request.GET:
+            return request.GET.get('format')
+        if mimeparse and 'HTTP_ACCEPT' in request.META:
+            supported_mime_types = set()
+            emitter_map = {}
+            for name, (klass, content_type) in Emitter.EMITTERS.items():
+                content_type_without_encoding = content_type.split(';')[0]
+                supported_mime_types.add(content_type_without_encoding)
+                emitter_map[content_type_without_encoding] = name
+            preferred_content_type = mimeparse.best_match(
+                list(supported_mime_types),
+                request.META['HTTP_ACCEPT'])
+            return emitter_map.get(preferred_content_type, None)
+
+    def form_validation_response(self, e):
+        """
+        Method to return form validation error information. 
+        You will probably want to override this in your own
+        `Resource` subclass.
+        """
+        resp = rc.BAD_REQUEST
+        resp.write(u' '+unicode(e.form.errors))
+        return resp
+
+    @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
+
+    def authenticate(self, request, rm):
+        actor, anonymous = False, True
+
+        for authenticator in self.authentication:
+            if not authenticator.is_authenticated(request):
+                if self.anonymous and \
+                    rm in self.anonymous.allowed_methods:
+
+                    actor, anonymous = self.anonymous(), True
+                else:
+                    actor, anonymous = authenticator.challenge, CHALLENGE
+            else:
+                return self.handler, self.handler.is_anonymous
+
+        return actor, anonymous
+
+    @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.)
+        """
+        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)
+
+        actor, anonymous = self.authenticate(request, rm)
+
+        if anonymous is CHALLENGE:
+            return actor()
+        else:
+            handler = actor
+
+        # Translate nested datastructs into `request.data` here.
+        if rm in ('POST', 'PUT'):
+            try:
+                translate_mime(request)
+            except MimerDataException:
+                return rc.BAD_REQUEST
+            if not hasattr(request, 'data'):
+                if rm == 'POST':
+                    request.data = request.POST
+                else:
+                    request.data = request.PUT
+
+        if not rm in handler.allowed_methods:
+            return HttpResponseNotAllowed(handler.allowed_methods)
+
+        meth = getattr(handler, self.callmap.get(rm, ''), None)
+        if not meth:
+            raise Http404
+
+        # Support emitter through (?P<emitter_format>) and ?format=emitter
+        # and lastly Accept: header processing
+        em_format = self.determine_emitter(request, *args, **kwargs)
+        if not em_format:
+            request_has_accept = 'HTTP_ACCEPT' in request.META
+            if request_has_accept and self.strict_accept:
+                return rc.NOT_ACCEPTABLE
+            em_format = self.default_emitter
+
+        kwargs.pop('emitter_format', None)
+
+        # 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 Exception, e:
+            result = self.error_handler(e, request, meth, em_format)
+
+        try:
+            emitter, ct = Emitter.get(em_format)
+            fields = handler.fields
+
+            if hasattr(handler, 'list_fields') and isinstance(result, (list, tuple, QuerySet)):
+                fields = handler.list_fields
+        except ValueError:
+            result = rc.BAD_REQUEST
+            result.content = "Invalid output format specified '%s'." % em_format
+            return result
+
+        status_code = 200
+
+        # If we're looking at a response object which contains non-string
+        # content, then assume we should use the emitter to format that 
+        # content
+        if self._use_emitter(result):
+            status_code = result.status_code
+            # Note: We can't use result.content here because that
+            # method attempts to convert the content into a string
+            # which we don't want.  when
+            # _is_string/_base_content_is_iter is False _container is
+            # the raw data
+            result = result._container
+<<<<<<< mine
+=======
+
+>>>>>>> theirs
+        srl = emitter(result, typemapper, handler, fields, anonymous)
+
+        try:
+            """
+            Decide whether or not we want a generator here,
+            or we just want to buffer up the entire result
+            before sending it to the client. Won't matter for
+            smaller datasets, but larger will have an impact.
+            """
+            if self.stream: stream = srl.stream_render(request)
+            else: stream = srl.render(request)
+
+            if not isinstance(stream, HttpResponse):
+                resp = HttpResponse(stream, mimetype=ct, status=status_code)
+            else:
+                resp = stream
+
+            resp.streaming = self.stream
+
+            return resp
+        except HttpStatusCode, e:
+            return e.response
+
+    @staticmethod
+    def _use_emitter(result):
+        """True iff result is a HttpResponse and contains non-string content."""
+        if not isinstance(result, HttpResponse):
+            return False
+        elif django.VERSION >= (1, 4):
+            return result._base_content_is_iter
+        else:
+            return not result._is_string
+
+    @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)
+
+
+    def error_handler(self, e, request, meth, em_format):
+        """
+        Override this method to add handling of errors customized for your 
+        needs
+        """
+        if isinstance(e, FormValidationError):
+            return self.form_validation_response(e)
+
+        elif isinstance(e, TypeError):
+            result = rc.BAD_REQUEST
+            hm = HandlerMethod(meth)
+            sig = hm.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)
+            return result
+        elif isinstance(e, Http404):
+            return rc.NOT_FOUND
+
+        elif isinstance(e, HttpStatusCode):
+            return e.response
+ 
+        else: 
+            """
+            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.
+            """
+            exc_type, exc_value, tb = sys.exc_info()
+            rep = ExceptionReporter(request, exc_type, exc_value, tb.tb_next)
+            if self.email_errors:
+                self.email_exception(rep)
+            if self.display_errors:
+                return HttpResponseServerError(
+                    format_error('\n'.join(rep.format_exception())))
+            else:
+                raise
 # Django imports
+import django
 from django.core import mail
 from django.contrib.auth.models import User
 from django.conf import settings
          response = resource(request, emitter_format='json')
 
          self.assertEquals(201, response.status_code)
-         self.assertTrue(response._is_string, "Expected response content to be a string")
+         is_string = (not response._base_content_is_iter) if django.VERSION >= (1,4) else response._is_string
+         self.assert_(is_string, "Expected response content to be a string")
 
          # compare the original data dict with the json response 
          # converted to a dict
 import time
+
+import django
 from django.http import HttpResponseNotAllowed, HttpResponseForbidden, HttpResponse, HttpResponseBadRequest
 from django.core.urlresolvers import reverse
 from django.core.cache import cache
     """
     CODES = dict(ALL_OK = ('OK', 200),
                  CREATED = ('Created', 201),
+                 ACCEPTED = ('Accepted', 202),
                  DELETED = ('', 204), # 204 says "Don't send a body!"
                  BAD_REQUEST = ('Bad Request', 400),
                  FORBIDDEN = ('Forbidden', 401),
                  NOT_FOUND = ('Not Found', 404),
+                 NOT_ACCEPTABLE = ('Not acceptable', 406),
                  DUPLICATE_ENTRY = ('Conflict/Duplicate', 409),
                  NOT_HERE = ('Gone', 410),
                  INTERNAL_ERROR = ('Internal Error', 500),
 
         class HttpResponseWrapper(HttpResponse):
             """
-            Wrap HttpResponse and make sure that the internal _is_string 
-            flag is updated when the _set_content method (via the content 
-            property) is called
+            Wrap HttpResponse and make sure that the internal
+            _is_string/_base_content_is_iter flag is updated when the
+            _set_content method (via the content property) is called
             """
             def _set_content(self, content):
                 """
-                Set the _container and _is_string properties based on the 
-                type of the value parameter. This logic is in the construtor
-                for HttpResponse, but doesn't get repeated when setting 
-                HttpResponse.content although this bug report (feature request)
-                suggests that it should: http://code.djangoproject.com/ticket/9403 
+                Set the _container and _is_string /
+                _base_content_is_iter properties based on the type of
+                the value parameter. This logic is in the construtor
+                for HttpResponse, but doesn't get repeated when
+                setting HttpResponse.content although this bug report
+                (feature request) suggests that it should:
+                http://code.djangoproject.com/ticket/9403
                 """
+                is_string = False
                 if not isinstance(content, basestring) and hasattr(content, '__iter__'):
                     self._container = content
-                    self._is_string = False
                 else:
                     self._container = [content]
-                    self._is_string = True
+                    is_string = True
+                if django.VERSION >= (1, 4):
+                    self._base_content_is_iter = not is_string
+                else:
+                    self._is_string = is_string
 
             content = property(HttpResponse._get_content, _set_content)            
 
 def validate(v_form, operation='POST'):
     @decorator
     def wrap(f, self, request, *a, **kwa):
-        
-        form_data = getattr(request, 'data')
-        if len(form_data) == 0:
-            form_data = getattr(request, operation)
-        form = v_form(form_data)
+        form = v_form(getattr(request, operation), request.FILES)
     
         if form.is_valid():
             setattr(request, 'form', form)

piston/utils.py.orig

+import time
+
+import django
+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 django.core.mail import send_mail, mail_admins
+from django.conf import settings
+from django.utils.translation import ugettext as _
+from django.template import loader, TemplateDoesNotExist
+from django.contrib.sites.models import Site
+from decorator import decorator
+
+from datetime import datetime, timedelta
+
+__version__ = '0.2.3rc1'
+
+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)
+
+class rc_factory(object):
+    """
+    Status codes.
+    """
+    CODES = dict(ALL_OK = ('OK', 200),
+                 CREATED = ('Created', 201),
+                 ACCEPTED = ('Accepted', 202),
+                 DELETED = ('', 204), # 204 says "Don't send a body!"
+                 BAD_REQUEST = ('Bad Request', 400),
+                 FORBIDDEN = ('Forbidden', 401),
+                 NOT_FOUND = ('Not Found', 404),
+                 NOT_ACCEPTABLE = ('Not acceptable', 406),
+                 DUPLICATE_ENTRY = ('Conflict/Duplicate', 409),
+                 NOT_HERE = ('Gone', 410),
+                 INTERNAL_ERROR = ('Internal Error', 500),
+                 NOT_IMPLEMENTED = ('Not Implemented', 501),
+                 THROTTLED = ('Throttled', 503))
+
+    def __getattr__(self, attr):
+        """
+        Returns a fresh `HttpResponse` when getting 
+        an "attribute". This is backwards compatible
+        with 0.2, which is important.
+        """
+        try:
+            (r, c) = self.CODES.get(attr)
+        except TypeError:
+            raise AttributeError(attr)
+
+        class HttpResponseWrapper(HttpResponse):
+            """
+            Wrap HttpResponse and make sure that the internal
+            _is_string/_base_content_is_iter flag is updated when the
+            _set_content method (via the content property) is called
+            """
+            def _set_content(self, content):
+                """
+                Set the _container and _is_string /
+                _base_content_is_iter properties based on the type of
+                the value parameter. This logic is in the construtor
+                for HttpResponse, but doesn't get repeated when
+                setting HttpResponse.content although this bug report
+                (feature request) suggests that it should:
+                http://code.djangoproject.com/ticket/9403
+                """
+                is_string = False
+                if not isinstance(content, basestring) and hasattr(content, '__iter__'):
+                    self._container = content
+                else:
+                    self._container = [content]
+                    is_string = True
+                if django.VERSION >= (1, 4):
+                    self._base_content_is_iter = not is_string
+                else:
+                    self._is_string = is_string
+
+            content = property(HttpResponse._get_content, _set_content)            
+
+        return HttpResponseWrapper(r, content_type='text/plain', status=c)
+    
+rc = rc_factory()
+    
+class FormValidationError(Exception):
+    def __init__(self, form):
+        self.form = form
+
+class HttpStatusCode(Exception):
+    def __init__(self, response):
+        self.response = response
+
+def validate(v_form, operation='POST'):
+    @decorator
+    def wrap(f, self, request, *a, **kwa):
+<<<<<<< mine
+        
+        form_data = getattr(request, 'data')
+        if len(form_data) == 0:
+            form_data = getattr(request, operation)
+        form = v_form(form_data)
+=======
+        form = v_form(getattr(request, operation), request.FILES)
+>>>>>>> theirs
+    
+        if form.is_valid():
+            setattr(request, '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 = time.time()
+            count, expiration = cache.get(ident, (1, None))
+
+            if expiration is None:
+                expiration = now + timeout
+
+            if count >= max_requests and expiration > now:
+                t = rc.THROTTLED
+                wait = int(expiration - now)
+                t.content = 'Throttled, wait %d seconds.' % wait
+                t['Retry-After'] = wait
+                return t
+
+            cache.set(ident, (count+1, expiration), (expiration - now))
+    
+        return f(self, request, *args, **kwargs)
+    return wrap
+
+def coerce_put_post(request):
+    """
+    Django doesn't particularly understand REST.
+    In case we send data over PUT, Django won't
+    actually look at the data and load it. We need
+    to twist its arm here.
+    
+    The try/except abominiation here is due to a bug
+    in mod_python. This should fix it.
+    """
+    if request.method == "PUT":
+        # Bug fix: if _load_post_and_files has already been called, for
+        # example by middleware accessing request.POST, the below code to
+        # pretend the request is a POST instead of a PUT will be too late
+        # to make a difference. Also calling _load_post_and_files will result 
+        # in the following exception:
+        #   AttributeError: You cannot set the upload handlers after the upload has been processed.
+        # The fix is to check for the presence of the _post field which is set 
+        # the first time _load_post_and_files is called (both by wsgi.py and 
+        # modpython.py). If it's set, the request has to be 'reset' to redo
+        # the query value parsing in POST mode.
+        if hasattr(request, '_post'):
+            del request._post
+            del request._files
+        
+        try:
+            request.method = "POST"
+            request._load_post_and_files()
+            request.method = "PUT"
+        except AttributeError:
+            request.META['REQUEST_METHOD'] = 'POST'
+            request._load_post_and_files()
+            request.META['REQUEST_METHOD'] = 'PUT'
+            
+        request.PUT = request.POST
+
+
+class MimerDataException(Exception):
+    """
+    Raised if the content_type and data don't match
+    """
+    pass
+
+class Mimer(object):
+    TYPES = dict()
+    
+    def __init__(self, request):
+        self.request = request
+        
+    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):
+        """
+        Gets a function ref to deserialize content
+        for a certain mimetype.
+        """
+        for loadee, mimes in Mimer.TYPES.iteritems():
+            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 type_formencoded in ctype:
+            return None
+        
+        return ctype
+
+    def translate(self):
+        """
+        Will look at the `Content-type` sent by the client, and maybe
+        deserialize the contents into the format they sent. This will
+        work for JSON, YAML, XML and Pickle. Since the data is not just
+        key-value (and maybe just a list), the data will be placed on
+        `request.data` instead, and the handler will have to read from
+        there.
+        
+        It will also set `request.content_type` so the handler has an easy
+        way to tell what's going on. `request.content_type` will always be
+        None for form-encoded and/or multipart form data (what your browser sends.)
+        """    
+        ctype = self.content_type()
+        self.request.content_type = ctype
+        
+        if not self.is_multipart() and ctype:
+            loadee = self.loader_for_type(ctype)
+            
+            if loadee:
+                try:
+                    self.request.data = loadee(self.request.raw_post_data)
+                        
+                    # Reset both POST and PUT from request, as its
+                    # misleading having their presence around.
+                    self.request.POST = self.request.PUT = dict()
+                except (TypeError, ValueError):
+                    # This also catches if loadee is None.
+                    raise MimerDataException
+            else:
+                self.request.data = None
+
+        return self.request
+                
+    @classmethod
+    def register(cls, loadee, types):
+        cls.TYPES[loadee] = types
+        
+    @classmethod
+    def unregister(cls, loadee):
+        return cls.TYPES.pop(loadee)
+
+def translate_mime(request):
+    request = Mimer(request).translate()
+    
+def require_mime(*mimes):
+    """
+    Decorator requiring a certain mimetype. There's a nifty
+    helper called `require_extended` below which requires everything
+    we support except for post-data via form.
+    """
+    @decorator
+    def wrap(f, self, request, *args, **kwargs):
+        m = Mimer(request)
+        realmimes = set()
+
+        rewrite = { 'json':   'application/json',
+                    'yaml':   'application/x-yaml',
+                    'xml':    'text/xml',
+                    'pickle': 'application/python-pickle' }
+
+        for idx, mime in enumerate(mimes):
+            realmimes.add(rewrite.get(mime, mime))
+
+        if not m.content_type() in realmimes:
+            return rc.BAD_REQUEST
+
+        return f(self, request, *args, **kwargs)
+    return wrap
+
+require_extended = require_mime('json', 'yaml', 'xml', 'pickle')
+    
+def send_consumer_mail(consumer):
+    """
+    Send a consumer an email depending on what their status is.
+    """
+    try:
+        subject = settings.PISTON_OAUTH_EMAIL_SUBJECTS[consumer.status]
+    except AttributeError:
+        subject = "Your API Consumer for %s " % Site.objects.get_current().name
+        if consumer.status == "accepted":
+            subject += "was accepted!"
+        elif consumer.status == "canceled":
+            subject += "has been canceled."
+        elif consumer.status == "rejected":
+            subject += "has been rejected."
+        else: 
+            subject += "is awaiting approval."
+
+    template = "piston/mails/consumer_%s.txt" % consumer.status    
+    
+    try:
+        body = loader.render_to_string(template, 
+            { 'consumer' : consumer, 'user' : consumer.user })
+    except TemplateDoesNotExist:
+        """ 
+        They haven't set up the templates, which means they might not want
+        these emails sent.
+        """
+        return 
+
+    try:
+        sender = settings.PISTON_FROM_EMAIL
+    except AttributeError:
+        sender = settings.DEFAULT_FROM_EMAIL
+
+    if consumer.user:
+        send_mail(_(subject), body, sender, [consumer.user.email], fail_silently=True)
+
+    if consumer.status == 'pending' and len(settings.ADMINS):
+        mail_admins(_(subject), body, fail_silently=True)
+
+    if settings.DEBUG and consumer.user:
+        print "Mail being sent, to=%s" % consumer.user.email
+        print "Subject: %s" % _(subject)
+        print body
+
 
 setup(
     name = "django-piston",
-    version = "0.2.3rc1",
+    version = "0.2.3",
     url = 'http://bitbucket.org/jespern/django-piston/wiki/Home',
 	download_url = 'http://bitbucket.org/jespern/django-piston/downloads/',
     license = 'BSD',

tests/test_project/apps/testapp/forms.py

 class EchoForm(forms.Form):
     msg = forms.CharField(max_length=128)
 
+class FormWithFileField(forms.Form):
+    chaff = forms.CharField(max_length=50)
+    le_file = forms.FileField()

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, Issue58Model, ListFieldsModel
-from forms import EchoForm
+from models import TestModel, ExpressiveTestModel, Comment, InheritedModel, PlainOldObject, Issue58Model, ListFieldsModel, CircularA, CircularB, CircularC
+from forms import EchoForm, FormWithFileField
 from test_project.apps.testapp import signals
 
 class EntryHandler(BaseHandler):
             return rc.CREATED
         else:
             super(Issue58Model, self).create(request)
+
+class FileUploadHandler(BaseHandler):
+    allowed_methods = ('POST',)
+    
+    @validate(FormWithFileField)
+    def create(self, request):
+        return {'chaff': request.form.cleaned_data['chaff'],
+                'file_size': request.form.cleaned_data['le_file'].size}
+
+class CircularAHandler(BaseHandler):
+    allowed_methods = ('GET',)
+    fields = ('name', 'link')
+    model = CircularA
+
+class CircularAHandler(BaseHandler):
+    allowed_methods = ('GET',)
+    fields = ('name', 'link')
+    model = CircularB
+
+class CircularAHandler(BaseHandler):
+    allowed_methods = ('GET',)
+    fields = ('name', 'link')
+    model = CircularC

tests/test_project/apps/testapp/models.py

 class Issue58Model(models.Model):
     read = models.BooleanField(default=False)
     model = models.CharField(max_length=1, blank=True, null=True)
+
+class CircularA(models.Model):
+    link = models.ForeignKey('testapp.CircularB', null=True)
+    name = models.CharField(max_length=15)
+
+class CircularB(models.Model):
+    link = models.ForeignKey('testapp.CircularC', null=True)
+    name = models.CharField(max_length=15)
+
+class CircularC(models.Model):
+    link = models.ForeignKey('testapp.CircularA', null=True)
+    name = models.CharField(max_length=15)

tests/test_project/apps/testapp/tests.py

 from django.test import TestCase
+from django.core.urlresolvers import reverse
 from django.contrib.auth.models import User
 from django.utils import simplejson
 from django.conf import settings
     print "Can't run YAML testsuite"
     yaml = None
 
-import urllib, base64
+import urllib, base64, tempfile
 
-from test_project.apps.testapp.models import TestModel, ExpressiveTestModel, Comment, InheritedModel, Issue58Model, ListFieldsModel
+from test_project.apps.testapp.models import TestModel, ExpressiveTestModel, Comment, InheritedModel, Issue58Model, ListFieldsModel, CircularA, CircularB, CircularC
 from test_project.apps.testapp import signals
 
 class MainTests(TestCase):
         resp = self.client.post('/api/issue58.json', outgoing, content_type='application/json',
                                 HTTP_AUTHORIZATION=self.auth_string)
         self.assertEquals(resp.status_code, 201)
+
+class Issue188ValidateWithFiles(MainTests):
+    def test_whoops_no_file_upload(self):
+        resp = self.client.post(
+            reverse('file-upload-test'),
+            data={'chaff': 'pewpewpew'})
+        self.assertEquals(resp.status_code, 400)
+    
+    def test_upload_with_file(self):
+        tmp_fs = tempfile.NamedTemporaryFile(suffix='.txt')
+        content = 'le_content'
+        tmp_fs.write(content)
+        tmp_fs.seek(0)
+        resp = self.client.post(
+            reverse('file-upload-test'),
+            data={'chaff': 'pewpewpew',
+                  'le_file': tmp_fs})
+        self.assertEquals(resp.status_code, 200)
+        self.assertEquals(simplejson.loads(resp.content),
+                          {'chaff': 'pewpewpew',
+                           'file_size': len(content)})
+
+class EmitterFormat(MainTests):
+    def test_format_in_url(self):
+        resp = self.client.get('/api/entries.json',
+                               HTTP_AUTHORIZATION=self.auth_string)
+        self.assertEquals(resp.status_code, 200)
+        self.assertEquals(resp['Content-type'],
+                          'application/json; charset=utf-8')
+        resp = self.client.get('/api/entries.xml',
+                               HTTP_AUTHORIZATION=self.auth_string)
+        self.assertEquals(resp.status_code, 200)
+        self.assertEquals(resp['Content-type'],
+                          'text/xml; charset=utf-8')
+        resp = self.client.get('/api/entries.yaml',
+                               HTTP_AUTHORIZATION=self.auth_string)
+        self.assertEquals(resp.status_code, 200)
+        self.assertEquals(resp['Content-type'],
+                          'application/x-yaml; charset=utf-8')
+
+    def test_format_in_get_data(self):
+        resp = self.client.get('/api/entries/?format=json',
+                               HTTP_AUTHORIZATION=self.auth_string)
+        self.assertEquals(resp.status_code, 200)
+        self.assertEquals(resp['Content-type'],
+                          'application/json; charset=utf-8')
+        resp = self.client.get('/api/entries/?format=xml',
+                               HTTP_AUTHORIZATION=self.auth_string)
+        self.assertEquals(resp.status_code, 200)
+        self.assertEquals(resp['Content-type'],
+                          'text/xml; charset=utf-8')
+        resp = self.client.get('/api/entries/?format=yaml',
+                               HTTP_AUTHORIZATION=self.auth_string)
+        self.assertEquals(resp.status_code, 200)
+        self.assertEquals(resp['Content-type'],
+                          'application/x-yaml; charset=utf-8')
+        
+    def test_format_in_accept_headers(self):
+        resp = self.client.get('/api/entries/',
+                               HTTP_AUTHORIZATION=self.auth_string,
+                               HTTP_ACCEPT='application/json')
+        self.assertEquals(resp.status_code, 200)
+        self.assertEquals(resp['Content-type'],
+                          'application/json; charset=utf-8')
+        resp = self.client.get('/api/entries/',
+                               HTTP_AUTHORIZATION=self.auth_string,
+                               HTTP_ACCEPT='text/xml')
+        self.assertEquals(resp.status_code, 200)
+        self.assertEquals(resp['Content-type'],
+                          'text/xml; charset=utf-8')
+        resp = self.client.get('/api/entries/',
+                               HTTP_AUTHORIZATION=self.auth_string,
+                               HTTP_ACCEPT='application/x-yaml')
+        self.assertEquals(resp.status_code, 200)
+        self.assertEquals(resp['Content-type'],
+                          'application/x-yaml; charset=utf-8')
+    
+    def test_strict_accept_headers(self):
+        import urls
+        self.assertFalse(urls.entries.strict_accept)
+        self.assertEquals(urls.entries.default_emitter, 'json')
+        resp = self.client.get('/api/entries/',
+                               HTTP_AUTHORIZATION=self.auth_string,
+                               HTTP_ACCEPT='text/html')
+        self.assertEquals(resp.status_code, 200)
+        self.assertEquals(resp['Content-type'],
+                          'application/json; charset=utf-8')
+        
+        urls.entries.strict_accept = True
+        resp = self.client.get('/api/entries/',
+                               HTTP_AUTHORIZATION=self.auth_string,
+                               HTTP_ACCEPT='text/html')
+        self.assertEquals(resp.status_code, 406)
+
+class CircularReferenceTest(MainTests):
+    def init_delegate(self):
+        self.a = CircularA.objects.create(name='foo')
+        self.b = CircularB.objects.create(name='bar')
+        self.c = CircularC.objects.create(name='baz')
+        self.a.link = self.b; self.a.save()
+        self.b.link = self.c; self.b.save()
+        self.c.link = self.a; self.c.save()
+
+    def test_circular_model_references(self):
+        self.assertRaises(
+            RuntimeError,
+            self.client.get,
+            '/api/circular_a/',
+            HTTP_AUTHORIZATION=self.auth_string)
+
+        
+        

tests/test_project/apps/testapp/urls.py

 from piston.resource import Resource
 from piston.authentication import HttpBasicAuthentication, HttpBasicSimple
 
-from test_project.apps.testapp.handlers import EntryHandler, ExpressiveHandler, AbstractHandler, EchoHandler, PlainOldObjectHandler, Issue58Handler, ListFieldsHandler
+from test_project.apps.testapp.handlers import EntryHandler, ExpressiveHandler, AbstractHandler, EchoHandler, PlainOldObjectHandler, Issue58Handler, ListFieldsHandler, FileUploadHandler, CircularAHandler
 
 auth = HttpBasicAuthentication(realm='TestApplication')
 
 popo = Resource(handler=PlainOldObjectHandler)
 list_fields = Resource(handler=ListFieldsHandler)
 issue58 = Resource(handler=Issue58Handler)
+fileupload = Resource(handler=FileUploadHandler)
+circular_a = Resource(handler=CircularAHandler)
 
 AUTHENTICATORS = [auth,]
 SIMPLE_USERS = (('admin', 'secr3t'),
 multiauth = Resource(handler=PlainOldObjectHandler, 
                         authentication=AUTHENTICATORS)
 
-urlpatterns = patterns('',
+urlpatterns = patterns(
+    '',
     url(r'^entries/$', entries),
     url(r'^entries/(?P<pk>.+)/$', entries),
     url(r'^entries\.(?P<emitter_format>.+)', entries),
     url(r'^abstract/(?P<id_>\d+)\.(?P<emitter_format>.+)$', abstract),
 
     url(r'^echo$', echo),
+    
+    url(r'^file_upload/$', fileupload, name='file-upload-test'),
 
     url(r'^multiauth/$', multiauth),
 
+    url(r'^circular_a/$', circular_a),
+
     # oauth entrypoints
     url(r'^oauth/request_token$', 'piston.authentication.oauth_request_token'),
     url(r'^oauth/authorize$', 'piston.authentication.oauth_user_auth'),

tests/test_project/settings.py

 import os
 DEBUG = True
+DATABASES = {
+    'default':
+        {
+        'ENGINE': 'django.db.backends.sqlite3',
+        'NAME': '/tmp/piston.db'
+    }
+}
 DATABASE_ENGINE = 'sqlite3'
 DATABASE_NAME = '/tmp/piston.db'
 INSTALLED_APPS = (
     'django.contrib.contenttypes', 
     'django.contrib.sessions', 
     'django.contrib.sites',
+    'django.contrib.admin',
     'piston',
     'test_project.apps.testapp',
 )

tests/test_project/urls.py

 from django.conf.urls.defaults import *
+from django.contrib import admin
+admin.autodiscover()
 
-
-urlpatterns = patterns('',
-    url(r'api/', include('test_project.apps.testapp.urls'))
+urlpatterns = patterns(
+    '',
+    url(r'api/', include('test_project.apps.testapp.urls')),
+    url(r'^admin/', include(admin.site.urls)),
 )
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.