Skip to content

Commit 360b92f

Browse files
author
Saverio Trioni
committed
Change of attribute names for field-serialization. \nAlso, better docs on lookups
1 parent 622bd39 commit 360b92f

File tree

2 files changed

+110
-62
lines changed

2 files changed

+110
-62
lines changed

documentation/examples.rst

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,29 @@ This will be serialized in a dict containing (JSON example):
2121
}
2222

2323

24+
How to change serialization depending on the request
25+
----------------------------------------------------
26+
27+
::
28+
29+
class Resource(RestfulResource):
30+
fieldset_marker = 'fs' # this is the name of the selecting parameter
31+
fields = FieldSpec(
32+
default=('a','b','c'),
33+
alt=('d','e','f'),
34+
alt2=('i','g','h')
35+
)
36+
37+
This resource will expose the corresponding fields whenever the request
38+
contains ``fs=alt`` or ``fs=alt2`` in the query. In any other case
39+
it exposes the default set of fields.
40+
41+
For the selection to be activated, both ``fieldset_marker`` must be set and
42+
``field_spec`` must be a mapping (a ``defaultdict`` or ``FieldSet`` instance is
43+
preferred, to allow fallback without raising exceptions).
44+
45+
46+
2447
How to filter over a boolean field
2548
----------------------------------
2649

@@ -34,4 +57,5 @@ How to filter over a boolean field
3457
)
3558

3659
Any request containing the parameter ``bool_field=`` will enable the
37-
corresponding filter.
60+
corresponding filter, by adding ``.filter(bool_field=True)`` or
61+
``.filter(bool_field=False)`` to the queryset.

restful/codecs.py

Lines changed: 85 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -5,63 +5,83 @@
55
'''
66
from StringIO import StringIO
77
from collections import defaultdict
8+
from xml.etree import ElementTree
9+
import re
10+
from django.conf import ConfigurationError
811
from django.core.serializers.json import DateTimeAwareJSONEncoder
912
from django.http import HttpResponseBadRequest, HttpResponse
1013
from django.utils import simplejson
1114
from django.utils.encoding import smart_unicode
1215
from django.utils.xmlutils import SimplerXMLGenerator
1316
from restful.utils import serialize
14-
from xml.etree import ElementTree
15-
import re
1617

1718

1819
class BaseRequestDecoder(object):
19-
content_type = None
20+
""" The base for request decoder mixins. Subclasses can be mixed together
21+
to provide support for multiple content types.
22+
"""
2023

2124
def decode_postdata(self, request, *args, **kwargs):
22-
raise NotImplementedError('This method must be implemented in subclasses')
25+
""" Subclasses must implement this method, by checking the compatibility
26+
of the request's CONTENT_TYPE with the subclass supported one. In case
27+
of a difference the subclass must delegate to another component.
28+
"""
29+
raise NotImplementedError('The method "decode_postdata()" must be implemented in subclasses')
2330

2431
def dispatch(self, request, *args, **kwargs):
25-
""" If the request is POST or PUT, load the postdata into self.data
26-
after converting to an object. """
32+
""" Load the request data into a dictionary, using any decoder
33+
implemented by subclasses.
34+
"""
2735
self.data = None
36+
self.request_content_type = request.META.get('CONTENT_TYPE', '').split(';')[0].strip()
2837
if request.method.lower() in ('put', 'post') and request.POST:
2938
try:
3039
self.data = self.decode_postdata(request)
3140
request.POST = dict()
3241
except NotImplementedError:
33-
return HttpResponseBadRequest('Cannot decode POST data of type %s.' % request.META.get('CONTENT_TYPE'))
34-
except Exception:
35-
return HttpResponseBadRequest('Error parsing data of type %s.' % request.META.get('CONTENT_TYPE'))
42+
return HttpResponseBadRequest('Cannot decode POST data of type %s.' % self.request_content_type)
43+
except Exception as e:
44+
# Any other error is a parsing error
45+
return HttpResponseBadRequest('Error parsing data of type %s.' % self.request_content_type)
3646
return super(BaseRequestDecoder, self).dispatch(request, *args, **kwargs)
3747

3848

3949
class DefaultRequestDecoder(BaseRequestDecoder):
40-
# By default, no content type will be accepted too
41-
content_type = 'application/x-www-form-urlencoded'
50+
""" The default request decoder, if used, loads the POST data into
51+
a dictionary when the Content-Type header is ot set or is set to
52+
form-encoded.
53+
54+
The proper QueryDict object from django request is returned, so list-type
55+
instances can be retrieved explicitly.
56+
"""
57+
content_types = ['application/x-www-form-urlencoded', '']
4258

4359
def decode_postdata(self, request, *args, **kwargs):
44-
if request.META.get('CONTENT_TYPE', 'application/x-www-form-urlencoded').startswith(DefaultRequestDecoder.content_type):
60+
if self.request_content_type in DefaultRequestDecoder.content_types:
4561
return request.POST
4662
else:
47-
return super(JSONRequestDecoder, self).decode_postdata(request, *args, **kwargs)
63+
return super(DefaultRequestDecoder, self).decode_postdata(request, *args, **kwargs)
4864

4965

5066
class JSONRequestDecoder(BaseRequestDecoder):
51-
content_type = 'application/json'
67+
""" The JSON request decoder. JSON specifies that there must be a root
68+
object, which is converted to a dictionary.
69+
"""
70+
content_types = ['application/json']
5271

5372
def decode_postdata(self, request, *args, **kwargs):
54-
if request.META.get('CONTENT_TYPE', '').startswith(JSONRequestDecoder.content_type):
73+
if self.request_content_type in JSONRequestDecoder.content_types:
5574
return simplejson.load(request)
5675
else:
5776
return super(JSONRequestDecoder, self).decode_postdata(request, *args, **kwargs)
5877

5978

6079
class XMLRequestDecoder(BaseRequestDecoder):
61-
content_type = 'application/xml'
80+
# FIXME: This should retrun a dict-like object, not a ElemetTree root.
81+
content_types = 'application/xml'
6282

6383
def decode_postdata(self, request, *args, **kwargs):
64-
if request.META.get('CONTENT_TYPE', '').startswith(XMLRequestDecoder.content_type):
84+
if self.request_content_type in XMLRequestDecoder.content_types:
6585
root = ElementTree.parse(request).getroot()
6686
return root
6787
else:
@@ -71,24 +91,48 @@ def decode_postdata(self, request, *args, **kwargs):
7191

7292

7393
class BaseResponseEncoder(object):
94+
95+
# The mimetype to send along with the response.
7496
mimetype = None
97+
98+
# The 'format' class attribute acts as default serialization format
99+
# for instances. If only basic mixins are used, the first mixin determines
100+
# the default format. Anyway the subclass can set its own default.
75101
format = None
76-
field_spec_marker = None
77-
field_spec = defaultdict()
78-
fields = ()
102+
103+
# A special request parameter which allows the requested resource to be
104+
# serialized into several different forms.
105+
fieldset_marker = None
106+
107+
# The field resolver object. Subclasses can use the FieldSpec class or a
108+
# simpler defaultdict for fallback behaviour. If the consumer can't
109+
# specify a serialization form, set it to just a tuple.
110+
fields = tuple() # for fixed serialization
111+
# fields = defaultdict(tuple) # for polymorphic serialization
112+
113+
def get_fields(self):
114+
if self.fieldset_marker and isinstance(self.fields, dict):
115+
return self.fields[self.request.GET.get(self.fieldset_marker)]
116+
elif isinstance(self.fields, tuple):
117+
return self.fields
118+
else:
119+
raise ConfigurationError("The 'fields' attribute must be either "
120+
"a dict (when 'fieldset_marker' is set) or a "
121+
"tuple.")
79122

80123
def render(self, response):
81-
raise NotImplementedError('This method must be implemented in subclasses')
82-
83-
def get_default_format(self):
84-
return None
85-
124+
raise NotImplementedError('%s extends BaseResponseEncoder but does not '
125+
'implement the render() method. Extend one of '
126+
'BaseResponseEncoder\'s subclasses, setting also '
127+
'a default format, or implement render() '
128+
'directly.' % self.__class__.__name__)
86129

87130
def dispatch(self, request, *args, **kwargs):
88-
self.format = re.sub(r'^\.+', '', kwargs.get('format') or self.get_default_format())
89-
print "format set to %s" % self.format
131+
# if the format has ben specified in the request, overwrite the
132+
# the default.
133+
if 'format' in kwargs:
134+
self.format = kwargs['format'].lstrip('.')
90135
response = super(BaseResponseEncoder, self).dispatch(request, *args, **kwargs)
91-
print "got response: %s" % response
92136
if isinstance(response, basestring):
93137
return HttpResponse(response)
94138
elif isinstance(response, HttpResponse):
@@ -98,29 +142,15 @@ def dispatch(self, request, *args, **kwargs):
98142
return self.render(data)
99143

100144

101-
def get_fields(self):
102-
if self.field_spec_marker:
103-
return self.field_spec[self.request.GET.get(self.field_spec_marker)]
104-
else:
105-
return self.fields
106-
107-
108-
def get_format(self):
109-
return self.format
110-
111-
112145
class JSONResponseEncoder(BaseResponseEncoder):
113146
mimetype = 'application/json'
114147
format = 'json'
115148

116149
def render(self, response):
117-
if self.format == JSONResponseEncoder.format:
118-
print "rendering %s in json" % `response`
119-
content = simplejson.dumps(response, cls=DateTimeAwareJSONEncoder, ensure_ascii=False, indent=2)
120-
return HttpResponse(content, mimetype=JSONResponseEncoder.mimetype)
121-
else:
150+
if self.format != JSONResponseEncoder.format:
122151
return super(JSONResponseEncoder, self).render(response)
123-
152+
content = simplejson.dumps(response, cls=DateTimeAwareJSONEncoder, ensure_ascii=False, indent=2)
153+
return HttpResponse(content, mimetype=JSONResponseEncoder.mimetype)
124154

125155

126156

@@ -129,23 +159,17 @@ class XMLResponseEncoder(BaseResponseEncoder):
129159
format = 'xml'
130160

131161
def render(self, response):
132-
if self.format == XMLResponseEncoder.format:
133-
stream = StringIO()
134-
135-
xml = SimplerXMLGenerator(stream, "utf-8")
136-
xml.startDocument()
137-
xml.startElement("response", {})
138-
139-
self._to_xml(xml, response)
140-
141-
xml.endElement("response")
142-
xml.endDocument()
143-
144-
content = stream.getvalue()
145-
146-
return HttpResponse(content, mimetype=XMLResponseEncoder.mimetype)
147-
else:
162+
if self.format != XMLResponseEncoder.format:
148163
return super(XMLResponseEncoder, self).render(response)
164+
stream = StringIO()
165+
xml = SimplerXMLGenerator(stream, "utf-8")
166+
xml.startDocument()
167+
xml.startElement("response", {})
168+
self._to_xml(xml, response)
169+
xml.endElement("response")
170+
xml.endDocument()
171+
content = stream.getvalue()
172+
return HttpResponse(content, mimetype=XMLResponseEncoder.mimetype)
149173

150174

151175
def _to_xml(self, xml, data):

0 commit comments

Comments
 (0)